【vue-cli3源码解析】02_vue create命令
前言
上一节我们说到vue create
最后会走入lib/create
文件,这一节我们需要研究这个文件。
配置项
在研究之前,先确定vue create
的配置项:
program.command('create <app-name>').description('create a new project powered by vue-cli-service')// 跳过提示并使用已保存或远程预置.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')// 跳过提示并使用默认预设.option('-d, --default', 'Skip prompts and use default preset')// 跳过提示并使用内联JSON字符串作为预设.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')// 安装依赖时使用指定的npm客户端.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')// 在安装依赖项时使用指定的npm注册表(仅针对npm).option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')// 使用初始化提交消息强制git初始化.option('-g, --git [message]', 'Force git initialization with initial commit message')// 跳过git初始化.option('-n, --no-git', 'Skip git initialization')// 如果目标目录存在,覆盖目标目录.option('-f, --force', 'Overwrite target directory if it exists')// 如果目标目录存在,合并目标目录.option('--merge', 'Merge target directory if it exists')// 获取远程预设, 使用git克隆时.option('-c, --clone', 'Use git clone when fetching remote preset')// 创建项目时使用指定的代理.option('-x, --proxy <proxyUrl>', 'Use specified proxy when creating project')// 创建项目时省略默认组件中的新手指导信息.option('-b, --bare', 'Scaffold project without beginner instructions')// 跳过显示Get started说明.option('--skipGetStarted', 'Skip displaying "Get started" instructions').action((name, options) => {if (minimist(process.argv.slice(3))._.length > 1) {console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))}// --git makes commander to default git to trueif (process.argv.includes('-g') || process.argv.includes('--git')) {options.forceGit = true}require('../lib/create')(name, options)})
执行vue create
时需要填入app-name
项目名称,否则就会报错,之后还可以携带参数。
整体流程
lib/create
主要导出了create
函数。其中create
函数接收两个参数,一个是项目名称,一个是配置项,比如输入:
vue create hello -d
则name
为hello
, options
为{ default: true }
。执行create
函数主要执行以下流程:
基础校验
在这一阶段,主要使用了两个npm
包:
inquirer
: 命令可视化选择界面;validate-npm-package-name
:校验项目名称。
先获得基本参数:
// 执行命令的当前目录路径 const cwd = options.cwd || process.cwd() // 是否是在当前目录,即如果输入的项目名称是., 则表示在当前目录创建Vue项目 const inCurrent = projectName === '.' // 获得项目名称,如果vue create 后加的是. ,则表示当前项目名称为这个文件夹的名字 const name = inCurrent ? path.relative('../', cwd) : projectName // 生成项目的目录地址 const targetDir = path.resolve(cwd, projectName || '.')
校验项目名称:
const result = validateProjectName(name) if (!result.validForNewPackages) {console.error(chalk.red(`Invalid project name: "${name}"`)) result.errors && result.errors.forEach(err => {console.error(chalk.red.dim('Error: ' + err)) }) result.warnings && result.warnings.forEach(warn => {console.error(chalk.red.dim('Warning: ' + warn)) }) exit(1) }
validateProjectName
即使用了validate-npm-package-name
这个Npm包,会告诉你项目名称是否符合npm package
格标准。如果已经存在这个目录,则会:
- 如果是
vue create xxx -f
,则先删除这个目录; - 否则,出现
inquirer
,供用户选择是重写、覆盖还是取消创建。
if (fs.existsSync(targetDir) && !options.merge) {if (options.force) {await fs.remove(targetDir)} else {await clearConsole()if (inCurrent) {const { ok } = await inquirer.prompt([{name: 'ok',type: 'confirm',message: `Generate project in current directory?`}])if (!ok) {return}} else {const { action } = await inquirer.prompt([{name: 'action',type: 'list',message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,choices: [{ name: 'Overwrite', value: 'overwrite' },{ name: 'Merge', value: 'merge' },{ name: 'Cancel', value: false }]}])if (!action) {return} else if (action === 'overwrite') {console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)await fs.remove(targetDir)}}}}
- 如果是
创建
Creator
并调用create
方法:// const Creator = require('./Creator') const creator = new Creator(name, targetDir, getPromptModules()) await creator.create(options)
其中讲解一下
getPromptModules
方法。它的源码如下:exports.getPromptModules = () => {return ['vueVersion','babel','typescript','pwa','router','vuex','cssPreprocessors','linter','unit','e2e' ].map(file => require(`../promptModules/${file}`))
所以我们在
new Creator
时,把以上的内容传递给了它的构造函数。再细看promptModules
文件夹下的文件,不难发现这些就是在创建vue
项目时可以选择的配置项。在
vue-cli
源码中使用到了 Inquirer这个包,它可以为node命令提供好看的操作界面,下面这些就是用这个包开发的:以
promptModules/vueVersion.js
为例,每个这样的文件都导出了一个函数,函数的参数名为cli
。这个cli
是个对象,有多种方法:injectFeature
新增特征,injectPrompt
是特征对应的选项,onPromptComplete
用户选择之后需要做的操作,比如保存用户的选项、增加对应的插件等。injectFeature
会新增一个vueVersion
变量来保存用户选择的vue
版本;而injectPrompt
则会提供vue
版本可以选择的值:vue2
还是vue3
module.exports = cli => {cli.injectFeature(// 新增特征,比如vue版本、eslint、vuex等)cli.injectPrompt(// 如果选择了某个特征,则提供该特征可以选择的值// 如需要安装vue,则会循环是安装2版本还是3版本)cli.onPromptComplete((answers, options) => {// 保存用户安装的Vue版本号})}
获取预设选项
上面说到了,调用了Creator
创建了一个实例:const creator = new Creator(name, targetDir, getPromptModules())
。
name
:项目名称;targetDir
: 构建项目的目录,比如xxxxx/projectName
;getPromptModules()
:导入各个模块的函数。
Creator构造函数
Creator
的构造函数如下:
constructor (name, context, promptModules) {super()// 项目名称this.name = name// 创建项目的路径this.context = process.env.VUE_CLI_CONTEXT = contextconst { presetPrompt, featurePrompt } = this.resolveIntroPrompts()// ...}
resolveIntroPrompts
resolveIntroPrompts
这个函数主要做了几个工作:
获得预设:用户使用
vue-cli
创建项目时,有时候会自定义预设并保存,这时候这些配置会被保存再一个叫做.vuerc
的文件中。保存的配置一般是这个样子(预设名字为HH
):返回
presetPrompt
:询问用户是选择vue-cli2默认配置项、vue-cli3默认配置项、还是默认自定义配置:返回
featurePrompt
:如果选择的是自定义配置,就会返回一个多选的配置项列表:(但此时还是一个空列表,因为这个列表要根据是不是选择自定义才会显示)
resolveOutroPrompts
继续回到构造函数:
constructor (name, context, promptModules) {super()// 项目名称this.name = name// 创建项目的路径this.context = process.env.VUE_CLI_CONTEXT = context// 获得 presetPrompt, featurePromptconst { presetPrompt, featurePrompt } = this.resolveIntroPrompts()this.presetPrompt = presetPromptthis.featurePrompt = featurePromptthis.outroPrompts = this.resolveOutroPrompts()// ...}
resolveOutroPrompts
这个函数主要是返回了几个询问用户问题的’弹窗’:
- 你是想将自定义的配置项的文件存放在
config.js
还是package.json
(Where do you prefer placing config for Babel, ESLint, etc.?); - 是否存放这些预设,如果是的话输入预设名;
- 如果有
npm
之外的下载依赖方式,询问是使用npm
还是yarn
等。
PromptModuleAPI
先图解一下特征和特征对应的弹窗选项:
constructor (name, context, promptModules) {super()// 项目名称this.name = name// 创建项目的路径this.context = process.env.VUE_CLI_CONTEXT = context// 获得 presetPrompt, featurePromptconst { presetPrompt, featurePrompt } = this.resolveIntroPrompts()this.presetPrompt = presetPromptthis.featurePrompt = featurePrompt// 获得选择之后的后续弹窗this.outroPrompts = this.resolveOutroPrompts()this.injectedPrompts = []this.promptCompleteCbs = []this.afterInvokeCbs = []this.afterAnyInvokeCbs = []// 绑定thisthis.run = this.run.bind(this)// 创建PromptModuleAPIconst promptAPI = new PromptModuleAPI(this)// 将各个模块的特征存进featurePrompt,// 特征对应的弹窗存进injectedPrompts// 弹窗选择之后对应的回调函数存入promptCompleteCbspromptModules.forEach(m => m(promptAPI))}
PromptModuleAPI
是一个类主要含有以下内容:
constructor
:将this
赋值给当前的create
;injectFeature
:新增特征,将各个特征存入featurePrompt
;injectPrompt
:特征对应的选择弹窗选项,将各个特征对应的弹窗选择存入injectedPrompts
;injectOptionForPrompt
:根据条件返回某些弹窗,即如果选择了ESLint
,则返回ESLint
对应的弹窗;onPromptComplete
:用户选择某个选择之后存入对应的回调函数,将回调函数存入promptCompleteCbs
中。
所以在Creator
中有几个变量容易混淆:
presetPrompt
:预设弹窗,询问用户是vue-cli2、vue-cli3还是自定义;featurePrompt
:特征弹窗,选择vue的版本、自定义哪些配置等(可以理解成问题);outroPrompts
: 询问用户配置项文件存在哪里、是否保存自定义配置项、选择哪种方式下载依赖包;injectedPrompts
:每个特征对应的选项弹窗,比如ESlint
有这几个版本;promptCompleteCbs
:你选择之后就调用的函数。
到这里new Creator()
就分析完了。然后就开始调用Creator
中的create
方法了:
await creator.create(options)
preset对象
在create
中有一个preset
对象,先来看一下preset
一般长什么样子:
- 使用vue2/3 默认预设时:
- 自定义预设时:
preset
一般有几个属性:
useConfigFiles
: 有些配置(ESLint
等是否要重新写在另外的文件而不是package.json
中);plugins
: 需要安装的依赖包;vueVersion
:vue
版本;cssPreprocessor
:使用哪种css
预处理语言。
获得用户选择的preset
async create (cliOptions = {}, preset = null) {const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUGconst { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this// 由于await creator.create(options),所以preset是nullif (!preset) {if (cliOptions.preset) {// vue create foo --preset bar 使用上一次存储的preset时preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)} else if (cliOptions.default) {// vue create foo --default 使用默认presetpreset = defaults.presets.default} else if (cliOptions.inlinePreset) {// vue create foo --inlinePreset {...} 使用内联JSON字符串作为预设try {preset = JSON.parse(cliOptions.inlinePreset)} catch (e) {error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)exit(1)}} else {preset = await this.promptAndResolvePreset()}}// ...
}
resolvePreset
当vue create
携带-p
或者--preset
时,会调用resolvePreset
async resolvePreset (name, clone) {let preset// 获得预设文件~/.vuerc中的所有预设const savedPresets = this.getPresets()if (name in savedPresets) {// 如果该预设存在,则返回preset = savedPresets[name]} else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {// 是否是采用内联的 JSON 字符串预设选项,如果是就会解析 .json 文件preset = await loadLocalPreset(path.resolve(name))} else if (name.includes('/')) {// 利用download-git-repo从远程获取 presetlog(`Fetching remote preset ${chalk.cyan(name)}...`)this.emit('creation', { event: 'fetch-remote-preset' })try {preset = await loadRemotePreset(name, clone)} catch (e) {error(`Failed fetching remote preset ${chalk.cyan(name)}:`)throw e}}if (!preset) {error(`preset "${name}" not found.`)const presets = Object.keys(savedPresets)if (presets.length) {log()log(`available presets:\n${presets.join(`\n`)}`)} else {log(`you don't seem to have any saved preset.`)log(`run vue-cli in manual mode to create a preset.`)}exit(1)}return preset}
promptAndResolvePreset
当如果vue create
不携带-p
或者--preset
时,则走:preset = await this.promptAndResolvePreset()
。
async promptAndResolvePreset (answers = null) {// 这里由于是空,所以会调用inquirer.prompt弹出弹窗if (!answers) {await clearConsole(true)// resolveFinalPrompts将之前的presetPrompt、featurePrompt、injectedPrompts和outroPrompts合并,形成完整的弹窗逻辑answers = await inquirer.prompt(this.resolveFinalPrompts())}debug('vue-cli:answers')(answers)if (answers.packageManager) {// 用户操作完毕之后,将答案记录下来saveOptions({packageManager: answers.packageManager})}let preset// 如果选择了默认或者自定义preset,则调用resolvePreset返回presetif (answers.preset && answers.preset !== '__manual__') {preset = await this.resolvePreset(answers.preset)} else {// manualpreset = {useConfigFiles: answers.useConfigFiles === 'files',plugins: {}}answers.features = answers.features || []// 前面说到了,选择了哪些模块之后,就会存入相应的回调函数,在这里将会执行这些函数this.promptCompleteCbs.forEach(cb => cb(answers, preset))}// 校验preset对不对validatePreset(preset)// 如果选择了保存本次预设,则存储该预设if (answers.save && answers.saveName && savePreset(answers.saveName, preset)) {log()log(`
【vue-cli3源码解析】02_vue create命令相关推荐
- vue cli3源码解析
vue-cli3 源码解析 脚手架代码入口点 从package.json文件中可以看到"vue-cli-service": "bin/vue-cli-service.js ...
- FileZilla Server源码解析之LIST命令
FileZilla Server源码解析之LIST命令 如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 FileZilla版本:Fil ...
- 【Vue.js源码解析 一】-- 响应式原理
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 课程目标 Vue.js 的静态成员和实例成员初始化过程 首次渲染的过程 数据响应式原理 – 最核心的特性之一 准备工作 ...
- js define函数_不夸张,这真的是前端圈宝藏书!360前端工程师Vue.js源码解析
优秀源代码背后的思想是永恒的.普适的. 这些年来,前端行业一直在飞速发展.行业的进步,导致对从业人员的要求不断攀升.放眼未来,虽然仅仅会用某些框架还可以找到工作,但仅仅满足于会用,一定无法走得更远.随 ...
- 【Vue.js源码解析 三】-- 模板编译和组件化
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 模板编译 模板编译的主要目的是将模板 (template) 转换为渲染函数 (render) <div> ...
- 史上最全的vue.js源码解析(四)
虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下.vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到.所以我近期会对vue.js的源码进行解读,分享值得去学习 ...
- Samba 源码解析之SMBclient命令流
smbclient提供了类似FTP式的共享文件操作功能, 本篇从源码角度讲解smbclient的实现,smbclient命令的具体使用可通过help命令和互联网查到大量资料. 以下从源码角度分析一个s ...
- 【Vue.js源码解析 二】-- 虚拟 DOM
前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 虚拟 DOM 基本介绍 什么是虚拟 DOM 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象 ...
- 【Vue3】源码解析
[Vue3]源码解析 首先得知道 Proxy Reflect Symbol Map和Set diff算法 patchChildren diff算法具体做了什么(重点)? patchKeyedChild ...
- Redis源码解析(15) 哨兵机制[2] 信息同步与TILT模式
Redis源码解析(1) 动态字符串与链表 Redis源码解析(2) 字典与迭代器 Redis源码解析(3) 跳跃表 Redis源码解析(4) 整数集合 Redis源码解析(5) 压缩列表 Redis ...
最新文章
- 6月机器学习热文TOP10,精选自1400篇文章
- redis设置允许远程访问
- 牛听听 总是获取音频流出错_【伤感听听|推荐】大度 什么
- java 上下文加载器_如何将JDK6 ToolProvider和JavaCompiler与上下文类加载器一起使用?...
- YTKNetwork源码详解
- 数据结构之:链表详解
- Launch debug in SWI1 workflow
- svn 合并和树冲突
- 设备怎样开启位置服务器,开启设备服务器
- 微课|中学生可以这样学Python(例8.21):选择法排序
- Myeclipse 使用JUnit 进行单元测试
- 自建latex服务器,通过在线服务器编译LaTeX
- c语言输入字符串_我们一起学C语言(四)
- java.lang.NoClassDefFoundError: org/springframework/dao/support/DaoSupport ...
- Debian10上使用360随身Wifi
- Unity 托管内存(Managed Memory)
- office365 onedrive 教育版市场价位分析选购指南
- mc服务器天赋系统,我的世界战斗狂人的最爱Mod,天赋系统乱入,玩家发展不受限制...
- C++ 实用趣味小程序
- DbVisualizer 9 解决中文乱码问题(win7,win10)
热门文章