有时候想写一下对需求的分析以及最后落地的过程, 这个处理需求的思路也是可以和大家交流一下的
因为自己有想做一些小demo项目, 在技术选型上的调整会导致每次都要初始化一个脚手架, 之前写过一个脚手架, 但是那个是基于vite、react、antd的场景, 很多其他场景可能技术栈并不一致
因此想做一个基于vite的上层 轻量
应用脚手架, 希望支持如下需求:
适用于react、vue、next项目
适用于monorepo、单应用项目
开发配置默认包含 eslint、prettier、lint-staged、husky 等常用配置
应用配置默认包含router、store、ts、组件库等
支持命令行初始化项目
易扩展, 后续对接新技术栈
配置如下:
技术栈 | 内置模块 | 可选模块 |
---|---|---|
monorepo、react | react-router、mobx、ts | antd、tailwindcss |
react | react-router、mobx、ts | antd、tailwindcss |
monorepo、vue3 | vue-router、pinia、ts | antd、element、tailwindcss |
vue3 | vue-router、pinia、ts | antd、element、tailwindcss |
next | ts | antd、tailwindcss |
其实这个场景和 create-vite
一模一样, 因此也借这个机会学习一下 vite 怎么做的
vite仓库是 monorepo
格式, 主要结构如下:
├── playground| ├── xxx # 测试vite的工程├── packages| ├── create-vite # 初始化vite脚手架工程| ├── vite # vite主要工程
具体看 create-vite
结构如下:
├── src| ├── index.ts # 入口文件├── templates # 提供的vite模板集合| ├── template-react├── package.json # 配置文件
使用到的依赖大致如下:
工具 | 作用 |
---|---|
unbuild | 打包工具 |
cross-spawn | 跨平台执行node命令 |
kolorist | 命令行颜色输出 |
minimist | 解析命令行 |
prompts | 命令行交互 |
下方参照vite脚手架的代码结构
首先创建这个脚手架仓库, 先命名为 hush-cli
, 因为我这里不涉及多个仓库, 所以把 create-hush-cli
放在根目录, 目录结构如下
├── playground| ├── hush-cli # 测试脚手架项目├── create-hush-cli # 初始化脚手架工程
create-hush-cli 目录结构如下
├── index.js├── templates├── src| ├── index.ts
根目录安装 ts, create-hush-cli 目录中安装上述vite中的依赖
pnpm add typescript -D -wpnpm add unbuild kolorist minimist prompts @types/minimist @types/prompts -F create-hush-cli
在 create-hush-cli/package.json
写下如下命令:
{ "bin": { "create-hush-cli": "index.js", "chc": "index.js" }, "files": ["index.js", "templates", "dist"], "scripts": { "dev": "unbuild --stub", "build": "unbuild", "typecheck": "tsc --noEmit" }}
unbuild 的 stub
配置会开启 stub 模式, 构建产物里将会使用 jiti 来做链接, 这样构建一次其他引用的地方会引用到最新代码, 相当于 watch
模式, 所以这里没有用 tsc
去运行 src/index.ts, 这里 dev
命令直接打包, 在 playground
中测试
在 create-hush-cli/src/index.ts
中写入如下内容
const getVersion = () => { console.log("version", "1.0.0");};getVersion();
执行 pnpm dev
命令, 会发现 create-hush-cli 根目录产生了一个dist文件夹, 这就是stub模式下打包的产物
在 playground
目录中创建一个 hush-cli 文件夹, 用来测试初始化仓库, 执行如下命令:
pnpm add create-hush-cli# 因为上述 bin 配置, 所以可以执行 chcpnpm exec chc
会发现控制台打印出来了 "version", "1.0.0"
这里穿插介绍一下 exec
, vite 初始化仓库命令为 npm create vite
npm create xxx# 等同如下npm exec create-xxx
当我们执行 create 命令时,会先补全包名为 create-xxx, 然后转换为 exec 命令执行,即 npm exec create-xxx; 接着执行包对应的 create-xxx 命令(package.json 的 bin 字段指定的命令)
所以我们需要发布一个 create-hush-cli
的npm包, 然后package.json中bin里边要提供 create-hush-cli
方法. 发布后即可通过如下方式安装
pnpm create hush-cli
了解 pnpm exec
修改一下 getVersion
方法返回值, 不用执行dev或者build命令, 直接继续执行 pnpm exec chva
会发现打印的是修改后的内容, 证明热更新成功
以上我的脚手架仓库创建好了, 后续开发流程为在 create-hush-cli
中添加脚手架模板, 执行 pnpm dev
, 在 playground
中测试
主要功能写在 /src/index.ts
中, 大致流程应该是:
询问一些基本信息, 项目名、框架、依赖等
根据用户选择的框架, 复制自己对应的项目模板过去
修改项目模板中的依赖、配置等
因此开始我们的 index.ts
的编写
首先看下vite脚手架初始化时是什么流程:
在控制台输入 pnpm create vite
, 展示如下一些信息
提示让我们填写项目名, 且直接回车会填写默认值 vite-project
, 继续往下, 发现一共是询问如下几个信息
这里使用 prompts
插件完成交互, 用 kolorist
做样式输出
因此我们的 index.ts
代码如下(做下简化, 以防代码量太大):
import prompts from "prompts";import { blue, cyan, green, magenta, red, reset, yellow } from "kolorist";const init = () => { let result: prompts.Answers< | "projectName" | "overwrite" | "packageName" | "framework" | "cliType" | "variant" >; try { result = await prompts( [ { type: "text", name: "projectName", message: reset("Project name:"), initial: "hush-project", }, { type: "select", name: "framework", message: reset("Select a framework:"), initial: 0, choices: [ { title: 'React', value: 'react' }, { title: 'Vue', value: 'vue' } ] }, ], { onCancel: () => { throw new Error(red("✖") + " Operation cancelled"); }, }, ) } catch (cancelled: any) { console.log(cancelled.message); return; }}init();
实际代码中这里会更加完善, 这里不做完整代码展示
如果 projectName
文件夹名称已存在则提示文件夹已存在, 是删除原文件夹继续还是退出还是将之前Ignore掉再继续
若 projectName
不符合 package.json
的name规范, 则需要输入package name
选择对应的框架时后续展示的分支会有所不同, react中才有swc选项
因此上述第一步 prompts
流程完毕后, 我们获取到 result
信息, 接下来即可执行第二步初始化项目仓库了
我们的模板都放在 templates
文件夹下, 该文件夹结构如下
├── templates # 提供的vite模板集合| ├── template-react| ├── template-react-mo| ├── template-vue| ├── template-vue-mo| ├── template-next
因此我们要根据用户选择的框架, 获取对应的模板文件夹, 然后复制到 projectName
文件夹中, 实现如下(简化版):
import path from "node:path";import { fileURLToPath } from "node:url";const cwd = process.cwd();const root = path.join(cwd, result.projectName);// 创建文件夹fs.mkdirSync(root, { recursive: true });// 获取目标路径const targetPath = path.join(root);// 获取模板文件夹(需要确保templates下已经存在该模板)const templateDir = path.resolve( fileURLToPath(import.meta.url), "../../templates/", `template-${framework.name}`,);// 复制模板到目标路径copy(templateDir, targetPath);
其中 copy 辅助方法如下:
function copyDir(srcDir: string, destDir: string) { fs.mkdirSync(destDir, { recursive: true }); for (const file of fs.readdirSync(srcDir)) { const srcFile = path.resolve(srcDir, file); const destFile = path.resolve(destDir, file); copy(srcFile, destFile); }}function copy(src: string, dest: string) { const stat = fs.statSync(src); if (stat.isDirectory()) { copyDir(src, dest); } else { fs.copyFileSync(src, dest); }}
测试一下, 建立一个 templates/template-react
, 里边写入一个 index.ts
测试文件
再去 playground/hush-cli
下执行 pnpm exec chc
, 该仓库上述已经建立过, 已经安装了 create-hush-cli
, 所以可以直接执行 pnpm exec chc
效果如下:
输入项目名
选择技术栈
选择仓库类型
选择依赖
完成后会在 playground/hush-cli
下生成一个 hush-project
文件夹, 里边有 index.ts
文件
至此通过命令行初始化项目可以了, 接下来做第三步修改项目模板中的一些配置等
修改 package.json
中的项目名:
const targetPath = path.join(root);const pkgPath = path.join(targetPath, `package.json`);const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));pkg.name = result.packageName || result.projectName;fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
接下来我们需要完善 templates
下的脚手架仓库
这里拿react脚手架做示例, 我们需要预安装如下插件:
插件名称 | 作用 |
---|---|
initialize-css | 样式初始化 |
mobx | 状态管理 |
tailwindcss | 样式框架 |
eslint | 代码校验工具 |
prettier | 代码格式化工具 |
lint-staged | 暂存区代码处理工具 |
husky | git hook 工具 |
commitlint | git提交命令校验工具 |
@hushaha/request | 请求工具 |
因此我们需要依次安装此些插件
代码目录结构如下:
|-- src |-- assets # 样式以及图片 |-- components # 基础组件 |-- config # 公共配置 |-- features # 业务组件 |-- pages # 路由对应页面 |-- routers # 路由配置 |-- stores # 全局状态 |-- types # types |-- utils # 工具库 |-- App.tsx # 主组件 |-- main.tsx # 入口文件
脚手架创建的具体流程可以查看这篇文章: vite-react-ts-cli
@hushaha/request
库的使用方法可以查看这篇文章:
q-request