1. 程式人生 > 程式設計 >手把手帶你搭建一個node cli的方法示例

手把手帶你搭建一個node cli的方法示例

前言

前端日常開發中,會遇見各種各樣的 cli,使用 vue 技術棧的你一定用過 @vue/cli,同樣使用 react 技術棧的人也一定知道 create-react-app 。利用這些工具能夠實現一行命令生成我們想要的程式碼模版,極大地方便了我們的日常開發,讓計算機自己去幹繁瑣的工作,而我們,就可以節省出大量的時間用於學習、交流、開發。

cli 工具的作用在於它能夠將我們開發過程中經常需要重複做的事情利用一行程式碼來解決,比如我們在寫需求的時候每新增一個頁面就需要相應的增加該頁面的初始化程式碼,而相同檔案型別的初始化程式碼往往是一樣的,比如 example.vue。同時我們還需要增加對應的路由,比如在 router.js 中增加對應的路由規則。這些工作都是很繁瑣又重複的,每次遇到這種情況都重複一遍嗎?是時候作出改變了,編寫自己的 cli 工具,一行命令,3 秒鐘進入 coding 狀態!

本文以自己的 fc-vue-cli 為例,將開發到釋出過程完整記錄下來,看完本文,你將學會如何從零開發一個 cli 專案,以及如何使用 npm 釋出自己的包。

提前放上該專案地址

原始碼地址: 原始碼

npm 地址: npm

原文地址(github上):

github

要實現的功能

fc-vue add-page
通過這行命令來新增一個頁面的模版檔案,省去了手動新建檔案,手動複製初始化程式碼的麻煩,同時新增上對應的路由配置

腳手架的名字定為 fc-vue,這個是通過 package.json 裡面的 name 欄位來定義的。

目錄結構

手把手帶你搭建一個node cli的方法示例

入口 (bin/index.js)

入口檔案只做了一件事,那就是判斷當前node的版本是否大於10,如果版本號<10則提醒使用者升級node

#!/usr/bin/env node

// 'use strict';
const chalk = require('chalk');

const currentNodeVersion = process.versions.node;
const major = currentNodeVersion.split('.')[0];
if (major < 10) {
 console.error(
 chalk.red(
  `You are running Node \n${currentNodeVersion} \nvue-assist-cli requires Node 10 or higher.\nPlease update your version of Node`
 )
 );
 process.exit(1);
}

require('../packages/init');

初始化命令 (packages/init.js)

在這裡初始化你要實現的命令,比如我要實現 add-page 功能,這裡要用到的 commander 庫。

const { program } = require('commander');
const { log } = require('./lib/util');

// 初始化版本,我們直接獲取package.json裡面的版本號就可以了
program.version(require('../package.json').version);
//開始新增命令 [name] 說明這個引數是可選的,我們想做到相容不同的使用方法所以把這個引數設定未可選
//.description裡面可以寫上這個命名的一些描述,當用戶fc-vue help add-page 的時候可以提供幫助文件
//.option 用來新增可選的引數
//.action用來響應使用者的輸入,這裡我們單獨用一個檔案./commands/add-page來處理
program
 .command('add-page [name]')
 .description(
  'add a page,預設加在./src/views 或 ./src/pages 或./src/page目錄下,同時新增路由\n支援"/"來建立子目錄例如:add-page user/login\n使用時,支援 fc-vue add-page 【回車】 來選擇輸入資訊'
 )
 .option('-s,--simple','建立簡單版的頁面,只新增一個.vue檔案')
 .option('-t,--title <title>','頁面標題')
 .action(require('./commands/add-page'))
 .on('--help',() => {
 log('支援 fc-vue add-page 【回車】 來選擇輸入資訊');
 });
//格式化命令列引數
program.parse(process.argv);

處理使用者輸入的命令 (packages/commands/add-page.js)

這裡需要使用到幾個庫, shelljs 用來處理 shell 命令的,我們用來操作檔案, chalk 用來給列印輸出增加樣式。函式通過 name,cmdObj 來獲取使用者的輸入,其中 name 是.command('add-page [name]')裡面的 name,cmdObj 物件裡面則包括其他引數

