手把手新建自定义脚手架(超详细)

4,120 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情

前言

新建项目,可用vue-cli等脚手架工具创建,但若需要根据项目的需要,新建自定义模板的项目时,vue-cli等工具就有点不方便了。虽然也可以先使用vue-cli创建,然后再根据项目手册将相关配置、api封装、依赖安装到项目中,但是这种方法不够灵活,而且繁琐。因此就需要一个自定义的脚手架,也能像vue-cli一样根据模板创建项目,但是可以更灵活,更加定制化。

实现思路

  • 从远端git仓库拉取项目模板,并初始化项目
  • 在拉取的模板基础上,根据用户在交互界面的输入信息,更新项目配置文件,并完成相关依赖安装
  • 发布到npm,可供当前npm仓库的所有用户全局安装使用

快速开始

第三方依赖

  • chalk:终端字体颜色
  • log-symbols:在终端上显示√或×等图标
  • ora:终端显示下载中的动画
  • download-git-repo:下载并提取git仓库
  • fs-extra:删除非空文件夹
  • inquirer:通用的命令行用户界面集合,用于交互
  • commander:解析命令和参数,用于处理用户输入的命令
  • shelljs:自动化处理重复的事

步骤

  1. 初始化一个npm项目,采用默认配置即可
mkdir test-cli && cd test-cli && npm init -y // -y 表示可按照默认配置初始化npm项目
  1. 安装第三方依赖
npm i chalk log-symbols ora download-git-repo fs-extra inquirer commander shelljs
  1. 在项目根目录下依次新建js文件,文件中的相关依赖采用import的方式的导入,并用export导出函数:
  • cli.js:入口文件,负责调用初始化项目的执行函数
  • init.js:初始化项目函数的具体实现,负责调用下载模板、询问配置信息、安装依赖等工作
  • clone.js:下载远端git仓库模板
  1. 修改package.json文件
  • type字段的值可能为commonjs(默认值),适用于Nodejs环境;也可能为module,即ES Module语法,适用于浏览器环境。本项目需要将type指定为module
  • 如何将一些可执行js文件暴露出去,需要在bin属性中新增命令以及对应的执行文件:
// package.json
{
  ...,
  "bin": {
    "test-cli": "./cli.js"
  }
}

执行命令为test-cli,执行文件为./cli.js

  1. 开发调试

在开发过程中,可通过将当前项目链接到全局的方式,然后再使用,避免每次将脚手架发布到npm仓库。在当前项目执行npm link,则可以在...\AppData\Roaming\npm\node_modules中找到当前项目的一个链接,然后就可以在全局使用test-cli命令来创建项目。

实现代码

  1. cli.js

引入commander,并定义创建项目的命令,本项目无法使用require的方式

#! /usr/bin/env node
// 必须在文件头添加如上内容指定运行环境为node
import initAction from './init.js'
import commander from 'commander' // 处理用户输入的命令

// 创建项目命令
commander
  .Command('create <name>') // 定义create子命令,<name>为必需参数,可在action的function中接收;如果需要设置为非必需参数,可使用[]
  .option('-f, --force', '强制覆盖本地同名项目') // 配置参数
  .description('使用脚手架创建项目') // 命令描述说明
  .action(initAction) // 执行函数

// 利用commander解析命令行输入,必须写在所有内容最后面
commander.parse(process.argv)
  1. init.js

思路:

  • 首先需要判断是否支持git,以及输入的项目名是否存在、是否合法
  • 如果存在重名项目,并且用户输入了-f强制覆盖,则先删除项目,然后在新建
  • 下载指定仓库地址的项目模板
  • 自定义稳定,询问使用者。便于使用者根据实际需要修改已拉取模板的package.json文件,未输入或者输入空格将保留原有的配置
  • 新拉取的模板有远端仓库的相关联的git信息,需要初始化或者删除相关文件。本脚手架采用重新初始化git的方式,进入项目路径,执行git init。
  • 脚手架的默认npm仓库地址为taobao的镜像,根据默认的npm仓库地址,安装依赖并显示安装进度
  • 完成项目的创建以及初始化
#! /usr/bin/env node

import fs from 'fs'
import fsExtra from 'fs-extra'
import ora from 'ora'
import shell from 'shelljs'
import chalk from 'chalk'
import symbol from 'log-symbols'
import inquirer from 'inquirer'
import clone from './clone.js'

const remote = 'http://xxxxx.git' // 远端仓库地址
let branch = 'master'
const registry = 'https://xxxx' // npm 仓库地址

