前端必修课:ES2017+下的构建工具原理与实战

栏目: Node.js · 发布时间: 5年前

内容简介:ES2017+,你不再需要纠结于复杂的构建工具技术选型。也不再需要gulp,grunt,yeoman,metalsmith,fis3。以上的这些构建工具,可以脑海中永远划掉。

ES2017+,你不再需要纠结于复杂的构建 工具 技术选型。

也不再需要gulp,grunt,yeoman,metalsmith,fis3。

以上的这些构建工具,可以脑海中永远划掉。

100行代码,你将透视构建工具的本质。

100行代码,你将拥有一个现代化、规范、测试驱动、高延展性的前端构建工具。

在阅读前,给大家一个小悬念:

  • 什么是链式操作、中间件机制?
  • 如何读取、构建文件树?
  • 如何实现批量模板渲染、代码转译?
  • 如何实现中间件间数据共享。

相信学完这一课后,你会发现————这些专业术语,背后的原理实在。。。太简单了吧!

构建工具体验:弹窗+uglify+模板引擎+babel转码...

如果想立即体验它的强大功能,可以命令行输入 npx mofast example ,将会构建一个 mofast-example 文件夹。

进入文件后运行 node compile ,即可体验功能。

顺便说一句, npx mofast example 命令行本身,也是用本课的构建工具实现的。——是不是不可思议?

本课程代码已在npm上进行发布,直接安装即可

npm i mofast -D 即可在任何项目中使用mofast,替代gulp/grunt/yeoman/metalsmith/fis3进行安装使用。

本课程github地址为: https://github.com/wanthering... 在学完课程后,你就可以提交PR,一起维护这个库,使它的扩展性越来越强!

第一步:搭建github/npm标准开发栈

请搭建好以下环境:

  • jest 测试环境
  • eslint 格式标准化环境
  • babel es2017代码环境

或者直接使用 npx lunz mofast

然后一路回车。

构建出的文件系统如下

├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── README.md
├── circle.yml
├── package.json
├── src
│   └── index.js
├── test
│   └── index.spec.js
└── yarn.lock

第二步: 搭建文件沙盒环境

构建工具,都需要进行文件系统的操作。

在测试时,常常污染本地的文件系统,造成一些重要文件的意外丢失和修改。

所以,我们往往会为测试做一个“沙盒环境”

在package.json同级目录下,输入命令

mkdir __mocks__ && touch __mocks__/fs.js
 
 yarn add memfs -D
 yarn add fs-extra

创建 __mocks__/fs.js 文件后,写入:

const { fs } = require('memfs')
module.exports = fs

然后在测试文件 index.spec.js 的第一行写下:

jest.mock('fs')
import fs from 'fs-extra'

解释一下: __mocks__中的文件将自动加载到测试的mock环境中,而通过jest.mock('fs'),将覆盖掉原来的fs操作,相当于整个测试都在沙盒环境中运行。

第三步:一个类的基础配置

src/index.js

import { EventEmitter } from 'events'

class Mofast extends EventEmitter {
  constructor () {
    super()
    this.files = {}
    this.meta = {}
  }

  source (patterns, { baseDir = '.', dotFiles = true } = {}) {
    // TODO: parse the source files
  }

  async dest (dest, { baseDir = '.', clean = false } = {}) {
    // TODO: conduct to dest
  }
}

const mofast = () => new Mofast()

export default mofast

使用EventEmitter作为父类,是因为需要emit事件,以监控文件流的动作。

使用this.files保存文件链。

使用this.meta 保存数据。

在里面写入了source方法,和dest方法。使用方法如下:

test/index.spec.js

import fs from 'fs-extra'
import mofast from '../src'
import path from "path"

jest.mock('fs')

// 准备原始模板文件
const templateDir = path.join(__dirname, 'fixture/templates')
fs.ensureDirSync(templateDir)
fs.writeFileSync(path.join(templateDir, 'add.js'), `const add = (a, b) => a + b`)


test('main', async ()=>{
  await mofast()
    .source('**', {baseDir: templateDir})
    .dest('./output', {baseDir: __dirname})

  const fileOutput = fs.readFileSync(path.resolve(__dirname, 'output/tmp.js'), 'utf-8')
  expect(fileOutput).toBe(`const add = (a, b) => a + b`)
})

