手寫webpack系列一:瞭解認識loader-utils

安穩.發表於2019-04-01

Created By JishuBao on 2019-03-29 12:38:22
Recently revised in 2019-04-01 12:38:22

 

  歡迎大家來到技術寶的掘金世界,您的star是我寫文章最大的動力!GitHub地址     

文章簡介:

1、webpack module簡介

2、rules的使用

3、loader-utils是什麼?

4、api實現

5、未完待續

一、webpack module簡介

現在前端的技術日新月異,現在比較火的前端打包工具! webpack,我想各位小夥伴應該也有所瞭解,那麼你知道如何手寫webpack嗎,作為手寫webpack系列的第一篇文章,我將帶領大家逐步走進webpack的世界。webpack中有一個及其重要的概念loader,那什麼是loader呢?在webpack的官網中,我們可以看到這樣一行對Loader的定義,原來是處理js字尾檔名除外的檔案。

webpack 可以使用 loader 來預處理檔案。這允許你打包除 JavaScript 之外的任何靜態資源。你可以使用 Node.js 來很簡單地編寫自己的 loader。

在loader中,我們們可以通過關鍵詞this訪問當前執行環境的所有變數

1、同步回撥時,可以執行this.callback(),預設第一個引數是err錯誤資訊(不報錯時返回null),第二個引數是解析完模組後的返回結果,第三個引數是sourceMap(可選),在鏈式呼叫時可將sourceMap傳給下一個loader;

2、非同步回撥時,可以執行this.async(),引數同上;

3、this.addDependency(filePath)可以把對應filePath的檔案新增到webpack的依賴樹,webpack可以監測它的檔案變動並重新整理(filePath要是絕對路徑);

4、this.resolve()可以解析處理檔案路徑;

5、this.query:獲取loader的配置選項。

瞭解或者使用過webpack的小夥伴都知道loader是使用在weboack的module裡面的,用於處理module檔案的。我們來看下module屬性對應的一些常用api!

  • module.noParse

noParse 配置項可以讓 Webpack 忽略對部分沒采用模組化的檔案的遞迴解析和處理,這樣做的好處是能提高構建效能。 原因是一些庫例如 jQuery 、ChartJS 它們龐大又沒有采用模組化標準,讓 Webpack 去解析這些檔案耗時又沒有意義。noParse 是可選配置項,型別需要是 RegExp、[RegExp]、function 其中一個.例如想要忽略掉 jQuery 、ChartJS,可以使用如下程式碼:

// 使用正規表示式
noParse: /jquery|chartjs/

// 使用函式,從 Webpack 3.0.0 開始支援
noParse: (content)=> {
  // content 代表一個模組的檔案路徑
  // 返回 true or false
  return /jquery|chartjs/.test(content);
}
複製程式碼
  • module.rules

配置模組的讀取和解析規則,通常用來配置 Loader。其型別是一個陣列,陣列裡每一項都描述瞭如何去處理部分檔案。 配置一項 rules 時大致通過以下方式:

1.條件匹配:通過 test 、 include 、 exclude 三個配置項來命中 Loader 要應用規則的檔案。

2.應用規則:對選中後的檔案通過 use 配置項來應用 Loader,可以只應用一個 Loader 或者按照從後往前的順序應用一組 Loader,同時還可以分別給 Loader 傳入引數。

3.重置順序:一組 Loader 的執行順序預設是從右到左執行,通過 enforce 選項可以讓其中一個 Loader 的執行順序放到最前或者最後。 下面來通過一個例子來說明具體使用方法:

module: {
  rules: [
    {
      // 命中 JavaScript 檔案
      test: /\.js$/,
      // 用 babel-loader 轉換 JavaScript 檔案
      // ?cacheDirectory 表示傳給 babel-loader 的引數,用於快取 babel 編譯結果加快重新編譯速度
      use: ['babel-loader?cacheDirectory'],
      // 只命中src目錄裡的js檔案,加快 Webpack 搜尋速度
      include: path.resolve(__dirname, 'src')
    },
    {
      // 命中 SCSS 檔案
      test: /\.scss$/,
      // 使用一組 Loader 去處理 SCSS 檔案。
      // 處理順序為從後到前,即先交給 sass-loader 處理,再把結果交給 css-loader 最後再給 style-loader。
      use: ['style-loader', 'css-loader', 'sass-loader'],
      // 排除 node_modules 目錄下的檔案
      exclude: path.resolve(__dirname, 'node_modules'),
    },
    {
      // 對非文字檔案採用 file-loader 載入
      test: /\.(gif|png|jpe?g|eot|woff|ttf|svg|pdf)$/,
      use: ['file-loader'],
    },
  ]
}
複製程式碼

