Node.js 自定义命令

最近有一个这样的需求: 写一个列表页面的模板, 然后所有的列表页面都基于这个模板去实现, 为了避免复制黏贴的操作所以决定用命令来实现一个, 正好 node 自带了这样的功能, 记录一下实现过程。

首先在桌面上新建一个文件夹, 名字无所谓只是为了测试而已, 新建一个main.js文件, 然后npm init一个package.json可以在这里看到 node 文件操作的方法, 我们的目的是从一个文件中拷贝出主要的代码到另一个文件中, 所以主要用到的就是读取和写入命令, 举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
// 这一句一定要有
#!/usr/bin/env node
// 导入文件模块
const fs = require('fs')
// 读取参数 前两个参数是自动生成的 后面才是我们要的参数
//[ '/usr/local/Cellar/node/10.10.0/bin/node',
//  '/usr/local/bin/自定义命令的文件夹',
//  'sdf' ]
var args = process.argv.slice(2)
// 读取文件内容 返回的是 buffer 
var content = fs.readFileSync('./template.js')
// 写入文件 如果没有就创建 有就覆盖
fs.writeFileSync(args[0], content)

在我们生成的package.json中, 我们需要加入一行我们自定义的命令以及执行的js路径, 比如这里我就叫mycmd

1
2
3
"bin": {
    "mycmd": "./main.js"
},

最后执行npm link然后就可以使用自定义的命令了, 比如mycmd hello, 如果没有报错的话, 目录的文件夹下会生成一个叫hello的文件, 并拥有template.js中的内容。

但实际的开发远远不是这么简单, 虽然功能实现了, 但是往往需要一些额外的操作, 我们目前的前端框架是 angular 封装的组件自然也是一个 angular 组件模板, 这就回有四个文件, .css .html 和两个 .ts 文件, 所以大概的工作流程是, 先使用 ng 命令创建一个组件, 然后把写好组件的信息拷贝到新创建的组件上。这里有两个问题:

1.如何让js执行其他命令 2.生成文件的命名处理和文件中参数的处理(比如页面的title)

第一个问题还是蛮好解决的, 我们可以借助child_process这个模块来执行其他的命令, 比如 shell python 都可以, 具体可以看这里

1
2
3
4
5
6
7
8
9
10
11
12
13
// 直接执行ng命令
var callfile = require('child_process');
callfile.exec('ng g c ' + args[0], function (error, stdout, stderr) {
    if (error !== null) {
        console.log(error)
    }
});
// 执行脚本文件
callfile.execFile('脚本文件路径.sh or .py',[参数数组], null, function (error, stdout, stderr) {
    if (error !== null) {
        console.log(error)
    }
});

到这里我们可以用我们的命令去创建一个 angular 组件了, 那接下来就是去替换掉 angular 组件的内容了。css 和 spec.ts 都还好说, 因为这两个基本上没有什么更改的内容, 主要的内容在 .html 和 .ts 上面。我们封装一个组件肯定要有一些参数的, 那么如何将这些参数替换成指定的内容呢~

其实我也没有找到太好的方法, 所以我用的是字符串替换, = = 类似于这样:

1
2
3
4
5
var content = fs.readFileSync('文件路径', "./templatelist.html")
var contentStr = content + ''
// 全局替换字符串
contentStr = contentStr.replace(new RegExp("mycmd_slot", 'g'), args[0])
fs.writeFileSync("./" + 参数名称 + "/" + 参数名称 + ".component.html", contentStr)

比如我的模板是这样的

1
<p>mycmd_slot</p>

那么生成出来的文件就是这样的:

1
<p>参数名称</p>

这个只是简单的处理方式, 但是到了 .ts 文件上就很头疼, 因为你不能确定你的组件就只有一个英文单词的名称, 所以肯定需要支持驼峰式命名组件, 而驼峰式命名组件又会被 angular 解析成横线式文件 + 驼峰式组件名称, 所以要特别处理组件注解和组件类名, 也就是这一部分:

