老工程打包提速折腾记(下)- roadhog 1.x 到 webpack 4.x

19年的时候搞过一波,具体见:上篇。两年过去了,模块又多了,电脑也变卡了,虽然打包交给公司内部发布平台的服务器了,然而3分多钟的打包时间对于测试和开发都是煎熬。另外,之前就存在修改代码热更新长达十几秒的等待,算上浏览器经常时不时的断掉websocket,只能手动刷新页面,常常需要20多秒才能看到代码修改的效果。是时候再折腾一波了。契机源于逛掘金看到这么篇文章:webpack实战之从roadhog2.x到webpack4.x。正如作者所说,搞完真的是掉了不少头发。

框架搭建于2017年,roadhog + dva + antd,在当时也算是很时髦的了。然而后续忙于迭代,忽略了几个关键库的升级,导致现在变得老旧,升级成本极高,只能在原来基础上修修补补,但毕竟提升有限。

改造前环境:

  • dva: 1.2.1
  • roadhog: 1.3.4
  • webpack:3.12.0
  • babel-core:6.26.0

可以看见webpack版本其实并不低,问题出在roadhog上。

roadhog是什么?

官方文档是这么说的:

roadhog 是一个包含 dev、build 和 test 的命令行工具,他基于 react-dev-utils,和 create-react-app 的体验保持一致。你可以想象他为可配置版的 create-react-app。
以下是特性:

  1. 开箱即用的 react 应用开发工具,内置 css-modules、babel、postcss、HMR 等
  2. create-react-app 的体验
  3. JSON 格式的 webpack 配置
  4. mock
  5. 基于 jest 的 test,包括 UI 测试

第2条和第3条还是很香的,可以快速跳过一系列复杂的webpack配置,开箱即用。同时也可以通过roadhogrc配置文件对webpack进行扩展,也支持自建webpack.config.js来对roadhog自带的配置进行扩展。
然而这种做法缺点也暴露无遗。

  • 首先,roadhog自带webpack配置,这部分对用户不可见,只能通过有限的方式对其进行扩展;
  • 其次,webpack后续版本的新特性,roadhog提供的配置不一定支持,通过自建webpack.config.js更改默认配置项也大都无效;
  • 最后,作者更新到2.0之后转战umi,不再维护。

以上种种直接导致了当项目模块变多之后,打包速度慢到无法忍受。网上也有不少关于提升roadhog编译和打包速度的文章,这些在上篇已经实践过。
这次为了彻底解决这个问题,决定抛弃roadhog,直接拥抱webpack4.x。

改造过程

先看看roadhog输出的是一份怎样的配置?

通过vscode调试结合源码,大致清楚了roadhog的原理。同时把工程里的development和production的最终webpack配置输出先保存下来作为后续修改的参考。

原先配置主要以下几部分:

  1. entry

    入口文件。 webpack4默认’./src/index.js’,现在可以不用配置。

  2. output

    输出文件的目录,这块也不用动,照搬原先配置。

  3. module

    文件解析,针对不同类型的文件配置不同的解析规则(rules),并且指定不同的解析器(loader)。类型文件可以有多种loader可供选择解析,可以按顺序组合他们。具体看官方文档。

    这块我仍然照搬原先配置,不同的是把js解析的babel-loader配置放到了bablerc里面,之前这部分配置也是roadhog内置的。

  4. plugins

    webpack插件,主要用来在webpack处理生命周期中的不同阶段提供处理的钩子。主要有两部分,一部分是webpack内置的插件,一部分是npm安装的插件。老配置里主要用到以下几个插件:

    • HotModuleReplacementPlugin:热更新插件
    • DllReferencePlugin: 预编译插件
    • DefinePlugin: 变量定义插件
    • HtmlWebpackPlugin:html文件处理插件
    • LoaderOptionsPlugin:loader配置插件

    这一块根据实际情况作了筛选和添加,具体见后面详细配置。因为webpack4废弃了一些插件,也内置了一些插件。具体需要查阅官方文档。

  5. devtool

    sourceMap

  6. externals

    不需要参与编辑的库文件

  7. devServer

    webpack启动服务的配置项,这块基本上也没怎么动。

第一次尝试

总的来说就是在原先项目配置的基础上,按照 webpack实战之从roadhog2.x到webpack4.x 做了调整,主要是babel-loader插件、css抽取使用MiniCssExtractPlugin,并根据需要把babel相关的都升级到了7x以上。
结果当然没有那么顺利,跟这篇文章里一样遇到了这几个同样的问题:

  1. style-loader与mini-css-extract-plugin存在冲突

    这个官方文档也说了,使用后者即可。

  2. antd的样式未加载

    原先配置:

    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }]
    复制代码

    style从true改成css就好了。

  3. export default失效

    查了下,下面这种引入方式,webpack4 不支持。

    // a.js
    var a = b = 1;
    export default {
      a,
      b
    };
    
    // c.js
    import {a} from 'a.js';
    复制代码

    必须改成下面这样才行:

    // a.js
    export const a = 1;
    export const b = 1;
    复制代码

    按照这种方式改了之后,编译不报错了,但是页面内容空白,路由没有挂载的感觉。

    试了很多办法都没有效果,我估计是babel那块升级到7.0版本以上引起的。而且roadhog 1x到2x有很多变化,因此webpack实战之从roadhog2.x到webpack4.x 的配置只能部分参考了。

