编写 universal
代码(也称为 isomorphic
)意味着编写在服务器和客户端上运行的代码。由于用例和平台 API 的差异,我们的代码在不同环境中运行时的行为不会完全相同。以下我们将介绍你需要注意的关键事项。
¥Writing universal
code (also called isomorphic
) means writing code that runs on both the server and the client. Due to use-case and platform API differences, the behavior of our code will not be exactly the same when running in different environments. Here we will go over the key things you need to be aware of.
服务器上的数据反应性(Data Reactivity on the Server)
¥Data Reactivity on the Server
在纯客户端应用中,每个用户都会在其浏览器中使用该应用的全新实例。对于服务器端渲染,我们想要的是相同的:每个请求都应该有一个全新的、独立的应用实例,这样就不会出现跨请求状态污染。
¥In a client-only app, every user will be using a fresh instance of the app in their browser. For server-side rendering we want the same: each request should have a fresh, isolated app instance so that there is no cross-request state pollution.
由于实际的渲染过程需要确定性,我们将服务器上的 “pre-fetching” 数据 - 这意味着当我们开始渲染时,我们的应用状态已经解析完毕。这意味着数据反应性在服务器上是不必要的,因此默认情况下处于禁用状态。禁用数据反应性还可以避免将数据转换为反应性对象所带来的性能成本。
¥Because the actual rendering process needs to be deterministic, we will also be “pre-fetching” data on the server - this means our application state will be already resolved when we start rendering. This means data reactivity is unnecessary on the server, so it is disabled by default. Disabling data reactivity also avoids the performance cost of converting data into reactive objects.
组件生命周期钩子(Component Lifecycle Hooks)
¥Component Lifecycle Hooks
由于没有动态更新,在所有 Vue 生命周期钩子中,只有 beforeCreate
和 created
会在服务端渲染 (SSR) 期间被调用。这意味着其他生命周期钩子(例如 beforeMount
或 mounted
)内的任何代码都将仅在客户端执行。
¥Since there are no dynamic updates, of all the Vue lifecycle hooks, only beforeCreate
and created
will be called during SSR. This means any code inside other lifecycle hooks such as beforeMount
or mounted
will only be executed on the client.
另外需要注意的是,你应该避免在 beforeCreate
和 created
中使用会产生全局副作用的代码,例如使用 setInterval
设置计时器。在客户端代码中,我们可能会设置一个计时器,然后在 beforeUnmount
或 destroyed
中将其拆除。但是,由于在 SSR 期间不会调用 destroy 钩子,因此计时器将永远存在。为了避免这种情况,请将你的副作用代码移到 beforeMount
或 mounted
中。
¥Another thing to note is that you should avoid code that produces global side effects in beforeCreate
and created
, for example setting up timers with setInterval
. In client-side only code we may setup a timer and then tear it down in beforeUnmount
or destroyed
. However, because the destroy hooks will not be called during SSR, the timers will stay around forever. To avoid this, move your side-effect code into beforeMount
or mounted
instead.
避免有状态单例(Avoid Stateful Singletons)
¥Avoid Stateful Singletons
编写纯客户端代码时,我们习惯于每次都在新的上下文中执行代码。但是,Node.js 服务器是一个长期运行的进程。当我们的代码需要进入进程时,它会被执行一次,然后就驻留在内存中。这意味着如果你创建一个单例对象,它将在每个传入请求之间共享。
¥When writing client-only code, we are used to the fact that our code will be evaluated in a fresh context every time. However, a Node.js server is a long-running process. When our code is required into the process, it will be evaluated once and then it stays in memory. This means if you create a singleton object, it will be shared between every incoming request.
因此,Quasar CLI 会为每个请求创建一个新的根 Vue 实例,其中包含一个新的 Router 和 Pinia 实例。这类似于每个用户在自己的浏览器中使用应用的全新实例的方式。如果我们在多个请求中使用共享实例,则很容易导致跨请求状态污染。
¥So, Quasar CLI creates a new root Vue instance with a new Router and Pinia instance for each request. This is similar to how each user will be using a fresh instance of the app in their own browser. If we would have used a shared instance across multiple requests, it will easily lead to cross-request state pollution.
你无需直接创建 Router 和 Pinia 实例,而是公开一个可重复执行的工厂函数,以便为每个请求创建新的应用实例:
¥Instead of directly creating a Router and Pinia instance, you’ll be exposing a factory function that can be repeatedly executed to create fresh app instances for each request:
import { defineRouter } from '#q-app/wrappers'
export default defineRouter((/* { store, ssrContext } */) {
const Router = new VueRouter({...})
return Router
})
import { defineStore } from '#q-app/wrappers'
import { createPinia } from 'pinia'
/*
* If not building with SSR mode, you can
* directly export the Store instantiation;
* * The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Store instance.
*/
export default defineStore((/* { ssrContext } */) => {
const pinia = createPinia()
// You can add Pinia plugins here
// pinia.use(SomePiniaPlugin)
return pinia
})
访问特定于平台的 API(Access to Platform-Specific APIs)
¥Access to Platform-Specific APIs
通用代码无法访问特定于平台的 API,因此,如果你的代码直接使用仅限浏览器的全局变量(例如 window
或 document
),则它们在 Node.js 中执行时会抛出错误,反之亦然。
¥Universal code cannot assume access to platform-specific APIs, so if your code directly uses browser-only globals like window
or document
, they will throw errors when executed in Node.js, and vice-versa.
对于服务器和客户端之间共享但使用不同平台 API 的任务,建议将特定于平台的实现封装在通用 API 中,或使用可为你执行此操作的库。例如,Axios 是一个 HTTP 客户端,它为服务器和客户端公开相同的 API。
¥For tasks shared between server and client but use different platform APIs, it’s recommended to wrap the platform-specific implementations inside a universal API, or use libraries that do this for you. For example, Axios is an HTTP client that exposes the same API for both server and client.
对于仅支持浏览器的 API,常用方法是在仅支持客户端的生命周期钩子中延迟访问它们。
¥For browser-only APIs, the common approach is to lazily access them inside client-only lifecycle hooks.
启动文件(Boot Files)
¥Boot Files
请注意,如果第三方库在编写时没有考虑通用性,那么将其集成到服务器渲染的应用中可能会比较棘手。你可能可以通过模拟一些全局变量来使其工作,但这会很不方便,并且可能会干扰其他库的环境检测代码。
¥Note that if a 3rd party library is not written with universal usage in mind, it could be tricky to integrate it into a server-rendered app. You might be able to get it working by mocking some of the globals, but it would be hacky and may interfere with the environment detection code of other libraries.
当你将第三方库添加到项目(通过 启动文件)时,请考虑它是否可以在服务器和客户端上运行。如果只需要在服务器上运行或仅在客户端上运行,请在 /quasar.config
文件中指定:
¥When you add a 3rd party library to your project (through a Boot File), take into consideration whether it can run on server and on client. If it needs to run only on server or only on client, then specify this in the /quasar.config
file:
return {
// ...
boot: [
'some-boot-file', // runs on both server & client
{ path: 'some-other', server: false } // this boot file gets embedded only on client-side
{ path: 'third', client: false } // this boot file gets embedded only on server-side
]
}
数据预取和状态(Data Pre-Fetching and State)
¥Data Pre-Fetching and State
在 SSR 期间,我们本质上是在渲染应用的 “snapshot”,因此如果应用依赖于某些异步数据,则需要在开始渲染过程之前预取并解析这些数据。
¥During SSR, we are essentially rendering a “snapshot” of our app, so if the app relies on some asynchronous data, this data need to be pre-fetched and resolved before we start the rendering process.
Quasar CLI 预取功能 就是为了解决这个问题而创建的。花点时间阅读一下。
¥The Quasar CLI PreFetch Feature has been created to solve this problem. Take a few moments to read about it.
本页面部分内容摘自官方 Vue.js 服务器端渲染指南。
¥Parts of this page are taken from the official Vue.js SSR guide.