文章已收录至https://lichong.work,转载请注明原文链接。 ps:欢迎关注公众号“Fun肆编程”或添加我的私人微信交流经验🤝 👍文章中优化的项目已整理为模板工程提交至GitHub,欢迎star😉

背景

最近接到一个项目,说是一个老的React项目,希望做个优化升级,我刚clone下来代码人就傻了(钱要少了),先看一眼原始项目吧: 原始项目

问题点

  1. 项目有依赖冲突混乱问题,所以npm install装不上,只能用cnpm或者--force强制安装,一些node版本下依然会出现问题
  2. 项目依赖版本过低不易维护,最好升级下版本(貌似是用的 NodeJS 4 !!!)
  3. 项目结构混乱,即引入了webpack-dev-server,还引入了express;express完全没有发挥优势反而增加项目复杂度
  4. 这个项目是基于一个叫UEditor的百度开源原生js项目做的二次开发及扩展,原生js部分直接写死在了HTML里,每次修改源码后都手动压缩混淆js为min文件再改HTML,究其原因还是开发者对webpack不熟悉,并且使用错误配置方式,将ouput输出到了public,但public同时还是资源目录(脑阔疼!!!)
  5. 打包时index.html中引入js文件时还需要手动指定min的压缩文件,没有充分发挥webpack的作用
  6. React编码错误,导致开发环境控制台有warning,会降低效率、增加后期维护成本(生命周期使用错误)
  7. 没有引入ESLint/Prettier/StyleLint等开发组件,导致多人开发时代码风格不一致,甚至存在部分漏洞
  8. 项目活文档不完善,只有一个默认的README.md,对新人十分不友好

优化过程

  1. 去掉express 项目最终是部署在nginx的,实在不理解引进express的目的是什么。可能当时比较火吧,大家都想玩玩,删掉express的配置js,再删掉package.json里的依赖。
npm uninstall -D express
  1. 尝试升级相关项目组件到最新版解决依赖冲突 其中最明显的就是node-sass,相信接触过老项目的应该都被这个坑过,这个组件Sass官方现在也已经不维护了,直接重构了叫sass,但是可以兼容之前的语法,果断弃旧纳新
npm uninstall -D node-sass sass-loader @babel/core @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react classnames
npm install -D sass sass-loader @babel/core @babel/plugin-proposal-class-properties @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react classnames
npm uninstall -S babel-polyfill prop-types react react-dom
npm install -S core-js prop-types react react-dom
  1. 引入webpack-merge用于区分开发环境和生产环境配置
npm install -D webpack-merge
  1. 升级webpack至最新版,开发环境下改用webpack-dev-server,引入copy-webpack-plugin解决问题4
  • 先安装开发组件:
npm install -D webpack webpack-cli webpack-dev-server css-loader html-webpack-plugin copy-webpack-plugin workbox-webpack-plugin babel-loader style-loader source-map-loader
  • 因为低版本的webpack配置和高版本的不太兼容,直接创建新的webpack配置文件吧: --------------build/webpack.common.config.js
const path = require('node:path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ESLintPlugin = require('eslint-webpack-plugin');

const config = {
  entry: {
    index: './src/index.js',
  },

  output: {
    path: path.join(__dirname, '../dist'),
    filename: '[name].bundle.js',
    clean: true,
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      favicon: './public/favicon.ico',
    }),
    new ESLintPlugin({
      emitError: true,
      emitWarning: true,
      failOnError: true,
      failOnWarning: true,
    }),
    new CopyWebpackPlugin({
      patterns: [
        {
          from: path.join(__dirname, '../', 'public', 'core'),
          to: path.join(__dirname, '../', 'dist', 'public', 'core'),
          toType: 'dir',
          force: true,
        },
        {
          from: path.join(__dirname, '../', 'public', 'DatePicker'),
          to: path.join(__dirname, '../', 'dist', 'public', 'DatePicker'),
          toType: 'dir',
          force: true,
        },
      ],
      options: {
        concurrency: 100,
      },
    }),
  ],

  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        include: path.join(__dirname, '../', 'src'),
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.s[ac]ss$/i,
        include: path.join(__dirname, '../', 'src', 'styles'),
        use: [
          'style-loader',
          {
            loader: 'css-loader',
            options: {
              sourceMap: true,
            },
          },
          {
            loader: 'sass-loader',
            options: {
              sourceMap: true,
            },
          },
        ],
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2|png|jpg|gif?)(\?[a-z0-9]+)?$/,
        type: 'asset/resource',
      },
    ],
  },

  performance: {
    hints: false,
    maxEntrypointSize: 1024000,
    maxAssetSize: 1024000,
  },

  ignoreWarnings: [/Failed to parse source map/],
};

