# 脚手架工具开发

# 写着前面

目前已经开发了一款自己的脚手架 —— karl-cli

NPM平台地址:https://www.npmjs.com/package/karl-cli (opens new window)

也可以查看karl-cli使用文档查看教程,里面包括了安装步骤、命令集合以及最重要的可能遇到的问题集😇

# 准备工作

# 使用到的NPM库

  • commander
  • download-git-repo
  • ejs
  • Inquirer
  • ora
  • chalk
  • log-symbols

TIP

每个NPM库的作用在接下来的文档中有说明,这里就不再赘述了

# 基础构建

  1. 新建index.js文件作为入口文件
  2. 运行npm init -y命令创建package.json文件
  3. 使index.js自动运行,需要在index.js文件的第一行加上如下代码:
#!/usr/bin/env node

TIP

这行代码叫做shebang或者hashbang,虽然可以写绝对路径但因为兼容性问题还是推荐上述这种写法

  1. 为了让终端能够识别自己创建的指令,如karl,需要在package.json中加入如下语句:
{
	...
  "bin": {
    "karl": "index.js"
  }
  ...
}
  1. 然后运行npm link指令

解释作用

这样会让这个bin和真正环境变量那个地方做一个链接,就能使能将karl这个作为终端命令配置到那个环境变量里去

  1. 如果在index.js里写入如下代码:
console.log("my cli")

然后输入命令karl后,终端中就能出现如下结果:

karl@KarldeMacBook-Pro Karl-cli % karl    
my cli

自行检查

完成上述操作后,文件夹内只有index.jspackage.json两个文件,并且能在终端打印出结果🎉

# 自定义指令集合

要完成这个功能需要下载第三方库——commander

需要在终端运行指令npm install commander,然后在相应页面导入:

const program = require('commander');

# 命令输出版本号使用示例

#! /usr/bin/env node

const program = require('commander');

program.version("1.1.1"); // [1]

program.parse(process.argv); // [2]

语句解释

[1] 这个语句的功能是在终端输入karl --version或者karl -V后输出当前版本号

[2] 这个语句的功能是将指令后的字符串进行解析,也就是再输入karl --version后能解析--version这个命令

# 运行结果

karl@KarldeMacBook-Pro Karl-cli % karl --version
1.1.1
karl@KarldeMacBook-Pro Karl-cli % karl -V       
1.1.1

以及可以输入karl --help

karl@KarldeMacBook-Pro Karl-cli % karl --help
Usage: karl [options]

Options:
  -V, --version  output the version number
  -h, --help     display help for command

# 选项处理功能的使用

# 增加自己的选项处理

代码如下:

program.option("-p --paper <paper>", "This is a option description");

然后在终端输入karl --help,得到以下结果:

karl@KarldeMacBook-Pro Karl-cli % karl --help   
Usage: karl [options]

Options:
  -V, --version  output the version number
  -p --paper    This is a option description
  -h, --help     display help for command

# 获取可选参数的值

根据官方文档的示例,具体操作如下:

const options = program.opts();
program.option("-p --paper <paper>", "This is a option description");
program.parse(process.argv);
console.log(options.paper);

首先需要在终端运行命令:

karl --paper mypaper

终端打印结果如下:

karl@KarldeMacBook-Pro Karl-cli % karl --paper mypaper
mypaper

注意事项

一定要在--paper后加上参数😖,否则会报如下错误:

karl@KarldeMacBook-Pro Karl-cli % karl --paper        
error: option '-p --paper <paper>' argument missing

# 添加指令

示例如下:

const program = require('commander');
program
    .command("create <project>")
    .description("Create your own project. For example: karl create demo")
    .action(project => console.log(project));

在终端运行指令后会输出结果:

karl@KarldeMacBook-Pro Karl-cli % karl create demo
demo

官方文档

GitHub平台地址:https://github.com/tj/commander.js

NPM平台地址:https://www.npmjs.com/package/commander

