webpack的loader的原理和實現


想要實現一個loader,需要首先了解loader的基本原理和用法。

1. 使用

loader是處理模塊的解析器。

  module: {
    rules: [
      {
        test: /\.css$/, use: [ // 多個loader,從右向左解析,即css-loader開始  MiniCssExtractPlugin.loader, 'css-loader' ] } }

2.自定義loader的查找規則

很多時候,我們可以自己定義loader, 比如在根目錄下新建一個loaders的文件夾,文件夾內實現各個loader的代碼。但是webpack不識別這些loader,我們需要配置使webpack識別這些自定義的loader。

有四種方式:

1. resolveLoader.moduels

  resolveLoader: {
    modules: ['node_modules', 'loaders'] // 先從node_modules中查找,沒有從loaders文件夾中查找loader1.js
 }, module: { rules: [ { test: /\.js/, use: ['loader1'] } ] }

2.resolveLoader.alias

  resolveLoader: {
    alias: {// 絕對路徑
      loader1: path.resolve(__dirname, 'loaders', 'loader1.js') } },

3.loader的絕對路徑

  module: {
    rules: [
      {
        test: /\.js/, use: [path.resolve(__dirname, 'loaders', 'loader1.js')] } ] }

4.npm link(待解決)

3. loader的標准

1.  一個loader只實現一個功能,復合設計的單一功能原則。

2.  loader的處理順序。

當一個文件需要多個loader時,從最后的loader開始執行,其傳入的參數是文件的原始內容。返回結果傳入倒數第二個loader, 作為其入參,依次處理,直到第一個loader。

3. loaders 處理的最終結果(最后一個loader返回值)是一個字符串/Buffer。

4. loader類型

loader的加載順序是按照pre->normal->inline->post的順序執行

1.pre-前置loader

rule.enforce = pre;

      { 
        test: /test\.js$/,
        loader: 'loader3',
        enforce: 'pre'
      },

2.normal-正常loader

 沒有任何特征的loader都是普通loader

3.inline-行內loader

// 對test.js使用loader1和loader2
import 'loader1!loader2!./test.js'; // 按照從右到左,先執行loader2

行內loader的一個應用場景是,loader中pitch的參數remainingRequest。其通過loaderUtils.stringifyRequest(this, XXXX)后,變為

"../loaders/css-loader.js!./style.css"

對於正常的.css文件,會根據webpack中的規則,從右向左加載。但是對於上面的行內loader,有三個標志符號指定哪些loader。

1)!  忽略普通loader

// 表示忽略webpack配置中的正常loader,然后按照loader類型的順序加載
require("!" + "../loaders/css-loader.js!./style.css")

2. -!  忽略普通和前置loader

// 表示忽略webpack配置中的正常和前置loader
require("-!" + "../loaders/css-loader.js!./style.css")

3. !! 只使用行內loader 忽略普通,前置,后置loader;

// 表示只使用行內loader, 忽略webpack配置中的loader
require("!!" + "../loaders/css-loader.js!./style.css")

4.post-后置loader

      { 
        test: /test\.js$/,
        loader: 'loader5',
        enforce: 'post'
      },

6. loaders的常見API

1. this.callback

當loader有單個返回值時可以直接使用return返回。當需要返回多個結果時,需要使用this.callback。

其預期參數如下:

this.callback(
    err: Error | null,
    content: string | Buffer,
    sourceMap?:SourceMap, // 可選傳參
    meta?:any //元數據,可以是任意值;當將AST作為參數傳遞時,可以提高編譯速度
}

⚠️: 使用該方法時,loader必須返回undefined。

2. 越過loader(Pitching loader)

含義:

Pitching loader指的是loader上的pitch方法。

語法:

module.exports = function (content) {
  console.log(this.data); // {value: 42}
  return stringorBuffer;
}
/**
 * 對於請求index.js的rule
 * use: ['loader1','loader2', 'loader3']
 *
 * @param {*} remainingRequest 
 * 剩余的請求。
 * 如果返回undefined,則按照remainingRequest的順序訪問下一個loader的pitch
 * 對於第一個被調用的pitch方法來說,其值為: loader2!loader3!index.js
 * 
 * @param {*} precedingRequest 
 * 前一個請求。
 * 1. 如果返回一個非undefined值,則直接進入precedingRequest所在的loader方法,
 * 並且將pitch的返回值作為該loader方法的參數。
 * 如果該loader不是FinalLoader,按照從右到左順序依次執行
 * 2. 有一個特殊情況,如果第一個pitch方法返回一個非undefined值,
 * 它必須是string|Buffer,因為它將作為該FinalLoader的返回值
 * 
 * @param {*} data 
 * pitch中的數據。
 * 初始值是空對象{},可以給其賦值,然后通過loader方法中的this.date共享該數據
 */
 module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 42;
  // 此處可以返回數據;但是如果是第一個pitch,只能返回string|Buffer,它就是最終結果
}

作用:

正常的loader加載順序是從右到左。但是在執行loader之前,會從左到右的調用loader上的pitch方法,可以根據該方法的返回值,決定后續的loader要跳過不執行。其方法中傳入的data數據可以通過loader方法中的this.data進行共享。

應用場景:

1 )最左側的兩個loader之間有關聯關系;手動加載loader。

如:style-loader和css-loader

2 ) pitch階段給data賦值,在執行階段從this.data取值

