关于前端:Vue-CLI-是如何实现的-终端命令行工具篇

42次阅读

共计 9473 个字符,预计需要花费 24 分钟才能阅读完成。

Vue CLI 是一个基于 Vue.js 进行疾速开发的残缺零碎,提供了终端命令行工具、零配置脚手架、插件体系、图形化治理界面等。本文暂且只剖析我的项目初始化局部,也就是终端命令行工具的实现。

  1. 用法

用法很简略,每个 CLI 都大同小异:

npm install -g @vue/cli

vue create vue-cli-test

目前 Vue CLI 同时反对 Vue 2 和 Vue 3 我的项目的创立(默认配置)。

下面是 Vue CLI 提供的默认配置,能够疾速地创立一个我的项目。除此之外,也能够依据本人的我的项目需要(是否应用 Babel、是否应用 TS 等)来自定义我的项目工程配置,这样会更加的灵便。看到 Successfully 就是我的项目初始化胜利了。

vue create  命令反对一些参数站长博客配置,能够通过 vue create –help  获取具体的文档:

用法:create [options] <app-name>

选项:

-p, –preset <presetName>       疏忽提示符并应用已保留的或近程的预设选项

-d, –default                   疏忽提示符并应用默认预设选项

-i, –inlinePreset <json>       疏忽提示符并应用内联的 JSON 字符串预设选项

-m, –packageManager <command>  在装置依赖时应用指定的 npm 客户端

-r, –registry <url>            在装置依赖时应用指定的 npm registry

-g, –git [message]             强制 / 跳过 git 初始化,并可选的指定初始化提交信息

-n, –no-git                    跳过 git 初始化

-f, –force                     覆写目标目录可能存在的配置

-c, –clone                     应用 git clone 获取近程预设选项

-x, –proxy                     应用指定的代理创立我的项目

-b, –bare                      创立我的项目时省略默认组件中的老手领导信息

-h, –help                      输入应用帮忙信息

具体的用法大家感兴趣的能够尝试一下,这里就不开展了,后续在源码剖析中会有相应的局部提到。

  1. 入口文件

本文中的 vue cli 版本为 4.5.9。若浏览本文时存在 break change,可能就须要本人了解一下啦

依照失常逻辑,咱们在 package.json 里找到了入口文件:

{

“bin”: {

“vue”: “bin/vue.js”

}

}

bin/vue.js 里的代码不少,无非就是在 vue  上注册了 create / add / ui  等命令,本文只剖析 create  局部,找到这部分代码(删除主流程无关的代码后):

// 查看 node 版本

checkNodeVersion(requiredVersion, ‘@vue/cli’);

// 挂载 create 命令

program.command(‘create <app-name>’).action((name, cmd) => {

// 获取额定参数

const options = cleanArgs(cmd);

// 执行 create 办法

require(‘../lib/create’)(name, options);

});

cleanArgs  是获取 vue create  前面通过 –  传入的参数,通过 vue create –help 能够获取执行的参数列表。

获取参数之后就是执行真正的 create  办法了,等等认真开展。

不得不说,Vue CLI 对于代码模块的治理十分细,每个模块基本上都是繁多功能模块,能够任意地拼装和应用。每个文件的代码行数也都不会很多,浏览起来十分难受。

  1. 输出命令有误,猜想用户用意

Vue CLI 中比拟有意思的一个中央,如果用户在终端中输出 vue creat xxx  而不是 vue create xxx,会怎么样呢?实践上应该是报错了。

如果只是报错,那我就不提了。看看后果:

终端上输入了一行很要害的信息 Did you mean create,Vue CLI 仿佛晓得用户是想应用 create  然而手速太快打错单词了。

这是如何做到的呢?咱们在源代码中寻找答案:

const leven = require(‘leven’);

// 如果不是以后已挂载的命令,会猜想用户用意

program.arguments(‘<command>’).action(cmd => {

suggestCommands(cmd);

});

