前言

前端日常开发中,会遇见各种各样的 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内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

手把手带你搭建一个node cli的方法示例的更多相关文章

  1. 利用Node实现HTML5离线存储的方法

    这篇文章主要介绍了利用Node实现HTML5离线存储的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  2. ios – 使用带有NodeJs HTTPS的certificates.cer

    我为IOS推送通知生成了一个.cer文件,我希望将它与NodeJSHTTPS模块一起使用.我发现HTTPS模块的唯一例子是使用.pem和.sfx文件,而不是.cer:有解决方案吗解决方法.cer文件可以使用两种不同的格式进行编码:PEM和DER.如果您的文件使用PEM格式编码,您可以像使用任何其他.pem文件一样使用它(有关详细信息,请参见Node.jsdocumentation):如果您的文件使

  3. 如何在XCode IDE中构建NodeJS?

    如何在XCodeIDE中将NodeJS构建为项目?NodeJS构建指令说它应该用以下内容构建:但是我希望在XCodeIDE中构建.我真正想要做的是在我的应用程序中嵌入NodeJS,所以我想如果我可以在XCode中构建NodeJS,那么我可以调整它以在我建立和运行NodeJS后添加我的应用程序.我想通过让V8在XCode中编译来取得一些进展,现在我正在尝试将NodeJS添加到V8项目中.解决方法在节点存储库根目录中运行./configure–xcode,您将获得所需的node.xcodeproj文件.

  4. 深入云存储系统Swift核心组件:Ring实现原理剖析

    它的目的是用于托管Rackspace的CloudFilesservice,原始项目代号是swift,所以沿用至今。Ring是Swift中最重要的组件,用于记录存储对象与物理位置间映射关系。先来看一下Swift文档中关于Ring的描述:Ring用来确定数据驻留在集群中的位置。有单独对应于Account数据库、container数据库和单个object的ring。Ring使用zone的概念来保证数据的隔离。每个partition的replica都确保放在了不同的zone中。本文逐步深入探讨了Swift如何通过

  5. Swift开发:创建XML文件,包含节点,属性值

    .append;//3创建第二个节点数据letitem2:Item=Item;for{letnode=Node;node.id=i+1;node.attributes=["ID":"\","Name":"N-\","disp":"1","Appliance":"1","Icon":"ic_switch_4"]item2.addNode;}xml.items?

  6. 泛型 – 符合Swift中Comparable的泛型类

    我正在尝试创建一个符合Comparable协议的简单通用节点类,以便我可以轻松地比较节点而无需访问其密钥.当我试图写

  7. swift3 – 将SceneKit对象放在SCNCamera当前方向的前面

    >生成SCNVector4,它定向节点,使其“面向”相机?但是让我有点失落.我看到了许多类似的问题,比如thisone,但没有答案.嘿,如果要将对象放在相对于另一个节点的某个位置,并且与参考节点的方向相同,则可以使用这个更简单的函数:如果您想将’node’2m放在某个’cameraNode’前面,你可以这样称呼:

  8. 如何在Swift中继承NSOperation以将SKAction对象排队以进行串行执行?

    Rob为子类化NSOperation提供了agreatObjective-Csolution,以实现SKAction对象的串行排队机制.我在自己的Swift项目中成功实现了这一点.要使用Actionoperation,请在客户端类中实例化NSOperationQueue类成员:在init方法中添加以下重要行:然后当您准备好向其添加SKActions时,它们会连续运行:您是否需要在任何时候终止操作:希望有所帮助!

  9. 核心数据 – 如何在Swift中定义CoreData关系?

    在CoreData中,我已经从Node到Tag定义了一个无序的多对多关系.我创建了一个这样的Swift实体:现在我想添加一个Tag到Node的一个实例,像这样:但是,这会失败,并显示以下错误:Terminatingappduetouncaughtexception‘NSinvalidargumentexception’,reason:‘Unacceptabletypeofvalueforto-ma

  10. 将“nil”值赋给Swift中的一般类型变量

    您需要将变量声明为可选项:不幸的是,这似乎触发了一个未实现的编译器功能:您可以通过使用NSObject的类型约束声明T来解决它:

随机推荐

  1. Error: Cannot find module ‘node:util‘问题解决

    控制台 安装 Vue-Cli 最后一步出现 Error: Cannot find module 'node:util' 问题解决方案1.问题C:\Windows\System32>cnpm install -g @vue/cli@4.0.3internal/modules/cjs/loader.js:638 throw err; &nbs

  2. yarn的安装和使用(全网最详细)

    一、yarn的简介:Yarn是facebook发布的一款取代npm的包管理工具。二、yarn的特点:速度超快。Yarn 缓存了每个下载过的包,所以再次使用时无需重复下载。 同时利用并行下载以最大化资源利用率,因此安装速度更快。超级安全。在执行代码之前,Yarn 会通过算法校验每个安装包的完整性。超级可靠。使用详细、简洁的锁文件格式和明确的安装算法,Yarn 能够保证在不同系统上无差异的工作。三、y

  3. 前端环境 本机可切换node多版本 问题源头是node使用的高版本

    前言投降投降 重头再来 重装环境 也就分分钟的事 偏要折腾 这下好了1天了 还没折腾出来问题的源头是node 使用的高版本 方案那就用 本机可切换多版本最终问题是因为nodejs的版本太高,导致的node-sass不兼容问题,我的node是v16.14.0的版本,项目中用了"node-sass": "^4.7.2"版本,无法匹配当前的node版本根据文章的提

  4. nodejs模块学习之connect解析

    这篇文章主要介绍了nodejs模块学习之connect解析,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. nodejs npm package.json中文文档

    这篇文章主要介绍了nodejs npm package.json中文文档,本文档中描述的很多行为都受npm-config(7)的影响,需要的朋友可以参考下

  6. 详解koa2学习中使用 async 、await、promise解决异步的问题

    这篇文章主要介绍了详解koa2学习中使用 async 、await、promise解决异步的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. Node.js编写爬虫的基本思路及抓取百度图片的实例分享

    这篇文章主要介绍了Node.js编写爬虫的基本思路及抓取百度图片的实例分享,其中作者提到了需要特别注意GBK转码的转码问题,需要的朋友可以参考下

  8. CentOS 8.2服务器上安装最新版Node.js的方法

    这篇文章主要介绍了CentOS 8.2服务器上安装最新版Node.js的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  9. node.js三个步骤实现一个服务器及Express包使用

    这篇文章主要介绍了node.js三个步骤实现一个服务器及Express包使用,文章通过新建一个文件展开全文内容,具有一定的参考价值,需要的小伙伴可以参考一下

  10. node下使用UglifyJS压缩合并JS文件的方法

    下面小编就为大家分享一篇node下使用UglifyJS压缩合并JS文件的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

返回
顶部