也來盤盤前端腳手架的那些事兒
本來早就想寫這篇文章的,由于有其他事情耽擱了(可能還是因為太懶),就拖到了現(xiàn)在,如果再不記下來,估計會拋到九霄云外了。
NodeJs的出現(xiàn),讓前端工程化的理念不斷深入,正在向正規(guī)軍靠近。先是帶來了Gulp、Webpack等強大的構(gòu)建工具,隨后又出現(xiàn)了vue-cli和create-react-app等完善的腳手架,提供了完整的項目架構(gòu),讓我們可以更多的關(guān)注業(yè)務(wù),而不必在項目基礎(chǔ)設(shè)施上花費大量時間。
但是,這些現(xiàn)成的腳手架未必就能滿足我們的業(yè)務(wù)需求,也未必是最佳實踐,這時我們就可以自己來開發(fā)一個腳手架。當(dāng)然,這其實很簡單,利用npm上現(xiàn)成的輪子就可以搞定,這里做個記錄,僅當(dāng)備忘,以拋磚引玉。
緣起
在上半年的一個項目中需要自定義一個腳手架,來幫助小伙伴們提高開發(fā)效率,統(tǒng)一代碼輸出質(zhì)量,并解決一些使用上的問題,當(dāng)然也是為了裝裝逼。
在使用腳手架方式構(gòu)建之前,我們遇到了這幾個問題:
-  每個項目創(chuàng)建的時候,需要去Git倉庫拉取項目模板或者拷貝之前的項目,這樣做有兩個問題
    
- 從Git拉取項目后,由于部分人員是有推送權(quán)限的,如果他誤操作將私有項目中的修改推送到了模板倉庫,可能會破壞Git上的項目模板
 - 從之前項目拷貝就無法獲取最新的項目模板,這就導(dǎo)致有些問題明明在最新的模板中已經(jīng)修復(fù),卻在新項目中依然存在
 
 - 項目模板需要填寫一些配置信息,開發(fā)人員很容易忘記填寫
 
所以我們來解決這些問題,思路如下:
- 從Git上拉取最新模板,最后干掉Git倉庫信息,切斷和遠程倉庫的關(guān)聯(lián)
 - 在初始化時,通過問答方式強制讓使用者輸入配置信息,再根據(jù)配置信息生成配置文件,有點類似VueCli初始化項目那樣。
 
當(dāng)然,除了上述需求,我們還可以再做些額外工作:
- 拉取完成后自動安裝項目依賴,并打開編輯器
 - 提供幫助信息及常用命令查看
 - 發(fā)布到NPM,所有人員都可以直接全局安裝使用
 - ...
 
急急如律令
實現(xiàn)可執(zhí)行模塊
首先,我們需要創(chuàng)建一個項目,這里就叫yncms-template-cli, 項目結(jié)構(gòu)如下:
- - commands // 此文件夾用于放置自定義命令
 - - utils
 - - index.js // 項目入口
 - - readme.md
 
為了測試,我們先在index.js放點內(nèi)容:
- #!/usr/bin/env node
 - // 必須在文件頭添加如上內(nèi)容指定運行環(huán)境為node
 - console.log('hello cli');
 
對于一般的nodejs項目,我們直接使用node index.js就可以了,但是這里是腳手架,肯定不能這樣。我們需要把項目發(fā)布到npm,用戶進行全局安裝,然后就可以直接使用我們自定義的命令,類似yncms-template這樣。
所以,我們需要將我們的項目做下改動,首先在packge.json中添加如下內(nèi)容:
- "bin": {
 - "yncms-template": "index.js"
 - },
 
這樣就可以將yncms-template定義為一個命令了,但此時僅僅只能在項目中使用,還不能作為全局命令使用,這里我們需要使用npm link將其鏈接到全局命令,執(zhí)行成功后在你的全局node_modules目錄下可以找到相應(yīng)文件。然后輸入命令測試一下,如果出現(xiàn)如下內(nèi)容說明第一步已經(jīng)成功一大半了:
- PS E:\WorkSpace\yncms-template-cli> yncms-template
 - hello cli
 
