在16年年末的時候,同事聊起腳手架。因爲公司業務的多樣性
,前端的靈活性
,讓咱們不得不思考更通用的腳手架。而不是伴隨着前端技術的發展,不斷的把時間花在配置
上。因而chef-cli誕生了。 18年年初,把過往一年的東西整理和總結下,從新加強了原有的腳手架project-next-cli, 不僅僅知足咱們團隊的需求,也能夠知足其餘人的需求。javascript
<!--more-->css
面向的目標用戶:前端
前端這幾年(13年-15年)處於高速發展,主要表現:vue
備註:如下發展過程出現,請不要糾結出現順序 [捂臉]java
當咱們真實開發中,會遇到各類各樣的業務需求(場景),根據需求和場景選用不一樣的技術棧,因爲技術的進步和不一樣瀏覽器運行時的限制,不得不配置對應的環境等,致使咱們從而知足業務需求。node
畫了一張圖來表示,業務,配置(環境),技術之間的關係react
因而明見流傳了一個新的職業,前端配置工程師 O(∩_∩)O~webpack
社區中存在着大量的專注型框架,主要針對一個目標任務作定製。好比下列腳手架git
vue-cli
提供利用vue開發webpack
, 以及 遠程克隆生成文件等 pwa
等模板,本文腳手架參考了vue-cli
的實現。es6
dva-cli
針對dva開發使用的腳手架
think-cli
針對 thinkjs項目建立項目
yeoman
是一款強壯的且有一系列工具的通用型腳手架,但yeoman發佈指定package名稱,和用其開發工具。具體可點擊這裏查看yeoman添加生成器規則
因爲公司形態決定了,業務類型多樣,前端技術發展迭代,爲了跟進社區發展,更好的完成下列目標而誕生。
依託於Github,根據Github API
來實現,以下:
curl -i https://api.github.com/orgs/project-scaffold/repos
curl -i https://api.github.com/repos/project-scaffold/cli/tags
根據github api
獲取到項目列表和版本號以後,根據輸入的名稱,選擇對應的版本下載到本地私有倉庫
,生成到執行目錄下。核心流程圖以下:。
>=6.0.0
遵照單一職責原則
,每一個文件爲一個單獨模塊,解決獨立的問題。能夠自由組合,從而實現複用。如下是最終的目錄結構:
├── 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
{ "presets": [ ["env", { "targets": { "node": "6.0.0" } }] ] }
{ "parserOptions": { "ecmaVersion": 7, "sourceType": "module", "ecmaFeatures": { "jsx": true } }, "extends": "airbnb-base/legacy", "rules": { "consistent-return": 1, "prefer-destructuring": 0, "no-mixed-spaces-and-tabs": 0, "no-console": 0, "no-tabs": 0, "one-var":0, "no-unused-vars": 2, "no-multi-spaces": 2, "key-spacing": [ 2, { "beforeColon": false, "afterColon": true, "align": { "on": "colon" } } ], "no-return-await": 0 }, "env": { "node": true, "es6": true } }
使用husky, 來定義git-hooks, 規範git代碼提交流程,這裏只作 commit
校驗
在package.json
配置以下:
"husky": { "hooks": { "pre-commit": "npm run lint" } }
統一配置和入口,分發到不一樣單一文件,執行輸出。核心代碼
function registerAction(command, type, typeMap) { command .command(type) .description(typeMap[type].desc) .alias(typeMap[type].alias) .action(async () => { try { if (type === 'help') { help(); } else if (type === 'config') { await project('config', ...process.argv.slice(3)); } else { await project(type); } } catch (e) { console.log(e); help(); } }); return command; }
配置用來獲取腳手架的基本設置, 如registry, type等基本信息。
project config set registry koajs # 設置本地倉庫下載源 project config get registry # 獲取本地倉庫設置的屬性 project config delete registry # 刪除本地設置的屬性
斷定本地設置文件存在 ===> 讀/寫
本地配置文件, 格式是 .ini
若中間每一步 數據爲空/文件不存在 則給予提示
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());
下面每一個命令的實現邏輯。
project i
Github API ===> 獲取項目列表 ===> 選擇一個項目 ===> 獲取項目版本號 ===> 選擇一個版本號 ===> 下載到本地倉庫
獲取項目列表
獲取tag列表
若中間每一步 數據爲空/文件不存在 則給予提示
請求代碼
function fetch(api) { return new Promise((resolve, reject) => { request({ url : api, method : 'GET', headers: { 'User-Agent': `${ua}` } }, (err, res, body) => { if (err) { reject(err); return; } const data = JSON.parse(body); if (data.message === 'Not Found') { reject(new Error(`${api} is not found`)); } else { resolve(data); } }); }); }
下載代碼
export const download = async (repo) => { const { url, scaffold } = await getGitInfo(repo); return new Promise((resolve, reject) => { downloadGit(url, `${dirs.download}/${scaffold}`, (err) => { if (err) { reject(err); return; } resolve(); }); }); };
// 獲取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('@'));
project init
獲取本地倉庫列表 ===> 選擇一個本地項目 ===> 輸入基本信息 ===> 編譯生成到臨時文件 ===> 複製並重名到目標目錄
若中間每一步 數據爲空/文件不存在/生成目錄已重複 則給予提示
// 獲取本地倉庫項目 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(); }); }) } }
project update
獲取本地倉庫列表 ===> 選擇一個本地項目 ===> 獲取版本信息列表 ===> 選擇一個版本 ===> 覆蓋原有的版本文件
若中間每一步 數據爲空/文件不存在 則給予提示
// 獲取本地倉庫列表 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('@'))
搜索遠程的github倉庫有哪些項目列表
project search
獲取github項目列表 ===> 輸入搜索的內容 ===> 返回匹配的列表
若中間每一步 數據爲空 則給予提示
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(''); }
以上是這款通用腳手架產生的背景,針對用戶以及具體實現,該腳手架目前還有一些能夠優化的地方:
硬廣:若是您以爲project-next-cli好用,歡迎star,也歡迎fork一塊維護。