随着对前端功能和性能的不断提高,前端早就不是一段内嵌于页面的一段JS代码了。已经进化为一个系统复杂的工程了。
下面我就结合element3组件库的搭建经验。带大家搭建一个mini版组件库。

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

一、前端工程化是什么

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

前端工程化大体可以分为四个方面内容。

  1. 模块化
    Element3组件库工程化实战 - 图1
    一个文件分拆为多个互相依赖的文件,最后进行统一打包和加载,保证高效多人协作。

    • JS模块 CMD AMD CommonJS 及 ES6 Module
    • CSS模块 Sass Less Stylus
    • 资源模块化 文件、CSS、图片通过JS进行统一依赖关联
  2. 组件化
    相对于文件的拆分,组件是对于UI层面的拆分,每一个组件需要包括对应的CSS、图片、JS逻辑、视图模板等并且能完成一个独立的功能。
    Element3组件库工程化实战 - 图2

  3. 自动化
    Element3组件库工程化实战 - 图3
    • 调试
    • 编译
    • 部署
    • 测试
    • 文档化
  4. 规范性Element3组件库工程化实战 - 图4
    • 项目目录结构
    • 语法提示
    • 编码风格规范
    • 联调规范
    • 文件命名规范
    • 代码样式规范
    • git flow

      二、实战步骤

      1. 开发规范

1.1 项目目录结构

  1. .
  2. ├── build # 编译脚本
  3. ├── coverage # 覆盖率报告
  4. ├── examples # 代码范例
  5. ├── lib # CSS样式 编译后
  6. ├── node_modules
  7. ├── packages # 组件代码
  8. ├── rollup-plugin-vue
  9. ├── scripts # 脚本 发布、提交信息检查
  10. ├── src # 通用代码
  11. ├── test # 测试
  12. └── types # TS类型定义

1.2 文件命名规范

  1. .
  2. ├── button
  3. ├── Button.vue # 组件SFC
  4. ├── __tests__
  5. └── Button.spec.js # 测试文件
  6. └── index.js # 组件入口

1.3 代码样式规范(ESLint)

  1. # .eslintrc.js
  2. module.exports = {
  3. root: true,
  4. env: {
  5. browser: true,
  6. es2020: true,
  7. node: true,
  8. jest: true
  9. },
  10. globals: {
  11. ga: true,
  12. chrome: true,
  13. __DEV__: true
  14. },
  15. extends: [
  16. 'plugin:json/recommended',
  17. 'plugin:vue/vue3-essential',
  18. 'eslint:recommended',
  19. '@vue/prettier'
  20. ],
  21. parserOptions: {
  22. parser: 'babel-eslint'
  23. },
  24. rules: {
  25. 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  26. 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  27. 'prettier/prettier': 'error'
  28. }
  29. }
  1. # .eslintignore
  2. src/utils/popper.js
  3. src/utils/date.js
  4. examples/play
  5. *.sh
  6. node_modules
  7. lib
  8. coverage
  9. *.md
  10. *.scss
  11. *.woff
  12. *.ttf
  13. src/index.js
  14. dist
  1. yarn add eslint
  2. yarn add eslint-formatter-pretty
  3. yarn add eslint-plugin-json
  4. yarn add eslint-plugin-prettier
  5. yarn add eslint-plugin-vue
  6. yarn add @vue/eslint-config-prettier
  7. yarn add babel-eslint
  8. yarn add prettier

package.json

  1. {
  2. "scripts": {
  3. "lint": "eslint --no-error-on-unmatched-pattern --ext .vue --ext .js --ext .jsx packages/**/ src/**/ --fix",
  4. },
  5. }

1.6 Git版本规范

分支管理

一般项目分主分支(master)和其他分支。
当有团队成员要开发新功能(Feather)或改 BUG(Fix) 时,就从 master 分支开一个新的分支。
比如你修改一个Bug应该用bug的编号作为分支(例:[Fix:12323])

Commit规范

  • 内容规范
  1. <type>(<scope>): <subject>
  2. <BLANK LINE>
  3. <body>
  4. <BLANK LINE>
  5. <footer>
  6. 复制代码