但是,目前這個命令只有我們自己電腦可以用,要想其他人也能安裝使用,需要將它發(fā)布到npm,大致流程如下:
- 注冊一個npm賬戶,已有賬戶的可以跳過這一步
 - 使用npm login登錄,需要輸入username、password、email
 - 使用npm public發(fā)布
 
這一步比較簡單,不多說,但是請注意如下幾點:
- 使用了nrm的需要先將源切換到npm官方源
 -  package.json中有幾個字段需要完善:
    
- name為發(fā)布的包名,不能和npm已有的包重復(fù)
 - version為版本信息,每次發(fā)布都必須要比線上的版本高
 - homepage、bugs、repository也可以添加上,對應(yīng)如下頁面
 
 
- 在readme.md加入腳手架介紹及使用方法,方便他人使用。如果需要在文檔中加入徽標(biāo),展示腳手架的下載次數(shù)之類的,可以在這里生成。
 
發(fā)布成功后,需要等待一會兒才可以在npm倉庫搜索到。
創(chuàng)建命令
既然是腳手架,肯定不能只讓它輸出一段文字吧,我們還需要定義一些命令,用戶在命令行輸入這些命令和參數(shù),腳手架會做出對應(yīng)的操作。這里不需要我們自己去解析這些輸入的命令和參數(shù),有現(xiàn)成的輪子(commander)可以使用,完全可以滿足我們的需要。
幫助(--help)
安裝好commander后,我們將index.js中內(nèi)容改為如下:
- #!/usr/bin/env node
 - const commander = require('commander');
 - // 利用commander解析命令行輸入,必須寫在所有內(nèi)容最后面
 - commander.parse(process.argv);
 
這時,雖然我們沒有定義任何命令,但是commander內(nèi)部給我們定義了一個幫助命令--help(簡寫-h):
- PS E:\WorkSpace\yncms-template-cli> yncms-template -h
 - Usage: index [options]
 - Options:
 - -h, --help output usage information
 
版本(--version)
接下來,我們再創(chuàng)建一個查詢版本的命令參數(shù),在index.js增加如下內(nèi)容:
- // 查看版本號
 - commander.version(require('./package.json').version);
 
這樣,我們在命令行就可以查看版本號了:
- PS E:\WorkSpace\yncms-template-cli> yncms-template -V
 - 1.0.10
 - PS E:\WorkSpace\yncms-template-cli> yncms-template --version
 - 1.0.10
 
默認參數(shù)是大寫V,如果需要改成小寫,將上面內(nèi)容做如下改動即可:
- // 查看版本號
 - commander
 - .version(require('./package.json').version)
 - .option('-v,--version', '查看版本號');
 
- PS E:\WorkSpace\yncms-template-cli> yncms-template -h
 - Usage: index [options]
 - Options:
 - -V, --version output the version number
 - -h, --help output usage information
 
init子命令
接下來,我們來定義一個init命令,如yncms-template init test。
在index.js中增加如下內(nèi)容:
- commander
 - .command('init <name>') // 定義init子命令,<name>為必需參數(shù)可在action的function中接收,如需設(shè)置非必需參數(shù),可使用中括號
 - .option('-d, --dev', '獲取開發(fā)版') // 配置參數(shù),簡寫和全寫中使用,分割
 - .description('創(chuàng)建項目') // 命令描述說明
 - .action(function (name, option) { // 命令執(zhí)行操作,參數(shù)對應(yīng)上面的設(shè)置的參數(shù)
 - // 我們需要執(zhí)行的所有操作,都在這里完成
 - console.log(name);
 - console.log(option.dev);
 - });
 
現(xiàn)在測試一下:
- PS E:\WorkSpace\yncms-template-cli> yncms-template init test -d
 - test
 - true
 
commander具體的用法,請自行查看官方文檔。
如此,一個自定義命令雛形就算完成了,然還有幾件事情要做:
- 實現(xiàn)init命令具體執(zhí)行的操作,下面會有單獨部分來說。
 - 為了方便維護,將命令action拆分到commands文件夾中
 
