webpack loader—自己寫一個按需載入外掛

圓兒圈圈發表於2018-09-04
寫在前面:

在開發的工程中,線上環境需要引入一些統計和列印日誌的js檔案。但是對於開發環境,加速打包速度減少頁面渲染時間很關鍵。我於是想根據開發環境,寫一個簡單的loader,按需載入一些資源。

例如:在index.js中,用自定義函式envLoader新增資源

index.js

//......

envLoader(
    '/vendor/log.js'
)

//......

複製程式碼

為了完成按需載入的功能。打算使用自定義的loader。 實現思路如下:

  1. 新增js loader 對index.js進行處理
  2. 解析envLoader函式
  3. 拿到傳入的引數並根據環境判斷是否載入。

結合官網的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)
    //....
})
複製程式碼

解析結果如下:

webpack loader—自己寫一個按需載入外掛

分析每個節點的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

webpack loader—自己寫一個按需載入外掛

webpack loader—自己寫一個按需載入外掛

各自驗證成功~測試通過

env-loader地址 詳細實現過程點這裡!!

相關文章