手把手教你写一个 Webpack Loader

本文示例源代码请戳github博客,建议你们动手敲敲代码。html

本文不会介绍loader的一些使用方法,不熟悉的同窗请自行查看 Webpack loader

一、背景

首先咱们来看一下为何须要loader,以及他能干什么?
webpack 只能理解 JavaScriptJSON 文件。loaderwebpack 可以去处理其余类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。node

本质上来讲,loader 就是一个 node 模块,这很符合 webpack 中「万物皆模块」的思路。既然是 node 模块,那就必定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块resource的时候调用该函数。在这个函数内部,咱们能够经过传入 this 上下文给 Loader API 来使用它们。最终装换成能够直接引用的模块。webpack

二、xml-Loader 实现

前面咱们已经知道,因为 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块须要导出一个函数。 这个导出的函数的工做就是得到处理前的原内容,对原内容执行处理后,返回处理后的内容。
一个简单的loader源码以下git

module.exports = function(source) {
  // source 为 compiler 传递给 Loader 的一个文件的原内容
  // 该函数须要返回处理后的内容,这里简单起见,直接把原内容返回了,至关于该 Loader 没有作任何转换
  return source;
};

因为 Loader 运行在 Node.js 中,你能够调用任何 Node.js 自带的 API,或者安装第三方模块进行调用:github

const xml2js = require('xml2js');
const parser = new xml2js.Parser();

module.exports =  function(source) {
  this.cacheable && this.cacheable();
  const self = this;
  parser.parseString(source, function (err, result) {
    self.callback(err, !err && "module.exports = " + JSON.stringify(result));
  });
};

这里咱们事简单实现一个xml-loader;web

注意:若是是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpackrequire,换句话说,它必定是一段可执行的 JS 脚本 (用字符串来存储),更准确来讲,是一个 node 模块的 JS 脚本,因此咱们须要用 module.exports =导出。

整个过程至关于这个 loader 把源文件npm

// 这里是 source 模块

转化为json

// example.js
module.exports = '这里是 source 模块';

而后交给 require 调用方:segmentfault

// applySomeModule.js
var source = require('example.js'); 
console.log(source); // 这里是 source 模块

写完后咱们要怎么在本地验证呢?下面咱们来写个简单的demo进行验证。api

2.一、验证

首先咱们建立一个根目录xml-loader,此目录下 npm init -y生成默认的package.json文件 ,在文件中配置打包命令

"scripts": {
    "dev": "webpack-dev-server"
  },

以后npm i -D webpack webpack-cli,安装完webpack,在根目录 建立配置文件webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  },
  module: {
    rules: [
      {
        test: /\.xml$/,
        use: ['xml-loader'],
      }
    ]
  },
  resolveLoader: {
    modules: [path.join(__dirname, '/src/loader')]
  },
  devServer: {
    contentBase: './dist',
    overlay: {
      warnings: true,
      errors: true
    },
    open: true
  }
}

在根目录建立一个src目录,里面建立index.js,

import data from './foo.xml';

function component() {
  var element = document.createElement('div');
  element.innerHTML = data.note.body;
  element.classList.add('header');
  console.log(data);
  return element;
}

document.body.appendChild(component());

同时还有一个foo.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<note>
    <to>Mary</to>
    <from>John</from>
    <heading>Reminder  dd</heading>
    <body>Call Cindy on Tuesday dd</body>
</note>

最后把上面的xml-loader放到src/loader文件夹下。
完整的demo源码请看
最终咱们的运行效果以下图
图片描述

至此一个简单的webpack loader就实现完成了。固然最终使用你能够发布到npm上。

三、一些议论知识补充

3.一、得到 Loader 的 options

当咱们配置loader时咱们常常会看到有这样的配置

ules: [{
    test: /\.html$/,
    use: [ {
      loader: 'html-loader',
      options: {
        minimize: true
      }
    }],
  }]

那么咱们在loader中怎么获取这写配置信息呢?答案是loader-utils。这个由webpack提供的工具。下面咱们来看下使用方法

const loaderUtils = require('loader-utils');
module.exports = function(source) {
  // 获取到用户给当前 Loader 传入的 options
  const options = loaderUtils.getOptions(this);
  return source;
};

没错就是这么简单。

3.二、加载本地 Loader

一、path.resolve
能够简单经过在 rule 对象设置 path.resolve 指向这个本地文件

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

二、ResolveLoader
这个就是上面我用到的方法。ResolveLoader 用于配置 Webpack 如何寻找 Loader。 默认状况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 须要修改 resolveLoader.modules
假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则须要以下配置:

module.exports = {
  resolveLoader:{
    // 去哪些目录下寻找 Loader,有前后顺序之分
    modules: ['node_modules','./loaders/'],
  }
}

加上以上配置后, Webpack 会先去 node_modules 项目下寻找 Loader,若是找不到,会再去 ./loaders/ 目录下寻找。
三、npm link
npm link 专门用于开发和调试本地 npm 模块,能作到在不发布模块的状况下,把本地的一个正在开发的模块的源码连接到项目的 node_modules 目录下,让项目能够直接使用本地的 npm 模块。 因为是经过软连接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 npm link 的步骤以下:

  • 确保正在开发的本地 npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
  • 在本地 npm 模块根目录下执行 npm link,把本地模块注册到全局;
  • 在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块连接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的package.json 文件中配置的模块名称。

连接好 Loader 到项目后你就能够像使用一个真正的 Npm 模块同样使用本地的 Loader 了。(npm link不是很熟,复制被人的)

3.三、缓存加速

在有些状况下,有些转换操做须要大量计算很是耗时,若是每次构建都从新执行重复的转换操做,构建将会变得很是缓慢。 为此,Webpack 会默认缓存全部 Loader 的处理结果,也就是说在须要被处理的文件或者其依赖的文件没有发生变化时, 是不会从新调用对应的 Loader 去执行转换操做的。

若是你想让 Webpack 不缓存该 Loader 的处理结果,能够这样:

module.exports = function(source) {
  // 关闭该 Loader 的缓存功能
  this.cacheable(false);
  return source;
};

3.四、处理二进制数据

在默认的状况下,Webpack 传给 Loader 的原内容都是 UTF-8 格式编码的字符串。 但有些场景下 Loader 不是处理文本文件,而是处理二进制文件,例如 file-loader,就须要 Webpack 给 Loader 传入二进制格式的数据。 为此,你须要这样编写 Loader:

module.exports = function(source) {
    // 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
    source instanceof Buffer === true;
    // Loader 返回的类型也能够是 Buffer 类型的
    // 在 exports.raw !== true 时,Loader 也能够返回 Buffer 类型的结果
    return source;
};
// 经过 exports.raw 属性告诉 Webpack 该 Loader 是否须要二进制数据 
module.exports.raw = true;

以上代码中最关键的代码是最后一行 module.exports.raw = true;,没有该行 Loader 只能拿到字符串。

3.五、同步与异步

Loader 有同步和异步之分,上面介绍的 Loader 都是同步的 Loader,由于它们的转换流程都是同步的,转换完成后再返回结果。 但在有些场景下转换的步骤只能是异步完成的,例如你须要经过网络请求才能得出结果,若是采用同步的方式网络请求就会阻塞整个构建,致使构建很是缓慢。

在转换步骤是异步时,你能够这样:

module.exports = function(source) {
    // 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result, sourceMaps, ast) {
        // 经过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    });
};

参考

编写一个webpack loader