Vue3 組件庫工程化實戰 --Element3

本文作者:花果山然叔

Element3 組件庫工程化實戰

隨着對前端功能和性能的不斷提高,前端早就不是一段內嵌於頁面的一段 JS 代碼了。已經進化爲一個系統複雜的工程了。 下面我就結合 element3 組件庫的搭建經驗。帶大家搭建一個 mini 版組件庫。

https://github.com/hug-sun/mini-element

一、前端工程化是什麼

前端工程化概述 https://juejin.im/post/6844904073817227277

前端工程化大體可以分爲四個方面內容。

  1. 模塊化一個文件分拆爲多個互相依賴的文件,最後進行統一打包和加載,保證高效多人協作。
  1. 組件化 相對於文件的拆分,組件是對於 UI 層面的拆分,每一個組件需要包括對應的 CSS、圖片、JS 邏輯、視圖模板等並且能完成一個獨立的功能。

  2. 自動化

  1. 規範性

二、實戰步驟

  1. 開發規範

  • JS 代碼規範

  • airbnb - 中文版

  • standard (24.5k star) 中文版

  • 百度前端編碼規範 3.9k

  • CSS 代碼規範

  • styleguide 2.3k

  • spec 3.9k

1.1 項目目錄結構

.
├── build        # 編譯腳本
├── coverage         # 覆蓋率報告
├── examples       # 代碼範例
├── lib         # CSS樣式 編譯後
├── node_modules 
├── packages      # 組件代碼
├── rollup-plugin-vue
├── scripts       # 腳本 發佈、提交信息檢查
├── src         # 通用代碼
├── test        # 測試
└── types        # TS類型定義

1.2 文件命名規範

.
├── button                   
│   ├── Button.vue        # 組件SFC
│   ├── __tests__        
│   │   └── Button.spec.js   # 測試文件
│   └── index.js        # 組件入口

1.3 代碼樣式規範(ESLint)

  • JS 代碼規範

  • airbnb - 中文版

  • standard (24.5k star) 中文版

  • 百度前端編碼規範 3.9k

  • CSS 代碼規範

  • styleguide 2.3k

  • spec 3.9k

# .eslintrc.js
module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
    node: true,
    jest: true
  },
  globals: {
    ga: true,
    chrome: true,
    __DEV__: true
  },
  extends: [
    'plugin:json/recommended',
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/prettier'
  ],
  parserOptions: {
    parser: 'babel-eslint'
  },
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'prettier/prettier''error'
  }
}
# .eslintignore
src/utils/popper.js
src/utils/date.js
examples/play
*.sh
node_modules
lib
coverage
*.md
*.scss
*.woff
*.ttf
src/index.js
dist
yarn add eslint
yarn add eslint-formatter-pretty
yarn add eslint-plugin-json
yarn add eslint-plugin-prettier
yarn add eslint-plugin-vue
yarn add @vue/eslint-config-prettier
yarn add babel-eslint
yarn add prettier

package.json

{
"scripts"{
   "lint""eslint --no-error-on-unmatched-pattern --ext .vue --ext .js --ext .jsx packages/**/ src/**/ --fix",
  },
}

1.6 Git 版本規範

分支管理

一般項目分主分支(master)和其他分支。 當有團隊成員要開發新功能 (Feather) 或改 BUG(Fix) 時,就從 master 分支開一個新的分支。 比如你修改一個 Bug 應該用 bug 的編號作爲分支(例:[Fix:12323])

Commit 規範

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
複製代碼

大致分爲三個部分 (使用空行分割):

  1. 標題行: 必填, 描述主要修改類型和內容

  2. 主題內容: 描述爲什麼修改, 做了什麼樣的修改, 以及開發的思路等等

  3. 頁腳註釋: 可以寫註釋,BUG 號鏈接

  1. scope: commit 影響的範圍, 比如: route, component, utils, build...

  2. subject: commit 的概述

  3. body: commit 具體修改內容, 可以分爲多行.

  4. footer: 一些備註, 通常是 BREAKING CHANGE 或修復的 bug 的鏈接.

示例

fix(修復 BUG)

如果修復的這個 BUG 隻影響當前修改的文件,可不加範圍。如果影響的範圍比較大,要加上範圍描述。