现在,我们以跑通这个test为目标,完成Mofast类的初步编写。

第四步:类gulp,链式文件流操作实现。

source函数:

将参数中的patterns, baseDir, dotFiles挂载到this上,并返回this, 以便于链式操作即可。

dest函数:

dest函数,是一个异步函数。

它完成两个操作:

  1. 将源文件夹中所有文件读取出来,赋值给this.files对象上。
  2. 将this.files对象中的文件,写入到目标文件夹的位置。

可以这两个操作分别独立成两个异步函数:

process(),和writeFileTree()

process函数

  1. 使用fast-glob包,读取目标文件夹下的所有文件的状态stats,返回一个由文件的状态stats组成的数组
  2. 从stats.path中取得绝对路径,采用fs.readFile()读取绝对路径中的内容content。
  3. 将content, stats, path一起挂载到this.files上。

注意,因为是批量处理,需要采用Promise.all()同时执行。

假如 /fixture/template/add.js 文件的内容为 const add = (a, b) => a + b

处理后的this.file对象示意:

{
    'add.js': {
        content: 'const add = (a, b) => a + b',
        stats: {...},
        path: '/fixture/template/add.js'
    }
}

writeFileTree函数

遍历this.file,使用fs.ensureDir保证文件夹存在后, 将this.file[filename].content写入绝对路径。

import { EventEmitter } from 'events'
import glob from 'fast-glob'
import path from 'path'
import fs from 'fs-extra'

class Mofast extends EventEmitter {
  constructor () {
    super()
    this.files = {}
    this.meta = {}
  }

  /**
   * 将参数挂载到this上
   * @param patterns  glob匹配模式
   * @param baseDir   源文件根目录
   * @param dotFiles   是否识别隐藏文件
   * @returns this 返回this,以便链式操作
   */
  source (patterns, { baseDir = '.', dotFiles = true } = {}) {
    //
    this.sourcePatterns = patterns
    this.baseDir = baseDir
    this.dotFiles = dotFiles
    return this
  }

  /**
   * 将baseDir中的文件的内容、状态和绝对路径,挂载到this.files上
   */
  async process () {
    const allStats = await glob(this.sourcePatterns, {
      cwd: this.baseDir,
      dot: this.dotFiles,
      stats: true
    })

    this.files = {}
    await Promise.all(
      allStats.map(stats => {
        const absolutePath = path.resolve(this.baseDir, stats.path)
        return fs.readFile(absolutePath).then(contents => {
          this.files[stats.path] = { contents, stats, path: absolutePath }
        })
      })
    )
    return this
  }

  /**
   * 将this.files写入目标文件夹
   * @param destPath 目标路径
   */
  async writeFileTree(destPath){
    await Promise.all(
      Object.keys(this.files).map(filename => {
        const { contents } = this.files[filename]
        const target = path.join(destPath, filename)
        this.emit('write', filename, target)
        return fs.ensureDir(path.dirname(target))
          .then(() => fs.writeFile(target, contents))
      })
    )
  }

  /**
   *
   * @param dest   目标文件夹
   * @param baseDir  目标文件根目录
   * @param clean   是否清空目标文件夹
   */
  async dest (dest, { baseDir = '.', clean = false } = {}) {
    const destPath = path.resolve(baseDir, dest)
    await this.process()
    if(clean){
      await fs.remove(destPath)
    }
    await this.writeFileTree(destPath)
    return this
  }
}

const mofast = () => new Mofast()

export default mofast

执行 yarn test ,测试跑通。

第五步:中间件机制

如果说我们正在编写的类,是一把枪。

那么中间件,就是一颗颗子弹。

你需要一颗颗将子弹推入枪中,然后一次全部打出去。

写一个测试用例,将add.js文件中的 const add = (a, b) => a + b 修改为 var add = (a, b) => a + b

test/index.spec.js

test('middleware', async () => {
  const stream = mofast()
    .source('**', { baseDir: templateDir })
    .use(({ files }) => {
      const contents = files['add.js'].contents.toString()
      files['add.js'].contents = Buffer.from(contents.replace(`const`, `var`))
    })

  await stream.process()
  expect(stream.fileContents('add.js')).toMatch(`var add = (a, b) => a + b`)
})

好,现在来实现middleware

在constructor里面初始化constructor数组

