详解使用 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 相关文章推荐
zTree插件之单选下拉菜单实例代码
Nov 07 Javascript
无刷新上传文件并返回自定义值
Jun 11 Javascript
js模仿java的Map集合详解
Jan 06 Javascript
JavaScript中将数组进行合并的基本方法讲解
Mar 07 Javascript
JS实现的表格行上下移动操作示例
Aug 03 Javascript
JS+HTML5实现的前端购物车功能插件实例【附demo源码下载】
Oct 17 Javascript
完美解决IE不支持Data.parse()的问题
Nov 24 Javascript
js实现随机点名小功能
Aug 17 Javascript
微信小程序模拟cookie的实现
Jun 20 Javascript
通过函数作用域和块级作用域看javascript的作用域链
Aug 05 Javascript
vue做移动端适配最佳解决方案(亲测有效)
Sep 04 Javascript
说说如何在Vue.js中实现数字输入组件的方法
Jan 08 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安全配置
2006/10/09 PHP
简单的PHP图片上传程序
2008/03/27 PHP
PHP调用Linux的命令行执行文件压缩命令
2013/01/27 PHP
对PHP语言认识上需要避免的10大误区
2014/06/12 PHP
PHP数组函数知识汇总
2016/05/12 PHP
php 判断IP为有效IP地址的方法
2018/01/28 PHP
比较简单的一个符合web标准的JS调用flash方法
2007/11/29 Javascript
js 强制弹出窗口代码研究-又一款代码
2010/03/20 Javascript
JS在可编辑的div中的光标位置插入内容的方法
2014/11/20 Javascript
node.js中的buffer.fill方法使用说明
2014/12/14 Javascript
JS动态修改表格cellPadding和cellSpacing的方法
2015/03/31 Javascript
BootStrap Fileinput初始化时的一些参数
2016/12/30 Javascript
JS如何设置元素样式的方法示例
2017/08/28 Javascript
基于bootstrap页面渲染的问题解决方法
2018/08/09 Javascript
Node.js创建一个Express服务的方法详解
2020/01/06 Javascript
Vue-cli3多页面配置详解
2020/03/22 Javascript
Vue中强制组件重新渲染的正确方法
2021/01/03 Vue.js
基于vue的video播放器的实现示例
2021/02/19 Vue.js
python使用三角迭代计算圆周率PI的方法
2015/03/20 Python
Python单链表简单实现代码
2016/04/27 Python
Python实现的破解字符串找茬游戏算法示例
2017/09/25 Python
详解如何使用Python编写vim插件
2017/11/28 Python
解决Jupyter NoteBook输出的图表太小看不清问题
2020/04/16 Python
python实现密度聚类(模板代码+sklearn代码)
2020/04/27 Python
python编写一个会算账的脚本的示例代码
2020/06/02 Python
python中random模块详解
2021/03/01 Python
美国大码时尚女装购物网站:ELOQUII
2017/12/28 全球购物
时尚孕妇装:HATCH Collection
2019/09/24 全球购物
综合办公室主任职责
2013/12/16 职场文书
教师党的群众路线对照检查材料
2014/09/24 职场文书
2014年国庆节演讲稿精选范文1500字
2014/09/25 职场文书
关于群众路线的心得体会
2014/11/05 职场文书
北京故宫导游词
2015/01/31 职场文书
2015年度保密工作总结
2015/04/24 职场文书
公司表扬信格式
2015/05/04 职场文书
超级详细实用的pycharm常用快捷键
2021/05/12 Python