赞
踩
Hello大家好我是⛄,之前我们已经成功搭建了一套Vue3的快速开发模板,提高我们搭建新项目的效率,但是当我们的模板逐渐增多,如果依然使用
git clone
的方式去下载模板较为繁琐。为了解决这个问题,我们就可以自己去搭建一个命令行交互式的工具包去生成我们需要的模板。
GitHub:
- Vue3开发模板:LonelySnowman/sv3-template
- 自定义cli工具包:LonelySnowman/arceus-cli
官方文档:SV3-Family | Vue3
arceus-cli/
|- bin/ # node 命令配置
|- src/ # 项目资源
|- command/ # 命令逻辑
|- utils/ # 公共方法
|- constants.ts # 存放公共变量
|- types.ts # 类型文件
|- index.ts # 命令入口文件
建议大家可以预先去了解下这些依赖的用途和一些基础的使用方法。
Tip:大家注意
chalk
和log-symbols
都要安装4版本,因为高版本已经不支持commonjs
,但是我们最后输出的是commonjs
模块。
命令行交互
打包工具
rollup(打包工具有很多选择,webpack,vite,rollup,tsup…)
@rollup/plugin-node-resolve:支持rollup打包node.js模块
@rollup/plugin-commonjs:支持rollup打包commonjs模块
@rollup/plugin-json:支持rollup打包json文件
rollup-plugin-typescript2:支持rollup打包ts文件
@rollup/plugin-terser:压缩打包代码
rollup-plugin-node-externals:使rollup自动识别外部依赖
rollup.config.js
。npm install -D rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-json rollup-plugin-typescript2 @rollup/plugin-terser rollup-plugin-node-externals
import { defineConfig } from 'rollup'; import nodeResolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import externals from "rollup-plugin-node-externals"; import json from "@rollup/plugin-json"; import terser from "@rollup/plugin-terser"; import typescript from 'rollup-plugin-typescript2'; export default defineConfig([ { input: { index: 'src/index.ts', // 打包入口文件 }, output: [ { dir: 'dist', // 输出目标文件夹 format: 'cjs', // 输出 commonjs 文件 } ], // 这些依赖的作用上文提到过 plugins: [ nodeResolve(), externals({ devDeps: false, // 可以识别我们 package.json 中的依赖当作外部依赖处理 不会直接将其中引用的方法打包出来 }), typescript(), json(), commonjs(), terser(), ], }, ]);
我们还需要在package.json
中配置一个打包命令。
{
// ...
"build": "rollup -c rollup.config.js --bundleConfigAsCjs"
}
create
指令,在我们的入口文件src/index.ts
编写。commander
,可以帮助我们解析用户在命令行输入的指令。首先初始化一个Command
对象,传入的参数作为我们的指令名称。
import { Command } from 'commander'
// 这里我们用 arceus 当作我的指令名称
// 命令行中使用 arceus xxx 即可触发
const program = new Command('arceus');
接下来我们就可以配置我们需要的命令了。
version
可以实现最基础的查看版本的指令。import { version } from '../package.json'
// .vesion 表示可以使用 -V --version 参数查看当前SDK版本
// 我们直接使用 package.json 中的 version 即可
program
.version(version);
// 调用 version 的参数可以自定义
// .version(version, '-v --version')
使用command
与action
实现自定义指令。
下面的示例就是我们编写好的指令,指令回调我们稍后实现,输入arceus update
会打印update command
,输入arceus create test
,会打印create test
。action 回调中会将 argument 中的参数传入。
// ... program .command('update') .description('更新 arceus 至最新版本') .action(async () => { console.log('update command') }); program .command('create') .description('创建一个新项目') .argument('[name]', '项目名称') .action(async (name) => { if(name) console.log(`create ${name}`) else console.log(`create command`) });
// ...
// parse 会解析 process.argv 中的内容
// 也就是我们输入的指令
program.parse();
src/utils/log.ts
中封装一个带icon的输出提示。log-symbols
,他内置了 error,success,warning,info 对应的 icon ,并且帮我们兼容不支持 icon 的终端。并且后续我们需要用到的ora
作为加载动画,它也是用的log-symbols
进行提示,我们这里保持一致。(大家也可以自定义一些emoji
图标,效果也不错,就是需要自己兼容终端不支持emoji
的情况)import logSymbols from 'log-symbols' export const log = { error: (msg: string) => { console.log(logSymbols.error, msg) }, success: (msg: string) => { console.log(logSymbols.success, msg) }, warning: (msg: string) => { console.log(logSymbols.warning, msg) }, info: (msg: string) => { console.log(logSymbols.info, msg) }, } export default log
我们先实现create
命令,可以让用户选择下载我们预设的模板。
src/command/create.ts
文件下编写create
命令核心代码。@inquirer/prompts
,可以帮助我们让用户在终端进行输入或选择的操作,更多使用方法请查阅官方文档:inquirer.js。import { select, input } from '@inquirer/prompts';
export default async function create(prjName?: string) {
// 文件名称未传入需要输入
if (!prjName) prjName = await input({ message: '请输入项目名称' });
// 如果文件已存在需要让用户判断是否覆盖原文件
const filePath = path.resolve(process.cwd(), prjName)
if (fs.existsSync(filePath)) {
const run = await isOverwrite(prjName)
if (run) {
await fs.remove(filePath)
} else {
return // 不覆盖直接结束
}
}
}
src/utils/file.ts
中封装一个判断用户是否覆盖的公共方法。import { select } from '@inquirer/prompts';
import log from "./log";
export const isOverwrite = async (fileName: string) => {
log.warning(`${fileName} 文件已存在 !`)
return select({
message: '是否覆盖原文件: ',
choices: [
{name: '覆盖', value: true},
{name: '取消', value: false}
]
});
}
src/constants.ts
保存我们拥有的模板,定义成map
的形式是方便我们根据key
获取项目的信息。import { TemplateInfo } from "./types"; // 这里保存了我写好的两个预设模板 感兴趣的大家可以看看往期文章 export const templates: Map<string, TemplateInfo> = new Map( [ ["sv3-template", { name: "sv3-template", downloadUrl: 'git@github.com:LonelySnowman/sv3-template.git', description: 'vue3快速开发模板', branch: 'main' }], ["sv3-template-thin", { name: "sv3-template-thin", downloadUrl: 'git@github.com:LonelySnowman/sv3-template.git', description: 'vue3快速开发模板(精简版)', branch: 'thin' }] ] )
import { select, input } from '@inquirer/prompts'; import { templates } from "../constants"; import { TemplateInfo } from "../types"; import log from "../utils/log"; export default async function create(prjName?: string) { // ... // 我们需要将我们的 map 处理成 @inquirer/prompts select 需要的形式 // 大家也可以封装成一个方法去处理 const templateList = [...templates.entries()].map((item: [string, TemplateInfo]) => { const [name, info] = item; return { name, value: name, description: info.description } }) // 选择模板 const templateName = await select({ message: '请选择需要初始化的模板:', choices: templateList, }); // 下载模板 const gitRepoInfo = templates.get(templateName) if (gitRepoInfo) { await clone(gitRepoInfo.downloadUrl , prjName, ['-b', `${gitRepoInfo.branch}`]) } else { log.error(`${templateName} 模板不存在`) } }
我们还需要实现我们刚刚使用过的clone
方法,下载仓库中的模板。
我们在src/utils/clone.ts
中实现下。
simple-git
用于拉取 git 仓库,progress-estimator
设置预估git clone
的时间并展示进度条。这里我就直接展示代码和注释了,思路都很简单。
import simpleGit, { SimpleGit, SimpleGitOptions } from 'simple-git'; import log from "./log"; import createLogger from "progress-estimator"; import chalk from "chalk"; const logger = createLogger({ // 初始化进度条 spinner: { interval: 300, // 变换时间 ms frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map(item=>chalk.blue(item)) // 设置加载动画 } }) const gitOptions: Partial<SimpleGitOptions> = { baseDir: process.cwd(), // 根目录 binary: 'git', maxConcurrentProcesses: 6, // 最大并发进程数 }; export const clone = async (url: string, prjName: string, options: string[]): Promise<any> => { const git: SimpleGit = simpleGit(gitOptions) try { // 开始下载代码并展示预估时间进度条 await logger(git.clone(url, prjName, options), '代码急速下载中: ', { estimate: 7000 // 展示预估时间 }) // 下面就是一些相关的提示 console.log() console.log(chalk.blueBright(`==================================`)) console.log(chalk.blueBright(`=== 欢迎使用 arceus-cli 脚手架 ===`)) console.log(chalk.blueBright(`==================================`)) console.log() log.success(`项目创建成功 ${chalk.blueBright(prjName)}`) log.success(`执行以下命令启动项目:`) log.info(`cd ${chalk.blueBright(prjName)}`) log.info(`${chalk.yellow('pnpm')} install`) log.info(`${chalk.yellow('pnpm')} run dev`) } catch (err: any) { log.error("下载失败") log.error(String(err)) } }
create
命令就编写完毕了,我们可以将其添加到src/index.ts
中去调用。// ...
program
.command('create')
.description('创建一个新项目')
.argument('[name]', '项目名称')
.action(async (dirName) => {
await create(dirName);
});
// ...
在src/utils/npm.ts
中编写方法,用于获取npm包的信息及版本号。
// npm 包提供了根据包名称查询包信息的接口
// 我们在这里直接使用 axios 请求调用即可
export const getNpmInfo = async (npmName: string) => {
const npmUrl = 'https://registry.npmjs.org/' + npmName
let res = {}
try {
res = await axios.get(npmUrl)
} catch (err) {
log.error(err as string)
}
return res
}
npm包信息中包含了该包的最新版本,我们在这里直接引用即可。
export const getNpmLatestVersion = async (npmName: string) => {
// data['dist-tags'].latest 为最新版本号
const { data } = (await getNpmInfo(npmName)) as AxiosResponse
return data['dist-tags'].latest
}
然后对比版本号版本,判断是否需要更新,如需更新进行提示。
export const isNeedUpdate = async (name: string, curVersion: string) => {
const latestVersion = await getNpmLatestVersion(name)
const need = lodash.gt(latestVersion, curVersion)
if(need) {
log.info(`检测到 arceus 最新版:${chalk.blueBright(latestVersion)} 当前版本:${chalk.blueBright(curVersion)} ~`)
log.info(`可使用 ${chalk.yellow('npm')} install arceus-cli@latest 更新 ~`)
}
return need
}
然后我们将这个判断更新的方法添加到create
方法中。
export default async function create(prjName?: string) {
// ...
await isNeedUpdate(name, version) // 检测版本更新
// ...
}
当我们发布新的版本,用户可以第一时间看到。
npm run build
,打包后的代码会输出到dist/index.js
中。node
在本地执行,先测试一下我们编写好的create
命令。node .\dist\index.js create
不出意外是可以看到我们写好的交互逻辑,如果有报错,大家可以根据对应的问题查询下,也可以给我留言。
npm
上。arceus
作为命令名调用。需要我们修改一下package.json
文件,下面是一些必要的配置,都加上了注释,我们需要重点关注bin
这一项。
bin
中的配置是一个对象,需要有 “key” 和 “value”。
{ "name": "arceus-cli", // 包名称 "version": "x.x.x", // 包版本 "description": "arceus脚手架", // 包描述 "main": "dist/index.js", // 库入口文件 "engines": { // 推荐使用 node 版本号 "node": ">=16" }, "keywords": [ // 包查询关键词 "sv3-template" ], "files": [ // npm 包需要上传的文件 "dist", "bin", "README.md" ], "author": { // 作者信息 "name": "lonelysnowman" }, "bin": { "arceus": "bin/index.js" // npm 会在 .bin 目录中配置 arceus 执行 bin/index.js }, // ... }
编写bin/index.js
#!/usr/bin/env node
require('../dist'); // 执行我们打包好的 dist/index.js 文件
需要在第一行加入#!/usr/bin/env node
,/usr/bin/env
就是告诉系统可以在PATH目录中查找,#!/usr/bin/env node
就是解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件。
然后我们就可以将npm包发布啦。
npm login # 发布前需要先登录下
npm publish # 会按照我们 package.json 中的 files 配置的文件发布 name 作为包名称
# 如果需要迭代包的版本 要先修改版本号再发布
npm version patch # 0.0.0 -> 0.0.1
npm version minor # 0.0.0 -> 0.1.0
npm version major # 0.0.0 -> 1.0.0
发布完成后我们就可以安装npm
全局包然后进行使用啦。
npm install arceus-cli -g
到这里我们就解决了所有的问题,实现了一个简易的cli工具,做到了clone在git仓库中的固定模板。如果我们想实现动态模板就需要使用其他的技术,可以使用字符串插值或者cjs模板,根据用户的选择动态生成需要的模板代码。大家可以根据自己的需要自行拓展
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。