Vue CLI 插件開發實戰——10 分鐘實現組件自動生成

前言

近期工做的過程當中跟 Vue CLI 的插件打交道比較多,想了想本身在學校寫項目的時候最煩的就是項目建立以後手動建立組件/頁面和配置路由,因而突發奇想決定寫一個腳手架的插件,自動實現建立組件/頁面和配置路由的功能css

本文會一步一步教你如何編寫一個本身的 Vue CLI 插件,併發布至 npm,爲全部由於這個問題而煩惱的同窗解放雙手。html

關注 「Hello FE」 獲取更多實戰教程,正好最近在抽獎,查看歷史文章便可獲取抽獎方法~vue

本教程的插件完整代碼放在了個人 GitHub 上,歡迎你們 Starvue-cli-plugin-generatorsgit

同時,我也將這個插件發佈到了 npm,你們能夠直接使用 npm 安裝並體驗添加組件的能力。github

PS:添加頁面和配置路由的能力還在開發中。web

體驗方式:正則表達式

  1. 經過 npm 安裝
npm install vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
複製代碼
  1. 經過 yarn 安裝
yarn add vue-cli-plugin-generators -D
vue invoke vue-cli-plugin-generators
複製代碼
  1. 經過 Vue CLI 安裝(推薦)
vue add vue-cli-plugin-generators
複製代碼

注意:必定要注意是複數形式的 generators,不是單數形式的 generatorgenerator 被前輩的佔領了。vue-cli

廢話很少說,咱們直接開始吧!npm

前置知識

要作好一個 Vue CLI 插件,除了要了解 Vue CLI 插件的開發規範以外,咱們還須要瞭解幾個 npm 包:json

  • chalk 讓你的控制檯輸出好看一點,爲文字或背景上色
  • glob 讓你可使用 Shell 腳本的方式匹配文件
  • inquirer 讓你可使用交互式的命令行來獲取須要的信息

主要出現的 npm 包就只有這三個,其餘的都是基於 Node.js 的各類模塊,好比 fspath,瞭解過 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 應該放在根目錄下,被命名爲 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 add vue-cli-plugin-generators 的項目中,package.json 發生了變化:

package.json

添加了兩條指令,讓咱們能夠經過 yarn add-componentyarn add-page 去添加組件/頁面。

雖然添加了這兩條指令,可是如今這兩條指令尚未被註冊到 vue-cli-service 中,這時候咱們就須要開始編寫 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-componentadd-page 指令的回調函數單獨抽了出來,分別放在了 src/add-component.jssrc/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 來體驗一下功能了:

yarn add-component

這裏咱們分別建立了 SFC/TSX/JSX 三種類型的組件,目錄結構以下:

.
├── HelloJSX
│   ├── HelloJSX.jsx
│   └── index.scss
├── HelloSFC
│   └── HelloSFC.vue
├── HelloTSX
│   ├── HelloTSX.tsx
│   └── index.scss
└── HelloWorld.vue
複製代碼

其中 HelloWorld.vueVue CLI 建立時自動生成的。

對應的文件中組件名稱和組件樣式類名也被替換了。

到這裏咱們就算完成了一個可以自動生成組件的 Vue CLI 插件了。

可是,還不夠!

Prompt

Prompt 會在建立新的項目或者在項目中添加新的插件時輸出交互式命令行,獲取 Generator 須要的信息,這些信息會在用戶輸入完成後以 options 的形式傳遞給 Generator,供 Generator 中的 ejs 模板渲染。

Prompt 應該放在根目錄下,被命名爲 prompt.js,它會在調用 vue add 或者 vue invoke 時被執行,執行順序位於 Generator 前。

在咱們的插件中,咱們並不須要在調用 vue add 或者 vue invoke 時就建立組件/頁面,所以不須要在這個時候獲取組件的相關信息。

UI

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 上,歡迎你們 Starvue-cli-plugin-generators

也歡迎你們經過 npm/yarn 安裝到本身的項目中體驗~

關注 「Hello FE」 獲取更多實戰教程

參考資料

相關文章
相關標籤/搜索