git 创建webpack项目_从0到1开发一个小程序cli脚手架(一)创建页面/组件模版篇...
github地址:
https://github.com/jinxuanzheng01/xdk-cli
cli工具是什么?
在正文之前先大致描述下什么是cli工具,
cli工具英文名command-line interface,也就是命令行交互接口,比較典型的幾個(gè)case例如,create-react-app,vue-cli,具體可以去百度一下,下面gif是小打卡目前用的一套自動(dòng)化發(fā)布工具?
可以看到整個(gè)發(fā)布流程大致是以選擇或默認(rèn)項(xiàng)的形式實(shí)現(xiàn),大致分析下面幾步
選擇打包形式 ? ?開(kāi)發(fā)模式/debug模式/發(fā)布模式
設(shè)置版本號(hào)
填寫(xiě)發(fā)布信息
選擇環(huán)境
是否提交版本commit
是不是非常無(wú)腦?是不是再也不用擔(dān)心線上發(fā)錯(cuò)環(huán)境了?有了它就算不同項(xiàng)目間,就算一天發(fā)n次版本還需要擔(dān)心什么呢?
當(dāng)然除了簡(jiǎn)單的發(fā)布功能還,還可以做很多的事情,比如創(chuàng)建page/component模版等一些更多有趣的事情
為了節(jié)約版面就不貼圖了,具體可以看下倉(cāng)庫(kù) ?https://github.com/jinxuanzheng01/xdk-cli(目前該工具是從小打卡現(xiàn)有的cli庫(kù)中抽離的部分功能)
明確痛點(diǎn)
也就是我為什么要做這么一個(gè)工具,其實(shí)最開(kāi)始我只是為了解決一個(gè)問(wèn)題,就是在整個(gè)發(fā)布流程中需要人工去改動(dòng)/確認(rèn)發(fā)布環(huán)境和版本信息,大致可以想象下把線下環(huán)境發(fā)布到線上的尷尬處境
后續(xù)發(fā)現(xiàn)從cli角度觸發(fā),很多東西都變得簡(jiǎn)單了,大致列了下:
環(huán)境變量切換(線上環(huán)境,線下環(huán)境)
創(chuàng)建啟動(dòng)模版,包括頁(yè)面,組件
自動(dòng)化發(fā)布
...
準(zhǔn)備工作
本文會(huì)以快速創(chuàng)建頁(yè)面模版文件為例教你怎么快速擼一個(gè)屬于自己的cli工具,
如果覺(jué)得自己做比較麻煩,可以clone下我的倉(cāng)庫(kù)自己改裝下
需要了解的三方庫(kù)
中間會(huì)用到一些第三方庫(kù)
commander, 一個(gè)解析命令行命令和參數(shù)工具
inquirer,常用交互式命令行用戶界面的集合
chalk,美化你的終端輸出樣式
fuzzy,字符串模糊匹配的插件,根據(jù)輸入關(guān)鍵詞進(jìn)行模糊匹配
json-format,json美化/格式化工具
其他的一些小知識(shí):比如path模塊,fs模塊,大家可以去node官網(wǎng)自行查看:https://nodejs.org/api/
搭建開(kāi)發(fā)環(huán)境
創(chuàng)建一個(gè)空文件夾,并且npm初始化, 并且創(chuàng)建一個(gè)index.js頁(yè)面,這個(gè)index.js將作為你整個(gè)包的入口文件
npm init -y安裝上述的三方包,當(dāng)然也可以后續(xù)按需安裝,這樣更能清楚每個(gè)包是做什么的
npm install @moyuyc/inquirer-autocomplete-prompt commander chalk commander fuzzy inquirer json-format --save在package.json里添加bin字段, 將自定義的命令軟連到全局環(huán)境,同時(shí)執(zhí)行npm link創(chuàng)建鏈接,這里如果報(bào)錯(cuò){code EACCES,errno:13,...},是因?yàn)闄?quán)限不足,可以嘗試sudo npm link
"bin": { "cli-demo": "./index.js" }在入口文件,index.js 行首加入一行#!/usr/bin/env node指定當(dāng)前腳本由node.js進(jìn)行解析
#!/usr/bin/env node// 輸出文本console.log('Hello World!!!');這時(shí)可以在命令行中執(zhí)行 cli-demo驗(yàn)收一下成果了
ok,可以看到當(dāng)在全局狀態(tài)下輸入自定義命令時(shí),正確運(yùn)行了入口文件,也就意味著的開(kāi)發(fā)玩具已經(jīng)搭建完成
Let‘ Go
整理邏輯
以快速創(chuàng)建頁(yè)面模版文件為例,就需要考慮需要哪些邏輯:
設(shè)置頁(yè)面名稱
找到已有模版文件
copy到項(xiàng)目中
修改app.json
識(shí)別命令行
在剛才的Hello World!!!環(huán)節(jié),已經(jīng)可以正確識(shí)別cli-demo,但是需要在一個(gè)cli工具中集成更多功能,可能需要有不同的執(zhí)行策略,以git為例:git clone, git status,git push,所以需要識(shí)別不同的命令和參數(shù),
是時(shí)候就需要用到commander這個(gè)第三方包幫助解析命令行參數(shù)了,當(dāng)然你也可以自己擼一個(gè)lib,本質(zhì)上還是方便解析process.argv
index.js (本質(zhì)上這個(gè)js就是一個(gè)路由)#!/usr/bin/env nodeconst version = require('./package').version; // 版本號(hào)/* = package import-------------------------------------------------------------- */const program = require('commander'); // 命令行解析/* = task events-------------------------------------------------------------- */const createProgramFs = require('./lib/create-program-fs'); // 創(chuàng)建項(xiàng)目文件/* = config-------------------------------------------------------------- */// 設(shè)置版本號(hào)program.version(version, '-v, --version');/* = deal receive command-------------------------------------------------------------- */program .command('create') .description('創(chuàng)建頁(yè)面或組件') .action((cmd, options) => createProgramFs(cmd));/* 后續(xù)可以根據(jù)不同的命令進(jìn)行不同的處理,可以簡(jiǎn)單的理解為路由 */// program// .command('build [cli]')// .description('執(zhí)行打包構(gòu)建')// .action((cmd, env) => callback);/* = main entrance-------------------------------------------------------------- */program.parse(process.argv)這時(shí)候當(dāng)鍵入cli-demo create時(shí)會(huì)自動(dòng)執(zhí)行createProgramFs
// createProgramFs.jsmodule.exports = function () { console.log('Hi, create-program-fs.js');};命令行輸入``cli-demo create``
可以看到已經(jīng)成功的開(kāi)辟出了一塊獨(dú)立的業(yè)務(wù)模塊,后續(xù)就只需要依據(jù)需求填補(bǔ)相應(yīng)的內(nèi)容即可
創(chuàng)建交互命令
收到執(zhí)行命令,這個(gè)時(shí)候按第一張圖,是需要開(kāi)始一系列QA(當(dāng)然你也可以不做交互式,直接配置命令行參數(shù)),
引入三方包 inquirer,來(lái)指定問(wèn)題隊(duì)列
const question = [ // 選擇模式使用 page -> 創(chuàng)建頁(yè)面 | component -> 創(chuàng)建組件 { type: 'list', name: 'mode', message: '選擇想要?jiǎng)?chuàng)建的模版', choices: [ 'page', 'component', ] }, // 設(shè)置名稱 { type: 'input', name: 'name', message: answer => `設(shè)置 ${answer.mode} 名稱 (e.g: index):`, },];module.exports = function() { // 問(wèn)題執(zhí)行 inquirer.prompt(question).then(answers => { console.log(answers); });};、
可以看到通過(guò)一系列QA交互,實(shí)際輸出拿到的是一個(gè)json對(duì)象,第一步已完成
創(chuàng)建模版文件
創(chuàng)建一個(gè)存放模版文件的文件夾template,并準(zhǔn)備好你希望的模版
項(xiàng)目中創(chuàng)建模版文件
為了方便閱讀,下面的代碼,需要明確下面變量的定義,
Config.dir_root ?= 命令行執(zhí)行目錄
Config.root ?= cli項(xiàng)目根目錄
Config.appRoot = 小程序項(xiàng)目路徑
Config.template = 模版目錄
這里有兩個(gè)點(diǎn),一個(gè)是執(zhí)行路徑的問(wèn)題,另一個(gè)是分包的問(wèn)題,具體如下:
執(zhí)行路徑
這里一定要弄明白__dirname, process.cwd()的區(qū)別,同時(shí)還有一些小程序是自己搭的gulp/webpack,可能小程序項(xiàng)目是在src目錄下,一定要分清楚
__dirname:被執(zhí)行js文件的絕對(duì)路徑,一般在index.js執(zhí)行時(shí)緩存起來(lái)作為項(xiàng)目的全局路徑,比如找到template文件夾就會(huì)使用 `${__dirname}/template`
process.cwd():當(dāng)前命令行運(yùn)行時(shí)的工作目錄,比如在/Users/xuan/Documents/cli-demo
如果當(dāng)前項(xiàng)目在src,或其他文件夾里怎么辦?可以提供一個(gè)給用戶項(xiàng)目中的配置文件,類(lèi)似于gulpfile.js或是webpack.config.js的形式,內(nèi)容例如(具體可以看git倉(cāng)庫(kù))
可以看到對(duì)象中app屬性,可以指定你當(dāng)前小程序項(xiàng)目的路徑
分包
因?yàn)樾〕绦虻姆职鼨C(jī)制會(huì)導(dǎo)致頁(yè)面實(shí)際路徑與在主包的路徑不相符,例如:
主包:pages/index/index
分包:pages/main_module/pages/habit_enlist/habit_enlist
解決這個(gè)問(wèn)題一方面是要有頁(yè)面創(chuàng)建要有一定的規(guī)范,統(tǒng)一格式,另一方面需要根據(jù)規(guī)則解析app.json,
上面的主包,分包路徑差不多是我目前使用的規(guī)范
解析app.json
// 獲取app.jsonfunction getAppJson() { let appJsonRoot = path.join(Config.appRoot, '/app.json'); try { return require(appJsonRoot); }catch (e) { Log.error(`未找到app.json, 請(qǐng)檢查當(dāng)前文件目錄是否正確,path: ${appJsonRoot}`); process.exit(1); // 異常退出 }}// 解析app.jsonlet parseAppJson = () => { // app Json 原文件 let appJson = __Data__.appJson = getAppJson(); // 獲取主包頁(yè)面 appJson.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = ''); // 獲取分包,頁(yè)面列表 appJson.subPackages.forEach(item => { __Data__.appModuleList[getPathSubSting(item.root)] = item.root; item.pages.forEach(path => __Data__.appPagesList[getPathSubSting(path)] = item.root); });};// __Data__.appPagesList = 小程序全部頁(yè)面// __Data__.appModuleList = 小程序全部分包頁(yè)面// item結(jié)構(gòu) {util_module: 'pages/util_module/'},這么定義結(jié)構(gòu)是為了方便后續(xù)取數(shù)question隊(duì)列里,增加刪選分包的選項(xiàng) // 設(shè)置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類(lèi)型本質(zhì)上是個(gè)列表,但是可以進(jìn)行模糊查詢,非常方便,像小打卡有接近30個(gè)分包的情況下效果尤為明顯
有了文件名,有了分包路徑,有了可供copy的模版,接下來(lái)就很簡(jiǎn)單了,把模版文件塞進(jìn)項(xiàng)目就可以了,下面是一串從倉(cāng)庫(kù)里copy的代碼,利用async/await很方便的寫(xiě)出一維代碼,基本上的流程:
獲取路徑 -> 校驗(yàn) -> 獲取文件信息 -> 復(fù)制文件 -> 修改app.json -> 輸出結(jié)果信息
async function createPage(name, modulePath = '') { // 獲取模版文件路徑 let templateRoot = path.join(Config.template, '/page'); if (!Util.checkFileIsExists(templateRoot)) { Log.error(`未找到模版文件, 請(qǐng)檢查當(dāng)前文件目錄是否正確,path: ${templateRoot}`); return; } // 獲取業(yè)務(wù)文件夾路徑 let page_root = path.join(Config.appRoot, modulePath, '/pages', name); // 查看文件夾是否存在 let isExists = await Util.checkFileIsExists(page_root); if (isExists) { Log.error(`當(dāng)前頁(yè)面已存在,請(qǐng)重新確認(rèn), path: ` + page_root); return; } // 創(chuàng)建文件夾 await Util.createDir(page_root); // 獲取文件列表 let files = await Util.readDir(templateRoot); // 復(fù)制文件 await Util.copyFilesArr(templateRoot, `${page_root}/${name}`, files); // 填充app.json await writePageAppJson(name, modulePath); // 成功提示 Log.success(`createPage success, path: ` + page_root);}擴(kuò)展
一個(gè)基本的快速創(chuàng)建頁(yè)面模版的cli工具就這樣完成,但是有可能需要更多的一些功能
自定義模版
比如說(shuō)每個(gè)項(xiàng)目的模版都有可能不太一樣,很大程度上需要根據(jù)項(xiàng)目進(jìn)行定制,這時(shí)候可能就需要前文提到的給用戶開(kāi)放config文件的插槽了
項(xiàng)目中的config:
// xdk.config.jsmodule.exports = { // 小程序路徑 app: './', // 模版文件夾 template: './template'};// create-program-fs.jsmodule.exports = function() { // 校驗(yàn):當(dāng)前是否存在配置文件 let customConfPath = `${Config.dir_root}/xdk.config.js`; if (!Util.checkFileIsExists(customConfPath)) { Log.error('當(dāng)前項(xiàng)目尚未創(chuàng)建xdk.config.js文件'); return; } // 獲取用戶配置項(xiàng) let {app, template = ''} = require(customConfPath); // 小程序目錄 Config.appRoot = path.resolve(path.join(Config.dir_root, app)); // 模版文件目錄(默認(rèn)使用cli提供的默認(rèn)模版,當(dāng)config文件有設(shè)置template路徑時(shí),使用自定義路徑) !!template && (Config.template = path.resolve(path.join(Config.dir_root, template)))); // 問(wèn)題執(zhí)行 inquirer.prompt(question).then(answers => { console.log(answers); });};發(fā)布的npm倉(cāng)庫(kù)
目前從開(kāi)發(fā)到調(diào)試本質(zhì)上是在本地提供服務(wù),利用npm link提供軟連接到全局PATH,
其實(shí)也可以直接發(fā)到npm上,讓其他使用的該cli的成員一建安裝,比如``npm install -g xxxxxxx``
教程的話百度,google有很多,作者表示很懶,遇到問(wèn)題下面留言吧。。
最后
可以看到整個(gè)功能邏輯相對(duì)于平時(shí)寫(xiě)的復(fù)雜的業(yè)務(wù)邏輯來(lái)說(shuō)相對(duì)簡(jiǎn)單,主要是工具庫(kù)的一些使用方面的東西,中間的難點(diǎn)可能就是node中概念性的一些東西,然而這些多看一下文檔基本就可以解決,希望大家可以從本文中了解到如何快速搭建一個(gè)屬于自己的cli工具
順便預(yù)告下后續(xù)的話可能會(huì)更新一些如何利用cli工具做到自動(dòng)化發(fā)布,版本號(hào)控制,環(huán)境變量切換,自動(dòng)生成文檔等一系列有趣的功能
總結(jié)
以上是生活随笔為你收集整理的git 创建webpack项目_从0到1开发一个小程序cli脚手架(一)创建页面/组件模版篇...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 微信公众号——永久素材的上传
- 下一篇: Linux利用nginx-gridfs搭