如何实现一个webpack yaml-loader

1、什么是 loader

loader 和 plugins 是 webpack 系统的两大重要组成元素。依靠对 loader、plugins 的不一样组合搭配,咱们能够灵活定制出高度适配自身业务的打包构建流程。javascript

loader 是 webpack 容纳各种资源的一个重要手段,它用于对模块的源代码进行转换,容许你在 import 或加载模块时预处理文件,利用 loader,咱们能够将各类类型的资源转换成 webpack 本质接受的资源类型,如 javascript。前端

2、如何编写一个 yaml-loader

一、YAML

yaml 语言多用于编写配置文件,结构与 JSON 相似,但语法格式比 JSON 更加方便简洁。yaml 支持注释,大小写敏感,使用缩进来表示层级关系:vue

#对象 
version: 1.2.4
#数组
author:
 - Mike
 - Hankle
#常量
name: "my project" #定义一个字符串
limit: 30 #定义一个数值
es6: true #定义一个布尔值
openkey: Null #定义一个null
#锚点引用
server:
 base: &base
 port: 8005
 dev:
 ip: 120.168.117.21
    <<: *base
 gamma:
 ip: 120.168.117.22
    <<: *base
复制代码

等同于:java

{
  "version": "1.2.4",
  "author": ["Mike", "Hankle"],
  "name": "my project",
  "limit": 30,
  "es6": true,
  "openkey": null,
  "server": {
    "base": {
      "port": 8005
    },
    "dev": {
      "ip": "120.168.117.21",
      "port": 8005
    },
    "gamma": {
      "ip": "120.168.117.22",
      "port": 8005
    }
  }
}
复制代码

在基于 webpack 构建的应用中,若是但愿可以引用 yaml 文件中的数据,就须要一个 yaml-loader 来支持编译。通常状况下,你都能在 npm 上找到可用的 loader,但若是万一没有对应的支持,或者你但愿有一些自定义的转换,那么就须要本身编写一个 webpack loader 了。node

二、loader 的原理

loader 是一个 node 模块,它导出为一个函数,用于在转换资源时调用。该函数接收一个 String/Buffer 类型的入参,并返回一个 String/Buffer 类型的返回值。一个最简单的 loader 是这样的:webpack

// loaders/yaml-loader.js
module.exports = function(source) {
  return source;
};
复制代码

loader 支持管道式传递,对同一类型的文件,咱们可使用多个 loader 进行处理,这批 loader 将按照“从下到上、从右到左”的顺序执行,并之前一个 loader 的返回值做为后一个 loader 的入参。这个机制无非是但愿咱们在编写 loader 的时候可以尽可能避免重复造轮子,只关注须要实现的核心功能。所以配置的时候,咱们能够引入 json-loader:es6

// webpack.config.js
const path = require("path");

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.yml$/,
        use: [
          {
            loader: "json-loader"
          },
          {
            loader: path.resolve(__dirname, "./loaders/yaml-loader.js")
          }
        ]
      }
    ]
  }
};
复制代码

三、开始

这样一来,咱们须要的 yaml-loader,就只作一件事情:将 yaml 的数据转化成为一个 JSON 字符串。所以,咱们能够很简单地实现这样一个 yaml-loader:web

var yaml = require("js-yaml");

module.exports = function(source) {
  this.cacheable && this.cacheable();
  try {
    var res = yaml.safeLoad(source);
    return JSON.stringify(res, undefined, "\t");
  } catch (err) {
    this.emitError(err);
    return null;
  }
};
复制代码

就是这么简单。可是可能有朋友会问,这里是由于有个现成的模块 js-yaml,能够直接将 yaml 转换成 JavaScript 对象,万一没有这个模块,该怎么作呢?是的,loader 的核心工做其实就是字符串的处理,这是个至关恶心的活儿,尤为是在这类语法转换的场景上,对源代码的字符串处理将变得极其复杂。这个状况下,咱们能够考虑另一种解法,借助 AST 语法树,来协助咱们更加便捷地操做转换。npm

四、利用 AST 做源码转换

