webpack loader—本身寫一個按需加載插件

寫在前面:

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

例如:在index.js中,用自定義函數envLoader添加資源node

index.jsreact

//......

envLoader(
    '/vendor/log.js'
)

//......

複製代碼

爲了完成按需加載的功能。打算使用自定義的loader。 實現思路以下:webpack

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

結合官網的loader api瞭解webpack loader的工做原理。git

將使用如下apigithub

  • loader-utils
  • schema-utils
  • this.async
  • this.cacheable
  • getOptions
  • validateOptions
  • urlToRequest

開始擼一個本身的loader (^-^)Vweb

Webpack Loader

loader 用於對模塊的源代碼進行轉換。loader 可使你在 import 或"加載"模塊時預處理文件。所以,loader 相似於其餘構建工具中「任務(task)」,並提供了處理前端構建步驟的強大方法。loader 能夠將文件從不一樣的語言(如 TypeScript)轉換爲 JavaScript,或將內聯圖像轉換爲 data URL。npm

1、基本用法

loader 是導出爲一個函數的 node 模塊。該函數在 loader 轉換資源的時候調用。給定的函數將調用 loader API,並經過 this 上下文訪問。json

loader是一個node module,那麼它的基本形式以下api

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);
  });
};
複製代碼

2、在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
   }
},
複製代碼

3、使用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");
}
複製代碼

4、使用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
        });
    })

}
複製代碼

5、文件的路徑處理

在節點分析中,拿到了自定義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);
複製代碼

6、測試

完成上面的步驟,已經開發完成了一個簡單的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地址 詳細實現過程點這裏!!

相關文章
相關標籤/搜索