手把手带你搭建一个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 字段来定义的。

目录结构

入口 (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

使用演示

结束

这样就实现了一个简单的 fc-vue add-page 功能,是不是很简单。

源代码地址: 源代码

npm 地址:npm

到此这篇关于手把手带你搭建一个 node cli的文章就介绍到这了,更多相关手把手带你搭建一个 node cli内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解如何使用Node.js编写命令工具——以vue-cli为例

    vue-cli全局安装之后,提供了vue命令和vue init.vue list.vue build三个子命令,通过命令可以搭建基于vue.js的脚手架项目.本文简单介绍一下这些命令是如何实现的. vue-cli的项目目录 如下图,由之前文章介绍,npm安装过程中,可以利用package.json中bin字段的配置,将bin目录下的命令文件软连到全局命令.也就是说在/usr/local/bin下生成了四个软连命令:vue.vue-build.vue-list.vue-init. vue命令的源码

  • 详解在vue-cli项目中安装node-sass

    1,使用save会在package.json中自动添加. npm install node-sass --save npm install sass-loader --save 2,通常使用npm安装会出现以下报错,安装失败.(网路问题) 3,可以通过淘宝的npm镜像安装node-sass,解决以上问题. npm install -g cnpm --registry=https://registry.npm.taobao.org (安装淘宝镜像) cnpm install node-sass -

  • Node.js编写CLI的实例详解

    Why Node.js? 常用的用来编写CLI的语言有 python, ruby, perl, Node.js等等. 为什么选取Node.js作为CLI的语言编写工具呢?有三个理由: 对Javascript语言更加熟悉 npm 完善的生态系统 npm OS无关的包管理机制 主要原因还是归咎于npm 完善的生态,目前npm官方有47.5万个三方包可供使用,你可以很方便的使用一些已经编写的很不错的轮子去帮你快速开发.另一方面,你不需要考虑跨平台的安装问题,比如OSX上的homebrew.Debian

  • 详解使用nodeJs安装Vue-cli

    前提:nodeJs本地已安装. 1.执行npm install --global vue-cli ,全局安装vue-cli ----因为默认是从国外服务器下,可以使用阿里巴巴在国内的镜像服务器. 产生通过config命令设置默认下载路径: npm config set registry https://registry.npm.taobao.org 然后再执行: npm install --global vue-cli 2.安装后,检查是否安装成功 vue -V (在此注意V为大写) 3.使用v

  • 用node开发并发布一个cli工具的方法步骤

    cli本质是一种用户操作界面,根据一些指令和参数来与程序完成交互并得到相应的反馈,好的cli还提供帮助信息,我们经常使用的vue-cli就是一个很好的例子.本文将使用nodejs从头开发并发布一款cli工具,用来查询天气. 项目效果图如下: 配置项目 初始化一个项目: npm init -y 编写入口文件index.js: module.exports = function(){ console.log('welcome to Anderlaw weather') } 创建bin文件 bin目录

  • 手把手教你如何使用nodejs编写cli命令行

    前端日常开发中,会遇见各种各样的cli,比如一行命令帮你打包的webpack,一行命令帮你生成vue项目模板的vue-cli,还有创建react项目的create-react-app等等等等.这些工具极大地方便了我们的日常工作,让计算机自己去干繁琐的工作,而我们,就可以节省出大量的时间用于学习.交流.开发. 逛steam . 但是有时候一些十分特别的需求,我们是找不到适合的cli工具去做的.比如说,你的项目十分庞大,你给项目添加一个新的路由,要经过 创建目录 -> 创建.vue文件 -> 更新

  • hadoop client与datanode的通信协议分析

    本文主要分析了hadoop客户端read和write block的流程. 以及client和datanode通信的协议, 数据流格式等. hadoop客户端与namenode通信通过RPC协议, 但是client 与datanode通信并没有使用RPC, 而是直接使用socket, 其中读写时的协议也不同, 本文分析了hadoop 0.20.2版本的(0.19版本也是一样的)client与datanode通信的原理与通信协议.  另外需要强调的是0.23及以后的版本中client与datanod

  • 详解用node编写自己的cli工具

    工作中接到新项目,开发前都需要先规划项目目录,然后一个个创建文件,搭建sass编译环境,下载jquery,Swiper等类库... 这些准备工作都要花上不少时间.每做一个项目,都会遇到同样的问题,再重复一遍吗? 是时候做点改变了:编写自己的cli工具,一行命令,3秒钟进入coding状态! 本文以自己的my-cli为例,将开发到发布过程完整记录下来,看完本文,你将学会如何从零开发一个cli项目,如何上传到github库,以及如何使用npm发布自己的包. 准备 开发一个cli工具前首先要想好它能做

  • node通过npm写一个cli命令行工具

    前言 如果你想写一个npm插件,如果你想通过命令行来简化自己的操作,如果你也是个懒惰的人,那么这篇文章值得一看. po主的上一篇文章介绍了定制自己的模版,但这样po主还是不满足啊,项目中我们频繁的需要新建一些页面,逻辑样式等文件,每次都手动new一个,然后复制一些基本代码进去非常的麻烦,所以就有了这篇文章.接下来就让po主为大家一步一步演示怎么做一个npm命令行插件. 注册npm账户 发布npm插件,首先肯定要有个npm帐号了,过程就不啰嗦了,走你. npm官网 有了账号后,我们通过npm in

  • 使用nodejs开发cli项目实例

    1.分分钟使用默认选项安装完nodejs 2.分分钟使用sudo npm install -g yo安装完yeoman 3.通过sudo npm install -g generator-cli-starter安装cli开发脚手架 OK, 现在我们可以使用yo cli-starter命令开始我们cli开发之旅了 创建cli项目 复制代码 代码如下: yo cli-starter 按照提示输入项目名称.命令名称,完成cli项目创建.后续的教程中我们将使用hi作为你的命令名称,如果你使用了其他命令名

随机推荐