在開發的工程中,線上環境須要引入一些統計和打印日誌的js文件。可是對於開發環境,加速打包速度減小頁面渲染時間很關鍵。我因而想根據開發環境,寫一個簡單的loader,按需加載一些資源。前端
例如:在index.js中,用自定義函數envLoader添加資源node
index.jsreact
//......
envLoader(
'/vendor/log.js'
)
//......
複製代碼
爲了完成按需加載的功能。打算使用自定義的loader。 實現思路以下:webpack
結合官網的loader api瞭解webpack loader的工做原理。git
將使用如下apigithub
開始擼一個本身的loader (^-^)Vweb
loader 用於對模塊的源代碼進行轉換。loader 可使你在 import 或"加載"模塊時預處理文件。所以,loader 相似於其餘構建工具中「任務(task)」,並提供了處理前端構建步驟的強大方法。loader 能夠將文件從不一樣的語言(如 TypeScript)轉換爲 JavaScript,或將內聯圖像轉換爲 data URL。npm
loader 是導出爲一個函數的 node 模塊。該函數在 loader 轉換資源的時候調用。給定的函數將調用 loader API,並經過 this 上下文訪問。json
loader是一個node module,那麼它的基本形式以下api
module.exports = function(source) {
return source;
};
複製代碼
return
或者 this.callback(err, value…)
將代碼返回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);
});
};
複製代碼
官網上介紹了配給單個和多個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環境。
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-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 parser把js程序轉換成描述程序語法結構的語法樹(AST)。產生的語法樹對於從程序轉換到靜態程序分析的各類用途都頗有用。
以前寫過一篇介紹AST的文章 點擊連接查看,這裏就不詳細展開。
使用方法:
esprima.parseScript(input, config, delegate)
esprima.parseModule(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
以前計算散列的,因此咱們必須避免絕對路徑,以確保不一樣編譯之間的哈希一致。
不要在模塊代碼中插入絕對路徑,由於當項目根路徑變化時,文件絕對路徑也會變化。
//獲取當前路徑下的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地址 詳細實現過程點這裏!!