在 Loader 需要傳入很多引數時,你還可以通過一個 Object 來描述,例如在上面的 babel-loader 配置中有如下程式碼:

use: [
  {
    loader:'babel-loader',
    options:{
      cacheDirectory:true,
    },
    // enforce:'post' 的含義是把該 Loader 的執行順序放到最後
    // enforce 的值還可以是 pre,代表把 Loader 的執行順序放到最前面
    enforce:'post'
  },
  // 省略其它 Loader
]
複製程式碼

上面的例子中 test include exclude 這三個命中檔案的配置項只傳入了一個字串或正則,其實它們還都支援陣列型別,使用如下:

{
  test:[
    /\.jsx?$/,
    /\.tsx?$/
  ],
  include:[
    path.resolve(__dirname, 'src'),
    path.resolve(__dirname, 'tests'),
  ],
  exclude:[
    path.resolve(__dirname, 'node_modules'),
    path.resolve(__dirname, 'bower_modules'),
  ]
}
複製程式碼

陣列裡的每項之間是或的關係,即檔案路徑符合陣列中的任何一個條件就會被命中。

  • parser

因為 Webpack 是以模組化的 JavaScript 檔案為入口,所以內建了對模組化 JavaScript 的解析功能,支援 AMD、CommonJS、SystemJS、ES6。 parser 屬性可以更細粒度的配置哪些模組語法要解析哪些不解析,和 noParse 配置項的區別在於 parser 可以精確到語法層面, 而 noParse 只能控制哪些檔案不被解析。 parser 使用如下:

module: {
  rules: [
    {
      test: /\.js$/,
      use: ['babel-loader'],
      parser: {
      amd: false, // 禁用 AMD
      commonjs: false, // 禁用 CommonJS
      system: false, // 禁用 SystemJS
      harmony: false, // 禁用 ES6 import/export
      requireInclude: false, // 禁用 require.include
      requireEnsure: false, // 禁用 require.ensure
      requireContext: false, // 禁用 require.context
      browserify: false, // 禁用 browserify
      requireJs: false, // 禁用 requirejs
      }
    },
  ]
}
複製程式碼

二、rules的使用

根據上文我們已經基本瞭解了rule的使用,裡面放一些loader處理對應的檔案。隨意開啟一個檔案,這裡已常用的loader file-loader舉例。

手寫webpack系列一:瞭解認識loader-utils
我們如果開啟大部分loader的原始碼,基本上你可以發現一個規律,基本上每個loader裡面都有這樣一句,引入了loader-utils這個工具類。

那麼utils-loader究竟是什麼呢?

三、loader-utils是什麼?

loader-utils是一個webpack工具類,通過一些方法配合loader處理檔案。讓我們一起來解讀一下。 本文旨在通過手寫loader-utils來了解loader-utils的內容。新建資料夾loader-utils,執行命令npm init初始化一個npm專案

npm init
複製程式碼

新建index.js作為webpack打包入口檔案,內容為空。新建webpack.conf.js作為webpack配置檔案,內容如下:

const path=require('path');
//path是node.js的一個模組,提供了一些用於處理檔案路勁的小工具
module.exports={
    entry:{
        main:"./index.js"//入口起點(entry point)指示 webpack 應該使用哪個模組,來作為構建其內部依賴圖的開始。進入入口起點後,webpack 會找出有哪些模組和庫是入口起點(直接和間接)依賴的
    },
    resolve:{
    },
    module:{
        rules:[
            {
                test:/\.js$/,//通過loader來預處理檔案 允許你打包除了js之外的任何靜態資源
                use:[
                    {
                        loader:path.resolve('./loader-util.js'),
                        options:{
                            name:'wjb'
                        }
                    }
                ]
            },
        ]
    },
    plugins:[
        
    ]

}
複製程式碼

新建loader-utils.js檔案作為webpack解析的loader。內容如下:

"use strict";

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