例如這次 BUG 修復影響到全局,可以加個 global。如果影響的是某個目錄或某個功能,可以加上該目錄的路徑,或者對應的功能名稱。

// 示例1
fix(global):修復checkbox不能複選的問題
// 示例2 下面圓括號裏的 common 爲通用管理的名稱
fix(common): 修復字體過小的BUG,將通用管理下所有頁面的默認字體大小修改爲 14px
// 示例3
fix: value.length -> values.length
複製代碼
feat(添加新功能或新頁面)
feat: 添加網站主頁靜態頁面

這是一個示例,假設對點檢任務靜態頁面進行了一些描述。
 
這裏是備註,可以是放BUG鏈接或者一些重要性的東西。
複製代碼
chore(其他修改)

chore 的中文翻譯爲日常事務、例行工作,顧名思義,即不在其他 commit 類型中的修改,都可以用 chore 表示。

chore: 將表格中的查看詳情改爲詳情
複製代碼

其他類型的 commit 和上面三個示例差不多,就不說了。

自動化提交驗證

驗證 git commit 規範,主要通過 git 的 pre-commit 鉤子函數來進行。當然,你還需要下載一個輔助工具來幫助你進行驗證。

下載輔助工具

npm i -D husky

package.json 加上下面的代碼

"husky"{
  "hooks"{
    "pre-commit""npm run lint",
    "commit-msg""node script/verify-commit.js",
    "pre-push""npm test"
  }
}
複製代碼

然後在你項目根目錄下新建一個文件夾 script,並在下面新建一個文件 verify-commit.js,輸入以下代碼:

const msgPath = process.env.HUSKY_GIT_PARAMS
const msg = require('fs')
.readFileSync(msgPath, 'utf-8')
.trim()

const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/

if (!commitRE.test(msg)) {
    console.log()
    console.error(`
        不合法的 commit 消息格式。
        請查看 git commit 提交規範:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
    `)

    process.exit(1)
}
複製代碼

現在來解釋下各個鉤子的含義:

  1. "pre-commit": "npm run lint",在 git commit 前執行 npm run lint 檢查代碼格式。

  2. "commit-msg": "node script/verify-commit.js",在 git commit 時執行腳本 verify-commit.js 驗證 commit 消息。如果不符合腳本中定義的格式,將會報錯。

  3. "pre-push": "npm test",在你執行 git push 將代碼推送到遠程倉庫前,執行 npm test 進行測試。如果測試失敗,將不會執行這次推送。

/scripts/verifyCommit.js

// Invoked on the commit-msg git hook by yorkie.

const chalk = require('chalk')
const msgPath = process.env.GIT_PARAMS
const msg = require('fs').readFileSync(msgPath, 'utf-8').trim()

const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?(.{1,10})?: .{1,50}/
const mergeRe = /^(Merge pull request|Merge branch)/

if (!commitRE.test(msg)) {
  if (!mergeRe.test(msg)) {
    console.log(msg)
    console.error(
      `  ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
        `invalid commit message format.`
      )}\n\n` +
        chalk.red(
          `  Proper commit message format is required for automated changelog generation. Examples:\n\n`
        ) +
        `    ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
        `    ${chalk.green(
          `fix(v-model): handle events on blur (close #28)`
        )}\n\n` +
        chalk.red(
          `  See https://github.com/vuejs/vue-next/blob/master/.github/commit-convention.md for more details.\n`
        )
    )
    process.exit(1)
  }
}
  1. 模塊化與組件化

npm init -y

https://github.com/cuixiaorui/course-vue3-test/tree/main/chapters/two 參考資料

2.1 編寫 Buttun 組件

yarn add vue@next

/packages/button/Button.vue

<template>
  <button
    class="el-button"
    @click="handleClick"
    :disabled="buttonDisabled || loading"
    :autofocus="autofocus"
    :type="nativeType"
    :class="[
      type ? 'el-button--' + type : '',
      buttonSize ? 'el-button--' + buttonSize : '',
      {
        'is-disabled': buttonDisabled,
        'is-loading': loading,
        'is-plain': plain,
        'is-round': round,
        'is-circle': circle,
      },
    ]"
  >
    <i class="el-icon-loading" v-if="loading"></i>
    <i :class="icon" v-if="icon && !loading"></i>
    <span v-if="$slots.default">
      <slot></slot>
    </span>
  </button>
