SSR项目其实是比较特殊的,服务端和客户端都会执行主要逻辑,这个在其他的场景下很少见。
因为宿主环境的不同,就会有很多不一致的特性。
最近发开中发现项目中的window
不好控制,经常存在服务端访问的情况,也就是NODE
环境下访问 window 会异常。
因为这个点思考了几个减少这层面心智负担的方式。
一般来讲服务端如果访问了 window
,异常抛的是 window is not defined
,所以用 globalThis 来代替 window
,就可以被访问到。
但是这里存在问题,比如 globalThis.location.href
, 服务端访问到这样的代码依然会出问题。
所以这个方案 PASS。
其实就是有个类似 isBrowser
的方法,判断当前是否在浏览器。
// 根据 env 判断
const isBrowser = () => {
return process.env.runtime !== 'nodejs';
};
// 也可以用window是否能访问判断
const isBrowser = () => {
try {
return !!window;
} catch {
return false;
}
};
// ...
先用这个方法前置判断,然后再执行业务逻辑:
if (isBrowser()) {
window;
}
其实现在框架就提供了这个方法,但是很多时候写代码会忘记包一下,或者无法判断是不是该有个前置判断。
当然这个方案我个人是更倾向下面这种写法:
// 自己封装的方法
import run from 'utils';
// 如果是浏览器,则执行回调
const val = run.browser(() => {
window;
return 1;
});
// 如果是服务端,则执行回调
const val = run.server(() => {
require('path');
return 1;
});
这样的代码更声明式
一些,也可以统一收口。
如果在浏览器未指定 type
属性的 script
标签中,声明名称未 window
的变量,会抛出变量已经定义的异常。
但是在 ESM
中是不存在这个问题的。
<script>
const window = 1;
console.log(window); // throw error
</script>
<script type="module">
const window = 1;
console.log(window); // 1
</script>
利用这个特性,就可以自己封装一个window:
const _window = globalThis;
export const window = new Proxy({}, {
get(target, key) {
if (isBrowser) {
return _window[key];
}
return null;
},
});
然后再所有调用window的地方,都使用可选链:
import { window } from '@/const/window';
console.log(window?.location.href);
这样在服务端执行的时候,发现不是浏览器,就会返回none,同时因为可选链的关系,就不会出问题。
然后在工程层面考虑,没有必要引用 window,在webpack或者其他工具的时候自动import就好了,同时window.
也可以在构建阶段分析出来,也可以在parser
阶段(babel)改成window?.
。
这个是我想的心智负担比较低的一个方案。