react-cli

🎉 react 移动端开发脚手架,技术栈 react + antd-moblie + typescript + react-router + redux

该脚手架基于 Create React App 创建,方便快速搭建 react 移动端项目。仓库地址 && 项目地址(请在移动端查看)

目录

✅ TypeScript 开发语言

✅ redux 状态管理

✅ react-router 路由管理

✅ axios 封装及接口管理

✅ 本地 mock server 支持

✅ 本地跨域配置

✅ esint + prettier 统一开发规范

✅ 支持自定义 webpack 配置

✅ rem 适配方案

✅ antd-moblie 组件按需加载

✅ 配置 alias 别名

✅ 配置打包分析

✅ 配置多环境变量

✅ TypeScript 开发语言

TypeScriptJavaScript 类型的超集,它可以编译成纯 JavaScript。它的最大特点就是支持强类型和 ES6 Class

▲ 回顶部

✅ redux 状态管理

目录结构

1
2
3
4
5
6
7
8
9
├─store
│ │ index.ts
│ │
│ ├─actions
│ │ user.ts
│ │
│ └─reducers
│ index.ts
│ user.ts

拆分 reducer

store/indexcombineReducers() 方法将多个小的 reducer 组合成一个 rootReducer,而每个小的 reducer 只关心自己负责的 action.type

src/index.tsx 中引入

1
2
3
4
5
6
7
8
9
10
11
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
)

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useSelector, useDispatch } from 'react-redux'
import { setAppUserInfo } from '@/store/actions/user'

function Index() {
const userInfo = useSelector((state: PageStateProps) => state.user)
const dispath = useDispatch()

const updateInfo = () => {
dispath(
setAppUserInfo({
userId: '413',
nickName: 'developer',
sex: 1
})
)
}
return (
<div className="page">
<div onClick={updateInfo}>
<Logo></Logo>
</div>
<div className="welcome">hello {userInfo.nickName}!</div>
</div>
)
}

▲ 回顶部

✅ react-router 路由管理

本项目采用 history 模式,如需使用 hash 模式,请使用 HashRouter 替换 BrowserRouter

basename 属性可以根据项目路径来修改,例如本项目地址为:http://yechuanjie.github.io/react-cli,则 basename="/react-cli",若不需要子路径,则默认basename = '/'

src/router/routes.ts

1
2
3
4
5
6
7
8
9
10
11
import { lazy } from 'react'
const Index = lazy(() => import('@/pages/index'))

export const routes: RouteConfig[] = [
{
path: '/index',
component: Index,
exact: true,
routes: []
}
]

src/router/index.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Suspense } from 'react'
import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom'
import { routes } from './routes'

const RouterView = () => (
<BrowserRouter basename="/react-cli">
<Suspense fallback={<div>加载中</div>}>
<Switch>
{routes.map(route => (
<Route key={route.path} path={route.path} component={route.component} exact={route.exact}></Route>
))}
<Redirect to="/index"></Redirect>
</Switch>
</Suspense>
</BrowserRouter>
)
export default RouterView

使用 lazy + Suspense 的方式实现路由懒加载以及组件异步加载

▲ 回顶部

✅ axios 封装及接口管理

axios 请求进行二次封装,统一请求方式、实现公共参数配置、实现统一的错误拦截处理,并返回与后端统一的 Promise<ResponseType> 对象

request 封装 ,src/api/request.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import axios, { AxiosRequestConfig, Method } from 'axios'
import envConfig from '@/config'
// 接口返回类型 (根据后端返回的格式定义)
interface ResponseType {
data: any
msg: string
code: number
}
export default function request(url: string, method: Method, data?: {}, loading?: boolean): Promise<ResponseType> {
// 请求公共参数配置
const publicParams = {
env: envConfig.ENV_TYPE,
mockType: 1,
source: 'h5'
}
// 合并公共参数
data = Object.assign({}, data, publicParams)
const options: AxiosRequestConfig = {
url,
method,
params: method.toUpperCase() === 'GET' || method.toUpperCase() === 'DELETE' ? data : null,
data: method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT' ? data : null
}

const AxiosInstance = initAxios(loading)
return new Promise((resolve, reject) => {
AxiosInstance(options)
.then(res => {
const data = res.data as ResponseType
// 这里可以添加和后台的 status 约定
// if (data.code !== 200) {
// Toast.info(data.msg)
// }
resolve(data)
})
.catch(err => {
reject(err)
})
})
}

接口管理 src/api/index.ts

1
2
3
import request from './request'

export const getList = (params: { type: number }) => request('/api/getInfo', 'GET', { ...params }, true)

使用封装的request

