從源碼分析url-loader 和 file-loader

url-loader

使用過webpack的開發(fā)者,基本上都聽說或者用過url-loader。

a loader for webpack which transforms files into base64 URIs.

url-loader 允許你有條件地將文件轉(zhuǎn)換為內(nèi)聯(lián)的 base64(減少小文件的 HTTP 請求數(shù)),如果文件大于該閾值,會自動的交給 file-loader 處理。

配置項

在看源碼前,我個人比較傾向于先仔細(xì)閱讀使用文檔,文檔的使用和配置往往可以幫助理解源碼閱讀。

image.png

options總共有6個配置屬性,一般使用都只是配置limit:

  • limit: 小于limit值會被url-loader轉(zhuǎn)換,默認(rèn)是base64
  • mimetype: 被轉(zhuǎn)換文件的mimetype,默認(rèn)取文件擴(kuò)展名
  • encoding: 默認(rèn)base64
  • generator:默認(rèn)轉(zhuǎn)成base64:xxxx,開發(fā)者可以通過generator自己實現(xiàn)轉(zhuǎn)換
  • fallback: 如果文件大于limit,把文件交給fallback 這個loader,默認(rèn)是file-loader
  • esModule:是否es module

源碼分析

如果只看url-loader的核心代碼(loader函數(shù)),代碼只有60行左右。
其中的 export const raw = true 是告訴 webpack 該 loader 獲取的 content 是 buffer 類型
( loader 第一個值的類型是 JavaScript 代碼字符串或者 buffer,開發(fā)者自行決定使用哪種類型)

export default function loader(content) {
  // Loader Options
  const options = getOptions(this) || {};

  validate(schema, options, {
    name: 'URL Loader',
    baseDataPath: 'options',
  });

  // No limit or within the specified limit
  if (shouldTransform(options.limit, content.length)) {
    const { resourcePath } = this;
    const mimetype = getMimetype(options.mimetype, resourcePath);
    const encoding = getEncoding(options.encoding);

    const encodedData = getEncodedData(
      options.generator,
      mimetype,
      encoding,
      content,
      resourcePath
    );

    const esModule =
      typeof options.esModule !== 'undefined' ? options.esModule : true;

    return `${
      esModule ? 'export default' : 'module.exports ='
    } ${JSON.stringify(encodedData)}`;
  }

  // Normalize the fallback.
  const {
    loader: fallbackLoader,
    options: fallbackOptions,
  } = normalizeFallback(options.fallback, options);

  // Require the fallback.
  const fallback = require(fallbackLoader);

  // Call the fallback, passing a copy of the loader context. The copy has the query replaced. This way, the fallback
  // loader receives the query which was intended for it instead of the query which was intended for url-loader.
  const fallbackLoaderContext = Object.assign({}, this, {
    query: fallbackOptions,
  });

  return fallback.call(fallbackLoaderContext, content);
}

// Loader Mode
export const raw = true;

獲取options以及校驗options

webpack官方文檔中的 編寫一個loader 一文介紹了編寫loader需要使用到的兩個工具庫:loader-utilsschema-utils

充分利用 loader-utils 包。它提供了許多有用的工具,但最常用的一種工具是獲取傳遞給 loader 的選項。schema-utils 包配合 loader-utils,用于保證 loader 選項,進(jìn)行與 JSON Schema 結(jié)構(gòu)一致的校驗。

  const options = getOptions(this) || {};

  validate(schema, options, {
    name: 'URL Loader',
    baseDataPath: 'options',
  });

validate 需要一個 schema 來對獲取到的 options 進(jìn)行校驗,url-loader 的 schema 如下圖:

image.png

判斷文件和limit的大小關(guān)系

content 是 Buffer 類型,buf.length 返回 buf 中的字節(jié)數(shù),單位和 limit 一致。

  if (shouldTransform(options.limit, content.length)) {
    // .... 
  }

shouldTransform 中,對 limit 執(zhí)行3種類型判斷 :

  1. 如果是 true 或者 false,直接 return limit
  2. 如果是字符串,轉(zhuǎn)成數(shù)字,再和 limit比較
  3. 如果是數(shù)字,直接和 limit 比較
  4. 不符合上述3種判斷,return true
function shouldTransform(limit, size) {
  if (typeof limit === 'boolean') {
    return limit;
  }

  if (typeof limit === 'string') {
    return size <= parseInt(limit, 10);
  }

  if (typeof limit === 'number') {
    return size <= limit;
  }

  return true;
}

轉(zhuǎn)化文件

