# PAAS应用
PAAS 应用的主要特点是页面是根据配置动态生成的
# 运行过程
浏览器打开 PAAS 应用 (opens new window)时,先是发出 https://.....meta
请求获取到元数据,这个元数据包含了当前应用的一些配置信息,比如菜单栏、路由、每个页面展示哪些组件等,整个应用就根据这些内容来动态渲染的
要实现动态渲染页面(组件),依靠的就是 VUE 提供的 render
方法
# render
类型:
(createElement: () => VNode) => VNode
详细:
字符串模板的代替方案,允许你发挥
JavaScript
最大的编程能力。该渲染函数接收一个createElement
方法作为第一个参数用来创建VNode
。如果组件是一个函数组件,渲染函数还会接收一个额外的
context
参数,为没有实例的函数组件提供上下文信息。Vue 选项中的
render
函数若存在,则Vue构造函数不会从template
选项或通过el
选项指定的挂载元素中提取出的HTML
模板编译渲染函数。
简单来说 render
中的参数 createElement方法
就是创建VNode的方法
我们来简单例子演示一下 render
方法的使用
模板形式
import components1 from './components1.vue'
Vue.component('components-1', components1)
// components1.vue
<h1>components-1</h1>
render形式
使用 render()
方法实现上面的 components1.vue
Vue.component('components-2', {
render: function (createElement) {
return createElement(
'h1',
{},
['components-1']
)
}
})
为什么可以使用render
方法创建组件?
这里简单介绍一下Vue的渲染机制
回顾一下下面几个使用场景
- 初始时 Vue 项目
var app = new Vue({
el: '#app', // id为app的DOM元素
data: {}
})
- 使用字符串注册组件
Vue.component('components-1', {template: '<h1>components-2</h1>'})
- 使用Vue模板注册组件
Vue.component('components-1', components1)
Vue 会将上面例子的组件使用一系列的方法转换成 render
函数,这个 render
方法的作用是生成 VNode
这也是为什么官网介绍 render
时有这么一句话 render 比模板更接近编译器 的原因
也就是说当实例组件时,如果有 render
方法那么就直接使用这个 render
方法,接下再看 render
的参数 createElement
的使用
# createElement
createElement
接收参数如下:
tag
: {String | Object | Function}表示一个 HTML 标签名、组件选项对象data
: 可选参数,表示VNode
的数据,完整的数据对象如下{ // 和`v-bind:class`一样的 API 'class': { foo: true, bar: false }, // 和`v-bind:style`一样的 API style: { color: 'red', fontSize: '14px' }, // 正常的 HTML 特性 attrs: { id: 'foo' }, // 组件 props props: { myProp: 'bar' }, // DOM 属性 domProps: { innerHTML: 'baz' }, // 事件监听器基于 "on" // 所以不再支持如 v-on:keyup.enter 修饰器 // 需要手动匹配 keyCode。 on: { click: this.clickHandler }, // 仅对于组件,用于监听原生事件,而不是组件使用 vm.$emit 触发的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定义指令. 注意事项:不能对绑定的旧值设值 // Vue 会为您持续追踨 directives: [ { name: 'my-custom-directive', value: '2' expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // 如果子组件有定义 slot 的名称 slot: 'name-of-slot' // 其他特殊顶层属性 key: 'myKey', ref: 'myRef' }
children
:{String | Array} 表示子节点
这里着重了解一下 createComponent
第一个参数 tag
, tag
可以有的类型以及对应的内部处理大致如下
如果是
String
类型选判断如果是内置的一些节点,则直接创建一个普通
VNode
如果是为已注册的组件名,则通过
createComponent
创建一个组件类型的VNode
如果是
tag
一个 Component 类型,则直接调用createComponent
创建一个组件类型的VNode
节点其它值暂不讨论
例子:
<div id="app">
{{ message }}
</div>
相当于我们编写的如下 render
函数:
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
}
上文的 createElement
方法,表示创建一个 id=app
的 div
,这个 div
内部有一个节点 this.message
, this.message
在这里表示是一个文本节点
对 render
方法有了一些了解之后,接下来我们开始正入正题
# PaaS应用创建
看下 PAAS 引擎中的主组件的部分代码
<template>
<div id="app">
<keep-alive :exclude="exclude" :include="include">
<router-view class="router-view"></router-view>
</keep-alive>
</div>
</template>
export default {
name: 'App',
watch: {
appMeta: {
immediate: true,
handler(meta = {}, oldMeta = {}) {
const self = this;
// 注册元数据所有的视图路由
const routes = [];
if (meta.views) {
// 遍历配置的路由信息,生成一条Vue路由信息
meta.views.forEach(view => {
const { name, path, alias } = view;
// 获取路由额外配置属性:页面标题、是否子路由、是否需要登录、别名、当前角色
const routeMeta = getRouteMeta(self.isDynamicTitle, self.role, view);
const routeView = {
name,
path,
meta: routeMeta,
props() {
return {
view: self.cache[name]
}
},
component: {
name: `router-view-${name}`,
beforeRouteLeave,
props: ['view'],
render(h) {
console.log('this1', this)
return renderView(h, this.view, self.appMeta)
}
}
};
// 如果已经包含这个路由,则不添加到routes中
if (!self.cache[name]) {
routes.push(routeView)
}
self.cache[name] = view;
// 如果存在children,则遍历并获取子路由,内容跟上面差不多
(view.children || []).forEach(child => {
const childRouteView = {
name: child.name,
path: child.path,
meta: getRouteMeta(self.isDynamicTitle, self.role, child, routeMeta),
props() {
return {
view: self.cache[child.name]
}
},
component: {
name: `router-view-${child.name}`,
beforeRouteLeave,
props: ['view'],
render(h) {
return renderView(h, this.view, self.appMeta)
}
}
};
if (!self.cache[child.name]) {
routes.push(childRouteView)
}
self.cache[child.name] = child
})
})
}
// 最后动态生成路由
if (routes && routes.length > 0) {
routes.push({ path: '*', redirect: { name: 'home' } });
self.$router.addRoutes(routes)
}
}
}
},
}
appMeta
就是元数据,handler
函数主要功能就是遍历元数据中的页面信息,动态生成 Vue 路由
重点需要注意的是以下几点
- 元数据中一条路由信息的结构信息如下
{
name:39f228fd-66cb-0b40-bffd-e1d1829f4aab, // 路由名称
path:/39f228fd-66cb-0b40-bffd-e1d1829f4aab, // 路由路径
body:{
header:{}, // 当前页面的头部信息。如:页面标题,是否显示页面标题等
content: {
item: [ // 当前页面包含的哪些组件
{
type: 'b2c-search', // 组件名称
_key: 'b453edb8-439b-498f-ba8b-bc5c9165b01c', // 唯一标识
props: { // 组件配置信息,通过props传入当前组件中
}
routeAlias: '/alias', // 路由别名
}
] ,
},
isLogin: false, // 进入页面前是否必须先登录
alias: [], // 别名
children: []
}
}
- 一个路由信息要加载的组件
component
也是通过render
方法创建的
component: {
name: `router-view-${child.name}`,
beforeRouteLeave,
props: ['view'],
render(h) {
return renderView(h, this.view, self.appMeta)
}
}
- 上面代码中的
render
方法执行的是renderView
函数,看下renderView
方法的定义
// node_modules/webapp/src/emulator/src/utils/renderView.js
export function renderView(h, view, appMeta) {
let isTabView = false;
let tabIndex = 0;
if (appMeta.tabs && appMeta.tabs.items) {
tabIndex = appMeta.tabs.items.findIndex(tabItem => tabItem.href && tabItem.href.name === view.name);
if (tabIndex > -1) {
isTabView = true
}
}
const globalHeader = get(appMeta, 'common.body.header', {});
const header = get(view, 'body.header', {});
const footer = get(view, 'body.footer', {});
const showHeader = isShowHeader(globalHeader, header);
return h(
'Page',
{
props: {
showHeader,
title: get(header, 'title', ''),
showHeaderBack: !isTabView,
showFooter: !footer.hide,
pageStyle: view.style,
headerStyle: header.style,
footerStyle: footer.style
}
},
[
renderViewHeader(h, header, showHeader),
renderViewArea(h, view, appMeta, renderAreaList),
isTabView ? renderTabFooter(h, appMeta, tabIndex) : null
]
)
}
renderView
方法接收的参数:
h
:render
的方法参数view
: 路由信息appMeta
: 元数据options
:额外配置
renderView
方法首先是从元数据获取一些配置信息,最后返回并执行 h()
方法,分析下传入的参数:
第一个参数字符串
page
,结合上文的h
方法的介绍,可以知道page
应该是个已经注册好的组件第二参数是给这个
page
组件传入的props
属性第三个参数表示这个
page
组件中包含哪些子组件,这些子组件分别使用renderViewHeader(h, header, showHeader)
、renderViewArea(h, view, appMeta, renderAreaList)
、renderTabFooter(h, appMeta, tabIndex)
生成
renderViewHeader
、 renderViewArea
、 renderTabFooter
这些方法内部也是调用传入的 h
方法生成的组件
这里唯一让人困惑的是 h
方法的第一参数是要已经定义好的组件,那么这些组件在什么时候注册的呢?*
# 注册组件
paas 中的组件在这里分为两类,一类是内置的基础组件,一类是我们开发的业务组件(包括钩子组件)
注册基础组件
内置的基础组件在入口文件 node_modules/webapp/src/engine/src/main.js
中注册的,我们要下具体代码
import InnerComponents from '@engine/components'
import BaseComponents from '@components'
Vue.use(InnerComponents);
Vue.use(BaseComponents);
BaseComponents
在node_modules/webapp/src/engine/src/components/index.js
目录下
/**
* 全局注册内置组件
*/
import Page from './Page'
import TabMenu from './TabMenu'
const components = [
Page,
TabMenu,
];
export default {
install(Vue) {
components.forEach(component => Vue.component(component.name, component))
},
components
};
export {
Page,
TabMenu
}
这个文件是把所有内置组件导入并使用 Vue.component
方法注册成全局组件,可以也看到上文中我们看到page
组件也是在这里注册的
InnerComponents
也是同理,它定义在 node_modules/webapp/src/component/packages/index.js
目录下
/**
* 全局注册内置组件
*/
import Banner from './banner'
import MenuGrid from './menu-grid'
// 就不一一列举了
const components = [
Banner,
MenuGrid,
// 就不一一列举了
];
export default {
install(Vue) {
components.forEach(component => Vue.component(component.name, component))
},
components
};
export {
Banner,
MenuGrid,
// 就不一一列举了
}
也是使用的 Vue.component(component.name, component)
注册的组件
注册业务组件
我们业务组件在引擎的 src
目录下并没有直接找到引用的地方,因为它们是通过 webpack
构建项目时处理的
我们查看 webpack
配置文件 module
部分有定义一个 rules
// node_modules/webapp/build/webpack.base.conf.js
rules: [
{
test: /register\.js$/, // node_modules/webapp/src/biz/register.js
include: [resolveUtils.resolve('src/biz')],
loader: 'biz-loader'
},
....
]
这个配置会使用自定义 loader
biz-loader
处理 node_modules/webapp/src/biz/register.js
文件,这个 loader
就是注册业务组件的关键
首先我们先看下 register.js
的内容
import Vue from 'vue'
import router from '@platform/router'
import store from '@platform/store'
import { BODY_PARSER_PROPS as bodyProps } from '@utils/variable'
import { error } from '@platform/log'
export const modules = [];
/**
* 根据 VueJS 组件特殊属性判断
* @param options
* @returns {boolean}
*/
function isVueOptions(options) {
if (!options) {
return false
}
return typeof options.template === 'string' || typeof options.render === 'function'
}
function getOptionItems(options = {}) {
return Array.isArray(options) ? options : (options.items || [])
}
function registerBody(id, { body, component, name }) {
const routeBody = body || component;
if (routeBody) {
if (typeof routeBody === 'function') {
// 异步组件
Vue.component(['parsed', id, name].join('-'), routeBody)
} else {
bodyProps.forEach(prop => {
getOptionItems(routeBody[prop]).forEach((item, index) => {
// 增加 parsed 前缀,方便识别
Vue.component(['parsed', id, name, prop, index].join('-'), item)
})
});
if (isVueOptions(routeBody)) {
Vue.component(['parsed', id, name].join('-'), routeBody)
}
}
}
}
/* eslint-disable */
function registerPlugin(id, module) {
if (module && module.install) {
modules.push(module)
} else {
error(`钩子组件${id ? `(${id})` : ''}不符合规范`)
}
}
/* eslint-disable */
function registerComponent(id, { component, routes, storeModule }) {
if (id && component) {
Vue.component(id, component);
if (routes) {
routes.forEach(route => {
const { body, component } = route;
if (body || typeof component === 'function' || bodyProps.some(prop => component && !!component[prop])) {
registerBody(id, route)
} else {
router.addRoutes([route])
}
})
}
const hasDefined = storeModule && store && store._modules
if (hasDefined && store._modules.get && !store._modules.get([id])) {
store.registerModule(id, storeModule)
}
}
}
这个文件只是定义了一些函数
我们先看下 biz-loade
的作用,根据 webpack
配置可找到自定义 loader
的配置位置
resolveLoader: {
modules: ['node_modules', path.join(__dirname, './loader')]
},
下面是 biz-loade.js
的代码
const fs = require('fs');
const path = require('path');
const argv = require('../argv').argv;
const loadJSON = require('../helper').loadJSON;
const parseComponents = require('../parseComponents');
const pluginIds = typeof argv.pluginIds === 'string' ? argv.pluginIds.split(',') : [];
const clients = typeof argv.clients === 'string' ? argv.clients.toLowerCase().split(',') : [];
const withMp = clients.indexOf('miniprogram') > -1
//开发者业务组件列表
let componentList = [];
const devComponentMap = {};
// 业务组件开发模式
console.log('argv', argv)
if (argv.devmode) {
parseComponents(function (data) {
const parsedDir = data.dir.replace(/\\/g, '/');
componentList.push(parsedDir);
devComponentMap[parsedDir] = data
})
} else {
componentList = require('../../src/biz/list')
}
function getComponentId(component) {
return argv.devmode ? loadJSON(`${component}/package.json`).name : component
}
if (componentList.length) {
console.log(
'[Biz components include list]\n>',
componentList.map(function (component) {
return component + (pluginIds.indexOf(getComponentId(component)) > -1 ? '[hook]' : '')
}).join('\n> ')
);
}
function exists(dir) {
return argv.devmode ?
fs.existsSync(dir) && fs.existsSync(path.join(dir, 'index.js')) :
fs.existsSync(path.join(__dirname, '../../node_modules', dir)) && fs.existsSync(path.join(__dirname, '../../node_modules', dir, 'index.js'))
}
function tryBlock(code, component) {
return argv.devmode ? code : `
try {
${code}
} catch (e) {
error(e, {tags: { component: '${ component }' }})
}`
}
function getPluginCode(component, componentId) {
return exists(component) ? tryBlock(
`registerPlugin('${ componentId }', require('${ component}').default || {})`,
componentId
) : ''
}
function getComponentCode(component, componentId) {
return exists(component + '/src') ? tryBlock(
`registerComponent('${ componentId }', require('${ component}/src').default || {})`,
componentId
) : ''
}
function genRegisterComponentCodes() {
console.log('componentList',componentList)
return componentList.map(function (component) {
const componentId = getComponentId(component);
// 非业务组件、钩子组件,忽略
if (argv.devmode && devComponentMap[component] && !devComponentMap[component].isComponent) {
return ''
}
return pluginIds.indexOf(componentId) === -1 ?
getComponentCode(component, componentId) :
getPluginCode(component, componentId)
}).join('\n')
}
function getWxmlCodes() {
const target = path.resolve(__dirname, '../MiniProgram/wxml').replace(/\\/g, '/')
console.log('__dirname', __dirname)
console.log('target', target)
// 非最终构建,即引用组件或清除引擎缓存之后的构建
const emulatorUse = argv.devmode === 'biz'
|| /buildDesigner\.js$/.test(argv['$0'])
|| /buildBiz\.js$/.test(argv['$0'])
return (emulatorUse || withMp) ? `\
const MpComponents = require('${target}').default;
Vue.use(MpComponents);\n
` : ''
}
module.exports = function (source) {
console.log('source', source)
return source + ';\n' + getWxmlCodes() + genRegisterComponentCodes()
};
首先定义了一个数组 componentList
,这个数组就是存放我们的业务组件和钩子组件信息的地方,在开发环境下 componentList
通过 parseComponents
方法进行填充,它实际又调用了 utils.parseComponents(path.join(__dirname, '../../..'), callback)
方法,这里给 utils.parseComponents
传递一下参数,这个参数是一下路径,就是我们项目根目录的路径
接下来我们看下 utils.parseComponents
的定义
const deepDirs = ['packages', 'workspace', 'modules'];
function parseComponents(rootDir, cb, isNotDeep, checkFiles, filter, parent) {
if (rootDir && fs.existsSync(rootDir)) {
const dirList = fs.readdirSync(rootDir);
dirList.forEach(biz => {
const currentPath = path.join(rootDir, biz);
if (isComponentDir(currentPath, checkFiles)) {
if (filter.test(biz)) {
cb(parseComponent(currentPath, parent))
}
} else if (!isNotDeep && deepDirs.indexOf(biz) > -1) {
parseComponents(currentPath, cb, isNotDeep, checkFiles, filter, biz)
}
});
}
}
parseComponents
函数中先是根据路径获取该路径下的所有文件并遍历他们,通过 isComponentDir(currentPath, checkFiles)
方法判断是否是业务组件或钩子组件(通过文件下是否包含 index.js
或者 package.json
),如果符合的话执行 cb(parseComponent(currentPath, parent))
方法,如果文件夹且名字是 ['packages', 'workspace', 'modules']
中的一个则继续执行自身函数获取文件,这些是我们开发的组件存放的地方,接下来我们看下parseComponent()
方法
function parseComponent(dir, parent) {
const packagePath = path.join(dir, 'package.json');
const packageInfo = loadJSON(packagePath);
const id = packageInfo.name || dir.replace(/\\/g, '/').split('/').pop();
return Object.assign(
{
title: packageInfo.description || id
},
packageInfo.componentConfig,
{
dir: dir,
id: id,
type: 'biz',
visible: pluginIds.indexOf(id) > -1 ? 0 : 1,
isComponent: parent !== 'modules' || pluginIds.indexOf(id) > -1
}
)
}
parseComponent
方法则是通过读取 package.json
文件,然后返回当前组件目录的一些基本信息 dir
(目录路径)、id
、isComponent
(是否是业务组件或钩子组件)等
回到 biz-loader
,这些组件信息就存入 componentList
数组中了
最后看下 biz-loader
的导出结果
module.exports = function (source) {
return source + ';\n' + getWxmlCodes() + genRegisterComponentCodes()
};
直接返回 register.js
内容加上两个函数 getWxmlCodes()
、genRegisterComponentCodes()
的返回结果,这里我们重点关注一下 genRegisterComponentCodes()
方法
function genRegisterComponentCodes() {
return componentList.map(function (component) {
const componentId = getComponentId(component);
// 非业务组件、钩子组件,忽略
if (argv.devmode && devComponentMap[component] && !devComponentMap[component].isComponent) {
return ''
}
return pluginIds.indexOf(componentId) === -1 ?
getComponentCode(component, componentId) :
getPluginCode(component, componentId)
}).join('\n')
}
genRegisterComponentCodes()
方法中遍历 componentList
,即上文收集到所以组件信息,然后根据钩子组件还是业务组件执行不同的方法
这里我们先看业务组件的处理,它是执行 getComponentCode(component, componentId)
function getComponentCode(component, componentId) {
return exists(component + '/src') ? tryBlock(
`registerComponent('${ componentId }', require('${ component}/src').default || {})`,
componentId
) : ''
}
这里返回的一个执行函数的字符串,这些函数将在 register.js
文件被读取时执行。
register.js
被 loader
处理后代码如下
import Vue from 'vue';
import router from '@platform/router';
import store from '@platform/store';
import { BODY_PARSER_PROPS as bodyProps } from '@utils/variable';
import { error } from '@platform/log';
export var modules = [];
function isVueOptions(options) {
if (!options) {
return false;
}
return typeof options.template === 'string' || typeof options.render === 'function';
}
function getOptionItems() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
return Array.isArray(options) ? options : options.items || [];
}
function registerBody(id, _ref) {
var body = _ref.body,
component = _ref.component,
name = _ref.name;
var routeBody = body || component;
if (routeBody) {
if (typeof routeBody === 'function') {
Vue.component(['parsed', id, name].join('-'), routeBody);
} else {
bodyProps.forEach(function (prop) {
getOptionItems(routeBody[prop]).forEach(function (item, index) {
Vue.component(['parsed', id, name, prop, index].join('-'), item);
});
});
if (isVueOptions(routeBody)) {
Vue.component(['parsed', id, name].join('-'), routeBody);
}
}
}
}
function registerPlugin(id, module) {
if (module && module.install) {
modules.push(module);
} else {
error('\u94A9\u5B50\u7EC4\u4EF6' + (id ? '(' + id + ')' : '') + '\u4E0D\u7B26\u5408\u89C4\u8303');
}
}
function registerComponent(id, _ref2) {
var component = _ref2.component,
routes = _ref2.routes,
storeModule = _ref2.storeModule;
if (id && component) {
Vue.component(id, component);
if (routes) {
routes.forEach(function (route) {
var body = route.body,
component = route.component;
if (body || typeof component === 'function' || bodyProps.some(function (prop) {
return component && !!component[prop];
})) {
registerBody(id, route);
} else {
router.addRoutes([route]);
}
});
}
var hasDefined = storeModule && store && store._modules;
if (hasDefined && store._modules.get && !store._modules.get([id])) {
store.registerModule(id, storeModule);
}
}
};
// 下面是loader处理添加的代码
const MpComponents = require('D:/Project/p_qudao/yunke-paas/node_modules/webapp/build/MiniProgram/wxml').default;
Vue.use(MpComponents);
registerPlugin('b2c-jssdk', require('D:/Project/p_qudao/yunke-paas/modules/b2c-jssdk').default || {})
registerPlugin('broker-common', require('D:/Project/p_qudao/yunke-paas/modules/broker-common').default || {})
registerComponent('b2c-bespeak-house', require('D:/Project/p_qudao/yunke-paas/packages/b2c-bespeak-house/src').default || {})
registerComponent('b2c-broadcast', require('D:/Project/p_qudao/yunke-paas/packages/b2c-broadcast/src').default || {})
registerComponent('b2c-my-listmenu', require('D:/Project/p_qudao/yunke-paas/packages/b2c-my-listmenu/src').default || {})
registerComponent('b2c-search', require('D:/Project/p_qudao/yunke-paas/packages/b2c-search/src').default || {})
那么 register.js
被调用时,将执行这些 registerPlugin
、registerComponent
方法,在 registerComponent
函数的定义中,终于找到了 Vue.component(id, component)
,在这里注册我们编写的业务组件的同时还处理组件中的路由和store
等
回到PAAS应用的入口函数可以找到register
确实会引入了
import { modules } from '@root/biz/register'
致此,原来我们的业务是这么注册进来~
# 例子
这例子需要实现的功能
结合
webpack
配置注册全局使用
render方法
渲染组件异步获取元数据,并动态更新组件
例子使用Vue-cli
初始的项目,首页添加register.js
文件
// src/biz/register.js
import Vue from 'vue'
function registerComponent(com) {
console.log('com', com)
Vue.component('page1', com)
}
export default function () {
console.log('加载了register')
}
register
内容很简单,提供一个注册组件的方法
然后给 webpack
添加处理 register.js
的 Loader
// build/loader/biz-loader.js
const fn = `registerComponent(require('../page/index.vue').default)`
module.exports = function (source) {
const abc = source + ';\n' + fn
console.log('source', abc)
return abc
};
biz-loader
Loader的内容也很简单,就是添加一个registerComponent(require('../page/index.vue').default)
的执行方法,
这里直接传入我们要动态注册的组件的路径
然后添加loader配置
// build/webpack.base.conf.js
module: {
rules: [
{
test: /register\.js$/,
include: resolve('src/biz'),
loader: 'biz-loader'
},
// ....
]
},
在入口函数中引入 register.js
import register from './biz/register'
然后在挂载组件App.vue
使用setTimeout
方法模拟异步更新数据,并加载组件
<template>
<div id="app">
<comment :is="view"></comment>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
meta: '是谁'
}
},
computed: {
view() {
console.log('this.meta', this.meta)
const vm = this
return {
render(createElement) {
return createElement('Page', {
props: {
meta: vm.meta
}
})
}
}
}
},
mounted() {
setTimeout(() => {
this.meta = 'lanjz'
}, 2000)
}
}
</script>
运行后页面将显示如下效果
两秒页面将更新