vue-cli
是 vue 官方出品的脚手架项目,用来快速搭建vue
项目,一键生成项目基础代码。
一直对其内部的运行原理比较好奇,并且开发脚手架的能力也是一名前端开发人员需要掌握的技能,所以这里以v4.5.15
版本为例,记录一下源码研究的过程和学习点。
入口
vue-cli
采用的仍然是通过lerna
管理的 monorepo,不知道未来会不会迁移到 pnpm,从package.json
可以看到核心仓库就是以下三个,其中@vue
就是一些核心代码,包括脚手架@vue/cli
、vue-cli-service
以及一些插件。
vue-cli
├─ docs // 文档
├─ packages
│ ├─ @vue
│ │ ├─ babel-preset-app
│ │ ├─ cli // @vue/cli
│ │ ├─ cli-init
│ │ ├─ cli-overlay
│ │ ├─ cli-plugin-babel
│ │ ├─ cli-plugin-e2e-cypress
│ │ ├─ cli-plugin-e2e-nightwatch
│ │ ├─ cli-plugin-e2e-webdriverio
│ │ ├─ cli-plugin-eslint
│ │ ├─ cli-plugin-pwa
│ │ ├─ cli-plugin-router
│ │ ├─ cli-plugin-typescript
│ │ ├─ cli-plugin-unit-jest
│ │ ├─ cli-plugin-unit-mocha
│ │ ├─ cli-plugin-vuex
│ │ ├─ cli-service // vue-cli-service
│ │ ├─ cli-shared-utils
│ │ ├─ cli-test-utils // 一些第三库的源代码,或者一些工具函数等
│ │ ├─ cli-ui
│ │ ├─ cli-ui-addon-webpack
│ │ └─ cli-ui-addon-widgets
├─ scripts // 一些管理monorepo的程序
@vue/cli
先从脚手架启动引导程序@vue/cli
开始分析,从package.json
注册的bin
属性找到入口程序为bin/vue.js
.
{
"bin": {
"vue": "bin/vue.js"
},
}
检查Nodejs版本
首先会通过semver
这个库检查使用者本地 Nodejs 的版本和在@vue/cli
的package.json
下要求的版本是否符合。
这里简单了解下 Nodejs 的版本管理,Nodejs 版本有以下几种:
CURRENT
:当前状态,也就是当前最新的 Nodejs 版本,ACTIVE
版本的 Nodejs 会维护持续 6 个月时间,6 个月之后奇数版本会不再维护,而偶数版本会变成ACTIVE
状态的LTS
版本ACTIVE
:活跃状态,是正在积极维护和升级的版本,包括一些 BUG 修复,功能改进等MAINTENANCE
:维护状态,只修复 BUG,维护时间不定EOL
:End of Life,也就是终止维护的版本LTS
:long-term support,也就是长期维护版本,这意味着重大的 Bug 将在后续的 30 个月内持续得到不断地修复。
如下图所示,Nodejs 12 已进入维护状态,并且在 2022 年 4 月份就会终止维护,到时候Nodejs 18 也会发布,Nodejs 17 也会终止维护。 反正一个原则是始终用 LTS 版本就行了。可以使用nvm
便捷的管理 Nodejs 的版本。
注册命令
然后使用commanderjs
注册命令create
,并且包含必填的app-name
参数,可以看到create
注册以后,会去加载上层lib
下的create.js
程序,这里还传递了项目名称和额外的 CLI 参数。
校验项目名称
create.js
内部首先会通过validate-npm-package-name
这个库去校验项目名称,并且会额外处理使用.
作为当前目录的情况,考虑的非常周到。
如果通过 CLI 指定--merge
,则会清空目标文件夹,否则会使用inquirer
在 CLI 发起选项选择是否合并目录文件或者选择清空。这里使用了fs-extra
来操作文件系统。
初始化Creator实例
在确认了目标文件夹以后,会初始化Creator
的实例,并传递 CLI 参数来调用实例的create
方法。
这里传递了三个初始化参数:
name
:项目名称targetDir
:创建项目的文件夹getPromptModules()
:加载位于上层promptModules
文件夹下的一些函数
位于promptModules
下的都是一些用户在创建项目时手动选择的功能项,例如vue
的版本,是否使用 TS,CSS预处理器等。
以../promptModules/babel.js
为例,可以看到其内部是一个函数,接收一个cli
对象,并且调用了cli
对象暴露的injectFeature
和onPromptComplete
这两个方法。
获取prompt选项
在Creator
的构造函数内部会初始化一些实例属性:
name
:项目名称context
:当前创建的目录presetPrompt
:preset
的选项;通过resolveIntroPrompts
获取preset
信息,其中包括加载用户创建的保存在本地.vuerc
下的preset
,以及@vue/cli
内置默认的preset
信息,然后组合成inquirer
选项参数保存在这个属性下
默认的preset
只有两个:选择vue2
和vue3
的版本,并且都会包含@vue/cli-plugin-babel
和@vue/cli-plugin-eslint
两个plugin
featurePrompt
:如果用户选择不使用任何preset
而是手动选择一些功能来创建项目,例如是否使用 TS,选择 CSS 预处理器等,那么就会从featurePrompt
加载一些选项
outroPrompts
:主要是通过inquirer
指定的一些选项,例如保存配置文件的方式、选择使用的依赖管理工具npm,yarn,pnpm等
之后会通过传入当前实例对象this
来初始化一个PromptModuleAPI
的实例。
PromptModuleAPI
内部会往自身的实例上挂载一个creator
对象,也就是Creator
的实例,这样在PromptModuleAPI
内部的实例就可以通过creator
对象访问Creator
的实例内部的属性和方法。
这时候初始化Creator
实例时传入的promptModules
内部的每个函数就会被调用,并传入PromptModuleAPI
的实例作为参数,每个函数内部通过调用PromptModuleAPI
内部的方法再向Creator
实例内部的属性注入自身的prompt
选项,不得不说逻辑有点绕,但是提高了程序的拓展性,以后@vue/cli
内部想拓展一些功能,只需要在promptModules
文件夹下编写程序即可。
injectFeature
:往实例的featurePrompt.choices
推入一些feature
injectPrompt
:往实例的injectedPrompts
推入一些prompt
injectOptionForPrompt
:往injectedPrompts.choices
推入一些选项onPromptComplete
:往实例的promptCompleteCbs
推入回调函数,在prompt
执行完以后执行
组合prompt并获取preset
初始化Creator
实例以后会调用create
方法,接收 CLI 命令行指定的所有参数,在使用vue create xx
命令没有指定任何其他参数的情况下会进入promptAndResolvePreset
方法。promptAndResolvePreset
内部主要做了两件事:
- 合并
Creator
实例的presetPrompt
,featurePrompt
,injectedPrompts
以及outroPrompts
这些选项,并且默认的选项presetPrompt
作为第一个,后续的prompt
会通过inquirer
的when
函数来判断其是否需要执行,如果用户选择不使用preset
,那么才会执行后续手动选择feature
的部分
- 当用户选择了
preset
以后,就会通过resolvePreset
再次获取preset
的信息
注入vue-cli-service
vue-cli-service
在这里也是作为一个plugin
,上文获取preset
信息以后,会在其默认plugins
的基础上再注册vue-cli-service
生成package.json
获取所有plugin
以后,会创建package.json
对象,写入plugin
的版本,并生成文件
初始化git
如果通过 CLI 指定初始化 git,这里还会调用git init
命令,初始化 git 本地存储服务。
安装依赖
@vue/cli
内部定义了依赖管理基类PackageManager
,内部会判断客户端使用的依赖管理工具,使用的源地址等信息,代码很多,就不一一展开了。这里安装的依赖从前面来看主要有三个:
@vue/cli-plugin-babel
@vue/cli-plugin-eslint
@vue/cli-service