// 猜想用户用意 function suggestCommands(unknownCommand) {

const availableCommands = program.commands.map(cmd => cmd._name);

let suggestion;

availableCommands.forEach(cmd => {

const isBestMatch =

leven(cmd, unknownCommand) < leven(suggestion || ”, unknownCommand);

if (leven(cmd, unknownCommand) < 3 && isBestMatch) {

suggestion = cmd;

}

});

if (suggestion) {

console.log(   + chalk.red(Did you mean ${chalk.yellow(suggestion)}?));

}

}

代码中应用了 leven 了这个包,这是用于计算字符串编辑间隔算法的 JS 实现,Vue CLI 这里应用了这个包,来别离计算输出的命令和以后已挂载的所有命令的编辑举例,从而猜想用户理论想输出的命令是哪个。

小而美的一个性能,用户体验极大晋升。

  1. Node 版本相关检查

3.1 Node 冀望版本

和 create-react-app  相似,Vue CLI 也是先查看了一下以后 Node 版本是否符合要求:

以后 Node 版本:process.version

冀望的 Node 版本:require(“../package.json”).engines.node

比方我目前在用的是 Node v10.20.1 而 @vue/cli 4.5.9  要求的 Node 版本是 >=8.9,所以是符合要求的。

3.2 举荐 Node LTS 版本

在 bin/vue.js  中有这样一段代码,看上去也是在查看 Node 版本:

const EOL_NODE_MAJORS = [‘8.x’, ‘9.x’, ’11.x’, ’13.x’];for (const major of EOL_NODE_MAJORS) {

if (semver.satisfies(process.version, major)) {

console.log(

chalk.red(

You are using Node ${process.version}.n +

Node.js ${major} has already reached end-of-life and will not be supported in future major releases.n +

It's strongly recommended to use an active LTS version instead.

)

);

}

}

可能并不是所有人都理解它的作用,在这里略微科普一下。

简略来说,Node 的主版本分为奇数版本和偶数版本。每个版本公布之后会继续六个月的工夫,六个月之后,奇数版本将变为 EOL 状态,而偶数版本变为 Active LTS 状态并且长期反对。所以咱们在生产环境应用 Node 的时候,应该尽量应用它的 LTS 版本,而不是 EOL 的版本。

EOL 版本:A End-Of-Life version of Node LTS 版本: A long-term supported version of Node

这是目前常见的 Node 版本的一个状况:

解释一下图中几个状态:

CURRENT:会修复 bug,减少新个性,一直改善

ACTIVE:长期稳固版本

MAINTENANCE:只会修复 bug,不会再有新的个性减少

EOL:当进度条走完,这个版本也就不再保护和反对了

通过下面那张图,咱们能够看到,Node 8.x 在 2020 年曾经 EOL,Node 12.x 在 2021 年的时候也会进入 MAINTENANCE 状态,而 Node 10.x 在 2021 年 4、5 月的时候就会变成 EOL。

Vue CLI 中对以后的 Node 版本进行判断,如果你用的是 EOL 版本,会举荐你应用 LTS 版本。也就是说,在不久之后,这里的应该判断会多出一个 10.x,还不快去给 Vue CLI 提个 PR(手动狗头)。

  1. 判断是否在以后门路

在执行 vue create  的时候,是必须指定一个 app-name,否则会报错:Missing required argument <app-name>。

那如果用户曾经本人创立了一个目录,想在以后这个空目录下创立一个我的项目呢?当然,Vue CLI 也是反对的,执行 vue create .  就 OK 了。

lib/create.js  中就有相干代码是在解决这个逻辑的。

async function create(projectName, options) {

// 判断传入的 projectName 是否是 .

const inCurrent = projectName === ‘.’;

// path.relative 会返回第一个参数到第二个参数的相对路径

// 这里就是用来获取当前目录的目录名

const name = inCurrent ? path.relative(‘../’, cwd) : projectName;

// 最终初始化我的项目的门路

const targetDir = path.resolve(cwd, projectName || ‘.’);

}

