在上一篇文章浅谈前端可视化编辑器的实现中,简略介绍了一下可视化编辑器的实现形式。用户通过利落拽组件的形式布局页面,而后通过 vue 的 render 函数将页面生产进去。这种形式对于高度自定义页面的业务场景是比拟适宜的,比如说公布一篇文章资讯,只须要配一个富文本,再加一些组件或者动画丰盛一下,经营同学就能够间接公布一篇文章。
但对于一些业务严密的页面,如果还要经营同学一个个利落组建拼凑页面,会非常浪费时间,并且因为特定业务场景去开发对应的业务组件过大,会造成编辑器组件库的臃肿。
目标
心愿可能针对可复用的繁多业务,抽离成页面模板,配置相干参数就能够生产不同页面,经营不须要关怀外部逻辑,只须要依据不同场景去生产产品。如下图所示:
这种根底页面还是比拟常见的,有一些外围业务性能。如果每次产出这种页面须要提需要给开发,是比拟浪费时间的。咱们开发把这业务抽成一个模板,经营同学只须要关注几点:
- 出图并配置图片
- 配置流动(抽奖)素材和流动 id
- 配置游戏链接
比起【下需要 - 需要评审 - 设计 - 开发 - 测试 - 上线】这种流程,页面模板在第二次流动上线的时候省去了下需要、需要评审、开发和测试这几个过程。性能在第一次上线的时候就验证过了,所以后续不必开发和测试染指,极大的晋升了生产力。
原理
vue 的核心思想之一——组件化
设计思路
-
本地模板开发
- 业务代码开发
- 本地预览
- 模板上传
-
编辑器加载模板
- 编辑器近程加载模板
- 参数配置
- 实时预览并公布
-
服务端生产
- 生成代码
- 生产部署
页面模板
在这一环节咱们的目标是要把页面打包成一个组件,比方首页打成 home.js 这样的组件,而后挂载到 VueRouter 上。
目录构造
|-- build // 构建脚本
|-- build-entry // 用于寄存依据模板生成的文件
|-- dist // 我的项目打包进去的 js
|-- src // 业务代码
|-- webpack.config.js // 构建命令
一、模板开发
-
在 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>
-
新建 pageA/datasource.json
用于定于配置项的内容,凋谢给用户调整参数。{ "home": { // home 页面所需参数 "img": "可配置图片", "title": "可配置的题目", }, }
-
新建 pageA/config.json
定义模板信息和模板的路由信息,routes 次要为了定义模板的路由的信息,前面有场景须要读取该字段{ "category": "流动", "title": "游戏下载 H5", "author": "yl", "description": "一般 H5 页面点击下载游戏(包含假抽奖)", "routes": [ { "pageType": "h5", "path": "/", "name": "home", "component": "home", "meta": {"title": "首页",} } ] }
-
新建 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
该文件为次要执行文件,依据定义好的字符串模板去生成所需的文件
-
读取命令行
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' // 公布线上
-
拆分门路,依据模板中定义的 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')
-
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)
-
执行编译命令
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。