jsliang 求职系列 - 31 - Webpack

5,283 阅读20分钟

一 目录

不折腾的前端,和咸鱼有什么区别

目录
一 目录
二 前言
三 Webpack 是什么
四 Webpack 核心概念
五 Webpack 构建流程
六 entry
七 output
八 loader
8.1 关于文件处理常见的 loader
8.2 关于语法检查常见 loader
8.3 关于 HTML 代码处理常见的 loader
8.4 关于 CSS 代码处理常见的 loader
8.5 关于 JS 代码处理常见的 loader
九 plugin
9.1 常见 plugin
9.2 提高效率的 plugin
十 loader 和 plugin 的区别
十一 resolve
十二 从 0 开始配置 Webpack
12.1 技术选型
12.2 Loader 配置 - 处理 CSS、Less
12.3 Loader 配置 - 处理图片
12.4 Loader 配置 - 处理字体
12.5 Loader 配置 - MPA 多页面打包通用方案
12.6 SourceMap
12.7 WebpackDevServer
12.8 babel 解析
12.9 React
12.10 性能优化
12.11 其他
十三 知识补充:懒加载
13.1 代码分割
13.2 实现原理
13.3 Vue 按需加载
十四 知识补充:热更新
14.1 开启热更新
14.2 热更新原理
十五 知识补充:3 种 hash
十六 知识补充:source map
十七 知识补充:Webpack 打包原理
十八 参考文献
18.1 Webpack 系列文章
18.2 Webpack 性能优化
18.3 Scope Hoisting
18.4 Tree Shaking
18.5 懒加载

二 前言

返回目录

都 2020 了,不会点 Webpack 好像有点说不过去。

但是事实上如果不是分配到【架构组】之类的团体中,感觉接触 Webpack 的概率会少点吧。

就好比 jsliang 在上家公司,就没机会接触 Webpack,都是用这别人已经配置好的方案,纯粹做一个业务仔~

三 Webpack 是什么

返回目录

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。

当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

所以,它的本质是一个模块打包器,其工作是将每个模块打包成相应的 bundle

四 Webpack 核心概念

返回目录

  • mode:模式。对应有开发模式、生产模式等
  • entry:入口
  • output:出口
  • loader:模块转换器,用于把模块原内容按照需求转换成新内容。Webpack 对于 .jpg.txt 等内容无法处理,就需要 file-loaderurl-loader 等进行协助处理。
  • plugins:扩展插件,在 Webpack 构建流程中的特定时机注入拓展逻辑来改变构建结果或者做其他你想做的事情。

五 Webpack 构建流程

返回目录

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。

这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  • 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
  • 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
  • 确定入口:根据配置中的 entry 找出所有的入口文件
  • 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

简单来说:

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler(钩子)
  • 编译:从 Entry 出发,针对每个 Module(模块)串行调用对应的 Loader 去翻译文件的内容,再找到该 Module 依赖的 Module,递归地进行编译处理
  • 输出:将编译后的 Module 组合成 Chunk,将 Chunk 转换成文件,输出到文件系统中(Chunk 就是打包过程中,入口模块引用其他模块,模块再引用模块,这个关系链连接的 Module 就形成了 Chunk

在这个过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

六 entry

返回目录

指定打包⼊口文件,有三种不同的形式:string | object | array

一对一:一个入口、一个打包文件

module.exports = {
  entry: './src/index.js'
}

多对一:多个入口、一个打包文件

module.exports = {
  entry: [
    './src/index1.js',
    './src/index2.js',
  ]
}

多对多:多个入口、多打包文件

module.exports = {
  entry: {
    'index1': "./src/index1.js",
    'index2': "./src/index2.js"
  }
}

七 output

返回目录

打包后的文件位置。

module.exports = {
  ...,
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
    filename: "[name].js"
  }
}
  • 可以指定一个固定的文件名称,如果是多入口多出口(entry 为对象),则不能使用单文件出口,需要使用下面的方式
  • 通过 Webpack 内置的变量占位符:[name]

八 loader

返回目录

loader 的执行顺序是从右向左执行的,也就是后面的 loader 先执行。

假如有配置:

// webpack.config.js
module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.(le|c)ss$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
        exclude: /node_modules/,
      },
    ],
  },
};

