摘要:行代碼,你將擁有一個現代化規范測試驅動高延展性的前端構建工具。在閱讀前,給大家一個小懸念什么是鏈式操作中間件機制如何讀取構建文件樹如何實現批量模板渲染代碼轉譯如何實現中間件間數據共享。函數將參數中的掛載到上,并返回以便于鏈式操作即可。
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()
使用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={}即可。
其實,前端構建工具背后的原理,遠比想像中更簡單。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/109768.html
摘要:從到再到搭建編寫構建一個前端項目選擇現成的項目模板還是自己搭建項目骨架搭建一個前端項目的方式有兩種選擇現成的項目模板自己搭建項目骨架。使用版本控制系統管理源代碼項目搭建好后,需要一個版本控制系統來管理源代碼。 從 0 到 1 再到 100, 搭建、編寫、構建一個前端項目 1. 選擇現成的項目模板還是自己搭建項目骨架 搭建一個前端項目的方式有兩種:選擇現成的項目模板、自己搭建項目骨架。 ...
摘要:從到再到搭建編寫構建一個前端項目選擇現成的項目模板還是自己搭建項目骨架搭建一個前端項目的方式有兩種選擇現成的項目模板自己搭建項目骨架。使用版本控制系統管理源代碼項目搭建好后,需要一個版本控制系統來管理源代碼。 從 0 到 1 再到 100, 搭建、編寫、構建一個前端項目 1. 選擇現成的項目模板還是自己搭建項目骨架 搭建一個前端項目的方式有兩種:選擇現成的項目模板、自己搭建項目骨架。 ...
摘要:從到再到搭建編寫構建一個前端項目選擇現成的項目模板還是自己搭建項目骨架搭建一個前端項目的方式有兩種選擇現成的項目模板自己搭建項目骨架。使用版本控制系統管理源代碼項目搭建好后,需要一個版本控制系統來管理源代碼。 從 0 到 1 再到 100, 搭建、編寫、構建一個前端項目 1. 選擇現成的項目模板還是自己搭建項目骨架 搭建一個前端項目的方式有兩種:選擇現成的項目模板、自己搭建項目骨架。 ...
摘要:流式構建改變了底層的流程控制,大大提高了構建工作的效率和性能,給用戶的直觀感覺就是更快。我的看法關于流式構建,短短幾句話無法講清它的來龍去脈,但是在的世界里,確實是至關重要的。 Grunt 一直是前端領域構建工具(任務運行器或許更準確一些,因為前端構建只是此類工具的一部分用途)的王者,然而它也不是毫無缺陷的,近期風頭正勁的 gulp.js 隱隱有取而代之的態勢。那么,究竟是什么使得 g...
閱讀 1307·2019-08-30 15:44
閱讀 1978·2019-08-30 13:49
閱讀 1651·2019-08-26 13:54
閱讀 3484·2019-08-26 10:20
閱讀 3238·2019-08-23 17:18
閱讀 3293·2019-08-23 17:05
閱讀 2129·2019-08-23 15:38
閱讀 1011·2019-08-23 14:35