github地址:github.com/jinxuanzhen…,有興趣的同窗能夠體驗一下javascript
原文地址:www.yuque.com/docs/share/…vue
在正文以前先大體描述下什麼是cli工具,
cli工具英文名command-line interface,也就是命令行交互接口,比較典型的幾個case例如,create-react-app,vue-cli,具體能夠去百度一下,下面gif是小打卡目前用的一套自動化發佈工具🔧java
能夠看到整個發佈流程大體是以選擇或默認項的形式實現,大體分析下面幾步node
是否是很是無腦?是否是不再用擔憂線上發錯環境了?有了它就算不一樣項目間,就算一天發n次版本還須要擔憂什麼呢?react
固然除了簡單的發佈功能還,還能夠作不少的事情,好比建立page/component模版等一些更多有趣的事情webpack
爲了節約版面就不貼圖了,具體能夠看下倉庫 github.com/jinxuanzhen…(目前該工具是從小打卡現有的cli庫中抽離的部分功能)git
也就是我爲何要作這麼一個工具,其實最開始我只是爲了解決一個問題,就是在整個發佈流程中須要人工去改動/確認發佈環境和版本信息,大體能夠想象下把線下環境發佈到線上的尷尬處境github
後續發現從cli角度觸發,不少東西都變得簡單了,大體列了下:web
本文會以快速建立頁面模版文件爲例教你怎麼快速擼一個屬於本身的cli工具,
若是以爲本身作比較麻煩,能夠clone下個人倉庫本身改裝下vue-cli
中間會用到一些第三方庫
其餘的一些小知識:好比path模塊,fs模塊,你們能夠去node官網自行查看:nodejs.org/api/
建立一個空文件夾,而且npm初始化, 而且建立一個index.js頁面,這個index.js將做爲你整個包的入口文件
npm init -y
複製代碼
安裝上述的三方包,固然也能夠後續按需安裝,這樣更能清楚每一個包是作什麼的
npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save
複製代碼
在package.json裏添加bin字段, 將自定義的命令軟連到全局環境,同時執行npm link建立連接,這裏若是報錯{code EACCES,errno:13,...},是由於權限不足,能夠嘗試sudo npm link
"bin": {
"cli-demo": "./index.js"
}
複製代碼
在入口文件,index.js 行首加入一行#!/usr/bin/env node
指定當前腳本由node.js進行解析
#!/usr/bin/env node // 指定運行環境 // 輸出文本 console.log('Hello World!!!'); 複製代碼
這時能夠在命令行中執行cli-demo
驗收一下成果了
ok,能夠看到當在全局狀態下輸入自定義命令時,正確運行了入口文件,也就意味着的開發玩具已經搭建完成
以快速建立頁面模版文件爲例,就須要考慮須要哪些邏輯:
在剛纔的Hello World!!!
環節,已經能夠正確識別cli-demo,可是須要在一個cli工具中集成更多功能,可能須要有不一樣的執行策略,以git爲例:git clone, git status,git push
,因此須要識別不一樣的命令和參數,
是時候就須要用到commander
這個第三方包幫助解析命令行參數了,固然你也能夠本身擼一個lib,本質上仍是方便解析process.argv
index.js (本質上這個js就是一個路由)
#!/usr/bin/env node const version = require('./package').version; // 版本號 /* = package import -------------------------------------------------------------- */ const program = require('commander'); // 命令行解析 /* = task events -------------------------------------------------------------- */ const createProgramFs = require('./lib/create-program-fs'); // 建立項目文件 /* = config -------------------------------------------------------------- */ // 設置版本號 program.version(version, '-v, --version'); /* = deal receive command -------------------------------------------------------------- */ program .command('create') .description('建立頁面或組件') .action((cmd, options) => createProgramFs(cmd)); /* 後續能夠根據不一樣的命令進行不一樣的處理,能夠簡單的理解爲路由 */ // program // .command('build [cli]') // .description('執行打包構建') // .action((cmd, env) => callback); /* = main entrance -------------------------------------------------------------- */ program.parse(process.argv) 複製代碼
這時候當鍵入cli-demo create
時會自動執行createProgramFs
createProgramFs.js
module.exports = function () { console.log('Hi, create-program-fs.js'); }; 複製代碼
命令行輸入 cli-demo create
能夠看到已經成功的開闢出了一塊獨立的業務模塊,後續就只須要依據需求填補相應的內容便可
收到執行命令,這個時候按第一張圖,是須要開始一系列QA(固然你也能夠不作交互式,直接配置命令行參數),
引入三方包 inquirer
,來指定問題隊列
const question = [ // 選擇模式使用 page -> 建立頁面 | component -> 建立組件 { type: 'list', name: 'mode', message: '選擇想要建立的模版', choices: [ 'page', 'component', ] }, // 設置名稱 { type: 'input', name: 'name', message: answer => `設置 ${answer.mode} 名稱 (e.g: index):`, }, ]; module.exports = function() { // 問題執行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; 複製代碼
能夠看到經過一系列QA交互,實際輸出拿到的是一個json對象,第一步已完成
建立一個存放模版文件的文件夾template,並準備好你但願的模版
爲了方便閱讀,下面的代碼,須要明確下面變量的定義, Config.dir_root = 命令行執行目錄 Config.root = cli項目根目錄 Config.appRoot = 小程序項目路徑 Config.template = 模版目錄
這裏有兩個點,一個是執行路徑的問題,另外一個是分包的問題,具體以下:
這裏必定要弄明白**__dirname, process.cwd()**的區別,同時還有一些小程序是本身搭的gulp/webpack,可能小程序項目是在src目錄下,必定要分清楚
__dirname: 被執行js文件的絕對路徑,通常在index.js執行時緩存起來做爲項目的全局路徑,好比找到template文件夾就會使用 ${__dirname}/template
process.cwd():當前命令行運行時的工做目錄,好比在/Users/xuan/Documents/cli-demo
若是當前項目在src,或其餘文件夾裏怎麼辦?能夠提供一個給用戶項目中的配置文件,相似於gulpfile.js或是webpack.config.js的形式,內容例如(具體能夠看git倉庫)
module.exports = { // 小程序路徑 app: './src', // 模版文件夾 template: './template' }; 複製代碼
能夠看到對象中app屬性,能夠指定你當前小程序項目的路徑
由於小程序的分包機制會致使頁面實際路徑與在主包的路徑不相符,例如:
解決這個問題一方面是要有頁面建立要有必定的規範,統一格式,另外一方面須要根據規則解析app.json,
上面的主包,分包路徑差很少是我目前使用的規範
解析app.json
// 獲取app.json function getAppJson() { let appJsonRoot = path.join(Config.appRoot, '/app.json'); try { return require(appJsonRoot); }catch (e) { Log.error(`未找到app.json, 請檢查當前文件目錄是否正確,path: ${appJsonRoot}`); process.exit(1); // 異常退出 } } // 解析app.json let parseAppJson = () => { // app Json 原文件 let appJson = __Data__.appJson = getAppJson(); // 獲取主包頁面 appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = ''); // 獲取分包,頁面列表 appJson.subPackages.forEach(item => { __Data__.appModuleList[getPathSubSting(item.root)] = item.root; item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root); }); }; // __Data__.appPagesList = 小程序所有頁面 // __Data__.appModuleList = 小程序所有分包頁面 // item結構 {util_module: 'pages/util_module/'},這麼定義結構是爲了方便後續取數 複製代碼
question隊列裏,增長刪選分包的選項
// 設置page所屬module { type: 'autocomplete', name: 'modulePath', message: 'Set page ownership module', choices: [], suggestOnly: false, source(answers, input) { // none 表明放在主包 return Promise.resolve(fuzzy.filter(input, ['none', ...Object.keys(__Data__.appModuleList)]).map(el => el.original)); }, filter(input) { if (input === 'none') { return ''; } return __Data__.appModuleList[input]; }, when(answer) { return answer.mode === 'page'; } } 複製代碼
autocomplete類型本質上是個列表,可是能夠進行模糊查詢,很是方便,像小打卡有接近30個分包的狀況下效果尤其明顯
有了文件名,有了分包路徑,有了可供copy的模版,接下來就很簡單了,把模版文件塞進項目就能夠了,下面是一串從倉庫裏copy的代碼,利用async/await很方便的寫出一維代碼,基本上的流程:
獲取路徑 -> 校驗 -> 獲取文件信息 -> 複製文件 -> 修改app.json -> 輸出結果信息
async function createPage(name, modulePath = '') { // 獲取模版文件路徑 let templateRoot = path.join(Config.template, '/page'); if (!Util.checkFileIsExists(templateRoot)) { Log.error(`未找到模版文件, 請檢查當前文件目錄是否正確,path: ${templateRoot}`); return; } // 獲取業務文件夾路徑 let page_root = path.join(Config.appRoot, modulePath, '/pages', name); // 查看文件夾是否存在 let isExists = await Util.checkFileIsExists(page_root); if (isExists) { Log.error(`當前頁面已存在,請從新確認, path: ` + page_root); return; } // 建立文件夾 await Util.createDir(page_root); // 獲取文件列表 let files = await Util.readDir(templateRoot); // 複製文件 await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files); // 填充app.json await writePageAppJson(name, modulePath); // 成功提示 Log.success(`createPage success, path: ` + page_root); } 複製代碼
一個基本的快速建立頁面模版的cli工具就這樣完成,可是有可能須要更多的一些功能
好比說每一個項目的模版都有可能不太同樣,很大程度上須要根據項目進行定製,這時候可能就須要前文提到的給用戶開放config文件的插槽了
項目中的config:
// xdk.config.js module.exports = { // 小程序路徑 app: './', // 模版文件夾 template: './template' }; // create-program-fs.js module.exports = function() { // 校驗:當前是否存在配置文件 let customConfPath = `${Config.dir_root}/xdk.config.js`; if (!Util.checkFileIsExists(customConfPath)) { Log.error('當前項目還沒有建立xdk.config.js文件'); return; } // 獲取用戶配置項 let {app, template = ''} = require(customConfPath); // 小程序目錄 Config.appRoot = path.resolve(path.join(Config.dir_root, app)); // 模版文件目錄(默認使用cli提供的默認模版,當config文件有設置template路徑時,使用自定義路徑) !!template && (Config.template = path.resolve(path.join(Config.dir_root, template)))); // 問題執行 inquirer.prompt(question).then(answers => { console.log(answers); }); }; 複製代碼
目前從開發到調試本質上是在本地提供服務,利用npm link提供軟鏈接到全局PATH,
其實也能夠直接發到npm上,讓其餘使用的該cli的成員一建安裝,好比npm install -g xxxxxxx, 具體教程的話百度,google有不少,做者表示很懶,遇到問題下面留言吧。。
能夠看到整個功能邏輯相對於平時寫的複雜的業務邏輯來講相對簡單,主要是工具庫的一些使用方面的東西,中間的難點可能就是node中概念性的一些東西,然而這些多看一下文檔基本就能夠解決,但願你們能夠從本文中瞭解到如何快速搭建一個屬於本身的cli工具
順便預告下後續的話可能會更新一些如何利用cli工具作到自動化發佈,版本號控制,環境變量切換,自動生成文檔等一系列有趣的功能