3)通過pitch可以跳過某些loader

執行順序

use: [
  'a-loader',
  'b-loader',
  'c-loader'
]
// 當所有的loader的pitch方法都返回undefined時,正確的執行順序如下
|- a-loader `pitch`
  |- b-loader `pitch`
    |- c-loader `pitch`
      |- requested module is picked up as a dependency
    |- c-loader normal execution
  |- b-loader normal execution
|- a-loader normal execution

如果某個loader的pitch方法返回一個非undefined的值,將會跳過剩余的loader。

//  如果上面的b-loader返回一個結果,則執行順序為
|- a-loader `pitch`
  |- b-loader `pitch` returns a module
|- a-loader normal execution

3. raw

設置loader的raw屬性為true,則內容變為二進制形式。針對圖片,文件等。

此時content.length就是文件的大小

7. loader工具庫中常見方法

loader-utils: 內含各種處理loader的options的各種工具函數

schema-utils:  用於校驗loader和plugin的數據結構

我們根據上面的要求,可以自己完成常見loader的實現。

1. loaderUtils.stringifyRequest(this, itemUrl)

將URL轉為適合loader的相對路徑

/Users/lyralee/Desktop/MyStudy/React/loaders/loaders/css-loader.js!/Users/lyralee/Desktop/MyStudy/React/loaders/src/style.css
// 使用了loaderUtils.stringifyRequest(this, XXXX)方法后
"../loaders/css-loader.js!./style.css"

2. loaderUtils.getOptions(this)

獲取loader的options對象

3. schemaUtils(schema, options)

校驗options的格式

8.自模擬實現loader

1. babel-loader

簡單的模擬實現babel-loader。它本身是基於@babel/core和其他插件和預設。

const babel = require('@babel/core');
const loaderUtils = require('loader-utils');
const path = require('path');

function loader(inputSource) {
  const loaderOptions = loaderUtils.getOptions(this);
  const options = {
    ...options,
    sourceMap: true, //是否生成映射
    filename: path.basename(this.resourcePath) //從路徑中獲取目標文件名
  }
  const {code, map, ast} = babel.transform(inputSource, loaderOptions);
  // 將內容傳遞給webpack
  /**
   * code: 處理后的字符串
   * map: 代碼的source-map
   * ast: 生成的AST
   */
  this.callback(null, code, map, ast);
}
module.exports = loader;

2. banner-loader

給解析的模塊添加注釋信息。該loader主要用於學習schema-utils的用法。

const babel = require('@babel/core');
// 獲取loader的options
const loaderUtils = require('loader-utils');
// 校驗loader的options
const validationOptions = require('schema-utils');
const fs = require('fs');

/**
 * 
 * @param {*} inputSource 
 * 該方法只接受內容作為入參,要注意使用該插件的順序,
 * 如果在其他返回多個參數的loader之后接受參數,會丟失內容
 */
