likes
comments
collection
share

探索create-vite源码,一看就懂!

作者站长头像
站长
· 阅读数 27

1.背景

2.指令说明

create-vite 是一个快速构建vite工程的脚手架,使用npm create vite创建工程,其中create是init的别名,具体可查看官方文档

3.调试准备

本文使用川哥项目地址:github.com/lxchuan12/v…

按以下顺序依次执行命令,克隆项目,调试源码

git clone https://github.com/lxchuan12/vite-analysis.git

cd vite-analysis/vite2/packages/create-vite

node index.js

安装依赖时,我这里报错了,说是因为我本地node版本过低14.18.1,所以我还装了nvm,安装了最新版本。 项目下载下来以后,先看README.md和CONTRIBUTING.md,这里会有对项目的一些说明,包括环境及调试方法。

4.项目结构概览

探索create-vite源码,一看就懂!

通过package.json文件里的信息我们可以知道,type类型是module,可执行的命令create-vite,入口文件是index.js

探索create-vite源码,一看就懂!

index.js文件内的变量及函数

探索create-vite源码,一看就懂!

探索create-vite源码,一看就懂!

5.代码分析

从index.js文件内部函数命名,我们也可以看出init应该是需要打断点的位置,上面一些其他的方法是为这个函数服务的。

探索create-vite源码,一看就懂!

// node依赖
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

// npm包 解析命令行参数
import minimist from 'minimist'

// npm包 命令行交互
import prompts from 'prompts'

// npm包 命令行颜色
import {
  blue,
  cyan,
  green,
  lightRed,
  magenta,
  red,
  reset,
  yellow
} from 'kolorist'

// 解析指令上附带的命令行参数
const argv = minimist(process.argv.slice(2), { string: ['_'] })

// 获取当前node进程所在的文件目录
const cwd = process.cwd()

// 框架
const FRAMEWORKS = [
  {
    name: 'vanilla',
    color: yellow,
    variants: [
      {
        name: 'vanilla',
        display: 'JavaScript',
        color: yellow
      },
      {
        name: 'vanilla-ts',
        display: 'TypeScript',
        color: blue
      }
    ]
  },
  ...
]

// 模板名称
const TEMPLATES = FRAMEWORKS.map(
  (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name]
).reduce((a, b) => a.concat(b), [])

// 重写名字映射(电脑系统不一样,gitignore前缀名称不一样)
const renameFiles = {
  _gitignore: '.gitignore'
}

// 初始化函数,核心方法
async function init() {
 ...
}

// 去除字符串的空格,并将\替换为空字符串
function formatTargetDir(targetDir) {
  return targetDir?.trim().replace(/\/+$/g, '')
}

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

// 校验包名是否符合规范
function isValidPackageName(projectName) {
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(
    projectName
  )
}

// 处理包名为合法名称
function toValidPackageName(projectName) {
  return projectName
    .trim()
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^[._]/, '')
    .replace(/[^a-z0-9-~]+/g, '-')
}

// 拷贝文件夹
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 isEmpty(path) {
  const files = fs.readdirSync(path)
  return files.length === 0 || (files.length === 1 && files[0] === '.git')
}

// 删除文件夹及文件
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 })
  }
}

// 包管理器信息
function pkgFromUserAgent(userAgent) {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1]
  }
}

// 错误补获
init().catch((e) => {
  console.error(e)
})



6.初始化函数分析

6.1 获取工程目录及模板

// 获取命令行参数的工程目录
let targetDir = formatTargetDir(argv._[0])

// 获取命令行参数的模板
let template = argv.template || argv.t

// 默认的项目名
const defaultTargetDir = 'vite-project'

// 获取项目名称
const getProjectName = () =>
    targetDir === '.' ? path.basename(path.resolve()) : targetDir

6.2 命令行的交互(框架、变体、项目名),有输入截取校验,没有则让用户输入及选择

let result = {}

try {
    result = await prompts(
      [
        {
          type: targetDir ? null : 'text',
          name: 'projectName',
          message: reset('Project name:'),
          initial: defaultTargetDir,
          onState: (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?`
        },
        ...
       ],
       {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
    
// user choice associated with prompts
const { framework, overwrite, packageName, variant } = result

探索create-vite源码,一看就懂!

探索create-vite源码,一看就懂!

6.3 重写目录或创建目录

// 目录拼接
const root = path.join(cwd, targetDir)

// 重写文件夹(清空再创建或直接创建)
if (overwrite) {
  emptyDir(root)
} else if (!fs.existsSync(root)) {
  fs.mkdirSync(root, { recursive: true })
}

6.3.1递归删除文件夹

扩展函数

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 })
  }
}

6.4 确定模板路径

// 获取模板路径
template = variant || framework || template

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

const templateDir = path.resolve(
fileURLToPath(import.meta.url),
'..',
`template-${template}`
)

6.5写入文件(包含gitignore文件的特殊处理)

const write = (file, content) => {
    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)
    }
  }

const files = fs.readdirSync(templateDir)

// 除了package.json文件,都执行write方法
for (const file of files.filter((f) => f !== 'package.json')) {
    write(file)
}

// 获取package.json文件内容
const pkg = JSON.parse(
    fs.readFileSync(path.join(templateDir, `package.json`), 'utf-8')
  )
  
// 重写name字段(因为目录名称不一样,所以这里需要特殊处理)
pkg.name = packageName || getProjectName()

// 再写入
write('package.json', JSON.stringify(pkg, null, 2))

6.6 根据所安装的包管理器,对应输出安装信息

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
  }

探索create-vite源码,一看就懂!

7 总结

通过调试源码熟悉了nodejs的api; process.cwd()获取当前node进程所在的文件目录, path.resolve()获取文件绝对路径, path.join()也是获取文件路径,但和resolve有区别。 总的来说,该源码流程就是根据命令行输入,解析出参数,如没有输入参数时,通过prompts进行命令行交互,根据用户的输入匹配已预设好的相应框架模板,然后创建目录名称,写入文件。

转载自:https://juejin.cn/post/7394395254567845899
评论
请登录