基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(2):分析入口文件

0 阅读10分钟

上一篇文章中我们寻找到了create-vite项目中,脚手架要运行的index入口文件,本篇主要讲解在入口文件,脚手架主要做了什么

最终实现效果:通过 pnpm create template-vue3-ts-preset 安装我们自己的项目

源码:这里

声明:本项目的核心源码也是基于开源vite修改而来,本质是想让大家明白创建一个脚手架并发布到npm上走完流程。

前置知识:了解npm、node 以及 《如何创建一个本地的脚手架》《基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(1):寻找 create-vite入口》

  1. 首先我们先观察代码最后一段,发现这样一句

image.png 此时我们可以看到有一个init方法执行了,那么经常编码的同学都知道这大概是一个程序的初始化功能,那么我们去文中寻找init这个方法

1. 首先第一句话为:

image.png 我们可以通过这句话拿到几个关键要素,argv以及formatTargetDir
2. 在全局搜索 这两个方法

image.png 首先这句话是采用了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


image.png 这个方法就比较简单了,接受一个参数 类型为string 去掉参数前后的空格以及末尾多余的“/”

所以:image.png 的作用是:获取用户输入内容的第三个字符串 首先判断它是否为空 如果为空则为undefined,如果不为空则去掉前后空格以及多余的“/”

接着我们继续往下看:

image.png 前三局句代码我们比较容易理解,他就是argv方法的第二个参数的前两个对象当中的,实际上我们获取到的:

  1. argTemplate 就是获取我们输入内容-t后的别名, string类型
  2. argOverwrite 则是判断目录是否要求被覆盖 boolean类型
  3. help 则是判断用户是否输入了 --help或者--h。boolean类型

下面的判断则是首先判断你是否输入了 --help或者--h 如果输入了就返回 帮助命令,然后推出 下面是 helpMessage: image.png 这些我们之后再研究,其实就是输出了一些文本命令,来帮助你需要vite做什么

接着我们继续往下看

image.png

第一句调用了pkgFromUserAgent 并传入了process.env.npm_config_user_agent

我们一步一步来分析:

  1. 首先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

  1. 紧接着我们分析 process.env.npm_config_user_agent,通过经验我们可以分析到,这个似乎是node的某个信息,我们尝试运行 node process.env.npm_config_user_agent 发现打印出来为undefined 经过查询可知 这句话实际上是需要通过npm、pnpm 等包管理器运行才能拿到,因为是需要获取包管理器的信息的,(尝试:将这句话写到某个node脚本中,然后通过package的script 运行)所以我们大致可以得到这样一个结论:这个方法的意思是解析出我们当前使用的具体是哪一个包管理器(npm/pnpm/yarn....)和版本信息包括获取node版本信息等等 例如:

image.png

image.png

image.png

第二句,则是调用了prompts.cancel 的方法传入了“peration cancelled”。

我们接着分析prompts.cancel

  1. 首先我们通过全局搜索prompts 发现它来源于一个第三方库:

image.png 这是一个CLI交互库,其主要作用是在终端中与用户进行交互式对话。比如,我们在创建项目时,需要一步一步输入项目名称,选择模版等等,大概分为一下使用场景:

image.png 由此我们可知:prompts.cancel('Operation cancelled')实际上就是推出或者取消操作 并输出'Operation cancelled'

接着我们继续往下看

image.png 我们会发现其实这里的注释大致已经讲清楚了他的作用,主要是用来获取项目名称和目标目录的。 这里我会逐步进行分析:

  1. 首先:let targetDir = argTargetDir 在前面提到 argTargetDir是用来获取我们输入的用空格隔开的第三个内容的,其实就是我们输入命令之后的内容。
  2. 紧接着这里判断我们的输入是否存在,如果不存在的话就执行下面的内容,如果存在的话,首先是异步执行promps.text 方法,这个方法在上面的使用场景可知,他是获取文本输入的,其中message主要是提示用户的文字,defaultValue是默认选项,如果用户直接回车,则返回这个,placeHolder则是灰色的提示文字。
  3. 紧接着会用 prompts.isCancel来判断用户的输入,因为这里具有默认值,如果rojectName 为空只有一种可能:那就是用户进行了推出或者取消操作,那么此时我们直接推出就好了
  4. 接着是将我们输入的文字进行格式化,去掉前后空格之类的,这个方法我们前面已经说过了

