用100行代码,完成本身的前端构建东西!

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={}即可。

实在,前端构建东西背地的道理,远比想像中更简朴。

    原文作者:刘羽冲
    原文地址: https://segmentfault.com/a/1190000019274156
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