那就是先处理 less-loader,再处理 css-loader,最后处理 style-loader

8.1 关于文件处理常见的 loader

返回目录

  • file-loader:当引入的文件是 .png.txt 等时,可以通过 file-loader 解析项目中的 url 引入。根据配置将文件拷贝到相应的路径,并修改打包后文件的引入路径,让它指向正确的文件。
  • url-loaderurl-loader 封装了 file-loader 且可以不依赖于 file-loader 单独使用,并且可以配置 limit。对小于 limit 大小的图片转换成 Base64,大于 limit 的时候使用 file-loader 里的方法。

8.2 关于语法检查常见 loader

返回目录

  • tslint-loader:通过 TSLint 检查 TypeScript 代码
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

8.3 关于 HTML 代码处理常见的 loader

返回目录

  • html-withimg-loader:处理 HTML 中的图片

8.4 关于 CSS 代码处理常见的 loader

返回目录

  • style-loader:动态创建 style 标签,将 CSS 代码插入到 head 中。
  • css-loader:负责处理 @importurl 等语句。例如 import css from 'file.css'url(image.png)
  • postcss-loader:负责进一步处理 CSS 文件,比如添加浏览器前缀,压缩 CSS 等。
  • less-loader:将 .less 文件内容转换成 CSS。
  • sass-loader:将 .sass 文件内容转换成 CSS。

8.5 关于 JS 代码处理常见的 loader

返回目录

  • babel-loader:将 JS 代码向低版本转换,我们需要使用 babel-loader
  • ts-loader:将 TypeScript 转换成 JavaScript

九 plugin

返回目录

9.1 常见 plugin

返回目录

  • clean-webpack-plugin:打包前自动清理 dist 目录,防止文件残留。
  • copy-webpack-plugin:将单个文件或者整个目录复制到构建目录
  • mini-css-extract-plugin:将 CSS 抽离出来单独打包并且通过配置可以设置是否压缩。
  • html-webpack-plugin:这个插件可以配置生成一个 HTML5 文件,其中 script 标签包含所有 Webpack 包。如果你设置多个入口点,你可以据此实现多页面应用打包。

9.2 提高效率的 plugin

返回目录

  • webpack-dashboard:可以更友好的展示相关打包信息。
  • webpack-merge:提取公共配置,减少重复配置代码
  • speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
  • size-plugin:监控资源体积变化,尽早发现问题
  • HotModuleReplacementPlugin:模块热替换

十 loader 和 plugin 的区别

返回目录

  • Loader

Loader 本质上就是一个函数,对接收到的内容进行转换,返回转换后的结果。

因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对不同类型的资源进行处理。

就好比 file-loader 或者 url-loader,配置之后就可以正确引用 png 等格式的图片、txt 等格式文件。

又好比 style-loader 以及 css-loader,引用后就可以对 CSS 内容进行预编译处理。

  • Plugin

Plugin 就是插件,就好比 jsliang 编写的 VS Code 插件一样,Plugin 拓展了 Webpack 的功能。

Plugin 就是在 Webpack 的生命周期中进行各种操作,从而达到使用者目的插件。

就好比 html-webpack-plugin,配合多入口形式使用之后,就可以实现多页面应用的功能。

又好比 clean-webpack-plugin 实现打包之前清空 dist 目录,copy-webpack-plugin 可以将单个文件或者整个目录复制到构建目录。

十一 resolve

返回目录

resolve 配置 Webpack 如何寻找模块所对应的文件。

Webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则。

// webpack.config.js
module.exports = {
  //....
  resolve: {
    modules: ['./src/components', 'node_modules'] // 从左到右依次查找
  }
}
  • resolve.modules:配置 Webpack 去哪些目录下寻找第三方模块,默认情况下,只会去 node_modules 下寻找,如果你在项目中某个文件夹下的模块经常被导入,不希望写很长的路径,那么就可以通过配置 resolve.modules 来简化。
  • resolve.alias:配置项通过别名把原导入路径映射成一个新的导入路径。
  • resolve.extensions:适配多端的项目中,可能会出现 .web.js, .wx.js,例如在转 Web 的项目中,我们希望首先找 .web.js,如果没有,再找 .jsextensions: ['web.js', '.js']

十二 从 0 开始配置 Webpack

返回目录

