• 如何自定义开发一个webpack loader ?
  • 发布于 2个月前
  • 103 热度
    0 评论
前言
我们在用webpack进行工程化的时候,需要处理各种各样的文件,它们有自己独特的语法,webapck无法直接对其处理,因为webpack默认只识别标准的JS语法和 JSON数据,因此我们需要对应的loader来处理其他类型的文件内容。 

什么是loader?
loader, 即用于对模块的源代码进行转换,其实就如它的名字一样——“加载器”,说白了就是一个内容转换器,把webpack不认识的内容文件,转换成标准的JS模块,然后交给wepack进行打包处理,其本质就是一个导出一个函数的JS模块。常见的loader 有如css-loader、sass-loader、ts-loader、url-loader、babel-loader等。每个loader就犹如流水线的一个环节,接收源码输入,输出处理之后的内容,交给下一个环节。

loader的分类
我们可以按照执行优先级把loader分成四类:pre loader (前置)、normal loader (普调)、inline loader (内联)、post loader (后置)。其中 pre 和 post loader,可以通过 rule 对象的 enforce 属性来指定,默认为normal loader。
对于inline loader,webpack允许我们在引入模块的时候,显式地指定处理此模块的loader,如:
import Styles from 'style-loader!css-loader?modules!./styles.css';
可以通过为内联 import 语句添加前缀,来覆盖配置中的所有 loader, pre loader 和 post loader。(实际用的较少,不做过多说明了,有兴趣的可以可以查看webpack文档的规则说明)

loader的执行顺序
通常对某一类型的文件处理,可能不止需要一个loader,例如处理scss文件,我们需要style-loader、css-loader、sass-loader来处理,那么它们的执行顺序是如何的?假设我们在webpack的rules里面的某个rule的use依次配置了3个loader,如 ['loader1', 'loader2', 'loader3'],loader的代码如下(loader就是导出一个函数,返回处理后的内容,pitch就是此函数的一个属性,其值也是一个函数,会先执行):
// loader-1
function loader1 (source) {
console.log('loader 1');
return source;
}

loader1.pitch = function () {
console.log('pitch 1');
};

module.exports = loader1;

// loader-2
function loader2 (source) {
console.log('loader 2');
return source;
}

loader2.pitch = function () {
console.log('pitch 2');
};

module.exports = loader2;
// 堆代码 duidaima.com
// loader-3
function loader3 (source) {
console.log('loader 3');
return source;
}

loader3.pitch = function () {
console.log('pitch 3');
};

module.exports = loader3;
可以发现以上代码的打印结果是:pitch 1 -> pitch 2 -> pitch 3 -> loader 3 -> loader 2 -> loader 1。
loader的执行,分为两个阶段,pitch 和 normal阶段(可以把它想象成类似于事件的捕获和冒泡阶段)。通常情况下我们都说的是normal阶段。
对于不同类型的rule,在normal阶段,其执行优先级为:pre > normal > inline > post,而在pitch阶段,则是反过来。
对于相同类型的rule, 或者某个rule里面的use数组,其总体执行过程如下:

如上图所示,如果在pitch阶段的时候,某个loader有返回值,则不会正常往下按照流程走,会直接跳到前一个loader的normal 阶段。
因为我们通常讨论 (或者默认情况下)的都是normal 阶段,因此loaders的执行顺序为:从后到前,从下到上,从右到左执行 (loader函数)。而在pitch阶段则反之。

同步、异步loader
一个loader函数,默认为同步loader。若想成为异步loader,则需要先调用this.async(),执行这个方法后,loader-runner内部会将此loader认为是异步的,并返回一个callback以供调用。
// sync loader
module.exports = function (source) {
// handle source content
return source;
// or
// this.callback(null, source);
};
// async loader
module.exports = function (source) {
const callback = this.async();
  setTimeout(() => {
console.log('loaded completely');
    callback(null, source);
  }, 1000);
return source;
};
实现一个自定义loader
当我们在开发实际项目的时候,也许会遇到要实现自定义loader的需求,例如很多跨平台框架 (或者需要多版本独立发版的项目) ,当平台 (版本) 实现代码之间差异很大的时候,对于同一个文件 (组件) ,我们需要为每个平台 (版本) 实现自己的代码,如有文件为:content.tsx,我们需要为h5平台独立定制内容的时候,会创建同名h5后缀的文件:content.h5.tsx,在引用此文件 (组件) 的地方,还是引用的content.tsx文件,但是当当前编译的平台是h5的时候,如果存在以当前平台名为后缀名的同名文件 (content.h5.tsx) 时候,就会替换原文件 (content.tsx) 的内容,这样我们就实现了多平台 (版本) ,为某平台 (版本) 定制内容打包的自定义loader,姑且把这个loader称为:replace-loader,文件路径为: scripts/loaders/replace-loader.js,源码如下:
const fs = require('fs');
const { argv } = require('yargs');
const platform = argv.platform;

module.exports = function (source) {
  // console.log(this.getOptions());
  const platFilePath = this.resourcePath.replace(/([\w-]+)(.ts|.tsx)$/, `$1.${platform}$2`);
  if (fs.existsSync(platFilePath)) {
    return fs.readFileSync(platFilePath, { encoding: 'utf8' });
  }
  return source;
};
我们只需要在webpack的rules添加这个自定义loader,代码如下:
{
  ...,
  resolveLoader: {
    modules: ['node_modules', 'scripts/loaders']
  },
  module: {
    rules: [
      ...,
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: [{
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-typescript']
          }
        }, {
          loader: 'replace-loader',
          options: {
            name: 'test'
          }
        }]
      },
      ...
    ]
  },
  ...
}

我们可以在resolveLoader中配置loader的查找目录,以上代码的意思是:当遇到 ts(x)的文件的时候,需先调用名为 'replace-loader' 的loader处理,会先去 'node_modules' 先找这个loader,不存在的话就会到 'scripts/loaders'目录去找名为: replace-loader.js的文件的loader,这样就能像其他loader一样进行配置了 (当然,你也可以不在resolveLoader进行额外配置,那么你引入loader的时候,就只能以 { loader: '你的loader文件路径' } 引入了,就不是很优雅看起来)。


在loader 函数内部,可通过this对象获取当前资源文件的路径、回调函数、options参数等等,来满足自己的功能。

结语
在实际项目中,虽然需要我们实现自定义的loader的场景不会太多,但是难免会有,所以我们必须得了解loader的工作原理,以及如何实现一个自定义loader,这样当我们遇到相应的场景的时候,就会从容很多了,也是了解前端项目工程化的必备知识。
用户评论