【搭建】自己写一个入门级前端脚手架

186 阅读6分钟

全文参考链接: zhurong-cli


基本功能

  1. 通过zr create <name>命令启动项目
  2. 询问用户需要下载的模板
  3. 远程拉取模板文件

搭建步骤拆解

  1. 创建项目
  2. 创建脚手架启动命令(使用commander )
  3. 询问用户问题, 获取创建所需信息(使用inqurer)
  4. 下载远程模板(用download-git-repo)
  5. 发布项目

创建项目

1. npm init初始化项目

2. 在package.json中添加如下内容:

"bin": {
    "zr": "./bin/cli.js"
  },

zr 和后面的值分别表示命令名和启动文件路径

3. 在./bin/cli.js中添加 helloworld 信息:

#! /usr/bin/env node
// 上面这一行在 js 可执行文件中必须有, 否则控制台不识别
console.log('zhurong-cli working ~');

4. 执行npm link

为了方便调试,将该项目链接至全局(如果是 MacOS 环境, 需要在命令前加 sudo);

npm link的作用如下:

  1. 假如有两个项目, 一个npm-link-module, 是我们要开发的 npm 模块; 另一个是npm-link-example, 使我们要运行 npm 模块的项目. 我们首先进入npm-link-module.
  2. 执行npm link后, npm-link-module会根据package.json上的配置, 被链接到全局, 路径是{prefix}/lib/node_modules/<package>(实际上会在该路径下, 新建一个链接至npm-link-module的快捷方式).
  3. 可用npm config get prefix命令获取到prefix的值(/usr/local)
  4. 然后进入npm-link-example项目, 执行npm link npm-link-module
  5. npm-link-module会被链接到npm-link-exapmle/node_modules下面(实际上是快捷方式)

5. 测试结果

随便打开一个终端, 路径不限, 输入命令zr即可, 然后控制台就会打印.bin/cli.js的执行结果:

qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr
zhurong-cli working ~

6. 加第一条功能: 限制用户启动脚手架的 node.js 版本

semver 是一个对npm 语义版本的识别工具, 可用于识别当前 node.js 版本, 它的功能相当于对运算符>重载.

npm install semver

./bin/cli.js中添加如下内容:

// ./bin/cli.js
const semver = require('semver');

function checkNodeVersion() {
  if (!semver.satisfies(process.version, '>=8.0.0')) {
    console.error('Please use node.js >= 8.0.0');
    process.exit(1);
  }
}

checkNodeVersion();

至此, 项目的第一部分: 创建项目 就此完成.


创建脚手架启动命令

1. 安装依赖

npm install commander

2. 用commander的不同方法在./bin/cli.js中创建命令

