详解使用 Node.js 开发简单的脚手架工具


Posted in Javascript onJune 08, 2018

前言

像我们熟悉的 vue-cli,react-native-cli 等脚手架,只需要输入简单的命令 vue init webpack project,即可快速帮我们生成一个初始项目。在实际工作中,我们可以定制一个属于自己的脚手架,来提高自己的工作效率。

为什么需要需要脚手架?

  1. 减少重复性的工作,不再需要复制其他项目再删除无关代码,或者从零创建一个项目和文件。
  2. 根据交互动态生成项目结构和配置文件等。
  3. 多人协作更为方便,不需要把文件传来传去。

思路

要开发脚手架,首先要理清思路,脚手架是如何工作的?我们可以借鉴 vue-cli 的基本思路。vue-cli 是将项目模板放在 git 上,运行的时候再根据用户交互下载不同的模板,经过模板引擎渲染出来,生成项目。这样将模板和脚手架分离,就可以各自维护,即使模板有变动,只需要上传最新的模板即可,而不需要用户去更新脚手架就可以生成最新的项目。那么就可以按照这个思路来进行开发了。

第三方库

首先来看看会用到哪些库。

commander.js,可以自动的解析命令和参数,用于处理用户输入的命令。
download-git-repo,下载并提取 git 仓库,用于下载项目模板。
Inquirer.js,通用的命令行用户界面集合,用于和用户进行交互。
handlebars.js,模板引擎,将用户提交的信息动态填充到文件中。
ora,下载过程久的话,可以用于显示下载中的动画效果。
chalk,可以给终端的字体加上颜色。
log-symbols,可以在终端上显示出 √ 或 × 等的图标。

初始化项目

首先创建一个空项目,暂时命名为 okii-cli,然后新建一个 index.js 文件,再执行 npm init 生成一个 package.json 文件。最后安装上面需要用到的依赖。

npm install commander download-git-repo inquirer handlebars ora chalk log-symbols -S

处理命令行

node.js 内置了对命令行操作的支持,在 package.json 中的 bin 字段可以定义命令名和关联的执行文件。所以现在

package.json 中加上 bin 的内容:

{
 "name": "okii-cli",
 "version": "1.0.0",
 "description": "基于node的脚手架工具",
 "bin": {
  "okii": "index.js"
 },
 ...
}

然后在 index.js 中来定义 init 命令:

#!/usr/bin/env node
const program = require('commander');

program.version('1.0.0', '-v, --version')
    .command('init <name>')
    .action((name) => {
      console.log(name);
    });
program.parse(process.argv);

调用 version('1.0.0', '-v, --version') 会将 -v 和 --version 添加到命令中,可以通过这些选项打印出版本号。

调用 command('init <name>') 定义 init 命令,name 则是必传的参数,为项目名。

action() 则是执行 init 命令会发生的行为,要生成项目的过程就是在这里面执行的,这里暂时只打印出 name。

其实到这里,已经可以执行 init 命令了。我们来测试一下,在 okii-cli 的同级目录下执行:

node ./okii-cli/index.js init HelloWorld

可以看到命令行工具也打印出了 HelloWorld,那么很清楚, action((name) => {}) 这里的参数 name,就是我们执行 init 命令时输入的项目名称。

命令已经完成,接下来就要下载模板生成项目结构了。

下载模板

download-git-repo 支持从 Github、Gitlab 和 Bitbucket 下载仓库,各自的具体用法可以参考官方文档。

由于是公司项目,所以把模板仓库放在了 Gitlab 上,那么在 action() 中进行操作下载模板:

#!/usr/bin/env node
const program = require('commander');
const download = require('download-git-repo');

program.version('1.0.0', '-v, --version')
    .command('init <name>')
    .action((name) => {
      download('http://xxxxxx:9999:HTML5/H5Template#master', name, {clone: true}, (err) => {
        console.log(err ? 'Error' : 'Success')
      })
    });
program.parse(process.argv);

download() 第一个参数就是仓库地址,但是有一点点不一样。实际的仓库地址是 http://xxxxxx:9999/HTML5/H5Template#master ,可以看到端口号后面的 '/' 在参数中要写成 ':',#master 代表的就是分支名,不同的模板可以放在不同的分支中,更改分支便可以实现下载不同的模板文件了。第二个参数是路径,上面我们直接在当前路径下创建一个 name 的文件夹存放模板,也可以使用二级目录比如 test/${name}

