关于create vite的源码分析

1,019 阅读17分钟

一、写在开头

注意:这里的说明非常重要!

PS:已经掌握的大佬请自行忽略下面的说明。

1、vue cli、vite、create vite、create vue

我们先说明下这几个具体是啥。

  • vite:vite是一个全新的构建工具,作用就像webpack一样,具体说明可查阅vite官方文档
  • vue cli:vue cli是以前创建vue项目的脚手架工具,底层构建工具是使用的webpack,可查阅vue cli官方文档
  • create vue:create vue是目前创建vue项目的脚手架工具,底层构建工具是使用的vite,是vue官网目前建议使用的构建vue项目的方式,可查阅vue官网
  • create vite:create vite也是一个脚手架工具,底层构建工具是使用的vite,但是构建的项目可以有多种框架,如vue(js/ts)、react(js/ts)、svelte(js/ts)、vanilla(js/ts)等。以创建vue项目为例,相比于上面专门用来创建vue项目的create vue来说,create vite创建vue项目配置会很少,额外需求需要用户自行安装插件,比较轻量多元;

现在我们已经搞清楚了这几个都是干嘛的,回到主题,本文就是分析create vite的源码,即这个脚手架工具是如何去创建我们的项目模板的。

2、npm init和npx

本文分析源码过程执行的是node调试,并不会使用npm init或者npx去安装执行该脚手架,但是实际开发使用脚手架时我们需要使用这个命令去执行脚手架,所以此处有必要讲讲npm init和npx。

这里我直接说结论,并附加对应的例子。

  1. npm init 不跟包名pkg,则默认只会创建package.json文件,需要用户交互输入一些信息。
npm init 
  1. npm init -y 跟了参数-y,则默认只会创建package.json文件,不需要用户交互输入,信息默认。
npm init -y
  1. npm init 和 npx 相似,在npm@6版本中,增加了一个新命令npm init <pkg>,npm init < pkg >输入了包名,则默认会执行npx create-< pkg >。对安装的pkg包添加create前缀,相当于为create开头的脚手架制定了一个特殊命令。
npm init pkg
// 上面命令等价于下面
npx create-pkg
  • 例子1:以react脚手架create-react-app为例创建react项目
    npm init react-app myReactProject
    // 上面命令等价于下面
    npx create-react-app myReactProject
    
  • 例子2:也可以跟版本信息,以脚手架create-vue为例创建vue项目,注意这里的latest其实对应了一个版本。
    npm init vue@latest myVueProject
    // 上面命令等价于下面
    npx create-vue@next myVueProject
    
    查看latest版本: image.png
  1. npx 可以避免全局安装的模块,npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去环境变量里面的 PATH 里找。如果依然找不到,就会帮你安装,下载到一个临时目录,使用以后再删除掉。
  • 例子1(不使用npx):以脚手架create-react-app为例创建react项目,需要先全局-g安装这个脚手架工具,然后使用终端命令create-react-app创建项目
    npm install -g create-react-app
    create-react-app my-react
    
  • 例子2(不使用npx):以脚手架vue cli 为例创建vue项目,需要先全局-g安装这个脚手架,然后使用终端命令vue create创建项目
    npm install -g vue-cli
    vue create my-vue
    
  • 例子3(使用npx):使用npx就不需要进行全局安装(下载到临时目录,使用完自动删除),以create vite脚手架为例创建项目(当然此处没有指定哪种框架,需要进行用户交互)
    npm init vite my-vite
    // 上面命令等价于下面(当然我们完全可以直接使用npx命令,不使用npm init,一个意思)
    npx create-vite my-vite
    

参考文献:

以上介绍的两部分内容,完全没有涉及到本期create vite源码的分析,但是笔者觉得非常有必要在这里详细的说明,一方面是让读者区分几个脚手架之间的关系,另一方面是加深对于包管理工具的认识,这点我认为对于新手来说非常重要!

前面我们知道了create vite是干嘛的,接下来我们具体分析create vite源码究竟是通过什么样的方式去创建项目的。

二、源码分析

