在上一篇文章浅谈前端可视化编辑器的实现中,简单介绍了一下可视化编辑器的实现方式。用户通过拖拉拽组件的方式布局页面,然后通过vue的render函数将页面生产出来。这种方式对于高度自定义页面的业务场景是比较适合的,比如说发布一篇文章资讯,只需要配一个富文本,再加一些组件或者动画丰富一下,运营同学就可以直接发布一篇文章。

但对于一些业务紧密的页面,如果还要运营同学一个个拖拉组建拼凑页面,会十分浪费时间,并且因为特定业务场景去开发对应的业务组件过大,会造成编辑器组件库的臃肿。

目的

希望能够针对可复用的单一业务,抽离成页面模板,配置相关参数就可以生产不同页面,运营不需要关心内部逻辑,只需要根据不同场景去生产产品。如下图所示:

image.png

这种基础页面还是比较常见的,有一些核心业务功能。如果每次产出这种页面需要提需求给开发,是比较浪费时间的。我们开发把这业务抽成一个模板,运营同学只需要关注几点:

  1. 出图并配置图片
  2. 配置活动(抽奖)素材和活动id
  3. 配置游戏链接

比起【下需求-需求评审-设计-开发-测试-上线】这种流程,页面模板在第二次活动上线的时候省去了下需求、需求评审、开发和测试这几个过程。功能在第一次上线的时候就验证过了,所以后续不用开发和测试介入,极大的提升了生产力。

原理

vue 的核心思想之一——组件化

设计思路

未命名文件.png

  1. 本地模板开发

    • 业务代码开发
    • 本地预览
    • 模板上传
  2. 编辑器加载模板

    • 编辑器远程加载模板
    • 参数配置
    • 实时预览并发布
  3. 服务端生产

    • 生成代码
    • 生产部署

页面模板

在这一环节我们的目的是要把页面打包成一个组件,比如首页打成home.js这样的组件,然后挂载到VueRouter上。

目录结构

|-- build // 构建脚本
|-- build-entry // 用于存放根据模板生成的文件
|-- dist // 项目打包出来的js
|-- src // 业务代码
|-- webpack.config.js // 构建命令

一、模板开发

  1. 在src下新建 pageA/home.vue
    home.vue:这里指代页面,用于挂载在router上。
    datasource:全局注入的配置项,提供给运营侧修改的参数都放在该对象维护

    <template>
      <div class="home">
     <img :src="datasource.home.img" />
     <p>{{ datasource.title }}</p>
      </div>
    </template>
    <script>
    export default {
      data() {
       return {
         datasource: window.datasource // 配置数据
       }
      }
    }
    </script>


  2. 新建pageA/datasource.json
    用于定于配置项的内容,开放给用户调整参数。

    {
      "home": { // home 页面所需参数
         "img": "可配置图片",
         "title": "可配置的标题",
      },
    }


  3. 新建pageA/config.json
    定义模板信息和模板的路由信息,routes 主要为了定义模板的路由的信息,后面有场景需要读取该字段

    {
      "category": "活动",
      "title": "游戏下载H5",
      "author": "yl",
      "description": "普通H5页面点击下载游戏(包括假抽奖)",
      "routes": [
       {
         "pageType": "h5",
         "path": "/",
         "name": "home",
         "component": "home",
         "meta": {
           "title": "首页",
         }
       }
      ]
    }
  4. 新建pageA/setting.vue
    定义了可配置参数,需要一个入口提供给用户配置,所以需要开发配置面板,为了能在编辑器里让用户操作datasource。

    <template>
      <div class="settings">
     <!--iview-->
     <FormItem label="可配置的图片">
       <Input v-model="datasource.home.img" type="text" />
     </FormItem>
     <FormItem label="可配置的标题">
       <Input v-model="datasource.home.title" type="text" />
     </FormItem>
      </div>
    </template>
    <script>
    export default {
      name: 'settings',
      data() {
      return {
       datasource: window['datasource'] // datasource 为全局变量
      }
      }
    }
    </script>

    至此,我们的业务代码和模板的基本配置信息已经写好了,接下来就是要像怎么让他在本地跑起来,并且能打包成一个个组件。

二、构建配置

先定义好启动命令

// dev 本地预览
npm run dev --target=pageA

// pro 构建并推送至远程
npm run build --target=pageA
新建build/build.html.js