1
2
3
4
5
6
7
8
import * as API from '@/api/index'
const updateInfo = async () => {
// get 请求
const list = await API.getList({ type: 1 })
console.info(list) // 请求结果就是封装后的 Promise<ResponseType> 类型
// 对于接口返回的数据格式,可以统一在global.d.ts里定义interface,假设你已经定义了 interface ListDetail, 然后如下使用
const data = list.data as ListDetail // 断言data类型,后续就可以直接使用定义好的数据结构
}

▲ 回顶部

✅ 本地 mock server 支持

src/mock 实现了本地 mock server 开发。

注意: nodejs 环境下默认不支持 esModules,将src/mock下的文件,修改为.mjs后缀,同时在package.jsonscripts中新增experimental-modules命令使其可以使用esModules

package.json

1
2
3
scripts: {
"mock": "node --experimental-modules src/mock/server.mjs"
}

本项目使用 express 作为服务器开发

src/mock/server.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import express from 'express'
import mockData from './mock.mjs'
import bodyParser from 'body-parser'

const app = express()
// body-parser 解析json格式数据
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))

const router = express.Router()

router.use('/', mockData)
app.use('/api', router)

app.listen(3001, () => {
console.log('Example app listening on port 3001!')
})

mock 数据根据需求在src/mock/mock.mjs中自定义修改,更多 mock 使用方式可以查看mock 官方示例>)。

src/mock/mock.mjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Mock from 'mockjs'
import express from 'express'
const router = express.Router()
// get类型接口 /api/getInfo 获取列表
router.get('/getInfo', (req, res) => {
console.info(req.query.type)
const data = Mock.mock({
'list|1-8': [
{
'name|1': ['John', 'Jessen', 'Mark'],
'desc|1': ['Hello', 'React-cli', 'Try it!']
}
]
})
return res.json({
data,
code: 200,
msg: ''
})
})

开启本地 mock 服务

1
yarn mock

本地开启 mock 服务后,所有本地 api 请求都会导致跨域问题,请参考✅ 本地跨域配置

▲ 回顶部

✅ 本地跨域配置

为解决本地接口请求跨域,需要使用到 http-proxy-middleware 中间件。在 src 根目录下创建setupProxy.js文件,注意这里只能使用 .js 后缀,因为该中间件默认读取的是 js 文件

src/setupProxy.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { createProxyMiddleware } = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
createProxyMiddleware('/api', {
// 代理服务器地址
target: 'http://localhost:3001',
secure: false,
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
})
)
}

这样一来,就可以愉快的在本地请求自己的mock数据啦!

▲ 回顶部

✅ eslint + prettier 统一开发规范

package.json文件中编写自定义eslint规则

1
2
3
4
5
6
7
8
{
"eslintConfig": {
"extends": "react-app",
"rules": {
"import/no-commonjs": 0
}
}
}

编写统一的prettier规范文件 .prettierrc

1
2
3
4
5
6
7
8
9
{
"singleQuote": true,
"semi": false,
"printWidth": 120,
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": true,
"trailingComma": "none"
}

▲ 回顶部

✅ 支持自定义 webpack 配置

通过 customize-cra 暴露 webpack 配置的config-overrides.js文件,使我们可以不用 eject 的方式就能在这里覆盖重写 webpack 配置,目前已支持几十种相关配置自定义,具体可查看customize-cra api docs>)。

▲ 回顶部

✅ rem 适配方案

项目已经配置好 rem 适配,下面仅做介绍:

antd-mobile 中的样式默认使用px作为单位,如果需要使用rem单位,推荐使用postcss-px2rem 搭配 src/utils/rem.ts一起使用。其中 src/utils/rem.ts 实现了一个极简的 rem 库。

postcss-px2rem 插件使用

  • 假如设计图给的宽度是 750,remUnit 设置为 75,这样我们写样式时,可以直接按照设计图标注的宽高来 1:1 还原开发。

  • PS: 如果引用了某些没有兼容 px2rem 第三方 UI 框架,有的 1rem = 100px(antd-mobile), 有的 1rem = 75px

  • 需要将 remUnit 的值设置为像素对应的一半(antd-mobile 即 50),即可以 1:1 还原组件,否则会样式会有变化,例如按钮会变小。

config-overrides.js,使用addPostcssPlugins设置

1
2
const { override, addPostcssPlugins } = require('customize-cra')
module.exports = override(addPostcssPlugins([require('postcss-px2rem')({ remUnit: 50 })]))

▲ 回顶部

✅ antd-moblie 组件按需加载

babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。

安装插件

1
yarn add babel-plugin-import

config-overrides.js,使用fixBabelImports设置

