在公司开发react的这几年

101 阅读4分钟

前言

这篇文章主要是介绍我在公司对于 react 的开发历史,也算记录自己的学习经历。

起航

公司 19 年才开始使用 react 框架,刚用的时候,是公司领导找的一个朋友来从头开始教我们搭建脚手架,初期版本用的是 react 15+webpack 3.x 版本搭建的,这个架子基本上算是领导朋友搭建的,我们只是在这上面做开发。在这个版本时期,我个人对于 react 还停留在会用阶段,脚手架搭建也不会,只能在前人的基础上开发,早期公司用 react 主要是开发移动端项目。

var webpack = require('webpack')

var path = require('path')

const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
  externals: {
    BMap: 'BMap',
  },
  entry: {
    vendor: ['react', 'react-dom', 'jquery'],
    'babel-polyfill': 'babel-polyfill',
    app: path.resolve(__dirname, '../src/app.js'),
  },

  output: {
    path: path.resolve(__dirname, `./dist`),
    publicPath: '/',
    filename: '[name]-[hash].js',
  },

  resolve: {
    alias: {
      common: path.resolve(__dirname, '../src/common'),
      component: path.resolve(__dirname, '../src/common/component'),
      static: path.resolve(__dirname, '../src/static'),
    },
    extensions: ['.web.js', '.js', '.json', '.jsx', '.less', '.css', '.scss'],
  },

  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        use: ['babel-loader'],
      },
      {
        test: /\.(less|css)$/,
        use: ExtractTextPlugin.extract({
          fallback: 'style-loader',
          use: [
            {
              loader: 'css-loader',
              options: {
                minimize: true,
                sourceMap: true,
              },
            },
            {
              loader: 'less-loader',
              options: {
                sourceMap: true,
              },
            },
          ],
        }),
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              name: 'img/[name].[hash:7].[ext]',
              //prefix: 'img'
            },
          },
        ],
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
              name: 'fonts/[name].[hash:7].[ext]',
            },
          },
        ],
      },
    ],
  },
  devServer: {
    proxy: {
      '/portal': {
        target: 'https://www.api.com/',
        changeOrigin: true,
        pathRewrite: {
          '^/portal': '/portal',
        },
      },
    },
    host: 'localhost',
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      filename: 'vendor.js',
      name: ['jquery'],
    }),

    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery',
      Promise: 'es6-promise',
    }),
    new webpack.ProvidePlugin({
      'window.store': 'store2',
      store: 'store2',
    }),

    new ExtractTextPlugin({
      filename: 'index-[contenthash].css',
      disable: false,
      allChunks: true,
    }),
  ],
}

深入

20年的时候,我的组长准备离职,我就接任了他的任务,继续维护公司的前端项目。

在开发了一段时间后,觉得早期直接用 webpack 搭建的脚手架不太友好,很多东西都要自己去下载,去配置。后面了解到 react 官方推荐的 create-react-app(cra),就着手升级脚手架。用 cra 搭建了两个版本的架子,早期使用 react-app-rewired+customize-cra 来组合配置管理 webpack,后面觉得太麻烦又换成了 craco 来管理 webpack,react 版本也升级到了 16.8 以后的版本。

const path = require('path')
const webpack = require('webpack')
const CracoLessPlugin = require('craco-less')
const WebpackBar = require('webpackbar') //进度条
const TerserWebpackPlugin = require('terser-webpack-plugin') //取消console日志打印
const PostCss = require('postcss-pxtorem') //配置rem,pc项目需要注释掉
const HtmlWebpackPlugin = require('html-webpack-plugin') //
const WebpackCDNInject = require('webpack-cdn-inject') // 增加CDN文件插入
const BundleAnalyzerPlugin =
  require('webpack-bundle-analyzer').BundleAnalyzerPlugin //打包文件分析:开发环境不需要启动注释掉
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') //压缩css
//多线程
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({
  size: os.cpus().length,
})

