如何开发一个最简易babel

185 阅读4分钟

我正在参加「掘金·启航计划」

目标

babel原名是6to5,主要用于转译等功能,还有很多这类3W问题就不赘述了,大家应该都清楚了。我们直奔主题,如何开发一个最简易的babel,主要是了解如何开发,而不着重于开发的babel本身的功能是否强大。

我们以雪碧图来举例。

拆解

目标清楚了,开始拆解任务:

1. 模块

2. 调试

3. 雪碧图

4. 封装

积累

针对拆分的任务,逐个击破。

模块

要想开发loader,我们首先得知道一个没有任何功能的loader长什么样。其实,loader是一个导出为函数的JavaScript模块:

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

或许我们平时没亲自开发过loader,从webpack配置文件中,也会经常看到那种多loader配置项,它们是串行执行,并且是从后开始执行。前一个loader会依赖于后一个loader的返回结果。

module.exports = {
  module: {
    rules: [
      {
        test: /.s[ac]ss$/i,
        use: [
          'style-loader', // 将 JS 字符串生成为 style 节点
          'css-loader', // 将 CSS 转化成 CommonJS 模块
          'sass-loader' // 将 Sass 编译成 CSS
        ],
      },
    ],
  },
}

看如上配置,css-loader需要sass-loader处理返回后的CSS样式,然后生成CommonJS模块传递给style-loader生成style节点。

调试

loader是一个导出为函数的JavaScript模块,这个清楚了,那要想实现一个最简易babel,一是需要在里面加功能,如生成雪碧图;二是如何验证代码呢,也就是如何调试?

你最先想到的可能是webpack,用webpack进行调试,当然可以,但不纯粹,因为其中包含了很多不需要的功能代码,也就是可能会产生副作用。推荐经常用的loader-runner来调试。

loader-runner

loader-runner允许你在不安装webpack的情况下运行开发调试loader。其实在webpack内部也是依赖于loader-runner来执行loader的。

接收两个参数,第一个是一个对象,配置入口文件和开发的loader;第二个参数是回调函数,返回执行后的结果。用法如下:

const { runLoaders } = require('loader-runner')
const path = require('path')
const fs = require('fs')
runLoaders({
  resource: './src/sprite.css',
  loaders: [path.resolve(__dirname, './loaders/sprite-loader.js')],
  readResource: fs.readFile.bind(fs)
}, (err, result) => {
  err ? console.error(err) : console.log(result)
})
上下文

loader上下文表示在开发的loader内可以通过this来访问的一些参数或方法。例如:获取loader使用时的传参,可以使用this.query

loader-runner.js

const { runLoaders } = require('loader-runner')
const path = require('path')
const fs = require('fs')
runLoaders({
  resource: './src/raw.text',
  loaders: [
    {
      loader: path.resolve(__dirname, './loaders/raw-loader.js'),
      options: {
        name: 'test' // 传入参数
      }
    }
  ],
  readResource: fs.readFile.bind(fs)
}, (err, result) => {
  err ? console.error(err) : console.log(result)
})

raw-loader.js

module.exports = function (source) {
  console.log('name:' + this.query.name) // name:test
  return source
}

在webpack早期的版本中,获取参数一般用loader-utilsgetOptions方法获取,如下:

const loaderUtils = require('loader-utils')
module.exports = function (source) {
  const { name } = loaderUtils.getOptions(this)
  console.log(name)
  return source
}

只是自从babel上下文支持更简单的query查询后,自从3+版本后,loader-utils已经移除了getOptions方法。

异常处理

异常处理是开发必不可少的。babel常用有两种方式:

  • throw
throw new Error('error...')
  • callback
this.callback(new Error('error...')) // 类似于throw抛错方式
// 不同之处有两点:1、可以是非异常返回null;2、可返回多个参数
this.callback(null, params1, params2...)
异步

上面介绍了两种异常处理方式,都是同步的。异步处理方式我们也是用的babel上下文提供的方法async()

this.callback = this.async() // 告诉loader将会异步的回调,返回this.callback

雪碧图

合成雪碧图的插件有,我们就不重复造轮子了。最常用的是spritesmith插件。

const matchedImgs = ['a.png', 'b.png']
spritesmith.run({src: matchedImgs}, (err, result) => {
    // 处理过程...

    // 返回处理后的资源
    callback(null, source)
  })

spritesmith插件接收两个参数,第一个是待合成图片数组,第二个参数是回调函数,返回合成后的结果。可以先测试单独组件的有效性。

实现

经过以上三个步骤的准备,现在我们完全可以来实现一个最简易雪碧图babel了。

sprite-loader.js

const spritesmith = require('spritesmith')
const fs = require('fs')
const path = require('path')
module.exports = function (source) {
  // 异步
  const callback = this.async()
  // 匹配需要合并的图片
  const imgs = source.match(/url\(\.\/imgs\/\w*\.jpg/g)
  const matchedImgs = []
  for (let i = 0; i < imgs.length; i++) {
    const img = imgs[i].match(/\/imgs\/sprite\w+\.jpg/g)[0]
    matchedImgs.push(path.join(__dirname, '../src', img))
  }
  // 调用插件
  spritesmith.run({src: matchedImgs}, (err, result) => {
    // 合成新的图片
    fs.writeFileSync(path.join(process.cwd(), 'dist/sprite.jpg'), result.image)
    // 替换新生成的图片地址,并生成新的样式文件
    source = source.replace(/url\(\.\/imgs\/\w*\.jpg\)/g, () => {
      return 'url("dist/sprite.jpg")'
    })
    fs.writeFileSync(path.join(process.cwd(), 'dist/index.css'), source)
    // 返回处理后的资源
    callback(null, source)
  })
}