阅读 116

create-vite 代码解析&实现一个自己的脚手架

学习任务

  • 源码:create-vite

  • create-vite 不到400行代码;

  • 可以学会如何写一个脚手架等等;

  • 注意:如果克隆的最新的代码(最新的create-vite已升级为 ts),按照我文中的方式不能调试。推荐使用 npx esno src/index.ts调试源码。

目录结构

image.png

create-vite的目录结构非常简单,根目录有一个index.js,用来指明脚本地址,src为项目目录,其余template-xxx为模板,用于创建模板。

依赖包说明

create-vite共使用了四个第三方包:

cross-spawn:运行批处理脚本

minimist:解析命令行参数

prompts:命令行交互式引导提示

kolorist:多彩命令输出

具体使用细节不再详述。

源码解析

获取命令参数

考虑用户首先使用npx create-vite xxx -t xxx创建项目,所以需要获取:

  • 项目目录

  • 模板名

这里使用minimist进行命令解析

const argv = minimist<{   t?: string   template?: string }>(process.argv.slice(2), { string: ['_'] })  const cwd = process.cwd() 复制代码

create-vite的命令为npm create vite@latest my-vue-app --template vue

argv最终为一个对象:

{      _: [ 'projectName' ],     t: templateName } 复制代码

其中_是一个数组,为用户输入的未带前缀属性的值,若运行上述命令,这里数组第一项就是my-vue-app

需要解释的是minimist第二个参数,这个在代码中做了注释,目的是为了将数字类型的项目名转换为字符串类型,见#4606 · vitejs/vite (github.com)

定义模板

既然是脚手架工具,必然少不了模板,vite是这样定义模板类型的:

type Framework = {   name: string   display: string   color: ColorFunc   variants: FrameworkVariant[] } type FrameworkVariant = {   name: string   display: string   color: ColorFunc   customCommand?: string } 复制代码

一般来说,Framework为某个框架,variants是框架下的不同脚手架实现,如js,ts版本,下面是一个示例:

 const FRAMEWORKS = [{     name: 'vue',     display: 'Vue',     color: green,     variants: [       {         name: 'vue',         display: 'JavaScript',         color: yellow       },       ...     ]   }] 复制代码

其中各字段用处为:

name:模板名,用于vite找到最终模板

display:用于命令行展示的名称

color:命令行展示时的文字颜色

customCommand:需要执行的命令

除了定义外,我们需要遍历这个Frameworks,将所有的模板名遍历出来,后面会用到:

const TEMPLATES = FRAMEWORKS.map(   (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name] ).reduce((a, b) => a.concat(b), []) 复制代码

这里也可以用flatMap实现

const TEMPLATES = FRAMEWORKS.flatMap(   (f) => (f.variants && f.variants.map((v) => v.name)) || [f.name] ); 复制代码

用户交互

作为一个脚手架工具,免不了与用户进行交互,这里vite使用的是prompts工具,他是一款轻量级,美观且用户友好的交互式提示库。prompts接收一个数组对象,返回一个promise包裹的最终结果。下面来看一个create vite都有哪些命令交互:

projectName

结合上文内容我们知道,用户在初始输入时可以输入projectName,不过也需要考虑用户没有输入的情况,这里会判断argTargetDir字段是否存在,不存在说明用户还没有输入,则提示用户输入,并提供一个默认的项目名称:

