玩转webpack
# 玩转webpack
[TOC]
# 一、基础篇
# 1.1 webpack与构建发展史
# 1.1.1为什么需要构建工具?
转换ES6语法(IE8-11还不支持ES6,Edge16后支持)
转换JSX(React的核心组成部分)(浏览器无法识别)
CSS前缀补全/预处理器(让css更有编程性,效率更高)
压缩混淆
图片压缩
# 1.1.2 配置文件名称
官方默认配置文件是:webpack.config.js
,也可以通过webpack --config
指定配置文件。
# 1.1.3 webpack 配置组成
module.exports = {
entry:'./src/index.js', //打包的入口文件,指定默认的entry为:./src/index.js
output:'./dist/main.js', //打包的输出,指定默认的output为:./dist/main.js
mode:'production', //环境
module:{
rules:{ //Loader配置
{test:/\.txt$/,use:'raw-loader'}
}
},
plugins:[ //插件配置
new HtmlWebpackPlugin({
template:'./src/index.html'
})
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
零配置webpack只需要包含entry和output两个字段,就可以打包。
# 1.1.4 安装webpack和webpack-cli
mkdir my-project // 创建空目录
cd my-project
npm init -y // 创建package.json -y:默认都选择yes
npm install webpack webpack-cli --save-dev
./node_modules/.bin/webpack -v // 检查是否安装成功
2
3
4
5
# 1.1.5 webpack初体验:一个最简单的例子
// my-project/webpack.config.js
'use strict'
// 指定打包的默认地址
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js' //打包的文件名
},
mode: 'production' //生产环境
}
2
3
4
5
6
7
8
9
10
11
12
13
// src/helloworld.js
export function helloworld() {
return 'Hello webpack';
}
2
3
4
// src/index.js
import { helloworld } from './helloworld';
document.write(helloworld());
2
3
然后就可以通过npm运行打包
./node_modules/.bin/webpack
于是就会生成dist/bundle.js。
手动建立dist/index.html并引入bundle.js,就可以在浏览器看打包后的效果。
# 1.1.6 通过npm script运行webpack
也可以这么打包
在package.json的scripts添加字段
"scripts":{
"build":"webpack"
}
2
3
// 删除之前生成的dist文件
rm -rf dist
npm run build
2
3
原理:模块局部安装会在node_moudles/.bin目录创建软连接。
# 1.2 webpack 基础用法
# 1.2.1 entry
entry用来指定webpack的打包入口。
对于非代码,比如图片、字体依赖也会不断加入依赖图中。
依赖图的入口是entry。
用法:
- 单入口:entry是一个字符串
// 适合单页面应用 module.exports = { entry:'./path/to/my/entry/file.js' };
1
2
3
4- 多入口:entry是一个对象
// 多页面应用 module.exports = { entry:{ app:'./src/app.js', adminApp:'./src/adminApp.js' } };
1
2
3
4
5
6
7
# 1.2.2 output
output用来告诉webpack如何将编译后的文件输出到磁盘。
用法:
单入口配置
output:{ filename:'bundle.js', path:_dirname+'/dist' }
1
2
3
4多入口配置
output:{ filename:'[name].js',//通过占位符确保文件名的唯一 path:_dirname+'/dist' }
1
2
3
4
# 1.2.3 loaders
webpack开篇即用只支持JS和JSON两种文件类型,通过 Loaders 去支持其他文件类型并且把它们转换成有效的模块,并且可以添加到依赖图中。
本身是一个函数,接受源文件作为参数,返回转换的结果。
常见的Loaders
- babel-loader:转换ES6、ES7等JS新特性语法。虽然功能强大,但也是慢的。
// 用 include 或 exclude 来避免不必要的转译 // 规避了对庞大的 node\_modules 文件夹或者 bower\_components 文件夹的处理
1
2
3
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
// 增加相应的参数设定
// 开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍
loader: 'babel-loader?cacheDirectory=true'
2
3
- css-loader:支持.css文件的加载和解析
- less-loader:将less文件转换成css
- ts-loader:TS转JS
- file-loader:进行图片、字体等的打包
- raw-loader:将文件以字符串的形式导入
// 直接返回JSON.stringify()后的内容
module.exports = function(content) {
...
return "module.exports = " + JSON.stringify(content);
}
2
3
4
5
thread-loader:多进程打包JS和CSS(打包速度更快)
用法
modules:{ rules:{ { test:/\.txt$/,use:'raw-loader'}// test指定匹配规则,use指定使用的loader名称 } }
1
2
3
4
5
# 1.2.4 plugins
插件用于bundle文件(打包输出的文件)的优化,资源管理和环境变量注入。
作用于整个构建过程。
常见的plugins
用法
// 放到plugins数组里 plugins: [ new HtmlWebpackPlugin({template:'./src/index.html'}) ]
1
2
3
4每次构建时都会重新构建一次 vendor;出于对效率的考虑,我们这里为大家推荐 DllPlugin。
DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包。
用 DllPlugin 处理文件,要分两步走:
- 基于 dll 专属的配置文件,打包 dll 库
- 基于 webpack.config.js 文件,打包业务代码
# 1.2.4.1 DefinePlugin
用来定义全局变量,在webpack打包的时候就会对这些变量做替换。
plugins: [
new webpack.DefinePlugin({
'process.env.status': 'dev'
})
]
2
3
4
5
编译完后字符串'dev'
,就会被变成变量dev
。
正确的姿势如下:
方法一
'process.env.status': '"dev"'
方法二
'process.env.status': JSON.stringify('dev')
# 1.2.4.2 webpack-bundle-analyzer
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin // 生成代码分析报告
new BundleAnalyzerPlugin(), // 使用默认配置
2
3
# 1.2.5 mode
Mode ⽤来指定当前的构建环境是:production、development 还是 none。
设置 mode 可以使⽤ webpack 内置的函数,默认值为 production。
webpack4新提出的。
mode的内置函数功能
# 1.2.6 解析ES6和React JSX
- 解析ES6
- 使⽤ babel-loader
- babel的配置⽂件是:.babelrc
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ use: 'babel-loader'
+ }
+ ]
+ }
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 增加ES6的babel preset配置
// .babelrc
{
"presets": [ // presets:多个plugin的集合
+ "@babel/preset-env"
],
"plugins": [ // plugin:用来支持某个功能
"@babel/proposal-class-properties"
]
}
2
3
4
5
6
7
8
9
babel 只转换新的JS语法,而不转换新的API,例如全局方法:Map、Set、Promise 这些是无法编译成 ES5,需要通过 babel-polyfill 进行。
npm i @babel/core @babel/preset-env babel-loader -D
// i:install -D: --save-dev
2
解析 React JSX
- 增加 React 的 babel preset 配置
{ "presets": [ "@babel/preset-env", + "@babel/preset-react" ], "plugins": [ "@babel/proposal-class-properties" ] }
1
2
3
4
5
6
7
8
9
```shell
// 通常框架、组件和utils等业务逻辑相关的包依赖放在dependencies(用户发布环境)
npm i react react-dom -S
// 对于构建、ESlint、单元测试等相关依赖放在devDependencies(本地开发环境)
npm i @babel/preset-react -D
2
3
4
5
6
# 1.2.7 解析CSS、Less和Sass
解析CSS
- css-loader ⽤于加载 .css ⽂件,并且转换成 commonjs 对象。
- style-loader 将样式通过
<style>
标签插入到head中。
const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + 'css-loader', + 'style-loader' + ] + } + ] + } };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19执行顺序不能错:先执行css-loader,将css转成commonJS模块;后执行style-loader,将js字符串生成style节点,向 DOM 插入 style 标签,并且将样式插入进去,这样网页才能解析到。
函数组合通常有两种方式,一种是从左到右(类似 unix 的 pipe),另外一种是从右到左(compose)。此处 webpack 选择的是 compose 方式,从右到左依次执行 loader,每个 loader 是一个函数。
npm i css-loader style-loader -D
1解析less
+ test: /\.less$/, + use: [ + 'less-loader', + 'css-loader', + 'style-loader' ]
1
2
3
4
5
6先执行less-loader,将less转换成css。
npm i less less-loader -D
1
# 1.2.8 解析图片和字体
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/,
+ use: [
+ 'file-loader'
+ ]
+ },
+ {
+ test: /\.(woff|woff2|eot|ttf|otf)$/,
+ use: [
+ 'file-loader'
+ ]
+ }
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
npm i file-loader -D
url-loader也可以处理图片和字体,它的内部其实也是用到了file-loader。好在它还可以设置较小资源自动base64。
module: { rules: [ + { + test: /\.(png|svg|jpg|gif)$/, + use: [{ + loader: 'url-loader’, + options: { + limit: 10240 //单位:字节 10k大小 + } + }] + } ] }
1
2
3
4
5
6
7
8
9
10
11
12
13对于比较大的文件可以这么处理:
- 将大文件发布到cdn,以cdn 的方式引入,而不打入包中。
- 将大文件发布到cdn,以cdn 的方式引入,而不打入包中。
webpack 目前的打包入口只能是以 js 为入口的,暂时还不支持以 html 为入口进行打包,也就是 webpack 默认是不会分析 html 文件里面的依赖(比如 src=xxx 或者 外部 css 中的语法)。
比如在html里用img的src引用图片。 解决办法:可以增加 html-loader 去处理 html,这样的话可以识别的了 img:src 这个属性。html-loader 提供了解析 html 里面的图片引入的能力
通过 link 去引入的css里面的图片处理。 解决办法:思路和 html-loader 比较像,可以去编写一个 loader 用于解析 html 的 link 语法,如果发现引入的是 css,那么对这个 css 的语法和里面的图片依赖进行解析,并且打包出一份新的 css 文件放到 dist 目录。
# 1.2.9 文件监听
- ⽂件监听是在发现源码发⽣变化时,⾃动重新构建出新的输出⽂件。
- webpack 开启监听模式,有两种⽅式:
- 启动 webpack 命令时,带上 --watch 参数。
- 在配置 webpack.config.js 中设置 watch: true。
- 唯⼀缺陷:每次需要⼿动刷新浏览器。
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "Hello webpack",
"main": "index.js",
"scripts": {
"build": "webpack ",
+ "watch": "webpack --watch"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2
3
4
5
6
7
8
9
10
11
12
13
原理分析
某个⽂件发⽣了变化,并不会⽴刻告诉监听者,⽽是先缓存起来,等 aggregateTimeout。
module.export = { //默认 false,也就是不开启 watch: true, //只有开启监听模式时,watchOptions才有意义 wathcOptions: { //默认为空,不监听的文件或者文件夹,支持正则匹配 ignored: /node_modules/, //监听到变化发生后会等300ms再去执行,默认300ms aggregateTimeout: 300, //判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒检查一次 poll: 1000 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.2.10 热更新及原理分析
webpack-dev-server
WDS 不刷新浏览器。
WDS 不输出⽂件,⽽是放在内存中,而不是放在本地磁盘文件里,构建速度更快。
使⽤ HotModuleReplacementPlugin插件(webpack内置)。
热更新有最核心的是 HMR Server 和 HMR runtime。 HMR Server 是服务端,用来将变化的 js 模块通过 websocket 的消息通知给浏览器端。
HMR Runtime是浏览器端,用于接受 HMR Server 传递的模块数据,浏览器端可以看到 .hot-update.json 的文件过来。
webpack 构建出来的 bundle.js 本身是不具备热更新的能力的,HotModuleReplacementPlugin 的作用就是将 HMR runtime 注入到 bundle.js,使得bundle.js可以和HMR server建立websocket的通信连接。
// package.json
{
"name": "hello-webpack",
"version": "1.0.0",
"description": "Hello webpack",
"main": "index.js",
"scripts": {
"build": "webpack ",
// open表示每次构建完成后自动开启一个浏览器
+ ”dev": "webpack-dev-server --open"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使⽤ webpack-dev-middleware
WDM 将 webpack 输出的⽂件传输给服务器 适⽤于灵活的定制场景
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-devmiddleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n');
});
2
3
4
5
6
7
8
9
10
11
12
原理分析
Webpack Compile: 将 JS 编译成 Bundle HMR Server: 将热更新的⽂件输出给 HMR Rumtime Bundle server: 提供⽂件在浏览器的访问 HMR Rumtime: 会被注⼊到浏览器, 更新⽂件的变化 bundle.js: 构建输出的⽂件
# 1.2.11 文件指纹策略:chunkhash、contenthash和hash
打包后输出的⽂件名的后缀
可以用作版本管理
没有更新的文件就可以继续使用本地浏览器的缓存
文件指纹如何生成?
- Hash:和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更
- Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值
- JS文件一般就用这个,这样打包一个文件就不会也影响另一个文件的 hash 值发生变化。
- 对于chunkhassh 还是有问题的, 当我们的 entry 文件发生改变(新增或删除)的时候,原先的 chunks ID 就有可能发生变化, 所以, 建议用HashedModuleIdsPlugin根据路径生成hashid。
- Contenthash:根据⽂件内容来定义 hash ,⽂件内容不变,则 contenthash 不变
- CSS文件一般就用这个。如果某一个页面用了JS资源和CSS资源,并且CSS也用的是Chunkhash的话,就会出现CSS的hash值随JS的变化而变化。
JS文件指纹设置
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: { // 设置 output 的 filename,使⽤ [chunkhash]
+ filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
}
};
2
3
4
5
6
7
8
9
10
- CSS文件指纹设置
module.exports = {
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path: __dirname + '/dist'
},
plugins: [ // 设置 MiniCssExtractPlugin 的 filename,使⽤ [contenthash]
// 用这个插件可以把style.loader从css.loader提出出来的css打包成独立文件
+ new MiniCssExtractPlugin({
+ filename: `[name][contenthash:8].css
+ });
]
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
install i mini-css-extra-plugin -D // 插件的使用还是要看一下官方文档
- 图片文件指纹设置
- 设置 file-loader 的 name,使⽤ [hash]
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [{
loader: 'file-loader’,
+ options: {
// hash值有32位,这里表示取前8位。
// 可以加个下划线,方便区分:[name]_[hash]
+ name: 'img/[name][hash:8].[ext] '
+ }
}]
}
]
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1.2.12 HTML、CSS和JS代码压缩
JS文件的压缩
内置了 uglifyjs-webpack-plugin
CSS文件的压缩
使⽤ optimize-css-assets-webpack-plugin
同时使⽤ cssnano
module.exports = { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name][chunkhash:8].js', path: __dirname + '/dist' }, plugins: [ + new OptimizeCSSAssetsPlugin({ + assetNameRegExp: /\.css$/g, + cssProcessor: require('cssnano’) + }) ] };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16html ⽂件的压缩
修改 html-webpack-plugin,设置压缩参数
module.exports = { entry: { app: './src/app.js', search: './src/search.js' }, output: { filename: '[name][chunkhash:8].js', path: __dirname + '/dist' }, plugins: [ + new HtmlWebpackPlugin({ + template: path.join(__dirname, 'src/search.html’), + filename: 'search.html’, + chunks: ['search’], + inject: true, + minify: { + html5: true, + collapseWhitespace: true, + preserveLineBreaks: false, + minifyCSS: true, + minifyJS: true, + removeComments: false + } + }) ] };
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