yaml-ast-parser 是一个将 yaml 转换成 AST 语法树的 node 模块,咱们把字符串解析的工做交给了 AST parser,而操做 AST 语法树远比操做字符串要简单、方便得多:json

const yaml = require("yaml-ast-parser");

class YamlParser {
  constructor(source) {
    this.data = yaml.load(source);
    this.parse();
  }

  parse() {
    // parse ast into javascript object
  }
}

module.exports = function(source) {
  this.cacheable && this.cacheable();
  try {
    const parser = new YamlParser(source);
    return JSON.stringify(parser.data, undefined, "\t");
  } catch (err) {
    this.emitError(err);
    return null;
  }
};
复制代码

这里咱们能够利用 AST parser 提供的方法直接转化出 json,若是没有或者有所定制,也能够手动实现一下 parse 的过程,仅仅只是一个树结构的迭代遍历而已,关键步骤是对 AST 语法树的各种型节点分别进行处理:

const yaml = require("yaml-ast-parser");
const types = yaml.Kind;

class YamlParser {
  // ...
  parse() {
    this.data = this.traverse(this.data);
  }

  traverse(node) {
    const type = types[node.kind];

    switch (type) {
      // 对象
      case "MAP": {
        const ret = {};
        node.mappings.forEach(mapping => {
          Object.assign(ret, this.traverse(mapping));
        });
        return ret;
      }
      // 键值对
      case "MAPPING": {
        let ret = {};
        // 验证
        const keyValid =
          yaml.determineScalarType(node.key) == yaml.ScalarType.string;
        if (!keyValid) {
          throw Error("键值非法");
        }

        if (node.key.value == "<<" && types[node.value.kind] === "ANCHOR_REF") {
          // 引用合并
          ret = this.traverse(node.value);
        } else {
          ret[node.key.value] = this.traverse(node.value);
        }
        return ret;
      }
      // 常量
      case "SCALAR": {
        return node.valueObject !== undefined ? node.valueObject : node.value;
      }
      // 数组
      case "SEQ": {
        const ret = [];
        node.items.forEach(item => {
          ret.push(this.traverse(item));
        });
        return ret;
      }
      // 锚点引用
      case "ANCHOR_REF": {
        return this.traverse(node.value);
      }
      default:
        throw Error("unvalid node");
    }
  }
}
// ...
复制代码

固然这样的实现略为粗糙,正常来讲,一些完备的 AST parser 通常都会自带遍历方法(traverse),这样的方法都是有作过优化的,咱们能够直接调用,尽可能避免本身手动实现。

按照相同的作法,你还能够实现一个 markdown-loader,甚至更为复杂的 vue-loader。

3、loader 的一些开发技巧

一、单一任务

只作一件事情,作好一件事情。loader 的管道(pipeline)设计正是但愿可以将任务拆解并独立成一个个子任务,由多个 loader 分别处理,以此来保证每一个 loader 的可复用性。所以咱们在开发 loader 前必定要先给 loader 一个准确的功能定位,从通用的角度出发去设计,避免作多余的事。

二、无状态

loader 应该是不保存状态的。这样的好处一方面是使咱们 loader 中的数据流简单清晰,另外一方面是保证 loader 具备良好可测性。所以咱们的 loader 每次运行都不该该依赖于自身以前的编译结果,也不该该经过除出入参外的其余方式与其余编译模块进行数据交流。固然,这并不表明 loader 必须是一个无任何反作用的纯函数,loader 支持异步,所以是能够在 loader 中有 I/O 操做的。

三、尽量使用缓存

在开发时,loader 可能会被不断地执行,合理的缓存可以下降重复编译带来的成本。loader 执行时默认是开启缓存的,这样一来, webpack 在编译过程当中执行到判断是否须要重编译 loader 实例的时候,会直接跳过 rebuild 环节,节省没必要要重建带来的开销。

当且仅当有你的 loader 有其余不稳定的外部依赖(如 I/O 接口依赖)时,能够关闭缓存:

this.cacheable && this.cacheable(false);
复制代码

若是你以为这篇内容对你有价值,欢迎点赞并关注咱们前端团队的 官网 和咱们的微信公众号 WecTeam,每周都有优质文章推送~

相关文章
相关标签/搜索