Vite实践之 "create-vite"源码研读

467 阅读2分钟

1. 前景

create-vite 是一个快速生成主流框架基础模板的工具,create-vite通过命令行调用 来实现vite项目创建的。 项目地址: create-vite

2. 概览

image.png

  • 脚手架可通过命令行指定项目名称及模板完成项目创建,如下:

    # npm 6.x
    npm create vite@latest my-vue-app --template vue
    
    # npm 7+, extra double-dash is needed:
    npm create vite@latest my-vue-app -- --template vue
    
    # yarn
    yarn create vite my-vue-app --template vue
    
    # pnpm
    pnpm create vite my-vue-app --template vue
    
  • 项目架构如下:

    create-vite项目结构图

  • 目前项目(4.2.0-beta.1版本)支持的预设模板包括以下几类: vanillavanilla-tsvuevue-tsreactreact-tsreact-swcreact-swc-tspreactpreact-tslitlit-tssveltesvelte-ts

  • 约为500行源码

3. 源码研读

3.1、流程主入口函数init研读

// node:fs,用于处理读文件读写、复制、删除、重命名等操作
import fs from 'node:fs'
// node:path:处理文件与目录路径
import path from 'node:path'
// node:url:处理和解析url的模块url
import { fileURLToPath } from 'node:url'

// minimist:轻量级的命令行参数解析引擎 ^1.2.8
import minimist from 'minimist'
// prompts:实现命令行交互式界面的工具 ^2.4.2
import prompts from 'prompts'
// kolorist:轻量级的色彩命令行文本工具  ^1.7.0
import {
  blue,
  cyan,
  green,
  lightGreen,
  lightRed,
  magenta,
  red,
  reset,
  yellow,
} from 'kolorist'

// 基础知识: slice(start,end):方法可从已有数组中返回选定的元素,返回一个新数组,包含从start到end(不包含该元素)的数组元素。(不会改变原数组)
// 见下文I注解 
const argv = minimist<{
  t?: string
  template?: string
}>(process.argv.slice(2), { string: ['_'] })
// 当前 Nodejs 的工作目录
const cwd = process.cwd()
// 默认文件路径
const defaultTargetDir = 'vite-project'

async function init() {
  // 获取arg._第一个参数并经过trim、干掉反斜杠/的操作
  const argTargetDir = formatTargetDir(argv._[0])
  // 命令行参数 --template 模板 或者 预设模板
  const argTemplate = argv.template || argv.t
  
  // 目标文件路径为处理的argTargetDir或默认的路径'vite-project'
  let targetDir = argTargetDir || defaultTargetDir
  // 获取项目名称:解析到的名称或预设名称
  const getProjectName = () =>
    targetDir === '.' ? path.basename(path.resolve()) : targetDir
  // 通过询问交互获取'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'相关参数信息
  let result: prompts.Answers<
    'projectName' | 'overwrite' | 'packageName' | 'framework' | 'variant'
  >
  try {
    result = await prompts(
      [],
      {},
    )
  } catch (cancelled: any) {
    console.log(cancelled.message)
    return
  }
  // 结构赋值 获取相关参数  
  const { framework, overwrite, packageName, variant } = result
  
  // 是否覆盖
  if (overwrite) {
    // 递归删除文件夹 
    emptyDir(root)   
  } else if (!fs.existsSync(root)) { // 以同步的方法检测目录是否存在,此处取反,则为不存在目录时
     // 创建文件夹
    fs.mkdirSync(root, { recursive: true })
  }
    
  
  // 根据配置模板文件及路径写入目标路径
  const templateDir = path.resolve(
    fileURLToPath(import.meta.url),
    '../..',
    `template-${template}`,
  )
  const files = fs.readdirSync(templateDir)
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
  }
  const pkg = JSON.parse(
    fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8'),
  )
  pkg.name = packageName || getProjectName()
  // 将获取的pkg.name同步至package.json文件
  write('package.json', JSON.stringify(pkg, null, 2) + '\n')
    
  
  // 目标信息输出及打印
  const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
  const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')
  
  const cdProjectName = path.relative(cwd, root)
  console.log(`\nDone. Now run:\n`)
  if (root !== cwd) {
    console.log(
      `  cd ${
        cdProjectName.includes(' ') ? `"${cdProjectName}"` : cdProjectName
      }`,
    )
  }
  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()
}

}
  • I.通过minimist 处理过的命令行参数会得到一个对象,非用户指定参数(即没有指定名称)的参数以数组的形式存储在_,如:npm create vite@latest my-vue-app --template vue中,process.argv.slice(2)['my-vue-app','--template','vue'],故而将其解析为:

    {
      _: ['my-vue-app'],
      template: 'vue'
    }
    

3.2、流程总结

  1. minimist命令行相关参数解析
  2. prompts命令行交互询问确认相关参数<framework, overwrite, packageName, variant >
  3. overwrite处理
  4. 目标路径确认、及模板SWC处理
  5. 文件写入write
  6. 结束打印log

3.3、扩展函数 (可跳过)

  • 文件判空

    function isEmpty(path: string) {
      const files = fs.readdirSync(path)
      return files.length === 0 || (files.length === 1 && files[0] === '.git')
    }
    
  • copy函数及copyDir函数

    // 文件复制
    function copy(src: string, dest: string) {
      const stat = fs.statSync(src)
      if (stat.isDirectory()) {
        copyDir(src, dest)
      } else {
        fs.copyFileSync(src, dest)
      }
    }
    // 文件夹复制
    function copyDir(srcDir: string, destDir: string) {
      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 emptyDir(dir: string) {
  if (!fs.existsSync(dir)) {
    return
  }
  for (const file of fs.readdirSync(dir)) {
    if (file === '.git') {
      continue
    }
    fs.rmSync(path.resolve(dir, file), { recursive: true, force: true })
  }
}
  • swc处理
function setupReactSwc(root: string, isTs: boolean) {
  editFile(path.resolve(root, 'package.json'), (content) => {
    return content.replace(
      /"@vitejs\/plugin-react": ".+?"/,
      `"@vitejs/plugin-react-swc": "^3.0.0"`,
    )
  })
  editFile(
    path.resolve(root, `vite.config.${isTs ? 'ts' : 'js'}`),
    (content) => {
      return content.replace('@vitejs/plugin-react', '@vitejs/plugin-react-swc')
    },
  )
}

function editFile(file: string, callback: (content: string) => string) {
  const content = fs.readFileSync(file, 'utf-8')
  fs.writeFileSync(file, callback(content), 'utf-8')
}

4. 总结

EOF,愿你千山暮雪海棠依旧