</template>
<script>
import { computed, inject, toRefs, unref, getCurrentInstance } from "vue";

export default {
  name: "ElButton",

  props: {
    type: {
      type: String,
      default: "default",
    },
    size: {
      type: String,
      default: "",
    },
    icon: {
      type: String,
      default: "",
    },
    nativeType: {
      type: String,
      default: "button",
    },
    loading: Boolean,
    disabled: Boolean,
    plain: Boolean,
    autofocus: Boolean,
    round: Boolean,
    circle: Boolean,
  },
  emits: ["click"],
  setup(props, ctx) {
    const { size, disabled } = toRefs(props);

    const buttonSize = useButtonSize(size);
    const buttonDisabled = useButtonDisabled(disabled);

    const handleClick = (evt) ={
      ctx.emit("click", evt);
    };

    return {
      handleClick,
      buttonSize,
      buttonDisabled,
    };
  },
};

const useButtonSize = (size) ={
  const elFormItem = inject("elFormItem"{});

  const _elFormItemSize = computed(() ={
    return unref(elFormItem.elFormItemSize);
  });

  const buttonSize = computed(() ={
    return (
      size.value ||
      _elFormItemSize.value ||
      (getCurrentInstance().proxy.$ELEMENT || {}).size
    );
  });

  return buttonSize;
};

const useButtonDisabled = (disabled) ={
  const elForm = inject("elForm"{});

  const buttonDisabled = computed(() ={
    return disabled.value || unref(elForm.disabled);
  });

  return buttonDisabled;
};
</script>

2.2 集成 Babel

yarn add babel
yarn add babel-plugin-syntax-dynamic-import
yarn add babel-plugin-syntax-jsx
yarn add babel-preset-env
yarn add @babel/plugin-proposal-optional-chaining
yarn add @babel/preset-env
yarn add @vue/babel-plugin-jsx

新建. babelrc 文件

{
  "presets"[["@babel/preset-env"{ "targets"{ "node""current" } }]],
  "plugins"[
    "syntax-dynamic-import",
    ["@vue/babel-plugin-jsx"],
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-nullish-coalescing-operator"
  ],
  "env"{
    "utils"{
      "presets"[
        [
          "env",
          {
            "loose": true,
            "modules""commonjs",
            "targets"{
              "browsers"["> 1%""last 2 versions""not ie <= 8"]
            }
          }
        ]
      ],
      "plugins"[
        [
          "module-resolver",
          {
            "root"["element-ui"],
            "alias"{
              "element-ui/src""element-ui/lib"
            }
          }
        ]
      ]
    },
    "test"{
      "plugins"["istanbul"],
      "presets"[["env"{ "targets"{ "node""current" } }]]
    },
    "esm"{
      "presets"[["@babel/preset-env"{ "modules"false }]]
    }
  }
}

2.2 集成 VTU

安裝依賴

yarn add jest
# 此版本這個支持Vue3.0
yarn add vue-jest@5.0.0-alpha.5
yarn add babel-jest             
yarn add @vue/compiler-sfc@3.0.2
yarn add @vue/test-utils@next
yarn add typescript

jest.config.js

module.exports = {
  testEnvironment: 'jsdom', // 默認JSdom
  roots: [
    '<rootDir>/src',
    '<rootDir>/packages',
  ], // 
  transform: {
    '^.+\\.vue$''vue-jest', // vue單文件
    '^.+\\js$''babel-jest' // esm最新語法 import
  },
  moduleFileExtensions: ['vue''js''json''jsx''ts''tsx''node'],
  testMatch: ['**/__tests__/**/*.spec.js'],
  // 別名
  moduleNameMapper: {
    '^element-ui(.*)$''<rootDir>$1',
    '^main(.*)$''<rootDir>/src$1'
  }
}

/packages/button/tests/Button.spec.js

import Button from "../Button.vue";
import { mount } from "@vue/test-utils";

it("content"() ={
  const Comp = {
    template: `<div><Button>默認按鈕</Button></div>`,
  };

  const wrapper = mount(Comp, {
    global: {
      components: {
        Button,
      },
    },
  });

  expect(wrapper.findComponent({ name: "ElButton" }).text()).toContain(
    "默認按鈕"
  );
});

