React

一、项目启动

  1. 了解需求背景
  2. 了解业务流程

    二、项目搭建初始化

    本案例使用脚手架 create-react-app 初始化了项目。此脚手架有利有弊,项目目录结构简洁,不需要太关心 webpack 令人头疼的配置;弊端在于,脚手架确实有些庞大,构建时间在 4mins 左右。也可以完全自己搭建一个项目。

    设置淘宝镜像仓库

    1. $ yarn config set registry registry.npm.taobao.org/ -g
    2. $ yarn config set sass_binary_site cdn.npm.taobao.org/dist/node-sass -g

    工程目录 init

    1. $ create-react-app qpj-web-pc --typescript
    2. $ tree -I "node_modules"
    3. .
    4. |-- README.md
    5. |-- package.json
    6. |-- public
    7. | |-- favicon.ico
    8. | |-- index.html
    9. | |-- logo192.png
    10. | |-- logo512.png
    11. | |-- manifest.json
    12. | `-- robots.txt
    13. |-- src
    14. | |-- App.css
    15. | |-- App.test.tsx
    16. | |-- App.tsx
    17. | |-- index.css
    18. | |-- index.tsx
    19. | |-- logo.svg
    20. | |-- react-app-env.d.ts
    21. | |-- reportWebVitals.ts
    22. | `-- setupTests.ts
    23. `-- tsconfig.json

    yarn build

    1. $ yarn build & tree -I "node_modules"
    2. .
    3. |-- README.md
    4. |-- build/ # 改造点(由于 `Jenkins` 构建打包脚本有可能已经写死了 `dist` 包名)
    5. |-- package.json
    6. |-- public
    7. | |-- favicon.ico
    8. | |-- index.html
    9. | |-- logo192.png
    10. | |-- logo512.png
    11. | |-- manifest.json
    12. | `-- robots.txt
    13. |-- src
    14. | |-- App.css
    15. | |-- App.test.tsx
    16. | |-- App.tsx
    17. | |-- index.css
    18. | |-- index.tsx
    19. | |-- logo.svg
    20. | |-- react-app-env.d.ts
    21. | |-- reportWebVitals.ts
    22. | `-- setupTests.ts
    23. `-- tsconfig.json

    连接 git 远程仓库

    1. $ git remote add origin

    添加 .gitignore

    1. $ echo -e " yarn.lock \n package-lock.json \n /dist \n .idea" >> .gitignore

    添加 eslint 代码及提交评论校验

    1. $ yarn add husky lint-staged @commitlint/cli @commitlint/config-conventional -D
    2. $ npx husky install
    3. $ npx husky add .husky/pre-commit "npx lint-staged"
    4. $ npx husky add .husky/prepare-commit-msg "npx commitlint -e"
  • 项目根目录新建 commitlint.config.js

    1. // commitlint.config.js
    2. module.exports = {
    3. extends: ['@commitlint/config-conventional'],
    4. rules: {
    5. 'type-enum': [
    6. 2,
    7. 'always',
    8. ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert'],
    9. ],
    10. 'subject-full-stop': [0, 'never'],
    11. 'subject-case': [0, 'never'],
    12. },
    13. }
  • vscode 扩展中搜索 ESLint 并安装,项目根目录新建 .eslintrc.js

  • Commit message 格式说明

:

  • type 值枚举如下:
    • feat: 添加新特性
    • fix: 修复 bug
    • docs: 仅仅修改了文档
    • style: 仅仅修改了空格、格式缩进、都好等等,不改变代码逻辑
    • refactor: 代码重构,没有加新功能或者修复 bug
    • perf: 增加代码进行性能测试
    • test: 增加测试用例
    • chore: 改变构建流程、或者增加依赖库、工具等
    • revert: 当前 commit 用于撤销以前的 commit
  • subject 是 commit 目的的简短描述,不超过 50 个字符,且结尾不加句号(.)
  • package.json 新加入如下配置:

    1. {
    2. ...,
    3. "lint-staged": {
    4. "src/**/*.{jsx,txs,ts,js,json,css,md}": [
    5. "eslint --quiet"
    6. ]
    7. },
    8. }
  • 可执行 npx eslint [filePath] \--fix 进行格式修复,无法修复的需手动解决

    三、项目配置一(功能配置)

    安装项目常用依赖库

    1. $ yarn add antd axios dayjs qs -S # UI 及工具库
    2. $ yarn add react-router-dom redux react-redux redux-logger redux-thunk -S # 路由及状态管理

    webpack 配置拓展很有必要

  • 根目录新建 config-overrides.js

  • 安装
  • $ yarn add react-app-rewired customize-cra -D
  • 修改 package.json 中启动项

    1. // package.json
    2. "scripts": {
    3. "start": "react-app-rewired start",
    4. "build": "react-app-rewired build",
    5. }
  • 使用 ```jsx // config-overrides.js const { override, // 主函数 fixBabelImports, // 配置按需加载 addWebpackExternals, // 不做打包处理配置 addWebpackAlias, // 配置别名 addLessLoader // lessLoader 配置,可更改主题色等 } = require(‘customize-cra’)

module.exports = override(//, config => config)

  1. <a name="wt6t0"></a>
  2. ### 配置按需加载
  3. ```jsx
  4. // config-overrides.js
  5. ...
  6. module.exports = override(
  7. fixBabelImports('import', {
  8. libraryName: 'antd',
  9. libraryDirectory: 'es', // library 目录
  10. style: true, // 自动打包相关的样式
  11. }),
  12. )

