从 0 搭建一个前端项目,我们要做什么?

4,019 阅读4分钟

不使用脚手架搭建 React 项目:https://github.com/zhuyuanmin/react-0-1-build。读者可根据提交的分支顺序一步步搭建,所有库都使用了最新版本,让我们在踩坑中成长!【master 分支:完整版,不包含 typescript ;typescript-react 分支: 包含 typescript 的完整版本】

一、项目启动

  1. 了解需求背景

  2. 了解业务流程

二、项目搭建初始化

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

  1. 设置淘宝镜像仓库

    $ yarn config set registry registry.npm.taobao.org/ -g

    $ yarn config set sass_binary_site cdn.npm.taobao.org/dist/node-s… -g

  2. 工程目录 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
    
  3. 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
    
  4. 连接 git 远程仓库

    $ git remote add origin zhu%40wetax.com:xxxmm@gitlab.yunpiaoer.com/front/qpj-w…

  5. 添加 .gitignore

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

  6. 添加 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,内容可参考文章配置:zhuanlan.zhihu.com/p/84329603 看第五点

    • Commit message 格式说明

      <type>: <subject>

      • 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 进行格式修复,无法修复的需手动解决

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

  1. 安装项目常用依赖库

    $ yarn add antd axios dayjs qs -S # UI 库 及工具库

    $ yarn add react-router-dom redux react-redux redux-logger redux-thunk -S # 路由及状态管理

  2. 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)
      
  3. 配置按需加载

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

    // config-overrides.js
    ...
    module.exports = override(
        addLessLoader({
            lessOptions: {
                javascriptEnabled: true,
                modifyVars: {
                    '@primary-color': '#1890ff',
                },
            }
        }),
    )
    
  5. 别名配置(typescript 项目这里有坑)

    // config-overrides.js
    const path = require('path')
    ...
    module.exports = override(
        addWebpackAlias({
            '@': path.resolve(__dirname, 'src'),
        }),
    )
    
  6. 去除注释、多进程打包压缩

    // 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
    })
    
  7. 解决埋下的两个坑

    • 修改打包出的文件夹名为 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": {
                    "@/*": ["*"]
                }
            }
        }
        
  8. 配置装饰器写法

    {
        "compilerOptions": {
            "experimentalDecorators": true,
            ...
        }
    }
    
  9. 配置开发代理

    • 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": ""
                  // }
              })
          )
      }
      
  10. 加入 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(
        <Provider store={store}>
            <ConfigProvider locale={zhCN} renderEmpty={Empty}>
            <App />
            </ConfigProvider>
        </Provider>,
        document.getElementById('root')
    )
    
  11. 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;
                }
            }
        }
    }
    
  12. 【新】配置 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
        <div>
          <h1 r-if={height < 170}>good</h1>
          <h1 r-else-if={height > 180}>best</h1>
          <h1 r-else>other</h1>
        </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` 的效果,不会渲染节点
        

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

  1. 实现组件懒加载 react-loadable
    import Loadable from 'react-loadable'
    
    const Loading = (props: any) => {
        if (props.error) {
            console.error(props.error)
            return <div>Error! <Button type="link" onClick={props.retry}>Retry</Button></div>
        } else if (props.timedOut) {
            return <div>Timeout! <Button onClick={props.retry}>Retry</Button></div>
        } else if (props.pastDelay) {
            return <div>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')
    
    
  2. 处理 axios 拦截响应
    const service = axios.create({
        baseURL: '/',
        timeout: 15000,
    })
    
    service.interceptors.request.use(function (config) {
        return config
    })
    
    service.interceptors.response.use(function (config) {
        return config
    })
    
  3. 处理 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 <NewComp {...props}>{ mapRoutes(item.routes, store) }</NewComp>
                    } else {
                        return <NewComp {...props} />
                    }
                }} />
            )
        })
    }
    
    const Routes = (props: any) => {
        return (
            <Router>
                <Switch>
                    { mapRoutes(routes, props.store) }
                    <Route component={() => (<div>404 Page not Found!</div>)} />
                </Switch>
            </Router>
        )
    }
    
    export default Routes
    

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

    import { Redirect, Route, Switch } from 'react-router-dom'
    
    <Switch>
        {props.children}
        <Route component={() => (<div>404 Page not Found!</div>)} />
        {props.redirect && <Redirect to={props.redirect} />}
    </Switch>
    
  4. 处理 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)
    

五、总结

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

六、题外话

基于 create-react-app 创建的 React 项目,本人实现了一个脚手架,以上配置默认已经全部加入实现,欢迎 Github 试用并 star 。链接:github.com/zhuyuanmin/…