基于vue的页面配置化的实现(上)——模板开发
在上一篇文章浅谈前端可视化编辑器的实现中,简单介绍了一下可视化编辑器的实现方式。用户通过拖拉拽组件的方式布局页面,然后通过vue的render函数将页面生产出来。这种方式对于高度自定义页面的业务场景是比较适合的,比如说发布一篇文章资讯,只需要配一个富文本,再加一些组件或者动画丰富一下,运营同学就可以直接发布一篇文章。
但对于一些业务紧密的页面,如果还要运营同学一个个拖拉组建拼凑页面,会十分浪费时间,并且因为特定业务场景去开发对应的业务组件过大,会造成编辑器组件库的臃肿。
目的
希望能够针对可复用的单一业务,抽离成页面模板,配置相关参数就可以生产不同页面,运营不需要关心内部逻辑,只需要根据不同场景去生产产品。如下图所示:
文章图片
这种基础页面还是比较常见的,有一些核心业务功能。如果每次产出这种页面需要提需求给开发,是比较浪费时间的。我们开发把这业务抽成一个模板,运营同学只需要关注几点:
- 出图并配置图片
- 配置活动(抽奖)素材和活动id
- 配置游戏链接
原理 vue 的核心思想之一——组件化
设计思路
文章图片
- 本地模板开发
- 业务代码开发
- 本地预览
- 模板上传
- 编辑器加载模板
- 编辑器远程加载模板
- 参数配置
- 实时预览并发布
- 服务端生产
- 生成代码
- 生产部署
目录结构
|-- build // 构建脚本
|-- build-entry // 用于存放根据模板生成的文件
|-- dist // 项目打包出来的js
|-- src // 业务代码
|-- webpack.config.js // 构建命令
一、模板开发
- 在src下新建 pageA/home.vue
home.vue:这里指代页面,用于挂载在router上。
datasource:全局注入的配置项,提供给运营侧修改的参数都放在该对象维护
文章图片
{{ datasource.title }}
- 新建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。
至此,我们的业务代码和模板的基本配置信息已经写好了,接下来就是要像怎么让他在本地跑起来,并且能打包成一个个组件。
先定义好启动命令
// dev 本地预览
npm run dev --target=pageA// pro 构建并推送至远程
npm run build --target=pageA
新建build/build.html.js 构建本地预览的html模板, {{ }}里的内容是用来替换字符串,在这个项目中字符串模板的插件用的是json-templater。
module.exports = `
{{title}} - 锐客网 .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;
}
`
新建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' }) }
"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 自定义插件开发。
【基于vue的页面配置化的实现(上)——模板开发】模板都开发完了,接下来要做的就是如何在编辑器里使用这些js。
推荐阅读
- 热闹中的孤独
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 放屁有这三个特征的,请注意啦!这说明你的身体毒素太多
- 一个人的旅行,三亚
- 布丽吉特,人生绝对的赢家
- 慢慢的美丽
- 尽力
- 一个小故事,我的思考。
- 家乡的那条小河
- 《真与假的困惑》???|《真与假的困惑》??? ——致良知是一种伟大的力量