在16年年底的時候,同事聊起腳手架。由于公司業(yè)務(wù)的多樣性,前端的靈活性,讓我們不得不思考更通用的腳手架。而不是伴隨著前端技術(shù)的發(fā)展,不斷的把時間花在配置上。于是chef-cli誕生了。 18年年初,把過往一年的東西整理和總結(jié)下,重新增強(qiáng)了原有的腳手架project-next-cli, 不單單滿足我們團(tuán)隊的需求,也可以滿足其他人的需求。
<!--more-->
project-next-cli
面向的目標(biāo)用戶:
- 公司業(yè)務(wù)雜,但有一定的積累
- 愛折騰的同學(xué)和團(tuán)隊
- 借助github大量開發(fā)模板開發(fā)
發(fā)展
前端這幾年(13年-15年)處于高速發(fā)展,主要表現(xiàn):
備注:以下發(fā)展過程出現(xiàn),請不要糾結(jié)出現(xiàn)順序 [捂臉]
- 庫/框架:jQuery, backbone, angular,react,vue
- 模塊化:commonjs, AMD(CMD), UMD, es module
- 任務(wù)管理器:npm scripts, grunt, gulp
- 模塊打包工具: r.js, webpack, rollup, browserify
- css預(yù)處理器:Sass, Less, Stylus, Postcss
- 靜態(tài)檢查器:flow/typescript
- 測試工具:mocha,jasmine,jest,ava
- 代碼檢測工具:eslint,jslint
開發(fā)
當(dāng)我們真實開發(fā)中,會遇到各種各樣的業(yè)務(wù)需求(場景),根據(jù)需求和場景選用不同的技術(shù)棧,由于技術(shù)的進(jìn)步和不同瀏覽器運(yùn)行時的限制,不得不配置對應(yīng)的環(huán)境等,導(dǎo)致我們從而滿足業(yè)務(wù)需求。
畫了一張圖來表示,業(yè)務(wù),配置(環(huán)境),技術(shù)之間的關(guān)系
前端配置工程師
于是明見流傳了一個新的職業(yè),前端配置工程師 O(∩_∩)O~
社區(qū)現(xiàn)狀
專一的腳手架
社區(qū)中存在著大量的專一型框架,主要針對一個目標(biāo)任務(wù)做定制。比如下列腳手架
vue-clivue-cli提供利用vue開發(fā)webpack, 以及 遠(yuǎn)程克隆生成文件等 pwa等模板,本文腳手架參考了vue-cli的實現(xiàn)。
dva-clidva-cli 針對dva開發(fā)使用的腳手架
think-clithink-cli 針對 thinkjs項目創(chuàng)建項目
通用腳手架
yeomanyeoman是一款強(qiáng)壯的且有一系列工具的通用型腳手架,但yeoman發(fā)布指定package名稱,和用其開發(fā)工具。具體可點擊這里查看yeoman添加生成器規(guī)則
開發(fā)初衷和目標(biāo)
由于公司形態(tài)決定了,業(yè)務(wù)類型多樣,前端技術(shù)發(fā)展迭代,為了跟進(jìn)社區(qū)發(fā)展,更好的完成下列目標(biāo)而誕生。
- 完成業(yè)務(wù):專心,穩(wěn)定,快速
- 團(tuán)隊規(guī)范:代碼規(guī)范,測試流程,發(fā)布流程
- 沉淀:專人做專事,持續(xù)穩(wěn)定的迭代更新,跟進(jìn)時代
- 效益:少加班,少造輪子,完成kpi,做更有意義的事兒
實現(xiàn)準(zhǔn)備
依托于Github,根據(jù)Github API來實現(xiàn),如下:
獲取項目curl -i https://api.github.com/orgs/project-scaffold/repos
獲取版本curl -i https://api.github.com/repos/project-scaffold/cli/tags
實現(xiàn)邏輯
根據(jù)github api獲取到項目列表和版本號之后,根據(jù)輸入的名稱,選擇對應(yīng)的版本下載到本地私有倉庫,生成到執(zhí)行目錄下。核心流程圖如下:。
總體設(shè)計
規(guī)范- 使用Node進(jìn)行腳手架開發(fā),版本選擇 >=6.0.0
- 選用async/await開發(fā),解決異步回調(diào)問題
- 使用babel編譯
- 使用ESLint規(guī)范代碼
功能遵守單一職責(zé)原則,每個文件為一個單獨模塊,解決獨立的問題。可以自由組合,從而實現(xiàn)復(fù)用。以下是最終的目錄結(jié)構(gòu):
├── 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
配置和主框架
使用babel-preset-env保證版本兼容
{"presets": [["env", {"targets": {"node": "6.0.0"}}]]
}
使用eslint管理代碼
{"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檢測提交
使用husky, 來定義git-hooks, 規(guī)范git代碼提交流程,這里只做 commit校驗
在package.json配置如下:
"husky": {"hooks": {"pre-commit": "npm run lint"}
}
入口
統(tǒng)一配置和入口,分發(fā)到不同單一文件,執(zhí)行輸出。核心代碼
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;
}
本地配置讀和寫
配置用來獲取腳手架的基本設(shè)置, 如registry, type等基本信息。
使用project config set registry koajs # 設(shè)置本地倉庫下載源project config get registry # 獲取本地倉庫設(shè)置的屬性project config delete registry # 刪除本地設(shè)置的屬性
邏輯判定本地設(shè)置文件存在 ===> 讀/寫
本地配置文件, 格式是 .ini
若中間每一步 數(shù)據(jù)為空/文件不存在 則給予提示
核心代碼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());
下面每個命令的實現(xiàn)邏輯。
下載
使用project i
邏輯Github API ===> 獲取項目列表 ===> 選擇一個項目 ===> 獲取項目版本號 ===> 選擇一個版本號 ===> 下載到本地倉庫
獲取項目列表
- https://api.github.com/orgs/p...
獲取tag列表
若中間每一步 數(shù)據(jù)為空/文件不存在 則給予提示
請求代碼
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
邏輯獲取本地倉庫列表 ===> 選擇一個本地項目 ===> 輸入基本信息 ===> 編譯生成到臨時文件 ===> 復(fù)制并重名到目標(biāo)目錄
若中間每一步 數(shù)據(jù)為空/文件不存在/生成目錄已重復(fù) 則給予提示
核心代碼// 獲取本地倉庫項目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}`;// 復(fù)制一份到臨時目錄,在臨時目錄編譯生成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);}
其中模板引擎編譯實現(xiàn)核心代碼如下:
// 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中間件實現(xiàn)
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
邏輯獲取本地倉庫列表 ===> 選擇一個本地項目 ===> 獲取版本信息列表 ===> 選擇一個版本 ===> 覆蓋原有的版本文件
若中間每一步 數(shù)據(jù)為空/文件不存在 則給予提示
核心代碼 // 獲取本地倉庫列表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('@'))
搜索
搜索遠(yuǎn)程的github倉庫有哪些項目列表
使用
project search
邏輯獲取github項目列表 ===> 輸入搜索的內(nèi)容 ===> 返回匹配的列表
若中間每一步 數(shù)據(jù)為空 則給予提示
核心代碼 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('');}
總結(jié)
以上是這款通用腳手架產(chǎn)生的背景,針對用戶以及具體實現(xiàn),該腳手架目前還有一些可以優(yōu)化的地方:
不同源,存儲不同的文件支持離線功能硬廣:如果您覺得project-next-cli好用,歡迎star,也歡迎fork一塊維護(hù)。
總結(jié)
以上是生活随笔為你收集整理的搭建一个通用的脚手架的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。