前端工程化3:如何编写一个Nodejs Cli应用/自定义脚手架

拖泥羊

什么是Nodejs Cli应用?

简单来说就是在命令行可以使用nodejs来执行的应用,例如:vue-cli、creat-react-app、webpack-cli等;在前端开发过程中我们会用到很多的工具,这些工具在安装过后可以直接使用命令行执行;注意在全局安装和在项目安装不同。

// 全局安装,直接执行命令
> npm install webpack webpack-cli -g
> webpack

// 项目安装,需要借助npx执行
> npm install webpack webpack-cli --save-dev
> npx webpack

Nodejs Cli应用的工作流程!

1、启动过程:

命令行执行命令 => 根据package.json中bin查询入口 => 执行入口js文件cli.js

2、执行过程:

命令行执行js文件功能启动=> 命令行询问用户问题 => 结合问题答案+模板等文件 => 生成结构文件

Nodejs Cli应用的入口文件:cli.js

1、入口文件路径 ,首先在package.json 中添加bin字段

{
  "name": "ncl",
  "main": "index.js",
  "bin": {
    "ncl": "./cli.js"   //入口文件,ncl和name保持一致
  },
  ...
}

2、入口文件特定的文件头 ,在cli.js顶部输入

#!/usr/bin/env node

3、入口文件权限
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

4、简单测试模块
npm link 可以将模块链接到全局,也可以链接到使用该模块的项目node_modules中;这样在开发模块的过程中,不用发布到npm也可以使用模块进行测试。

> npm link // 在自定义模块项目的目录执行,将模块连接到全局
> ncl // 直接执行模块,使用模块名

Nodejs Cli应用的示例

该示例的功能实现一个自定义的脚手架:在命令行询问用户一些简单的问题作为参数,然后自动生成一些项目文件。其中的文件可以通过模板生成,也可以传递数据到模板。
image.png

1、安装一些依赖模块

> npm install inquirer --save //nodejs环境下,实现命令行的用户交互插件 
> npm install ejs --save //模板引擎

2、cli.js中定义命令行询问用户问题

  • inquire.prompt进行命令行的用户询问操作
  • inquirer.prompt返回值为一个promise对象
  • inquirer.prompt的参数为一个数组
const inquirer = require('inquirer')

inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: '请输入项目名称?'
    }
])
.then(anwsers => {
    // anwsers: { name: "xxx" } //anwsers返回一个结果对象
})

3、获取模板目录和目标生成目录

const path = require('path')


// 模板目录
// __dirname 获取当前执行代码文件的绝对路径
// tmplDir 为templates的绝对路径
const tmplDir = path.join(__dirname, 'templates')

// 目标目录
// process.cwd()返回 Node.js 进程的当前工作目录。
// process参考api文档:http://nodejs.cn/api/process.html
const destDir = process.cwd()

4、模板引擎渲染模板

const ejs = require('ejs')
const path = require('path')

// 通过模板引擎渲染文件
// 参数1:fileDir为文件的绝对路径
// 参数2:渲染模板所需变量,存在anwsers对象里面
// 参数3:回调函数,result为新文件
ejs.renderFile(fileDir, anwsers, (err, result) => {
  if (err) throw err

  // 将结果写入目标文件路径
  fs.writeFileSync(fileDestDir, result)
})
// 一个package.json作为模板的示例:
{
"name": "<%= name %>",
  "version": "<%= version %>",
  "description": "<%= description %>",
  "author": "<%= author %>",
  "bin": "cli.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "ISC"
}

5、读取目录下的文件

  // 将模板下的文件全部转换到目标目录
  // 参数1:path
  // 参数2:回调函数,参数files为文件相对路径组成的数组
  fs.readdir(tmplDir, (err, files) => {
    if (err) throw err
    files.forEach(file => {
      // 通过模板引擎渲染文件
      // 处理 file 
      })
    })
  })

6、将文件写入路径

// 将结果写入目标文件路径
// 参数1:文件的绝对路径
// 参数2:文件内容
fs.writeFileSync(fileDestDir, result)

模板文件相关

这里是根据自己的需求将需要自动生成的文件放到模板目录下,没有变化的或者统一的文件就不需要使用模板引擎。
1、模板路径:templates;

