Webpack的高级应用
1.source-map
作为一个开发工程师——无论是什么开发,要求开发环境最不可少的一点功能就是——debug
功能。 之前我们通过webpack
, 将我们的源码打包成了bundle.js
。
试想:实际上客户端(浏览器)读取的是打包后的bundle.js
,那么当浏览器执行代码报错的时候,报错的信息自然也是bundle
的内容。 我们如何将报错信息(bundle
错误的语句及其所在行列)映射到源码上?souce-map
就可以实现这类功能,webpack
已经内置了sourcemap
的功能,我们只需要通过简单的配置,将可以开启它。
module.exports = {
// 开启 source map
// 开发中推荐使用 'source-map'
// 生产环境一般不开启 sourcemap
devtool: 'source-map',
}
devtool
共提供了以下SourceMap
模式:
模式 | 解释 |
---|---|
eval | 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL. |
source-map | 生成一个SourceMap文件,可锁定代码原来位置 |
hidden-source-map | 和 source-map 一样,但不会在 bundle 末尾追加注释. |
inline-source-map | 生成一个 DataUrl 形式的 SourceMap 文件. |
eval-source-map | 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap. |
cheap-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的sourcemap) |
cheap-module-source-map | 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。 |
1.1 eval
每个module会封装到eval里包裹起来执行,并且会在末尾追加注释。可锁定代码原来位置
npm run start
之后,可锁定代码原来位置
1.2 source-map
生成一个SourceMap文件。可锁定代码原来位置
在bundle末尾会追加注释:
1.3 hidden-source-map
和source-map一样,但不会在bundle末尾追加注释。不能锁定代码原来位置
1.4 inline-source-map
不会专门生成map文件,生成一个DataUrl形式的SourceMap文件。可锁定代码原来位置.
1.5 eval-source-map
不会专门生成map文件,每个module会通过eval()来执行,并生成一个DataUrl形式的SourceMap。
1.6 cheap-source-map
生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的sourcemap)
1.7 cheap-module-source-map
生成一个没有列信息(column-mappings)的SourceMaps文件,同时loader的sourcemap也被简化为只包含对应行的。
1.8 注意点
生产环境一般不会开启sourcemap功能,有2点原因:
- 通过bundle和sourcemap文件,可以反编译出源码——也就是说,线上有sourcemap文件的话就意味着有暴露源码的风险
- sourcemap文件的体积相对巨大,不宜放在生产环境
1.9 思考
问题:有时候我们期望能第一时间通过线上的错误信息,来追踪到源码位置,从而快速解决掉bug以减轻损失。但又不希望sourcemap文件暴露在生产环境,有比较好的方案吗?
2.devServer
开发环境下,我们往往需要启动一个web服务,方便我们模拟一个用户从浏览器中访问我们的web服务,读取我们的打包产物,以观测我们的代码在客户端的表现。webpack内置了这样的功能,我们只需要简单的配置就可以开启它。
2.1 基本使用
安装 devServer:
npm install -D webpack-dev-server
基础使用:
//webpack.config.dev.js
devServer: {
static: {
directory: path.join(__dirname, 'dist')
}, // 默认是把dist目录作为web服务的根目录
compress: true, // 可选择开启gzips压缩功能,对应静态资源请求的响应头里的Content-Encoding: gzip
port: 3000, // 端口号
},
为了方便,我们配置一下工程的脚本命令,在package.json的scripts里:
{
"scripts": {
"start": "webpack serve --mode development"
}
}
如果您需要指定配置文件的路径,请在命令的后面添加 --config [path], 比如:
webpack serve --mode development --config webpack.config.js
然后npm run start
之后就可以在日志里看到———它启动了一个http服务。 (webpack-dev-server的最底层实现是源自于node的http模块。)
2.2 添加响应头
有些场景需求下,我们需要为所有响应添加headers, 来对资源的请求和响应打入标志,以便做一些安全防范,或者方便发生异常后做请求的链路追踪。比如:
module.exports = {
devServer: {
headers: {
'X-Token': 'ZlcjLCe+sAW1S4QC8Z'
}
}
}
2.3 开启代理
我们打包出的bundle里有时会含有一些对特定接口的网络请求(ajax/fetch)。要注意,此时客户端地址是在http://localhost:3000/
下,假设我们的接口来自http://localhost:4001/
,那控制台就会出现跨域错误,在开发环境下,我们可以使用devServer自带的proxy功能来解决这个问题。
我们新搭建一个服务,在当前项目下新建 server.js:
const http = require('http');
const app = http.createServer((req, res) => {
if (req.url === '/api/user') {
res.end('hello node')
}
})
app.listen(4001, 'localhost', () => {
console.log('localhost listening on 4001')
})
再次打开一个终端执行node server.js,启动服务:
浏览器输入:
下面我们开始请求,请求我们可以使用浏览器自带的方法fetch,这个方法返回的是一个promise
fetch('http://localhost:4001/api/user')
.then(val => val.text()) // res.text()可以把返回的结果变成文本)
.then(res => {
console.log(res)
})
但我们希望能省略http://localhost:4001/
这一块内容:
fetch('/api/user')
.then(val => val.text()) // res.text()可以把返回的结果变成文本)
.then(res => {
console.log(res)
})
但这时候路径不对:
下面来解决跨域问题:
module.exports = {
//...
devServer: {
proxy: [
{
// 如果用户在地址栏一旦请求了一个资源叫 /api 的话,我们就给他指向到 http://localhost:4001 服务器上去
context: ['/api'], // 可以是字符串或字符串数组
target: 'http://localhost:4001',
//重写路径,上面这个不用重写路径
// pathRewrite: {'^/api' : ''},
},
],
}
}
默认情况下,将不接受在 HTTPS 上运行且证书无效的后端服务器。 如果需要,可以
这样修改配置:
module.exports = {
//...
devServer: {
proxy: [
{
context: ['/api'], // 可以是字符串或字符串数组
target: 'https://other-server.example.com',
secure: false,
},
],
};
2.4 https服务
如果想让我们的本地http服务变成https服务,我们只需要这样配置:
devServer: {
server: 'https',
}
重新启动服务, 我们发现访问http://localhost:port是无法访问我们的服务的,我们需要在地址栏里加前缀: https,注意: 由于默认配置使用的是自签名证书,所以有的浏览器会告诉你是不安全的,但我们依然可以继续访问它。当然我们也可以提供自己的证书。
2.5 http2
devServer: {
server: 'spdy',
},
http2默认自带https自签名证书,当然我们仍然可以通过https配置项来使用自己的证书
2.6 historyApiFallback
如果我们的应用是个SPA(单页面应用),当路由到/some 时(可以直接在地址栏里输入),会发现此时刷新页面后,控制台会报错:
GET http://localhost:3000/some 404 (Not Found)
此时打开network,刷新并查看,就会发现问题所在———浏览器把这个路由当作了静态资源的地址去请求,然而我们并没有打包出/some这样的资源,所以这个访问无疑是404的。如何解决它?我们可以通过配置来提供页面代替任何404的静态资源响应:
module.exports = {
//...
devServer: {
historyApiFallback: true
}
}
此时重启服务刷新后发现请求变成了index.html, 当然, 在多数业务场景下,我们需要根据不同的访问路径定制替代的页面,这种情况下,我们可以使用rewrites这个配置项。 类似这样:
module.exports = {
//...
devServer: {
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/views/landing.html' },
{ from: /^\/subpage/, to: '/views/subpage.html' },
{ from: /./, to: '/views/404.html' },
]
}
}
}
2.7 开发服务器主机
如果我们在开发环境中起了一个devserve服务,并希望在同一局域网下的同事也能访问它,只需要配置:
devServer: {
host: '0.0.0.0'
}
这时候,如果我们的同事跟我们处在同一局域网下,就可以通过局域网ip来访问我们的服务:
3.模块热替换与热加载
这一块内容之后专门写篇文章详细介绍。
4.代码规范
这一块内容之后专门写篇文章详细介绍。
5.模块与依赖
5.1 什么是模块
能在webpack工程化环境里成功导入的模块,都可以视作webpack模块。 与Node.js 模块相比webpack 模块 能以各种方式表达它们的依赖关系。下面是一些示例:
ES2015 import
语句CommonJS require()
语句AMD define
和require
语句css/sass/less
文件中的@import
语句stylesheet url(...)
或者HTML <img src=...>
文件中的图片链接
Webpack 天生支持如下模块类型:
ECMAScript
模块CommonJS
模块AMD
模块Assets
WebAssembly
模块
而我们早就发现——通过 loader 可以使 webpack 支持多种语言和预处理器语法编写的模块。loader 向 webpack 描述了如何处理非原生模块,并将相关依赖引入到你的 bundles中。包括且不限于:
- TypeScript
- Sass
- Less
- JSON
5.2 compiler与Resolvers
在我们运行webpack的时候(就是我们执行webpack命令进行打包时),其实就是相当于执行了下面的代码:
const webpack = require('webpack');
const compiler = webpack({
// ...这是我们配置的webpackconfig对象
})
webpack的执行会返回一个描述webpack打包编译整个流程的对象,我们将其称之为compiler。 compiler对象描述整个webpack打包流程———它内置了一个打包状态,随着打包过程的进行,状态也会实时变更,同时触发对应的webpack生命周期钩子。 (简单点讲,我们可以将其类比为一个Promise对象,状态从打包前,打包中到打包完成或者打包失败。) 每一次webpack打包,就是创建一个compiler对象,走完整个生命周期的过程。
而webpack中所有关于模块的解析,都是compiler对象里的内置模块解析器去工作的————简单点讲,你可以理解为这个对象上的一个属性,我们称之为Resolvers。 webpack的Resolvers解析器的主体功能就是模块解析,它是基于enhanced-resolve 这个包实现的。换句话讲,在webpack中,无论你使用怎样的模块引入语句,本质其实都是在调用这个包的api进行模块路径解析。
5.3 模块解析(resolve)
webpack通过Resolvers实现了模块之间的依赖和引用。举个例子:
import _ from 'lodash';
// 或者
const add = require('./utils/add');
所引用的模块可以是来自应用程序的代码,也可以是第三方库。 resolver 帮助webpack 从每个require/import 语句中,找到需要引入到 bundle 中的模块代码。当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。(webpack_resolver的代码实现很有思想,webpack基于此进行treeshaking,这个概念我们后面会讲到)。
5.3.1 webpack中的模块路径解析规则
通过内置的enhanced-resolve,webpack 能解析三种文件路径:
绝对路径
import '/home/me/file'
import 'C:\\Users\\me\\file'
由于已经获得文件的绝对路径,因此不需要再做进一步解析。
相对路径
import '../utils/reqFetch'
import './styles.css'
这种情况下,使用import
或require
的资源文件所处的目录,被认为是上下文目录。在import/require
中给定的相对路径,enhanced-resolve
会拼接此上下文路径,来生成模块的绝对路径path.resolve(__dirname,RelativePath)
。这也是我们在写代码时最常用的方式之一,另一种最常用的方式则是模块路径。
模块路径
import 'module'
import 'module/lib/file'
也就是在resolve.modules中指定的所有目录检索模块(node_modules里的模块已经
被默认配置了)。 你可以通过配置别名的方式来替换初始模块路径,具体请参照下面
resolve.alias 配置选项。
5.3.2 resolve配置路径别名
若模块路径隐藏太深,可以简化路径写法(路径别名)
//webpack.config.common.js
module.exports = {
resolve: {
alias: {
"aliasCommon":path.resolve(__dirname,"../src/common/"),
"@":path.resolve(__dirname,"./src")
}
}
}
import common from "aliasCommon/common.js"
5.3.3 外部扩展(Externals)
有时我们为了减小bundle体积,会把一些不变的第三方库用cdn的形式引入。比如jQuery: index.html
。在public/index
模版文件中添加:
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>
然后在src/index.js
中测试一下:
import $ from "jquery"
console.log($)
这个时候我们想在我们的代码里使用引入的jquery———但似乎三种模块引入方式都不行。这时候怎么办呢? webpack给我们提供了Externals的配置属性,让我们可以配置外部扩展模块:
//webpack.config.common.js
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
这时候控制台打印成功。这说明我们已经在代码中使用它。 注意:我们如何得知 { jquery:‘jQuery’} 中的 ‘jQuery’? 其实就是cdn里打入到window中的变量名,比如jQuery不仅有jQuery变量名,还有$,那么我们也可以写成这样子:
module.exports = {
//...
externals: {
jquery: '$',
},
};
但这种手动把cdn加入到index.html
模板的方法有点麻烦,我们可以做以下配置就不用每次都手动在index.html
模板上添加cdn了:
externalsType: "script",//一定要配置
externals: {
jquery:[
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js',
'$'
]
},
module.exports={
...
externalsType:'script', // 选择加载类型
externals:{
jquery:[
'https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js',
'$'
]
}
}
5.3.4 依赖图
当 webpack 开始工作时,它会根据我们写好的配置,从 入口(entry) 开始,webpack 会递归的构建一个 依赖关系图,这个依赖图包含着应用程序中所需的每个模块,然后将所有模块打包为bundle(也就是output的配置项)。
单纯讲似乎很抽象,我们更期望能够可视化打包产物的依赖图,下边以webpack-bundle-analyzer为例,它将bundle内容展示为一个便捷的、交互式、可缩放的树状图形式。
安装webpack-bundle-analyzer:
npm install webpack-bundle-analyzer -D
然后我们配置它:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
// ...others
new BundleAnalyzerPlugin()
]
}
这时我们执行打包命令,发现控制台里打印出下面这样的日志:
我们打开http://127.0.0.1:8888
就可以看到各模块的关系了。
注意: 对于 HTTP/1.1 的应用程序来说,由 webpack 构建的 bundle 非常强大。当浏览器发起请求时,它能最大程度的减少应用的等待时间。 而对于 HTTP/2 来说,我们还可以使用代码分割进行进一步优化。
6.PostCSS和CSS模块
PostCSS是一个用JavaScript工具和插件转换CSS代码的工具。比如可以使用 Autoprefixer 插件自动获取浏览器的流行度及支持的属性,并为css规则自动添加前缀,将最新的css语法转换为大多数浏览器所支持的语法。
这一块详情可见我写的文章https://blog.csdn.net/fageaaa/article/details/145816255中的5.3.2
。
7.Web Works
Web Works
是html5
的新特性,在这就不多说。
有时我们需要在客户端进行大量的运算,但又不想让它阻塞我们的js主线程。你可能第一时间考虑到的是异步。但事实上,运算量过大(执行时间过长)的异步也会阻塞js事件循环,甚至会导致浏览
器假死状态。
这时候,HTML5的新特性 WebWorker就派上了用场。在此之前,我们简单的了解下这个特性。
html5之前,打开一个常规的网页,浏览器会启用几个线程?
一般而言,至少存在三个线程(公用线程不计入在内):分别是js引擎线程(处理js)、GUI渲染线程(渲染页面)、浏览器事件触发线程(控制交互)。当一段JS脚本长时间占用着处理机,就会挂起浏览器的GUI更新,而后面的事件响应也被排在队列中得不到处理,从而造成了浏览器被锁定进入假死状态。
现在如果遇到了这种情况,我们可以做的不仅仅是优化代码——html5提供了解决方案webworker。
webWorkers提供了js的后台处理线程的API,它允许将复杂耗时的单纯js逻辑处理放在浏览器后台线程中进行处理,让js线程不阻塞UI线程的渲染。
多个线程间也是可以通过相同的方法进行数据传递。
它的使用方式如下:
//new Worker(scriptURL: string | URL, options?: WorkerOptions)
new Worker("someWorker.js");
也就是说,需要单独写一个js脚本,然后使用new Worker来创建一个Work线程实例。这意味着并不是将这个脚本当做一个模块引入进来,而是单独开一个线程去执行这个脚本。
我们知道,常规模式下,我们的webpack工程化环境只会打包出一个bundle.js,那我们的worker脚本怎么办?也许你会想到设置多入口(Entry)多出口(ouotput)的方式。事实上不需要那么麻烦,webpack4的时候就提供了worker-loader专门配置webWorker。令人开心的是,webpack5之后就不需要用loader了,因为webpack5内置了这个功能。
我们来试验一下:
创建一个work脚本 work.js,我们甚至不需要写任何内容,我们的重点不是webWorker的使用,而是在webpack环境中使用这个特性。当然,也可以写点什么,比如:
self.onmessage = ({ data: { question } }) => {
self.postMessage({
answer: 42,
})
}
在 src/index.js
中使用它:
// 下面的代码属于业务逻辑
const worker = new Worker(new URL('./work.js', import.meta.url));
worker.postMessage({
question:
'hi,那边的workder线程,请告诉我今天的幸运数字是多少?',
});
worker.onmessage = ({ data: { answer } }) => {
console.log(answer);
};
这时候我们执行打包命令,会发现,dist目录下会新增一个bundle:
总结:webpack5以来内置了很多功能,让我们不需要过多的配置,比如之前讲过的hot模
式,和现在的web workder。
8.TypeScript
详细内容见我写的文章https://blog.csdn.net/fageaaa/article/details/145816255中的第三节。
9.多页面应用
9.1 entry配置
这下面有多个入口,而且还可以把多个文件合并成一个文件。
entry:{
main:{
import:['./src/another-module.js','./src/third-module.js'],
},
index:{
import:'./src/index.js',
},
},
9.2 index.html模板配置
想要自定义一些html相关的配置项时,可以通过htmlwebpackplugin的模板配置生成。(前面已经有过应用)
9.3 环境搭建
多个html文件时则需通过多个htmlwebpackplugin来分别指定。
module.exports={
...
plugins:[
new HtmlWebpackPlugin({
title:'多页面应用',
template:'./index.html',
inject:'body', // 定义script标签的生成位置
filename:'chanel1/index.html',
chunks:['main','lodash'], // 分别使用各自的js
publicPath:'http://www.xxx.com' // 通过配置publicPath来指定当前文件的公共路径
}),
new HtmlWebpackPlugin({
template:'./index2.html',
inject:'body',
filename:'chanel2/index2.html',
chunks:['main2','lodash'], // 分别使用各自的js
publicPath:'http://www.bbb.com' // 通过配置publicPath来指定当前文件的公共路径
})
],
entry:{
main:{
import:['./src/app.js','./src/work.js'],
dependOn:'lodash',
filename:'chanel1/[name].js'
},
main2:{
import:'./src/craft.js',
dependOn:'lodash',
filename:'chanel2/[name].js'
},
lodash:{
import:'lodash',
filename:'common/[name].js'
}
}
}
10.Tree Shaking
tree Shaking 是一种通过消除最终文件中未使用的代码来优化体积的方法。
10.1 注意点
- tree Shaking只支持esm语法
- tree Shaking在development模式不起作用
- 若是第三方库,在代码上没使用,只是import,也是活代码,不会进行tree Shaking
10.2 实例
//src/math.js
export function square(x) {
return x * x;
}
export function cube(x) {
return x * x * x;
}
//webpack.config.js
module.exports={
...
mode:'development',
devtool: "inline-source-map",
optimization:{
...
usedExports:true,//表示esm方式引用的模块可以被tree shaking优化
},
}
配置完这些后,更新入口脚本,使用其中一个新方法:
//src/app.js
import { cube } from './math.js';
function component() {
const element = document.createElement('pre');
element.innerHTML = [
'Hello webpack!',
'5 cubed is equal to ' + cube(5)
].join('\n\n');
return element;
}
document.body.appendChild(component())
注意,我们没有从 src/math.js 模块中 import 另外一个 square 方法。这个函数就是所谓的“未引用代码(dead code)”,也就是说,应该删除掉未被引用的 square 方法。
现在运行npm run build ,并查看输出的bundle:
虽然我们没有引用 square ,但它仍然被包含在 bundle 中。如果此时修改配置:
//webpack.config.js
module.exports={
...
mode:'production',
devtool: "inline-source-map",
optimization:{
...
usedExports:true,//表示esm方式引用的模块可以被tree shaking优化
},
}
打包后发现无用的代码全部都消失了。
webpack是如何完美的避开没有使用的代码的呢?很简单:就是 Webpack 没看到你使用的代码。Webpack 跟踪整个应用程序的import/export 语句,因此,如果它看到导入的东西最终没有被使用,它会认为那是未引用代码(或叫做“死代码”—— dead-code ),并会对其进行 tree-shaking 。
10.3 死代码和活代码
死代码并不总是那么明确的。下面是一些死代码和“活”代码的例子:
// 这会被看作“活”代码,不会做 tree-shaking
import { add } from './math'
console.log(add(5, 6))
// 导入并赋值给 JavaScript 对象,但在接下来的代码里没有用到
// 这就会被当做“死”代码,会被 tree-shaking
import { add, minus } from './math'
console.log(add(5, 6))
// 导入但没有赋值给 JavaScript 对象,也没有在代码里用到
// 这会被当做“死”代码,会被 tree-shaking
import { add, minus } from './math'
console.log('hello webpack')
// 导入整个库,但是没有赋值给 JavaScript 对象,也没有在代码里用到
// 非常奇怪,这竟然被当做“活”代码,因为 Webpack 对库的导入和本地代码导入的处理
方式不同。
import { add, minus } from './math'
import 'lodash'
console.log('hello webpack')
10.4 sideEffects
tree-shaking
会导致无差别删除代码,我们可以通过配置sideEffects
来手动调节模块。在package.json
中编辑,下方示例即为对css后缀的文件不过滤(即视为有副作用,不能tree-shaking
)。
"sideEffects":["*.css"],
sideEffects
有三个可能的值:
- true
如果不指定其他值的话。这意味着所有的文件都有副作用,也就是没有一个文件可以tree-shaking。 - false
告诉Webpack 没有文件有副作用,所有文件都可以 tree-shaking。 - 数组[…]
是文件路径数组。它告诉 webpack,除了数组中包含的文件外,你的任何文件都没有副作用。因此,除了指定的文件之外,其他文件都可以安全地进行 tree-shaking。
11.渐进式网络应用程序PWA
简而言之,PWA是一个网站,但是它们采用了最新的Web标准来允许在用户在设备上安装它。他是一种提供了类似于native app(原生应用程序) 体验的 web app(网络应用程序)。PWA 可以用来做很多
事。其中最重要的是,在离线(offline)时应用程序能够继续运行功能。当没有网络连接时,它可以离线使用,它可以缓存上一次联网交互过程中的数据。这是通过使用名为 Service Workers 的 web 技术来实现的。
11.1 非离线环境下运行
在项目主页面初始化项目:
//src/index.js
console.log("项目启动了");
initPageStyle();
function initPageStyle(){
document.body.style.backgroundColor="red"
}
npx webpack serve
运行项目在浏览器上可见:
说明项目启动成功。这时候我们关闭webpack-dev-serve服务,就等同于在离线条件下,刷新浏览器这时候浏览器如下:
11.2 添加Workbox
我们应该要实现的是,停止server
然后刷新,仍然可以看到应用程序正常运行。
添加workbox-webpack-plugin
插件,然后调整webpack.config.js
文件,以实现离线环境下依然能够访问应用。
npm install workbox-webpack-plugin --save-dev
onst WorkboxPlugin = require('workbox-webpack-plugin')
module.exports={
...
plugins:[
...
new WorkboxPlugin.GenerateSW({
// 这些选项帮助快速启用 ServiceWorkers
clientsClaim: true,
skipWaiting: true // 不允许遗留任何“旧的” ServiceWorkers
//还有其它很多配置这里不一一讲述
})
]
}
通过npx webpack
命令打包(一定要打包)后在dist
文件夹下生成两个关于Workbox
的文件:
11.3 注册Service-Worker
在主页面追加代码
if('serviceWorker' in navigator){
window.addEventListener('load',()=>{
navigator.serviceWorker.register('/service-worker.js')
.then(registration=>{
console.log('SW 注册成功',registration)
})
.catch(registrationError=>{
console.log('SW 注册失败',registrationError)
})
})
}
11.4 测试离线环境
下面来测试一下。可以用webpack-dev-server
提供的服务测试,也可以用http-server
测试。这里用一下http-server
。
本地安装 http-server:
npm install http-server --save-dev
在package.json中配置脚本:
"scripts": {
...
"start":"http-server dist"
},
先npx webpack
打包生成service-worker
文件,然后npm run start
运行项目后,浏览器会正常显示。之后停止运行项目,中断显示停止:
刷新浏览器,应用程序还在正常运行。
11.5 停止使用缓存内容
程序之所以能在离线时候还能正常运行,是因为chrome浏览器会把最近的页面缓存下来,我们可以把缓存的内容给清除掉。在另外一个标签页输入chrome://serviceworker-internals/
网址,
取消注册,再去刷新项目网页,这时候程序就不会运行了:
12.shimming预置依赖
12.1 shimming预置全局变量
使用ProvidePlugin 后,能够在 webpack 编译的每个模块中,通过访问一个变量来获取一个 package。如果 webpack 看到模块中用到这个变量,它将在最终bundle 中引入给定的 package。
之前是在单个文件引入lodash,如:
//确保自己npm安装了lodash
import _lodash from 'lodash';
console.log("lodash:"+_lodash.add(1,7))
现在可以这样子处理,先配置webpack
:
//用于预置依赖
const webpack=require("webpack");
module.exports={
...
plugins:[
...
// _webpackLodash指定为全局变量
new webpack.ProvidePlugin({
_webpackLodash:'lodash'
}),
],
}
然后可以在所有页面使用_webpackLodash:
还可以使用 ProvidePlugin 暴露出某个模块中单个导出,通过配置一个“数组路径”(例如 [module, child, …children?] )实现此功能。所以,我们假想如下,无论 join 方法在何处调用,我们都只会获取到 lodash 中提供的 join 方法。
//用于预置依赖
const webpack=require("webpack");
module.exports={
...
plugins:[
...
// _webpackLodash指定为全局变量
new webpack.ProvidePlugin({
// _webpackLodash:'lodash'
_lodash_join: ['lodash', 'join'],
_lodash_compact: ['lodash', 'compact'],
}),
],
}
然后可以在所有页面使用_lodash_join和_lodash_compact:
假如我们自己开发了一个插件,想让它应用到当前项目。插件如下:
//src/plugins/index.js
export function add(a,b){
return a+b;
}
export function minus(a,b){
return a-b;
}
export default {
add,
minus
}
我们可以在webpack配置文件追加:
new webpack.ProvidePlugin({
// _webpackLodash:'lodash'
_lodash_join: ['lodash', 'join'],
_lodash_compact: ['lodash', 'compact'],
$my_plugins:[path.resolve(__dirname, "src/plugins/index.js")]
}),
然后可以在所有页面都可以使用$my_plugins:
12.2 细粒度Shimming
一些遗留模块依赖的 this 指向的是 window 对象。
在主页面追加以下代码,会报错
this.alert('测试细粒度Shimming')//会报错,这里this为undefined
我们需要让this指向window:先安装imports-loader
npm install imports-loader --save-dev
在rules中配置imports以覆盖exports的指向:
{
test: require.resolve('./src/app.js'),
use: 'imports-loader?wrapper=window'
}
引入之后这个文件中this就为window了。
12.3 全局Exports
让我们假设,某个 library 创建出一个全局变量,它期望使用者使用这个变量。为此,我们可以在项目配置中,添加一个小模块来演示说明:
//src/common/global1.js
const file = 'blah.txt';
const helpers = {
test: function () {
console.log('test something');
},
parse: function () {
console.log('parse something');
},
};
假如我们想要把global1.js以commonJS的形式导出,然后在app.js界面使用。我们需要安装exports-loader
npm install exports-loader --save-dev
添加规则
{
test: require.resolve('./src/common/global1.js'),
loader: "exports-loader",
options: {
type:"commonjs",//type有两个值,分别是commonJs和module
exports:"file,helpers"
},
},
然后去app.js中使用:
//测试全局Exports
const { file, helpers } = require('./common/global1.js');
console.log(file)
console.log(helpers)
同理,文件中内容也可以以esm的形式引入。如下:
//src/common/global2.js
const book = '战争与和平';
const helpers = {
read: function () {
console.log('read book');
},
buy: function () {
console.log('buy book');
},
};
添加规则
{
test: require.resolve('./src/common/global2.js'),
loader: "exports-loader",
options: {
type:"module",//type有两个值,分别是commonJs和module
exports:"book,named helpers bookHelpers"//给helpersc重命名为bookHelpers
},
},
然后去app.js中使用:
import { book, bookHelpers } from "./common/global2.js";
console.log(book)
bookHelpers.buy();//buy book
在使用第三方库时候,有时候不知道用什么方式导入(用import还是require),那我们就自己给没有导出语句的模块添加导出语句。
13.polyfills
注:现目前建议不再使用polyfill,而是改为core.js
目前为止,我们讨论的所有内容 都是处理那些遗留的 package,让我们进入到第二
个话题:polyfill。
13.1 使用polyfills
npm install @babel/polyfill -D
app.js中内容如下:
console.log(Array.from([1, 2, 3], x => x + x))
由于我们都是在chrome浏览器上运行,所以输出结果是[2,4,6]没有什么问题。但如果是低版本浏览器就不一样了。我们对上面进行打包会发现打包工具并没有把高版本语法转为低版本语法:
于是我们在该代码上面追加垫片:
import '@babel/polyfill'
console.log(Array.from([1, 2, 3], x => x + x))
再次打包,会发现高版本语法已经被转为低版本语法了。
13.2 优化polyfills
13.2.1 缺点
不建议像上面一样直接使用 import @babel/polyfill 。这样子会导致全局引入整个polyfill 包,体积大且污染全局环境。
13.2.2 优化操作
babel-preset-env可以通过 browserlist来转译那些浏览器中不支持的特性。preset可以使用useBuiltIns选项,默认false,这种方式可以将全局 babel-polyfill 导入,改进为细粒度更高的import格式。
本地安装@babel/preset-env 及相关包:
npm install core-js@3 babel-loader @babel/core @babel/preset-env -D
在webpack.config.js中配置:
{
test: /\.m?js$/,//注意问号是可选的意思,表示前面一个字符选和不选
exclude: /node_modules/,//注意:需在exclude中排除node_modules包
use: {
loader: 'babel-loader',
//配置对象
options: {
// 预设
presets:[
[
'@babel/preset-env',
{
targets:[
'last 1 version',
'> 1%'
],
useBuiltIns:'usage',
corejs:3
}
]
],
}
}
},
这样子就可以删除import ‘@babel/polyfill’,而且打包之后体积会变小!!!
如果需要兼容async/await语法则还需要添加regeneratorRuntime模块:
npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime
添加规则:
{
test: /\.m?js$/,//注意问号是可选的意思,表示前面一个字符选和不选
exclude: /node_modules/,//注意:需在exclude中排除node_modules包
use: {
loader: 'babel-loader',
//配置对象
options: {
// 预设
presets:[
[
'@babel/preset-env',
{
targets:[
'last 1 version',
'> 1%'
],
useBuiltIns:'usage',
corejs:3
}
]
],
plugins: [
[
'@babel/plugin-transform-runtime'
]
]
}
}
},
这个在之前的https://blog.csdn.net/fageaaa/article/details/145953959中的第四节讲过。
14.创建Library
除了打包应用程序,webpack 还可以用于打包 JavaScript library。
14.1 创建Library
假设我们正在编写一个名为 webpack-numbers 的小的 library,可以将数字1到5转换为文本表示,反之亦然,例如将 2 转换为 ‘two’。使用 npm 初始化项目,然后安装 webpack,webpack-cli和 lodash:
npm i webpack webpack-cli lodash -D
我们将 lodash 安装为 devDependencies 而不是 dependencies ,因为我们不需要将其打包到我们的库中,否则我们的库体积很容易变大。
//src/ref.json
[
{
"num": 1,
"word": "One"
},
{
"num": 2,
"word": "Two"
},
{
"num": 3,
"word": "Three"
},
{
"num": 4,
"word": "Four"
},
{
"num": 5,
"word": "Five"
},
{
"num": 0,
"word": "Zero"
}
]
import _ from 'lodash';
import numRef from './ref.json';
export function numToWord(num) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.num === num ? ref.word : accum;
},
''
);
}
export function wordToNum(word) {
return _.reduce(
numRef,
(accum, ref) => {
return ref.word === word && word.toLowerCase() ? ref.num :
accum;
},
-1
);
}
14.2 Webpack配置
我们可以从如下 webpack 基本配置开始:
const path = require("path");
module.exports={
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'webpack-numbers.js',
},
}
在上面的例子中,我们将通知 webpack 将 src/index.js 打包到 dist/webpack-numbers.js 中。
14.3 导出Library
到目前为止,一切都应该与打包应用程序一样,这里是不同的部分 - ----我们需要通过
output.library 配置项暴露从入口导出的内容。
const path = require("path");
module.exports={
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'webpack-numbers.js',
library: "webpackNumbers",
},
}
我们暴露了 webpackNumbers,以便用户可以通过 script 标签使用。在根目录下新建demo文件夹,在demo文件夹新建index.html来测试:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="../dist/webpack-numbers.js"></script>
<script>
console.log(window.webpackNumbers===webpackNumbers)//true
console.log(window.webpackNumbers.wordToNum('Five'));//5
</script>
</body>
</html>
虽然我们写的库可以被script 标签引用而发挥作用,但它不能运行在 CommonJS、AMD、Node.js 等环境中。作为一个库作者,我们希望它能够兼容不同的环境,也就是说,用户应该能够通过以下方式使用打包后的库:
CommonJS:
const webpackNumbers = require('webpack-numbers');
// ...
webpackNumbers.wordToNum('Two');
AMD:
require(['webpackNumbers'], function (webpackNumbers) {
// ...
webpackNumbers.wordToNum('Two');
});
script tag:
<!DOCTYPE html>
<html>
...
<script src="https://example.org/webpack-numbers.js">
</script>
<script>
// ...
// Global variable
webpackNumbers.wordToNum('Five');
// Property in the window object
window.webpackNumbers.wordToNum('Five');
// ...
</script>
</html>
我们更新 output.library 配置项,将其 type 设置为 ‘umd’ :
const path = require("path");
module.exports={
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'webpack-numbers.js',
library: {
name: 'webpackNumbers',
type: 'umd',
},
},
}
现在 webpack 将打包一个库,其可以与 CommonJS、AMD 以及 script 标签使用。
下面来测试一下:
//demo/commonJsTest.js
const {numToWord,wordToNum} =require("../dist/webpack-numbers")
console.log(numToWord(2))
在命令行执行命令,说明可以兼容commonJs
14.4 外部化lodash
npx webpack
打包项目,发现打包体积比较大这是因为在这个库中我们把lodash第三方库也打包进来了。
我们可以使用 externals 配置来减少打包体积:
const path = require("path");
module.exports={
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "./dist"),
filename: 'webpack-numbers.js',
clean: true,
library: {
name: 'webpackNumbers',
type: 'umd',
},
globalObject: "globalThis"
},
externals: {
lodash: {
commonjs: 'lodash',
commonjs2: 'lodash',
amd: 'lodash',
root: '_',
},
},
}
这意味着你的 library 需要一个名为 lodash 的依赖,这个依赖在使用者环境中必须存在且可用。如果没有就会自动去安装。现在再来打包,会发现体积小很多:
14.5 外部化限制
对于想要实现从一个依赖中调用多个文件的那些 library:
import A from 'library/one';
import B from 'library/two';
// ...
无法通过在 externals 中指定整个 library 的方式,将它们从 bundle 中排除。而是需要逐个或者使用一个正则表达式,来排除它们。
module.exports = {
//...
externals: [
'library/one',
'library/two',
// 匹配以 "library/" 开始的所有依赖
/^library\/.+$/,
],
};
14.6 发布为npm package
这个很常见,有空我专门写一篇文章讲讲怎么样去开发并且发布组件库。