14

ISOMORPHIC 的升级之路 – ThoughtWorks洞见

 4 years ago
source link: https://insights.thoughtworks.cn/isomorphic/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ISOMORPHIC 的升级之路

近些年来,史诗级网游 Web Online 中,一个新兴职业 —— Isomorphic JavaScript Application —— 越来越多地得到了玩家的青睐。

Web Online 是一款由地球 Online 玩家基于其游戏平台进行二次开发创作出的网络游戏,特点在于可以自由创建 PC 与 NPC,不过部分地图对于 NPC 的创建管控较严格。主要游戏方式为玩家控制其 PC 与他人的 NPC 聊天以交换情报,也常有自动化的 PC 周期性与 NPC 进行交流。偶尔也有不怀好意的玩家操纵大量 PC 向 NPC 发送大量虚假信息进行攻击。

1.jpeg

(该游戏不需要下载,投递简历到《Web 开发工程师》职位即可立即注册)

不过作为高阶职业,Isomorphic JavaScript Application,或者称为 Universal JavaScript Application(以下简称 UJS),并不能在创号时直接选择,需要满足前置的种族及职业要求,包括但不仅限于:

  • JavaScript Application(种族)
  • Frontend Application(职业)

而修炼职业 UJS 后,能够将相关技能更进一步地提升,以及习得部分新的高阶技能。

虽然目前已经有不少玩家都宣称自己是 UJS 职业,不过角色与角色的水平差异依然十分巨大。为此,本攻略中将对 UJS 的相关技能以及等级划分进行归纳,方便广大玩家参考。

需要特别注意的是,并非任何战斗中都需要用到所有技能的最高等级,在无意义的情况下空大只是单纯地浪费 MP(Mana Points),请根据自己的实际情况选择合适的技能。

这里拟定战斗场景为一个简单的 Tab 切换,效果类似于(在线示例):

1-image1.gif

(示例效果:通过点击修改卡片内容)

职业技能零:浏览器端渲染

该职业由于大部分玩家都已满修,可能不具备研究价值,可根据需要直接跳转到下一技能的攻略内容。

作为 Web 世界的事实专属种族(目前已经一定程度上侵略到其它世界),「浏览器端执行」是 JavaScript 与生俱来的种族技能,为此浏览器端渲染(CSR)也几近标配。

即便如此,鉴于玩家的水平和任务需求,实际使用的技能等级还是具备很大差异:

  • Level 0:无・CSR。应用无法基于静态文件服务器(例如 GitHub Pages)提供。
  • Level 1:伪・CSR。应用获取或生成 HTML 片段,并拼接到页面 HTML 中。
  • Level 2:简・CSR。应用支持全量创建和增量修改页面内容,但需要独立代码实现。
  • Level 3:精・CSR。应用支持全量创建和增量修改页面内容,仅需要一份代码实现。

最早期的 Web 中,所有内容都由服务端页面产生,用户通过超链接访问或者表单提交来进行页面跳转与交互。这时候并不能够进行浏览器端渲染,技能等级为 Level 0。

随着局部刷新需求的出现,部分应用中会通过重置 innerHTML 属性的方式进行内容替换,这样的情况下无需进行整页刷新即可更新内容,使用方式类似于(在线示例):

btn1.addEventListener('click', () => {
  outlet.innerHTML = `
    <div class="card" style="width: 18rem;">
      <div class="card-body">...</div>
    </div>
  `
})

为了保持示例代码的简介,整个示例中将不使用任何外部的 JavaScript 依赖,因此只有 VanillaJS 的使用。界面使用 Bootstrap 的默认主题。

这时的技能等级可以达到 Level 1,虽然名义上是局部刷新,但是重新渲染的计算成本较高,并且代码维护及状态管理困难。

为了降低更新视图内容的成本,应用中往往会引入精确视图操作,类似于(在线示例):

btn1.addEventListener('click', () => {
  outlet.querySelector('.card-title').textContent = 'Button 1 Active'
  outlet.querySelector('.card-text span').textContent = 'foo'
})

这样可以有效避免渲染引擎的额外开销,进入到 Level 2。

为了进一步简化维护成本,屏蔽视图操作,可以对视图层进行抽象,而后基于数据驱动的手段来触发视图更新,类似于(在线示例):

<div class="card" style="width: 20rem;">
  <div class="card-body">
    <h5 class="card-title">{{ title }}</h5>
    ...
    <p class="card-text">Some quick example text to build on the card: {{ keyword }}.</p>
    ...
  </div>