1、克隆项目与调试

这里我依旧直接用川哥的创建好的项目了,大家也可以直接用vite原项目地址进行克隆。

git clone https://github.com/lxchuan12/vite-analysis.git
cd vite-analysis/vite2
# npm i -g pnpm
pnpm install

注意:

  • vite项目包含了很多工具,我们要分析的create vite在【packages/create-vite】中,所以我们后面分析的源码其实在create-vite/index.js中。 image.png
  • 等下我们会在index.js中打断点,在node中执行这个index.js文件去创建vue项目,默认生成的项目是存放在node启动的目录中,因此我们会在外面目录执行node命令,避免如果直接node index.js时生成的项目会和index.js同目录。
# 在这个 index.js 文件中打断点
# 在命令行终端调试,注意我此处没有cd到create-vite目录下 node index.js,而是在外层目录node,原因上面说了
node vite2/packages/create-vite/index.js

注意:此处不再赘述如何进入调试模式,有不清楚的读者可自行搜索或者查看我上一期的文章element-ui中关于make new自动化生成组件,里面有介绍几种进入node进入调试默认方式,我这里采用的是【JavaScript Debug Terminal】方式调试的。

image.png

2、实际效果

在分析源码之前,我们先另开正常终端去执行一下index.js文件,看下最终的效果,然后我们再根据效果去分析源码。

  • 新开终端,执行index.js,此处我额外传递了项目路径(名称)参数,其实不传默认会以vite-project为项目(路径)名称,这里其实可以指定嵌套目录的,当然我这里没有嵌套,那么这个自然就作为项目名,后面我也会再次提到;
  • 进行了一些用户交互,用户自定义选择何种框架及变体语言;
  • 生成我们需要的vue+ts的项目; image.png

3、源码分析

我们回到前面的调试界面。

特别声明:当分析多种情况时,为了让读者清晰知道当前情况发生时,值是多少,下面声明了目前调试所处的路径和执行方式,这样有时候我会直接表明当前情况该值为XXX,方便读者有个清晰的认知。

  • 我当前的工作目录,所处的目录是
D:\coderali\web-code\source-code\read-code\10_手撕create vite源码\vite-analysis>
  • 下面的分析过程都是基于我在上面这个工作目录中执行下面的终端命令,创建项目调试。
node .\vite2\packages\create-vite\index.js test1-vite
# 其实实际完整如下,比较长,多担待
D:\coderali\web-code\source-code\read-code\10_手撕create vite源码\vite-analysis>node .\vite2\packages\create-vite\index.js test1-vite

(1)、几个插件说明