命令行交互

命令行交互功能可以在用户执行 init 命令后,向用户提出问题,接收用户的输入并作出相应的处理。这里使用 inquirer.js 来实现。

const inquirer = require('inquirer');
inquirer.prompt([
  {
    type: 'input',
    name: 'author',
    message: '请输入作者名称'
  }
]).then((answers) => {
  console.log(answers.author);
})

通过这里例子可以看出,问题就放在 prompt() 中,问题的类型为 input 就是输入类型,name 就是作为答案对象中的 key,message 就是问题了,用户输入的答案就在 answers 中,使用起来就是这么简单。更多的参数设置可以参考官方文档。

通过命令行交互,获得用户的输入,从而可以把答案渲染到模板中。

渲染模板

这里用 handlebars 的语法对 HTML5/H5Template 仓库的模板中的 package.json 文件做一些修改

{
 "name": "{{name}}",
 "version": "1.0.0",
 "description": "{{description}}",
 "scripts": {
  "test": "echo \"Error: no test specified\" && exit 1"
 },
 "author": "{{author}}",
 "license": "ISC"
}

并在下载模板完成之后将用户输入的答案渲染到 package.json 中

program.version('1.0.0', '-v, --version')
.command('init <name>')
.action((name) => {
  inquirer.prompt([
  {
    name: 'description',
    message: '请输入项目描述'
  },
  {
    name: 'author',
    message: '请输入作者名称'
  }
  ]).then((answers) => {
    download('xxxxx#master',name,{clone: true},(err) => {
      const meta = {
        name,
        description: answers.description,
        author: answers.author
      }
      const fileName = `${name}/package.json`;
      const content = fs.readFileSync(fileName).toString();
      const result = handlebars.compile(content)(meta);
      fs.writeFileSync(fileName, result);
    })
  })
});

这里使用了 node.js 的文件模块 fs,将 handlebars 渲染完后的模板重新写入到文件中。

视觉美化

在用户输入答案之后,开始下载模板,这时候使用 ora 来提示用户正在下载中。

const ora = require('ora');
// 开始下载
const spinner = ora('正在下载模板...');
spinner.start();

// 下载失败调用
spinner.fail();

// 下载成功调用
spinner.succeed();

然后通过 chalk 来为打印信息加上样式,比如成功信息为绿色,失败信息为红色,这样子会让用户更加容易分辨,同时也让终端的显示更加的好看。

const chalk = require('chalk');
console.log(chalk.green('项目创建成功'));
console.log(chalk.red('项目创建失败'));

除了给打印信息加上颜色之外,还可以使用 log-symbols 在信息前面加上 √ 或 × 等的图标

const chalk = require('chalk');
const symbols = require('log-symbols');
console.log(symbols.success, chalk.green('项目创建成功'));
console.log(symbols.error, chalk.red('项目创建失败'));

完整示例

#!/usr/bin/env node
const fs = require('fs');
const program = require('commander');
const download = require('download-git-repo');
const handlebars = require('handlebars');
const inquirer = require('inquirer');
const ora = require('ora');
const chalk = require('chalk');
const symbols = require('log-symbols');

program.version('1.0.0', '-v, --version')
  .command('init <name>')
  .action((name) => {
    if(!fs.existsSync(name)){
      inquirer.prompt([
        {
          name: 'description',
          message: '请输入项目描述'
        },
        {
          name: 'author',
          message: '请输入作者名称'
        }
      ]).then((answers) => {
        const spinner = ora('正在下载模板...');
        spinner.start();
        download('http://xxxxxx:9999:HTML5/H5Template#master', name, {clone: true}, (err) => {
          if(err){
            spinner.fail();
            console.log(symbols.error, chalk.red(err));
          }else{
            spinner.succeed();
            const fileName = `${name}/package.json`;
            const meta = {
              name,
              description: answers.description,
              author: answers.author
            }
            if(fs.existsSync(fileName)){
              const content = fs.readFileSync(fileName).toString();
              const result = handlebars.compile(content)(meta);
              fs.writeFileSync(fileName, result);
            }
            console.log(symbols.success, chalk.green('项目初始化完成'));
          }
        })
      })
    }else{
      // 错误提示项目已存在,避免覆盖原有项目
      console.log(symbols.error, chalk.red('项目已存在'));
    }
  })
program.parse(process.argv);

效果如下:

