一套自生成组件系统的构想与实践
栏目: JavaScript · 发布时间: 6年前
内容简介:一套功能类似于有赞商城后台管理系统中店铺-微页面的系统,该系统实现用户可以选择所需要的组件,拖动调整组件的顺序,以及编辑组件的内容并且在移动端展示等相关功能,如下图所示。仔细想了一想,该系统需要实现下面三大功能服务端渲染?还是用原生js封装组件?使用框架选 react 还是 vue?(其实我不会react,但请容许我装个b []~( ̄▽ ̄)~*)
一套功能类似于有赞商城后台管理系统中店铺-微页面的系统,该系统实现用户可以选择所需要的组件,拖动调整组件的顺序,以及编辑组件的内容并且在移动端展示等相关功能,如下图所示。
开始前的思考
系统主要功能
仔细想了一想,该系统需要实现下面三大功能
- 组件系统
- 移动端组件生成系统
- 后台管理系统组件编辑功能
基于什么开发
服务端渲染?还是用原生js封装组件?使用框架选 react 还是 vue?(其实我不会react,但请容许我装个b []~( ̄▽ ̄)~*)
因为之前且尝试开发过element ui库的组件,详情点这,对于element ui的架构有一定的了解,于是打算把基本的功能照搬element ui的架构,先基于vue开发一套组件系统,其他的功能再自行定制。
构建时的思考
构建 工具 的搭建
构建工具我选择了webpack,大版本为4.0,试着吃吃螃蟹。先思考下需要webpack的哪些功能:
功能 | 相关插件 |
---|---|
es6-es5 | babel-loader |
sass-css | sass-loader css-loader style-loader |
css文件抽离 | mini-css-extract-plugin |
html模版 | html-loader 以及 html-webpack-plugin |
vue文件解析 | vue-loader |
图片文件解析 | file-loader |
markdown转换vue | vue-markdown-loader |
删除文件 | clean-webpack-plugin 或者脚本 |
热更新 | HotModuleReplacementPlugin |
webpack配置合并 | webpack-merge |
基本上就是以上的loader及插件了。
因为组件库涉及到多个功能的打包,比如组件库,预览时的配置,以及后面会提到的和其他项目的集成,所以对于webpack的配置,可以学习vue-cli中的配置,将公共的配置抽离,特殊的配置通过webpack-merge插件完成合并。
这里将不同的需求及功能抽离成了如上图所示的几个webpack配置, 其中webpack.base.js为公共的配置,如下图所示,分别处理了vue文件,图片以及js文件
这篇文章的目的是主要提供一个思路,所以这里不详细讲解webpack的相关配置。
除了构建工具,还能不能更加高效的完成开发?
其实对于开发来说,提高效率的主要方式就是将相同的事物封装起来,就好比一个函数的封装,这里因为组件文件的结构是相似的,所以我学习element ui的做法,将组件的创建过程封装成脚本,运行命令就能够直接生成好文件,以及添加配置文件。代码如下
const path = require('path') const fileSave = require('file-save') const getUnifiedName = require('../utils').getUnifiedName const uppercamelcase = require('uppercamelcase') const config = require('../config') const component_name = process.argv[2] //组件文件名 横杠分隔 const ComponentName = uppercamelcase(component_name) //组件帕斯卡拼写法命名 const componentCnName = process.argv[3] || ComponentName //组件中文名 const componentFnName = '$' + getUnifiedName(component_name) //组件函数名 /** 以下模版字符串不能缩进,否则创建的文件内容排版会混乱 **/ const createFiles = [ { path: path.join(config.packagesPath, component_name, 'index.js'), content: `import Vue from 'vue' import ${ComponentName} from './src/main.vue' const Component = Vue.extend(${ComponentName}) ${ComponentName}.install = function(Vue) { Vue.component(${ComponentName}.name, ${ComponentName}) Vue.prototype.${componentFnName} = function() { const instance = new Component() instance.$mount() return instance } } export default ${ComponentName}` }, { path: path.join(config.packagesPath, component_name, 'src', 'main.scss'), content: `@import '~@/style/common/variable.scss'; @import '~@/style/common/mixins.scss'; @import '~@/style/common/functions.scss';` }, { path: path.join(config.packagesPath, component_name, 'src', 'main.vue'), content: `<template> </template> <script> export default { name: '${getUnifiedName(component_name)}' } </script> <style lang="scss" scoped> @import './main.scss'; </style>` }, { path: path.join(config.examplesPath, 'src', 'doc', `${component_name}.md`), content: `## ${ComponentName} ${componentCnName} <div class="example-conainer"> <div class="phone-container"> <div class="phone-screen"> <div class="title"></div> <div class="webview-container"> <sg-${component_name}></sg-${component_name}> </div> </div> </div> <div class="edit-container"> <edit-component> </div> </div> <script> import editComponent from '../components/edit-components/${component_name}' export default { data() { return { } }, components: { editComponent } } </script> ` }, { path: path.join(config.examplesPath, 'src/components/edit-components', `${component_name}.vue`), content: `` } ] const componentsJson = require(path.join(config.srcPath, 'components.json')) const docNavConfig = require(path.join(config.examplesPath, 'src', 'router', 'nav.config.json')) if(docNavConfig[component_name]) { console.log(`${component_name} 已经存在,请检查目录或者components.json文件`) process.exit(0) } if(componentsJson[component_name]) { console.log(`${component_name} 已经存在,请检查目录或者nav.config.json文件`) process.exit(0) } createFiles.forEach(file => { fileSave(file.path) .write(file.content, 'utf8') .end('\n'); }) componentsJson[component_name] = {} componentsJson[component_name].path = `./packages/${component_name}/index.js` componentsJson[component_name].cnName = componentCnName componentsJson[component_name].fnName = componentFnName componentsJson[component_name].propsData = {} docNavConfig[component_name] = {} docNavConfig[component_name].path = `./src/doc/${component_name}.md` docNavConfig[component_name].cnName = componentCnName docNavConfig[component_name].vueRouterHref = '/' + component_name docNavConfig[component_name].fnName = componentFnName fileSave(path.join(config.srcPath, 'components.json')) .write(JSON.stringify(componentsJson, null, ' '), 'utf8') .end('\n'); fileSave(path.join(config.examplesPath, 'src', 'router', 'nav.config.json')) .write(JSON.stringify(docNavConfig, null, ' '), 'utf8') .end('\n'); console.log('组件创建完成') 复制代码
以及删除组件
const path = require('path') const fsdo = require('fs-extra') const fileSave = require('file-save') const config = require('../config') const component_name = process.argv[2] const files = [{ path: path.join(config.packagesPath, component_name), type: 'dir' }, { path: path.join(config.examplesPath, 'src', 'doc', `${component_name}.md`), type: 'file' }, { path: path.join(config.srcPath, 'components.json'), type: 'json', key: component_name }, { path: path.join(config.examplesPath, 'src', 'router', 'nav.config.json'), type: 'json', key: component_name }] files.forEach(file => { switch(file.type) { case 'dir': case 'file': removeFiles(file.path) break; case 'json': deleteJsonItem(file.path, file.key); break; default: console.log('unknow file type') process.exit(0); break; } }) function removeFiles(path) { fsdo.removeSync(path) } function deleteJsonItem(path, key) { const targetJson = require(path) if(targetJson[key]) { delete targetJson[key] } fileSave(path) .write(JSON.stringify(targetJson, null, ' '), 'utf8') .end('\n'); } console.log('组件删除完成') 复制代码
如何开发vue组件
用过vue的同学应该知道vue开发组件有两种方式,一种是 vue.component()的方式,另一种是vue.extend()方式,可以在上面的创建文件代码中看见,这两种方式我都用到了。原因是,对于配置组件的页面,需要用到动态组件,对于移动端渲染,动态组件肯定是不行的,所以需要用到函数形式的组件。
如何打包vue组件
打包vue组件,当然不能将其他无用的功能打包进库中,所以再来一套单独的webpack配置
const path = require('path') const merge = require('webpack-merge') const webpackBaseConfig = require('./webpack.base') const miniCssExtractPlugin = require('mini-css-extract-plugin') const config = require('./config') const ENV = process.argv.NODE_ENV module.exports = merge(webpackBaseConfig, { output: { filename: 'senguo.m.ui.js', path: path.resolve(config.basePath, './dist/ui'), publicPath: '/dist/ui', libraryTarget: 'umd' }, externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' } }, module: { rules: [ { test: /\.(sc|c)ss$/, use: [miniCssExtractPlugin.loader, {loader: 'css-loader'}, {loader: 'sass-loader'}] } ] }, plugins: [ new miniCssExtractPlugin({ filename: "sg-m-ui.css" }) ] }) 复制代码
先看看组件的入口文件,这是通过配置文件自动生成的,所以不必操心什么,本文的会奉上精简版的vue组件开发webpack脚手架,可以直接拿去用哦。
//文件从 build/bin/build-entry.js生成 import SgAlert from './packages/alert/index.js' import SgSwipe from './packages/swipe/index.js' import SgGoodsList from './packages/goods-list/index.js' const components = [SgAlert,SgSwipe,SgGoodsList] const install = function(Vue) { components.forEach(component => { component.install(Vue) }) } /* istanbul ignore if */ if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } // module.exports = {install} // module.exports.default = module.exports export default {install} 复制代码
是不是很简单啊。
该如何看见我开发的组件?
因为开发组件时肯定是需要一套webpack的配置用于启动web服务和热更新,所以在build文件夹中,编写了另外一套webpack配置用于开发时预览组件
<--webpack.dev.js--> const path = require('path') const webpack = require('webpack') const merge = require('webpack-merge') const webpackBaseConfig = require('./webpack.base') const webpackCleanPlugin = require('clean-webpack-plugin') const config = require('./config') module.exports = merge(webpackBaseConfig, { module: { rules: [{ test: /\.(sc|c)ss$/, use: [{ loader: 'style-loader' }, { loader: 'vue-style-loader', }, { loader: 'css-loader' }, { loader: 'sass-loader' }] }] }, devServer: { host: '0.0.0.0', publicPath: '/', hot: true, }, plugins: [ new webpackCleanPlugin( ['../dist'], { root: config.basePath, allowExternal: true } ), new webpack.HotModuleReplacementPlugin() ] }) 复制代码
<--webpack.demo.js--> const path = require('path') const merge = require('webpack-merge') const webpackDevConfig = require('./webpack.dev') const config = require('./config') const htmlWebpackPlugin = require('html-webpack-plugin') const webpackDemoConfig= merge(webpackDevConfig, { entry: path.resolve(config.examplesPath, 'index.js'), output: { filename: 'index.js', path: path.resolve(config.basePath, './dist'), publicPath: '/' }, module: { rules: [{ test: /\.md$/, use: [ { loader: 'vue-loader' }, { loader: 'vue-markdown-loader/lib/markdown-compiler', options: { raw: true } }] }, { test: /\.html$/, use: [{loader: 'html-loader'}] }, ] }, plugins: [ new htmlWebpackPlugin({ template: path.join(config.examplesPath, 'index.html'), inject: 'body' }) ] }) module.exports = webpackDemoConfig 复制代码
在其中可以看见使用了md文件,使用md文件的目的是:
- 可以在开发时直接预览组件
- 可以很方便的编写说明文档
通过vue-markdown-loader就可以将md文件解析成vue文件了,这个库是element ui 的官方人员开发的,其实原理很简单,就是将md文档先解析成html文档,再将html文档放入vue文档的template标签内,script 和 style标签单独抽离并排放置,就是一个vue的文档了,解析完后交给vue-loader处理就可以将md文档内容渲染到页面了。
那么预览页面的路由该如何处理呢?
就像上面创建文件那样,通过配置文件以及脚本动态生成路由文件,运行之前,先创建路由js文件即可
配置文件一览
{ "main": { "path": "./src/pages/main.vue", "cnName": "首页", "vueRouterHref": "/main" }, "alert": { "path": "./src/doc/alert.md", "cnName": "警告", "vueRouterHref": "/alert" }, "swipe": { "path": "./src/doc/swipe.md", "cnName": "轮播", "vueRouterHref": "/swipe" }, "goods-list": { "path":"./src/doc/goods-list.md", "cnName": "商品列表", "vueRouterHref": "/goods-list" } } 复制代码
构建完成的路由文件
//文件从 build/bin/build-route.js生成 import Vue from 'vue' import Router from 'vue-router' const navConfig = require('./nav.config.json') import SgMain from '../pages/main.vue' import SgAlert from '../doc/alert.md' import SgSwipe from '../doc/swipe.md' import SgGoodsList from '../doc/goods-list.md' Vue.use(Router) const modules = [SgMain,SgAlert,SgSwipe,SgGoodsList] const routes = [] Object.keys(navConfig).map((value, index) => { let obj = {} obj.path = value.vueRouterHref, obj.component = modules[index] routes.push(obj) }) export default new Router({ mode: 'hash', routes }) 复制代码
就这样,从组件的创建到项目的运行都是自动的啦。
介个编辑拖拽的功能要咋弄呢?
当然是用的插件啦,简单粗暴,看这里,它是基于 Sortable.js 封装的,有赞貌似用的也是这个库。
但看到右边的那个编辑框,我不禁陷入了沉思,怎么样才能做到只开发一次,这个配置页面就不用管理了?
编辑组件的组件???
由于中文的博大精深,姑且将下面的关键字分为两种:
- 用于移动端展示的组件------ 功能组件
- 用于编辑功能组件的组件---- 选项组件
分析需求可以发现,功能组件的内容都是可以由选项组件编辑的,最初我的想法是,选项组件的内容也根据配置文件生成,比如组件的props数据,这样就不用开发选项组件了,仔细一想还是太年轻了,配置项不可能满足设计稿以及不同的需求。
只能开发另一套选择组件咯,于是乎将选项组件的内容追加到自动生成文件的列表,这样微微先省点事。
组件间的通信咋办?
功能组件与选项组件间的通信可不是一件简单的事,首先要所有的组件实现同一种通信方式,其次也不能因为参数的丢失而导致报错,更重要的是,功能组件在移动端渲染后需要将选项组件配置的选项还原。
嗯,用那些方式好呢?
vuex? 需要对每一个组件都添加状态管理,麻烦
eventBus? 我怕我记不住事件名
props?是个好办法,但是选项组件要怎么样高效的把配置的数据传递出来呢?v-model就是一个很优雅的方式
首先功能组件的props与选项组件的v-model绑定同一个model,这样就能实现高效的通信,就像这样:
<--swipe.md--> ## Swipe 轮播 <div class="example-conainer"> <div class="phone-container"> <div class="phone-screen"> <div class="title"></div> <div class="webview-container" ref="phoneScreen"> <sg-swipe :data="data"></sg-swipe> </div> </div> </div> <div class="edit-container"> <edit-component v-model="data"> </div> </div> <script> import editComponent from '../components/edit-components/swipe' export default { data() { return { data: { imagesList: ['https://aecpm.alicdn.com/simba/img/TB183NQapLM8KJjSZFBSutJHVXa.jpg'] } } }, components: { editComponent } } </script> 复制代码
就这样,完美解决组件间通信,但是这是静态的组件,别忘了还有一个难点,那就是动态组件该如何进行参数传递,以及知道传递什么参数而不会导致报错。
拖拽系统的构建
先看个示例图
其中左侧手机里的内容是用v-for渲染的动态组件,右侧选项组件也是动态组件,这样就实现了上面所想的,功能组件和选项组件只需开发完成,配置页面就会自动添加对应的组件,而不用管理,如下图所示
但这样就会有一个问题,每个组件内部的数据不一致,得知道选中的组件是什么,以及知道该如何传递正确的数据,还记得之前的配置文件吗?其实这些组件也是读取的配置文件渲染的,配置文件如下:
{ "alert": { // 组件名 "path": "./packages/alert/index.js", "cnName": "警告", "fnName": "$SgAlert", "propsData": {} //props需要传递的数据 }, "swipe": { "path": "./packages/swipe/index.js", "cnName": "轮播", "fnName": "$SgSwipe", "propsData": { "imagesList": ["https://aecpm.alicdn.com/simba/img/TB183NQapLM8KJjSZFBSutJHVXa.jpg"] } }, "goods-list": { "path": "./packages/goods-list/index.js", "cnName": "商品列表", "fnName": "$SgGoodsList", "propsData": { } } } 复制代码
每一个组件的配置都添加了propsData,里面的元素和组件props数据以及选项组件v-model关联,这样就不用担心缺失字段而报错了,但是这样的做法给开发添加了麻烦。
组件编写的过程中还得将数据手动添加到配置文件,看能不能直接读取vue文件的props解决这个问题
到了这一步,组件以及组件的编辑拖拽功能均已完成,要考虑的是,如何把编辑拖拽功能页面集成到现有的后台系统中去,因为拖拽编辑组件的功能是给客户用的,这里为了效率和组件系统一同开发了。
如何与现有商户后台系统集成
vue路由的配置,每一个路由都对应一个组件,那么这个系统也可以这样做,只需要把中间那部分拖拽配置组件的页面打包后引入到父工程(商户后台管理系统)中去就好了,那么该如何处理呢?其实很简单,将webpack打包入口设置成相对应的vue文件就行,就像这样。
const path = require('path') const merge = require('webpack-merge') const webpackBaseConfig = require('./webpack.base') const config = require('./config') const miniCssExtractPlugin = require('mini-css-extract-plugin') const ENV = process.argv.NODE_ENV module.exports = merge(webpackBaseConfig, { entry: path.resolve(config.examplesPath, 'src/manage-system-app.vue'), output: { filename: 'components-manage.js', path: path.resolve(config.basePath, './dist/components-manage'), publicPath: '/dist/components-manage', libraryTarget: 'umd' }, externals: { vue: { root: 'Vue', commonjs: 'vue', commonjs2: 'vue', amd: 'vue' } }, module: { rules: [ { test: /\.(sc|c)ss$/, use: [ miniCssExtractPlugin.loader, {loader: 'css-loader'}, {loader: 'sass-loader'} ] } ] }, plugins: [ new miniCssExtractPlugin({ filename: "components-manage.css" }) ] }) 复制代码
然后在父工程引入组件库以及样式文件,再将路由对应的组件配置成这个打包后的js文件就行。
import EditPage from '@/pages/EditPage.js' new Router({ routes: [{ path: '/edit-page', components: EditPage }] }) 复制代码
组件渲染系统
这还不简单么,看代码就懂了。
class InsertModule { constructor(element, componentsData, thatVue) { if(element instanceof String) { const el = document.getElementById(element) this.element = el ? el : document.body } else if(element instanceof HTMLElement) { this.element = element } else { return console.error('传入的元素不是一个dom元素id或者dom元素') } if(JSON.stringify(componentsData) == '[]') { return console.error('传入的组件列表为空') } this.componentsData = componentsData this.vueInstance = thatVue this.insertToElement() } insertToElement() { this.componentsData.forEach((component, index) => { const componentInstance = (this.vueInstance[component.fnName] && this.vueInstance[component.fnName] instanceof Function && this.vueInstance[component.fnName]({propsData: component.propsData}) || {} ) if (componentInstance.$el) { componentInstance.$el.setAttribute('component-index', index) componentInstance.$el.setAttribute('isComponent', "true") componentInstance.$el.setAttribute('component-name', component.fnName) this.element.appendChild( componentInstance.$el ) } else { console.error(`组件 ${component.fnName} 不存在`) } }) } } const install = function(Vue) { Vue.prototype.$insertModule = function(element, componentsData) { const self = this; return new InsertModule(element, componentsData, self) } } /* istanbul ignore if */ if (typeof window !== 'undefined' && window.Vue) { install(window.Vue); } export default {install} 复制代码
这里将 组件的props数据传入至组件完成相关配置,这也是之前为什么选择prosp通信的原因
this.vueInstance[component.fnName]({propsData: component.propsData}) <-- swipe.js --> import Vue from 'vue' import Swipe from './src/main.vue' const Component = Vue.extend(Swipe) Swipe.install = function(Vue) { Vue.component(Swipe.name, Swipe) Vue.prototype.$SgSwipe = function(options) { const instance = new Component({ data: options.data || {}, propsData: {data: options.propsData || {}} //这里接收了数据 }) instance.$mount() return instance } } export default Swipe 复制代码
就系介样,渲染完成,200元一条的8g内存的梦啥时候能够实现?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 干货 | 解释 ReGenesis 构想
- 云端木犀-MAE初步构想
- 失眠应用:一个深夜开发者的程序构想
- APT攻击演练之捕鱼人的构想
- React 组件模式-有状态组件 x 无状态组件、容器组件 x 展示组件、高阶组件 x 渲染回调(函数作为子组件)
- Serverless 组件开发尝试:全局变量组件和单独部署组件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。