</div>

为了避免在示例中引入编译过程造成不必要的开销,示例的源码中直接手写了编译后的模版的类似代码。

这样,视图的创建和更新过程得到了统一,并且由于静态内容与动态数据分离,有效降低了应用维护成本,达到了 Level 3。

职业技能一:服务器端渲染

服务端渲染(SSR)大体可以分为三个等级:

  • Level 0:无・SSR。服务端返回空白页面,在浏览器端渲染应用视图。
  • Level 1:伪・SSR。服务端返回 App Shell 级别的应用骨架,在浏览器端渲染实际内容。
  • Level 2:准・SSR。服务端返回用户无关的公共内容,在浏览器端补全用户相关内容。
  • Level 3:真・SSR。服务端返回基于用户的完整真实内容。

前端模版可以分为两种类型:渲染前模版与渲染后模版。(这里只考虑在浏览器中处理的模版,所有在非浏览器中的预处理操作不在此列)

  • 对于渲染前模版,模版自身不会被浏览器渲染,而是被模版引擎直接作为文本处理。因此模版内容任何情况都不会暴露到页面中。典型代表包括 Angular(JIT)、Vue;
  • 对于渲染后模版,模版自身会被浏览器端当作内容渲染,而后模版引擎基于由模版生成的 DOM 树进行后续操作。如果模版引擎出现错误,模版内容可能被暴露给用户。典型代表包括 AngularJS、Vue。

所有手写 Virtual DOM 的场景在机制上等价于渲染前模版,部分视图框架(库)支持多种模式。

对于渲染前模版,由于自身并不被浏览器渲染,因此在 JavaScript 执行前无法提供任何内容,以至于初始等级即为 Level 0。不过即便对于渲染后模版,玩家也基本会为了一致性与安全性,通过样式手动屏蔽模版内容(xxx-cloak),从而主动降级至 Level 0。

示例中假设服务端提供的初始状态为按钮 B 当前活跃,并且已经切换了两次。

Level 0 中的首屏渲染效果如下(在线示例):

2-image2.jpg

(SSR lv.0 (无 SSR) 效果,示例中的导航栏仅用于示例间的跳转,可以视作应用外内容)

这里 SSR 并没有提供任何内容,或者说根本没有 SSR。在没有 CSR(例如 Disable JavaScript)的情况下不具备任何价值。

为了避免完全的白屏,我们可以让 SSR 提供无意义的内容,例如线框图,首屏效果如下(在线示例):

3-image8.jpg

(SSR lv.1 (伪 SSR) 效果,假装这些方块是线框图)

这时首屏并不是空白,而是一个向用户表明「渲染中」状态的过渡视图。这样就提升到了 Level 1,伪 SSR。之所以仍然是「伪」,是因为这里的 SSR 自身不交付任何价值,仅仅作为 CSR 的附属(提供页面渲染过程加速的表象),所以只能算作 CSR+,而非 SSR。

为了让 SSR 能够交付价值,需要在首屏内容中提供实际内容。一个简单的方案是服务端永远作为匿名用户(或者登录中),仅用于产生公共内容,类似于(在线示例):

4-image5.jpg

(SSR lv.2 (准 SSR) 效果)

这时已经非常接近最终视图,唯一的区别是「Hi, anonymous user」。由于服务端并不处理用户信息,仅仅提供公共可用内容,所以需要在 CSR 阶段将「anonymous user」替换为实际用户名,才算完成视图的实际渲染。

Level 2 已经能够在 SSR 中直接交付价值,即便 CSR 失败或者被禁用,用户仍然可以完成对基本内容的浏览。并且由于内容与用户无关,仍然不需要在服务器端进行计算过程(非实时数据敏感的页面),可以在构建时完成全部操作,或者使用基于 API 事件的动态构建策略并缓存。

当然,如果有需要,也能在服务器端直接提供最终视图,效果类似于(在线示例):

5-image3.jpg

(SSR lv.3 (真 SSR) 效果)

为了能够在不使用 CSR 的情况下也能得到最终视图,需要在服务器端进行身份认证(一般基于 Cookie),并且根据用户身份返回专有内容,这时候需要在服务器端进行实时渲染(每个用户的首次访问),可能会占用一定的计算资源。

