本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第37期,链接:vite 3.0 都发布了,这次来手撕 create-vite 源码。
create-vite 是一个快速构建vite工程的脚手架
npm create
官网说明使用npm命令npm create vite创建工程npm create是npm init的alias,详情可查阅npm文档
源码概览
接着,我们克隆vite仓库,分析其源码
git clone https://github.com/vitejs/vite.git
cd vite/packages/create-vite
打开README.md,有一段兼容性说明表示我们需要使用至少18+的NodeJS版本。配置完环境后,执行install下载依赖。从工程结构中可以很清晰的看到,create-vite预设了多种框架及变体的工程模板。当前支持的有:
vanillavanilla-tsvuevue-tsreactreact-tsreact-swcreact-swc-tspreactpreact-tslitlit-tssveltesvelte-tssolidsolid-tsqwikqwik-ts
再看package.json,可以了解到以下信息:
- 项目模块化规范使用的是ES Module;
- 可执行命令为
create-vite或cva; - 项目使用
unbuild编译打包,这是一个基于rollup的打包器
以及项目的一些依赖:
"devDependencies": {
"@types/minimist": "^1.2.4",
"@types/prompts": "^2.4.7",
"cross-spawn": "^7.0.3",
"kolorist": "^1.8.0",
"minimist": "^1.2.8",
"prompts": "^2.4.2",
"unbuild": "^2.0.0"
}
流程拆分
接下来,我们打开项目源码的入口文件,进行流程拆分
// node依赖
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// ???
import spawn from 'cross-spawn'
// 解析命令行的参数
import minimist from 'minimist'
// 命令行交互
import prompts from 'prompts'
// 终端颜色输出
import {
blue,
cyan,
green,
lightBlue,
lightGreen,
lightRed,
magenta,
red,
reset,
yellow,
} from 'kolorist'
// 当前路径
const cwd = process.cwd()
// 默认工程名
const defaultTargetDir = 'vite-project'
// 声明主函数
async function init() {}
// 执行主函数
init().catch((e) => {
console.error(e)
})
获取工程目录
// 工程名参数
const argTargetDir = formatTargetDir(argv._[0])
// 模板参数
const argTemplate = argv.template || argv.t
let targetDir = argTargetDir || defaultTargetDir
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
function formatTargetDir(targetDir: string | undefined) {
return targetDir?.trim().replace(/\/+$/g, '')
}
命令交互获取参数
try {
result = await prompts(
[
{
type: argTargetDir ? null : 'text',
name: 'projectName',
message: reset('Project name:'),
initial: defaultTargetDir,
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
},
}
...
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
},
},
)
} catch (cancelled: any) {
console.log(cancelled.message)
return
}
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result
初始化目录(重写已有目录||创建不存在的目录)
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 templateDir = path.resolve(
fileURLToPath(import.meta.url),
'../..',
`template-${template}`,
)
_中间有一段处理自定义命令的逻辑(不详细剖析)_获取到预设的框架模板,将其写入先前创建好的工程目录中
const write = (file: string, content?: string) => {
const targetPath = path.join(root, renameFiles[file] ?? file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
copy(path.join(templateDir, file), targetPath)
}
}
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
package.json单独处理(修改工程名)
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
)
pkg.name = packageName || getProjectName()
write('package.json', JSON.stringify(pkg, null, 2) + '\n')
打印完成信息
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
const cdProjectName = path.relative(cwd, root)
console.log(`\nDone. Now run:\n`)
if (root !== cwd) {
console.log(
` cd ${
cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
}`,
)
}
switch (pkgManager) {
case 'yarn':
console.log(' yarn')
console.log(' yarn dev')
break
default:
console.log(` ${pkgManager} install`)
console.log(` ${pkgManager} run dev`)
break
}
总结
曾经小试牛刀尝试过开发一个简易的前端脚手架。参考:【脚手架之旅】前端脚手架入门。与此相比,整体流程大致相同,通过命令行交互获取相应参数,再根据用户需求拉取项目模板并完成初始化。
通过深入阅读 create-vite 源码,对前端工程化有了更深入的理解,也进一步计划在公司团队中打造统一规范标准的前端工程。