详解使用 Node.js 开发简单的脚手架工具

完成之后,就可以把脚手架发布到 npm 上面,通过 -g 进行全局安装,就可以在自己本机上执行 okii init [name] 来初始化项目,这样便完成了一个简单的脚手架工具了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery 插件 任意位置浮动固定层
Dec 25 Javascript
firefox下input type=&quot;file&quot;的size是多大
Oct 24 Javascript
JavaScript模板入门介绍
Sep 26 Javascript
在父页面调用子页面的JS方法
Sep 29 Javascript
无刷新预览所选择的图片示例代码
Apr 02 Javascript
如何在node的express中使用socket.io
Dec 15 Javascript
Jquery中offset()和position()的区别分析
Feb 05 Javascript
javascript中offset、client、scroll的属性总结
Aug 13 Javascript
jquery分页插件jquery.pagination.js实现无刷新分页
Apr 01 Javascript
javaScript事件机制兼容【详细整理】
Jul 23 Javascript
layui表格 列自动适应大小失效的解决方法
Sep 06 Javascript
原理深度解析Vue的响应式更新比React快
Apr 04 Javascript
使用JavaScript生成罗马字符的实例代码
Jun 08 #Javascript
jQuery实现表单动态加减、ajax表单提交功能
Jun 08 #jQuery
Node.js中你不可不精的Stream(流)
Jun 08 #Javascript
用react-redux实现react组件之间数据共享的方法
Jun 08 #Javascript
vue指令只能输入正数并且只能输入一个小数点的方法
Jun 08 #Javascript
bootstrap treeview 树形菜单带复选框及级联选择功能
Jun 08 #Javascript
原生实现一个react-redux的代码示例
Jun 08 #Javascript
You might like
解决php中Cannot send session cache limiter 的问题的方法
2007/04/27 PHP
Apache+php+mysql在windows下的安装与配置图解(最新版)
2008/11/30 PHP
JS 实现导航栏悬停效果
2013/09/23 Javascript
js 获取浏览器版本以此来调整CSS的样式
2014/06/03 Javascript
用简洁的jQuery方法toggleClass实现隔行换色
2014/10/22 Javascript
javascript中scrollTop详解
2015/04/13 Javascript
jquery简单实现图片切换效果的方法
2015/05/12 Javascript
基于jQuery实现的无刷新表格分页实例
2016/02/17 Javascript
JS检测移动端横竖屏的代码
2016/05/30 Javascript
jQuery Chart图表制作组件Highcharts用法详解
2016/06/01 Javascript
深入理解jQuery 事件处理
2016/06/14 Javascript
基于jQuery Easyui实现登陆框界面
2017/07/10 jQuery
js 事件的传播机制(实例讲解)
2017/07/20 Javascript
Vue监听事件实现计数点击依次增加的方法
2018/09/26 Javascript
百度小程序之间的页面通信过程详解
2019/07/18 Javascript
JQuery 实现文件下载的常用方法分析
2019/10/29 jQuery
vue祖孙组件之间的数据传递案例
2020/12/07 Vue.js
[06:20]2015国际邀请赛第三日top10
2015/08/08 DOTA
python常见数制转换实例分析
2015/05/09 Python
Python获取指定文件夹下的文件名的方法
2018/02/06 Python
python 实现在Excel末尾增加新行
2018/05/02 Python
tensorflow实现加载mnist数据集
2018/09/08 Python
pytorch模型预测结果与ndarray互转方式
2020/01/15 Python
如何用python处理excel表格
2020/06/09 Python
如何使用amaze ui的分页样式封装一个通用的JS分页控件
2020/08/21 HTML / CSS
欧洲著名的珠宝和手表网上商城:uhrcenter
2017/04/10 全球购物
美国婚礼装饰和活动用品批发供应商:Event Decor Direct
2018/10/12 全球购物
职专应届生求职信
2013/11/16 职场文书
2014全国两会学习心得体会2000字
2014/03/10 职场文书
技校毕业生自荐信
2014/06/03 职场文书
学校志愿者活动总结
2014/06/27 职场文书
企业委托书范本
2014/09/13 职场文书
党员对照检查材料整改措施思想汇报
2014/09/26 职场文书
幽默导游词应该怎么写?
2019/08/26 职场文书
MySQL REVOKE实现删除用户权限
2021/06/18 MySQL
Python中Selenium对Cookie的操作方法
2021/07/09 Python