describe("size"() ={
  it("should have a el-button--mini class when set size prop value equal to mini"() ={
    const wrapper = mount(Button, {
      props: {
        size: "mini",
      },
    });

    expect(wrapper.classes()).toContain("el-button--mini");
  });

  it("should have a el-button--mini class by elFormItem "() ={
    const wrapper = mount(Button, {
      global: {
        provide: {
          elFormItem: {
            elFormItemSize: "mini",
          },
        },
      },
    });

    expect(wrapper.classes()).toContain("el-button--mini");
  });
  it("should have a el-button--mini class by $ELEMENT value "() ={
    const wrapper = mount(Button, {
      global: {
        config: {
          globalProperties: {
            $ELEMENT{
              size: "mini",
            },
          },
        },
      },
    });

    expect(wrapper.classes()).toContain("el-button--mini");
  });
});

it("type"() ={
  const wrapper = mount(Button, {
    props: {
      type: "primary",
    },
  });

  expect(wrapper.classes()).toContain("el-button--primary");
});

it("plain"() ={
  const wrapper = mount(Button, {
    props: {
      plain: true,
    },
  });

  expect(wrapper.classes()).toContain("is-plain");
});
it("round"() ={
  const wrapper = mount(Button, {
    props: {
      round: true,
    },
  });

  expect(wrapper.classes()).toContain("is-round");
});

it("circle"() ={
  const wrapper = mount(Button, {
    props: {
      circle: true,
    },
  });

  expect(wrapper.classes()).toContain("is-circle");
});
it("loading"() ={
  const wrapper = mount(Button, {
    props: {
      loading: true,
    },
  });

  expect(wrapper.find(".el-icon-loading").exists()).toBe(true);
  expect(wrapper.classes()).toContain("is-loading");
});

describe("icon"() ={
  it("should show icon element"() ={
    const wrapper = mount(Button, {
      props: {
        icon: "el-icon-edit",
      },
    });

    expect(wrapper.find(".el-icon-edit").exists()).toBe(true);
  });

  it("should not show icon element when set loading prop equal to true"() ={
    const wrapper = mount(Button, {
      props: {
        loading: true,
        icon: "el-icon-edit",
      },
    });

    expect(wrapper.find(".el-icon-edit").exists()).toBe(false);
  });
});

describe("click"() ={
  it("should emit click event "() ={
    const wrapper = mount(Button);

    wrapper.trigger("click");

    expect(wrapper.emitted("click")).toBeTruthy();
  });

  it("should not emit click event when disabled equal to true"() ={
    const wrapper = mount(Button, {
      props: {
        disabled: true,
      },
    });

    wrapper.trigger("click");

    expect(wrapper.emitted("click")).toBeFalsy();
  });

  it("should not emit click event when elForm disabled equal to true"() ={
    const wrapper = mount(Button, {
      global: {
        provide: {
          elForm: {
            disabled: true,
          },
        },
      },
    });

    wrapper.trigger("click");

    expect(wrapper.emitted("click")).toBeFalsy();
  });

  it("should not emit click event when loading prop equal to true"() ={
    const wrapper = mount(Button, {
      props: {
        loading: true,
      },
    });

    wrapper.trigger("click");

    expect(wrapper.emitted("click")).toBeFalsy();
  });
});

it("native-type"() ={
  const wrapper = mount(Button, {
    props: {
      nativeType: "button",
    },
  });

  expect(wrapper.attributes("type")).toBe("button");
});

測試

"test""jest --runInBand"# 序列化執行

2.4 樣式打包

yarn add gulp
yarn add gulp-autoprefixer
yarn add gulp-sass
yarn add gulp-cssmin

# cp-cli
yarn add cp-cli
yarn add tslib

/bin/gen-cssfile

package.json

"build:theme""gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",

2.4 Rollup 打包

https://www.rollupjs.com/ Rollup 中文網 https://juejin.im/post/6844903731343933453 使用 rollup 打包 JS

yarn add rollup
yarn add rollup-plugin-peer-deps-external
yarn add rollup-plugin-scss
yarn add rollup-plugin-terser
yarn add rollup-plugin-vue
yarn add @rollup/plugin-node-resolve
yarn add @rollup/plugin-commonjs
yarn add @rollup/plugin-json
yarn add @rollup/plugin-replace
yarn add @rollup/plugin-babel
yarn add rollup-plugin-vue

