create-vite源码解析

177 阅读1分钟

介绍

create-vite是vite官方提供的类似create-react-app的模板插件,用来生成vite应用的,同时内置了vite与react、svelte、当然还有vue等框架的模板,并且也有社区模板供大家使用

使用

create-vite作为一个单独的包发布在了npm市场,使用可以执行以下命令

根据使用的包管理器 三选一

$ npm init vite@latest
$ yarn create vite、
$ pnpm create vite

然后按照提示操作即可!

还可以通过附加的命令行选项直接指定项目名称和你想要使用的模板。例如,要构建一个 Vite + Vue 项目,运行:

# npm 6.x
npm init vite@latest my-vue-app --template vue

# npm 7+, 需要额外的双横线:
npm init 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

源码解析

npm init

一开始看到这里的时候就有点奇怪为什么我们执行npm init vite / yarn create vite命令时会使用create-vite这个npm包?这个命令和平常我们直接使用npm init有什么区别?最后翻了一下npm文档看到了关于这个命令的解释

npm init <initializer> can be used to set up a new or existing npm package.

initializer in this case is an npm package named create-<initializer>, which will be installed by npx, and then have its main bin executed -- presumably creating or updating package.json and running any other initialization-related operations.

The init command is transformed to a corresponding npx operation as follows:

  • npm init foo -> npx create-foo
  • npm init @usr/foo -> npx @usr/create-foo
  • npm init @usr -> npx @usr/create

Any additional options will be passed directly to the command, so npm init foo --hello will map to npx create-foo --hello.

If the initializer is omitted (by just calling npm init), init will fall back to legacy init behavior. It will ask you a bunch of questions, and then write a package.json for you. It will attempt to make reasonable guesses based on existing fields, dependencies, and options selected. It is strictly additive, so it will keep any fields and values that were already set. You can also use -y/--yes to skip the questionnaire altogether. If you pass --scope, it will create a scoped package.

大致意思就是init后面的初始化工具是可选的,如果携带了初始化工具名,就会到npm市场上寻找create-初始化工具名开头的包,并执行这个包里的bin文件。回到开头,我们在执行npm init vite的时候其实也就是在执行create-vite这个包

create-vite

从vitejs仓库拉下来这个项目,因为vitejs是monorepo,所以我就直接完整拉下来了,首先习惯性的打开package.json看几个关键信息

{
  "name": "create-vite",
  // 前几天发布的vite3
  "version": "3.0.0", 
  // esm
  "type": "module",
  "bin": {
    // 入口文件
    // 这里呼应了上面的npm init
    "create-vite": "index.js",
    "cva": "index.js"
  },
}

根据bin字段我们知道入口文件是index.js,打开梳理了之后我们能看到最先执行也是最核心的就是这个函数,执行完这个函数这个包也就执行完了。分析过程写在了注释中

init().catch((e) => {
  console.error(e)
})

async function init() {
  // 获取npm init vite xxx中的xxx 也就是你要创建的项目名
  let targetDir = formatTargetDir(argv._[0])
  let template = argv.template || argv.t
  // 默认项目名
  const defaultTargetDir = 'vite-project'
  const getProjectName = () =>
    targetDir === '.' ? path.basename(path.resolve()) : targetDir

  let result = {}
  
  // 从这里开始就是一些列的命令行交互,执行顺序就是数组的顺序
  // 用了prompts这个包,具体怎么使用没细看,分析了一下每一步的作用
  // 1、让你输入项目名
  // 2、判断当前目录有没有叫这个名字的文件夹,有的话是否要覆盖
  // 3、不覆盖的话就退出交互
  // 4、判断你这个项目名是否合法,主要就是用正则验证
  // 5、选择你想要使用的内置模板,也就是项目根目录中template开头的那些文件夹
  // 6、最后选择模板的衍生,例如是js还是ts
  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?`
        },
        {
          type: (_, { overwrite } = {}) => {
            if (overwrite === false) {
              throw new Error(red('✖') + ' Operation cancelled')
            }
            return null
          },
          name: 'overwriteChecker'
        },
        {
          type: () => (isValidPackageName(getProjectName()) ? null : 'text'),
          name: 'packageName',
          message: reset('Package name:'),
          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) => {
            const frameworkColor = framework.color
            return {
              title: frameworkColor(framework.name),
              value: framework
            }
          })
        },
        {
          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
              }
            })
        }
      ],
      {
        onCancel: () => {
          throw new Error(red('✖') + ' Operation cancelled')
        }
      }
    )
  } catch (cancelled) {
    console.log(cancelled.message)
    return
  }

  // 获取刚刚交互的结果
  const { framework, overwrite, packageName, variant } = result
  
  const root = path.join(cwd, targetDir)
  // 如果第二步确认清空的话,先删掉那个文件夹里面的所有文件
  if (overwrite) {
    emptyDir(root)
  } else if (!fs.existsSync(root)) {
    fs.mkdirSync(root, { recursive: true })
  }

  // determine template
  template = variant || framework || template

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

  // 这里用了fileURLToPath(import.meta.url)获取当前路径,不知道为什么不用cwd
  const templateDir = path.resolve(
    fileURLToPath(import.meta.url),
    '..',
    `template-${template}`
  )

  // 写入模板中的文件到项目下
  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)
  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()

  write('package.json', JSON.stringify(pkg, null, 2))

  // 这里应该是获取包管理器
  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()
}

分析过程省略了挺多东西,主要就是懒得写了

之后在分析一下vitejs仓库中最主要的vite吧!