如果你须要实现一个 CLI,这个逻辑是能够拿来即用的。

  1. 查看利用名

Vue CLI 会通过 validate-npm-package-name  这个包来查看输出的 projectName 是否符合规范。

const result = validateProjectName(name);if (!result.validForNewPackages) {

console.error(chalk.red(Invalid project name: "${name}"));

exit(1);

}

对应的 npm 命名标准能够见:Naming Rules

  1. 若指标文件夹已存在,是否笼罩

这段代码比较简单,就是判断 target  目录是否存在,而后通过交互询问用户是否笼罩(对应的是操作是删除原目录):

// 是否 vue create -mif (fs.existsSync(targetDir) && !options.merge) {

// 是否 vue create -f

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 {

// 如果有目标目录,则询问如何解决:Overwrite / Merge / Cancel

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

],

},

]);

// 如果抉择 Cancel,则间接停止

// 如果抉择 Overwrite,则先删除原目录

// 如果抉择 Merge,不必预处理啥

if (!action) {

return;

} else if (action === ‘overwrite’) {

console.log(nRemoving ${chalk.cyan(targetDir)}...);

await fs.remove(targetDir);

}

}

}

}

  1. 整体谬误捕捉

在 create  办法的最外层,放了一个 catch  办法,捕捉外部所有抛出的谬误,将以后的 spinner  状态进行,退出过程。

module.exports = (…args) => {

return create(…args).catch(err => {

stopSpinner(false); // do not persist

error(err);

if (!process.env.VUE_CLI_TEST) {

process.exit(1);

}

});

};

  1. Creator 类

在 lib/create.js  办法的最初,执行了这样两行代码:

const creator = new Creator(name, targetDir, getPromptModules());await creator.create(options);

看来最重要的代码还是在 Creator  这个类中。

关上 Creator.js  文件,好家伙,500+ 行代码,并且引入了 12 个模块。当然,这篇文章不会把这 500 行代码和 12 个模块都理一遍,没必要,感兴趣的本人去看看好了。

本文还是梳理主流程和一些有意思的性能。

8.1 constructor 构造函数

先看一下 Creator  类的的构造函数:

module.exports = class Creator extends EventEmitter {

constructor(name, context, promptModules) {

super();

this.name = name;

this.context = process.env.VUE_CLI_CONTEXT = context;

// 获取了 preset 和 feature 的 交互抉择列表,在 vue create 的时候提供抉择

const {presetPrompt, featurePrompt} = this.resolveIntroPrompts();

this.presetPrompt = presetPrompt;

this.featurePrompt = featurePrompt;

// 交互抉择列表:是否输入一些文件

this.outroPrompts = this.resolveOutroPrompts();

this.injectedPrompts = [];

this.promptCompleteCbs = [];

this.afterInvokeCbs = [];

this.afterAnyInvokeCbs = [];

this.run = this.run.bind(this);

const promptAPI = new PromptModuleAPI(this);

// 将默认的一些配置注入到交互列表中

promptModules.forEach(m => m(promptAPI));

}

};

构造函数嘛,次要就是初始化一些变量。这里次要将逻辑都封装在 resolveIntroPrompts / resolveOutroPrompts  和 PromptModuleAPI  这几个办法中。

次要看一下 PromptModuleAPI 这个类是干什么的。

module.exports = class PromptModuleAPI {

constructor(creator) {

this.creator = creator;

}

// 在 promptModules 里用

injectFeature(feature) {

this.creator.featurePrompt.choices.push(feature);

}

// 在 promptModules 里用

injectPrompt(prompt) {

this.creator.injectedPrompts.push(prompt);

}

// 在 promptModules 里用

injectOptionForPrompt(name, option) {

this.creator.injectedPrompts

.find(f => {

return f.name === name;

})

.choices.push(option);

}

// 在 promptModules 里用

onPromptComplete(cb) {

this.creator.promptCompleteCbs.push(cb);

}

};