第二次尝试

又是一阵网上冲浪,终于找到了一个跟我们项目的代码库。babel-core跟我们的版本都是一致的,其他的都大差不差,一试页面出来了,路由也有了,唯一的问题是自定义的antd theme不生效。这块地方又卡了我很久很久。问题其实就是less-loader那块。

antd主题设置官网有介绍,2x.ant.design/docs/react/…
文档里是这么写的:

样式必须加载 less 格式。

如果你在使用 babel-plugin-import 的 style 配置来引入样式,需要将配置值从 ‘css‘ 改为 true,这样会引入 less 文件。

改回来之后还是不行,最后发现代码库里针对antd的less文件和项目里的less文件是分成两个loader来处理。顿时恍然大悟,因为我们项目里的样式主要分成两部分:

  • 一部分是antd组件的样式,class形如”ant-btn”这样;

  • 另外一部分就是我们自己写的样式,class形如”container___3nzXj“。也就是说我们自己的样式为了避免污染,使用了css modules,写法如下:

    import React from 'react';
    import style from './App.css';
    
    export default () => {
      return (
        <h1 className={style.title}>
          Hello World
        </h1>
      );
    };
    复制代码

而antd不需要,因此需要分成两个loader。同时我们在antd的less-loader配置里注入主题变量值。最终less的loader配置如下:

{
  test: /\.less$/,
  include: [/antd/],
  use: [
    'style-loader',
    'css-loader',
    {
      loader: 'postcss-loader', //自动加前缀
      options: {
        plugins: [
          require('autoprefixer')({
            overrideBrowserslist: ['last 5 version'],
          }),
        ]
      },
    },
    {
      loader: 'less-loader',
      options: {
        javascriptEnabled: true,
        // 主题变量注入
        modifyVars: {  
          'primary-color': '#2171FF',
          'link-color': '#2171FF',
          'border-radius-base': '2px',
          'font-size-base': '14px',
          'animation-duration-slow': '.2s',
          'animation-duration-base': '.2s',
          'input-height-lg': '30px',
          'btn-height-lg': '30px',
          'label-color': '#222',
          'btn-font-size-lg': '14px',
          'menu-dark-bg': '#001529',
          'menu-dark-submenu-bg': '#000',
        },
      },
    },
  ],
},
{
  test: /\.less$/,
  exclude: [/antd/],
  use: [
    'style-loader',
    {
      loader: 'css-loader',
      options: {
        importLoaders: 1,
        sourceMap: true,
        modules: true,
        localIdentName: '[local]___[hash:base64:5]',
      },
    },
    {
      loader: 'postcss-loader',
      options: {
        plugins: [
          require('autoprefixer')({
            overrideBrowserslist: ['last 5 version'],
          }),
        ],
      },
    },
    {
      loader: 'less-loader',
      options: {
        javascriptEnabled: true,
      },
    },
  ],
}
复制代码

至此development环境的配置完成。production的配置基本上没太大变化。唯一指的注意的是webpack取消了CommonsChunkPlugin,需要改用optimization配置项下的splitChunks选项使用。

  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          name: 'vendor',
          test: /[\\/]node_modules[\\/]/,
          chunks: 'all',
          priority: 10, // 优先级
        },
      },
    },
  }
复制代码

我们把配置分成三个文件,基础配置 base,开发环境配置 dev,生产环境 prod,通过 webpack-merge 根据不同环境合并配置。

详细配置

base

// 公共
const path = require('path'); //node.js自带的路径参数
const APP_PATH = path.resolve(__dirname, '../src'); //源文件目录
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const devMode = process.env.NODE_ENV !== 'production';

