一、项目启动

了解需求背景

了解业务流程

二、项目搭建初始化

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

设置淘宝镜像仓库

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

工程目录 init

$ create-react-app qpj-web-pc --typescript
$ tree -I "node_modules"

    .
    |-- README.md
    |-- package.json
    |-- public
    |   |-- favicon.ico   
    |   |-- index.html
    |   |-- logo192.png   
    |   |-- logo512.png   
    |   |-- manifest.json 
    |   `-- robots.txt
    |-- src
    |   |-- App.css
    |   |-- App.test.tsx  
    |   |-- App.tsx
    |   |-- index.css
    |   |-- index.tsx
    |   |-- logo.svg
    |   |-- react-app-env.d.ts
    |   |-- reportWebVitals.ts
    |   `-- setupTests.ts 
    `-- tsconfig.json

yarn build试试

$ yarn build & tree -I "node_modules"

    .
    |-- README.md
    |-- build/ # 改造点(由于 `Jenkins` 构建打包脚本有可能已经写死了 `dist` 包名)
    |-- package.json
    |-- public
    |   |-- favicon.ico   
    |   |-- index.html
    |   |-- logo192.png   
    |   |-- logo512.png   
    |   |-- manifest.json 
    |   `-- robots.txt
    |-- src
    |   |-- App.css
    |   |-- App.test.tsx  
    |   |-- App.tsx
    |   |-- index.css
    |   |-- index.tsx
    |   |-- logo.svg
    |   |-- react-app-env.d.ts
    |   |-- reportWebVitals.ts
    |   `-- setupTests.ts 
    `-- tsconfig.json

连接git远程仓库

$ git remote add origin yuanmin.zhu%40wetax.com.cn:wd246800mm@gitlab.yunpiaoer.com/front/qpj-web-pc.git

添加.gitignore

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

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

$ yarn add husky lint-staged @commitlint/cli @commitlint/config-conventional -D
$ npx husky install
$ npx husky add .husky/pre-commit "npx lint-staged"
$ npx husky add .husky/prepare-commit-msg "npx commitlint -e"

  • 项目根目录新建 commitlint.config.js
// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
      'type-enum': [
          2,
          'always',
          ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert'],
      ],
      'subject-full-stop': [0, 'never'],
      'subject-case': [0, 'never'],
  },
}
  • vscode 扩展中搜索 ESLint 并安装,项目根目录新建.eslintrc.js,内容可参考文章配置: 看第五点
  • type 值枚举如下:

feat: 添加新特性
fix: 修复 bug
docs: 仅仅修改了文档
style: 仅仅修改了空格、格式缩进、都好等等,不改变代码逻辑
refactor: 代码重构,没有加新功能或者修复 bug
perf: 增加代码进行性能测试
test: 增加测试用例
chore: 改变构建流程、或者增加依赖库、工具等
revert: 当前 commit 用于撤销以前的 commit

  • subject 是 commit 目的的简短描述,不超过 50 个字符,且结尾不加句号(.)
  • package.json 新加入如下配置:
{
    ...,
    "lint-staged": {
        "src/**/*.{jsx,txs,ts,js,json,css,md}": [
            "eslint --quiet"
        ]
    },
}
  • 可执行 npx eslint [filePath] --fix 进行格式修复,无法修复的需手动解决

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

安装项目常用依赖库

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

webpack 配置拓展很有必要

  • 根目录新建 config-overrides.js,详细使用可访问:简书:React 之 config-overrides文件配置
  • 安装

$ yarn add react-app-rewired customize-cra -D

  • 修改 package.json 中启动项
// package.json
"scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
}
  • 使用
// config-overrides.js
const {
    override, // 主函数
    fixBabelImports, // 配置按需加载
    addWebpackExternals, // 不做打包处理配置
    addWebpackAlias, // 配置别名
    addLessLoader // lessLoader 配置,可更改主题色等
} = require('customize-cra')

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

配置按需加载

// config-overrides.js
...
module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es', // library 目录
        style: true, // 自动打包相关的样式
    }),
)

更改主题色

// config-overrides.js
...
module.exports = override(
    addLessLoader({
        lessOptions: {
            javascriptEnabled: true,
            modifyVars: {
                '@primary-color': '#1890ff',
            },
        }
    }),
)

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

// config-overrides.js
const path = require('path')
...
module.exports = override(
    addWebpackAlias({
        '@': path.resolve(__dirname, 'src'),
    }),
)

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

// config-overrides.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
...
module.exports = override(/* ... */, config => {
    config.plugins = [...config.plugins, {
        new UglifyJsPlugin({
            uglifyOptions: {
                warnings: false,
                compress: {
                    drop_debugger: true,
                    drop_console: true,
                },
            },
        }),
        new HardSourceWebpackPlugin()
    }]
    return config
})

解决埋下的两个坑

  • 修改打包出的文件夹名为 dist
// 修改打包路径除了output,这里也要修改
const paths = require('react-scripts/config/paths')
paths.appBuild = path.join(path.dirname(paths.appBuild), 'dist')

module.exports = override(/* ... */, config => {
    config.output.path = path.resolve(__dirname, 'dist')

    return config
})
  • 解决 typescript 别名配置

查阅相关资料,需要在 tsconfig.json 中添加一项配置

{
    ...
    "extends": "./paths.json"
}
  • 新建文件 paths.json
{
    "compilerOptions": {
        "baseUrl": "src",
        "paths": {
            "@/*": ["*"]
        }
    }
}

