内容简介: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函数,是一个异步函数。
它完成两个操作:
- 将源文件夹中所有文件读取出来,赋值给this.files对象上。
- 将this.files对象中的文件,写入到目标文件夹的位置。
可以这两个操作分别独立成两个异步函数:
process(),和writeFileTree()
process函数
- 使用fast-glob包,读取目标文件夹下的所有文件的状态stats,返回一个由文件的状态stats组成的数组
- 从stats.path中取得绝对路径,采用fs.readFile()读取绝对路径中的内容content。
- 将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上。
总结:
好了,回答最开始的问题:
什么是链式操作?
答: 返回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+下的构建工具原理与实战》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 算法面试必修课:动态规划基础题型归纳(一)
- PyFlink漫谈|PyFlink必修课!一小时吃透PyFlink
- 营销者的必修课:区分“人工智能”和“计算机视觉”
- 【Java必修课】判断String是否包含子串的四种方法及性能对比
- 前端科普系列(三):CommonJS 不是前端却革命了前端
- 前端科普系列(三):CommonJS 不是前端却革命了前端
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。