工具终将被更好的工具替代,但是解决问题的思路会永远传承下去。随着前端进入深水区,更加内卷,出现了许多webpack的替代品。在学习和了解这些替代品之前,让我们重温下webpack的一些核心概念。
webpack核心概念是什么?
webpack会将所有的文件理解成「模块module」。这些模块有不同的文件格式,比如js、css、text。
webpack会从entry定义的文件出发,根据「模块引用」依次找到它依赖的各个子模块,然后将它们都打包输出到output。
webpack原生支持js,如果是ts、css、text等其他格式的「模块」,就需要第三方loader来解析了。
如果想在构建的特定时刻,进行进一步控制,就需要plugin来挂入“钩子”,帮忙实现了。
loader和plugin的区别?
loader是用来解析不同的模块。比如css-loader,可以支持 require('./main.css')
读取css文件;比如style-loader可以将css文件合并到javascript中。
plugin是用来扩展webpack功能,实现特定时机的钩子。比如 mini-css-extract-plugin
可以放在css-loader后,在解析css之后,将其输出为单独的文件。
代码案例:
1 | const path = require('path'); |
编译原理层面优化
TreeShaking
主要从编译原理角度解决问题。会把引入的,但是没用过的var、func、obj等shake(抖)掉,从而减少产物体积。
坑点:
- 由于它是基于静态导入分析代码依赖关系,也就是import和export可以,但是require以及import()不可以。
- 对于class,它可以shake整个class,但是没法shake其上具体的函数。webpack的shake不支持检查class的fucntion是否被调用。
根据第一点,在配合babel使用的时候,babel 的 env插件的"modules": false
这样设置。防止babel转换引用模块的代码,出现webpack无法理解的问题。
Prepack
Prepack 由 Facebook 开源,它采用较为激进的方法:在保持运行结果一致的情况下,改变源代码的运行逻辑,输出性能更高的 JavaScript 代码。
原理:实际上 Prepack 就是一个部分求值器,编译代码时提前将计算结果放到编译后的代码中,而不是在代码运行时才去求值。
流程:
- 借助AST解析源码关系
- 实现了一个 JavaScript 解释器,用于执行源码。借助这个解释器 Prepack 才能掌握源码具体是如何执行的,并把执行过程中的结果返回到输出中。
比如源码中是:
1 | import React, {Component} from 'react'; |
转化后u就是:
1 | console.log("hello webpack"); |
接入方式:
1 | const PrepackWebpackPlugin = require('prepack-webpack-plugin').default; |
Scope Hoisting
作用域提升。将打包结果,都提升到全局下的一个闭包作用域。
优势:
- 闭包少,性能更高
- 产物体积更小
使用:
1 | const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); |
webpack如何缩小文件搜索范围?
webpack文件搜索原理
Webpack 启动后会从配置的 Entry 出发,解析出文件中的导入语句,再递归的解析。
在遇到导入语句时 Webpack 会做两件事情:
- 根据导入语句去寻找对应的要导入的文件。例如
require('react')
导入语句对应的文件是./node_modules/react/react.js
,require('./util')
对应的文件是./util.js
。 - 根据找到的要导入文件的后缀,使用配置中的 Loader 去处理文件。例如使用 ES6 开发的 JavaScript 文件需要使用 babel-loader 去处理。
优化手段
- 针对loader:可以使用include和exclude,来缩小和排除范围
- 针对require引入的依赖:默认是像nodejs那样,一直向上寻找,直到
/node_modules
。可以修改resolve.modules
属性,将其只在工作目录 - 减少对require依赖的不同的endpoint的依赖:通过修改
resolve.mainFields
指定只搜索依赖库的pakcage.json中指定的入口。
比如package.json中存在 main、browser、jsnext:main 3种入口,默认情况下,webpack只会按照顺序找到main的入口,但是可以通过制定 resolve.mainFields 配置,来修改webpack查找的顺序。
- 可以声明只解析特定后缀的依赖
- 针对递归解析:可以通过
module.noParse
跳过指定模块的递归解析。比如对于 react.min.js 没必要解析。
如何利用多核CPU(并行)?
HappyPack
原理:在递归解析时,webpack的loader做文件解析和转换,这个非常耗时。HappyPack其实就是把这些loader进行调度,分配给多个进程。
代码实现:它还支持提前创建pool,实现复用和限制。
1 | const HappyPack = require('happypack'); |
ParallelUglifyPlugin
原理:webpack中要实现压缩代码的逻辑。这个就是uglifyjs实现的。社区里为了利用多核cpu,也出了一个plugin插件,支持并行调用压缩代码。
如何代码复用?
使用 CommonsChunkPlugin
提取公共代码。pass,太常用。
DLL Plugin
原理:类似windows的 .dll
文件。是一种动态链接库。在一个动态链接库中可以包含给其他模块调用的函数和数据。包含大量复用模块的动态链接库只需要编译一次,在之后的构建过程中被动态链接库包含的模块将不会再重新编译,而是直接使用动态链接库中的代码。
要给 Web 项目构建接入动态链接库的思想,需要完成以下事情:
- 把网页依赖的基础模块抽离出来,打包到一个个单独的动态链接库中去。一个动态链接库中可以包含多个模块。
- 当需要导入的模块存在于某个动态链接库中时,这个模块不能被再次被打包,而是去动态链接库中获取。
- 页面依赖的所有动态链接库需要被加载。
编译出的效果其实还是JS文件。只是使用了DLL的思想,以react.dll.js
为例:
1 | var _dll_react = (function(modules) { |
react.manifest.json文件也是由 DllPlugin 生成出,用于描述动态链接库文件中包含哪些模块, 以 react.manifest.json文件为例,其文件内容大致如下:
1 | { |
编译动态链接库:
1 | const path = require('path'); |
使用动态链接库:
1 | const path = require('path'); |
如何写loader?
作用:用于处理不同后缀的模块。
实现:一个xml-loader。这样可以在代码中,直接 import xxx from ‘xxxx.xml’
1 | const xml2js = require('xml2js'); |
可以参考:
https://segmentfault.com/a/1190000018980814
如何写plugin?
核心要素:
- Plugin本身是个类。上面提供apply方法,供webpack核心引擎调用
- apply参数就会被注入 compiler (Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,可以理解成webpack实例)。
- 可以给 compiler绑定事件回调,也可以像全局广播。
参考:https://www.webpackjs.com/api/compiler-hooks/#afterenvironment
1 | /** |
- 回调函数中的compilation,包含了当前的模块资源、编译生成资源、变化的文件等。重新触发构建时,里面会有新的内容。
- 也可以给它绑定各种钩子。参考:https://www.webpackjs.com/api/compilation-hooks/
webpack内部流程:(https://www.zoo.team/article/webpack-plugin)
webpack内部把这些回调理解成钩子(hook),并且基于 tapable 组织复杂的事件流,在特定时机,执行plugin传入的钩子。
例子:根据react文件信息,自动生成router文件:
1 | const fs = require('fs'); |
如何分析编译构建过程?
plugin/loader耗时
在支持抖音电商前端时,经常遇到那种古早项目,webpack打包和流水线编译动辄十几分钟。
为了分析webpack的插件/loader的耗时,引入 speed-measure-webpack-plugin 库。
写法:
1 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); |
耗时结果分析出来后,基本都是图片处理的插件占了大头(9分钟):
解决方案:关闭这个插件的压缩功能即可。
bundle依赖和映射关系
在响应内部的质量建设时,需要把代码中的es6代码都降低到es5。在配置babel后,编译依然存在es6语法。由于代码已经被转译,并且文件名均为hash后的名字,所以对应不上到底是哪里出的问题。
为了分析bundle的大小和依赖关系,以及源码和产物之间的映射关系,引入 webpack-bundle-analyzer 库。
最终跑出了左侧的图。找到 31.xxx.js 文件,点进去,能看到是 tinycolor.js
的问题。
提了个 https://github.com/bgrins/TinyColor/pull/263,虽然没有合并,但是作者也在最新版本中解决了产物出现es6语法的问题。