Abo

从零开始的 webpack 世界

webpack是一个静态模块打包器。当webpack用来处理应用程序时,会构建一个模块关系依赖图,其中包含应用程序所需的各个模块,然后将这些模块打包成一个或多个bundle


1

希望项目能早点有出路

什么是webpack

它是一个模块打包器,也可以引用官网的一幅图解释,我们可以看到webpack,可以分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js 、 .css 、 .jpg 、.png。配合其官网的首页图不难理解其含义:一切文件如:JavaScript 、CSS、 SASS 、 IMG/PNG等,在 Webpack 眼中都是一个个模块,通过对 Webpack 进行配置,对模块进行组合和打包。经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

核心概念

  • Entry 入口
  • Output 输出结果
  • Loader 模块转换器
  • Module 模块
  • Chunk 代码块
  • Plugin 插件

Entry

entry 是配置模块的入口,它指示 Webpack执行构建的第一步。
可以在配置文件中配置 entry 属性,来指定一个或多个入口点

entry 类型有三种:字符串、数组、对象。

  • String“./src/entry” 入口模块的文件路径,可以是相对路径
  • array : [“./src/entry1”, “./src/entry2”] 入口模块的文件路径,可以是相对路径。与字符串类型不同的是数组可将多个文件打包为一个文件
  • object{ a: ‘./src/entry-a’, b: [‘./src/entry-b1’, ‘./app/entry-b2’]} 配置多个入口,每个入口有一个 Chunk

Output

output 配置如何输出最终想要的代码。output是一个 object,里面包含一系列配置项。

  • filename 用于输出文件的文件名。
  • path 目标输出目录的绝对路径,必须是绝对路径

module、Loader

module 配置如何处理模块

配置Loader

Loader 可以看作具有文件转换功能的翻译员,配置里的 module.rules 数组配置了一组规则,告诉 Webpack 在遇到哪些文件时使用哪些 Loader 去加载和转换。

  • use 属性的值需要是一个由 Loader 名称组成的数组,Loader 的执行顺序是由后到前的;
  • 每一个 Loader 都可以通过 URL querystring的方式传入参数,例如 css-loader?minimize 中的 minimize 告诉 css-loader 要开启 CSS压缩。

Plugins

Plugin 用于扩展 Webpack 功能,各种各样的 Plugin 几乎让 Webpack 可以做任何构建相关的事情。

配置 Plugin

Plugin的配置很简单,plugins 配置项接受一个数组,数组里每一项都是一个要使用的 Plugin 的实例,Plugin 需要的参数通过构造函数传入。

当然使用 Plugin*的难点在于掌握 Plugin 本身提供的配置项,而不是如何在 Webpack 中接入 Plugin。

Chunk

Chunk,代码块,即打包后输出的文件。

Webpack会为每个 Chunk 取一个名称,可以根据 Chunk 的名称来区分输出的文件名。

filename: '[name].js'

一个入口文件,默认 chunknamemain。 除了内置变量 name,与 chunk 相关的变量还有:

  • id Chunk 的唯一标识,从0开始
  • name Chunk 的名称
  • hash Chunk 的唯一标识的 Hash
  • chunkhash Chunk 内容的 Hash

Bundle.js

bundle

Babel

了解 ES 6 的同学都应该知道,目前部分浏览器和 Node.js 已经支持 ES6,但由于它们对 ES6 所有的标准支持不全,这导致在开发中不敢全面地使用 ES6。 通常我们需要把采用 ES6 编写的代码转换成目前已经支持良好的 ES5 代码,这包含两件事:

  1. 把新的 ES6 语法用 ES5 实现,例如 ES6 的 class 语法用 ES5 的 prototype 实现。
  2. 给新的 API 注入 polyfill ,例如使用新的 fetch API 时注入对应的 polyfill 后才能让低端浏览器正常运行。

Babel 可以方便的完成以上2件事。 Babel 是一个 JavaScript 编译器,能将 ES6 代码转为 ES5 代码,让你使用最新的语言特性而不用担心兼容性问题,并且可以通过插件机制根据需求灵活的扩展。