拉取項目
上面,我們定義了init命令,但是并沒有達到初始化項目的目的,接下來我們就實現(xiàn)一下。
一般來說,項目模板有兩種處理方式:
- 將項目模板和本腳手架放在一起,好處是用戶安裝腳手架后,模板在本地,初始化會比較快;缺點是項目模板更新比較麻煩,因為和腳手架耦合在一起了
 - 將項目放置到單獨的GIT倉庫,好處是模板更新比較簡單,因為是相互獨立的,只需要維護模板自己的倉庫即可,另外可以控制拉取權(quán)限,因為如果是私有項目,那么沒有權(quán)限的人員是無法拉取成功的;缺點就是每次初始化都要去GIT拉取,可能會慢點,不過影響不大,所以建議選擇此種方式
 
首先,我們利用download-git-repo封裝一個clone方法,用于從git拉取項目。
- // utils/clone.js
 - const download = require('download-git-repo');
 - const symbols = require('log-symbols'); // 用于輸出圖標(biāo)
 - const ora = require('ora'); // 用于輸出loading
 - const chalk = require('chalk'); // 用于改變文字顏色
 - module.exports = function (remote, name, option) {
 - const downSpinner = ora('正在下載模板...').start();
 - return new Promise((resolve, reject) => {
 - download(remote, name, option, err => {
 - if (err) {
 - downSpinner.fail();
 - console.log(symbols.error, chalk.red(err));
 - reject(err);
 - return;
 - };
 - downSpinner.succeed(chalk.green('模板下載成功!'));
 - resolve();
 - });
 - });
 - };
 
- // commands/init.js
 - const shell = require('shelljs');
 - const symbols = require('log-symbols');
 - const clone = require('../utils/clone.js');
 - const remote = 'https://gitee.com/letwrong/cli-demo.git';
 - let branch = 'master';
 - const initAction = async (name, option) => {
 - // 0. 檢查控制臺是否可以運行`git `,
 - if (!shell.which('git')) {
 - console.log(symbols.error, '對不起,git命令不可用!');
 - shell.exit(1);
 - }
 - // 1. 驗證輸入name是否合法
 - if (fs.existsSync(name)) {
 - console.log(symbols.warning,`已存在項目文件夾${name}!`);
 - return;
 - }
 - if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
 - console.log(symbols.error, '項目名稱存在非法字符!');
 - return;
 - }
 - // 2. 獲取option,確定模板類型(分支)
 - if (option.dev) branch = 'develop';
 - // 4. 下載模板
 - await clone(`direct:${remote}#${branch}`, name, { clone: true });
 - };
 - module.exports = initAction;
 
測試一下,不出意外就可以成功拉取項目了。
這里拉取的項目是和遠程倉庫關(guān)聯(lián)的,我們需要將其刪掉(由于我們項目是svn管理,所以直接把.git文件夾刪掉,如果使用git的話,可以git init初始化即可),清理掉一些多余文件:
- // commands/init.js
 - // 5. 清理文件
 - const deleteDir = ['.git', '.gitignore', 'README.md', 'docs']; // 需要清理的文件
 - const pwd = shell.pwd();
 - deleteDir.map(item => shell.rm('-rf', pwd + `/${name}/${item}`));
 