接着我们继续往下看:

image.png 通过注释我们可知,这段代码的主要是在目录不为空的情况下进行的逻辑处理

  1. 首先这个判断中fs.existsSync(targetDir) fs是node当中查询本地文件的一个模块,这里主要是判断是否存在我们输入的文件夹名称,后半部分是判断 这个文件夹是否为空然后取反,结合起来就是:判断是否存在当前文件夹并且不为空。
  2. 紧接着就是判断目标文件夹不为空是否覆盖,如果覆盖则overwrite为yes,否则通过prompts,给用户提供了三个选择:推出 / 删除目录下的所有文件然后继续(等于清空文件夹)/ 不管里面有没有东西,都在里面创建项目
  3. 接着判断overwrite 为取消,如果是的话,就推出
  4. 这里的switch判断主要是对两种选择做出了操作,一个是yes,(清空文件夹),一个是no(推出程序)

接着我们继续往下看:

image.png

  1. 接着packageName是获取当前文件夹的绝对路径,作用是在创建我们的项目时指定路径,fs在对文件操作时需要绝对路径
  2. 紧接着使用isValidPackageName判断路径是否规范,如果中间有空格的话可能就会不合法
  3. 在路径不合法情况下为用户弹出一个输入内容,toValidPackageName(packageName)方法会给你一个合法的名称,如果你仍然坚持不合法的名称就会触发 输出 Invalid package.json name 但是此时程序并没有推出,但是也无法进行下一步,直到你输入一个正确的名称或者推出
  4. 这里判断你有没有推出,如果为true就推出
  5. 接着将这个绝对路径的名称赋值给packageName

接着我们继续往下看:

// Choose a framework and variant 内容中会有这一句注释(选择框架或者变体)这里就是需要我们执行create vite 输入名称之后 需要后续执行的操作

image.png

  1. 首先将我们在 create vite --t xxx 之后输入的内容赋值给 template ,然后将hasInvalidArgTemplate先设fasle
  2. 紧接着判断argTemplate 是否存在并且是否不在我们的模版名称之内:TEMPLATES的定义如下:

image.png 可以看到的是将 FRAMEWORKS中提取所有的variant.name 并扁平化一个数组,后续调用的时候用了includes方法来判断是否匹配

搜索FRAMEWORKS 我们大致可以看到这是一个数组,里面的内容其实就和我们外面的模版基本匹配,然后提取了一些要素, image.png

  1. 接着我们回到原文,当存在名称并不在模版中时候,将 template为空并将hasInvalidArgTemplate 修改为true

  2. 接着判断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 等时候,则会跳过这段代码

  3. 我们接着来看 判断中首先会触发一个互动,满足4的条件之后,option 展示FRAMEWORKS的所有选项,前文我们已经提到了,这里会展示第一层所有选项,最后将这个选项返回给framework

接着我们继续往下

image.png

  1. 这里首先会再次判断是否为空,为空直接推出
  2. 接着再次出发点一个互动,让选择以那种变体开发,也就是我们在创建项目时选择ts js...等一些操作
  3. 这里的逻辑操作基本和上面一样,所需要注意的只有getFullCustomCommand方法

