前言
奇思妙想的一天,有时感觉在每日的开发中,如果单单的去做业务系统,总感觉有点乏味.有时做完一个系统,再做其他系统时总有很多相似的地方.但却没有一个去承载这些系统相似功能或组件的地方.所以想法子能不能将哪些功能组件进行抽离,直接安装一个js文件包就能直接使用已抽离的功能组件,就像Element-ui
一样:
一.站在element3-ui的肩膀上造轮子
写一套组件库,最基础的包括组件工程环境配置,业务组件,每个组件的展示以及对应的代码块,为了保证质量还得编写组件的测试用例.而element3-ui 虽然没有像element-ui功能那么多,但这些基本都能够满足.可复用性比较强,但有一个缺点就是它是基于vue3的,所以要将它的一些配置改成vue2,下面是其他组件库的一些背调.
组件库 | git-star 数 | 优缺点 |
---|---|---|
element-ui | 50.5k | 功能齐全,配置过多 |
ant design vue | 14.8k | 功能齐全,但公司平日使用少 |
iview | 23.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…