开箱即用的 react 移动端脚手架

4,585 阅读8分钟

前言

如果你使用 React,如果你因 create-react-app 各种配置困惑,如果你每次新建 React 项目都要进行繁琐的配置,如果你对项目结构以及接口管理还存在疑惑,那本文也许能帮助到你。

你的点赞是我创作的动力~

🎉 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  状态管理

目录结构

├─store
│  │ index.ts
│  │
│  ├─actions
│  │   user.ts
│  │
│  └─reducers
│      index.ts
│      user.ts

拆分 reducer

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

src/index.tsx 中引入

import { Provider } from 'react-redux'
import store from './store'

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

使用

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 属性可以根据项目路径来修改,例如本项目地址为:yechuanjie.com/react-cli,则 basename="/react-cli",若不需要子路径,则默认basename = '/'

src/router/routes.ts

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

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

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

import request from './request'

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

使用封装的request

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

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

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

src/mock/server.mjs

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

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 服务

yarn mock

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

▲  回顶部

✅ 本地跨域配置

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

src/setupProxy.js

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规则

{
  "eslintConfig": {
    "extends": "react-app",
    "rules": {
      "import/no-commonjs": 0
    }
  }
}

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

{
  "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设置

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

▲  回顶部

✅ antd-moblie 组件按需加载

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

安装插件

yarn add babel-plugin-import

config-overrides.js,使用fixBabelImports设置

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设置

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 会提示无法识别别名

{
  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@/*": ["*"]
    }
  }
}

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

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

▲  回顶部

✅ 配置打包分析

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

首先安装它:

yarn add webpack-bundle-analyzer

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

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
"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
  # 测试环境
  # must start with REACT_APP_
  REACT_APP_ENV = 'development'
  • .env.staging
  # 预发布环境
  # must start with REACT_APP_
  REACT_APP_ENV = 'staging'
  • .env.production
  # 正式环境
  # must start with REACT_APP_
  REACT_APP_ENV = 'production'

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

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

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

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

config/index.ts

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

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

config/.env.development.ts

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

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

import config from '@/config'
console.info(config)
// config
{
  ENV_TYPE: '测试环境',
  BASE_URL: '//test.xxx.com'
  OTHER_GLOBAL_VAR: 'xxx'
}

▲  回顶部

本文仅供参考,若有不对的地方,欢迎在评论区留下您的宝贵意见~

本文使用 mdnice 排版