{   // 路径   type: argTargetDir ? null : "text",   name: "projectName",   message: reset("project name"),   initial: defaultTargetDir,   onState(state) {     targetDir = formatTargetDir(state.value) || defaultTargetDir;   }, }, 复制代码

overwrite

若用户提供的项目名称在当前路径下已存在且不为空,则需要提醒用户是否继续,如果用户选择否,则退出命令。

{   // 若文件夹已存在或不为空   type: () =>     !fs.existsSync(targetDir) || isEmptyDir(targetDir)       ? null       : "confirm",   name: "overwrite",   message: `${     targetDir === "." ? "Current Dir" : "target directory" + targetDir   } is not empty. Remove existing files and continue?`, }, {   type: (_, { overwrite }: { overwrite?: boolean }) => {     console.log(overwrite);     if (overwrite === false) {        // 用户选择 X ,抛出错误退出。       throw new Error(red("✖") + " Operation cancelled");     }     return null;   },   name: "overwriteChecker", }, 复制代码

packageName

由于package.json的name字段有命名要求:

The name field contains your package's name, and must be lowercase and one word, and may contain hyphens and underscores.

所以需要对于不符合条件的projectName,我们需要提示用户重新输入一个项目名称:

{   type: () => (isValidPackageName(getProjectName()) ? null : 'text'),   name: 'packageName',   message: reset('Package name:'),   initial: () => toValidPackageName(getProjectName()),   validate: (dir) =>     isValidPackageName(dir) || 'Invalid package.json name' }, 复制代码

framework

最后一步就是选择模板了,如果用户在初始化输入中输入了正确的模板名,则当前步骤则会跳过,直接进入到模板生成的步骤。否则会提示用户进行模板选择:

{   type:     argTemplate && TEMPLATES.includes(argTemplate) ? null : 'select',   name: 'framework',   message:     typeof argTemplate === 'string' && !TEMPLATES.includes(argTemplate)       ? reset(           `"${argTemplate}" 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.display || framework.name),       value: framework     }   }) }, {   type: (framework: Framework) =>     framework && framework.variants ? 'select' : null,   name: 'variant',   message: reset('Select a variant:'),   choices: (framework: Framework) =>     framework.variants.map((variant) => {       const variantColor = variant.color       return {         title: variantColor(variant.display || variant.name),         value: variant.name       }     }) } ], 复制代码

模板构建

当用户交互完成后,脚手架工具的输入阶段就算告一段落了,目前拿到的字段有:targetDir,template, framework, overwrite, packageName, variant,接下来就要根据这些参数进行模板的构建工作,具体的步骤如下,大家可以依照代码进行对应:

  • 创建文件夹 | 清空文件夹

const root = path.join(cwd, targetDir) if (overwrite) {     emptyDir(root) } else if (!fs.existsSync(root)) {     fs.mkdirSync(root, { recursive: true }) } 复制代码

  • 获取项目模板

由于使用了多个输入方式进行模板选择(初始输入,命令行选择),故需要最终确认模板名是什么:

const template: string = variant || framework?.name || argTemplate 复制代码

  • 获取用户包管理器名称、版本

由于接下来可能会执行一些命令,所以需要提前获取下用户环境下的包管理器,这里使用的是process.env.npm_config_user_agent,这个是node的环境变量,可以直接获取到当前的包管理器类型及node版本号:

const pkgInfo = pkgFromUserAgent(process.env.npm_config_user_agent) 复制代码

pkgFromUserAgent最终返回的是一个含有name和version的对象,即包管理器名称和版本号。

  • 判断模板是否存在customCommand,如果存在则运行命令,并退出

这里主要就是根据前面获取的包管理器类型对customCommand进行修改,以适应用户的环境。

customCommand命令为npm create vue@latest TARGET_DIR, 那么若pkgInfoname字段是pnpm,用户输入的TARGET_DIRvite-app,则修改后的命令为:pnpm create vue@latest vite-app

命令执行完毕后,这里分支的任务就完成了,所以调用process.exit(status ?? 0)返回执行结果并退出。

  • 项目生成

    如果不存在customCommand命令,则会进行项目生成,主要步骤如下,这里不再进行代码分析:

    • 根据模板名找到模板路径,将模板文件迁移到projectName所在的文件夹

    • 更新模板package.json中的name字段为packageName||projectName

    • 引导用户进入项目目录、安装依赖、启动项目。

总结

create-vite的源码相对来说还是比较简单易读的,我也在阅读的过程中学到的不少知识,除此之外,我还在create-vite的基础上进行修改,改造了一款kakachake/create-want脚手架,欢迎大家使用npx create-want-app进行试用,它可以检测template文件夹下的模板,自动生成上述提到的Framework对象供开发者进行模板选择,并且可以使用配置文件增加自定义配置:

image.png

image.png

image.png

目前可以支持vitereact官方的项目搭建,实际上可以成为一个脚手架模板的整合,其他开发者也可以用提pr的方式上传自己的模板到create-want-apptemplate目录下,遵守命名规范即可。


作者:叁十四城
链接:https://juejin.cn/post/7170528293345558558


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