每次写 Vue 组件的时候,都要在 Components 文件夹里面手动新建一个文件,然后再把 template、script 和 style 的结构写一遍或者从其他文件 copy 一份出来再粘贴进去实在是费工夫。说实话,我(bu)比(shi)较(lan)懒 (我写 React 组件的时候却是这么干的)。

所以,我现在想搞个简单的办法:通过一个 node 命令就可以帮我快速的创建一个 Vue 的组件文件,并且帮我把基本的模板都写好了的。

思路

我们经常在启动项目的时候都会用到 yarn serve,而 serve 的执行者是 vue-cli-service。所以,首先就想到了用 vue-cli-service 跑一个自己的命令。正好 Vue Cli 官方文档 也有 Service 插件的说明文档。

插件内部的整体思路大概是这样的:

graph TD
A(用户输入组件名称 - 可能包含路径) --> B(验证组件是否存在)
B -- 不存在 --> C(验证路径是否存在)
B -- 存在-重新输入 --> A
C -- 存在 --> E(配置组件模板参数)
C -- 不存在 --> D(创建路径目录)
D --> E
E --> F(配置模板参数 - 装饰器 & Scoped)
F --> G(创建组件文件并写入组件模板)

有了思路以后再去看 Vue Cli 里面的一些源码,尤其是 Creator 和 Service 里面的代码。因为在我们使用 Create 创建项目的时候就会很多地方是我们这个插件所需要用到的东西。比如:输入内容、选择选项、是否选择等等。

准备

根据上面的思路图和查看 Vue Cli Creator 和 Service 的一些源码,能够收集到如下的一些必要 node api 库:

  • fs - 文件/文件夹操作
  • path - 获取文件路径
  • globby - 遍历目录查找文件或文件夹
  • slash - 优化在 Linux 和 window 环境下获得的路径
  • inquirer - 命令面板交互 api,包含输入、选择、是否等等操作
  • chalk - 控制台对输出 log 进行样式处理

收集好这些必要的库后,我们参照 package.json 里面的 scripts 内容自定义一个我们要的启动命令:

"comps": "vue-cli-service comps"

然后在当前项目目录的 node_modules下找到 @vue/cli-service/lib/commands 目录,并在 commands 目录新建一个 comps.js 文件。这个 comps.js 文件则就是我们后面要着重写的一个 node 服务了。

根据 Service 插件书写规范,我们来注册一个简单的 service 命令:

1
2
3
4
5
6
7
8
9
module.exports = (api, options) => {

api.registerCommand('comps', args => {

console.log('测试 comps 命令');

});

};

然后在外面的 Service.js 内引入我们的 comps.js 文件作为 Service 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
const builtInPlugins = [
'./commands/serve',
'./commands/build',
'./commands/inspect',
'./commands/help',
'./commands/comps', // 这里就是我们的插件文件
// config plugins are order sensitive
'./config/base',
'./config/css',
'./config/dev',
'./config/prod',
'./config/app'
].map(idToPlugin)

保存好之后,在当前项目的命令行窗口输入yarn comps回车就可以看到控制层输入了测试 comps 命令 的信息了。

插件业务逻辑实现

刚才从启动命令,到注入 Service 插件以及创建插件文件都以及处理好了,现在我们开始插件的业务逻辑实现。

1、获取组件目录地址

通过 Service 插件的 api 接口可以得到当前命令所处的项目目录:

1
2
3
4
5
6
7
module.exports = (api, options) => {
// 获取当前项目路径
let context = api.getCwd();
api.registerCommand('comps', args => {
console.log('当前路径:', context);
});
};

因为我是要在组件目录,所以我需要将工作目录指向组件目录:

1
2
3
4
5
6
7
module.exports = (api, options) => {
// 获取当前项目路径
let context = api.resolve('/src/components');
api.registerCommand('comps', args => {
console.log('当前路径:', context);
});
};

为了达到目录可配置,我采用了 vue.config.js 文件的方法配置 pluginOptions项:

1
2
3
4
5
6
7
module.exports = {
pluginOptions: {
comps: {
baseDir: '/src/components'
}
}
}

文档这里有说到采用 vue.config.js 或是在 package.json 内配置 vue属性

重新修改下 comps.js 如下:

1
2
3
4
5
6
7
8
9
module.exports = (api, options) => {
let componentDir = options.pluginOptions.comps.baseDir;
// 获取目标路径
let context = api.resolve(componentDir);

api.registerCommand('comps', args => {
console.log('当前路径:', context);
});
};

