本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第37期,链接:若川视野 x 源码共读 第37期 | vite 3.0 都发布了,这次来手撕 create-vite 源码。
本篇文章将主要介绍针对create-vite的源码解析:
一、create-vite的基本使用
npm create vite@latest
当执行npm create/yarn create/pnpm create时,会先去查找一个npm包,包名为create/xxx;也就是说,当你执行npm create vite@latest时,实际上是在执行npx create-vite@latest;
二、源码解析
1、目录结构
由template-*开头的目录代表的是create-vite支持的模版。create-vite的主文件是index.js。
2、index.js文件内容
- 其中
init函数为整个create-vite的主要处理函数、FRAMEWORKS为一个配置对象,主要记录create-vite支持哪些模版,已经对应模版支持的js/ts等:
TEMPLATES变量,是一个数组,主要是从FRAMEWORKS变量中取出当前所支持的框架信息:
init函数:
该函数的主要作用:
- 提供交互信息
- 根据用户选择创建文件
1、提示交互信息
create-vite创建一个项目需要获取几个必要信息
- 使用什么模版
- 使用js/ts
- 项目名称
- 是否复写选中的文件夹
init函数中用于处理交互的逻辑为:
// 使用prompts包实现交互,第一个参数为对象数组
result = await prompts(
[
// 每一个对象都代表了与用户交互的一个问题
{
// 根据targetDir是否存在,决定当前交互信息是否展示;同时type代表的了问题的类 // 型,详情可以直接查看prompts的官方文档
type: targetDir ? null : 'text',
// name属性值将会作为存储用户对本问题回答的内容的变量
name: 'projectName',
// message代表用户看到的问题
message: reset('Project name:'),
initial: defaultTargetDir,
// 一个事件,即用户做出回应后触发的事件
onState: (state) => {
targetDir = formatTargetDir(state.value) || defaultTargetDir
}
},
// 为了节省篇幅,这里省略了很多的条件
],
{
onCancel: () => {
throw new Error(red('✖') + ' Operation cancelled')
}
}
)
上面的代码主要就做了以下几件事:
- 要求用户给出一个项目名称(存储项目的文件夹名称)
- 当给出的文件夹已存在时,提示是否覆写文件夹内容
- 如果用户选择了不覆写,给出本次创建行为取消的提示;整体流程终止(通过抛出一个错误,中断程序)
- 要求用户给出packageName,用于给package.json文件的name属性赋值
- 要求用户确定项目需要使用的框架
- 要求用户确定项目需要使用的语言: js/ts
上述的流程中还有一些细节点没有一一列出,比如设置packageName时会有针对给出的值做有效校验的isValidPackageName函数:
function isValidPackageName(projectName) {
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
projectName
)
}
2、根据用户选择创建文件
- 根据用户选择的框架类型,以及需要使用的语言是js/ts确定将会使用的模版文件:
// 确定需要使用的模版
// variant代表本次使用的语言: vanilla-ts/vanilla、vue-ts/vue....
// framework代表本次使用的框架: vanilla、vue、react...
// template代表执行create-vite时传入的template参数
template = variant || framework || template
// 确定模版的本地路径
// fileURLToPath,nodejs原生模块url中的函数,用于将文件URL解码为路径字符串,
// 并确保在将给定的文件URL转换为路径时正确地附加/调整了URL控制字符(/,%)。
// import.meta是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象.
// 这里的import.meta.url代表了index.js文件的本地URL
// "file:///Users/xxx/vite/packages/create-vite/index.js",
const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'..',
`template-${template}`
)
- 根据模版文件,创建项目
// write函数:简单的文件拷贝/文件内容写入
const write = (file, content) => {
// renameFiles对象主要包含了针对与.gitignore文件的处理,即模版文件中的git忽略文件名 // 为_gitignore,为了防止模版文件夹中的git忽略文件影响到整个项目
const targetPath = renameFiles[file]
? path.join(root, renameFiles[file])
: path.join(root, file)
if (content) {
fs.writeFileSync(targetPath, content)
} else {
// 根据原文件路径,目标路径完成简单的文件拷贝
copy(path.join(templateDir, file), targetPath)
}
}
// 简单的文件拷贝过程
function copy(src, dest) {
const stat = fs.statSync(src)
if (stat.isDirectory()) {
copyDir(src, dest)
} else {
fs.copyFileSync(src, dest)
}
}
// 具体的文件生成过程
// 读取模版文件夹下的所有文件,调用write函数,完成项目的生成
const files = fs.readdirSync(templateDir)
for (const file of files.filter((f) => f !== 'package.json')) {
write(file)
}
// 更改package.json文件的name属性值
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))
- 项目创建完成,给出后续提示
// process.env.npm_config_user_agent: 用户设置的npm包管理器:yarn/npm/pnpm
const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
switch (pkgManager) {
case 'yarn':
console.log(' yarn')
console.log(' yarn dev')
break
default:
console.log(` ${pkgManager} install`)
console.log(` ${pkgManager} run dev`)
break
}
总结
- import.meta:是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象,其中包含了当前文件的本地URL
- 可以使用URL.fileURLToPath处理本地URL,用于解决多平台下的路径问题