更改主题色

  1. // config-overrides.js
  2. ...
  3. module.exports = override(
  4. addLessLoader({
  5. lessOptions: {
  6. javascriptEnabled: true,
  7. modifyVars: {
  8. '@primary-color': '#1890ff',
  9. },
  10. }
  11. }),
  12. )

别名配置(typescript 项目这里有坑)

  1. // config-overrides.js
  2. const path = require('path')
  3. ...
  4. module.exports = override(
  5. addWebpackAlias({
  6. '@': path.resolve(__dirname, 'src'),
  7. }),
  8. )

去除注释、多进程打包压缩

  1. // config-overrides.js
  2. const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
  3. const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
  4. ...
  5. module.exports = override(/* ... */, config => {
  6. config.plugins = [...config.plugins, {
  7. new UglifyJsPlugin({
  8. uglifyOptions: {
  9. warnings: false,
  10. compress: {
  11. drop_debugger: true,
  12. drop_console: true,
  13. },
  14. },
  15. }),
  16. new HardSourceWebpackPlugin()
  17. }]
  18. return config
  19. })

解决埋下的两个坑

修改打包出的文件夹名为 dist

  1. // 修改打包路径除了output,这里也要修改
  2. const paths = require('react-scripts/config/paths')
  3. paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')
  4. module.exports = override(/* ... */, config => {
  5. config.output.path = path.resolve(__dirname, 'dist')
  6. return config
  7. })

解决 typescript 别名配置

  • 查阅相关资料,需要在 tsconfig.json 中添加一项配置
    1. {
    2. ...
    3. "extends": "./paths.json"
    4. }

    新建文件 paths.json

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

    配置装饰器写法

    1. {
    2. "compilerOptions": {
    3. "experimentalDecorators": true,
    4. ...
    5. }
    6. }

    配置开发代理

  • 在 src 目录新建 setupProxy.js ```jsx // src/setupProxy.js const proxy = require(‘http-proxy-middleware’).createProxyMiddleware

module.exports = function(app) { // app 为 Express 实例,此处可以写 Mock 数据 app.use( proxy(‘/api’, { “target”: “https://qpj-test.fapiaoer.cn“, “changeOrigin”: true, “secure”: false, // “pathRewrite”: { // “^/api”: “” // } }) ) }

  1. <a name="uRhGJ"></a>
  2. ### 加入 polyfill 和 antd 组件国际化处理
  3. ```jsx
  4. // src/index.tsx
  5. import React from 'react'
  6. import ReactDOM from 'react-dom'
  7. // 注入 store
  8. import { Provider } from 'react-redux'
  9. import store from '@/store/store'
  10. import { ConfigProvider, Empty } from 'antd'
  11. import App from './App'
  12. import zhCN from 'antd/es/locale/zh_CN'
  13. import 'moment/locale/zh-cn'
  14. // polyfill
  15. import 'core-js/stable'
  16. import 'regenerator-runtime/runtime'
  17. ReactDOM.render(
  18. <Provider store={store}>
  19. <ConfigProvider locale={zhCN} renderEmpty={Empty}>
  20. <App />
  21. </ConfigProvider>
  22. </Provider>,
  23. document.getElementById('root')
  24. )

CSS Modules

create-react-app 自带支持以 xxx.module.(c|le|sa)ss 的样式表文件,使用上 typescript 项目中要注意:

  1. const styles = require('./index.module.less')
  2. retrun (
  3. <div className={`${styles.container}`}>
  4. <Table
  5. columns={columns}
  6. className={`${styles['border-setting']}`}
  7. dataSource={props.store.check.items}
  8. rowKey={record => record.id}
  9. pagination={false}
  10. />
  11. <div className="type-check-box"></div>
  12. </div>
  13. )
  1. // index.module.less
  2. .container {
  3. padding: 24px;
  4. background-color: #fff;
  5. height: 100%;
  6. overflow: auto;
  7. .border-setting {
  8. tr {
  9. td:nth-child(3) {
  10. border-left: 1px solid #F0F0F0;
  11. border-right: 1px solid #F0F0F0;
  12. }
  13. }
  14. td {
  15. text-align: left !important;
  16. }
  17. }
  18. :global { // 这个标识之后,其子代元素可以不需要使用 `styles['type-check-box']` 的方式,直接写 `className`
  19. .type-check-box {
  20. .ant-checkbox-wrapper + .ant-checkbox-wrapper{
  21. margin-left: 0;
  22. }
  23. }
  24. }
  25. }