src/index.js > constructor

constructor () {
    super()
    this.files = {}
    this.middlewares = []
  }

创建一个use函数,用来将中间件推入数组,就像一颗颗子弹推入弹夹。

src/index.js > constructor

use(middleware){
    this.middlewares.push(middleware)
    return this
  }

在process异步函数中,处理完文件之后,立即执行中间件。 注意,中间件的参数应该是this,这样就可以取到挂载在主类上面的 this.filesthis.baseDir 等参数了。

src/index.js > process

async process () {
    const allStats = await glob(this.sourcePatterns, {
      cwd: this.baseDir,
      dot: this.dotFiles,
      stats: true
    })

    this.files = {}
    await Promise.all(
      allStats.map(stats => {
        const absolutePath = path.resolve(this.baseDir, stats.path)
        return fs.readFile(absolutePath).then(contents => {
          this.files[stats.path] = { contents, stats, path: absolutePath }
        })
      })
    )


    for(let middleware of this.middlewares){
      await middleware(this)
    }
    return this
  }

最后,我们新写了一个方法fileContents,用于读取文件对象上面的内容,以便进行测试

fileContents(relativePath){
    return this.files[relativePath].contents.toString()
  }

执行一下 yarn test ,测试通过。

第六步: 模板引擎、babel转译

既然已经有了中间件机制.

我们可以封装一些常用的中间件,例如ejs / handlebars模板引擎

使用前的文件内容是:

my name is <%= name %>my name is {{ name }}

