搭建一個通用的腳手架

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

在16年年末的時候,同事聊起腳手架。因爲公司業務的多樣性,前端的靈活性,讓咱們不得不思考更通用的腳手架。而不是伴隨着前端技術的發展,不斷的把時間花在配置上。因而chef-cli誕生了。 18年年初,把過往一年的東西整理和總結下,從新加強了原有的腳手架project-next-cli, 不僅僅知足咱們團隊的需求,也能夠知足其餘人的需求。前端

project-next-cli

面向的目標用戶:vue

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

image

發展

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

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

  • 庫/框架: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

開發

當咱們真實開發中,會遇到各類各樣的業務需求(場景),根據需求和場景選用不一樣的技術棧,因爲技術的進步和不一樣瀏覽器運行時的限制,不得不配置對應的環境等,致使咱們從而知足業務需求。git

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

image

前端配置工程師

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

社區現狀

專注的腳手架

社區中存在着大量的專注型框架,主要針對一個目標任務作定製。好比下列腳手架vue-cli

  1. vue-cli

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

  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一塊維護。

相關文章
相關標籤/搜索