1
2
3
4
5
6
7
8
// 注解中 名称需要小写横线式
@Component({
    selector: 'app-cmd-test', 
    templateUrl: './cmd-test.component.html',
    styleUrls: ['./cmd-test.component.css']
})
// 类名 需要驼峰式
export class CmdTestComponent implements OnInit {

那么如果按照替换字符串来说的话, 需要驼峰转小写和小写转驼峰的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 驼峰转横线命名
function getKebabCase(str) {
    return str.replace( /[A-Z]/g, function( i ) {
        return '-' + i.toLowerCase()
    })
}

// 横线命名转驼峰
function getCamelCase(str) {
    return str.replace( /-([a-z])/g, function( all, i ){
        return i.toUpperCase()
    } )
}

所以执行的替换脚本大概是这样

1
2
3
4
5
6
7
8
9
var contentTs = fs.readFileSync('文件路径', "./template.ts")
var contentStrTs = contentTs + ''
// 注意如果是驼峰命名要转横线去替换文件(Component模板名)
var realFileName = getKebabCase(args[0])
contentStrTs = contentStrTs.replace(new RegExp("my_cmd_slot", 'g'), realFileName)
// 类名首字母大写
var realClassName = args[0].substring(0,1).toUpperCase() + args[0].substring(1);
contentStrTs = contentStrTs.replace(new RegExp("my_classname_slot", 'g'), realClassName)
fs.writeFileSync("./" + realFileName + "/" + realFileName + ".component.ts", contentStrTs)

template.js文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Component, OnInit } from '@angular/core';
// 组件名字参数用 my_cmd_slot 
@Component({
    selector: 'app-my_cmd_slot',
    templateUrl: './my_cmd_slot.component.html',
    styleUrls: ['./my_cmd_slot.component.css']
})
// 类名参数用 my_classname_slot 
export class my_classname_slotComponent implements OnInit {

    constructor() { }

    ngOnInit() {
        console.log('hello world')
    }
}

这样对于驼峰命名的类比如aFreeMiBand, 在@Component注解中会变为a-free-mi-band, 而类名会变为AFreeMiBand这样就完全符合 angular 组件的命名规则了。为了方便, 我直接把这个带有 main.js 和 package.json 的文件夹拷贝到项目中, 这个时候问题又来了, 因为路径的原因, 我们往往不能在这个文件夹下执行命令, 那么怎么才能在工程的任意文件夹下执行命令, 并且能准确拷贝template.js中的内容呢?

其实就是使用相对路径而已。

1
2
3
const path = require('path')
// 这个后面的文件路径是指自己定义的命令文件夹下的路径, 前面是固定形式
var content = fs.readFileSync(path.join(__dirname, "./templatelist.html"))

最后, 我们在我们 web 工程的根目录下面 npm link 自己定义命令的文件夹 之后, 就可以随意使用自己的命令了。

完整的组件替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
var callfile = require('child_process')

var args = process.argv.slice(2)

if (args.length === 0) {
    console.log("请带上文件名称参数")
    return
}

callfile.exec('ng g c ' + args[0],function (error, stdout, stderr) {
    if (error !== null) {
        console.log(error)
    }
    else {
        // 写入html
        var content = fs.readFileSync(path.join(__dirname, "./templatelist.html"))
        var contentStr = content + ''
        contentStr = contentStr.replace(new RegExp("crm_cmd_slot", 'g'), args[0])
        // 注意如果是驼峰命名要转横线去替换文件
        var realFileName = getKebabCase(args[0])
        fs.writeFileSync("./" + realFileName + "/" + realFileName + ".component.html", contentStr)

        // 写入ts
        var contentTs = fs.readFileSync(path.join(__dirname, "./templatelist.ts"))
        var contentStrTs = contentTs + ''
        // 注意如果是驼峰命名要转横线去替换文件(Component模板名)
        contentStrTs = contentStrTs.replace(new RegExp("crm_cmd_slot", 'g'), realFileName)
        // 类名首字母大写
        var realClassName = args[0].substring(0,1).toUpperCase() + args[0].substring(1);
        contentStrTs = contentStrTs.replace(new RegExp("crm_classname_slot", 'g'), realClassName)
        fs.writeFileSync("./" + realFileName + "/" + realFileName + ".component.ts", contentStrTs)
    }
});

// 驼峰转横线命名
function getKebabCase(str) {
    return str.replace( /[A-Z]/g, function( i ) {
        return '-' + i.toLowerCase()
    })
}

// 横线命名转驼峰
function getCamelCase(str) {
    return str.replace( /-([a-z])/g, function( all, i ){
        return i.toUpperCase()
    } )
}