先简略回顾下 webpack 原理
Webpack
能够看做是模块打包机,把解析的所有模块变成一个对象,而后通过入口模块去加载咱们的货色,而后顺次实现递归的依赖关系,通过入口来运行所有的文件。因为 webpack
只意识js,所以须要通过一系列的 loader
和 plugin
转换成适合的格局供浏览器运行。
loader
次要是对资源进行加载/转译的预处理工作,其本质是一个函数,在该函数中对接管到的内容进行转换,返回转换后的后果。某种类型的资源能够应用多个loader
,执行程序是从右到左,从下到上。plugin
(插件)次要是扩大webpack
的性能,其本质是监听整个打包的生命周期。webpack
基于事件流框架Tapable
, 运行的生命周期中会播送出很多事件,plugin
能够监听这些事件,在适合的机会通过webpack
提供的 API 扭转输入后果。
webpack 装置
新建一个目录,进入目录初始化 package.json
,并装置 webpack
依赖
// 初始化包npm init -y// 装置依赖npm i webpack webpack-cli -D
根底配置
webpack
默认配置文件名字为 webpack.config.js
,于是在我的项目根目录下新建一个名为 webpack.config.js
的文件,在配置文件里写最简略的单页面配置:
let path = require("path");module.exports = { mode: "development", entry: "./src/js/index.js", output: { filename: "js/bundle.js", path: path.resolve("dist"), publicPath: "http://cdn.xxxxx" }}
配置详解
mode - 打包模式
development
为开发模式,打包后代码不会被压缩production
为生产模式,打包后代码为压缩代码
- entry - 入口文件
output - 打包文件配置
filename
:打包后文件,filename 的值可设置成带hash
戳的文件:js/bundle.[hash].js
/js/bundle.[hash:8].js
(只显示 8 位 hash 戳)path
:打包文件门路,需为绝对路径publicPath
:上线的cdn地址
TIP: 上述代码中 path
为内置模块,无需装置,间接引入即可。
新建后还需在我的项目根目录下的 src/js
目录下新建 index.js
文件,而后轻易输出一句 js 代码。
配置后可应用 webpack
命令尝试打包,若报错找不到命令可 npm i webpack -g
全局装置后再打包,打包胜利后会输入到我的项目根目录下的 dist
目录。
我的项目目录构造大抵如下
├─package.json├─webpack.config.js├─src| ├─js| | └index.js├─dist
html 文件打包
因为 webpack
只意识 js
,因而需通过 html-webpack-plugin
插件打包 html 文件
npm i html-webpack-plugin -D
装置后在 webpack.config.js
配置文件中
let HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = { plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html" }) ]}
production
模式下能够开启 html 文件的压缩配置:
plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", minify: { removeAttributeQuotes: true, collapseWhitespace: true }, hash: true })]
配置详解
plugins - webpack 插件配置
html-wepack-plugin配置
- template - html 模板文件的绝对/绝对路径
minify - 压缩配置
removeAttributeQuotes
:删除属性双引号collapseWhitespace
:代码压缩成一行
- hash - 引入文件带上hash戳
TIP: 如果不指定模板 template
配置,将是插件默认的 html文件,而不是我的项目中的 html 文件
开启服务
webpack 通过装置 webpack-dev-server
开启服务
npm i webpack-dev-server -D
配置 webpack.config.js
devServer: { port: 5000, compress: true, open: true, client: { progress: true }}
配置详解
devServer -
webpack-dev-server
配置- port - 端口号
- compress - 开启
gzip
压缩 - open - 启动后主动把页面关上
client
progress
:在浏览器中以百分比显示编译进度
配置好可运行 webpack-dev-server
命令查看成果,若找不到命令可 npm i webpack-dev-server -g
全局装置下
跨域
开发过程中容易遇到接口跨域问题,可通过 devServer.proxy
配置解决
假如接口地址为 http://localhost:3000/api/users
,对 /api/users
的申请可如下配置
devServer: { proxy: { '/api': 'http://localhost:3000', },},
但理论我的项目中接口的地址有很多种可能,个别不会有 /api
目录,即个别接口地址为http://localhost:3000/users
,因而枚举配置会很麻烦,可通过代理申请解决
即先申请 http://localhost:3000/api/users
接口地址,而后通过 devServer 代理到 http://localhost:3000/users
本文通过 express
开启接口服务,接口地址为 http://localhost:3000/user
,接口代码不再赘述,前期上传残缺的源码,可通过 node "我的项目门路\webpack5\src\js\server.js"
启动接口服务,而后配置 webpack.config.js
devServer: { proxy: { "/api": { target: "http://localhost:3000/", pathRewrite: { "/api": "" }, }, }}
devServer
配置详解
proxy - 代理配置
- target - 接口域名
- pathRewrite - 接口门路重写,把申请代理到接口服务器上
mock 接口数据
当后端接口没有写好,又不心愿被阻塞进度,能够通过 mock 后期跟后端约定好的接口数据格式来模仿调试页面。可应用有自定义函数和利用自定义中间件的能力的配置 devServer.setupMiddlewares
,在 middlewares.unshift
中的回调函数应用 res.send
把须要 mock 的数据传递进去:参考webpack视频解说:进入学习
devServer: { setupMiddlewares: (middlewares, devServer) => { if (!devServer) { throw new Error("webpack-dev-server is not defined"); } middlewares.unshift({ name: "user-info", // `path` 是可选的,接口门路 path: "/user", middleware: (req, res) => { // mock 数据模仿接口数据 res.send({ name: "moon mock" }); }, }); return middlewares; },}
款式解决
款式解决须要用到的 loader 及其作用:
less-loader
:加载和转译 LESS 文件postcss-loader
:应用 PostCSS 加载和转译 CSS/SSS 文件,如能够解决autoprefixer
css 包,为css增加浏览器前缀css-loader
:解析@import
andurl()
语法,应用 import 加载解析后的css文件,并且返回 CSS 代码mini-css-extract-plugin
的loader
:抽取出 css 文件,通过 link 标签引入 html 文件
装置依赖,若应用的是 sass,则把 less
less-loader
换成 node-sass
sass-loader
即可
npm i mini-css-extract-plugin css-loader postcss-loader autoprefixer less-loader less -D
配置 webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: "css/main.css", // 抽离的css文件名 }) ], module: { rules: [ { test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"], }, { test: /\.less$/, use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader", "less-loader"], }, ] }}
还需新建并配置 postcss.config.js
module.exports = { plugins: [require("autoprefixer")]};
上述文件配置好后,打包后会发现 css3 款式还是没有增加前缀,还需配置 package.json
的 browserlist
能力失效
"browserslist": [ "last 1 version", "> 1%", "IE 10"],
js 解决及语法校验
es6
或更高级的语法需转化成 es5
,并应用 eslint
标准代码:
babel-loader
:加载 ES2015+ 代码,而后应用 Babel 转译为 ES5@babel/preset-env
:根底的ES语法分析包,各种转译规定的对立设定,目标是通知loader要以什么规定来转化成对应的js版本@babel/plugin-transform-runtime
:解析 generator 等高级语法,但不蕴含 include 语法,include 语法需装置@babel/polyfill
。官网文档说上线需带上@babel/runtime
这个补丁,该包还做了一些办法抽离的优化,如 class 语法的抽离(抽离出 classCallCheck 办法)@babel/polyfill
:解析更高级的语法,如promise
,include
等,在js文件中require
引入即可eslint-loader
:校验 js 是否符合规范,可自行在 eslint 网站上配置下载
装置依赖
npm i @babel/core babel-loader @babel/preset-env @babel/plugin-transform-runtime [email protected]/polyfill -Dnpm i @babel/runtime eslint-loader eslint -S
webpack.config.js
{ test: /\.js$/, use: { loader: "eslint-loader", options: { enforce: "pre", // 定义为前置loader,在normal的loader前执行 }, },},{ test: /\.js$/, // enforce 默认为 normal 一般loader use: { loader: "babel-loader", options: { presets: ["@babel/preset-env"], // 把es6转成es5 plugins: ["@babel/plugin-transform-runtime"], //作用? }, }, include: path.resolve(__dirname, "src"), exclude: /node_modules/,},
配置 source-map
源码映射配置 source-map
的值:
- source-map 映射源码 会独自生成source-map文件 出错了会标识以后报错的行和列 大而全
- eval-source-map 不会产生独自的文件,可显示行和列
- cheap-module-source-map 不会标识列,会生成独自的映射文件
- cheap-module-eval-source-map 不会产生文件 集成在打包后的文件中 不会产生列
webpack.config.js
devtool: "eval-source-map",
引入js全局变量
有三种形式能够引入全局变量
expose-loader
可把变量裸露到 window
全局对象上,以 jquery 为例,先装置依赖
npm i jquery expose-loader -D
而后在 webpack.config.js
中配置 loader,把 $
裸露到 window 全局对象上
module: { rules: [{ test: require.resolve('jquery'), use: [{ loader: 'expose-loader', options: '$' }] }]}
除了上述办法外还能够在入口 js 文件中裸露
require("expose-loader?$!jquery");
providePlugin
可应用 webapck 内置插件 providePlugin
给每个模块中注入变量,还是以 jquery 为例
在 webapck.config.js
中配置
const webpack = require("webpack");module.exports = { plugins: [ new webpack.ProvidePlugin({ $: 'jquery' }); ]}
而后在任意js模块中能够间接应用$调用,无需引入jquery包
// in a module$('#item'); // <= works// $ is automatically set to the exports of module "jquery"
通过 cdn 引入
还能够通过 cdn 链接的形式引入全局变量,但如果此时js文件中多写了 import $ from 'jquery',就会把 jquery 也打包进去,可应用 external 避免将某些 import
的包(package)打包到 bundle 中
index.html
<script src="https://code.jquery.com/jquery-3.1.0.js" integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk=" crossorigin="anonymous"></script>
webpack.config.js
module.exports = { //... externals: { jquery: 'jQuery', },};
这样就剥离了那些不须要改变的依赖模块,换句话,上面展现的代码还能够失常运行:
import $ from 'jquery';$('.my-element').animate(/* ... */);
下面的例子。属性名称是 jquery
,示意应该排除 import $ from 'jquery'
中的 jquery
模块。为了替换这个模块,jQuery
的值将被用来检索一个全局的 jQuery
变量。换句话说,当设置为一个字符串时,它将被视为全局的
(定义在下面和上面)。
款式压缩和 js 压缩
production
模式下需压缩 css 可应用插件 css-minimizer-webpack-plugin
,但应用了此插件压缩 css, 会导致 js 不压缩,所以须要装置 js 压缩插件 terser-webpack-plugin
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");const TerserPlugin = require("terser-webpack-plugin");module.exports = { optimization: { minimize: true, minimizer: [ new CssMinimizerPlugin(), // 压缩js new TerserPlugin({ test: /\.js(\?.*)?$/i }), ], },}
图片解决
须要 loader 解析图片资源:
file-loader
:将文件的import/require()解析为url,并将文件发送到输入文件夹(dist文件夹),并返回(绝对)URLurl-loader
:像file-loader
一样工作,但如果文件小于限度,能够返回 data URL,即把图片变成base64html-loader
:能够解析html标签引入的图片,能够通过查问参数 attrs,指定哪个标签属性组合(tag-attribute combination)应该被解决,默认值:attrs=img:src
装置依赖
npm i file-loader url-loader html-loader -D
配置 webpack.config.js
module: { rules: [ { test: /\.jpg|png|jpeg$/, use: { loader: "file-loader", options: { outputPath: "images/", name: "[name].[ext]", // 如果不写文件名,则会生成随机名字 // publicPath: "http://cdn.xxx.com/images", // 可配置生产环境的cdn地址前缀 }, }, }, { test: /\.(html)$/, use: { loader: "html-loader", options: { esModule: false, }, }, }, ]}
TIP: url-loader能够应用 options.limit 限度,小于多少k时应用base64转换,大于这个体积应用file-loader打包
html-loader 配置报错问题
html-loader
需敞开 es6 模块化,应用commonjs解析,否则会报错。起因次要是两个 loader 解析图片的形式不一样。
我的项目目录构造大抵如下
├─.eslintrc.json├─package-lock.json├─package.json├─postcss.config.js├─webpack.config.js├─src| ├─index.html| ├─js| | ├─index.js| | ├─server.js| | └test.js| ├─image| | └logo.png| ├─css| | ├─a.css| | └index.css├─dist
resolve 配置
resolve 罕用的属性配置:
modules
:通知 webpack 解析模块时应该搜寻的目录。绝对路径和相对路径都能应用,然而要晓得它们之间有一点差别。- 应用绝对路径,将只在给定目录中搜寻。应用相对路径,通过查看当前目录以及先人门路。
- 如果想要优先于某个目标目录搜寻,则需把该目录放到目标目录后面,可详看官网例子
alias
:设置别名,方便使用,上面的例子利用于src
目录下的门路应用mainFields
:当从 npm 包中导入模块时(例如,import * as D3 from 'd3'),此选项将决定在 package.json 中应用哪个字段导入模块。依据 webpack 配置中指定的 target 不同,默认值也会有所不同。这里 browser 属性是最优先选择的,因为它是 mainFields 的第一项extensions
:尝试按程序解析这些后缀名。当引入的文件不带后缀名,且有多个文件有雷同的名字,但后缀名不同,webpack 会解析列在数组首位的后缀的文件 并跳过其余的后缀。
let path = require("path");module.exports = { resolve: { modules: [path.resolve("node_modules")], alias: { "@": path.resolve(__dirname, "src"), }, mainFields: ["browser", "module", "main"], extensions: [".js", ".json", ".vue"], },}
多页面配置
多页面顾名思义就是多个 html 页面,因而个别也会有多个 js 入口文件。
上面的配置中 entry 的 key
值对应的是 output 属性的 [name]
值,HtmlWebpackPlugin 中的属性 chunks
示意引入 [name]
对应的 js 代码文件,不指定 chunks
值将引入所有打包进去的 js 文件。
即本例的 [name]
别离为 home
和 other
,即打包进去是 home.js 和 other.js,最终打包的成果是 home.html
引入的是 home.js
,other.html
引入的是 other.js
文件
配置 webpack.config.js
let path = require("path");let HtmlWebpackPlugin = require("html-webpack-plugin");module.exports = { mode: "development", entry: { home: "./src/js/index.js", other: "./src/js/other.js", }, output: { filename: "js/[name].js", path: path.resolve("dist") }, plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", filename: "home.html", chunks: ['home'] }), new HtmlWebpackPlugin({ template: "./src/other.html", filename: "other.html", chunks: ['other'] }), ],}
webpack 小插件利用
clean-webpack-plugin
革除插件,可用于革除上一次的打包文件,革除目录为 output.path
的值
装置依赖
npm i clean-webpack-plugin -D
配置 webpack.config.js
const { CleanWebpackPlugin } = require("clean-webpack-plugin"); module.exports = { plugins: [ new CleanWebpackPlugin(), ]}
copy-webpack-plugin
拷贝插件,把某个文件夹导出到打包文件夹中,如文档文件夹(如 doc 文件夹)
装置依赖
npm i copy-webpack-plugin -D
配置 webpack.config.js
const CopyWebpackPlugin = require("copy-webpack-plugin"); // 拷贝文件module.exports = { plugins: [ new CopyWebpackPlugin({ patterns: [ { from: "./doc", to: "./doc", }, ], }), ]}
插件配置属性
patterns
- from: 源文件,绝对于当前目录门路
- to:指标文件,绝对于output.path文件门路,会生成到 dist/doc 目录下
webpack.BannerPlugin
版权申明插件,webpack 内置插件,无需装置
配置 webpack.config.js
const webpack = require("webpack");module.exports = { plugins: [ new webpack.BannerPlugin("copyright by Moon in 2022"), ]}
watch
能够监听文件变动,当它们批改后会从新编译,能够用在实时打包的场景下
配置 webpack.config.js
watch: true,watchOptions: { poll: 1000, //每秒查看一次变动 aggregateTimeout: 600, // 防抖 ignored: /node_modules/,},
配置属性
watchOptions
监听参数poll
: 每n毫秒查看一次变动aggregateTimeout
:防抖,当第一个文件更改,会在从新构建前减少提早。这个选项容许 webpack 将这段时间内进行的任何其余更改都聚合到一次从新构建里。以毫秒为单位ignored
:对于某些零碎,监听大量文件会导致大量的 CPU 或内存占用。能够应用正则排除像node_modules
如此宏大的文件夹
配置后在命令窗口输出 npm run build
就能够进行监控并实时打包了,如图实时打包了一次
环境变量
通过 webpack 内置插件 DefinePlugin
定义 DEV
环境变量。
还能够把开发和生产模式不同的 webpack 配置抽离进去,即把 webpack.config.js
文件一分为三
- 公共配置放在
webpack.config.base.js
文件 - 开发模式配置放在
webpack.config.dev.js
文件,通过webpack-merge
合并webpack.config.base.js
文件和webpack.config.dev.js
文件的配置 - 生产模式配置放在
webpack.config.prod.js
文件 (和开发模式配置文件逻辑统一)
webpack.config.dev.js
文件残缺代码如下:
let { merge } = require("webpack-merge");let base = require("./webpack.config.base.js");let HtmlWebpackPlugin = require("html-webpack-plugin");const webpack = require("webpack");module.exports = merge(base, { mode: "development", devtool: "eval-source-map", plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", }), new webpack.DefinePlugin({ ENV: JSON.stringify("dev"), }), ], devServer: { compress: true, client: { progress: true }, port: 5000, // mock数据 setupMiddlewares: (middlewares, devServer) => { if (!devServer) { throw new Error("webpack-dev-server is not defined"); } middlewares.unshift({ name: "fist-in-array", // `path` 是可选的 path: "/user", middleware: (req, res) => { res.send({ name: "moon mock" }); }, }); return middlewares; }, },});
应用环境变量后目录构造大抵如下
├─.eslintrc.json├─package-lock.json├─package.json├─postcss.config.js├─webpack.config.base.js├─webpack.config.dev.js├─webpack.config.prod.js├─src| ├─index.html| ├─js| | ├─index.js| | ├─server.js| | └test.js| ├─image| | └logo.png| ├─css| | ├─a.css| | └index.css├─doc| └notes.md├─dist
更改配置文件后,打包命令也要做适当调整,打包时须要指定配置文件:
// 开发模式webpack --config webpack.config.dev.js// 生产模式webpack --config webpack.config.prod.js
生产模式配置文件和公共配置文件源码前期上传
热更新
webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。这个机制能够做到不必刷新浏览器而将新变更的模块替换掉旧的模块。默认启用热更新,无需配置,它会主动利用 webpack.HotModuleReplacementPlugin
,这是启用 HMR 所必须的。
优化
上面的配置代码都是在 webpack 配置文件中,不再赘述
放大构建范畴
include/exclude 选其一即可
module: { rules: [ { test: /\.js$/, include: path.resolve(__dirname, "src"), // exclude: /node_modules/, }, ]}
module.noParse
因为webpack会通过入口文件解析 import
, require
援用的包,还会去剖析包的依赖,但有些包是没有依赖的,因而能够通过 noParse
不解析某个援用包中的依赖关系,来进步构建性能。适宜没有依赖项的包,如 jquery
module: { noParse: /jquery/,}
webpack.IgnorePlugin
webpack 内置插件 IgnorePlugin 能够阻止生成用于导入的模块,或要求调用与正则表达式或筛选函数匹配的模块。如 moment 包内引入了很多语言包,这些语言包都放在 locale 文件夹下,但大部分理论场景只会援用一个的语言包,因而打包时可疏忽 moment 目录下的 locale 语言包
new webpack.IgnorePlugin({ resourceRegExp: /^\.\/locale$/, contextRegExp: /moment$/,}),
疏忽后再从新再js文件中引入某个语言包就能失常应用了
import "moment/locale/zh-cn";moment.locale("zh-cn");
抽离公共代码
个别用在多页利用场景或者是单个 js 文件太大,申请须要很长时间,须要拆成几个js文件,优化申请速度,应用 optimization 的 splitChunks 属性来优化。
splitChunks.cacheGroups 缓存组能够继承和/或笼罩来自 splitChunks.*
的任何选项。然而 test
、priority
和 reuseExistingChunk
只能在缓存组级别上进行配置。将它们设置为 false
以禁用任何默认缓存组。
看上面配置之前先理解splitChunks的几个属性:
priority
:抽离代码的优先级,值越高越先被抽离,避免某些模块在后面的模块抽离完了前面没被抽离到,在本例中是避免vendor
模块被common
模块抽离完了没被抽离到name
:每个模块(chunk)的文件名,不定义将是随机名字test
:匹配目录chunks
:抉择哪些 chunk 进行优化initial
:从入口处开始提取代码,若有异步模块思考前面两个值async
:异步模块all
:能够存在异步和非异步模块
minSize
:生成 chunk 的最小体积,此处为不便测试设置为 0minChunks
:拆分前必须共享模块的最小 chunks 数,以后代码块援用多少次才被抽离,此处为不便设置设置为 1
本例中宰割了 common 和 vendor 两个 chunk
optimization: { // 宰割代码块 splitChunks: { // 缓存组 cacheGroups: { // 公共模块 commons: { name: "common", chunks: "initial", minSize: 0, minChunks: 1, }, vendor: { name: "vendor", priority: 1, test: /[\\/]node_modules[\\/]/, chunks: "all", //包含异步和非异步代码块 }, }, }, },
为不便大家了解,献上打包后的目录树结构
├─index.html├─js| ├─common.js| ├─common.js.LICENSE.txt| ├─main.js| ├─main.js.LICENSE.txt| ├─vendor.js| └vendor.js.LICENSE.txt├─images| └logo.png├─doc| └notes.md├─css| └main.css
这一块比拟难了解,倡议多试几次打包比照差别就懂了
懒加载
通过 es6 的 import()
语法实现懒加载,通过 jsonp
实现动静加载文件,import 函数返回的是 promise 对象。vue 懒加载,react 懒加载都是这样实现的。举个简略的栗子,某些 js 文件在按钮点击后再申请加载。
此处省略获取 button dom元素对象的代码button.addEventListener('click', function(){ import('./test.js').then(data => { console.log(data); })})
除了以上的优化办法之外,还有dll预构建,多线程构建/压缩,利用缓存晋升二次构建速度,动静 polyfill
等等,可依据理论状况自行抉择优化计划,这里不一一赘述
webpack自带优化
tree-shaking
应用 import
语法在生产环境下没用到的代码不会被打包, 即 tree shaking, require
语法不反对tree-shaking
scope hosting
scope hosting(作用域晋升),举个栗子:
let a = 1let b = 2let c = 3let d = a+b+cconsole.log(d)
代码打包进去只有最初一句, webpack打包会主动省略一些能够简化的代码
手写繁难less-loader
less-loader.js
在 /loaders/less-loader.js
目录文件中引入 less
插件
const less = require("less");function loader(source) { let css = ""; less.render(source, function (err, res) { css = res.css; });}module.exports = loader;
webpack.config.js
写入以下配置
resolveLoader: { alias: { "lessLoader": path.resolve(__dirname, "loaders", "less-loader")) }},module: { rules: [ { test: /\.less/, use: ["style-loader", "lessLoader"] } ]}