webpack系列一之如何写一个自己的loader

目录
文章目录隐藏
  1. 添加 sourcemap
  2. 模块
  3. 如何编译 JSX

【webpack 系列一】如何写一个自己的 loader
这里以 babel-loader 为例,看我们如何写一个自己的 loader。首先,我们参考这篇官方教程,虽然写的很粗略,但是我们可以学会写一个简单的 loader。

最简单的 loader 是一个什么都不做,原样返回 JS 代码的 loader,像这样:

module.exports = function (source) {
  return source
}

但是我们的 babel-loader 显然需要调用 babel 来编译代码,我们查一下 babel-core 文档,可以调用 babel.transform API 来编译代码。再加上一些 presets 的设置,我们可以把上面的代码做一下改造如下:

var babel = require("babel-core")
module.exports = function (source) {
  var babelOptions = {
    presets: ['env']
  }
  var result = babel.transform(source, babelOptions)
  return result.code
}

关于 Babel 的 API 用法不在这里详细解释,有兴趣的可以直接去看官方的文档。我们这里做了一个很简单的转换,就是把 接收到的 source 源码,用 babel 编译一下,然后返回编译后的代码。

那么问题来了,我们要如何指定使用自己的 loader 呢,可以参考下面这种写法,直接在 webpack config 文件里面加一个 resolveLoader 的配置即可,我们这里把 bable-loader 指定为自己写的。

resolveLoader: {
    alias: {
      "babel-loader": resolve('./build/babel-loader.js')
    }
  },

然后我们就可以运行 npm run dev 编译自己的 JS 代码,比如我写了这么几行代码:

class People {
  constructor (name) {
    this.name = name
  }

  sayName () {
    console.log(`Hello there, I'm ${this.name}`)
  }
}

const lily = new People('Lily')
lily.sayName()

经过我们的 babel-loader 编译后,最终输出的代码是这样的:

var People = function () {
  function People(name) {
    _classCallCheck(this, People);

    this.name = name;
  }

  _createClass(People, [{
    key: 'sayName',
    value: function sayName() {
      console.log('Hello there, I\'m ' + this.name);
    }
  }]);

  return People;
}();

var lily = new People('Lily');
lily.sayName();

可以看到其中 class , 字符串模板,const 等都被 babel 编译过。

添加 sourcemap

上面的简单代码并没有实现 sourcemap 功能,如果需要支持 sourcemap,显然需要把 babel-core 产生的 sourcemap 传给 webpack。之前因为只返回编译后的代码,所以我们直接返回了字符串,如果需要同时返回编译的代码和 sourcemap,我们需要这个接口 this.callback,官方文档上是这么说的:

this.callback(
  err: Error | null,
  content: string | Buffer,
  sourceMap?: SourceMap,
  meta?: any
);

那么我们查一下 babel 的文档之后,很简单就能获取 sourcemap 了,其实就是 result.map,改动后的代码如下:

var babel = require("babel-core")

module.exports = function (source, inputSourceMap) {
  var babelOptions = {
    presets: ['env'],
    inputSourceMap: inputSourceMap,
    sourceMaps: true
  }
  var result = babel.transform(source, babelOptions)
  this.callback(null, result.code, result.map)
}

这里 sourceMaps: true 是告诉 babel 要生成 sourcemap,因为默认情况下它是不会生成的。然后刷新页面应该就能看到 sourcemap 了。

然而,根据你的 webpack 配置不同,很可能并看不到~~~这是因为 sourcemap 其实是要交给 webpack 来管理的,我们只是把 sourcemap 传给了 webpack,而它在最终编译出的代码中到底要不要显示是 webpack 自己的配置决定的。那么我们在 webpack.config.js 中添加一行代码打开 sourcemap 功能即可:

devtool: 'eval-source-map'

这样再刷新页面就能看到 sourcemap 了,确实可以,然而 sourcemap 的文件名怎么是 unknown? 我们在创建 sourcemap 的时候需要指定一下文件名,不然确实会出现 unknown 的问题。

那么问题又来了,怎么获取文件名呢?

可以通过 this.request 来获取当前的文件名,比如这个示例中的 this.request 是:

~/github/my-webpack-loader/build/babel-loader.js!~/github/my-webpack-loader/src/main.js