大致分为三个部分(使用空行分割):

  1. 标题行: 必填, 描述主要修改类型和内容
  2. 主题内容: 描述为什么修改, 做了什么样的修改, 以及开发的思路等等
  3. 页脚注释: 可以写注释,BUG 号链接
  • type: commit 的类型
    • feat: 新功能、新特性
    • fix: 修改 bug
    • perf: 更改代码,以提高性能
    • refactor: 代码重构(重构,在不影响代码内部行为、功能下的代码修改)
    • docs: 文档修改
    • style: 代码格式修改, 注意不是 css 修改(例如分号修改)
    • test: 测试用例新增、修改
    • build: 影响项目构建或依赖项修改
    • revert: 恢复上一次提交
    • ci: 持续集成相关文件修改
    • chore: 其他修改(不在上述类型中的修改)
    • release: 发布新版本
    • workflow: 工作流相关文件修改
  1. scope: commit 影响的范围, 比如: route, component, utils, build…
  2. subject: commit 的概述
  3. body: commit 具体修改内容, 可以分为多行.
  4. footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接.

示例

fix(修复BUG)

如果修复的这个BUG只影响当前修改的文件,可不加范围。如果影响的范围比较大,要加上范围描述。

例如这次 BUG 修复影响到全局,可以加个 global。如果影响的是某个目录或某个功能,可以加上该目录的路径,或者对应的功能名称。

  1. // 示例1
  2. fix(global):修复checkbox不能复选的问题
  3. // 示例2 下面圆括号里的 common 为通用管理的名称
  4. fix(common): 修复字体过小的BUG,将通用管理下所有页面的默认字体大小修改为 14px
  5. // 示例3
  6. fix: value.length -> values.length
  7. 复制代码

feat(添加新功能或新页面)
  1. feat: 添加网站主页静态页面
  2. 这是一个示例,假设对点检任务静态页面进行了一些描述。
  3. 这里是备注,可以是放BUG链接或者一些重要性的东西。
  4. 复制代码

chore(其他修改)

chore 的中文翻译为日常事务、例行工作,顾名思义,即不在其他 commit 类型中的修改,都可以用 chore 表示。

  1. chore: 将表格中的查看详情改为详情
  2. 复制代码

其他类型的 commit 和上面三个示例差不多,就不说了。

自动化提交验证

验证 git commit 规范,主要通过 git 的 pre-commit 钩子函数来进行。当然,你还需要下载一个辅助工具来帮助你进行验证。

下载辅助工具

  1. npm i -D husky

package.json 加上下面的代码

  1. "husky": {
  2. "hooks": {
  3. "pre-commit": "npm run lint",
  4. "commit-msg": "node script/verify-commit.js",
  5. "pre-push": "npm test"
  6. }
  7. }
  8. 复制代码

然后在你项目根目录下新建一个文件夹 script,并在下面新建一个文件 verify-commit.js,输入以下代码:

  1. const msgPath = process.env.HUSKY_GIT_PARAMS
  2. const msg = require('fs')
  3. .readFileSync(msgPath, 'utf-8')
  4. .trim()
  5. const commitRE = /^(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|release|workflow)(\(.+\))?: .{1,50}/
  6. if (!commitRE.test(msg)) {
  7. console.log()
  8. console.error(`
  9. 不合法的 commit 消息格式。
  10. 请查看 git commit 提交规范:https://github.com/woai3c/Front-end-articles/blob/master/git%20commit%20style.md
  11. `)
  12. process.exit(1)
  13. }
  14. 复制代码

现在来解释下各个钩子的含义:

  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

  1. // Invoked on the commit-msg git hook by yorkie.
  2. const chalk = require('chalk')
  3. const msgPath = process.env.GIT_PARAMS
  4. const msg = require('fs').readFileSync(msgPath, 'utf-8').trim()
  5. const commitRE = /^(revert: )?(feat|fix|docs|dx|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release)(\(.+\))?(.{1,10})?: .{1,50}/
  6. const mergeRe = /^(Merge pull request|Merge branch)/
  7. if (!commitRE.test(msg)) {
  8. if (!mergeRe.test(msg)) {
  9. console.log(msg)
  10. console.error(
  11. ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
  12. `invalid commit message format.`
  13. )}\n\n` +
  14. chalk.red(
  15. ` Proper commit message format is required for automated changelog generation. Examples:\n\n`
  16. ) +
  17. ` ${chalk.green(`feat(compiler): add 'comments' option`)}\n` +
  18. ` ${chalk.green(
  19. `fix(v-model): handle events on blur (close #28)`
  20. )}\n\n` +
  21. chalk.red(
  22. ` See https://github.com/vuejs/vue-next/blob/master/.github/commit-convention.md for more details.\n`
  23. )
  24. )
  25. process.exit(1)
  26. }
  27. }

