补充2021-2-22:
随着webpack5的稳定,很多loader成为时代的眼泪了,需要补充一番。

整理本文的目的是总结webpack的特性,书写的重点还是原理、实现。为此我创建了一个仓库。https://github.com/Otto-J/learn-webpack

没题图没更新完

基础知识

这里快速过一遍。

配置使用

安装依赖

  1. npm install webpack webpack-cli -D
  2. touch webpack.config.js

配置文件:

  1. /**
  2. * webpack Config
  3. * @type {import('webpack').Configuration}
  4. */
  5. const config = {
  6. entry: "/src/index.js",
  7. output: "dist/bundle.js",
  8. mode: "none",
  9. };
  10. module.exports = config;

配置比较简单,看line4,在js中使用 type来辅助书写cofnig,体验代码补齐的舒爽。同理js都可以这样来操作。

后续通过 npx webpack 来启动即可。

那么就正式开始吧。

webpack是如何打包的

要按照 CommonJS —> ESModules 的方式渐进对比。

CommonJS 打包

我们先写一段 CommonJs,对应 /step1-commonjs 文件夹。

打包,运行页面,观察打包之后的代码。对应 step1-commonjs/dist/mian.js 里的代码。

  1. (()=>{
  2. var __webpack_modules__ = [] // 模块
  3. var __webpack_module_cache__= {} // 模块缓存
  4. function __webpack_require__(moduleId){} // 也就是 rquire 方法
  5. ;(()=>{})();
  6. })()

整体结构大致如上。相关注释也进行了添加。

总的来说:

  • 被引用的模块会封装为函数,形参为 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 中。
image.png
但后续没有其他操作了。

接下来引入 style-loader 看是怎么注入到head里的。

  1. 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);
  2. 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可以串联,执行顺序和配置顺序是相反的。后者接收前一个返回值作为参数。

本质是一个模块用于处理数据:

  1. module.exports = function(source){
  2. return source
  3. }

还需要添加 option配置。可以利用 loader-utils 获取配置项。
loader如果不返回参数,可以使用 callback回调方法,拥有: error content sourceMap ast 参数。

  1. const loaderUtils = require('loader-utils')
  2. module.exports = function(source){
  3. const opt = loaderUtils.getOptions(this)
  4. this.callback(null,source)
  5. }

这里可以参照 less-loader ,里面的处理异步就有 async/await 和 this.async()

学以致用,实现一个路径替换的loader,把资源路径进行替换。

可以实现一个 markdown-loader ,看 step5 文件夹

https://webpack.docschina.org/contribute/writing-a-loader/

plugin

钩子函数。

plugin里有哪些生命周期钩子啊?哎

基本用法

  1. compiler.hooks.xxx.tap()

基本结构

  1. module.exports = Class XxxPlugin{
  2. constructor(options) this.options = options
  3. apply(compiler){
  4. compiler.hooks.xx.tap('XxxPlugin',()=>{})
  5. }
  6. }

开发重点是理解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

  1. config={
  2. cache: {
  3. type: "filesystem",
  4. }
  5. }

打包优化

  • 定位体积大的模块
  • 删除无关依赖
  • 抽离CDN
  • dll&dllReference 方式优化

webpack.DllPlugin

definePlugin 内置插件


优化

减少转译

就是优化 babel了
对于js,要使用babel,那得指定 include exclude 了

  1. loader:'babel-loader?cacheDirectory=true'

并行转换

自然就是别人已经做好的 happyPack 了
通过这个插件可以多线程打包,压榨你的cpu

类库提前打包引入

别人写vue我们就不要打包了,一般也不变,把公共代码抽离单独文件。
entry.vendor
内置插件 webpack.DllPlugin

减少打包的体积

你打包的文件太大了怎么办,记得可视化webpack分析。

思路,不打包,少打包

不打包

分离cdn

少打包

按需加载,页面按需加载。
大型组件,用到啥打包啥
代码用不到的 tree-shaking 掉。

主要是吧,webpack5就要出来了。