Babel 执行编译的过程中,会从项目根目录下的 .babelrc 文件读取配置。.babelrc 是一个 JSON 格式的文件

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
"es2015",
"stage-0"
],
"plugins": [
["transform-runtime", {
"polyfill": false,
"regenerator": true
}]
]
}

babel-loader

babel-loader 是必须的,在 Webpack 通过 Loader 接入 Babel,完成转码

babel-preset-es2015和babel-preset-stage-0

presets 属性告诉 Babel 要转换的源码使用了哪些新的语法特性,一个 Presets 对一组新语法特性提供支持,多个 Presets 可以叠加。 Presets 其实是一组 Plugins 的集合,每一个 Plugin 完成一个新语法的转换工作。

  1. 已经被写入 ECMAScript 标准里的特性,由于之前每年都有新特性被加入到标准里,所以又可细分为:
  • es2015 包含在2015里加入的新特性;
  • es2016 包含在2016里加入的新特性;
  • es2017 包含在2017里加入的新特性;
  • es2017 包含在2017里加入的新特性;
  • env 包含当前所有 ECMAScript 标准里的最新特性。
  1. 被社区提出来的但还未被写入 ECMAScript 标准里特性,这其中又分为以下四种:
  • stage0 只是一个美好激进的想法,有 Babel 插件实现了对这些特性的支持,但是不确定是否会被定为标准;
  • stage1 值得被纳入标准的特性;
  • stage2 该特性规范已经被起草,将会被纳入标准里;
  • stage3 该特性规范已经定稿,各大浏览器厂商和 Node.js 社区开始着手实现;
  • stage4 在接下来的一年将会加入到标准里去。

3.为了支持一些特定应用场景下的语法,和 ECMAScript 标准没有关系,例如 babel-preset-react 是为了支持 React 开发中的 JSX 语法。

babel-plugin-transform-runtime

Plugin 属性告诉 Bable 要使用那些插件,插件可以控制如何转换代码。

官方给出的定义是: babel-plugin-transform-runtimeBabel 官方提供的一个插件,作用是减少冗余代码。 但是我们为什么要使用它?主要是因为使用 ES 7async 函数。

因为 runtime 编译器插件做了以下三件事:

  • 当你使用 generators/async 函数时,自动引入 babel-runtime/regenerator
  • 自动引入 babel-runtime/core-js 并映射 ES 6 静态方法和内置插件。
  • 移除内联的 Babel helper 并使用模块 babel-runtime/helpers 代替。

Mode

mode 配置项是 webpack4 新增的配置项,这个配置项是必须的。

如果使用的不是 webpack4 ,该配置项了解一下即可,使用了 webpack4 以上版本,也可以了解一下即可。
事实上它并非必须的,但是如果忽略它, webpack 会有个警告。

该配置项的属性值类型是 String,并有三个可选项:

  • production
  • development
  • none

production 默认值

production 默认值会给你提供一系列有效的默认值以便部署你的应用,它注重:

  • 小的输出体积
  • 运行快速的代码
  • 忽略仅在开发时需要的代码
  • 不暴露源码和文件路径
  • 易于使用的输出产物

development 默认值

该默认顾名思义是注重最好的开发体验,它注重:

  • 浏览器调试工具
  • 快速开发周期中的快速增量编译
  • 在运行过程中提供有效的错误信息

当人对于初学者来说,对于 webpack 的优化还有一段距离,首先我们大体了解一下即可。

它还有很多其他的优化默认值,但是效果可能并不美好,因为它针对的某些特定的大型项目,而我们只是入门,所以了解即可。

Module