我们先看下该文件一共使用到的几个插件或node模块。

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
// 处理参数,git地址:https://github.com/substack/minimist
import minimist from 'minimist'
//处理用户交互输入,git地址:https://github.com/terkelg/prompts
import prompts from 'prompts'
// 处理文字颜色,git地址:https://github.com/marvinhagemeister/kolorist
import { blue, cyan, green, lightRed, magenta, red, reset, yellow } from 'kolorist'
  • fs:node中处理文件输入输出的io模块,后面我们读写文件要用;
  • path:node中用于处理路径的模块,后面处理文件路径要用;
  • fileURLToPath:把file:///x/x/x/x.y的本地文件路径字符串转成D:\\x\\x\\绝对路径字符串,此处主要是处理后面import.meta.url的路径,获取index.js在用户电脑中的路径地址,进而获取和index.js同级的各template模板文件地址,由于在node中import.meta.url是以file协议存在的,不能直接使用,因此转成磁盘绝对地址。
    # 为了方便理解,举个我在调试中的例子
    console.log(import.meta.url); 
    # 打印结果:由于还有中文,进行了一定程度编码
    file:///D:/coderali/web-code/source-code/read-code/10_%E6%89%8B%E6%92%95create%20vite%E6%BA%90%E7%A0%81/vite-analysis/vite2/packages/create-vite/index.js
    console.log(fileURLToPath(import.meta.url));
    # 打印结果:下面这种路径就可以使用fs直接读写了
    D:\\coderali\\web-code\\source-code\\read-code\\10_手撕create vite源码\\vite-analysis\\vite2\\packages\\create-vite\\index.js
    
    关于import.meta.url可参考下图说明,详情可查阅阮一峰老师的es5入门image.png
  • minimist:用于处理用户用该插件创建项目时传递的参数,本文中主要提取自定义的项目名及-t或者-template参数指定后面跟的模板参数,如vue-ts,详情后面会讲,此处留个印象。例如:
    # 如:
    node index.js -a beep -b boop foo bar
    # minimist会处理成:
    { _: ['foo', 'bar'] , a: beep', b: 'boop' }
    # 说明:每一个-x后面紧接的参数都会以键值对形式存在,其余前面没有以-x跟的都合成数组,作为_的键值。
    
    # 如:
    node index.js test-vite -t vue-ts
    # 处理成
    { _: ['vue-ts'], t: 'vue-ts'}
    
  • prompts:处理用户终端交互的插件,如让用户在终端自定义项目名、选择项目框架、选择变体等等;
  • kolorist:处理字体颜色的,指定上面插件在用户交互过程中对应字体的颜色; image.png

上面几个插件,大家需要知道是干嘛的,这样等下分析到时能反应过来。

(2)、TEMPLATES

// process.argv.slice(2)获取我输入的参数,并用minimist处理,得到的argv为{_: ['test1-vite']}
const argv = minimist(process.argv.slice(2), { string: ['_'] })
// 返回当前 Node进程执行的目录,
// 如我当前是D:\\coderali\\web-code\\source-code\\read-code\\10_手撕create vite源码\\vite-analysis
const cwd = process.cwd()
// 该数组对象主要存储了几种框架数据,及对应的模板信息,颜色color是用来指定用户交互时文字颜色
const FRAMEWORKS = [
  {
    name: 'vanilla',
    color: yellow,
    variants: [
      {
        name: 'vanilla',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'vanilla-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'vue',
    color: green,
    variants: [
      {
        name: 'vue',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'vue-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'react',
    color: cyan,
    variants: [
      {
        name: 'react',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'react-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'preact',
    color: magenta,
    variants: [
      {
        name: 'preact',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'preact-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'lit',
    color: lightRed,
    variants: [
      {
        name: 'lit',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'lit-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  {
    name: 'svelte',
    color: red,
    variants: [
      {
        name: 'svelte',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'svelte-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  }
]

// 此处用了较为长的链式调用处理,得到的所有的框架对应的模板值,
// TEMPLATES为[ "vanilla", "vanilla-ts", "vue", "vue-ts", "react", "react-ts", "preact", "preact-ts", "lit", "lit-ts", "svelte", "svelte-ts", ]
const TEMPLATES = FRAMEWORKS.map(
  (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), [])

// 单独为.gitignore处理映射,此处解释一下,.开头的配置文件会影响cli工具和编译器的行为。
// 因此一般.开头的配置文件在cli中使用_开头名称替代。
// 以当前为例,cli模板中是_gitignore文件,然后读取写入到用户项目中是.gitignore,后面用到时再说明一次。
const renameFiles = {
  _gitignore: '.gitignore'
}

image.png

(3)、init函数

分析完上面的代码,我们把下面所有的函数都折叠,你会发现,下面只做了一件事情,调用了init函数,而init函数内部调用了外面这些函数。 image.png 下面针对init函数内部执行过程,及调用到外面的函数进行分析。

提醒:由于外面的函数是单独存在的,因此下面在分析init内部执行时,如果调用了下面的某个函数,那么我会直接把对应函数代码放在一起,方便分析。同时由于我是代码片段单独截取分析的,读者阅读时最好在自己电脑打开该项目,方便知道目前代码处理哪里,再次强调,处了声明处于外面的函数外,其余下面的代码都处于init函数中。

(4)、targetDir的处理

提示:

  1. targetDir是项目路径,不是项目名称,这里是允许传递嵌套的路径的。如指定foo/bar时,targetDir是foo/bar,项目名称后面会截取,为bar;如果只传了foo,则项目路径和项目名称就都为foo了,此处我们指定的是test1-vite,则,targetDir和项目名称都为test1-vite,下面会讲到。
  2. 最好不要用./或者/开头作为项目路径,虽然生成的项目依旧会按这个路径生成,但是由于package.json文件中的name字段值是取得这个路径处理后的,后面会提到,如指定为./test-vite/test时,name会变成-test-vite-test,就会比较奇怪。

下面继续分析。

   // 使用到的外面函数:
   // 该函数用于处理项目路径,去除首尾空格,去除尾部的/
   function formatTargetDir(targetDir) {
      return targetDir?.trim().replace(/\/+$/g, '')
   }
   
  // ----------分割线:下面是init内部执行代码-------------
  // 获取自定义项目路径,注意如果我们没指定路径时,则为undefined,此处是test1-vite
  let targetDir = formatTargetDir(argv._[0])
  // 判断是否使用-t或者-template指定了模板,其实--t或--template也可以,我们没有指定此处为undefined
  let template = argv.template || argv.t
  // 用户未指定时,项目路径(名称)的默认值,此处我们指定了test1-vite,后面就不会用到defaultTargetDir这个字段
  const defaultTargetDir = 'vite-project'
  // path.resolve()是获取当前工作目录,前面我们有提到过,为了方便调试,a和b变量是我的测试代码
  // 此处为:"D:\\coderali\\web-code\\source-code\\read-code\\10_手撕create vite源码\\vite-analysis"
  // let a = path.resolve();
  // path.basename截取工作目录的最后一个路径,此处为vite-analysis
  // let b = path.basename(path.resolve());
  
  // 定义一个方法:判断如果指定了项目路径为.,则返回当前工作目录的最后一位,此处我们是test1-vite
  const getProjectName = () => targetDir === '.' ? path.basename(path.resolve()) : targetDir

  let result = {}

(5)、prompts

下面代码主要是执行prompts插件的交互操作,详情可以查看最上面导入插件处注明的github地址,可参考上面的使用方式,下面就简单概述下几个交互的过程,读者可根据注释理解。

// 下面用到的外部方法
// 该函数判断传入的项目路径字段是否合法
function isValidPackageName(projectName) {
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
    projectName
  )
}

// 处理传入的项目路径,前面我有提示,如果指定./test-vite/test时,经过下面处理就会变成-test-vite-test,比较奇怪
// 所以指定项目路径时前面不要用./或者/
function toValidPackageName(projectName) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z0-9-~]+/g, '-')
}

// -----------分割线----------
// 整体使用try catch捕获异步交互行为的错误
try {
    // 注意,下面的几个预设的交互不一定都会执行,主要是看最早是否指定项目路径及模板参数等进行判断。
    // 另外,最后的result对象中的键值对,键值是每个交互的name,值是交互选择或者输入的值。
    result = await prompts(
      [
      // 第一个交互,判断是否指定项目路径,若无,则需用户输入,用户若直接回车,则取defaultTargetDir默认值
        {
          type: targetDir ? null : 'text',
          name: 'projectName',
          message: reset('Project name:'),
          initial: defaultTargetDir,
          onState: (state) => {
            console.log(state)
            targetDir = formatTargetDir(state.value) || defaultTargetDir
          }
        },
        {
          // 第二个交互:判断创建的项目路径是否已经存在,如果存在,询问用户是否删除
          type: () =>
            !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
          name: 'overwrite',
          message: () =>
            (targetDir === '.'
              ? 'Current directory'
              : `Target directory "${targetDir}"`) +
            ` is not empty. Remove existing files and continue?`
        },
        {
          // 第三个交互:如果上面这个交互不执行,则该交互也不会执行,如果上面冲突了,
          // 提示用户删除,跳到该交互,如果用户输入no,则报错,若输入yes,
          // 则记录当前overwrite字段结果为true,继续下一个交互。
          type: (_, { overwrite } = {}) => {
            if (overwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          },
          name: 'overwriteChecker'
        },
        {
        // 第四个交互:根据前面声明的getProjectName方法获取项目路径,并isValidPackageName判断是否合规。
        // 该值最后是作为package.json文件的name字段的值
        // 当然如果之前指定的项目路径在这里被处理成packageName后不合法,也可以在这里二次自定义
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          // 对文件名进行replace,移除不合法的字符
          initial: () => toValidPackageName(getProjectName()),
          validate: (dir) =>
          // 替换完不合法的再匹配正则,确定名称是否合规
            isValidPackageName(dir) || 'Invalid package.json name'
        },
        {
        // 第五个交互:判断是否有指定模板,及模板是否是指定范围中的一个(其实这里知道模板就确定了框架),无或不在则交互选择框架
          type: template && TEMPLATES.includes(template) ? null : 'select',
          name: 'framework',
          message:
            typeof template === 'string' && !TEMPLATES.includes(template)
              ? reset(
                  `"${template}" isn't a valid template. Please choose from below: `
                )
              : reset('Select a framework:'),
          initial: 0,
          choices: FRAMEWORKS.map((framework) => {
            // 注意这里的framework.color不是颜色字符串,而是kolorist插件的颜色方法,如red()
            const frameworkColor = framework.color
            return {
              // 此处就是select,看到的是title,拿到的值是value,注意此处framework是对象
              title: frameworkColor(framework.name),
              value: framework
            }
          })
        },
        {
          //第六个交互:看上一个交互选的框架是否有要求选模板,有就选,没有就过。
          // 翻到前面FRAMEWORKS数组对象,其实每一个框架都提供了至少两种模板,所以才要选一个
          type: (framework) =>
            framework && framework.variants ? 'select' : null,
          name: 'variant',
          message: reset('Select a variant:'),
          // ts-ignore为忽略ts检验报错
          // @ts-ignore
          choices: (framework) =>
            framework.variants.map((variant) => {
              const variantColor = variant.color
              return {
                title: variantColor(variant.name),
                value: variant.name
              }
            })
        }
      ],
      {
        // 用户取消行为
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    return
  }

上述交互过程其实不难,主要是我们不熟悉prompts的使用,经过上面的分析,大概的使用方式相信读者已经大致了解,具体详情请查询对应github地址,前面插件描述章节有提供,请自行查阅。

(6)、交互结果、模板路径、项目路径

下面主要是对上面交互结果获取,创建的目标项目路径的绝对路径获取,以及需要使用的模板文件的路径获取。

  // 额外调用的外部函数
  // 若目标目录存在,则递归删除
  function emptyDir(dir) {
    if (!fs.existsSync(dir)) {
      return
    }
    for (const file of fs.readdirSync(dir)) {
      fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
    }
  }

  // -----------分割线
  // 获取上面交互的结果:框架信息、是否情况已存在文件、package.json的name值、模板
  const { framework, overwrite, packageName, variant } = result
  // 拼接node终端路径和项目路径,得到项目绝对路径
  const root = path.join(cwd, targetDir)

  // 冲突时如果用户确定移除原文件,则移除,移除完再创建目标项目目录
  if (overwrite) {
    emptyDir(root)
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root, { recursive: true })
  }

  // 获取模板,优先从交互值选,后自定义的,当然自定义有效的话variant就不会选,为undefined了。
  // 个人觉得这里framework没意义,该值为对象,我们是要获取模板,可改成template = variant || template
  template = variant || framework || template

  console.log(`\nScaffolding project in ${root}...`)

  console.log()
  // 下面的变量c和d是方便调试的测试代码,可以看到当前调试时对应的路径;
  // 前面有提到过用import.meta.url获取当前文件index.js的文件路径,在node中是file协议的,且有编码过;
  // 此处用fileURLToPath转成磁盘文件绝对路径。
  //c为 "file:///D:/coderali/web-code/source-code/read-code/10_%E6%89%8B%E6%92%95create%20vite%E6%BA%90%E7%A0%81/vite-analysis/vite2/packages/create-vite/index.js"
  // let c = import.meta.url;
  // d为"D:\\coderali\\web-code\\source-code\\read-code\\10_手撕create vite源码\\vite-analysis\\vite2\\packages\\create-vite\\index.js"
  // let d = fileURLToPath(import.meta.url);
  // "D:\\coderali\\web-code\\source-code\\read-code\\10_手撕create vite源码\\vite-analysis\\vite2\\packages\\create-vite\\template-vue-ts"
  
  // 获取到index.js的绝对路径,并拼接获取指定模板的绝对路径
  const templateDir = path.resolve(
    fileURLToPath(import.meta.url),
    '..',
    `template-${template}`
  )

index.js文件和其他模板文件夹

image.png

(7)、写入模板文件,完成项目创建

// 调用的外部方法
 // 获取当前文件状态,是文件则直接复制到目标文件中,是目录则调用copyDir方法
   function copy(src, dest) {
    const stat = fs.statSync(src)
    if (stat.isDirectory()) {
      copyDir(src, dest)
    } else {
      fs.copyFileSync(src, dest)
    }
  }
  
  // 创建目标文件目录,拼接目标文件路径,把当前文件目录内文件(目录)通过copy方法复制到目标目录
  function copyDir(srcDir, destDir) {
    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 pkgFromUserAgent(userAgent) {
    if (!userAgent) return undefined
    const pkgSpec = userAgent.split(' ')[0]
    const pkgSpecArr = pkgSpec.split('/')
    return {
      name: pkgSpecArr[0],
      version: pkgSpecArr[1]
    }
  }
  

// -----------分割线-------------
// 写入文件的方法:
  const write = (file, content) => {
    // 拼接目标文件绝对路径,注意特例是_gitignore,
    const targetPath = renameFiles[file] ? path.join(root, renameFiles[file]) : path.join(root, file)
    if (content) {
      // 只有package.json会执行进来,content重写了name值
      fs.writeFileSync(targetPath, content)
    } else {
      // 复制文件到目标目录
      copy(path.join(templateDir, file), targetPath)
    }
  }

  // 读取模板文件目录
  const files = fs.readdirSync(templateDir)
  // 遍历模板目录内每个文件并调用write进行复制
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
  }
  // 单独处理package.json文件,该文件不进行复制,拿到文件内容改name字段值调用write方法写入目标文件
  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获取当前包管理工具名称和版本
  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm'

 // 下面就一些终端打印输出的信息
  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(`  cd ${path.relative(cwd, root)}`)
  }
  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()
}

以上就是整个create vite脚手架执行项目创建的过程。

三、总结和收获

1、总结

整体来说create vite脚手架创建项目的逻辑不算复杂,主要是以下几个内容:

  • 创建好不同框架模板的文件,让用户使用脚手架时能根据自定义框架去使用不同的模板文件;
  • 使用node的path模块,处理脚手架内以入口文件index.js为基准,去获取模块文件目录的路径,并根据当前用户node终端运行的环境及自定义项目路径去确定最终项目绝对路径;
  • 使用prompts插件来实现终端中用户的交互行为,让用户自定义去选择要创建的项目所使用的框架和语言变体类型(js/ts),同时收集用户的交互信息;
  • 使用node的fs模块,根据用户的交互信息确定需要使用的模板文件,根据模板内所有文件的绝对路径分别把文件复制到目标项目的绝对路径下,本质上就是把模板文件整体的复制一份作为用户创建的项目,当然以.开头的配置文件和package.json文件进行了单独处理;

2、收获

  • 了解到了prompts插件、minimist插件、kolorist插件的一些常规使用;
  • node中url模块的fileURLToPath函数的作用,及es6中import.meta.url的作用,这在脚手架工具中还是很有用的;
  • 加深了node中fs模块处理文件的了解,直接写入、复制、删除等,及对应递归方法的设计;
  • 其实通过create vite脚手架的分析,我们也完全可以自己写出一个脚手架的工具,当然这里采用的主要是fs文件写入,其实也可以把模板文件放在github上,然后利用插件实现git clone模板文件的方式来代替fs;