2、获取用户输入的组件名(可能含路径)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注入对话框
async injectPrompt(options) {
return inquirer.prompt(options);
}

// 获取用户输入的组件名称(可能包含的路径)
async getComponentName() {
const { componentName } = await this.injectPrompt({
name: 'componentName',
message: ' 请输入组件名称'
});
return componentName || '';
}

3、检查组件是否存在

通过 globby api 可以快速的帮我们找到所有组件,然后通过比对用户输入的组件名称进行校验组件是否已经存在:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 检查用户输入的组件是否存在
async existComponent() {
const files = await globby(['**'], {
cwd: this.context,
expandDirectories: {
files: [this.componentName],
extensions: ['vue']
},
deep: true,
onlyFiles: true
});
return (
files &&
files.length &&
!!files.filter(item => item.indexOf(`${this.componentName}.vue`) > -1)
.length
);
}

4、校验组件名称是否符合规范

组件名称建议采用大驼峰的书写方式,并且不能以非字母或特殊字符开始:

1
2
3
4
5
6
7
// 检查组件名称是否规范
async checkComponentName() {
if (!/^[a-zA-Z][a-zA-Z\/]*$/g.test(this.componentName)) {
return true;
}
return false;
}

5、校验带路径的组件路径是否存在目录

有时候components里面可能会包含其他组件的文件夹,所以在用户输入组件名称的时候难免会有带上路径。这个时候就需要对这个路径进行校验是否存在,不存在则需要创建目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 解析输入的组件路径
splitPath() {
if (this.componentName.indexOf('/') > -1) {
return this.componentName.split('/');
}
return [];
}
// 检查目录是否存在
existDir(filePath) {
return fs.existsSync(filePath);
}

// 创建目录
mkdir(filePath) {
fs.mkdirSync(filePath);
}
const paths = this.splitPath();
let tempPath = '',
tempContext = '';
if (paths.length) {
// 判断目录层级是否存在,不存在则创建
for (const index in paths) {
if (index < paths.length - 1) {
tempPath = `${tempPath}/${paths[index]}`;
tempContext = slash(path.join(this.context, tempPath));
if (!this.existDir(tempContext)) {
this.mkdir(tempContext);
}
}
}
}

6、配置模板参数 - 修饰器与 Scoped

组件当中包含引入的一些修饰器,比如:Prop、Emit 等,或者需要对采用 scoped 模式对样式进行作用域限制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 获取用户选择的装饰器配置信息
async getDecorator() {
const { decorator } = await this.injectPrompt({
type: 'checkbox',
message: '请选择您需要的装饰器(多选)',
name: 'decorator',
choices: [
{
name: 'Prop'
},
{
name: 'Model'
},
{
name: 'Watch'
},
{
name: 'Emit'
},
{
name: 'Mixins'
},
{
name: 'Provide'
},
{
name: 'Inject'
}
]
});
return decorator;
}

// 获取用户对组件样式作用域的设置
async getScoped() {
const { scoped } = await this.injectPrompt({
type: 'confirm',
name: 'scoped',
message: '样式是否只对当前组件有效? (默认为否)',
default: false
});
return scoped;
}

7、创建组件文件

拿到用户选择的模板配置数据之后,需要把这些数据塞入模板内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 设置模板内容
async replaceTemplate() {
const decorator = await this.getDecorator();
const scoped = await this.getScoped();
let componentName = this.componentName;
if (this.componentName.indexOf('/') > -1) {
componentName = this.componentName.match(/\/\w*$/)[0].slice(1);
}
this.template = `
<template>

</template>

<script lang="ts">
import { Component, Vue ${decorator.length ? `, ${decorator.join(', ')}` : ''} } from 'vue-property-decorator';

@Component
export default class ${componentName} extends Vue {

}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style ${scoped ? 'scoped' : ''} lang="scss">

</style>`;
return true;
}
// 创建文件并写入内容
writeFile() {
fs.writeFileSync(
path.join(this.context, `${this.componentName}.vue`),
this.template
);
this.log('组件创建成功!\n');
}

// 获取配置信息并写入文件
async createFile() {
if (await this.replaceTemplate()) {
this.writeFile();
}
}

至此,所有的操作流程都以及完成了,然后我们在命令行内输入yarn comps进行测试查看效果。

该插件已经发布,详细可以看点击这里查看插件文档