rules 配置模块的读取和解析规则,通常用来配置 Loader。其类型是一个数组,数组里每一项都描述了如何去处理部分文件。配置一项 rules 时大致可通过以下方式来实现:

  1. 条件匹配:通过 testincludeexclude 三个配置项来命中 Loader 要应用规则的文件。
  2. 应用规则:对选中后的文件通过 use 配置项来应用 Loader,可以只应用一个 Loader 或者按照从后往前的顺序应用一组 Loader,同时还可以分别给 Loader 传入参数。
  3. 重置顺序:一组 Loader 的执行顺序默认是 从右到左 执行,通过 enforce 选项可以让其中一个 Loader 的执行顺序放到最前或者最后。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module: {
rules: [
{
// 命中 JavaScript 文件
test: /\.js$/,
// 用 babel-loader 转换 JavaScript 文件
// ?cacheDirectory 表示传给 babel-loader 的参数,用于缓存 babel 编译结果加快重新编译速度
use: ['babel-loader?cacheDirectory'],
// 只命中src目录里的js文件,加快 Webpack 搜索速度
include: path.resolve(__dirname, 'src')
},
{
// 命中 SCSS 文件
test: /\.scss$/,
// 使用一组 Loader 去处理 SCSS 文件。
// 处理顺序为从后到前,即先交给 sass-loader 处理,再把结果交给 css-loader 最后再给 style-loader。
use: ['style-loader', 'css-loader', 'sass-loader'],
// 排除 node_modules 目录下的文件
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 对非文本文件采用 file-loader 加载
test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
use: ['file-loader'],
},
]
}

Webpack 在遇到以 .css 结尾的文件时先使用 css-loader 读取 CSS 文件,再交给 style-loaderCSS 内容注入到 JavaScript 里。

style-loader 的工作原理大概是这样的:把 CSS 内容用 JavaScript 里的字符串存储起来, 在网页执行 JavaScript 时通过 DOM 操作动态地往 head 标签里插入 style 标签。

除此之外,我们还可以通过一个 Object 方式来写这一块的代码

1
2
3
4
5
6
7
8
9
10
11
12
use: [
{
loader:'babel-loader',
options:{
cacheDirectory:true,
},
// enforce:'post' 的含义是把该 Loader 的执行顺序放到最后
// enforce 的值还可以是 pre,代表把 Loader 的执行顺序放到最前面
enforce:'post'
},
// 省略其它 Loader
]

alias

resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:

1
2
3
4
5
6
// Webpack alias 配置
resolve:{
alias:{
components: './src/components/'
}
}

当你通过 import Button from ‘components/button’ 导入时,实际上被 alias 等价替换成了 import Button from ‘./src/components/button’

以上 alias 配置的含义是把导入语句里的 components 关键字替换成 ./src/components/

这样做可能会命中太多的导入语句,alias 还支持 $ 符号来缩小范围到只命中以关键字结尾的导入语句:

1
2
3
4
5
resolve:{
alias:{
'react$': '/path/to/react.min.js'
}
}

react$ 只会命中以 react 结尾的导入语句,即只会把 import ‘react’ 关键字替换成 import ‘/path/to/react.min.js’

extensions

在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:

extensions: [‘.js’, ‘.json’]

这个配置项应该有用,在处理 TypeScriptvue 这些文件的时候,可以设置一下

enforceExtension

resolve.enforceExtension 如果配置为 true 所有导入语句都必须要带文件后缀, 例如开启前 import ‘./foo’ 能正常工作,开启后就必须写成 import ‘./foo.js’。

对于 Resolve 的其他配置项,大体了解一下即可,我觉得可能用的不是特别多。

额外:Babel 与 Babel Preset

Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性,这对生产力的提升大有帮助。

实现Babel代码转换功能的核心,就是Babel插件(plugin)。

原始代码 –> [Babel Plugin] –> 转换后的代码

Babel Plugin例子

源代码如下,这里用到了两个ES6才支持的新特性:箭头函数、for…of。在只支持ES5的浏览器里,这两段代码会报错。

因此,可以借助插件将代码转成ES5。

1
2
3
4
5
6
7
// index.js
// 箭头函数
[1,2,3].map(n => n + 1);

// 模板字面量
let nick = '程序猿小卡';
let desc = `hello ${nick}`;

安装依赖:

1
2
3
npm install --save-dev babel-cli 
npm install --save-dev babel-plugin-transform-es2015-arrow-functions
npm install --save-dev babel-plugin-babel-plugin-transform-es2015-template-literals

执行转换,通过--plugins声明依赖的插件,多个插件之间采用,进行分隔。