const initAction = async (name, option) => {
  // 检查控制台是否可运行git
  if (!shell.which('git')) {
    console.log(symbol.error, 'git命令不可用!');
    shell.exit(1); // 退出
  }
  // 验证name输入是否合法
  if (name.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
    console.log(symbol.error, '项目名称存在非法字符!');
    return;
  }
  // 验证name是否存在
  if (fs.existsSync(name) && !option.force) {
    console.log(symbol.error, `已存在项目文件夹${name}`);
    return;
  } else if (option.force) {
    // 强制覆盖
    const removeSpinner = ora(`${name}已存在,正在删除文件夹…`).start();
    try {
      fsExtra.removeSync(`./${name}`)
      removeSpinner.succeed(chalk.green('删除成功'))
    } catch(err) {
      console.log(err);
      removeSpinner.fail(chalk.red('删除失败'))
      return;
    }
  }
  // 下载模板
  await clone(`direct:${remote}#${branch}`, name, {
    clone: true
  })
  // 下载完毕后,定义自定义问题
  let questions = [
    {
      type: 'input',
      message: `请输入项目名称:(${name})`,
      name: 'name',
      validate(val) {
        if (val.match(/[^A-Za-z0-9\u4e00-\u9fa5_-]/g)) {
          return '项目名称包含非法字符'
        }
        return true;
      }
    },
    {
      type: 'input',
      message: '请输入项目关键词(,分割):',
      name: 'keywords'
    },
    {
      type: 'input',
      message: '请输入项目简介:',
      name: 'description'
    },
    {
      type: 'input',
      message: '请输入您的名字:',
      name: 'author'
    },
  ];
  // 通过inquirer获取用户输入的回答
  let answers = await inquirer.prompt(questions);
  // 将用户配置信息打印一下,确认是否正确
  console.log('---------------------');
  console.log(answers);
  // 确认是否创建
  let confirm = await inquirer.prompt([{
    type: 'confirm',
    message: '是否确认创建项目',
    default: 'Y',
    name: 'isConfirm'
  }]);
  if (!confirm.isConfirm) {
    return false;
  }
  // 根据用户输入,调整配置文件
  // 读取package.json文件
  let jsonData = fs.readFileSync(`./${name}/package.json`, function(err, data) {
    console.log('读取文件', err, data);
  })
  jsonData = JSON.parse(jsonData)
  Object.keys(answers).forEach(item => {
    if (item === 'name') {
      // 如果未输入项目名,则使用文件夹名
      jsonData[item] = answers[item] && answers[item].trim() ? answers[item] : name
    } else if (answers[item] && answers[item].trim()) {
      jsonData[item] = answers[item]
    }
  })
  console.log('jsonData', jsonData);
  // 写入
  let obj = JSON.stringify(jsonData, null, '\t')
  fs.writeSync(`./${name}/package.json`, obj, function(err, data) {
    console.log('写入文件', err, data);
  })
  // 初始化git
  if (shell.exec(`cd ${shell.pwd()}/${name} && git init`).code !== 0) {
    console.log(symbol.error, chalk.red('git 初始化失败'));
    shell.exit(1)
  }
  // 自动安装依赖
  const installSpinner = ora('正在安装依赖…').start();
  if (shell.exec(`cd ${shell.pwd()}/${name} && npm config set registry ${registry} && npm install -d`).code !== 0) {
    console.log(symbol.error, chalk.yellow('自动安装依赖失败,请手动安装'));
    shell.exit(1)
  }
  installSpinner.succeed(chalk.green('依赖安装成功'))
  installSpinner.succeed(chalk.green('项目创建完成'))
  shell.exit(1)
}

export default initAction;
  1. clone.js

拉取git仓库,成功后返回项目内容

import download from "download-git-repo";
import ora from "ora";
import chalk from "chalk";
import logSymbols from "log-symbols";

export default function (remote, name, option) {
  const cloneSpinner = ora('正在拉取项目…').start();
  return new Promise((resolve, reject) => {
    download(remote, name, option, err =>{
      if (err) {
        cloneSpinner.fail();
        console.log(logSymbols.error, chalk.red(err));
        reject(err)
        return
      }
      cloneSpinner.succeed(chalk.green('拉取成功'))
      resolve();
    })
  })
}

发布与更新

  1. 发布方式一
  • 使用npm login登录当前npm仓库,需要相关权限
  • 使用npm publish将当前项目发布到npm仓库,稍等一会儿刷新即可使用
  1. 发布方式二
  • 在当前项目执行npm pack,将项目打包成.tgz文件,并上传到npm 仓库

每次更新代码后,需要更新package.json中的version字段,可手动修改,也可以使用命令:

  • npm version major:大版本加1
  • npm version minor:中版本加1
  • npm version patch:小版本加1

如何使用

  • 根据本文中的步骤和代码创建脚手架项目
  • 修改本文中涉及的git模板仓库地址、远端分支(github的主分支为main)、npm仓库地址,其他可根据需要修改
  • 发布到公网或者内网的npm仓库地址,首先全局安装:npm install test-cli -g,
  • 然后就可以使用test-cli create projectName 创建并初始化项目
  • 项目地址

原创不易,转载请注明出处