走进 San CLI(下):实现原理
作者:胡粵
上期我們討論了 San CLI 的使用,這期我們?cè)偕钊胍稽c(diǎn),來(lái)看看 San CLI 的實(shí)現(xiàn)原理。
核心模塊和核心概念
為了方便理解下文的 San CLI 的整體工作流程(主流程),我們先來(lái)看下 San CLI 的核心模塊和核心概念。
核心模塊
San CLI 的核心模塊包括:
- san-cli:負(fù)責(zé) San CLI 的主流程和實(shí)現(xiàn)核心功能;
- san-cli-service:Service 層,負(fù)責(zé) Service 流程;
- san-cli-command-init:實(shí)現(xiàn) san init 命令的 Command 插件;
- san-cli-plugin-*:Service 插件;
- san-cli-utils:工具庫(kù),在插件中也可以直接使用;
- san-cli-webpack:webpack build 和 dev-server 的通用邏輯,webpack 自研插件等。
核心概念
核心概念主要有流程和插件,其中,流程又分為主流程和 Service 流程,插件又分為 Command 插件 和 Service 插件:
- 流程:San CLI 的流程分為兩段,主流程和 Service 流程:
- 主流程: san-cli/index.js 的流程,是整個(gè) San CLI 的工作流程。當(dāng)我們?cè)诿钚休斎?San CLI 相關(guān)的命令后,比如 san init、 san serve(對(duì)應(yīng) npm start) 和 san build(對(duì)應(yīng) npm run build),就會(huì)進(jìn)入主流程,主流程會(huì)執(zhí)行對(duì)應(yīng)命令的 handler。有的命令的 handler 不直接包含處理邏輯,而是引入 san-cli-service,比如 san serve 和 san build,輸入這類命令時(shí),就會(huì)從主流程進(jìn)入 Service 流程。
- Service 流程: san-cli-service/Service.js 的流程,主要處理 Webpack 構(gòu)建相關(guān)的邏輯。
- 插件:用于擴(kuò)展 San CLI 的功能:
- Command 插件:用于主流程,執(zhí)行 Command 插件定義的命令后主流程就會(huì)執(zhí)行對(duì)應(yīng)的 Command 插件的 hanlder;
- Service 插件:用于 Service 流程,處理 Webpack 構(gòu)建相關(guān)的邏輯。
整體工作流程
San CLI 的整體工作流程,即主流程,在 san-cli/index.js 中,大致如下:
a.添加全局 option;
b.添加中間件:
i.設(shè)置全局 logLevel;
ii.設(shè)置 NODE_ENV 環(huán)境變量;
iii.給 argv 添加日志等屬性和方法。
sanrc.json 是配置 San CLI 的文件,和 san.config.js 不同,后者是配置項(xiàng)目的文件,詳見 San CLI
官方文檔。
結(jié)合核心模塊的主流程如下圖所示:
san init
san init 命令用于初始化項(xiàng)目,用法在上期已有所介紹,這期我們來(lái)看該命令是怎么做到初始化一個(gè)項(xiàng)目的。
流程
san init 初始化項(xiàng)目的原理,簡(jiǎn)單來(lái)說,就是通過 git 命令遠(yuǎn)程拉取項(xiàng)目腳手架模板的代碼庫(kù)到本地,或者直接使用本地的項(xiàng)目腳手架模板的代碼庫(kù),然后使用 vinyl-fs 將拉取到的代碼庫(kù)的文件依次處理,處理完成就得到了一個(gè)初始化好的項(xiàng)目。
vinyl-fs 是 gulp 的核心。
san init 主要由四步串行任務(wù)組成:
對(duì)應(yīng)的流程圖如下圖所示:
其中,檢查目錄和離線包狀態(tài)的流程圖如下:
設(shè)計(jì)思路
san init 的具體實(shí)現(xiàn)在 san-cli-command-init 模塊中, san-cli-command-init 模塊是一個(gè) Command 插件,其核心是一個(gè) TaskList 類, san init 的執(zhí)行過程的本質(zhì)就是:傳入上述 4 個(gè)任務(wù)組成的數(shù)組來(lái)實(shí)例化 TaskList 并調(diào)用實(shí)例的 run 方法。
我們來(lái)看下 TaskList 的內(nèi)部實(shí)現(xiàn)。
當(dāng)調(diào)用 TaskList 實(shí)例的 run 方法時(shí),首先會(huì)對(duì)執(zhí)行實(shí)例化 TaskList 時(shí)傳入的任務(wù)列表的第一個(gè)任務(wù)進(jìn)行處理。
因?yàn)檫@些任務(wù)本質(zhì)上都是一個(gè)個(gè)函數(shù),所以先給第一個(gè)任務(wù)加上一些方法,比如表示這個(gè)任務(wù)已完成的 complete 方法,加完方法后,就調(diào)用第一個(gè)任務(wù)函數(shù)。
調(diào)用第一個(gè)任務(wù)函數(shù),就意味著第一個(gè)任務(wù)開始執(zhí)行了,當(dāng)?shù)谝粋€(gè)任務(wù)該做的事情都做完后,最后就會(huì)在這個(gè)任務(wù)函數(shù)中調(diào)用之前給這個(gè)任務(wù)函數(shù)加上的 complete 方法。
complete 方法就做一件事情,讓 TaskList 實(shí)例開始處理下一個(gè)任務(wù),處理方式和上面說的一樣,只是簡(jiǎn)單地重復(fù)。
最后, TaskList 實(shí)例發(fā)現(xiàn)沒有下一個(gè)任務(wù)了,就收工了。
TaskList 源碼簡(jiǎn)化版
我們用簡(jiǎn)化版的源碼來(lái)看下 TaskList 的使用和實(shí)現(xiàn)。
TaskList 的使用:
// 任務(wù)列表,4 個(gè)任務(wù)函數(shù)分別對(duì)應(yīng) san init 的 4 個(gè)串行任務(wù) const taskList = [checkStatus, download, generator, installDep]; // 傳入任務(wù)列表來(lái)實(shí)例化 TaskList const tasks = new TaskList(taskList); // 按任務(wù)列表的順序依次執(zhí)行任務(wù) tasks.run();以 checkStatus 為例,看下任務(wù)函數(shù)的實(shí)現(xiàn):
function checkStatus(task) {// 檢查目錄和離線包狀態(tài)// ……// 檢查完了,告訴 tasks 這個(gè)任務(wù)完成了task.complete(); }TaskList 的實(shí)現(xiàn):
class TaskList {constructor(taskList) {this._taskList = taskList;// 當(dāng)前任務(wù)的索引this._index = 0;}run() {const currentTask = this._taskList[this._index];// 給當(dāng)前任務(wù)的函數(shù)加上 complete 方法currentTask.complete = () => {// 當(dāng)前任務(wù)完成,執(zhí)行下一個(gè)任務(wù)this.next();};// 真正開始執(zhí)行當(dāng)前任務(wù)// 把任務(wù)函數(shù)作為入?yún)⑹怯糜谠谌蝿?wù)函數(shù)里調(diào)用 complete 方法,具體可見上面的 checkStatus 任務(wù)函數(shù)的實(shí)現(xiàn)currentTask(currentTask);}next() {this._index++;if (this._index >= this._taskList.length) {// 所有任務(wù)都完成了,收工return;} else {// 還有任務(wù)沒完成,繼續(xù)執(zhí)行下一個(gè)任務(wù)this.run();}} }我們可以看出, san init 的設(shè)計(jì)模式還是比較優(yōu)秀的,舉個(gè)栗子,如果我們想給 san init 添加多一個(gè)任務(wù),那只需要關(guān)注該任務(wù)本身的實(shí)現(xiàn),實(shí)現(xiàn)好后放入實(shí)例化 TaskList 時(shí)傳入的任務(wù)列表中,就可以了,可擴(kuò)展性非常好。
插件機(jī)制
San CLI 的插件分為 Command 插件和 Service 插件,在上期我們以實(shí)際例子討論了具體怎么開發(fā)一個(gè) Command 插件或 Service 插件了,這期我們就來(lái)看看 San CLI 的插件機(jī)制。
Command 插件
Command 插件相對(duì) Service 插件來(lái)說,機(jī)制比較簡(jiǎn)單。
Command 插件實(shí)際是 yargs 的插件系統(tǒng)的擴(kuò)展,yargs 是一個(gè) npm 包,用它我們可以定義我們自己的命令行命令。
回顧下我們?cè)谏掀趧?chuàng)建的 Command 插件:
// san-command-hello.js exports.command = 'hello'; exports.builder = {name: {type: 'string'} }; exports.desc = '熱情地向給定對(duì)象打招呼'; exports.handler = argv => {console.log(`${argv.name},你好呀!`); };之所以要這么寫,是因?yàn)檫@是 yargs 對(duì)定義一個(gè)命令的要求。定義好命令后,就在項(xiàng)目的 package.json 里聲明這個(gè)命令。
當(dāng)我們執(zhí)行任何一個(gè) san 的命令時(shí) —— 注意,是任何一個(gè) —— 在真正執(zhí)行這個(gè)命令之前,San CLI 會(huì)先去讀取 package.json 里聲明的命令,然后找到命令的定義并傳入 yargs,此時(shí),yargs 就知道了都有些什么命令,在此之后,San CLI 才把我們執(zhí)行的命令的名字和參數(shù)傳如 yargs,yargs 拿到命令的名字和參數(shù)后,就回去執(zhí)行對(duì)應(yīng)命令的 hanlder。
Command 插件的機(jī)制就是這樣。
另外還值得注意的是,在 san-cli/lib/commander.js 里定義了一個(gè)名為 Command 的類,這個(gè)類對(duì) yargs 插件做了一些定制,比如通過中間件機(jī)制添加了常用的方法和屬性到 argv 對(duì)象中,方便下游 handler 直接使用。
Service 插件
San CLI 的 Service 插件機(jī)制借鑒了 Vue CLI 的 Service 插件機(jī)制,但有一些不同之處:
Vue CLI 注冊(cè)一個(gè)新命令是通過 Service 插件來(lái)完成的,具體是使用 Service 插件的
registerCommandAPI 方法實(shí)現(xiàn);而 San CLI 把注冊(cè)一個(gè)新命令的邏輯從 Service
插件里分離了出來(lái),成為了一個(gè)獨(dú)立的部分,也就是前面介紹過的 Command 插件。
Vue CLI 的一個(gè)命令對(duì)應(yīng)一個(gè)或多個(gè) Service 插件,也就是說,一個(gè)命令的實(shí)現(xiàn)由一個(gè)或多個(gè) Service 插件來(lái)完成;而
San CLI 的一個(gè)命令對(duì)應(yīng)零個(gè)或所有 Service 插件(引入的),一個(gè)命令對(duì)應(yīng)零個(gè)的情況是這個(gè)命令是一個(gè)純的 Command
插件,一個(gè)命令對(duì)應(yīng)所有 Service 插件的情況是這個(gè)命令在它對(duì)應(yīng)的 Command 插件的邏輯里觸發(fā)了 Service 流程,而
Service 流程會(huì)依次注冊(cè)并執(zhí)行所有的 Service 插件。
下面我們會(huì)以 san serve 命令為例,分別看下 Service 流程、Service 插件的設(shè)計(jì)思路和 Service 類的簡(jiǎn)化版源碼
Service 流程
Service 流程,即 Service 的整個(gè)工作流程:
handler;
a.loadEnv:加載 env 文件;
b.loadProjectOptions:加載 san.config.js;
c.init:service 啟動(dòng):
i.初始化插件,即依次執(zhí)行插件;
ii.依次執(zhí)行 webpackChain 回調(diào)棧;
iii.依次執(zhí)行 webpackConfig 回調(diào)棧;
對(duì)應(yīng)的流程圖如下:
我們自定義的 Service 插件的具體執(zhí)行時(shí)機(jī)是在 3-1-1 “初始化插件,即依次執(zhí)行插件” 這一步,對(duì)應(yīng)上圖中的 “初始化插件(插件.apply)” 這一步。
上圖中的 “初始化 plugin 變量并加載傳入的 plugin” 這一步和 “加載 config 中 plugins 里設(shè)置的插件” 這一步,其實(shí)都是在加載 Service 插件,只不過前者是在加載內(nèi)置插件和 sanrc.json 里預(yù)設(shè)的插件,而后者主要是在加載 san.config.json 里的插件。
加載 Service 插件的流程圖如下:
設(shè)計(jì)思路
輸入 san serve 命令,觸發(fā)對(duì)應(yīng)的 handler。 handler 主要就做兩件事情:一是實(shí)例化 Service,二是調(diào)用 Service 實(shí)例的 run 方法。
實(shí)例化 Service 時(shí),會(huì)加載內(nèi)置 Service 插件和 sanrc.json 里預(yù)設(shè)的 Service 插件。如果我們自定義的 Service 插件預(yù)設(shè)在了 sanrc.json 里,比如上期的 san-cli-plugin-get-entry,這個(gè)時(shí)候就會(huì)被加載了。
實(shí)例化完 Service,就調(diào)用 Service 實(shí)例的 run 方法。
調(diào)用 run 方法時(shí),首先會(huì)加載 san.config.js 里的 Service 插件,當(dāng)然也包括我們放在 san.config.js 里的自定義 Service 插件;然后,該加載的 Service 插件都加載完了,這時(shí)就準(zhǔn)備依次執(zhí)行它們了。
在執(zhí)行每個(gè) Service 插件之前,會(huì)先實(shí)例化 PluginAPI。 PluginAPI 實(shí)例給 Service 插件提供了用于處理 Webpack 構(gòu)建相關(guān)邏輯的方法,比如 configWebpack,通過這個(gè)方法我們可以在 Service 插件里獲取和修改 Webpack 配置,比如在上期我們寫的 Service 插件示例里,就用這個(gè)方法獲取了網(wǎng)站的入口文件名。
最后,把 PluginAPI 實(shí)例作為入?yún)?lái)調(diào)用 Service 插件定義的 apply 函數(shù),就正式開始了 Service 插件的執(zhí)行。
Service 源碼簡(jiǎn)化版
Service 的使用:
Service 的實(shí)現(xiàn):
class Service {constructor(plugins) {// 加載內(nèi)置 Service 插件和 `sanrc.json` 里預(yù)設(shè)的 Service 插件// 這是第一批 Service 插件this.plugins = this.loadPlugin(plugins);}run() {// 獲取 san.config.js 里的配置const projectOptions = this.loadProjectOptions();// 加載 san.config.js 里配置的 Service 插件// 這是第二批 Service 插件const morePlugins = this.loadPlugin(projectOptions.plugins);// 合并兩批 Service 插件this.plugins = [...this.plugins, ...morePlugins];// 依次處理 Service 插件this.plugins.forEach(plugin => {// 實(shí)例化 PluginAPI,得到用于處理 Webpack 構(gòu)建相關(guān)邏輯的方法const pluginApi = new PluginAPI();// 把用于處理 Webpack 構(gòu)建相關(guān)邏輯的方法傳入 Service 插件定義的 apply 函數(shù),正式開始了Service 插件的執(zhí)行plugin.apply(pluginApi);});}loadPlugin() {// 主要是統(tǒng)一各路 Service 插件的形式,方便后續(xù)調(diào)用// 具體做了些什么可見前面的加載 Service 插件的流程圖// ……}loadProjectOptions() {// 實(shí)現(xiàn)獲取 san.config.js 里的配置// ……} }PluginAPI 的實(shí)現(xiàn):
class PluginAPI {configWebpack(fn) {// 實(shí)現(xiàn) configWebpack// ……} }最后
感謝你閱讀到了這里,以上便是《走進(jìn) San CLI(下):實(shí)現(xiàn)原理》的全部?jī)?nèi)容。
如果你都看懂了,請(qǐng)收下我的膝蓋:
作為 San 生態(tài)系列文章的第二彈的《走進(jìn) San CLI》,也告一段落了,敬請(qǐng)期待下周同樣精彩的 San 生態(tài)系列文章之 San CLI UI !
新的一年San-CLI還會(huì)有持續(xù)的開發(fā)和優(yōu)化,比如eject功能、CLI和Service要不要分離,想了解后續(xù)的更新,可以關(guān)注San CLI 的 GitHub,歡迎 star、歡迎 issues、歡迎 pr。地址:https://github.com/ecomfe/san-cli
作者:胡粵
總結(jié)
以上是生活随笔為你收集整理的走进 San CLI(下):实现原理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百度牵头,全球首个面向商业化运营的Rob
- 下一篇: 《2020年AI新基建发展白皮书》重磅发