热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

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

这篇文章主要介绍了手把手带你搭建一个nodecli的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

前言

前端日常开发中,会遇见各种各样的 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 ', '页面标题')
 .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: '请输入页面的名称&#63;[支持多级目录,例如:user/login]',
 },
 {
  name: 'TITLE',
  type: 'input',
  message: '请输入页面标题(meta.title)',
 },
 {
  type: 'list',
  name: 'SIMPLE',
  message: 'What is the template type&#63;',
  choices: [
  'normal:【同时创建 .vue .js .[style]】 ',
  'simple: 【只创建 .vue】',
  ],
  filter: function (val) {
  return val.split(':')[0] === 'simple' &#63; 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
  &#63; `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内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!


推荐阅读
  • 解决lib-flexible安装过程中遇到的错误问题
    在安装 lib-flexible 时,遇到了 `saveError ENOENT: No such file or directory` 错误,具体表现为无法打开 `E:\Github\SDIO\package.json` 文件。解决此问题的关键在于确保项目根目录下存在 `package.json` 文件,并且在正确的项目路径中执行安装命令。建议先检查项目结构,确认文件是否存在,然后再尝试重新安装依赖。 ... [详细]
  • 深入浅出 webpack 系列(二):实现 PostCSS 代码的编译与优化
    在前一篇文章中,我们探讨了如何通过基础配置使 Webpack 完成 ES6 代码的编译。本文将深入讲解如何利用 Webpack 实现 PostCSS 代码的编译与优化,包括配置相关插件和加载器,以提升开发效率和代码质量。我们将详细介绍每个步骤,并提供实用示例,帮助读者更好地理解和应用这些技术。 ... [详细]
  • 本文介绍了如何使用 Node.js 和 Express(4.x 及以上版本)构建高效的文件上传功能。通过引入 `multer` 中间件,可以轻松实现文件上传。首先,需要通过 `npm install multer` 安装该中间件。接着,在 Express 应用中配置 `multer`,以处理多部分表单数据。本文详细讲解了 `multer` 的基本用法和高级配置,帮助开发者快速搭建稳定可靠的文件上传服务。 ... [详细]
  • 在 Axublog 1.1.0 版本的 `c_login.php` 文件中发现了一个严重的 SQL 注入漏洞。该漏洞允许攻击者通过操纵登录请求中的参数,注入恶意 SQL 代码,从而可能获取敏感信息或对数据库进行未授权操作。建议用户尽快更新到最新版本并采取相应的安全措施以防止潜在的风险。 ... [详细]
  • 本文详细介绍了在 Vue.js 前端框架中集成 vue-i18n 插件以实现多语言支持的方法。通过具体的配置步骤和示例代码,帮助开发者快速掌握如何在项目中实现国际化功能,提升用户体验。同时,文章还探讨了常见的多语言切换问题及解决方案,为开发人员提供了实用的参考。 ... [详细]
  • 在GitHub上克隆vue-element-admin项目时遇到依赖安装错误
    在 GitHub 上克隆 vue-element-admin 项目后,使用 `npm install` 安装依赖时遇到了未知的 Git 错误。具体错误信息为 `npm ERR! code 128`,提示命令执行失败。这可能是由于网络问题、Git 配置不正确或某些依赖包的仓库地址无效导致的。建议检查网络连接、更新 Git 版本并确保所有依赖项的 URL 正确无误。 ... [详细]
  • 微软推出Windows Terminal Preview v0.10
    微软近期发布了Windows Terminal Preview v0.10,用户可以在微软商店或GitHub上获取这一更新。该版本在2月份发布的v0.9基础上,新增了鼠标输入和复制Pane等功能。 ... [详细]
  • Ext JS MVC系列一:环境搭建与框架概览
    本文主要介绍了如何在项目中使用Ext JS 4作为前端框架,并详细讲解了Ext JS 4的MVC开发模式。文章将从项目目录结构、相关CSS和JS文件的引用以及MVC框架的整体认识三个方面进行总结。 ... [详细]
  • Framework7:构建跨平台移动应用的高效框架
    Framework7 是一个开源免费的框架,适用于开发混合移动应用(原生与HTML混合)或iOS&Android风格的Web应用。此外,它还可以作为原型开发工具,帮助开发者快速创建应用原型。 ... [详细]
  • 单元测试:使用mocha和should.js搭建nodejs的单元测试
    2019独角兽企业重金招聘Python工程师标准BDD测试利器:mochashould.js众所周知对于任何一个项目来说,做好单元测试都是必不可少 ... [详细]
  • 技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统
    技术分享:使用 Flask、AngularJS 和 Jinja2 构建高效前后端交互系统 ... [详细]
  • 在ElasticStack日志监控系统中,Logstash编码插件自5.0版本起进行了重大改进。插件被独立拆分为gem包,每个插件可以单独进行更新和维护,无需依赖Logstash的整体升级。这不仅提高了系统的灵活性和可维护性,还简化了插件的管理和部署过程。本文将详细介绍这些编码插件的功能、配置方法,并通过实际生产环境中的应用案例,展示其在日志处理和监控中的高效性和可靠性。 ... [详细]
  • Web开发框架概览:Java与JavaScript技术及框架综述
    Web开发涉及服务器端和客户端的协同工作。在服务器端,Java是一种优秀的编程语言,适用于构建各种功能模块,如通过Servlet实现特定服务。客户端则主要依赖HTML进行内容展示,同时借助JavaScript增强交互性和动态效果。此外,现代Web开发还广泛使用各种框架和库,如Spring Boot、React和Vue.js,以提高开发效率和应用性能。 ... [详细]
  • 如何在启用ECMAScript 6特性的情况下高效运行Node.js应用?本文将详细介绍如何在Node.js环境中启用并充分利用ES6的新特性,包括模块化、箭头函数、模板字符串等,以提升代码的可读性和开发效率。同时,文章还会探讨性能优化策略,确保应用在启用ES6特性后仍能保持高效运行。 ... [详细]
  • 掌握PHP编程必备知识与技巧——全面教程在当今的PHP开发中,了解并运用最新的技术和最佳实践至关重要。本教程将详细介绍PHP编程的核心知识与实用技巧。首先,确保你正在使用PHP 5.3或更高版本,最好是最新版本,以充分利用其性能优化和新特性。此外,我们还将探讨代码结构、安全性和性能优化等方面的内容,帮助你成为一名更高效的PHP开发者。 ... [详细]
author-avatar
大脸猫妈妈-啊珍妮妮_786
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有