2. 模块化与组件化

  1. npm init -y

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

2.1 编写Buttun组件

  1. yarn add vue@next

/packages/button/Button.vue

  1. <template>
  2. <button
  3. class="el-button"
  4. @click="handleClick"
  5. :disabled="buttonDisabled || loading"
  6. :autofocus="autofocus"
  7. :type="nativeType"
  8. :class="[
  9. type ? 'el-button--' + type : '',
  10. buttonSize ? 'el-button--' + buttonSize : '',
  11. {
  12. 'is-disabled': buttonDisabled,
  13. 'is-loading': loading,
  14. 'is-plain': plain,
  15. 'is-round': round,
  16. 'is-circle': circle,
  17. },
  18. ]"
  19. >
  20. <i class="el-icon-loading" v-if="loading"></i>
  21. <i :class="icon" v-if="icon && !loading"></i>
  22. <span v-if="$slots.default">
  23. <slot></slot>
  24. </span>
  25. </button>
  26. </template>
  27. <script>
  28. import { computed, inject, toRefs, unref, getCurrentInstance } from "vue";
  29. export default {
  30. name: "ElButton",
  31. props: {
  32. type: {
  33. type: String,
  34. default: "default",
  35. },
  36. size: {
  37. type: String,
  38. default: "",
  39. },
  40. icon: {
  41. type: String,
  42. default: "",
  43. },
  44. nativeType: {
  45. type: String,
  46. default: "button",
  47. },
  48. loading: Boolean,
  49. disabled: Boolean,
  50. plain: Boolean,
  51. autofocus: Boolean,
  52. round: Boolean,
  53. circle: Boolean,
  54. },
  55. emits: ["click"],
  56. setup(props, ctx) {
  57. const { size, disabled } = toRefs(props);
  58. const buttonSize = useButtonSize(size);
  59. const buttonDisabled = useButtonDisabled(disabled);
  60. const handleClick = (evt) => {
  61. ctx.emit("click", evt);
  62. };
  63. return {
  64. handleClick,
  65. buttonSize,
  66. buttonDisabled,
  67. };
  68. },
  69. };
  70. const useButtonSize = (size) => {
  71. const elFormItem = inject("elFormItem", {});
  72. const _elFormItemSize = computed(() => {
  73. return unref(elFormItem.elFormItemSize);
  74. });
  75. const buttonSize = computed(() => {
  76. return (
  77. size.value ||
  78. _elFormItemSize.value ||
  79. (getCurrentInstance().proxy.$ELEMENT || {}).size
  80. );
  81. });
  82. return buttonSize;
  83. };
  84. const useButtonDisabled = (disabled) => {
  85. const elForm = inject("elForm", {});
  86. const buttonDisabled = computed(() => {
  87. return disabled.value || unref(elForm.disabled);
  88. });
  89. return buttonDisabled;
  90. };
  91. </script>

2.2 集成Babel

  1. yarn add babel
  2. yarn add babel-plugin-syntax-dynamic-import
  3. yarn add babel-plugin-syntax-jsx
  4. yarn add babel-preset-env
  5. yarn add @babel/plugin-proposal-optional-chaining
  6. yarn add @babel/preset-env
  7. yarn add @vue/babel-plugin-jsx

新建.babelrc文件

  1. {
  2. "presets": [["@babel/preset-env", { "targets": { "node": "current" } }]],
  3. "plugins": [
  4. "syntax-dynamic-import",
  5. ["@vue/babel-plugin-jsx"],
  6. "@babel/plugin-proposal-optional-chaining",
  7. "@babel/plugin-proposal-nullish-coalescing-operator"
  8. ],
  9. "env": {
  10. "utils": {
  11. "presets": [
  12. [
  13. "env",
  14. {
  15. "loose": true,
  16. "modules": "commonjs",
  17. "targets": {
  18. "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
  19. }
  20. }
  21. ]
  22. ],
  23. "plugins": [
  24. [
  25. "module-resolver",
  26. {
  27. "root": ["element-ui"],
  28. "alias": {
  29. "element-ui/src": "element-ui/lib"
  30. }
  31. }
  32. ]
  33. ]
  34. },
  35. "test": {
  36. "plugins": ["istanbul"],
  37. "presets": [["env", { "targets": { "node": "current" } }]]
  38. },
  39. "esm": {
  40. "presets": [["@babel/preset-env", { "modules": false }]]
  41. }
  42. }
  43. }

