hi ARAM

如何在SSR项目中减少访问window的心智负担

2023/02/12

SSR项目其实是比较特殊的,服务端和客户端都会执行主要逻辑,这个在其他的场景下很少见。

因为宿主环境的不同,就会有很多不一致的特性。

最近发开中发现项目中的window不好控制,经常存在服务端访问的情况,也就是NODE环境下访问 window 会异常。

因为这个点思考了几个减少这层面心智负担的方式。

globalThis

一般来讲服务端如果访问了 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;
});

这样的代码更声明式一些,也可以统一收口。

使用Proxy包装window

如果在浏览器未指定 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?.

这个是我想的心智负担比较低的一个方案。


上一篇:站点怎么去掉URL中.html资源后缀的
下一篇:关于在前端发起HTTP请求的思考