const fs = require('fs');
const shell = require('shelljs');
const chalk = require('chalk');
const { askQuestions,askCss } = require('../lib/ask-page');
const checkContext = require('../lib/checkContext');
const copyTemplate = require('../lib/copy-template');
const addRouter = require('../lib/add-router');
const { error,log,success } = require('../lib/util');
shell.config.fatal = true;

module.exports = async (name,cmdObj) => {
 try {
 //預設使用less,let cssType = 'less';
 let simple = cmdObj.simple;
 let title = cmdObj.title;
 if (!name && (simple || title)) {
  error('錯誤的命令,缺少頁面名稱');
  process.exit(1);
 }
 //如果使用者沒有輸入name,[fc-vue add-page] 則進入問答模式,通過一問一答獲取使用者的輸入
 if (!name) {
  const answers = await askQuestions();
  // console.log(answers);
  name = answers.FILENAME;
  title = answers.TITLE;
  simple = answers.SIMPLE;
  if (!simple) {
  const res = await askCss();
  cssType = res.CSS_TYPE;
  }
 }
 //其他情況則可以通過option拿到引數
 // console.log(process.cwd());
 //檢查上下文環境,並返回目標檔案目錄路徑
 let { destDir,destDirRootName,rootDir } = checkContext(
  name,cmdObj,'page'
 );
 //複製模版到目標檔案
 let { destFile } = copyTemplate(destDir,simple,cssType);

 if (fs.existsSync(destFile)) {
  await addRouter(name,rootDir,title);
  log(`成功建立${name},請在${destDir}下檢視`);
 } else {
  console.error(
  chalk.red(`建立失敗,請到專案【根目錄】或者【@src】目錄下執行該操作`)
  );
 }
 } catch (error) {
 console.error(chalk.red(error));
 console.error(
  chalk.red(
  `建立頁面失敗,請確保在專案【根目錄】或者【@src】目錄下執行該操作\n,否則請聯絡@zhongyi`
  )
 );
 }
};

問答模式 (packages/lib/ask-page.js)

這裡需要用到 inquirer 。這個就很簡單了,基本上就是以陣列的方式列出你想讓使用者輸入的內容,每個問題的互動可以選擇 input 輸入,list 選擇等等。在這裡獲取到的使用者輸入我們就可以在 packages/commands/add-page.js 呼叫,然後拿到這些引數。

const inquirer = require('inquirer');