2.2 集成VTU

安装依赖

  1. yarn add jest
  2. # 此版本这个支持Vue3.0
  3. yarn add vue-jest@5.0.0-alpha.5
  4. yarn add babel-jest
  5. yarn add @vue/compiler-sfc@3.0.2
  6. yarn add @vue/test-utils@next
  7. yarn add typescript

jest.config.js

  1. module.exports = {
  2. testEnvironment: 'jsdom', // 默认JSdom
  3. roots: [
  4. '<rootDir>/src',
  5. '<rootDir>/packages',
  6. ], //
  7. transform: {
  8. '^.+\\.vue$': 'vue-jest', // vue单文件
  9. '^.+\\js$': 'babel-jest' // esm最新语法 import
  10. },
  11. moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
  12. testMatch: ['**/__tests__/**/*.spec.js'],
  13. // 别名
  14. moduleNameMapper: {
  15. '^element-ui(.*)$': '<rootDir>$1',
  16. '^main(.*)$': '<rootDir>/src$1'
  17. }
  18. }

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

  1. import Button from "../Button.vue";
  2. import { mount } from "@vue/test-utils";
  3. it("content", () => {
  4. const Comp = {
  5. template: `<div><Button>默认按钮</Button></div>`,
  6. };
  7. const wrapper = mount(Comp, {
  8. global: {
  9. components: {
  10. Button,
  11. },
  12. },
  13. });
  14. expect(wrapper.findComponent({ name: "ElButton" }).text()).toContain(
  15. "默认按钮"
  16. );
  17. });
  18. describe("size", () => {
  19. it("should have a el-button--mini class when set size prop value equal to mini", () => {
  20. const wrapper = mount(Button, {
  21. props: {
  22. size: "mini",
  23. },
  24. });
  25. expect(wrapper.classes()).toContain("el-button--mini");
  26. });
  27. it("should have a el-button--mini class by elFormItem ", () => {
  28. const wrapper = mount(Button, {
  29. global: {
  30. provide: {
  31. elFormItem: {
  32. elFormItemSize: "mini",
  33. },
  34. },
  35. },
  36. });
  37. expect(wrapper.classes()).toContain("el-button--mini");
  38. });
  39. it("should have a el-button--mini class by $ELEMENT value ", () => {
  40. const wrapper = mount(Button, {
  41. global: {
  42. config: {
  43. globalProperties: {
  44. $ELEMENT: {
  45. size: "mini",
  46. },
  47. },
  48. },
  49. },
  50. });
  51. expect(wrapper.classes()).toContain("el-button--mini");
  52. });
  53. });
  54. it("type", () => {
  55. const wrapper = mount(Button, {
  56. props: {
  57. type: "primary",
  58. },
  59. });
  60. expect(wrapper.classes()).toContain("el-button--primary");
  61. });
  62. it("plain", () => {
  63. const wrapper = mount(Button, {
  64. props: {
  65. plain: true,
  66. },
  67. });
  68. expect(wrapper.classes()).toContain("is-plain");
  69. });
  70. it("round", () => {
  71. const wrapper = mount(Button, {
  72. props: {
  73. round: true,
  74. },
  75. });
  76. expect(wrapper.classes()).toContain("is-round");
  77. });
  78. it("circle", () => {
  79. const wrapper = mount(Button, {
  80. props: {
  81. circle: true,
  82. },
  83. });
  84. expect(wrapper.classes()).toContain("is-circle");
  85. });
  86. it("loading", () => {
  87. const wrapper = mount(Button, {
  88. props: {
  89. loading: true,
  90. },
  91. });
  92. expect(wrapper.find(".el-icon-loading").exists()).toBe(true);
  93. expect(wrapper.classes()).toContain("is-loading");
  94. });
  95. describe("icon", () => {
  96. it("should show icon element", () => {
  97. const wrapper = mount(Button, {
  98. props: {
  99. icon: "el-icon-edit",
  100. },
  101. });
  102. expect(wrapper.find(".el-icon-edit").exists()).toBe(true);
  103. });
  104. it("should not show icon element when set loading prop equal to true", () => {
  105. const wrapper = mount(Button, {
  106. props: {
  107. loading: true,
  108. icon: "el-icon-edit",
  109. },
  110. });
  111. expect(wrapper.find(".el-icon-edit").exists()).toBe(false);
  112. });
  113. });
  114. describe("click", () => {
  115. it("should emit click event ", () => {
  116. const wrapper = mount(Button);
  117. wrapper.trigger("click");
  118. expect(wrapper.emitted("click")).toBeTruthy();
  119. });
  120. it("should not emit click event when disabled equal to true", () => {
  121. const wrapper = mount(Button, {
  122. props: {
  123. disabled: true,
  124. },
  125. });
  126. wrapper.trigger("click");
  127. expect(wrapper.emitted("click")).toBeFalsy();
  128. });
  129. it("should not emit click event when elForm disabled equal to true", () => {
  130. const wrapper = mount(Button, {
  131. global: {
  132. provide: {
  133. elForm: {
  134. disabled: true,
  135. },
  136. },
  137. },
  138. });
  139. wrapper.trigger("click");
  140. expect(wrapper.emitted("click")).toBeFalsy();
  141. });
  142. it("should not emit click event when loading prop equal to true", () => {
  143. const wrapper = mount(Button, {
  144. props: {
  145. loading: true,
  146. },
  147. });
  148. wrapper.trigger("click");
  149. expect(wrapper.emitted("click")).toBeFalsy();
  150. });
  151. });
  152. it("native-type", () => {
  153. const wrapper = mount(Button, {
  154. props: {
  155. nativeType: "button",
  156. },
  157. });
  158. expect(wrapper.attributes("type")).toBe("button");
  159. });