image.png 这个方法实际上就是将 预设中的的包管理器(npm、pnpm等) 转换成我们使用的包管理器。 接下来我们一步一步分析这个方法

  1. 首先这个方法接受两个参数:customCommand:string以及pkgInfo?: PkgInfo 类型PkgInfo=interface PkgInfo { name: string version: string }
  2. 第一句的意思是判断当前是否使用了包管理器 有的话就用当前的,没有的话就用npm
  3. 第二句的意思是判断当前是不是yarn 如果是的话 版本必须为1.xxx 否则返回false这是一个boolean类型
  4. 接着返回customCommand 也就是我们输入的命令 首先用正则匹配npm 在满足条件的情况下,判断是否为 bun 、pnpm 满足的情况下返回对应的 包命令, 最后如果有其他情况则保留原有的格式
  5. 当不满足npm正则时候则匹配 isYarn 变量 如果满足就返回空,如果不满足的话就就去掉@latest
  6. 最后再用正则匹配下 pm exec 如果满足则将npm exec 替换成运行临时包的命令

好了,让我们在回到之前的代码当中, 然后判断下是否为空,为空推出。 最终将这个值赋值给template #我们接着来分析

image.png

  1. 首先我们将 向path.jion方法中传入全局变量中的cwd(也就是当前工作目录),在传入 我们targetDir(我们 在vite 后跟的第一个字符串),生成完整的绝对路径返回给root
  2. 紧接着通过 fs.mkdirSync 来创建新的文件夹
  3. 接着创建一个变量isReactSwc 默认为false
  4. 再然后我们拿到template 也就是包名,判断一下是否包含-swc这个字段,如果包含就把template设置为true,然后把名称中的swc删除掉,这一步其实是在针对react 因为这里reactswc模式要比babel-loader要快
  5. 再然后将获得的pkginfo做一下判断如果有就用输入的,如果没有默认用npm
  6. 再然后就用FRAMEWORKS循环对比判断其中的variants对象中的name是否与tamplate匹配 如果匹配就返回其中的customCommand解构给customCommand

我们接着往下

image.png

  1. 当结构完成之后,首先判断结构的值存不存在,如果存在的话,就调用getFullCustomCommand方法将名称以及包管理器名称作为参数传入 我们之前已经分析过getFullCustomCommand最后返回 成我们命令行中对应的包名+模版名称
  2. 然后通过结构拿到我们对应的包管理器名称,
  3. 再然后把命令参数中的TARGET_DIR 替换成我们所需的项目目录名称
  4. 接着同步执行这个命令,并传入管理器名称,目录名称 等,
  5. 当代码执行完毕以后,使用process.exit进行推出,
  6. 然后打印Scaffolding project in ${root}...

    我们接着往下

image.png

  1. 这里定义了一个templateDir 这个主要是用来获取模版的绝对路径的因为template之前源于variant variant本身并没有完整的模版名,所以这里需要拼接写 模版名,最终得到一个完整的外部的模版名称,
  2. 再然后就比较简单了,创建write利用fs模块 来创建文件了,首先先判断是文件还是文件夹,有如果是文件夹就创建文件夹没否则就拷贝文件(单个文件)
  3. 再然后就获取目前模板的绝对路径,赋值给files
  4. 然后就是设置package.json的一些关键信息
  5. 再然后我们需要写入自己的packge.json文件,因为package.json文件本身包含一些文件信息,所以不能直接拷贝
  6. 接下来的if (isReactSwc) 则是针对react-swc的操作,如果选择了这个那么就得是ts

然后接下来的就主要是输入到log中的内容了。

  1. 首先设置一个doneMessage为空字符串
  2. 然后获取我们创建的项目和当前目录之间的差异路径
  3. 将差异路径赋值给donemessage
  4. 然后判断创建目录的路径是否等于当前目录的路径,如果相等的话 ,就将其中的正则匹配通过三元表达式赋值给doneMessage 5.因为之前我们已经哪都pkgManager 他就包管理器名称,这里主要是针对yarn进行区分,然后拼接对应的字符串,最后通过 prompts.outro(doneMessage) 输出到页面

到此整个npm crete vite的功能基本已经实现了:接下来我们总结下这个主要干了些什么事情:

总结:

1. 首先是获取项目名称以及目标目录,
2. 对目标目录进行处理
3. 再然后就是获取我们想创建什么样的项目
4. 执行创建命令创建对应的项目。