前言

上一节我们说到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

namehellooptions{ default: true }。执行create函数主要执行以下流程:

基础校验

在这一阶段,主要使用了两个npm包:

  • inquirer: 命令可视化选择界面;
  • validate-npm-package-name :校验项目名称。
  1. 先获得基本参数:

    // 执行命令的当前目录路径
    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 || '.')
    
  2. 校验项目名称:

    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格标准。

  3. 如果已经存在这个目录,则会:

    • 如果是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)}}}}
    
  4. 创建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: 需要安装的依赖包;
  • vueVersionvue版本;
  • 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命令相关推荐

  1. vue cli3源码解析

    vue-cli3 源码解析 脚手架代码入口点 从package.json文件中可以看到"vue-cli-service": "bin/vue-cli-service.js ...

  2. FileZilla Server源码解析之LIST命令

    FileZilla Server源码解析之LIST命令 如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 FileZilla版本:Fil ...

  3. 【Vue.js源码解析 一】-- 响应式原理

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 课程目标 Vue.js 的静态成员和实例成员初始化过程 首次渲染的过程 数据响应式原理 – 最核心的特性之一 准备工作 ...

  4. js define函数_不夸张,这真的是前端圈宝藏书!360前端工程师Vue.js源码解析

    优秀源代码背后的思想是永恒的.普适的. 这些年来,前端行业一直在飞速发展.行业的进步,导致对从业人员的要求不断攀升.放眼未来,虽然仅仅会用某些框架还可以找到工作,但仅仅满足于会用,一定无法走得更远.随 ...

  5. 【Vue.js源码解析 三】-- 模板编译和组件化

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 模板编译 模板编译的主要目的是将模板 (template) 转换为渲染函数 (render) <div> ...

  6. 史上最全的vue.js源码解析(四)

    虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下.vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到.所以我近期会对vue.js的源码进行解读,分享值得去学习 ...

  7. Samba 源码解析之SMBclient命令流

    smbclient提供了类似FTP式的共享文件操作功能, 本篇从源码角度讲解smbclient的实现,smbclient命令的具体使用可通过help命令和互联网查到大量资料. 以下从源码角度分析一个s ...

  8. 【Vue.js源码解析 二】-- 虚拟 DOM

    前言 笔记来源:拉勾教育 大前端高薪训练营 阅读建议:建议通过左侧导航栏进行阅读 虚拟 DOM 基本介绍 什么是虚拟 DOM 虚拟 DOM(Virtual DOM) 是使用 JavaScript 对象 ...

  9. 【Vue3】源码解析

    [Vue3]源码解析 首先得知道 Proxy Reflect Symbol Map和Set diff算法 patchChildren diff算法具体做了什么(重点)? patchKeyedChild ...

  10. Redis源码解析(15) 哨兵机制[2] 信息同步与TILT模式

    Redis源码解析(1) 动态字符串与链表 Redis源码解析(2) 字典与迭代器 Redis源码解析(3) 跳跃表 Redis源码解析(4) 整数集合 Redis源码解析(5) 压缩列表 Redis ...

最新文章

  1. 6月机器学习热文TOP10,精选自1400篇文章
  2. redis设置允许远程访问
  3. 牛听听 总是获取音频流出错_【伤感听听|推荐】大度 什么
  4. java 上下文加载器_如何将JDK6 ToolProvider和JavaCompiler与上下文类加载器一起使用?...
  5. YTKNetwork源码详解
  6. 数据结构之:链表详解
  7. Launch debug in SWI1 workflow
  8. svn 合并和树冲突
  9. 设备怎样开启位置服务器,开启设备服务器
  10. 微课|中学生可以这样学Python(例8.21):选择法排序
  11. Myeclipse 使用JUnit 进行单元测试
  12. 自建latex服务器,通过在线服务器编译LaTeX
  13. c语言输入字符串_我们一起学C语言(四)
  14. java.lang.NoClassDefFoundError: org/springframework/dao/support/DaoSupport ...
  15. Debian10上使用360随身Wifi
  16. Unity 托管内存(Managed Memory)
  17. office365 onedrive 教育版市场价位分析选购指南
  18. mc服务器天赋系统,我的世界战斗狂人的最爱Mod,天赋系统乱入,玩家发展不受限制...
  19. C++ 实用趣味小程序
  20. DbVisualizer 9 解决中文乱码问题(win7,win10)

热门文章

  1. 期权和期货的差别有哪些?
  2. 整数分解为若干项之和 递归思想
  3. 点击下载链接弹出空白页面
  4. Win10桌面我的电脑等图标设置
  5. mysql 临时表 with_[Mysql] mysql临时表corrupt
  6. Git入门和web前端初窥
  7. element-ui表单input输入框获取自动聚焦功能
  8. [推测]百度搜索结果右侧相关企业的展现
  9. 云服务器与普通服务器有哪些区别?
  10. 售价高达4999元,YVR 2一体机正式发布