webpack构建分享
- 为什么选择webpack?(文档梳理于2020年4月)
- 支持多种模块标准。AMD、CommonJS、ES6 Module。其他工具大多支持一两种,webpack可以帮我们处理好不同类型模块之间的依赖关系。
- 完备的code splitting解决方案,可以有效的减小资源体积,提升首页渲染速度
- 可以处理各种资源,除了js外,webpack可以处理样式、模板,图片等。
- 拥有庞大的社区支持。webpack除了核心库以外, 还有无数开发者来编写周边插件和工具,绝大多数需求你可以有多个解决方案选型。
一、命令行打包
npx webpack --entry=./index.js --output-filename=bundle.js --mode=development
复制代码
命令中的–xx 其实就是我们常用的配置文件中的配置项。
...
"scripts" : {
"build": "webpack --entry=./index.js --output-filename=bundle.js --mode=development"
}
...
复制代码
二、CommonJS 和 ES6 Module对比
- CommonJS模块导入
- 模块第一次被加载,这是首先执行该模块js代码,然后导出内容。
- 模块曾经被加载过,这是模块代码不再次执行,直接导出上次执行结果。
一旦引入过一次,module对象loaded属性默认false, 加载后置为true
// foo.js
console.log("running foo.js")
module.exports = {
name: 'foo',
add: function(a, b) {
return a + b;
}
}
// index.js
const add = require("./foo.js").add; // 一个地方引入
const sum = add(2, 3);
console.log("sum": sum)
const moduleName = require("./foo.js").name; // 两个地方引入
console.log("end");
// 输出
// running foo.js // 输出一次
// sum: 5
// ends
复制代码
- ES6 module(自动采用严格模式)
- 动态与静态。
CommonJS的模块依赖关系发生在代码运行阶段
ES6 Module 的模块依赖关系发生在代码编译阶段。
ES6 Module的导入导出语句是声明式的,不支持表达式,变量。因此是一种静态模块结构。
编译阶段就可以建里模块依赖关系。
优点:
- 死代码检测和排除。
- 相对于CommonJS ,无论哪种情况都是导入module.而ES6 Module 直接导入变量,减少引用层及,效率更高。
- 值拷贝与动态映射(mp/index.js)
- 循环依赖。(mp/index.js)
CommonJS中循环依赖会有问题。
ES6 Module 是动态映射,所以会随着原有模块中的值的变化而变化。
三、webpack基础配置文件webpack.config.js
配置文件:指示webpack干了哪些活,运行webpack时,会加载里边的配置,配置文件遵循commonJS语法 ,基于nodejs平台运行。
- context: 资源入口entry的路径前缀,使用绝对路径的形式.
- entry: 包括多种形式: 字符串,数组,对象,函数。
字符串:直接写文件路径。
数组:作用是将多个资源先合并.最后一个元素作为实际的入口路径。等同于在入口文件引入数组前边得模块。
对象:定义多入口key是chunkname名字,value是入口路径。
- 使用数组或者字符串定义单入口无法改变chunk name,只能使用默认chunkname。
函数:
module.exports = {
...
entry: () => ({
index: ["babel-polyfill", "./src/index.js"],
lib: './scr/lib.js',
vender: ["react", "react-dom"]
})
...
}
复制代码
- entry中可以提取vender,一般vender指第三方模块集中打包产生的bundle。
webpack3和4在提取公共模块都给出了不同的解决方案。无需指定vender。
- output: 配置资源出口
module.exports = {
...
output: {
filename: './[name].[hash:8].js' // 输出文件名称
path: path.join(__dirname, './src') // 文件路径前缀 必须使用绝对路径
// 由于chunkFilename没有[name],所以
// 打包出来会有很多 1.sd8s9s9d.js 这种就是打包出来的异步chunk.
chunkFilename: './[name].[hash:8].js' // chunk 输出文件名称
publicPath: '' // 资源加载路径
}
...
}
复制代码
[] 中变量名称包括:
[contenthash]: 根据文件内容生成hash值, 不同文件hash一定不一样
作用:1. 区分不同的chunk 2. 控制客户端缓存。hash和chunkhash,chunk内容改变时,文件名称会发生变化,从而使用户下一次请求请求新资源,不会使用本地缓存。
- publicPath: 很多webpack的插件包括WDS中都设有publicPath属性,建议生产环境和开发环境的publicPath设置成相同的。这样方便本地调试,
因为webpack会在引入js文件、css文件、图片文件的路径都是:根目录+publicPath+filename(name)
- resolve
module.exports = {
...
resolve: {
extensions: ['.js', '.vue', '.jsx'] // dynamic引入的时候 可省略扩展名称
alias: {
"@": path.join(__dirname, "src") // 创建别名 无需写太多../
},
modules: [] // 数组 webpack在打包模块时从 查找的文件.
}
...
}
复制代码
- 传统webpack打包开发环境是使用webpack-dev-server启动websocket监听打包文件,对客户端进行实时热更新。
// 没有输出 只会再内存中打包
devServer: {
publicPath: isEnvProduction ? path.join(__dirname, "./build") : '/',
port: 3100,
// 启用gzip压缩
compress: true,
// 开启HMR攻能, 重新生效需要重启
hot: true,
// 自动打开默认浏览器
open:true
},
复制代码
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js", // 开发环境 WDS
"dev:hot": "cross-env NODE_ENV=development webpack-dev-server --hot",
"dev:dashboard": "cross-env NODE_ENV=development webpack-dashboard -- webpack-dev-server",
"build": "cross-env NODE_ENV=production webpack --config webpack.config.js",
"build:size": "bundlesize"
},
复制代码
四、loader
与loader的相关配置项都在module对象中rules: [] 标识代表了模块处理规则。
- 常用的两项test和use
// use中的loader 顺序很关键,webpack打包时按照数组从后往前的顺序将资源交给loader处理。
// 最后生效的放在前边。
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: [loader, loader, loader]
use: [
{
loader: "",
options: {// loader的具体配置 有些loader用query代替本质没有太大区别
}
},
{
loader: ""
}
]
},
{
test: /\.css$/,
use: []
}
]
}
...
}
复制代码
- exclude和include
两个属性是为了优化打包速度
exclude表示忽略正则匹配到的文件,优先级更高
include表示只对匹配到的模块生效,
{
test: /\.js$/,
use: [loader, loader, loader],
exclude: /node_modules/ // 很多npm模块已经编译为es5的,没必要使用babel-loader额外处理
include: /src/ // includ
},
复制代码
- exclude优先级更高
{
exclude: /node_modules/,
include: /node_modules\/element-ui/ // 即使加上include 无法覆盖exclude
}
// 可以写成
{
exclude: /src\/lib/,
include: /src/ // 除了src/lib以外还是可以被匹配打包
}
复制代码
- rsource和issuer
resource 默认被加载的模块,上边是说的exclude和include就是对resource的配置
issuer 加载者,可以配置加载resource的模块是具体哪些。
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
issuer: {
test: /\.js$/,
include: /src/
}
}
复制代码
- enforce
enforce 决定loader的打包顺序。
loader默认都是normal类型,pre和post是两种需要自己配置的种类
pre: 是指在所有loader之前执行,保证代码没有被其他loader编译过
post: 是指在所有loader之后执行。
// 如eslint-loader 对代码进行质量检测 可以配置enforce: "pre"
{
test: /\.(js|jsx)$/,
use: ["eslint-loader"],
enforce: "pre"
}
复制代码
- oneOf
oneOf中的loader只会匹配一个。注意:不能有两个配置处理同一种类型文件。如果js文件又要babel-loader处理也要eslint-loader处理,就需要提取出一个.
module.exports = {
module: {
rules: {
{
test: /.js$/,
loader: "eslint-loader",
enforce: "pre"
}
oneOf: [
{
test: /.js$/,
loader: "babel-loader"
}
]
}
}
}
复制代码
- 常用loader
webpack本身不具有预处理器,它只认识javascript,对于其他类型资源要用对应的预编译器进行转义,变成webpack能够接收的形式再进行。
- babel-loader
用来处理es6+代码规范并将其编译为es5,因为babel-loader需要在exclude里边添加node_modules,否则babel将编译其中所有模块,严重影响打包速度。
babel-loader v8.x.x
: webpack与babel协同工作模块
@babel/core v7.x.x
: babel编译器核心模块
@babel/preset-env v7.x.x
: babel官方预置器, 可根据用户设置目标, 添加所需配置编译es6代码
{
test: /.(jsx|js)$/,
use:[
{
loader: "babel-loader",
options: {
cacheDirectory: true // 启用缓存机制, 重复打包未改变的模块防止重复编译
// 此处可接收一个字符串路径作为缓存路径
// 如果为true,路径为node_modules/.cache/babel-loader
// 见demo
}
}
]
}
复制代码
- .babelrc
.babelrc文件需要的配置项主要有预设(presets)和插件(plugins)。
(1)常用的presets:
- @babel/preset-env
// 是一个智能预设,可让您使用最新的JavaScript,而无需微观管理目标环境所需的语法转换(以及可选的浏览器polyfill)。这既使您的生活更轻松,又使JavaScript包更小!如babel-loader + @babel/preset-env可以转换箭头函数
// 地址:https://www.babeljs.cn/docs/babel-preset-env
["@babel/preset-env",{
"modules": false,
}],
复制代码
- @babel/polyfill
只需要引入即可。
可以解决所有兼容性问题,引入全部兼容性代码,会导致打包出来的代码体积过大。
- core-js
解决全部引入兼容处理,减小代码题集
无需引入@babel/polyfill
["@babel/preset-env",{
"modules": false,
// 按需加载 不全部引入兼容性
"useBuiltIns": "usage",
// 指定corejs版本
"corejs": {
"version": 3
},
// 指定兼容浏览器版本
"targets": {
"chrome": "60",
"firefox": "60",
"ie": "9"// 等等
}
}],
复制代码
- @babel/preset-react
{
// 包括
// @babel/plugin-syntax-jsx
// @babel/plugin-transform-react-jsx
// @babel/plugin-transform-react-display-name
// 开发环境配置
// @babel/plugin-transform-react-jsx-self
// @babel/plugin-transform-react-jsx-source
"presets": [
"@babel/preset-react"
]
}
复制代码
- @babel/preset-flow
- @babel/preset-typescript
(2)常用的plugins:
- react-hot-loader
- @babel/plugin-transform-runtime: 依赖@babel/runtime 需要同时安装
作用:
- 提高代码重用性,缩小编译后的代码体积。
- 防止污染全局作用域。(启用corejs配置)
babel-polyfill会将Promise等添加成全局变量,污染全局空间。
- @babel/plugin-proposal-decorators
类装饰器: 必须和 @babel/plugin-proposal-class-properties同事使用,
什么叫类装饰器呢?给类添加一些属性,方法“`javascript
@isTestable(true)
class MyClass { }
// 常见的
@hot
@withRouter
@connect
且@babel/plugin-proposal-decorators要在@babel/plugin-proposal-class-properties前边(官网)```javascript
{
"plugins": [
[
"@babel/plugin-proposal-decorators", // 装饰器
{
"legacy": true // 设置为true 必须在loose打开模式下使用
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true // babel编译时,对类的属性采用赋值表达式,而不是Object.defineProperty(更简洁)
}
]
]
}
复制代码
legacy如果设置为true,则loose模式必须打开
loose打开作用:“`javascript
// loose true 使用赋值表达式
class Bork {
static a = ‘foo’;
static b;
x = 'bar';
y;
复制代码
}
// loose false
var Bork = function Bork() {
babelHelpers.classCallCheck(this, Bork);
Object.defineProperty(this, “x”, {
configurable: true,
enumerable: true,
writable: true,
value: ‘bar’
});
Object.defineProperty(this, “y”, {
configurable: true,
enumerable: true,
writable: true,
value: void 0
});
};
Object.defineProperty(Bork, “a”, {
configurable: true,
enumerable: true,
writable: true,
value: ‘foo’
});
Object.defineProperty(Bork, “b”, {
configurable: true,
enumerable: true,
writable: true,
value: void 0
});
- @babel/plugin-syntax-dynamic-import
代码中支持动态导入支持例如:
路由懒加载,打包生成异步chunks, 实现splitChunks```javascript
import()
复制代码
- 总览:
{
"presets": [
["@babel/preset-env",{ "modules": false }],
"@babel/preset-react"
],
"plugins": [
"react-hot-loader/babel", // 热更新
"@babel/plugin-transform-runtime",
[
"@babel/plugin-proposal-decorators", // 装饰器
{
"legacy": true
}
],
[
"@babel/plugin-proposal-class-properties",
{
"loose": true
}
],
"@babel/plugin-syntax-dynamic-import", // 动态导入支持
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": true // `style: true` 会加载 less 文件
}] // 配置antd
]
}
复制代码
5.2 style-loader css-loader
css-loader: 处理css各种样式语法, 加载语法(@import、url())
style-loader: 把样式插入页面。要和css-loader配合使用,并且要在css-loader处理之后使用。
样式预编译预言下载相应的loader
- less: less less-loader
- sass: sass-loader node-sass
5.3 ts-loader
用来编译ts代码
5.4 file-loader
用于打包文件类型资源。并返回publicPath
npm i file-loader --save
// 此处打包出来的图片,就可以使用js加载图片了
{
test: /\.(png|jpg|gif)$/,
use: "file-loader"
}
import packageImage from 'big.png';
console.log(packageImage) // c6f482a9a192005e.........png
复制代码
- publicPath再回味:资源引入路径。
此处打包出来的图片是js将会从publicPath后拼入图片打包出来后的名称。
- file-loader可以指定path
引入规则:
publicPath + path(loader支持可以指定) + name
output: {
publicPath: path.join(__dirname, "build")
},
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: {
loader: "file-loader",
options: {
name: "static/image/[name].[hash:8].[ext]", // name 指定文件名 一般包含路径
path: '' // 指定文件打包路径
publicPath: 'static/image' // 找资源的计出路径 会覆盖output的路径
}
}
}
]
}
复制代码
5.5 url-loader
依赖file-loader, 和file-loader作用相似,不同点是可以是指一个limit,当大于limit时,file-loader一样返回publicPath,
小于limit时候返回base64文件。这样可以有效减少请求所带来的消耗
- url-loader 不可以配置path,因此大于limit的文件资源打包输出路径要写在name中。
5.6 antd
antd 内部样式采用使用less预备编译器
npm i antd less less-loader
所以webpack 需要配置对应的less loader
// .babelrc
["import", {
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css" // `style: true` 会加载 less 文件
}] // 配置antd
// webpack.config.js
use: [
"style-loader",
"css-loader",
{
loader: "less-loader",
options: {
javascriptEnabled: true //必须添加
}
}
]
复制代码
5.7 loader原理浅析
自定义loader:
5.7.1 基本实现
新建loader -> npm init -y -> 安装自定义loader -> 打包webpack -> 打包结果loader加载结果
5.7.2 缓存
当输入和其他依赖没有发生变化时,loader直接使用缓存。而不重新打包,webpack中使用this.cacheable进行控制。
module.exports = function() {
if (this.cacheable) {
this.cacheable() // webpack内置方法
}
}
复制代码
5.7.3 获取options
npm i loader-utils
使用loaderUtils 获取options对象
module.exports = function(content) {
const options = loaderUtils.getOptions(this) || {}
console.log("options", options)
}
复制代码
五、代码分片
实现高性能应用其中重要的一点是尽可能的让用户每次只加载必要的资源, 优先级不太高的资源则采用异步加载。Webpack的code splitting 可以把代码按照特定的形式进行拆分,使用户按需加载。
- CommonWebpackPlugin(Webpack 4以下)
它可以将多个Chunk中的部分提取出来,比如第三方库。
这样带来的好处有:
1.开发过程减少重复打包,提升开发速度
2.减小整体资源体积
3.分片后的代码可以更有效地利用客户端缓存
module.exports = {
...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'common', // 用于指定公共chunk的名字
filename: 'common.js', // 提取后资源名字
chunks: ["a", "b"], // 设置提取范围
minChunks: 3, // 至少有三个模块引用了该模块
// import from "./utils"
// Infinity 代码无限高,所有模块都不会被提取
})
// manifest提取webpack运行时
// 为了使用长效缓存策略,打包生成一个manifest.js文件,app.js 中的变化指挥影响manifest.js,而它是一个很小的文件。之前提取的vendor和hash都不会变化。
new webpack.optimize.CommonChunkPlugin({
name: 'manifest', // manifest的提取必须出现在后边,否则webpack无法正常提取模块
})
]
...
}
复制代码
缺点:
1.一个CommonsChunkPlugin只能提取一个vendor,如果要提取多个需要配置多个插件,增加重复代码。
2.manifest.js实际上会多加载一个资源,影响性能
3.CommonsChunkPlugin提取公共模块时候会破坏原有Chunk中依赖关系。
- optimization.splitChunks(Wepback 4)
webpack4 推荐使用,替代CommonsChunkPlugin, 每个chunk上有一个id(0,1,2…)有异步chunk没有name 只有id。
module.exports = {
mode: 'production',
optimization: {
splitChunks: {
chunks: 'all' // 此处指定all,指对所有chunk生效,否则默认只对异步chunk生效。
// 方便提取所有js文件的公共模块。
}
}
}
复制代码
splitChunks默认提取条件:
1.提取后chunk来自node_modules目录。
2.提取后js文件大于30k, css文件大于50k,都是gz前。如果太小优化效果不明显
3.在异步chunks中按需加载数小于等于5.因为每个请求需要消耗资源。如果某个npm包只在异步chunk中需要,首屏不需要引入。
4.首次加载,并行请求的提取资源数小于等于3.和上一条类似,首屏要求性能更高,所以默认值低。
- 配置:
splitChunks: {
chunks: "async", // 默认对异步chunk进行提取
minSize: { // 提取后最小体积
javascript: 30000,
style: 50000
},
maxSize: 0,
minChunks: 1, // 最小被引入次数
maxAsyncRequests: 5, // 最大异步chunk加载资源数
maxInitialRequests: 3 // 最大首屏加载资源数
automaticNamedelimiter: '~', // 命名分割符号
name: true, // 意味着splitChunks根据cacheGroups和作用范围为新生成的chunk命名,
// 以automaticNamedelimiter分隔
cacheGroups: { // 分离chunks时的规则,想禁用置为false。默认venders和default。
vendor: { // 提取所有node_modules的模块,
test: /[\\/]nodeModules[\\/]/,
priority: -10 // 优先级
},
default: { // 作用于被多次引用的模块
minChunks: 2, // 最小引用次数
priority: -20, // 优先级
reuseExestingChunks: true
}
}
}
复制代码
- 资源异步加载(异步chunks)
3.1官方主要推荐import()函数。与es6的import函数不同,通过import()函数加载的模块及其依赖会被异步的进行加载,并返回一个Promise对象。
3.2异步chunk没有名字所以使用chunk的id命名在output.chunkFilename定义异步chunkname(见demo)
六、样式处理
遗留问题:我们使用style-loader生成style标签插入到html,无法单独输出CSS文件。一般在生产环境中,我们希望样式存在于单独CSS文件使用link标签引入,而不是在style标签中,因为文件更有利于客户端进行缓存。(只需要开发环境使用)
需要做的事情:
- css不放在js中,防止js文件过大
- css提取压缩
- css引入与在js文件中的兼容
- webpack(4.0之前版本)
npm i extract-text-webpack-plugin --save
此插件在webpack4中已经不推荐使用
代码中[name]是指chunkname
fallback: 是指当插件无法提取样式时所采用的loader。
use: 是指在使用插件提取前先使用css-loader预编译样式文件。
const ExtractTextWebpackPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: new ExtractTextWebpackPlugin({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
plugins: [
new ExtractTextWebpackPlugin("[name].css")
]
}
复制代码
- mini-css-extract-plugin
自从webpack4.0开始官方推荐使用mini-css-extract-plugin
, 这是extract-text-webpack-plugin的升级版,比后者拥有更好的性能和特性。
(以前的webpack版本无法使用)。
支持异步加载css,当我们加载异步chunks中引用到的css,在之前的版本是通过link标签同步加载的。
举例: 假设a.js 通过import() 加载了b.js, 在b.js里边加载b.css。那么之前版本使用插件将同步加载b.css。而mini-css-extract-plugin会单独打包出一个0.css,这个样式文件时通过a.js动态插入link标签引入的。(此处可以把开发环境的样式打包配置修改一下演示)
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports ={
module: {
rules: [
{
test: /\.css$/,
use: [
loader: MiniCssExtractPlugin.loader,
option: {
publicPath: '../../'
}
]
}
]
}
plugins: [
new MiniCssExtractPlugin({
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[id].[contenthash:8].css'
}),
]
}
复制代码
1)无需配置fallback
2)publicPath是指资源引入路径,由于异步样式在js中引入所以需要向上层文件回退两级。
3)还需要配置异步资源名字,资源名字和chunk名称相同。
- 值得注意: 在开发环境中我们更加重视打包速度,所以不适用样式提取插件,直接使用style-loader插入style标签。
- postcss-loader
是一款使用插件去转换css得工具,有许多非常好用得插件,比如prefixer, cssnext以及cssModules。这些特性都是由对应的postcss插件去实现的。postcss的使用需要和webpack结合起来。
1)Autoprefixer: Autoprefixer将使用基于当前浏览器支持的特性和属性数据去为你添加前缀.
没有浏览器差异已经完全符合W3C标准的css2.1属性display,position等,Autoprefixer不会为其加前缀
而像css3属性transform就会为其加前缀,其中–webkit是chrome和safari前缀,–ms则是ie的前缀
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
option: {
publicPath: '../../'
}
},
{
loader: "css-loader"
},
{
loader: "postcss-loader",
options: {
ident: "postcss",
plugins: () => {
// 帮助postcss找到package.json中的browserslist:通过配置browserslist来设置css兼容性
require("postcss-preset-env")
}
}
}
]
}
]
}
}
// package.json
{
"browserslist": {
"development": [
"last 1 chrome version" // 兼容最近的一个chrome版本
"last 1 firefox version" // 兼容最近的一个firefox版本
"last 1 safari version" // 兼容最近的一个safari版本
]
"production": [
">0.2%", // 大于百分之99.8%的浏览器
"not dead", // 不兼容已经死的浏览器 不投入使用 IE10
"not op_mini all" // 不用op_mini的所有浏览器
]
}
}
// 参考地址: https://github.com/browserslist/browserslist
复制代码
.app {
display: none;
position: relative;
transform: rotate(45deg);
}
// -->> 转化成
.app {
display: none;
position: relative;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
复制代码
2)Stylelint: css质量检测工具, 像eslint一样,可以为其添加各种规则,来统一项目代码风格。
3)CSS Module: 无需额外安装模块,只需要再css-loader中options: {
modules: true
}
4)CSS Next(感兴趣了解一下): 好用的地方在于通过var()和calc()进行css属性值的计算,也有@apply这样的应用大段规则的写法,也可以借此去了解一些新的css草案特性
:root {
--fontSize: 1rem;
--mainColor: #12345678;
--centered: {
display: flex;
align-items: center;
justify-content: center;
}
}
body {
color: var(--mainColor);
font-size: var(--fontSize);
line-height: calc(var(--fontSize) * 1.5);
padding: calc((var(--fontSize) / 2) + 1px);
}
.centered {
@apply --centered;
}
// -->> 编译为
body {
color: rgba(18, 52, 86, 0.47059);
font-size: 16px;
font-size: 1rem;
line-height: 24px;
line-height: 1.5rem;
padding: calc(0.5rem + 1px);
}
.centered {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
复制代码
七、生产环境配置
生产环境中如何让用户更快的加载资源, 设计到资源的压缩, 如何添加环境变量优化打包、最大限度利用资源缓存。包括
- 环境变量使用
- sourceMap 使用
- 资源压缩
- 优化hash与缓存
- 动态HTML生成
- mode配置环境变量
webpack4中增加了一个mode配置,可以接收”production”和”development”;
这样做的webpack会自动添加很多适用于生产环境的配置项,目的是为了减少人为配置的复杂程度,转化成了更有语义性的配置。
webpack鼓励配置越写越少,而不是越来越多。
- 如果启用了Mode: “production”, webpack会自动定义好process.env.NODE_ENV = “production”;也可以人为的设置好环境变量,再根据环境变量配置mode。
mode选项 | 描述 | 特点 |
---|---|---|
development | 会将process.env.NODE_ENV设置位development,启用NamedChunksPlugin和NamedModulesPlugin | 会将 |
production | NODE_ENV置为production,启用flagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin, NoEmmitOnErrorPlugin,OccurrenceOrderPlugin,SideEffectsFlagPlugin和UglifyJsPlugin | 能让代码优化上线运行的环境 |
- source map
webpack对于源码的编译处理会癌变代码的结构,位置,甚至所处文件。因此每一步处理要生成source map,跟随文件一步步被传递,直到生成最后的map文件。这个文件后缀加上.map。
module.exports = {
...
devtool: "source-map"
...
}
// 在生成bundle文件同时,会在文件种加上一句注释来辨识对应的map文件。
// 当我们打开开发者工具时,map文件被同时加载
// map文件很大,但是只要不打开开发者工具,浏览器就不会加载他们。
// 因此对于用户影响不大。
// cheap: 不包含列信息
// module: 简化loader的sourcemap,支持babel预编译
// eval: 提高持续构建效率
复制代码
source有很多简略版本:cheap-source-map, eval-source-map
cheap-module-eval-source-map 推荐在开发环境使用,属于打包速度和源码信息还原的折中使用。
- 安全问题: source-map的问题,就是任何人都可以通过开发者工具: 看到项目的源码,对于安全问题是个隐患。
对于此webpac有两种解决策略: 1.hidden-source-map 2. nosources-source-map.
hidden-source-map: webpack仍然回打包出所有的map文件,但是js文件中不会添加对应的map文件引用。所以打开浏览器开发者工具是无法看到map文件的, 浏览器就无法对bundle文件进行解析。如果想要追溯源码需要借助一些第三方服务。如Sentry。docs.sentry.io/
nosources-source-map: 可以隐藏文件源码内容,但是在console面板仍然可以看到具体行数。对于找错误基本可以满足,安全性要比source-map高一些。
另外也可以通过nginx针对公司内网给.map文件设置白名单,这样在客户端就看不到.map文件了。
- 资源压缩
将资源发布到线上环境,我们通常会进行代码压缩,或者叫uglify。压缩后的代码基本上可读性极差,减小了打包体积,也在一定程度上提升了代码的安全性。
3.1 压缩js Webpack推荐使用terser-webpack-plugin
npm i terser-webpack-plugin
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
...
mode: "production", // 如果配置了生产模式,则启动默认压缩模式
optimization: {
minimize: true,
// 也可以手动覆盖默认模式
minimizer: [
// 压缩js 去除死代码
new TerserPlugin({
// 为了匹配.map
test: /\.js(\?.*)?$/i,
exclude: /\/excludes/,// 排除某些目录
// include: // 对某些目录生效
// cache // 是否缓存
// parallel: 是否允许多个进程同时压缩,传入数字指定进程数
// sourceMap:
// terserOption: {} 可以设置是否对变量重命名,兼容ie8等
}
}),
]
}
...
}
复制代码
3.2 压缩css Webpack推荐使用optimize-css-assets-webpack-plugin
npm i optimize-css-assets-webpack-plugin
之前的mini-css-extract-plugin是将样式提取出来,接着使用optimize-css-assets-webpack-plugin进行压缩,压缩器本质是用cssnano
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
...
mode: "production", // 如果配置了生产模式,则启动默认压缩模式
optimization: {
minimize: true,
// 也可以手动覆盖默认模式
minimizer: [
// 压缩css
// progecss
new OptimizeCSSAssetsPlugin({
// 生效范围, 之压缩匹配到的资源
assetNameRegExp: /\.css$/g,
// 压缩css所使用的处理器,默认cssnano
cssProcessor: require("cssnano"),
// 处理器配置
cssProcessorOptions: {
preset: ['default', { discardComments: { removeAll: true } }]
}
})
}),
]
}
...
}
复制代码
- 缓存
浏览器已经获取过的资源,就会被缓存下来,浏览器会在过期之前反复的使用本地缓存进行响应。当资源更新的时候,我们希望所有用户使用加载新得资源,此时最好的办法就是更改资源url,这样迫使用户加载更新的资源。
4.1 资源hash
- hash 例如:[hash:10].js 和 [hash:10].css。防止客户端修改代码,浏览器由于强缓存原因没有从新从服务器请求资源。
- chunkhash 根据chunk生成hash,但是有一些css是在js中引入的,所以如果改变了js文件的代码,相应的css的文件也会被重新打包,浏览器也会重新加载资源,这样就没有合理利用浏览器缓存
- [contenthash] 根据文件生成hash值,不同文件hash值一定不同。
4.2 babel 打包缓存是为了优化打包时间。
- 动态输出html
使用html-webpack-plugin(详见demo)
new HtmlWebpackPlugin({
title: "我的webpack",
template: 'public/index.html',
inject: true,
// 压缩html代码配置
minify: {
// 移除空格
collapseWhitespace: true,
// 移除注释
removeComments: true
}
// favicon: "./favicon.ico"
}),
复制代码
- bundle体积监控和分析
为了保证良好的用户体验,我们可以对bundle体积进行持续监控,防止不必要的冗余模块被添加。Vs Code种可使用Import Cost对引入模块的大小进行实时监控。
Webpack打包有一个插件 webpack-bundle-analyzer : 生成一张bundle的模块组成结构图,每个模块体积一目了然,自动化怼资源体积进行监控。
const BundleAnalyzer = require("webpack-bundle-analyzer");
module.exports = {
...
mode: "production", // 如果配置了生产模式,则启动默认压缩模式
plugins: [
new BundleAnalyzer()
]
...
}
复制代码
我们也可以将其作为自动化测试的一部分,来保证输出资源如果超限了,可以在知情的情况发布出去。
八、打包优化及开发环境配置
8.1 HappyPack
8.2 noParse
8.3 Tree Shaking
8.4 webpack-dashboard
8.5 HMR
- HappyPack
npm i happypack
这个打包工作可以显著缩短打包时间:
(1) 从配置文件入口获取打包入口
(2) 匹配loader规则,并对入口模块编译
(3) 查找依赖对新模块重复进行第2步骤,递归的过程
webpack 需要一步步的单线程的转移,一个模块可能依赖几个模块,这些模块之间没有依赖关系,webpack也只能逐个转义。happypack可以开启多个线程,对不同模块转义,提升本地打包速度。
const HappyPack = require("happypack")
module.exports = {
// 所有的loader配置写入对应id的插件中
...
module: {
rules: [
{
test: /\.js$/,
loader: 'happypack/loader?id=js'
}
]
},
plugins: [
new HappyPack({
id: 'js',
loader: 'babel-loader',
options: {} // babel-options
})
]
...
}
复制代码
- noParse
有些库我们不希望webpack去解析的,可以在module.noParse对其进行忽略。
module.exports = {
...
module: {
noParse: /lodash/
}
...
}
复制代码
这样webpack依然会对lodash进行打包, 只不过不会对其进行任何解析.
- tree shaking
tree shaking 在开发环境无效
,但是在生产环境的压缩会移除掉“死代码”.
// index.js
import { foo } from './utils.js';
foo();
// utils.js
export function foo() {
console.log("foo");
}
export function bar() {
console.log("bar");
}
// package.json
"sideEffects": false
"sideEffects": ["*.css", "*.less"] // 标记一些文件不被tree shaking
/*
所有代码都没有副作用,都可以进行tree shaking
问题:会把css, @babel等代码干掉
*/
复制代码
有些库本身的接口为了获得更好的兼容性,使用CommonJS导出的,尽管仅仅引用了这个库的一个接口,整个库也被加载进来,而打包出来的bundle体积没有减小,这是因为tree shaking
只能对ES6 Module 生效,但是该库是使用CommonJS 导出的。所以,tree shaking没有起作用。有的npm包,提供了es6 module 和commonJS两种导出形式,尽可能使用es6导出的模块.
[@babel/preset-env,{ modules: false }] 是禁用了babel的模块依赖解析,否则tree shaking 接收到的都是转换成commonJS的代码 无法进行。
- treeshaking 本身不能去除死代码。只是给死代码坐上标记,结合terser-webpack-plugin去除死代码。
- webpack-dashboard
目前windows10 只能查看主要的日志,不支持鼠标滚动(demo中)
plugins:[
// 开发环境:控制台几个面板展示不同方面的信息。log:webpack本身日志。
// modules参与打包的模块。problems:打包过程的错误和警告
isEnvDevelopment && new DashboardPlugin(),
]
复制代码
- HMR热替换
webpack-dev-server只要检测到代码改动就会自动重新构建,然后出发网页刷新。这种一般被称为live-reload。热替换再live-reload的计出上又进了一步,可以让代码再网页不刷新的前提下得到最新的改动,我们甚至不需要重新发起请求就能看到更新后的效果。Hot Module Replacement, HMR。
webpack本身提供了HMR插件,但是再触发过程中,会发生很多意想不到的问题,导致模块更新应用的表现和正常加载的表现不一致。
// webpack 内置HMR 方案
module.exports = {
// 所有的loader配置写入对应id的插件中
...
devServer: {
...
hot: true
},
plugins: [
// 开启devserver的HMR
isEnvDevelopment && new webpack.HotModuleReplacementPlugin()
]
...
}
复制代码
但是HMR本身有很多问题出现, 所以社区中涌现了许多第三方解决方案。比如react组件的热更新react-hot-loader处理。
npm i react-hot-loader
首先看需要更改哪些上层配置:
// .babelrc
"plugins": ["react-hot-loader/babel"]
// 保证react-hot-loader 在react 和react-dom前先加载
// webpack.config.js
resolve: {
alias: {
"react-dom": "@hot-loader/react-dom"
},
}
entry: [
isEnvDevelopment && 'react-hot-loader/patch',
"./index.js",
// paths.AppPaths
]
// 入口文件用hot 包裹整个外层组件
import {hot} from "react-hot-loader/root"
@hot
class App extends Component {
render() {
return (
<div></div>
)
}
}
export default App;
// scirpt
"webpack-dev-server --hot"
复制代码
这时就基本上实现了一个HRM。但是仍有不足当我们在asyncChunk中作修改无法热替换.所以要解决code splitting中的热替换, 所以如果需要异步热替换需要将异步chunk的chunk组件传入hot函数确保正确加载。
HMR原理:
- WDS与浏览器之间维护了一个webpacksocket, 当本地资源发生变化时, WDS会向浏览器推送更新事件, 并带上这次构建的hash, 让客户端与上一次资源进行比对。通过hash的比对防止冗余更新的出现。
- 当客户端知道新得构建和上一次构建的区别之后,就会像WDS发起一个请求来更改文件列表,即哪些文件有了改动,hto-update.json见(demo).
- 有了需要更新的chunk hash 客户端就可以继续向WDS发送一个请求获取更新增量hot-update.js
PS:
- create-react-app
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw), // react-app内部插件将%PUBLIC_URL% 替换成public目录下的文件
复制代码
- SizePlugins
计算两次打包体积差异, 可以监控体积变化尽早发现.