手撕create-vite源码

188 阅读4分钟
create-vite

create-vite是什么呢,它是一个快速创建流行框架的工具。 主要是一些命令:(readme文件中获取的)

# 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

在读源码之前,我需要了解一些关于node的package.json知识,否则我很难阅读懂这个代码(拿到这个代码我怎么获取一些基础信息,代码的入口文件是什么等等),首先我们在生成一个node工程的时候,我们会使用npm init命令进行项目的初始化,填写一些必要信息,这个很重要,此过程会生成一个package.json文件,记录你的初始化信息。

package.json文件的作用:对项目或者模块包的描述,里面包含许多元信息。比如项目名称,项目版本,项目执行入口文件,项目贡献者等等。npm install 命令会根据这个文件下载所有依赖模块

我打开一个工程第一步就是看package.json文件,通过这个文件我可以知道工程的入口文件(main),工程的依赖dependencies(生产),devDependencies(开发),内部命令对应的可执行文件的路径(bin)。等等很多信息。 参考blog.csdn.net/song_6666/a…

那么通过分析package.json,入口文件为index.js,好家伙一看内容

import './dist/index.mjs'

这么简单吗,一看还没有dist文件夹,咋办,看package.json吧,看到了dev命令

"dev": "unbuild --stub",

一个新东西,就在www.npmjs.com/package/unb… 看了一下,有一个build.config.ts文件里面告诉了我们入口的文件是如下的这个文件:

entries: ['src/index'],

打包出来的文件如下:

image.png

好吧正式开始撕它。

简单的进行分类:

  • 引入工具类(重点:minimist(解析命令参数) prompts(询问选择) kolorist(终端颜色打印))
  • 变量定义
  • 内置工具函数
  • init函数(重点)
  • 启动init

文章主要围绕内置的工具函数和init函数进行解读:

  1. 内置工具函数:

    formatTargetDir:将字符串中末尾的反斜杠 /替换为空字符串(传入的参数是命令行第一个参数),最主要的是此函数里面用了trim,当年写代码没有意识,被很多空字符串坑惨了,看到了这里想起很多教训,才体会得到trim的重要性。

    copy:同步获取文件信息,判断是文件夹还是文件,文件直接复制,文件夹调用copyDir。

    isValidPackageName:正则校验包名是否有效。

    toValidPackageName:使用正则将包名变的有效

    copyDir:此函数先创建一个文件夹,然后将传入的文件夹路径中的信息读取再次调用copy函数,和递归很类似。

    isEmpty:判断文件是否为空,当文件存在一个且只为git文件时,为空时返回true。

    emptyDir:清除文件夹,但是不清除git文件。

    pkgFromUserAgent:通过用户代理判断返回对应的命令(npm yarn pnpm)

  2. init函数流程梳理:

    首先使用解析一下命令行参数, 将初始值读取到:如图示例就将对应的创建文件名称读取到了

image.png 然后使用prompts库函数进行对用户询问,没有的就进行选择,有的设置默认值让你进行确认,

const { framework, overwrite, packageName, variant } = result;

将用户填写的值取出来,然后合成路径

const root = _nodePath.default.join(cwd, targetDir);

根据选项是否进行文件夹清除或者递归创建文件夹:

if (overwrite) {
    emptyDir(root);
} else if (!_nodeFs.default.existsSync(root)) {
    _nodeFs.default.mkdirSync(root, { recursive: true });
}

确认用户要创建的js模板(vue等):

const template = variant || framework || argTemplate;

根据环境创建命令,就是根据你的电脑环境决定使用npm, yarn 或者pnpm来创建环境:

const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent)
  const pkgManager = pkgInfo ? pkgInfo.name : 'npm'
  const isYarn1 = pkgManager === 'yarn' && pkgInfo?.version.startsWith('1.')

  const { customCommand } =
    FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}
  if (customCommand) {
    const fullCustomCommand = customCommand
      .replace('TARGET_DIR', targetDir)
      .replace(/^npm create/, `${pkgManager} create`)
      // Only Yarn 1.x doesn't support `@version` in the `create` command
      .replace('@latest', () => (isYarn1 ? '' : '@latest'))
      .replace(/^npm exec/, () => {
        // Prefer `pnpm dlx` or `yarn dlx`
        if (pkgManager === 'pnpm') {
          return 'pnpm dlx'
        }
        if (pkgManager === 'yarn' && !isYarn1) {
          return 'yarn dlx'
        }
        // Use `npm exec` in all other cases,
        // including Yarn 1.x and other custom npm clients.
        return 'npm exec'
      })

    const [command, ...args] = fullCustomCommand.split(' ')
    const { status } = spawn.sync(command, args, {
      stdio: 'inherit'
    })
    process.exit(status ?? 0)
  }

调试的时候发现了这样的情况,源代码为:

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

编译完成后:

 const templateDir = _nodePath.default.resolve(
  (0, _nodeUrl.fileURLToPath)("file:///D:/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/vite-main/packages/create-vite/src/index.ts"),
  '../..',
  `template-${template}`);

其实就是获取对应的模板路径。

然后使用如下的代码,读取对应文件夹下的template-xxx,然后将其写到你填写的路径下去:

const write = (file, content) => {
    const targetPath = _nodePath.default.join(root, renameFiles[file] ?? file);
    if (content) {
      _nodeFs.default.writeFileSync(targetPath, content);
    } else {
      copy(_nodePath.default.join(templateDir, file), targetPath);
    }
  };

  const files = _nodeFs.default.readdirSync(templateDir);
  for (const file of files.filter((f) => f !== 'package.json')) {
    write(file);
  }

单独处理了一下package.json文件:

  const pkg = JSON.parse(
  _nodeFs.default.readFileSync(_nodePath.default.join(templateDir, `package.json`), 'utf-8'));


  pkg.name = packageName || getProjectName();

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

最后就是将信息打印一下,告诉你完成了:

if (root !== cwd) {
    console.log(`  cd ${_nodePath.default.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;}

  1. 总结:大佬还是牛,用了很少的库就完成了一份在外人看起来很复杂的工作,其实自己一读也没有那么复制,总之多看牛人的东西才可以促进自己进步,之前我一直觉得他到底时怎么把工程给创建的,想了很多,什么机器学习啥的都想过,天马行空的想法都有,但是其实大佬用了一个最简单的方式,就是我把所有的模板全部实现一下,让你们选择后copy一下就行,让我想到了一句话:想得多,困难会越来越多,做的多,困难会越来越少。