<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          自動生成組件代碼—— Vue CLI 插件開發(fā)實戰(zhàn)

          共 22456字,需瀏覽 45分鐘

           ·

          2021-05-15 18:04

          點上方藍字關(guān)注公眾號「程序員成長指北

          前言

          近期工作的過程中跟 Vue CLI 的插件打交道比較多,想了想自己在學(xué)校寫項目的時候最煩的就是項目創(chuàng)建之后手動創(chuàng)建組件/頁面和配置路由,于是突發(fā)奇想決定寫一個腳手架的插件,自動實現(xiàn)創(chuàng)建組件/頁面和配置路由的功能。

          本文會一步一步教你如何編寫一個自己的 Vue CLI 插件,并發(fā)布至 npm,為所有因為這個問題而煩惱的同學(xué)解放雙手。

          關(guān)注 「Hello FE」 獲取更多實戰(zhàn)教程,正好最近在抽獎,查看歷史文章即可獲取抽獎方法~

          本教程的插件完整代碼放在了我的 GitHub 上,歡迎大家 Starvue-cli-plugin-generators[1]

          同時,我也將這個插件發(fā)布到了 npm,大家可以直接使用 npm 安裝并體驗添加組件的能力。

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

          體驗方式:

          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

          注意:一定要注意是復(fù)數(shù)形式的 generators,不是單數(shù)形式的 generator,generator 被前輩的占領(lǐng)了。

          廢話不多說,我們直接開始吧!

          前置知識

          要做好一個 Vue CLI 插件,除了要了解 Vue CLI 插件的開發(fā)規(guī)范之外,我們還需要了解幾個 npm 包:

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

          主要出現(xiàn)的 npm 包就只有這三個,其他的都是基于 Node.js 的各種模塊,比如 fs 和 path,了解過 Node.js 的同學(xué)應(yīng)該不陌生。

          項目初始化

          創(chuàng)建一個空的文件夾,名字最好就是你的插件的名字。

          這里我的名字是 vue-cli-plugin-generators,你可以取一個自己喜歡的名字,不過最好是見名知義的那種,比如 vue-cli-plugin-component-generator 或者 vue-cli-plugin-page-generator,一看就知道是組件生成器和頁面生成器。

          至于為什么一定要帶上 vue-cli-plugin 的前綴這個問題,可以看一下官方文檔:命名和可發(fā)現(xiàn)性[2]

          然后初始化我們的項目:

          npm init

          輸入一些基本的信息,這些信息會被寫入 package.json 文件中。

          創(chuàng)建一個基本的目錄結(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)建好了之后就可以開始編碼了。

          目錄解析

          一些不重要的文件就不講解了,主要講解一下作為一個優(yōu)秀的 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 可以為你的項目創(chuàng)建文件、編輯文件、添加依賴。

          Generator 應(yīng)該放在根目錄下,被命名為 generator.js 或者放在 generator 目錄下,被命名為 index.js,它會在調(diào)用 vue add 或者 vue invoke 時被執(zhí)行。

          來看下我們這個項目的 generator/index.js

          /**
           * @file Generator
           */

          'use strict';

          // 前置知識中提到的美化控制臺輸出的包
          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('');
          };

          所以,當我們執(zhí)行 vue add vue-cli-plugin-generators 的時候,generator/index.js會被執(zhí)行,你就可以看到你的控制臺輸出了這樣的指引信息:

          vue add

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

          package.json

          添加了兩條指令,讓我們可以通過 yarn add-component 和 yarn add-page 去添加組件/頁面。

          雖然添加了這兩條指令,但是現(xiàn)在這兩條指令還沒有被注冊到 vue-cli-service 中,這時候我們就需要開始編寫 Service 了。

          Service

          Service 可以為你的項目修改 Webpack 配置、創(chuàng)建 vue-cli-service 命令、修改 vue-cli-service 命令。

          Service 應(yīng)該放在根目錄下,被命名為 index.js,它會在調(diào)用 vue-cli-service 時被執(zhí)行。

          來看一下我們這個項目的 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 指令的回調(diào)函數(shù)單獨抽了出來,分別放在了 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} 組件名稱 默認 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 創(chuàng)建的項目中默認路徑為 src/components
            const baseDir = `${api.getCwd()}/src/components`;
            // 遍歷組件文件 返回組件路徑列表
            const existComponent = glob.sync(`${baseDir}/*`);

            // 替換組件路徑列表中的基礎(chǔ)路徑 返回組件名稱列表
            const existComponentName = existComponent.map((name) =>
              name.replace(`${baseDir}/`'')
            );

            // 判斷組件是否已存在
            const isExist = existComponentName.some((name) => {
              // 正則表達式匹配從控制臺輸入的組件名稱是否已經(jīng)存在
              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} 是否需要為組件創(chuàng)建文件夾 默認 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. )'
                )}
          `
          ,
                defaulttrue
              }
            ]);

            // 根據(jù)不同的組件類型 生成對應(yīng)的 template 路徑
            let src = path.resolve(
              __dirname,
              `../generator/template/component/${componentType}/Template${suffix(
                componentType
              )}
          `

            );
            // 組件目標路徑 默認未生成組件文件夾
            let dist = `${baseDir}/${componentName}${suffix(componentType)}`;
            // 根據(jù)不同的組件樣式類型 生成對應(yīng)的 template 路徑
            let styleSrc = path.resolve(
              __dirname,
              `../generator/template/component/style/index${componentStyleType}`
            );
            // 組件樣式目標路徑 默認未生成組件文件夾
            let styleDist = `${baseDir}/${componentName}${componentStyleType}`;

            // 需要為組件創(chuàng)建文件夾
            if (shouldMkdir) {
              try {
                // 創(chuàng)建組件文件夾
                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') {
                // 創(chuàng)建的組件類型為 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 {
                // 創(chuàng)建的組件類型為 TSX/JSX 則將組件樣式 template 注入單獨的樣式文件
                fs.writeFileSync(
                  dist,
                  template.replace(
                    // 當不需要創(chuàng)建組件文件夾時 樣式文件應(yīng)該以 [組件名稱].[組件樣式類型] 的方式引入
                    /import '\.\/index\.css';/gi,
                    `import './${
                      shouldMkdir ? 'index' : `${componentName}`
                    }
          ${componentStyleType}';`

                  )
                );
                fs.writeFileSync(styleDist, style);
              }
              // 組件創(chuàng)建完成 打印組件名稱和組件文件路徑
              log.success(
                `Success: Component ${chalk.bold(
                  componentName
                )}
           was created in ${chalk.bold(dist)}`

              );
            } catch (e) {
              log.error(e);
              return;
            }
          };

          上面的代碼是 add-component 指令的執(zhí)行邏輯,比較長,可以稍微有點耐心閱讀一下。

          由于 add-page 指令的執(zhí)行邏輯還在開發(fā)過程中,這里就不貼出來了,大家可以自己思考一下,歡迎有好想法的同學(xué)為這個倉庫提 PR:vue-cli-plugin-generators[3]。

          現(xiàn)在我們可以來執(zhí)行一下 yarn add-component 來體驗一下功能了:

          yarn add-component

          這里我們分別創(chuàng)建了 SFC/TSX/JSX 三種類型的組件,目錄結(jié)構(gòu)如下:

          .
          ├── HelloJSX
          │   ├── HelloJSX.jsx
          │   └── index.scss
          ├── HelloSFC
          │   └── HelloSFC.vue
          ├── HelloTSX
          │   ├── HelloTSX.tsx
          │   └── index.scss
          └── HelloWorld.vue

          其中 HelloWorld.vue 是 Vue CLI 創(chuàng)建時自動生成的。

          對應(yīng)的文件中組件名稱和組件樣式類名也被替換了。

          到這里我們就算完成了一個能夠自動生成組件的 Vue CLI 插件了。

          但是,還不夠!

          Prompt

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

          Prompt 應(yīng)該放在根目錄下,被命名為 prompt.js,它會在調(diào)用 vue add 或者 vue invoke 時被執(zhí)行,執(zhí)行順序位于 Generator 前。

          在我們的插件中,我們并不需要在調(diào)用 vue add 或者 vue invoke 時就創(chuàng)建組件/頁面,因此不需要在這個時候獲取組件的相關(guān)信息。

          UI

          UI 會在使用 vue ui 指令打開圖形化操作界面后給到用戶一個圖形化的插件配置功能。

          這個部分的內(nèi)容比較復(fù)雜,講解起來比較費勁,大家可以到官網(wǎng)上閱讀:UI 集成[4]。

          在我們的插件中,我們并不需要使用 vue ui 啟動圖形化操作界面,因此不需要編寫 UI 相關(guān)的代碼。

          深入學(xué)習

          我們可以到 `Vue CLI` 插件開發(fā)指南[5]中查看更詳細的指南,建議閱讀英文文檔,沒有什么教程比官方文檔更加合適了。

          總結(jié)

          一個優(yōu)秀的 Vue CLI 插件應(yīng)該有四個部分:

          .
          ├── README.md
          ├── generator.js  # Generator(可選)
          ├── index.js      # Service 插件
          ├── package.json
          ├── prompts.js    # Prompt 文件(可選)
          └── ui.js         # Vue UI 集成(可選)
          • Generator 可以為你的項目創(chuàng)建文件、編輯文件、添加依賴。

          • Service 可以為你的項目修改 Webpack 配置、創(chuàng)建 vue-cli-service 命令、修改 vue-cli-service 命令。

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

          • UI 會在使用 vue ui 指令打開圖形化操作界面后給到用戶一個圖形化的插件配置功能。

          四個部分各司其職才能更好地實現(xiàn)一個完美的插件!

          本教程的插件完整代碼放在了我的 GitHub 上,歡迎大家 Starvue-cli-plugin-generators[6]

          也歡迎大家通過 npm/yarn 安裝到自己的項目中體驗~

          關(guān)注 「Hello FE」 獲取更多實戰(zhàn)教程

          參考資料

          • `Vue CLI` 插件開發(fā)指南[7]

          參考資料

          [1]

          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 插件開發(fā)指南: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html

          [6]

          vue-cli-plugin-generators: https://github.com/wjq990112/vue-cli-plugin-generators

          [7]

          Vue CLI 插件開發(fā)指南: https://cli.vuejs.org/zh/dev-guide/plugin-dev.html


          如果覺得這篇文章還不錯
          點擊下面卡片關(guān)注我
          來個【分享、點贊、在看】三連支持一下吧

             “分享、點贊、在看” 支持一波  

          瀏覽 26
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  影音先锋三级网址 | 91 无码 国产 | 后入丰满少妇 | 日韩~欧美~中文字幕 | 夜夜干天天撸 |