Feflow(Front-end flow)是騰訊IVWEB團隊的前端工程化解決方案,致力於改善多類型項目的開發流程中的規範和非業務相關的問題,可讓開發者將絕大部分精力集中在業務開發上,從而提升研發效率。它能夠自動化地完成項目建立,開發,構建和規範檢查到最終項目上線,而且更加標準化。前端
本文主要如下面幾個角度進行分析node
feflow目錄結構以下:git
簡單分析一波,package.json中bin字段指向./bin/feflow,這個文件直接require("./lib/feflow")
, 那麼入口就在這個文件裏,在這個文件裏,主要作了這幾件事:github
feflow對象是由從core中導出的Feflow對象new出來的new Feflow(args)
,咱們再看core中的index.js文件,其中聲明瞭Feflow這個類,定義了包括init這些類方法,Feflow作了如下幾個事情:web
以上就是feflow提供的原子性內核操做,簡單來講就是初始化(包括激活日誌模塊,檢查運行環境,配置的生成),響應命令和加載插件這三個原子操做。 咱們來看看做者是怎麼基於內核的生態作相關拓展的,也就是看一下內部的插件中實現了哪些功能,內部插件internal目錄以下 shell
在feflow中,插件體如今拓展命令上,好比internal中的generator插件,在cmd中註冊init命令,以下,其中的上下文ctx,在Feflow類中以require('./generator')(this)
的形式將自生實例傳入,這樣就註冊了一個命令,調用這個命令須要執行的方法在第四個參數中傳入。npm
module.exports = function (ctx) {
const cmd = ctx.cmd;
cmd.register('lint', 'Lint you project use eslint-config-ivweb.', {}, require('./linter'));
};
複製代碼
feflow中內部插件就是這樣拓展,那外部插件,也就是用戶本身去下載的插件怎麼集成到feflow中呢,這個過程是這樣的,在Feflow.init方法中調用了loadPlugins模塊,這模塊負責把用戶插件目錄下的有效配置文件導出,再調用內核中的loadPlugin操做將之加載入,其關鍵是如何把內部的實例共享給外部的插件使用,內部細節在後面的插件機制中詳解。json
如上就是feflow的架構概要,包括內核提供的操做,init、call和loadPlugin,還有很是重要的內外部插件機制的簡單描述。固然不止這些,還有日誌模塊、更新模塊,咱們用後面的篇幅詳細分析一下這些重要的模塊是如何實現的。前端工程化
按照 feflow github上的使用方式,咱們能夠獲得這些有效命令 初始化項目promise
初始化 feflow init
cd <folder>
本地開發 feflow dev
代碼檢查 feflow lint
生產環境打包 feflow build
安裝 腳手架或插件 feflow install <package>
先從最基本的入手,看一下是如何讓系統響應feflow
這個自定義命令的。 咱們找到項目/
目錄下package.json
文件,在其中有這個內容
"bin": {
"feflow": "./bin/feflow"
}
複製代碼
這個bin目錄就是用來指定各個內部命令對應的可執行文件的位置,在這裏feflow對應的執行文件就是當前bin目錄下的feflow文件,在改目錄下運行feflow,npm就會去找對應的執行文件,若是不在當前目錄,想要在全局均可執行feflow命令呢,咱們須要在當前目錄下執行npm link
,該命令的做用是將bin字段對應的文件建立一個軟鏈將其添加進系統PATH,window下在C:\Users\Administrator\AppData\Roaming\npm
路徑下就能夠看見全部的全局軟鏈,好比說在個人目錄下找到了這兩個文件
他們的做用都是去調用對應的執行文件,可是爲何會有兩個呢?從文件內容和後綴名能夠看出一個是shell腳本,一個是cmd腳本,他們存在的意義是在不一樣的console環境去作相同的事,shell腳本能夠在git-bash、commder之類的console裏去使用,cmd腳本容許從window的CMD去使用全局命令。
到這裏node的自定義命令的實現方式也就說得差很少了,咱們回到feflow中容許咱們使用的參數,init
、dev
、lint
build
和install
,node接受參數的方法也很容易理解,其中最關鍵的是node中的process
對象,它提供了當前node進程的相關信息,咱們能夠從process.argv中拿到開啓當前進程命令行中的參數信息,第一個元素爲process.execPath,第二個元素爲當前執行的JavaScript文件路徑,剩餘的元素爲其餘命令行參數。
咱們來看看feflow中是怎麼作的
const args = minimist(process.argv.slice(2));
複製代碼
做者使用了minimist這個輕量級的命令行參數解析引擎,爲何用minimist不用其餘的呢,node.js的命令行參數解析工具備不少,好比:argparse、optimist、yars、commander。可是optimist和yargs內部使用的解析引擎正是minimist,它小巧精悍,簡單好用。這裏minimist將命令行參數解析成對象,以便後面的操做。
在feflow中會有一些問詢操做,做者選用的是inquirer這個庫,promise的操做風格更符合做者風格,選用inquirer也就不足爲奇了,固然還有一個緣由是後面做者使用的Yeoman的問詢操做promting底層也是用的inquirer。
在feflow中還有一個模塊涉及到命令行操做,core中的command文件,它提供了命令註冊和返回命令方法的功能,相比於EventEmitter
的實例,這裏的commander更加智能,若是咱們輸入一個錯誤的命令,好比誤輸入flo
可是正確的命令是flop
,command模塊依然能夠準確識別,並以flop
命令執行。
Feflow中的插件分爲內外兩類,外部插件容許開發者在npm上下載其餘feflow的插件搭配使用;內部插件則由做者維護開發,是集成在feflow中的,其都是在core提供的init方法中加載。可是插件處理方法和加載方式不一樣。
內部插件調用方法
require('../internal/build')(this);
複製代碼
以上插件就提供了dev
,build
命令,調用的過程爲加載一個模塊,該模塊每每是一個類,最早調用的構造函數,將Feflow的實例傳入,再以此調用這個模塊實例的靜態方法。
咱們以generator插件爲例,講解一下feflow如何生成一個可用的腳手架,Generator的使用過程如上所說,調用構造函數傳入實例,這裏調用的類方法爲init,在這裏面作的工做爲
這裏面有兩個關鍵點
增量更新做者這樣調用
self.execNpmCommand('install', needUpdatePlugins, false, baseDir)
複製代碼
這個方法將開發者對feflow的配置(npm包代理)和命令行參數(是否全局安卓)concat爲一個命令行字符串args,並傳入spawn,以下代碼:
const npm = spawn('npm', args, {cwd: where});
let output = '';
npm.stdout.on('data', (data) => {
output += data;
}).pipe(process.stdout);
npm.stderr.on('data', (data) => {
output += data;
}).pipe(process.stderr);
npm.on('close', (code) => {
if (!code) {
resolve({cod: 0, data: output});
} else {
reject({code: code, data: output});
}
});
複製代碼
spawn由cross-spawn
導出,cross-spawn具備原生spawn的功能和類似的調用方法,但又沒有原生spawn的各類問題,能夠理解爲無反作用的spawn。命令交給spawn子進程去執行,輸入一個流對象。增量更新的原理爲找到兩個版本的差分包,也就是補丁,文件校驗事後,將補丁安裝致本地文件便可。
腳手架做者底層使用的是yeoman,yeoman是一個通用的腳手架搭建工具,其優點在於能夠搭建任何語言的腳手架,而且Yeoman自己並不作任何配置,所有都由其內部的generator實現,再借助yeoman-environment
這個工具能夠容許開發者部署已經安裝好的generator,看做者是如何實現這個邏輯的
run(name) {
const ctx = this.ctx;
const pluginDir = ctx.pluginDir;
let path = pathFn.join(pluginDir, name, 'app/index.js');
if (!fs.existsSync(path)) {
path = pathFn.join(pluginDir, name, 'generators', 'app/index.js');
}
yeomanEnv.register(require.resolve(path), name);
yeomanEnv.run(name, this.args, err => {
});
}
複製代碼
這裏並無調用yeomanEnv.lookup這方法去尋找用戶所安裝的全部generator,由於比較坑的一點是lookup即使是尋找到安裝的generator後並不會把已安裝generator的列表返回,因此得去插件安裝目錄匹配開發者想要安裝的腳手架。幸運的是,yeomanEnv.run方法並不只僅依賴於yeomanEnv.lookup,只要是在yeomanEnv註冊過的generator均可以執行。
導入外部插件的一個關鍵點是如何共享Feflow實例, 這裏很巧妙地使用了node的vm(virtual machine)機制解決了這問題, 可直接使用feflow變量來訪問執行上下文,其內部就是使用vm來加載外部插件腳本,至關於模板引擎實現原理中的new Function或eval來解析並執行字符串代碼。
script = '(function(exports, require, module, __filename, __dirname, feflow){' +
script + '});';
const fn = vm.runInThisContext(script, path);
return fn(module.exports, require, module, path, pathFn.dirname(path), self);
複製代碼
把外部插件包裝成一個帶參函數傳入沙箱,編譯執行後返回該函數並傳入全局變量執行,便可完成對外部插件的加載,能夠說很是巧妙了。
feflow在執行命令前都會自檢一次是否能夠更新,當前版本不知足遠程庫feflow-cli兼容版本的要求時就會要求更新,而且是強制性的,判斷是否要更新是藉助於語義化版本控制規範(SemVer),須要更新時則調用execNpmCommand
方法更新。
semver.satisfies(version, compatibleVersion)
複製代碼