配置装饰器写法

{
    "compilerOptions": {
        "experimentalDecorators": true,
        ...
    }
}

配置开发代理

  • src 目录新建 setupProxy.js
// 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": ""
            // }
        })
    )
}

加入 polyfillantd 组件国际化处理

// src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
// 注入 store
import { Provider } from 'react-redux'
import store from '@/store/store'

import { ConfigProvider, Empty } from 'antd'
import App from './App'
import zhCN from 'antd/es/locale/zh_CN'
import 'moment/locale/zh-cn'

// polyfill
import 'core-js/stable'
import 'regenerator-runtime/runtime'

ReactDOM.render(
    
        
        
        </ConfigProvider>
    Provider>,
    document.getElementById('root')
)

CSS Modules

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

const styles = require('./index.module.less')

retrun (
    <div classname={`${styles.container}`}>
        <table
            columns={columns}
            className={`${styles['border-setting']}`}
            dataSource={props.store.check.items}
            rowKey={record => record.id}
            pagination={false}
        />
        <div classname="type-check-box"></div>
    div>
)
// index.module.less
.container {
padding: 24px;
background-color: #fff;
height: 100%;
overflow: auto;
.border-setting {
tr {
td:nth-child(3) {
border-left: 1px solid #F0F0F0;
border-right: 1px solid #F0F0F0;
}
}
td {
text-align: left !important;
}
}
:global { // 这个标识之后,其子代元素可以不需要使用 `styles['type-check-box']` 的方式,直接写 `className`
.type-check-box {
.ant-checkbox-wrapper + .ant-checkbox-wrapper{
margin-left: 0;
}
}
}
}

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

  • 安装依赖

$ yarn add babel-react-rif babel-react-rfor babel-react-rmodel babel-react-rshow -D

  • 配置 .babelrc
// .babelrc
{
    ...,
    "plugins": [
        "babel-react-rif",
        "babel-react-rfor",
        "babel-react-rmodel",
        "babel-react-rshow",
    ]
}
  • 使用示例

r-if

  <h1 r-if={height < 170}>goodh1>
  <h1 r-else-if={height > 180}>besth1>
  <h1 r-else>otherh1>
</div>

r-for

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

r-model

<input onchange={this.callback} type="text" r-model={inputVale} />

r-show

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

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

实现组件懒加载 react-loadable

import Loadable from 'react-loadable'

const Loading = (props: any) => {
    if (props.error) {
        console.error(props.error)
        return 
Error! <button type="link" onClick={props.retry}>Retry</Button>div>
    } else if (props.timedOut) {
        return 

Timeout! Retry</Button>div>
    } else if (props.pastDelay) {
        return 

Loading...</div>
    } else {
        return null
    }
}

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

const Home = loadable('/homePage/Home')

处理 axios 拦截响应

const service = axios.create({
    baseURL: '/',
    timeout: 15000,
})

service.interceptors.request.use(function (config) {
    return config
})

service.interceptors.response.use(function (config) {
    return config
})

处理 React router 的嵌套配置

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

// router/router.config.ts
const routes = [
    {
        path: '/home',
        component: loadable('components/Index'),
        exact: true,
    },
    {
        path: '/new',
        component: loadable('components/New'),
        redirect: '/new/list',
        // exact: true,
        routes: [
            {
                path: '/new/list',
                component: loadable('components/NewList'),
                exact: true,
            },
            {
                path: '/new/content',
                component: loadable('components/NewContent'),
                exact: true,
            },
        ],
    },
]

export default routes
// router/router.ts
import React from 'react'
import { Switch, BrowserRouter as Router, Route } from 'react-router-dom'
import routes from './index'

function mapRoutes(routes: any[], store: object): any {
    return routes.map((item: any, index: number) => {
        return (
            <route exact={item.exact || false} path={item.path} key={index} render={props => {
                const NewComp = item.component
                Object.assign(props, {
                    redirect: item.redirect || null,
                    permission: item.permission || [],
                    ...store
                })
                if (item.routes) {
                    return { mapRoutes(item.routes, store) }</NewComp>
                } else {
                    return <newcomp {...props}  < span>>
                }
            }} />
        )
    })
}

const Routes = (props: any) => {
    return (
        
            
                { mapRoutes(routes, props.store) }
                <route component={() => (
404 Page not Found!</div>)} />
            </Switch>
        Router>
    )
}

export default Routes

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

import { Redirect, Route, Switch } from 'react-router-dom'


    {props.children}
    <route component={() => (
404 Page not Found!</div>)} />
    {props.redirect && }
</Switch>

处理 React store

// store/store.ts
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import reducers from './reducer'

const store = process.env.NODE_ENV === 'development'
    ? createStore(reducers, applyMiddleware(thunk, logger))
    : createStore(reducers, applyMiddleware(thunk))

export default store

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

// App.tsx
import { dispatchActions } from '@/store/reducer'

export default connect((state: any) => ({ store: state }), dispatchActions)(App)

五、总结

自此项目搭建就全部完成了,剩下的就是业务代码了。相信你可以得到如下收获:
① 项目构建在宏观上有个极大的能力提升;
②项目整体功能了解清晰;
③ 排查问题不慌乱;
④ 封装能力有加强;
⑤ 业务功能很清楚。

本文转载自微信公众号 前端学习栈