Package.json

"build:next""rollup -c",
import pkg from './package.json'
// 等 rollup-plugin-vue 發版後在切換官方版
// 暫時先用本地的 rollup-plugin-vue
// 修復了 render 函數的編譯問題,但是還沒發版
// import vuePlugin from 'rollup-plugin-vue'
const vuePlugin = require('./rollup-plugin-vue/index')
import scss from 'rollup-plugin-scss'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import replace from '@rollup/plugin-replace'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'

const name = 'Element3'

const createBanner = () ={
  return `/*!
  * ${pkg.name} v${pkg.version}
  * (c) ${new Date().getFullYear()} kkb
  * @license MIT
  */`
}

const createBaseConfig = () ={
  return {
    input: 'src/entry.js',
    external: ['vue'],
    plugins: [
      peerDepsExternal(),
      babel(),
      resolve({
        extensions: ['.vue''.jsx']
      }),
      commonjs(),
      json(),
      vuePlugin({
        css: true
      }),
      scss()
    ],
    output: {
      sourcemap: false,
      banner: createBanner(),
      externalLiveBindings: false,
      globals: {
        vue: 'Vue'
      }
    }
  }
}

function mergeConfig(baseConfig, configB) {
  const config = Object.assign({}, baseConfig)
  // plugin
  if (configB.plugins) {
    baseConfig.plugins.push(...configB.plugins)
  }

  // output
  config.output = Object.assign({}, baseConfig.output, configB.output)

  return config
}

function createFileName(formatName) {
  return `dist/element3-ui.${formatName}.js`
}

// es-bundle
const esBundleConfig = {
  plugins: [
    replace({
      __DEV__: `(process.env.NODE_ENV !== 'production')`
    })
  ],
  output: {
    file: createFileName('esm-bundler'),
    format: 'es'
  }
}

// es-browser
const esBrowserConfig = {
  plugins: [
    replace({
      __DEV__: true
    })
  ],
  output: {
    file: createFileName('esm-browser'),
    format: 'es'
  }
}

// es-browser.prod
const esBrowserProdConfig = {
  plugins: [
    terser(),
    replace({
      __DEV__: false
    })
  ],
  output: {
    file: createFileName('esm-browser.prod'),
    format: 'es'
  }
}

// cjs
const cjsConfig = {
  plugins: [
    replace({
      __DEV__: true
    })
  ],
  output: {
    file: createFileName('cjs'),
    format: 'cjs'
  }
}
// cjs.prod
const cjsProdConfig = {
  plugins: [
    terser(),
    replace({
      __DEV__: false
    })
  ],
  output: {
    file: createFileName('cjs.prod'),
    format: 'cjs'
  }
}

// global
const globalConfig = {
  plugins: [
    replace({
      __DEV__: true,
      'process.env.NODE_ENV'true
    })
  ],
  output: {
    file: createFileName('global'),
    format: 'iife',
    name
  }
}
// global.prod
const globalProdConfig = {
  plugins: [
    terser(),
    replace({
      __DEV__: false
    })
  ],
  output: {
    file: createFileName('global.prod'),
    format: 'iife',
    name
  }
}

const formatConfigs = [
  esBundleConfig,
  esBrowserProdConfig,
  esBrowserConfig,
  cjsConfig,
  cjsProdConfig,
  globalConfig,
  globalProdConfig
]

function createPackageConfigs() {
  return formatConfigs.map((formatConfig) ={
    return mergeConfig(createBaseConfig(), formatConfig)
  })
}

export default createPackageConfigs()

2.3 編寫 Entry 入口

  1. 自動化

3.1 文檔自動化

文檔自動化其實就是根據代碼自動生成開發文檔。比如 element3 項目中的。https://element3-ui.com/ 其實可以用 StoryBook。 這個我們後面寫專題更新。大家保持關注。

3.2 規範檢查

yarn add husky

.huskyrc

{
    "hooks"{
        "pre-commit""npm run lint",
        "commit-msg""node scripts/verifyCommit.js",
        "pre-push""npm run test"
  },
}

3.4 迴歸測試

GitHub Action

.github/workflows/main.yml

3.3 持續集成 CI