function loader(inputSource) {
  // 該loader啟用緩存
  this.cacheable(); 
  // 用於異步操作中
  const callback = this.async();
  const schema = {
    type: 'object',
    properties: {
      text: { type: 'string' },
      filename: { type: 'string'}
    }
  }
  const options = loaderUtils.getOptions(this);
  // 校驗options格式是否符合自定義的格式schema
  validationOptions(schema, options);
  const { code } = babel.transform(inputSource);
  // 讀取外部文件,作為注釋的內容
  fs.readFile(options.filename, 'utf8', (err, text) => {
    callback(null, options.text + text + code);
  })
}
module.exports = loader;

按照loader中的要求,options必須含有兩個字段,filename和text,否則會報錯

          {
            loader: 'banner-loader',
            options: {
              text: '/***lyra code ***/',
              filename: path.resolve(__dirname, 'banner.txt')
            }
          }

3. less-loader

const less = require('less');

module.exports = function(content) {
  const callback = this.async();
  less.render(content, {filename: this.resource}, (err, result) => {
    callback(null, result.css)
  })
}

4. css-loader

/**
 * 主要實現處理@import 和 url() 語法,基於postcss
 */
 //通過js插件處理樣式
const postcss = require('postcss');
// css選擇器的詞法分析器,用於解析和序列化css選擇器
const Tokenizer = require("css-selector-tokenizer");

module.exports = function(content) {
  const callback = this.async();
  const options = {
    importItems: [],
    urlItems: []
  };
  postcss([createPlugin(options)]).process(content).then(result => {
    const {importItems, urlItems} = options;
    let requires = importItems.map(itemUrl => (
      `require(${itemUrl});`
      )
    ).join('');  
    // require(url)返回一個打包后的絕對路徑
    let cssstring = JSON.stringify(result.css).replace(/_CSS_URL_(\d+)/g, function(match, g1) {
      // "background-image: url('" + require('" + url + "')";
      return '"+ require("' + urlItems[+g1] + '").default + "';
    });

    cssstring = cssstring.replace(/@import\s+['"][^'"]+['"];/g, '');
    callback(null, `${requires}module.exports=${cssstring}`);
  })
}
// 自定義的js插件
function createPlugin({urlItems, importItems}) {
  return function(css) {
    // 遍歷@import規則
    css.walkAtRules(/^import$/, function(result) {
      importItems.push(result.params);
    })
    // 遍歷每一條樣式
    css.walkDecls(function(decl) {
      // 解析樣式屬性的值
      const values = Tokenizer.parseValues(decl.value);
      values.nodes.forEach(value => {
        value.nodes.forEach(item => {
          if(item.type === 'url') {
            let url = item.url;
            item.url = "_CSS_URL_" + urlItems.length;
            urlItems.push(url);
          }
        })  
      })
      // 將解析后值返回序列化
      decl.value = Tokenizer.stringifyValues(values);
    })
  }
}

5.style-loader

const loaderUtils = require('loader-utils');

module.exports.pitch = function(remainingRquest, precedingRequest, data){
  const script = (
    `
      const style = document.createElement('style');
      style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRquest)});
      document.head.appendChild(style);
    `
  )
  return script;
}

6. file-loader

/**
 * 獲取內容了;修改名稱;在打包文件夾中輸出
 */
const { interpolateName, getOptions } = require('loader-utils');

module.exports = function(content) {
  const { name='[name].[hahs].[ext]' } = getOptions(this) || {};
  const outFilename = interpolateName(this, name, {content});
  this.emitFile(outFilename, content);
  return `module.exports=${JSON.stringify(outFilename)}`
}
// 內容二進制形式
module.exports.raw = true;

7.url-loader 

/**
 * 當小於limit時,使用base64;
 * 當大於limit時,根據file-loader處理
 */
const { getOptions } = require('loader-utils');
const fileLoader = require('file-loader');
const mime = require('mime');

module.exports = function(content) { 
  const { limit=10*1024 } = getOptions(this) || {};
  if (content.length < limit) {
    const base64 = `data:${mime.getType(this.resourcePath)};base64,${content.toString('base64')}`
    return `module.exports = "${base64}"`
  }
  return fileLoader.call(this, content)
}
module.exports.raw = true;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM