寫在前面:
在開發的工程中,線上環境需要引入一些統計和列印日誌的js檔案。但是對於開發環境,加速打包速度減少頁面渲染時間很關鍵。我於是想根據開發環境,寫一個簡單的loader,按需載入一些資源。
例如:在index.js中,用自定義函式envLoader新增資源
index.js
//......
envLoader(
'/vendor/log.js'
)
//......
複製程式碼
為了完成按需載入的功能。打算使用自定義的loader。 實現思路如下:
- 新增js loader 對index.js進行處理
- 解析envLoader函式
- 拿到傳入的引數並根據環境判斷是否載入。
結合官網的loader api瞭解webpack loader的工作原理。
將使用以下api
- loader-utils
- schema-utils
- this.async
- this.cacheable
- getOptions
- validateOptions
- urlToRequest
開始擼一個自己的loader (^-^)V
Webpack Loader
loader 用於對模組的原始碼進行轉換。loader 可以使你在 import 或"載入"模組時預處理檔案。因此,loader 類似於其他構建工具中“任務(task)”,並提供了處理前端構建步驟的強大方法。loader 可以將檔案從不同的語言(如 TypeScript)轉換為 JavaScript,或將內聯影象轉換為 data URL。
一、基本用法
loader 是匯出為一個函式的 node 模組。該函式在 loader 轉換資源的時候呼叫。給定的函式將呼叫 loader API,並通過 this 上下文訪問。
loader是一個node module,那麼它的基本形式如下
module.exports = function(source) {
return source;
};
複製程式碼
- loader只能傳入一個包含包含資原始檔內容的字串(source)
- 如果是同步loader,可以用
return
或者this.callback(err, value…)
將程式碼返回 - 非同步loader:在一個非同步的模組中,回傳時需要呼叫 Loader API 提供的回撥方法
this.async
來獲取 callback 函式:
module.exports = function(content, map, meta) {
var callback = this.async();
someAsyncOperation(content, function(err, result) {
if (err) return callback(err);
callback(null, result, map, meta);
});
};
複製程式碼
二、在webpack中引入loader
官網上介紹了配給單個和多個loader的方法。
主要原理是path.resolve
方法,給loader新增路徑。也可以使用resolveLoader.modules
統一配置多個loader的路徑。
webpack會在這些目錄中搜尋loaders,我在專案中新建了loaders本地目錄,並修改檔案如下:
webpack.config.js
module.exports = {
//...
resolveLoader: {// 配置查詢loader的目錄
modules: [
'node_modules',
path.resolve(__dirname, 'src', 'loaders')
]
},
module: {
rules:[
{
test: /\.js$/,
use: [
{
loader: 'env-loader',
options: {
env: process.env.NODE_ENV
}
},
{
loader:'babel-loader',
options: {
presets: ['env','es2015','react'],
}
},
]
}]
},
//...
};
複製程式碼
注意:loader的執行方式是從右到左,鏈式執行,上一個 Loader 的處理結果給下一個接著處理
在package.json中定義了根據環境打包的命令
"scripts": {
"webpack": "cross-env NODE_ENV=development webpack-dev-server --open --mode development",
"test": "cross-env NODE_ENV=test webpack --mode development",
"dev": "cross-env NODE_ENV=dev webpack --mode development",
"prd": "cross-env NODE_ENV=prd webpack --mode development",
"boot":"cross-env NODE_ENV=boot webpack --mode development"
},
複製程式碼
通過設定NODE_ENV
來區分dev、prd環境。
Q1:怎麼獲取命令中設定的環境引數
A1:process.env
物件上可以獲取到打包時定義的NODE_ENV,在webpack.config.js中引入env-loader的時候,可以將引數傳遞給 loader 的options
選項。
webpack.config.js
{
loader: 'env-loader',
options: {
env: process.env.NODE_ENV
}
},
複製程式碼
三、使用loader工具庫,解析loader傳參
- loader-utils 包。它提供了許多有用的工具,但最常用的一種工具是獲取傳遞給 loader 的選項
- schema-utils 包配合 loader-utils,用於保證 loader 選項,進行與 JSON Schema 結構一致的校驗
在loader中使用loader-utils包的getOptions
方法,拿到loader的option選項({env:'dev'}。用schema-utils 包配合 loader-utils,用於保證 loader 選項,進行與 JSON Schema結構一致的校驗
。在index.js中新增這兩個包:
env-loader/index.js
const loaderUtils = require('loader-utils')
const validate = require('schema-utils');
let json = {
"type": "object",
"properties": {
"content": {
"type": "string"
}
}
}
module.exports = function(source) {
this.cacheable();
let callback = this.async();
let options = loaderUtils.getOptions(this) //{env:'dev'}
validate(json, options, "env-loader");
}
複製程式碼
四、使用esprima解析js節點
Esprima parser把js程式轉換成描述程式語法結構的語法樹(AST)。產生的語法樹對於從程式轉換到靜態程式分析的各種用途都很有用。
之前寫過一篇介紹AST的文章 點選連結檢視,這裡就不詳細展開。
使用方法:
esprima.parseScript(input, config, delegate)
esprima.parseModule(input, config, delegate)
複製程式碼
- input入是表示要解析的程式的字串
- config是用於自定義解析行為的物件(可選)
- delegate是為每個節點呼叫的回撥函式(可選)
將source作為input引數,程式將會被解析成AST。
node返回每個節點對應的Syntax,meta是節點在程式中的具體位置。
esprima.parseModule(source, {}, async(node, meta)=> {
console.log(node.meta)
//....
})
複製程式碼
解析結果如下:
分析每個節點的Syntax是否滿足判斷條件,這裡判斷node的type型別和正在執行的函式callee的name==='envLoader'和type==='Identifier',對滿足條件的節點進行處理。
function judgeType(node) {
return (node.type === 'CallExpression')
&& (node.callee.name === 'envLoader')
&& (node.callee.type === 'Identifier')
}
if (judgeType(node)) {
flag = true
node.arguments.map(argument=>{
entries.push({
val: argument.value,
start: meta.start.offset,
end: meta.end.offset
});
})
}
複製程式碼
五、檔案的路徑處理
在節點分析中,拿到了自定義envLoader函式中傳入的外部資源地址,接下來要再loader中。
在loader中一般使用require()
或者import
方法。這是因為webpack是在將模組路徑轉換為模組id
之前計算雜湊的,所以我們必須避免絕對路徑,以確保不同編譯之間的雜湊一致。
不要在模組程式碼中插入絕對路徑,因為當專案根路徑變化時,檔案絕對路徑也會變化。
loaderUtils.urlToRequest可以將一些資源URL轉換為webpack模組請求。
//獲取當前路徑下的src資料夾
let downloadPath = path.resolve(process.cwd(), 'src')
if(env == 'prd'){
//如果是prd環境
//使用loaderUtils將請求轉換為module
const saveUrl = loaderUtils.urlToRequest(`${extName}`,downloadPath);// "path/to/module.js"
//將轉換好的module引入
var replaceText = `import "${saveUrl}"`
}else{
//其他環境
var replaceText = 'function envLoad(){}'
}
//將envLoader函式替換
source = source.replace(transText, replaceText);
複製程式碼
六、測試
完成上面的步驟,已經開發完成了一個簡單的loader,並且可以在本地執行。接下來讓我們用一個簡單的單元測試,來保證 loader 能夠按照我們預期的方式正確執行。
我們將使用 Jest 框架。然後還需要安裝 babel-jest 和允許我們使用 import / export 和 async / await 的一些預設環境(presets)。
6.1 安裝依賴
npm install --save-dev jest babel-jest babel-preset-env
複製程式碼
.babelrc
{
"presets": [[
"env",
{
"targets": {
"node": "4"
}
}
]]
}
複製程式碼
我們的 loader 將會處理 .js 檔案,並且將任何例項中的
envLoader('xxx')
複製程式碼
在開發環境下替換成function envLoad(){}
,在生產環境下替換成 import '路徑/xxx.js'。
在test資料夾下新建example.js
envLoader(
'/vendor/lodash.min.js'
)
複製程式碼
我們將會使用 Node.js API 和 memory-fs 去執行 webpack。
npm install --save-dev webpack memory-fss
複製程式碼
test/compiler.js
import path from 'path';
import webpack from 'webpack';
import memoryfs from 'memory-fs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [{
test: /\.js$/,
use: {
loader: path.resolve(__dirname, '../src/loaders/env-loader'),
options: {
env: process.env.NODE_ENV
}
}
}]
}
});
compiler.outputFileSystem = new memoryfs();
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err || stats.hasErrors()) reject(err);
resolve(stats);
});
});
};
複製程式碼
最後,我們來編寫測試,並且新增 npm script 執行它。
import compiler from './compiler.js';
test('envLoader to import', async () => {
const stats = await compiler('example.js');
const output = stats.toJson().modules[0].source;
if(process.env.NODE_ENV == 'prd'){
expect(output).toBe('import "/Users/yuan/Documents/yuanyuan/Project/env-loader/src/vendor/lodash.min.js"');
}else{
expect(output).toBe('function envLoad(){}');
}
});
複製程式碼
package.json
{
"scripts": {
"test-boot": "cross-env NODE_ENV=boot jest",
"test-prd": "cross-env NODE_ENV=prd jest"
}
}
複製程式碼
分別執行兩個script
各自驗證成功~測試通過
env-loader地址 詳細實現過程點這裡!!