这里咱们也简略说一下,promptModules  返回的是所有用于终端交互的模块,其中会调用 injectFeature 和 injectPrompt 来将交互配置插入进去,并且会通过 onPromptComplete  注册一个回调。

onPromptComplete 注册回调的模式是往 promptCompleteCbs 这个数组中 push 了传入的办法,能够猜想在所有交互实现之后应该会通过以下模式来调用回调:

this.promptCompleteCbs.forEach(cb => cb(answers, preset));

回过来看这段代码:

module.exports = class Creator extends EventEmitter {

constructor(name, context, promptModules) {

const promptAPI = new PromptModuleAPI(this);

promptModules.forEach(m => m(promptAPI));

}

};

在 Creator  的构造函数中,实例化了一个 promptAPI  对象,并遍历 prmptModules  把这个对象传入了 promptModules  中,阐明在实例化 Creator  的时候时候就会把所有用于交互的配置注册好了。

这里咱们留神到,在构造函数中呈现了四种 prompt:presetPrompt,featurePrompt,injectedPrompts,outroPrompts,具体有什么区别呢?下文有有具体开展。

8.2 EventEmitter 事件模块

首先,Creator  类是继承于 Node.js 的 EventEmitter 类。家喻户晓,events  是 Node.js 中最重要的一个模块,而 EventEmitter 类就是其根底,是 Node.js 中事件触发与事件监听等性能的封装。

在这里,Creator  继承自 EventEmitter , 应该就是为了不便在 create  过程中 emit  一些事件,整顿了一下,次要就是以下 8 个事件:

this.emit(‘creation’, { event: ‘creating’}); // 创立 this.emit(‘creation’, { event: ‘git-init’}); // 初始化 gitthis.emit(‘creation’, { event: ‘plugins-install’}); // 装置插件 this.emit(‘creation’, { event: ‘invoking-generators’}); // 调用 generatorthis.emit(‘creation’, { event: ‘deps-install’}); // 装置额定的依赖 this.emit(‘creation’, { event: ‘completion-hooks’}); // 实现之后的回调 this.emit(‘creation’, { event: ‘done’}); // create 流程完结 this.emit(‘creation’, { event: ‘fetch-remote-preset’}); // 拉取近程 preset

咱们晓得事件 emit  肯定会有 on  的中央,是哪呢?搜了一下源码,是在 @vue/cli-ui 这个包里,也就是说在终端命令行工具的场景下,不会触发到这些事件,这里简略理解一下即可:

const creator = new Creator(”, cwd.get(), getPromptModules());

onCreationEvent = ({event}) => {

progress.set({id: PROGRESS_ID, status: event, info: null}, context);

};

creator.on(‘creation’, onCreationEvent);

简略来说,就是通过 vue ui  启动一个图形化界面来初始化我的项目时,会启动一个 server 端,和终端之间是存在通信的。server 端挂载了一些事件,在 create 的每个阶段,会从 cli 中的办法触发这些事件。

  1. Preset(预设)

Creator  类的实例办法 create  承受两个参数:

cliOptions:终端命令行传入的参数

preset:Vue CLI 的预设

9.1 什么是 Preset(预设)

Preset 是什么呢?官网解释是一个蕴含创立新我的项目所需预约义选项和插件的 JSON 对象,让用户无需在命令提醒中抉择它们。比方:

{

“useConfigFiles”: true,

“cssPreprocessor”: “sass”,

“plugins”: {

“@vue/cli-plugin-babel”: {},

“@vue/cli-plugin-eslint”: {

“config”: “airbnb”,

“lintOn”: [“save”, “commit”]

}

},

“configs”: {

“vue”: {…},

“postcss”: {…},

“eslintConfig”: {…},

“jest”: {…}

}

}

在 CLI 中容许应用本地的 preset 和近程的 preset。

正文完
 0