项目需求

一套功能类似于有赞商城后台管理系统中店铺-微页面的系统,该系统实现用户可以选择所需要的组件,拖动调整组件的顺序,以及编辑组件的内容并且在移动端展示等相关功能,如下图所示。

开始前的思考

系统主要功能

仔细想了一想,该系统需要实现下面三大功能

  • 组件系统
  • 移动端组件生成系统
  • 后台管理系统组件编辑功能

基于什么开发

服务端渲染?还是用原生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 = componentFnNamefileSave(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_ENVmodule.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文件的目的是:

  1. 可以在开发时直接预览组件
  2. 可以很方便的编写说明文档

通过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封装的,有赞貌似用的也是这个库。

但看到右边的那个编辑框,我不禁陷入了沉思,怎么样才能做到只开发一次,这个配置页面就不用管理了?

编辑组件的组件???

由于中文的博大精深,姑且将下面的关键字分为两种:

  1. 用于移动端展示的组件------ 功能组件
  2. 用于编辑功能组件的组件---- 选项组件

分析需求可以发现,功能组件的内容都是可以由选项组件编辑的,最初我的想法是,选项组件的内容也根据配置文件生成,比如组件的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_ENVmodule.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 = componentsDatathis.vueInstance = thatVuethis.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内存的梦啥时候能够实现?

结束语

最后,奉上此系统精简版的webpack配置,除了没拖拽系统以及组件渲染系统,其他的基本都支持,可以在此配置上定制自己的功能,编写自己的组件系统,但是强烈建议阅读element ui的脚手架配置,尝试从0-1定制自己的脚手架哦。

github.com/Richard-Cho…

一套自生成组件系统的构想与实践相关推荐

  1. 文档生成组件工作估算

    笔者最近做一个系统的工作量预估,其中包含一个通用组件,文档生成组件的工作量预估,在此分享下. 功能要求:通过定制文档模板,依据设置的文档数据,抽取数据或图表,生成文档,记录所有生成文档,实现文档查询. ...

  2. mall-swarm是一套微服务商城系统

    介绍: mall-swarm是一套微服务商城系统,采用了 Spring Cloud Hoxton & Alibaba.Spring Boot 2.3.Oauth2.MyBatis.Elasti ...

  3. 最新鲁班H5页面生成工具系统源码+功能强大/仿易企秀

    正文: 最新鲁班H5页面生成工具系统源码+功能强大/仿易企秀,这系统的功能真的非常强大,都是主流很高级的一些技术开发的. Vue2.0开发,通过拖拽的形式,生成页面的工具,类似易企秀.百度H5等工具. ...

  4. TOPERS中间件之一---嵌入式组件系统TECS

    TECS(TOPPERS Embedded Component System)为TOPPERS推出的一个针对嵌入式系统,将各种软件模块封装为组件,并将组件结合在一起以实现快速构建大规模嵌入式系统软件的 ...

  5. 用ECS做HexMap:自动生成地图系统

    基于Unity2019最新ECS架构开发MMO游戏笔记16 自动生成地图系统 AutoCreateMapSystem 神奇的六边形 六边形实体 创建者和创建六边形单元系统 更新计划 作者的话 ECS系 ...

  6. 前端vue uni-app自定义精美海报生成组件

    在当前技术飞速发展的时代,软件开发的复杂度也在不断提高.传统的开发方式往往将一个系统做成整块应用,一个小的改动或者一个小功能的增加都可能引起整体逻辑的修改,从而造成牵一发而动全身的情况.为了解决这个问 ...

  7. 从零搭建一套结构光3D重建系统[理论+源码+实践]

    01 背景介绍 图1 典型3D结构光系统 尽管结构光作为一种已经相当成熟,并且广泛应用的三维重建技术,不同于深度学习,依旧缺乏相关的课程,网上的开源资料寥寥无几,即使有,也是晦涩难懂,许多刚入门的研究 ...

  8. Unity* 实体组件系统 (ECS)、C# 作业系统和突发编译器入门

    Unity* 中的全新 C# 作业系统和实体组件系统不仅可以让您轻松利用以前未使用的 CPU 资源,还可以帮助您更高效地运行所有游戏代码.然后,您可以使用这些额外的 CPU 资源来添加更多场景动态和沉 ...

  9. Angular-cli生成组件修改css成less或sass

    使用cli命令生成组件: ng generate component 组件名 生成出来的组件文件有:html / ts / css / spec.ts 问题我是一个less重度患者怎么可能再去写css ...

最新文章

  1. cocos2d-x触摸事件优先级
  2. 计算机科学与技术导论
  3. python 矩阵元素赋值_对numpy中数组元素的统一赋值实例
  4. 程序员的10个快乐瞬间!
  5. 中间滑动 头部底部固定_固定抗震成品支座功能特点及作用
  6. windows应用迁移到linux下
  7. python DataFrame数据分组统计groupby()函数
  8. STM32驱动SPI FLASH(W25Q64)
  9. 威富通移动支付开发文档
  10. 一起学爬虫(Python) — 04
  11. numpy: np.asarray 函数
  12. 怎样在mac系统里将文件拷贝到移动硬盘教程
  13. AH快递单打印查询软件V3.68
  14. LIME-论文阅读笔记
  15. 编程中经常遇到的调试没问题,运行却出错的一种原因
  16. 360又抢了12306的风头:它为什么能提前49天订票?
  17. Java中事务的处理全解析
  18. 大鱼号的收益怎么样?自媒体平台大鱼号,最大方的就是它
  19. Python startswith endswith
  20. 软件公司,销售管理门道(七)销售协同

热门文章

  1. java 随手记 百度影音API调用
  2. 中国数字出版产业前景动态分析及投资战略研究报告2022-2027年
  3. JavaScript上传图片转base64
  4. tp报错: SQLSTATE[42000]: Syntax error or access violation: 1055 Expression #1 of SELECT list is
  5. java字符串处理将品牌和型号分开
  6. 【Swift初见】Swift数组
  7. java p12证书验证_authentication - 使用.p12证书验证Selenium WebDriver(Java) - SO中文参考 - www.soinside.com...
  8. JUnit 4 如何正确测试异常
  9. 点石互动--Zac 之:站内链接的优化
  10. react+ts+redux+react-router-dom+less+反向代理+antd