输入 {name: 'jack}

得出结果 my name is jack

以及babel转译:

使用前文件内容是:

const add = (a, b) => a + b

转译后得到 var add = function(a, b){ return a + b}

好, 我们来书写测试用例:

// 准备原始模板文件
fs.writeFileSync(path.join(templateDir, 'ejstmp.txt'), `my name is <%= name %>`)
fs.writeFileSync(path.join(templateDir, 'hbtmp.hbs'), `my name is {{name}}`)

test('ejs engine', async () => {
  await mofast()
    .source('**', { baseDir: templateDir })
    .engine('ejs', { name: 'jack' }, '*.txt')
    .dest('./output', { baseDir: __dirname })
  const fileOutput = fs.readFileSync(path.resolve(__dirname, 'output/ejstmp.txt'), 'utf-8')
  expect(fileOutput).toBe(`my name is jack`)
})

test('handlebars engine', async () => {
  await mofast()
    .source('**', { baseDir: templateDir })
    .engine('handlebars', { name: 'jack' }, '*.hbs')
    .dest('./output', { baseDir: __dirname })
  const fileOutput = fs.readFileSync(path.resolve(__dirname, 'output/hbtmp.hbs'), 'utf-8')
  expect(fileOutput).toBe(`my name is jack`)
})

test('babel', async () => {
  await mofast()
    .source('**', { baseDir: templateDir })
    .babel()
    .dest('./output', { baseDir: __dirname })
  const fileOutput = fs.readFileSync(path.resolve(__dirname, 'output/add.js'), 'utf-8')
  expect(fileOutput).toBe(`var add = function (a, b) { return a + b; }`)
})

engine() 有三个参数

  • type: 指定模板类型
  • locals: 提供输入的参数
  • patterns: 指定匹配格式

babel() 有一个参数

  • patterns: 指定匹配格式

engine() 实现原理:

通过 nodejsassert ,确保 typeejshandlebars 之一

通过 jstransformer + jstransformer-ejsjstransformer-handlebars

判断locals的类型,如果是函数,则传入执行上下文,使得可以访问files和meta等值。 如果是对象,则把meta值合并进去。

使用 minimatch ,匹配文件名是否符合给定的 pattern ,如果符合,则进行处理。 如果不输入 pattern ,则处理全部文件。

创立一个中间件,在中间件中遍历files,将单个文件的contents取出来进行处理后,更新到原来位置。

将中间件推入数组

babel()实现原理

通过 nodejsassert ,确保 typeejshandlebars 之一

通过 buble 包(简化版的bable),进行转换代码转换。

使用 minimatch ,匹配文件名是否符合给定的 pattern ,如果符合,则进行处理。 如果不输入 pattern ,则处理所有 jsjsx 文件。

创立一个中间件,在中间件中遍历files,将单个文件的contents取出来转化为es5代码后,更新到原来位置。

接下来,安装依赖

yarn add jstransformer jstransformer-ejs jstransformer-handlebars minimatch buble

并在头部进行引入

src/index.js

import assert from 'assert'
import transformer from 'jstransformer'
import minimatch from 'minimatch'
import {transform as babelTransform} from 'buble'

补充engine和bable方法

engine (type, locals, pattern) {
    const supportedEngines = ['handlebars', 'ejs']
    assert(typeof (type) === 'string' && supportedEngines.includes(type), `engine must be value of ${supportedEngines.join(',')}`)
    const Transform = transformer(require(`jstransformer-${type}`))
    const middleware = context => {
      const files = context.files

      let templateData
      if (typeof locals === 'function') {
        templateData = locals(context)
      } else if (typeof locals === 'object') {
        templateData = { ...locals, ...context.meta }
      }

      for (let filename in files) {
        if (pattern && !minimatch(filename, pattern)) continue
        const content = files[filename].contents.toString()
        files[filename].contents = Buffer.from(Transform.render(content, templateData).body)
      }
    }
    this.middlewares.push(middleware)
    return this
  }

  babel (pattern) {
    pattern = pattern || '*.js?(x)'
    const middleware = (context) => {
      const files = context.files
      for (let filename in files) {
        if (pattern && !minimatch(filename, pattern)) continue
        const content = files[filename].contents.toString()
        files[filename].contents = Buffer.from(babelTransform(content).code)
      }
    }
    this.middlewares.push(middleware)
    return this
  }

第七步: 过滤文件

书写测试用例

test/index.spec.js

test('filter', async () => {
  const stream = mofast()
  stream.source('**', { baseDir: templateDir })
    .filter(filepath => {
      return filepath !== 'hbtmp.hbs'
    })

  await stream.process()

  expect(stream.fileList).toContain('add.js')
  expect(stream.fileList).not.toContain('hbtmp.hbs')
})

新增了一个fileList方法,可以从this.files中获取到全部的文件名数组。

依然,通过注入中间件的方法,创建filter()方法。

src/index.js

filter (fn) {
    const middleware = ({files}) => {
      for (let filenames in files) {
        if (!fn(filenames, files[filenames])) {
          delete files[filenames]
        }
      }
    }
    this.middlewares.push(middleware)
    return this
  }

  get fileList () {
    return Object.keys(this.files).sort()
  }

跑一下 yarn test ,通过测试

第八步: 打包发布

这时,基本上一个小型构建工具的全部功能已经实现了。

这时输入 yarn lint 统一文件格式。

再输入 yarn build 打包文件,这时出现 dist/index.js 即是npm使用的文件

在package.json中增加main字段,指向 dist/index.js

增加files字段,指示npm包仅包含dist文件夹即可

"main": "dist/index.js",
  "files": ["dist"],

然后使用

npm publish

即可将包发布在npm上。

总结:

好了,回答最开始的问题:

什么是链式操作?

答: 返回this

什么是中间件机制

答:就是将一个个异步函数推入堆栈,最后遍历执行。

如何读取、构建文件树。

答:文件树,就是key为文件相对路径,value为文件内容等信息的对象this.files。

读取文件树,就是取得相对路径数组后,采用Promise.all批量fs.readFile取文件内容后挂载到this.files上去。

构建文件树,就是this.files采用Promise.all批量fs.writeFile到目标文件夹。

如何实现模板渲染、代码转译?

答:就是从文件树上取出文件,ejs.render()或bable.transform()之后放回原处。

如何实现中间件间数据共享?

答:contructor中创建this.meta={}即可。

其实,前端构建工具背后的原理,远比想像中更简单。


以上所述就是小编给大家介绍的《前端必修课:ES2017+下的构建工具原理与实战》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Practical JavaScript, DOM Scripting and Ajax Projects

Practical JavaScript, DOM Scripting and Ajax Projects

Frank Zammetti / Apress / April 16, 2007 / $44.99

http://www.amazon.com/exec/obidos/tg/detail/-/1590598164/ Book Description Practical JavaScript, DOM, and Ajax Projects is ideal for web developers already experienced in JavaScript who want to ......一起来看看 《Practical JavaScript, DOM Scripting and Ajax Projects》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具