如何搭建一个属于自己的脚手架

11,680 阅读5分钟

写在前面

本文比较基操,主要是有一个流程概念。第二弹已出:传送门

效果栗子

脚手架概念

所谓的脚手架,在我看来,就是一个集成项目初始化、调试、构建、测试、部署等等流程,能够让使用者专注于code的工具。用白话说就是,一个建筑已经搭好架子,我们只需要不断加入砖头就行。

一个完整的脚手架一般包含三个方面的内容:

  • 脚手架命令脚本:我们所需要安装到全局的脚手架,通过它可以方便的开始一个项目的开发。(也是本文讲解目标)
  • scripts包:一般我们会将打包、编译、测试以及读取自定义配置文件等等操作(例如webpack相关配置操作,本地服务器相关内容等等),单独做成npm包。让使用者不必关心这些操作,专心code
  • 模板文件:显而易见,就是我们初始化项目的时候,所拉取的项目内容。

常见的脚手架栗子:

  • create-react-app
  • vue-cli
  • ...

新建项目

$ mkdir project && cd project
$ npm init -y

package.json文件中,加入bin字段

{
  //...
  "bin": {
    "hello": "./index.js"
  },
  //...
}

bin的作用就是官网是这样解释的:

许多npm包都具有一个或多个要安装到PATH中的可执行文件。package.json中提供一个字段bin,该字段是命令名到本地文件名的映射。在安装时,npm会将文件符号链接到prefix/bin以进行全局安装或./node_modules/.bin/本地安装。

说白了,就是在安装的时候,会创建一个快捷方式,通过快捷方式能够很方便的使用对应的node脚本命令。这也是为什么我们可以直接随便打开命令行,通过cra方式创建项目。

而对应的index.js文件,在开头必须以#!/usr/bin/env nodeusr/bin/env表示可以去PATH目录中查找脚本解释器,同时指定使用node去执行该文件。

#!/usr/bin/env node

console.log('hello world!');

然后再通过npm link,在全局中创建符号链接,将package.json里的bin字段内容进行映射链接。

$ npm link

那么在任何地方都可以直接使用hello命令。

命令行操作

这边,我是使用commander.js去读取命令。

commander文档

命令

const program = require('commander');

program
    .command('create <name>')
    .description('请输入项目名称')
    .action(name => {
        console.log(`你要创建的项目名称:${name}`);
    });

program.parse(process.argv);

.command()的第一个参数可以配置命令名称及参数,参数支持必选(尖括号表示)、可选(方括号表示)及变长参数(点号表示,如果使用,只能是最后一个参数)。

版本提示

常见的像是create-react-app -V,一般就是读取自身的package.json中的version字段,然后使用program.version

const program = require('commander');
const packageJson = require('./package.json');

program.version(packageJson.version);

help信息

helpcommander.js基于代码自动生成的,默认的帮助选项是-h--help

那么就简单的介绍完一些常规操作了,更多操作可以去官方文档上看,这里就不过多阐述了。

交互问题

很多脚手架在使用的时候,会和用户进行一些交互操作。这个可以通过inquirer.js去使用。

inquirer文档

//...
const answer = await inquirer.prompt([
    {
        type: 'input',
        name: 'name',
        message: '请输入项目名称',
    },
]);

提示反馈

在命令行中执行脚本命令后,一般会有一些比较友好的提示,例如:loadingsuccesserror等等。这个可以使用ora

ora文档

const ora = require('ora');
const loading = ora('Loading unicorns');

loading.text = '疯狂加载中';
loading.color = 'green';
loading.start();

还有例如colorschalk等等,就不一一阐述了。

拉取模板文件

脚手架最重要的一部就是根据用户的输入,去拉取不同的模板文件。比如我输入语言选择typescript,那么拉取的模板文件自然是支持typescript的,这块是使用download-git-repo

download-git-repo文档

download-git-repo支持三大平台下载:

  • GitHub - github:owner/name or owner/name
  • GitLab - gitlab:owner/name
  • Bitbucket - bitbucket:owner/name

同时他还支持通过#去拉取不同分支上的代码,当然默认是master。那么我们这里其实就可以直接通过用户的输入,去拉取不同branch上的代码,去实现拉取不同的模板文件。(或者就建立多个仓库,拉不同仓库代码)。

const download = require('download-git-repo');
const downloadAdress = lang => `owner/name#${LANG_LIST[lang]}`;

program
  .command('create')
  .description('初始化项目')
  .action(async () => {
    const answer = await inputer.prompt([
      //...
      {
        type: 'list',
        message: '使用哪种语言进行开发',
        name: 'lang',
        choices: ['typescript', 'javascript'],
      },
    ]);
    
    download(
      downloadAdress(answer.lang),
      `./${answer.name}`,	// path
      downloadCallback.bind(null, answer),	// callback
    );
  });

修改package.json

因为拉取的模板文件,package.json都是固定写好的。而大多数脚手架都是在初始化的时候,根据用户输入,来替换package.json中的相应字段。

这个就是一个文件读写操作,通过readFileSync去读文件内容,再替换相应字段,再重新写入。

const filename = `${answer.name}/package.json`;

if (fs.existsSync(filename)) {
  let newPagJson = fs.readFileSync(filename).toString();

  newPagJson = JSON.parse(newPagJson);
  newPagJson = {...newPagJson, ...answer};
  newPagJson = JSON.stringify(newPagJson, null, '\t');

  fs.writeFileSync(filename, newPagJson);
  
  //...
}

利用stringify的其他参数,将newPagJson格式化,使其重新写入也是格式正常的。

发布

在开发完成后,我们一般都会选择发布到npm平台上。

npm链接

发布流程也很简单:

  • npm login
  • npm publish,记得更新version

删除发布

因为每次更新发布,都会在npm的历史里留存,如果想删除某次或者整个npm包,可以使用npm unpublish

使用也很简单

$ npm unpublish [<@scope>/]<pkg>@<version>
$ npm unpublish [<@scope>/]<pkg> --force

前者表示删除某个版本npm包,只是删除后,该version再也不能使用了,也不能重新发布该version

后者表示删除整个npm包,删除后需要24小时后,才能重新发布。

检查更新

脚手架的发布后,可能有些用户没有手动更新,如果没有什么提示功能,可能一直不会选择更新。那么我们可以在脚手架使用的时候,去判断最新版本与当前版本是否一致,并决定是否提示更新。

因为拉取版本是需要花费时间的,所以一般是间隔性的判断。

这里我是保存上次拉取时间,通过当前时间戳和上次拉取时间戳之差,判断本次是否需要判断。

const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const resolve = _path => path.join(__dirname, _path);

const timePath = resolve('index.txt');

const MAX_TIME = 86400000;

const checkTime = () => {
  const lastTime = +fs.readFileSync(timePath).toString();
  const nowTime = new Date().getTime();

  if (lastTime && nowTime - lastTime <= MAX_TIME) {
    return;
  }

  fs.writeFileSync(timePath, nowTime);

  const lastV = execSync('npm view yourNpmPackage version', { encoding: 'utf8' });
  return lastV;
};

这块涉及的其实就是node基本api操作。如果对node不熟悉可以多看看node文档,因为脚手架的开发,和node息息相关的。

node文档

以上的内容,其实还是属于基操的。

我也是在这周空闲的时候有想法,决定自己做一个脚手架,一边复习一边学习。之后也会尽量完善它,之后如果有新的内容,也会分享出来。