首先: const program = require('commander'); 然后用法如下:

  1. .version(require('../package.json').version)
    1. 设置当前版本号
    2. , 默认为-V, --version
    3. , 用法和option基本一致.
  2. .usage('<command> [option]') 用于自定义帮助信息
  3. .command('create <app-name>')
    1. 定义命令和参数
    2. 用户在输入命令时, 根据命令的语义, 也可能会输入参数, 如上面的app-name
    3. command()的第一个参数为命令名称, 参数也可以写进命令名称里, 也可以用.arguments()单独指定. 尖括号表示必选参数, 方括号表示可选参数
    4. 第二个参数表示对命令的说明
  4. .description('create a new project') 对一条命令或一个选项的描述, 在用户值输入zr启动脚手架, 但不输入任何命令与选项, 或者用户调用脚手架的说明文档时会有体现
  5. .option('-f, --force', 'overwrite target directory if it exist')
    1. option 用于定义选项, 具体用法点这里查看
    2. 任意一个选项的本质就是一个 label-value 对, 其中 value 可为 true/false, 也可为字符串. 选项有其简称(如-f), 有其对应的全称(如--force), 全称也就是 label-value 对中的 label. 选项还有其用法说明字符串, 还有默认值.
    3. 如果 label 为一个中间带横线的名字, 则转到本地 js 处理时, 自动将其转为驼峰命名的变量. 如: label 为--a-bc-de, 则本地 js 脚本处理时, 其 label 被转为aBcDe
  6. .action((name, action) => { // doSomething... })

接下来就是确定action中的函数内容. 创建lib/create.js, 并将 action 中的内容写进该文件中. 实践: 将./bin/cli.js中的内容改为如下内容:

#! /usr/bin/env node

const program = require('commander');

program
  .command('create <name>')
  .description('create a new project')
  .option('-f, --force', 'overwrite target directory if it exist')
  .action((name, options) => {
    require('../lib/create')(name, options);
  });
  
program
  .version(require('../package.json').version)
  .usage('<command> [option]');
  
program.parse(process.argv); // 解析用户执行命令传入参数

然后, 在./lib/create.js中写如下内容:

module.exports = async (name, options) => {
  console.log(`>>> create name: ${name}, options: ${JSON.stringify(options)}`);
}

然后, 在控制台上分别输入zr, zr create, zr create myFirstProject, zr create myFirstProject -f, 结果如下:

qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr
Usage: zr <command> [option]

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

Commands:
  create [options] <name>  create a new project
  help [command]           display help for command

qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create
error: missing required argument 'name'

qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create myFirstProject
>>> create name: myFirstProject, options: {}

qiuyue@B-K4WTQ05P-0033 > ~/learning/zhurong-cli > zr create myFirstProject -f
>>> create name: myFirstProject, options: {"force":true}

接下来, 将./lib/create.js的逻辑补充完整:

在创建一个项目的时候, 需要考虑这样一种情况: 项目是否已经存在?

  • 不存在: 直接创建即可;
  • 存在:
    • force === true时, 移除原来的项目(目录), 直接创建;
    • force === false时, 询问用户是否要覆盖原来的项目?
      • 是, 要覆盖, 移除原来的项目(目录), 直接创建;
      • 否, 退出

为实现上述逻辑, 这里用到了 fs 的扩展工具fs-extra, 其特点是支持 Promise 来处理异步的文件管理逻辑 npm install fs-extra 然后, 改写 create.js 逻辑如下:

const path = require('path');
const fs = require('fs-extra');

module.exports = async (name, options) => {
  console.log(`>>> create name: ${name}, options: ${JSON.stringify(options)}`);

  // 获取当前终端所处的目录, 如 /Users/wanhaosheng/learning/zhurong-cli
  const cwd = process.cwd();

  // 需要创建的目录地址; 以用户的使用习惯来说, 用户在哪个目录开启终端, 就在哪个目录新建项目
  const targetAir = path.join(cwd, name);

  // 目录是否已经存在?
  if (fs.existsSync(targetAir)) {
    if (options.force) {
      // 是, 且用户要求强制创建
      await fs.remove(targetAir);
    } else {
      // 否, 先询问用户是否要强制覆盖
      // doSomething... 询问逻辑留到下面再处理
    }
  }

  // doSomething... 创建新项目的逻辑
}

3. 创建更多命令

总结具体的步骤:

  • ./bin/cli.js中配置各种命令
  • ./lib下写各种命令的处理逻辑
// bin/cli.js
// 配置 config 命令
program
  .command('config [value]')
  .description('inspect and modify the config')
  .option('-g, --get <path>', 'get value from option')
  .option('-s, --set <path> <value>')
  .option('-d, --delete <path>', 'delete option from config')
  .action((value, options) => {
    console.log(value, options);
  });
// 配置 ui 命令
program
  .command('ui')
  .description('start add open roc-cli ui')
  .option('-p, --port <port>', 'Port used for the UI Server')
  .action((option) => {
    console.log(option);
  });

遗留问题: 像.option('-s, --set <path> <value>')这样, 一个 option 带两个值的情况很少见, 官方文档没有提及, 经本人亲手实验, 第二个值是会被忽略的

4. 完善帮助信息

现在当输入zr --help时, 结尾缺少说明信息, 且说明性信息应为绿色更合适. 因此这里引入chalk处理.

npm install chalk@4.0.0

Note: chalk 从 5.0.0 版只支持 ESModule. 因此请安装 4.0.0 版本.

然后补充如下内容:

// ./bin/cli.js
const chalk = require('chalk');

// 监听 --help 执行, 也同样能监听到 -h
program.on('--help', () => {
  // 新增说明信息
  console.log(`\r\nRun ${chalk.cyan(`zr <command> --help`)} for detailed usage of given command\r\n`);
});

5. 再给脚手架加一个 logo

npm install figlet 代码如下:

console.log('\r\n' + figlet.textSync('ZR', {
  font: 'Ghost',
  horizontalLayout: 'default',
  verticalLayout: 'default',
  width: 80,
  whitespaceBreak: true
}));

询问用户问题, 获取创建所需信息

npm install inquirer 接下来的步骤只需要看官方文档说明即可 在./lib/create.js中增加如下内容:

const { action } = await inquirer.prompt([
  {
    name: 'action', // 存储当前问题回答的变量
    type: 'list', // 表示提问的类型
    message: 'Target directory already exists Pick an action:', // 问题的描述
    default: true, // 默认值
    choices: [ // 可选的选项
      { name: 'Overwrite', value: true },
      { name: 'Cancel', value: false }
    ]
  }
]);

if (!action) {
  // 如果用户不想强制重新初始化项目, 则直接跳出即可
  return;
} else {
  // 移除已经存在的目录, 然后跳出, 继续初始化项目
  console.log('\r\nRemoving...');
  await fs.remove(targetAir);
  console.log(`has removed ${targetAir}`);
}

// 初始化项目...

生成项目文件的核心逻辑

1. 利用 Metalsmith 生成项目

npm install metalsmith

后续补充...

2. 利用 shelljs 使任务执行终端命令

npm install shelljs

目标: 在刚刚 init 的项目的目录中调用npm install, 来给新项目安装好所有依赖包;

由于这个安装过程很长, 因此控制台需要显示一个 loading 图标, 这一点用 ora 来实现:

npm install ora

const shell = require('shelljs');
const ora = require('ora');

const spinner = ora().start();

shell.exec('npm install', function(code, _stdout, stderr) {
    if (code !== 0) {
        console.log(chalk.redBright('Npm executed error:')(;
        spinner.fail(stderr);
    } else {
        spinner.succeed('Done.');
    }
});