在 webpack 性能优化 (上) 中, 我们从 代码分离, Loader, webpack 解析(resolve), webpack 外部扩展(Externals) ,Dlls 优化构建速度, 等方面分析了优化手段, 这篇文章让我们接着来撸.
图片等静态文件 dev prod
通常来说, 我们会通过使用 file-loader,url-loader 等 loader 来处理项目中的静态文件, 如图片字体等文件
- // 这样最终 dist 文件中就会生成 font 文件夹存放字体文件
- {
- test: /\.(woff|svg|eot|ttf)\??.*$/,
- loader: "url-loader",
- options: {
- limit: 8192,
- name: "font/[name].[hash:6].[ext]"
- }
- }
limit 属性是在文件大小超出 limit 的值才会单独打包, 否则使用 base64 的方式引用通常适用于小图片, 这就是我们通常的文件处理方式.
使用 base64 引入图片的好处是减少 http 请求数, 但相应的问题是 base64 占用的空间比普通的图片文件大一点.
当然我们还有另外一种方案, 具体做法是将项目的中静态文件统一存放在 static 文件夹下, 最后使用 CopyWebpackPlugin 将 static 文件夹拷贝到 dist 目录下
- new CopyWebpackPlugin([
- {
- from: path.resolve(SRC_PATH, 'img'),
- to: 'img'
- }
- ]),
这样做的好处是我们的静态资源不经过 webpack 的处理, 可以提升构建速度, 但问题也是很明显的, 那就是维护的成本增大并可能出现一些意外的情况, 比如:
这样处理的问题是可能开发环境引用路径和打包文件访问图片路径不一致问题, 这里可以通过 output.publicPath 属性来配置解决
- output: {
- // 打包文件中通过相对路径引用的资源都会被配置的路径所替换
- publicPath: '/assets/'
- }
- // 对于这种结构的项目当然不合适使用这种方法
- |- /static
- |- /components
- | |- /my-component
- | | |- index.jsx
- | | |- index.CSS
- | | |- icon.svg
- | | |- img.PNG
当然从我们实际项目的测试效果来看, 我只能说这种处理方式并不算是很优秀, 仅供参考.
source map dev
在开发环境中, 我们比较关注调试的方便程度, 而原始 webpack 打包后的 bundle 文件中可能包含来自多个文件的内容, 对于程序的报错信息往往简单的指向这个 bundle 文件:
而 source map 是为了帮助我们定位程序出现的错误对应的源代码的位置. 使用 sourceMap 报错信息正确的指向了源码的错误位置.
- //1 使用 devtool 选项配置, 有多个选项可选
- module.exports = {
- devtool: 'inline-source-map',
- };
- //2 使用 plugins 方式进行更细粒度的配置
- module.exports = {
- plugins: [
- new webpack.SourceMapDevToolPlugin({
- filename: '[name].js.map',
- exclude: ['vendor.js']
- })
- ]
- };
- // 在使用 uglifyjs-webpack-plugin 时 需要开启 sourceMap 选项
devtool 文档
UglifyJsPlugin 配合 tree shaking prod
对于 JS 压缩 在 webpack <= 3.x 的版本中:
- //1 使用 -p(production)标记来压缩 JS
- //2 使用内置 plugin(webpack.optimize.UglifyJsPlugin)
- //3 使用外部引入 plugin(uglifyjs-webpack-plugin)
- module.exports = {
- plugins: [
- new webpack.optimize.UglifyJsPlugin({
- compress: {
- warnings: false
- },
- output: {
- //remove all comments
- comments: false
- }
- }),
- ]
- };
在 webpack4 中
- const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
- module.exports = {
- //1 设置 mode
- mode: ""production
- //2 minimize
- optimization:{
- minimize: true,
- //3 或者指定其他插件
- minimizer: [
- new UglifyJsPlugin({
- sourceMap: true
- })
- ]
- },
- //3 若需要 sourceMap 需要设置 devtool 的值
- devtool: 'source-map',
- };
- // 可选的压缩插件
- UglifyJSPlugin, ClosureCompilerPlugin
- BabelMinifyWebpackPlugin,
此处需要注意. 若是在使用了 UglifyJSPlugin 且开启 sourceMap 后, 需要同时给 devtool 设置值. 同样的若是设置了 devtool 的值, 则 UglifyJSPlugin 也需要开启 sourceMap. 否则不会生成. map 的源代码对应文件.
在开启 JS 的压缩后 我们的 tree shaking 登场了, tree shaking 是什么? 为什么需要使用?
tree shaking 是一个术语, 用于描述移除 JS 中未引用的代码. 使用它能优化输出. 未开启 tree shaking 的实例:
- //tool.JS
- export function square(x) {
- return x * x;
- }
- export function cube(x) {
- return x * x * x;
- }
- //index.JS
- import { square } from "./tool.js"
- // 最终输出 在关闭 UglifyJSPlugin 插件后测试结果
我们可以看到 cube 这个我们并没有引用的模块也被打包进源码了.
使用 tree shaking 来优化输出, 在 package.JSON 中:
- //webpack4
- //1 将文件标记为无副作用
- {
- "name": "web",
- "version": "1.0.0",
- // 若是整个项目都无副作用 直接设置为 false
- "sideEffects": false
- // 若是部分文件确实有副作用
- "sideEffects": [file_path1, file_path2]
- }
- //2 开启 JS 压缩 使用上述方法开启即可
- //webpack2/3
- //1 需要配置 .babelrc modules false
- {
- "presets": [
- [
- "env",
- {
- "modules": false
- }
- ]
- ]
- }
- //2 开启 JS 压缩
「副作用」的定义是, 在导入时会执行特殊行为的代码, 而不是仅仅暴露一个 export 或多个 export. 举例说明, 例如 polyfill, 它影响全局作用域, 并且通常不提供 export.
对于开启后的压缩代码中, 我们搜索 "*" 号 只得到一个结果, 测试成功.
最后我们简单解释下设置 modules false 的作用. tree shaking 本身是依赖于 ES6 的静态导入, 也就是我们常用的 import export.ES6 模块化中一个文件能够输出多个模块, 而我们可以只导入需要的模块. 对比 commonjs 的动态导入模块化标准, 一个文件只有一个输出, 因此不难发现, tree shaking 在 commonjs 模块化的系统中是发挥不了作用的.
而 modules 的意义是启用将 ES6 模块语法转换为另一种模块类型, 默认值'commonjs', 将该设置为 false 即不转化, 也就是 ES6 模块语法, 所以在此我们需要将 modules 设置为 false.
modules 的取值有'amd' | 'umd' | 'systemjs' | 'commonjs' | false. 在 webpack4 中已经可以不用此方法来检测重复模块了
Split CSS prod
一个 Web 项目中 CSS 是关键的一环, 若没有额外配置 CSS 最终会被打包进入 JS 文件中, 但熟悉浏览器渲染的开发者应该会清楚, 浏览器在渲染页面时会解析 DOM 树和 CSS 树, 最后将之对应合并呈现渲染好的页面. 将 CSS 放在 JS 中引入势必会延缓 CSS 树的计算. 所以将 CSS 从 JS 中分离, 打包成单独的 CSS 文件, 然后和 JS 并行加载是我们项目的一个提升点, 这样可以加快界面渲染速度, 也可以单独做缓存.
- // 使用插件 extract-text-webpack-plugin
- {
- test: /\.CSS$/,
- use: ExtractTextPlugin.extract({
- // 用于 CSS 未被提取(allChunks: false)
- fallback: "style-loader",
- use: 'css-loader'
- })
- }
- new ExtractTextPlugin({
- filename: 'common.[chunkhash].css',
- allchunk: true
- })
当然 webpack-dev-server 是不支持 extract-text-webpack-plugin 抽离的 CSS 热替换的, 所以此插件不建议再 dev 环境中使用, 如果非要使用可以考虑 CSS-hot-loader.
清理 /dist 文件夹 prod
webpack 将打包的文件放在 dist 文件夹中, 若是使用了 hash 文件名, 则每次文件变动后重新打包生成的文件名都会不同, 这会造成 dist 目录越来越混乱, 好的做法是每次打包前先清理 dist 文件夹:
new CleanWebpackPlugin(pathsToClean, cleanOptions)
在内存中编译 dev
webpack-dev-server 大家都不陌生, 开发环境必备, webpack 内部依赖了 webpack-hot-middleware,webpack-dev-middleware 两个插件.
webpack-dev-middleware 提供了在内存中编译功能, 它在文件更改后自动编译文件并保存在内存中, 具体表现为, 刷新浏览器即可看到我们的更改.
webpack-hot-middleware 提供了服务端推送功能, 通常和 webpack-dev-middleware 配合使用, 当文件更改并自动编译完成后, 服务端通过 SSE(服务端发送事件)将更改信息推送到客户端, 客户端会接收到一个 JSON 文件, 其中包含了更改了的文件的一些信息, 客户端会根据这些信息主动向服务端获取最新的文件.
若无文件更改 webpack-hot-middleware 也会在一定时间间隔后遍历内存文件检测是否更改, 然后通过事件流的方式向客户端发送消息.
webpack-dev-middleware 和 webpack-hot-middleware 都是 express 的标准插件
我相信各位项目中这两个功能都是已经开启的我就不再具体说他们的配置了, 这里我们主要说下在 node 服务端怎么使用这两个插件达到热更新的目的.
我们以 koa 为例, 如何在 koa 中开启热更新调试我们的项目呢?
- // 新建 App.JS 作为 koa 服务端入口 App.JS
- import Koa from "koa";
- import views from "koa-views";
- import webpack from "webpack";
- import webpack_config from "../webpack/webpack.config.js";
- import { devMiddleware, hotMiddleware } from 'koa-webpack-middleware'
- var App = new Koa()
- var compiler = webpack(webpack_config)
- App.use(views("./template", {map: {html: "ejs"}}));
- App.use(devMiddleware(compiler,{
- publicPath:"/"
- }));
- App.use(hotMiddleware(compiler))
koa-webpack-middleware 将 express 的中间件 (webpack-dev-middleware 和 webpack-hot-middleware) 进行封装, 将我们 koa 中间件的 next 方法传递到 express 的第三个参数中进行封装.
最简单的配置如上. 但这种配置会有一个问题就是刷新 404 的问题.
hotMiddleware 会在匹配到项目跟路由时直接返回内存中的 HTML 文件给客户端. 但是其他的路由如 react 的路由时, 它不会去匹配, 最终会返回一个 404
- // 会返回 template/index.HTML 但这时是空的
- // 也就是没有导入 JS 的 HTML
- await ctx.render("index");
解决, 当用户访问时在 webpack 编译输出的最后阶段获取到文件信息, 取出获取到的 HTML 文件写入 template 下的 index.HTML 文件, 最后返回它, 具体操作如下:
- compiler.plugin("emit",(comilation,callback) => {
- const assets = comilation.assets;
- let file, data;
- Object.keys(assets).forEach(key => {
- if(key.match(/\.HTML$/)){
- file = path.resolve(__dirname,"./template/index.html");
- data = assets[key].source();
- fs.writeFileSync(file,data);
- }
- });
- callback();
- })
当然上述方法略显笨重, 且需要理解的东西较多, 不太推荐, 这里有一个插件能解决上述问题 connect-history-API-fallback, 大家自己学习下即可.
我的博客地址
来源: https://juejin.im/post/5bfb5c9c5188250c1021337c