diy在线定制网站系统,网站备案有什么作用,网站免费建站app,微商代理平台前言
vue-cli 和 create-react-app 等 cli 脚手架工具用于快速搭建应用#xff0c;无需手动配置复杂的构建环境。本文介绍如何使用 rollup 搭建一个脚手架工具。
脚手架工具的工作流程简言为#xff1a;提供远端仓库各种模版 用户通过命令选择模版 拉取仓库代码 …前言
vue-cli 和 create-react-app 等 cli 脚手架工具用于快速搭建应用无需手动配置复杂的构建环境。本文介绍如何使用 rollup 搭建一个脚手架工具。
脚手架工具的工作流程简言为提供远端仓库各种模版 用户通过命令选择模版 拉取仓库代码
分别对应如下几个重要模块
配置 打包命令配置 命令行交互如 create、-v 等其中 create 是核心逻辑用于根据用户的选择拉取远程仓库中相应的初始代码。发布至 npm
1. 初始化项目
初始化项目
npm init -y生成typescript 配置文件 tsconfig.json在此之前需要确保全局安装了 TypeScript
npm install -g typescript // 如果已经安装无需理会
npx tsc --initpackage.json 中添加依赖
devDependencies: {inquirer/prompts: ^3.2.0,rollup/plugin-commonjs: ^25.0.3, rollup/plugin-json: ^6.0.1, rollup/plugin-node-resolve: ^15.1.0, rollup/plugin-terser: ^0.4.3, types/fs-extra: ^11.0.2,types/lodash: ^4.14.199,types/node: ^16.18.40,axios: ^1.5.0,chalk: ^4.1.2,commander: ^11.0.0,figlet: ^1.8.0,fs-extra: ^11.1.1,lodash: ^4.17.21,log-symbols: ^4.1.0,ora: 5,progress-estimator: ^0.3.1,pure-thin-cli: ^0.1.8,rollup: ^4.6.1,rollup-plugin-dts: ^5.3.0, rollup-plugin-esbuild: ^5.0.0,rollup-plugin-node-externals: ^5.1.2, rollup-plugin-typescript2: ^0.36.0, simple-git: ^3.19.1,tslib: ^2.6.1,typescript: ^5.2.2
}本文用到的所有依赖说明下文中也会一一介绍依赖的用处与用法
devDependencies: {// 用于命令行交互。inquirer/prompts: ^3.2.0,// Rollup 相关的插件用于模块打包rollup/plugin-commonjs: ^25.0.3, // 支持rollup打包commonjs模块rollup/plugin-json: ^6.0.1, // 支持rollup打包json文件rollup/plugin-node-resolve: ^15.1.0, // 用于帮助 Rollup 解析和处理 Node.js 模块Node.js 的 CommonJS 模块规范rollup/plugin-terser: ^0.4.3, // Rollup 构建过程中对生成的 JavaScript 代码进行压缩和混淆以减小最终输出文件的体积。// TypeScript 的类型定义文件types/fs-extra: ^11.0.2,types/lodash: ^4.14.199,types/node: ^16.18.40,// 用于发起 HTTP 请求。 axios: ^1.5.0,// 在命令行中输出彩色文本。chalk: ^4.1.2,// 命令行界面的解决方案 commander: ^11.0.0,// 优化打印效果figlet: ^1.8.0,// 扩展了标准 fs 模块的文件系统操作fs-extra: ^11.1.1,// 一个提供实用函数的 JavaScript 库。 lodash: ^4.17.21,// 在命令行中显示日志符号。 log-symbols: ^4.1.0,// 创建可旋转的加载器 ora: 5,// 估算操作进度。 progress-estimator: ^0.3.1,// 一个特定于项目或定制的 CLI 工具 pure-thin-cli: ^0.1.8,rollup: ^4.6.1,rollup-plugin-dts: ^5.3.0, // 是一个 Rollup 插件它的主要作用是处理 TypeScript 的声明文件.d.ts 文件rollup-plugin-esbuild: ^5.0.0,rollup-plugin-node-externals: ^5.1.2, // 使rollup自动识别外部依赖rollup-plugin-typescript2: ^0.36.0, // 支持rollup打包ts文件// 用于 Git 命令的 Node.js 封装。 simple-git: ^3.19.1,// TypeScript 运行时库。 tslib: ^2.6.1,typescript: ^5.2.2
},目录结构如下index.js命令入口文件command命令逻辑utils公共方法 2. 配置打包命令
下载依赖
pnpm add -D rollup rollup/plugin-node-resolve rollup/plugin-commonjs rollup/plugin-json rollup-plugin-typescript2 rollup/plugin-terser rollup-plugin-node-externals依赖说明
rollup打包工具有很多选择如webpackrollup/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
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 中配置打包命令-c 指定 rollup 配置文件--bundleConfigAsCjs 将配置转为 commonjs 执行。
{......scripts: {......build: rollup -c rollup.config.js --bundleConfigAsCjs},
}index.ts 中加入一定代码后执行 npm run build 测试打包结果
发现如下报错这是因为默认生成的 tsconfig.json 中 moudle 为 commonjs改为 module: ES2015, module: ES2020, module: ES2022, or module: ESNext 后即可。 再次执行 npm run build打包配置完毕 3. 命令行交互配置依赖介绍
查看 create-react-app cli 的源码发现其使用了如下依赖我们也选择一部分安装 本文使用了如下依赖
commander解析命令行指令ora终端加载动画progress-estimator终端加载条动画log-symbols终端输出符号chalk终端字体美化inquirer/prompts终端输入交互
其中最重要的是 commander下载pnpm install commander -D用于解析用户在命令行输入的指令可以到官方文档查看基本使用并应用到 src/index.ts 中。 4. -v --version指令配置
src/index.ts 中尝试导入 Command 和 version 遇到如下报错 根据提示将 tsconfig.json 中默认注释的 moduleResolution 放开即可发现导入 package.json 仍报错将 resolveJsonModule 取消注释。 继续完善 index.ts自定义指令名称为 benchu和 vite、vue-cli 一样就是一个命令名添加 -v 或 --version 返回版本号版本号为 package.json 中的 version 字段该字段会在每次提交上传至 npm 时强制更新。
import { Command } from commander
import { version } from ../package.jsonconst program new Command(benchu)
program.version(version, -v, --version, 获取版本号)program.parse()打包后测试自定义命令 benchu尝试查看版本与帮助说明 5. create 指令配置
声明创建命令如使用 vue-cli 创建项目时输入的 vue create可以让用户选择下载预设的模板我们也命名一个 create 指令。
5.1 让用户输入项目名称 并 选择初始模版
在 src/command/create.ts 文件下编写 create 命令核心代码导出一个可以传入项目名称的方法如果用户直接传入了项目名称则让用户选择模板否则需要先让用户输入项目名称
create 后可以紧跟参数 name代表项目名称该参数为可选参数可以只执行 create后续步骤中要求用户输入项目名。
import { Command } from commander
import { version } from ../package.jsonconst program new Command(benchu)
program.version(version, -v, --version, 版本号)// create 指令
program.command(create).description(初始化新项目).argument([name], 项目名称) // [name]表示可选参数name表示必填参数.action((dirName) {console.log(init, dirName)})program.parse()重新打包执行 npm run build 后运行 dist/index.js 查看 command 此时已经可以看到 create 命令且正确拿到了 dirName 接下来处理 create 核心逻辑部分首先在 src/index.ts 中将 dirName 交予 create command/create.ts首先需要确认预设模版与对应远端仓库的关系如本文连接到本人 gitee 的某个仓库其中不同分支对应不同模版可以供用户选择
mastervitetsvue3axiospiniaelementvitetsvue3axiospiniaelementtailwindelement_layoutvitetsvue3axiospiniaelementtailwind搭建完毕layout
inquirer/prompts可以帮助我们让用户在终端进行输入或选择的操作本文使用到了 input 和 select更多使用方法可查阅官方文档inquirer.js。
select 要求数据格式如下
import { select, input } from inquirer/prompts
export interface TemplateInfo {name: string // 模板名称downloadUrl: string // 模板下载地址description: string // 模板描述branch: string // 模板分支
}export const templates: Mapstring, TemplateInfo new Map([[Vite-Vue3-TypeScript-template,{name: Vite-Vue3-TypeScript-template,downloadUrl: gitgitee.com:tian__shuai/template-vite5--vue3.git,description: vite vue3 ts初始模版,branch: master,},],[Vite-Vue3-TypeScript-ElementUI-template,{name: Vite-Vue3-TypeScript-ElementUI-template,downloadUrl: gitgitee.com:tian__shuai/template-vite5--vue3.git,description: vite vue3 ts elementplus 初始模版,branch: element,},],[Vite-Vue3-TypeScript-ElementUI-layout-template,{name: Vite-Vue3-TypeScript-ElementUI-layout-template,downloadUrl: gitgitee.com:tian__shuai/template-vite5--vue3.git,description: vite vue3 ts elementplus layout 初始模版,branch: element_layout,},],
])export async function create(projectName?: string) {if (!projectName) {projectName await input({ message: 请输入项目名称 })}// 初始化模版列表const templateList Array.from(templates).map((item: [string, TemplateInfo]) {const [name, info] itemreturn {name,value: name,description: info.description,}})// 选了哪个模版const templateName await select({message: 请选择模版,choices: templateList,})// 选中模版的详情const info templates.get(templateName)console.log(info)console.log(create, projectName)
}测试是否可以获取用户选择的模版详情执行 npm run build node dist/index.js create 输入项目名称后通过上下键选择需要的模版最下方就是定义的 description 选择后获取到该模版的 info create 时如果传递参数 name则不会触发 input而是直接进入 select 选择模版 5.2 下载选择的模版
新建 src/utils/clone.ts用于处理下载模版使用 simple-git 拉取 git 仓库progress-estimator 设置预估 git clone 的时间并展示进度条。
参考 simple-git官方文档 git clone 需要传入三个参数
url仓库地址localPath目标路径options分支信息 src/utils/clone.ts接收这三个参数
import simpleGit from simple-git
export const clone (url: string, projectName: string, options: string[]) {}将上一步 src/utils/create.ts 通过 select 拿到的模版 info 传入 src/utils/clone.ts 处理。 在项目根目录下创建 project 目录用于存储下载的项目模版 完善 command/clone.ts
import { simpleGit, SimpleGit, SimpleGitOptions } from simple-gitconst getOptions: PartialSimpleGitOptions {baseDir: ${process.cwd()}/project, // 指定 simple-git 操作的目录默认为 process.cwd() 表示当前目录我这里设置为根目录下的 project 目录方便查看克隆多个项目的效果binary: git, // 指定 git 的二进制文件位置maxConcurrentProcesses: 6, // 最大并发进程数trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}export const clone async (url: string,projectName: string,branchOptions: string[]
) {const git: SimpleGit simpleGit(getOptions)try {await git.clone(url, projectName, branchOptions)console.log()console.log(代码下载完成)console.log()console.log( 欢迎使用 benchu-cli )console.log()console.log()console.log( pnpm install 安装依赖 pnpm run dev 运行项目 )} catch (e) {console.log(clone error, e)}
}优化下载时的样式使用 progress-estimator 添加进度条progress-estimator官方文档地址。
import { simpleGit, SimpleGit, SimpleGitOptions } from simple-git
import createLogger from progress-estimator// 初始化进度条
const logger createLogger({spinner: {interval: 100, // 100毫秒刷新一次frames: [⠋, ⠙, ⠹, ⠸], // 进度条的样式},
})const getOptions: PartialSimpleGitOptions {baseDir: ${process.cwd()}/project, // 指定 simple-git 操作的目录默认为 process.cwd() 表示当前目录binary: git, // 指定 git 的二进制文件位置maxConcurrentProcesses: 6, // 最大并发进程数trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}export const clone async (url: string,projectName: string,branchOptions: string[]
) {const git: SimpleGit simpleGit(getOptions)try {await logger(git.clone(url, projectName, branchOptions), 代码下载中..., {estimate: 5000, // 预计下载时间})console.log()console.log(代码下载完成)console.log()console.log( 欢迎使用 benchu-cli )console.log()console.log()console.log( pnpm install 安装依赖 pnpm run dev 运行项目 )} catch (e) {console.log(clone error, e)}
}效果 继续优化样式使用 chalk 添加颜色chalk官方文档。 src/command/clone.ts 完整代码
import { simpleGit, SimpleGit, SimpleGitOptions } from simple-git
import createLogger from progress-estimator
import chalk from chalk// 初始化进度条
const logger createLogger({spinner: {interval: 100, // 100毫秒刷新一次frames: [⠋, ⠙, ⠹, ⠸].map((item) chalk.blue(item)), // 进度条的样式},
})const getOptions: PartialSimpleGitOptions {baseDir: ${process.cwd()}/project, // 指定 simple-git 操作的目录默认为 process.cwd() 表示当前目录binary: git, // 指定 git 的二进制文件位置maxConcurrentProcesses: 6, // 最大并发进程数trimmed: false, // git 输出的结果是否自动去除前后多余的空白字符
}export const clone async (url: string,projectName: string,branchOptions: string[]
) {const git: SimpleGit simpleGit(getOptions)try {await logger(git.clone(url, projectName, branchOptions), 代码下载中..., {estimate: 5000, // 预计下载时间})console.log()console.log(chalk.green(代码下载完成))console.log()console.log( 欢迎使用 benchu-cli )console.log()console.log()console.log( pnpm install 安装依赖 pnpm run dev 运行项目 )} catch (e) {console.log(clone error, e)}
}5.3 项目名相同检查是否需要覆盖更新
command/create.ts 中加入如下代码判断项目名是否重复并询问用户是否需要覆盖如果覆盖删除原有项目并让用户重新选择模版下载。
import path from path
import fs from fs-extra// 省略其余代码 ...// 是否覆盖同名项目
export function isOverwrite(projectName: string) {return select({message: 项目已存在是否覆盖,choices: [{ name: 覆盖, value: true },{ name: 不覆盖, value: false },],})
}export async function create(projectName?: string) {if (!projectName) {projectName await input({ message: 请输入项目名称 })}// 判断是否覆盖同名项目const projectPath path.resolve(${process.cwd()}/project, projectName) // 这里的路径保持和 clone.ts 中 simple-git 的 dirName 一致if (fs.existsSync(projectPath)) {const isRun await isOverwrite(projectName)if (isRun) {await fs.remove(projectPath)} else {return}}const templateName await select({message: 请选择模版,choices: templateList,})const info templates.get(templateName)// 下载模版if (info) {clone(info.downloadUrl, projectName, [-b, info.branch])}
}选择覆盖后原有的项目删除选择新模版后重新下载 至此一个功能极简的 脚手架工具 已经完成下面需要考虑发布到 npm。
6. 发布至npm
发布前需要修改测试阶段创建的 src/project 目录当时为了方便观察 create 效果然而给用户使用时采用默认的 process.cwd() 即可。 6.1 README.md
需要添加 README.md 说明文档分享一个在线生成图标网站用于生成 npm 版本图标 也可以添加一些 iconicon库地址 README.md 6.2 完善package.json
6.2.1 bin
补全 package.json其中 bin 配置了命令 benchu这个命令会映射到项目中的 bin/index.js 即输入 benchu 就等于执行 bin/index.js相当于测试阶段打包后执行 node dist/index.js 或 npx benchu 。 在运行 benchu 命令时实际上执行的脚本是 bin/index.js需要确保这个文件具有正确的可执行权限并在文件头部指定 Node.js 环境。
bin/index.js
#!/usr/bin/env node
require(../dist) // 执行编译后的文件 dist/index.js6.2.2 files
files 字段在 package.json 中用于指定哪些文件或目录应包含在发布到 npm 注册表的包中如果未设置 files 字段npm 默认会包括除了 .gitignore 和 .npmignore 文件中忽略的内容外的所有文件。
我这里只将 bin、dist、README.md 发布到npm。 6.3 发包
确认打包完成
检查 npm 源如果是 淘宝源 则需要改回 npm 源。
查看npm镜像源地址
npm config get registry我这里是淘宝镜像 切换到 npm 源
npm config set registry https://registry.npmjs.org/登录 npm
npm login发布
npm publish注意 每次 publish 都会强制要求更新 package.json 中的 version可以手动修改也可以通过命令 npm version patch执行此命令前需要保证 Git 工作目录中没有未提交的更改或未跟踪的文件。
发布完毕后即可在 npm 看到该包 可以看到 package.json 中 files 字段 7. 测试发布的包
全局下载 benchu-cli
npm install benchu-cli -g执行命令
benchu create8. 检查 npm 包的版本并提示更新
当 benchu-cli 更新后需要给用户提示
command/create.ts 中在检查是否需要覆盖项目后检查是否需要版本更新
安装 axiosget 获取 npm 包的详情
pnpm install axios -D// ... 忽略其余包
import { name, version } from ../../package.json
import axios from axios// 获取 npm 包的最新版本
const getNpmInfo async (name: string) {const npmUrl https://registry.npmjs.org/${name}let res {}try {res await axios.get(npmUrl)} catch (e) {console.log(e)}return res
}
const getNpmLatestVersion async (name: string) {const { data } (await getNpmInfo(name)) as AxiosResponseconsole.info(data)
}// 检查版本
const checkVersion async (name: string, version: string) {const lastestVersion await getNpmLatestVersion(name)
}export async function create(projectName?: string) {// 项目名是否为空、是否需要覆盖项目 的逻辑...// 检查版本更新await checkVersion(name, version)// 提供模版选择、克隆仓库 的逻辑...
}npm run build 重新打包并执行 node dist/index.js create可以看到 npm 包的详情其中 dist-tags 字段记录了最新版本。 继续优化使用 lodash 的 gt 比较版本号并给出更新提示我们也可以再定义一个命令 update 用于更新 benchu-cli。
// 用于检查 npm 包的版本是否需要更新 npm 包
import { name, version } from ../../package.json
import { gt } from lodash
import axios, { AxiosResponse } from axios
import chalk from chalk// 获取 npm 包的最新版本
const getNpmInfo async (cliName: string) {const npmUrl https://registry.npmjs.org/${cliName}let res {}try {res await axios.get(npmUrl)} catch (e) {console.log(e)}return res
}
const getNpmLatestVersion async (cliName: string) {const { data } (await getNpmInfo(cliName)) as AxiosResponsereturn data[dist-tags].latest
}// 检查版本
export const checkVersion async () {const lastestVersion await getNpmLatestVersion(name)const needUpdate gt(lastestVersion, version)if (needUpdate) {console.warn(${chalk.greenBright(name)} 版本需要更新当前版本${chalk.blueBright(version)}, 最新版本${chalk.blueBright(lastestVersion)})console.log(可使用${chalk.yellow(npm install benchu-clilatest -g)} 或 ${chalk.yellow(benchu update)}更新)}
}手动修改 package.json 中的版本再次执行 create 测试 9. update命令配置
上一步中通过比较 本地 与 npm 中 benchu-cli 的最新版本给出用户提示让其更新我们也可以新增 update 命令用于更新包。
src/index.ts 新增 update 命令
import { Command } from commander
import { version } from ../package.json
import { create } from ./command/create
import { update } from ./command/updateconst program new Command(benchu)
program.version(version, -v, --version, 版本号)// create 指令
program.command(create).description(初始化新项目).argument([name], 项目名称) // [name]表示可选参数name表示必填参数.action((dirName) {create(dirName) // 不考虑不传参的情况统一交予 create 函数处理})// update 指令
program.command(update).description(更新 benchu-cli 工具).action(async () {await update()})program.parse()command/update.ts通过 process 下载最新的包child_process 是 Node.js 的核心模块之一用于创建和管理子进程以便运行系统命令、脚本或其他可执行文件。
import process from child_process
import chalk from chalk
import ora from ora// 进度条
const spinner ora({text: benchu-cli 正在更新,spinner: {interval: 100, // 100毫秒刷新一次frames: [⠋, ⠙, ⠹, ⠸].map((item) chalk.blue(item)), // 进度条的样式},
})export const update () {spinner.start() // 开始动画process.exec(npm install benchu-clilatest -g, (error, stdout) {if (error) {spinner.fail()console.log(chalk.red(error))} else {spinner.succeed()console.log(chalk.green(更新成功))}})
}npm run build node dist/index.js update 10. 优化终端打印样式
至此功能已经完全实现但是控制台打印样式太丑了 utils/log.ts使用 log-symbols 封装 loglog-symbols官方地址。
下载 // 优化终端打印样式
import logSymbol from log-symbolsconst log {success: (message: string) {console.log(logSymbol.success, message)},error: (message: string) {console.log(logSymbol.error, message)},info: (message: string) {console.log(logSymbol.info, message)},warn: (message: string) {console.log(logSymbol.warning, message)},
}export default log原先的 clone.ts 使用 log 优化 clone.ts 打印
export const clone async (url: string,projectName: string,branchOptions: string[]
) {const git: SimpleGit simpleGit(getOptions)try {await logger(git.clone(url, projectName, branchOptions), 代码下载中..., {estimate: 5000, // 预计下载时间})console.log()console.log(chalk.blackBright())console.log(chalk.blackBright( 欢迎使用 benchu-cli ))console.log(chalk.blackBright())console.log()log.success(项目创建成功 ${chalk.blueBright(projectName)})log.success(执行以下命令启动项目)log.info(cd ${chalk.blueBright(projectName)})log.info(${chalk.yellow(pnpm)} install)log.info(${chalk.yellow(pnpm)} run dev)} catch (e) {log.error(chalk.red(代码下载失败))}
}继续优化使用 figlet 添加打印效果figlet官方文档。如这种效果 安装 figlet 及其类型声明文件
pnpm install figlet types/figlet注意要添加到生产依赖 clone.ts
const goodPrinter async (message: string) {const data await figlet(message)console.log(chalk.rgb(40, 156, 193).visible(data))
}