【新】配置 React jsx 指令式属性 r-ifr-forr-modelr-show,提升开发效率:

  • 安装依赖

    1. $ yarn add babel-react-rif babel-react-rfor babel-react-rmodel babel-react-rshow -D
  • 配置 .babelrc :

    1. // .babelrc
    2. {
    3. ...,
    4. "plugins": [
    5. "babel-react-rif",
    6. "babel-react-rfor",
    7. "babel-react-rmodel",
    8. "babel-react-rshow",
    9. ]
    10. }
  • 使用示例:

  • r-if

    1. <div>
    2. <h1 r-if={height < 170}>good</h1>
    3. <h1 r-else-if={height > 180}>best</h1>
    4. <h1 r-else>other</h1>
    5. </div>
  • r-for

    1. {/* eslint-disable-next-line no-undef */}
    2. <div r-for={(item, index) in [1, 2, 3, 4]} key={index}>
    3. 内容 {item + '-' + index}
    4. </div>
  • r-model

    1. <input onChange={this.callback} type="text" r-model={inputVale} />
  • r-show

    1. <div r-show={true}>内容</div> # 注意:这是 `r-if` 的效果,不会渲染节点

    四、项目配置二(优化配置)

    实现组件懒加载 react-loadable

    ```typescript import Loadable from ‘react-loadable’

const Loading = (props: any) => { if (props.error) { console.error(props.error) return

Error!
} else if (props.timedOut) { return
Timeout!
} else if (props.pastDelay) { return
Loading…
} else { return null } }

const loadable = (path: any) => { return Loadable({ loader: () => import(@/pages${path}), loading: Loading, delay: 200, timeout: 10000, }) }

const Home = loadable(‘/homePage/Home’)

  1. <a name="KZWMh"></a>
  2. ### 处理 axios 拦截响应
  3. ```typescript
  4. const service = axios.create({
  5. baseURL: '/',
  6. timeout: 15000,
  7. })
  8. service.interceptors.request.use(function (config) {
  9. return config
  10. })
  11. service.interceptors.response.use(function (config) {
  12. return config
  13. })

处理 React router 的嵌套配置

React 中不支持类似 Vue Router 路由配置方式,React 中一切皆组件,路由也是组件,需要用到路由要临时加上路由组件,写起来就很繁琐,但可以自己实现路由配置表方式。

  1. // router/router.config.ts
  2. const routes = [
  3. {
  4. path: '/home',
  5. component: loadable('components/Index'),
  6. exact: true,
  7. },
  8. {
  9. path: '/new',
  10. component: loadable('components/New'),
  11. redirect: '/new/list',
  12. // exact: true,
  13. routes: [
  14. {
  15. path: '/new/list',
  16. component: loadable('components/NewList'),
  17. exact: true,
  18. },
  19. {
  20. path: '/new/content',
  21. component: loadable('components/NewContent'),
  22. exact: true,
  23. },
  24. ],
  25. },
  26. ]
  27. export default routes
  28. // router/router.ts
  29. import React from 'react'
  30. import { Switch, BrowserRouter as Router, Route } from 'react-router-dom'
  31. import routes from './index'
  32. function mapRoutes(routes: any[], store: object): any {
  33. return routes.map((item: any, index: number) => {
  34. return (
  35. <Route exact={item.exact || false} path={item.path} key={index} render={props => {
  36. const NewComp = item.component
  37. Object.assign(props, {
  38. redirect: item.redirect || null,
  39. permission: item.permission || [],
  40. ...store
  41. })
  42. if (item.routes) {
  43. return <NewComp {...props}>{ mapRoutes(item.routes, store) }</NewComp>
  44. } else {
  45. return <NewComp {...props} />
  46. }
  47. }} />
  48. )
  49. })
  50. }
  51. const Routes = (props: any) => {
  52. return (
  53. <Router>
  54. <Switch>
  55. { mapRoutes(routes, props.store) }
  56. <Route component={() => (<div>404 Page not Found!</div>)} />
  57. </Switch>
  58. </Router>
  59. )
  60. }
  61. export default Routes

子路由承载页面需要加上如下代码:

  1. import { Redirect, Route, Switch } from 'react-router-dom'
  2. <Switch>
  3. {props.children}
  4. <Route component={() => (<div>404 Page not Found!</div>)} />
  5. {props.redirect && <Redirect to={props.redirect} />}
  6. </Switch>

处理 React store

  1. // store/store.ts
  2. import { createStore, applyMiddleware } from 'redux'
  3. import thunk from 'redux-thunk'
  4. import logger from 'redux-logger'
  5. import reducers from './reducer'
  6. const store = process.env.NODE_ENV === 'development'
  7. ? createStore(reducers, applyMiddleware(thunk, logger))
  8. : createStore(reducers, applyMiddleware(thunk))
  9. export default store

为了方便使用,避免每个组件都需要 connect ,这边实现了 redux store 的全局注入,但是如果项目庞大的话就会损耗些性能。

  1. // App.tsx
  2. import { dispatchActions } from '@/store/reducer'
  3. export default connect((state: any) => ({ store: state }), dispatchActions)(App)