构建本地预览的html模板, {{ }}里的内容是用来替换字符串,在这个项目中字符串模板的插件用的是json-templater。

module.exports = `
<!DOCTYPE html>
  <html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{title}}</title>
    <script src="./vue.runtime.min.js"></script>
    <script src="./vue-router.js"></script>
    <style>
      .settings {
        position: fixed;
        width: 30%;
        height: 100%;
        right: 0;
        top: 0;
        background: #ffffff;
        box-shadow: 1px 1px 5px 2px #aaaaaa;
        padding: 20px;
        box-sizing: border-box;
        overflow: auto;
      }
      .build-html-button {
        position:fixed;
        right: 0;
        bottom: 0;
        width: 100px;
        height: 50px;
        background: green;
        color: #fff;
        z-index: 999;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script>
    
      /* 注册settings */
      Vue.component('settings');
      Vue.use(Vuex);
      /* 路由 */
      var routes = {{routes}};
      var router = new VueRouter({
        mode: 'hash',
        routes: routes
      });
      var datasource = new Vue.observable({{project}})
      var instance = new Vue({
        data: {
          sShow: false
        },
        router: router,
        render: function (h, context) {
          var self = this
          return h(
            'div',
            {
              class: {
                panel: true
              },
            },
            [
              h('router-view', {}, null),
              h('div', {
                style: {
                  position: 'relative',
                  zIndex: 99999
                }
              }, [
                h('button',{
                  class: {
                    'build-html-button': true
                  },
                  domProps: {
                    innerHTML: '配置面板'
                  },
                  on: {
                    click: function(e) {
                      console.log('触发')
                      self.sShow = !self.sShow
                      console.log(self.sShow)
                    }
                  },
                }),
                h('settings', {
                  style: {
                    display: self.sShow ? 'block' : 'none'
                  }
                }, null),
              ]),
            ]
          )
        },
      }).$mount('#app');
    </script>
  </body>
 </html>
`
新建build/build.entry.js

用于构建出webpack里所需要的entry,同样的,{{ }}的内容也是用于替换字符串

module.exports = `
import {{name}} from \'../src/pages/{{target}}/{{name}}.vue\'

{{name}}.install = function(Vue) {
  Vue.component({{name}}.name, {{name}})
}
const install = function(Vue, opts = {}) {
  Vue.component({{name}}.name, {{name}})
}

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default {{name}}

`
新建build/build.settings.js
module.exports = `
/***/
import settings from \'../src/pages/{{target}}/settings.vue\'

settings.install = function(Vue) {
  Vue.component(settings.name, settings)
}
/***/
const install = function(Vue, opts = {}) {
  Vue.component(settings.name, settings)
}

/* istanbul ignore if */
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

export default settings

`
新建build/build.js

