1

在上一篇基于vue的页面配置化的实现(上)——模板开发中,我们完成了模板开发。

本篇主要分两个部分:

  1. 如何实现让运营在编辑器里配置这些参数,以及在编辑器里实时预览模板的效果呢
  2. 根据模板js和配置参数在服务端生成页面

编辑器

image.png

先看下效果,最左侧是页面信息,如果有多个页面,则显示多栏。中间是实时预览效果,右侧则是配置datasource的地方。考虑到页面可能根据链接参数有不同表现,所以正上方是支持自定义链接参数以便于实时预览。右上角则是进行保存发布操作。

设计

在本地模板开发阶段,我们最后拿到了以下几个打包后的文件:

接下来就是怎么使用这些js和json

image.png

实现

首先我们远程拉取模板信息,实现过程不赘述,假设我们已经拿到了下面几个关键信息

  • home.js:模板的页面组件
  • settings.js:配置面板组件
  • datasource.json:模板配置字段
  • config.json:模板基础信息

左侧页面列表

根据模板 config.json 我们可以遍历出页面的路由信息

右侧配置面板

远程加载配置面板组件

这里的目的最终是要实现配置面板settings跟页面的datasource绑定在一起,实现通过配置面板来修改datasource。

  1. 封装一个远程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)
     })
      }
    }
    
  2. 定义组件

    const template = await tLoader.load(name, `${url}?t=${new Date().getTime()}`)
    Vue.component('settings', template)
  3. 使用

    <component is="settings"></component>
  4. 将项目的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
父页面
  1. 新建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 页面
  1. 监听父页面传递过来的消息,并加载并定义模板路由组件

    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
         }
       })
  2. 加载路由组件

    <!--current.name 用于切换模板的路由-->
    <component
         :is="current.name"
         v-if="mounted"
         class="page"
       />

至此,编辑器的部分就完成了

服务端实现

本节不讨论如何存储模板和项目配置信息,存储的方案可以放在cdn上,也可以存储在数据库中;也不会讨论部署,只会讨论如何生成一个页面。

如果仔细看下来,大概能知道服务端要如何实现生产页面。还记得在本地模板开发的时候,本地预览的实现吗?没错,原理都是一致的。

实现

  1. 用 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>
    
  2. 渲染模板

    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 异步加载路由组件

JontyLu
29 声望6 粉丝

引用和评论

0 条评论