繼 24 個例項入門並掌握「Webpack4」(二) 後續:
十七、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 函式涉及到了遞迴和閉包,形參和實參,需要大家多看幾遍,加深理解