如何从 0 开始配置一个属于自己的 Webpack 脚手架呢?那就涉及到选型问题。

12.1 技术选型

返回目录

  • 移动端 || PC
  • MPA || SPA
  • HTML || 模板引擎
  • CSS || 预处理器
  • JavaScript ES5 || ES6
  • 本地发布服务(数据模拟)
  • React || Vue
  • 多人项目 || 单人项目
  • 语法规范 Eslint
  • 单元测试
  • 提交规范

12.2 Loader 配置 - 处理 CSS、Less

返回目录

  • use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader']
  • less less-loader:解析 .less 文件
  • postcss-loader autoprefixer:对 flex 布局等进行前缀补充

12.3 Loader 配置 - 处理图片

返回目录

  • file-loader:解析 .txt.png.md 等格式文件
  • url-loaderlimit: 1024,判断大小是否处理成 base64 格式

12.4 Loader 配置 - 处理字体

返回目录

  • url-loader

12.5 Loader 配置 - MPA 多页面打包通用方案

返回目录

  • 安装 glob
  • entryhtmlwebpackplugin 动态生成

12.6 SourceMap

返回目录

  • 开发环境配置:devtool: "cheap-module-eval-source-map
  • 线上生成配置(不推荐):devtool: "cheap-module-source-map"

12.7 WebpackDevServer

返回目录

  • 安装
  • 配置:devServer
  • HMR(热模块替换,Hot Module Replacement)
  • 开启 JS 模块的 HMR,需要 Webpack 配合

12.8 babel 解析

返回目录

  • 安装:npm i babel-loader @babel/core @babel-preset-env -D
    • @babel/corebabel 核心
    • babel-loaderbabelWebpack 的连接桥梁
    • babel-preset-env:输出什么样的代码,用它来解决
  • babel-loader:解析 ES6+
  • @babel/polyfill:垫片。包含所有 ES6+ 新特性代码
  • .babelrc

12.9 React

返回目录

  • 安装:react react-dom
  • 使用:@babel/preset-react

12.10 性能优化

