通过生成的 jsonData,可以拿到页面的基本信息 pageInfo,如:背景色、title等,还有页面布局 layout。主要处理两个问题:
- 通过 jsonData 数据实现页面渲染;
- 打包生成 html。
# 打包目录结构
└───build 目录:主要存放 webpack 相关配置
| └───static.js // 封装 webpack.run() 方法
| └───webpack.base.config.js // webpack 基本配置
|───src
| └───App.vue // server 端入口
| └───main.js // client 端入口
| └──index.tpl.html // 页面模板
| └───components.js // 组件引进注册
|───create-html
| └───待页面名称 目录
| └───config.js // webpack 发布相关配置
| └───data.js // jsonData 获取相关逻辑
|───html 目录:打包输出
| └───static 目录:静态资源目录
| └───...
| └──待页面名称.html // 输出的页面
|───index.js // 打包入口文件
# 打包相关
# webpack 公共基本配置 - webpack.base.config.js
用到的 webpack 插件有:vue-loader、optimize-css-assets-webpack-plugin、html-webpack-plugin、html-webpack-inline-source-plugin、ptimize-css-assets-webpack-plugin、babel-loader等。
常规的 webpack 配置跳过,说下 plugins 配置:
plugins(type) {
const common = [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({
filename: '[name].[contenthash:6].css',
chunkFilename: '[id].[contenthash:6].css',
}),
new OptimizeCssAssetsWebpackPlugin(),
]
const onDemand = {
server: [],
client: [
new HtmlWebpackPlugin({
filename: 'index.template.html',
template: path.resolve(__dirname, '../src/index.tpl.html'),
inlineSource: '.css$',
minify: {
collapseWhitespace: true,
},
}),
new OptimizeCSSAssetsPlugin({}),
new HtmlWebpackInlineSourcePlugin(),
],
}
return [].concat(common).concat(onDemand[type] || [])
},
这通过传入 type 类型判断,判断是 client 端还是 server 端。公共需要的是 vue-loader,但 client 需要的是 html-webpack-plugin。
# client webpack 配置
除了 webpack.base.config.js 公共配置,client 的 webpack 需要配置的有:entry、output、端判断变量等。
const clientWebpackConfig = {
...webpackBaseConfig,
mode,
entry: path.resolve(__dirname, './src/main.js'),
output: {
path: path.resolve(__dirname, '.', publishConfig.outputPath),
filename: '[name].[contenthash:8].js',
publicPath: publishConfig.publicPath,
},
plugins: [
...webpackBaseConfig.plugins('client'),
new webpack.DefinePlugin({
'process.env': JSON.stringify({
IS_SERVER: false,
}),
}),
],
}
# server webpack 配置
除了 webpack.base.config.js 公共配置,server 的 webpack 需要配置的有:entry、output、端判断变量等。
const serverWebpackConfig = {
...webpackBaseConfig,
mode,
entry: {
app: path.resolve(__dirname, './src/App.vue'),
},
output: {
path: path.resolve(__dirname, '.', publishConfig.outputPath),
libraryTarget: 'commonjs',
publicPath: publishConfig.publicPath,
},
plugins: [
...webpackBaseConfig.plugins('server'),
new webpack.DefinePlugin({
'process.env': JSON.stringify({
IS_SERVER: true,
}),
}),
],
}
# static.js - webpack.run 封装
这里封装要给 Static 类,输入 webpack 配置,输出打包好的 bundle。
const webpack = require('webpack')
class Static {
constructor(options) {
this.options = options
}
run() {
const webpacks = []
Object.keys(this.options).forEach((k) => {
webpacks.push(webpack(this.options[k]))
})
return Promise.all(
webpacks.map(
(web) =>
new Promise((reslove) => {
web.run((err, stats) => {
reslove()
})
})
)
)
}
}
module.exports = {
Static,
}
# server 入口 - 渲染生成页面
通过封装一个入口组件 App.vue,将 jsonData 通过 props 传入,然后通过 js 引进相关的渲染组件,最后通过 v-for
配合动态组件<component :is="" />
可以实现 组件渲染。
<template>
<div class="h5-page" :style="pageStyle">
<div class="layout-item" v-for="(item, index) in layout" :key="index">
<component :is="item.component" :dynamicStyle="item.config"></component>
</div>
</div>
</template>
<script>
import components from "./components.js";
export default {
name: "H5Page",
components: components,
props: {
pageConfig: {
type: Object,
default: () => [],
},
},
data() {
return {
layout: [],
};
},
computed: {
pageStyle() {
return {
backgroundColor:
this.$props.pageConfig.pageInfo.backgroundColor || "#fff",
};
},
},
created() {
this.bindComponent();
},
methods: {
/**
* 给每个布局绑定唯一组件
*/
bindComponent() {
if (!Object.keys(components).length) {
return;
}
// 给 layout 绑定对应组件
const layout = this.$props.pageConfig.layout || [];
if (!layout.length) {
return;
}
layout.forEach((item) => {
item.component = components[item.type];
});
this.layout = layout;
},
},
};
</script>
<style lang="scss">
.h5-page {
width: 100vw;
}
</style>
这里将组件注入单独抽离,后期有新的组件使用,改动 components.js 即可。
/************************ 组件注册 ************************/
if (!process.env.IS_SERVER) {
// 解决:UnhandledPromiseRejectionWarning: ReferenceError: window is not defined 问题
const H5Editor = require('h5-editor')
module.exports = {
EdText: H5Editor.default.EdText,
EdImage: H5Editor.default.EdImage
}
}
注意:上面有一个全局变量(通过 webpack.DefinePlugin 插件注入变量)判断是服务端还是客户端的判断,因为打包使用的是vue-server-renderer (opens new window)相关逻辑。故:存在两个端,client 和 server,server 端没有 window、document 等对象。
# client 入口 - 注入全局数据 window.__initData__
主要做两件事:
- 挂载 Vue 实例到目标节点;
- 注入全局 window.__ininData__
import Vue from 'vue'
import App from './App.vue'
let props = {}
if (window.__initData__) {
props = { pageConfig: window.__initData__.pageConfig }
}
new Vue({
components: { App: App },
render: (h) => h('App', { props }),
}).$mount('#app')
# 页面 html 模板
<!DOCTYPE html>
<html style="font-size:100px;">
<head>
<title>{{ title }}</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport"
content="width=device-width,initial-scale=1.0,initial-scale=1.0,maximum-scale=1.0,user-scalable=no,minimal-ui">
<meta name="description" content="{{description}}" />
</head>
<body style="font-size: 16px">
<div id="app">
<!--vue-ssr-outlet-->
</div>
<script>
{{{ script }}}
</script>
</body>
</html>
# index.js - 构建
打包使用的是vue-server-renderer (opens new window)相关逻辑。
主要做几件事:
- 通过 webpack 打包输出 templateHtml 和 bundle.js(还没有注入页面数据 window.__initData__)
const compiler = new Static({
app: appWebpackConfig,
client: clientWebpackConfig,
})
await compiler.run()
- 通过
vue-server-renderer
结合 templateHtml 和 bundle.js 生成 renderer 对象
const renderer = require('vue-server-renderer').createRenderer({
template: fs.readFileSync('./html/static/index.template.html', 'utf-8'),
})
const main = require(path.resolve(
__dirname,
`./${publishConfig.outputPath}/app.js`
))
通过
/create-html/待打包页面名/data.js
脚本获取 jsonData 数据将 jsonData 相关数据通过 context,注入到模板页面中
const pageConfigData = await dataHandler()
const { title = '', description = '' } = pageConfigData.pageInfo
const context = {
title,
description,
script: `window.__initData__=${JSON.stringify({
pageConfig: pageConfigData,
})}`,
}
const app = new Vue({
data: {},
components: { App: main.default },
render: (h) => h('App', { props: { pageConfig: pageConfigData } }),
})
- 通过 renderer 对象输出 html 并写入到目标目录下。
renderer.renderToString(app, context, (err, html) => {
if (!html) {
return
}
const filePath = path.resolve(__dirname, `${publishConfig.filePath}`)
fse.ensureDirSync(filePath)
// name 为 nodejs 命令行参数:待打包页面名称
fs.writeFile(`${filePath}/${name}.html`, html, function(err) {
if (err) {
console.error(`>>>> 生成 ${name} 页面失败!`, err)
return
}
console.log(
`${publishConfig.filePath}/${name}.html` + ':数据写入成功!'
)
})
})
over~