Webpack概述
什么是Webpack
官方给出的定义:Webpack是现代JS应用程序的静态打包器(moudule bundler)。
Webpack是现在最流行的模块打包工具之一。
中文文档:webpack.docschina.org/
为什么要使用Webpack
- ES Modules 等新特性存在环境兼容问题
- 模块文件过多,网络请求频繁,影响效率
- 所有的前端资源都需要模块化
前面两个问题我们可以使用一系列模块化工具可以实现,而所有前端资源的模块化无法用一般的模块化工具实现。我们需要工具来统一解决这些问题,而它就是webpack打包工具。
总之,我们使用webpack打包工具是为了解决前端整体资源的模块化,并不单指JavaScript模块化。
Webpack核心概念
- 入口(Entry)
- 出口(Output)
- 加载器(Loader)
- 插件(Plugins)
- 模式(Mode)
- 模块(Module)
- 依赖图(Dependency Graph)
入口(Entry):打包时第一个访问的源码文件。默认是src/index.js(可以通过配置文件指定),Webpack通过入口,加载整个项目的依赖。
出口(Output):打包后输出的文件名称,默认是dist/main.js(可以通过配置文件指定)
加载器(Loader):专门用来处理一类文件(非JS)的工具。webpack默认只能识别JS文件,想要处理其他类型的文件,需要对应的loader,如(css-loader | html-loader | file-loader)。以-loader为后缀。
常用加载器:www.webpackjs.com/loaders/
插件(Plugins):实现loader之外的其他功能。
- Plugin是webpack的支柱,用来实现丰富的功能
- 命名方式xxx-webpack-plugin(如html-webpack-plugin),以-webpack-plugin为后缀
- 常用插件:www.webpackjs.com/plugins/
Loader和Plugins本质上都是npm包。
模式(Mode):用来区分环境的关键字。不同环境的打包逻辑不同,因此,需要区分。 三种模式:
- development(自动优化打包速度,添加一些调试过程中的辅助)
- production(自动优化打包结果)
- none(运行最原始的打包,不做任何额外处理)
模块(Module):Webpack中,模块的概念比较宽泛,一切皆为模块。
- JS模块
- 一段CSS
- 一张图片
- 一个字体文件 详情:www.webpackjs.com/concepts/mo…
依赖图(Dependency Graph):Webpack从入口文件开始加载整个项目的文件依赖,形成的依赖关系图就是依赖图。
Webpack最佳实践
初始化项目
mkdir myproject
cd myproject
npm init -y
安装Webpack
# 局部安装,推荐
npm install -D webpack webpack-cli
# 全局安装,不推荐,这会将你项目中的 webpack 锁定到指定版本,
# 并且在使用不同的 webpack 版本的项目中, 可能会导致构建失败
npm install -G webpack webpack-cli
创建入口文件,myproject/src/index.js
执行打包(必须指定mode,npm版本需在5以上)
npx webpack ./src/index.js --output-path ./dist --mode=development
Webpack版本
Webpack4于2018年2月发布
Webpack5于2020年10月发布
安装命令需要注意(默认安装5)
npm install webpack -D # webpack5
npm install webpack@4 -D #webpack4
添加配置文件
假如所有参数都用命令行来设置,会导致命令行特别长,难敲,也容易出错。因此,我们使用默认的设置文件要方便些。以后我们也都是在配置文件上进行配置,来实现我们想要的效果。
-
配置文件是用来简化命令行选项的
配置前:
npx webpack ./src/index.js --output-path ./dist --mode=development配置后:
npx webpack -
默认的配置文件名称是webpack.config.js
- webpack.config.js是以CommonJS规范进行组织的
- 使用Webpack的过程,大部分就是跟配置文件打交道的过程
配置详情:www.webpackjs.com/configurati…
常见配置项:
- mode(模式)
- entry(入口)
- output(出口)
- module(模块配置一不同类型文件的配置一loader配置)
- plugins(插件)
- devserver(开发服务器的配置) 最佳实践项目中,创建文件,myproject/webpack.config.js
内容为:
const path = require('path')
module.exports = {
mode: 'development',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
module: {
rules: [//这里添加loader的处理规则
//{
// test: /\.css$/i,
// use: ['style-loader', 'css-loader']
//}
]
},
// 开发服务器
devServer: {
},
// 插件配置
plugins: [
]
}
重新运行打包命令,即可实现打包:
npx webpack
Webpack基础
打包css
打包逻辑
- 非JS文件打包,需要对应的loader
- css-loader将css转化为JS(将css输出到打包后的JS文件中)
- 把包含css内容的JS代码,挂载到页面的
<style>标签当中
- 引入css
// 在js文件中调用css模块 import "./css/main.css" - 安装loader
npm i css-loader style-loader -D - 配置webpack.config.js
- 匹配后缀名:
// 表示正则匹配以.css结尾不区分大小写的文件 test: /\.css$/i,- 指定加载器:
// 先调用的loader要放在后面 use: ['style-loader', 'css-loader']
打包less
打包逻辑和上面类似:
- 引入less
// 在js文件中调用less模块 import "./css/main.less" - 安装loader
npm i less less-loader -D - 配置
- 匹配后缀名:
// 表示正则匹配以.less结尾不区分大小写的文件 test: /\.less$/i,- 指定加载器:
// 先调用的loader要放在后面 use: ['style-loader', 'css-loader', less-loader]
打包成独立的CSS文件
有的时候我们不想把css样式挂载到html文件的<style>标签中,而是单独打包成一个css文件。
- 安装插件
npm i mini-css-extract-plugin -D - 引入插件(在 webpack.config.js 中)
const MiniCssExtractPlugin = require('mini-css-extract-plugin') - 替换
style-loader(在 webpack.config.js 中)use: [MiniCssExtractPlugin.loader, 'css-loader']- style-loader:将css打包到
<style>标签中 - MiniCssExtractPlugin.loader:将CSS打包到独立文件中
- style-loader:将css打包到
- 配置插件(在 webpack.config.js 中)
有关mini-css-extract-plugin的详细配置信息,可以在npmjs官网上找到:www.npmjs.com/package/min…// 插件配置 plugins: [ new MiniCssExtractPlugin({ filename: 'css/[name].css' // 表示将文件打包到css文件夹中,文件名不变 }) ]
添加样式前缀
- 安装
npm install postcss-loader autoprefixer -D - 配置 webpack.config.js
use: [MiniCssExtractPlugin.loader,'css-loader','postcss-loader'] - 配置 postcss.config.js
plugins: [require('autoprefixer')] - 配置需要兼容的浏览器
- 方法一,package.json中指定 browserslist (推荐)
详情参考: www.npmjs.com/packaee/bro…"browserslist": [ "last 1 version", // 最新版本 "> 1%" // 代表全球使用率超过1%的浏览器 ] - 方法二,在项目根目录创建 .browserslistrc 文件
last 1 version > 1%
- 方法一,package.json中指定 browserslist (推荐)
格式校验
-
安装
npm i stylelint stylelint-config-standard stylelint-webpack-plugin -D-
stylelint
其中有很多校验规则,比如 number-teading-zero:
line-height: .5;// 错误
line-height: 0.5;// 正确
官网:stvlelint.io/ -
stylelint-config-standard
规则集,包含很多大公司所使用的校验规则,具体可以在github上看到: github.com/stvlelint/s…
-
stylelint-webpack-plugin
stylelint的webpack插件 webpack.docschina.org/plugins/stv…
-
-
引入 webpack.config.js
const StylelintPlugin = require('stylelint-webpack-plugin'); -
配置插件 webpack.config.js
// 插件配置 plugins: [ new StylelintPlugin({ // 指定需要格式校验的文件 files: ['src/css/*.{css,less,sass,scss}'] }) ] -
指定校验规则(在package.json中指定stylelint)
"stylelint": { "extends":"stylelint-config-standard", "rules": {//自定义规则 //"number-teading-zero": "never" } }指定现则配有三种方式。按加载的先后顺字依次是:
- 在 package.json 中的stylelint属性指定规则
- 在 .stylelintrc中指定规则
- 在 stylelint.config.js中指定规则 这里我们推荐第一种
压缩CSS
- 安装
npm install optimize-css-assets-webpack-plugin -D - 引入 webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); - 配置插件 webpack.config.js
plugins: [ new OptimizeCssAssetsPlugin() ]
打包HTML
打包HTML文件需要使用插件html-webpack-plugin,它的主要作用是:
- 生成HTML文件(用于服务器访问),并在HTML中加载所有的打包资源
- 指定HTML模板、设置HTML变量、压缩HTML
使用流程:
- 安装
npm i html-webpack-plugin -D - 引入 webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin') - 配置插件 webpack.config.js
plugins: [ new HtmlWebpackPlugin({ // 指定打包后的html名称 filename: 'index.html', // 使用html模板创建 // template: './src/index.html', // 指定html模板中使用的变量, 可使用EJS模板引擎的语法调用 // title: 'webpack Demo', // 设置html压缩, 一般只有在mode: 'production'时才压缩 minify: { collapseWhitespace: true, keepClosingSlash: true, removeComments: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, useShortDoctype: true } }), //打包其他的html文件 new HtmlWebpackPlugin({ // 指定打包后的html名称 filename: 'about.html', // 使用html模板创建 // template: './src/about.html', // 指定html模板中使用的变量, 可使用EJS模板引擎的语法调用 // title: 'about Demo' }) ]
详细使用方法:www.npmjs.com/package/htm…
EJS 官网:ejs.bootcss.com
打包JS
转译
转译目的:
将ES6+转成ES5,从而保证,巧在低版本浏览器的兼容性。
- 安装
@babel/core包含一系列转换插件的插件集,'@babel/preset-env'是转换规则集,包含各个版本最新的转换规则。npm install babel-loader @babel/core @babel/preset-env -D # 假如报错'源文本中存在无法识别的标记',就使用如下命令 # npm install babel-loader '@babel/core' '@babel/preset-env' -D - 配置 webpack.config.js
配置详见:www.npmjs.com/package/bab…module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { targets: "defaults" }] ], plugins: ['@babel/plugin-proposal-class-properties'] } } } ] }
但是,@babel/preset-env只能转译基本语法(promise不能转换),我们可以使用@babel/polyfill转译所有JS新语法。然而这种方法也有弊端,那就是它会将所有新语法转译,导致转译后的文件太大。于是,我们需要使用core-js。
-
@babel/polyfill安装npm i @babel/polyfill -D # 假如报错'源文本中存在无法识别的标记',就使用如下命令 # npm i '@babel/polyfill' -D -
@babel/polyfill引用(在入口文件中引用)import '@babel/polyfill' -
core-js安装npm i core-js -D -
core-js配置 (在webpack.config.js中)- 注释
@babel/polyfill引用
// 不注释掉,@babel/polyfill还是会将所有新语法转译 // import '@babel/polyfill'- 按需加载 useBuiltIns: 'usage'
- 指定版本 corejs: 3 // 在 package,json 中可以看到版本号
module: { rules: [ { test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, //targets: "defaults" // 手动指定兼容浏览器版本 targets: { Chrome: '58', ie: '9', firefox: '60', safari: '10', edge: '17' } } ] ], plugins: ['@babel/plugin-proposal-class-properties'] } } } ] } - 注释
JS代码格式校验
-
安装
npm i eslint eslint-config-airbnb-base eslint-webpack-plugin eslint-plugin-import -D-
eslint(校验JS代码格式的工具)
官网: eslint.org/
-
eslint-config-airbnb-base(最流行的JS代码格式规范)
-
eslint-webpack-plugin(Webpack的eslint插件)
-
eslint-plugin-import
用于在 package.json 中读取 eslintconfig 配置项
-
-
配置
-
eslint-webpack-plugin
在入口文件中引入:
const ESLintPlugin = require('eslint-webpack-plugin");在 webpack.config.js 中配置:
plugins: [ new ESLintPlugin({ // 自动解决常规的代码格式报错 fix: true }) ]忽略下一行代码的格式校验,只需要在相应报错代码前一行加入以下注释
//eslint-disable-next-line -
eslintConfig (在package.json中配置)
"eslintConfig": {"extends": "airbnb-base"}
-
打包图片
-
file-loader
将用到的图片复制到输出目录,用不到的图片不复制。
- 图片文件引入
// 在相关JS文件中进行图片文件引入 import img from './file.png'- 安装
npm i file-loader -D- 配置(在webpack.config.js中)
module: { rules: [ { test: /\.(png|jpe?g|gif)$/i, loader: 'file-loader', options: { name: '[name].[ext]', // [name]表示保留原文件名,.[ext]表示保留原后缀名 outputPath: 'images'// 输出到单独目录 } } ] }PS:按如上设置,将图片输出到单独目录时,css文件调用 url() 函数,可能会出现图片无法加载的情况,这是因为路径被改变导致的,我们只需要将设置中关于less和css的处理部分的
// less use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] // css use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']改成
// less use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader', 'postcss-loader', 'less-loader' ] // css use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader', 'postcss-loader' ] -
url-loader
一个网站包含各种大大小小的文件,假如每个文件都去请求获取就会造成效率低下,网页加载慢。这时候我们可以使用url-loader对文件资源进行处理。
url-loader可以设置处理原则:
- 小于一定大小的文件,会被打包成
Data URLs格式的URL - 大于一定大小的文件,则调用文件加载器 file-loader 进行处理
Data URLs是一种特殊的URL协议,用来表示文件。文件的内容就在URL中。通过这种方式,可以减少浏览器的请求。Data URLs的格式如下:简单举两个例子。
html文件内容,可表示为:
data:text/html;charset=UTF-8,<h1>html content<h1>图片文件内容,可表示为:
data:image/png;base64,iVBORwKGgoAAANSUhE...SuQmCCurl-loader 如何安装配置呢?答案如下:
- 安装
npm i url-loader -D- 配置(在webpack.config.js中) 将 file-loader 配置部分替换为如下配置:
module: { rules: [ { test: /\.(png|jpe?g|gif)$/i, loader: 'url-loader', options: { limit: 10 * 1024, // 小于10KB的文件转换成Data URLs字符串 // 大于 10KB 文件调用 file-loader 进行处理 name: '[name].[ext]',// [name]表示保留原文件名,.[ext]表示保留原后缀名 outputPath: 'images'// 输出到单独目录 } } ] } - 小于一定大小的文件,会被打包成
-
html-loader
当HTML代码中图片标签存在src属性时,我们发现打包后的HTML文件中该src内容并没有被处理,这样会导致图片无法被加载。因此,我们需要html-loader来处理。
html-loader的作用就是将HTML内容导出为字符串,负责引入img,从而能被url-loader处理。- 安装
npm i html-loader -D- 配置(在webpack.config.js中)
module: { rules: [ { test: /\.(png|jpe?g|gif)$/i, loader: 'url-loader', options: { limit: 10 * 1024, // 小于10KB的文件转换成Data URLs字符串 // 大于 10KB 文件调用 file-loader 进行处理 name: '[name].[ext]', // [name]表示保留原文件名,.[ext]表示保留原后缀名 outputPath: 'images', // 输出到单独目录 // url-loader 默认采用ES Modules规范进行解析,但是html-loader引入图片采用的是 CommonJS 规范,这会导致解析错误 // 解决: 关闭url-loader的ES Modules规范,强制采用CommonJS 规范 esModule: false } }, { test: /\.(htm|html)$/i, loader: 'html-loader', options: { // Webpack4 只需要在url-loader中设置esModule: false // Webpack5在html-loader中也需要设置 esModule: false } } ] }配置详情:www.npmjs.com/package/htm…
html-loader适用于静态html,对于需要存在ejs语法的html,使用html-loader会造成与html-webpack-plugin冲突,无法实现ejs语法效果。
-
html-loader与html-webpack-plugin的冲突
原因:html-webpack-plugin会检测是否有其他loader处理了html文件,html-loader处理过后,html-webpack-plugin就不再处理,造成ejs语法效果失效。
解决:不使用html-loader,在html中图片的引用改为采用ejs语法表示。
<img src="<%= requires('./images/telephone.png') %>" alt="telephone.png">
总结:静态html可采用html-loader来处理,如果需要使用ejs语法,则需要禁用html-loader,修改html文件中图片引用路径为ejs语法表示,采用html-webpack-plugin来处理
打包字体
-
字体文件
可在阿里巴巴矢量库下载:www.iconfont.cn/
-
file-loader
打包字体文件还是用到file-loader。
- 安装
npm i file-loader -D- 配置(在webpack.config.js中)
module: { rules: [ { test:/\.(eot|svg|ttf|woff|woff2)$/i, loader: 'file-loader', options: { name: 'fonts/[name].[ext]', // 输出到单独目录fonts,[name]表示保留原文件名,.[ext]表示保留原后缀名 } } ] }PS:按如上设置,将字体文件输出到单独目录时,css文件调用 url() 函数,可能会出现字体文件无法加载的情况,这是因为路径被改变导致的,我们只需要将设置中关于less和css的处理部分的
// less use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] // css use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']改成
// less use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader', 'postcss-loader', 'less-loader' ] // css use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: '../' } }, 'css-loader', 'postcss-loader' ]
复制不需要处理的文件
copy-webpack-plugin
不需要处理的其他文件,可以直接使用copy-webpack-plugin复制到输出目录。
- 安装
npm install copy-webpack-plugin -D - 配置(在webpack.config.js中)
const CopyPlugin = require("copy-webpack-plugin"); module.exports = { plugins: [ new CopyPlugin({ patterns: [ { from: "source", to: "dest" },// from 源文件/文件夹路径, to 目标文件/文件夹路径 { from: "other", to: "public" }, ], }), ], };
打包之前删除历史文件
clean-webpack-plugin
使用详情:www.npmjs.com/package/cle…
- 安装
npm install clean-webpack-plugin -D - 配置(在webpack.config.js中)
const { CleanWebpackPlugin } = require('clean-webpack-plugin');// 必须利用解构引用 module.exports = { plugins: [ new CleanWebpackPlugin(), ], };
资源模块(Asset modules)
资源模块是webpack5的一个新特性,是一种模块类型,它允许使用资源文件,而无需配置额外loader。
资源文件包含:字体、图片、图标、HTML等等。
特点:不使用file-loader、url-loader也能加载图片和字体
官方文档:webpack.docschina.org/guides/asse…
-
asset/resource 发送一个单独的文件并导出URL(之前通过用file-loader实现)
-
asset/inline 导出一个资源的dataURL(之前通过使用url-loader实现)
-
asset/source 导出资源的源代码(之前通过使用raw-loader实现)
-
asset 在导出一个dataURL和发送一个单独的文件之间自动选择(之前通过使用url-loader实现)
-
配置(在webpack.config.js中)
将使用file-loader和url-loader的部分配置进行修改,test文件匹配不需要更改。
// 打包字体配置 { test:/\.(eot|svg|ttf|woff|woff2)$/i, type: asset, parser: { dataUrlCondition: { // 假如不设置,maxSize默认就是8KB // 超过8KB调用asset/resource // 小于8KB调用asset/inline maxSize: 8 * 1024 } }, generator: { filename: 'fonts/[name][ext]'// 输出到单独目录font,和file-loader不一样,保留原后缀名是[ext]而不是.[ext] } }, // 打包图片配置 { test: /\.(png|jpe?g|gif)$/i, type: asset, // 这里省略maxsize的设置,采用默认的8KB,如需修改参考上面字体相关部分设置 generator: { filename: 'images/[name][ext]'// 输出到单独目录images,和file-loader不一样,保留原后缀名是[ext]而不是.[ext] } },
-
开发服务器(Dev Server)
webpack dev server
-
作用:发布web服务,提高开发效率
-
详情:
-
安装
npm i webpack-dev-server -D -
配置(在webpack.config.js中)
// 开发服务器 devServer: { // 指定加载内容的路径,一般是打包输出路径 contentBase: resolve(__dirname, 'output'), // 启用gzip压缩 compress: true, // 热更新,webpack4 // hot: true, // 自动更新,webpack5,禁用hot liveReload: true, // 服务器端口号 port: 9200 // 配置代理,解决跨域问题 proxy: { 'api': {// 表示对 http://localhost:9200/api 进行处理 // 替换,例如 http://localhost:9200/api/users 替换为 https://api.github.com/api/users target:'https://api.github.com', // 域名路径替换,http://localhost:9200/api/users 替换为 https://api.github.com/users pathRewrite: { '^/api': '',// 表示匹配路径开始处的'/api' }, // 不能使用 localhost:9200 作为github的主机名 changeOrigin: true } } }, // 配置目标 target: "web", -
服务器启动命令
Webpack4:
npx webpack-dev-serverWebpack5:
npx webpack serve只要修改保存了代码内容,在浏览器网址 http://localhost:9200/ 中就可以即时自动更新网页效果,提升了开发效率。
webpack进阶
区分打包环境
在实际工作过程中,我们需要根据不同的环境设置不同的打包配置,比如生产环境需要设置代码压缩,开发环境就不需要,因为压缩过后不好阅读代码。
有两种方式进行环境的区分:通过环境变量区分 和 通过配置文件区分。
-
通过环境变量区分
-
在命令行中设置环境变量:
# 设置env.production, 不赋值默认设置为true # webpack 4 npx webpack --env.production # webpack 5 npx webpack --env production -
在 webpack.config.js 中判断env
读取环境变量env.production,再根据环境变量指定不同的配置
// 修改为箭头函数传参 module.exports = (env, argv) => { // 开发环境配置 const config = { mode: 'development', // 更多配置... } if (env.production) { // 生产环境配置 config.mode = 'production', // 更多配置... } return config } -
切换回开发环境配置打包
# 设置env.production, 赋值为空字符串 # webpack 4 npx webpack --env.production="" # webpack 5 npx webpack --env production=""
-
-
通过配置文件区分
-
安装 webpack-merge (用于将多个配置合并到一起)。
npm i webpack-merge -D -
新建两个配置文件:webpack.dev.conf.js 和 webpack.prod.conf.js,分别写入开发环境和生产环境下所需配置,提取它们公共配置部分,新建 webpack.base.conf.js 并写入。
webpack.base.conf.js// 公共配置部分webpack.dev.conf.js
// 开发环境配置部分 const { merge } = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const devWebpackConfig = merge(baseWebpackConfig, { // 开发模式对应的配置... }) module.exports = devWebpackConfigwebpack.prod.conf.js
// 生产环境配置部分 const { merge } = require('webpack-merge') const baseWebpackConfig = require('./webpack.base.conf') const prodWebpackConfig = merge(baseWebpackConfig, { // 生产模式对应的配置... }) module.exports = prodWebpackConfig -
执行打包时根据不同环境指定配置文件。
# 开发环境 npx webpack --config webpack.dev.conf.js # 生产环境 npx webpack --config webpack.prod.conf.js也可以在package.json中进行配置,来简化命令:
"scripts": { "dev": "npx webpack --config webpack.dev.conf.js", "prod": "npx webpack --config webpack.prod.conf.js" },打包
# 开发环境打包 npm run dev # 生产环境打包 npm run prod
-
自定义plugin
webpack插件是一个具有apply方法的Javascript对象。apply方法会被webpackcompiler调用,并目在整个编译生命周期都可以访问compiler对象。
通过在生命周期的钩子中挂载函数,来实现功能扩展。
-
生命周期
生命周期就是整个生命过程中的关键节点。
程序的生命周期:初始化->挂载->渲染->展示->销毁 -
钩子
钩子是提前在可能增加功能的地方,埋好(顺设)一个函数。生命周期中的函数就是钩子。
webpack常用钩子:www.webpackjs.com/api/compile…钩子 描述 类型 environment 环境准备好 SyncHook compile 编译开始 SyncHook compilation 编译结束 SyncHook emit 打包资源到output之前 AsyncSeriesHook afterEmit 打包资源到output之后 AsyncSeriesHook done 打包完成 SyncHook 需要操作的代码必须加在emit或者之前的钩子上。
自定义plugin的基本流程:
- 项目根目录新建plugin文件夹,新建my-plugin.js文件,写入代码
class MyPlugin { constructor(options) { console.log('插件选项', options)// 如果有配置选项,可以在这里做处理 } // 必须带有 apply 方法 apply(compiler) { compiler.hooks.emit.tap('MyPlugin', (compilation) => { // compilation是此次打包的上下文 // 这里是你需要进行的操作 console.log('webpack 构建过程开始', compilation) }) } } module.exports = MyPlugin - 在webpack.config.js中进行引用、配置
const MyPlugin = require('./plugin/my-plugin') module.exports = { plugins: [ new MyPlugin({ // 传入配置项 }), ] }
详情:webpack.docschina.org/concepts/pl…
自定义loader
loader本质上就是一个ES Module模块,它导出一个函数,在函数中对打包资源进行转换。
现在我们制作一个读取markdown(.md)文件内容的loader
-
安装相关包
npm i marked loader-utils -Dmarked(将markdown语法转成html),loader-utils(接受loader的配置项)
-
项目根目录新建loader文件夹,在其中新建markdown-loader.js,写入相关代码
const { getOptions } = require('loader-utils'); const marked = require('marked') // 自定义loader,建议使用普通函数 module.exports = function(source) { // 获取loader配置选项,方便进行处理 const options = getOptions(this) // 对输入的内容进行处理 const html = marked(source) // 一般loader需要返回JS代码,除非返回给下一个loader处理 // return 'module.exports = ${(JSON.stringify(html))}' // 返还给下一个loader处理 return html } -
在src目录下新建一个test.md文件,写入内容
# 主题 爱我中华 -
在index.js中进行md文件引用
import MDtest from './test.md' console.log(MDtest) -
在webpack.config.js中进行引用、配置
{ test:/\.md$/i, use: ['html-loader', { loader: './loader/markdown-loader.js', options: {} }] }
自定义loader单独使用需保证最后输出为JS代码,假如有多个自定义loader被连续使用,则需保证最后一个loader输出JS代码
代码分离(Code Spilitting)
如果把所有代码都打包到一起,可能导致最终的代码文件非常大,从而影响加载时间;而且,很多代码是初始加载时不需要的:因此,我们可以根据代码使用的紧急程度,将代码分割打包后,按需加载。
代码分离有三种方式:
- 多入口打包
- 提取公共模块
- 动态导入
多入口打包方式
在 webpack.config.js :
- 配置entry(后面写成对象)
entry: {index:'./src/index.js', about:'./src/about.js'}, - 配置output.filename(不能写成固定名称,否则报错)
output: { // filename: 'bundle.js', filename: '[name].bundle.js', path: path.join(__dirname, 'output') }, - 配置HtmlWebpackPlugin(不同页面加载各自的bundle)
// HtmlWebpackPlugin关于index.html部分的配置 // 表示index.html加载index.bundle.js chunks:['index'] // HtmlWebpackPlugin关于about.html部分的配置 // 表示about.html加载about.bundle.js chunks:['about']
提取公共模块方式
如果多个页面都用到了一个公共文件(例如:jQuery),每个页 面都将公共文件打包一次是不合理的。更好的办法是将公共文件 提取出来。
例如:京东的商品页超过1000000个,如果打包的1000000个文件都包含jQuery,打包文件会超过80G(88KB*1000000)
我们只需要在 webpack.config.js 中 进行如下设置:
// 将公共文件提取出来,单独打包
optimization: {
splitChunks: {
chunks:'all'
}
},
动态导入方式
动态导入又分为两种方式:
-
懒加载
默认不加载,事件触发后才加载。我们可以通过类似以下代码实现懒加载
document.getElementById('btn').onclick = () => { // 只有在点击事件触发之后才会导入该wp.js文件 import('./wp').then( alert('这是动态导入') ) }上面的写法固然可以实现懒加载,但其中的wp.js经过打包后的文件名不受控制,所以我们需要增加一条命令来设置加载名称。
document.getElementById('btn').onclick = () => { // 只有在点击事件触发之后才会导入该wp.js文件 // 通过/* webpackChunkName: 'desc' */注释来指定wp.js打包后的名称为desc.bundle.js import(/* webpackChunkName: 'desc' */'./wp').then( alert('这是懒加载方式') ) } -
预加载
只需要在懒加载的基础上增加一条注释命令实现
document.getElementById('btn').onclick = () => { // 只有在点击事件触发之后才会导入该wp.js文件 // 通过/* webpackChunkName: 'desc' */注释来指定wp.js打包后的名称为desc.bundle.js // 通过/* webpackPrefetch: true */注释来指定加载模式为预加载 import(/* webpackChunkName: 'desc', webpackPrefetch: true */'./wp').then( alert('这是预加载方式') ) }预加载的效果是先等待其他资源加载,在浏览器空闲时,再加载。这是一个很好的效果,相比懒加载,减少了事件触发时所需要的加载时间。
缺点:在移动端有兼容性问题
源码映射(Source Map)
-
什么是SourceMap
SourceMap是一种源代码和构建后代码之间的映射技术。 通过.map文件,将构建后的代码与源代码之间建立映射关系。
-
为什么要用sourceMap
构建后的代码,出了问题之后不好定位。有了sourceMap后,可以快涑定位问题代码
-
如何生成sourceMap
在webpack.config.js中增加配置
// 映射模式,webpack4一共13种,webpack5一共26种 // 不同模式对应不同的效果,我们这里选择的是'source-map',实际生产不会采用该模式 devtool: 'source-map' -
如何选取合适的映射模式(个人建议-不绝对)
- 开发环境(eval-cheap-module-source-map)
- 生产环境(none | nosources-source-map)
使用SourceMap不生效,需要确认两个条件:一、是否使用谷歌浏览器;二、是否在浏览器中开启相关source-map设置(默认是开启的)
删除冗余代码(Tree Shaking)
Tree Shaking 的作用是删除未引用代码(dead code), 返回剩下的代码。 未引用代码包含:
- 只声明,未使用的函数
- 只引入,未使用的代码
如上图,依赖关系就是树,未使用的代码就像多余的树叶,使用Tree Shaking(摇树)技术“将多余的树叶摇下来”,就能使代码更精简,体积更小,从而提高加载速度。
使用前提:
- 使用ES Modules规范的模块,才能执行Tree Shaking
- Tree Shaking依赖于ES Modules的静态语法分析 如何使用:
- 生产模式:Tree Shaking会自动开启
- 开发模式:有 usedExports 和 sideEffects 两种方法
usedExports
-
配置usedExports(标记没用的代码)
在 webpack.config.js 中
optimization: { // 标记没用的代码 usedExports: true, }, -
使用terser-webpack-plugin(删除没用的代码)
-
安装
webpack 5无需安装,webpack 4需要单独安装
npm i terser-webpack-plugin -D -
配置(在 webpack.config.js 中)
const TerserPlugin = require('terser-webpack-plugin') optimization: { // 删除 unused harmony export XXXXX 标记的代码 minimize: true, minimizer: [new TerserPlugin()] },
-
Tree Shaking与 Source Map存在兼容性问题
Source Map 的 eval模式会将JS输出为字符串(不是ES Modules规范),导致Tree Shaking失效。
因此,同时使用Source Map时,其模式只能指定为source-map | inline-source-map | hidden-source-map | nosources-source-map中的一种
sideEffects
无副作用:如果一个模块单纯地导入导出变量,那它就无副作用
有副作用:如果一个模块还修改其他模块或者全局的一些东西,就有副作用。比如:修改全局变量,在原型上扩展方法,css的引入。
sideEffects的作用:删除未使用且无副作用的模块。
sideEffects的使用:
- 开启副作用(在 webpack.config.js 中配置)
optimization: { // 开启副作用 sideEffects: true, }, - 标识代码是否有副作用(在 package.json 中配置)
// false,表示所有代码都没有副作用(告诉webpack可以安全地删除未用的exports) // true,表示所有代码都有副作用 // 数组,告诉webpack哪些模块有副作用,不删除,比如`['./src/wp.js','*.css']` "sideEffects": false,
缓存
有些时候,我们只修改了小部分内容然后又重新打包,如果所有内容都重新打包,那么效率就会很低,但利用缓存技术,未更改的内容直接从缓存读取,只打包修改的部分,这样效率会高很多。
webpack关于缓存的设置页比较简单:
-
Babel缓存
在webpack.config.js中,关于babel-loader的设置,options对象增加一条属性
cacheDirectory: true即可{ test: /\.m?js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { // 第二次及之后构建时,会读取之前的缓存 cacheDirectory: true, presets: [ ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, targets: "defaults" } ] ], plugins: ['@babel/plugin-proposal-class-properties'] } } }, -
文件资源缓存
利用文件资源缓存技术在实际项目中可大大减少文件的重复请求,减轻服务器负担,提升效率。对于同一个文件资源我们只需要请求一次文件内容,之后从缓存中读取文件资源。但这也会存在一个问题:如果代码在缓存期内,代码更新后会看不到实时效果。
解决方案:将代码文件名称设置为哈希名称,当名称发生变化,就加载最新的内容。
Webpack中哈希名称设置:在webpack.config.js中,
output: { //filename: 'bundle.js', //filename: '[name].bundle.js', // [hash],整个项目打包生成的hash值 // [chunkhash],不同chunk打包时生成的hash值 // [contenthash],不同内容打包时生成的hash值 filename: '[name].[hash].js', path: path.join(__dirname, 'output') },
模块解析(resolve)
通过resolve配置项配置模块解析的规则,我们可以简化模块导入的代码。比如引入时省略css文件后缀名(import './css/style'),再简化表示文件目录(import '@/style')等等。
在webpack.config.js中进行配置:
// 模块的解析规则
resolve: {
alias: {// 配置模块加载的路径别名
// 使用 @ 代替 src 目录
'@': resolve('src')
},
// 引入模块时可以省略.js和.json后缀
extensions: ['.js', '.json'],
// 指定模块默认加载的路径
modules: [resolve(__dirname, './node_modules'), 'node_modules']
},
排除依赖(externals)
有些时候,我们需要排除打包依赖项,防止对某个依赖项进行打包。 一般来说,一些成熟的第三方库,是不需要打包的。 例如:jQuery,然后我们可以在模板文件中直接引入CDN中的压缩,这样就不用对其打包。
在webpack.config.js中进行配置:
// 排除打包依赖项
externals: {
'jquery': 'jQuery'
},
在模板文件中直接引入CDN中的压缩:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
详情:www.webpackjs.com/configurati…
模块联邦
模块联邦是Webpack 5新增的特性。它使得多个应用,可以共享一个模块,即本地可以调远程的模块。这相当于是微前端的一个解决方案。
模块提供方配置(webpack.config.js),来暴露模块给调用方使用:
// 引入模块联邦插件
const Mfp = require('webpack').container.ModuleFederationPlugin
module.exports = (env, argv) => {
// 插件配置
plugins: [
// 模块提供方
new Mfp({
// 应用名称,给调用方使用
name: 'app1',
// 调用方引入的文件名称
filename: 'app1.js',
// 暴露模块
exposes: {
// 模块名称: 模块对应的代码路径
'./Sitename': './src/Sitename.js'
}
}),
]
}
模块使用方配置(webpack.config.js):
// 引入模块联邦插件
const Mfp = require('webpack').container.ModuleFederationPlugin
module.exports = (env, argv) => {
// 插件配置
plugins: [
// 导入模块
remotes: {
// 应用别名(自己设置): "远程应用名称@远程应用地址/远程导出的文件名称"
// 假如远程应用地址未启用则模块无法调用
appone: 'aap1@localhost:3001/app1.js'
}
]
}
模块使用方在项目js文件中调用:
// import(应用别名/模块名称).then(模块的使用代码)
import('appone/Sitename').then(res => {
res.default()
//...
})