如何编写本身的webpack加载器

所谓的加载器其实就是一个导出函数的node模块。若是有资源须要该加载器处理,webpack就会自动调用该加载器。这个返回的函数经过上下文提供的this能够访问Loader API.javascript

设置

在咱们深刻了解不一样种类的加载器以及他们的用法、相关案例以前,咱们先来看看在本地开发和测试加载器的三种方式吧。
想要测试一个加载器,你能够在rules对象里面用pathresolve一个本地文件:css

{
  test: /\.js$/
  use: [
    {
      loader: path.resolve('path/to/loader.js'),
      options: {/* ... */}
    }
  ]
}

想要测试多个加载器,你能够经过resolveLoader.modules配置项告诉webpack应该去哪儿寻找加载器。
好比项目里面有个目录/loaders:html

resolveLoader: {
  modules: [
    'node_modules',
    path.resolve(__dirname, 'loaders')
  ]
}

最后,若是你以前已经单独为加载器建立了一个仓库,不要忘了使用npm link将加载器连接到你当前要测试的项目。java

简单用法

加载器处理资源的时候只是传入一个字符串参数,也就是那个资源文件里面的内容。
同步加载器能够简单地返回一个表明被处理资源的值。不过更复杂状况下,也能够经过this.callback(err, values...)返回任意数量的值。错误(Errors)要么被传入那个this.callback函数里面,要么就在同步加载器里面抛出。
加载器应该返回1个或者2个值。第一个应该是String或者Buffer类型值,表明处理中的JS代码;第二个是一个JS对象,就是所谓的SourceMap .node

复杂用法

多个加载器链式调用时,值得注意的就是,他们的执行顺序是倒叙的---根据数组格式写法,要么从右向左,要么从下向上。webpack

  1. 最后一个加载器,最早被调用,它要处理原始的资源。
  2. 第一个加载器,最后被调用,应该返回JS代码以及一个可选的 source map.
  3. 中间的加载器,每个加载器处理的都是前一个加载器执行完的结果。

所以,下面的例子中,foo-loader会首先被调用,处理原始资源,而后返回值传给bar-loader.bar-loader执行完后返回最终的JS结果,有必要的话还会返回Source map.web

{
  test: /\.js/,
  use: [
    'bar-loader',
    'foo-loader'
  ]
}

编写原则

加载器编写须要遵循如下原则。根据重要性排列以下。其中有些只在特定场景下使用。想了解更多能够查看随后的详情介绍。npm

  • 尽量简单
  • 使用链式
  • 模块化输出
  • 确保无状态
  • 利用加载器公共功能
  • 标记加载器依赖
  • 解析模块依赖
  • 抽离公共代码
  • 避免绝对路径
  • 使用peer dependencies

简单

加载器应该只作某个单一的事情。这不但使得维护更容易,同时也容许在更多场景下去链式使用加载器。(其实就是功能比较单一的加载器在复杂的场景下能够被链式的组合使用)json

链式调用

充分利用加载器能够链式调用这一特性。好比,咱们应该编写5个加载器,让每一个加载器去处理一项任务,而不是编写一个处理5项任务的加载器。这种分离不但使得加载器极其简单,并且有时候还能在你原先没有想到场景下使用。
考虑一下。使用加载器options选项或者query参数提供的数据去渲染一个模板文件的场景。这个加载器会首先读取源模板文件,执行,并最终返回一个包含所有html代码的字符串。然而为了符合上面准则(简单,单一原则),apply-loader能够用来简单的连接其余开源加载器。api

  • jade-loader:将模板转换为一个导出函数的模块。
  • apply-loader:使用加载器options执行那个函数,并返回原生的html。
  • html-loader:传入html,并返回一个有效的JS模块。
事实上加载器能够连接也就意味着他们不必定都返回JS代码。只要加载器执行对列下一个加载器可以处理,加载器就能返回任意类型的模块。

模块化

保持模块化输出。加载器产生的模块应该遵循一样的设计原则。

无状态

保证加载器在模块转换的时候不要保持状态。买一次执行加载器都不该该收到其余已编译或者同一个模块往次编译加过的影响。

加载器实用工具

使用加载器工具包loader-utils。它提供了不少有用的工具,尤为重要的是获取传入加载器的options的工具。同时使用schema-utils包能够被用来保证加载器options校验的一致性。下面有一个简短的案列。

import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string'
    }
  }
}

export default function(source) {
  const options = getOptions(this);

  validateOptions(schema, options, 'Example Loader');

  // Apply some transformations to the source...

  return `export default ${ JSON.stringify(source) }`;
};

加载器依赖

