浅析 create-vite 它做了什么?

359 阅读4分钟

随着前端工程化的不断发展,各种构建工具层出,而号称 “下一代前端开发与构建工具”的Vite,也吸引了大家的关注,今天就 create-vite 我们来看看他做了什么。

在Vite 官网 可以看到,如何使用 npm/pnpm/yarn 去初始化一个 vite 项目的步骤,也可以选择对应的框架模板,这里我们主要阐述 create-vite 是如何完成一个项目创建的。我们以 3.0 版本为例,因为后面的版本已经是 ts 了,会增加阅读难度。

create 命令

create 是 init 的一个别名,可以参考官网这边的介绍这边就不做赘述

  • npm init(create) vite --> npm exec create-vite
  • npm exec create-vite 会执行 package.json 中的 bin 中的内容

调试

  1. 代码仓库 github.com/vitejs/vite… 准备代码,这是一个 pnpm monorepo 项目
  2. 安装依赖,打开 vite-3.0\packages\create-vite\index.js
  3. 打开 debug 终端,index.js 的入口打断点,终端执行 node index.js 即可

image.png

代码分析

通过 prompts 实现的交互这个我们放在下面来讲

1. 判断用户输入的项目名称是否符合要求

// 获取输入的项目名,同时会作为目录
{
  type: targetDir ? null : 'text',
  name: 'projectName',
  message: reset('Project name:'),
  initial: defaultTargetDir,
  onState: (state) => {
    targetDir = formatTargetDir(state.value) || defaultTargetDir
  }
},
// 检测输入的目录是否已存在
{
  type: () =>
    // existsSync 以同步的方法检测目录是否存在,如果已存在,文件夹内是否有文件。
    !fs.existsSync(targetDir) || isEmpty(targetDir) ? null : 'confirm',
  name: 'overwrite',
  message: () =>
    (targetDir === '.'
      ? 'Current directory'
      : `Target directory "${targetDir}"`) +
    ` is not empty. Remove existing files and continue?`
},
// 目录已存在确认是否覆盖
{
  type: (_, { overwrite } = {}) => {
    if (overwrite === false) {
      throw new Error(red('✖') + ' Operation cancelled')
    }
    return null
  },
  name: 'overwriteChecker'
},
// isValidPackageName 校验文件名是否正确,不正确的话 toValidPackageName 进行转换,与用户确认
{
  type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
  name: 'packageName',
  message: reset('Package name:'),
  initial: () => toValidPackageName(getProjectName()),
  validate: (dir) =>
    isValidPackageName(dir) || 'Invalid package.json name'
}

2. 选择对应项目

// 项目模板选择
{
  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) => {
    const frameworkColor = framework.color
    return {
      title: frameworkColor(framework.name),
      value: framework
    }
  })
},
// 选择变种 js/ts
{
  type: (framework) =>
    framework && framework.variants ? 'select' : null,
  name: 'variant',
  message: reset('Select a variant:'),
  // @ts-ignore
  choices: (framework) =>
    framework.variants.map((variant) => {
      const variantColor = variant.color
      return {
        title: variantColor(variant.name),
        value: variant.name
      }
    })
}

3. 函数 formatTargetDir

去除路径两端空格,并将其中的 \ 替换成空字符

/**
 * @param {string | undefined} targetDir
 */
function formatTargetDir(targetDir) {
  return targetDir?.trim().replace(/\/+$/g, '')
}

4. 函数 copy

复制文件到指定目录

function copy(src, dest) {
  const stat = fs.statSync(src)
  if (stat.isDirectory()) {
    copyDir(src, dest)
  } else {
    fs.copyFileSync(src, dest)
  }
}

5. 函数 isValidPackageName toValidPackageName

isValidPackageName 判断包名是否正确;toValidPackageName 去除特殊字符转换成 "-"

/**
 * @param {string} projectName
 */
function isValidPackageName(projectName) {
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
    projectName
  )
}

/**
 * @param {string} projectName
 */
function toValidPackageName(projectName) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z0-9-~]+/g, '-')
}

6. 函数 copyDir isEmpty emptyDir

copyDir 复制文件到指定目录; isEmpty 判断当前文件是否空; emptyDir 清空当前目录

/**
 * @param {string} srcDir
 * @param {string} destDir
 */
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)
  }
}

/**
 * @param {string} path
 */
function isEmpty(path) {
  // readdirSync 方法将返回一个包含“指定目录下所有文件名称”的数组对象
  const files = fs.readdirSync(path)
  return files.length === 0 || (files.length === 1 && files[0] === '.git')
}

/**
 * @param {string} dir
 * 同步删除文件和目录
 */
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 })
  }
}

7. 函数 pkgFromUserAgent

得到 npm 包管理器相关信息

/**
 * @param {string | undefined} userAgent process.env.npm_config_user_agent
 * @returns object | undefined
 */
function pkgFromUserAgent(userAgent) {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1]
  }
}

拓展库

kolorist 让输出文字颜色多彩

可以改变 console.log() 输出内容的颜色,原来我们改变输出文字的颜色需要手动增加 style 来控制,可以查看在 mdn 的定义

console.log("This is %cMy stylish message", "color: yellow; font-style: italic; background-color: blue;padding: 2px");
console.log('%c哦吼', 'color:red')

import { red } from 'kolorist'
console.log(red('哦吼'))

image.png

minimist 命令参数解析

minimist 主要用于解析命令行后面的参数,这里是源码,大家有兴趣的可以看一下

image.png 由上图我们可以看出,process.argv 是通过空格拆分命令的,process.argv.slice(2) 就能获取到后面的自定义参数。上图我们可以看到两种不同的参数 hello --template vue,这种解析起来就比较繁琐,通过 minimist 就可以很好的解决。

image.png

未指定值得参数会以数组的形式存放在 _ 里面,其余的以 key value 的形式保存。

fs node 文件操作

  • fs.existsSync 同步检测目录是否存在
  • fs.mkdirSync 同步创建目录
  • fs.writeFileSync 同步写入文件
  • fs.readdirSync 同步读取目录
  • fs.readFileSync 同步读取文件
  • fs.statSync 方法可以获取文件信息
  • fs.copyFileSync 同步复制文件到指定目录
  • fs.rmSync 同步删除文件 ...

prompts 和控制台互动

我们输入项目名称、选择框架版本等操作,就是通过 prompts 来实现的,如下大家可以试试简易的交互行为。

import prompts from 'prompts'

// prompts 的使用
let res = await prompts([{
  type: 'text', // 输入字段的类型
  name: 'user', // 输入字段的key值
  initial: 'Why should I?', // 默认值
  message: '请输入用户名' // 输入的标题
}, {
  type: 'number',
  name: 'age',
  message: '请输入年龄',
  validate: val => val < 18 ? '年龄不得小于18' : true // 校验器,通过的条件作为返回true
}, {
  type: 'select',
  name: '性别',
  message: '请选择性别',
  // 不设置 value ,保存的就是下标
  choices: [{
    title: '男', value: '男'
  }, '女']
}], {
  onCancel: () => {
    throw new Error('x  操作终止')
  }
})

// 输入值变化,会触发 onState
// onState {
// 	aborted:false,
// 	exited:false,
// 	value:'12'
// }

console.log('prompts', res);

image.png

总结

看完 create-vite 解了之前很多疑惑,控制台交互、输出文字增加样式、强大的文件操作等,从命令敲下去的那一刻开始,所有的流程都有迹可循,需要我们进行抽丝剥茧来了解这个过程。