一起养成写作习惯!这是我参与「掘金日新计划 · 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:自动化处理重复的事
步骤
- 初始化一个npm项目,采用默认配置即可
mkdir test-cli && cd test-cli && npm init -y // -y 表示可按照默认配置初始化npm项目
- 安装第三方依赖
npm i chalk log-symbols ora download-git-repo fs-extra inquirer commander shelljs
- 在项目根目录下依次新建js文件,文件中的相关依赖采用import的方式的导入,并用export导出函数:
- cli.js:入口文件,负责调用初始化项目的执行函数
- init.js:初始化项目函数的具体实现,负责调用下载模板、询问配置信息、安装依赖等工作
- clone.js:下载远端git仓库模板
- 修改package.json文件
- type字段的值可能为commonjs(默认值),适用于Nodejs环境;也可能为module,即ES Module语法,适用于浏览器环境。本项目需要将type指定为module
- 如何将一些可执行js文件暴露出去,需要在bin属性中新增命令以及对应的执行文件:
// package.json
{
...,
"bin": {
"test-cli": "./cli.js"
}
}
执行命令为test-cli,执行文件为./cli.js
- 开发调试
在开发过程中,可通过将当前项目链接到全局的方式,然后再使用,避免每次将脚手架发布到npm仓库。在当前项目执行npm link,则可以在...\AppData\Roaming\npm\node_modules中找到当前项目的一个链接,然后就可以在全局使用test-cli命令来创建项目。
实现代码
- 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)
- 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;
- 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();
})
})
}
发布与更新
- 发布方式一
- 使用npm login登录当前npm仓库,需要相关权限
- 使用npm publish将当前项目发布到npm仓库,稍等一会儿刷新即可使用
- 发布方式二
- 在当前项目执行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 创建并初始化项目
- 项目地址
原创不易,转载请注明出处。