Travis CI 提供的是持續集成服務,它僅支持 Github,不支持其他代碼託管。它需要綁定 Github 上面的項目,還需要該項目含有構建或者測試腳本。只要有新的代碼,就會自動抓取。然後,提供一個虛擬機環境,執行測試,完成構建,還能部署到服務器。只要代碼有變更,就自動運行構建和測試,反饋運行結果。確保符合預期以後,再將新代碼集成到主幹。

這個項目需要 Travis 在提交後自動進行測試並且向 codecov 提供測試報告。

登錄 TravicCI 網站

登錄 https://www.travis-ci.org / 網站

使用 github 賬號登錄系統

配置. travis.yml

運行自動化測試框架

language: node_js               # 項目語言,node 項目就按照這種寫法就OK了
node_js:
- 13.2.0    # 項目環境
cache:    # 緩存 node_js 依賴,提升第二次構建的效率
  directories:
  - node_modules
test:
  - npm run test # 運行自動測試框架

參考教程:Travis CI Tutorial

上傳配置到 github

啓動持續集成

通過 github 賬號登錄 travis

獲取持續集成通過徽標

將上面 URL 中的 {GitHub 用戶名} 和 {項目名稱} 替換爲自己項目的即可,最後可以將集成完成後的 markdown 代碼貼在自己的項目上

http://img.shields.io/travis/{GitHub 用戶名}/{項目名稱}.svg
複製代碼

3.5 持續交付 CD - 上傳 Npm 庫

創建發佈腳本

publish.sh

#!/usr/bin/env bash
npm config get registry # 檢查倉庫鏡像庫
npm config set registry=http://registry.npmjs.org
echo '請進行登錄相關操作:'
npm login # 登陸
echo "-------publishing-------"
npm publish # 發佈
npm config set registry=https://registry.npm.taobao.org # 設置爲淘寶鏡像
echo "發佈完成"
exit

執行發佈

./publish.sh
複製代碼

填入 github 用戶名密碼後

3.7 覆蓋率測試 Codecov

Codecov 是一個開源的測試結果展示平臺,將測試結果可視化。Github 上許多開源項目都使用了 Codecov 來展示單測結果。Codecov 跟 Travis CI 一樣都支持 Github 賬號登錄,同樣會同步 Github 中的項目。

yarn add codecov
 "scripts"{
 ...,
 "codecov""codecov"
 }
  1. 其他

4.1 標準的 README 文檔

4.2 開源許可證

每個開源項目都需要配置一份合適的開源許可證來告知所有瀏覽過我們的項目的用戶他們擁有哪些權限,具體許可證的選取可以參照阮一峯前輩繪製的這張圖表:

那我們又該怎樣爲我們的項目添加許可證了?其實 Github 已經爲我們提供了非常簡便的可視化操作: 我們平時在逛 github 網站的時候,發現不少項目都在 README.md 中添加徽標,對項目進行標記和說明,這些小圖標給項目增色不少,不僅簡單美觀,而且還包含清晰易懂的信息。

  1. 打開我們的開源項目並切換至 Insights 面板

  2. 點擊 Community 標籤

  3. 如果您的項目沒有添加 License,在 Checklist 裏會提示您添加許可證,點擊 Add 按鈕就進入可視化操作流程了

4.3 申請開源徽標 (Badge)

Github 徽章 https://docs.github.com/cn/free-pro-team@latest/actions/managing-workflow-runs/adding-a-workflow-status-badge

三、附錄

3.1 Vue 組件與插件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta  />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="/node_modules/vue/dist/vue.global.js"></script>
    <script src="/dist/element3-ui.global.js"></script>
    <link href="/lib/theme-chalk/index.css" rel="stylesheet" />
    <style></style>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { createApp, reactive, computed, watchEffect } = Vue;

      const MyButton = {
        name: "MyButton",
        data: function () {
          return {
            count: 0,
          };
        },
        template:
          '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
      };

      // 添加插件
      MyButton.install = (app) => app.component("MyButton", MyButton);

      // 組件庫
      const Element = {
          MyButton,
          install: app ={
              app.use(MyButton)
          }
      }

      const MyComponent = {
        template: `
                <my-button />
            `,
      };

      createApp(MyComponent)
        // .use(MyButton)
        .use(Element)
        .mount("#app");
    </script>
  </body>
</html>

3.2 rollup 打包