这两个文档说明都是一样的,但是都是英文的文档,如果想查看中文文档,可以点击这里 (opens new window)

# 克隆模板到本地

首先需要用到的库是——download-git-repo

需要在终端运行指令npm install download-git-repo,然后在相应页面导入:

const download = require('download-git-repo');

TIP

因为download函数的是回调函数的形式,容易形成回调地狱,因此也可以将其转换为Promise的形式,方式如下:

const { promisify } = require('util');
const download = promisify(require('download-git-repo'));

# 克隆GitHub仓库

# 使用http的url下载

download('direct:https://gitlab.com/flippidippi/download-git-repo-fixture/repository/archive.zip', 'test/tmp', function (err) {
  console.log(err ? 'Error' : 'Success')
})

# 使用直接url的git克隆

download('direct:https://gitlab.com/flippidippi/download-git-repo-fixture.git#vue', 'test/tmp', { clone: true }, function (err) {
  console.log(err ? 'Error' : 'Success')
})

然后在想要下载的文件夹下运行指令karl create demo,就能创建一个名字为demo的项目文件了。

官方文档

GitLab平台地址:https://gitlab.com/flippidippi/download-git-repo

NPM平台地址:https://www.npmjs.com/package/download-git-repo

# 指令创建vue模板

首先需要用到的库是——ejs

需要在终端运行指令npm install ejs,然后在相应页面导入:

const ejs = require('ejs');

然后传入参数渲染文件得到字符串:

ejs.renderFile(absolutPath,{ name, lowerName: name.at(0).toLowerCase() + name.slice(1) }, {}, 
(err, str) => {
  if (err) {
    console.log(err);
    reject(err);
    return;
  }
  resolve(str);
})

注意事项

函数参数的absolutPath要用模板文件(也就是.ejs文件)的绝对路径,所以可以使用path模块的resolve函数进行路径拼接,渲染完用fs模块的promises.writeFile写入即可

同时也可以用ejs来进行NPM包的安装,使用如下,直接在ejs文件里用相应的语法进行更新package.json文件:

{
  "name": "<%= name %>",
  "version": "<%= answers.version %>",
  "description": "<%= answers.description %>",
  "main": "index.js",
  "scripts": {
    "dev": "vuepress dev docs",
    "build": "vuepress build docs",
    "deploy": "bash ./deploy.sh"
  },
  "author": "<%= answers.author %>",
  "license": "ISC",
  "dependencies": {
    "vue": "^2.7.0",
    "vuepress": "^1.9.7",
    "vue-template-compiler": "^2.6.10"<% if(answers.plugins.length) { %><%= "," %><% } %>
    <%_ answers.plugins.forEach(function(item, index, arr){ -%>
    <% if(item === "backToTop") { %><%- '"@vuepress/plugin-back-to-top": "^1.9.7"' %><% if(arr.at(-1) !== 'backToTop') { %><%= "," %> <% } %><% } -%>
    <%_ if(item === "codeCopy") { %><%- '"vuepress-plugin-code-copy": "^1.0.6"' %><% if(arr.at(-1) !== 'codeCopy') { %><%= "," %> <% } %><% } -%>
    <%_ if(item === "readingProgress") { %><%- '"vuepress-plugin-reading-progress": "^1.0.10"' %><% if(arr.at(-1) !== 'readingProgress') { %><%= "," %> <% } %><% } -%>
    <%_ if(item === "cutePet") { %><%- '"@vuepress-reco/vuepress-plugin-kan-ban-niang": "^1.0.5"' %><% } %>
    <%_ }); -%>
  }
}

官方文档

GitHub平台地址:https://github.com/mde/ejs

NPM平台地址:https://www.npmjs.com/package/ejs

# 终端提示功能

首先需要用到的库是——Inquirer

需要在终端运行指令npm install inquirer,然后在相应页面导入:

const inquirer = require('inquirer');

简单的使用如下:

inquirer
    .prompt([{
        type: 'list',
        name: 'choice',
        message: 'your choice:',
        default: 0,
        choices: [
            { value: 'hjy', name: 'hjy' },
            { value: 'lio', name: 'lio' }
        ]
    }])
    .then(answers => {
      console.log('answers', answers.choice);
    })
    .catch(error => {
        if(error.isTtyError) {
            // Prompt couldn't be rendered in the current environment
        } else {
            // Something else went wrong
        }
    });

prompt类型

Defaults: input

Possible values: input, number, confirm, list, rawlist, expand, checkbox, password, editor

官方文档

GitHub平台地址:https://github.com/SBoudrias/Inquirer.js

NPM平台地址:https://www.npmjs.com/package/inquirer

# 其他的库

ora (opens new window):下载过程久的话可以用于显示加载动画

const ora = require('ora');

const spinner = ora('Loading unicorns').start();

setTimeout(() => {
	spinner.color = 'yellow';
	spinner.text = 'Loading rainbows';
}, 1000);

chalk (opens new window):给终端字体加上颜色


console.log(chalk.blue('Hello world!'));

log-symbols (opens new window):提供了一些符号

const ls = require('log-symbols');
console.log(ls.success, 'success');
console.log(ls.info, 'info');
console.log(ls.error, 'error');
console.log(ls.warning, 'warning');

结果如下:

✔ success
ℹ info
✖ error
⚠ warning

友情提醒

可能在终端上会更好看一点,这里效果不太好

# 打印安装node_modules的信息

具体思路就是开辟一个子进程,然后把子进程的打印内容通过管道(pipe)传输到主进程中即可,具体如下:

const { spawn } = require('child_process');

const commandSpawn = (...args) => {
  return new Promise((resolve, reject) => {
    const child_process = spawn(...args);
    child_process.stdout.pipe(process.stdout);
    child_process.stderr.pipe(process.stderr);
    child_process.on("close", () => {
      // 子进程完成操作
      resolve();
    });
  })
}

# 遇到的问题

# 文件夹的名字

最开始将文件夹的名字设置为我的脚手架后运行npm init -y,出现如下报错信息:

karl@KarldeMacBook-Pro 我的脚手架 % npm init -y
npm ERR! Invalid name: "我的脚手架"

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/karl/.npm/_logs/2022-07-12T08_26_15_839Z-debug.log

解决方法

提示中显示Invalid name,所以将文件名改为英文,然后重新输入命令npm init -y即可

# 输入自定义指令无效

再运行npm link后,然后在终端输入karl会出现如下报错:

karl@KarldeMacBook-Pro Karl-cli % karl
zsh: /usr/local/bin/karl: bad interpreter: /user/bin/env: no such file or directory

解决方法

这是因为第一次写shebang的时候把usr写为了user,虽然usr是user的缩写,但是不能写错了,否则会出错,改过来后终端就能运行karl这个自定义指令了🤡

# 克隆模板时报错

# 问题1 git clone失败

报错时使用的是如下的代码:

download('direct:https://github.com/ox4f5da2/karl-cli-template.git#main', project, { clone: true });

运行后就有如下报错:

karl@KarldeMacBook-Pro Karl-cli % karl create demo
/Users/karl/Documents/我的脚手架/Karl-cli/node_modules/git-clone/index.js:33
            cb && cb(new Error("'git clone' failed with status " + status));
                     ^

Error: 'git clone' failed with status 128
    at ChildProcess.<anonymous> (/Users/karl/Documents/我的脚手架/Karl-cli/node_modules/git-clone/index.js:33:22)
    at ChildProcess.emit (node:events:390:28)
    at maybeClose (node:internal/child_process:1064:16)
    at Socket.<anonymous> (node:internal/child_process:450:11)
    at Socket.emit (node:events:390:28)
    at Pipe.<anonymous> (node:net:672:12)

解决方法

这是因为没有在URL前加上direct:这串标识符导致的,加上后运行又报错了😭

# 问题2 克隆无效

