在上一篇文章浅谈前端可视化编辑器的实现中,简略介绍了一下可视化编辑器的实现形式。用户通过利落拽组件的形式布局页面,而后通过vue的render函数将页面生产进去。这种形式对于高度自定义页面的业务场景是比拟适宜的,比如说公布一篇文章资讯,只须要配一个富文本,再加一些组件或者动画丰盛一下,经营同学就能够间接公布一篇文章。

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

目标

心愿可能针对可复用的繁多业务,抽离成页面模板,配置相干参数就能够生产不同页面,经营不须要关怀外部逻辑,只须要依据不同场景去生产产品。如下图所示:

这种根底页面还是比拟常见的,有一些外围业务性能。如果每次产出这种页面须要提需要给开发,是比拟浪费时间的。咱们开发把这业务抽成一个模板,经营同学只须要关注几点:

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

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

原理

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

设计思路

  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.remainremain.forEach(r => {  arg = r.split('=')  config[arg[0]] = arg[1]})let idx = 2;const cooked = argv.cookedconst length = argv.cooked.lengthwhile ((idx += 2) <= length) {  config[cooked[idx - 2]] = cooked[idx - 1]}// 获取项目名称,示例演示我的项目名为 pageAconst 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`)  }})// 配置面板组件不是路由组件,但也须要生成entryvrcomponents['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.jsonconst entriesPath = path.join(__dirname, `../build-entry/entry.json`)fs.writeFileSync(entriesPath, JSON.stringify(vrcomponents, null, 2), 'utf8')
  3. html 模板生成
    在后面的html的模板有几个要害的模板字符串须要替换

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

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

    const endOfLine = require('os').EOL// 生成 routesconst 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}// 生成 datasourceconst datasource = require(path.resolve(__dirname, `../src/${target}/datasource.json`))// 生成 htmlconst 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页面的成果。

执行npm run build命令后打包进去的我的项目构造如下

咱们须要近程应用这些组件,能够在webpack写一个自定义上传插件,将打包好的内容上传到服务器,提供给编辑器和服务端应用。能够参考webpack 自定义插件开发。

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