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。
以下是特性:
- 开箱即用的 react 应用开发工具,内置 css-modules、babel、postcss、HMR 等
- create-react-app 的体验
- JSON 格式的 webpack 配置
- mock
- 基于 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配置输出先保存下来作为后续修改的参考。
原先配置主要以下几部分:
-
entry
入口文件。 webpack4默认’./src/index.js’,现在可以不用配置。
-
output
输出文件的目录,这块也不用动,照搬原先配置。
-
module
文件解析,针对不同类型的文件配置不同的解析规则(rules),并且指定不同的解析器(loader)。类型文件可以有多种loader可供选择解析,可以按顺序组合他们。具体看官方文档。
这块我仍然照搬原先配置,不同的是把js解析的babel-loader配置放到了bablerc里面,之前这部分配置也是roadhog内置的。
-
plugins
webpack插件,主要用来在webpack处理生命周期中的不同阶段提供处理的钩子。主要有两部分,一部分是webpack内置的插件,一部分是npm安装的插件。老配置里主要用到以下几个插件:
- HotModuleReplacementPlugin:热更新插件
- DllReferencePlugin: 预编译插件
- DefinePlugin: 变量定义插件
- HtmlWebpackPlugin:html文件处理插件
- LoaderOptionsPlugin:loader配置插件
这一块根据实际情况作了筛选和添加,具体见后面详细配置。因为webpack4废弃了一些插件,也内置了一些插件。具体需要查阅官方文档。
-
devtool
-
externals
不需要参与编辑的库文件
-
devServer
webpack启动服务的配置项,这块基本上也没怎么动。
第一次尝试
总的来说就是在原先项目配置的基础上,按照 webpack实战之从roadhog2.x到webpack4.x 做了调整,主要是babel-loader插件、css抽取使用MiniCssExtractPlugin,并根据需要把babel相关的都升级到了7x以上。
结果当然没有那么顺利,跟这篇文章里一样遇到了这几个同样的问题:
-
style-loader与mini-css-extract-plugin存在冲突
这个官方文档也说了,使用后者即可。
-
antd的样式未加载
原先配置:
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": true }] 复制代码
style从true改成css就好了。
-
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各个主要配置项的作用,其实一套配下来还是收获不少的。