const askQuestions = () => {
 const questions = [
 {
  name: 'FILENAME',type: 'input',message: '請輸入頁面的名稱?[支援多級目錄,例如:user/login]',},{
  name: 'TITLE',message: '請輸入頁面標題(meta.title)',{
  type: 'list',name: 'SIMPLE',message: 'What is the template type?',choices: [
  'normal:【同時建立 .vue .js .[style]】 ','simple: 【只建立 .vue】',],filter: function (val) {
  return val.split(':')[0] === 'simple' ? true : false;
  },];
 return inquirer.prompt(questions);
};

檢查使用者執行命令時所在的環境 (packages/lib/checkContext.js)

因為我們不確定使用者會不會按照我們所期望的方式來使用,所以在這裡我們加上一些判斷,來確保使用者的行為規範,否則就丟擲錯誤,提示使用者該怎麼使用。主要就是確保使用者在專案根目錄或者 src 目錄路徑下執行命令。然後還要確認使用者所在專案的目錄結構是否符合我們所提供的規範(基本上也是社群的規範)。最後當然還要判斷下這個需要新增的頁面是否已經存在。

const fs = require('fs');
const path = require('path');
const { error } = require('./util');
/**
 * 檢查 使用者是否在專案根目錄或者./src目錄下執行,是否有約定的專案目錄結構,是否已經存在該元件
 * @param {Stirng} name
 * @param {Object} cmdObj
 * @return {Object} {destDirRootName,destDir,rootDir} 目標資料夾名稱,目標檔案路徑,專案所在目錄
 */
const checkContext = (name,type) => {
 // console.log(process.cwd());
 let destDir,destDirRoot,destDirRootName;
 const curDir = path.resolve('.');
 let rootDir = '.';
 const basename = path.basename(curDir);

 //相容 使用者在 ./src目錄下執行該命令
 if (basename === 'src') {
 rootDir = path.resolve('..',rootDir);
 }
 //判斷下專案根目錄rootDir下面有沒有src目錄,如果沒有那說明使用者沒有在正確的路徑下執行該命令
 if (!fs.existsSync(path.join(rootDir,'src'))) {
 error(`建立頁面失敗,請到專案【根目錄】或者【@src】目錄下執行該操作`);
 process.exit(1);
 }
 // -c
 if (type === 'component') {
 //建立一個元件。相容元件不同的目錄名稱 支援 src/components src/component 三種任一種

 if (fs.existsSync(path.resolve(rootDir,'src/components'))) {
  destDir = path.resolve(rootDir,'src/components',name);
 } else if (fs.existsSync(path.resolve(rootDir,'src/component'))) {
  destDir = path.resolve(rootDir,'src/component',name);
 } else {
  error('您的通用元件存放檔案目錄不符合規範,請將其放在 /src/components下');
 }
 } else {
 // 相容路由頁面不同的目錄名稱 支援 src/views src/pages src/page 三種任一種
 if (fs.existsSync(path.resolve(rootDir,'src/views'))) {
  destDir = path.resolve(rootDir,'src/views',name);
  destDirRootName = 'views';
 } else if (fs.existsSync(path.resolve(rootDir,'src/pages'))) {
  destDir = path.resolve(rootDir,'src/pages',name);
  destDirRootName = 'pages';
 } else if (fs.existsSync(path.resolve(rootDir,'src/page'))) {
  destDir = path.resolve(rootDir,'src/page',name);
  destDirRootName = 'page';
 } else {
  error(
  '您的頁面元件存放檔案目錄不符合規範,請將其放在 /src/view 或者 /src/pages 或者 /src/page 目錄'
  );
 }
 }

 //是否已經存在該元件
 if (
 (cmdObj.simple && fs.existsSync(destDir + '.vue')) ||
 (!cmdObj.simple && fs.existsSync(destDir + '/index.vue'))
 ) {
 error(`${name} 頁面/元件 已經存在,建立失敗!`);
 process.exit(1);
 }
 return { destDirRootName,rootDir };
};

module.exports = checkContext;

複製模版到目標路徑 (packages/lib/copy-template.js)

當確認過上下文環境,拿到了使用者的輸入引數,這個時候我們就可以愉快的進行頁面新增工作了,也就是複製我們事先準備好的模版到目標檔案。這裡需要考慮使用者選擇的是 normal 還是 simple 型別的根據不同的型別來新增不通的頁面模版。當然同時還支援 less,scss 等。 比如使用者執行 fc-vue add-page user/login --title=登入頁 這個時候將會在 src/views/user/login 下建立初始化的模版檔案包括 .js .vue .less

const shell = require('shelljs');
const path = require('path');
shell.config.fatal = true;

/**
 *
 * @param {String} destDir 目標檔案路徑
 * @param {Boolean} simple
 * @param {less,scss,sass,stylus} cssType
 * @return { sourceDir,destFile} 模版原檔案,生成的目標檔案
 */
const copyTemplate = (destDir,cssType) => {
 let sourceDir,destFile;
 // -s
 if (simple) {
 //建立一個簡單版.vue檔案
 sourceDir = path.resolve(
  __dirname,'../../template/vue-page-simple-template.vue'
 );
 shell.mkdir('-p',destDir.slice(0,destDir.lastIndexOf('/')));
 destDir += '.vue';
 shell.cp('-R',sourceDir,destDir);
 destFile = destDir;
 } else {
 shell.mkdir('-p',destDir);
 sourceDir = path.resolve(
  __dirname,`../../template/vue-page-template-${cssType}/*`
 );
 shell.cp('-R',destDir);
 destFile = path.resolve(destDir,'index.vue');
 }
 return { sourceDir,destFile };
};

module.exports = copyTemplate;

新增路由 (package/lib/add-router.js)

新增頁面模版的同時我們希望能夠自動配置上路由。其實思路很簡單,就是讀取 router.js 然後往裡面插入使用者新增的頁面所在的路由。我們約定 src/views 目錄下面的元件都是頁面級的,也就是說/user/login/index.vue 對應的路由就是/user/login。 比如使用者執行 fc-vue add-page user/login --title=登入頁,那麼在 src/router/index.js 裡面就會加上一條路由規則,如下(src/router/index.js)

import Vue from 'vue';
import VueRouter from 'vue-router';
import Home from '../views/Home.vue';
Vue.use(VueRouter);
const routes = [
******這裡有很多其他程式碼*****
 {
  path: '/user/login',name: 'user/login',meta: {
  title: '登入頁'
  },component: () =>
  import(/* webpackChunkName: "user/login" */ './views/user/login/index.vue'),}
 ];

const router = new VueRouter({
 mode: 'history',base: process.env.BASE_URL,routes,});

export default router;

回到新增路由配置的實現,packages/lib/add-router.js。

const fs = require('fs');
const path = require('path');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);

/**
 *
 * @param {String} name 頁面名稱
 * @param {String} rootDir 專案所在目錄
 * @param {Boolean} simple 簡單模式
 * @param {String} destDirRootName 目標資料夾的名稱 pages views page
 * @param {String} title 頁面標題
 */
const addRouter = async (name,title) => {
 let routerPath,pagePath;
 if (fs.existsSync(path.resolve(rootDir,'./src/router.js'))) {
 routerPath = path.resolve(rootDir,'./src/router.js');
 } else if (fs.existsSync(path.resolve(rootDir,'./src/router/index.js'))) {
 routerPath = path.resolve(rootDir,'./src/router/index.js');
 } else {
 error(
  '您的專案路由檔案不符合規範,請將其放在/src/router.js或者/src/router/index.js'
 );
 }
 pagePath = `./${destDirRootName}/${name}/index.vue`;
 if (simple) {
 pagePath = `./${destDirRootName}/${name}.vue`;
 }
 try {
 let content = await readFile(routerPath,'utf-8');
 //找到 const routes = 與 ]; 之間的內容,也就是routes陣列
 const reg = /const\s+routes\s*\=([\s\S]*)\]\s*\;/;

 const pathStr = `path: '/${name}',`;
 const nameStr = `name: '${name}',`;
 const metaStr = title
  ? `meta: {
  title: '${title}'
  },`
  : '';
 let componentStr = `component: () =>
  import(/* webpackChunkName: "${name}" */ '${pagePath}'),`;

 content = content.replace(reg,function (match,$1,index) {
  $1 = $1.trim();
  if (!$1.endsWith(',')) {
  $1 += ',';
  }
  if (title) {
  return `const routes = ${$1}
 {
 ${pathStr}
 ${nameStr}
 ${metaStr}
 ${componentStr}
 }
];`;
  } else {
  return `const routes = ${$1}
 {
 ${pathStr}
 ${nameStr}
 ${componentStr}
 }
];`;
  }
 });
 try {
  await writeFile(routerPath,content,'utf-8');
 } catch (err) {
  error(err);
 }
 } catch (err) {
 error(err);
 }
};

module.exports = addRouter;

釋出到 npm

主要是配置好 package.json 檔案。bin 裡面定義好 npm 包的入口。

 "name": "fc-vue","version": "1.0.6","bin": {
 "fc-vue": "bin/index.js"
 },

執行npm login 先登入

npm publish 釋出,每次釋出的版本號不能重複複製程式碼

安裝使用

$ npm i -g fc-vue
$ fc-vue add-page

使用演示

手把手帶你搭建一個node cli的方法示例

結束

這樣就實現了一個簡單的 fc-vue add-page 功能,是不是很簡單。

原始碼地址: 原始碼

npm 地址:npm

到此這篇關於手把手帶你搭建一個 node cli的文章就介紹到這了,更多相關手把手帶你搭建一個 node cli內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!