1
2
3
4
5
6
7
8
9
10
const { override, fixBabelImports } = require('customize-cra')
// 引用 antd 后设置按需引入后,在打包的时候会生成很多 .map 文件
process.env.GENERATE_SOURCEMAP = 'false'
module.exports = override(
/* 按需引入antd-mobile */
fixBabelImports('import', {
libraryName: 'antd-mobile',
style: 'css'
})
)

▲ 回顶部

✅ 配置 alias 别名

config-overrides.js,使用addWebpackAlias设置

1
2
3
4
5
6
7
8
9
10
const { override, addWebpackAlias } = require('customize-cra')
const path = require('path')
const resolve = dir => path.join(__dirname, dir)
module.exports = override(
addWebpackAlias({
'@/': resolve('src'),
'@/pages': resolve('./src/pages'),
'@/api': resolve('./src/api')
})
)

tsconfig.json

根目录的 tsconfig.json 文件中也需要设置别名的支持,否则 ts 会提示无法识别别名

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
}
}
}

Tips: 推荐使用 vscode 开发,安装 path-intellisense插件, 并在 setting.json 中设置别名映射,就能在使用别名时提示文件路径

1
2
3
"path-intellisense.mappings": {
"@": "\${workspaceRoot}/src"
}

▲ 回顶部

✅ 配置打包分析

webpack-bundle-analyzer 是一款分析代码大小的插件

首先安装它:

1
yarn add webpack-bundle-analyzer

config-overrides.js 中,使用 addWebpackPlugin 设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const { override, addWebpackPlugin } = require('customize-cra')

const analyze = process.env.REACT_APP_ENV === 'development' //是否分析打包数据

module.exports = override(
analyze
? addWebpackPlugin(
new BundleAnalyzerPlugin({
analyzerMode: 'static' //输出静态报告文件report.html,而不是启动一个web服务
})
)
: undefined
)

▲ 回顶部

✅ 配置多环境变量

package.json 里的 scripts 配置 build:dev build:sta build:pro来执行不同环境

  • yarn start 启动本地 , 默认执行 development
  • yarn build:dev 打包测试环境 , 执行 development
  • yarn build:sta 打包预发布环境 , 执行 staging
  • yarn build:pro 打包正式环境 , 执行 production
1
2
3
4
5
6
"scripts": {
"start": "react-app-rewired start",
"build:dev": "dotenv -e .env.development react-app-rewired build",
"build:sta": "dotenv -e .env.staging react-app-rewired build",
"build:pro": "dotenv -e .env.production react-app-rewired build"
}
配置详情

根目录 下创建不同的环境变量文件,如 .env.development.env.staging.env.production,就如你所看到的 scripts ,通过 dotenv 可以指定不同的环境变量文件。

在代码中可以通过 process.env.REACT_APP_ENV 访问所在的环境变量。除了 REACT_APP_* 变量之外,在你的应用代码中始终可用的还有两个特殊的变量NODE_ENVBASE_URL

  • .env.development
1
2
3
# 测试环境
# must start with REACT_APP_
REACT_APP_ENV = 'development'
  • .env.staging
1
2
3
# 预发布环境
# must start with REACT_APP_
REACT_APP_ENV = 'staging'
  • .env.production
1
2
3
# 正式环境
# must start with REACT_APP_
REACT_APP_ENV = 'production'

这里我们并没有定义全部环境变量,只定义了基础的环境类型 REACT_APP_ENV developmentstagingproduction 。变量我们统一在 src/config/env.*.ts 里进行管理

question: 为什么要在 config 中新建三个文件,而不是直接写在环境变量文件里呢?

  • 修改变量方便,无需重新启动项目

  • 引入方式更符合模块化标准

config/index.ts

1
2
3
// 根据build命令指定的环境,引入不同配置
const config = require('./env.' + process.env.REACT_APP_ENV)
export default config.default

每种环境单独去配置公共变量,以测试环境配置为例

config/.env.development.ts

1
2
3
4
5
6
// 测试环境配置
export default {
ENV_TYPE: '测试环境',
BASE_URL: '//test.xxx.com' // api请求地址
OTHER_GLOBAL_VAR: 'xxx' // 可添加自定义的公共变量
}

根据环境变量不同,config 配置就会不同

1
2
3
4
5
6
7
8
import config from '@/config'
console.info(config)
// config
{
ENV_TYPE: '测试环境',
BASE_URL: '//test.xxx.com'
OTHER_GLOBAL_VAR: 'xxx'
}

▲ 回顶部

-------------本文结束感谢您的阅读-------------
  • 本文作者: Ye ChuanJie
  • 本文链接: https://yechuanjie.github.io/blog/react-cli/
  • 发布时间: 2020年07月13日 - 09:44
  • 最后更新: 2024年06月28日 - 10:05
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!