脚手架

一、要实现什么

希望像@vue/cli一样的方式
1.安装我的脚手架包,基于@vue/cli
2.能全局使用我的命令来创建项目,能提示并根据用户输入的参数来创建
3.创建的项目是写好的脚手架模板,集成了所需的组件和工具和配置
4.cd进新创建的项目
5.执行npm install,执行完npm install后自动执行一个脚本,来安装本地组件库,减少了手动操作,像patch-package
6.执行npm run dev,自动启动项目,并自动打开浏览器

二、需要怎么做

1.要是一个npm包的形式
2.安装后要能全局使用命令,来下载模板项目
3.开发模板项目,上传到git

1.新建文件夹

新建目录,cd进mycli目录,开发mycli的npm包

2.执行npm init

生成package.json

3.打开package.json

以下为常用字段说明:

3.1 name: 包名称
3.2 version: 版本号
3.3 description: 描述
3.4 main: 入口文件
3.5 scripts: 脚本
3.6 author: 作者
3.7 license: 许可证
3.8 keywords: 关键字
3.9 dependencies: 依赖
3.10 bin: 命令

4.main字段说明

这里在根目录下新建入口文件index.js。如果想以esmodule的方式导入导出模块,需要文件后缀为.mjs,并且设置type: “module”

5.scripts字段说明

它的每一个属性都对应一个脚本。我们在项目当中运行npm run xxx的时候,主要分为以下几步:
1、从package.json当中读取scrips选项。
2、以传给npm run命令的第一个参数作为键,在scripts中找到要执行的命令,没有找到会报错。
3、找到命令后,自动创建一个shell,其中只要是shell可以运行的命令,就可以写在npm script当中。
4、将当前目录下的node_modules/.bin这个子目录加入PATH变量(这就意味着,当前目录的node_modules/.bin子目录里的所有脚本,都可以直接用脚本名调用,而不需要加路径)
5、在这个shell上执行上述命令

6.bin字段说明

1、首先我们要清楚,能在命令行中识别的命令,一定是在环境变量中能找到的,所以当我们安装nodejs后,会在电脑环境变量中增加nodejs命令目录,然后就能全局使用node和node全局目录下的所有命令
2、为什么全局安装 @vue/cli 后添加的命令为 vue。
包安装时根据package.json中bin对象,key作为环境变量中可执行命令,value指向实际运行的js文件
在根目录下新建bin目录(bin 目录用来存放可执行命令的文件夹),在bin目录下新建mycli.js(可以不带后缀名),内容如下:

1
2
#!/usr/bin/env node // 告诉系统此脚本用node执行
require('./index.js') // 入口文件

在package.json中添加bin字段,内容如下:

1
2
3
"bin": {
"mycli": "bin/mycli.js"
}

这样,在包全局安装的时候,下载包到全局 node_modules 中
js读取package.json的bin字段创建可执行文件放在全局node_modules中(能在环境变量中找到),指向bin/mycli.js文件
然后就能在全局使用mycli命令了
执行mycli命令,会在环境变量中查找,然后使用node环境执行bin/mycli.js文件

7.准备项目模板

以上步骤准备好了npm包,接下来就准备好项目模板,上传至git仓库,在脚本中使用

三、实战代码

1.包结构

1
2
3
4
5
6
7
F:\PRIVATEREPO\MYCLI
│ index.js
│ package.json

└─bin
mycli.js

2.mycli.js

1
2
#!/usr/bin/env node // 告诉系统此脚本用node执行
require('./index.js') // 入口文件

3.package.json

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
{
"name": "mycli",
"version": "1.0.0",
"description": "我的脚手架",
"main": "index.js",
"scripts": {
"dev": "node bin/mycli",
"test": "echo \"Error: no test specified\" && exit 1"
},
"bin": {
"mycli": "bin/mycli"
},
"keywords": [
"cli",
"typescript",
"node"
],
"author": "me",
"license": "ISC",
"dependencies": {
"co": "^4.6.0",
"co-prompt": "^1.0.0",
"commander": "^2.15.1",
"ora": "^5.4.1"
}
}

4.index.js

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
'use strict'
// 操作命令行
const exec = require('child_process').exec;
const co = require('co');
const prompt = require('co-prompt');
const ora = require('ora');
const program = require('commander');
const packageInfo = require('./package.json');
const spinner = ora('正在生成...');

// commander是一个命令行解析器
program.version(packageInfo.version)
// 以下是执行mycli init的时候做的事情。可以扩展其他命令,比如增加模板,下载指定模板等
program
.command('init') // init
.description('生成一个项目')
.alias('i') // 简写
.action(() => {
// 可以按命令写在单独模块里,在入口文件里引入。这里可以将action代码折叠,看项目思路

// 下载项目
const resolve = (result) => {
const { projectName } = result;
// git命令,远程拉取项目并自定义项目名
const url = 'https://github.com/***/***.git'
const cmdStr = `git clone ${url} ${projectName}`;

spinner.start();

exec(cmdStr, (err) => {
execRm(err, projectName);
});
};

// 进入项目,删除模板项目的.git文件夹
const execRm = (err, projectName) => {
if (err) {
console.log(JSON.stringify(err));
console.log('fail', '请重新运行!');
process.exit();
}
// 删除 git 文件,以下删除命令在windows下是可行的,mac没测试,可能命令不同
exec('cd ' + projectName + ' && rd /s /q ".git"', (err, out) => {
execFinish(err, projectName);
});
}

// 完成,提示cd项目,npm install
const execFinish = (err, projectName) => {
spinner.stop();

if (err) {
console.log(JSON.stringify(err));
console.log('err', '请手动删除或重新初始化模板项目的.git文件夹!');
process.exit();
}

console.log('suc', '初始化完成!');
console.log(`cd ${projectName} && npm install`);
process.exit();
};

// 处理用户输入
co(function *() {
const projectName = yield prompt('项目名字: ');
return new Promise((resolve, reject) => {
resolve({
projectName,
});
});
}).then(resolve);
});

program.parse(process.argv);
if(!program.args.length){
program.help()
}

5.准备项目模板

1.模板中的package.json中的dependencies要设置好,确保安装完即可正常运行项目,开发时注意最好局部安装而非全局安装
2.package.json中的scripts字段,有个postinstall脚本,这个脚本会在所有包安装完成后执行,在下面这个例子中,some-script.js 会在所有依赖安装完成后运行。所以可以利用他,实现像patch-package的功能,比如安装完依赖后自动npm link 本地组件库,就不用每次都手动npm link了。

1
2
3
4
5
{
"scripts": {
"postinstall": "node some-script.js"
}
}

四、延伸

child_process模块

通过以下方法,你可以在Node.js应用程序中调用npm install命令,安装所需的依赖包,也可以执行其他命令。

1
2
3
4
5
6
7
8
9
10
// 这种方式会阻塞事件循环,直到命令执行完成。
const { exec } = require('child_process');
exec('npm install <package-name>', (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
1
2
3
4
5
6
7
8
9
10
11
// 这种方式不会阻塞事件循环,适合需要同时处理其他任务的场景。
const { spawn } = require('child_process');
const npm = spawn('npm', ['install', '<package-name>']);

npm.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

npm.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});