const NPM_LIFECYCLE_EVENT = process.env.npm_lifecycle_event //获取package.json中的scripts启动类型
const pageconfig = require(`./pages.config`) //项目配置
module.exports = {
  eslint: {
    enable: false,
  },
  webpack: {
    //别名
    alias: {
      '@': path.resolve('src'),
    },
    plugins: [
      new WebpackBar(), //进度条
      new HappyPack({
        //用id来标识处理那里类文件
        id: 'happyBabel',
        //如何处理  用法和loader 的配置一样
        loaders: [
          {
            loader: 'babel-loader?cacheDirectory=true',
          },
        ],
        //共享进程池
        threadPool: happyThreadPool,
        //允许 HappyPack 输出日志
        verbose: true,
      }),
      new webpack.DefinePlugin({
        //配置引用变量
        'process.env': {
          PRD_KEY: JSON.stringify(pageconfig.prdKey), //区分产品线
          API_URL: JSON.stringify(pageconfig.api), //api 有多个API 单独配置多个
          CDN_URL: JSON.stringify(pageconfig.cdn_common), //cdn
        },
      }),
      //new BundleAnalyzerPlugin(), //打包文件分析:开发环境不需要启动注释掉
    ],
    configure: (webpackConfig, { env, paths }) => {
      //重写 webpack 任意配置
      paths.appBuild = `./dist` //'dist'
      webpackConfig.output = {
        ...webpackConfig.output,
        path: path.resolve(__dirname, `./dist`), // 修改输出文件目录
      }
      //禁止生产环境生成sourceMap文件
      webpackConfig.devtool =
        env === 'development' ? 'cheap-module-source-map' : false
      webpackConfig.externals = {
        jquery: '$',
      } // 使用cdn引入
      webpackConfig.plugins.push(
        new HtmlWebpackPlugin({
          template: './public/index.html',
          inject: true,
          isRem: true, //判断是否需要rem,如果需要改成true
        }),
        new WebpackCDNInject({
          //cdn 有需要的写配置 注意路径配置,HtmlWebpackPlugin和WebpackCDNInject同时使用HtmlWebpackPlugin要在前
          head: pageconfig.isShowConsole
            ? pageconfig.cdn_console.concat(pageconfig.cdn_common)
            : pageconfig.cdn_common,
        }),
      )
      webpackConfig.optimization = {
        minimize: true,
        minimizer: [
          new TerserWebpackPlugin({
            cache: true,
            parallel: true,
            sourceMap: false,
            terserOptions: {
              ecma: undefined,
              warnings: false,
              parse: {},
              compress: {
                drop_console: false,
                drop_debugger: false, // 移除断点
                pure_funcs:
                  NPM_LIFECYCLE_EVENT.indexOf('build') === 0
                    ? ['console.log']
                    : '', // 生产环境下移除console
              },
            },
          }),
          new OptimizeCssAssetsPlugin({ parallel: true }),
        ],
        splitChunks: {
          chunks: 'async',
          minSize: 30000,
          maxSize: 0,
          minChunks: 1,
          maxAsyncRequests: 5,
          maxInitialRequests: 3,
          automaticNameDelimiter: '~',
          name: true,
          cacheGroups: {
            vendors: {
              name: 'vendors',
              test: /node_modules/,
              minChunks: 1,
              priority: -20,
            },
            echarts: {
              name: 'echarts',
              test: /echarts/,
              minChunks: 1,
              priority: -10,
            },
            reactDayPicker: {
              name: 'react-day-picker',
              test: /react-day-picker/,
              minChunks: 1,
              priority: -10,
            },
            default: {
              minChunks: 2,
              priority: -20,
              reuseExistingChunk: true,
            },
          },
        },
      }
      return webpackConfig
    },
  },
  babel: {
    plugins: [
      //按需加载antd
      [
        'import',
        {
          libraryName: 'antd-mobile',
          style: true,
        },
      ],
    ],
  },
  plugins: [
    {
      //自定义antd样式
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            modifyVars: {
              '@fill-body': '#ffffff',
              '@brand-primary': '#1FC926',
              '@color-text-base': '#ffffff',
            },
            javascriptEnabled: true,
          },
        },
      },
    },
  ],
  style: {
    postcss: {
      //设置rem
      plugins: [
        PostCss({
          rootValue: 50,
          propList: ['*'],
        }),
      ],
    },
  },
}

成熟

在开发过程中,我发现了一个痛点,就是来一个新项目就要复制一份基础框架,然后重新安装所有依赖。

我们开发项目的前端页面都是跟着后台服务走的,公司有一个网关系统,主要用来登录和管理菜单,其余系统通过 iframe 的方式嵌入到网关系统,这样就导致后台创建一个新项目,前端也要跟着复制一个新的基础框架,然后写代码,期间研究过乾坤微前端,不太适合公司现有框架。

在复制开发了 10 多个项目后,有点受不了了。决定再次重构公司的脚手架。在这期间公司的 pc 项目也在准备转 react 模式(早期 react 基本上开发的都是移动端项目),然后了解到 antd,并且了解了 antd 推荐的脚手架 Umi,决定用 Umi 再次重构脚手架。这次重构需要解决的最大难点就是一个架子多项目打包。