测试

  1. "test": "jest --runInBand", # 序列化执行

2.4 样式打包

  1. yarn add gulp
  2. yarn add gulp-autoprefixer
  3. yarn add gulp-sass
  4. yarn add gulp-cssmin
  5. # cp-cli
  6. yarn add cp-cli
  7. yarn add tslib

/bin/gen-cssfile

package.json

  1. "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

  1. yarn add rollup
  2. yarn add rollup-plugin-peer-deps-external
  3. yarn add rollup-plugin-scss
  4. yarn add rollup-plugin-terser
  5. yarn add rollup-plugin-vue
  6. yarn add @rollup/plugin-node-resolve
  7. yarn add @rollup/plugin-commonjs
  8. yarn add @rollup/plugin-json
  9. yarn add @rollup/plugin-replace
  10. yarn add @rollup/plugin-babel
  11. yarn add rollup-plugin-vue

Package.json

  1. "build:next": "rollup -c",
  1. import pkg from './package.json'
  2. // 等 rollup-plugin-vue 发版后在切换官方版
  3. // 暂时先用本地的 rollup-plugin-vue
  4. // 修复了 render 函数的编译问题,但是还没发版
  5. // import vuePlugin from 'rollup-plugin-vue'
  6. const vuePlugin = require('./rollup-plugin-vue/index')
  7. import scss from 'rollup-plugin-scss'
  8. import peerDepsExternal from 'rollup-plugin-peer-deps-external'
  9. import resolve from '@rollup/plugin-node-resolve'
  10. import commonjs from '@rollup/plugin-commonjs'
  11. import json from '@rollup/plugin-json'
  12. import replace from '@rollup/plugin-replace'
  13. import babel from '@rollup/plugin-babel'
  14. import { terser } from 'rollup-plugin-terser'
  15. const name = 'Element3'
  16. const createBanner = () => {
  17. return `/*!
  18. * ${pkg.name} v${pkg.version}
  19. * (c) ${new Date().getFullYear()} kkb
  20. * @license MIT
  21. */`
  22. }
  23. const createBaseConfig = () => {
  24. return {
  25. input: 'src/entry.js',
  26. external: ['vue'],
  27. plugins: [
  28. peerDepsExternal(),
  29. babel(),
  30. resolve({
  31. extensions: ['.vue', '.jsx']
  32. }),
  33. commonjs(),
  34. json(),
  35. vuePlugin({
  36. css: true
  37. }),
  38. scss()
  39. ],
  40. output: {
  41. sourcemap: false,
  42. banner: createBanner(),
  43. externalLiveBindings: false,
  44. globals: {
  45. vue: 'Vue'
  46. }
  47. }
  48. }
  49. }
  50. function mergeConfig(baseConfig, configB) {
  51. const config = Object.assign({}, baseConfig)
  52. // plugin
  53. if (configB.plugins) {
  54. baseConfig.plugins.push(...configB.plugins)
  55. }
  56. // output
  57. config.output = Object.assign({}, baseConfig.output, configB.output)
  58. return config
  59. }
  60. function createFileName(formatName) {
  61. return `dist/element3-ui.${formatName}.js`
  62. }
  63. // es-bundle
  64. const esBundleConfig = {
  65. plugins: [
  66. replace({
  67. __DEV__: `(process.env.NODE_ENV !== 'production')`
  68. })
  69. ],
  70. output: {
  71. file: createFileName('esm-bundler'),
  72. format: 'es'
  73. }
  74. }
  75. // es-browser
  76. const esBrowserConfig = {
  77. plugins: [
  78. replace({
  79. __DEV__: true
  80. })
  81. ],
  82. output: {
  83. file: createFileName('esm-browser'),
  84. format: 'es'
  85. }
  86. }
  87. // es-browser.prod
  88. const esBrowserProdConfig = {
  89. plugins: [
  90. terser(),
  91. replace({
  92. __DEV__: false
  93. })
  94. ],
  95. output: {
  96. file: createFileName('esm-browser.prod'),
  97. format: 'es'
  98. }
  99. }
  100. // cjs
  101. const cjsConfig = {
  102. plugins: [
  103. replace({
  104. __DEV__: true
  105. })
  106. ],
  107. output: {
  108. file: createFileName('cjs'),
  109. format: 'cjs'
  110. }
  111. }
  112. // cjs.prod
  113. const cjsProdConfig = {
  114. plugins: [
  115. terser(),
  116. replace({
  117. __DEV__: false
  118. })
  119. ],
  120. output: {
  121. file: createFileName('cjs.prod'),
  122. format: 'cjs'
  123. }
  124. }
  125. // global
  126. const globalConfig = {
  127. plugins: [
  128. replace({
  129. __DEV__: true,
  130. 'process.env.NODE_ENV': true
  131. })
  132. ],
  133. output: {
  134. file: createFileName('global'),
  135. format: 'iife',
  136. name
  137. }
  138. }
  139. // global.prod
  140. const globalProdConfig = {
  141. plugins: [
  142. terser(),
  143. replace({
  144. __DEV__: false
  145. })
  146. ],
  147. output: {
  148. file: createFileName('global.prod'),
  149. format: 'iife',
  150. name
  151. }
  152. }
  153. const formatConfigs = [
  154. esBundleConfig,
  155. esBrowserProdConfig,
  156. esBrowserConfig,
  157. cjsConfig,
  158. cjsProdConfig,
  159. globalConfig,
  160. globalProdConfig
  161. ]
  162. function createPackageConfigs() {
  163. return formatConfigs.map((formatConfig) => {
  164. return mergeConfig(createBaseConfig(), formatConfig)
  165. })
  166. }
  167. export default createPackageConfigs()