1
`npm bin`/babel --plugins babel-plugin-transform-es2015-arrow-functions,babel-plugin-transform-es2015-template-literals index.js

转换结果如下:

1
2
3
4
5
6
[1, 2, 3].map(function (n) {
return n + 1;
});

let nick = '程序猿小卡';
let desc = 'hello ' + nick;

转换命令中,插件名称可以省去babel-plugin前缀:

1
`npm bin`/babel --plugins transform-es2015-arrow-functions,transform-es2015-template-literals index.js

也可以改成配置文件 .babelrc,plugins字段中声明的插件会按照顺序执行。

1
2
3
4
5
6
{
"plugins": [
"transform-es2015-arrow-functions",
"transform-es2015-template-literals"
]
}

再次执行转换命令:

1
`npm bin`/babel

Babel Preset简介

Babel插件一般尽可能拆成小的力度,开发者可以按需引进。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。

这样的好处显而易见,既提高了性能,也提高了扩展性。比如开发者想要体验ES6的箭头函数特性,那他只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。

但很多时候,逐个插件引入的效率比较低下。比如在项目开发中,开发者想要将所有ES6的代码转成ES5,插件逐个引入的方式令人抓狂,不单费力,而且容易出错。

这个时候,可以采用Babel Preset。

可以简单的把Babel Preset视为Babel Plugin的集合。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。

下面通过例子说明。

Babel Preset例子

还是原来的代码,这次采用babel-preset-es2015进行转换。

首先,安装依赖:

1
2
npm install --save-dev babel-cli 
npm install --save-dev babel-preset-es2015

执行转换,通过--presets声明依赖的preset,多个preset之间用,做分隔。

1
2
`npm bin`/babel --presets babel-preset-es2015 index.js
`npm bin`/babel --presets es2015 index.js # 也可以去掉 babel-preset 前缀

同样可以采用配置文件 .babelrc。

1
2
3
{
"presets": [ "es2015" ]
}

转换命令:

1
`npm bin`/babel

Plugin与Preset执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

比如.babelrc配置如下,那么执行的顺序为:

  1. Plugin:transform-react-jsx、transform-async-to-generator
  2. Preset:es2016、es2015
1
2
3
4
5
6
7
8
9
10
{
"plugins": [
"transform-react-jsx",
"transform-async-to-generator"
],
"presets": [
"es2015",
"es2016"
]
}

下面通过简单例子进行说明。

Plugin、Preset混用例子

例子:将 index.jsx 编译成 index.js,并且采用 ES5规范。这里包含两个步骤:

  1. jsx 语法转成 js 语法。
  2. 将 ES6规范 转成 ES5规范。

源代码如下:

1
2
3
4
5
6
7
// index.jsx
var profile = <div>
<img src="avatar.png" className="profile" />
<h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>;

var foo = () => "foo";

安装依赖:

1
2
3
npm install --save-dev babel-cli 
npm install --save-dev babel-plugin-transform-react-jsx
npm install --save-dev babel-preset-es2015

配置文件 .babelrc:

1
2
3
4
{
"plugins": [ "transform-react-jsx" ],
"presets": [ "es2015" ]
}

执行转换:

1
`npm bin`/babel index.jsx

转换结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict";

var profile = React.createElement(
"div",
null,
React.createElement("img", { src: "avatar.png", className: "profile" }),
React.createElement(
"h3",
null,
[user.firstName, user.lastName].join(' ')
)
);

var foo = function foo() {
return "foo";
};

附录

  1. loader整理
  • less-loader 引入less文件
  • css-loader 引入css文件
  • style-loader 引入样式
  • bable-core 转换es6
  • babel-loader 引入转换后的js
  • html-loader 转换html的import
  1. 插件整理
  • html-webpack-plugin 重建html,包含webpack打包后的正确引用inject
  • clean-webpack-plugin 清除历史build文件(dist)后再重新打包
  • extract-text-webpack-plugin 分离css文件
  • CommonsChunkPlugin 公共模块提取单独打包
  • uglifyjs-webpack-plugin 压缩js文件