module.exports = config;

-------------- build/webpack.dev.config.js

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const { merge } = require('webpack-merge');
const path = require('node:path');
const common = require('./webpack.common.config.js');

const port = process.env.PORT || 8080;

module.exports = merge(common, {
  mode: 'development',
  devtool: 'eval-source-map',
  devServer: {
    hot: true,
    host: 'localhost',
    port,
    historyApiFallback: true,
    open: true,
  },
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        include: path.join(__dirname, '../', 'src'),
        use: [
          {
            loader: require.resolve('babel-loader'),
            options: {
              plugins: [
                require.resolve('react-refresh/babel'),
              ],
            },
          },
        ],
      },
      {
        test: /\.js$/,
        enforce: 'pre',
        use: ['source-map-loader'],
      },
    ],
  },
  plugins: [
    new ReactRefreshWebpackPlugin(),
  ],
});

--------------build/webpack.prod.config.js

const { merge } = require('webpack-merge');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const common = require('./webpack.common.config.js');

module.exports = merge(common, {
  mode: 'production',
  devtool: 'source-map',
  plugins: [
    new WorkboxWebpackPlugin.GenerateSW(),
  ],
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
});

  1. 引入ESLint
npm install -D eslint eslint-config-airbnb eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks eslint-webpack-plugin @babel/eslint-parser

编辑配置文件: --------------eslintrc.js

module.exports = {
  env: {
    browser: true,
    node: true,
    es2021: true,
  },
  extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended', 'airbnb', 'prettier'],
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: '2021',
    sourceType: 'module',
  },
  plugins: ['react', 'react-hooks'],
  settings: {
    react: {
      version: '17.0.2',
    },
  },
  // 声明全局变量
  globals: {
    baidu: true,
    UE: true,
    command: true,
    $: true,
  },
  parser: '@babel/eslint-parser',
  rules: {
    eqeqeq: 2, // 必须使用 === 和 !==
    'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
    'no-empty-function': 2, // 禁止空函数
    'no-multi-spaces': 2, // 禁止使用多个空格
    'no-trailing-spaces': 2, // 禁止禁用行尾空格
    'space-infix-ops': 2, // 要求操作符周围有空格
    'space-in-parens': 2, // 强制在圆括号内使用一致的空格
    'no-var': 2, // 要求使用 let 或 const 而不是 var,
    'no-unused-vars': 2, // 禁止出现未使用过的变量
    'react/prop-types': 0, // 防止在react组件定义中缺少props验证(禁用)
    'react/no-array-index-key': 0, // 防止在数组中使用index作为key(禁用)
    'react/no-danger': 0, // 防止使用危险的 JSX 属性 (react/no-danger)
    'jsx-a11y/click-events-have-key-events': 0, // 点击事件必须绑定一个键盘事件以帮助残疾人士(禁用)
    'jsx-a11y/interactive-supports-focus': 0, // 必须设置tabIndex允许元素获取焦点从而使用键盘tab就可以触发点击帮助残疾人士(禁用)
    'import/no-extraneous-dependencies': [
      'error',
      {
        devDependencies: true,
        optionalDependencies: true,
        peerDependencies: true,
        packageDir: './',
      },
    ],
  },
};

--------------eslintignore

/public/
build
.vscode
.idea
node_modules

  1. 引入Prettier
npm install -D prettier eslint-config-prettier