不过,Level 3 的 SSR 也能提供最为全面的内容交付能力,只要语义化标签使用合理,即便在 Disable JavaScript 的条件下,用户依然能够完成应用的业务流程。

职业技能二:状态过渡

已经单独修炼了 CSR 和 SSR 技能之后,不过要将两者有机结合仍然需要额外的技能。状态过渡大体可以分为三个等级:

  • Level 0:Rebuild。销毁 SSR 产生的全部内容,由 CSR 重新创建内容。
  • Level 1:Rehydrate。在可能的情况下复用 SSR 的视图节点,不再重新创建。
  • Level 2:Resume。集成 SSR 的应用状态,不再重复初始化过程(例如 API 请求)。
  • Level 3:Replay。将 CSR 发生之前的用户交互过程反映到 CSR 结果。

融合 SSR 与 CSR 的最简单方案是:移除所有 SSR 的内容,然后以全新的面貌进行 CSR。这时候如果内容较多,可能会造成页面的卡顿或者闪烁,并且如果存在当前选中文本内容也会被一并清除,效果类似于(在线示例):

6-image7.gif

(Transition lv.0 (Rebuild) 效果)

Level 0 虽然操作简单,但容易对用户造成明显的不适感。

为了避免页面节点的重绘,可以在 CSR 过程中复用 SSR 结果中已经存在的元素节点,能够一定程度上优化过渡效果(在线示例):

7-image6.gif

(Transition lv.1 (Rehydrate) 效果)

到了 Level 1 阶段,由于视图节点得到了复用,过渡的体验得到了一定的提升(比如文本选中状态得到了保留)。

重用视图节点的过程一般称为 Hydration,其中会对普通的视图节点进行一定的预处理,以便于运行时类库的使用。不过本示例中其实并没有实际的处理过程。

不过,很容易发现,当前的 CSR 过程虽然复用了节点,仍然没有复用 SSR 中的应用状态,而是重新初始化了应用。为了实现状态复用,可以在 SSR 过程中将应用状态进行序列化,而后由 CSR 过程读取:

<script id="application-state" type="application/json">
{
  "title": "Button 2 Active",
  "count": 2
}
</script>

而后 CSR 中便可复用 SSR 的应用状态,类似于(在线示例):

8-image9.gif

(Transition lv.2 (Resume) 效果)

到了 Level 2 之后,不仅节点保持不变,而且应用状态也被保持,基本对用户透明。

不过还有一个问题是,在 CSR 开始之前,如果用户尝试对应用进行交互,那么其操作会被无视,类似于:

9-image4.gif

(Transition lv.2 (Resume) 效果)

为了能够允许在 CSR 发生前进行一定程度的交互,需要进行事件重放。简单来说需要预先引入一个极小的运行时(原则上应当放在 <head> 内,必要情况下可以内嵌),基于事件代理来记录相关事件,形如:

export function record(eventNames) {
  for (const eventName of eventNames) {
    document.addEventListener(eventName, (event) => {
      list.push({ name: eventName, element: event.target })
    })
  }
}

实践中应当基于应用根节点而非文档节点,以免记录到应用外部事件。

之后就可以在 CSR 启动完成后重放相关事件:

export function replay() {
  for (const { name: eventName, element } of list) {
    element.dispatchEvent(new Event(eventName))
  }
}

接着就能升级到 Level 3,在 CSR 开始前的交互过程也能够被记录(在线示例):

10-image10.gif

(Transition lv.3 (Replay) 效果)

实际应用中,交互与交互之间可能是相互影响的,为此需要定义 Critical Events,例如:

  • Critical Events:button->click,input->focusout;
  • Non-Critical Events:input->input,*->mouseenter;

Non-Critical Events 允许连续发生,而一旦出现 Critical Events 则立刻阻止用户后续交互(例如弹出遮罩层),直到 CSR 启动并且响应 Critical Events 之后,再重新开放交互,从而避免不预期的应用状态。

本攻略主要讲解了 Isomorphic 基本技能的概念以及强度设定,具体实践中可能还有其他不同的技能效果和考量维度,部分经验丰富的玩家甚至能够创造自己的专属技能。

虽然看攻略会一定程度上减少探索的乐趣,不过迫于生活的压力可能更看重通关效率。不论哪个种族哪种职业,打怪升级之路都绝非一帆风顺。不过在了解攻略后,是否对这个职业的角色更感兴趣了呢?


更多精彩洞见,请关注微信公众号:ThoughtWorks洞见


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK