create-vite是vite官方推出的脚手架工具,通过几行简单的命令可以交互式完成vite项目的创建,通过内置丰富的项目模板即可生成多种类型的项目,下面来看一下vite如何创建项目
如何调试
vite和create-vite维护在同一个repo中,进入到子目录下调试即可
- clone项目并安装依赖(使用pnpm workspace管理依赖)
- 进入packages/create-vite 执行build
- 运行构建产物dist/index.mjs
按照上述步骤即可打开create-vite的交互式界面
目录结构
create-vite目录结构比较简单,代码量也很少
- __tests__ 单元测试文件
- src下是源码,只有一个文件
- template-* 项目模板,vite提前创建好了template
- build.config.ts 构建工具配置(create-vite用的是unbuild)
关于unbuild
unbuild是一款很简洁的build工具,不像webpack那样“沉重”,create-vite基于unbuild+rollup实现打包功能
贴一下unbuild的配置(很容易看懂)
export default defineBuildConfig({
entries: ['src/index'],
clean: true,
rollup: {
inlineDependencies: true,
esbuild: {
minify: true,
},
},
alias: {
prompts: 'prompts/lib/index.js',
},
hooks: {
'rollup:options'(ctx, options) {
options.plugins = [
options.plugins,
licensePlugin(
path.resolve(__dirname, './LICENSE'),
'create-vite license',
'create-vite',
),
]
},
},
})
执行流程分析
看完了目录结构及构建工具等内容,下面来看一下源码吧(src/index.ts)
生成交互式ui
大家在使用各种cli工具时肯定见过 “选择” “输入”这样的功能,那如何简单实现这种效果呢
vite使用的是prompts(之前我碰到类似的需求也用这个),通过简单配置即可生成交互ui,下面是vite中的调用代码
try {
result = await prompts(
[
{
type: argTargetDir ? null : 'text',
name: 'projectName',
message: reset('Project name:'),
initial: defaultTargetDir,
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
},
}
// some code
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
},
},
)
} catch (cancelled: any) {
// some code
}
没有很复杂的功能,prompts主要用来获取用户输入,create-vite根据用户输入或命令行参生成配置
生成创建项目的config
vite在获取prompts及命令行入参后合成一份构建配置,包含创建一个vite项目需要的全部信息
const argTargetDir = formatTargetDir(argv._[0])
const argTemplate = argv.template || argv.t
// 获取dir相关信息
let targetDir = argTargetDir || defaultTargetDir
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
// 项目名称,如果没有指定会使用默认名称
const { framework, overwrite, packageName, variant } = result
// result 是prompts返回的结果,包含框架等信息
const root = path.join(cwd, targetDir)
if (overwrite) {
emptyDir(root)
} else if (!fs.existsSync(root)) {
fs.mkdirSync(root, { recursive: true })
}
// 目录非空的晴空下可以选择清空目录
let template: string = variant || framework?.name || argTemplate
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
// 获取package manager
这里的代码基本就是按照流程写的(个人感觉清晰但不优雅,不喜勿喷)
对于react+swc的情况有额外的判断,我猜测因为只有react有swc的选项,所以这里处理的比较简单
let template: string = variant || framework?.name || argTemplate
let isReactSwc = false
if (template.includes('-swc')) {
isReactSwc = true
template = template.replace('-swc', '')
}
// some code
if (isReactSwc) {
setupReactSwc(root, template.endsWith('-ts'))
}
// 替换配置文件
function setupReactSwc(root: string, isTs: boolean) {
editFile(path.resolve(root, 'package.json'), (content) => {
return content.replace(
/"@vitejs\/plugin-react": ".+?"/,
`"@vitejs/plugin-react-swc": "^3.0.0"`,
)
})
editFile(
path.resolve(root, `vite.config.${isTs ? 'ts' : 'js'}`),
(content) => {
return content.replace('@vitejs/plugin-react', '@vitejs/plugin-react-swc')
},
)
}
customCommand
如果官方配置不能满足需求,create-vite还支持部分非官方配置,在选择variant时选择Other即可进入这一部分逻辑
//customCommand option
{
name: 'create-vite-extra',
display: 'create-vite-extra ↗',
color: reset,
customCommand: 'npm create vite-extra@latest TARGET_DIR',
},
选择后vite通过npm create调用create-vite-extra完成创建过程。
我看到这个npm create还是有点懵逼的,查了一下了解到npm create就是npm init的别名。
而且你会发现包名是create-vite-extra,这里调用的居然是vite-extra,因为在执行 npm create [name]时npm会自动在前面加上create找到create-vite-extra(可以用执行npm create react-app试一下,可以自动找到CRA)
写入template
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'../..',
`template-${template}`
)
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
create-vite解析出template后将内部的template复制到目标目录,完成创建
总结一下create-vite的执行流程
工具函数
create-vite内有很多工具函数值得一看,下面选几个看一下
isEmpty && emptyDir
function isEmpty(path: string) {
const files = fs.readdirSync(path)
return files.length === 0 || (files.length === 1 && files[0] === '.git')
}
imEmpty通过readdir判断一个目录是否为空,这里对.git文件做了特殊处理(如果有大佬知道为什么可以在评论区留言,非常感谢)
function emptyDir(dir: string) {
if (!fs.existsSync(dir)) {
return
}
for (const file of fs.readdirSync(dir)) {
if (file === '.git') {
continue
}
fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
}
}
emptyDir通过readdir获取所有目录,然后通过fs.rm删除,猜测没有直接用rm -rf是想对.git进行判断
copy && copyDir
copy和copyDir主要用来实现复制template功能
function copy(src: string, dest: string) {
const stat = fs.statSync(src)
if (stat.isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
}
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)
}
}
copy会通过fs.stat对文件类型进行判断,如果是file则通过copyFile直接完成复制,如果是dir执行copyDir
copyDir会获取该目录下的所有file和dir,通过调用copy完成复制(copy内部再进行file的dir的判断,重复刚刚的流程)
pkgFromUserAgent
creact-vite通过process.env获取用户的package manager(我从来没获取过,学到了)
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
function pkgFromUserAgent(userAgent: string | undefined) {
if (!userAgent) return undefined
const pkgSpec = userAgent.split(' ')[0]
const pkgSpecArr = pkgSpec.split('/')
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1],
}
}
获取template dir
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'../..',
`template-${template}`,
)
获取template dir的方式很有意思,指利用的是import.meta.url,该属性会返回一个file链接(file://XXXXXX/XXXXXX)通过fileURLToPath即可获取到path
总结
create-vite的代码很是很容易读懂的,推荐愿意读源码的同学尝试一下,个人认为create-vite代码比较好的几个点。
- 代码简洁易读
- 支持customCommand,通过npm create引入第三方template,增加了灵活性
- 工具函数很简洁,值得一看
下一篇可能会写only-allow或者esbuild相关的内容,如果有大佬对源码阅读感兴趣可以在评论区讨论。