create-vite源码解析

221 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,  点击了解详情一起参与。

这是源码共读的第37期,链接:若川视野 x 源码共读 第37期 | vite 3.0 都发布了,这次来手撕 create-vite 源码

本篇文章将主要介绍针对create-vite的源码解析:

一、create-vite的基本使用

npm create vite@latest
image.png

当执行npm create/yarn create/pnpm create时,会先去查找一个npm包,包名为create/xxx;也就是说,当你执行npm create vite@latest时,实际上是在执行npx create-vite@latest;

二、源码解析

1、目录结构

image.png

template-*开头的目录代表的是create-vite支持的模版。create-vite的主文件是index.js

2、index.js文件内容

image.png
  • 其中init函数为整个create-vite的主要处理函数、FRAMEWORKS为一个配置对象,主要记录create-vite支持哪些模版,已经对应模版支持的js/ts等:
image.png
  • TEMPLATES变量,是一个数组,主要是从FRAMEWORKS变量中取出当前所支持的框架信息:
image.png
  • init函数:
initjs.png

该函数的主要作用:

  • 提供交互信息
  • 根据用户选择创建文件

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,用于解决多平台下的路径问题