近期工做的过程当中跟 Vue CLI
的插件打交道比较多,想了想本身在学校写项目的时候最烦的就是项目建立以后手动建立组件/页面和配置路由,因而突发奇想决定写一个脚手架的插件,自动实现建立组件/页面和配置路由的功能。css
本文会一步一步教你如何编写一个本身的 Vue CLI
插件,并发布至 npm
,为全部由于这个问题而烦恼的同窗解放双手。html
关注 「Hello FE」 获取更多实战教程,正好最近在抽奖,查看历史文章便可获取抽奖方法~vue
本教程的插件完整代码放在了个人 GitHub 上,欢迎你们 Star:vue-cli-plugin-generatorsgit
同时,我也将这个插件发布到了 npm
,你们能够直接使用 npm
安装并体验添加组件的能力。github
PS:添加页面和配置路由的能力还在开发中。web
体验方式:正则表达式
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
废话很少说,咱们直接开始吧!npm
要作好一个 Vue CLI
插件,除了要了解 Vue CLI
插件的开发规范以外,咱们还须要了解几个 npm
包:json
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 注入 <style></style> 标签中并添加样式类型
fs.writeFileSync(
dist,
template
// 替换组件样式为 template 并添加样式类型
.replace(
/<style>\s<\/style>/gi,
() =>
`<style${ // 当组件样式类型为 CSS 时不须要添加组件样式类型 componentStyleType !== '.css' ? ` lang="${ // 当组件样式类型为 Stylus 时须要作一下特殊处理 componentStyleType === '.styl' ? 'stylus' : componentStyleType.replace('.', '') }"` : '' }>\n${style}</style>`
)
);
} 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
指令打开图形化操做界面后给到用户一个图形化的插件配置功能。
四个部分各司其职才能更好地实现一个完美的插件!
本教程的插件完整代码放在了个人 GitHub 上,欢迎你们 Star:vue-cli-plugin-generators
也欢迎你们经过 npm/yarn
安装到本身的项目中体验~
关注 「Hello FE」 获取更多实战教程