搭建一個通用的腳手架

jiangtao_發表於2018-02-02

原文地址: github.com/jiangtao/bl…,轉載請註明出處。

在16年年底的時候,同事聊起腳手架。由於公司業務的多樣性,前端的靈活性,讓我們不得不思考更通用的腳手架。而不是伴隨著前端技術的發展,不斷的把時間花在配置上。於是chef-cli誕生了。 18年年初,把過往一年的東西整理和總結下,重新增強了原有的腳手架project-next-cli, 不單單滿足我們團隊的需求,也可以滿足其他人的需求。

project-next-cli

面向的目標使用者:

  • 公司業務雜,但有一定的積累
  • 愛折騰的同學和團隊
  • 藉助github大量開發模板開發

image

發展

從本人做前端開始(13年),前端這幾年處於高速發展,主要表現:

備註:以下發展過程出現,請不要糾結出現順序 [捂臉]

  • 庫/框架:jQuery, backbone, angular,react,vue
  • 模組化:commonjs, AMD(CMD), UMD, es module
  • 工作管理員:npm scripts, grunt, gulp
  • 模組打包工具: r.js, webpack, rollup, browserify
  • css前處理器:Sass, Less, Stylus, Postcss
  • 靜態檢查器:flow/typescript
  • 測試工具:mocha,jasmine,jest,ava
  • 程式碼檢測工具:eslint,jslint

開發

當我們真實開發中,會遇到各種各樣的業務需求(場景),根據需求和場景選用不同的技術棧,由於技術的進步和不同瀏覽器執行時的限制,不得不配置對應的環境等,導致我們從而滿足業務需求。

畫了一張圖來表示,業務,配置(環境),技術之間的關係

image

前端配置工程師

於是明見流傳了一個新的職業,前端配置工程師 O(∩_∩)O~

社群現狀

專一的腳手架

社群中存在著大量的專一型框架,主要針對一個目標任務做定製。比如下列腳手架

  1. vue-cli

vue-cli提供利用vue開發webpack, pwa等模板,本文腳手架參考了vue-cli的實現。

  1. dva-cli

dva-cli主要針對dva開發使用的腳手架

  1. labrador

labrador是一種微信小程式元件化開發框架, 雖說小程式目前已經支援元件,但該腳手架的其他特性,也很贊。感興趣的可以瞭解。

社群中有很多優秀的專一型腳手架出現,這裡不在列舉。前端社群的火爆,讓我輩前端汲取精華,不斷前進。

通用腳手架

  1. yeoman

yeoman是一款強壯的且有一系列工具的通用型腳手架,但yeoman釋出指定package名稱,和用其開發工具。具體可點選這裡檢視yeoman新增生成器規則

開發初衷和目標

由於金融公司形態決定了,業務型別多樣,前端技術發展迭代,為了跟進社群發展,更好的完成下列目標而誕生。

  • 完成業務:專心,穩定,快速
  • 團隊規範:程式碼規範,釋出流程,持續整合/交付/部署
  • 沉澱:持續穩定的引入新技術
  • 效益:少加班,少造輪子,完成kpi,做更有意義的事兒

實現準備

依託於Github,根據Github API來實現,如下:

  1. 獲取專案
curl -i https://api.github.com/orgs/project-scaffold/repos
複製程式碼
  1. 獲取版本
curl -i https://api.github.com/repos/project-scaffold/cli/tags
複製程式碼

實現邏輯

根據github api獲取到專案列表和版本號之後,就可以做一個通用的腳手架. 以下程式碼是核心程式碼,便於理解。

總體設計

  1. 規範
  • 使用Node進行腳手架開發,版本選擇 >=6.0.0
  • 選用async/await開發,解決非同步回撥問題
  • 使用babel編譯
  • 使用ESLint規範程式碼
  1. 功能

遵守單一職責原則,每個檔案為一個單獨模組,解決獨立的問題。可以自由組合,從而實現複用。以下是最終的目錄結構:

├── LICENSE
├── README.md
├── bin
│   └── project
├── package.json
├── src
│   ├── clear.js
│   ├── config.js
│   ├── helper
│   │   ├── metalAsk.js
│   │   ├── metalsimth.js
│   │   └── render.js
│   ├── index.js
│   ├── init.js
│   ├── install.js
│   ├── list.js
│   ├── project.js
│   ├── search.js
│   ├── uninstall.js
│   ├── update.js
│   └── utils
│       ├── betterRequire.js
│       ├── check.js
│       ├── copy.js
│       ├── defs.js
│       ├── git.js
│       ├── loading.js
│       └── rc.js
└── yarn.lock
複製程式碼

下面我們說一下每個命令的實現邏輯。

下載

  1. 使用
project i
複製程式碼
  1. 邏輯
Github API ===> 獲取專案列表 ===> 選擇一個專案 ===> 獲取專案版本號 ===> 選擇一個版本號 ===> 下載到本地倉庫
複製程式碼

若中間每一步 資料為空/檔案不存在 則給予提示

  1. 核心程式碼

  // 獲取github專案列表
  const repos = await repoList();

  choices = repos.map(({ name }) => name);
  answers = await inquirer.prompt([
    {
      type   : 'list',
      name   : 'repo',
      message: 'which repo do you want to install?',
      choices
    }
  ]);
  // 選擇的專案
  const repo = answers.repo;

  // 專案的版本號劣幣愛哦
  const tags = await tagList(repo);

  if (tags.length === 0) {
    version = '';
  } else {
    choices = tags.map(({ name }) => name);

    answers = await inquirer.prompt([
      {
        type   : 'list',
        name   : 'version',
        message: 'which version do you want to install?',
        choices
      }
    ]);
    version = answers.version;
  }
  // 下載
  await download([repo, version].join('@'));
