奇思妙想之开发一套公司的组件库

1,596 阅读4分钟

前言

奇思妙想的一天,有时感觉在每日的开发中,如果单单的去做业务系统,总感觉有点乏味.有时做完一个系统,再做其他系统时总有很多相似的地方.但却没有一个去承载这些系统相似功能或组件的地方.所以想法子能不能将哪些功能组件进行抽离,直接安装一个js文件包就能直接使用已抽离的功能组件,就像Element-ui一样:

image.png

image.png

一.站在element3-ui的肩膀上造轮子

写一套组件库,最基础的包括组件工程环境配置,业务组件,每个组件的展示以及对应的代码块,为了保证质量还得编写组件的测试用例.而element3-ui 虽然没有像element-ui功能那么多,但这些基本都能够满足.可复用性比较强,但有一个缺点就是它是基于vue3的,所以要将它的一些配置改成vue2,下面是其他组件库的一些背调.

组件库git-star 数优缺点
element-ui50.5k功能齐全,配置过多
ant design vue14.8k功能齐全,但公司平日使用少
iview23.8k功能较全, 但社区不怎么活跃

确定了使用element3-ui,那么直接上git

二.在element3-ui项目中创建自己的ui组件库

因为element3里面大部分是使用vue3 + ts的写法,而自己想要的是基于vue2,所以里面的组件基本是不能用的,直接删除package/element3下的所有文件.一边模仿,一边照搬,说动就动.

改造的目录结构,新建fst-ui文件夹放置如下文件:

    |-- .babelrc
    |-- components.json  // 所有的组件json
    |-- package.json
    |-- README.md
    |-- rollup.config.js  // 组件打包配置
    |-- dist
    |   |-- fst-ui.umd.js // 打包之后生成的dist文件
    |-- lib               // 打包之后的样式文件
    |   |-- theme-chalk
    |       |-- base.css
    |       |-- icon.css
    |       |-- index.css
    |       |-- fonts
    |           |-- element-icons.ttf
    |           |-- element-icons.woff
    |-- scripts           // js脚本文件
    |   |-- generateCssFile.js
    |-- src              // 源文件
        |-- index.js
        |-- components   // 所有的组件
        |   |-- theme-chalk
        |       |-- .gitignore
        |       |-- gulpfile.js
        |       |-- package.json
        |       |-- README.md
        |       |-- src
        |           |-- base.scss
        |           |-- icon.scss
        |           |-- index.scss
        |           |-- common
        |           |   |-- var.scss
        |           |-- fonts
        |           |   |-- element-icons.ttf
        |           |   |-- element-icons.woff
        |           |-- mixins
        |               |-- config.scss
        |               |-- function.scss
        |               |-- mixins.scss
        |               |-- utils.scss
        |-- directives   // 所有的指令
        |   |-- clickoutside.js
        |   |-- mousewheel.js
        |   |-- repeatClick.js
        |-- mixins       // 混入一些全局方法
        |   |-- emitter.js
        |   |-- focus.js
        |   |-- migrating.js
        |-- utils        // 工具函数
            |-- dom.js
            |-- merge.js
            |-- scroll-into-view.js
            |-- scrollbar-width.js
            |-- shared.js
            |-- types.js
            |-- util.js
            |-- vdom.js
