debug(調試)webpack:
因為plugin和loader的編寫需要nodejs環境,需要追蹤一些參數,這時候調試就顯得很重要了,但調試webpack不像在浏覽器中debug那麼輕松,需要一些配置:
首先在
package.json
中加入如下代碼:
改指令表示調試webpack并停在第一行
"scripts": {
"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
}
調試分為兩種:
- 浏覽器中調試
- vscode中調試
浏覽器中debug:
指令行直接執行
npm run start
出現上述調試,說明
啟動成功
,
打開浏覽器
,
打開開發者工具
,可以看到控制台多了一個選項
到這裡說明調試成功了
vscode中調試(我更喜歡的方式):
更改
package.json
如下:
"scripts": {
"start": "node --inspect-brk=5858 ./node_modules/webpack/bin/webpack.js"
},
點選
vscode
中如下按鍵建立
launch.json
檔案:
更改
launch.json
檔案如下:
端口好要對上
{
// 使用 IntelliSense 了解相關屬性。
// 懸停以檢視現有屬性的描述。
// 欲了解更多資訊,請通路: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "啟動程式",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start"],
"port": 5858
}
]
}
此時按
F5
或者點選此處即可開始
debug
效果如下:
同理,如果你是
vue
或者
react
的腳手架建立出來的項目,隻需要把
script
中調試的
js
檔案換成
cli
中的
核心起始js檔案
即可,以
vuecli
為例,
package.json
中
script
換成如下:
"scripts": {
"debug": "node --inspect-brk=9229 ./node_modules/@vue/cli-service/bin/vue-cli-service.js serve"
}
loader:
loader
本質上是一個函數
在一個
loader.js
檔案中編寫如下代碼:
module.exports = function (content, map, meta) {
console.log(content);
return content;
}
webpack.config.js
配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: 'babelLoader',
},
]
},
}
那麼當執行
webpack
的時候,所有js檔案都會經過我們編寫的
loader
,
content
中的内容即是源碼,我們可以通過一些正則,方法把源碼轉換後打包,這就是
loader
的原理了
loader的分類:
同步:
寫法一:
//同步loader
module.exports = function (content, map, meta) {
return content;
}
寫法二:
module.exports = function (content, map, meta) {
this.callback(null, content, map, meta);
}
異步:
// 異步loader
module.exports = function (content, map, meta) {
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 1000)
}
一般推薦的寫法是異步
loader
,因為不會對打包造成阻塞,性能較好
loader中一些工具方法:
-
getOptions
-
validate
loader.js
代碼如下:
const { getOptions } = require('loader-utils');
const { , } = require('schema-utils');
const schema = require('./schema');
module.exports = function (content, map, meta) {
// 擷取options
const options = getOptions(this);
// 校驗options是否合法
validate(schema, options, {
name: 'loader' //填一個loader的名字,控制台會提示哪個loader的配置校驗失敗
})
return content;
}
webpack.config.js
配置如下:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
loader: './loader.js',
options: {
name:"zhangsan"
}
},
]
},
}
./schema.json
如下:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "名稱~"
}
},
"additionalProperties": false //是否允許options中有額外的屬性
}
./schema.json
是從來配置
option
傳入的規則,不符合規則,則會報錯,
getOptions
是用來取到傳入的
loader
的
option
配置,
validate
則是配合
./schema.json
中的規則來校驗拿到的
options
配置是否符合預期
實際應用:
使用@babel/core編譯js檔案:
編寫
loader.js
如下:
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');
const babelSchema = require('./babelSchema.json');
// babel.transform用來編譯代碼的方法
// 是一個普通異步方法
// util.promisify将普通異步方法轉化成基于promise的異步方法
const transform = util.promisify(babel.transform);
module.exports = function (content, map, meta) {
// 擷取loader的options配置
const options = getOptions(this) || {};
// 校驗babel的options的配置
validate(babelSchema, options, {
name: 'Babel Loader'
});
// 建立異步
const callback = this.async();
// 使用babel編譯代碼
transform(content, options)
.then(({code, map}) => callback(null, code, map, meta))
.catch((e) => callback(e))
}
plugin:
plugin要比loader強大很多,不僅能影響源碼,還能影響檔案
webpack.config.js
編寫如下:
/const Plugin1 = require('./plugins/Plugin')
module.exports = {
plugins: [
new Plugin1()
]
}
Plugin.js
編寫如下:
class Plugin1 {
apply(complier) {
complier.hooks.emit.tap('Plugin1', (compilation) => {
console.log('emit.tap 111');
})
}
}
module.exports = Plugin1;
apply
是一種固定的寫法,可以參考官網,
apply
中我們可以注冊自己的鈎子,那麼在
webpack
執行的時候,執行到
emit
階段,就會觸發我們的鈎子,對檔案進行操作
實際應用:
編寫一個插件,增加一個a.txt檔案,檢視文檔可知,可在
additionalAssets
這個鈎子中增加資源
plugin.js
代碼如下:
class Plugin2 {
apply(compiler) {
// 初始化compilation鈎子
compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
const content = 'hello plugin2';
// 往要輸出資源中,添加一個a.txt
compilation.assets['a.txt'] = {
// 檔案大小
size() {
return content.length;
},
// 檔案内容
source() {
return content;
}
}
cb();
})
})
}
}
module.exports = Plugin2;
webpack.config.js
中使用:
// const Plugin2 = require('./plugins/Plugin2')
module.exports = {
plugins: [
new Plugin2()
]
}
那麼打包後,dist檔案中就會新增一個a.txt的資源檔案了
這種添加資源的方式挺麻煩的内容和大小都要自己填寫,有沒有自動計算的方式呢?
利用
webpack
提供的一些工具函數,可以更加友善的生成和操作檔案
改寫
plugin.js
如下:
const fs = require('fs');
const util = require('util');
const path = require('path');
const webpack = require('webpack');
const { RawSource } = webpack.sources;
// 将fs。readFile方法變成基于promise風格的異步方法
const readFile = util.promisify(fs.readFile);
class Plugin2 {
apply(compiler) {
compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
//讀取b.txt檔案,讀取後是個buffer
const data = await readFile(path.resolve(__dirname, 'b.txt'));
compilation.assets['a.txt'] = new RawSource(data);
cb();
})
})
}
}
module.exports = Plugin2;
以上介紹了一些常用的工具方法,更多方法還得參考官網
實際應用:
編寫一個
copy plugin
:用于複制資源,複制public中的資源到dist目錄下
plugin.js
代碼如下:
const path = require('path');
const fs = require('fs');
const {promisify} = require('util')
const { validate } = require('schema-utils');
//可以比對檔案清單,過濾一些檔案
const globby = require('globby');
const webpack = require('webpack');
const schema = require('./schema.json');
const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources
class CopyWebpackPlugin {
constructor(options = {}) {
// 驗證options是否符合規範
validate(schema, options, {
name: 'CopyWebpackPlugin'
})
this.options = options;
}
apply(compiler) {
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
// 将from中的資源複制到to中,輸出出去
const { from, ignore } = this.options;
const to = this.options.to ? this.options.to : '.';
// context就是webpack配置
// 運作指令的目錄
const context = compiler.options.context; // process.cwd()
// 将輸入路徑變成絕對路徑
const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);
// 1. 過濾掉ignore的檔案
// globby(要處理的檔案夾,options)
const paths = await globby(absoluteFrom, { ignore });
console.log(paths); // 所有要加載的檔案路徑數組
// 2. 讀取paths中所有資源
const files = await Promise.all(
paths.map(async (absolutePath) => {
// 讀取檔案
const data = await readFile(absolutePath);
// basename得到最後的檔案名稱
const relativePath = path.basename(absolutePath);
// 和to屬性結合
// 沒有to --> reset.css
// 有to --> css/reset.css
const filename = path.join(to, relativePath);
return {
// 檔案資料
data,
// 檔案名稱
filename
}
})
)
// 3. 生成webpack格式的資源
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename
}
})
// 4. 添加compilation中,輸出出去
assets.forEach((asset) => {
compilation.emitAsset(asset.filename, asset.source);
})
cb();
})
})
}
}
module.exports = CopyWebpackPlugin;
schema.json
驗證規則如下:
{
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"ignore": {
"type": "array"
}
},
"additionalProperties": false
}
webpack.config.js
代碼如下:
const CopyWebpackPlugin = require('./plugins/CopyWebpackPlugin')
module.exports = {
plugins: [
new CopyWebpackPlugin({
from: 'public',
to: 'css',
ignore: ['**/index.html']
})
]
}
首先我的
public
目錄如下:
執行
npx webpack
指令輸出如下:
插件是成功運作的
plugin和loader編寫的思路大緻就是這樣了,更加深入的使用,無非就是熟讀文檔和參與到更多的實踐中去