Loader 是什么

为了 让 Webpack 支持特殊资源(非 JavaScript)加载,Webpack 设计出了 Loader(加载器) 的机制。由于 Webpack 内置的加载器只支持 JS 的脚本,因此需要为其它的模块文件增加对应的加载器,将之最终处理为符合 JS 语法的内容交由 Webpack 处理,打包进最后的 Bundle.js 中。换言之,Loader 是将应用整体模块化的基石。

加载器在本质上都是一个使用 loader api 导出的一个执行函数,它会在其内读取文件,并将之处理输出,它的返回值可以为代码字符串或者 Buffer,但最终为Webpack 所用时,加载器的输出总是为 js 字符串(见下文流程示例)。

特性与使用

特性

  • 支持链式传递,会按照指定的顺序依次执行。
  • 运行在 Node.js 环境中。
  • 可同步,可异步。
  • 支持 qs 格式的参数传入。
  • 单一职责。
  • 无状态性。

配置

  1. rules: [
  2. { test: /\.css$/, // 使用正则匹配文件后缀加载对应loader
  3. use: [// 从右到左执行
  4. 'style-loader',
  5. 'css-loader'
  6. ]
  7. },
  8. {// 处理等级,使用这个属性可以无视书写顺序决定该loader的执行顺序。
  9. enforce: 'pre',
  10. test: /\.(js|vue)$/,
  11. loader: 'eslint-loader',// 单个loader不需要使用 use
  12. exclude: /node_modules/ // 排除
  13. }]

使用示例(以 SCSS 文件为例)

例如加载 .scss 文件时,需要先后使用 sass-loadercss-loaderstyle-loader 三个加载器,前者将 scss 文件编译为 css,再由 css-loader 将文件转为一个包含 CSS 字符串的数组 ,最后通过 style-loader 将数组中的内容生成 <style /> 标签注入到 HTML 文件中。

编写示例(以 md 转 html 为例)

以下是一个最简单的 markdown-loader 的示例:

  1. const marked = require('marked')
  2. module.exports = source => {// 输入的 source 是文件内容
  3. const html = marked(source)
  4. // const code = `module.exports = ${JSON.stringify(html)}` 使用 CommonJs的规范也可以。
  5. const code = `export default ${JSON.stringify(html)}`
  6. return code // 以符合js字符串的形式输出,或输出html文件流内容
  7. }

底层工作流程

在 Webpack 编译中,涉及 Loader 的部分大致如下:

参数配置阶段

在这个阶段,Webpack 会读取当前配置文件,将其与默认参数进行合并,值得一提的是,loader 也有默认参数,但不对用户开放。

  1. options = new WebpackOptionsDefaulter().process(options);
  2. compiler = new Compiler(options.context);
  3. compiler.options = options;

工厂类创建阶段

NormalModuleFactory 被用于创建模块后续执行 Loader 时的实例,其中有两点较为关键,解析路径和生成匹配规则,在以上内容完成以后,会通过 factory 创建出对应的 NormalModule 实例。

解析路径

在钩子 reslove 中,Webpack 将解析 loader 模块实际的对应路径。其中有包含了针对 inline-loaderconfig-loader 两项不同的解析逻辑,在该钩子中,针对 inline-loader 的解析会被优先执行。而由于配置参数中 enforce 属性的存在, config-loader 在解析后会按照 enforce 的优先级分为 preLoader、loader、postLoader 三类,其中,postLoader 的执行要比 inline-loader 更晚,因此如若不指定 enforce: post ,所有的 config-loader 都将比 inline-loader 执行得更快。

生成匹配规则

配置项中的 rules 与 exclude 项会在此用于生成工厂类中的 ruleSet 属性。该属性的值为一个可以用于根据路径名匹配出文件所有匹配的 loader 列表的类 RuleSet 。这个类可以通过 exec() 匹配出实际需要使用的 loader 列表。在每一个 NormalModule 被创建的时候, exec() 的方法都会被执行。

运行阶段

对于没打包过的模块, NormalModule 实例上的 doBuild() 将会被调用,它将会将所有对应的 loader 按优先级顺序执行一遍,最后产出对应到 bundle.js 里的结果(一个 IIFE 函数)。

Loader 是如何被运行的

加载器的运行使用了一个已经独立出来的库 loader-runner,其内分为 loadLoader 与 loaderRunner 两个部分。 loadLoader 用于兼容加载不同模块化规范下的加载器,而 loaderRunner 则负责在加载器被加载后执行它们,通过 runLoaders() 启动它的执行。在 runLoaders() 中,如若不传入 loaderContext ,loader-runner 会在每次运行时创建一个新的 loaderContext该对象就是实际编写 loader 时对应的 this

执行细节

  1. - a-loader `pitch`
  2. |- b-loader `pitch`
  3. |- c-loader `pitch`
  4. |- requested module is picked up as a dependency
  5. |- c-loader normal execution
  6. |- b-loader normal execution
  7. |- a-loader normal execution

每一个 loader 都包含一个属性 .pitch ,它会先于 loader 的自有逻辑执行,递归触发 iteratePitchingLoaders() ,它会记录加载器的 pitch 状态,并且通过计数器 loaderIndex++ 来记录已执行到的加载器序号,直到确定所有加载器都被 pitch 之后,才会进入实际的模块处理状态。而如果加载器在 pitch 阶段返回值,Webpack 将会跳过其余加载器,并将以此返回值作为 Webpack 处理的输入。

在通过 loaderIndex 确定了所有对应的加载器都已被读取后,Webpack 会执行 processResource() 来添加依赖并读取模块内容。之后递归触发 iterateNormalLoaders() ,重复利用 loaderIndex-- 来标记当前执行在哪一个加载器,这也是为什么在加载器配置中, use 总是从右向左执行的原因。

无论在 pitch 还是 normal 阶段,它们的实际执行都是在 runSyncOrAsync() 方法内,在其中,他会判断 isSync(默认为 true ) 这一属性是否为真来决定当前加载器是一个同步的加载器还是异步的,这一属性可以在编写加载器通过调用 this.sync() 将之置为 false ,但即便不在加载器代码中修改这一属性,只要加载器的返回是一个 Promise ,Webpack 仍会将其视作异步的加载器。

  1. if(isSync) {
  2. isDone = true;
  3. if(result === undefined)
  4. return callback();
  5. if(result && typeof result === "object" && typeof result.then === "function") { // 此处为判断返回值是否是一个Promise
  6. return result.catch(callback).then(function(r) {
  7. callback(null, r);
  8. });
  9. }
  10. return callback(null, result);
  11. }