function loader(content) {

    var publicPath="a";
   
    console.log('進入了loader內部');

    return `${publicPath};`;
}

module.exports = loader;
複製程式碼

修改package.json檔案,新建dev和入口檔案

{
  "name": "loader-util",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --config webpack.conf.js"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "webpack": "^4.29.6"
  },
  "devDependencies": {
    "webpack-cli": "^3.3.0"
  }
}
複製程式碼

執行命令npm run dev

npm run dev
複製程式碼

手寫webpack系列一:瞭解認識loader-utils
會發現已經進入了你寫的這個loader裡面了

四、api實現

新建loader-equal-utils資料夾存放模仿loader-utils工具類的功能。新建index.js資料夾。

  • parseQuery(解析被呼叫loader的配置選項)

將傳遞的字串(例如loaderContext.resourceQuery)解析為查詢字串,並返回一個物件。

const params = loaderUtils.parseQuery(this.resourceQuery); // resource: `file?param1=foo`
if (params.param1 === "foo") {
	// do something
}
複製程式碼

新建parseQuery.js檔案,首先安裝json5,json5是json的一個超集,具體的不再贅述。

'use strict';

const JSON5 = require('json5');

const specialValues = {
  null: null,
  true: true,
  false: false,
};

function parseQuery(query) {
  if (query.substr(0, 1) !== '?') {//如果字串不是以?開頭 就丟擲錯誤
    throw new Error(
      "A valid query string passed to parseQuery should begin with '?'"
    );
  }

  query = query.substr(1);//將query變成去掉?的字串

  if (!query) {//如果是空
    return {};
  }

  if (query.substr(0, 1) === '{' && query.substr(-1) === '}') {//如果是物件 返回解析
    return JSON5.parse(query);
  }

  const queryArgs = query.split(/[,&]/g);//將字串以, &符號分割成字串陣列
  const result = {};//定義物件儲存數值

  queryArgs.forEach((arg) => {//遍歷陣列
    const idx = arg.indexOf('=');//找到字元中是=的下標

    if (idx >= 0) {//當下表大於0,即存在=時
      let name = arg.substr(0, idx);//將=號之前  即name
      let value = decodeURIComponent(arg.substr(idx + 1));//加密value

      if (specialValues.hasOwnProperty(value)) {//當有值是true false 或者undefined時
        value = specialValues[value];//將value變成true false undefined
      }
       
      if (name.substr(-2) === '[]') {
       
        name = decodeURIComponent(name.substr(0, name.length - 2));

        if (!Array.isArray(result[name])) {
          result[name] = [];//將name鍵賦值為[]空陣列值
        }

        result[name].push(value);//將resultpush成值
      } else {
        name = decodeURIComponent(name);
        result[name] = value;
      }
    } else {
      if (arg.substr(0, 1) === '-') {
        result[decodeURIComponent(arg.substr(1))] = false;
      } else if (arg.substr(0, 1) === '+') {
        result[decodeURIComponent(arg.substr(1))] = true;
      } else {
        result[decodeURIComponent(arg)] = true;
      }
    }
  });

  return result;
}

module.exports=parseQuery;
複製程式碼

在laoder-utils檔案匯入parseQuery,進行測試

手寫webpack系列一:瞭解認識loader-utils

更多引數解析:

                             -> Error
?                            -> {}
?flag                        -> { flag: true }
?+flag                       -> { flag: true }
?-flag                       -> { flag: false }
?xyz=test                    -> { xyz: "test" }
?xyz=1                       -> { xyz: "1" } // numbers are NOT parsed
?xyz[]=a                     -> { xyz: ["a"] }
?flag1&flag2                 -> { flag1: true, flag2: true }
?+flag1,-flag2               -> { flag1: true, flag2: false }
?xyz[]=a,xyz[]=b             -> { xyz: ["a", "b"] }
?a%2C%26b=c%2C%26d           -> { "a,&b": "c,&d" }
?{data:{a:1},isJSON5:true}   -> { data: { a: 1 }, isJSON5: true }
複製程式碼
  • parseString(是將字串轉化為json物件)

新建parseString.js檔案。

'use strict';

