clientRouting

环境:Browser

默认情况下,vite-plugin-ssr 执行 服务端路由

我们可以通过修改 /renderer/_default.client.js 来选择 客户端路由

  1. 设置 export const clientRouting = true
  2. 更新并适配 客户端 render() hook.

React example:

Vue example:

用例和选项

// /renderer/_default.page.client.js
// Environment: Browser

export { render }

// 开启客户端路由
export const clientRouting = true

// 请参阅下面的 `Link prefetching` 部分。 默认值:`{ when: 'HOVER' }`。
export const prefetchStaticAssets = { when: 'VIEWPORT' }

// 你的 UI 框架是否允许 hydration 被中止
// (允许 vite-plugin-ssr 在用户点击链接完成之前中止 hydration)
// 只有 React 用户应该将 `hydrationCanBeAborted` 设置为 `true`
// (其他框架,如 Vue,会在 hydration 中止时抛出错误)
export const hydrationCanBeAborted = true

// 自定义页面过渡动画
export { onPageTransitionStart }
export { onPageTransitionEnd }

import { renderToDom, hydrateDom } from 'some-ui-framework'

async function render(pageContext) {
    // `pageContext.isHydration` 由 `vite-plugin-ssr` 设置,
    // 当页面已经渲染为 HTML 时为 `true`
    if (pageContext.isHydration) {
      // 我们给第一个页面 hydrate(由于我们做首屏 SSR,
      // 第一个页面已经渲染为 HTML,我们只需要对其 hydrate)
      await hydrate(pageContext.Page)
    } else {
      // 我们渲染一个新页面。 (当用户导航到新页面时。)
      await renderToDom(pageContext.Page)
    }
  }
}

function onPageTransitionStart(pageContext) {
  console.log('Page transition start')
  // `pageContext.isBackwardNavigation` 也在 `render(pageContext)`
  // 和 `onPageTransitionEnd(pageContext)` 中设置。
  console.log('Is backwards navigation?', pageContext.isBackwardNavigation)
  // 例如:
  document.body.classList.add('page-transition')
}
function onPageTransitionEnd(pageContext) {
  console.log('Page transition end')
  // 例如:
  document.body.classList.remove('page-transition')
}

请注意,pageContext 在页面导航时被完全丢弃并重新创建。 这就是它被称为 pageContext(而不是 appContext)的原因

我们可以继续使用 <a href="/some-url"> 链接:客户端路由器自动拦截对 <a> 元素的点击。

我们可以通过添加 rel="external" 属性来跳过客户端路由器,例如 <a rel="external" href="/some/url">客户端路由器不会拦截我</a>

我们可以使用 navigate('/some/url') 以编程方式将用户导航到新页面。

默认情况下,客户端路由器在页面更改时滚动到页面顶部;

如果我们想保留滚动位置,可以使用<a keep-scroll-position /> / navigate('/some/url', { keepScrollPosition: true }) (对于 嵌套布局 很有用)

状态初始化

通常,在使用 Apollo GraphQL、Redux 或 Vuex 等工具时,我们会在渲染 HTML 时确定服务端 UI 的初始状态,然后使用该初始状态初始化客户端

根据工具,我们执行以下操作之一:

  • 初始化状态一次
  • 重新初始化每个页面导航的状态

初始化一次:

// /renderer/_default.page.server.js
// Environment: Node.js

export { render }
export { passToClient }

import { escapeInject, dangerouslySkipEscape } from 'vite-plugin-ssr'
import { renderToHtml } from 'some-ui-framework'
import { getInitialState } from './getInitialState'

const passToClient = ['initialState']

// `render()` hook 只在第一个页面被调用。
// (而在页面导航时也会调用 `onBeforeRender()`。)
async function render(pageContext) {
	const initialState = await getInitialState()

	// 我们使用 `initialState` 来渲染 HTML,
	// 因此 HTML 包含 `initialState` 的内容。
	const pageHtml = await renderToHtml(pageContext.Page, initialState)

	const documentHtml = escapeInject`<!DOCTYPE html>
    <html>
      <body>
        <div>${dangerouslySkipEscape(pageHtml)}</div>
      </body>
    </html>`

	return {
		documentHtml,
		pageContext: {
			initialState,
		},
	}
}
// /renderer/_default.page.client.js
// Environment: Browser

export { render }

import { initClientSide } from './initClientSide'

async function render(pageContext) {
	// 第一个页面渲染为 HTML,并且 `pageContext.isHydration === true`
	if (pageContext.isHydration) {
		// `pageContext.initialState` is available here
		initClientSide(pageContext.initialState)
	} else {
		// 请注意,`pageContext.initialState` 在这里不可用,
		// 因为我们的 `render()` hook 只在第一个页面被调用
	}

	// ...
}

在每个页面导航上初始化:

// /renderer/_default.page.server.js
// Environment: Node.js

export { onBeforeRender }
export { passToClient }

import { getInitialState } from './getInitialState'

const passToClient = ['initialState']

// `onBeforeRender()` hook 在第一个页面和页面导航时被调用
// (而 `render()` 只在第一个页面被调用。)
async function onBeforeRender() {
	const initialState = await getInitialState()
	return {
		pageContext: {
			initialState,
		},
	}
}
// /renderer/_default.page.client.js
// Environment: Browser

export { render }

import { initClientSide } from './initClientSide'

async function render(pageContext) {
	// 我们为每个页面渲染初始化状态。
	// 因此,不仅是第一个页面,还有任何后续页面导航。
	initClientSide(pageContext.initialState)

	// ...
}

默认情况下,只要用户将鼠标悬停在链接 <a href="/some-url"> 上,就会加载 /some-url 的静态资源。 这意味着静态资源通常在用户点击链接之前就已加载。

我们可以通过使用视图窗口预获取来更快地获取资源:链接一旦出现在用户浏览器的视图窗口中就会被预获取。

// /renderer/_default.page.client.js
// Environment: Browser

// 一进入视图窗口就预获取链接
export const prefetchStaticAssets = { when: 'VIEWPORT' }

// 当用户的鼠标悬停在链接上时预获取链接
export const prefetchStaticAssets = { when: 'HOVER' }

// 禁用预获取
export const prefetchStaticAssets = false

视图窗口预获取在开发中被禁用。(因为它会使 Vite 转换所有预加载的页面,从而明显减慢开发速度)

我们可以通过设置 <a data-prefetch="true" href="/some-url" /> 来覆盖单个链接的链接预获取行为

仅预获取静态资源;页面的 pageContext 不会被预获取,请参阅 #246

We can also have viewport prefetching for mobile users while having hover prefetching for desktop users: 我们还可以为移动用户开启视图窗口预获取,而为桌面用户开启 hover 预获取:

// 对于小屏幕,例如移动设备,视图窗口预获取可能是一个明智的策略
export const prefetchStaticAssets = window.matchMedia('(max-width: 600px)').matches
	? { when: 'VIEWPORT' }
	: { when: 'HOVER' }
// 或者我们为任何没有鼠标的设备启用视图窗口预获取:移动设备和平板电脑
// (但不包括具有触摸屏的笔记本电脑)。
export const prefetchStaticAssets = window.matchMedia('(any-hover: none)').matches
	? { when: 'VIEWPORT' }
	: { when: 'HOVER' }

另请参考: