Webpack
随着 Web 前端的不断发展,传统网页开发在逐渐往 Web 应用(Web Application,简称 WebAPP)的开发方式转变,页面开始变得越来越复杂,复杂的应用场景必然引起技术的进步,出现新的技术手段来解决现有问题。
前端模块化和工程化的呼声越来越高,随着前些年大行其道的 Grunt、Gulp、FIS(百度) 等构建工具的发展,带动了一波前端工程化热。
近几年,经过 React、Vue 库这些年的扩张,大型 Webapp 不再局限于手写 jQuery 操作 DOM,让大型 Webapp 有了全新的开发体验。在这个过程中,前端逐渐发展成了模块化和单页应用(single-page application,简称 SPA)为主的形式,在这种形态和 React、Vue 这些库的普及下,Webpack 越来越被更多成为主流构建工具。
Webpack 绝不仅仅是一个打包工具,系统的学习 Webpack 之后,我们可以基于 Webpack 做很多提升研发效率的事情。作为笔试必考、面试必问、工作必用的内容,Webpack 正变得越来越重要,尤其对于大型公司,会不会 Webpack 甚至能直接决定你是否被录用。
除了Webpack还有Vite(Vue的作者尤雨溪开发的)、Rollup、Parcel,也是打包工具,只是它们的应用面不一样。
#主要内容
- Webpack 开发配置
- Webpack 内核原理
- 工程化实践
#官方资源
英文文档:https://webpack.js.org/(opens new window)
中文文档:https://webpack.docschina.org/(opens new window)
#常见的问题
什么是模块化?
模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。(百度百科)
前端模块化一般指得是 JavaScript 的模块,最常见的是 Nodejs 的 NPM 包,每个模块可能是最小甚至是最优的代码组合,也可能是解决某些问题有很多特定模块组成的大模块。
多模块化的规范:
CommonJS、AMD和ES6 Module规范(另外还有CMD、UMD等)。为什么是 Webpack?
Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(static module bundler)
Webpack的工作流程是怎么样的?Webpack可以做什么?
webpack 是从入口文件开始,经过模块依赖加载、分析和打包三个流程完成项目的构建。
Webpack 还可以轻松的解决传统构建工具解决的问题:
- 模块化打包,一切皆模块,JS 是模块,CSS 等也是模块;
- 语法糖转换:比如 ES6 转 ES5、TypeScript;
- 预处理器编译:比如 Less、Sass 等;
- 项目优化:比如压缩、CDN;
- 解决方案封装:通过强大的 Loader 和插件机制,可以完成解决方案的封装,比如 PWA;
- 流程对接:比如测试流程、语法检测等。
Webpack 中的概念有哪些?
五大核心概念:入口、输出、插件、模块转化器、模式。
#环境准备
#安装Node.js
首先进入 Node.js 的官网 (opens new window),选择对应系统的下载包 (opens new window)进行下载安装,对于 windows 用户,直接下载安装包安装即可,如果是 Macos 用户,推荐使用 brew (opens new window)进行安装。
Node.js 版本众多,包括稳定版和开发版,可能不同的项目需要的 Node.js 版本不同,这里我推荐大家安装 8.9 以上版本,对于已经安装了 Node.js 的朋友,可以使用 nvm (opens new window)(windows 版本 (opens new window))对 Node.js 进行进行版本管理,(另外阿里有个 tnvm (opens new window),也是管理 Node.js 版本的,增加了 alinode 版本系列的 Node.js)。
#Node.js 包管理工具
Node.js 之所以这么流行,离不开庞大的社区建设,这里第一功劳就是 NPM 团队的贡献,使用 Node.js 写的代码,可以打包发布到 JavaScript 包管理平台 npmjs.com (opens new window)(这个存放包的地方一般也被称为仓库)上,当我们项目需要使用某个包(模块)时,可以直接使用包管理工具来安装(下载)对应的包,我们也可以免费注册一个账号,发布自己的公共包和私有包供其他人使用。
NPM 是围绕着语义版本控制(semver) (opens new window)思想而设计的,我们在软件版本中碰见的:rc、1.x.x、alpha、beta等名词都可以在 semver.org (opens new window)得到解释介绍,简单来说规范是主版本号.次版本号.修订号(MAJOR.MINOR.PATCH):
1.主版本号:当你做了不兼容的 API 修改; 2.次版本号:当你做了向下兼容的功能性新增; 3.修订号:当你做了向下兼容的问题修正;
例如:
|
name:上面的代码就是表明了这个项目为demo,这样如果我们将来发布到 npmjs.com (opens new window)会以这个来命名,除了这种方式的名称,还有一种命名的方式是@scope/name的方式,是作用域包,例如我们用来转化 ES6 语法的@babel/core就是@babel的作用域,详细介绍可以查看 package.json 的文档(opens new window)dependencies:是demo这个项目的依赖,就是 demo 这个包内离开webpack这个包就不能使用了,对应的还有devdependencies,开发以来,一般需要二次开发 demo 的时候需要安装的包,实际项目中,webpack是构建工具,代码不会直接用 webpack 的 API,而只在开发和打包的时候采用,所以正确做法是放在devdependencies中。
注意到dependencies 中webpack 的后面版本号前面加了^,意思是主版本是4的最新版本,每次执行安装命令的时候,会更新符合这个规则的最新包,可以在npm semver range 部分 (opens new window)看到更详细的介绍。
#NPM 的常用命令
下面介绍下 NPM 的常用命令:安装、删除、初始化、配置。
#安装和删除
安装某个 NPM 包,使用命令:
|
如果执行命令的目录下有package.json,则可以直接用npm install安装package.json中的所有依赖。
#本地模式和全局模式
npm 的包安装,分为本地模式和全局模式,默认是本地模式,即在执行npm install命令的当前目录创建node_modules,然后下载安装包及其依赖到node_modules目录。
全局模式是指安装到全局路径的方式。在 Node.js 的 require 依赖之时,会优先查找自己当前文件的node_modules,如果没有,则循环遍历上层的node_modules,如果便历到根目录还找不到,则会使用全局模式安装的模块,另外全局模式安装的包可以指定全局命令,只需要在package.json增加bin字段并且指向包内对应的文件即可。全局安装一个包,使用命令npm install --global,--global可以简写为-g。
#初始化一个 NPM 项目
npm init 用来初始化生成一个新的 package.json 文件。
输入npm init并且根据对应的提示回答问题,会向用户提问一系列问题。
如果你觉得不用修改默认配置,一路回车就可以了。

如果使用了
-f(代表force)、-y(代表yes),则跳过提问阶段,直接生成一个新的package.json文件。
#设置 NPM 镜像
由于 NPM 网站经常不稳定,所以国内有很多镜像可以使用,淘宝 NPM 镜像 (opens new window)是国内最大的一家 NPM 镜像网站,还有cnpm包可以替换官方 NPM 来使用,使用cnpm直接使用淘宝镜像安装 NPM 包。
单次使用镜像方法:
|
设置默认 npm 使用淘宝镜像方法:
|
使用下面的命令可以安装 cnpm 包,之后直接像使用 npm 一样使用 cnpm 即可,不需要添加register
|
#NPM 其他常用命令
- npm set:设置环境变量,例如:
npm set init-author-name 'Your name',初始化的时候会使用默认环境变量; - npm info:查看某个包的信息,例如:
npm info lodash; - npm search:查找 npm 仓库,后面可以跟字符串或者正则表达式,例如:
npm search webpack; - npm list:树形的展现当前项目安装的所有模块,以及对应的依赖,例如:
npm list --global查看全局安装的模块。
可以使用npm –help查看
#NPM Scripts
NPM 不仅可以用于模块管理,还可以用于执行脚本。
package.json 文件中可以添加 scripts 字段,用于指定脚本命令,供 NPM 直接调用。例如:
|
在package.json添加上面字段之后,可以直接使用npm run build和npm run start命令了,实际上:
npm run build:相当于执行了当前项目中目录下的webpack命令;npm run start:相当于执行了node src/scripts/dev.js。
另外npm run start还可以简写成npm start。
Tips:除了 npm 外,还有一些包管理工具,主要是针对 npm 的下载速度慢、
node_modules混乱等缺点设计的,例如yarn (opens new window)和 pnpm (opens new window)。
#安装 Webpack-cli
Webpack-cli (opens new window)是 Webpack 的 CLI (Command-line interface)工具,如果在项目中,我们可以使用下面的方式安装:
|
如果想全局使用webpack的命令,可以使用npm install -g webpack-cli安装。
Tips:这里建议在项目中安装 webpack-cli 并且使用
--save-dev的配置将 webpack-cli 放到开发依赖中。
后面使用webpack可以有两种方式:
配置scripts脚本
"build": "webpack"使用
npx命令npx webpack
核心概念
#HelloWorld
首先是创建项目,创建一个名字为webpack-learn的文件夹(这里千万别创建成webpack),并且进入文件夹使用npm init进行初始化:
|
创建目录src,其结构如下:

创建hello-world.js文件:
|
创建 index.js文件:
|
下面尝试一下webpack打包:
|
说明:
module.exports与require是node.js所遵循的Commonjs规范;module.exports导出了一个字符串;require引用的如果是js文件,则可以省略;
打包结果:

执行成功,index.js 文件被打包到了dist文件夹下了,同时提示我们默认使用了production mode,即生产环境,打开dist/main.js,里面的代码的确是被压缩的,说明是生产环境打包;
下面继续修改package.json添加scripts:
|

同样可以尝试build脚本,这里来说明一下这两个模式的内容,模式其实就是webpack内置了一些打包的参数。
| 选项 | 描述 |
|---|---|
development |
会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。 |
production |
会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 production。为模块和 chunk 启用确定性的混淆名称,FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin 和 TerserPlugin 。 |
none |
不使用任何默认优化选项 |
如果没有设置,webpack 会给 mode 的默认值设置为 production。上面的内容,我们后续会介绍到!
#CLI进阶
一般的CLI的命令都会有一个help命令:
|
说明:
--config,-c:指定一个 Webpack 配置文件的路径;--mode:指定打包环境的 mode,取值为development和production,分别对应着开发环境和生产环境;--json:输mode出 Webpack 打包的结果,可以使用webpack --json > stats.json方式将打包结果输出到指定的文件;--progress:显示 Webpack 打包进度;--watch,-w:watch 模式打包,监控文件变化之后重新开始打包;
#配置文件
Webpack 是可配置的模块打包工具,我们可以通过修改 Webpack 的配置文件(webpack.config.js)来对 Webpack 进行配置,Webpack 的配置文件是遵循 Node.js 的 CommonJS 模块规范的,即:
- 通过
require()语法导入其他文件或者使用 Node.js 内置的模块 - 普通的 JavaScript 编写语法,包括变量、函数、表达式等
说白了,webpack.config.js是一个 Node.js 的模块,简单的 webpack.config.js 示例
|
上面示例中,使用 CommonJS 的require引入 Node.js 内置的path模块,然后通过module.exports将 Webpack 的配置导出。
Tips:Webpack 的配置是一个 Node.js 模块,所以并不只是 JSON 对象。
默认情况下,Webpack 会查找执行目录下面的webpack.config.js作为配置,如果需要指定某个配置文件,可以使用下面的命令:
|
如果 Webpack 不是全局安装,则可以在项目目录下实行:
|
或者使用npx
|
打包结果展示:

说明:为什么main.js还在dist,别急,后面会进行打包之后删除;
除了配置文件的语法多样之外,对于配置的类型也是多样的,最常见的是直接作为一个对象来使用,除了使用对象,Webpack 还支持函数、Promise 和多配置数组。
#核心概念
讲完 Webpack 配置文件,下面讲下配置文件中的配置项:
| 参数 | 说明 |
|---|---|
entry |
入口起点(entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图(dependency graph) (opens new window)的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 |
output |
默认值是 ./src/index.jsoutput 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。 |
mode |
通过选择 development, production 或 none 之中的一个,来设置 mode 参数,可以启用 webpack 内置在相应环境下的优化。其默认值为 production。 |
loader |
模块转化器,模块的处理器,对模块进行转换处理;webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 模块 (opens new window),以供应用程序使用,以及被添加到依赖图中。 |
plugin |
插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。 |
还有一些其他的概念:
| 参数 | 说明 |
|---|---|
module |
开发中每一个文件都可以看做 module,模块不局限于 js,也包含 css、图片等 |
chunk |
代码块,一个 chunk 可以由多个模块组成 |
bundle |
最终打包完成的文件,一般就是和 chunk 一一对应的关系,bundle 就是对 chunk 进行便意压缩打包等处理后的产出 |
Webpack 不仅仅支持 js 配置,还支持 ts(TypeScript)、CoffeeScript 甚至 JSX (opens new window)语法的配置,不同语言其实核心配置项都不变,只不过语法不同而已 ——原因:是由于有很多不同的loaders!
常见配置
#mode模式
Webpack4.0 开始引入了mode配置。
通过配置mode=development或者mode=production来制定是开发环境打包,还是生产环境打包,比如生产环境代码需要压缩,图片需要优化,Webpack 默认mode是生产环境,即mode=production。
除了在配置文件中设置mode:
|
还可以在命令行中设置mode:
|
#entry入口
Webpack 的两个核心概念:entry和output,即入口和输出。
#context
context即项目打包的相对路径上下文,如果指定了context="/User/test/webpack",那么设置的entry和output的相对路径都是相对于/User/test/webpack的,包括在 JavaScript 中引入模块也是从这个路径开始的。
由于context的作用,决定了context值必须是一个绝对路径。
|
Tips:在实际开发中
context一般不需要配置,不配置则默认为process.cwd()即工作目录。 工作目录(英语:Working directory),计算机用语。使用者在作业系统内所在的目录,使用者可在此用相对档名存取档案 —— 维基百科。
Webpack 的entry支持多种类型,包括字符串、对象、数组。从作用上来说,包括了单文件入口和多文件入口两种方式。
#单文件入口
单文件的用法如下:
|
单文件入口可以快速创建一个只有单一文件入口的情况,例如 library 的封装。
但是单文件入口的方式相对来说比较简单,在扩展配置的时候灵活性较低。
entry还可以传入包含文件路径的数组,当entry为数组的时候也会合并输出,例如下面的配置:
|
上面配置无论是字符串还是字符串数组的 entry,实际上都是只有一个入口,但是在打包产出上会有差异:
- 如果直接是 string 的形式,那么 webpack 就会直接把该 string 指定的模块(文件)作为入口模块
- 如果是数组
[string]的形式,那么 webpack 会自动生成另外一个入口模块,并将数组中每个元素指定的模块(文件)加载进来,并将最后一个模块的 module.exports 作为入口模块的 module.exports 导出。
#多文件入口
多文件入口是使用对象语法来通过支持多个entry,多文件入口的对象语法相对于单文件入口,具有较高的灵活性,例如多页应用、页面模块分离优化。
多文件入口的语法如下:
|
上面的语法将entry分成了 3 个独立的入口文件,这样会打包出来三个对应的 bundle,在后面的文章还会介绍使用splitChunks抽离一个项目中多个entry的公共代码。
Tips:对于一个 HTML 页面,推荐只有一个
entry,通过统一的入口,解析出来的依赖关系更方便管理和维护。
多入口的应用场景是:旧的JSP或者PHP项目,前端需要工程化工具提供css预编译、JS压缩。
#output输出
webpack 的output是指定了entry对应文件编译打包后的输出 bundle。
output的常用属性是:
path:此选项制定了输出的 bundle 存放的路径,比如dist、output等filename:这个是 bundle 的名称publicPath:指定了一个在浏览器中被引用的 URL 地址,后面详细介绍
当不指定 output 的时候,默认输出到 dist/main.js ,即 output.path 是dist,output.filename 是 main。
一个 webpack 的配置,可以包含多个entry,但是只能有一个output。
对于不同的entry可以通过output.filename占位符语法来区分,比如:
|
其中[name]就是占位符,它对应的是entry的key(home、search、list),所以最终输出结果是:
|
#占位符
Webpack 目前支持的占位符列出来:
| 占位符 | 含义 |
|---|---|
[hash] |
模块标识符的 hash |
[chunkhash] |
chunk 内容的 hash |
[name] |
模块名称 |
[id] |
模块标识符 |
[query] |
模块的 query,例如,文件名 ? 后面的字符串 |
[function] |
一个 return 出一个 string 作为 filename 的函数 |
[hash]:是整个项目的 hash 值,其根据每次编译内容计算得到,每次编译之后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 发生改变;在一个项目中虽然入口不同,但是 hash 是相同的;
hash 无法实现前端静态资源在浏览器上长缓存,这时候应该使用 chunkhash;
[chunkhash]:根据不同的入口文件(entry)进行依赖文件解析,构建对应的 chunk,生成相应的 hash;只要组成 entry 的模块文件没有变化,则对应的 hash 也是不变的,所以一般项目优化时,会将公共库代码拆分到一起,因为公共库代码变动较少的,使用 chunkhash 可以发挥最长缓存的作用;[contenthash]:使用 chunkhash 存在一个问题,当在一个 JS 文件中引入了 CSS 文件,编译后它们的 hash 是相同的。而且,只要 JS 文件内容发生改变,与其关联的 CSS 文件 hash 也会改变,针对这种情况,可以把 CSS 从 JS 中使用mini-css-extract-plugin (opens new window)或 extract-text-webpack-plugin (opens new window)抽离出来并使用 contenthash。
[hash]、[chunkhash]和[contenthash]都支持[xxx:length]的语法,[hash] 和 [chunkhash] 的长度可以使用 [hash:16](默认为 20)来指定。
占位符是可以组合使用的,例如
[name]-[hash:8]
#output.publicPath
对于使用<script> 和 <link>标签时,当文件路径不同于他们的本地磁盘路径(由output.path指定)时,output.publicPath被用来作为src或者link指向该文件。这种做法在需要将静态文件放在不同的域名或者 CDN 上面的时候是很有用的。
|
则输出:
|
上面的/assets/logo.png就是根据publicPath输出的,output.path制定了输出到本地磁盘的路径,而output.publicPath则作为实际上线到服务器之后的 url 地址。所以在上 CDN 的时候可以这样配置:
|
则输出:
|
#output.library
如果打包的目的是生成一个供别人使用的库,那么可以使用output.library来指定库的名称,库的名称支持占位符和普通字符串:
|
#output.libraryTarget
使用output.library 确定了库的名称之后,还可以使用output.libraryTarget指定库打包出来的规范,output.libraryTarget取值范围为:var、assign、assign-properties,this、window、global、commonjs、commonjs2、amd-require、amd、umd、umd2、system、jsonp,默认是var,下面通过打包后的代码不同,来看下差别。
|
使用空index.js打包,结果如下,产出dist/var.js:

下面的代码示例一样:
|
注意:
libraryTarget=global的时候,如果target=node才是 global,默认target=web下 global 为 window,保险起见可以使用this。
#externals(必看)
externals配置项用于去除输出的打包文件中依赖的某些第三方 js 模块(例如 jquery,vue 等等),减小打包文件的体积。举例说明:
index.html
|
webpack.config.js
|
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
|
该功能通常在开发自定义 js 库(library)的时候用到,用于去除自定义 js 库依赖的其他第三方 js 模块。这些被依赖的模块应该由使用者提供,而不应该包含在 js 库文件中。例如:开发一个 jQuery 插件或者 Vue 扩展,不需要把 jQuery 和 Vue 打包进的 bundle,引入库的方式应该交给使用者。
使用者应该怎么提供这些被依赖的模块给的 js 库(library)使用呢?
这就要看的 js 库的导出方式是什么,以及使用者采用什么样的方式使用的库。例如:
| js library 导出方式 | output.libraryTarget | 使用者引入方式 | 使用者提供给被依赖模块的方式 |
|---|---|---|---|
| 默认的导出方式 | output.libraryTarget=‘var’ | 只能以 <script> 标签的形式引入的库 |
只能以全局变量的形式提供这些被依赖的模块 |
| commonjs | output.libraryTarget=‘commonjs’ | 只能按照 commonjs 的规范引入的库 | 被依赖模块需要按照 commonjs 规范引入 |
| amd | output.libraryTarget=‘amd’ | 只能按照 amd 规范引入 | 被依赖模块需要按照 amd 规范引入 |
| umd | output.libraryTarget=‘umd’ | 可以用<script>、commonjs、amd 引入 |
被依赖模块需要按照对应方式引入 |
如果不是在开发一个 js 库,即没有设置 output.library, output.libraryTarget 等配置信息,那么生成的打包文件只能以 <script> 标签的方式在页面中引入,因此那些被去除的依赖模块也只能以全局变量的方式引入。
#target
在项目开发中,不仅仅是开发 web 应用,还可能开发的是 Node.js 服务应用、或者 electron 这类跨平台桌面应用,这时候因为对应的宿主环境不同,所以在构建的时候需要特殊处理。webpack 中可以通过设置target来指定构建的目标(target)。
可以接受的值是string, [string], false
- 设置String
|
官方 (opens new window)支持的选项:
| 选项 | 描述 |
|---|---|
async-node |
编译为类 Node.js 环境可用(使用 fs 和 vm 异步加载分块) |
electron-main |
编译为 Electron (opens new window)主进程。 |
electron-renderer |
编译为 Electron (opens new window)渲染进程,使用 JsonpTemplatePlugin, |
FunctionModulePlugin 来为浏览器环境提供目标,使用 NodeTargetPlugin 和 ExternalsPlugin |
|
| 为 CommonJS 和 Electron 内置模块提供目标。 | |
electron-preload |
编译为 Electron (opens new window)渲染进程, |
使用 NodeTemplatePlugin 且 asyncChunkLoading 设置为 true ,FunctionModulePlugin 来为浏览器提供目标,使用 NodeTargetPlugin 和 ExternalsPlugin 为 CommonJS 和 Electron 内置模块提供目标。 |
|
node |
编译为类 Node.js 环境可用(使用 Node.js require 加载 chunks) |
node-webkit |
编译为 Webkit 可用,并且使用 jsonp 去加载分块。支持 Node.js 内置模块和 nw.gui(opens new window) |
| 导入(实验性质) | |
nwjs[[X].Y] |
等价于 node-webkit |
web |
编译为类浏览器环境里可用 (默认) |
webworker |
编译成一个 WebWorker |
esX |
编译为指定版本的 ECMAScript。例如,es5,es2020 |
browserslist |
从 browserslist-config 中推断出平台和 ES 特性 (如果 browserslist 可用,其值则为默认) |
- 设置[string]
当传递多个目标时,将使用共同的特性子集:
webpack.config.js
|
webpack 将生成 web 平台的运行时代码,并且只使用 ES5 相关的特性。
目前并不是所有的 target 都可以进行混合。
webpack.config.js
|
此时会导致错误。webpack 暂时不支持 universal 的 target。
- false
如果上述列表中的预设 target 都不符合你的需求,你可以将 target 设置为 false,这将告诉 webpack 不使用任何插件。
webpack.config.js
|
或者可以使用你想要指定的插件
webpack.config.js
|
当没有提供 target 或 environment (opens new window)特性的信息时,将默认使用 ES2015。
#devtool(必看)
devtool是来控制怎么显示sourcemap (opens new window),通过 sourcemap 可以快速还原代码的错误位置。
但是由于 sourcemap 包含的数据量较大,而且生成算法需要计算量支持,所以 sourcemap 的生成会消耗打包的时间,下面的表格整理了不同的devtool值对应不同的 sourcemap 类型对应打包速度和特点。
| devtool | 构建速度 | 重建 | 生产环境 | 品质+建议 |
|---|---|---|---|---|
| 留空,none | ++ | ++ | yes | 打包后的代码(默认) |
| eval | + | ++ | no | 生成后的代码(开发+高性能) |
| eval-cheap-source-map | o | + | no | 转换过的代码(仅限行) |
| eval-cheap-module-source-map | – | + | no | 原始源代码(仅限行) |
| eval-source-map | –– | o | no | 原始源代码(开发+sourcemap) |
| cheap-source-map | o | – | no | 转换过的代码(仅限行) |
| cheap-module-source-map | – | – | no | 原始源代码(仅限行) |
| source-map | –– | –– | yes | 原始源代码(生产+sourcemap) |
| inline-cheap-source-map | o | – | no | 转换过的代码(仅限行) |
| inline-cheap-module-source-map | – | – | no | 原始源代码(仅限行) |
| inline-source-map | –– | –– | no | 原始源代码 |
| eval-nosources-cheap-source-map | o | + | no | 转换过的代码(不包括源代码) |
| eval-nosources-source-map | –– | o | no | 原始源代码 |
| hidden-source-map | – | – | yes | 原始源代码 |
| nosources-source-map | – | – | yes | 无源代码内容 |
++快速,+比较快,o中等,-比较慢,--慢上表中,未完全展示,请知悉
一般在实际项目中,推荐生产环境不使用或者使用 source-map(如果有 Sentry (opens new window)这类错误跟踪系统),开发环境使用cheap-module-eval-source-map。
#resolve(必看)
resolve 配置是帮助 Webpack 查找依赖模块的,通过 resolve 的配置,可以帮助 Webpack 快速查找依赖,也可以替换对应的依赖(比如开发环境用 dev 版本的 lib 等)。resolve 的基本配置语法如下:
|
下面来介绍下常用的 resolve 配置。
#extensions
resolve.extensions是帮助 Webpack 解析扩展名的配置,默认值:['.wasm', '.mjs', '.js', '.json'],所以引入 js 和 json 文件,可以不写它们的扩展名,通常可以加上 .css、.less等,但是要确保同一个目录下面没有重名的 css 或者 js 文件,如果存在的话,还是写全路径吧。
|
#alias
resolve.alias 是最常用的配置,通过设置 alias 可以帮助 webpack 更快查找模块依赖,而且也能使编写代码更加方便。例如,在实际开发中经常会把源码都放到src文件夹,目录结构如下:
|
在src/pages/demo/index.js中如果要引用src/lib/utils.js那么可以通过:import utils from '../../lib/utils'; ,如果目录更深一些,会越来越难看,这是可以通过设置 alias 来缩短这种写法,例如:
|
经过设置了 alias,可以在任意文件中,不用理会目录结构,直接使用require('@lib/utils')或者require('src/lib/utils')来帮助 Webpack 定位模块。
- alias 的名字可以使用
@!~等这些特殊字符,实际使用中 alias 都使用一种,或者不同类型使用一种,这样可以跟正常的模块引入区分开,增加辨识度;- 使用
@注意不要跟 npm 包的scope (opens new window)冲突!- 这时在 vscode 中会导致检测不到 utils 中的内容,不能帮快速编写代码,可以通过在项目根目录创建 jsconfig.json 来帮助定位:
|
alias 还常被用于给生产环境和开发环境配置不同的 lib 库,例如下面写法,在线下开发环境使用具有 debug 功能的 dev 版本 San (opens new window):
|
alias 还支持在名称末尾添加$符号来缩小范围只命中以关键字结尾的导入语句,这样可以做精准匹配:
|
#mainFields
使用mainField字段,修改了webpack在打包过程中,去解析模块的package.json中的指定字段(默认为main),作为入口文件。
有一些用到的模块会针对不同宿主环境提供几份代码,例如提供 ES5 和 ES6 的两份代码,或者提供浏览器环境和 nodejs 环境两份代码,这时候在package.json文件里会做如下配置:
|
在 Webpack 中,会根据resolve.mainFields的设置去决定使用哪个版本的模块代码,在不同的target下对应的resolve.mainFields默认值不同,默认target=web对应的默认值为:
|
所以在target=web打包时,会寻找browser版本的模块代码。
下面是不常用的或者比较简单的配置:
resolve.mainFiles:解析目录时候的默认文件名,默认是index,即查找目录下面的index+resolve.extensions文件;resolve.modules:查找模块依赖时,默认是node_modules;resolve.symlinks:是否解析符合链接(软连接,symlink (opens new window));resolve.plugins:添加解析插件,数组格式;resolve.cachePredicate:是否缓存,支持 boolean 和 function,function 传入一个带有 path 和 require 的对象,必须返回 boolean 值。
举例:首先给 A 模块目录下的 package.json 新增 idebug 字段,指向该模块的入口文件(假设为 src/index.ts )
|
其次在 C 模块目录里的 webpack 配置项更改 resolve 配置,将新增的 idebug 字段作为 mainFields 数组的第一个属性:
|
上述
idebug名字可以自定义,只要保证 A 模块中的 package.json 和 C 模块的 webpack 配置项中的mainFields中的名字一致即可;
这样在运行 C 模块的 Webpack 时,就不会去找本目录下的 node_modules 中的 A 模块,而是去加载 ../A/src/index.ts 文件,达到了 C 模块和 A 模块源码联调的目的;
一些说明:
- 一般使用
path.resolve()来获取绝对路径 - 配置
alias的时候,可以用$结尾,兼容引入子包的问题 - 一般来讲,webpack 项目都有 dev 和 production 两套配置流程,只在 dev 的时候采用上述方案, production 还是保持原有的配置。
#module模块
在 webpack 解析模块的同时,不同的模块需要使用不同类型的模块处理器来处理,这部分的设置就在module配置中。
module 有两个配置:module.noParse和module.rules,
#module.noParse
module.noParse配置项可以让 Webpack 忽略对部分没采用模块化的文件的递归解析和处理,这样做的好处是能提高构建性能,接收的类型为正则表达式,或者正则表达式数组或者接收模块路径参数的一个函数:
|
Tips:这里一定要确定被排除出去的模块代码中不能包含
import、require、define等内容,以保证 webpack 的打包包含了所有的模块,不然会导致打包出来的 js 因为缺少模块而报错。
#parser来控制模块化语法
因为webpack是以模块化的JavaScript文件为入口,所以内置了对模块化JavaScript的解析功能,支持AMD、Commonjs、SystemJs、ES6。
parse属性可以更细粒度的配置哪些模块语法要解析,哪些不解析。
简单来说,如果设置parser.commonjs=false,那么代码里面使用commonjs的require语法引入模块,对应的模块就不会被解析到依赖中,也不会被处理,支持的选项包括:
|
Tips:parser是语法层面的限制,noParse只能控制哪些文件不进行解析。
#module.rules
module.rules是在处理模块时,将符合规则条件的模块,提交给对应的处理器来处理,通常用来配置 loader,其类型是一个数组,数组里每一项都描述了如何去处理部分文件。每一项 rule 大致可以由以下三部分组成:
- 条件匹配:通过
test、include、exclude等配置来命中可以应用规则的模块文件; - 应用规则:对匹配条件通过后的模块,使用
use配置项来应用loader,可以应用一个 loader 或者按照从后往前的顺序应用一组 loader,当然还可以分别给对应 loader 传入不同参数; - 重置顺序:一组 loader 的执行顺序默认是从后到前(或者从右到左)执行,通过
enforce选项可以让其中一个 loader 的执行顺序放到最前(pre)或者是最后(post)。
#条件匹配
如上所述,条件匹配相关的配置有test、include、exclude、resource、resourceQuery和issuer。条件匹配的对象包括三类:resource,resourceQuery和issuer。
- resource:请求文件的绝对路径。它已经根据 resolve 规则解析;
- issuer: 被请求资源(requested the resource)的模块文件的绝对路径,即导入时的位置。
举例来说明:从 app.js 导入 './style.css?inline':
resource是/path/to/style.css;resourceQuery是?之后的inline;issuer是/path/to/app.js。
来看下 rule 对应的配置与匹配的对象关系表:
| rule 配置项 | 匹配的对象 |
|---|---|
test |
resource类型 |
include |
resource类型 |
exclude |
resource类型 |
resource |
resource类型 |
resourceQuery |
resourceQuery类型 |
issuer |
issuer类型 |
举例说明,下面rule 的配置项,匹配的条件为:来自src和test文件夹,不包含node_modules和bower_modules子目录,模块的文件路径为.tsx和.jsx结尾的文件。
|
#Loader解析处理器
loader是解析处理器,通过loader可以将 ES6 语法的 js 转化成 ES5 的语法,可以将图片转成 base64 的dataURL,在 JavaScript 文件中直接import css 和 html 也是通过对应的loader来实现的。
在使用对应的loader之前,需要先安装它,例如,要在 JavaScript 中引入 less,则需要安装less-loader:
|
然后在module.rules中指定*.less文件都是用less-loader:
|
简单来理解上面的配置,test项使用 /\.less$/正则匹配需要处理的模块文件(即 less 后缀的文件),然后交给less-loader来处理,这里的less-loader是个 string,最终会被作为require()的参数来直接使用。
这样 less 文件都会被less-loader处理成对应的 css 文件。
除了直接在webpack.config.js是用 loader 的方式之外,还可以在对应的 JavaScript 文件中是用 loader:
|
上面的代码,实际是将loader.html的内容转化成 string 变量,直接给输出了,等同于:
|
加上下面配置的效果:
|
如果没有 html-loader,直接
require一个 html 文件,会被当初 js 模块来执行,则会报错。
require('html-loader!./loader.html')中!类似 Unix 系统中命令行的管道,这里!隔开的命令是从右到左解析的,即先加载loader.html然后在将加载的文件内容传给html-loader处理。
综上,loader 有两种配置方式:
使用 webpack.config.js 的配置方式:
module.exports = {
module: {
rules: [{test: /\.html$/, use: ['html-loader']}]
}
};在 JavaScript 文件内使用内联配置方式:
const html = require('html-loader!./loader.html');
// or
import html from 'html-loader!./loader.html';Tips:use 中传递字符串(如:
use: ['style-loader'])是 loader 属性的简写方式(如:use: [{loader: 'style-loader'}])
#Loader 的参数
给 loader 传参的方式有两种:
- 通过
options传入 - 通过
query的方式传入:
|
#Loader 的解析顺序
对于一些类型的模块,简单配置一个 loader 是不能够满足需求的,例如 less 模块类型的文件,只配置了 less-loader 仅仅是将 Less 语法转换成了 CSS 语法,但是 JS 还是不能直接使用,所以还需要添加css-loader来处理,这时候就需要注意 Loader 的解析顺序了。前面已经提到了,Webpack 的 Loader 解析顺序是从右到左(从后到前)的,即:
|
如果需要调整 Loader 的执行顺序,可以使用enforce,enforce取值是pre|post,pre表示把放到最前,post是放到最后:
|
#oneOf:只应用第一个匹配的规则
oneOf表示对该资源只应用第一个匹配的规则,一般结合resourceQuery,具体代码来解释:
|
#plugin插件
plugin是 Webpack 的重要组成部分,通过plugin可以解决loader解决不了的问题。
Webpack 本身就是有很多插件组成的,所以内置了很多插件,可以直接通过webpack对象的属性来直接使用,例如:webpack.DefinePlugin
|
除了内置的插件,也可以通过 NPM 包的方式来使用插件:
|
Tips:
loader面向的是解决某个或者某类模块的问题,而plugin面向的是项目整体,解决的是loader解决不了的问题。
完整的配置
参考网址:https://webpack.docschina.org/configuration/#options(opens new window)
|
#Babel核心知识
在 webpack 中编写 JavaScript 代码,可以使用最新的 ES 语法,而最终打包的时候,webpack 会借助 Babel 将 ES6+语法转换成在目标浏览器可执行 ES5 语法。所以 Babel 是一个重要的知识点需要掌握。
#什么是 Babel
Babel 是 JavaScript 的编译器,通过 Babel 可以将写的最新 ES 语法的代码轻松转换成任意版本的 JavaScript 语法。随着浏览器逐步支持 ES 标准,不需要改变代码,只需要修改 Babel 配置即可以适配新的浏览器。
举例说明,下面是 ES6 箭头函数语法的代码:
|
经过 Babel 处理后,可以转换为普通的 ES5 语法:
|
#Babel介绍
下面来介绍下 Babel 的安装和功能及其配置文件。
#初体验(环境安装)
使用 babel-cli 命令行工具
Babel 本身自己带有 CLI(Command-Line Interface,命令行界面) 工具,可以单独安装使用。下面在项目中安装 @babel/cli 和 @babel/core。
|
然后创建一个babel.js文件:
|
然后执行npx babel babel.js,则会看到输出的内容,此时可能会看到输出的内容跟源文件内容没有区别,这是因为没有加转换规则,下面安装@babel/preset-env。然后执行 CLI 的时候添加 --presets flag:
|
最终输出的代码就是转换为 ES5 的代码了:
|
如果要输出结果到固定文件,可以使用 --out-file 或 -o 参数:npx babel babel.js -o output.js。
Babel 7 使用了 @babel 命名空间来区分官方包,因此以前的官方包
babel-xxx改成了@babel/xxx。
#配置文件
除了使用命令行配置 flag 之外,Babel 还支持配置文件,配置文件支持两种:
- 使用
package.json的babel属性; - 在项目根目录单独创建
.babelrc或者.babelrc.js文件。
直接上对应的示例:
|
Babel会在正在被转义的文件当前目录中查找一个 .babelrc 文件。 如果不存在,它会向外层目录遍历目录树,直到找到一个 .babelrc 文件,或一个 package.json 文件中有 "babel": {} 。
#env 选项
如果希望在不同的环境中使用不同的 Babel 配置,那么可以在配置文件中添加env选项:
|
env 选项的值将从 process.env.BABEL_ENV 获取,如果没有的话,则获取 process.env.NODE_ENV 的值,它也无法获取时会设置为 "development"。
#插件和 Preset
Babel 的语法转换是通过强大的插件系统来支持的,Babel 的插件分为两类:转换插件和语法解析插件。
不同的语法对应着不同的转换插件,比如要将箭头函数转换为 ES5 函数写法,那么可以单独安装@babel/plugin-transform-arrow-functions插件,转换插件主要职责是进行语法转换的,而解析插件则是扩展语法的,比如要解析jsx这类 React 设计的特殊语法,则需要对应的 jsx 插件。
如果不想一个个的添加插件,那么可以使用插件组合 preset(插件预设,插件组合更加好理解一些),最常见的 preset 是@babel/preset-env。
之前的preset是按照TC39提案阶段来分的,比如看到babel-preset-stage-1代表,这个插件组合里面是支持 TC39 Stage-1 阶段的转换插件集合。
TC39指的是技术委员会(Technical Committee)第 39 号,它是 ECMA 的一部分,ECMA 是 「ECMAScript」规范下的 JavaScript 语言标准化的机构。ES6 出来之后,
TC39精简了提案的修订过程,新流程设计四个 Stage 阶段:
- Stage 0 - 设想(Strawman):只是一个想法;
- Stage 1 - 建议(Proposal):这是值得跟进的;
- Stage 2 - 草案(Draft):初始规范,应该提供规范初稿;
- Stage 3 - 候选(Candidate):不会有太大的改变,在对外发布之前只是修正一些问题;
- Stage 4 - 完成(Finished):当规范的实现至少通过两个验收测试时,进入 Stage 4,会被包含在 ECMAScript 的下一个修订版中。
@babel/preset-env是 Babel 官方推出的插件预设,它可以根据开发者的配置按需加载对应的插件,通过@babel/preset-env可以根据代码执行平台环境和具体浏览器的版本来产出对应的 JavaScript 代码,例如可以设置代码执行在 Node.js 8.9 或者 iOS 12 版本。
#polyfill
polyfill:英文原意为一种用于衣物、床具等的聚酯填充材料,例如装修时候的腻子,作用是抹平坑坑洼洼的墙面;
在 JavaScript 中表示一些可以抹平浏览器实现差异的代码,比如某浏览器不支持 Promise,可以引入
es6-promise-polyfill等库来解决,而这往往可以交给babel来处理,参考:最佳实践
Babel 只负责进行语法转换,即将 ES6 语法转换成 ES5 语法,但是如果在 ES5 中,有些对象、方法实际在浏览器中可能是不支持的,例如:Promise、Array.prototype.includes,这时候就需要用@babel/polyfill来做模拟处理。
@babel/polyfill使用方法是先安装依赖,然后在对应的文件内显性的引入:
|
在文件内直接import或者require进来:
|
#runtime
@babel/polyfill虽然可以解决模拟浏览器不存在对象方法的事情,但是有以下两个问题:
- 直接修改内置的原型,造成全局污染;
- 无法按需引入,Webpack 打包时,会把所有的 Polyfill 都加载进来,导致产出文件过大。
为了解决这个问题,Babel 社区又提出了
@babel/runtime的方案,@babel/runtime不再修改原型,而是采用替换的方式,比如用 Promise,使用@babel/polyfill会产生一个window.Promise对象,而@babel/runtime则会生成一个_Promise(示例而已)来替换掉代码中用到的Promise。另外
@babel/runtime还支持按需引入。
下面以转换Object.assign为例,来看下@babel/runtime怎么使用。
- 安装依赖
@babel/runtime:npm i @babel/runtime; - 安装
npm i -D @babel/plugin-transform-runtime作为 Babel 插件; - 安装需要转换
Object.assign的插件:npm i -D @babel/plugin-transform-object-assign
编写一个runtime.js文件,内容如下:
|
执行npx babel runtime.js --plugins @babel/plugin-transform-runtime,@babel/plugin-transform-object-assign,最终的输出结果是:
|
代码中自动引入了@babel/runtime/helpers/extends这个模块(所以要添加@babel/runtime依赖啊)。
@babel/runtime也不是完美的解决方案,由于@babel/runtime不修改原型,所以类似[].includes()这类使用直接使用原型方法的语法是不能被转换的。
@babel/polyfill实际是core-js(opens new window)和regenerator-runtime(opens new window)的合集,所以如果要按需引入@babel/polyfill的某个模块,可以直接引入对应的 core-js 模块,但是手动引入的方式还是太费劲。
#@babel/preset-env
前面介绍了@babel/preset-env可以零配置的转化 ES6 代码,可以扩展添加配置。
在@babel/preset-env的选项中,useBuiltIns和target是最重要的两个,useBuiltIns用来设置浏览器 polyfill,target是为了目标浏览器或者对应的环境(browser/node)。
#useBuiltIns
前面介绍了@babel/polyfill和@babel/runtime两种方式来实现浏览器 polyfill,两种方式都比较繁琐,而且不够智能,可以使用@babel/preset-env的useBuildIn选项做 polyfill,这种方式简单而且智能。
useBuiltIns默认为 false,可以使用的值有 usage 和 entry:
|
usage表示明确使用到的 Polyfill 引用。在一些 ES2015+ 语法不支持的环境下,每个需要用到 Polyfill 的引用时,会自动加上,例如:const p = new Promise();
[1, 2].includes(1);
'foobar'.includes('foo');一般情况下,个人建议直接使用
usage就满足日常开发了。需要提一下的是,polyfill 用到的
core-js是可以指定版本的,比如使用core-js@3,则首先安装依赖npm i -S core-js@3,然后在 Babel 配置文件.babelrc中写上版本。//.babelrc
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}使用
useBuiltIns='usage'编译之后,上面代码变成,真正的做到了按需加载,而且类似[].includes()这类直接使用原型方法的语法是能被转换的:"use strict";
require("core-js/modules/es.object.to-string.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.array.includes.js");
require("core-js/modules/es.string.includes.js");
var p = new Promise();
[1, 2].includes(1);
'foobar'.includes('foo');entry表示替换import "@babel/polyfill";新版本的 Babel,会提示直接引入
core-js或者regenerator-runtime/runtime来代替@babel/polyfill的全局声明根据
targets中浏览器版本的支持,将 polyfill 拆分引入,仅引入有浏览器不支持的 polyfill。
结论:
所以,entry 相对usage使用起来相对麻烦一些,首先需要手动显性的引入@babel/polyfill,而且根据配置targets来确定输出,这样会导致代码实际用不到的 polyfill 也会被打包到输出文件,导致文件比较大。
#target
假设希望代码中使用 ES6 的模板字面量语法,但是实际执行代码的宿主浏览器是 IE 10 却不支持,那么可以使用target指定目标浏览器了。
|
如果代码是在 Node.js 环境执行的,则可以指定 Node.js 的版本号:
|
targets.browsers需要使用 browserslist 的配置方法,但是其设置会被targets.[chrome, opera, edge, firefox, safari, ie, ios, android, node, electron]覆盖;targets.node设置为true或"current"可以根据当前 Node.js 版本进行动态转换,也可以设置为具体的数字表示需要支持的最低 Node.js 版本;"node": "current"
// 等价于
// "node": process.versions.nodetargets.esmodules设置使用 ES Modules 语法,最新浏览器支持,这个在后面 Webpack 插件章节会详细介绍如何实现 Modern Mode。
browserslist 介绍和配置规则本文最后部分会介绍。
#@babel/plugin-transform-runtime
作用:可以重用 Babel 注入的辅助代码以节省代码大小。举例参考:最佳实践
Babel 使用非常小的辅助函数来实现诸如_extend,默认情况下,这将添加到需要它的每个文件中。这种重复有时是不必要的,尤其是当您的应用程序分布在多个文件中时。这就是@babel/plugin-transform-runtime插件的用武之地:所有帮助程序都将引用该模块@babel/runtime以避免在编译输出中出现重复,运行时将被编译到您的构建中。
如果您直接导入core-js (opens new window)或@babel/polyfill (opens new window)以及它提供的内置函数 (opens new window),例如Promise,Set和Map,则会污染全局范围。
配置说明:
|
transform-runtime转换插件做了三两件事:
@babel/runtime/regenerator使用generators/async函数时自动需要(可使用regenerator选项切换);- 必要时可以使用
core-js的帮助器,而不是假设它将由用户进行polyfilled(可通过corejs选项进行切换); - 自动删除内联Babel帮助器,并使用模块
@babel/runtime/helpers代替(可通过helpers选项切换)。
基本上,你可以使用Promise、Set、Symbol等内置插件,也可以无缝地使用所有需要polyfill的Babel功能,没有全局污染,使其非常适用于js库。
请确保你将@babel/runtime作为依赖项。
#Webpack 中使用 Babel
下面在 webpack 中使用 Babel 就变得很简单了,借助loaders(模块解析器):
步骤:
首先安装 npm 依赖,然后修改
webpack.config.js。安装依赖包:
# 安装开发依赖
npm i webpack@5 babel-loader webpack-cli@4 @babel/core @babel/preset-env @babel/plugin-transform-runtime core-js@3 -D
# 将 runtime 作为依赖
npm i @babel/runtime -S创建
webpack.config.js文件,内容如下:// webpack.config.js
module.exports = {
entry: './babel.js',
mode: 'development',
devtool: false,
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
"corejs": 3
},
],
],
},
},
],
},
],
},
};上面的
webpack.config.js文件直接将 Babel 的配置写到了options中,还可以在项目根目录下创建.babelrc或者使用package.json的 babel 字段。➜ npx webpack
asset main.js 260 KiB [emitted] (name: main)
runtime modules 1.47 KiB 6 modules
modules by path ./node_modules/core-js/internals/*.js 73.5 KiB 98 modules
modules by path ./node_modules/core-js/modules/*.js 56.4 KiB 32 modules
./babel.js 99 bytes [built] [code generated]
webpack 5.48.0 compiled successfully in 1829 ms
#Babel 原理
Babel 是一个 JavaScript 的静态分析编译器,所谓静态分析指的是在不需要执行代码的前提下对代码进行分析和处理的过程(执行时进行代码分析叫动态分析)。要实现 Babel 从一个语法转换成另外一个语法,需要经过三个主要步骤:解析(Parse),转换(Transform),生成(Generate)。
- 解析:指的是首先将代码经过词法解析和语法解析,最终生成一颗 AST(抽象语法树),在 Babel 中,语法解析器是
Babylon(opens new window); - 转换:得到 AST 之后,可以对其进行遍历,在此过程中对节点进行添加、更新及移除等操作,Babel 中 AST 遍历工具是
@babel/traverse(opens new window) - 生成:经过一系列转换之后得到的一颗新树,要将树转换成代码,就是生成的过程,Babel 用到的是
@babel/generator(opens new window)

Babel 的语法解析器 Babylon 目前已经放到 @babel/parser (opens new window)维护,除了 Babylon, JavaScript 解析器比较著名的还有acorn (opens new window)、Esprima (opens new window)。
#AST
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。
它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。
比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于
if-condition-then这样的条件跳转语句,可以使用带有两个分支的节点来表示。 ——维基百科
Babylon 解析 JavaScript 得到的 AST 是符合 ESTree (opens new window)。AST 是经过词法解析和语法解析两个步骤解析出来,组织成与程序结构对应的树状结构表示。
例如下面的代码,可以用下图中的树来表示:
|

也可以使用 JavaScript 对象来表示,例如下面代码(放入source.js文件):
|
对应的 JavaScript 对象为:
|
在上面的对象中,AST 的每一层都有相同结构的树分支。
Tips:AST Explorer (opens new window)可以在线解析 JavaScript 代码的 AST 结构,还可以在线编写转换函数,学习 AST 的好帮手。
现在开始动手使用 @babel/parse来生成下 AST,将square函数代码放入source.js,然后新建一个 js 文件(index.js),内容如下:
|
然后执行node index.js就可以看到对应的 JavaScript 对象表示的 AST 结构了。
|
更多
@babel/core方法parse的介绍在这里:https://babeljs.io/docs/en/babel-core#parse(opens new window)
在 Babel 中,除了基本的 JavaScript 语法,还支持扩展支持了很多其他的语法,例如 jsx (opens new window)、flow (opens new window)等。
#遍历
如果想处理 AST 那么就需要进行树的遍历,学过算法的应该知道树的遍历包括深度优先和广度优先。
这里 Babel 提供了@babel/traverse (opens new window),可以直接来遍历,不需要手动来写遍历代码。
按照上一小节的代码,继续修改index.js文件:
|
执行上面代码,最终得到一个类似 Html 结构的树形结构:
|
遍历的时候,进入(Enter)某个节点时会调用对应的enter函数,当退出(Exit)某个节点时,会调用exit函数。
当谈及“进入”一个节点,实际上是说在访问它们, 之所以使用这样的术语是因为有一个访问者模式(Visitor)的概念。
还可以针对某个类型的节点进行遍历,如下面代码:
|
每次进入和退出函数都会接收一个path的参数,path 是表示两个节点之间连接的对象。例如,如果有下面这样一个节点及其子节点︰
|
将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:
|
path.parent为当前节点的父节点信息,path.node则是当前节点的信息。通过操作path对象就可以对 AST 产生影响(对象引用类型)。
#生成
生成是使用了@babel/generator,比较简单,直接看下面例子:
|
#Babel 插件编写
在 Babel 中,代码会由 Babel 先行解析成 AST,Babel 插件做的事情就是写vistor而已,所以 Babel 插件固定的模板如下:
|
Tips:如果想继续深入了解 Babel 的原理和插件编写相关知识,可以查看《Babel 插件手册 (opens new window)》这个 Github 项目。
#Browserslist
实际开发项目中,肯定知道自己的项目运行在什么浏览器内,比如做移动开发,不可能需要兼容 IE10 以下的浏览器,所以如果做了很多兼容 IE10 以下浏览器的工作,那么就是无用功。
通过设置目标浏览器,可以让的代码更有针对性的输出兼容性代码(包括 CSS 前缀、JS 的 Polyfill 等),而不是无脑的全部兼容。
Browserslist (opens new window)就是帮助来设置目标浏览器的工具,Browserslist 被广泛的应用到 Babel、postcss-preset-env、autoprefixer 等开发工具上。
browserslist 实际上就是声明了一段浏览器的集合,的工具可以根据这段集合描述,针对性的输出兼容性代码。
#配置
Browserslist 的配置可以放在 package.json 中,也可以单独放在配置文件.browserslistrc中。所有的工具都会主动查找 browserslist 的配置文件,根据 browserslist 配置找出对应的目标浏览器集合。
在package.json 中的配置是增加一个browserslist数组属性:
|
或者在项目的根目录下创建一个.browerslistrc文件:
|
#常见集合范围说明
| 范围 | 说明 |
|---|---|
last 2 versions |
caniuse.com (opens new window)网站跟踪的最新两个版本,假如 iOS 12 是最新版本,那么向后兼容两个版本就是 iOS 11 和 iOS 12 |
> 1% |
全球超过 1%人使用的浏览器,类似> 5% in US则指代美国 5%以上用户 |
cover 99.5% |
覆盖 99.5%主流浏览器 |
chrome > 50 ie 6-8 |
指定某个浏览器版本范围 |
unreleased versions |
说有浏览器的 beta 版本 |
not ie < 11 |
排除 ie11 以下版本不兼容 |
since 2013 last 2 years |
某时间范围发布的所有浏览器版本 |
maintained node versions |
所有被 node 基金会维护的 node 版本 |
current node |
当前环境的 node 版本 |
dead |
通过last 2 versions筛选的浏览器中,全球使用率低于0.5%且官方声明不在维护或者事实上已经两年没有再更新的版本 |
| defaults | 默认配置,> 0.5% last 2 versions Firefox ESR not dead |
#浏览器名称列表(大小写不敏感)
Android:安卓 webview 浏览器;Baidu: 百度浏览器;BlackBerry/bb:黑莓浏览器;Chrome:chrome 浏览器;ChromeAndroid/and_chr:chrome 安卓移动浏览器;Edge:微软 Edge 浏览器;Electron:Electron;Explorer/ie:ie 浏览器;ExplorerMobile/ie_mob:ie 移动浏览器;Firefox/ff:火狐浏览器; *FirefoxAndroid/and_ff:火狐安卓浏览器;iOS/ios_saf:iOS Safari 浏览器;Node:nodejs;Opera:opera 浏览器;OperaMini/op_mini:operaMini 浏览器;OperaMobile/op_mob:opera 移动浏览器;QQAndroid/and_qq:QQ 安卓浏览器;Samsung:三星浏览器;Safari:桌面版本 Safari;UCAndroid/and_uc:UC 安卓浏览器
整个目标浏览器的集合是取并集,即满足上面的全部条件。
#Browserslist 的环境变量
还可以为不同的环境配置不同的目标浏览器。通过设置BROWSERSLIST_ENV 或者 NODE_ENV可以配置不同的环境变量。默认情况下会优先从 production 对应的配置项加载。在配置文件中,可以通过设置对应的环境目标浏览器:
|
或者使用.browserslistrc:
|
Browserslist 配置不仅仅 Babel 会用到,其他编译工具也可能会用到,例如后面章节介绍 Webpack CSS 相关配置使用到 postcss 的
autoprefixer插件也会用到。
#Babel Polyfill 的最佳实践
通过上面的介绍,可能觉得useBuiltIns:'usage'可以完美的解决的 Polyfill 问题,它是按需引入模块,根据.browserslist+业务实际代码来设置引入 Polyfill,不会多余的引入。
但是在构建的时候发现实际还是有问题的:
|
根据上述的 useBuiltIns:'usage' 配置编译后:
|
通过上述的构建之后的代码,发现asyncGeneratorStep和 _asyncToGenerator 这两个函数是被内联进来,而不是 import 进来的。
如果这样的话,在多个文件中用到了async和await关键字,那么每个文件都会编译出一遍asyncGeneratorStep和 _asyncToGenerator函数。这样的代码明显是重复了,再解决了这个问题,Babel polyfill 的方案就完美了,要解决这个问题,需要用到@babel/plugin-transform-runtime这个 Babel 插件。
知道 Babel 在每个需要转换的代码前面都会插入一些helpers代码,这可能会导致多个文件都会有重复的 helpers 代码。
@babel/plugin-transform-runtime 的 helpers 选项就可以把这些代码抽离出来。
所以,Babel 的 Polyfill 的最佳实践是如下的 Babel 配置:
|
npx babel babel.js之后的代码:
|
#模块化
#模块化规范
三大 JavaScript 主流模块规范:CommonJS、AMD 和 ES6 Module。
#CommonJS
CommonJS (opens new window)规范是 2009 年一名来自 Mozilla 团队的工程师 Kevin Dangoor 开始设计一个叫 ServerJS 的项目提出来的,随着 Node.js 的广泛应用,被广泛接受。通过 ServerJS 这个名字就可以知道,CommonJS 主要是服务端用的模块规范。
|
上面的代码就是 CommonJS 语法,使用了require来导入一个模块,module.exports导出模块。在 Node.js 中实际代码 (opens new window)会被处理成下面代码而被应用:
|
CommonJS 的问题:Commonjs规范是不适合浏览器的,但由于有了打包工具的支撑,通过处理的Commonjs也可以在浏览器中使用。
CommonJS 规范是 JavaScript 中最常见的模块格式规范,这一标准的设计初衷是为了让 JavaScript 在多个环境下都实现模块化。
起先主要应用在 Node.js 服务端中,但是 Node.js 中的实现依赖了 Node.js 本身功能的实现,包括了 Node.js 的文件系统等,这个规范在浏览器环境是没法使用的。
后来随着 Browserify (opens new window)和 Webpack 等打包工具的崛起,通过处理的 CommonJS 前端代码也可以在浏览器中使用。
#AMD
AMD 规范 (opens new window)是在 CommonJS 规范之后推出的一个解决 web 页面动态异步加载 JavaScript 的规范,相对于 CommonJS 规范,它最大特点是浏览器内支持、实现简单、并且支持异步加载,AMD 规范最早是随着RequireJS (opens new window)发展而提出来的,它最核心的是两个方法:
require():引入其他模块;define():定义新的模块。
基本语法如下:
|
AMD 提出来之后,也有很多变种的规范提出来,比如国内 Sea.js (opens new window)的 CMD (opens new window),还有兼容 CommonJS 和 AMD 的 UMD 规范 (opens new window)(Universal Module Definition)。虽然 AMD 的模式很适合浏览器端的开发,但是随着 npm 包管理的机制越来越流行,这种方式可能会逐步的被淘汰掉。
AMD 规范的问题:依赖前置加大了开发的难度,无论在阅读代码还是编写代码的时,都会导致引入的模块内容是条件性执行的。
在 AMD 规范中,要声明一个模块,那么需要指定该模块用到的所有依赖项,这些依赖项会被当做形参传到
factory(define方法传入的函数叫做factory)中,对于依赖的模块会提前执行,这种做法叫做 依赖前置。依赖前置加大了开发的难度,无论在阅读代码还是编写代码的时,都会导致引入的模块内容是条件性执行的。而且不管 AMD 还是 CommonJS 都没有统一浏览器和客户端的模块化规范。
#ES6 Module
ES6 Module,又称为 ES2015 modules,是 ES2015 标准提出来的一种模块加载方式,也是 ECMAScript 官方提出的方案,作为 ES 标准,不仅仅在 Web 现代浏览器(例如 Chrome)上面得到实现,而且在 Node.js 9+ 版本也得到原生支持(需要加上--experimental-modules使用)。
|
对于前端项目,可以通过 Babel 或者 Typescript 进行提前体验。
随着主流浏览器逐步开始支持 ES Modules(ESM) 标准,越来越多的目光投注于 Node.js 对于 ESM 的支持实现上。
目前 Node.js 使用 CommonJS 作为官方的模块解决方案,虽然内置的模块方案促进了 Node.js 的流行,但是也为引入新的 ES Modules 造成了一定的阻碍。不过 Node.js 9.0+ 已经支持 ESM 语法,看示例:
|
需要添加 flag --experimental-modules 来启动 ESM 语法支持,文件则必须使用 .mjs 后缀: node --experimental-modules some-esm-file.mjs 。

#Webpack工作原理简析
在 Web 前端,不仅仅只有 JavaScript,还有 CSS、HTML、图片、字体、富媒体等众多资源,还有一些资源是以类似「方言」的方式存在着,比如 less、sass、各种 js 模板库等,这些资源并不能被直接用在 JavaScript 中,如果在 JavaScript 中像使用模块一样使用,那么可以极大的提高的开发体验:
|
这时候,就需要 Webpack 了!在 Webpack 中,一切皆模块!
webpack的编译逻辑:
- Webpack 会要对整个代码进行静态分析,分析出各个模块的类型和它们依赖关系
- 将不同类型的模块提交给对应的加载器(loader)来处理。
比如:一个用 Less 写的样式,可以先用 less-loader 将它转成一个 CSS 模块,然后再通过 css-loader 把他插入到页面的 <style> 标签中执行,甚至还可以通过插件将这部分 CSS 导出为 CSS 文件,使用link标签引入到页面中。
#webpack对Module 的增强
在 Webpack 中,不仅可以为所欲为的使用 CommonJS 、AMD 和 ES6 Module 三大规范(比如一个文件中混合使用三种规范),还可以使用 Webpack 对 Module 的增强方法和属性。
#import()和神奇注释
在 Webpack 中,import不仅仅是 ES6 Module 的模块导入方式,还是一个类似require的函数(其实这是 ES2015 loader 规范 (opens new window)的实现),可以通过import('path/to/module')的方式引入一个模块,import()返回的是一个Promise对象:
|
参考先前的一章,来创建基础的ES6的webpack项目。
下面看看使用import()和 import 的打包有什么区别:
|
执行下命令:
|

通过打包后的 log 和dist文件夹内容发现,的代码被分割成了两个文件,一个是main.js一个是lazy_js.js,这是因为相对于import from的静态分析打包语法,import()是动态打包语法,即的内容不是第一时间打进main.js的,而是通过异步的方式加载进来的。代码分割是 webpack 进行代码结构组织,实现动态优化的一个重要功能
与
import()用法一样的是require.ensure的方法,这个方法已经被import()方式替换掉;针对
import()打包产物跟普通的静态分析打包的实现不同之处,后面原理篇讲解打包产出物的时候会详细介绍。
下面再来看下import()的神奇注释特性,上面index.js的代码修改成下面这样,增加注释webpackChunkName: 'lazy-name'
|
则打包后的结果,lasy_js.js变成了toimc-lazy.js了,这个文件的名字就是在import()注释里面指定的webpackChunkName,这就是神奇注释(Magic Comments)。

目前支持的注释有:
webpackInclude:如果是 import 的一个目录,则可以指定需要引入的文件特性,例如只加载 json 文件:/\.json$/;webpackExclude:如果是 import 的一个目录,则可以指定需要过滤的文件,例如/\.noimport\.json$/;webpackChunkName:这是 chunk 文件的名称,例如 toimc-lazy;webpackPrefetch: 是否预取模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;webpackPreload: 是否预加载模块,及其优先级,可选值true、或者整数优先级别,0 相当于 true,webpack 4.6+支持;webpackMode: 可选值lazy/lazy-once/eager/weak。
|
这里最复杂的是webpackMode:
lazy:是默认的模式,为每个import()导入的模块,生成一个可延迟加载 chunk;lazy-once:生成一个可以满足所有import()调用的单个可延迟加载 chunk,此 chunk 将在第一次 import() 调用时获取,随后的 import() 调用将使用相同的网络响应;注意,这种模式仅在部分动态语句中有意义,例如 import(./locales/${language}.json),其中可能含有多个被请求的模块路径;eager:不会生成额外的 chunk,所有模块都被当前 chunk 引入,并且没有额外的网络请求。仍然会返回 Promise,但是是 resolved 状态。和静态导入相对比,在调用import()完成之前,该模块不会被执行。weak:尝试加载模块,如果该模块函数已经以其他方式加载(即,另一个 chunk 导入过此模块,或包含模块的脚本被加载)。仍然会返回 Promise,但是只有在客户端上已经有该 chunk 时才成功解析。如果该模块不可用,Promise 将会是 rejected 状态,并且网络请求永远不会执行。当需要的 chunks 始终在(嵌入在页面中的)初始请求中手动提供,而不是在应用程序导航在最初没有提供的模块导入的情况触发,这对于 Server 端渲染(SSR,Server-Side Render)是非常有用的。
通过上面的神奇注释,import()不再是简单的 JavaScript 异步加载器,还是任意模块资源的加载器,举例说明:如果页面用到的图片都放在src/assets/img文件夹下,你们可以通过下面方式将用到的图片打包到一起:
|
prefetch 优先级低于 preload,preload 会并行或者加载完主文件之后立即加载;prefetch 则会在主文件之后、空闲时在加载。prefetch 和 preload 可以用于提前加载图片、样式等资源的功能。
#require.resolve() 和require.resolveWeak()
require.resolve() 和 require.resolveWeak()都可以获取模块的唯一 ID(moduleId),区别在于require.resolve()会把模块真实引入进 bundle,而require.resolveWeak()则不会,配合require.cache和 __webpack_modules__可以用于判断模块是否加载成功或者是否可用。
|
#require.context()
require.context(directory, includeSubdirs, filter)可以批量将directory内的文件全部引入进文件(这个在视频中有介绍到),并且返回一个具有resolve的 context 对象,使用context.resolve(moduleId)则返回对应的模块。
- directory:目录 string;
- includeSubdirs:是否包含子目录,可选,默认值是 true;
- filter:过滤正则规则,可选项。
Tips:注意
require.context()会将所有的文件都引入进 bundle!
#require.include()
require.include()已被废弃,不久将被删除。
require.include(dependency)顾名思义为引入某个依赖,但是并不执行它,可以用于优化 chunk,例如下面示例代码:
|
#资源的模块化处理
Webpack 还对一些常用的 Node.js 模块和属性进行了 mock,例如:在 web 的 js 文件中可以直接引入 Node.js 的querystring模块,这个模块实际引入的是node-libs-browser (opens new window)来对 Node.js 核心库 polyfill,详细在 web 页面中可以用的 Node.js 模块,可以参考node-libs-browser (opens new window)Readme 文件的表格。
#@import和import
在 Webpack 中的 css (包括其预处理语言,例如 Less、Sass)等,都可以在内部通过@import语法直接引入使用:
|
除了样式文件中的@import,在 JavaScript 文件中,还支持直接使用 ES6 Module 的import(包括 require)直接引入样式文件,例如:
|
JavaScript 的这种语法其实是 CSS Modules 语法 (opens new window),目前浏览器支持程度有限,但是在 Webpack 中,可以通过配置 loader 优先使用这种方式!
后面讲解 CSS 样式配置的时候会更加详细的讲解 CSS Modules
#把资源作为模块引入
在 Webpack 中,除了 CSS 可以直接使用import语法引入,类似 HTML 和页面模板等,可以直接使用对应的 loader ,通过下面的语法来引入:
|
上面的代码得到html变量实际为引入的loader.html的 string 片段。除了 html,类似模板文件,还可以直接转换为对应的 render 函数,例如下面代码使用了ejs-loader (opens new window):
|
原理篇将动手手写一个
markdown-loader,更深入的了解其功能和原理实现。
后续的一章,将会介绍webpack中重要的loaders处理,webpack借助loaders来处理各式各样的资源。
#TS&JS处理
#TypeScript
TypeScript (opens new window)是微软公司提出来的一个 JavaScript 的超集语言,主要作用是为 JavaScript 增加静态类型检测系统和 ECMAScript 语法的扩展。
#tsconfig.json
TypeScript 代码不能直接在浏览器执行,所以需要编译器将 TypeScript 文件转换成可以在浏览器执行的 JavaScript。
NPM 安装编译器方法:
|
这时候新建一个 ts 文件(TypeScript 文件扩展名是ts):
|
然后使用 tsc hello.tc,执行编译后hello.ts被编译成了hello.js:
|
为了方便编译器和编辑器识别 TypeScript 项目,TypeScript 约定了tsconfig.json文件来存储项目配置,如果运行 tsc 时不指定输入文件,编译器则会查找项目目录中的这个文件,如果找不到则会依次向父级目录查找。比如这样:
|
关于tsconfig.json更多配置,可以继续浏览:
#ts-loader
如果要让 Webpack 来识别ts文件,需要配合 TypeScript 的 loader,使用ts-loader可以方便地构建浏览器可以运行的 JS 代码。
安装ts-loader的命令为:npm i ts-loader --save-dev,
然后配置项目目录中的 webpack.config.js:
|
为了方便配置 TypeScript 项目,还可以在 Webpack 的项目根目录创建一个tsconfig.json文件。
#CSS处理
#样式处理
Webpack处理css,需要借助:
- css-loader:让webpack认识css文件;
- style-loader:把webpack处理完的css加载到html中,相当于是
<style>引入; - Less-loader,scss-loader等:把scss,less这些语言转化为css;
所以,这三者的使用顺序是:style-loader, css-loader, scss-loader,webpack中是顺序使用,逆序书写。
#css-loader
首先添加 css-loader:
|
然后给webpack.config.js添加rule:
|
这时候修改app.js 添加下面代码:
|
效果如下: 
这时候 CSS 会被转成字符串, JS 就可以直接使用。
除了上面直接在webpack.config.js中添加rule,还可以在 JavaScript 中直接使用下面的方式引入:
|
上面代码中
import css from 'css-loader!./css/index.css'是 webpack loader 的内联写法。
#style-loader
有了 css-loader 可以识别 CSS 语法了,下面就需要 style-loader 出场了。简单来说,style-loader 是将 css-loader 打包好的 CSS 代码以<style>标签的形式插入到 HTML 文件中,所以style-loader是和css-loader成对出现的,并且style-loader是在css-loader之后。首先安装style-loader:
|
#预处理器loader
由于 CSS 标准自诞生以来,一直致力于在表现力层面的发展,相对基本语法和核心机制并没有实质性的变化,所以产生了好多 CSS 的预处理器。预处理器补足了 CSS 的一些语法上的缺陷,支持变量、运算、函数、作用域、继承、嵌套写法等,使用 CSS 预处理器可以大大的提升开发效率和体验,同时能够更好的做好模块化开发。
Tips:CSS 核心语法直到近些年才有大的发展,比如自定义属性(custom properties,又称为变量 variables) 、嵌套写法,但是已经远远的落后于 CSS 预处理器的发展。
常见的 CSS 预处理器有:Less (opens new window),Sass 及其语法变种 Scss (opens new window)和Stylus (opens new window)。
下面以 Less 预处理器为例,介绍 CSS 预处理器的用法。首先安装对应的 loader:less-loader (opens new window):
|
然后修改webpack.config.js:
|
less-loader只是将 Less 语法编译成 CSS,后续还需要使用css-loader和style-loader处理才可以,所以一般来说需要配合使用:
|
注意一些预处理语言需要安装对应的解析器,例如 sass-loader,需要同时安装 node-sass:
npm install sass-loader node-sass --save-dev
#扩展1:mini-css-extract-plugin
CSS 作为<style>标签放到 HTML 内还是不够的,还需要将 CSS 以<link>的方式通过 URL 的方式引入进来,这时候就需要使用mini-css-extract-plugin (opens new window)这个插件了,首先安装它:
|
mini-css-extract-plugin这个使用的时候需要分别配置 loader 和 plugin,loader 需要放在css-loader之后代替style-loader:
|
#扩展2:CSS Modules(React风格)
CSS Modules 指的是所有的 CSS 类名及其动画名都只是局部作用域的 CSS 文件。
CSS Modules 既不是官方标准,也不是浏览器的特性,而是在构建过程中对 CSS 类名选择器限定作用域的一种方式,如的广告样式、某个 UI 通用弹层 SDK 这类样式,都需要避免自己的命名跟宿主环境的样式冲突或者避免被 AdBlock (opens new window)这类广告拦截器拦截掉。
CSS Modules 主要解决的问题有:
- 解决 CSS 类都是全局的,容易造成全局污染(样式冲突);
- JS 和 CSS 共享类名;
- 可以方便的编写出更加健壮和扩展方便的 CSS。
这类 CSS 模块化的解决方案很早之前前端社区就有一些讨论和方案,比如最早的通过 CSS 命名约定的BEM (opens new window)、OOCSS (opens new window)等,再到 React 中使用的用 JavaScript 来写 CSS 规则的 CSS in JS (opens new window)方案,再到通过编译工具来帮助 JavaScript 可以使用 CSS 的 CSS Modules 方案。
下面来看下 CSS Modules 究竟是什么,来看下代码表现,首先创建一个app.css文件,内容如下:
|
知道了,在 JS 中可以直接import一个 CSS 文件:
|
那么 CSS Modules 中,JS 可以直接使用 CSS 的类名作为对象值,例如下面代码:
|
在 css-loader 增加modules的选项,说明打开 CSS Modules 支持。
|
跟 CSS Modules 相关的配置还有很多,具体可以在css-loader对应的文档 (opens new window)找到。
#CSS 后处理器—PostCSS
后处理器 PostCSS(另外也有称 PostCSS也为另外一种 CSS 预处理器的),不过 PostCSS的出现的确解决了很多问题,比如:
类似不同浏览器前缀的写法,只需要使用引入一个名字叫Autoprefixer (opens new window)的 PostCSS插件就可以使用标准的语法,在构建的过程中,PostCSS会根据适配的浏览器使用 Autoprefixer 插件自动添加不同浏览器的适配。
|
PostCSS是一个使用 JavaScript 插件来转换 CSS 的工具,PostCSS核心是将 CSS 解析成 AST,然后通过各种插件做各种转换,最终生成处理后的新 CSS,跟 Babel 在功能和实现上都类似,这里就不再详细讲解实现原理了。在语法转换上还有一个开源项目cssnext (opens new window),使用最新的 CSS 标准来写 CSS,通过 cssnext 可以转换成对应的 CSS 版本。
#postcss-loader
使用 PostCSS需要安装postcss-loader (opens new window),然后按照 loader 顺序,在 css-loader 之前(注意 loader 顺序:从右到左,从后到前)加上 postcss-loader:
|
如果有 CSS 预处理语言,则配置写法:
|
#PostCSS配置
通过 PostCSS的强大插件系统,不仅可以处理 CSS 语法,还可以处理 CSS 预处理器的语法,实现的功能也有很多,包括:
- 添加前缀
- 最新语法转义
- 压缩
- 扩展 CSS 的语言特性。
配置了 postcss-loader 之后,WebPack 就可以使用 PostCSS来处理 CSS了。
但是 PostCSS本身只不过是将 CSS 解析成 AST ,真正起作用的还需要依赖其强大的插件系统。所以,PostCSS配置其实主要是配置其使用哪些插件,PostCSS的配置写法有以下三种方式:
- 通过配置文件
postcss.config.js,一般放置在项目的根目录下; - 通过 loader 的配置项
options; - 直接在 package.json 中添加个
postcss属性。
#postcss.config.js
postcss.config.js完全是按 Node.js 模块写法来写,使用什么插件就引入什么插件依赖:
|
#loader 配置项 options
在webpack.config.js中,直接配置了postcss-loader之后,然后通过loader的options可以配置 PostCSS的参数。
|
#package.json 中添加个postcss属性
最后一种方式是在package.json文件中添加postcss属性,这种方式受限于 json 的语法,可扩展性较弱,一般不推荐!
|
下面介绍几个项目中可能用到的 PostCSS插件,带大家学习下 PostCSS插件的基本用法。
#Autoprefixer
Autoprefixer这个插件前面内容已经简单提到过,就是给 css 补齐各种浏览器私有的前缀,例如-webkit、-moz、-ms等,当然还会处理各种兼容性问题,比如 flex 语法,不能简单添加-webkit就解决,还需要处理成-webkit-box这类老版本的标准。
Autoprefixer 还支持各种 IDE 插件,可以在 IDE 中直接转换对应的 css 文件(不推荐这样用,多人合作项目跟进 IDE 配置不同,转换的文件也会存在差异)。
Autoprefixer 的主要参数就是 browserslist,即需要代码支持的浏览器列表,这部分内容在 babel 章节已经介绍过了。其他相关的参数说明可以在文档中找到:https://github.com/postcss/autoprefixer#options。
#雪碧图
CSS 使用小图标图片的时候,经常做的优化项目是将小图标的图片合并成雪碧图 (opens new window)(CSS Sprite),雪碧图的好处是将页面用到的小图片合并到一张大图中,然后使用background-position重新定位,这样节省了 HTTP 的请求数。
在 Webpack 中可以借助 PostCSS 来给图片做雪碧图,经过简单的配置之后,生成雪碧图就是全自动的过程了。下面来看看怎么操作。
首先安装 postcss-sprites(opens new window)
|
然后修改 PostCSS 的postcss.config.js,增加插件的调用:
|
然后修改webpack.config.js在css-loader之前配置上postcss-loader(注意 loader 加载顺序,从后往前):
|
好了,下面的 CSS 中使用了spritePath: './src/assets/img/'路径的图片就会被处理了,例如下面的文件:
|
经过打包之后,输出 log 如下,可见生成了一个新的图片文件99b0de3534d3e852ea4ce83b15cbad60.png:
#postcss-preset-env
postcss-preset-env (opens new window)是跟 babel 的 preset-env 类似的功能,通过它可以安心的使用最新的 CSS 语法来写样式,不用关心浏览器兼容性,浏览器兼容的问题交给了 postcss-preset-env 和 WebPack,在打包构建的时候,会根据不同的配置输出对应支持的 CSS 文件。
postcss-preset-env 支持的 CSS 标准,完全可以媲美 CSS 预处理器的功能,所以如果对 cssnext 新的标准比较熟悉,可以直接用新标准来写样式,这样等到浏览器支持新标准之后可以无缝切换到 cssnext 语法,那么可以直接抛弃 CSS 预处理器,直接使用 cssnext 语法来写样式,通过 WebPack 和 postcss-preset-env 来构建。
#PreCSS
如果厌倦 cssnext 的变量定义方式,想使用 Sass 的语法,而又不想引入 Sass 这个 CSS 预处理器,PreCSS (opens new window)就是你的选择。使用 PreCSS,可以写类 Sass 和 cssnext 语法的 CSS,详细可以参考它的文档 (opens new window)。
#cssnano
cssnano (opens new window)是一个强大的 PostCSS插件,在 CSS 压缩优化中会经常被用到,它有别于常规的 CSS 压缩工具只是去除空格注释,还支持根据 CSS 语法解析结果智能压缩代码,比如合并一些类写法:
|
经过 cssnano 处理之后的 CSS 文件,会合并压缩一些类,缩短一些常见的值,例如颜色值等:
|
cssnano 的配置会在 WebPack 优化章节继续详细介绍。
#理解 css-loader 的 importLoaders 参数
在 css-loader 的文档中,有个比较引起疑惑的参数项:importLoaders,这个参数用于配置 css-loader 作用于 @import的资源之前有多少个 loader。
示例代码如下:
|
通过示例并不能看出来配置importLoaders是否对项目打包有什么差异,下面通过实例代码来看下加上importLoaders和没添加有什么区别。
首先创建两个文件:style.css和body.css,style.css中通过@import 'body.css';引入body.css:
|
在这两个文件中,分别添加了两个特殊属性的 CSS 值:display: flex;,目的是使用autoprefixer对其进行处理,如果 postcss-loader 都起作用,则 display: flex;都会被处理添加对应的浏览器前缀,如果importLoaders设置不同,则根据文档输出的 CSS 会有差异,具体的差异就是需要理解的地方。
第二步,创建 entry 文件import-loader.js和 WebPack 配置文件webpack.config.importLoader.js,在 WebPack 配置文件中,一个 css-loader 没有使用importLoaders,一个使用了importLoaders=1,内容如下(为了方便查看 CSS 的差异,这里使用了mini-css-extract-plugin (opens new window)直接打包出两个 CSS 文件):
|
第三步创建 PostCSS配置文件,添加autoprefixer,增加一个 IE10 浏览器的配置:
|
都准备完毕了,下面执行命令webpack --config webpack.importLoader.js,打包后的文件内容如下:
|
通过观察打包出来的两个 CSS 文件发现,使用@import 'body.css'引入了body.css文件之后,body.css的 CSS 因为配置了不同的importLoaders所以表现不一样:
- 未使用
importLoaders:被styles.css引入的body.css内的display: flex;未添加了前缀,说明 PostCSS没有作用到@import引入的文件中; - 使用了
importLoaders=1:被styles.css引入的body.css内的display: flex;也被添加了前缀,说明 PostCSS作用到了被@import引入的文件中。
Tips:除了设置 css-loader 的
importLoaders,如果使用 PostCSS则可以使用它的插件:postcss-import (opens new window)同样可以处理@import引入的 CSS 代码:
|
下面大家测试一下这个案例:
|
#HTML处理
在项目中除了需要 JavaScript、CSS 和图片等静态资源,还需要页面来承载这些内容和页面结构,怎么在 Webpack 中处理 HTML。并且项目也不仅仅是单页应用(Single-Page Application,SPA),也可能是多页应用,所以还需要使用 Webpack 来给多页应用做打包。本小节将讲解这俩问题。
#html-webpack-plugin
有了 JavaScript 文件,还缺 HTML 页面,要让 Webpack 处理 HTML 页面需要只需要使用html-webpack-plugin (opens new window)插件即可,首先安装它:
|
然后修改对应的 webpack.config.js 内容:
|
只需要简单配置,执行webpack打包之后,发现 log 中显示,在dist文件夹中生成一个index.html的文件:

打开后发现 HTML 的内容如下:
|
除了 HTML 外,的 entry 也被主动插入到了页面中,这样打开index.html就直接加载了main.js了。
如果要修改 HTML 的title和名称,可以如下配置:
|
形成出来的foo.html的文件如下:
|
#Template
虽然可以简单的修改 Title 这里自定义内容,但是对于日常项目来说,这远远不够。
希望 HTML 页面需要根据的意愿来生成,也就是说内容是来定的,甚至根据打包的 entry 最后结果来定,这时候就需要使用html-webpack-plugin的template功能了。
比如我在index.js中,给id="app"的节点添加内容,这时候 HTML 的内容就需要自定义了,至少应该包含一个含有id="app"的 DIV 元素:
|
可以创建一个自己想要的 HTML 文件,比如index.html,在里面写上想要的内容:
|
把 webpack.config.js 更改如下:
|
这时候,打包之后的 HTML 内容就变成了:
|
也是添加上了main.js内容。
#使用JS模板引擎
HTML 毕竟还是有限,这时候还可以使用 JavaScript 模板引擎来创建html-webpack-plugin的 Template 文件。
下面我以pug (opens new window)模板引擎为例,来说明下怎么使用模板引擎文件的 Template。
首先创建个index.pug文件,内容如下:
|
如果不理解 Pug 模板的语法,可以简单看下文档,我这里简单解释下,首先在头部加了 title 和一个script标签,然后在 body 中内容为 h1、id="app"的 div 和引入(include)了一个 footer.pug的文件:
|
这时候需要修改 webpack.config.js 内容:
|
但是只修改template='src/index.pug'是不够的,因为.pug这样的文件 Webpack 是不会解析的,所以需要加上 Pug 的 loader:pug-html-loader (opens new window),除了这个插件还需要安装html-loader (opens new window)。首先通过npm i -D pug-html-loader html-loader安装它们,然后修改 webpack.config.js 内容,添加 rule:
|
最后,得到的index.html内容如下:
|
Pug 引擎被转换成 HTML代码,里面包含了:main.js和footer.pug的内容。
关于html-webpack-plugin (opens new window)的参数这里就不展开了,可以查阅它的 README 文档。
Tips:使用 JavaScript 模板引擎,还可以定义一些变量,通过 html-webpack-plugin 传入进去。
#多页项目配置
要做一个多页项目的配置,那么需要考虑以下几个问题:
- 多页应用,顾名思义最后打包生成的页面也是多个,即 HTML 是多个;
- 多页应用不仅仅是页面多个,入口文件也是多个;
- 多页应用可能页面之间页面结构是不同的,比如一个网站项目,典型的三个页面是:首页、列表页和详情页,肯定每个页面都不一样。
下面来一个一个的问题解决:
#多页面问题
多页面就是指的多个 HTML 页面,这时候可以直接借助 html-webpack-plugin 插件来实现,只需要多次实例化一个 html-webpack-plugin 的实例即可,例如:
下面是同一个 template,那么可以只修改filename输出不同名的 HTML 即可:
|
对于页面结构不同的 HTML 页面的配置,使用不同的 template 即可。
|
#多入口问题
上面的多页面解决是多次实例化 html-webpack-plugin,根据传入的参数不同(主要是 filename 不同),打包出两个文件,但是这两个文件的特点是引入的 JavaScript 文件都是一样的,即都是main.js。
对于多入口,并且入口需要区分的情况,那么需要怎么处理呢?
这时候就需要借助 html-webpack-plugin 的两个参数了:chunks和excludeChunks:
chunks是当前页面包含的 chunk 有哪些,可以直接用 entry 的key来命名- excludeChunks`则是排除某些 chunks。
例如,现在有两个 entry,分别是index.js和list.js,希望index.html跟index.js是一组,list.html跟list.js是一组,那么 webpack.config.js 需要修改为:
|
打包出来内容:

#最佳实践
现在说下我在项目中的一般做法,个人认为这是多页应用的最佳实践。
步骤与思路:
- 约定的目录结构(方便写逻辑代码);
- 使用js读取目录结构与文件;
- 创建entries数组;
- 创建HtmlWebPackPlugin数组;
- 最后拼接成webpack配置。
首先,需要规定下目录结构规范,一般项目会有下面的目录规范:
|
保证 template 和实际的 entry 是固定的目录,并且名字都是对应的。
这时候可以写个 Node.js 代码遍历对应的路径,然后生成 webpack.config.js 的 entry和html-webpack-plugin内容。
这里我使用了globby (opens new window)这个 NPM 模块,先写了读取src/pages/*.js的内容,然后生成entry:
注意:
globby这个库,目前12.x的版本是ES Module的版本,用法发生了重大变化。
|
而上面的代码,可以参考如下命令安装:
|
scripts/resolve.js文件:
|
scripts/utils.js文件:
|
在 webpack.config.js 用的时候,直接require引入刚刚写的这个文件,然后:
|
打包结果:

这里还有一份webpack4中尝试的项目:项目地址(opens new window)
#图片资源处理
#图片引入方式
图片是前端项目必不可少的静态资源,在日常开发中,可能会在下面三种情况使用图片:
- HTML 中通过
<img>标签等方式引入; - CSS 中通过
src等方式引入; - JavaScript 中使用图片的 URL 或者内容(比如 Canvas 等)。
最笨最直接的方式就是直接写死线上的地址,例如在页面中,引入<img>如下:
|
上面地址的http://s.bxstatic.com是一个 CDN (opens new window)静态域名,后面是完整的路径,这样上线的时候地址就可以直接使用,线下开发的时候可以提前将静态资源打包好上传到线上。这样操作很费劲,而且 CDN 每次静态资源更新都要需要刷新缓存,如果使用 MD5 命名图片的时候就更麻烦了。
在 Webpack 中,则可以使用 loader 的方式完成图片的引入。
例如在 CSS 文件中,直接相对路径使用背景图片:
|
在 HTML 中也可以直接使用相对路径:
|
问题:
- 怎么让 Webpack 识别图片,并且能够打包输出呢?
- 上面的路径不喜欢使用相对路径,怎么办?有没有使用alias的方式?
#url-loader
这时候就需要借助 loader 了,这里有两个 loader 可以使用:file-loader (opens new window)和url-loader (opens new window)。
file-loader和url-loader是经常在一些 Webpack 配置中看到的两个 loader,下面介绍下两者的区别:
file-loader:能够根据配置项复制使用到的资源(不局限于图片)到构建之后的文件夹,并且能够更改对应的链接;url-loader:包含 file-loader 的全部功能,并且能够根据配置将符合配置的文件转换成 Base64 方式引入,将小体积的图片 Base64 引入项目可以减少 http 请求,也是一个前端常用的优化方式。
下面以url-loader为例说明下 Webpack 中使用方法。
首先是安装对应的 loader:npm install -D url-loader。
下面创建一个项目,目录结构如下:
|
首先在index.css中引入 small.png,
|
然后在index.js中引入了index.css和large.png:
|
最后在index.html中通过<img>引入large.png:
|
这时候修改webpack.config.js:
|
执行webpack之后的 log:

这时候发现,打包出来的文件都比较大,通过查看内容发现,的图片被Base64处理了,然后直接引入了:

这是因为url-loader本身优先是将资源Base64引入的。虽然图片Base64可以减少 http 请求,但是对于1M+这么大的图片都Base64处理,范围增加了 CSS、JavaScript 等文件的大小,而且将这么大的Base64反解成可以使用的图片渲染出来,时间消耗也是很大的。
所以这时候需要使用url-loader的limit选项来控制不超过一定限制的图片才使用Base64:
|
这时候再执行webpack,发现多打出一个ad19429dc9b3ac2745c760bb1f460892.png的图片,这张图片就是large.png的图片,因为超过了limit=3*1024显示所以没有被处理成Base64。
继续查看index.html和main.js(index.js打包出来的文件),发现使用large.png的地址都被 Webpack 自动替换成了新的路径ad19429dc9b3ac2745c760bb1f460892.png。
#HTML 和 CSS 中使用 alias
前面提到过,除了使用相对路径的方式引入静态资源,还可以使用别名(alias)的方式,url-loader也会给处理这种情况的引用。
修改index.html和index.css:
|
然后修改webpack.config.js增加resolve.alias:
|
这时候执行webpack发现报错了:
|
这是因为在 HTML 和 CSS 使用alias必须要前面添加~,即:
|
修改完后,直接执行webpack既可以看到正确的结果了。
Tips:HTML 中使用
<img>引入图片等静态资源的时候,需要添加html-loader配置,不然也不会处理静态资源的路径问题。
svg-url-loader (opens new window)的工作原理类似于 url-loader,除了它利用 URL encoding (opens new window)而不是 Base64 对文件编码。对于 SVG 图片这是有效的,因为 SVG 文件恰好是纯文本,这种编码规模效应更加明显,使用方法如下:
|
Tips:svg-url-loader 拥有改善 IE 浏览器支持的选项,但是在其他浏览器中更糟糕。如果你需要兼容 IE 浏览器,设置
iesafe: true选项。
#图片优化
图片体积是个经常诟病的问题,一个页面中,完全一样内容的图片,在肉眼可见的范围内并不一定有差异但是体积却相差甚大。
所以图片优化也是在前端项目中经常做的事情,在 Webpack 中可以借助img-webpack-loader (opens new window)来对使用到的图片进行优化。
它支持 JPG、PNG、GIF 和 SVG 格式的图片,因此在碰到所有这些类型的图片都会使用它。
|
image-webpack-loader (opens new window)这个 loader 不能将图片嵌入应用,所以它必须和
url-loader以及svg-url-loader一起使用。
为了避免同时将它复制粘贴到两个规则中(一个针对 JPG/PNG/GIF 图片, 另一个针对 SVG ),使用 enforce: 'pre' 作为单独的规则涵盖在这个 loader:
|
通过enforce: 'pre'提高了 img-webpack-loader 的优先级,保证在url-loader和svg-url-loader之前就完成了图片的优化。
另外img-webpack-loader (opens new window)默认的配置就已经适用于日常开发图片的压缩优化需求了,但是如果你想更进一步去配置它,参考插件选项 (opens new window)。要选择指定选项,请查看国外牛人写的一个图像优化指南 (opens new window)。
#其他资源处理
#字体、多媒体
对于字体、富媒体等静态资源,可以直接使用url-loader或者file-loader进行配置即可,不需要额外的操作,具体配置内容如下:
|
Tips:如果不需要 Base64,那么可以直接使用 file-loader,需要的话就是用
url-loader,还需要注意,如果将正则(test)放在一起,那么需要使用[ext]配置输出的文件名。
#JSON/CSV/XML数据
如果项目需要加载的类似 JSON、CSV、TSV 和 XML 等数据,那么需要单独给它们配置相应的 loader。对 JSON 的支持实际上是内置的,类似于 Node.js,这意味着import Data from'./data.json'导入数据默认情况将起作用。要导入 CSV,TSV 和 XML,可以使用csv-loader (opens new window)和xml-loader (opens new window)。
首先是安装它们的 loader:npm i -D xml-loader csv-loader,然后增加文件 loader 配置如下:
|
现在,您可以导入这四种类型的数据中的任何一种(JSON,CSV,TSV,XML),并且导入它的 Data 变量将包含已解析的 JSON 以便于使用。
#配置 CDN 域名
一般静态资源上线的时候都会放到 CDN,假设的 CDN 域名和路径为:http://bd.bxstatic.com/img/,这时候只需要修改output.publicPath即可:
|
修改后执行webpack打包后的结果如下:
|
说明 Webpack 为自动替换了路径,并且加上了 CDN 域名。