其中 ~ 是被我省略的绝对路径

可以看出 this.request 就是一个加载文件的请求,包含了两部分,通过 ! 分割,前一部分是 对应的 loader,后一部分是文件的路径。我们一行代码就可以把文件名提取出来:

this.request.split('!')[1].split('/').pop()

然后在 babel.transform 的配置中增加一行:

filename: this.request.split('!')[1].split('/').pop()

再刷新页面就可以看到正常的 sourcemap 了。

模块

我们现在已经支持 编译代码 和 sourcemap,下面我们要支持 modules,我们把 main.js 代码拆成两部分:

people.js

export default class People {
  constructor (name) {
    this.name = name
  }

  sayName () {
    console.log(`Hello there, I'm ${this.name}`)
  }
}

main.js

import People from './people'

const lily = new People('Lily')
lily.sayName()

那么我们需要怎么做处理呢? 对 JS 文件来说,我们最好的方式是不作任何特殊处理。上面的代码其实已经可以正常打包模块了。那么是怎么做到的呢?

因为 JS 在 webpack 中是一等公民,webpack 把所有的资源都当做 JS 来加载。webpack 默认支持常见的 AMD,CMD,ES6,nodejs 等常见的模块加载方式,并且会自动做 bundle(打包)和 tree-shaking。反而,babel 只是把 ES6 模块编译成了 nodejs 模块,它并不会做 bundle。

比如我们的 people.js,经过 babel-loader 编译出来的代码是这样的:

// 省略几个工具方法。。。
var People = function () {
  function People(name) {
    _classCallCheck(this, People);

    this.name = name;
  }

  _createClass(People, [{
    key: "sayName",
    value: function sayName() {
      console.log("Hello there, I'm " + this.name);
    }
  }]);

  return People;
}();

exports.default = People;

webpack 会识别 exports.default 语法,并且最终把两个文件合并一个大的文件。反而,如果我们在 babel-loader 中做了打包,会导致 webpack 无法做 tree-shaking 优化。

这也是 webpack 的各种 JS loader,如 vue-loader, jsx-loader 等的共同做法,即把 modules bundle 交给 webpack 处理,让 webpack 做最大程度的优化。

当然,这种做法仅仅对 JS 有效,因为 JS 是 webpack 的一等公民,webpack 内置了对 JS 模块的完整支持,而其他的文件,比如 css, html 等,我们都需要把他们转成 JS 然后交给 webpack。这也是为什么 webpack 中有 style-loader, url-loader 却没有一个 js-loader 的原因。

我们的 babel-loader 代码就已经写完了。其实总共就 10 行代码,最终完整代码如下所示:

var babel = require("babel-core")

module.exports = function (source, inputSourceMap) {
  var babelOptions = {
    presets: ['env'],
    inputSourceMap: inputSourceMap,
    filename: this.request.split('!')[1].split('/').pop(),
    sourceMaps: true
  }
  var result = babel.transform(source, babelOptions)
  this.callback(null, result.code, result.map)
}

那么我们去看一下官方的 babel-loader 是如何实现的。显然他的代码比我们的代码多很多,他写了那么多,其实主要是增加了 Cache,以及增加了对异常的处理。有兴趣的可以自己去研究一下。

关于 webpack 的模块加载机制,我们后续再详细解读。

如何编译 JSX

如果我们是使用 React 写的组件,那么同样可以通过 babel-loader 来编译。关于如何编译 JSX,babel 官网这里做了很详细的文档 transform-react-jsx

简单来说,就是 babel-core 本身虽然不支持 jsx,会报语法错误,但是我们可以通过加载一个插件就能支持 jsx 用法如下:

require("babel-core").transform("code", {
  plugins: ["transform-react-jsx"]
});

那么我们只要在 上面我们写的 bable-loader 的代码中加入一行 plugins: [“transform-react-jsx”] 就可以支持 jsx 的编译了,是不是很简单。

react 因为用的 JSX,而 jsx 因为全部编译成了 JS 所以它的 loader 很简单。但是 Vue 的组件并不是可以直接就全部编译成 JS,而是包括了 html,JS,CSS 三部分,所以 vue-loader 相对来说就复杂很多了。

「点点赞赏,手留余香」

11

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。
码云笔记 » webpack系列一之如何写一个自己的loader

发表回复