返回目录

  • 缩小 loader 的文件范围:loaderinclude 配置,可以指定 src 目录,减少检查范围。
  • 优化 resolve.modules 配置:配置 Webpack 去哪些目录下寻找第三方模块,默认 node_modules
  • 分离 CSS:MiniCssExtractPlugin
  • hashchunkhashcontenthash 区别
    • hash 作用域 JS、CSS,图片的 hash 有区别,每次打包构建都会变化一次。
    • chunkhashchunk 为单位,修改了那部分就改动哪部分的 hash。(同时依赖的模块也会改变 hash
    • contenthash 只有自己内容发生改变,才发生改变(区别于 chunkhash
    • 所以 JS 适用于 chunkhash;CSS 适用于 contenthash;Image 适用于 hash
  • 压缩 CSS:optimize-css-assets-webpack-plugincssnano
  • 压缩 HTML:html-webpack-plugin
  • 压缩图片:img-webpack-loader
  • 分离 Webpack 配置:分离 base.configdev.configmpa.configpro.config 4 个,通过 merge 进行 config 配置的合并

更多看这里:

Webpack 性能优化

12.11 其他

返回目录

  • 如何简单编写一个 Webpack 解析器
  • 如何编写一个 Webpack loader
  • 如何编写一个 Webpack plugin

十三 知识补充:懒加载

返回目录

懒加载或者按需加载,是一种很好的优化网页或应用的方式。

这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。

这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

13.1 代码分割

返回目录

代码分割(code splitting)是指:将脚本中无需立即调用的代码在代码构建时转变为异步加载的过程。

在 Webpack 构建时,会避免加载已声明要异步加载的代码,异步代码会被单独分离出一个文件,当代码实际调用时被加载至页面。

代码分割技术的核心是 异步加载资源

可喜的是,浏览器允许我们这么做,W3C stage 3 规范: whatwg/loader 对其进行了定义:你可以通过 import() 关键字让浏览器在程序执行时异步加载相关资源。

在 Vue 中,可以直接使用 import() 关键字做到这一点,而在 React 中,你需要使用 react-loadable 去完成同样的事。

13.2 实现原理

返回目录

  1. 将需要进行懒加载的子模块单独打包成文件(children chunk
  2. 借助函数来实现延迟进行子模块的加载代码(import

print.js

console.log('输出 1');

export default () => {
  console.log('输出 2');
};

index.js

const btn = document.querySelector('.btn');
btn.onclick = import('./print.js').then((module) => {
  const print = module.default;
  print();
});

13.3 Vue 按需加载

返回目录

Vue 的特点就是 SPA - Single Page Application(单页应用程序)。

只有第一次加载页面,以后的每次页面切换,只需要进行组件替换。

它减少了请求次数,加快页面响应速度,降低对服务器压力等等。

但是,因为 Vue 是 SPA,所以首页第一次加载时会把所有组件以及组件相关资源全部加载,从而导致网站首页打开速度变慢,降低用户体验。

Vue 项目中,可以结合 Webpack,在 vue-router 通过 import 进行动态加载:

const routes = [{
  path: '/',
  name: 'Home',
  component: () => import('../views/Home.vue')
}];

十四 知识补充:热更新

返回目录

刷新我们一般分为两种:

  • 一种是页面刷新,不保留页面状态,就是简单粗暴,直接 window.location.reload()
  • 另一种是基于 WDSWebpack-dev-server)的模块热替换,只需要局部刷新页面上发生变化的模块,同时可以保留当前的页面状态,比如复选框的选中状态、输入框的输入等。

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR

这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

14.1 开启热更新

返回目录

在 Webpack 的 webpack.config.js 中:

  1. 配置 devServerhottrue
  2. plugins 中增加 new webpack.HotModuleReplacementPlugin()
// webpack.config.js
const webpack = require('webpack');
module.exports = {
  //....
  devServer: {
    hot: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin() // 热更新插件
  ]
}

并且在入口文件配置:

if(module && module.hot) {
  module.hot.accept()
}

此时修改代码的时候,只有对应部分的内容才会相应更新。

14.2 热更新原理

返回目录

HMR 的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diffchunk 需要更新的部分)。

实际上 webpack-dev-serverWDS)与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。

客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该 chunk 的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loadervue-loader 都是借助这些 API 实现 HMR

十五 知识补充:3 种 hash

返回目录

文件指纹是打包后输出的文件名的后缀,对应着 3 种 hash

  • hash 是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的 hash 值都会更改,并且全部文件都共用相同的 hash 值。(粒度整个项目)
  • chunkhash 是根据不同的入口进行依赖文件解析,构建对应的 chunk(模块),生成对应的 hash 值。只有被修改的 chunk(模块)在重新构建之后才会生成新的 hash 值,不会影响其它的 chunk。(粒度 entry 的每个入口文件)
  • contenthash 是跟每个生成的文件有关,每个文件都有一个唯一的 hash 值。当要构建的文件内容发生改变时,就会生成新的 hash 值,且该文件的改变并不会影响和它同一个模块下的其它文件。(粒度每个文件的内容)

十六 知识补充:source map

返回目录

source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map

map 文件只要不打开开发者工具,浏览器是不会加载的。

线上环境一般有三种处理方案:

  • hidden-source-map:借助第三方错误监控平台 Sentry 使用
  • nosources-source-map:只会显示具体行数以及查看源代码的错误栈。安全性比 source map
  • source map:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)

注意:避免在生产中使用 inline-eval-,因为它们会增加 bundle 体积大小,并降低整体性能。

十七 知识补充:Webpack 打包原理

返回目录

在 Webpack 简单实现中,简单的做了下如何将一份代码进行打包:

  1. 利用 babel 完成代码转换,并生成单个文件的依赖
  2. 生成依赖图谱
  3. 生成最后打包代码

十八 参考文献

返回目录

本系列参考文献有 51 篇文章。

18.1 Webpack 系列文章

返回目录

其他

2020 年文章

2019 年文章

2018 年文章

2017 文章

18.2 Webpack 性能优化

返回目录

2019 年文章

2018 年文章

2017 年文章

18.3 Scope Hoisting

返回目录

18.4 Tree Shaking

返回目录

18.5 懒加载

返回目录


jsliang 的文档库由 梁峻荣 采用 知识共享 署名-非商业性使用-相同方式共享 4.0 国际 许可协议 进行许可。
基于 github.com/LiangJunron… 上的作品创作。
本许可协议授权之外的使用权限可以从 creativecommons.org/licenses/by… 处获得。