webpack
- 1. 简介
- 1.1 webpack 是什么
- 1.2 webpack 五个核心概念
- 1.2.1 入口 - Entry
- 1.2.2 出口 - Output
- 1.2.3 Loader
- 1.2.4 插件 - Plugins
- 1.2.6 模式 - Mode
- 2. webpack 初体验
- 2.1 初始化配置
- 2.1.1. 准备
- 2.1.2. 写代码
- 2.1.3 编译打包应用
- 3. webpack 开发环境的基本配置
- 3.1 打包样式资源
- 3.2 打包 HTML 资源
- 3.3 打包图片资源
- 3.4 打包其他资源
- 3.5 开发环境配置 - devServer
- 总结
- 4. webpack 生产环境的基本配置
- 4.1 提取 css 成单独文件
- 4.2 css 兼容性处理
- 4.3 压缩 css
- 4.4 js语法检查
- 4.5 js 兼容性处理
- 4.6 js 压缩
- 4.7 HTML 压缩
- 4.8 生产环境配置
- 5. webpack 优化配置
- 5.1 开发环境性能优化
- 5.1.1 优化打包构建速度
- HMR
- 5.1.2 优化代码调试
- source-map
- 5.2 生产环境性能优化
- 5.2.1 优化打包构建速度
- oneOf
- babel缓存
- 多进程打包
- externals
- dll
- 5.2.2 优化代码运行的性能
- 缓存(hash-chunkhash-contenthash)
- tree shaking
- code split
- 懒加载/预加载
- PWA(渐进式 Web 应用)配置详解
1. 简介
1.1 webpack 是什么
webpack 是一种前端资源构建工具,一个静态模块打包器(module bundler)。
在 webpack 看来, 前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理。
它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)。
1.2 webpack 五个核心概念
- entry:入口,指示打包的起点文件
- output:输出,指示打包资源bundles输出路径和命名
- loader:处理非js文件(webpack自身只理解js、json),相当于翻译官
- plugins:功能插件,可以做到优化、压缩、定义环境变量等
- mode:模式分为 development和production 模式
1.2.1 入口 - Entry
入口(Entry)是 webpack 分析构建内部依赖图的起点模块(模块就是一个文件)。
// webpack.config.js:
module.exports = {entry: './path/to/my/entry/file.js'
};
1.2.2 出口 - Output
output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist
。
/*webpack.config.js webpack的配置文件作用: 指示 webpack 干哪些活(当你运行 webpack 指令时,会加载里面的配置)所有构建工具都是基于nodejs平台运行的~模块化默认采用commonjs。
*/
const path = require('path');
module.exports = {entry: './path/to/my/entry/file.js',output: {// 输出路径:// --dirname: nodeJs的变量,代表当前文件的绝对路径// resolve: nodeJs里path模块的方法,用来拼接绝对路径path: path.resolve(__dirname, 'dist'),// 输出文件名filename: 'my-first-webpack.bundle.js'}
};
1.2.3 Loader
loader
让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解JavaScript);
loader
可以将所有类型的文件转换为 webpack 能够处理的有效模块。
loader的使用:
- 需要先下载;
- 无需
requrie
。
// webpack.config.js:
const { resolve } = require('path');
const config = {output: {filename: 'my-first-webpack.bundle.js',path: resolve(__dirname, 'dist')},module: {rules: [// 不同类型的文件必须配置不同的规则来处理{test: /\.css$/, // 匹配什么样的文件// use数组中loader的执行顺序:从下到上,从右到左(后进先出)use: ['style-loader', 'css-loader' ]}]}
};
module.exports = config;
1.2.4 插件 - Plugins
插件(Plugins)可以用于执行范围更广(比loader)的任务。插件的范围包括:从打包优化和压缩,一直到重新定义环境中的变量等。
plugin的使用:
- 需要先下载;
- 还需
requrie
。
// webpack.config.js:
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件
const config = {module: {rules: [{ test: /\.txt$/, use: 'raw-loader' }]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})]
};
module.exports = config;
1.2.6 模式 - Mode
模式(Mode)指示 webpack 使用相应模式的配置;
- mode分为 ‘development’ 和 ‘production’;
- 生产环境和开发环境将 ES6 模块化编译成浏览其能识别的模块化;
- 生产环境比开发环境多一个压缩js代码;
选项 | 描述 | 特点 |
---|---|---|
development | 会将DefinePlugin 中process.env.NODE_ENV 的值设置为development 。启用NamedChunksPlugin 和NamedModulesPlugin 。 | 能让代码本地调试运行的环境 |
production | 会将DefinePlugin 中process.env.NODE_ENV 的值设置为production 。启用FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 TerserPlugin 。 | 能让代码优化上线运行的环境 |
// webpack.config.js:
module.exports = {mode: 'production'
};
2. webpack 初体验
2.1 初始化配置
2.1.1. 准备
- 初始化 package.json
- 下载并安装 webpack
//初始化 package.json
npm init
//下载并安装 webpack
npm i webpack webpack-cli -D
// 也下载loader和 plugins
npm i xxx-loader xxx-plugin -D
2.1.2. 写代码
需要在项目的根目录写一个名为webpack.config.js
的配置文件。
webpack初体验
├── src
│ └── index.js
│ └── test.json
├── webpack.config.js
├── package.json
代码
// 1. src/index.js
import data from './test.json';
console.log(data);
function add(x, y) {return x + y;
}
add(1, 2)
console.log(add(1, 2))// 2. src/test.json
{"testJson": "test json"
}// 3. webpack.config.js
const { resolve } = require('path'); //
module.exports = {entry: './src/index.js',output: {filename: 'built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: ['style-loader','css-loader' ]}]},plugins: [],mode: 'development'
}
2.1.3 编译打包应用
在package.json
中添加 脚本命令"dev": "SET NODE_OPTIONS=--openssl-legacy-provider & webpack"
//package.json
{"name": "webpack_test","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "SET NODE_OPTIONS=--openssl-legacy-provider & webpack"},"author": "","license": "ISC","devDependencies": {"css-loader": "^5.0.0","style-loader": "^2.0.0","webpack": "^4.41.6","webpack-cli": "^3.3.11"}
}
# 打包执行
npm run dev
3. webpack 开发环境的基本配置
3.1 打包样式资源
- 创建文件
- 下载安装 loader 包
npm i css-loader style-loader less-loader less -D
- 修改配置文件
/*webpack.config.js webpack的配置文件作用: 指示 webpack 干哪些活(当你运行 webpack 指令时,会加载里面的配置)所有构建工具都是基于nodejs平台运行的~模块化默认采用commonjs。
*/// resolve用来拼接绝对路径的方法
const { resolve } = require('path');module.exports = {// webpack配置// 入口起点entry: './src/index.js',// 输出output: {// 输出文件名filename: 'built.js',// 输出路径// __dirname nodejs的变量,代表当前文件的目录绝对路径path: resolve(__dirname, 'build')},// loader的配置module: {rules: [// 详细loader配置// 不同文件必须配置不同loader处理{// 匹配哪些文件test: /\.css$/,// 使用哪些loader进行处理use: [// use数组中loader执行顺序:从右到左,从下到上 依次执行// 创建style标签,将js中的样式资源插入进行,添加到head中生效'style-loader',// 将css文件变成commonjs模块加载js中,里面内容是样式字符串(转换后得到的commonjs模块可以理解为:用js给元素动态添加样式的那种代码);'css-loader']},{test: /\.less$/,use: ['style-loader','css-loader',// 将less文件编译成css文件// 需要下载 less-loader和less'less-loader']}]},// plugins的配置plugins: [// 详细plugins的配置],// 模式mode: 'development', // 开发模式// mode: 'production'
}
- 运行指令: webpack
3.2 打包 HTML 资源
-
创建文件
-
下载安装 plugin 包
npm install --save-dev html-webpack-plugin
- 修改配置文件
/*loader: 1. 下载 2. 使用(配置loader)plugins: 1. 下载 2. 引入 3. 使用
*/
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/index.js',output: {filename: 'built.js',path: resolve(__dirname, 'build')},module: {rules: [// loader的配置]},plugins: [// plugins的配置// html-webpack-plugin// 功能:默认会创建一个空的HTML,自动引入打包输出的所有资源(JS/CSS)// 需求:需要有结构的HTML文件/*** html-webpack-plugin* 1) 不传参的情况 - new HTMLWebpackPlugin():会在配置的output文件夹创建一个空的html, 自动引入打包输出的所有资源,包括js, css...* 2) 参数template:复制设置的'./src/index.html'文件到配置的output文件夹,并自动引入打包输出的所有资源*/new HtmlWebpackPlugin({// 复制 './src/index.html' 文件,并自动引入打包输出的所有资源(JS/CSS)template: './src/index.html'})],mode: 'development'
};
- 运行指令: webpack
3.3 打包图片资源
-
创建文件
-
下载安装 loader 包
npm install --save-dev html-loader url-loader file-loader
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/index.js',output: {filename: 'built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.less$/,// 要使用多个loader处理用useuse: ['style-loader', 'css-loader', 'less-loader']},{// 问题:默认处理不了html中img图片// 处理图片资源test: /\.(jpg|png|gif)$/,// 使用一个loader// 下载 url-loader file-loaderloader: 'url-loader',options: {// 图片大小小于8kb,就会被base64处理// 优点: 减少请求数量(减轻服务器压力)// 缺点:图片体积会更大(文件请求速度更慢)limit: 8 * 1024,// 问题:因为url-loader默认使用es6模块化解析,而html-loader引入图片是commonjs// 解析时会出问题:[object Module]// 解决:关闭url-loader的es6模块化,使用commonjs解析esModule: false,// 给图片进行重命名// [hash:10]取图片的hash的前10位// [ext]取文件原来扩展名name: '[hash:10].[ext]'}},{test: /\.html$/,// 处理html文件的img图片(负责引入img,从而能被url-loader进行处理)loader: 'html-loader'}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development'
};
- 运行指令: webpack
3.4 打包其他资源
-
创建文件
-
下载安装 loader 包
npm install --save-dev html-loader url-loader file-loader
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/index.js',output: {filename: 'built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: ['style-loader', 'css-loader']},// 打包其他资源(除了html/js/css资源以外的资源,比如字体文件等){// 排除css/js/html资源exclude: /\.(css|js|html|less)$/,loader: 'file-loader',options: {name: '[hash:10].[ext]'}}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development'
};
- 运行指令: webpack
3.5 开发环境配置 - devServer
- devServer:开发服务器,用来自动化(自动编译,自动打开浏览器,自动刷新浏览器)
- 特点:只会在内存中编译打包,不会有任何输出(不会在项目中新建一个文件夹)
-
创建文件
-
修改配置文件
/*开发环境配置:能让代码运行运行项目指令:webpack 会将打包结果输出出去npx webpack-dev-server 只会在内存中编译打包,没有输出
*/const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [// loader的配置{// 处理less资源test: /\.less$/,use: ['style-loader', 'css-loader', 'less-loader']},{// 处理css资源test: /\.css$/,use: ['style-loader', 'css-loader']},{// 处理图片资源test: /\.(jpg|png|gif)$/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',// 关闭es6模块化esModule: false,outputPath: 'imgs'}},{// 处理html中img资源test: /\.html$/,loader: 'html-loader'},{// 处理其他资源exclude: /\.(html|js|css|less|jpg|png|gif)/,loader: 'file-loader',options: {name: '[hash:10].[ext]',outputPath: 'media'}}]},plugins: [// plugins的配置new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development',devServer: {// 项目构建后路径contentBase: resolve(__dirname, 'build'),// 启动gzip压缩compress: true,// 端口号port: 3000,// 自动打开浏览器open: true}
};
- 运行指令:
npx webpack-dev-server
总结
- 打包样式资源:
- 安装
css-loader
,style-loader
,less-loader
,less
。 - 配置 webpack.config.js 来处理 .css 和 .less 文件。
- 安装
- 打包 HTML 资源:
- 安装
html-webpack-plugin
。 - 使用
HtmlWebpackPlugin
自动生成包含 JS/CSS 的 HTML 文件。
- 安装
- 打包图片资源:
- 安装
url-loader
,file-loader
,html-loader
。 - 配置
url-loader
处理图片资源,支持 base64 编码。
- 安装
- 打包其他资源:
- 使用
file-loader
处理非 CSS/JS/HTML 资源。
- 使用
- 开发环境配置:
- 配置
devServer
自动编译、自动打开浏览器并实时刷新。 - 设置
contentBase
,compress
,port
,open
等选项。
- 配置
4. webpack 生产环境的基本配置
4.1 提取 css 成单独文件
-
下载安装包
-
下载插件
npm install --save-dev mini-css-extract-plugin
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: [// 创建style标签,将样式放入// 'style-loader', // 这个loader取代style-loader。作用:提取js中的css成单独文件MiniCssExtractPlugin.loader,// 将css文件整合到js文件中'css-loader']}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'}),new MiniCssExtractPlugin({// 对输出的css文件进行重命名filename: 'css/built.css'})],mode: 'development'
};
- 运行指令: webpack
4.2 css 兼容性处理
-
创建文件
-
下载 loader
npm install --save-dev postcss-loader postcss-preset-env
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');// 设置nodejs环境变量
// process.env.NODE_ENV = 'development';module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: [MiniCssExtractPlugin.loader,'css-loader',/*css兼容性处理:postcss --> postcss-loader postcss-preset-env帮postcss找到package.json中browserslist里面的配置,通过配置加载指定的css兼容性样式"browserslist": {// 开发环境 --> 设置node环境变量:process.env.NODE_ENV = development"development": ["last 1 chrome version","last 1 firefox version","last 1 safari version"],// 生产环境:默认是看生产环境"production": [">0.2%","not dead","not op_mini all"]}*/// 使用loader的默认配置// 'postcss-loader',// 修改loader的配置{loader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [// postcss的插件require('postcss-preset-env')()]}}]}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'}),new MiniCssExtractPlugin({filename: 'css/built.css'})],mode: 'development'
};
- 修改 package.json
"browserslist": {"development": ["last 1 chrome version","last 1 firefox version","last 1 safari version"],"production": [">0.2%","not dead","not op_mini all"]
}
- 运行指令: webpack
4.3 压缩 css
- 创建文件
- 下载安装包
npm install --save-dev optimize-css-assets-webpack-plugin
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')// 设置nodejs环境变量
// process.env.NODE_ENV = 'development';module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: [MiniCssExtractPlugin.loader,'css-loader',{loader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [// postcss的插件require('postcss-preset-env')()]}}]}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'}),new MiniCssExtractPlugin({filename: 'css/built.css'}),// 压缩cssnew OptimizeCssAssetsWebpackPlugin()],mode: 'development'
};
- 运行指令: webpack
4.4 js语法检查
- 创建文件
- 下载安装包
npm install --save-dev eslint-loader eslint eslint-config-airbnb-base eslint-plugin-import
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [/*语法检查: eslint-loader eslint注意:只检查自己写的源代码,第三方的库是不用检查的设置检查规则:package.json中eslintConfig中设置~"eslintConfig": {"extends": "airbnb-base"}airbnb --> eslint-config-airbnb-base eslint-plugin-import eslint*/{test: /\.js$/,exclude: /node_modules/,loader: 'eslint-loader',options: {// 自动修复eslint的错误fix: true}}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development'
};
- 配置 package.json
"eslintConfig": {
"extends": "airbnb-base",
"env": {
"browser": true
}
}
- 运行指令: webpack
4.5 js 兼容性处理
JS兼容性处理
1)什么是JS兼容性处理?
其实就是把ES6及以上(后边简写为ES6+)新增的语法、API处理成ES5及以下的版本,解决某些浏览器(ie)上的兼容性报错问题。
2)Babel简介: Babel
是一个工具链,主要用于在当前和旧的浏览器或环境中,将ES6+代码转换为JavaScript向后兼容版本的代码。
3)polyfill是啥?
⁉️:不知道大家之前有没有过这种疑问:只知道babel是处理ES6+兼容的,polyfill分别是干嘛的😵?
🅰️:其实,ES6+语法和新增的API需要分开进行处理,polyfill是针对新增API的~(比如:箭头函数是新增语法,Promise是新增的API)
4)请大家跟我一起看一段代码(下面JS兼容性处理相关内容将以这个作为例子):console.log(add(2, 5));// 新API const promise = new Promise(resolve => { setTimeout(() => {console.log('定时器执行完了~');resolve(); }, 1000); });console.log(promise);
上面代码如果不做任何兼容性处理,运行结果如下: ie无法识别ES6+的内容
- 下载安装包
npm install --save-dev babel-loader @babel/core @babel/preset-env @babel/polyfill core-js
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [/*js兼容性处理:babel-loader @babel/core 1. 基本js兼容性处理 --> @babel/preset-env问题:只能转换基本语法,如promise高级语法不能转换2. 全部js兼容性处理 --> @babel/polyfill 问题:我只要解决部分兼容性问题,但是将所有兼容性代码全部引入,体积太大了~3. 需要做兼容性处理的就做:按需加载 --> core-js*/ {test: /\.js$/,exclude: /node_modules/,loader: 'babel-loader',options: {// 预设:指示babel做怎么样的兼容性处理presets: [['@babel/preset-env',{// 按需加载/** https://babeljs.io/docs/en/babel-preset-env#usebuiltins* useBuiltIns:配置@babel/preset-env如何处理polyfills,取“usage”,“entry”,“false”之一,默认为“false”* - 当使用usage或entry选项时,@babel/preset-env将添加对core-js模块的直接引用,类似import(或require)。这意味着core-js将相对于文件本身进行解析,并且按需引入。*/useBuiltIns: 'usage',// 指定core-js版本corejs: {version: 3},// 指定兼容性做到哪个版本浏览器targets: {chrome: '60',firefox: '60',ie: '9',safari: '10',edge: '17'}}]]}}]},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development'
};
- 运行指令: webpack
4.6 js 压缩
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],// 生产环境下会自动压缩js代码mode: 'production'
};
- 运行指令: webpack
4.7 HTML 压缩
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},plugins: [new HtmlWebpackPlugin({template: './src/index.html',// 压缩html代码minify: {// 移除空格collapseWhitespace: true,// 移除注释removeComments: true}})],mode: 'production'
};
- 运行指令:webpack
4.8 生产环境配置
const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';// 复用loader
const commonCssLoader = [MiniCssExtractPlugin.loader,'css-loader',{// 还需要在package.json中定义browserslistloader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [require('postcss-preset-env')()]}}
];module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [{test: /\.css$/,use: [...commonCssLoader]},{test: /\.less$/,use: [...commonCssLoader, 'less-loader']},/*正常来讲,一个文件只能被一个loader处理。当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:先执行eslint 在执行babel*/{// 在package.json中eslintConfig --> airbnbtest: /\.js$/,exclude: /node_modules/,// 优先执行enforce: 'pre',loader: 'eslint-loader',options: {fix: true}},{test: /\.js$/,exclude: /node_modules/,loader: 'babel-loader',options: {presets: [['@babel/preset-env',{useBuiltIns: 'usage',corejs: {version: 3},targets: {chrome: '60',firefox: '50'}}]]}},{test: /\.(jpg|png|gif)/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',outputPath: 'imgs',esModule: false}},{test: /\.html$/,loader: 'html-loader'},{exclude: /\.(js|css|less|html|jpg|png|gif)/,loader: 'file-loader',options: {outputPath: 'media'}}]},plugins: [new MiniCssExtractPlugin({filename: 'css/built.css'}),new OptimizeCssAssetsWebpackPlugin(),new HtmlWebpackPlugin({template: './src/index.html',minify: {collapseWhitespace: true,removeComments: true}})],mode: 'production'
};
5. webpack 优化配置
5.1 开发环境性能优化
5.1.1 优化打包构建速度
HMR
Hot Module Replacement,也称为HMR,是一种在应用程序运行时替换、添加或删除模块的技术,无需完全刷新页面就能更新这些模块。
- 修改 JS 文件,无需刷新页面,而能够直接在页面进行代码更新。
- 修改 CSS 文件,无需刷新页面,改动的样式能直接呈现。
- 修改配置文件
/*HMR: hot module replacement 热模块替换 / 模块热替换作用:一个模块发生变化,只会重新打包这一个模块(而不是打包所有模块) 极大提升构建速度样式文件:可以使用HMR功能:因为style-loader内部实现了~js文件:默认不能使用HMR功能 --> 需要修改js代码,添加支持HMR功能的代码注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。html文件: 默认不能使用HMR功能.同时会导致问题:html文件不能热更新了~ (不用做HMR功能)解决:修改entry入口,将html文件引入
*/const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: ['./src/js/index.js', './src/index.html'],output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [// loader的配置{// 处理less资源test: /\.less$/,use: ['style-loader', 'css-loader', 'less-loader']},{// 处理css资源test: /\.css$/,use: ['style-loader', 'css-loader']},{// 处理图片资源test: /\.(jpg|png|gif)$/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',// 关闭es6模块化esModule: false,outputPath: 'imgs'}},{// 处理html中img资源test: /\.html$/,loader: 'html-loader'},{// 处理其他资源exclude: /\.(html|js|css|less|jpg|png|gif)/,loader: 'file-loader',options: {name: '[hash:10].[ext]',outputPath: 'media'}}]},plugins: [// plugins的配置new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development',devServer: {contentBase: resolve(__dirname, 'build'),compress: true,port: 3000,open: true,// 开启HMR功能// 当修改了webpack配置,新配置要想生效,必须重新webpack服务hot: true}
};
- 运行指令: webpack
5.1.2 优化代码调试
source-map
Sourcemap 协议最初由 Google 设计并率先在 Closure Inspector
实现,它能够将经过压缩、混淆、合并的代码还原回未打包状态,帮助开发者在生产环境中精确定位问题发生的行列位置。
目前很多第三方库在发布的文件中都会同时提供一个 .map 后缀的 Source Map 文件。例如 jQuery。我们可以打开它的
Source Map 文件看一下,如下图所示:
这是一个 JSON 格式的文件,为了更容易阅读,我提前对该文件进行了格式化。这个 JSON
里面记录的就是转换后和转换前代码之间的映射关系,主要存在以下几个属性:
- version 是指定所使用的 Source Map 标准版本;
- sources 中记录的是转换前的源文件名称,因为有可能出现多个文件打包转换为一个文件的情况,所以这里是一个数组;
- names 是源代码中使用的一些成员名称,我们都知道一般压缩代码时会将我们开发阶段编写的有意义的变量名替换为一些简短的字符,这个属性中记录的就是原始的名称;
- mappings 属性,这个属性最为关键,它是一个叫作 base64-VLQ 编码的字符串,里面记录的信息就是转换后代码中的字符与转换前代码中的字符之间的映射关系
- 修改配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: ['./src/js/index.js', './src/index.html'],output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [// loader的配置{// 处理less资源test: /\.less$/,use: ['style-loader', 'css-loader', 'less-loader']},{// 处理css资源test: /\.css$/,use: ['style-loader', 'css-loader']},{// 处理图片资源test: /\.(jpg|png|gif)$/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',// 关闭es6模块化esModule: false,outputPath: 'imgs'}},{// 处理html中img资源test: /\.html$/,loader: 'html-loader'},{// 处理其他资源exclude: /\.(html|js|css|less|jpg|png|gif)/,loader: 'file-loader',options: {name: '[hash:10].[ext]',outputPath: 'media'}}]},plugins: [// plugins的配置new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'development',devServer: {contentBase: resolve(__dirname, 'build'),compress: true,port: 3000,open: true,hot: true},devtool: 'eval-source-map'
};/*source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-mapsource-map:外部错误代码准确信息 和 源代码的错误位置inline-source-map:内联只生成一个内联source-map错误代码准确信息 和 源代码的错误位置hidden-source-map:外部错误代码错误原因,但是没有错误位置不能追踪源代码错误,只能提示到构建后代码的错误位置eval-source-map:内联每一个文件都生成对应的source-map,都在eval错误代码准确信息 和 源代码的错误位置nosources-source-map:外部错误代码准确信息, 但是没有任何源代码信息cheap-source-map:外部错误代码准确信息 和 源代码的错误位置 只能精确的行cheap-module-source-map:外部错误代码准确信息 和 源代码的错误位置 module会将loader的source map加入内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快开发环境:速度快,调试更友好速度快(eval>inline>cheap>...)eval-cheap-souce-mapeval-source-map调试更友好 souce-mapcheap-module-souce-mapcheap-souce-map--> eval-source-map / eval-cheap-module-souce-map生产环境:源代码要不要隐藏? 调试要不要更友好内联会让代码体积变大,所以在生产环境不用内联nosources-source-map 全部隐藏hidden-source-map 只隐藏源代码,会提示构建后代码错误信息--> source-map / cheap-module-souce-map
*/
- 运行指令: webpack
5.2 生产环境性能优化
5.2.1 优化打包构建速度
oneOf
打包时每个文件都会经过所有 loader 处理,虽然因为 test
正则原因实际没有处理上,但是都要过一遍。比较慢。所以使用OneOf,匹配上一个 loader, 剩下的就不匹配了。开发模式和生产模式都可以用
配置文件
const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';// 复用loader
const commonCssLoader = [MiniCssExtractPlugin.loader,'css-loader',{// 还需要在package.json中定义browserslistloader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [require('postcss-preset-env')()]}}
];module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},module: {rules: [{// 在package.json中eslintConfig --> airbnbtest: /\.js$/,exclude: /node_modules/,// 优先执行enforce: 'pre',loader: 'eslint-loader',options: {fix: true}},{// 以下loader只会匹配一个// 注意:不能有两个配置处理同一种类型文件oneOf: [{test: /\.css$/,use: [...commonCssLoader]},{test: /\.less$/,use: [...commonCssLoader, 'less-loader']},/*正常来讲,一个文件只能被一个loader处理。当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:先执行eslint 在执行babel*/{test: /\.js$/,exclude: /node_modules/,loader: 'babel-loader',options: {presets: [['@babel/preset-env',{useBuiltIns: 'usage',corejs: {version: 3},targets: {chrome: '60',firefox: '50'}}]]}},{test: /\.(jpg|png|gif)/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',outputPath: 'imgs',esModule: false}},{test: /\.html$/,loader: 'html-loader'},{exclude: /\.(js|css|less|html|jpg|png|gif)/,loader: 'file-loader',options: {outputPath: 'media'}}]}]},plugins: [new MiniCssExtractPlugin({filename: 'css/built.css'}),new OptimizeCssAssetsWebpackPlugin(),new HtmlWebpackPlugin({template: './src/index.html',minify: {collapseWhitespace: true,removeComments: true}})],mode: 'production'
};
babel缓存
目标:让第二次打包构建速度更快。
在babel-loader配置中添加缓存配置:cacheDirectory: true
const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');/*缓存:babel缓存cacheDirectory: true--> 让第二次打包构建速度更快文件资源缓存hash: 每次wepack构建时会生成一个唯一的hash值。问题: 因为js和css同时使用一个hash值。如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样问题: js和css的hash值还是一样的因为css是在js中被引入的,所以同属于一个chunkcontenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样 --> 让代码上线运行缓存更好使用
*/// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';// 复用loader
const commonCssLoader = [MiniCssExtractPlugin.loader,'css-loader',{// 还需要在package.json中定义browserslistloader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [require('postcss-preset-env')()]}}
];module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.[contenthash:10].js',path: resolve(__dirname, 'build')},module: {rules: [{// 在package.json中eslintConfig --> airbnbtest: /\.js$/,exclude: /node_modules/,// 优先执行enforce: 'pre',loader: 'eslint-loader',options: {fix: true}},{// 以下loader只会匹配一个// 注意:不能有两个配置处理同一种类型文件oneOf: [{test: /\.css$/,use: [...commonCssLoader]},{test: /\.less$/,use: [...commonCssLoader, 'less-loader']},/*正常来讲,一个文件只能被一个loader处理。当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:先执行eslint 在执行babel*/{test: /\.js$/,exclude: /node_modules/,loader: 'babel-loader',options: {presets: [['@babel/preset-env',{useBuiltIns: 'usage',corejs: { version: 3 },targets: {chrome: '60',firefox: '50'}}]],// 开启babel缓存// 第二次构建时,会读取之前的缓存cacheDirectory: true}},{test: /\.(jpg|png|gif)/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[hash:10].[ext]',outputPath: 'imgs',esModule: false}},{test: /\.html$/,loader: 'html-loader'},{exclude: /\.(js|css|less|html|jpg|png|gif)/,loader: 'file-loader',options: {outputPath: 'media'}}]}]},plugins: [new MiniCssExtractPlugin({filename: 'css/built.[contenthash:10].css'}),new OptimizeCssAssetsWebpackPlugin(),new HtmlWebpackPlugin({template: './src/index.html',minify: {collapseWhitespace: true,removeComments: true}})],mode: 'production',devtool: 'source-map'
};
多进程打包
- 安装 loader 包
npm i thread-loader -D
- 修改配置文件
/*** 多进程打包* 1.下载thread-loader——》cnpm i thread-loader -D* 2.哪个loader需要多进程打包就使用该loader,一般给babel-loader使用,使用如下:* // 对js进行兼容性处理{test: /\.js$/,exclude: /node_modules/,use: [// 开启多进程打包// 启动进程600ms,进程通信也有开销// 只有工作消耗时间比较长,才使用多进程打包'thread-loader',{loader: 'babel-loader',options: {presets: ["@babel/preset-env"],plugins: [["@babel/plugin-transform-runtime"]],//开启缓存,第二次构建时,只处理发生变化的js文件cacheDirectory:true},},]},*/
const { resolve } = require('path');
// 引入workbox
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 将css单独提出文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 引入压缩插件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
// 将通用的loader抽离出来
// 设置nodejs的环境变量
// process.env.NODE_ENV = 'development';
const commonLoaders = [MiniCssExtractPlugin.loader, // 使用该loader将js中的css导入到文件中,并引入html文件'css-loader',{loader: 'postcss-loader',options: {postcssOptions: {ident: 'postcss',plugins: [['postcss-preset-env', {}]]}},},
]
module.exports = {mode: 'development',// 单入口// entry: './src/js/index.js',// 多入口:有几个入口就输出几个bundle// entry: {// index: './src/js/index.js',// test: './src/js/test.js',// },entry: ['babel-polyfill', './src/js/index.js', './src/index.html'],output: {filename: 'js/[name][contenthash:10].js',path: resolve(__dirname, 'build')},module: {rules: [{// 以下loader只会匹配一下,提升构建速度// 注意:不能有两个loader处理同一个文件oneOf: [// 对js进行兼容性处理{test: /\.js$/,exclude: /node_modules/,use: ['thread-loader',{loader: 'babel-loader',options: {presets: ["@babel/preset-env"],plugins: [["@babel/plugin-transform-runtime"]],//开启缓存,第二次构建时,只处理发生变化的js文件cacheDirectory:true},},]},// css文件处理{test: /\.css$/,use: [...commonLoaders],},// less文件处理{test: /\.less$/,use: [...commonLoaders,'less-loader'],},// image图片处理{test: /\.(jpg|png|gif)$/,type:"asset",//解析parser: {//转base64的条件dataUrlCondition: {maxSize: 8 * 1024, // 8kb}},generator:{ //与output.assetModuleFilename是相同的,这个写法引入的时候也会添加好这个路径filename:'img/[name][contenthash:10].[ext]',//打包后对资源的引入,文件命名已经有/img了publicPath:'./'},},// 处理html中的image资源{test: /\.html$/,loader: 'html-loader',generator:{ //与output.assetModuleFilename是相同的,这个写法引入的时候也会添加好这个路径filename:'img/[name][contenthash:10].[ext]',//打包后对资源的引入,文件命名已经有/img了publicPath:'./'},},// 字体图标等其他资源处理{exclude: /\.(html|css|js|jpg|png|gif)$/,type: 'asset/resource',generator:{ //与output.assetModuleFilename是相同的,这个写法引入的时候也会添加好这个路径filename:'otherSource/[name][contenthash:10].[ext]',//打包后对资源的引入,文件命名已经有/img了publicPath:'./'},},]},],},plugins: [new HtmlWebpackPlugin({template: './src/index.html',}),// 使用miniCssExtractPlugin插件将css提取为一个文件new MiniCssExtractPlugin({// 对输出的文件重命名filename: 'css/built[contenthash:10].css',}),// 调用压缩css插件new OptimizeCssAssetsWebpackPlugin(),// 调用PWA插件new WorkboxWebpackPlugin.GenerateSW({/*** 1.帮助serviceworker快速启动* 2.删除旧的serviceworker* 生成一个serviceworker的配置文件*/clientsClaim: true,skipWaiting: true,}),],devServer: {// 开发服务器项目路径static: resolve(__dirname, 'build'),// 是否开启gzip压缩compress: true,// 端口号port: 3000,// 是否自动打开浏览器open: true,// 打开HMR功能hot: true,},// 开启追踪源代码报错功能devtool: 'eval-source-map',// 使用代码分割功能/*** 可以将node_modules中的代码单独打包成一个trunk最终输出* 自动分析多入口文件中,是否有公共的依赖,如果有会打包成一个单独的trunk*/optimization: {splitChunks: {chunks: 'all'}},
}
externals
externals 配置项提供了阻止将某些 import 的包(package)打包到 bundle
中的功能,在运行时(runtime)再从外部获取这些扩展依赖(external dependencies)
externals用法:
module.exports={configureWebpack:congig =>{externals:{key: value}}
}
语法说明:
- key是第三方依赖库的名称,同package.json文件中的dependencies对象的key一样
- value值可以是字符串、数组、对象。应该是第三方依赖编译打包后生成的js(要用CDN引入的js文件)文件,执行后赋值给window的全局变量名称。
配置文件
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {entry: './src/js/index.js',output: {filename: 'js/built.js',path: resolve(__dirname, 'build')},plugins: [new HtmlWebpackPlugin({template: './src/index.html'})],mode: 'production',externals: {// 拒绝jQuery被打包进来jquery: 'jQuery'}
};
dll
dll的功能就是对代码分割的一种高级应用,将项目中很少变动的依赖先配置出来,提前打包在一个文件中,然后以script标签的方式注入到index.html文件中,接着在build配置中引入打包好的dll配置,这样build执行时会跳过dll中已经打包的模块,从而加快了build的打包时间
- 创建webpack.dll.js配置文件,配置需要单独打包的js
/*** 使用dll技术,对某些库(第三方库:jquery、react、vue...)进行单独打包* 当你运行webpack时,默认查找webpack.config.js配置文件* 需求:需要运行 webpack.dll.js 文件* --> webpack --config webpack.dll.js* 将下面配置的jquery单独打包到dll文件夹下,下面需要将打包的jquery重新引入到我们的项目中*/const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {mode: 'production',entry: {// 最终打包生成的[name]-->jquery// ['jquery']-->要打包的库是jqueryjquery: ['jquery']},// 输出文件output: {filename: '[name].js', // 生成的文件名path: resolve(__dirname, 'dll'),library: '[name]_[hash]', // 打包的库里面向外暴露出去的内容叫什么名字},plugins: [new webpack.DllPlugin({name: '[name]_[hash]', // 映射库的暴露的内容名称path: resolve(__dirname, 'dll/manifest.json') // 输出文件路径}),],
}
- 执行命令:webpack --config webpack.dll.js
- 修改配置webpack.config.js文件,将单独打包的js引入到我们的入口文件
/*** 将webpack.dll插件打包的文件导入到index.js中* 1、配置webpack.DllReferencePlugin,告诉webpack哪些包要打包,哪些不用打包,以及其映射名称* 2、下载安装:cnpm i add-asset-html-webpack-plugin -D* 3、配置插件:* // 将某个文件打包输出,并在html中引入该资源* // 将某个文件打包输出,并在html中引入该资源new AddAssetHtmlWebpackPlugin({// 指定要打包的文件路径filepath: resolve(__dirname, 'dll/jquery.js'),// 输出目录outputPath: 'dll',// html引入的路径publicPath: './dll',attributes: {nomodule: false,},}),*/
const { resolve } = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
module.exports = {// 模式mode: 'development',// 入口文件entry: './src/index.js',// 输出output: {filename: 'built.js',path: resolve(__dirname, 'build'),},module: {// 需要使用的loaderrules: []},// 插件plugins: [new HtmlWebpackPlugin({template: './src/index.html',}),// 告诉webpack哪些库不参与打包,并且使用名称也得变new webpack.DllReferencePlugin({manifest: resolve(__dirname, 'dll/manifest.json')}),// 将某个文件打包输出,并在html中引入该资源new AddAssetHtmlWebpackPlugin({// 指定要打包的文件路径filepath: resolve(__dirname, 'dll/jquery.js'),// 输出目录outputPath: 'dll',// html引入的路径publicPath: './dll',attributes: {nomodule: false,},}),],
}
5.2.2 优化代码运行的性能
缓存(hash-chunkhash-contenthash)
浏览器在多次访问静态资源文件的时候,如果该文件已经在本地缓存,并且它的名称没有改变,那么浏览器会认为它没有被更新,便不会去请求服务器,而是使用该静态资源的缓存版本。如果某个文件被更改,我们希望浏览器应该重新请求它,而不是使用缓存版本,所以文件被修改后,它的名称也应该被改变。
在 webpack 中,最后生成的 chunk 文件都是由一个个的 module 编译后组合而成的,浏览器请求的 JS文件实际上就是一个个的 chunk 。当我们修改某个 module 后,其所属 chunk 打包后的 JS文件名称应该也要发生变化,否则浏览器是不会请求最新版本的 JS 文件的!
输出文件名
在 webpack 中,有 3 种哈希占位符,通过它们修改文件名来控制缓存。
- hash:只要改动某个模块,所有 chunk 的 hash 都会被修改(不常用);
- chunkhash:只要改动某个模块,依赖它的 chunk 的 hash 就会被修改(常用);
- contenthash:只要改动某个模块,这个模块的 hash 就会被更改 ,通常用于被抽离的代码,如抽离 css 。如果不抽离该模块,contenthash 和 chunkhash 的产生的效果一致 (常用,webpack3 不支持 contenthash);
当 hash 变化后,文件名就会变化(如果输出文件名使用了上面的 3 中哈希占位符),浏览器就会去重新请求对应的资源。
hash
准备测试的文件。
//index1.js
console.log('index1.js');
import('./async').then((res) => {console.log(res);
})
//index2.js
console.log('index2.js');
import('./async').then((res) => {console.log(res);
})
//async.js
console.log('async.js');
export default {desc: 'async.js'
}
//webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var {CleanWebpackPlugin
} = require('clean-webpack-plugin');
module.exports = {mode: 'development',watch: true,entry: {index1: './index1.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',//配置hashfilename: '[name].[hash:16].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({template: './index.html'}),]
}
编译后控制台打印如下。
Asset Size Chunks Chunk Names0.a535734c8f8ddf89.js 515 bytes 0 [emitted] [immutable]index.html 374 bytes [emitted]
index1.a535734c8f8ddf89.js 8.54 KiB index1 [emitted] [immutable] index1
index2.a535734c8f8ddf89.js 8.53 KiB index2 [emitted] [immutable] index2
可以发现,所有 chunk 的 hash 都是 a535734c8f8ddf89 。
修改 async.js、index1.js 或 index2.js 其中的某个文件都将导致所有 chunk 重新构建,这里修改async.js 后,控制台打印如下。
Asset Size Chunks Chunk Names0.160f75be0b2a9d7a.js 519 bytes 0 [emitted] [immutable]index.html 374 bytes [emitted]
index1.160f75be0b2a9d7a.js 8.54 KiB index1 [emitted] [immutable] index1
index2.160f75be0b2a9d7a.js 8.53 KiB index2 [emitted] [immutable] index2
可以发现,每次的改动都将导致整个项目重新构建。
chunkhash
把 webpack.config.js 中输出配置名称中的 hash 改为 chunkhash 。
//webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var {CleanWebpackPlugin
} = require('clean-webpack-plugin');
module.exports = {mode: 'development',watch: true,entry: {index1: './index1.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].[chunkhash:16].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({template: './index.html'}),]
}
编译构建后,控制台打印如下。
Asset Size Chunks Chunk Names0.ef0a90ca4be41633.js 519 bytes 0 [emitted] [immutable]index.html 374 bytes [emitted]
index1.e04fb78630b9ed06.js 8.55 KiB index1 [emitted] [immutable] index1
index2.a82fa8cf316672d3.js 8.54 KiB index2 [emitted] [immutable] index2
可以发现每个 chunk 都有自己的专属 hash,在修改 index1.js 或 index2.js 的时候,也只会导致被修改的文件所属的
chunk 重新构建。
修改 index1.js 后,控制台打印如下。
Asset Size Chunks Chunk Namesindex.html 374 bytes [emitted]
index1.219644b9fa78ecf2.js 8.53 KiB index1 [emitted] [immutable] index1
修改 index2.js 后,控制台打印如下。
Asset Size Chunks Chunk Namesindex.html 374 bytes [emitted]
index2.2fb396992e95ea30.js 8.53 KiB index2 [emitted] [immutable] index2
修改 async.js 后,控制台打印如下。
Asset Size Chunks Chunk Names0.ef0a90ca4be41633.js 519 bytes 0 [emitted] [immutable]index.html 374 bytes [emitted]
index1.b19fb895cd7844da.js 8.53 KiB index1 [emitted] [immutable] index1
index2.89c07defcd285597.js 8.53 KiB index2 [emitted] [immutable] index2
index1.js 和 index2.js 所属的 chunk 重新构建了,这是因为 index1 chunk 和 index2 chunk需要懒加载 async.js ,而懒加载 async.js 是根据 async.js 所属的 chunk 打包后的文件名来加载的,当 async.js 发生变化后,它所属的 chunk 打包后的文件名会因为 hash 变化而变化,index1 chunk 和 index2 chunk 中懒加载 相关的的路径名称也跟着发生了变化,从而导致 index1 chunk 和 index2 chunk 的 hash变化而重新构建。
因为懒加载流程是属于 runtime (安装、加载和连接模块的逻辑)部分的代码,所以可以将 runtime 部分的代码抽离出来,然后再次修改 async.js ,发现 index1 chunk 和 index2 chunk 的代码就不会重新构建了。
//webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var {CleanWebpackPlugin
} = require('clean-webpack-plugin');
module.exports = {mode: 'development',watch: true,entry: {index1: './index1.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].[chunkhash:16].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({template: './index.html'}),],optimization: {// 'single' - 表示抽离所有 chunk 公用的 runtime 代码runtimeChunk: 'single' }
}
编译打包后,控制台打印如下。
Asset Size Chunks Chunk Names0.ef0a90ca4be41633.js 519 bytes 0 [emitted] [immutable]index.html 425 bytes [emitted]index1.ce2de40546b020f7.js 545 bytes index1 [emitted] [immutable] index1index2.cf1286e262093666.js 553 bytes index2 [emitted] [immutable] index2
runtime.a0f05e8cde7456ae.js 8.97 KiB runtime [emitted] [immutable] runtime
修改 async.js 后,控制台打印如下。
Asset Size Chunks Chunk Names0.0f79481d25aeed80.js 523 bytes 0 [emitted] [immutable]index.html 425 bytes [emitted]
runtime.fa3fbcea2005af1c.js 8.97 KiB runtime [emitted] [immutable] runtime
这下 index1.js 和 index2.js 对应的 chunk 便不会重新构建了,因为我们把连接模块的逻辑都抽离出来了,改变async.js 会导致 async.js 和 runtime 代码对应的 chunk 重新构建。
contenthash
contenthash 只有在抽离某个 module 的时候才会显示它的作用,否则它和 chunkhash 的表现一致,这里以抽离 css为例。先安装 css-loader 和 mini-css-extract-plugin 。
yarn add css-loader mini-css-extract-plugin -d
//index1.css
body , html {height: 100%;width: 100%;margin: 0;padding: 0;
}//index1.js
console.log('index1.js');
import('./async').then((res) => {console.log(res);
})
import './index1.css';//webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ProgressPlugin = require('webpack').ProgressPlugin;
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin').CleanWebpackPlugin;
module.exports = {mode: 'development',watch: true,entry: {index1: './index1.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].[chunkhash].js'},module: {rules: [{test: /\.css$/,use: [MiniCssExtractPlugin.loader, // 抽取css文件的loader,不再插入head里'css-loader']}]},plugins: [new ProgressPlugin(),new CleanWebpackPlugin(),new MiniCssExtractPlugin({filename: '[name].[contenthash].css'}),new HtmlWebpackPlugin({template: './index.html',})],optimization: {// 'single' - 表示抽离所有 chunk 公用的 runtime 代码runtimeChunk: 'single'}
}
编译打包后,控制台打印如下。
Asset Size Chunks Chunk Names0.a84498019577afde7588.js 1.07 KiB 0 [emitted] [immutable]index.html 499 bytes [emitted]index1.470f6e18ca06585dd954.js 1.07 KiB index1 [emitted] [immutable] index1
index1.8db6dca1b242dbbabe70.css 87 bytes index1 [emitted] [immutable] index1index2.82d191b5754b0e2e2ca8.js 537 bytes index2 [emitted] [immutable] index2
runtime.fca50e4fde877e19576a.js 8.98 KiB runtime [emitted] [immutable] runtime
修改 index1.js 后,控制台打印如下。
Asset Size Chunks Chunk Namesindex.html 499 bytes [emitted]
index1.2748748868607facc4a8.js 1.08 KiB index1 [emitted] [immutable] index1
index1.css 和 index1.js 都属于 index1 这个 chunk 。当修改 index1.js 后,并不会引起indx1.css 重新构建,因为 index1.css 不依赖 index1.js 。contenthash 的作用就是:当模块自身发生变化的时候,才会去重新构建这个模块。
tree shaking
Tree-shaking
它的名字来源于通过摇晃(shake)JavaScript代码的抽象语法树(AST),是一种用于优化JavaScript代码的技术,主要用于移除未被使用的代码,使得最终生成的代码包含应用程序中实际使用的部分。这主要用于减小应用程序的体积,提高加载性能。在前端开发中,特别是在使用模块化工具(如Webpack、Rollup等)构建应用程序时,通常会引入许多库和模块。然而,应用程序可能只使用了这些库的一小部分功能,导致最终生成的代码包含了大量未被使用的代码。Tree-shaking通过静态分析代码来确定哪些代码是可到达的并且被使用的,然后去除那些未被使用的部分。这样一来,可以显著减小最终部署的代码大小,提高应用程序的性能。
Tree Shaking 较早前由 Rich Harris 在 Rollup 中率先实现,Webpack 自 2.0
版本开始接入,已经成为一种应用广泛的性能优化手段。
1.首先,要明确一点:Tree Shaking 只支持 ESM 的引入方式,不支持 Common JS 的引入方式。
- ESM: export + import
- Common JS: module.exports + require
摇树优化是一种在打包编译时移除未使用代码的技术,可以显著减少最终打包文件的大小。为了使 Tree Shaking 有效工作,你的代码必须基于 ES6 模块的静态结构特性。
静态 vs. 非静态结构
-
静态结构:所有导入和导出都是固定的,不依赖于运行时条件。
import a from './a'; import b from './b'; export default a;
-
非静态结构:导入或导出依赖于运行时变量或条件语句。
console.log('index.js'); const flag = Math.random(); if (flag) {module.exports = 1; } else {module.exports = {}; }if (flag) {require('./a.js'); } else {require('./b.js'); }
准备 Demo
-
unused.js
:const a = 1; const b = () => {return 'test.js'; }; export {a, b};
-
math.js
:export function add(a, b) {return a + b; } export function sub(a, b) {return a - b; }
-
index.js
:import {add} from './math'; import * as unused from './unused';const result = add(55, 45); console.log(result);
开发环境测试
-
webpack.config.js
(开发模式):var HtmlWebpackPlugin = require('html-webpack-plugin'); var { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'development',entry: {index: './index.js',},optimization: {usedExports: true,},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({template: './index.html'}),] };
在开发环境下,即使配置了
optimization.usedExports: true
,WebPack 只会标记未使用的代码,并不会真正删除它们。
生产环境测试
-
webpack.config.js
(生产模式):var HtmlWebpackPlugin = require('html-webpack-plugin'); var { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'production',entry: {index: './index.js',},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({template: './index.html'}),] };
在生产模式下,WebPack 会对代码进行极致优化,未使用的代码会被移除。
sideEffects
sideEffects
是指有副作用的代码,例如修改全局变量或对象的属性。如果模块中包含这样的代码,那么即使该模块的部分导出未被使用,整个模块也可能不会被 Tree Shaking 掉。
-
如果项目中的所有模块都没有副作用,可以在
package.json
中设置"sideEffects": false
来加速 Tree Shaking 过程。 -
如果某些特定的模块具有副作用,可以在
package.json
中指定这些模块的路径。 -
package.json
示例:{"name": "webpack","version": "1.0.0","main": "index.js","license": "MIT","scripts": {"dev": "webpack"},"sideEffects": ["./math.js"] }
通过以上步骤,你可以有效地利用 Tree Shaking 技术来优化你的 JavaScript 应用程序。确保在生产构建中启用适当的 Webpack 设置以获得最佳效果。
code split
Webpack 提供了几种不同的方法来实现代码分离,以优化加载时间和用户体验。以下是三种主要的代码分离方式:
- 入口起点(Entry Points)
- 防止重复(使用 SplitChunksPlugin 去重和分离 chunk)
- 动态导入(通过模块的内联函数调用懒加载)
1. 入口起点
通过配置多个入口点,可以将项目代码分割成多个独立的 chunk。
-
index.js
:console.log('index.js');
-
index2.js
:console.log('index2.js');
-
webpack.config.js
:const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'development',entry: {index: './index.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({ template: './index.html' })] };
打包后的目录结构会包含两个独立的 chunk 文件:index.js
和 index2.js
。
2. 动态导入
利用 JavaScript 的动态 import()
语法,可以在运行时按需加载模块,从而实现代码的懒加载。
-
index.js
:console.log('index.js');import('./async').then((res) => {console.log(res); });import('./async2').then((res) => {console.log(res); });
-
webpack.config.js
:const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'development',entry: {index: './index.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].js',chunkFilename: '[name].async.js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({ template: './index.html' })] };
打包后的目录结构会包含三个 chunk 文件:index.js
、0.async.js
和 1.async.js
。
3. 防止重复(使用 SplitChunksPlugin)
当多个 chunk 引入了相同的模块时,可以通过 SplitChunksPlugin
来提取这些公共模块,避免重复打包。
-
initial-common.js
:function initialCommon(a, b) {return a * b; } var res = initialCommon(1, 2); console.log(res);
-
async-common.js
:function asyncCommon(a, b) {return a * b; } var res = asyncCommon(1, 2); console.log(res);
-
index1-async1-common.js
:function index1Async1Common(a, b) {return a * b; } var res = index1Async1Common(1, 2); console.log(res);
-
async1.js
:console.log('async1.js'); import './async-common'; import './index1-async1-common';
-
async2.js
:console.log('async2.js'); import './async-common';
-
index1.js
:console.log('index1.js'); import './initial-common'; import './index1-async1-common';import('./async1').then((res) => {console.log(res); });import('./async2').then((res) => {console.log(res); });
-
index2.js
:console.log('index2.js'); import './initial-common';
-
webpack.config.js
:const HtmlWebpackPlugin = require('html-webpack-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = {mode: 'development',entry: {index1: './index1.js',index2: './index2.js'},output: {publicPath: '',path: __dirname + '/dist',filename: '[name].js',chunkFilename: '[name].async.js'},module: {rules: []},plugins: [new CleanWebpackPlugin(),new HtmlWebpackPlugin({ template: './index.html' })],optimization: {splitChunks: {chunks: 'all',minSize: 0,minChunks: 2,automaticNameDelimiter: '~',cacheGroups: {vendors: {test: /[\\/]node_modules[\\/]/,priority: -10},common: {priority: -20,reuseExistingChunk: true}}}} };
打包后的目录结构将会根据配置生成相应的 chunk 文件,并且共享的代码会被提取到单独的文件中,例如 common~index1~index2.async.js
和 0.async.js
等。
懒加载/预加载
1. 基本配置
首先,我们需要一个基本的 Webpack 配置文件,这里使用单入口形式:
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = {mode: "production",entry: "./src/js/index.js", // 单入口output: {filename: "js/[name].[contenthash:10].js",path: path.resolve(__dirname, "build"),},plugins: [new HtmlWebpackPlugin({template: "./src/index.html", // 引用 HTML 模板minify: { // 压缩 HTMLcollapseInlineTagWhitespace: true,removeComments: true,}}),],optimization: {splitChunks: {chunks: 'all' // 可以将 node_modules 中的模块单独打包成一个 chunk}},devtool: 'source-map',
};
2. 懒加载 (Lazy Loading)
懒加载是一种按需加载代码的技术,它允许你只在需要时才加载特定的 JavaScript 模块。这可以显著减少初始加载时间,并提高应用性能。
2.1 动态导入语法
Webpack 支持 ES6 的 import()
语法来进行动态导入。这种语法返回一个 Promise,可以用来异步加载模块。
-
示例代码:
// src/js/index.js console.log('index.js 被加载了');document.getElementById('btn').onclick = async function() {const { mul } = await import(/* webpackChunkName: 'test' */ './test');console.log(mul(4, 5)); };
-
懒加载模块 (
src/js/test.js
):export function mul(a, b) {return a * b; }
-
HTML 文件 (
src/index.html
):<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Lazy Loading Example</title> </head> <body><h1>Lazy Loading with Webpack</h1><button id="btn">Load Test Module</button> </body> </html>
通过上述配置,当你点击按钮时,test.js
将会被按需加载,而不是在页面初始化时就加载。
3. 预加载 (Preloading)
预加载是一种在后台提前加载资源的技术,可以在用户真正需要这些资源之前就开始加载它们。这样可以减少用户等待的时间,提高用户体验。
3.1 使用 webpackPrefetch
你可以通过在动态导入语句中添加 webpackPrefetch: true
来实现预加载。
-
示例代码:
// src/js/index.js console.log('index.js 被加载了');document.getElementById('btn').onclick = async function() {const { mul } = await import(/* webpackChunkName: 'test', webpackPrefetch: true */ './test');console.log(mul(4, 5)); };
-
预加载模块 (
src/js/test.js
):export function mul(a, b) {return a * b; }
-
HTML 文件 (
src/index.html
):<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Preloading Example</title> </head> <body><h1>Preloading with Webpack</h1><button id="btn">Load Test Module</button> </body> </html>
在上述配置中,import(/* webpackChunkName: 'test', webpackPrefetch: true */ './test')
会生成一个预加载的 <link>
标签,以便在浏览器空闲时预先加载 test.js
。
4. 区别与总结
- 懒加载:通过动态导入
import()
语法,在需要时异步加载模块。 - 预加载:通过
webpackPrefetch: true
注释,在浏览器空闲时提前加载可能需要的资源。
区别:
- 预加载会在使用之前提前请求 JS 文件,但不会立即调用;懒加载是当文件被需要时才会请求。
- 正常加载被认为是并行加载(同一时间加载多个文件),但可能没有人使用,所以提倡懒加载。
- 懒加载中的预加载会在其他文件加载完毕后,在浏览器空闲的时候进行预加载,以备打算使用该模块的人等待时间过长。
通过合理使用懒加载和预加载,你可以显著提升应用的加载性能和用户体验。确保在实际项目中根据具体需求选择合适的加载方式,并进行适当的配置。
PWA(渐进式 Web 应用)配置详解
PWA,即渐进式 Web 应用,是一种利用现代 Web 技术来提供类似原生应用体验的 Web 应用。它支持离线访问、推送通知以及后台同步等功能,极大地增强了用户体验。本文将介绍如何使用 Webpack 配置一个 PWA 项目,并通过示例代码展示其核心功能。
1. Webpack 配置
为了构建一个 PWA,我们需要在 Webpack 配置中添加一些插件和加载器来处理 CSS、JavaScript 以及其他资源文件。此外,我们还需要使用 workbox-webpack-plugin
来生成 Service Worker 文件,以便实现离线缓存和其他 PWA 功能。
基本配置
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin"); // HTML 模板
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // 提取 CSS
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin"); // 压缩 CSS
const WorkboxWebpackPlugin = require('workbox-webpack-plugin'); // 使用 PWA
process.env.NODE_ENV = "production";// 复用 CSS 的 loader
let commonCssLoader = [{loader: MiniCssExtractPlugin.loader,options: {publicPath: '../', // 避免 CSS 中的路径引入错误}},'css-loader',{loader: 'postcss-loader',options: {ident: 'postcss',plugins: () => [require('postcss-preset-env')()]}}
];module.exports = {mode: "production", // 生产模式entry: "./src/js/index.js",output: {filename: "built.[contenthash:10].js",path: path.resolve(__dirname, "build")},module: {rules: [// CSS 和 Less{test: /\.css$/,use: [...commonCssLoader]},{test: /\.less$/,use: [...commonCssLoader, 'less-loader']},// JavaScript 兼容性处理{test: /\.js$/,exclude: /node_modules/,loader: "babel-loader",options: {presets: [["@babel/preset-env",{useBuiltIns: "usage",corejs: { version: 3 },targets: {chrome: "60",firefox: "60",ie: "9",safari: "10",edge: "17"}}]],cacheDirectory: true}},// 图片处理{test: /\.(jpg|png|gif)$/,loader: 'url-loader',options: {limit: 8 * 1024,name: '[contenthash:10].[ext]',outputPath: 'imgs',esModule: false}},// HTML 处理{test: /\.html$/,loader: 'html-loader'},// 其他资源处理{exclude: /\.(js|html|css|less|json|jpg|png|gif)$/,loader: 'file-loader',options: {outputPath: 'media'}}]},plugins: [new HtmlWebpackPlugin({template: "./src/index.html", // 引用 HTML 模板minify: { // 压缩 HTMLcollapseInlineTagWhitespace: true,removeComments: true}}),new MiniCssExtractPlugin({filename: "css/built.[contenthash:10].css" // 设置 CSS 输出}),new OptimizeCssAssetsWebpackPlugin(), // 压缩 CSS 插件new WorkboxWebpackPlugin.GenerateSW({ // 生成 Service WorkerclientsClaim: true,skipWaiting: true})]
};
2. 主入口文件 (index.js)
主入口文件需要导入样式和执行必要的逻辑,并且注册 Service Worker 以启用 PWA 特性。
import '../css/c.less';
import '../css/a.css';
import '../css/b.css';
import '../css/iconfont.css';
import '../css/index.css';
import { mul } from './test';const add = (x, y) => x + y;
new Promise((resolve) => {console.log('promise');setTimeout(() => {console.log('timeout');resolve();}, 0);
}).then(() => {console.log('p_end');
});
console.log(add(2, 3));console.log(mul(3, 3));// 注册 Service Worker
if ('serviceWorker' in navigator) {window.addEventListener('load', () => {navigator.serviceWorker.register('/service-worker.js').then((res) => {console.log('注册 service-worker 成功', res);}).catch((e) => {console.log(e);});});
}
3. ESLint 配置
如果你希望 ESLint 支持浏览器全局变量,可以在 package.json
中添加或修改 eslintConfig
字段:
{"eslintConfig": {"env": {"browser": true,"es6": true},"extends": "eslint:recommended","rules": {// 自定义规则}}
}
4. 离线访问验证
当你的 PWA 应用已经部署并且 Service Worker 已经注册后,你可以尝试断开网络连接并刷新页面,检查是否仍然可以正常加载。如果一切设置正确,你应该能看到应用依然可用,并且控制台会显示从 Service Worker 缓存中加载资源的信息。