多多读书
1168 字
6 分钟
npm create是怎么初始化Vite项目的?
2023-10-01

在创建 Vite 项目时,通常会使用npm create vite@latest指令来进行初始化。然而,你可能只是简单地复制并执行该指令,而没有了解其背后的初始化模板过程。今天,我们来了解一下这个过程,相信看过之后你也可以编写一个指令来安装你的项目模板。

npm create是什么#

npm create实际上就是我们常用的npm init。在 npm 文档中并没有npm create独立的相关文档,但你在npm init文档中是能够找到的。

有点离谱,原来它还有这么多别名。所以,npm create vite@latest和下面这些指令都是等效的:

npm init vite@latest
npm innit vite@latest
npx create-vite@latest

使用 npm create 实际上会转换成 npm execnpm x),从而实现安装包和执行命令的功能,即:

npm create vite@latest -> npm exec create-vite@latest

不管这些指令怎么变,实际上都是执行create-vite包的指令。

create-vite指令#

接下来看看create-vite包是如何初始化项目的,可以在 Github 找到 Vite 的源码 https://github.com/vitejs/vite

可以看到,目录下有一系列模板和一个指令入口文件,不难想到,执行指令时就是把这些模板拷贝到你的项目下的。

可以在package.json找到指令名称和文件入口。

"bin": {
  "create-vite": "index.js",
  "cva": "index.js"
}

然后来看看入口文件index.js

#!/usr/bin/env node

import './dist/index.mjs'

可执行脚本通常需要在开头加上#!/usr/bin/env node这个特殊注释,用于声明使用 Node 来执行。下一行是导入./dist/index.mjs文件,它是打包后的文件,而源码则位于./src/index.ts文件内。

init()函数#

整个操作的执行都在 init 函数中,在理解代码逻辑时,可以从 init 函数入手。为了方便说明,下面我会对部分代码进行简化处理。

// 获取目录名称参数
const argTargetDir = formatTargetDir(argv._[0])
// 获取模板参数
const argTemplate = argv.template || argv.t
// 默认目录名 vite-project
let targetDir = argTargetDir || defaultTargetDir
const getProjectName = () =>
  targetDir === '.' ? path.basename(path.resolve()) : targetDir

let result // 交互命令选择结果

try {
  result = await prompts(
    // prompts 交互命令
  )
} catch (cancelled: any) {
}

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

let template: string = variant || framework?.name || argTemplate

// ....
// 读取模板的文件,除package.json都写入项目目录
const files = fs.readdirSync(templateDir)
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()
// 写入package.json文件到项目
write('package.json', JSON.stringify(pkg, null, 2) + '\n')

上述内容大致介绍了整个模板创建的流程,具体的细节可以自行查看代码。接下来说下使用的包和命令交互流程。

minimist 包解析参数#

minimist 包用于解析预设的参数,上面提到的目录名和模板等。

import minimist from 'minimist'

const argv = minimist(process.argv.slice(2), { string: ['_'] })

kolorist 包输出颜色#

kolorist 包用于设置命令行输出不同颜色。

import { yellow } from 'kolorist'

// console.log(yellow('Vanilla'));
// 源码中FRAMEWORKS定义中添加color函数,后面交互定义时调用
const FRAMEWORKS = [
  {
    name: 'vanilla',
    display: 'Vanilla',
    color: yellow,
  },
  // ...
]

prompts 包定义交互#

定义交互命令使用的 prompts 包,就是上面代码省略的部分。prompts 函数传入一个数组,包含整个流程的交互定义,这个包参数比较多,不详细说明。

import prompts from 'prompts'

result = await prompts([
  {
    type: argTargetDir ? null : 'text',
    name: 'projectName',
    message: reset('Project name:'),
    initial: defaultTargetDir,
    onState: (state) => {
      targetDir = formatTargetDir(state.value) || defaultTargetDir
    },
  },
])

const { packageName } = result
console.log(packageName)

cross-spawn 包生成子进程#

在我们选择模板时,有一个Others选项。选择完模板后,判断模板参数中是否包含customCommand。如果包含,则根据当前使用的包管理器替换成对应指令,然后开启子进程执行。

const { customCommand } = FRAMEWORKS.flatMap((f) => f.variants).find((v) => v.name === template) ?? {}

if (customCommand) {
  const fullCustomCommand = customCommand
    .replace(/^npm create /, () => {
      if (pkgManager === 'bun') {
        return 'bun x create-'
      }
      return `${pkgManager} create `
    })
    .replace('@latest', () => (isYarn1 ? '' : '@latest'))
    .replace(/^npm exec/, () => {
      if (pkgManager === 'pnpm') {
        return 'pnpm dlx'
      }
      if (pkgManager === 'yarn' && !isYarn1) {
        return 'yarn dlx'
      }
      if (pkgManager === 'bun') {
        return 'bun x'
      }
      return 'npm exec'
    })

  const [command, ...args] = fullCustomCommand.split(' ')
  const replacedArgs = args.map((arg) => arg.replace('TARGET_DIR', targetDir))
  const { status } = spawn.sync(command, replacedArgs, {
    stdio: 'inherit',
  })
  process.exit(status ?? 0)
}

最后,利用上面提到的流程和 npm 包,你是否也能够写一个这样的指令?把一些公共模板整理一下,每次新建项目时就可以直接用指令生成了。

npm create是怎么初始化Vite项目的?
https://fuwari.vercel.app/posts/20231011/
作者
我也困了
发布于
2023-10-01
许可协议
CC BY-NC-SA 4.0