编辑配置文件: --------------.prettierrc.json

{
  "semi": true,
  "singleQuote": true,
  "jsxSingleQuote": true,
  "trailingComma": "all",
  "printWidth": 120,
  "bracketSameLine": true,
  "tabWidth": 2,
  "useTabs": false,
  "quoteProps": "as-needed",
  "bracketSpacing": true,
  "arrowParens": "always",
  "requirePragma": false,
  "insertPragma": false,
  "proseWrap": "preserve",
  "htmlWhitespaceSensitivity": "ignore",
  "vueIndentScriptAndStyle": false,
  "endOfLine": "lf",
  "embeddedLanguageFormatting": "auto"
}

  1. 引入Husky,增加提交前的git hook husky
  2. 编辑项目简易活文档,方便新手上手。

优化后

  1. 编辑 README.md 提供项目完整介绍文档(包括项目简介、开发规范、项目管理、环境搭建、构建部署、团队协作等)等项目活文档;
  2. 修改 public 作为静态公用资源文件夹而非 output 输出目录,避免混淆源代码和编译后的文件,更规范;
  3. 升级所有组件,引用新版稳定组件代替已弃用组件;
  4. 升级至 React17,可以使用 Hook 方式了;
  5. 升级至 Webpack5,最大化简化配置,同时提供自动压缩 js 能力,不需要手动编译为 min 文件了;
  6. 重构 webpack 配置文件,区别开发环境和生产环境,开发环境提供 source-map 文件控制台可以打印源码行数,方便排查问题,生产环境提供最优化的打包;
  7. 细化规范 package.json,支持 ie10 及以上浏览器,可以控制台输入 browserslist 查看支持浏览器;
  8. 引入 eslint,规范化开发,非必要最好不要修改规范,每一条规范都是有理由的;
  9. 引入 prettier,保证多人开发不同开发工具场景下格式统一;
  10. 修改升级后的 eslint 问题和代码格式化问题;
  11. 修改控制台部分 undefined 报错问题;
  12. 去掉 html 中重复引入的 js;
  13. 集成husky,提供提交代码前自动 eslint --fix 和 prettier 格式化功能,并校验提交信息是否符合规范;

文章已收录至https://lichong.work,转载请注明原文链接。 ps:欢迎关注公众号“Fun肆编程”或添加我的私人微信交流经验🤝 👍文章中优化的项目已整理为模板工程提交至GitHub,欢迎star😉

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~往期精选🪶~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

【Docker】入门教程-基本概念解读 【前端-开发环境】使用NVM实现不同nodejs版本的自由切换(NVM完整安装使用手册) 【前端-NPM私服】内网使用verdaccio搭建私有npm服务器 【前端-IE兼容】Win10和Win11使用Edge调试前端兼容IE6、IE7、IE8、IE9、IE10、IE11问题 【工具-TWRP-frp-Termux】旧手机暴改成免费云服务器-MIUI刷TWRP安装magisk获取root 【工具-Shell脚本】java程序产品包模板-linux和windows通用shell启动停止脚本(无需系统安装Java运行环境) 【工具-Nginx】从入门安装到高可用集群搭建 【工具-Nginx】Nginx高性能通用配置文件-注释版-支持防刷限流、可控高并发、HTTP2、防XSS、Gzip、OCSP Stapling、负载、SSL 【工具-WireShark】网络HTTP抓包使用教程 【后端-maven打包】通过profile标签解决同时打jar包 war包需求 【架构-DDD】使用领域驱动设计-互联网未来架构设计之道(一) 【后端-SpringCache】基于Spring Cache封装一个能够批量操作的Redis缓存记录下踩坑历程(pipeline或mget封装) 【后端-SkyWalking】SkyWalking前后端开发环境搭建详细教程步骤-6.x/7.x/8.x版本通用-插件二次开发利器(一) 【后端-Quartz】Springboot整合Quartz支持集群环境-设计业务与框架分离及实现定时任务调度

✨欢迎为耿直少年点赞、关注、收藏!!!

👇👇👇