手把手帶你搭建一個node cli的方法示例
前言
前端日常開發(fā)中,會遇見各種各樣的 cli,使用 vue 技術棧的你一定用過 @vue/cli
,同樣使用 react 技術棧的人也一定知道 create-react-app
。利用這些工具能夠實現一行命令生成我們想要的代碼模版,極大地方便了我們的日常開發(fā),讓計算機自己去干繁瑣的工作,而我們,就可以節(jié)省出大量的時間用于學習、交流、開發(fā)。
cli 工具的作用在于它能夠將我們開發(fā)過程中經常需要重復做的事情利用一行代碼來解決,比如我們在寫需求的時候每新增一個頁面就需要相應的增加該頁面的初始化代碼,而相同文件類型的初始化代碼往往是一樣的,比如 example.vue。同時我們還需要增加對應的路由,比如在 router.js 中增加對應的路由規(guī)則。這些工作都是很繁瑣又重復的,每次遇到這種情況都重復一遍嗎?是時候作出改變了,編寫自己的 cli 工具,一行命令,3 秒鐘進入 coding 狀態(tài)!
本文以自己的 fc-vue-cli 為例,將開發(fā)到發(fā)布過程完整記錄下來,看完本文,你將學會如何從零開發(fā)一個 cli 項目,以及如何使用 npm 發(fā)布自己的包。
提前放上該項目地址
源代碼地址: 源代碼
npm 地址: npm
原文地址(github上):
要實現的功能
fc-vue add-page
通過這行命令來新增一個頁面的模版文件,省去了手動新建文件,手動復制初始化代碼的麻煩,同時添加上對應的路由配置
腳手架的名字定為 fc-vue,這個是通過 package.json 里面的 name 字段來定義的。
目錄結構
入口 (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支持"/"來創(chuàng)建子目錄例如:add-page user/login\n使用時,支持 fc-vue add-page 【回車】 來選擇輸入信息' ) .option('-s, --simple', '創(chuàng)建簡單版的頁面,只新增一個.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()); //檢查上下文環(huán)境,并返回目標文件目錄路徑 let { destDir, destDirRootName, rootDir } = checkContext( name, cmdObj, 'page' ); //復制模版到目標文件 let { destFile } = copyTemplate(destDir, simple, cssType); if (fs.existsSync(destFile)) { await addRouter(name, rootDir, simple, destDirRootName, title); log(`成功創(chuàng)建${name},請在${destDir}下查看`); } else { console.error( chalk.red(`創(chuàng)建失敗,請到項目【根目錄】或者【@src】目錄下執(zhí)行該操作`) ); } } catch (error) { console.error(chalk.red(error)); console.error( chalk.red( `創(chuàng)建頁面失敗,請確保在項目【根目錄】或者【@src】目錄下執(zhí)行該操作\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', type: 'input', message: '請輸入頁面標題(meta.title)', }, { type: 'list', name: 'SIMPLE', message: 'What is the template type?', choices: [ 'normal:【同時創(chuàng)建 .vue .js .[style]】 ', 'simple: 【只創(chuàng)建 .vue】', ], filter: function (val) { return val.split(':')[0] === 'simple' ? true : false; }, }, ]; return inquirer.prompt(questions); };
檢查用戶執(zhí)行命令時所在的環(huán)境 (packages/lib/checkContext.js)
因為我們不確定用戶會不會按照我們所期望的方式來使用,所以在這里我們加上一些判斷,來確保用戶的行為規(guī)范,否則就拋出錯誤,提示用戶該怎么使用。主要就是確保用戶在項目根目錄或者 src 目錄路徑下執(zhí)行命令。然后還要確認用戶所在項目的目錄結構是否符合我們所提供的規(guī)范(基本上也是社區(qū)的規(guī)范)。最后當然還要判斷下這個需要添加的頁面是否已經存在。
const fs = require('fs'); const path = require('path'); const { error } = require('./util'); /** * 檢查 用戶是否在項目根目錄或者./src目錄下執(zhí)行,是否有約定的項目目錄結構,是否已經存在該組件 * @param {Stirng} name * @param {Object} cmdObj * @return {Object} {destDirRootName ,destDir,rootDir} 目標文件夾名稱,目標文件路徑,項目所在目錄 */ const checkContext = (name, cmdObj, type) => { // console.log(process.cwd()); let destDir, destDirRoot, destDirRootName; const curDir = path.resolve('.'); let rootDir = '.'; const basename = path.basename(curDir); //兼容 用戶在 ./src目錄下執(zhí)行該命令 if (basename === 'src') { rootDir = path.resolve('..', rootDir); } //判斷下項目根目錄rootDir下面有沒有src目錄,如果沒有那說明用戶沒有在正確的路徑下執(zhí)行該命令 if (!fs.existsSync(path.join(rootDir, 'src'))) { error(`創(chuàng)建頁面失敗,請到項目【根目錄】或者【@src】目錄下執(zhí)行該操作`); process.exit(1); } // -c if (type === 'component') { //創(chuàng)建一個組件。兼容組件不同的目錄名稱 支持 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('您的通用組件存放文件目錄不符合規(guī)范,請將其放在 /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( '您的頁面組件存放文件目錄不符合規(guī)范,請將其放在 /src/view 或者 /src/pages 或者 /src/page 目錄' ); } } //是否已經存在該組件 if ( (cmdObj.simple && fs.existsSync(destDir + '.vue')) || (!cmdObj.simple && fs.existsSync(destDir + '/index.vue')) ) { error(`${name} 頁面/組件 已經存在,創(chuàng)建失敗!`); process.exit(1); } return { destDirRootName, destDir, rootDir }; }; module.exports = checkContext;
復制模版到目標路徑 (packages/lib/copy-template.js)
當確認過上下文環(huán)境,拿到了用戶的輸入參數,這個時候我們就可以愉快的進行頁面添加工作了,也就是復制我們事先準備好的模版到目標文件。這里需要考慮用戶選擇的是 normal 還是 simple 類型的根據不同的類型來添加不通的頁面模版。當然同時還支持 less,scss 等。 比如用戶執(zhí)行 fc-vue add-page user/login --title=登錄頁
這個時候將會在 src/views/user/login
下創(chuàng)建初始化的模版文件包括 .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, simple, cssType) => { let sourceDir, destFile; // -s if (simple) { //創(chuàng)建一個簡單版.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', sourceDir, 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。 比如用戶執(zhí)行 fc-vue add-page user/login --title=登錄頁
,那么在 src/router/index.js 里面就會加上一條路由規(guī)則,如下(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, rootDir, simple, destDirRootName, 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( '您的項目路由文件不符合規(guī)范,請將其放在/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;
發(fā)布到 npm
主要是配置好 package.json 文件。bin 里面定義好 npm 包的入口。
"name": "fc-vue", "version": "1.0.6", "bin": { "fc-vue": "bin/index.js" },
運行npm login 先登錄
npm publish 發(fā)布,每次發(fā)布的版本號不能重復復制代碼
安裝使用
$ npm i -g fc-vue $ fc-vue add-page
使用演示
結束
這樣就實現了一個簡單的 fc-vue add-page 功能,是不是很簡單。
源代碼地址: 源代碼
npm 地址:npm
到此這篇關于手把手帶你搭建一個 node cli的文章就介紹到這了,更多相關手把手帶你搭建一個 node cli內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
node.js express中app.param的用法詳解
express.js是nodejs的一個MVC開發(fā)框架,并且支持jade等多種模板。下面這篇文章主要給大家介紹了關于node.js express中app.param用法的相關資料,文中通過示例代碼介紹的非常詳細,需要的朋友可以參考借鑒,下面來一起看看吧。2017-07-07