rollup 是一款小巧的 javascript 模塊打包工具,更適合於庫應用的構建工具; 可以將小塊代碼編譯成大塊複雜的代碼,基於 ES6 modules, 它可以讓你的 bundle 最小化,有效減少文件請求大小, vue 在開發的時候用的是 webpack, 但是最後將文件打包在一起的時候用的是 rollup.js

首次發表在個人博客

https://juejin.im/post/6844903570974703629 Rollup 基礎

Button

/src/MyButton.js

export default {
  name: "MyButton",
  data: function () {
    return {
      count: 0,
    };
  },
  template:
    '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
};

入口

/src/entry.js

import MyButton from "./MyButton";
import SfcButton from "./SfcButton.vue";
import JsxButton from "./JsxButton.vue";

// 添加插件
MyButton.install = (app) => app.component("MyButton", MyButton);
SfcButton.install = (app) => app.component("SfcButton", SfcButton);
JsxButton.install = (app) => app.component("JsxButton", JsxButton);

// 組件庫
const Element = {
  MyButton,
  SfcButton,
  JsxButton,
  install: (app) ={
    app.use(MyButton);
    app.use(SfcButton);
    app.use(JsxButton);
  },
};

export default Element;

格式聲明

https://juejin.im/post/6885542715782594568 AMD CMD UMD 區別

const vuePlugin = require("../../rollup-plugin-vue/index");
import babel from "@rollup/plugin-babel";
// import vuePlugin from "rollup-plugin-vue";
const es = {
  input: "src/entry.js",
  output: {
    file: "dist/index.js",
    name: "Element",
    format: "iife",
    globals: {
      vue: "Vue",
    },
  },
  external: ["vue"],
  plugins: [
    babel(),
    vuePlugin({
      css: true,
    }),
  ],
};

import { terser } from "rollup-plugin-terser";
const minEs = {
  input: "src/entry.js",
  external: ["vue"],
  output: {
    file: "dist/index.min.js",
    name: "Element",
    format: "umd",
  },
  plugins: [
    babel(),
    vuePlugin({
      css: true,
    }),
    terser(),
  ],
};

const cjs = {
  input: "src/entry.js",
  external: ["vue"],
  output: {
    file: "dist/index.cjs.js",
    name: "Element",
    format: "cjs",
  },
  plugins: [
    babel(),
    vuePlugin({
      css: true,
    }),
  ],
};

export default [es, minEs, cjs];

測試頁面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta  />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <script src="/node_modules/vue/dist/vue.global.js"></script>
    <script src="dist/index.js"></script>
    <style></style>
  </head>

  <body>
    <div id="app"></div>
    <script>
      const { createApp, reactive, computed, watchEffect } = Vue;

      const MyComponent = {
        template: `
                <my-button />
                <sfc-button />
                <jsx-button />
            `,
      };

      createApp(MyComponent)
        .use(Element)
        .mount("#app");
    </script>
  </body>
</html>

單文件組件

<template>
  <button>Sfc 666</button>
</template>
<script>
export default {
  name: "SfcButton",
};
</script>
const vuePlugin = require("../../rollup-plugin-vue/index");
// import vuePlugin from "rollup-plugin-vue";
# plugin
vuePlugin({
css: true,
}),

JSX 支持

jsx 的定義

JSX 是一種類似於 XML 的 JavaScript 語法擴展 JSX 不是由引擎或瀏覽器實現的。相反,我們將使用像 Babel 這樣的轉換器將 JSX 轉換爲常規 JavaScript。基本上,JSX 允許我們在 JavaScript 中使用類似 HTML 的語法。

jsx 的優勢

  1. 可以將 模版分離 這樣模版的每個部分更加獨立,又可以隨機的組合,複用性更高。相比與組件的組合,粒度更細

  2. 使用 js 可配置每項要渲染的 dom,更加動態可配置化

import babel from "@rollup/plugin-babel";
# plugin
babel(),
<script>
export default {
  name: "JsxButton",
  render() {
    return <button>JSX 666</button>;
  },
};
</script>

3.3 Vue-cli 插件開發

請參考 https://juejin.cn/post/6899334776860180494

大家可以關注 Element3 和花果山團隊我們會持續更新最棒的內容。 也歡迎 star 和 pr

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sieRdVFVeM2jj8oghYLEpw