打包多页面
打包多个页面可以有多种配置方式:
1.html-webpack-plugin:
module.exports = {
entry: {
index: "./src/index.js", // 指定打包输出的chunk名为index
foo: "./src/foo.js" // 指定打包输出的chunk名为foo
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html", // 要打包输出哪个文件,可以使用相对路径
filename: "index.html", // 打包输出后该html文件的名称
chunks: ["index"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,index页面只引入index.js
}),
new HtmlWebpackPlugin({
template: "./src/index.html", // 要打包输出哪个文件,可以使用相对路径
filename: "foo.html", // 打包输出后该html文件的名称
chunks: ["foo"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,foo页面只引入foo.js
}),
]
}
打包多页面时,关键在于 chunks 属性的配置,因为在没有配置 chunks 属性的情况下,打包输出的 index.html 和 foo.html 都会同时引入 index.js 和 foo.js。
所以必须配置 chunks 属性,来指定打包输出后的 html 文件中要引入的输出模块,数组的元素为 entry 属性值为对象的时候指定的 chunk 名,如上配置,才能实现,index.html 只引入 index.js,foo.html 只引入 foo.js 文件。
2.AutoWebPlugin
上面的方法没添加一个html页面就要多配置一次,为了解决这个问题,所以可以采用本方法。
首先,项目源码目录结构如下:
├── pages
│ ├── index
│ │ ├── index.css // 该页面单独需要的 CSS 样式
│ │ └── index.js // 该页面的入口文件
│ └── login
│ ├── index.css
│ └── index.js
├── common.css // 所有页面都需要的公共 CSS 样式
├── google_analytics.js
├── template.html
└── webpack.config.js
从目录结构中可以看成出下几点要求:
(1)所有单页应用的代码都需要放到一个目录下,例如都放在 pages 目录下; (2)一个单页应用一个单独的文件夹,例如最后生成的 index.html 相关的代码都在 index 目录下,login.html 同理; (3)每个单页应用的目录下都有一个 index.js 文件作为入口执行文件。
const { AutoWebPlugin } = require('web-webpack-plugin');
// 使用本文的主角 AutoWebPlugin,自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('pages', {
template: './template.html', // HTML 模版文件所在的文件路径
postEntrys: ['./common.css'],// 所有页面都依赖这份通用的 CSS 样式文件
// 提取出所有页面公共的代码
commonsChunk: {
name: 'common',// 提取出公共代码 Chunk 的名称
},
});
module.exports = {
// AutoWebPlugin 会为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到所有由 autoWebPlugin 生成的入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
}),
plugins: [
autoWebPlugin,
],
};
AutoWebPlugin 会找出 pages 目录下的2个文件夹 index 和 login,把这两个文件夹看成两个单页应用。 并且分别为每个单页应用生成一个 Chunk 配置和 WebPlugin 配置。 详细请看:web-webpack-plugin的AutoWebPlugin
配置source-map
source-map 就是源码映射,主要是为了方便代码调试,因为我们打包上线后的代码会被压缩等处理,导致所有代码都被压缩成了一行,如果代码中出现错误,那么浏览器只会提示出错位置在第一行,这样我们无法真正知道出错地方在源码中的具体位置。webpack 提供了一个 devtool 属性来配置源码映射。
devtool 常见的有 6 种配置: 1、source-map: 这种模式会产生一个.map文件,出错了会提示具体的行和列,文件里面保留了打包后的文件与原始文件之间的映射关系,打包输出文件中会指向生成的.map文件,告诉js引擎源码在哪里,由于源码与.map文件分离,所以需要浏览器发送请求去获取.map文件,常用于生产环境.
2.eval: 这种模式打包速度最快,不会生成.map文件,会使用eval将模块包裹,在末尾加入sourceURL,常用于开发环境
3.eval-source-map: 每个 module 会通过 eval() 来执行,并且生成一个 DataUrl 形式的 SourceMap (即 base64 编码形式内嵌到 eval 语句末尾), 但是不会生成 .map 文件,可以减少网络请求**,但是打包文件会非常大**。
4.cheap-source-map: 加上 cheap,就只会提示到第几行报错,少了列信息提示,同时不会对引入的库做映射,可以提高打包性能,但是会产生 .map 文件。
5.cheap-module-source-map: 和 cheap-source-map 相比,加上了 module,就会对引入的库做映射,并且也会产生 .map 文件,用于生产环境。
6.cheap-module-eval-source-map: 常用于开发环境,使用 cheap 模式可以大幅提高 souremap 生成的效率,加上 module 同时会对引入的库做映射,eval 提高打包构建速度,并且不会产生 .map 文件减少网络请求。
凡是带 eval 的模式都不能用于生产环境,因为其不会产生 .map 文件,会导致打包后的文件变得非常大。通常我们并不关心列信息,所以都会使用 cheap 模式,但是我们也还是需要对第三方库做映射,以便精准找到错误的位置。
区分环境问题
可以将webpack配置分为:
- webpack.config.dev.js
- webpack.config.prod.js
- webpack.config.base.js base.js主要是通用的配置,是所有环境都会执行的配置。但是有些配置也要区分环境,需要用cross-env 传入的NODE_ENV来区分配置。
将prod与dev以及其他环境的单独配置与通用配置结合时,可以用到webpack-merge插件,将多个配置合并在一起。如下:
const merge = require('webpack-merge');
const prodWebpackConfig = require('./webpack.config.prod.js');
const {BundleAnalyzerPlugin} = require('webpack-bundle-analyzer');
module.exports = merge(prodWebpackConfig, {
// 增加 webpack-bundle-analyzer 配置
plugins: [new BundleAnalyzerPlugin()]
});
抽离公共代码
在一个项目里,我们会把哥哥页面的公共部分抽离出来作为commonjs,并且将其中所有页面都依赖的公共库文件抽出来作为basejs。
1 CommonsChunkPlugin
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
new CommonsChunkPlugin({
// 从哪些 Chunk 中提取
chunks: ['a', 'b'],
// 提取出的公共部分形成一个新的 Chunk,这个新 Chunk 的名称
name: 'common'
})
以上配置就能从网页 A 和网页 B 中抽离出公共部分,放到 common 中。 下面我们需要配置base.js.
//base.js
// 所有页面都依赖的基础库
import 'react';
import 'react-dom';
// 所有页面都使用的样式
import './base.css';
接着再修改 Webpack 配置,在 entry 中加入 base,相关修改如下:
module.exports = {
entry: {
base: './base.js'
},
};
为了从 common 中提取出 base 也包含的部分,还需要配置一个 CommonsChunkPlugin,相关代码如下:
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
})
由于 common 和 base 公共的部分就是 base 目前已经包含的部分,所以这样配置后 common 将会变小,而 base 将保持不变。 针对 CSS 资源,以上理论和方法同样有效,也就是说你也可以对 CSS 文件做同样的优化。
以上方法可能会出现 common.js 中没有代码的情况,原因是去掉基础运行库外很难再找到所有页面都会用上的模块。 在出现这种情况时,你可以采取以下做法之一:
- (1) CommonsChunkPlugin 提供一个选项 minChunks,表示文件要被提取出来时需要在指定的 Chunks 中最小出现最小次数。 假如 minChunks=2、chunks=['a','b','c','d'],任何一个文件只要在 ['a','b','c','d'] 中任意两个以上的 Chunk 中都出现过,这个文件就会被提取出来。
- (2) 根据各个页面之间的相关性选取其中的部分页面用 CommonsChunkPlugin 去提取这部分被选出的页面的公共部分,而不是提取所有页面的公共部分,而且这样的操作可以叠加多次。 这样做的效果会很好,但缺点是配置复杂,你需要根据页面之间的关系去思考如何配置,该方法不通用。但CommonsChunkPlugin 会去将所有 entry 中的公有模块遍历出来再进行编译压缩混淆,这个过程是非常缓慢的。
详细请看:提取公共代码
2.splitChunks
splitChunks的默认配置:
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: 'async', // 三选一: "initial" | "all" | "async" (默认)
minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了
maxSize: 0, // 文件的最大尺寸,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize
minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小
maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候就不会抽取 initial common 了
automaticNameDelimiter: '~', // 打包文件名分隔符
name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合并成一个
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
priority: -10 // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
},
default: {
minChunks: 2,
priority: -20, // 优先级
reuseExistingChunk: true // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的chunk,不会再重新产生一个
}
}
}
}
};
上述默认配置是利用import动态加载来打包chunk的。 index.js文件里利用import引入react文件:
import(/* webpackChunkName: "react" */ 'react');
再利用webpack-bundle-analyzer来进行打包依赖分析,如下:
由此可以知道:
- 1)index.js打包出来了两个文件react.js和main.js;
- 2)react.js是被拆分出来的,内容实际是 react;
- 3)react.js被拆分出来是因为splitChunks默认配置chunks='async'。
实践:
optimization:{
splitChunks: {
cacheGroups: {
default: false,
commons: {
test: /react|lodash|mobx/,
name: 'split-vendor',
chunks: 'all'
}
}
},
},
runtimeChunk
优化持久化缓存的, runtime 指的是 webpack 的运行环境(具体作用就是模块解析, 加载) 和 模块信息清单, 模块信息清单在每次有模块变更(hash 变更)时都会变更, 所以我们想把这部分代码单独打包出来, 配合后端缓存策略, 这样就不会因为某个模块的变更导致包含模块信息的模块(通常会被包含在最后一个 bundle 中)缓存失效. optimization.runtimeChunk 就是告诉 webpack 是否要把这部分单独打包出来.
按需加载
即在需要使用的时候才打包输出,webpack 提供了 import() 方法,传入要动态加载的模块,来动态加载指定的模块,当 webpack 遇到 import()语句的时候,不会立即去加载该模块,而是在用到该模块的时候,再去加载,也就是说打包的时候会一起打包出来,但是在浏览器中加载的时候并不会立即加载,而是等到用到的时候再去加载,比如,点击按钮后才会加载某个模块,如:
const button = document.createElement("button");
button.innerText = "点我"
button.addEventListener("click", () => { // 点击按钮后加载foo.js
import("./foo").then((res) => { // import()返回的是一个Promise对象
console.log(res);
});
});
document.body.appendChild(button);
从中可以看到,import() 返回的是一个 Promise 对象,其主要就是利用 JSONP 实现动态加载,返回的 res 结果不同的 export 方式会有不同,如果使用的 module.exports 输出,那么返回的 res 就是 module.exports 输出的结果;如果使用的是 ES6 模块输出,即 export default 输出,那么返回的 res 结果就是 res.default,如:
{default: "foo", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
实践
方法一:Route+import()异步加载
/**
* 异步加载组件
* @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
* @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
*/
function getAsyncComponent(load) {
return class AsyncComponent extends PureComponent {
componentDidMount() {
// 在高阶组件 DidMount 时才去执行网络加载步骤
load().then(({default: component}) => {
// 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
this.setState({
component,
})
});
}
render() {
const {component} = this.state || {};
// component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
return component ? createElement(component) : null;
}
}
}
结果:
方法二:React.lazy()
const HotList = React.lazy(() => import(/* webpackChunkName: "index-page" */ '../../components/hotList'));
方法三:异步回调+import()
结果图:
并且在onclick事件触发后,才会异步请求执行foo.js。
thread-loader
把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行
在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:
- 这些 loader 不能产生新的文件。
- 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
- 这些 loader 无法获取 webpack 的选项设置。
- 每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。
请仅在耗时的 loader 上使用
// node-sass 中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用 thread-loader 时,需要设置 workerParallelJobs: 2
// https://webpack.docschina.org/guides/build-performance/#sass
const threadLoader = workerParallelJobs => {
const options = { workerParallelJobs }
if (constants.APP_ENV === 'dev') {
Object.assign(options, { poolTimeout: Infinity })
}
return { loader: 'thread-loader', options }
}
module.exports = [
{
test: /\.svg$/,
loader: [cacheLoader, threadLoader(), '@svgr/webpack'],
include: [resolve('src')]
}
]
详细配置描述
//配置项
use: [
{
loader: "thread-loader",
// 有同样配置的 loader 会共享一个 worker 池(worker pool)
options: {
// 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
// 或者,在 require('os').cpus() 是 undefined 时回退至 1
workers: 2,
// 一个 worker 进程中并行执行工作的数量
// 默认为 20
workerParallelJobs: 50,
// 额外的 Node.js 参数
workerNodeArgs: ['--max-old-space-size=1024'],
// Allow to respawn a dead worker pool
// respawning slows down the entire compilation
// and should be set to false for development
poolRespawn: false,
// 闲置时定时删除 worker 进程
// 默认为 500ms
// 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
poolTimeout: 2000,
// 池(pool)分配给 worker 的工作数量
// 默认为 200
// 降低这个数值会降低总体的效率,但是会提升工作分布更均一
poolParallelJobs: 50,
// 池(pool)的名称
// 可以修改名称来创建其余选项都一样的池(pool)
name: "my-pool"
}
},
// your expensive loader (e.g babel-loader)
]
预热
可以通过预热 worker 池(worker pool)来防止启动 worker 时的高延时。
这会启动池(pool)内最大数量的 worker 并把指定的模块载入 node.js 的模块缓存中。
const threadLoader = require('thread-loader');
threadLoader.warmup({
// pool options, like passed to loader options
// must match loader options to boot the correct pool
}, [
// modules to load
// can be any module, i. e.
'babel-loader',
'babel-preset-es2015',
'sass-loader',
]);
cache-loader
npm install --save-dev cache-loader
在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里。
//文件webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};
⚠️ 请注意,保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader
具体实践:
const cacheLoader = {
loader: 'cache-loader',
options: {
// provide a cache directory where cache items should be stored
cacheDirectory: resolve('.cache-loader')
}
}
thread-loader+cache-loader实践结果
HappyPack
HappyPack 允许 Webpack 使用 Node 多线程进行构建来提升构建的速度。 编译jsx文件配置:
new HappyPack({
id: 'jsx',
threads: 4,
loaders: ['babel-loader?presets[]=react,presets[]=latest&compact=false'],
})
其中,threads 指明 HappyPack 使用多少子进程来进行编译,一般设置为 4 为最佳。
编译 .scss 文件的 loader 这样写:
new HappyPack({
id: 'scss',
threads: 4,
loaders: [
'style-loader',
'css-loader',
'postcss-loader',
'sass-loader',
],
})//其中,需要注意的一点就是,在使用 HappyPack 的情况下,我们需要单独创建一个 postcss.config.js 文件,不然,在编译的时候,就会报错。
//postcss.config.js
module.exports = {
autoprefixer: {
browsers: ['last 3 versions'],
}
};
但是单独使用效果不佳,搭配dll plugin,如下小节。
实践:项目newcanary
DllPlugin
首先.dll在window系统中称为动态链接库,动态链接库包含的是,可以在其他模块中进行调用的函数和数据。 动态链接库提供了将应用模块化的方式,应用的功能可以在此基础上更容易被复用。将各个模块中公用的部分给打包成为一个公用的模块。这个模块就包含了我们的其他模块中需要的函数和数据。
使用 DllPlugin 的时候,会生成一个 manifest.json 这个文件,所存储的就是各个模块和所需公用模块的对应关系。
我们需要一个文件,这个文件包含所有的第三方或者公用的模块和库,我们在此将其命名为 vendor.js,文件的内容如下:
import 'react';
import 'react-dom';
配置webpack.config.vendor.js文件:
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
vendor: [path.join(__dirname, 'src', 'vendor.js')],
},
output: {
path: path.join(__dirname, 'dist-[hash]'),
filename: '[name].js',
library: '[name]',
},
plugins: [
new webpack.DllPlugin({
path: path.join(__dirname, 'dll', '[name]-manifest.json'),
filename: '[name].js',
name: '[name]',
}),
]
};
//package.json
"build:dll": "cross-env NODE_ENV=production webpack --config webpack.dll.config.js"
这时候,我们就需要用到 DllReferencePlugin 了。
在我们的主要配置文件中,加入以下的配置:
const DLL_PATH = './../dll'
// ... 其他完美的配置
plugins: [
new webpack.DllReferencePlugin({
manifest: require(`${DLL_PATH}/vendor.manifest.json`)
}),
new AddAssetHtmlPlugin({//Add a JavaScript or CSS asset to the HTML generated by html-webpack-plugin html文件里就会引入dll文件
filepath: path.resolve(__dirname, `${DLL_PATH}/**/*.js`),
includeSourcemap: false
}),
],
这样就完成了DllPlugin配置
效果:
项目:ts-react-webpack(newcanary项目效果忒不明显)
Tree Shaking
tree-shaking就是摇晃树木将无用部分摇掉。也就是代码中引入却未用的部分打包时将被去掉。如上图所示。
详细可参考文章:cloud.tencent.com/developer/a…
命中缓存问题
serviceWorker webpack是使用:serviceworker-webpack-plugin插件
scope hosting
scope hosting(作用域提升)
webpack工具
1 打包依赖plugin:webpack-bundle-analyzer
效果图
2.时间分析plugin:
//webpack.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({config})
结果
参考文献:
深入浅出webpack4:webpack.wuhaolin.cn 使用 HappyPack 和 DllPlugin 来提升你的 Webpack 构建速度:segmentfault.com/a/119000001…