手把手带你搭建一个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 相关文章推荐
使用IE6看老赵的博客 jQuery初探
Jan 17 Javascript
Extjs Ext.MessageBox.confirm 确认对话框详解
Apr 02 Javascript
JS判断是否为数字,是否为整数,是否为浮点数的代码
Apr 24 Javascript
js限制文本框只能输入中文的方法
Aug 11 Javascript
jQuery+css实现的换页标签栏效果
Jan 27 Javascript
原生JS和jQuery操作DOM对比总结
Jan 19 Javascript
JS实现简易刻度时钟示例代码
Mar 11 Javascript
xmlplus组件设计系列之网格(DataGrid)(10)
May 05 Javascript
react-native之ART绘图方法详解
Aug 08 Javascript
Vue 项目中遇到的跨域问题及解决方法(后台php)
Mar 28 Javascript
koa2使用ejs和nunjucks作为模板引擎的使用
Nov 27 Javascript
vue使用vuex实现首页导航切换不同路由的方法
May 08 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结合表单实现一些简单功能的例子
2011/06/04 PHP
PHP基于phpqrcode生成带LOGO图像的二维码实例
2015/07/10 PHP
ThinkPHP5 验证器的具体使用
2018/05/31 PHP
个人总结的一些关于String、Function、Array的属性和用法
2007/01/10 Javascript
新浪的图片新闻效果
2007/01/13 Javascript
简单三步,搞掂内存泄漏
2007/03/10 Javascript
用JavaScript隐藏控件的方法
2009/09/21 Javascript
从零开始学习jQuery (十) jQueryUI常用功能实战
2011/02/23 Javascript
javascript椭圆旋转相册实现代码
2012/01/16 Javascript
使用javascript实现有效时间的控制,并显示将要过期的时间
2014/01/02 Javascript
js实现点击图片将图片地址复制到粘贴板的方法
2015/02/16 Javascript
JavaScript转换二进制编码为ASCII码的方法
2015/04/16 Javascript
Angularjs中的验证input输入框只能输入数字和小数点的写法(推荐)
2017/08/16 Javascript
jQuery实现倒计时功能 jQuery实现计时器功能
2017/09/19 jQuery
express+mockjs实现模拟后台数据发送功能
2018/01/07 Javascript
手把手教你vue-cli单页到多页应用的方法
2018/05/31 Javascript
微信小程序onLaunch异步,首页onLoad先执行?
2018/09/20 Javascript
Python中第三方库Requests库的高级用法详解
2017/03/12 Python
Python中如何优雅的合并两个字典(dict)方法示例
2017/08/09 Python
浅谈tensorflow1.0 池化层(pooling)和全连接层(dense)
2018/04/27 Python
详解python持久化文件读写
2019/04/06 Python
tensorflow如何继续训练之前保存的模型实例
2020/01/21 Python
python mock测试的示例
2020/10/19 Python
通过canvas转换颜色为RGBA格式及性能问题的解决
2019/11/22 HTML / CSS
购买大码女装:Lane Bryant
2016/09/07 全球购物
Mytheresa英国官网:拥有160多个奢侈品品牌
2016/10/09 全球购物
计算s=f(f(-1.4))的值
2014/05/06 面试题
毕业生自我鉴定
2013/11/05 职场文书
毕业自我评价
2014/02/05 职场文书
会计岗位描述
2014/02/22 职场文书
文化活动实施方案
2014/03/28 职场文书
员工三分钟演讲稿
2014/08/19 职场文书
创业计划书之网吧
2019/10/10 职场文书
启动Tomcat时出现大量乱码的解决方法
2021/06/21 Java/Android
python geopandas读取、创建shapefile文件的方法
2021/06/29 Python
gateway网关接口请求的校验方式
2021/07/15 Java/Android