1. fst-ui文件下各个详细配置
  • package.json 在项目中所用到的一些包,以及执行的指令,包括组件打包,运行开发,主题编译等。
{
  "name": "fst-ui",
  "version": "0.0.1",
  "description": "企业级组件库",
  "author": "1397798719@qq.com",
  "license": "ISC",
  "main": "./dist/fst-ui.umd.js",
  "files": [
    "dist",
    "lib"
  ],
  "publishConfig": {
    "access": "public"
  },
  "repository": {
    "type": "git",
    "url": ""
  },
  "scripts": {
    "test": "jest --config jest.conf.js",
    "test:coverage": "jest --config jest.conf.js --coverage",
    "dev": "npm run dev:umd & npm run dev:es & npm run dev:unpkg",
    "dev:umd": "rollup -c -w --format umd --file dist/fst-ui.umd.js",
    "dev:es": "rollup -c -w --format es --file dist/fst-ui.esm.js",
    "dev:unpkg": "rollup -c -w --format iife --file dist/fst-ui.min.js",
    "build:theme": "node scripts/generateCssFile.js && gulp build --gulpfile src/components/theme-chalk/gulpfile.js && cp-cli src/components/theme-chalk/lib lib/theme-chalk",
    "build": "npm run build:umd & npm run build:es & npm run build:unpkg",
    "build:umd": "rollup -c --format umd --file dist/fst-ui.umd.js",
    "build:es": "rollup -c --format es --file dist/fst-ui.esm.js",
    "build:unpkg": "rollup -c --format iife --file dist/fst-ui.min.js"
  },
  "dependencies": {
    "@babel/core": "^7.11.4",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1",
    "@babel/plugin-proposal-optional-chaining": "^7.12.7",
    "@babel/preset-env": "^7.12.10",
    "@rollup/plugin-babel": "^5.2.2",
    "@rollup/plugin-commonjs": "^15.0.0",
    "@rollup/plugin-image": "^2.0.5",
    "@rollup/plugin-json": "^4.1.0",
    "@rollup/plugin-node-resolve": "^11.0.0",
    "@vue/babel-plugin-jsx": "^1.0.0-rc.4",
    "@vue/babel-preset-jsx": "^1.1.2",
    "@vue/compiler-sfc": "^3.0.0-rc.6",
    "@vue/test-utils": "^1.2.1",
    "babel-jest": "^27.0.6",
    "cp-cli": "^2.0.0",
    "eslint": "^7.7.0",
    "gulp": "^4.0.2",
    "gulp-autoprefixer": "^7.0.1",
    "gulp-cssmin": "^0.2.0",
    "gulp-sass": "^4.1.0",
    "jest": "^27.0.6",
    "node-sass": "^4.14.1",
    "normalize-wheel": "^1.0.1",
    "rollup": "^2.26.4",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "rollup-plugin-scss": "^2.6.1",
    "rollup-plugin-terser": "^7.0.2",
    "rollup-plugin-vue": "5.1.6",
    "sass": "^1.26.5",
    "sass-loader": "^8.0.2",
    "util": "^0.12.3",
    "vue": "^2.6.11",
    "vue-jest": "^3.0.7",
    "vue-template-compiler": "^2.6.11"
  }
}
  • rollup.config.js 使用rollup针对文件的路口,对vue的文件,进行转换,压缩,最后生成一个umd格式的js文件
// rollup.config.js
import pkg from './package.json'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import { terser } from 'rollup-plugin-terser'
import scss from 'rollup-plugin-scss'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import vue from 'rollup-plugin-vue'
import babel from '@rollup/plugin-babel'
import commonjs from '@rollup/plugin-commonjs'
import image from '@rollup/plugin-image'

const name = 'fst'
const createBanner = () => {
  return `/*!
  * ${pkg.name} v${pkg.version}
  * (c) ${new Date().getFullYear()} kkb
  * @license MIT
  */`
}
const config = {
  input: 'src/index.js',
  external: ['vue'],
  output: {
    name,
    sourcemap: false,
    banner: createBanner(),
    exports: 'named',
    externalLiveBindings: false,
    globals: {
      vue: 'Vue'
    }
  },
  plugins: [
    peerDepsExternal(),
    resolve({
      extensions: ['.vue', '.jsx', '.js']
    }),
    vue({
      css: true,
      compileTemplate: true
    }),
    babel({
      exclude: 'node_modules/**',
      extensions: ['.js', '.jsx', '.vue'],
      babelHelpers: 'bundled'
    }),
    commonjs(),
    terser(),
    json(),
    scss(),
    image()
  ]
}
export default config
  • 主题的相关和element-ui差不多,可以直接照搬过来
2. website文件夹的改造

其实website在现在的element3-ui也是不能用的,所以自己瞎折腾,将原来的website文件夹也改了,一些能够用到的比如显示的组件,布局组件,以及一些配置文件,尽量的保留。一些关于ts相关的和vue3的都去掉,重新创建了一个基于vue2的项目,最终生成的目录结果如下。