module.exports = {
  entry: {
    index: './src/index.js',
  },
  module: {
    rules: [
      {
        exclude: [
          /\.(html|ejs)$/,
          /\.(js|jsx)$/,
          /\.(css|less|scss|sass)$/,
          /\.json$/,
          /\.svg$/,
          /\.tsx?$/,
        ],
        loader: 'url-loader',
        options: { limit: 10000, name: 'static/[name].[hash:8].[ext]' },
      },
      {
        test: /(\.js|\.jsx)$/,
        use: {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
          },
        },
        include: APP_PATH,
        exclude: /node_modules/,
      },
      {
        test: /\.css$/,
        use: [devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader'],
      },
      {
        test: /\.less$/,
        include: [/antd/],
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader', //自动加前缀
            options: {
              plugins: [
                require('autoprefixer')({
                  overrideBrowserslist: ['last 5 version'],
                }),
              ],
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              modifyVars: {
                'primary-color': '#2171FF',
                'link-color': '#2171FF',
                'border-radius-base': '2px',
                'font-size-base': '14px',
                'animation-duration-slow': '.2s',
                'animation-duration-base': '.2s',
                'input-height-lg': '30px',
                'btn-height-lg': '30px',
                'label-color': '#222',
                'btn-font-size-lg': '14px',
                'menu-dark-bg': '#001529',
                'menu-dark-submenu-bg': '#000',
              },
            },
          },
        ],
      },
      {
        test: /\.less$/,
        exclude: [/antd/],
        use: [
          devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              sourceMap: true,
              modules: true,
              localIdentName: '[local]___[hash:base64:5]',
            },
          },
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                require('autoprefixer')({
                  overrideBrowserslist: ['last 5 version'],
                }),
              ],
            },
          },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
            },
          },
        ],
      },
      {
        test: /\.html$/,
        loader: 'html-loader',
        options: { name: '[name].[ext]' },
      },
      {
        test: /\.svg$/,
        loader: 'file-loader',
        options: { name: 'static/[name].[hash:8].[ext]' },
      },
    ],
  },
  plugins: [
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../public'),
      },
      {
        from: 'src/assets',
        to: 'assets',
      },
    ]),
  ],
  resolve: {
    extensions: [
      '.web.js',
      '.web.jsx',
      '.web.ts',
      '.web.tsx',
      '.js',
      '.json',
      '.jsx',
      '.ts',
      '.tsx',
    ],
  },
  externals: { echarts: 'echarts' },
};

复制代码

dev

// 开发
const path = require('path');
// const webpack = require('webpack');
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.base.conf.js');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const DIST_PATH = path.resolve(__dirname, '../dist');

module.exports = merge(baseWebpackConfig, {
  mode: 'development',
  // 源错误检查
  devtool: 'cheap-module-eval-source-map',
  output: {
    filename: '[name].js',
    path: DIST_PATH,
    publicPath: '/', // 解决多路由错乱
    libraryTarget: 'var',
    chunkFilename: '[name].async.js',
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve('src', 'index.ejs'),
      inject: 'body',
      minify: {
        html5: true,
      },
      hash: false,
      isDev: true,
    }),
  ],
  devServer: {
    port: 9001,
    contentBase: path.resolve('src', 'public'),
    compress: true, // 启用 gzip
    historyApiFallback: true,
    hot: true, //开启
    https: false,
    noInfo: true,
    disableHostCheck: true,
    stats: {
      modules: false,
      assets: false,
      entrypoints: false,
      cachedModules: false,
      cachedAssets: false,
      children: false,
      chunks: false,
      chunkGroups: false,
      chunkModules: false,
      chunkOrigins: false,
      warnings: false,
    }
  },
});

复制代码

prod

// 生产
const path = require('path');
const merge = require('webpack-merge'); //合并配置
const baseWebpackConfig = require('./webpack.base.conf');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin'); // 复制图片,字体等静态资源
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const DIST_PATH = path.resolve(__dirname, '../dist');

module.exports = merge(baseWebpackConfig, {
  mode: 'production', //mode是webpack4新增的模式
  stats: {
    all: false,
    timings: true, // 时间分析
    assets: true, // 输出最后的打包文件
    errors: true, // 遇到错误时,输出内容
    warnings: false, // 静默warning
    moduleTrace: true, // 遇到错误时,定位文件
    errorDetails: true, // 输出具体错误
  },
  output: {
    filename: '[name].[chunkhash:8].js',
    path: DIST_PATH,
    publicPath: '/business/',
    libraryTarget: 'var',
    chunkFilename: '[name].[chunkhash:8].async.js',
  },
  plugins: [
    new CleanWebpackPlugin(['../dist'], { allowExternal: true }), // 删除dist 文件
    new MiniCssExtractPlugin({
      filename: '[name].[chunkhash:8].css',
      chunkFilename: '[name].[chunkhash:8].css',
      ignoreOrder: true,
    }),
    new HtmlWebpackPlugin({
      template: path.resolve('src', 'index.ejs'),
      filename: 'index.html',
      minify: {
        removeComments: true, // 移注释
        collapseWhitespace: true, // 移空格
        removeAttributeQuotes: true, // 移引号
      },
    }),
    new CopyWebpackPlugin([
      {
        from: 'src/assets',
        to: 'assets',
      },
    ]),
  ],
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendor: {
          name: 'vendor',
          test: /[\\/]node_modules[\\/]/,
          chunks: 'all',
          priority: 10, // 优先级
        },
      },
    },
  },
});

复制代码

扩展研究

尝试过happyPack和预编译dllPlugin,速度提升微乎其微,暂时先不用了。

改造结果

编译时间从原先40-50s提速到20-30s,热更新编译时间从原先10-20s提速到2-3s。打包速度从原先300s左右提升到100s以内。打包体积缩小近一半。最最重要的是,页面访问速度感( xin )觉( li )变( zuo )快( yong )了。

后记

不得不说,webpack配置还是很伤脑子的,各个配置项之间可能存在互相影响的情况,有些错误有很玄乎。但是如果对照着官方文档来,明白了webpack各个主要配置项的作用,其实一套配下来还是收获不少的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享