複製程式碼

生成專案

  1. 使用
project init
複製程式碼
  1. 邏輯
獲取本地倉庫列表 ===> 選擇一個本地專案 ===> 輸入基本資訊 ===> 編譯生成到臨時檔案 ===> 複製並重名到目標目錄
複製程式碼

若中間每一步 資料為空/檔案不存在/生成目錄已重複 則給予提示

  1. 核心程式碼

  // 獲取本地倉庫專案
  const list = await readdir(dirs.download);

  // 基本資訊
  const answers = await inquirer.prompt([
    {
      type   : 'list',
      name   : 'scaffold',
      message: 'which scaffold do you want to init?',
      choices: list
    }, {
      type   : 'input',
      name   : 'dir',
      message: 'project name',
      // 必要的驗證
      async validate(input) {
        const done = this.async();

        if (input.length === 0) {
          done('You must input project name');
          return;
        }

        const dir = resolve(process.cwd(), input);

        if (await exists(dir)) {
          done('The project name is already existed. Please change another name');
        }

        done(null, true);
      }
    }
  ]);
  const metalsmith = await rc('metalsmith');
  if (metalsmith) {
    const tmp = `${dirs.tmp}/${answers.scaffold}`;
    // 複製一份到臨時目錄,在臨時目錄編譯生成
    await copy(`${dirs.download}/${answers.scaffold}`, tmp);
    await metal(answers.scaffold);
    await copy(`${tmp}/${dirs.metalsmith}`, answers.dir);
    // 刪除臨時目錄
    await rmfr(tmp);
  } else {
    await copy(`${dirs.download}/${answers.scaffold}`, answers.dir);
  }
複製程式碼

其中模板引擎編譯實現核心程式碼如下:

// metalsmith邏輯
function metal(answers, tmpBuildDir) {
    return new Promise((resolve, reject) => {
    metalsmith
	  .metadata(answers)
      .source('./')
      .destination(tmpBuildDir)
      .clean(false)
      .use(render())
      .build((err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve(true);
      });
  });
}
// metalsmith render中介軟體實現
function render() {
    return function _render(files, metalsmith, next) {
    const meta = metalsmith.metadata();

    /* eslint-disable */
    
    Object.keys(files).forEach(function(file){
      const str = files[file].contents.toString();

      consolidate.swig.render(str, meta, (err, res) => {
        if (err) {
          return next(err);
        }

        files[file].contents = new Buffer(res);
        next();
      });
    })
    
  }
}

複製程式碼

升級/降級版本

  1. 使用
project update
複製程式碼
  1. 邏輯
獲取本地倉庫列表 ===> 選擇一個本地專案 ===> 獲取版本資訊列表 ===> 選擇一個版本 ===> 覆蓋原有的版本檔案
複製程式碼

若中間每一步 資料為空/檔案不存在 則給予提示

  1. 核心程式碼
  // 獲取本地倉庫列表
  const list = await readdir(dirs.download);

  // 選擇一個要升級的專案
  answers = await inquirer.prompt([
    {
      type   : 'list',
      name   : 'scaffold',
      message: 'which scaffold do you want to update?',
      choices: list,
      async validate(input) {
        const done = this.async();

        if (input.length === 0) {
          done('You must choice one scaffold to update the version. If not update, Ctrl+C');
          return;
        }

        done(null, true);
      }
    }
  ]);

  const repo = answers.scaffold;

  // 獲取該專案的版本資訊
  const tags = await tagList(repo);

  if (tags.length === 0) {
    version = '';
  } else {
    choices = tags.map(({ name }) => name);

    answers = await inquirer.prompt([
      {
        type   : 'list',
        name   : 'version',
        message: 'which version do you want to install?',
        choices
      }
    ]);
    version = answers.version;
  }
  // 下載覆蓋檔案
  await download([repo, version].join('@'))
複製程式碼

配置

配置用來獲取腳手架的基本設定, 如registry, type等基本資訊。

  1. 使用
project config set registry koajs # 設定本地倉庫下載源

project config get registry # 獲取本地倉庫設定的屬性

project config delete registry # 刪除本地設定的屬性
複製程式碼
  1. 邏輯
判定本地設定檔案存在 ===> 讀/寫
複製程式碼

若中間每一步 資料為空/檔案不存在 則給予提示

  1. 核心程式碼
switch (action) {
    case 'get':
      console.log(await rc(k));
      console.log('');
      return true;

    case 'set':
      await rc(k, v);
      return true;

    case 'remove':
      await rc(k, v, true);
      return true;

    default:
      console.log(await rc());
複製程式碼

搜尋

搜尋遠端的github倉庫有哪些專案列表

  1. 使用

project search

複製程式碼
  1. 邏輯
獲取github專案列表 ===> 輸入搜尋的內容 ===> 返回匹配的列表
複製程式碼

若中間每一步 資料為空 則給予提示

  1. 核心程式碼
 const answers = await inquirer.prompt([
    {
      type   : 'input',
      name   : 'search',
      message: 'search repo'
    }
  ]);

  if (answers.search) {
    let list = await searchList();

    list = list
      .filter(item => item.name.indexOf(answers.search) > -1)
      .map(({ name }) => name);

    console.log('');
	  if (list.length === 0) {
		  console.log(`${answers.search} is not found`);
	  }
	  console.log(list.join('\n'));
	  console.log('');
  }
複製程式碼

總結

以上是這款通用腳手架產生的背景,針對使用者以及具體實現,該腳手架目前還有一些可以優化的地方:

  1. 不同源,儲存不同的檔案
  2. 支援離線功能

硬廣:如果您覺得project-next-cli好用,歡迎star,也歡迎fork一塊維護。

相關文章