# 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-loaderLoader的内容也很简单,就是添加一个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>
运行后页面将显示如下效果

两秒页面将更新
