手把手带你搭建一个node cli的方法示例


Posted in Javascript onAugust 07, 2020

前言

前端日常开发中,会遇见各种各样的 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, simple, destDirRootName, 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',
  type: 'input',
  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, cmdObj, 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, destDir, 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, simple, 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', 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。 比如用户执行 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, 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(
  '您的项目路由文件不符合规范,请将其放在/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内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
Mootools 1.2教程(21)——类(二)
Sep 15 Javascript
基于JQuery的一句代码实现表格的简单筛选
Jul 26 Javascript
Javascript中的this绑定介绍
Sep 22 Javascript
jQuery处理xml格式的返回数据(实例解析)
Nov 28 Javascript
纯js模仿windows系统日历
Feb 04 Javascript
JSONP基础知识详解
Mar 19 Javascript
ES6使用Set数据结构实现数组的交集、并集、差集功能示例
Oct 31 Javascript
React 组件中的 bind(this)示例代码
Sep 16 Javascript
微信小程序实现动态获取元素宽高的方法分析
Dec 10 Javascript
node学习笔记之读写文件与开启第一个web服务器操作示例
May 29 Javascript
策略模式实现 Vue 动态表单验证的方法
Sep 16 Javascript
JavaScript实现指定数量的并发限制的示例代码
Mar 10 Javascript
Vue两种组件类型:递归组件和动态组件的用法
Aug 06 #Javascript
vue数据更新UI不刷新显示的解决办法
Aug 06 #Javascript
基于vue 动态菜单 刷新空白问题的解决
Aug 06 #Javascript
基于JavaScript的数据结构队列动画实现示例解析
Aug 06 #Javascript
解决vue动态下拉菜单 有数据未反应的问题
Aug 06 #Javascript
JavaScript中ES6规范中let和const的用法和区别
Aug 06 #Javascript
在vue项目中利用popstate处理页面返回的操作介绍
Aug 06 #Javascript
You might like
php常用数学函数汇总
2014/11/21 PHP
php模仿asp Application对象在线人数统计实现方法
2015/01/04 PHP
smarty的section嵌套循环用法示例
2016/05/28 PHP
PHP实现的链式队列结构示例
2017/09/15 PHP
javascript 关闭IE6、IE7
2009/06/01 Javascript
javascript生成随机颜色示例代码
2014/05/05 Javascript
html的DOM中document对象anchors集合用法实例
2015/01/21 Javascript
JavaScript实现基于Cookie的存储类实例
2015/04/10 Javascript
在easyUI开发中,出现jquery.easyui.min.js函数库问题的解决办法
2015/09/11 Javascript
Nodejs获取网络数据并生成Excel表格
2020/03/31 NodeJs
基于Vuejs和Element的注册插件的编写方法
2017/07/03 Javascript
webpack构建的详细流程探底
2018/01/08 Javascript
vue watch监听对象及对应值的变化详解
2018/02/24 Javascript
vue router动态路由下让每个子路由都是独立组件的解决方案
2018/04/24 Javascript
让你5分钟掌握9个JavaScript小技巧
2018/06/09 Javascript
Nuxt的路由配置和参数传递方式
2020/11/06 Javascript
Python求导数的方法
2015/05/09 Python
python实现马耳可夫链算法实例分析
2015/05/20 Python
Python制作刷网页流量工具
2017/04/23 Python
python判断数字是否是超级素数幂
2018/09/27 Python
python 3.3 下载固定链接文件并保存的方法
2018/12/18 Python
详解python 模拟豆瓣登录(豆瓣6.0)
2019/04/18 Python
python绘制规则网络图形实例
2019/12/09 Python
python——全排列数的生成方式
2020/02/26 Python
python中re模块知识点总结
2021/01/17 Python
西班牙网上书店:Casa del Libro
2016/11/01 全球购物
英国顶级家庭折扣店:The Works
2017/09/06 全球购物
英国最大的独立摄影零售商:Park Cameras
2019/11/27 全球购物
Prototype中如何为一个元素添加一个方法
2014/12/08 面试题
《一个中国孩子的呼声》教学反思
2014/02/12 职场文书
2014年民政局关于保密工作整改措施
2014/09/19 职场文书
2014年工程部工作总结
2014/11/25 职场文书
党建工作目标管理责任书
2015/01/29 职场文书
工作时间调整通知
2015/04/24 职场文书
上班迟到检讨书
2015/05/06 职场文书
信息简报范文
2015/07/21 职场文书