该文件为主要执行文件,根据定义好的字符串模板去生成所需的文件

  1. 读取命令行

    const argv = JSON.parse(process.env.npm_config_argv)
    const remain = argv.remain
    remain.forEach(r => {
      arg = r.split('=')
      config[arg[0]] = arg[1]
    })
    let idx = 2;
    const cooked = argv.cooked
    const length = argv.cooked.length
    while ((idx += 2) <= length) {
      config[cooked[idx - 2]] = cooked[idx - 1]
    }
    // 获取项目名称,示例演示项目名为 pageA
    const target = config['--target']
    
    let env = ''
    if (cooked[1] === 'dev') env = 'development' // 本地启动服务
    else if (cooked[1] === 'pro') env = 'production' // 发布线上
  2. 拆分路径,根据模板中定义的config.json里的routes字段生成entry

    const fs = require('fs')
    const render = require('json-templater/string')
    const entryTemplate = require('./build.entry')
    const settingsTemplate = require('./build.settings')
    
    // 获取目标项目的config配置
    const configJson = require(path.resolve(__dirname, `../src/${target}/config.json`))
    
    // config.json 之前含有路由信息,可以便利出所有路由组件
    let result = fs.readdirSync(path.resolve(process.cwd(), `./src/${target}`))
    result.forEach((item) => {
      let vname = ''
      if (/(\.vue)$/.test(item)) {
         vname = item.split('.vue')[0]
         vrcomponents[vname] = path.join(__dirname, `../build-entry/${vname}.js`)
      }
    })
    // 配置面板组件不是路由组件,但也需要生成entry
    vrcomponents['settings'] = path.join(__dirname, `../build-entry/settings.js`)
    
    // 遍历vrcomponents,生成入口编译文件
    Object.keys(vrcomponents).forEach((name) => {
      let template = null
    
      // 生成入口编译文件
      if (name === 'settings') {
       template = render(settingsTemplate, {
           target
       })
      } else {
       template = render(entryTemplate, {
           target,
           name
       })
      }
      // 将通过json-template生成的entry放在build-entry目录下
      const output_path = path.join(__dirname, `../build-entry/${name}.js`)
      fs.writeFileSync(output_path, template)
    })
    
    // 生成 entry.json
    const entriesPath = path.join(__dirname, `../build-entry/entry.json`)
    fs.writeFileSync(entriesPath, JSON.stringify(vrcomponents, null, 2), 'utf8')
  3. html 模板生成
    在前面的html的模板有几个关键的模板字符串需要替换

    • routes
    // 我们要做的生成这个{{ }} 的routes
    var routes = {{ routes }};
    var router = new VueRouter({ mode: 'hash', routes: routes });
    • project
    // 我们将datasource作为全局变量,使用Vue.observable对datasource进行状态管理,从而实现页面所有组件共享该datasource
    var datasource = new Vue.observable({{project}})

    接下来就是怎么生成上面的routes和project

    const endOfLine = require('os').EOL
    
    // 生成 routes
    const routesChildren = (arr) => {
      const res = []
      arr.forEach((item) => {
       let obj = ''
       Object.keys(item).forEach(name => {
           // window[`${page}`] 是将路由挂载在全局
           // 这里没有考虑到children的情况,可以根据业务自行调整
           if (name === 'component') obj += `component: window['${item[name]}'],${endOfLine}`
           else obj += `${name}: ${JSON.stringify(item[name])},${endOfLine}`
       })
       res.push(render(`{${obj}}`))
     })
      return res
    }
    // 生成 datasource
    const datasource = require(path.resolve(__dirname, `../src/${target}/datasource.json`))
    
    // 生成 html
    const html = render(htmlTemplate, {
      title: configJson.title, // 页面标题
      project: JSON.stringify(datasource), // 被observable的数据源
      routes: `[${routesChildren(configJson.routes).join(',' + endOfLine)}]`
    })
    const htmlPath = path.join(__dirname, `../build-entry/index.html`)
    fs.writeFileSync(htmlPath, html)
  4. 执行编译命令

    const childProcess = require('child_process')
    if (env === 'development') {
      childProcess.execSync(`npm run server --target=${target}`, { stdio: 'inherit' })
    } else {
      childProcess.execSync(`npm run build:webpack --target=${target} --env=${env}`, { stdio: 'inherit' })
    }
在package.json新增scripts
"scripts": {
    "clean": "rimraf dist",
    "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.js ",
    "build": "npm run clean && node build/build.js",
    "dev": "node build/build.js",
    "server": "cross-env NODE_ENV=development webpack-dev-server --config webpack.config.js --mode development"
  },
新建 webpack.config.js
const webpackConfig = {
  // ...
  entry: require(path.join(__dirname, './build-entry/entry.json')),
  output: {
      path: path.resolve(process.cwd(), `./dist/${target}`),
      filename: `[name].js`,
      libraryExport: 'default', // 对外暴露default属性
      library: `[name]`,
      // export to AMD, CommonJS, or window 这一个设置十分重要
      libraryTarget: 'umd'
  },
  // ...
  devServer: {}, // 在这里devServer的参数即可本地预览
  plugins: {
      new HtmlWebpackPlugin({
          title: `${name}`,
          template: './build-entry/index.html', // template指向的是上门生成的html
          inject: 'head',
          hash: true,
          filename: path.resolve(__dirname, `./dist/${target}/index.html`)
      }),
      // uploadPlugins 构建完可以上传到cdn或者服务端存储,可以自行开发插件
  }
}

至此,本地模板开发的工作就结束,最后本地预览的效果如下图所示,可以通过右侧的settings配置面板改变datasource,去实时调整左侧home页面的效果。

image.png

执行npm run build命令后打包出来的项目结构如下
image.png
我们需要远程使用这些组件,可以在webpack写一个自定义上传插件,将打包好的内容上传到服务器,提供给编辑器和服务端使用。可以参考webpack 自定义插件开发

模板都开发完了,接下来要做的就是如何在编辑器里使用这些js。


JontyLu
29 声望6 粉丝

引用和评论

0 条评论