在16年年底的時候,同事聊起腳手架。由於公司業務的多樣性
,前端的靈活性
,讓我們不得不思考更通用的腳手架。而不是伴隨着前端技術的發展,不斷的把時間花在配置
上。於是chef-cli誕生了。 18年年初,把過往一年的東西整理和總結下,重新增強了原有的腳手架project-next-cli, 不單單滿足我們團隊的需求,也可以滿足其他人的需求。
<!--more-->
project-next-cli
面向的目標用戶:
- 公司業務雜,但有一定的積累
- 愛折騰的同學和團隊
- 藉助github大量開發模板開發
發展
前端這幾年(13年-15年)處於高速發展,主要表現:
備註:以下發展過程出現,請不要糾結出現順序 [捂臉]
- 庫/框架: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
開發
當我們真實開發中,會遇到各種各樣的業務需求(場景),根據需求和場景選用不同的技術棧,由於技術的進步和不同瀏覽器運行時的限制,不得不配置對應的環境等,導致我們從而滿足業務需求。
畫了一張圖來表示,業務,配置(環境),技術之間的關係
前端配置工程師
於是明見流傳了一個新的職業,前端配置工程師 O(∩_∩)O~
社區現狀
專一的腳手架
社區中存在着大量的專一型框架,主要針對一個目標任務做定製。比如下列腳手架
vue-cli
提供利用vue開發webpack
, 以及 遠程克隆生成文件等 pwa
等模板,本文腳手架參考了vue-cli
的實現。
dva-cli
針對dva開發使用的腳手架
think-cli
針對 thinkjs項目創建項目
通用腳手架
yeoman
是一款強壯的且有一系列工具的通用型腳手架,但yeoman發佈指定package名稱,和用其開發工具。具體可點擊這裏查看yeoman添加生成器規則
開發初衷和目標
由於公司形態決定了,業務類型多樣,前端技術發展迭代,爲了跟進社區發展,更好的完成下列目標而誕生。
- 完成業務:專心,穩定,快速
- 團隊規範:代碼規範,測試流程,發佈流程
- 沉澱:專人做專事,持續穩定的迭代更新,跟進時代
- 效益:少加班,少造輪子,完成kpi,做更有意義的事兒
實現準備
依託於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
獲取到項目列表和版本號之後,根據輸入的名稱,選擇對應的版本下載到本地私有倉庫
,生成到執行目錄下。核心流程圖如下:。
總體設計
- 規範
- 使用Node進行腳手架開發,版本選擇
>=6.0.0
- 選用async/await開發,解決異步回調問題
- 使用babel編譯
- 使用ESLint規範代碼
- 功能
遵守單一職責原則
,每個文件爲一個單獨模塊,解決獨立的問題。可以自由組合,從而實現複用。以下是最終的目錄結構:
├── 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, 規範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一塊維護。