function parseString(str){
    console.log(str)
    try {
        if (str[0] === '"') {
            console.log('我進入了1')
            console.log(str)
            console.log(JSON.parse(str))
          return JSON.parse(str);
        }
    
        if (str[0] === "'" && str.substr(str.length - 1) === "'") {//如果是以''包裹的字串
        console.log(str)
        console.log('我進入了2')
          return parseString(
            str
              .replace(/\\.|"/g, (x) => (x === '"' ? '\\"' : x))//轉化為以""包裹的字串
              .replace(/^'|'$/g, '"')
          );
        }
    
        return JSON.parse('"' + str + '"');
      } catch (e) {
          console.log('wobaocuole')
          console.log(e);
        return str;
      }
}

module.exports=parseString;
複製程式碼

在laoder-utils檔案匯入parseString,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • getOptions(檢索被呼叫loader的配置選項)

新建getOptions.js檔案。

檢索載入程式呼叫選項的推薦方法:
1.如果this.query是字串:嘗試解析查詢字串並返回一個新物件
2.如果它不是有效的查詢字串則丟擲
3.如果this.query是物件,它只是返回this.query
4.在任何其他情況下,它只是返回 null

// inside your loader
const options = loaderUtils.getOptions(this);
複製程式碼
'use strict';

const parseQuery=require('./parseQuery');//引入parseQuery模組

function getOptions(loaderContext){
    const query=loaderContext.query;//拿到Loader上下文的query

    if(typeof query==="string" && query!==''){//query是字串且query不是空字串
        return parseQuery(query);//返回物件
    }
    
    if (!query || typeof query !== 'object') {//如果不存在或者不是物件返回null
        // Not object-like queries are not supported.
        return null;
    }

    return query;
}

module.exports=getOptions;
複製程式碼

在laoder-utils檔案匯入getOptions,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • stringifyRequest(將一個請求轉化為非絕對路徑的可被require或import的字串;)

新建stringifyRequest檔案

將請求轉換為可在內部使用require()或import在避免絕對路徑時使用的字串。JSON.stringify(...)如果您在載入器中生成程式碼,請使用它。為什麼這有必要?由於webpack在模組路徑轉換為模組ID之前計算雜湊值,因此我們必須避免絕對路徑以確保跨不同編譯的一致雜湊。

前置知識:

  1. path.posix是跨平臺相容方案,path.win32則是相容windows
  1. isAbsolute判斷路徑是否為絕對路徑,值得注意的是在posix上 path.isAbsolute('/foo/bar'); // true path.isAbsolute('/baz/..'); // true path.isAbsolute('qux/'); // false path.isAbsolute('.'); // false 在win上path.isAbsolute('//server'); // true path.isAbsolute('\\server'); // true path.isAbsolute('C:/foo/..'); // true path.isAbsolute('C:\foo\..'); // true path.isAbsolute('bar\baz'); // false path.isAbsolute('bar/baz'); // false path.isAbsolute('.'); // false
  1. path.relative(from, to)方法根據當前工作目錄返回 from 到 to 的相對路徑。 如果 from 和 to 各自解析到相同的路徑(分別呼叫 path.resolve() 之後),則返回零長度的字串。如果將零長度的字串傳入 from 或 to,則使用當前工作目錄代替該零長度的字串。例如,在 POSIX 上:path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb'); // 返回: '../../impl/bbb',在 Windows 上:path.relative('C:\orandea\test\aaa', 'C:\orandea\impl\bbb');// 返回: '..\..\impl\bbb'如果 from 或 to 不是字串,則丟擲 TypeError。
'use strict';

const path=require('path');

const matchRelativePath=/^\.\.?[/\\]/;//相對路徑正規表示式

function isAbsolutePath(str) {//是否為絕對路徑
    return path.posix.isAbsolute(str) || path.win32.isAbsolute(str);
    //前者為跨平臺 後者為windows 判斷是否為絕對路徑
}

function isRelativePath(str) {//是否是相對路徑 是返回true 否則返回false
    return matchRelativePath.test(str);
}

function stringifyRequest(loaderContext, request) {
    const splitted = request.split('!');//分割!
    const context =
      loaderContext.context ||
      (loaderContext.options && loaderContext.options.context);//獲取內容
  
    return JSON.stringify(
      splitted
        .map((part) => {
          // First, separate singlePath from query, because the query might contain paths again
          const splittedPart = part.match(/^(.*?)(\?.*)/);
          const query = splittedPart ? splittedPart[2] : '';
          let singlePath = splittedPart ? splittedPart[1] : part;
  
          if (isAbsolutePath(singlePath) && context) {
            singlePath = path.relative(context, singlePath);
  
            if (isAbsolutePath(singlePath)) {
              // If singlePath still matches an absolute path, singlePath was on a different drive than context.
              // In this case, we leave the path platform-specific without replacing any separators.
              // @see https://github.com/webpack/loader-utils/pull/14
              return singlePath + query;
            }
  
            if (isRelativePath(singlePath) === false) {
              // Ensure that the relative path starts at least with ./ otherwise it would be a request into the modules directory (like node_modules).
              singlePath = './' + singlePath;
            }
          }
  
          return singlePath.replace(/\\/g, '/') + query;
        })
        .join('!')
    );
}
複製程式碼

在laoder-utils檔案匯入stringifyRequest,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • urlToRequest(將url轉換成適合webpack環境的模組請求)
const url = "/path/to/module.js";
const root = "./root";
const request = loaderUtils.urlToRequest(url, root); // "./root/path/to/module.js"
複製程式碼

新建urlToRequest檔案

'use strict';

//我們不能使用path的isAbsolute方法 因為他對正斜槓開頭的路徑同樣有效
const matchNativeWin32Path = /^[A-Z]:[/\\]|^\\\\/i;

function urlToRequest(url, root){
  // 不能寫空的url字串
  if (url === '') {
    return '';
  }

  const moduleRequestRegex = /^[^?]*~/;

  let request;

  if (matchNativeWin32Path.test(url)) {
    // 如果是絕對路徑 保持url不變
    request = url;
  }else if (root !== undefined && root !== false && /^\//.test(url)) {
    // 如果根路徑定義 且url是相對路徑
    switch (typeof root) {
        // 1. 如果根路徑是字串 根路徑作為url路徑的字首
        case 'string':
          // 特殊的: `~` 根轉換為模組請求
          if (moduleRequestRegex.test(root)) {
            request = root.replace(/([^~/])$/, '$1/') + url.slice(1);
          } else {
            request = root + url;
          }
          break;
        // 2. 如果root為true,則絕對路徑允許
        //    *當window路徑不是以`/`,一直支援絕對路徑
        case 'boolean':
          request = url;
          break;
        default:
          throw new Error(
            "Unexpected parameters to loader-utils 'urlToRequest': url = " +
              url +
              ', root = ' +
              root +
              '.'
          );
      }
  }else if (/^\.\.?\//.test(url)) {
    //相對路徑
    request = url;
  } else {
    // 像一個相對
    request = './' + url;
  }

  
  // A `~` makes the url an module
  if (moduleRequestRegex.test(request)) {
    request = request.replace(moduleRequestRegex, '');
  }

  return request;

}

module.exports = urlToRequest;
複製程式碼

在loader-utils檔案匯入urlToRequest,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • getHashDigest(通過限制字元長度獲取檔案部分雜湊值)
const digestString = loaderUtils.getHashDigest(buffer, hashType, digestType, maxLength);
複製程式碼
    1. buffer:應該雜湊的內容
    1. hashType之一sha1,md5,sha256,sha512或任何其他的node.js支援的雜湊型別
    1. digestType一hex,base26,base32,base36,base49,base52,base58,base62,base64
    1. maxLength 字元的最大長度

新建getHashDigest檔案

'use strict';

const baseEncodeTables = {//基本的型別
  26: 'abcdefghijklmnopqrstuvwxyz',
  32: '123456789abcdefghjkmnpqrstuvwxyz', // no 0lio
  36: '0123456789abcdefghijklmnopqrstuvwxyz',
  49: 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no lIO
  52: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
  58: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', // no 0lIO
  62: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
  64: '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_',
};

function encodeBufferToBase(buffer, base) {//buffer轉化為base
  const encodeTable = baseEncodeTables[base];
  if (!encodeTable) {//如果沒有 丟擲錯誤 找不到base型別
    throw new Error('Unknown encoding base' + base);
  }

  const readLength = buffer.length;//快取的長度
  const Big = require('big.js');

  Big.RM = Big.DP = 0;//初始化小數點的最大位數和最小位數
  let b = new Big(0);//初始化Big

  for (let i = readLength - 1; i >= 0; i--) {
    b = b.times(256).plus(buffer[i]);//b乘以256再加
  }

  let output = '';
  while (b.gt(0)) {
    output = encodeTable[b.mod(base)] + output;
    b = b.div(base);
  }

  Big.DP = 20;
  Big.RM = 1;

  return output;
}

function getHashDigest(buffer, hashType, digestType, maxLength) {
  hashType = hashType || 'md5';
  maxLength = maxLength || 9999;

  const hash = require('crypto').createHash(hashType);

  hash.update(buffer);

  if (
    digestType === 'base26' ||
    digestType === 'base32' ||
    digestType === 'base36' ||
    digestType === 'base49' ||
    digestType === 'base52' ||
    digestType === 'base58' ||
    digestType === 'base62' ||
    digestType === 'base64'
  ) {
    return encodeBufferToBase(hash.digest(), digestType.substr(4)).substr(
      0,
      maxLength
    );
  } else {
    return hash.digest(digestType || 'hex').substr(0, maxLength);
  }
}

module.exports = getHashDigest;
複製程式碼

在loader-utils檔案匯入getHashDigest,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • interpolateName(自定義資源名稱、hash等)

前置知識 path.parse() 方法返回一個物件,其屬性表示 path 的重要元素。 尾部的目錄分隔符將被忽略,參閱 path.sep。返回物件包含以下屬性 <string> root <string> base <string> name <string> ext <string>

在posix上:
path.parse('/home/user/dir/file.txt');
// 返回:
// { root: '/',
//   dir: '/home/user/dir',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file' }
┌─────────────────────┬────────────┐
│          dir        │    base    │
├──────┬              ├──────┬─────┤
│ root │              │ name │ ext │
"  /    home/user/dir / file  .txt "
└──────┴──────────────┴──────┴─────┘
("" 行中的所有空格都應該被忽略,它們純粹是為了格式化)
在window上:
path.parse('C:\\path\\dir\\file.txt');
// 返回:
// { root: 'C:\\',
//   dir: 'C:\\path\\dir',
//   base: 'file.txt',
//   ext: '.txt',
//   name: 'file' }
┌─────────────────────┬────────────┐
│          dir        │    base    │
├──────┬              ├──────┬─────┤
│ root │              │ name │ ext │
" C:\      path\dir   \ file  .txt "
└──────┴──────────────┴──────┴─────┘
("" 行中的所有空格都應該被忽略,它們純粹是為了格式化)
複製程式碼

新建interpolateName檔案

'use strict';

const path = require('path');
const emojisList = require('./emoj-list');
const getHashDigest = require('./getHashDigest');

const emojiRegex = /[\uD800-\uDFFF]./;//判斷是否是emoj表情
const emojiList = emojisList.filter((emoji) => emojiRegex.test(emoji));//將不是emoj表情的排除掉
const emojiCache = {};//emoj表情快取

function encodeStringToEmoji(content, length) {//字串轉emoj表情
  if (emojiCache[content]) {
    return emojiCache[content];
  }

  length = length || 1;//如果沒有指定長度就預設為1

  const emojis = [];//空陣列

  do {//將表情存進emojis陣列裡面
    if (!emojiList.length) {
      throw new Error('Ran out of emoji');
    }

    const index = Math.floor(Math.random() * emojiList.length);

    emojis.push(emojiList[index]);
    emojiList.splice(index, 1);
  } while (--length > 0);

  const emojiEncoding = emojis.join('');//將陣列轉化為字串

  emojiCache[content] = emojiEncoding;

  return emojiEncoding;//表情組成的字串
}

function interpolateName(loaderContext, name, options) {
  let filename;

  if (typeof name === 'function') {//如果是函式
    filename = name(loaderContext.resourcePath);
  } else {
    filename = name || '[hash].[ext]';
  }

  const context = options.context;
  const content = options.content;
  const regExp = options.regExp;

  let ext = 'bin';
  let basename = 'file';
  let directory = '';
  let folder = '';

  if (loaderContext.resourcePath) {//如果存在路徑
    const parsed = path.parse(loaderContext.resourcePath);//返回一個物件
    let resourcePath = loaderContext.resourcePath;

    if (parsed.ext) {
      ext = parsed.ext.substr(1);
    }

    if (parsed.dir) {
      basename = parsed.name;
      resourcePath = parsed.dir + path.sep;//path.sep在window上是\ posix是/
    }

    if (typeof context !== 'undefined') {
      directory = path
        .relative(context, resourcePath + '_')
        .replace(/\\/g, '/')
        .replace(/\.\.(\/)?/g, '_$1');
      directory = directory.substr(0, directory.length - 1);
    } else {
      directory = resourcePath.replace(/\\/g, '/').replace(/\.\.(\/)?/g, '_$1');
    }

    if (directory.length === 1) {
      directory = '';
    } else if (directory.length > 1) {
      folder = path.basename(directory);
    }
  }

  let url = filename;

  if (content) {
    // Match hash template
    url = url
      // `hash` and `contenthash` are same in `loader-utils` context
      // let's keep `hash` for backward compatibility
      .replace(
        /\[(?:([^:\]]+):)?(?:hash|contenthash)(?::([a-z]+\d*))?(?::(\d+))?\]/gi,
        (all, hashType, digestType, maxLength) =>
          getHashDigest(content, hashType, digestType, parseInt(maxLength, 10))
      )
      .replace(/\[emoji(?::(\d+))?\]/gi, (all, length) =>
        encodeStringToEmoji(content, parseInt(length, 10))
      );
  }

  url = url
    .replace(/\[ext\]/gi, () => ext)
    .replace(/\[name\]/gi, () => basename)
    .replace(/\[path\]/gi, () => directory)
    .replace(/\[folder\]/gi, () => folder);

  if (regExp && loaderContext.resourcePath) {
    const match = loaderContext.resourcePath.match(new RegExp(regExp));

    match &&
      match.forEach((matched, i) => {
        url = url.replace(new RegExp('\\[' + i + '\\]', 'ig'), matched);
      });
  }

  if (
    typeof loaderContext.options === 'object' &&
    typeof loaderContext.options.customInterpolateName === 'function'
  ) {
    url = loaderContext.options.customInterpolateName.call(
      loaderContext,
      url,
      name,
      options
    );
  }

  return url;
}