若是加载器使用了外部资源(好比,从文件系统读入),就必需要指明。在watch模式下,这个信息能够用来清除失效的加载器缓存,并从新编译。下面是使用addDependency方法实现这一功能的一个简短案例:
loader.js:

import path from 'path';

export default function(source) {
  var callback = this.async();
  var headerPath = path.resolve('header.js');

  this.addDependency(headerPath);

  fs.readFile(headerPath, 'utf-8', function(err, header) {
    if(err) return callback(err);
    callback(null, header + "\n" + source);
  });
};

模块依赖

因为模块的类型各类各样,因此指定模块依赖的方式也各有不一样。好比在css中,咱们就使用@importurl(....)来指定相关依赖的。模块系统会解析这些依赖的。
下面的两种方法能够完成:

  • 把他们所有转换为require语句。
  • 经过this.resolve函数解析路径。

css-loader加载器就是第一种方案很好的一个案例。它把样式表文件里面的@import和url所有转换为require语句去引入相关资源了。
less-loader因为要处理复杂的变量和mixins,因此根本无法把每个@import给转换为require。所以,less-loader在less编译器的基础上扩展了自定义的路径解析逻辑。而后它利用上述第二种方案里面的this.resolve去解析相关依赖。

注意:若是语言只接受相对路径的url,你可使用`~`去引用已安装的模块(好比`node_modules`里的模块)。这种状况下看起来是这样子的`url('~some-library/image.jpg')`.

公共代码

为了不在加载器处理的模块里面生成重复的代码,应该在加载器里面生成一个运行时文件放在独立的模块里面,而后让每个加载器处理的模块require那个共享的运行时文件。

绝对路径

不要在代码里面使用绝对路径,不然一旦项目根目录迁移,之前的哈希名称什么的会破坏掉。loader-utils里面的stringifyRequest方法能够把绝对路径转换为相对路径。

同伴依赖

若是你的加载器只是在别的包上封装而成的。你应该把那个包指定为一个peerDependency.这样容许开发者在package.json里面指定准确的版本号。
好比。sass-loader指定node-sass做为peer dependency

"peerDependencies": {
  "node-sass": "^4.0.0"
}

测试

如今你已经根据上面的原则编写好本身的加载器了,并在本地运行起来了,接下来干吗呢?我们运行一个简单的单元测试以确保加载器符合咱们的预期吧。这儿咱们使用Jest这个框架。为了使用import / export async / await咱们把babel-jest以及一些babel预设都给安装上。如今开始安装并把他们保存为devDependencies

npm install --save-dev jest babel-jest babel-preset-env

.babelrc

{
  "presets": [[
    "env",
    {
      "targets": {
        "node": "4"
      }
    }
  ]]
}

咱们的加载器会处理txt格式的文件,而且仅仅只是使用传入加载器options里面的name参数去替换txt文件里面的[name].而后就会输出转换好的JS代码咯。
src/loader.js

import { getOptions } from 'loader-utils';

export default function loader(source) {
  const options = getOptions(this);

  source = source.replace(/\[name\]/g, options.name);

  return `export default ${ JSON.stringify(source) }`;
};

而后使用这个加载器去处理下面这个文件:
test/example.txt

Hey [name]!

请注意接下来的这一步哦,咱们要使用Node API和memory-fs来执行webpack。这样就避免了把输出结果输出到硬盘上了,经过访问stats数据,咱们就能够拿到转换好了的模块了。

npm install --save-dev webpack memory-fs

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: /\.txt$/,
        use: {
          loader: path.resolve(__dirname, '../src/loader.js'),
          options: {
            name: 'Alice'
          }
        }
      }]
    }
  });

  compiler.outputFileSystem = new memoryfs();

  return new Promise((resolve, reject) => {
    compiler.run((err, stats) => {
      if (err) reject(err);

      resolve(stats);
    });
  });
}
上面,咱们内联了webpack的配置项。可是你依然可让导出的函数接受一个webpack配置项做为参数,这样就可使用同一个编译器模块去测试不一样的设置了。

如今,咱们就能够编写测试案例,并添加npm脚本,跑起来咯:
test/loader.test.js

import compiler from './compiler.js';

test('Inserts name and outputs JavaScript', async () => {
  const stats = await compiler('example.txt');
  const output = stats.toJson().modules[0].source;

  expect(output).toBe(`export default "Hey Alice!\\n"`);
});

package.json

"scripts": {
  "test": "jest"
}

一切就绪,如今就能够跑起来看看咱们的加载器测试是否经过咯。

PASS  test/loader.test.js
  ✓ Inserts name and outputs JavaScript (229ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.853s, estimated 2s
Ran all test suites.

成功了!如今你就能够去开发,测试,部署你本身的加载器咯。期待你的分享!

相关文章
相关标签/搜索