若是你常常接触一些公司的活动页,可能会常常头疼如下问题:这些项目周期短,需求频繁,迭代快,技术要求不高,成长空间也小。可是咱们仍是快马加鞭的赶着产品提来的一个个需求,随着公司规模的增长,咱们不可能无限制的增长人手不断地重复着这些活动。这里我就不具体介绍一些有的没的的一些概念了,由于要介绍的概念实在太多了,做为一个前端的咱们,直接上代码撸就行了!!!! 想要了解更多,也欢迎访问:javascript
blogshtml
咱们的目标是实现一个页面制做后台,在后台中咱们能够对页面进行 组件选择 --> 布局样式调整 --> 发布上线 --> 编辑修改
这样的流程操做。前端
首先是要能提供组件给用户进行选择,那么咱们须要一个组件库
,而后须要对选择的组件进行布局样式调整,因此咱们须要一个页面编辑后台
接着咱们须要将编辑产出的数据渲染成真实的页面,因此咱们须要一个node服务
和用于填充的template 模板
。发布上线,这个直接对接各个公司内部的发布系统就行了,这里咱们不作过多阐述。最后的编辑修改功能也就是针对配置的修改,因此咱们须要一个数据库,这里我选择的是用了mysql
。固然你也能够顺便作作权限管理,页面管理....等等之类的活。 啰嗦了这么长,咱们来画个图,了解下大概的流程:vue
首先咱们来实现组件这一部分,由于组件关联着后台编辑的预览和最后发布的使用。组件设计咱们应该尽可能保持组件的对外一致性,这样在进行渲染的时候,咱们能够提供一个统一的对外数据接口。这里咱们的技术选型是基于 Vue 的,因此下面的代码部分也主要是基于 Vue 的,可是万变不离其宗,其余语言也相似。java
根据上图,咱们的组件是会被一个个拆分单独发布到 npm
仓库的,为何这么设计呢?其实以前也考虑过设计成一个组件库,全部组件都包含在一个组件库内,这样只须要发布一个组件库包,用的时候按需加载就行了。后来在实践的过程当中发现这样并不合适协同开发,其余前端若是想贡献组件,接入的改形成本也很大。举个🌰:小明在业务中写了个Button
组件,这个组件常常会被其余项目复用,他想把这个组件贡献到咱们的系统中,被模板使用,若是是一个组件库的话,他首先得拉取咱们组件库的代码,而后按照组件库的规范格式进行提交。这样一来,偷懒的小明可能就不太愿意这么干,最爽的方法固然是在本地构建一个npm库,开发选用的是用TypeScript
仍是其余的咱们不关心,选用的 Css 预处理器咱们也不关心,甚至编码规范的ESLint
咱们也不关心。最后只需经过编译后的文件便可。这样就避免了一个组件库的约束。依托于NPM完善的发布/拉取,以及版本控制机制,可让咱们少作一些额外的工做,也能够快速的把平台搭建起来。node
说了这么多,代码呢?,咱们以一个Button
为例,咱们对外提供这样的形式组件:mysql
<template>
<div :style="data.style.container" class="w_button_container">
<button :style="data.style.btn"> {{data.context}}</button>
</div>
</template>
<script> export default { name: 'WButton', props: { data: { type: Object, default: () => {} } } } </script>
复制代码
能够看到咱们只对外暴露了一个props
,这样作法的好处是能够统一组件对外暴露的数据,组件内部爱怎么玩怎么玩。注意,这里咱们也能够引入一些第三方组件库,好比mint-ui
之类的。webpack
在写代码前,咱们先考虑一下须要实现哪些功能:git
props
的功能按照顺序,咱们先来实现组件的属性编辑功能。咱们要考虑,一个组件暴露出哪些可配置的信息。这些可配置的信息如何同步到后台编辑区,让使用者进行编辑,一个按钮的可配置信息多是这样:github
若是把这些配置所有写在后台库里面,根据当前选择的组件加载不一样的配置,维护起来会至关麻烦,并且随着组件数量的增长,也会变得臃肿,因此咱们能够将这些配置存储在服务端,后台只须要根据存储的规则进行解析即可,举个例子,咱们其实能够存储这样的编辑配置:
[
{
"blockName": "按钮布局设置",
"settings": {
"src": {
"type": "input",
"require": true,
"label": "按钮文案"
}
}
}
]
复制代码
咱们在编辑后台,经过接口请求到这些配置,即可以进行规则渲染:
/** * 根据类型,选择建立对应的组件 * @param {VNode} vm * @returns {any} */
createEditorElement (vm: VNode) {
let dom = null
switch (vm.config.type) {
case 'align':
dom = this.createAlignElement(vm)
break;
case 'select':
dom = this.createSelectElement(vm)
break;
case 'actions':
dom = this.createActionElement(vm)
break;
case 'vue-editor':
dom = this.createVueEditor(vm)
break;
default:
dom = this.createBasicElement(vm)
}
return dom
}
复制代码
首先咱们须要考虑的是,组件怎么进行注册?由于组件被用户选用的时候,咱们是须要渲染该组件的,因此咱们能够提供一段 node 脚原本遍历所需组件,进行组件的安装注册:
// 定义渲染模板和路径
var OUTPUT_PATH = path.join(__dirname, '../packages/index.js');
console.log(chalk.yellow('正在生成包引用文件...'))
var INSTALL_COMPONENT_TEMPLATE = ' {{name}}';
var IMPORT_TEMPLATE = 'import {{componentName}} from \'{{name}}\'';
var MAIN_TEMPLATE = `/* Automatic generated by './compiler/build-entry.js' */ {{include}} const components = [ {{install}} ] const install = function(Vue) { components.map((component) => { Vue.component(component.name, component) }) } /* istanbul ignore if */ if (typeof window !== 'undefined' && window.Vue) { install(window.Vue) } export { install, {{list}} } `;
// 渲染引用文件
var template = render(MAIN_TEMPLATE, {
include: includeComponentTemplate.join(endOfLine),
install: installTemplate.join(`,${endOfLine}`),
version: process.env.VERSION || require('../package.json').version,
list: listTemplate.join(`,${endOfLine}`)
});
// 写入引用
fs.writeFileSync(OUTPUT_PATH, template);
复制代码
最后渲染出来的文件大概是这样:
import WButton from 'w-button'
const components = [
WButton
]
const install = function(Vue) {
components.map((component) => {
Vue.component(component.name, component)
})
}
/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
export {
install,
WButton
}
复制代码
这个也是组件库的通用写法,因此这里的思想就是把发布到npm
上的组件,进行聚合,聚合成一个组件包引用,咱们在后台编辑的时候,是须要全量引入的:
import * as W_UI from '../../packages'
Vue.use(W_UI)
复制代码
这样,咱们组件便注册完了,组件选择区,主要是提供组件的可选项,咱们能够遍历组件,提供一个个 List 让用户选择,固然若是咱们每一个组件若是只提供一个组件名,用户可能并不知道组件长什么样,因此咱们最好能够提供一下组件长什么样的缩略图。这里咱们能够在组件发布的时候,也经过 node 脚本进行。这里要实现的代码比较多,我就大体说一下过程,由于也不是核心逻辑,无关紧要,只能说有了体验上会好一点:
puppeteer
,调整页面到手机端模式, 进行当前 dev-server 截图。这样,就能够在加载组件选择区的时候,为组件附上缩略图。
当用户在选择区选择了组件,咱们须要展现在预览区域,那么咱们怎么知道用户选择了哪些组件呢?总不能提早所有把组件写入渲染区域,经过 v-if
来判断选择吧?固然没有这么蠢,Vue 已经提供了动态组件的功能了:
<div :class="[index===currentEditor ? 'active' : '']" :is="select.name" :data="select.data">
</div>
复制代码
为何咱们不用缩略图代替真实组件?一方面生成的缩略图尺寸存在问题,另外一方面,咱们须要编辑的联动性,就是编辑区的编辑须要及时的反馈给用户。
说了这么多,貌似一切都很顺利,可是这样在实践的时候,发现了存在一个明显的问题就是:咱们中间的预览区域其实就是为了尽量模拟移动端页面效果。可是若是咱们加入了一些包含相似 position: fixed
样式的组件,会发现样式上就出现了明显的问题。典型的好比Dialog Loading
等。 因此咱们参考了 m-ui
组件库的设计,将中间预览操做容器展现为一个iframe
。将iframe
大小调整为375 * 667
,模拟 iPhone 6 的手机端。这样就不会存在样式问题了。但是这样又出现了另外一个难点,那就是左侧的编辑数据如何及时的反应到iframe
中?没错,就是postMessgae
,大体思路以下:
利用 vuex
作数据存储池,全部的变化,经过 postMessgae
进行同步,这样咱们只用确保数据池中的数据变化,即可以映射到渲染层的变化。好比,咱们在预览区进行了组件选择和拖拽排序,那么咱们只需经过vuex
出发同步信息即可:
// action.ts
const action = {
setCurrentPage ({commit, state}, page: number) {
// 更新当前store
commit('setCurrentPage',page)
// 对应postMessage
helper.postMsgToChild({type: 'syncState', value: state})
},
// ...
}
复制代码
模板的设计实现,我参考了 Vue-cli 2.x
版本的思想,把这里的模板,存在了对应的 git
仓库中。当用户须要进行页面构建的时候,直接从 git 仓库中拉取对应的模板便可。固然拉取完,也会缓存一份在本地,之后渲染,直接从本地缓存中读取便可。咱们如今把中心放在模板的格式和规范上。模板咱们采用什么样的语法无所谓,这里我才用了和 Vue-cli
同样的Handlerbars
引擎。这里直接上咱们模板的设计:
<template>
<div class="pg-index" :style="{backgroundColor: '{{bgColor}}'}">
<div class="main-container" :style="{ backgroundColor: '{{bgColor}}', backgroundImage: '{{bgImage}}' ? 'url({{bgImage}})' : null, backgroundSize: '{{bgSize}}', backgroundRepeat: 'no-repeat' }">
{{#components}}
<div class="cp-module-editor {{className}} {{data.className}}">
<{{name}} class="temp-component" :data="{{tostring data}}" data-type="{{upcasefirst name}}"></{{name}}>
</div>
{{/components}}
</div>
</div>
</template>
<script></script>
复制代码
为了简化逻辑,咱们把模板都设计成流式布局,全部组件一个个堆叠往下顺序排列。这个文件即是咱们vue-webpack-simple
的模板中的App.vue
。咱们对其进行了改写。这样在数据填充万,即可以渲染出一个 Vue 单文件。这里我只举着一个例子,咱们还能够实现多页模板等等复杂的模板,根据需求拉取不一样的模板便可。
当后台提交渲染请求的时候,咱们的 node 服务所作的工做主要是:
拉取也就是去指定仓库中经过download-git-repo
插件进行拉取模板。编译其实也就是经过metalsmith
静态模板生成器把模板做为输入,数据做为填充,按照handlebars
的语法进行规则渲染。最后产出build
构建好的目录。在这一步,咱们以前所需的组件,会被渲染进package.json
文件。咱们来看一下核心代码:
// 这里就像一个管道,以数据入口为生成源,经过renderTemplateFiles编译产出到目标目录
function build(data, temp_dest, source, dest, cb) {
let metalsmith = Metalsmith(temp_dest)
.use(renderTemplateFiles(data))
.source(source)
.destination(dest)
.clean(false)
return metalsmith.build((error, files) => {
if (error) console.log(error);
let f = Object.keys(files)
.filter(o => fs.existsSync(path.join(dest, o)))
.map(o => path.join(dest, o))
cb(error, f)
})
}
function renderTemplateFiles(data) {
return function (files) {
Object.keys(files).forEach((fileName) => {
let file = files[fileName]
// 渲染方法
file.contents = Handlebars.compile(file.contents.toString())(data)
})
}
}
复制代码
最后咱们获得的是一个 Vue 项目,此时还不能直接跑在浏览器端,这里就涉及到当前发布系统所支持的形式了。怎么说?若是你的公司发布系统须要在线编译,那么你能够把源文件直接上传到git仓库,触发仓库的 WebHook 让发布系统替你发掉这个项目便可。若是大家的发布系统是须要你编译后提交编译文件进行发布的,那么你能够经过 node 命令,进行本地构建,产出 HTML,CSS,JS。直接提交给发布系统便可。 到这里,咱们的任务就差很少了~具体的核心实心大多已经阐述清楚,若是实现当中有什么问题和不妥,也欢迎一块儿探讨交流!!
实现这样一套页面构建系统,其实我这里简化了不少东西,旨在给你们提供一种思路。另外,其实咱们的页面所有在服务端构建的时候产出,咱们能够再服务端这一层作不少工做,好比页面的性能优化,由于页面数据咱们所有都有,咱们也能够作页面的预渲染,骨架屏,ssr,编译时优化等等。并且咱们也能够对产出的活动页作数据分析~有不少想象的空间。