從0到1開發一個小程式cli腳手架(一)--建立頁面/元件模版篇

萩水發表於2019-07-18

github地址:github.com/jinxuanzhen…,有興趣的同學可以體驗一下

原文地址:www.yuque.com/docs/share/…

cli工具是什麼?

在正文之前先大致描述下什麼是cli工具,
cli工具英文名command-line interface,也就是命令列互動介面,比較典型的幾個case例如,create-react-app,vue-cli,具體可以去百度一下,下面gif是小打卡目前用的一套自動化釋出工具?

QQ20190716-183106-HD (1).gif

可以看到整個釋出流程大致是以選擇或預設項的形式實現,大致分析下面幾步

  • 選擇打包形式    開發模式/debug模式/釋出模式
  • 設定版本號
  • 填寫釋出資訊
  • 選擇環境
  • 是否提交版本commit

是不是非常無腦?是不是再也不用擔心線上發錯環境了?有了它就算不同專案間,就算一天發n次版本還需要擔心什麼呢?

當然除了簡單的釋出功能還,還可以做很多的事情,比如建立page/component模版等一些更多有趣的事情

為了節約版面就不貼圖了,具體可以看下倉庫  github.com/jinxuanzhen…(目前該工具是從小打卡現有的cli庫中抽離的部分功能)

明確痛點

也就是我為什麼要做這麼一個工具,其實最開始我只是為了解決一個問題,就是在整個釋出流程中需要人工去改動/確認釋出環境和版本資訊,大致可以想象下把線下環境釋出到線上的尷尬處境

後續發現從cli角度觸發,很多東西都變得簡單了,大致列了下:

  • 環境變數切換(線上環境,線下環境)
  • 建立啟動模版,包括頁面,元件
  • 自動化釋出
  • ...

準備工作

本文會以快速建立頁面模版檔案為例教你怎麼快速擼一個屬於自己的cli工具,
如果覺得自己做比較麻煩,可以clone下我的倉庫自己改裝下

需要了解的三方庫

中間會用到一些第三方庫

  • commander, 一個解析命令列命令和引數工具
  • inquirer,常用互動式命令列使用者介面的集合
  • chalk,美化你的終端輸出樣式
  • fuzzy,字串模糊匹配的外掛,根據輸入關鍵詞進行模糊匹配
  • json-format,json美化/格式化工具

其他的一些小知識:比如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驗收一下成果了

image.png

ok,可以看到當在全域性狀態下輸入自定義命令時,正確執行了入口檔案,也就意味著的開發玩具已經搭建完成

Let‘ Go

整理邏輯

以快速建立頁面模版檔案為例,就需要考慮需要哪些邏輯:

  • 設定頁面名稱
  • 找到已有模版檔案
  • copy到專案中
  • 修改app.json

識別命令列

在剛才的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

image.png

可以看到已經成功的開闢出了一塊獨立的業務模組,後續就只需要依據需求填補相應的內容即可

建立互動命令

收到執行命令,這個時候按第一張圖,是需要開始一系列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);
    });
};
複製程式碼

demo1 (1).gif

可以看到通過一系列QA互動,實際輸出拿到的是一個json物件,第一步已完成

建立模版檔案

建立一個存放模版檔案的資料夾template,並準備好你希望的模版

image.png

專案中使用模版檔案

為了方便閱讀,下面的程式碼,需要明確下面變數的定義, 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屬性,可以指定你當前小程式專案的路徑

分包

因為小程式的分包機制會導致頁面實際路徑與在主包的路徑不相符,例如:

  • 主包:pages/index/index
  • 分包:pages/main_module/pages/habit_enlist/habit_enlist

解決這個問題一方面是要有頁面建立要有一定的規範,統一格式,另一方面需要根據規則解析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個分包的情況下效果尤為明顯

QQ20190717-162222 (1).gif

有了檔名,有了分包路徑,有了可供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倉庫

目前從開發到除錯本質上是在本地提供服務,利用npm link提供軟連線到全域性PATH,
其實也可以直接發到npm上,讓其他使用的該cli的成員一建安裝,比如npm install -g xxxxxxx, 具體教程的話百度,google有很多,作者表示很懶,遇到問題下面留言吧。。

最後

可以看到整個功能邏輯相對於平時寫的複雜的業務邏輯來說相對簡單,主要是工具庫的一些使用方面的東西,中間的難點可能就是node中概念性的一些東西,然而這些多看一下文件基本就可以解決,希望大家可以從本文中瞭解到如何快速搭建一個屬於自己的cli工具

順便預告下後續的話可能會更新一些如何利用cli工具做到自動化釋出,版本號控制,環境變數切換,自動生成文件等一系列有趣的功能

下文地址: 從0到1開發一個小程式cli腳手架(二) --版本釋出/管理篇

相關文章