2.3 编写Entry入口

3. 自动化

3.1 文档自动化

文档自动化其实就是根据代码自动生成开发文档。比如element3项目中的。
https://element3-ui.com/
image.png
其实可以用StoryBook。
这个我们后面写专题更新。大家保持关注。

3.2 规范检查

  1. yarn add husky

.huskyrc

  1. {
  2. "hooks": {
  3. "pre-commit": "npm run lint",
  4. "commit-msg": "node scripts/verifyCommit.js",
  5. "pre-push": "npm run test"
  6. },
  7. }

3.4 回归测试

GitHub Action

  1. .github/workflows/main.yml

3.3 持续集成CI

Travis CI 提供的是持续集成服务,它仅支持 Github,不支持其他代码托管。它需要绑定 Github 上面的项目,还需要该项目含有构建或者测试脚本。只要有新的代码,就会自动抓取。然后,提供一个虚拟机环境,执行测试,完成构建,还能部署到服务器。只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码集成到主干。

这个项目需要Travis在提交后自动进行测试并且向codecov提供测试报告。

  • 测试
  • 报告分析

登录TravicCI网站

登录https://www.travis-ci.org/网站

使用github账号登录系统

配置.travis.yml

运行自动化测试框架

  1. language: node_js # 项目语言,node 项目就按照这种写法就OK了
  2. node_js:
  3. - 13.2.0 # 项目环境
  4. cache: # 缓存 node_js 依赖,提升第二次构建的效率
  5. directories:
  6. - node_modules
  7. test:
  8. - npm run test # 运行自动测试框架