上述代码运行后只能产生空文件,然后等待很长时间也没有什么反应,终端也不停止运行,然后就知道又出问题了🤕,然后发现默认下载的是master分支上的代码,如果在其他分支上的话需要在最后加上#main,也就是如下代码:

download('direct:https://github.com/ox4f5da2/karl-cli-template.git#main', project, { clone: true });

这样子就能在想要的文件夹下克隆模板啦😏

# 安装inquirer包使用失败

直接运行npm install inquirer后,package.json中会显示安装的版本如下:

{
  ...
  "dependencies": {
   ...
    "inquirer": "^9.0.0"
  }
}

接下来require并使用prompt函数的时候会报如下的错误信息[心累啊😞]:

/Users/karl/Documents/我的脚手架/Karl-cli/lib/core/prompt.js:1
const inquirer = require('inquirer');
                 ^

Error [ERR_REQUIRE_ESM]: require() of ES Module /Users/karl/Documents/我的脚手架/Karl-cli/node_modules/inquirer/lib/inquirer.js from /Users/karl/Documents/我的脚手架/Karl-cli/lib/core/prompt.js not supported.
Instead change the require of inquirer.js in /Users/karl/Documents/我的脚手架/Karl-cli/lib/core/prompt.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (/Users/karl/Documents/我的脚手架/Karl-cli/lib/core/prompt.js:1:18)
    at Object.<anonymous> (/Users/karl/Documents/我的脚手架/Karl-cli/lib/core/myCommandActions.js:9:18)
    at Object.<anonymous> (/Users/karl/Documents/我的脚手架/Karl-cli/lib/core/create.js:5:5)
    at Object.<anonymous> (/Users/karl/Documents/我的脚手架/Karl-cli/index.js:9:23) {
  code: 'ERR_REQUIRE_ESM'
}

看报错信息写着代替require而使用import,那么就简单了,在package.json中加入"type": "module"就解决了,事情真有这么简单嘛?[我看未必,不出意外的话要出意外了]果然,虽然inquirer可以正常使用了,但是其他require的其他包不能用了,因为只能用import导入,太麻烦了😞,然后给报了如下错误信息:

file:///Users/karl/Documents/%E6%88%91%E7%9A%84%E8%84%9A%E6%89%8B%E6%9E%B6/Karl-cli/index.js:5
const program = require('commander');
                ^

ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/Users/karl/Documents/我的脚手架/Karl-cli/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
    at file:///Users/karl/Documents/%E6%88%91%E7%9A%84%E8%84%9A%E6%89%8B%E6%9E%B6/Karl-cli/index.js:5:17
    at ModuleJob.run (node:internal/modules/esm/module_job:183:25)
    at async Loader.import (node:internal/modules/esm/loader:178:24)
    at async Object.loadESM (node:internal/process/esm_loader:68:5)
    at async handleMainPromise (node:internal/modules/run_main:63:12)

最终的解决方法

这是由于依赖版本的问题导致的,所以只要降低版本即可,然后打开NPM平台,搜索inquirer,查看历史Versions,选择了8.2.4的版本,然后运行npm install inquirer@8.2.4,完美解决问题😉

# 自动执行npm install命令

原本自执行函数用的如下方式:

await commandSpawn('npm', ['install']);

这个在Mac电脑上可以正常执行,但是在windows电脑上会报错,具体原因是因为在Windows电脑上,执行npm命令的时候其实执行的是npm.cmd,在Mac上执行的是npm。所以可以用process.platform判断平台然后执行,更改后如下:

// 自动安装node_modules文件夹
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
await commandSpawn(command, ['install']);

不出意外的话还是出了意外😯,node_modules文件夹没有生成成功,原因是需要填写cwd参数,那加上就好了,最后版本如下:

// 自动安装node_modules文件夹
const command = process.platform === 'win32' ? 'npm.cmd' : 'npm';
await commandSpawn(command, ['install'], { cwd: `./${project}` });