继 24 个实例入门并掌握「Webpack4」(二) 后续:
- PWA 设置
- TypeScript 设置
- Eslint 设置
- 运用 DLLPlugin 加速打包速率
- 多页面打包设置
- 编写 loader
- 编写 plugin
- 编写 Bundle
十七、PWA 设置
本节运用 demo15 的代码为基本
我们来模仿日常平凡开辟中,将打包完的代码防备到效劳器上的操纵,起首打包代码 npm run build
然后装置一个插件 npm i http-server -D
在 package.json 中设置一个 script 敕令
{
"scripts": {
"start": "http-server dist",
"dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
"build": "webpack --config ./build/webpack.prod.conf.js"
}
}
运转 npm run start
如今就起了一个效劳,端口是 8080,如今接见 http://127.0.0.1:8080 就可以看到效果了
假如你有在跑别的项目,端口也是 8080,端口就争执,记得先封闭其他项目的 8080 端口,再
npm run start
我们按 ctrl + c 封闭 http-server 来模仿效劳器挂了的场景,再接见 http://127.0.0.1:8080 就会是如许
页面接见不到了,因为我们效劳器挂了,PWA 是什么手艺呢,它可以在你第一次接见胜利的时候,做一个缓存,当效劳器挂了今后,你依旧可以接见这个网页
起首装置一个插件:workbox-webpack-plugin
npm i workbox-webpack-plugin -D
只需要上线的代码,才须要做 PWA 的处置惩罚,翻开 webpack.prod.conf.js
const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 插件
const prodConfig = {
plugins: [
// 设置 PWA
new WorkboxPlugin.GenerateSW({
clientsClaim: true,
skipWaiting: true
})
]
}
从新打包,在 dist 目次下会多出 service-worker.js
和 precache-manifest.js
两个文件,经由过程这两个文件就可以使我们的网页支撑 PWA 手艺,service-worker.js 可以明白为另类的缓存
还须要去营业代码中运用 service-worker
在 app.js 中加上以下代码
// 推断该浏览器支不支撑 serviceWorker
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js')
.then(registration => {
console.log('service-worker registed')
})
.catch(error => {
console.log('service-worker registed error')
})
})
}
从新打包,然后运转 npm run start
来模仿效劳器上的操纵,最好用无痕情势翻开 http://127.0.0.1:8080 ,翻开掌握台
如今文件已被缓存住了,再按 ctrl + c 封闭效劳,再次革新页面也照样能显现的
TypeScript设置
TypeScript 是 JavaScript 范例的超集,它可以编译成纯 JavaScript
新建文件夹,npm init -y
,npm i webpack webpack-cli -D
,新建 src 目次,建立 index.ts 文件,这段代码在浏览器上是运转不了的,须要我们打包编译,转成 js
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return 'Hello, ' + this.greeting
}
}
let greeter = new Greeter('world')
alert(greeter.greet())
npm i ts-loader typescript -D
新建 webpack.config.js 并设置
const path = require('path')
module.exports = {
mode: 'production',
entry: './src/index.ts',
module: {
rules: [
{
test: /\.ts?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
在 package.json 中设置 script
{
"scripts": {
"build": "webpack"
}
}
运转 npm ruh build
,报错了,缺乏 tsconfig.json 文件
当打包 typescript 文件的时候,须要在项目的根目次下建立一个 tsconfig.json 文件
以下为简朴设置,更多概况看官网
{
"compileerOptions": {
"outDir": "./dist", // 写不写都行
"module": "es6", // 用 es6 模块引入 import
"target": "es5", // 打包成 es5
"allowJs": true // 许可在 ts 中也能引入 js 的文件
}
}
再次打包,翻开 bundle.js 文件,将代码悉数拷贝到浏览器掌握台上,运用这段代码,可以看到弹窗涌现 Hello,world,申明 ts 编译打包胜利
引入第三方库
npm i lodash
import _ from 'lodash'
class Greeter {
greeting: string
constructor(message: string) {
this.greeting = message
}
greet() {
return _.join()
}
}
let greeter = new Greeter('world')
alert(greeter.greet())
lodash 的 join 要领须要我们通报参数,然则如今我们什么都没传,也没有报错,我们运用 typescript 就是为了范例搜检,在引入第三方库的时候也能云云,但是如今缺并没有报错或许提醒
我们还要装置一个 lodash 的 typescript 插件,如许就可以辨认 lodash 要领中的参数,一旦运用的不对就会报错出来
npm i @types/lodash -D
装置完今后可以发明下划线 _ 报错了
须要改成 import * as _ from 'lodash'
,将 join 要领通报的参数删除,还可以发明 join 要领的报错,这就表现了 typescript 的上风,同理,引入 jQuery 也要引入一个 jQuery 对应的范例插件
怎样晓得运用的库须要装置对应的范例插件呢?
翻开TypeSearch,在这里对应的去搜刮你想用的库有无范例插件,假如有只须要 npm i @types/jquery -D
即可
十九、Eslint 设置
建立一个空文件夹,npm init -y
,npm webpack webpack-cli -D
起手式,今后装置 eslint 依靠
npm i eslint -D
运用 npx 运转此项目中的 eslint 来初始化设置,npx eslint --init
这里会有挑选是 React/Vue/JavaScript,我们一致都先挑选 JavaScript。选完后会在项目的根目次下新建一个 .eslintrc.js
设置文件
module.exports = {
env: {
browser: true,
es6: true
},
extends: 'eslint:recommended',
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
},
rules: {}
}
内里就是 eslint 的一些范例,也可以定义一些划定规矩,详细看 eslint 设置划定规矩
在 index.js 中随意写点代码来测试一下 eslint
eslint 报错提醒,变量定义后却没有运用,假如在编辑器里没涌现报错提醒,须要在 vscode 里先装置一个 eslint 扩大,它会依据你当前目次的下的 .eslintrc.js
文件来做作为校验的划定规矩
也可以经由过程敕令行的情势,让 eslint 校验全部 src 目次下的文件
假如你以为某个划定规矩很贫苦,想屏蔽掉某个划定规矩的时候,可以如许,依据 eslint 的报错提醒,比方上面的 no-unused-vars
,将这条划定规矩复制一下,在 .eslintrc.js
中的 rules 里设置一下,"no-unused-vars": 0
,0 示意禁用,保留后,就不会报错了,然则这类体式格局是适用于全局的设置,假如你只想在某一行代码上屏蔽掉 eslint 校验,可以如许做
/* eslint-disable no-unused-vars */
let a = '1'
这个 eslint 的 vscode 扩大和 webpack 是没有什么关联的,我们如今要讲的是怎样在 webpack 里运用 eslint,起首装置一个插件
npm i eslint-loader -D
在 webpack.config.js 中举行设置
/* eslint-disable no-undef */
// eslint-disable-next-line no-undef
const path = require('path')
module.exports = {
mode: 'production',
entry: {
app: './src/index.js' // 须要打包的文件进口
},
module: {
rules: [
{
test: /\.js$/, // 运用正则来婚配 js 文件
exclude: /nodes_modules/, // 消除依靠包文件夹
use: {
loader: 'eslint-loader' // 运用 eslint-loader
}
}
]
},
output: {
// eslint-disable-next-line no-undef
publicPath: __dirname + '/dist/', // js 援用的途径或许 CDN 地点
// eslint-disable-next-line no-undef
path: path.resolve(__dirname, 'dist'), // 打包文件的输出目次
filename: 'bundle.js' // 打包后临盆的 js 文件
}
}
因为 webpack 设置文件也会被 eslint 校验,这里我先写上解释,封闭校验
假如你有运用 babel-loader 来转译,则 loader 应当这么写
loader: ['babel-loader', 'eslint-loader']
rules 的实行递次是从右往左,从下往上的,先经由 eslint 校验推断代码是不是相符范例,然后再经由过程 babel 来做转移
设置完 webpack.config.js,我们将 index.js 复原回之前报错的状况,不要运用解释封闭校验,然后运转打包敕令,记得去 package.json 设置 script
会在打包的时候,提醒代码不合格,不仅仅是临盆环境,开辟环境也可以设置,可以将 eslint-loader 设置到 webpack 的大众模块中,如许更有利于我们搜检代码范例
如:设置 fix 为 true,它会帮你自动修复一些毛病,不能自动修复的,照样须要你本身手动修复
{
loader: 'eslint-loader', // 运用 eslint-loader
options: {
fix: true
}
}
关于 eslint-loader,webpack 的官网也给出了设置,感兴趣的朋侪本身去看一看
二十、运用 DLLPlugin 加速打包速率
本节运用 demo15 的代码为基本
我们先装置一个 lodash 插件 npm i lodash
,并在 app.js 文件中写入
import _ from 'lodash'
console.log(_.join(['hello', 'world'], '-'))
在 build 文件夹下新建 webpack.dll.js 文件
const path = require('path')
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
}
}
这里运用 library,遗忘的朋侪可以回忆一下第十六节,自定义函数库里的内容,定义了 library 就相当于挂载了这个全局变量,只需在掌握台输入全局变量的称号就可以够显现内里的内容,比方这里我们是 library: '[name]'
对应的 name 就是我们在 entry 里定义的 vendors
在 package.json 中的 script 再新增一个敕令
{
"scripts": {
"dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
"build": "webpack --config ./build/webpack.prod.conf.js",
"build:dll": "webpack --config ./build/webpack.dll.js"
}
}
运转 npm run build:dll
,会天生 dll 文件夹,而且文件为 vendors.dll.js
翻开文件可以发明 lodash 已被打包到了 dll 文件中
那我们要怎样运用这个 vendors.dll.js 文件呢
须要再装置一个依靠 npm i add-asset-html-webpack-plugin
,它会将我们打包后的 dll.js 文件注入到我们天生的 index.html 中
在 webpack.base.conf.js 文件中引入
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/vendors.dll.js') // 对应的 dll 文件途径
})
]
}
运用 npm run dev
来翻开网页
如今我们已把第三方模块零丁打包成了 dll 文件,并运用
然则如今运用第三方模块的时候,要用 dll 文件,而不是运用 /node_modules/ 中的库,继承来修正 webpack.dll.js 设置
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {
vendors: ['lodash', 'jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
// 用这个插件来剖析打包后的这个库,把库里的第三方映照关联放在了这个 json 的文件下,这个文件在 dll 目次下
path: path.resolve(__dirname, '../dll/[name].manifest.json')
})
]
}
保留后从新打包 dll,npm run build:dll
修正 webpack.base.conf.js 文件,增添 webpack.DllReferencePlugin 插件
module.exports = {
plugins: [
// 引入我们打包后的映照文件
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/vendors.manifest.json')
})
]
}
今后再 webpack 打包的时候,就可以够连系之前的全局变量 vendors 和 这个新天生的 vendors.manifest.json 映照文件,然厥后对我们的源代码举行剖析,一旦剖析出运用第三方库是在 vendors.dll.js 里,就会去运用 vendors.dll.js,不会去运用 /node_modules/ 里的第三方库了
再次打包 npm run build
,可以把 webpack.DllReferencePlugin 模块解释后再打包对比一下
解释前 4000ms 摆布,解释后 4300ms 摆布,虽然只是快了 300ms,然则我们现在只是实验性的 demo,现实项目中,比方拿 vue 来讲,vue,vue-router,vuex,element-ui,axios 等第三方库都可以打包到 dll.js 里,那个时候的打包速率就可以提拔很多了
还可以继承拆分,修正 webpack.dll.js 文件
const path = require('path')
const webpack = require('webpack')
module.exports = {
mode: 'production',
entry: {
lodash: ['lodash'],
jquery: ['jquery']
},
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, '../dll'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: path.resolve(__dirname, '../dll/[name].manifest.json') // 用这个插件来剖析打包后的这个库,把库里的第三方映照关联放在了这个 json 的文件下,这个文件在 dll 目次下
})
]
}
运转 npm run build:dll
可以把之前打包的 vendors.dll.js 和 vendors.manifest.json 映照文件给删撤除
然后再修正 webpack.base.conf.js
module.exports = {
plugins: [
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/lodash.dll.js')
}),
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll/jquery.dll.js')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/lodash.manifest.json')
}),
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll/jquery.manifest.json')
})
]
}
保留后运转 npm run dev
,看看能不能胜利运转
这还只是拆分了两个第三方模块,就要一个个设置过去,有无什么方法能轻便一点呢? 有!
这里运用 node 的 api,fs 模块来读取文件夹里的内容,建立一个 plugins 数组用来寄存大众的插件
const fs = require('fs')
const plugins = [
// 开辟环境和临盆环境两者均须要的插件
new HtmlWebpackPlugin({
title: 'webpack4 实战',
filename: 'index.html',
template: path.resolve(__dirname, '..', 'index.html'),
minify: {
collapseWhitespace: true
}
}),
new webpack.ProvidePlugin({ $: 'jquery' })
]
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
console.log(files)
写完可以先输出一下,把 plugins 给解释掉,npm run build
打包看看输出的内容,可以看到文件夹中的内容以数组的情势被打印出来了,今后我们对这个数组做一些轮回操纵就好了
完全代码:
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
// 寄存大众插件
const plugins = [
// 开辟环境和临盆环境两者均须要的插件
new HtmlWebpackPlugin({
title: 'webpack4 实战',
filename: 'index.html',
template: path.resolve(__dirname, '..', 'index.html'),
minify: {
collapseWhitespace: true
}
}),
new webpack.ProvidePlugin({ $: 'jquery' })
]
// 自动引入 dll 中的文件
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => {
if (/.*\.dll.js/.test(file)) {
plugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
})
)
}
if (/.*\.manifest.json/.test(file)) {
plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
)
}
})
module.exports = {
entry: {
app: './src/app.js'
},
output: {
path: path.resolve(__dirname, '..', 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
}
]
},
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
limit: 1000, // size <= 1KB
outputPath: 'images/'
}
},
// img-loader for zip img
{
loader: 'image-webpack-loader',
options: {
// 紧缩 jpg/jpeg 图片
mozjpeg: {
progressive: true,
quality: 65 // 紧缩率
},
// 紧缩 png 图片
pngquant: {
quality: '65-90',
speed: 4
}
}
}
]
},
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file
publicPath: 'fonts/',
outputPath: 'fonts/'
}
}
}
]
},
plugins,
performance: false
}
运用 npm run dev
翻开网页也没有问题了,如许自动注入 dll 文件也搞定了,今后还要再打包第三方库只需增添到 webpack.dll.js 内里的 entry
属性中就可以够了
二十一、多页面打包设置
本节运用 demo20 的代码为基本
在 src 目次下新建 list.js 文件,内里写 console.log('这里是 list 页面')
在 webpack.base.conf.js 中设置 entry,设置两个进口
module.exports = {
entry: {
app: './src/app.js',
list: './src/list.js'
}
}
假如如今我们直接 npm run build
打包,在打包自动天生的 index.html 文件中会发明 list.js 也被引入了,申明多进口打包胜利,但并没有完成多个页面的打包,我想打包出 index.html 和 list.html 两个页面,而且在 index.html 中引入 app.js,在 list.html 中引入 list.js,该怎么做?
为了轻易演示,先将 webpack.prod.conf.js
中 cacheGroups
新增一个 default
属性,自定义 name
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
jquery: {
name: 'jquery', // 零丁将 jquery 拆包
priority: 15,
test: /[\\/]node_modules[\\/]jquery[\\/]/
},
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors'
},
default: {
name: 'code-segment'
}
}
}
}
翻开 webpack.base.conf.js
文件,将 HtmlWebpackPlugin
拷贝一份,运用 chunks
属性,将须要打包的模块对应写入
// 寄存大众插件
const plugins = [
new HtmlWebpackPlugin({
title: 'webpack4 实战',
filename: 'index.html',
template: path.resolve(__dirname, '..', 'index.html'),
chunks: ['app', 'vendors', 'code-segment', 'jquery', 'lodash']
}),
new HtmlWebpackPlugin({
title: '多页面打包',
filename: 'list.html',
template: path.resolve(__dirname, '..', 'index.html'),
chunks: ['list', 'vendors', 'code-segment', 'jquery', 'lodash']
}),
new CleanWebpackPlugin(),
new webpack.ProvidePlugin({ $: 'jquery' })
]
打包后的 dist 目次下天生了两个 html
翻开 index.html 可以看到引入的是 app.js,而 list.html 引入的是 list.js,这就是 HtmlWebpackPlugin
插件的 chunks
属性,自定义引入的 js
假如要打包三个页面,再去 copy HtmlWebpackPlugin
,经由过程在 entry 中设置,假如有四个,五个,如许手动的复制就比较贫苦了,可以写个要领自动天生 HtmlWebpackPlugin
设置
修正 webpack.base.conf.js
const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin')
const makePlugins = configs => {
// 基本插件
const plugins = [
new CleanWebpackPlugin(),
new webpack.ProvidePlugin({ $: 'jquery' })
]
// 依据 entry 自动天生 HtmlWebpackPlugin 设置,设置多页面
Object.keys(configs.entry).forEach(item => {
plugins.push(
new HtmlWebpackPlugin({
title: '多页面设置',
template: path.resolve(__dirname, '..', 'index.html'),
filename: `${item}.html`,
chunks: [item, 'vendors', 'code-segment', 'jquery', 'lodash']
})
)
})
// 自动引入 dll 中的文件
const files = fs.readdirSync(path.resolve(__dirname, '../dll'))
files.forEach(file => {
if (/.*\.dll.js/.test(file)) {
plugins.push(
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, '../dll', file)
})
)
}
if (/.*\.manifest.json/.test(file)) {
plugins.push(
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, '../dll', file)
})
)
}
})
return plugins
}
const configs = {
entry: {
index: './src/app.js',
list: './src/list.js'
},
output: {
path: path.resolve(__dirname, '..', 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader'
}
]
},
{
test: /\.(png|jpg|jpeg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
limit: 1000, // size <= 1KB
outputPath: 'images/'
}
},
// img-loader for zip img
{
loader: 'image-webpack-loader',
options: {
// 紧缩 jpg/jpeg 图片
mozjpeg: {
progressive: true,
quality: 65 // 紧缩率
},
// 紧缩 png 图片
pngquant: {
quality: '65-90',
speed: 4
}
}
}
]
},
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'url-loader',
options: {
name: '[name]-[hash:5].min.[ext]',
limit: 5000, // fonts file size <= 5KB, use 'base64'; else, output svg file
publicPath: 'fonts/',
outputPath: 'fonts/'
}
}
}
]
},
performance: false
}
makePlugins(configs)
configs.plugins = makePlugins(configs)
module.exports = configs
再次打包后效果雷同,假如还要增添页面,只需在 entry 中再引入一个 js 文件作为进口即可
多页面设置实在就是定义多个 entry,合营 htmlWebpackPlugin 天生多个 html 页面
二十二、编写 loader
新建文件夹,npm init -y
,npm i webpack webpack-cli -D
,新建 src/index.js,写入 console.log('hello world')
新建 loaders/replaceLoader.js
文件
module.exports = function(source) {
return source.replace('world', 'loader')
}
source 参数就是我们的源代码,这里是将源码中的 world 替换成 loader
新建 webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /.js/,
use: [path.resolve(__dirname, './loaders/replaceLoader.js')] // 引入自定义 loader
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
目次组织:
打包后翻开 dist/main.js 文件,在最底部可以看到 world 已被改成了 loader,一个最简朴的 loader 就写完了
增添 optiions 属性
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
module: {
rules: [
{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js'),
options: {
name: 'xh'
}
}
]
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
修正 replaceLoader.js 文件,保留后打包,输出看看效果
module.exports = function(source) {
console.log(this.query)
return source.replace('world', this.query.name)
}
打包后天生的文件也改成了 options 中定义的 name
更多的设置见官网 API,找到 Loader Interface,内里有个 this.query
假如你的 options 不是一个对象,而是按字符串情势写的话,能够会有一些问题,这里官方引荐运用 loader-utils 来猎取 options 中的内容
装置 npm i loader-utils -D
,修正 replaceLoader.js
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
console.log(options)
return source.replace('world', options.name)
}
console.log(options)
与 console.log(this.query)
输出内容一致
假如你想通报分外的信息出去,return 就不好用了,官网给我们供应了 this.callback API,用法以下
this.callback(
err: Error | null,
content: string | Buffer,
sourceMap?: SourceMap,
meta?: any
)
修正 replaceLoader.js
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const result = source.replace('world', options.name)
this.callback(null, result)
}
现在没有用到 sourceMap(必需是此模块可剖析的源映照)、meta(可所以任何内容(比方一些元数据)) 这两个可选参数,只将 result 返回归去,保留从新打包后,效果和 return 是一样的
假如在 loader 中写异步代码,会怎样
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
setTimeout(() => {
const result = source.replace('world', options.name)
return result
}, 1000)
}
报错 loader 没有返回,这里运用 this.async 来写异步代码
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()
setTimeout(() => {
const result = source.replace('world', options.name)
callback(null, result)
}, 1000)
}
模仿一个同步 loader 和一个异步 loader
新建一个 replaceLoaderAsync.js
文件,将之前写的异步代码放入,修正 replaceLoader.js
为同步代码
// replaceLoaderAsync.js
const loaderUtils = require('loader-utils')
module.exports = function(source) {
const options = loaderUtils.getOptions(this)
const callback = this.async()
setTimeout(() => {
const result = source.replace('world', options.name)
callback(null, result)
}, 1000)
}
// replaceLoader.js
module.exports = function(source) {
return source.replace('xh', 'world')
}
修正 webpack.config.js
,loader 的实行递次是从下到上,先实行异步代码,将 world 改成 xh,再实行同步代码,将 xh 改成 world
module: {
rules: [
{
test: /.js/,
use: [
{
loader: path.resolve(__dirname, './loaders/replaceLoader.js')
},
{
loader: path.resolve(__dirname, './loaders/replaceLoaderAsync.js'),
options: {
name: 'xh'
}
}
]
}
]
}
保留后打包,在 mian.js 中可以看到已改成了 hello world
,运用多个 loader 也完成了
假如有多个自定义 loader,每次都经由过程 path.resolve(__dirname, xxx)
这类体式格局去写,有无更好的要领?
运用 resolveLoader
,定义 modules,当你运用 loader 的时候,会先去 node_modules
中去找,假如没找到就会去 ./loaders
中找
const path = require('path')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
resolveLoader: {
modules: ['node_modules', './loaders']
},
module: {
rules: [
{
test: /.js/,
use: [
{
loader: 'replaceLoader.js'
},
{
loader: 'replaceLoaderAsync.js',
options: {
name: 'xh'
}
}
]
}
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
二十三、编写 plugin
起首新建一个文件夹,npm 起手式操纵一番,详细的在前几节已说了,不再赘述
在根目次下新建 plugins 文件夹,新建 copyright-webpack-plugin.js
,平常我们用的都是 xxx-webpack-plugin
,所以我们定名也按如许来,plugin 的定义是一个类
class CopyrightWebpackPlugin {
constructor() {
console.log('插件被运用了')
}
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
在 webpack.config.js 中运用,所以每次运用 plugin 都要运用 new,因为本质上 plugin 是一个类
const path = require('path')
const CopyrightWebpackPlugin = require('./plugins/copyright-webpack-plugin')
module.exports = {
mode: 'development',
entry: {
main: './src/index.js'
},
plugins: [new CopyrightWebpackPlugin()],
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
}
}
保留后打包,插件被运用了,只不过我们什么都没干
假如我们要通报参数,可以如许
new CopyrightWebpackPlugin({
name: 'xh'
})
同时在 copyright-webpack-plugin.js
中吸收
class CopyrightWebpackPlugin {
constructor(options) {
console.log('插件被运用了')
console.log('options = ', options)
}
apply(compiler) {}
}
module.exports = CopyrightWebpackPlugin
我们先把 constructor 解释掉,期近将要把打包的效果,放入 dist 目次之前的这个时候,我们来做一些操纵
apply(compiler) {}
compiler 可以看做是 webpack 的实例,详细见官网 compiler-hooks
hooks 是钩子,像 vue、react 的生命周期一样,找到 emit
这个时候,将打包效果放入 dist 目次前实行,这里是个 AsyncSeriesHook
异步要领
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
console.log(11)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
因为 emit 是异步的,可以经由过程 tapAsync 来写,当要把代码放入到 dist 目次之前,就会触发这个钩子,走到我们定义的函数里,假如你用 tapAsync 函数,记得末了要用 cb() ,tapAsync 要通报两个参数,第一个参数通报我们定义的插件称号
保留后再次打包,我们写的内容也输出了
compilation 这个参数里寄存了此次打包的一切内容,可以输出一下 compilation.assets
看一下
返回效果是一个对象,main.js
是 key,也就是打包后天生的文件名及文件后缀,我们可以来模仿一下
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
// 天生一个 copyright.txt 文件
compilation.assets['copyright.txt'] = {
source: function() {
return 'copyright by xh'
},
size: function() {
return 15 // 上面 source 返回的字符长度
}
}
console.log('compilation.assets = ', compilation.assets)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
在 dist 目次下天生了 copyright.txt
文件
之前引见的是异步钩子,如今运用同步钩子
class CopyrightWebpackPlugin {
apply(compiler) {
// 同步钩子
compiler.hooks.compile.tap('CopyrightWebpackPlugin', compilation => {
console.log('compile')
})
// 异步钩子
compiler.hooks.emit.tapAsync(
'CopyrightWebpackPlugin',
(compilation, cb) => {
compilation.assets['copyright.txt'] = {
source: function() {
return 'copyright by xh'
},
size: function() {
return 15 // 字符长度
}
}
console.log('compilation.assets = ', compilation.assets)
cb()
}
)
}
}
module.exports = CopyrightWebpackPlugin
二十四、编写 Bundle
模块剖析
在 src 目次下新建三个文件 word.js
、message.js
、index.js
,对应的代码:
// word.js
export const word = 'hello'
// message.js
import { word } from './word.js'
const message = `say ${word}`
export default message
// index.js
import message from './message.js'
console.log(message)
新建 bundle.js
const fs = require('fs')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
console.log(content)
}
moduleAnalyser('./src/index.js')
运用 node 的 fs 模块,读取文件信息,并在掌握台输出,这里全局装置一个插件,来显现代码高亮,npm i cli-highlight -g
,运转 node bundle.js | highlight
index.js 中的代码已被输出到掌握台上,而且代码有高亮,轻易浏览,读取进口文件信息就完成了
如今我们要读取 index.js 文件中运用的 message.js 依靠,import message from './message.js'
装置一个第三方插件 npm i @babel/parser
@babel/parser 是 Babel 中运用的 JavaScript 剖析器。
官网也供应了响应的示例代码,依据示例代码来模仿,修正我们的文件
const fs = require('fs')
const parser = require('@babel/parser')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
console.log(
parser.parse(content, {
sourceType: 'module'
})
)
}
moduleAnalyser('./src/index.js')
我们运用的是 es6 的 module 语法,所以 sourceType: 'module'
保留后运转,输出了 AST (笼统语法树),内里有一个 body 字段,我们输出这个字段
const fs = require('fs')
const parser = require('@babel/parser')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
console.log(ast.program.body)
}
moduleAnalyser('./src/index.js')
打印出了两个 Node 节点,第一个节点的 type 是 ImportDeclaration(引入的声明),对比我们在 index.js 中写的 import message from './message.js'
,第二个节点的 type 是 ExpressionStatement (表达式的声明),对比我们写的 console.log(message)
运用 babel 来帮我们天生笼统语法树,我们再导入 import message1 from './message1.js'
再运转
笼统语法树将我们的 js 代码转成了对象的情势,如今就可以够遍历笼统语法树天生的节点对象中的 type,是不是为 ImportDeclaration
,就可以找到代码中引入的依靠了
再借助一个东西 npm i @babel/traverse
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
traverse(ast, {
ImportDeclaration({ node }) {
console.log(node)
}
})
}
moduleAnalyser('./src/index.js')
只打印了两个 ImportDeclaration,遍历完毕,我们只须要取到依靠的文件名,在打印的内容中,每一个节点都有个 source
属性,内里有个 value
字段,示意的就是文件途径及文件名
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencise = []
traverse(ast, {
ImportDeclaration({ node }) {
dependencise.push(node.source.value)
}
})
console.log(dependencise)
}
moduleAnalyser('./src/index.js')
保留完从新运转,输出效果:
['./message.js', './message1.js']
如许就对进口文件的依靠剖析就剖析出来了,如今把 index.js 中引入的 message1.js
的依靠给删除,这里有个注重点,打印出来的文件途径是相对途径,相对于 src/index.js
文件,然则我们打包的时候不能是进口文件(index.js)的相对途径,而应当是根目次的相对途径(或许说是绝对途径),借助 node 的 api,引入一个 path
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencise = []
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
console.log(dirname)
dependencise.push(node.source.value)
}
})
// console.log(dependencise)
}
moduleAnalyser('./src/index.js')
输出为 ./src
,继承修正
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const newFile = path.join(dirname, node.source.value)
console.log(newFile)
dependencise.push(node.source.value)
}
输出为 src\message.js
windows 和 类 Unix(linux/mac),途径是有区分的。windows 是用反斜杠 支解目次或许文件的,而在类 Unix 的体系中是用的
/。
因为我是 windows 体系,所以这里输出为 src\message.js
,而类 Unix 输出的为 src/message.js
.\src\message.js
这个途径是我们真正打包时要用到的途径
newFile .\src\message.js
[ '.\\src\\message.js' ]
既存一个相对途径,又存一个绝对途径
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencise = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const newFile = '.\\' + path.join(dirname, node.source.value)
console.log('newFile', newFile)
dependencise[node.source.value] = newFile
}
})
console.log(dependencise)
return {
filename,
dependencise
}
}
moduleAnalyser('./src/index.js')
newFile .\src\message.js
{ './message.js': '.\\src\\message.js' }
因为我们写的代码是 es6,浏览器没法辨认,照样须要 babel 来做转换
npm i @babel/core @babel/preset-env
'use strict'
var _message = _interopRequireDefault(require('./message.js'))
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_message.default)
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const moduleAnalyser = filename => {
const content = fs.readFileSync(filename, 'utf-8')
const ast = parser.parse(content, {
sourceType: 'module'
})
const dependencise = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(filename)
const newFile = '.\\' + path.join(dirname, node.source.value)
dependencise[node.source.value] = newFile
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
return {
filename,
dependencise,
code
}
}
const moduleInfo = moduleAnalyser('./src/index.js')
console.log(moduleInfo)
剖析的效果就在掌握台上打印了
{ filename: './src/index.js',
dependencise: { './message.js': '.\\src\\message.js' },
code:
'"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);' }
现在我们只对一个模块举行剖析,接下来要对全部项目举行剖析,所以我们先剖析了进口文件,再剖析进口文件中所运用的依靠
依靠图谱
建立一个函数来轮回依靠并天生图谱
// 依靠图谱
const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArray = [entryModule]
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const { dependencise } = item
// 假如进口文件有依靠就去做轮回依靠,对每一个依靠做剖析
if (dependencise) {
for (const j in dependencise) {
if (dependencise.hasOwnProperty(j)) {
graphArray.push(moduleAnalyser(dependencise[j]))
}
}
}
}
console.log('graphArray = ', graphArray)
}
将进口的依靠,依靠中的依靠悉数都剖析完放到 graphArray 中,掌握台输出的打印效果
可以看到 graphArray 中一共有三个对象,就是我们在项目中引入的三个文件,悉数被剖析出来了,为了轻易浏览,我们建立一个 graph 对象,将剖析的效果顺次放入
// 依靠图谱
const makeDependenciesGraph = entry => {
const entryModule = moduleAnalyser(entry)
const graphArray = [entryModule]
for (let i = 0; i < graphArray.length; i++) {
const item = graphArray[i]
const { dependencise } = item
// 假如进口文件有依靠就去做轮回依靠,对每一个依靠做剖析
if (dependencise) {
for (const j in dependencise) {
if (dependencise.hasOwnProperty(j)) {
graphArray.push(moduleAnalyser(dependencise[j]))
}
}
}
}
// console.log('graphArray = ', graphArray)
// 建立一个对象,将剖析后的效果放入
const graph = {}
graphArray.forEach(item => {
graph[item.filename] = {
dependencise: item.dependencise,
code: item.code
}
})
console.log('graph = ', graph)
return graph
}
输出的 graph 为:
末了在 makeDependenciesGraph
函数中将 graph 返回,赋值给 graphInfo,输出的效果和 graph 是一样的
const graghInfo = makeDependenciesGraph('./src/index.js')
console.log(graghInfo)
天生代码
如今已拿到了一切代码天生的效果,如今我们借助 DependenciesGraph(依靠图谱) 来天生真正能在浏览器上运转的代码
最好放在一个大的闭包中来实行,防止污染全局环境
const generateCode = entry => {
// makeDependenciesGraph 返回的是一个对象,须要转换成字符串
const graph = JSON.stringify(makeDependenciesGraph(entry))
return `
(function (graph) {
})(${graph})
`
}
const code = generateCode('./src/index.js')
console.log(code)
我这里先把输出的 graph 代码花样化了一下,可以发如今 index.js
用到了 require
要领,message.js
中不仅用了 require
要领,还用 exports
对象,然则在浏览器中,这些都是不存在的,假如我们直接去实行,是会报错的
let graph = {
'./src/index.js': {
dependencise: { './message.js': '.\\src\\message.js' },
code: `
"use strict";\n\n
var _message = _interopRequireDefault(require("./message.js"));\n\n
function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
console.log(_message.default);
`
},
'.\\src\\message.js': {
dependencise: { './word.js': '.\\src\\word.js' },
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;'
},
'.\\src\\word.js': {
dependencise: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;'
}
}
接下来要去组织 require 要领和 exports 对象
const generateCode = entry => {
console.log(makeDependenciesGraph(entry))
// makeDependenciesGraph 返回的是一个对象,须要转换成字符串
const graph = JSON.stringify(makeDependenciesGraph(entry))
return `
(function (graph) {
// 定义 require 要领
function require(module) {
};
require('${entry}')
})(${graph})
`
}
const code = generateCode('./src/index.js')
console.log(code)
graph 是依靠图谱,拿到 entry 后去实行 ./src/index.js
中的 code,也就是下面高亮部份的代码,为了直观我把前面输出的 graph 代码拿下来参考:
let graph = {
'./src/index.js': {
dependencise: { './message.js': '.\\src\\message.js' },
code: `
"use strict";\n\n
var _message = _interopRequireDefault(require("./message.js"));\n\n
function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
console.log(_message.default);
`
}
}
为了让 code 中的代码实行,这里再运用一个闭包,让每一个模块里的代码放到闭包里来实行,如许模块的变量就不会影响到外部的变量
return `
(function (graph) {
// 定义 require 要领
function require(module) {
(function (code) {
eval(code)
})(graph[module].code)
};
require('${entry}')
})(${graph})
`
闭包里通报的是 graph[module].code
,如今 entry 也就是 ./src/index.js
这个文件,会传给 require 中的 module 变量,现实上去找依靠图谱中 ./src/index.js
对应的对象,然后再去找到 code 中对应的代码,也就是下面这段代码,被我花样化过,为了演示效果
'use strict'
var _message = _interopRequireDefault(require('./message.js'))
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_message.default)
然则我们会发明,这里 _interopRequireDefault(require('./message.js'))
引入的是 ./message.js
相对途径,比及第二次实行的时候,require(module)
这里的 module
对应的就是 ./message.js
它会到 graph 中去找 ./message.js
下对应的 code,但是我们在 graph 中存的是 '.\\src\\message.js'
绝对途径,如许就会找不到对象
因为我们之前写代码的时候引入的是相对途径,如今我们要把相对途径转换成绝对途径才准确实行,定义一个 localRequire 要领,如许当下次去找的时候就会走我们本身定义的 localRequire,实在就是一个相对途径转换的要领
return `
(function (graph) {
// 定义 require 要领
function require(module) {
// 相对途径转换
function localRequire(relativePath) {
return require(graph[module].dependencise[relativePath])
}
(function (require, code) {
eval(code)
})(localRequire, graph[module].code)
};
require('${entry}')
})(${graph})
`
我们定义了 localRequire 要领,并把它通报到闭包里,当实行了 eval(code)
时实行了 require
要领,就不是实行外部的 require(module)
这个要领,而是实行我们通报进去的 localRequire 要领
我们在剖析出的代码中是如许引入 message.js
的
var _message = _interopRequireDefault(require('./message.js'))
这里挪用了 require('./message.js')
,就是我们上面写的 require
要领,也就是 localRequire(relativePath)
所以 relativePath 就是 './message.js'
这个要领返回的是 require(graph[module].dependencise[relativePath])
这里我把参数带进去,就是如许:
graph('./src/index.js').dependencise['./message.js']
let graph = {
'./src/index.js': {
dependencise: { './message.js': '.\\src\\message.js' },
code: `
"use strict";\n\n
var _message = _interopRequireDefault(require("./message.js"));\n\n
function _interopRequireDefault(obj){ return obj && obj.__esModule ? obj : { default: obj }; } \n\n
console.log(_message.default);
`
}
}
对比着图谱就可以发明终究返回的就是 '.\\src\\message.js'
绝对途径,返回绝对途径后,我们再挪用 require(graph('./src/index.js').dependencise['./message.js'])
就是实行外部定义的 require(module)
这个要领,从新递归的去实行,光如许还不够,这只是完成了 require 要领,还差 exports 对象,所以我们再定义一个 exports 对象
return `
(function (graph) {
// 定义 require 要领
function require(module) {
// 相对途径转换
function localRequire(relativePath) {
return require(graph[module].dependencise[relativePath])
}
var exports = {};
(function (require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports
};
require('${entry}')
})(${graph})
`
末了要记得 return exports
将 exports 导出,如许下一个模块在引入这个模块的时候才拿到导出的效果,如今代码天生的流程就写完了,终究返回的是一个大的字符串,保留再次运转 node bundle.js | highlight
这里我是 windows 环境,将输出完的代码直接放到浏览器里不可,我就把紧缩的代码花样化成下面这类模样,再放到浏览器里就可以输出胜利了
;(function(graph) {
function require(module) {
function localRequire(relativePath) {
return require(graph[module].dependencise[relativePath])
}
var exports = {}
;(function(require, exports, code) {
eval(code)
})(localRequire, exports, graph[module].code)
return exports
}
require('./src/index.js')
})({
'./src/index.js': {
dependencise: { './message.js': '.\\src\\message.js' },
code:
'"use strict";\n\nvar _message = _interopRequireDefault(require("./message.js"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message.default);'
},
'.\\src\\message.js': {
dependencise: { './word.js': '.\\src\\word.js' },
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.default = void 0;\n\nvar _word = require("./word.js");\n\nvar message = "say ".concat(_word.word);\nvar _default = message;\nexports.default = _default;'
},
'.\\src\\word.js': {
dependencise: {},
code:
'"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.word = void 0;\nvar word = \'hello\';\nexports.word = word;'
}
})
将上面代码放入浏览器的掌握台中,回车就可以输出 say hello
总结
这就是打包东西打包后的内容,时期触及了 node 学问,运用 babel 来转译 ast(笼统语法树),末了的 generateCode 函数触及到了递归和闭包,形参和实参,须要人人多看几遍,加深明白