自動(dòng)生成組件代碼—— Vue CLI 插件開(kāi)發(fā)實(shí)戰(zhàn)
點(diǎn)上方藍(lán)字關(guān)注公眾號(hào)「TianTianUp」
前言
近期工作的過(guò)程中跟 Vue CLI 的插件打交道比較多,想了想自己在學(xué)校寫(xiě)項(xiàng)目的時(shí)候最煩的就是項(xiàng)目創(chuàng)建之后手動(dòng)創(chuàng)建組件/頁(yè)面和配置路由,于是突發(fā)奇想決定寫(xiě)一個(gè)腳手架的插件,自動(dòng)實(shí)現(xiàn)創(chuàng)建組件/頁(yè)面和配置路由的功能。
本文會(huì)一步一步教你如何編寫(xiě)一個(gè)自己的 Vue CLI 插件,并發(fā)布至 npm,為所有因?yàn)檫@個(gè)問(wèn)題而煩惱的同學(xué)解放雙手。
關(guān)注 「Hello FE」 獲取更多實(shí)戰(zhàn)教程,正好最近在抽獎(jiǎng),查看歷史文章即可獲取抽獎(jiǎng)方法~
本教程的插件完整代碼放在了我的 GitHub 上,歡迎大家 Star:vue-cli-plugin-generators[1]
同時(shí),我也將這個(gè)插件發(fā)布到了 npm,大家可以直接使用 npm 安裝并體驗(yàn)添加組件的能力。
PS:添加頁(yè)面和配置路由的能力還在開(kāi)發(fā)中。
體驗(yàn)方式:
通過(guò) npm安裝
npm install vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
通過(guò) yarn安裝
yarn add vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
通過(guò) Vue CLI安裝(推薦)
vue add vue-cli-plugin-generators
注意:一定要注意是復(fù)數(shù)形式的 generators,不是單數(shù)形式的 generator,generator 被前輩的占領(lǐng)了。
廢話不多說(shuō),我們直接開(kāi)始吧!
前置知識(shí)
要做好一個(gè) Vue CLI 插件,除了要了解 Vue CLI 插件的開(kāi)發(fā)規(guī)范之外,我們還需要了解幾個(gè) npm 包:
chalk讓你的控制臺(tái)輸出好看一點(diǎn),為文字或背景上色glob讓你可以使用Shell腳本的方式匹配文件inquirer讓你可以使用交互式的命令行來(lái)獲取需要的信息
主要出現(xiàn)的 npm 包就只有這三個(gè),其他的都是基于 Node.js 的各種模塊,比如 fs 和 path,了解過(guò) Node.js 的同學(xué)應(yīng)該不陌生。
項(xiàng)目初始化
創(chuàng)建一個(gè)空的文件夾,名字最好就是你的插件的名字。
這里我的名字是 vue-cli-plugin-generators,你可以取一個(gè)自己喜歡的名字,不過(guò)最好是見(jiàn)名知義的那種,比如 vue-cli-plugin-component-generator 或者 vue-cli-plugin-page-generator,一看就知道是組件生成器和頁(yè)面生成器。
至于為什么一定要帶上 vue-cli-plugin 的前綴這個(gè)問(wèn)題,可以看一下官方文檔:命名和可發(fā)現(xiàn)性[2]。
然后初始化我們的項(xiàng)目:
npm init
輸入一些基本的信息,這些信息會(huì)被寫(xiě)入 package.json 文件中。
創(chuàng)建一個(gè)基本的目錄結(jié)構(gòu):
.
├── 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
目錄結(jié)構(gòu)創(chuàng)建好了之后就可以開(kāi)始編碼了。
目錄解析
一些不重要的文件就不講解了,主要講解一下作為一個(gè)優(yōu)秀的 Vue CLI 插件,需要哪些部分:
.
├── README.md
├── generator.js # Generator(可選)
├── index.js # Service 插件
├── package.json
├── prompts.js # Prompt 文件(可選)
└── ui.js # Vue UI 集成(可選)
主要分為 4 個(gè)部分:Generator/Service/Prompt/UI。
其中,Service 是必須的,其他的部分都是可選項(xiàng)。
先來(lái)講一下各個(gè)部分的作用:
Generator
Generator 可以為你的項(xiàng)目創(chuàng)建文件、編輯文件、添加依賴(lài)。
Generator 應(yīng)該放在根目錄下,被命名為 generator.js 或者放在 generator 目錄下,被命名為 index.js,它會(huì)在調(diào)用 vue add 或者 vue invoke 時(shí)被執(zhí)行。
來(lái)看下我們這個(gè)項(xiàng)目的 generator/index.js:
/**
* @file Generator
*/
'use strict';
// 前置知識(shí)中提到的美化控制臺(tái)輸出的包
const chalk = require('chalk');
// 封裝的打印函數(shù)
const log = require('../src/utils/log');
module.exports = (api) => {
// 執(zhí)行腳本
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('');
};
所以,當(dāng)我們執(zhí)行 vue add vue-cli-plugin-generators 的時(shí)候,generator/index.js 會(huì)被執(zhí)行,你就可以看到你的控制臺(tái)輸出了這樣的指引信息:

同時(shí)你還會(huì)發(fā)現(xiàn),執(zhí)行了 vue add vue-cli-plugin-generators 的項(xiàng)目中,package.json 發(fā)生了變化:

添加了兩條指令,讓我們可以通過(guò) yarn add-component 和 yarn add-page 去添加組件/頁(yè)面。
雖然添加了這兩條指令,但是現(xiàn)在這兩條指令還沒(méi)有被注冊(cè)到 vue-cli-service 中,這時(shí)候我們就需要開(kāi)始編寫(xiě) Service 了。
Service
Service 可以為你的項(xiàng)目修改 Webpack 配置、創(chuàng)建 vue-cli-service 命令、修改 vue-cli-service 命令。
Service 應(yīng)該放在根目錄下,被命名為 index.js,它會(huì)在調(diào)用 vue-cli-service 時(shí)被執(zhí)行。
來(lái)看一下我們這個(gè)項(xiàng)目的 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 中注冊(cè) add-component 指令
api.registerCommand('add-component', async () => {
await addComponent(api);
});
// 向 vue-cli-service 中注冊(cè) add-page 指令
api.registerCommand('add-page', async () => {
await addPage(api);
});
};
為了代碼的可讀性,我們把 add-component 和 add-page 指令的回調(diào)函數(shù)單獨(dú)抽了出來(lái),分別放在了 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) => {
// 交互式命令行參數(shù) 獲取組件信息
// componentName {string} 組件名稱(chēng) 默認(rèn) 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'
}
]);
// 組件名稱(chēng)校驗(yàn)
if (!componentName.trim() || /[^A-Za-z0-9]/g.test(componentName)) {
log.error(
`Error: Please input a correct name. ${chalk.bold('( PascalCase )')}`
);
return;
}
// 項(xiàng)目中組件文件路徑 Vue CLI 創(chuàng)建的項(xiàng)目中默認(rèn)路徑為 src/components
const baseDir = `${api.getCwd()}/src/components`;
// 遍歷組件文件 返回組件路徑列表
const existComponent = glob.sync(`${baseDir}/*`);
// 替換組件路徑列表中的基礎(chǔ)路徑 返回組件名稱(chēng)列表
const existComponentName = existComponent.map((name) =>
name.replace(`${baseDir}/`, '')
);
// 判斷組件是否已存在
const isExist = existComponentName.some((name) => {
// 正則表達(dá)式匹配從控制臺(tái)輸入的組件名稱(chēng)是否已經(jīng)存在
const reg = new RegExp(
`^(${componentName}.[vue|jsx|tsx])$|^(${componentName})$`,
'g'
);
return reg.test(name);
});
// 存在則報(bào)錯(cuò)并退出
if (isExist) {
log.error(`Error: Component ${chalk.bold(componentName)} already exists.`);
return;
}
// 交互式命令行 獲取組件信息
// componentType {'sfc'|'tsx'|'jsx'} 組件類(lèi)型 默認(rèn) sfc
// componentStyleType {'.css'|'.scss'|'.sass'|'.less'|'.stylus'} 組件樣式類(lèi)型 默認(rèn) .scss
// shouldMkdir {boolean} 是否需要為組件創(chuàng)建文件夾 默認(rèn) 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
}
]);
// 根據(jù)不同的組件類(lèi)型 生成對(duì)應(yīng)的 template 路徑
let src = path.resolve(
__dirname,
`../generator/template/component/${componentType}/Template${suffix(
componentType
)}`
);
// 組件目標(biāo)路徑 默認(rèn)未生成組件文件夾
let dist = `${baseDir}/${componentName}${suffix(componentType)}`;
// 根據(jù)不同的組件樣式類(lèi)型 生成對(duì)應(yīng)的 template 路徑
let styleSrc = path.resolve(
__dirname,
`../generator/template/component/style/index${componentStyleType}`
);
// 組件樣式目標(biāo)路徑 默認(rèn)未生成組件文件夾
let styleDist = `${baseDir}/${componentName}${componentStyleType}`;
// 需要為組件創(chuàng)建文件夾
if (shouldMkdir) {
try {
// 創(chuàng)建組件文件夾
fs.mkdirSync(`${baseDir}/${componentName}`);
// 修改組件目標(biāo)路徑
dist = `${baseDir}/${componentName}/${componentName}${suffix(
componentType
)}`;
// 修改組件樣式目標(biāo)路徑
styleDist = `${baseDir}/${componentName}/index${componentStyleType}`;
} catch (e) {
log.error(e);
return;
}
}
// 生成 SFC/TSX/JSX 及 CSS/SCSS/Sass/Less/Stylus
try {
// 讀取組件 template
// 替換組件名稱(chēng)為控制臺(tái)輸入的組件名稱(chēng)
const template = fs
.readFileSync(src)
.toString()
.replace(/helloworld/gi, componentName);
// 讀取組件樣式 template
// 替換組件類(lèi)名為控制臺(tái)輸入的組件名稱(chēng)
const style = fs
.readFileSync(styleSrc)
.toString()
.replace(/helloworld/gi, componentName);
if (componentType === 'sfc') {
// 創(chuàng)建的組件類(lèi)型為 SFC 則將組件樣式 template 注入 <style></style> 標(biāo)簽中并添加樣式類(lèi)型
fs.writeFileSync(
dist,
template
// 替換組件樣式為 template 并添加樣式類(lèi)型
.replace(
/<style>\s<\/style>/gi,
() =>
`<style${
// 當(dāng)組件樣式類(lèi)型為 CSS 時(shí)不需要添加組件樣式類(lèi)型
componentStyleType !== '.css'
? ` lang="${
// 當(dāng)組件樣式類(lèi)型為 Stylus 時(shí)需要做一下特殊處理
componentStyleType === '.styl'
? 'stylus'
: componentStyleType.replace('.', '')
}"`
: ''
}>\n${style}</style>`
)
);
} else {
// 創(chuàng)建的組件類(lèi)型為 TSX/JSX 則將組件樣式 template 注入單獨(dú)的樣式文件
fs.writeFileSync(
dist,
template.replace(
// 當(dāng)不需要?jiǎng)?chuàng)建組件文件夾時(shí) 樣式文件應(yīng)該以 [組件名稱(chēng)].[組件樣式類(lèi)型] 的方式引入
/import '\.\/index\.css';/gi,
`import './${
shouldMkdir ? 'index' : `${componentName}`
}${componentStyleType}';`
)
);
fs.writeFileSync(styleDist, style);
}
// 組件創(chuàng)建完成 打印組件名稱(chēng)和組件文件路徑
log.success(
`Success: Component ${chalk.bold(
componentName
)} was created in ${chalk.bold(dist)}`
);
} catch (e) {
log.error(e);
return;
}
};
上面的代碼是 add-component 指令的執(zhí)行邏輯,比較長(zhǎng),可以稍微有點(diǎn)耐心閱讀一下。
由于 add-page 指令的執(zhí)行邏輯還在開(kāi)發(fā)過(guò)程中,這里就不貼出來(lái)了,大家可以自己思考一下,歡迎有好想法的同學(xué)為這個(gè)倉(cāng)庫(kù)提 PR:vue-cli-plugin-generators[3]。
現(xiàn)在我們可以來(lái)執(zhí)行一下 yarn add-component 來(lái)體驗(yàn)一下功能了:

這里我們分別創(chuàng)建了 SFC/TSX/JSX 三種類(lèi)型的組件,目錄結(jié)構(gòu)如下:
.
├── HelloJSX
│ ├── HelloJSX.jsx
│ └── index.scss
├── HelloSFC
│ └── HelloSFC.vue
├── HelloTSX
│ ├── HelloTSX.tsx
│ └── index.scss
└── HelloWorld.vue
其中 HelloWorld.vue 是 Vue CLI 創(chuàng)建時(shí)自動(dòng)生成的。
對(duì)應(yīng)的文件中組件名稱(chēng)和組件樣式類(lèi)名也被替換了。
到這里我們就算完成了一個(gè)能夠自動(dòng)生成組件的 Vue CLI 插件了。
但是,還不夠!
Prompt
Prompt 會(huì)在創(chuàng)建新的項(xiàng)目或者在項(xiàng)目中添加新的插件時(shí)輸出交互式命令行,獲取 Generator 需要的信息,這些信息會(huì)在用戶(hù)輸入完成后以 options 的形式傳遞給 Generator,供 Generator 中的 ejs 模板渲染。
Prompt 應(yīng)該放在根目錄下,被命名為 prompt.js,它會(huì)在調(diào)用 vue add 或者 vue invoke 時(shí)被執(zhí)行,執(zhí)行順序位于 Generator 前。
在我們的插件中,我們并不需要在調(diào)用 vue add 或者 vue invoke 時(shí)就創(chuàng)建組件/頁(yè)面,因此不需要在這個(gè)時(shí)候獲取組件的相關(guān)信息。
UI
UI 會(huì)在使用 vue ui 指令打開(kāi)圖形化操作界面后給到用戶(hù)一個(gè)圖形化的插件配置功能。
這個(gè)部分的內(nèi)容比較復(fù)雜,講解起來(lái)比較費(fèi)勁,大家可以到官網(wǎng)上閱讀:UI 集成[4]。
在我們的插件中,我們并不需要使用 vue ui 啟動(dòng)圖形化操作界面,因此不需要編寫(xiě) UI 相關(guān)的代碼。
深入學(xué)習(xí)
我們可以到 `Vue CLI` 插件開(kāi)發(fā)指南[5]中查看更詳細(xì)的指南,建議閱讀英文文檔,沒(méi)有什么教程比官方文檔更加合適了。
總結(jié)
一個(gè)優(yōu)秀的 Vue CLI 插件應(yīng)該有四個(gè)部分:
.
├── README.md
├── generator.js # Generator(可選)
├── index.js # Service 插件
├── package.json
├── prompts.js # Prompt 文件(可選)
└── ui.js # Vue UI 集成(可選)
Generator可以為你的項(xiàng)目創(chuàng)建文件、編輯文件、添加依賴(lài)。Service可以為你的項(xiàng)目修改Webpack配置、創(chuàng)建vue-cli-service命令、修改vue-cli-service命令。Prompt會(huì)在創(chuàng)建新的項(xiàng)目或者在項(xiàng)目中添加新的插件時(shí)輸出交互式命令行,獲取Generator需要的信息,這些信息會(huì)在用戶(hù)輸入完成后以options的形式傳遞給Generator,供Generator中的ejs模板渲染。UI會(huì)在使用vue ui指令打開(kāi)圖形化操作界面后給到用戶(hù)一個(gè)圖形化的插件配置功能。
四個(gè)部分各司其職才能更好地實(shí)現(xiàn)一個(gè)完美的插件!
本教程的插件完整代碼放在了我的 GitHub 上,歡迎大家 Star:vue-cli-plugin-generators[6]
也歡迎大家通過(guò) npm/yarn 安裝到自己的項(xiàng)目中體驗(yàn)~
關(guān)注 「Hello FE」 獲取更多實(shí)戰(zhàn)教程
參考資料
`Vue CLI` 插件開(kāi)發(fā)指南[7]
參考資料
vue-cli-plugin-generators: https://github.com/wjq990112/vue-cli-plugin-generators
[2]命名和可發(fā)現(xiàn)性: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html#%E5%91%BD%E5%90%8D%E5%92%8C%E5%8F%AF%E5%8F%91%E7%8E%B0%E6%80%A7
[3]vue-cli-plugin-generators: https://github.com/wjq990112/vue-cli-plugin-generators
[4]UI 集成: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html#ui-%E9%9B%86%E6%88%90
[5]Vue CLI 插件開(kāi)發(fā)指南: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html
vue-cli-plugin-generators: https://github.com/wjq990112/vue-cli-plugin-generators
[7]Vue CLI 插件開(kāi)發(fā)指南: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html