参考教程:Travis CI Tutorial

上传配置到github

启动持续集成

通过github账号登录travis

Element3组件库工程化实战 - 图6

Element3组件库工程化实战 - 图7

获取持续集成通过徽标

将上面 URL 中的 {GitHub 用户名} 和 {项目名称} 替换为自己项目的即可,最后可以将集成完成后的 markdown 代码贴在自己的项目上

Element3组件库工程化实战 - 图8

  1. http://img.shields.io/travis/{GitHub 用户名}/{项目名称}.svg
  2. 复制代码

Element3组件库工程化实战 - 图9

3.5 持续交付CD - 上传Npm库

创建发布脚本

publish.sh

  1. #!/usr/bin/env bash
  2. npm config get registry # 检查仓库镜像库
  3. npm config set registry=http://registry.npmjs.org
  4. echo '请进行登录相关操作:'
  5. npm login # 登陆
  6. echo "-------publishing-------"
  7. npm publish # 发布
  8. npm config set registry=https://registry.npm.taobao.org # 设置为淘宝镜像
  9. echo "发布完成"
  10. exit

执行发布

  1. ./publish.sh
  2. 复制代码

填入github用户名密码后

3.7 覆盖率测试Codecov

Codecov是一个开源的测试结果展示平台,将测试结果可视化。Github上许多开源项目都使用了Codecov来展示单测结果。Codecov跟Travis CI一样都支持Github账号登录,同样会同步Github中的项目。

  1. yarn add codecov
  1. "scripts": {
  2. ...,
  3. "codecov": "codecov"
  4. }

4. 其他

4.1 标准的README文档

4.2 开源许可证

每个开源项目都需要配置一份合适的开源许可证来告知所有浏览过我们的项目的用户他们拥有哪些权限,具体许可证的选取可以参照阮一峰前辈绘制的这张图表:

Element3组件库工程化实战 - 图10

那我们又该怎样为我们的项目添加许可证了?其实 Github 已经为我们提供了非常简便的可视化操作: 我们平时在逛 github 网站的时候,发现不少项目都在 README.md 中添加徽标,对项目进行标记和说明,这些小图标给项目增色不少,不仅简单美观,而且还包含清晰易懂的信息。

  1. 打开我们的开源项目并切换至 Insights 面板
  2. 点击 Community 标签
  3. 如果您的项目没有添加 License,在 Checklist 里会提示您添加许可证,点击 Add 按钮就进入可视化操作流程了

Element3组件库工程化实战 - 图11

Element3组件库工程化实战 - 图12

4.3 申请开源徽标 (Badge)

Element3组件库工程化实战 - 图13

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

三、附录

3.1 Vue组件与插件

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <title>Document</title>
  8. <script src="/node_modules/vue/dist/vue.global.js"></script>
  9. <script src="/dist/element3-ui.global.js"></script>
  10. <link href="/lib/theme-chalk/index.css" rel="stylesheet" />
  11. <style></style>
  12. </head>
  13. <body>
  14. <div id="app"></div>
  15. <script>
  16. const { createApp, reactive, computed, watchEffect } = Vue;
  17. const MyButton = {
  18. name: "MyButton",
  19. data: function () {
  20. return {
  21. count: 0,
  22. };
  23. },
  24. template:
  25. '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
  26. };
  27. // 添加插件
  28. MyButton.install = (app) => app.component("MyButton", MyButton);
  29. // 组件库
  30. const Element = {
  31. MyButton,
  32. install: app => {
  33. app.use(MyButton)
  34. }
  35. }
  36. const MyComponent = {
  37. template: `
  38. <my-button />
  39. `,
  40. };
  41. createApp(MyComponent)
  42. // .use(MyButton)
  43. .use(Element)
  44. .mount("#app");
  45. </script>
  46. </body>
  47. </html>

3.2 rollup打包

Element3组件库工程化实战 - 图14

rollup是一款小巧的javascript模块打包工具,更适合于库应用的构建工具;可以将小块代码编译成大块复杂的代码,基于ES6 modules,它可以让你的 bundle 最小化,有效减少文件请求大小,vue在开发的时候用的是webpack,但是最后将文件打包在一起的时候用的是 rollup.js

首次发表在个人博客

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

Button

/src/MyButton.js

  1. export default {
  2. name: "MyButton",
  3. data: function () {
  4. return {
  5. count: 0,
  6. };
  7. },
  8. template:
  9. '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
  10. };

