上一篇文章中我们寻找到了create-vite项目中,脚手架要运行的index入口文件,本篇主要讲解在入口文件,脚手架主要做了什么
最终实现效果:通过 pnpm create template-vue3-ts-preset
安装我们自己的项目
源码:这里
声明:本项目的核心源码也是基于开源vite修改而来,本质是想让大家明白创建一个脚手架并发布到npm上走完流程。
前置知识:了解npm、node 以及 《如何创建一个本地的脚手架》、《基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(1):寻找 create-vite入口》
- 首先我们先观察代码最后一段,发现这样一句
此时我们可以看到有一个
init
方法执行了,那么经常编码的同学都知道这大概是一个程序的初始化功能,那么我们去文中寻找init
这个方法
1. 首先第一句话为:
我们可以通过这句话拿到几个关键要素,argv以及formatTargetDir
2. 在全局搜索 这两个方法
首先这句话是采用了
mri
的方法 那么mri
是啥,通过ai搜索我们可知,这个是一个命令参数解析器,我们先不管他的ts类型,先从这个方法的第一个参数看起
process.argv.slice(2
):process
是一个全局的node对象,argv
是node中的一个方法,然后用slice(2)
来截取,通过询问或查询资料可以得到:process.argv
是获取命令行参数数组的第三个值。- 第二个参数
{
alias: { h: 'help', t: 'template' },
boolean: ['help', 'overwrite'],
string: ['template'],
}
这是一个对象 具体作用未知,然后我们查阅node文档可知 mri 方法的作用:就是用于解析命令行参数,并返回一个对象,接受两个参数,第一个参数是node中输入的命令,第二个参数是一些配置选项
所以这个方法其实是拿到我们cmd中输入的一些命令
,例如:
import mri from 'mri'
const argv = mri<{
template?: string
help?: boolean
overwrite?: boolean
}>(process.argv.slice(2), {
alias: { h: 'help', t: 'template' },
boolean: ['help', 'overwrite'],
string: ['template'],
})
const init = async () => {
console.log(argv)
const s=(argv._[0])
console.log(s)
}
init()
运行:node index.js 1234
输出:{ _: [ '1234' ] } 1234
这个方法就比较简单了,接受一个参数 类型为string 去掉参数前后的空格以及末尾多余的“/”
所以:
的作用是:
获取用户输入内容的第三个字符串 首先判断它是否为空 如果为空则为undefined,如果不为空则去掉前后空格以及多余的“/”
接着我们继续往下看:
前三局句代码我们比较容易理解,他就是argv方法的第二个参数的前两个对象当中的,实际上我们获取到的:
argTemplate
就是获取我们输入内容-t后的别名,strin
g类型argOverwrite
则是判断目录是否要求被覆盖boolean
类型help
则是判断用户是否输入了 --help或者--h。boolean
类型
下面的判断则是首先判断你是否输入了 --help或者--h 如果输入了就返回 帮助命令,然后推出
下面是 helpMessage:
这些我们之后再研究,其实就是输出了一些文本命令,来帮助你需要vite做什么
接着我们继续往下看
第一句调用了pkgFromUserAgent 并传入了process.env.npm_config_user_agent
我们一步一步来分析:
- 首先pkgFromUserAgent 方法:
function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined {
if (!userAgent) return undefined
const pkgSpec = userAgent.split(' ')[0]
const pkgSpecArr = pkgSpec.split('/')
return {
name: pkgSpecArr[0],
version: pkgSpecArr[1],
}
}
很显然,这个方法就是将参数userAgent
用空格进行切割,然后取第0位 然后将第0位在用/进行分割 将分割出来的数据第一个返回name的value
第二个为version的value
- 紧接着我们分析
process.env.npm_config_user_agent,
通过经验我们可以分析到,这个似乎是node的某个信息,我们尝试运行node process.env.npm_config_user_agent
发现打印出来为undefined 经过查询可知 这句话实际上是需要通过npm、pnpm 等包管理器运行才能拿到,因为是需要获取包管理器的信息的,(尝试:将这句话写到某个node脚本中,然后通过package
的script 运行)所以我们大致可以得到这样一个结论:这个方法的意思是解析出我们当前使用的具体是哪一个包管理器(npm/pnpm/yarn....)和版本信息包括获取node版本信息等等
例如:
第二句,则是调用了prompts.cancel
的方法传入了“peration cancelled”。
我们接着分析prompts.cancel
:
- 首先我们通过全局搜索
prompts
发现它来源于一个第三方库:
这是一个CLI交互库,其主要作用是在终端中与用户进行交互式对话。比如,我们在创建项目时,需要一步一步输入项目名称,选择模版等等,大概分为一下使用场景:
由此我们可知:
prompts.cancel('Operation cancelled')
实际上就是推出或者取消操作 并输出'Operation cancelled'
接着我们继续往下看
我们会发现其实这里的注释大致已经讲清楚了他的作用,主要是用来获取项目名称和目标目录的。
这里我会逐步进行分析:
- 首先:
let targetDir = argTargetDir
在前面提到argTargetDir
是用来获取我们输入的用空格隔开的第三个内容的,其实就是我们输入命令之后的内容。 - 紧接着这里判断我们的输入是否存在,如果不存在的话就执行下面的内容,如果存在的话,首先是异步执行
promps.text
方法,这个方法在上面的使用场景可知,他是获取文本输入的,其中message
主要是提示用户的文字,defaultValue
是默认选项,如果用户直接回车,则返回这个,placeHolder
则是灰色的提示文字。 - 紧接着会用
prompts.isCancel
来判断用户的输入,因为这里具有默认值,如果rojectName
为空只有一种可能:那就是用户进行了推出或者取消操作,那么此时我们直接推出就好了 - 接着是将我们输入的文字进行格式化,去掉前后空格之类的,这个方法我们前面已经说过了
接着我们继续往下看:
通过注释我们可知,这段代码的主要是在目录不为空的情况下进行的逻辑处理
- 首先这个判断中
fs.existsSync(targetDir)
fs是node当中查询本地文件的一个模块,这里主要是判断是否存在我们输入的文件夹名称,后半部分是判断 这个文件夹是否为空然后取反,结合起来就是:判断是否存在当前文件夹并且不为空。 - 紧接着就是判断目标文件夹不为空是否覆盖,如果覆盖则
overwrite为yes
,否则通过prompts
,给用户提供了三个选择:推出 / 删除目录下的所有文件然后继续(等于清空文件夹)/ 不管里面有没有东西,都在里面创建项目 - 接着判断
overwrite
为取消,如果是的话,就推出 - 这里的
switch
判断主要是对两种选择做出了操作,一个是yes,(清空文件夹)
,一个是no(推出程序)
接着我们继续往下看:
- 接着
packageName
是获取当前文件夹的绝对路径,作用是在创建我们的项目时指定路径,fs在对文件操作时需要绝对路径 - 紧接着使用
isValidPackageName
判断路径是否规范,如果中间有空格的话可能就会不合法 - 在路径不合法情况下为用户弹出一个输入内容,
toValidPackageName(packageName)
方法会给你一个合法的名称,如果你仍然坚持不合法的名称就会触发 输出Invalid package.json name
但是此时程序并没有推出,但是也无法进行下一步,直到你输入一个正确的名称或者推出 - 这里判断你有没有推出,如果为
true
就推出 - 接着将这个绝对路径的名称赋值给
packageName
接着我们继续往下看:
// Choose a framework and variant 内容中会有这一句注释(选择框架或者变体)这里就是需要我们执行create vite 输入名称之后 需要后续执行的操作
- 首先将我们在
create vite --t xxx
之后输入的内容赋值给template
,然后将hasInvalidArgTemplate
先设fasle
- 紧接着判断
argTemplate
是否存在并且是否不在我们的模版名称之内:TEMPLATES
的定义如下:
可以看到的是将
FRAMEWORKS
中提取所有的variant.name
并扁平化一个数组,后续调用的时候用了includes
方法来判断是否匹配
搜索FRAMEWORKS 我们大致可以看到这是一个数组,里面的内容其实就和我们外面的模版基本匹配,然后提取了一些要素,
-
接着我们回到原文,当存在名称并不在模版中时候,将
template
为空并将hasInvalidArgTemplate
修改为true
-
接着判断
template
是否存在,由上面可知,当输入--t后的名称存在并且不在预设模版中时template
就为undefined
,此时就会执行这个if当中的代码,否则就跳过,(验证触发: pnpm create vite --t demo 输入这条命令 node就会执行 "${argTemplate}" isn't a valid template. Please choose from below: 当输入 pnpm create vite --t template-solid-ts 等时候,则会跳过这段代码
) -
我们接着来看 判断中首先会触发一个互动,满足4的条件之后,
option
展示FRAMEWORKS
的所有选项,前文我们已经提到了,这里会展示第一层
所有选项,最后将这个选项返回给framework
接着我们继续往下
- 这里首先会再次判断是否为空,为空直接推出
- 接着再次出发点一个互动,让选择以那种变体开发,也就是我们在创建项目时选择ts js...等一些操作
- 这里的逻辑操作基本和上面一样,所需要注意的只有
getFullCustomCommand
方法
这个方法实际上就是将 预设中的的包管理器(npm、pnpm等) 转换成我们使用的包管理器。
接下来我们一步一步分析这个方法
- 首先这个方法接受两个参数:
customCommand:string
以及pkgInfo?: PkgInfo
类型PkgInfo=interface PkgInfo { name: string version: string }
- 第一句的意思是判断当前是否使用了包管理器 有的话就用当前的,没有的话就用npm
- 第二句的意思是判断当前是不是yarn 如果是的话 版本必须为1.xxx 否则返回false这是一个
boolean
类型 - 接着返回
customCommand
也就是我们输入的命令 首先用正则匹配npm
在满足条件的情况下,判断是否为 bun 、pnpm 满足的情况下返回对应的 包命令, 最后如果有其他情况则保留原有的格式 - 当不满足npm正则时候则匹配
isYarn
变量 如果满足就返回空,如果不满足的话就就去掉@latest
- 最后再用正则匹配下 pm exec 如果满足则将npm exec 替换成运行临时包的命令
好了,让我们在回到之前的代码当中, 然后判断下是否为空,为空推出。 最终将这个值赋值给template
#我们接着来分析
- 首先我们将 向path.jion方法中传入全局变量中的cwd(也就是当前工作目录),在传入 我们
targetDir(我们 在vite 后跟的第一个字符串)
,生成完整的绝对路径返回给root
- 紧接着通过
fs.mkdirSync
来创建新的文件夹 - 接着创建一个变量
isReactSwc
默认为false
- 再然后我们拿到
template
也就是包名,判断一下是否包含-swc这个字段,如果包含就把template
设置为true
,然后把名称中的swc删除掉,这一步其实是在针对react 因为这里react
有swc
模式要比babel-loade
r要快 - 再然后将获得的
pkginfo
做一下判断如果有就用输入的,如果没有默认用npm
- 再然后就用
FRAMEWORKS
循环对比判断其中的variants
对象中的name
是否与tamplate
匹配 如果匹配就返回其中的customCommand
解构给customCommand
我们接着往下
- 当结构完成之后,首先判断结构的值存不存在,如果存在的话,就调用
getFullCustomCommand
方法将名称以及包管理器名称作为参数传入 我们之前已经分析过getFullCustomCommand
最后返回 成我们命令行中对应的包名+模版名称 - 然后通过结构拿到我们对应的包管理器名称,
- 再然后把命令参数中的
TARGET_DIR
替换成我们所需的项目目录名称 - 接着同步执行这个命令,并传入管理器名称,目录名称 等,
- 当代码执行完毕以后,使用
process.exit
进行推出, - 然后打印
Scaffolding project in ${root}...
我们接着往下
- 这里定义了一个
templateDir
这个主要是用来获取模版的绝对路径的因为template
之前源于variant
variant
本身并没有完整的模版名,所以这里需要拼接写 模版名,最终得到一个完整的外部的模版名称, - 再然后就比较简单了,创建
write
利用fs
模块 来创建文件了,首先先判断是文件还是文件夹,有如果是文件夹就创建文件夹没否则就拷贝文件(单个文件
) - 再然后就获取目前模板的绝对路径,赋值给
files
- 然后就是设置package.json的一些关键信息
- 再然后我们需要写入自己的packge.json文件,因为package.json文件本身包含一些文件信息,所以不能直接拷贝
- 接下来的
if (isReactSwc)
则是针对react-swc
的操作,如果选择了这个那么就得是ts
然后接下来的就主要是输入到log中的内容了。
- 首先设置一个doneMessage为空字符串
- 然后获取我们创建的项目和当前目录之间的差异路径
- 将差异路径赋值给donemessage
- 然后判断创建目录的路径是否等于当前目录的路径,如果相等的话 ,就将其中的正则匹配通过三元表达式赋值给doneMessage 5.因为之前我们已经哪都pkgManager 他就包管理器名称,这里主要是针对yarn进行区分,然后拼接对应的字符串,最后通过 prompts.outro(doneMessage) 输出到页面
到此整个npm crete vite的功能基本已经实现了:接下来我们总结下这个主要干了些什么事情:
总结:
1. 首先是获取项目名称以及目标目录,
2. 对目标目录进行处理
3. 再然后就是获取我们想创建什么样的项目
4. 执行创建命令创建对应的项目。