26 页面编译和运行:如何设计Vue.js搭建页面的渲染策略?
你好,我是杨文坚。
上节课,我们学习了如何实现“页面搭建”功能,实现流程可以分成两个关键点,“布局设计”和“填充物料”。有了“页面搭建”功能,我们可以通过可视化操作界面,生成完整的页面数据,这个数据,我们约定称为“页面布局数据”。
根据页面功能维度的五大功能模块,“页面搭建”“页面编译和运行”“页面发布流程”“页面版本管理”和“页面渲染方式”,有了页面布局数据,接下来,我们要做的就是基于页面布局数据,进行“页面编译和运行”。
到这里,你可能有疑问,为什么不能像页面搭建功能那样,直接通过AMD模块或者ESM模块方式,进行组装渲染运行呢?为什么还要进行页面编译?
为什么要进行页面编译
回忆一下我们之前做过的编译操作。在“物料组件产物管理”中,我们把物料的Vue.js组件,编译成了多种JavaScript模块格式;上节课“页面搭建”,我们在搭建页面的时候,基于组件的一种或多种模块格式,进行搭建页面的可视化操作渲染。
在页面搭建过程中,每个物料都是独立加载对应物料组件的JavaScript文件,同时,也加载物料组件的CSS文件进行渲染。所以,每个物料组件渲染的时候,就需要两个HTTP请求,来请求物料JavaScript和CSS的静态资源。
设想一下,如果页面依赖了20个不同的物料,按照页面搭建的方式进行渲染,就需要等40(20x2=40)个HTTP请求,加载完组件资源,最后才能渲染出完整的页面。所以,按照物料组件独立加载文件的形式,来组装渲染页面,等到HTTP请求和响应,非常浪费时间,降低了用户体验。
但是,页面搭建,是面向企业内部员工操作的,加载时间久勉强可以接受,而且页面搭建功能,需要让物料能独立渲染和独立操作,所以,物料也就必须独立请求对应的组件资源。
而前台场景,面向的是外部客户,要尽可能提升页面的用户体验,减少加载时间。所以,我们就需要合并页面依赖的物料组件资源,也就是多个组件的JavaScript文件和CSS文件,变成一个JavaScript文件和一个CSS文件。
这就要根据页面布局数据,整合需要用到的多个物料的JavaScript和CSS文件,各自编译成一个Bundle文件。
一句话总结页面编译的作用就是:“页面编译,目的是为了减少HTTP请求,提升用户体验。”从技术角度上看,页面编译产出的Bundle文件,也提供新的一种页面组装物料的渲染方式。
好,我们明确了需要页面编译,但是,棘手的问题来了,怎么进行页面动态编译呢?
前端程序员通常在前端编译页面的时候,选择在开发阶段,写死固定编译脚本,来配置构建工具(例如Webpack、Vite之类)进行编译代码。但是在我们运营搭建平台里,页面数据和依赖都是动态内容,不可能写死固定配置脚本,要怎么实现搭建页面的动态编译呢?
如何实现搭建页面的动态编译
我们先看看常规情况,编译前端页面代码需要哪些要素。
基于Webpack、Rollup和Vite的配置规范,你可以总结出三个基本要素。
- 准备编译入口文件
- 配置插件来编译多种语言和语法
- 分离JavaScript和CSS的代码
我们逐一分析每个基础要素,看看在技术视角上,怎么选择方案来解决。
- 准备编译入口文件
我们都知道,无论是Webpack、Rollup还是Vite,要编译JavaScript代码,就必须提供入口文件。
但是,上节课我们在渲染页面搭建功能的时候,每个物料组件,会独立加载文件和渲染。这些文件都是物料级别的组件文件,没有页面级别的入口文件。那我们要怎么提供页面级别的入口文件呢?
这就需要基于页面布局数据来拼接代码,生成页面入口文件。拼接代码,估计很多人首先想到的,就是用字符串的方式来拼接代码。
const code0 = `import Vue from 'vue';`;
const code1 = `const a = 1`;
const code2 = `const b = 2`;
const code3 = `function add(num1, num2) {
return num1 + num2;
}`
const code4 = `const c = add(a, b)`
const code = `
${code0}
${code1}
${code2}
${code3}
${code4}
`
// 最后用Node.js的fs API生成文件
字符串拼接方式,固然简单明了,但也存在安全隐患。毕竟是通过字符串拼接出来的实际代码,我们无法保证拼接后的代码语法正确。很可能出现每个代码块字符串都没问题,但是拼接后,带来一些换行或者标点符号的冲突,导致语法出错。
那有没有更安全的办法,实现代码的拼接呢?答案是有的,就是基于ESTree来生成JavaScript代码。
ESTree,你可以直接理解成JavaScript的抽象语法树,也就是AST。
AST全称Abstract Syntax Tree,是源码语法结构的一种抽象表示,以“树状结构”来描述一种开发语言的源码语法。通常用于代码编译、编辑器的语法高亮、语法错误提示和代码自动补全等场景。
ESTree是JavaScript社区讨论出来的一种抽象语法树(AST),简单来讲,就是用JSON来描述JavaScript语法。说到这里,你是不是觉得有点熟悉,之前我们课程提到的JSON Schema,就是用JSON描述JSON,这里的ESTree,就是用JSON描述JavaScript。
例如上面代码案例中,个别代码片段,可以这么用ESTree描述。
// 代码片段 const a = 1
// 变成ESTree如下所示
const estree1 = {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: 'a'
},
init: {
type: 'NumericLiteral',
value: 1
}
}
],
kind: 'const'
};
// 代码片段
/*
function add(num1, num2) {
return num1 + num2;
}
*/
// 变成ESTree如下所示
const estree3 = {
type: 'FunctionDeclaration',
id: {
type: 'Identifier',
name: 'add'
},
generator: false,
async: false,
params: [
{
type: 'Identifier',
name: 'num1'
},
{
type: 'Identifier',
name: 'num2'
}
],
body: {
type: 'BlockStatement',
body: [
{
type: 'ReturnStatement',
argument: {
type: 'BinaryExpression',
left: {
type: 'Identifier',
name: 'num1'
},
operator: '+',
right: {
type: 'Identifier',
name: 'num2'
}
}
}
],
directives: []
}
};
更多ESTree的抽象语法树规范,你可以查看 https://github.com/estree/estree GitHub 里ESTree的社区文档。
有了ESTree来描述代码片段,我们可以通过拼接JSON的方式,实现完整的代码抽象语法树。
有了完整的抽象语法树,接下来要考虑怎么把它变成实际的JavaScript代码。
你可以直接使用Babel的工具 ,就是@babel/generator这个npm模块,进行语法树的转换,具体操作就像这段代码。
import generator from '@babel/generator';
const estree1 = {
type: 'VariableDeclaration',
declarations: [
{
type: 'VariableDeclarator',
id: {
type: 'Identifier',
name: 'a'
},
init: {
type: 'NumericLiteral',
value: 1
}
}
],
kind: 'const'
};
const estreeProgram: any = {
type: 'File',
errors: [],
program: {
type: 'Program',
sourceType: 'module',
interpreter: null,
body: [],
directives: [estree1]
},
comments: []
};
const result = generator(estreeProgram);
console.log(result.code); // 输出代码 const a = 1;
使用Babel工具,我们可以用ESTree,拼接JavaScript代码的抽象语法树,最终生成完整的代码了。但是在这个过程中,一行代码,需要用好几行甚至好几十行 ESTree的JSON进行描述,是不是觉得很繁琐?那有没有更加便捷的方式呢?
答案是肯定的。ESTree就是为了避免字符串拼接代码可能出现的语法问题,那么我们可以把比较复杂的JavaScript代码片段,通过工具,转成ESTree,这就提供了可以用于拼接的ESTree。
要把JavaScript代码转成ESTree,我们可以用Babel提供的另一个npm模块,@babel/parser,来处理。这个模块可以把JavaScript代码,转成Babel风格的ESTree。
现在入口文件的拼接实现方式就很清晰,用ESTree来处理代码拼接,最后通过Babel的npm模块,实现ESTree和JavaScript代码的互相转化。
2. 配置插件来编译多种语言和语法
完成了页面入口文件的动态生成,接下来我们看页面编译的第二点,配置插件来编译多种语言和语法。
不同构建工具,插件配置是有差异的,之前我们学习了Webpack、Rollup和Vite这三个构建工具,其中,插件配置最方便的就是Vite,自带了JavaScript的ES6语法的编译和CSS文件抽离功能,无需其它插件配置。所以,动态编译构建工具,我们就选择Vite。
3. 分离JavaScript和CSS代码
因为,Vite的默认配置,支持把代码中的JavaScript和CSS代码进行编译分离,最后拆分成两个Bundle文件。我们就直接使用。
最后,就是执行编译操作了,可以直接使用Vite的Node.js API,进行动态编译入口文件,大致代码就像这样。
import path from 'node:path';
import { build } from 'vite';
import type { InlineConfig } from 'vite';
// 动态编译入口文件的方法
async function buildEntryFile(fullEntryFilePath: string) {
const config: InlineConfig = {
build: {
emptyOutDir: false,
outDir: path.dirname(fullEntryFilePath),
lib: {
name: 'MyBundle',
entry: fullEntryFilePath,
formats: ['iife'],
fileName: () => {
return 'bundle.js';
}
},
rollupOptions: {
preserveEntrySignatures: 'strict',
external: ['vue'],
output: {
globals: {
vue: 'Vue'
},
assetFileNames: 'bundle[extname]'
}
}
}
};
await build(config);
}
这个页面布局数据,我演示一下怎么使用。
{
"layout": {
"rows": [
{
"uuid": "4890074a-09f7-4b95-bd34-fecbe6e066db",
"columns": [
{
"name": "首屏广告",
"uuid": "fc90dcbf-d70b-40f4-aa14-b64be2632092",
"width": 1000
}
]
},
{
"uuid": "c248318f-ffd1-42d7-9f27-56533f7c4453",
"columns": [
{
"name": "其它广告位1",
"uuid": "ac873013-d448-4ca8-b4bb-729b844ee262",
"width": 600
},
{
"uuid": "079c3fe5-f8af-475a-82ed-feb01b5730ee",
"width": 400
}
]
},
{
"uuid": "8d0bb922-5d8c-4a67-80b0-4babaf3e2f97",
"columns": [
{
"name": "促销商品",
"uuid": "e9b94120-ebe8-4418-9b13-7ea10095676d",
"width": 1000
}
]
}
],
"width": 1000
},
"moduleMap": {
"ac873013-d448-4ca8-b4bb-729b844ee262": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
},
"e9b94120-ebe8-4418-9b13-7ea10095676d": {
"materialData": {},
"materialName": "@my/material-product-list",
"materialVersion": "0.9.0"
},
"fc90dcbf-d70b-40f4-aa14-b64be2632092": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
}
}
}
最后的动态生成入口文件。
import Vue from "vue";
import MyMaterialBannerSlides from "../../../material/@my/material-banner-slides/0.9.0/index.esm.js";
import MyMaterialProductList from "../../../material/@my/material-product-list/0.9.0/index.esm.js";
const {
h,
createApp,
defineComponent
} = Vue;
const materialDeps = {
"@my/material-banner-slides": MyMaterialBannerSlides,
"@my/material-product-list": MyMaterialProductList
};
const pageLayoutData = {
"layout": {
"rows": [{
"uuid": "4890074a-09f7-4b95-bd34-fecbe6e066db",
"columns": [{
"name": "首屏广告",
"uuid": "fc90dcbf-d70b-40f4-aa14-b64be2632092",
"width": 1000
}]
}, {
"uuid": "c248318f-ffd1-42d7-9f27-56533f7c4453",
"columns": [{
"name": "其它广告位1",
"uuid": "ac873013-d448-4ca8-b4bb-729b844ee262",
"width": 600
}, {
"uuid": "079c3fe5-f8af-475a-82ed-feb01b5730ee",
"width": 400
}]
}, {
"uuid": "8d0bb922-5d8c-4a67-80b0-4babaf3e2f97",
"columns": [{
"name": "促销商品",
"uuid": "e9b94120-ebe8-4418-9b13-7ea10095676d",
"width": 1000
}]
}],
"width": 1000
},
"moduleMap": {
"ac873013-d448-4ca8-b4bb-729b844ee262": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
},
"e9b94120-ebe8-4418-9b13-7ea10095676d": {
"materialData": {},
"materialName": "@my/material-product-list",
"materialVersion": "0.9.0"
},
"fc90dcbf-d70b-40f4-aa14-b64be2632092": {
"materialData": {},
"materialName": "@my/material-banner-slides",
"materialVersion": "0.9.0"
}
}
};
const moduleComponentMap = {};
Object.keys(pageLayoutData.moduleMap).forEach(uuid => {
const materialName = pageLayoutData.moduleMap[uuid].materialName;
moduleComponentMap[uuid] = materialDeps[materialName];
});
const App = defineComponent({
setup() {
const Rows = pageLayoutData.layout.rows.map((row, rowIndex) => {
const Columns = row.columns.map((col, colIndex) => {
const Material = moduleComponentMap[col.uuid];
const props = pageLayoutData.moduleMap[col.uuid]?.materialData || {};
const Mod = h(Material || 'div', props);
return h('div', {
style: {
width: col.width,
display: 'flex'
},
'data-col': colIndex
}, Mod);
});
return h('div', {
style: {
width: row.width,
margin: '10px 0',
display: 'flex',
flexDirection: 'row'
},
'data-row': rowIndex
}, Columns);
});
return () => {
return h('div', {
style: {
width: pageLayoutData.layout.width,
margin: '0 auto'
}
}, Rows);
};
}
});
const app = createApp(App);
app.mount('#app');
页面编译的技术实现流程,就是这样的三步,我们简单总结一下。
- 首先基于页面布局数据,用ESTree拼接和生产入口文件。
- 然后,用构建工具,例如Vite,基于入口文件,把所有的物料文件进行打包编译。
- 最后,整个页面的静态资源通过编译,生成一个JavaScript的Bundle文件,和一个CSS的Bundle文件。
在我们课程的代码案例里,你可以通过创建页面,提交发布页面,来动态编译页面的Bundle文件。最终可以在页面列表中,点击去访问Bundle文件渲染的预览页面。
实现了页面编译,我们继续学习今天的第二个知识点——页面运行。这里的页面运行,不是简单的页面加载和渲染,而是要有一定的渲染策略。那为什么要设计渲染策略呢?
如何设计渲染策略
运营搭建平台,最终生产的页面是提供给外部客户使用的,页面的稳定性和安全性就很重要。
在上一步,页面编译内容是把所有物料组件的JavaScript文件,编译成一个JavaScript的Bundle文件。如果基于合并后的Bundle文件,运行渲染页面的时候,某个物料组件的JavaScript代码出错,容易导致整个页面崩溃白屏。这时候,就会造成页面的不可用,进而变成生产故障。
但是,我们的页面编译,又是用来解决多HTTP文件请求问题的,目的就是提供较好的用户体验。
所以页面渲染策略,就是要在“用户体验”和“页面稳定性”做一定的权衡处理。怎么设计渲染策略呢?
既然是要做权衡,那就有优先级的选择,我们可以根据渲染方式的优先级,设计渲染策略。
- 第一步,优先使用编译后的Bundle文件渲染页面,提升用户的体验。
- 第二步,监听页面报错,如果JavaScript的Bundle文件报错,导致页面白屏,就进入多模块独立文件渲染模式。
- 第三步,判断浏览器兼容性,选择合适的多模块的物料独立渲染方式。
如果浏览器支持ESM,就基于ESM模块格式,每个模块单独加载文件独立渲染,尽量隔离掉错误的干扰。如果浏览器不支持ESM,就用AMD模块格式,加载RequireJS的AMD运行环境,再让每个模块单独加载文件独立渲染,尽量隔离掉异常物料组件的报错干扰。
这里的物料独立渲染,就是把每个物料当做一个Vue.js应用来渲染,基于createApp这个API来独立渲染每个物料。替换掉Bundle文件聚合所有物料组件,渲染一个应用的模式。
具体渲染策略实现流程,就像这样。
渲染策略的要点,最核心的就是独立物料模块渲染,其实就是把物料组件当做独立的应用来渲染。Bundle文件渲染方式是只渲染一个Vue.js应用,当应用里出现报错,导致整体页面奔溃,就多变成模块独立加载渲染。这个时候,每个物料是独立的Vue.js应用进行独立渲染。隔离出错误模块或错误代码。
所以页面渲染策略的思路可以用一句话总结:“尽量保证页面功能全部渲染,如果出现异常,就降级成部分模块渲染,保证大部分功能可用性”。
总结
今天我们学习了“页面编译”和“页面运行”。其中,页面编译,就是基于页面布局数据,动态编译出页面完整的JavaScript和CSS的Bundle文件,目的是为了减少HTTP文件请求,提升用户体验。
动态编译过程中,需要注意三方面。
- 用ESTree处理代码拼接。在动态生成编译的入口文件时,要用ESTree动态生成JS代码,主要是尽量减少拼接代码带来的语法错误。
- 不同处理的ESTree语法有差异。处理ESTree的不同工具,都有一定的抽象语法树差异,这里建议用Babel的工具链,用 @babel/parser 把JavaScript代码转成ESTree,用 @babel/generator 把ESTree转成JavaScript代码。
- ESTree也不是绝对安全。基于ESTree动态拼接代码,只能尽量避免JavaScript语法问题,但不是绝对能解决语法问题,在处理过程中还是要注意拼接代码的语法检查。
页面运行,核心就是要设计页面的渲染策略,保证页面功能的可用性和稳定性。
- 渲染策略是优先用Bundle文件渲染,保证用户体验。
- 检查页面报错情况,如果是Bundle文件报错导致页面崩溃,就进入兜底渲染环节。
- 兜底渲染主要是把页面物料文件独立加载,独立渲染,隔离错误干扰。
页面渲染策略其实就是一种兜底措施,平衡“用户体验”和“功能稳定性”。如果是由于提升用户体验带来了渲染问题,那就必须舍弃优化方式,进行降级处理。换句话说就是,牺牲用户体验,来保证功能的稳定性。
思考题
想一想,页面渲染策略中,Bundle文件渲染不能兼顾“用户体验”和“技术稳定性”吗?渲染策略必须做降级处理,牺牲用户体验,变成物料独立加载渲染吗?
期待看到你的思考。希望通过今天的学习,你能掌握动态拼接页面代码的技术知识,同时,也能理解如何做好页面渲染策略的设计。下节课见。
完整的代码在这里
- 庄周梦蝶 👍(1) 💬(1)
有没有demo源码,能跑出效果的源码
2023-03-07 - 庄周梦蝶 👍(1) 💬(2)
有点不太懂
2023-03-07 - 戡玉 👍(1) 💬(0)
这个低码雏形很棒,老师的代码真是不错,质量过硬,能学到东西!
2023-09-14 - ifelse 👍(0) 💬(0)
学习打卡
2024-09-28 - Geek_12e8fd 👍(0) 💬(0)
页面渲染策略中,Bundle 文件渲染确实在某些情况下可能无法同时兼顾“用户体验”和“技术稳定性”。然而,这并不意味着渲染策略必须总是做降级处理,牺牲用户体验来变成物料独立加载渲染。以下是对这个问题的详细分析: Bundle 文件渲染的优势: 减少HTTP请求数量:通过合并多个物料组件的JavaScript和CSS文件,Bundle 文件可以显著减少HTTP请求的数量,加快页面加载速度,从而提升用户体验。 优化资源加载:浏览器可以并行加载多个资源,而Bundle 文件通常经过压缩和优化,使得资源加载更加高效。 Bundle 文件渲染的挑战: 技术稳定性风险:如果Bundle 文件中的某个物料组件出现错误,可能会导致整个页面崩溃,影响用户体验。这是因为所有物料组件都打包在同一个Bundle文件中,它们之间的依赖关系紧密耦合。 更新和维护困难:当需要更新或修复某个物料组件时,可能需要重新编译整个Bundle文件,增加了开发和维护的复杂性。 渲染策略的优化: 错误隔离和容错处理:在Bundle 文件渲染过程中,可以实施错误隔离和容错处理机制。例如,可以使用try-catch语句捕获物料组件的错误,并优雅地降级处理或展示错误信息,而不是让整个页面崩溃。这样可以确保大部分功能仍然可用,同时提供反馈给用户或开发者。 动态拆分Bundle文件:根据页面布局数据和物料组件的依赖关系,可以将Bundle文件动态拆分成多个较小的Bundle文件。每个小Bundle文件包含一组相关的物料组件。这样,即使某个小Bundle文件出现错误,也不会影响其他小Bundle文件的正常加载和渲染。这种策略可以在一定程度上平衡用户体验和技术稳定性。 渐进式加载:使用渐进式加载技术,先加载并渲染页面的核心部分,然后逐步加载其他非核心部分。这样可以确保用户能够尽快看到页面的核心内容,同时后台继续加载其他物料组件,提供更加丰富的功能和体验。 监控和日志记录:实施全面的监控和日志记录机制,对Bundle文件的加载和渲染过程进行实时监控和记录。当出现问题时,可以迅速定位并解决问题,减少对用户体验的影响。 综上所述,虽然Bundle 文件渲染在某些情况下可能无法同时兼顾“用户体验”和“技术稳定性”,但通过优化渲染策略和实施相应的技术手段,可以在一定程度上平衡这两方面的需求。渲染策略不必总是做降级处理,而是可以根据具体情况选择最合适的渲染方式,以确保用户体验和技术稳定性的最大化。
2024-06-14 - 冰糖爱白开水 👍(0) 💬(0)
有没有这样实现的?组件打包到一块,在一个应用里渲染。
2024-03-22 - Akili 👍(0) 💬(0)
老师,请教一个问题,如果不是这种为运营搭建的物料系统,而是独立的vue项目,改如何做版本管理啊。
2023-07-10