在上一篇基于 vue 的页面配置化的实现(上)——模板开发中,咱们实现了模板开发。
本篇次要分两个局部:
- 如何实现让经营在编辑器里配置这些参数,以及在编辑器里实时预览模板的成果呢
- 依据模板 js 和配置参数在服务端生成页面
编辑器
先看下成果,最左侧是页面信息,如果有多个页面,则显示多栏。两头是实时预览成果,右侧则是配置 datasource 的中央。思考到页面可能依据链接参数有不同体现,所以正上方是反对自定义链接参数以便于实时预览。右上角则是进行保留公布操作。
设计
在本地模板开发阶段,咱们最初拿到了以下几个打包后的文件:
接下来就是怎么应用这些 js 和 json
实现
首先咱们近程拉取模板信息,实现过程不赘述,假如咱们曾经拿到了上面几个要害信息
- home.js:模板的页面组件
- settings.js:配置面板组件
- datasource.json:模板配置字段
- config.json:模板根底信息
左侧页面列表
依据模板 config.json 咱们能够遍历出页面的路由信息
右侧配置面板
近程加载配置面板组件
这里的目标最终是要实现配置面板 settings 跟页面的 datasource 绑定在一起,实现通过配置面板来批改 datasource。
-
封装一个近程 tLoader 的办法,拉取近程组件
function loadJS(url = '') {if (!url) return if (!loadJS.cache) loadJS.cache = {} if (loadJS.cache[url]) return Promise.resolve() return new Promise((resolve, reject) => { _loadjs( url, () => {loadJS.cache[url] = 'cached' resolve()}, () => {console.error(`${url}加载失败 `) reject(new Error(`${url}加载失败 `)) } ) }) function _loadjs(url, fn, fail) {var script = document.createElement('script') script.src = url script.async = true script.onload = fn script.onerror = fail ;(document.body || document.head).appendChild(script) } } export default {requestCache: {}, componentCache: {}, getComponent(name, cdn) {return this.componentCache[name] }, async load(name, cdnPath) {let component = this.getComponent(name) if (component) return component let request = ''let url ='' if (!this.requestCache[name]) { url = cdnPath // url += '?t=' + new Date().getTime() this.requestCache[name] = request = loadJS(cdnPath) } else request = this.requestCache[name] return await request.then((res) => {component = window[name] this.componentCache[name] = component return component }).catch(err => {console.log('加载失败', url) console.error(err) }) } }
-
定义组件
const template = await tLoader.load(name, `${url}?t=${new Date().getTime()}`) Vue.component('settings', template)
-
应用
<component is="settings"></component>
-
将我的项目的 datasource 定义为全局变量,合并模板的 data 和我的项目的 data
const templateData = await axios.get('datasource.json') const projectData = await axios.get('projectDatasource.json') window['datasource'] = Object.merge(templateData, projectData)
正中间预览面板
原理:应用 iframe 加载页面,而后通过 Vue.observable()绑定 top.datasource
父页面
-
新建 iframe
<iframe ref="page-iframe" :key="iframeKey" frameborder="0" :src="pageRoute" @load="iframeMounted" /> <script> export default { methods: { // iframe 加载实现后,告诉子页面能够开始加载模板的路由组件了 iframeMounted() { this.post({ type: 'initPage', data: {js: `${js}.js`, // 加载模板路由 js 的 url 地址,让子页面可能加载并加载 } }) } } } </script>
pageRoute 是用来加载 iframe 地址的,能够动静更新路由参数。
iframe 页面
-
监听父页面传递过去的音讯,并加载并定义模板路由组件
window.addEventListener('message', function(d) {const { type, data} = d.data switch (type) { case 'initPage': //!!!,绑定父窗口的 datasource window['datasource'] = Vue.observable(top.datasource) // 挂载 page 的 component,原理跟父页面的 settings 的加载形式统一 const template = await tLoader.load(data.name, `${data.js}?t=${new Date().getTime()}`) Vue.component(data.name, template) this.current.name = data.name // 以后页面 break default: break } })
-
加载路由组件
<!--current.name 用于切换模板的路由 --> <component :is="current.name" v-if="mounted" class="page" />
至此,编辑器的局部就实现了
服务端实现
本节不探讨如何存储模板和我的项目配置信息,存储的计划能够放在 cdn 上,也能够存储在数据库中;也不会探讨部署,只会探讨如何生成一个页面。
如果认真看下来,大略能晓得服务端要如何实现生产页面。还记得在本地模板开发的时候,本地预览的实现吗?没错,原理都是统一 的。
实现
-
用 ejs 作为模板语言, 新建 index.ejs,用于渲染出 html
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="renderer" content="webkit"/> <meta name="force-rendering" content="webkit"/> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/> <meta name="viewport" id="viewport" content="width=device-width,initial-scale=1.0"> <title></title> <style> html{ width: 100%; height: 100%; } <%= styles %> </style> <!-- import vue and ui js --> <% for(var j of js) { %> <script src="<%=j%>"></script> <% } %> </head> <body> <div id="app"></div> <!-- template router js --> <% for(var j in interactjs) { %> <script src="<%=interactjs[j]%>"></script> <% } %> <script> /* 路由 */ var routes = [<% for (var n of routes) { %> {<% for (var i in n) { %> <% if (i === 'component') { %> <%- i %>: <%- "window['" + n[i] + "']" %>, <% } else { %> <%- i %>: <%- JSON.stringify(n[i]) %>, <% } %> <% } %> }, <% } %> ] var router = new VueRouter({ mode: 'hash', routes: routes }); var datasource = new Vue.observable(<%- JSON.stringify(project) %>) var instance = new Vue({ router: router, render: function (h, context) { return h( 'div', {}, [ h('router-view', {on: {} }, null), ] ) }, }).$mount('#app'); </script> </body> </html>
-
渲染模板
const fs = require('fs') const request = require('request') const prettier = require('prettier') // 下载模板的路由组件 js /* const dlJs = (url, stream) => new Promise(resolve => {request(url).pipe(stream).on('finish', () => {resolve() }) }) */ // 模板路由组件 js const interactjs = {'home': `./home.js?t=${Date.now()}` } // 从模板的 config.json 读取路由信息 const routes = require('./config.json') // 获取 datasource.json const project = require('./datasource.json') // 获取 ejs const pageTemplate = fs.readFileSync(path.join(__dirname, 'template/index.ejs'), 'utf8') // render 渲染 ret = ejs.render(pageTemplate, {styles: styles.join(''), js: ['./vue.runtime.min.js', './vue-router.js'], interactjs, css, routes, project, }) // 格式化文档 const prettierHtml = prettier.format(ret, { parser: 'html', printWidth: 80, singleQuote: true, }) // 生成文件 fs.writeFileSync(path.join(projectPath, 'index.html'), prettierHtml) // TODO // 将 ./*.js 的文件拷贝或下载到与 index.html 同级目录下
最初生成的工程跟咱们在模板开发的时候本地预览的 dist 简直统一的,只是生产不再须要 settings
优化的思路: window 间接注入能够换成 system.js 异步加载路由组件