shouldTransform 返回 true后,需要把當(dāng)前文件轉(zhuǎn)化成Data URLs (默認(rèn)base64):

    const { resourcePath } = this;
    const mimetype = getMimetype(options.mimetype, resourcePath);
    const encoding = getEncoding(options.encoding);
    
    const encodedData = getEncodedData(
      options.generator,
      mimetype,
      encoding,
      content,
      resourcePath
    );

    const esModule =
      typeof options.esModule !== 'undefined' ? options.esModule : true;

    return `${
      esModule ? 'export default' : 'module.exports ='
    } ${JSON.stringify(encodedData)}`;

其中 this.resourcePath 是文件的絕對路徑,通過 getMimetype 得到文件最終生成的 mimeType ,通過 getEncoding 得到編碼方式。

mimetype 首先通過 path.extname 獲取當(dāng)前擴(kuò)展名,再通過 mime-types 把擴(kuò)展名包裝成對應(yīng)的mimeType

function getMimetype(mimetype, resourcePath) {
  if (typeof mimetype === 'boolean') {
    if (mimetype) {
      const resolvedMimeType = mime.contentType(path.extname(resourcePath));
      if (!resolvedMimeType) {
        return '';
      }

      return resolvedMimeType.replace(/;\s+charset/i, ';charset');
    }
    return '';
  }

  if (typeof mimetype === 'string') {
    return mimetype;
  }

  const resolvedMimeType = mime.contentType(path.extname(resourcePath));

  if (!resolvedMimeType) {
    return '';
  }

  return resolvedMimeType.replace(/;\s+charset/i, ';charset');
}

那下面這段代碼有什么用處呢?

resolvedMimeType.replace(/;\s+charset/i, ';charset')

原因是 mime-typescontentType 返回值 ; charset 中間是有空格的:

image.png

而在Data URL 的書寫格式中,mediatype ;charset 是無空格的:

image.png

因此需要做一步替換去除空格。

如上文配置項部分所說,encoding 默認(rèn)是base64,開發(fā)者可以自行決定是否覆蓋:

 function getEncoding(encoding) {
  if (typeof encoding === 'boolean') {
    return encoding ? 'base64' : '';
  }

  if (typeof encoding === 'string') {
    return encoding;
  }

  return 'base64';
}

最后,根據(jù) getMimeTypegetEncoding 的返回值,對文件內(nèi)容進(jìn)行拼接轉(zhuǎn)化。
如果存在 options.generator,則執(zhí)行 options.generator 對內(nèi)容做轉(zhuǎn)化。
如果不存在,則按照 Data URL 格式拼接文件:

 data:[<mediatype>][;base64],<data>
function getEncodedData(generator, mimetype, encoding, content, resourcePath) {
  if (generator) {
    return generator(content, mimetype, encoding, resourcePath);
  }

  return `data:${mimetype}${encoding ? `;${encoding}` : ''},${content.toString(
    encoding || undefined
  )}`;
}

對于不符合shouldTransform的文件,則繼續(xù)往下執(zhí)行。

執(zhí)行fallback loader

首先獲取 fallback loader 和 options :

 const {
    loader: fallbackLoader,
    options: fallbackOptions,
  } = normalizeFallback(options.fallback, options);

url-loader 默認(rèn)使用 file-loader 作為處理 >limit 文件 的 loader。

  • 如果開發(fā)者沒有配置 options.fallback,就直接使用 url-loader 的 options 作為 file-loader的options。
  • 如果開發(fā)者配置了options.fallback
    • 如果 fallback 類型是 string,loader 名稱和 options 通過?隔開
    • 如果 fallback 是 object,loader 名稱和 options 分別為 fallback 的屬性(這種寫法在 url-loader 的文檔沒有介紹)

normalizeFallback如下:

export default function normalizeFallback(fallback, originalOptions) {
  let loader = 'file-loader';
  let options = {};

  if (typeof fallback === 'string') {
    loader = fallback;

    const index = fallback.indexOf('?');

    if (index >= 0) {
      loader = fallback.substr(0, index);
      options = loaderUtils.parseQuery(fallback.substr(index));
    }
  }

  if (fallback !== null && typeof fallback === 'object') {
    ({ loader, options } = fallback);
  }

  options = Object.assign({}, originalOptions, options);

  delete options.fallback;

  return { loader, options };
}