入口

/src/entry.js

  1. import MyButton from "./MyButton";
  2. import SfcButton from "./SfcButton.vue";
  3. import JsxButton from "./JsxButton.vue";
  4. // 添加插件
  5. MyButton.install = (app) => app.component("MyButton", MyButton);
  6. SfcButton.install = (app) => app.component("SfcButton", SfcButton);
  7. JsxButton.install = (app) => app.component("JsxButton", JsxButton);
  8. // 组件库
  9. const Element = {
  10. MyButton,
  11. SfcButton,
  12. JsxButton,
  13. install: (app) => {
  14. app.use(MyButton);
  15. app.use(SfcButton);
  16. app.use(JsxButton);
  17. },
  18. };
  19. export default Element;

格式声明

https://juejin.im/post/6885542715782594568 AMD CMD UMD区别

  • amd – 异步模块定义,用于像 RequireJS 这样的模块加载器
  • cjs – CommonJS,适用于 Node 和 Browserify/Webpack
  • es – 将软件包保存为 ES 模块文件
  • iife – 一个自动执行的功能,适合作为<script>标签。(如果要为应用程序创建一个捆绑包,您可能想要使用它,因为它会使文件大小变小。)
  • umd – 通用模块定义,以 amd,cjs 和 iife 为一体
  1. const vuePlugin = require("../../rollup-plugin-vue/index");
  2. import babel from "@rollup/plugin-babel";
  3. // import vuePlugin from "rollup-plugin-vue";
  4. const es = {
  5. input: "src/entry.js",
  6. output: {
  7. file: "dist/index.js",
  8. name: "Element",
  9. format: "iife",
  10. globals: {
  11. vue: "Vue",
  12. },
  13. },
  14. external: ["vue"],
  15. plugins: [
  16. babel(),
  17. vuePlugin({
  18. css: true,
  19. }),
  20. ],
  21. };
  22. import { terser } from "rollup-plugin-terser";
  23. const minEs = {
  24. input: "src/entry.js",
  25. external: ["vue"],
  26. output: {
  27. file: "dist/index.min.js",
  28. name: "Element",
  29. format: "umd",
  30. },
  31. plugins: [
  32. babel(),
  33. vuePlugin({
  34. css: true,
  35. }),
  36. terser(),
  37. ],
  38. };
  39. const cjs = {
  40. input: "src/entry.js",
  41. external: ["vue"],
  42. output: {
  43. file: "dist/index.cjs.js",
  44. name: "Element",
  45. format: "cjs",
  46. },
  47. plugins: [
  48. babel(),
  49. vuePlugin({
  50. css: true,
  51. }),
  52. ],
  53. };
  54. export default [es, minEs, cjs];

测试页面

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8" />
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  6. <meta http-equiv="X-UA-Compatible" content="ie=edge" />
  7. <title>Document</title>
  8. <script src="/node_modules/vue/dist/vue.global.js"></script>
  9. <script src="dist/index.js"></script>
  10. <style></style>
  11. </head>
  12. <body>
  13. <div id="app"></div>
  14. <script>
  15. const { createApp, reactive, computed, watchEffect } = Vue;
  16. const MyComponent = {
  17. template: `
  18. <my-button />
  19. <sfc-button />
  20. <jsx-button />
  21. `,
  22. };
  23. createApp(MyComponent)
  24. .use(Element)
  25. .mount("#app");
  26. </script>
  27. </body>
  28. </html>

单文件组件

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

JSX支持

jsx的定义

JSX 是一种类似于 XML 的 JavaScript 语法扩展 JSX 不是由引擎或浏览器实现的。相反,我们将使用像 Babel 这样的转换器将 JSX 转换为常规 JavaScript。基本上,JSX 允许我们在 JavaScript 中使用类似 HTML 的语法。

jsx的优势

  1. 可以将 模版分离 这样模版的每个部分更加独立,又可以随机的组合,复用性更高。相比与组件的组合,粒度更细
  2. 使用 js 可配置每项要渲染的 dom,更加动态可配置化
  1. import babel from "@rollup/plugin-babel";
  2. # plugin
  3. babel(),
  1. <script>
  2. export default {
  3. name: "JsxButton",
  4. render() {
  5. return <button>JSX 666</button>;
  6. },
  7. };
  8. </script>

3.3 Vue-cli插件开发

请参考 https://juejin.cn/post/6899334776860180494