第一章 Webpack简介
Webpack是什么
Webpack 是一个开源的 JavaScript 模块打包工具。其核心功能就是解决模块之间的依赖,把各个模块按照特定的规则和顺序组织在一起。主要是用来将前端资源打包、压缩、优化。
JavaScript中的模块
在大多数程序语言中(如C、C++、Java),开发者都可以直接使用模块化进行开发。对于JavaScript来说,情况则有所不同。在过去的很长一段时间里,JavaScript这门语言并没有模块这一概念。如果工程中有多个JS文件,我们只能通过script标签将它们一个个插入页面中。
为何偏偏JavaScript没有模块呢?如果要追溯历史原因,JavaScript之父——Brendan Eich最初设计这门语言时只是将它定位成一个小型的脚本语言,用来实现网页上一些简单的动态特性,远没有考虑到会用它实现今天这样复杂的场景,模块化当然也就显得多余了。
从2009年开始,JavaScript社区开始对模块化进行不断的尝试,并依次出现了AMD、CommonJS、CMD等解决方案。但这些都只是由社区提出的,并不能算语言本身的特性。而在2015年,ECMAScript 6.0(ES6)正式定义了JavaScript模块标准,使这门语言在诞生了20年之后终于拥有了模块这一概念。
Webpack的优势
- Webpack默认支持多种模块标准,包括AMD、CommonJS,以及最新的ES6模块,而其他工具大多只能支持一到两种。
- Webpack有完备的代码分割(code splitting)解决方案,相当于可以实现js的懒加载。
- Webpack可以处理各种类型的资源。包括JavaScript、样式、模板,甚至图片等。
- Webpack拥有庞大的社区支持。
简单使用
安装
- 先安装nodeJs。
- 新建一个工程目录,从命令行进入该目录,并执行npm的初始化命令。
npm init
- 执行安装Webpack的命令。
npm install webpack webpack-cli –-save-dev
- 查看版本号。
npx webpack-v npx webpack-cli-v
Hello World
在工程目录下添加以下几个文件。
index.js:
import addContent from './add-content.js';
document.write('My first Webpack app.<br />');
addContent();
add-content.js:
export default function() {
document.write('Hello world!');
}
index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>My first Webpack app.</title>
</head>
<body>
<script data-src="./dist/bundle.js"></script>
</body>
</html>
然后在控制台输入打包命令:
npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
用浏览器打开index.html,可以看到在页面上会显示“My first Webpack app.Hello world!”。
使用npm scripts简化命令
编辑工程中的package.json文件:
……
"scripts": {
"build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
},
……
重新执行打包,这次输入npm命令即可:
npm run build
使用配置文件
在工程根目录下创建webpack.config.js,并添加如下代码:
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
mode: 'development',
}
然后就可以去掉package.json中配置的打包参数了:
……
"scripts": {
"build": "webpack"
},
……
webpack-dev-server
使用webpack-dev-server可以实现代码热部署,修改js,css等文件后不需要重新编译而可以自动刷新,方便开发调试。
第二章 模块打包
CommonJS
CommonJS是由JavaScript社区于2009年提出的包含模块、文件、IO、控制台在内的一系列标准。在Node.js的实现中采用了CommonJS标准的一部分,并在其基础上进行了一些调整。我们所说的CommonJS模块和Node.js中的实现并不完全一样,现在一般谈到CommonJS其实是Node.js中的版本,而非它的原始定义。
导出
导出是一个模块向外暴露自身的唯一方式。在CommonJS中,通过module.exports可以导出模块中的内容,如:
module.exports = {
name: 'calculater',
add: function(a, b) {
return a + b;
}
};
导入
在CommonJS中使用require进行模块导入。如:
// calculator.js
module.exports = {
add: function(a, b) {return a + b;}
};
// index.js
const calculator = require('./calculator.js');
const sum = calculator.add(2, 3);
console.log(sum); // 5
ES6 Module
在JavaScript之父Brendan Eich最初设计这门语言时,原本并没有包含模块的概念。基于越来越多的工程需求,为了使用模块化进行开发,JavaScript社区中涌现出了多种模块标准,其中也包括CommonJS。一直到2015年6月,由TC39标准委员会正式发布了ES6(ECMAScript 6.0),从此JavaScript语言才具备了模块这一特性。
导出
在ES6 Module中使用export命令来导出模块。export有两种形式:
- 命名导出
- 默认导出
一个模块可以有多个命名导出。它有两种不同的写法:
// 写法1
export const name = 'calculator';
export const add = function(a, b) { return a + b; };
// 写法2
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };
与命名导出不同,模块的默认导出只能有一个。如:
export default {
name: 'calculator',
add: function(a, b) {
return a + b;
}
};
导入
ES6 Module中使用import语法导入模块。首先我们来看如何加载带有命名导出的模块,请看下面的例子:
// calculator.js
const name = 'calculator';
const add = function(a, b) { return a + b; };
export { name, add };
// index.js
import { name, add } from './calculator.js';
add(2, 3);
接下来处理默认导出,请看下面这个例子:
// calculator.js
export default {
name: 'calculator',
add: function(a, b) { return a + b; }
};
// index.js
import myCalculator from './calculator.js';
calculator.add(2, 3);
加载npm模块
与Java、C++、Python等语言相比,JavaScript是一个缺乏标准库的语言。当开发者需要解决URL处理、日期解析这类很常见的问题时,很多时候只能自己动手来封装工具接口。而npm提供了这样一种方式,可以让开发者在其平台上找到由他人所开发和发布的库,并安装到项目中,来快速地解决问题,这就是npm作为包管理器为开发者带来的便捷。
很多语言都有包管理器,比如Java的Maven,Ruby的gem。目前,JavaScript最主流的包管理器有两个——npm和yarn。两者的仓库是共通的,只是在使用上有所区别。截至目前,npm平台上已经有几十万个模块(package,也可称之为包),并且这个数字每天都在增加,各种主流的框架类库都可以在npm平台上找到。作为开发者,每个人也都可以自己封装模块并上传到npm,通过这种方式来与他人共享代码。
第三章 资源输入输出
资源处理流程
根据指定的一个/多个入口(entry),告诉 webpack 从哪个目录的哪个文件开始打包。webpack 会从入口模块开始检索,并将具有依赖关系的模块构成一个(树) chunk(一般来说一个入口及其依赖会产生一个 chunk)。最终会将 chunk 打包为 bundle。
entry 入口
entry的配置可以有多种形式:字符串、数组、对象、函数。可以根据不同的需求场景来选择。
// 字符串
module.exports = {
entry: './src/index.js',
};
// 数组
module.exports = {
entry: ['babel-polyfill', './src/index.js'] ,
};
// 对象
module.exports = {
entry: {
// chunk name为index,入口路径为./src/index.js
index: './src/index.js',
// chunk name为lib,入口路径为./src/lib.js
lib: './src/lib.js',
},
};
// 函数
module.exports = {
entry: () => './src/index.js',
};
output 出口
接着我们来看资源输出相关的配置,所有与出口相关的配置都集中在output对象里。请看下面的例子:
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
// 控制输出资源的文件名
filename: 'bundle.js',
// 绝对路径,打包后的目录 默认为 dist
path: path.join(__dirname, 'assets'),
// 会为请求的静态资源加上这个前缀
publicPath: '/dist/',
},
};
第四章 预处理器
loader --- 就是一个函数,接受源码字符串,通过转换,返回输出的内容。
output = loader(input)
出现的原因是因为,webpack 默认只能处理 js 代码。
loader 使用
module.exports = {
module: {
publicPath: [{
// 接收一个正则表达式或正则表达式的数组 匹配到的文件才使用此规则
test: /\.css/,
// 接收一个数组,其中包含此规则使用的loader
use: ['css-loader'],
// 接收一个正则表达式或文件绝对路径的字符串,用来排除或包含那个
exclude: /node_modules/,
include: /src/,
}]
},
};
常用的loader
- css-loader 处理css
- style-loader 将样式包装为style标签插入页面
- babel-loader 处理ES6+并便以为ES5
- ts-loader 用于连接Webpack与Typescript
- html-loader 用于将HTML文件转化为字符串并格式化
- handlebars-loader 用于处理handlebars模板
- file-loader 用于打包文件类型资源
- url-loader 和file-loader效果类似,但可以设置文件转换base64的阈值
- vue-loader 用于处理vue组件
第五章 样式处理
分离样式
将js中的引入样式提取为css文件,然后按需加载。
extract-text-webpack-plugin webpack4 之前使用
mini-css-extract-plugin webpack4 及其之后的版本使用
样式预处理
增强css语法,在编译时在转换为css,主要方便开发,如解决在css中无法定义变量的问题。
- Sass与SCSS
- Less
CSS Module
直接配置 css-loader 的 option 就可以实现分离样式的效果。
隔离作用域,防止冲突
第六章 代码分片
实现高性能应用其中重要的一点就是尽可能地让用户每次只加载必要的资源,优先级不太高的资源则采用延迟加载等技术渐进式地获取,这样可以保证页面的首屏速度。
- 通过入口来划分,配置多个 entry(提取 vendor)。
- CommonsChunkPlugin webpack4 之前使用。
- SplitChunks webpack4 及其之后的版本使用。
- 通过 import() 异步加载资源。
第七章 生产环境配置
在开发过程中,需要区分开发环境和生产环境,开发环境一般完成基本的编译打包工作,让代码能在浏览器运行就好,而生产环境为了更小的包体通常还会进行压缩、tree-shaking等操作。将这两种环境的操作区分开来,一般有两种方案:
通过命令传入环境变量
// package.json
{ ...
"scripts": {
"dev": "ENV=development webpack-dev-server",
"build": "ENV=production webpack"
},
}
// webpack.config.js
const ENV = process.env.ENV;
const isProd = ENV === 'production';
module.exports = {
output: {
filename: isProd ? 'bundle@[chunkhash].js' : 'bundle.js',
},
// mode模式如果为production,webpack会默认添加一些配置,帮助压缩代码
mode: ENV,
};
不同环境不同配置文件
为两种环境分别写一个配置文件,公用的部分可以通过webpack-merge来合并。
{ ...
"scripts": {
"dev": " webpack-dev-server --config=webpack.development.config.js",
"build": " webpack --config=webpack.production.config.js"
},
}
source map
主要用于报错时回溯调用信息。
module.exports = {
devtool: 'source-map',
};
资源压缩
js 压缩
webpack4 及其以上版本 mode: production 的时候会自动压缩打包后的 bundle 默认使用的是 terser
webpack4 之前的版本使用 uglifyJS (只支持 es5)
css 压缩
需要先将 css 提取到单独的文件中
optimize-css-assets-webpck-plugin
缓存
以 hash 作为文件名的一部分,保证文件内容不变,生产的文件名称不变。
第八章 打包优化
HappyPack 多进程打包工具 thread-loader
tree shaking 标记死代码,通过压缩工具去除。
第九章 开发环境调优
HMR 模块热替换(hot module replace),不需要刷新浏览器即可看到更改。针对开发环境,需要 webpack-dev-server 配合。
开启HMR:
const webpack = require('webpack');
module.exports = { // ...
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: { hot: true, },
};
HMR原理:
- 首先浏览器端会有HMR runtime,webpack会起一个webpack-dev-server(WDS),两者依靠websocket通信;
- 当WDS监听到文件变化,会向客户端推送更新事件,并带上构建的hash。客户端根据这个hash和之前资源的对比,判断是否需要更新;
- 当需要更新,客户端就会向WDS请求更改的资源列表。WDS会返回需要构建的chunk name和资源版本hash。客户端再根据这些信息向WDS请求增量更新的资源;
- 拿到更新的资源,HMR runtime就会开始决定哪些地方需要替换。webpack会暴露一个module.hot接口,用于给使用者知道热替换的时机,module.accept则是设置需要替换的模块。一般loader都会设置相应的模块热替换的补丁操作,对替换模块进行操作。如果runtime对某个模块没有检测到对HMR的update handler,则会将替换操作冒泡到父级模块,以此类推。
第十章 更多 JavaScript 打包工具
Rollup更加专注于JavaScript的打包,它自身附加的代码更少,具备tree shaking,且可以输出多种形式的模块。
Parcel在资源处理流程上做了改进,以追求更快的打包速度。同时其零配置的特性可以减少很多项目开发中花费在环境搭建上面的成本。
在进行技术选型的时候,我们不仅要结合目前工具的一些特性,也要看其未来的发展路线图。如果其能在后续保持良好的社区生态及维护状况,对于项目今后的发展也是非常有利的。
总结
这本书主要讲了 Webpack 的发展史以及如何使用和调优。但目前使用的vue,react等项目都有自己的项目构建工具,其实就是对 Webpack 的封装,比如:@vue/cli 等。这些工具对本书中的大部分场景都进行了优化,我们在实际开发开始中对打包关注的比较少了,主要重点还是在业务开发。但了解 Webpack 对更好地理解前端项目是有很大帮助的。当然,如果要自己开js的一下插件或组件时,就必须要了解一下了。