2、通常将整理好的项目结构整体拷贝到templates下,例如:vue的示例源文件、lint文件、package.json等等;
image.png

测试模块执行

本地开发可以使用npm link关联模块目录和依赖此模块的项目node_modules目录;也可以发布到npm源上后直接安装使用模块。

1、关联模块:

> cd nodejs-cli-sample //Nodejs Cli应用的目录
> npm link // 将模块连接到全局

2、执行模块:

> cd nodejs-cli-demo // 在项目目录执行模块
> ncl // 直接执行模块,使用模块名(ncl 是项目nodejs-cli-sample的名称)

发布Nodejs Cli应用

1、可以直接使用npm publish发布到源上

> npm publish --registry=https://registry.xxxx

2、要考虑到npm源是否有写权限,可以发布到自己公司的npm源上或者yarn源上

// 淘宝镜像源是只读的,publish不上去
// 发布到yarn的镜像源之后,使用淘宝镜像源时可以手动同步加快模块下载速度
yarn publish --registry https://registry.yarnpkg.com/

完整示例代码

1、NodeJs Cli应用cli.js 入口文件

#!/usr/bin/env node


// Node CLI 应用入口文件必须要有这样的文件头
// 如果是 Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
// 具体就是通过 chmod 755 cli.js 实现修改

const fs = require('fs')    // 文件读写
const path = require('path')    // 路径获取
const inquirer = require('inquirer')    //命令行用户交互
const ejs = require('ejs')  // 模板引擎


// 脚手架的工作过程:启动 => 命令行询问用户问题 => 结合问题答案+模板 => 生成结构文件

inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: '请输入项目名称(\'\')'
    },
    {
        type: 'input',
        name: 'version',
        message: '请输入项目版本号(1.0.0)'
    },
    {
        type: 'input',
        name: 'description',
        message: '请输入项目备注(\'\')'
    },
    {
        type: 'input',
        name: 'author',
        message: '请输入作者名称(\'\')'
    }
])
.then(anwsers => {
    // anwsers: { name: "xxx" } //anwsers返回一个结果对象

    // 模板目录绝对路径
    const tmplDir = path.join(__dirname, 'templates')
    // 目标目录
    const destDir = process.cwd()

    // 读取目录下所有文件
    let readFiles = (dir) => {
        return new Promise((resolve, reject)=>{
            // 参数1:目录路径
            // 参数2:回调函数(错误对象,files为文件相对路径组成的数组)
            fs.readdir(dir, (err, files) => {
                if (err) reject(err)
                resolve(files)
            })
        })
    }

    // 处理模板文件
    let ejsRender = (file) => {
        return new Promise((resolve, reject)=>{
            // 模板文件绝对路径
            let dir = path.join(tmplDir, file)
            // 参数1:文件路径
            // 参数2:数据对象
            // 参数3:回调函数(错误对象,渲染后的新文件)
            ejs.renderFile(dir, anwsers, (err, result) => {
                if (err) reject(err)
                resolve(result)
            })
        })
    }


    // 1、先读取目录下所有文件
    // 2、使用ejs渲染所有模板
    // 3、再将新文件写到目标路径
    readFiles(tmplDir).then((files)=>{
        files.forEach(file => {
            ejsRender(file).then((result)=>{
                // 目标文件绝对路径,file其实是文件相对路径
                let fileDestDir = path.join(destDir, file)
                // 将结果写入目标文件路径
                // 参数1:文件绝对路径
                // 参数2:渲染后新文件
                fs.writeFileSync(fileDestDir, result)
            },throwError)
        })

    },throwError)
})

/**
 * 错误处理函数
 * @param {*错误对象} error 
 */
function throwError(error){
    throw error
}

2、package.json 示例模板文件,使用的ejs模板引擎

{
"name": "<%= name %>",
  "version": "<%= version %>",
  "description": "<%= description %>",
  "author": "<%= author %>",
  "bin": "cli.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "license": "ISC"
}

特别鸣谢:拉勾教育前端高薪训练营

阅读 1.5k

33 声望
3 粉丝
0 条评论
33 声望
3 粉丝
文章目录
宣传栏