zoukankan      html  css  js  c++  java
  • 用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.files、this.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() 实现原理:

    通过nodejs的assert,确保type为ejs和handlebars之一

    通过jstransformer+jstransformer-ejs和jstransformer-handlebars

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

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

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

    将中间件推入数组

    babel()实现原理

    通过nodejs的assert,确保type为ejs和handlebars之一

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

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

    创立一个中间件,在中间件中遍历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上。

    资源搜索网站大全 https://www.renrenfan.com.cn 广州VI设计公司https://www.houdianzi.com

    总结:

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

    什么是链式操作?

    答: 返回this

    什么是中间件机制

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

    如何读取、构建文件树。

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

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

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

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

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

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

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

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

  • 相关阅读:
    Halcon各个算子角度计算方式汇总
    阈值分割算子之OSTU算法
    Halcon18--深度学习应用问题记录
    Halcon18新技术
    Halcon之3D重建
    关于数组排序(顺序,逆序)
    封装jq的ajax
    h5在微信生成分享海报(带二维码)
    使用mintUI的总结
    时间戳转日期
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/14111079.html
Copyright © 2011-2022 走看看