编写通用的代码

尽管代码同构可以避免维护两个平台的代码,但是我们在编写含有服务端渲染的项目代码时,也需要注意并遵循一些原则,从而避免 bug 的产生。

服务端的数据响应性

在只有客户端的应用中,每个用户都在各自的浏览器中使用一个干净的应用实例。对于服务端渲染来说,我们也希望如此:每个请求拥有一个干净的相互隔离的应用实例,以避免跨请求的状态污染。

服务端渲染只负责提供首屏的静态 HTML,这可能会从服务器 “预获取” 一些数据,这意味着应用状态在我们开始渲染之前已经被解析好了。所以数据响应式相关的特性在服务端是不必要的,因此它默认是不开启的,禁用数据响应性也避免了将数据转换为响应式对象的性能损耗。代码如下:

<script setup>
    const count = ref(0)
    // 此时的数据响应式将不会生效,服务端渲染返回的HTML中,count还是0
    setTimeout(()=>{
        count.value = 2
    },1000)
</script>
html

上面的代码中,count 的响应式在服务端渲染时将不会生效。

组件生命周期钩子

因为服务端渲染没有响应式以及动态更新,所以针对组件来说,唯一会在服务端渲染过程中被调用的生命周期钩子是 beforeCreatecreated。这意味着其他生命周期钩子(例如 beforeMountmounted)只会在客户端被执行。

所以,如果首屏页面需要的一些数据是从后端服务获取的,那么这部分逻辑应该放在 created 中,代码如下:

<script>
    import axios from 'axios'
    export default {
    async created(){
        // 请求首屏数据
        const data = await axios.get(`/api/foo/1`)
    }
}
</script>
html

注意,beforeCreatecreated 相关逻辑会在服务端渲染时执行,当页面被浏览器打开时,客户端也会执行这里的逻辑,由于此刻数据变化的可能性非常小,所以就是客户端又渲染了一遍这里的逻辑,但是页面基本不会改变。当然,我们也可以通过环境标志位 import.meta.env.SSR 来规定一些逻辑只在服务端渲染时执行,代码如下:

export default {
    async created(){
        // 只在服务端渲染时执行这部分逻辑
        if (import.meta.env.SSR) {
            const data = await axios.get(`/api/post/1`)
        }
    }
}
javascript

另外,在服务端渲染时,注意避免代码在 beforeCreatecreated 中产生全局的副作用,例如通过 setInterval 设置定时器。在只有客户端的代码中,我们可以设置定时器,然后在 beforeUnmountunmounted 时销毁。然而,因为销毁相关的钩子在服务端渲染中不会被调用,这些定时器就会永久地保留下来。为了避免这件事,可以把有副作用的代码移至 beforeMountmounted 中。

如果项目中使用了 Vuex 来获取数据,那么可以在服务端渲染中提前设置 store 的内容,在客户端渲染时就可以拿到。关于 Vuex 的服务端渲染这部分内容将会在第 12 章深入讲解。

访问特定平台的 API

当代码可能会在浏览器和 Node.js 服务端都运行时,就需要考虑特有平台 API 的使用,因此如果代码直接使用只存在于浏览器的全局变量(例如 windowdocument),它们会在 Node.js 中执行的时候抛出错误,反之亦然。

基于此,在编码时就要做好平台的逻辑判断。而对于一些可以共享平台的 API,例如 Axios,既可以在服务端使用,也可以在浏览器端使用类似的库,因此更加推荐使用。

举一个例子,对于服务端渲染来说,由于采用的是 Node.js 环境,所以需要对 window 对象做兼容处理,这里推荐使用 jsdom 库:

npm install jsdom --save
bash

然后可以将 jsdom 库相关逻辑添加到 server.js 中,代码如下:

const jsdom = require('jsdom')
const { JSDOM } = jsdom

/* 模拟 window 对象逻辑 */
const resourceLoader = new jsdom.ResourceLoader({
    userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1",
}); // 模拟 UA
const dom = new JSDOM('', {
    url: 'https://app.nihaohijie.com.cn/index.html', // 模拟 url
    resources: resourceLoader
});

global.window = dom.window
global.document = window.document
global.navigator = window.navigator
window.nodeis = true; //可自行设置给 window 标识出 node 环境的标志位
javascript

这样,就可以在 Node.js 中使用模拟的 window 对象了,不仅扩展了一些功能,同时可以防止真正需要在浏览器使用的 window 代码,在 Node.js 端使用而报错的场景出现。