近期工作的过程中跟 Vue CLI
的插件打交道比较多,想了想自己在学校写项目的时候最烦的就是项目创建之后手动创建组件/页面和配置路由,于是突发奇想决定写一个脚手架的插件,自动实现创建组件/页面和配置路由的功能。
本文会一步一步教你如何编写一个自己的 Vue CLI
插件,并发布至 npm
,为所有因为这个问题而烦恼的同学解放双手。
关注 「Hello FE」 获取更多实战教程,正好最近在抽奖,查看历史文章即可获取抽奖方法~
本教程的插件完整代码放在了我的 GitHub 上,欢迎大家 Star:vue-cli-plugin-generators
同时,我也将这个插件发布到了 npm
,大家可以直接使用 npm
安装并体验添加组件的能力。
PS:添加页面和配置路由的能力还在开发中
体验方式:
npm
安装npm install vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
复制代码
yarn
安装yarn add vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
复制代码
Vue CLI
安装(推荐)vue add vue-cli-plugin-generators
复制代码
注意:一定要注意是复数形式的 generators
,不是单数形式的 generator
,generator
被前辈的占领了。
废话不多说,我们直接开始吧!
要做好一个 Vue CLI
插件,除了要了解 Vue CLI
插件的开发规范之外,我们还需要了解几个 npm
包:
chalk
让你的控制台输出好看一点,为文字或背景上色glob
让你可以使用 Shell
脚本的方式匹配文件inquirer
让你可以使用交互式的命令行来获取需要的信息主要出现的 npm
包就只有这三个,其他的都是基于 Node.js
的各种模块,比如 fs
和 path
,了解过 Node.js
的同学应该不陌生。
创建一个空的文件夹,名字最好就是你的插件的名字。
这里我的名字是 vue-cli-plugin-generators
,你可以取一个自己喜欢的名字,不过最好是见名知义的那种,比如 vue-cli-plugin-component-generator
或者 vue-cli-plugin-page-generator
,一看就知道是组件生成器和页面生成器。
至于为什么一定要带上 vue-cli-plugin
的前缀这个问题,可以看一下官方文档:命名和可发现性。
然后初始化我们的项目:
npm init
复制代码
输入一些基本的信息,这些信息会被写入 package.json
文件中。
创建一个基本的目录结构:
.
├── LICENSE
├── README.md
├── generator
│ ├── index.js
│ └── template
│ └── component
│ ├── jsx
│ │ └── Template.jsx
│ ├── sfc
│ │ └── Template.vue
│ ├── style
│ │ ├── index.css
│ │ ├── index.less
│ │ ├── index.sass
│ │ ├── index.scss
│ │ └── index.styl
│ └── tsx
│ └── Template.tsx
├── index.js
├── package.json
├── src
│ ├── add-component.js
│ ├── add-page.js
│ └── utils
│ ├── log.js
│ └── suffix.js
└── yarn.lock
复制代码
目录结构创建好了之后就可以开始编码了。
一些不重要的文件就不讲解了,主要讲解一下作为一个优秀的 Vue CLI
插件,需要哪些部分:
.
├── README.md
├── generator.js # Generator(可选)
├── index.js # Service 插件
├── package.json
├── prompts.js # Prompt 文件(可选)
└── ui.js # Vue UI 集成(可选)
复制代码
主要分为 4 个部分:Generator/Service/Prompt/UI
。
其中,Service
是必须的,其他的部分都是可选项。
先来讲一下各个部分的作用:
Generator
可以为你的项目创建文件、编辑文件、添加依赖。
Generator
应该放在根目录下,被命名为 generator.js
或者放在 generator
目录下,被命名为 index.js
,它会在调用 vue add
或者 vue invoke
时被执行。
来看下我们这个项目的 generator/index.js
:
*
* @file Generator
*/
'use strict';
// 前置知识中提到的美化控制台输出的包
const chalk = require('chalk');
// 封装的打印函数
const log = require('../src/utils/log');
module.exports = (api) => {
// 执行脚本
const extendScript = {
scripts: {
'add-component': 'vue-cli-service add-component',
'add-page': 'vue-cli-service add-page'
}
};
// 拓展 package.json 为其中的 scripts 中添加 add-component 和 add-page 两条指令
api.extendPackage(extendScript);
// 插件安装成功后 输出一些提示 可以忽略
console.log('');
log.success(`Success: Add plugin success.`);
console.log('');
console.log('You can use it with:');
console.log('');
console.log(` ${chalk.cyan('yarn add-component')}`);
console.log(' or');
console.log(` ${chalk.cyan('yarn add-page')}`);
console.log('');
console.log('to create a component or page.');
console.log('');
console.log(`${chalk.green.bold('Enjoy it!')}`);
console.log('');
};
复制代码
所以,当我们执行 vue add vue-cli-plugin-generators
的时候,generator/index.js
会被执行,你就可以看到你的控制台输出了这样的指引信息:
同时你还会发现,执行了 vue add vue-cli-plugin-generators
的项目中,package.json
发生了变化:
添加了两条指令,让我们可以通过 yarn add-component
和 yarn add-page
去添加组件/页面。
虽然添加了这两条指令,但是现在这两条指令还没有被注册到 vue-cli-service
中,这时候我们就需要开始编写 Service
了。
Service
可以为你的项目修改 Webpack
配置、创建 vue-cli-service
命令、修改 vue-cli-service
命令。
Service
应该放在根目录下,被命名为 index.js
,它会在调用 vue-cli-service
时被执行。
来看一下我们这个项目的 index.js
:
/**
* @file Service 插件
*/
'use strict';
const addComponent = require('./src/add-component');
const addPage = require('./src/add-page');
module.exports = (api, options) => {
// 向 vue-cli-service 中注册 add-component 指令
api.registerCommand('add-component', async () => {
await addComponent(api);
});
// 向 vue-cli-service 中注册 add-page 指令
api.registerCommand('add-page', async () => {
await addPage(api);
});
};
复制代码
为了代码的可读性,我们把 add-component
和 add-page
指令的回调函数单独抽了出来,分别放在了 src/add-component.js
和 src/add-page.js
中:
前方代码量较大,建议先阅读注释理解思路。
/**
* @file Add Component 逻辑
*/
'use strict';
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const chalk = require('chalk');
const inquirer = require('inquirer');
const log = require('./utils/log');
const suffix = require('./utils/suffix');
module.exports = async (api) => {
// 交互式命令行参数 获取组件信息
// componentName {string} 组件名称 默认 HelloWorld
const { componentName } = await inquirer.prompt([
{
name: 'componentName',
type: 'input',
message: `Please input your component name. ${chalk.yellow(
'( PascalCase )'
)}`,
description: `You should input a ${chalk.yellow(
'PascalCase'
)}, it will be used to name new component.`,
default: 'HelloWorld'
}
]);
// 组件名称校验
if (!componentName.trim() || /[^A-Za-z0-9]/g.test(componentName)) {
log.error(
`Error: Please input a correct name. ${chalk.bold('( PascalCase )')}`
);
return;
}
// 项目中组件文件路径 Vue CLI 创建的项目中默认路径为 src/components
const baseDir = `${api.getCwd()}/src/components`;
// 遍历组件文件 返回组件路径列表
const existComponent = glob.sync(`${baseDir}/*`);
// 替换组件路径列表中的基础路径 返回组件名称列表
const existComponentName = existComponent.map((name) =>
name.replace(`${baseDir}/`, '')
);
// 判断组件是否已存在
const isExist = existComponentName.some((name) => {
// 正则表达式匹配从控制台输入的组件名称是否已经存在
const reg = new RegExp(
`^(${componentName}.[vue|jsx|tsx])$|^(${componentName})$`,
'g'
);
return reg.test(name);
});
// 存在则报错并退出
if (isExist) {
log.error(`Error: Component ${chalk.bold(componentName)} already exists.`);
return;
}
// 交互式命令行 获取组件信息
// componentType {'sfc'|'tsx'|'jsx'} 组件类型 默认 sfc
// componentStyleType {'.css'|'.scss'|'.sass'|'.less'|'.stylus'} 组件样式类型 默认 .scss
// shouldMkdir {boolean} 是否需要为组件创建文件夹 默认 true
const {
componentType,
componentStyleType,
shouldMkdir
} = await inquirer.prompt([
{
name: 'componentType',
type: 'list',
message: `Please select your component type. ${chalk.yellow(
'( .vue / .tsx / .jsx )'
)}`,
choices: [
{ name: 'SFC (.vue)', value: 'sfc' },
{ name: 'TSX (.tsx)', value: 'tsx' },
{ name: 'JSX (.jsx)', value: 'jsx' }
],
default: 'sfc'
},
{
name: 'componentStyleType',
type: 'list',
message: `Please select your component style type. ${chalk.yellow(
'( .css / .sass / .scss / .less / .styl )'
)}`,
choices: [
{ name: 'CSS (.css)', value: '.css' },
{ name: 'SCSS (.scss)', value: '.scss' },
{ name: 'Sass (.sass)', value: '.sass' },
{ name: 'Less (.less)', value: '.less' },
{ name: 'Stylus (.styl)', value: '.styl' }
],
default: '.scss'
},
{
name: 'shouldMkdir',
type: 'confirm',
message: `Should make a directory for new component? ${chalk.yellow(
'( Suggest to create. )'
)}`,
default: true
}
]);
// 根据不同的组件类型 生成对应的 template 路径
let src = path.resolve(
__dirname,
`../generator/template/component/${componentType}/Template${suffix(
componentType
)}`
);
// 组件目标路径 默认未生成组件文件夹
let dist = `${baseDir}/${componentName}${suffix(componentType)}`;
// 根据不同的组件样式类型 生成对应的 template 路径
let styleSrc = path.resolve(
__dirname,
`../generator/template/component/style/index${componentStyleType}`
);
// 组件样式目标路径 默认未生成组件文件夹
let styleDist = `${baseDir}/${componentName}${componentStyleType}`;
// 需要为组件创建文件夹
if (shouldMkdir) {
try {
// 创建组件文件夹
fs.mkdirSync(`${baseDir}/${componentName}`);
// 修改组件目标路径
dist = `${baseDir}/${componentName}/${componentName}${suffix(
componentType
)}`;
// 修改组件样式目标路径
styleDist = `${baseDir}/${componentName}/index${componentStyleType}`;
} catch (e) {
log.error(e);
return;
}
}
// 生成 SFC/TSX/JSX 及 CSS/SCSS/Sass/Less/Stylus
try {
// 读取组件 template
// 替换组件名称为控制台输入的组件名称
const template = fs
.readFileSync(src)
.toString()
.replace(/helloworld/gi, componentName);
// 读取组件样式 template
// 替换组件类名为控制台输入的组件名称
const style = fs
.readFileSync(styleSrc)
.toString()
.replace(/helloworld/gi, componentName);
if (componentType === 'sfc') {
// 创建的组件类型为 SFC 则将组件样式 template 注入 标签中并添加样式类型
fs.writeFileSync(
dist,
template
// 替换组件样式为 template 并添加样式类型
.replace(
/`
)
);
} else {
// 创建的组件类型为 TSX/JSX 则将组件样式 template 注入单独的样式文件
fs.writeFileSync(
dist,
template.replace(
// 当不需要创建组件文件夹时 样式文件应该以 [组件名称].[组件样式类型] 的方式引入
/import '\.\/index\.css';/gi,
`import './${
shouldMkdir ? 'index' : `${componentName}`
}${componentStyleType}';`
)
);
fs.writeFileSync(styleDist, style);
}
// 组件创建完成 打印组件名称和组件文件路径
log.success(
`Success: Component ${chalk.bold(
componentName
)} was created in ${chalk.bold(dist)}`
);
} catch (e) {
log.error(e);
return;
}
};
复制代码
上面的代码是 add-component
指令的执行逻辑,比较长,可以稍微有点耐心阅读一下。
由于 add-page
指令的执行逻辑还在开发过程中,这里就不贴出来了,大家可以自己思考一下,欢迎有好想法的同学为这个仓库提 PR:vue-cli-plugin-generators。
现在我们可以来执行一下 yarn add-component
来体验一下功能了:
这里我们分别创建了 SFC/TSX/JSX
三种类型的组件,目录结构如下:
.
├── HelloJSX
│ ├── HelloJSX.jsx
│ └── index.scss
├── HelloSFC
│ └── HelloSFC.vue
├── HelloTSX
│ ├── HelloTSX.tsx
│ └── index.scss
└── HelloWorld.vue
复制代码
其中 HelloWorld.vue
是 Vue CLI
创建时自动生成的。
对应的文件中组件名称和组件样式类名也被替换了。
到这里我们就算完成了一个能够自动生成组件的 Vue CLI
插件了。
但是,还不够!
Prompt
会在创建新的项目或者在项目中添加新的插件时输出交互式命令行,获取 Generator
需要的信息,这些信息会在用户输入完成后以 options
的形式传递给 Generator
,供 Generator
中的 ejs
模板渲染。
Prompt
应该放在根目录下,被命名为 prompt.js
,它会在调用 vue add
或者 vue invoke
时被执行,执行顺序位于 Generator
前。
在我们的插件中,我们并不需要在调用 vue add
或者 vue invoke
时就创建组件/页面,因此不需要在这个时候获取组件的相关信息。
UI
会在使用 vue ui
指令打开图形化操作界面后给到用户一个图形化的插件配置功能。
这个部分的内容比较复杂,讲解起来比较费劲,大家可以到官网上阅读:UI 集成。
在我们的插件中,我们并不需要使用 vue ui
启动图形化操作界面,因此不需要编写 UI
相关的代码。
我们可以到 Vue CLI
插件开发指南中查看更详细的指南,建议阅读英文文档,没有什么教程比官方文档更加合适了。
一个优秀的 Vue CLI
插件应该有四个部分:
.
├── README.md
├── generator.js # Generator(可选)
├── index.js # Service 插件
├── package.json
├── prompts.js # Prompt 文件(可选)
└── ui.js # Vue UI 集成(可选)
复制代码
Generator
可以为你的项目创建文件、编辑文件、添加依赖。
Service
可以为你的项目修改 Webpack
配置、创建 vue-cli-service
命令、修改 vue-cli-service
命令。
Prompt
会在创建新的项目或者在项目中添加新的插件时输出交互式命令行,获取 Generator
需要的信息,这些信息会在用户输入完成后以 options
的形式传递给 Generator
,供 Generator
中的 ejs
模板渲染。
UI
会在使用 vue ui
指令打开图形化操作界面后给到用户一个图形化的插件配置功能。
四个部分各司其职才能更好地实现一个完美的插件!
如果大家想学习前端方面的技术,我把我多年的经验分享给大家,还有一些学习资料,微信:mkm8555