编写通用的代码
尽管代码同构可以避免维护两个平台的代码,但是我们在编写含有服务端渲染的项目代码时,也需要注意并遵循一些原则,从而避免 bug
的产生。
服务端的数据响应性
在只有客户端的应用中,每个用户都在各自的浏览器中使用一个干净的应用实例。对于服务端渲染来说,我们也希望如此:每个请求拥有一个干净的相互隔离的应用实例,以避免跨请求的状态污染。
服务端渲染只负责提供首屏的静态 HTML
,这可能会从服务器 “预获取” 一些数据,这意味着应用状态在我们开始渲染之前已经被解析好了。所以数据响应式相关的特性在服务端是不必要的,因此它默认是不开启的,禁用数据响应性也避免了将数据转换为响应式对象的性能损耗。代码如下:
<script setup>
const count = ref(0)
// 此时的数据响应式将不会生效,服务端渲染返回的HTML中,count还是0
setTimeout(()=>{
count.value = 2
},1000)
</script>
html
上面的代码中,count
的响应式在服务端渲染时将不会生效。
组件生命周期钩子
因为服务端渲染没有响应式以及动态更新,所以针对组件来说,唯一会在服务端渲染过程中被调用的生命周期钩子是 beforeCreate
和 created
。这意味着其他生命周期钩子(例如 beforeMount
或 mounted
)只会在客户端被执行。
所以,如果首屏页面需要的一些数据是从后端服务获取的,那么这部分逻辑应该放在 created
中,代码如下:
<script>
import axios from 'axios'
export default {
async created(){
// 请求首屏数据
const data = await axios.get(`/api/foo/1`)
}
}
</script>
html
注意,beforeCreate
和 created
相关逻辑会在服务端渲染时执行,当页面被浏览器打开时,客户端也会执行这里的逻辑,由于此刻数据变化的可能性非常小,所以就是客户端又渲染了一遍这里的逻辑,但是页面基本不会改变。当然,我们也可以通过环境标志位 import.meta.env.SSR
来规定一些逻辑只在服务端渲染时执行,代码如下:
export default {
async created(){
// 只在服务端渲染时执行这部分逻辑
if (import.meta.env.SSR) {
const data = await axios.get(`/api/post/1`)
}
}
}
javascript
另外,在服务端渲染时,注意避免代码在 beforeCreate
或 created
中产生全局的副作用,例如通过 setInterval
设置定时器。在只有客户端的代码中,我们可以设置定时器,然后在 beforeUnmount
或 unmounted
时销毁。然而,因为销毁相关的钩子在服务端渲染中不会被调用,这些定时器就会永久地保留下来。为了避免这件事,可以把有副作用的代码移至 beforeMount
或 mounted
中。
如果项目中使用了 Vuex
来获取数据,那么可以在服务端渲染中提前设置 store
的内容,在客户端渲染时就可以拿到。关于 Vuex
的服务端渲染这部分内容将会在第 12 章深入讲解。
访问特定平台的 API
当代码可能会在浏览器和 Node.js 服务端都运行时,就需要考虑特有平台 API 的使用,因此如果代码直接使用只存在于浏览器的全局变量(例如 window
或 document
),它们会在 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 端使用而报错的场景出现。