React 有个 portal API,简单的来说就是可以将子组件渲染到父组件以外的地方。官网上说 portal 的典型用例是当父组件有​​overflow: hidden​​​或​​z-index​​样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。

但是当时对此例并无太多感觉,这就导致了我一度以为这是个没什么卵用的 API,大多数的使用场景应该是在封装一些公共组件上。
但是有这样一个需求,一个后台管理系统中,我们的各个业务页面基本都是在布局中显示的,通过布局中的侧边栏/标签栏/面包屑进行页面切换。

而这些通过 vue 实现基本上都是用 vue-router。如下面两个页面:

import Layout from '@/layout'

export default [
{
name: 'Admin',
path: '/admin',
component: Layout,
redirect: '/admin/role',
meta: { title: '后台管理', icon: 'el-icon-monitor' },
children: [
{
path: 'role',
name: 'AdminRole',
meta: { title: '角色管理', noCache: true },
component: () => import('@/views/admin/role')
},
{
path: 'user',
name: 'AdminUser',
meta: { title: '账号管理', noCache: true },
component: () => import('@/views/admin/user')
}
]
}
]

如此便可以通过前端路由,实现对 layout 布局组件的复用。

对于不需要 layout 的页面,只需:

{
path: '/404',
hidden: true,
component: () => import('@/views-constant/error-page/404')
}

不使用 layout 即可。而整个 app 其实就是一个个嵌套的 router-view。最外层的整个 app 是 router-view,里面的业务页面也是 router-view。

这样的架构,几乎就可以满足中小型后台的全部需求了。

但是,需求总是不可控的。

前些日子,后台需要做个页面。这个页面需要在管理员确认一些信息后,将页面内容打印下来。打印页面自然是用 window.print 方法就好。但是问题出在,window.print 会把整个页面都打印下来,其中就包括侧边栏/导航栏/面包屑这些属于布局的部分。因此要实现这个功能的核心就是在某些情况下,我们可以动态的控制 layout 是否显示。

但是实际上,通过 vue-router 我们知道。平时的业务页面都是 layout 的子组件。如果我们把 layout 隐藏,业务页面自然也会被隐藏,这可如何是好?

答案就是标题。

使用 portal 方法即可。在我们隐藏 layout 的时候,实际上就是隐藏项目最外层的那个 router-view。我们要做的就是隐藏最外面的 router-view 同时把当前页面 的 router-view 渲染到最外层 router-view 同级就好。

但是问题是 vue 并没有 portal API。不过好在有一个 vue 库叫做 ​​portal-vue​​ 可以帮助我们实现 portal 功能,用起来也十分简单:

import PortalVue from 'portal-vue'

Vue.use(PortalVue)

而使用起来只需要两个标签即可完成 portal 功能:

如此便可以将 portal 内的节点渲染到 portal-target 中,两者通过 name&to 进行关联传送。无论两个组件的相对位置如何。

带入到我们的需求中,我们只需要将内部渲染业务页面的 router-view 传送渲染到最外部的 router-view 下面就好,具体代码如下:


<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<portal v-if="$store.state.app.printing" to="app">
<router-view :key="key" />
portal>
<router-view v-else :key="key" />
keep-alive>
transition>
section>
template>

<template>
<div id="app">
<router-view v-show="!$store.state.app.printing" />
<portal-target name="app" />
div>
template>

这两个页面通过 name&to 进行关联,通过 vuex 进行公共状态共享。

这样我们在需要的时候,将 vuex 中相应的 printing 置为 true。便可以将内部 router-view 的节点内容渲染到最外部的 router-view 同级。此时的效果就类似隐藏了布局,但是实际上 portal-vue 做的绝不是仅仅移动 dom 那么简单。portal-vue 做的是 vNode 层面的移动,有兴趣可以自行研究。