module.exports = interpolateName;
複製程式碼

在loader-utils檔案匯入interpolateName,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • isUrlRequest(判斷是否是路徑)

新建isUrlRequest檔案

'use strict';

const path = require('path');

function isUrlRequest(url, root) {
  // An URL is not an request if

  // 1. It's an absolute url and it is not `windows` path like `C:\dir\file`
  if (/^[a-z][a-z0-9+.-]*:/i.test(url) && !path.win32.isAbsolute(url)) {
    return false;
  }

  // 2. It's a protocol-relative
  if (/^\/\//.test(url)) {
    return false;
  }

  // 3. It's some kind of url for a template
  if (/^[{}[\]#*;,'§$%&(=?`´^°<>]/.test(url)) {
    return false;
  }

  // 4. It's also not an request if root isn't set and it's a root-relative url
  if ((root === undefined || root === false) && /^\//.test(url)) {
    return false;
  }

  return true;
}

module.exports = isUrlRequest;
複製程式碼

在loader-utils檔案匯入isUrlRequest,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • getCurrentRequest(獲取當前請求)
'use strict';

function getCurrentRequest(loaderContext) {
  if (loaderContext.currentRequest) {
    return loaderContext.currentRequest;
  }

  const request = loaderContext.loaders
    .slice(loaderContext.loaderIndex)
    .map((obj) => obj.request)
    .concat([loaderContext.resource]);

  return request.join('!');
}

module.exports = getCurrentRequest;
複製程式碼

在loader-utils檔案匯入getCurrentRequest,進行測試

手寫webpack系列一:瞭解認識loader-utils

  • getRemainingRequest(獲取請求)

新建getRemainingRequest檔案

'use strict';

function getRemainingRequest(loaderContext) {
  if (loaderContext.remainingRequest) {
    return loaderContext.remainingRequest;
  }

  const request = loaderContext.loaders
    .slice(loaderContext.loaderIndex + 1)
    .map((obj) => obj.request)
    .concat([loaderContext.resource]);

  return request.join('!');
}

module.exports = getRemainingRequest;
複製程式碼

在loader-utils檔案匯入getRemainingRequest,進行測試

手寫webpack系列一:瞭解認識loader-utils

五、未完待續

揭開了loader-utils神祕的面紗 為我們手寫webpack打下了堅實的基礎

相關文章