import { defineConfig } from '@umijs/max'
import { resolve } from 'path'
import routes from './route_config'
import { globalVariable, buildPath, moduleName } from './project.config'
import { DEFAULT_COLOR } from './src/common/constant'
const prdEnv = process.env.NODE_ENV === 'production'
export default defineConfig({
  alias: {
    '@': resolve(__dirname, 'src'),
    _test: resolve(__dirname, 'src/modules/test'),
  },
  theme: {
    '@primary-color': DEFAULT_COLOR,
  },
  define: globalVariable,
  hash: true,
  history: {
    type: 'hash',
  },
  icons: { autoInstall: {} },
  locale: {
    antd: true, // 如果项目依赖中包含 `antd`,则默认为 true
  },
  outputPath: buildPath,
  publicPath: prdEnv ? './' : '/',
  routes: [
    { exact: true, path: '/', redirect: '/404' },
    {
      path: '/',
      component: '@/layouts/index',
      routes: routes[moduleName],
    },
  ],
  tailwindcss: {},
  title: '加载中...',
  proxy: {
    '/api': {
      target: 'https://www.api.cn',
      changeOrigin: true,
      pathRewrite: {
        '^/api': '',
      },
    },
  },
})

主要是通过 package.json 的命令来区分环境和所要打包的项目

{
  "name": "xiaoliu",
  "version": "2.0.0",
  "private": true,
  "keywords": ["umi", "react", "antd"],
  "author": "xiaoliu",
  "scripts": {
    "analyze:test": "cross-env ANALYZE=1 max build ",
    "dev:test": "max build",
    "prd:test": "max build",
    "start:test": "max dev",
    "start": "npm run start:test"
  },
  "dependencies": {
    "@ant-design/compatible": "^5.1.1",
    "@ant-design/pro-components": "^2.3.51",
    "@dnd-kit/core": "^6.0.8",
    "@dnd-kit/sortable": "^7.0.2",
    "@dnd-kit/utilities": "^3.2.1",
    "@umijs/max": "^4.0.72",
    "@wangeditor/editor": "^5.1.23",
    "@wangeditor/editor-for-react": "^1.0.6",
    "ahooks": "^3.7.8",
    "antd": "^5.7.0",
    "axios": "^1.4.0",
    "classnames": "^2.3.2",
    "echarts": "^5.4.2",
    "immutable": "^4.3.0",
    "mockjs": "^1.1.0",
    "nprogress": "^0.2.0",
    "query-string": "^8.1.0",
    "react-markdown": "^8.0.6",
    "react-syntax-highlighter": "^15.5.0",
    "rehype-katex": "^6.0.2",
    "rehype-raw": "^6.1.1",
    "remark-gfm": "^3.0.1",
    "remark-math": "^5.1.1"
  },
  "devDependencies": {
    "@iconify-json/ant-design": "^1.1.4",
    "@types/echarts": "^4.9.17",
    "@types/react": "^18.0.0",
    "@types/react-dom": "^18.0.0",
    "cross-env": "^7.0.3",
    "prettier": "^2.7.1",
    "prettier-plugin-organize-imports": "^2",
    "prettier-plugin-packagejson": "^2",
    "tailwindcss": "^3",
    "typescript": "^4.1.2"
  }
}
//project.config.ts
//冒号前面表示环境、冒号后面表示打包的项目
// start:test 本地启动
// dev:test 开发环境打包
// prd:test 正式环境打包
const startType = process.env.npm_lifecycle_event!.split(':')[0] // 获取package.json中的scripts启动类型
const moduleName = process.env.npm_lifecycle_event!.split(':')[1] // 获取package.json中的scripts启动模块
/**
 * 启动配置
 */
const startConfig: G.Obj = require(`./config/${startType}/pages.config`)
/**
 * 构建路径
 */
const buildPath = `./build/${startType}/${moduleName}/dist`
/**
 * 功能模块接口所属接口
 */
const modulesAPI = {
  test: 'api_test',
}
/**
 * 全局变量
 */
const globalVariable = {
  'process.env.API_URL': startConfig[modulesAPI[moduleName]], // api 有多个API 单独配置多个
}

export { globalVariable, buildPath, moduleName }

路由配置根据项目的不同分成不同的文件夹,同一个项目的路由都放在一个目录下,统一输出

//route_config

import testRoutes from './test'
//routeConfig 就是关键点,根据命令行冒号后面的内容来判断需要打包哪些路由
const routeConfig = {
  test: testRoutes,
}
export default routeConfig

//test
const testRoutes = [
  {
    exact: true,
    path: '/test',
    title: '列表',
    component: '@/modules/test/list',
  },
  {
    exact: true,
    path: '/test/view',
    title: '详情',
    component: '@/modules/test/view',
  },
]

export default testRoutes

src/modules 文件夹,所有的项目都写在这里,根据类型分别定义项目主目录

modules/test1 、 modules/test2,当前项目的代码都写在目录里面。

总结

对于 react 的开发,已经从最初的 class 到现在全面转向了 hooks,今年也融入了 ts,继续在前端努力进步。