1. 前景
-
在MiyueFE 月佬的文章中初识川佬 若川的源码共读活动,了解之后加入源码共读交流群,期望与两位大佬共同学习,一起进步。
-
本文参加了由川佬@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
-
这是源码共读的第37期,链接:juejin.cn/post/712908…
create-vite是一个快速生成主流框架基础模板的工具,create-vite通过命令行调用 来实现vite项目创建的。 项目地址: create-vite
2. 概览
-
脚手架可通过命令行指定项目名称及模板完成项目创建,如下:
# npm 6.x npm create vite@latest my-vue-app --template vue # npm 7+, extra double-dash is needed: npm create vite@latest my-vue-app -- --template vue # yarn yarn create vite my-vue-app --template vue # pnpm pnpm create vite my-vue-app --template vue -
项目架构如下:
-
目前项目(
4.2.0-beta.1版本)支持的预设模板包括以下几类:vanilla、vanilla-ts、vue、vue-ts、react、react-ts、react-swc、react-swc-ts、preact、preact-ts、lit、lit-ts、svelte、svelte-ts -
约为500行源码
3. 源码研读
3.1、流程主入口函数init研读
// node:fs,用于处理读文件读写、复制、删除、重命名等操作
import fs from 'node:fs'
// node:path:处理文件与目录路径
import path from 'node:path'
// node:url:处理和解析url的模块url
import { fileURLToPath } from 'node:url'
// minimist:轻量级的命令行参数解析引擎 ^1.2.8
import minimist from 'minimist'
// prompts:实现命令行交互式界面的工具 ^2.4.2
import prompts from 'prompts'
// kolorist:轻量级的色彩命令行文本工具 ^1.7.0
import {
blue,
cyan,
green,
lightGreen,
lightRed,
magenta,
red,
reset,
yellow,
} from 'kolorist'
// 基础知识: slice(start,end):方法可从已有数组中返回选定的元素,返回一个新数组,包含从start到end(不包含该元素)的数组元素。(不会改变原数组)
// 见下文I注解
const argv = minimist<{
t?: string
template?: string
}>(process.argv.slice(2), { string: ['_'] })
// 当前 Nodejs 的工作目录
const cwd = process.cwd()
// 默认文件路径
const defaultTargetDir = 'vite-project'
async function init() {
// 获取arg._第一个参数并经过trim、干掉反斜杠/的操作
const argTargetDir = formatTargetDir(argv._[0])
// 命令行参数 --template 模板 或者 预设模板
const argTemplate = argv.template || argv.t
// 目标文件路径为处理的argTargetDir或默认的路径'vite-project'
let targetDir = argTargetDir || defaultTargetDir
// 获取项目名称:解析到的名称或预设名称
const getProjectName = () =>
targetDir === '.' ? path.basename(path.resolve()) : targetDir
// 通过询问交互获取'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'相关参数信息
let result: prompts.Answers<
'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
>
try {
result = await prompts(
[],
{},
)
} catch (cancelled: any) {
console.log(cancelled.message)
return
}
// 结构赋值 获取相关参数
const { framework, overwrite, packageName, variant } = result
// 是否覆盖
if (overwrite) {
// 递归删除文件夹
emptyDir(root)
} else if (!fs.existsSync(root)) { // 以同步的方法检测目录是否存在,此处取反,则为不存在目录时
// 创建文件夹
fs.mkdirSync(root, { recursive: true })
}
// 根据配置模板文件及路径写入目标路径
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)
}
const pkg = JSON.parse(
fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
)
pkg.name = packageName || getProjectName()
// 将获取的pkg.name同步至package.json文件
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 isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
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
}
console.log()
}
}
-
I.通过
minimist处理过的命令行参数会得到一个对象,非用户指定参数(即没有指定名称)的参数以数组的形式存储在_,如:npm create vite@latest my-vue-app --template vue中,process.argv.slice(2)为['my-vue-app','--template','vue'],故而将其解析为:{ _: ['my-vue-app'], template: 'vue' }
3.2、流程总结
minimist命令行相关参数解析prompts命令行交互询问确认相关参数<framework,overwrite,packageName,variant>overwrite处理- 目标路径确认、及模板
SWC处理- 文件写入
write- 结束打印
log
3.3、扩展函数 (可跳过)
-
文件判空
function isEmpty(path: string) { const files = fs.readdirSync(path) return files.length === 0 || (files.length === 1 && files[0] === '.git') } -
copy函数及copyDir函数// 文件复制 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) } } -
删除文件夹
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 })
}
}
swc处理
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')
},
)
}
function editFile(file: string, callback: (content: string) => string) {
const content = fs.readFileSync(file, 'utf-8')
fs.writeFileSync(file, callback(content), 'utf-8')
}
4. 总结
EOF,愿你千山暮雪海棠依旧