系列文章链接:
为什么需要构建工具?
- 转换
ES6+
语法;- 转换
JSX
语法 /Vue
指令;CSS
私有前缀补全 / 预处理器(less
,sass
);- 压缩混淆 / 图片压缩;
为什么选择 Webpack?
早期的打包工具有 Grount
,把打包构建分成一个一个的任务,队列式的处理每一个任务,如解析 html
任务、解析 CSS
任务、解析 JS
任务、图片压缩任务、代码压缩任务等等,每一个任务处理完成之后会将任务结果存放在本地磁盘的 .temp
目录,由于产生了 IO
操作,会导致打包速度比较慢。
后来产生了 Glup
,原理与 Grount
类似,管道式的处理打包任务,不同的是 Gulp
有文件流的概念,每一个任务构建后的结果不会存放磁盘,而是存在内存中,在下一个步骤中可以直接使用上一个步骤内存中的结果,提高了打包速度。
目前最火爆的打包工具是 Webpack
,在打包性能优于上面工具的基础上,更归功于丰富的生态社区、配置灵活的 loader
和 plugin
,可以通过很灵活的配置完成团队项目个性化的打包需求,并且拥有强大的官方团队进行更新迭代,维护了众多稳定的 loader
和 plugin
,更新速度非常快。
安装 Webpack 及打包命令
安装:
$ npm install webpack webpack-cli -D
使用 Webpack
进行打包执行的其实是 ./node_modules/.bin
目录的 webpack
文件。
# 打包命令
$ ./node_modules/.bin/webpack
为了方便项目中通常将打包命令配置在 package.json
的 scripts
中。
/* 打包命令配置 */
{
"scripts": {
"build": "webpack"
}
}
执行配置后的打包命令:
$ npm run build
Webpack 基础配置
零配置
在 Webpack4
中,在不编写配置文件也可以进行打包,这就是 4.x
版本号称的 “零配置”,其实内部默认对入口文件(entry
)和出口文件(output
)进行了配置。
/* 零配置默认值 */
module.exports = {
entry: './src/index.js',
output: './dist/main.js'
}
mode
mode
是 Webpack4
新提出的概念,用来指定当前构建环境是开发环境(production
)、生产环境(development
)或 none
,默认为 production
,设置 mode
可以使用 Webpack
的一些参数值和内置的函数,也可以在打包时针对不同的环境配置不同的打包和优化策略。
mode
配置示例:
module.exports = {
// ...
mode: 'development'
// ...
}
设置为
development
开启的参数如下:
- 设置
process.env.NODE_ENV
值为development
;- 开启
NamedChunksPlugin
、NamedModulesPlugin
,在代码热更新阶段标识更新的chunk
和具体模块。设置为
production
开启的参数如下:
- 设置
process.env.NODE_ENV
值为production
;- 开启
FlagDependencyUsagePlugin
、FlagIncludedChunksPlugin
、NoEmitOnErrorsPlugin
、ModuleConcatenationPlugin
、OccurrenceOrderPlugin
、SideEffectsFlagPlugin
、TerserPlugin
,开启这些插件Webpack
会对JS
压缩,识别package.json
文件中标识代码是否存在副作用的参数等。将
mode
设置为none
不开启任何优化选项。
entry
entry
用于配置打包文件的入口,这个文件中会存在一些依赖关系,依赖的模块又存在依赖关系,最后形成一棵依赖树,Webpack
则将这些模块根据依赖关系,最后打包成多个静态资源,entry
主要有两种应用场景(单页应用和多页应用),配置如下。
/* 单入口(SPA) */
module.exports = {
entry: './src/index.js'
}
/* 多入口(多页应用) */
module.exports = {
entry: {
app: './src/pages/app.js',
adminApp: './src/pages/adminApp.js'
}
}
output
entry
配置是用于指定的是源代码,那 output
就是用于指定 Webpack
打包后的结果代码,即用来告诉 Webpack
如何将编译后的文件输出到磁盘。
module.exports = {
// ...
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
}
output
属性值为对象,其中 filename
属性用于指定打包输出后的文件名,path
用于指定打包输出的目录,如果是多页应用,可以使用占位符保证打包后输出多个出口文件名字的唯一性。
/* 多页应用 */
module.exports = {
entry: {
app: './src/pages/app.js',
adminApp: './src/pages/adminApp.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
}
上面的 [name]
打包后最后输出的出口文件与入口配置的文件名对应,即 app.js
和 adminApp.js
。
loaders
Webpack
默认情况下只支持 js
和 json
两种文件类型,loader
(加载器)是专门用来支持其他文件类型并把其他文件转换成有效的模块添加到依赖树中,每一个 loader
都默认导出一个函数,接受源文件作为参数,并返回转换的结果,loaders
选项是专门用来配置这些加载器的。
常见 loader 表:
名称 | 描述 |
---|---|
babel-loader | 转化 ES6、ES7 等 JS 新特性 |
css-loader | 支持 .css 文件的加载和解析 |
less-loader | 将 less 文件转换成 css |
ts-loader | 将 TS 转换成 JS |
file-loader | 对图片、字体等文件的打包 |
raw-loader | 将文件以字符串的形式导入 |
thread-loader | 多进程打包 JS 和 CSS |
配置示例:
module.exports = {
// ...
module: {
rules: [
{
test: /\.txt$/, // 指定匹配规则(文件后缀名)
use: 'raw-loader' // 指定使用的 loader 名称
}
]
}
}
解析 ES6+ 语法
安装依赖:
$ npm install babel-loader @babel/preset-env -D
loader
配置示例:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
use: 'babel-loader'
}
// ...
]
}
// ...
}
在工程中添加
.babelrc
文件来对解析的ES6+
语法进行配置。
.babelrc
配置示例:
/* 以 babel7 为例 */
{
"presets": [
"@babel/preset-env"
],
"plugins": [
"@babel/proposal-class-properties"
// ...
]
}
babel
配置主要包含两部分,presets
和plugins
,plugins
中配置的每一项都是为了解析某一个语法,而presets
配置的是这些功能的集合。
解析 React 的 JSX 语法
由于 React
是在 .js
或 .jsx
的文件中使用 JSX
语法,所以解析 JSX
语法也是解析 JS
工作的一部分,同样需要 babel-loader
,需要在 Webpack
配置文件的 loader
配置中增加识别 .jsx
文件以及在 .babelrc
配置文件的 presets
中专门增加解析 JSX
功能的集合。
安装依赖:
$ npm install babel-loader @babel/preset-env @babel/preset-react -D
loader
配置示例:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(jsx?)$/,
use: 'babel-loader'
}
// ...
]
}
// ...
}
.babelrc
配置示例:
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/proposal-class-properties"
// ...
]
}
解析 CSS
解析
CSS
主要靠css-loader
和style-loader
:
css-loader
:用于加载.css
文件,并转换成CommonJS
对象;style-loader
:将样式通过<style></style>
标签插入到html
文件的head
中。
安装依赖:
$ npm install css-loader style-loader -D
loader
配置示例:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
// ...
]
}
// ...
}
值得注意的是,在处理同一个类型文件使用多个
loader
时,是链式调用的,loader
的执行顺序是从右向左的,所以在编写解析CSS
加载器配置时应该style-loader
在前,css-loader
在后,即先通过css-loader
解析.css
文件,将解析好的结果传递给style-loader
处理并插入到页面的head
中。
解析 Less 和 Sass
Less
和 Sass
作为 CSS
的预编译语言,加入了很多编程的特性,功能更强,对样式的组织也更加的灵活,但是浏览器依然不识别,所以也需要 Webpack
进行编译转换。
安装依赖(Less
):
$ npm install css-loader style-loader less-loader less -D
安装依赖(Sass
):
$ npm install css-loader style-loader sass-loader node-sass -D
loader
配置示例:
/* less 配置 */
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
// ...
]
}
// ...
}
/* sass 配置 */
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
// ...
]
}
// ...
}
解析图片和字体资源
Webpack
对其他类型的文件进行打包编译需要依赖 file-loader
(专门用于处理文件)。
安装依赖:
$ npm install file-loader -D
file-loader
配置示例:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|svg|gif|jpe?g)$/, // 解析图片
use: 'file-loader'
},
{
test: /\.(woff2?|eot|ttf|otf)$/, // 解析字体
use: 'file-loader'
}
// ...
]
}
// ...
}
也可以使用
url-loader
来实现对图片和字体资源的解析,url-loader
相比file-loader
而言,支持更颗粒化的解析方式,可以配置解析后出口文件的具体目录,也可以根据资源大小设置将资源转换成base64
。
安装依赖:
$ npm install url-loader -D
url-loader
配置示例:
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|svg|gif|jpe?g)$/, // 解析图片
use: [
{
loader: 'url-loader',
options: {
limit: 10240, // 资源小于该数值转为 base64
name: 'img/[name].[ext]' // 打包后的输出路径
}
}
]
}
// 字体资源同理...
]
}
// ...
}
PostCSS 对 CSS 的增强
在开发 CSS
时,存在着很多让我们头疼的的问题,比如有些 CSS3
的属性由于各浏览器的实现标准不同要加上不同的私有前缀,也比如为了在移动端进行页面适配使用的 rem
、vw
单位与 px
的转换问题等等,其中的一部分问题其实是可以在 Webpack
构建的过程中直接解决的。
安装依赖:
$ npm install postcss-loader autoprefixer postcss-px2rem -D
可以通过 Webpack
配置文件中直接配置,也可以 PostCSS
配置文件中进行配置。
配置示例:
/* 配置在 Webpack 配置文件 */
module.exports = {
module: {
rules: [
// ...
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
{
loader: 'postcss-loader', // 使用 postcss-loader
options: {
plugins: [
// 添加私有前缀
require('autoprefixer')({
// 兼容浏览器版本(最后两个版本、使用率大于 1%,ios 7 以上)
browers: ['last 2 version', '>1%', 'ios 7']
}),
// px 自动转换 rem
require('postcss-px2rem')({
remUnit: 75, // 75 px 等于 1 rem
remPrecision: 8 // 换算结果小数点后面保留几位小数
})
// ...
]
}
}
]
}
// ...
]
}
}
/* 在 postcss.config.js 中配置 */
module.exports = {
plugins: [
require('autoprefixer')({
browers: ['last 2 version', '>1%', 'ios 7']
}),
require('postcss-px2rem')({
remUnit: 75,
remPrecision: 8
})
// ...
]
}
如果使用其他 PostCSS
的功能也是类似的,需要先下载对应的 PostCSS
插件,然后在配置文件中进行配置。
plugins
plugins
通常用来对 Webpack
打包功能的增强,对打包过程和出口文件大小的优化、资源管理和环境变量的注入,可以作用域整个构建过程。
常见 plugin 表:
名称 | 描述 |
---|---|
CommonsChunkPlugin | 将 chunks 相同的模块代码提取成公共 js |
CleanWebpackPlugin | 清理构建目录 |
ExtracTextWebpackPlugin | 将 CSS 从 bundle 文件里提取成一个独立的 .css 文件 |
CopyWebpackPlugin | 将文件或者文件夹拷贝到构建的输出目录 |
HtmlWebpackPlugin | 创建 html 文件并注入 bundle 文件 |
UglifyjsWebpackPlugin | 压缩 .js 文件 |
ZipWebpackPlugin | 将打包出的资源生成一个 .zip 包 |
自动生成 index.html
自动生成 index.html
文件主要通过 HtmlWebpackPlugin
插件实现。
安装依赖:
$ npm install html-webpack-plugin -D
配置示例:
// 引入插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// ...
plugins: [
// 创建插件的实例
new HtmlWebpackPlugin({
template: './src/index.html', // 模板文件路径
filename: 'index.html' // 输出文件名称
})
// ...
]
}
抽取 CSS 文件
Webpack
在上面对 CSS
的解析中,使用了 css-loader
和 style-loader
,通过构建后的结果发现 .css
文件被注入到了 .js
文件中,在生产环境通常会为了减小出口文件的体积对 .css
文件进行抽离,在 Webpack4
中使用 MiniCssExtractPlugin
插件来实现。
安装依赖:
$ npm install mini-css-extract-plugin -D
配置示例:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 用于将 CSS 抽离的加载器
'css-loader',
]
}
// ...
]
},
plugins: [
// ...
new MiniCssExtractPlugin({
filename: '[name].css' // 抽离的文件名
})
// ...
]
// ...
}
解析
.css
文件无论哪种方式需要使用css-loader
,但MiniCssExtractPlugin
提供的loader
与style-loader
的功能是互斥的,style-loader
用于将解析的CSS
注入,而MiniCssExtractPlugin.loader
意在单独抽离。
自动清理构建目录
如果输出的文件配置了 hash
且在每次构建时没有及时删除指定的输出目录(如 dist
),会导致输出目录中的文件越来越多,不容易区分哪些是新构建出来的文件,所以应该让 Webpack
在每次构建之前清除输出的目录。
当然清除的方式可以多种,比如手动删除,或者在 package.json
中配置的构建命令中增加前置命令如下。
/* 不优雅的方式 */
{
"scripts": {
"build": "rm -rf ./dist && webpack"
}
}
这种方式并不优雅,完全可以通过 Webpack
配置中增加 CleanWebpackPlugin
插件来解决这个问题,这样在每次构建之前就会自动清除输出目录。
安装依赖:
$ npm install clean-webpack-plugin -D
配置示例:
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
// ...
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + 'dist'
}
plugins: [
new CleanWebpackPlugin()
]
// ...
}
watch
Webpack
中,文件监听是指发现源文件发生变化时,自动重新构建出新的输出文件。
Webpack
开启监听模式有两种方式:
- 启动
Webpack
命令时带上--watch
参数,可以在package.json
中进行配置;- 在
Webpack
配置文件中进行设置。
添加参数:
/* 配置命令 */
{
"scripts": {
"build": "webpack",
"watch": "webpack --watch"
}
}
执行命令:
$ npm run watch
Webpack
配置文件:
/* webpack 配置文件 */
module.exports = {
// ...
watch: true, // 开启监听
watchOptions: {
// 忽略监听的文件目录
ignored: /node_modules/,
// 监听到发生变化会等待 300ms 去重新构建(防止多次保存),默认 300ms
aggregateTimeout: 300,
// 每秒检查 1000 次
poll: 1000
}
// ...
}
watch
监听原理分析:
Webpack
会轮询的判断文件的最后编辑时间是否发生变化,如果某个文件发生变化不会立即重新构建,而是会将变化缓存起来,等待aggregateTimeout
配置的时间后重新构建,这样做是为了防止短时间的多次变化或产生了新的变化文件,在该时间到达时将变化的文件列表进行统一构建,以提高性能,这样的监听方式的缺陷是浏览器不会自动刷新,需要手动刷新查看文件修改后的效果。
devServer
webpack-dev-server
是在本地启动服务来监听文件的变化,不是以输出文件的形式更新,而是即时将重新构建的结果放在内存中。
安装依赖:
$ npm install webpack-dev-server -D
配置示例:
module.exports = {
// ...
mode: 'development', // 由于 webpack-dev-server 在开发环境中使用
devServer: {
host: 'localhost', // 本地服务的域名
contentBase: './dist', // server 作用的目录
port: 8080, // 端口号
compress: true // 是否启动服务器压缩
}
// ...
}
配置服务启动命令:
{
"scripts": {
"dev": "webpack-dev-server --open"
}
}
devtool
在 Webpack
构建后的代码中,经过了压缩、混淆等,会出现一个新的问题,就是代码执行出错后不容定位错误是在源代码中哪一个位置产生的,而 devtool
的配置的作用就是让我们更容易定位错误的位置。
devtool
的关键词:
关键词 | 作用 |
---|---|
evel | 使用 evel 函数包裹代码 |
source-map | 产生 .map 文件 |
cheap | 不含列信息 |
inline | 将 .map 内容作为 DataURI 嵌入,不单独生成 .map 文件 |
module | 包含 loader 的 source-map |
devtool
属性的值就是由上面的关键词组成的,不同的名字会使用不同的调试策略,而名字中的关键词则包含了上面特性,下面是 Webpcak
官网给出的不同构建策略对应的信息。
devtool | 首次构建 | 二次构建 | 是否适合生产环境 | 可以定位的代码 |
---|---|---|---|---|
none | +++ | +++ | yes | 最终输出的代码 |
eval | +++ | +++ | no | Webpack 生成的代码(一个个的模块) |
cheap-eval-source-map | + | ++ | no | 经过 loader 转换后的代码(只能看到行) |
cheap-module-eval-source-map | o | ++ | no | 源代码(只能看到行) |
eval-source-map | -- | + | no | 源代码 |
cheap-source-map | + | o | no | 经过 loader 转换后的代码(只能看到行) |
cheap-module-source-map | o | - | no | 源代码(只能看到行) |
inline-cheap-source-map | + | o | no | 经过 loader 转换后的代码(只能看到行) |
inline-cheap-module-source-map | o | - | no | 源代码(只能看到行) |
source-map | -- | -- | yes | 源代码 |
inline-source-map | -- | -- | no | 源代码 |
hidden-source-map | -- | -- | yes | 源代码 |
nosources-source-map | -- | -- | yes | 无源代码 |
+++
非常快速,++
快速,+
比较快,o
中等,-
比较慢,--
慢
配置示例:
module.exports = {
// ...
devtool: 'evel'
// ...
}
使用不同的 devtool
会带来不同的效果,使用 evel
不安全,使用 inline
注入会增加打包文件的大小、线上环境生成 .map
文件会容易被人反编译进而暴露业务逻辑等等,所以在 devtool
使用时还是根据自己的需要和安全考虑来权衡。
热更新
配置热更新的方式
webpack-dev-server
可以配合自带的插件实现热更新,即文件修改后,不刷新浏览器的情况下自动构建并在浏览器中响应渲染。
/* 使用 webpack-dev-server */
const webpack = require('webpack');
module.exports = {
// ...
mode: 'development',
devServer: {
host: 'localhost',
contentBase: './dist',
port: 8080,
compress: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
// ...
}
想要颗粒度更细致的控制 Webpack
的热更新,也可以使用另一种方式,即借助 Express
或 Koa
自己创建一个服务,并借助 webpack-dev-middleware
来实现热更新,这种方式更适合灵活的定制化场景。
/* 使用 webpack-dev-middleware */
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js');
const app = express();
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
app.listen(3000, function () {
console.log('server start 3000');
});
热更新原理简介
热更新的实现分为服务端和浏览器两个部分:
- Webpack Dev Server
Webpack Compile
:Webpack
的编译器,作用是将JS
编译成Bundle
;HMR Server
:将热更新的文件输出给HMR Runtime
;Bundle Server
:提供文件在浏览器以服务器的方式访问。- Browser
HMR Runtime
:开发阶段打包过程中,会被注入到浏览器,使浏览器的bundle.js
和服务器建立websocket
链接,以更新文件的变化;bundle.js
:构建输出的文件。
Webpack
在将本地文件显示在浏览器其实有两个阶段:
- 第一个阶段为启动阶段通过
Webpack Compile
将文件系统中的文件进行构建,然后将文件传递给Bundle Server
,Bundle Server
将bundle.js
响应给浏览器;- 第二个阶段为热更新阶段,依然通过
Webpack Compile
对文件系统中修改的文件进行构建,将构建后的结果传递给HMR Server
,HMR Server
通过Websocket
协议将文件变化的结果通知浏览器端的HMR Runtime
,执行代码并刷新页面。
未完待续…