|-- .eslintignore
    |-- .gitignore
    |-- babel.config.js
    |-- cypress.json
    |-- package.json
    |-- README.md
    |-- vue.config.js
    |-- loader
    |   |-- md-loader
    |       |-- config.js
    |       |-- containers.js
    |       |-- fence.js
    |       |-- index.js
    |       |-- util.js
    |-- public
    |   |-- favicon.ico
    |   |-- index.html
    |-- scripts
    |   |-- deploy.js
    |   |-- iconInit.js
    |   |-- preview.js
    |-- src
        |-- app.vue
        |-- bus.js
        |-- color.js
        |-- icon.json
        |-- main.js
        |-- util.js
        |-- assets  // 资源文件
        |   |-- images
        |   |   |-- element-logo-small.svg
        |   |   |-- element-logo.svg
        |   |   |-- web.png
        |   |-- styles
        |       |-- common.scss
        |       |-- fonts
        |           |-- icomoon.eot
        |           |-- icomoon.svg
        |           |-- icomoon.ttf
        |           |-- icomoon.woff
        |           |-- style.css
        |-- components  // 页面显示用的组件
        |   |-- demo-block.vue
        |   |-- footer-nav.vue
        |   |-- header.vue
        |   |-- search.vue
        |   |-- side-nav.vue
        |-- demo-styles // 各个md文档的样式
        |   |-- i18n.scss
        |   |-- icon.scss
        |   |-- index.scss
        |-- docs       // 每个组件的md文档
        |   |-- i18n.md
        |   |-- icon.md
        |   |-- installation.md
        |   |-- quickstart.md
        |-- dom
        |   |-- class.js
        |-- i18n     // 语言配置
        |   |-- component.json
        |   |-- page.json
        |   |-- route.json
        |   |-- theme-editor.json
        |   |-- title.json
        |-- locale
        |   |-- format.js
        |   |-- index.js
        |   |-- lang
        |       |-- zh-CN.js
        |-- pages   // 页面
        |   |-- component.vue
        |   |-- index.vue
        |-- route   // 路由
            |-- index.js
            |-- nav.config.json
  • package.json 基本就是vue-cli项目的一些配置,并没有做很大的改造
{
  "name": "website",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "@babel/core": "^7.14.5",
    "filemanager-webpack-plugin": "^2.0.5",
    "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
    "@babel/plugin-proposal-optional-chaining": "^7.14.5",
    "@babel/preset-env": "^7.14.5",
    "@vue/babel-plugin-jsx": "^1.0.6",
    "@vue/cli-plugin-e2e-cypress": "^4.5.13",
    "@vue/cli-plugin-router": "^4.5.13",
    "@vue/cli-plugin-unit-jest": "^4.5.13",
    "@vue/compiler-sfc": "^3.1.1",
    "@vue/component-compiler-utils": "^2.6.0",
    "@vue/eslint-config-standard": "^6.0.0",
    "@vue/test-utils": "^1.2.1",
    "axios": "^0.21.1",
    "babel-loader": "^8.2.2",
    "babel-plugin-syntax-dynamic-import": "^6.18.0",
    "chokidar": "^3.5.1",
    "core-js": "^3.6.5",
    "cross-env": "^7.0.3",
    "css-loader": "^5.2.6",
    "fst-ui": "1.0.6",
    "element-ui": "^2.15.2",
    "eslint-plugin-import": "^2.23.4",
    "eslint-plugin-node": "^11.1.0",
    "eslint-plugin-promise": "^5.1.0",
    "eslint-plugin-standard": "^5.0.0",
    "file-loader": "^6.2.0",
    "highlight.js": "^9.3.0",
    "html-webpack-plugin": "^5.3.1",
    "json-loader": "^0.5.7",
    "json-templater": "^1.2.0",
    "markdown-it-anchor": "^5.0.2",
    "markdown-it-chain": "^1.3.0",
    "markdown-it-container": "^2.0.0",
    "md-enhance-vue": "^1.0.4",
    "md-loader": "^0.1.0",
    "mini-css-extract-plugin": "^1.6.0",
    "mitt": "^2.1.0",
    "node-sass": "^4.14.1",
    "optimize-css-assets-webpack-plugin": "^6.0.0",
    "progress-bar-webpack-plugin": "^2.1.0",
    "style-loader": "^2.0.0",
    "transliteration": "^1.1.11",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "uppercamelcase": "^3.0.0",
    "url-loader": "^4.1.1",
    "vue": "^2.5.21",
    "vue-jest": "^3.0.7",
    "vue-loader": "^15.7.0",
    "vue-router": "^3.5.1",
    "vue-template-es2015-compiler": "^1.6.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^4.5.13",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^6.2.2",
    "sass-loader": "^8.0.2",
    "vue-template-compiler": "^2.5.21"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}
  • vue.config.js的配置 因为要将md文件转换成组件,所有这里直接用了element-ui里的md-loader包。配置好md的loader,那么相应的md文件就可以直接被识别并且被转义。如果在遇到md中,有'''html的代码块,是直接被转换成具体vue组件并显示,具体实现细节可以参考element-ui的源码.
// vue.config.js
const path = require('path')
module.exports = {
	devServer: {
		port: 8088,
	},
	chainWebpack: (config) => {
		config
			// app entry
			.entry('app')
			.clear()
			.add(path.resolve(__dirname, './src/main.js'))
			.end()

		// 添加解析 md 的 loader
		config.module
			.rule('md2vue')
			.test(/\.md$/)
			.use('vue-loader')
			.loader('vue-loader')
			.end()
			.use('md-loader')
			.loader(path.resolve(__dirname, './loader/md-loader/index.js'))
			.end()
	},
}
  • main入口文件 这个入口文件直接使用element-ui的,基本相差无几。去掉了element-ui的多语言功能,保留必要的功能文件。
import Vue from 'vue'
import EntryApp from './app'
import VueRouter from 'vue-router'

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import FstUI from 'fst-ui'
import 'fst-ui/lib/theme-chalk/index.css'
import axios from 'axios'
import routes from './route'
import hljs from 'highlight.js'
import demoBlock from './components/demo-block'
import MainHeader from './components/header'
import SideNav from './components/side-nav'
import FooterNav from './components/footer-nav'
import title from './i18n/title'

import './demo-styles/index.scss'
import './assets/styles/common.scss'
import './assets/styles/fonts/style.css'
import icon from './icon.json'

Vue.use(ElementUI)
Vue.use(FstUI)
Vue.use(VueRouter)

Vue.component('demo-block', demoBlock)
Vue.component('main-header', MainHeader)
Vue.component('side-nav', SideNav)
Vue.component('footer-nav', FooterNav)

const globalEle = new Vue({
	data: { $isEle: false }, // 是否 ele 用户
})

Vue.mixin({
	computed: {
		$isEle: {
			get: () => globalEle.$data.$isEle,
			set: (data) => {
				globalEle.$data.$isEle = data
			},
		},
	},
})

Vue.prototype.$icon = icon // Icon 列表页用
Vue.prototype.$axios = axios // 请求
const router = new VueRouter({
	mode: 'hash',
	routes,
})
router.afterEach(async (route) => {
	Vue.nextTick(() => {
		const blocks = document.querySelectorAll('pre code:not(.hljs)')
		Array.prototype.forEach.call(blocks, hljs.highlightBlock)
	})
	const data = title
	for (const val in data) {
		if (new RegExp('^' + val, 'g').test(route.name)) {
			document.title = data[val]
			return
		}
	}
	document.title = 'Element'
})

new Vue({
	// eslint-disable-line
	...EntryApp,
	router,
}).$mount('#app')

三.最终的呈现效果

包地址
文档地址

四.总结

组件化建设最终也都是服务于业务,但大都数也并没有那么复杂,可以多看下几个开源项目,再结合自己业务或许就有了点苗头。通过这次造轮子,如果仔细研究下,就会发现不管是element3 或者element-ui很多地方写的真好,如:

  • yarn项目的模块化
  • new下就是一个新组件
  • 自定义主题
  • 一键发布(git,npm)
  • 多语言切换
  • md文件内直接写vue代码
  • 等 最后:如果有组件库相关的优化或者建议,欢迎私信哈

参考

juejin.cn/post/684490…
juejin.cn/post/696649…
juejin.cn/post/693744…