來點個性化
在上述過程中,我們實現(xiàn)了一個腳手架的基本功能,大致分為三個流程(拉取模板->創(chuàng)建項目->收尾清理),也解決了上面我項目中遇到的第一個問題。接下來,我們就來看下第二個問題如何解決。
解決的思路就是在創(chuàng)建項目的時候,就通過命令行強制要求開發(fā)人員輸入對應(yīng)的配置,然后自動寫入配置文件,這樣就可以有效避免忘記填寫的尷尬。當(dāng)然通過這種方式也可以實現(xiàn)根據(jù)用戶的輸入來動態(tài)初始化項目,達到個性化的目的。
這里我們直接使用現(xiàn)成的輪子inquirer就可以搞定,效果和VueCli創(chuàng)建項目一樣,支持很多類型,比較強大,也比較簡單,具體用法看官方文檔就可以了。這里我直接上代碼,在第4步(下載模板)前面增加如下:
- // init.js
 - const inquirer = require('inquirer');
 - // 定義需要詢問的問題
 - const questions = [
 - {
 - type: 'input',
 - message: '請輸入模板名稱:',
 - name: 'name',
 - validate(val) {
 - if (!val) return '模板名稱不能為空!';
 - if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) return '模板名稱包含非法字符,請重新輸入';
 - return true;
 - }
 - },
 - {
 - type: 'input',
 - message: '請輸入模板關(guān)鍵詞(;分割):',
 - name: 'keywords'
 - },
 - {
 - type: 'input',
 - message: '請輸入模板簡介:',
 - name: 'description'
 - },
 - {
 - type: 'list',
 - message: '請選擇模板類型:',
 - choices: ['響應(yīng)式', '桌面端', '移動端'],
 - name: 'type'
 - },
 - {
 - type: 'list',
 - message: '請選擇模板分類:',
 - choices: ['整站', '單頁', '專題'],
 - name: 'category'
 - },
 - {
 - type: 'input',
 - message: '請輸入模板風(fēng)格:',
 - name: 'style'
 - },
 - {
 - type: 'input',
 - message: '請輸入模板色系:',
 - name: 'color'
 - },
 - {
 - type: 'input',
 - message: '請輸入您的名字:',
 - name: 'author'
 - }
 - ];
 - // 通過inquirer獲取到用戶輸入的內(nèi)容
 - const answers = await inquirer.prompt(questions);
 - // 將用戶的配置打印,確認一下是否正確
 - console.log('------------------------');
 - console.log(answers);
 - let confirm = await inquirer.prompt([
 - {
 - type: 'confirm',
 - message: '確認創(chuàng)建?',
 - default: 'Y',
 - name: 'isConfirm'
 - }
 - ]);
 - if (!confirm.isConfirm) return false;
 
獲取到用戶輸入的配置以后,就可以寫入配置文件或者做個性化的處理了,這個太簡單,我這里就不贅述了。
錦上添花
到這里,一個完全滿足需求的腳手架就完成了,但是作為一個有追求的程序員,我們可以在界面和易用性上面再做點什么:
- 為異步操作加上loding動畫,可以直接使用ora
 
- const installSpinner = ora('正在安裝依賴...').start();
 - if (shell.exec('npm install').code !== 0) {
 - console.log(symbols.warning, chalk.yellow('自動安裝失敗,請手動安裝!'));
 - installSpinner.fail(); // 安裝失敗
 - shell.exit(1);
 - }
 - installSpinner.succeed(chalk.green('依賴安裝成功!'));
 
- 在操作成功或者失敗給出圖標(biāo)提示,使用log-symbols
 
- 可以給文字加點顏色,同理用現(xiàn)成的輪子Chalk
 
- 在安裝依賴或者其他耗時比較長的時候,用戶可能會把終端切到后臺,這時我們的操作完成后可以使用node-notifier發(fā)出系統(tǒng)通知給予用戶提示。
 
- notifier.notify({
 - title: 'YNCMS-template-cli',
 - icon: path.join(__dirname, 'coulson.png'),
 - message: ' ♪(^∀^●)ノ 恭喜,項目創(chuàng)建成功!'
 - });
 
- 在創(chuàng)建項目的時候,我們可能會需要執(zhí)行一些shell命令,可以使用shelljs來完成,例如我們要在項目創(chuàng)建結(jié)束后打開vscode并退出終端
 
- // 8. 打開編輯器
 - if (shell.which('code')) shell.exec('code ./');
 - shell.exit(1);
 
結(jié)語
到這里,會發(fā)現(xiàn)開發(fā)一個腳手架其實很簡單,都是使用現(xiàn)成的輪子就可以搞定,不曉得哪位大牛說過玩NodeJS就是玩輪子。
除了上述方法,我們也可以直接通過大名鼎鼎的Yeoman來創(chuàng)建,不過個人覺得沒必要,畢竟這玩意也不難。
一個好的腳手架應(yīng)該是能夠解決工作中遇到的問題,提高開發(fā)效率的。






















 
 
 









 
 
 
 