Feflow 源碼解讀

Feflow 源碼解讀

Feflow(Front-end flow)是騰訊IVWEB團隊的前端工程化解決方案,致力於改善多類型項目的開發流程中的規範和非業務相關的問題,可讓開發者將絕大部分精力集中在業務開發上,從而提升研發效率。它能夠自動化地完成項目建立,開發,構建和規範檢查到最終項目上線,而且更加標準化。前端

本文主要如下面幾個角度進行分析node

  • 架構簡要解析
  • 命令行交互(CLI)
  • 插件機制
  • 更新能力

架構簡要解析

feflow目錄結構以下:git

入口文件

簡單分析一波,package.json中bin字段指向./bin/feflow,這個文件直接require("./lib/feflow"), 那麼入口就在這個文件裏,在這個文件裏,主要作了這幾件事:github

  • 接收參數
  • 判斷運行環境
  • 調用feflow.init()
  • 執行命令對應的操做

內核操做

feflow對象是由從core中導出的Feflow對象new出來的new Feflow(args),咱們再看core中的index.js文件,其中聲明瞭Feflow這個類,定義了包括init這些類方法,Feflow作了如下幾個事情:web

  • 初始化各類須要的路徑、日誌系統以及拿到相關的用戶和本地配置文件
  • 提供init方法,加載內部、外部插件,初始化feflow須要的環境,更新策略。
  • 提供call方法,調用參數對應的方法,這裏使用了參數混淆機制,支持模糊匹配參數。
  • 提供loadPlugin方法,註冊插件,巧妙運用了node的vm 沙箱機制。

以上就是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路徑下就能夠看見全部的全局軟鏈,好比說在個人目錄下找到了這兩個文件

  • cnpm
  • cnpm.cmd

他們的做用都是去調用對應的執行文件,可是爲何會有兩個呢?從文件內容和後綴名能夠看出一個是shell腳本,一個是cmd腳本,他們存在的意義是在不一樣的console環境去作相同的事,shell腳本能夠在git-bash、commder之類的console裏去使用,cmd腳本容許從window的CMD去使用全局命令。

到這裏node的自定義命令的實現方式也就說得差很少了,咱們回到feflow中容許咱們使用的參數,initdevlint buildinstall,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)
複製代碼

總結

一些思考

  • 對於一些工具類庫能夠考慮收編在一塊兒發佈一個feflow-util的npm包,這樣作的好處是當某些類庫發生一些重大的更變時咱們能夠只須要在其中進行修改,對於引用這個類庫包的文件就能夠不用修改,達到一個類庫解耦的目的。
  • feflow其實在致力於提升開發效率上已經作得很好了,多樣化的腳手架,規範開發風格,eslint等這些幾乎涵蓋開發的方方面面,可是有一點卻沒有涉及到,測試環境,這裏的測試環境只是針對後臺接口來講,咱們能夠將之拓展出mock模塊,生成對應的mock數據,和腳手架脫離,這個想法有幾個關鍵點須要實現
    • 根據什麼規則來生成mock數據
    • mock數據如何部署可用

亮點

  • feflow其本質是爲了提升開發效率,規範開發流程的解決方案,在這一點上確實是作到了,而且得益於優良的架構,其拓展性,和體系都爲完善,總體代碼可讀性很是高。
  • 源碼中回調和異步問題用promise解決,不能解決的就用bluebird加強一下promise解決,能夠說做者的一手promise用得爐火純青,很是值得學習。
  • 同時也運用了不少巧妙的設計,好比插件機制中的插件加載的方法,巧妙運用node中的沙箱注入插件依賴。
相關文章
相關標籤/搜索