补充2021-2-22:
随着webpack5的稳定,很多loader成为时代的眼泪了,需要补充一番。
整理本文的目的是总结webpack的特性,书写的重点还是原理、实现。为此我创建了一个仓库。https://github.com/Otto-J/learn-webpack
基础知识
这里快速过一遍。
配置使用
安装依赖
npm install webpack webpack-cli -D
touch webpack.config.js
配置文件:
/**
* webpack Config
* @type {import('webpack').Configuration}
*/
const config = {
entry: "/src/index.js",
output: "dist/bundle.js",
mode: "none",
};
module.exports = config;
配置比较简单,看line4,在js中使用 type来辅助书写cofnig,体验代码补齐的舒爽。同理js都可以这样来操作。
后续通过 npx webpack
来启动即可。
那么就正式开始吧。
webpack是如何打包的
要按照 CommonJS —> ESModules 的方式渐进对比。
CommonJS 打包
我们先写一段 CommonJs,对应 /step1-commonjs
文件夹。
打包,运行页面,观察打包之后的代码。对应 step1-commonjs/dist/mian.js
里的代码。
(()=>{
var __webpack_modules__ = [] // 模块
var __webpack_module_cache__= {} // 模块缓存
function __webpack_require__(moduleId){} // 也就是 rquire 方法
;(()=>{})();
})()
整体结构大致如上。相关注释也进行了添加。
总的来说:
- 被引用的模块会封装为函数,形参为 module,module.export 为模块内容
- 实现一个
require
方法,用来兼容 CommonJS 里的 require,返回的是 exports里的内容 - 主动调用模块的文件,调用
require
来实现调用
ES打包
操作和上面一致。对应 /step2-es
文件夹。
不同的是 多了一个 __webpack_require__.r
,主要是用来标记 esModule
总体上还是定义了 require 方法。
按需加载的打包
也就是异步、动态import,动态导入,按需加载。而 require 只要声明就会加载。
引入插件 babel-plugin-dynamic-import-webpack
实现
对应 /step3-dynamic
文件夹。
观察 /dist/main.js
:
- 观察
__webpack_require__.e
,也就是requireEnsure
- window.webpackJsonp =[] 实现异步插入script标签,把模块注入数组
载入样式
我们引入 css-loader
,引入一个css文件,可以看到,打包之后的结果中,把css字符串放入 css-loader-export
中。
但后续没有其他操作了。
接下来引入 style-loader
看是怎么注入到head里的。
var update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_b_css__WEBPACK_IMPORTED_MODULE_1__.default, options);
var update = injectTag()(cssText)
这里有个 inject-style-into-styletag-js
,这是一个
追踪观察 addStyle
,继续定位到 style.styleSheet.cssText = replaceText(index, css);
将 css-loader 中所加载到的所有样式模块,通过创建 style 标签的方式添加到页面上。
实际上,webpackv4抽离css用的是一个插件。
总结
简单过了打包,知道了产物,就能明白webpack做了什么。
webpack工作流程
- webpack启动,读取配置
- 获取入口,开始遍历执行
- 遍历过程中的文件使用不同的loader来处理,最终转为js模块
- 产出打包结果
其中,可以引入插件,插件可以添加钩子,对打包过程和结果进行干预
- compiler 包含了完整配置,可以访问到webpack的内部环境。
- compilation 构建过程产生的数据,掌控每个环节。
loader
loader可以串联,执行顺序和配置顺序是相反的。后者接收前一个返回值作为参数。
本质是一个模块用于处理数据:
module.exports = function(source){
return source
}
还需要添加 option配置。可以利用 loader-utils
获取配置项。
loader如果不返回参数,可以使用 callback回调方法,拥有: error
content
sourceMap
ast
参数。
const loaderUtils = require('loader-utils')
module.exports = function(source){
const opt = loaderUtils.getOptions(this)
this.callback(null,source)
}
这里可以参照 less-loader
,里面的处理异步就有 async/await 和 this.async()
学以致用,实现一个路径替换的loader,把资源路径进行替换。
可以实现一个 markdown-loader ,看 step5
文件夹
https://webpack.docschina.org/contribute/writing-a-loader/
plugin
钩子函数。
plugin里有哪些生命周期钩子啊?哎
基本用法
compiler.hooks.xxx.tap()
基本结构
module.exports = Class XxxPlugin{
constructor(options) this.options = options
apply(compiler){
compiler.hooks.xx.tap('XxxPlugin',()=>{})
}
}
开发重点是理解compilation 和 compiler ,两种对应的钩子事件。
tapable库。
使用
常见的loader,这里做个列举吧备忘
- file-loader url-loader
- babel-loader
- style-loader css-loader
- postcss-loader
- eslint-loader
- vue-loader
缓存
大名鼎鼎的构建优化神器cache-loader
已经被弃用了,直接使用内置的缓存配置即可。
Consider upgrading webpack to version 5 and setup cache https://webpack.js.org/configuration/other-options/#cache
config={
cache: {
type: "filesystem",
}
}
打包优化
- 定位体积大的模块
- 删除无关依赖
- 抽离CDN
- dll&dllReference 方式优化
definePlugin 内置插件
优化
减少转译
就是优化 babel了
对于js,要使用babel,那得指定 include exclude 了
loader:'babel-loader?cacheDirectory=true'
并行转换
自然就是别人已经做好的 happyPack 了
通过这个插件可以多线程打包,压榨你的cpu
类库提前打包引入
别人写vue我们就不要打包了,一般也不变,把公共代码抽离单独文件。
entry.vendor
内置插件 webpack.DllPlugin
减少打包的体积
你打包的文件太大了怎么办,记得可视化webpack分析。
思路,不打包,少打包
不打包
分离cdn
少打包
按需加载,页面按需加载。
大型组件,用到啥打包啥
代码用不到的 tree-shaking 掉。
主要是吧,webpack5就要出来了。