引入 loader,然后執(zhí)行 loader 并返回結(jié)果:

  // Require the fallback.
  const fallback = require(fallbackLoader);

  // Call the fallback, passing a copy of the loader context. The copy has the query replaced. This way, the fallback
  // loader receives the query which was intended for it instead of the query which was intended for url-loader.
  const fallbackLoaderContext = Object.assign({}, this, {
    query: fallbackOptions,
  });

  return fallback.call(fallbackLoaderContext, content);

流程圖

image.png

file-loader

Instructs webpack to emit the required object as file and to return its public URL

file-loader 可以指定要放置資源文件的位置,以及如何使用哈希等命名以獲得更好的緩存。這意味著可以通過工程化方式就近管理項目中的圖片文件,不用擔(dān)心部署時 URL 的問題。

配置項

image.png

源碼分析

獲取options以及根據(jù)schema校驗options:

 const options = getOptions(this);

  validate(schema, options, {
    name: 'File Loader',
    baseDataPath: 'options',
  });

獲取 context 及生成文件名稱。使用loader-utils提供的interpolateName獲取文件的hash值,并生成唯一的文件名:

  const context = options.context || this.rootContext;
  const name = options.name || '[contenthash].[ext]';

  const url = interpolateName(this, name, {
    context,
    content,
    regExp: options.regExp,
  });

其中 interpolateName 的定義如下:

interpolatename(loadercontext, name, options)
//  loadercontext 為 loader 的上下文對象,name 為文件名稱模板,options 為配置對象

根據(jù)配置的 outputPath 和 publicPath 生成最終的 outputPath 和 publicPath,如果想要在dev和prod環(huán)境寫不同的值,就可以把outputPath和publicPath寫成函數(shù)形式:

// 處理outputPath
  let outputPath = url;

  if (options.outputPath) {
    if (typeof options.outputPath === 'function') {
      outputPath = options.outputPath(url, this.resourcePath, context);
    } else {
      outputPath = path.posix.join(options.outputPath, url);
    }
  }

  // 處理publicPath
  let publicPath = `__webpack_public_path__ + ${JSON.stringify(outputPath)}`;

  if (options.publicPath) {
    if (typeof options.publicPath === 'function') {
      publicPath = options.publicPath(url, this.resourcePath, context);
    } else {
      publicPath = `${
        options.publicPath.endsWith('/')
          ? options.publicPath
          : `${options.publicPath}/`
      }${url}`;
    }

    publicPath = JSON.stringify(publicPath);
  }

  if (options.postTransformPublicPath) {
    publicPath = options.postTransformPublicPath(publicPath);
  }

處理emitFile。如果沒有配置emitFile或者配置了emitFile,最后會執(zhí)行this.emitFile在outputPath生成一個文件:

  if (typeof options.emitFile === 'undefined' || options.emitFile) {
    const assetInfo = {};

    if (typeof name === 'string') {
      let normalizedName = name;

      const idx = normalizedName.indexOf('?');

      if (idx >= 0) {
        normalizedName = normalizedName.substr(0, idx);
      }

      const isImmutable = /\[([^:\]]+:)?(hash|contenthash)(:[^\]]+)?]/gi.test(
        normalizedName
      ); 

      if (isImmutable === true) {
       //  指定 asset 是否可以長期緩存的標(biāo)志位(包括哈希值)
        assetInfo.immutable = true;
      }
    }

    assetInfo.sourceFilename = normalizePath(
      path.relative(this.rootContext, this.resourcePath)
    );
   
    this.emitFile(outputPath, content, null, assetInfo);
  }

webpack官方文檔里emitFile只有3個參數(shù):

emitFile(name: string, content: Buffer|string, sourceMap: {...})

關(guān)于emitFile第四個參數(shù)是在file-loader里的issue上看到的。

導(dǎo)出最終路徑:

  const esModule =
    typeof options.esModule !== 'undefined' ? options.esModule : true;

  return `${esModule ? 'export default' : 'module.exports ='} ${publicPath};`;

總結(jié)

file-loader可能會生成文件(emitFile),返回的是文件的路徑;
url-loader 小于limit的文件返回的是Data URL字符串,其他文件返回執(zhí)行 fallback loader 的結(jié)果;

url-loaderfile-loader 的唯一聯(lián)系就是 url-loaderfile-loader 作為默認(rèn)的 fallback loader,而我們也經(jīng)常在項目的 url-loader 配置寫 file-loader 的配置:

{
  limit: CDN_THRESHOLD,
  publicPath: `http://${genIp()}:9081/`,
  name: '[path]/[name].[ext]',
}

其中的 publicPathname 都屬于 file-loader 的 options。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容