yinguangyao / blog

关于 JavaScript 前端开发、工作经验的一点点总结。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

记一个同构问题

yinguangyao opened this issue · comments

commented

问题描述

ATM BillPayment 上线后,local 在 live 环境发现输入框的日期默认是 07-22 的,于是报了问题。

我看了一下代码,进来页面的时候,我默认设置的是当天的日期,不可能是 07-22 的。在本地跑一下发现是正常的,但是打开 live 后不管怎么刷新页面,日期就是停留在 07.22 这天。

问题定位

于是,我先怀疑这里是否有问题。问了一下 QA,上次发布 live 的日期是不是 07.22,他说是的。

我打开 UAT 环境的看了一下,发现停留在了 07.23,也刚好是那天发布了 UAT。

看起来时间都停留在了发布的那天。

image

第一步,肯定是先翻自己的代码。这段逻辑是放到 vuex 的 state 里面的。

虽然是个闭包,但正常情况下,我们刷新浏览器也会重新计算 date。可是这里没有重新计算。

问题思考

为什么页面上的日期会停留在发布服务的那天呢?

页面上的日期和服务有什么关系呢?

…...

是不是服务端渲染的问题呢?

这就不得不讲一下模块和同构了。

闭包 & 模块

当我们在不同模块中引入同一个模块 m,那么这个 m 被执行了几次呢?

一次?还是引入几次就执行几次?

答案是一次。为什么只执行了一次呢?

如果你用立即执行函数做过简单的模块,那么下面这段代码就豁然开朗。

image

在 webpack 中,加载模块也是进行了缓存,可以查看 webpack 将文件编译之后,他们是如何实现 require 的。

image

很显然,在 webpack 中对第一次引入的模块进行了缓存,后续读取都是从缓存中来的。webpack 中解析模块代码使用的还是 eval

而在 Node 中,CommonJS 的原理也是类似,只是解析代码这块儿是用 C++ 来实现的。这里是源码地址:https://github.com/nodejs/node/blob/a6a3684984/lib/internal/modules/cjs/loader.js

image

这说明了什么呢?我们的 state 模块,不管是被引入多少次,那么都只会被加载一次。

同构

在 Nuxt 中,服务端渲染是通过 vue-server-renderer 的 renderToString 方法来实现的。

一般情况下,SPA 应用对 SEO 不友好,首页加载比较慢,所以才出现了服务端渲染。

但服务端渲染带来的是无法保留 SPA 无刷新切换页面的体验。

而在实际场景下,往往只需要对首页进行服务端渲染,其他页面保持 SPA 的体验,所以有了同构应用的诞生。

同构的意思是,一份代码,既可以跑在浏览器端,又可以跑在服务端。

image

同构的一个核心是在于实现路由同构,比如我们使用 express 作为服务端框架,那么我们需要一份代码来将 express-router 和 react-router/vue-router 的差异抹平。

当我们编写了一份 router 文件,这份文件既可以作为 express-router 的配置,也可以作为 react-router/vue-router 的配置。

比如在客户端我们需要根据 router 来拿到 component 和 store 进行客户端渲染,在服务端我们依然要根据 router 来拿到 component 和 store 做服务端渲染。

这一步往往在路由中间件里面去初始化 store,拿到组件,将其渲染成字符串给浏览器。

在 nuxt 中,也是使用了路由中间件的形式。

为什么刷新不生效?

因为在服务启动的时候,就已经加载了 state 文件,后续不管怎么刷新页面,都只是在路由中间件里面去使用初始化模块来创建 Store,这个时候日期肯定就停留在发布的那天了。

这里我写了一个简单的 Demo 来帮助了解:

image

那么如何解决这个问题呢?其实也很简单,我们只需要将日期放到 state 函数里面就行了,每次创建 Store 的时候都会执行 State 函数重新计算。

image

为什么在浏览器刷新生效?

在浏览器端,刷新的时候 JS 文件都会被重新加载,模块也会被重新执行,日期自然是当前最新的日期。而在 Node 端,这些模块都被加载到了内存里面,所以不会重新执行了。

runInNewContext

但是在本地 dev 模式下,即使把日期放到外面,刷新后也会起效。这是因为本地开发模式开启了 runInNewContext

nuxt 会从 nuxt.config.js 里面读取了 runInNewContext 判断是否在新的上下文中运行。

image

这个 runInNewContext 是什么呢?这个 Node 提供的 vm 模块中的一个方法,它可以提供一个 JS 沙盒,允许你在新的上下文中运行代码,从而实现和当前上下文的隔离。有点儿类似于 eval

Nuxt 底层依然使用了 vue-server-renderer 中的 createBundleRenderer 方法,里面有个 runInNewContext 选项,会判断是否创建一个新的上下文去运行。

image

官方的解释是,当访问页面的时候,会判断是否使用 runInNewContext 来执行路由 Bundle 文件(Page、Store 等等),这也是为什么开启了这个选项之后就可以生效。

更新另一个问题

最近在给小课加 SSR 的时候,在 Nuxt 里面还遇到一个问题。
那就是在使用 vue-property-decorator 的时候,组件内部使用 fetch 在服务端获取数据后直出,fetch 方法却一直不执行,最后才发现需要用 nuxt-property-decorator,并且在 nuxt.config.js 的 build 选项里面写上这个:

babel: {
      presets({ isServer }) {
        return [
          [
            "@nuxt/babel-preset-app", { loose: true }
          ]
        ]
      }
    },
commented

感觉平时源码看多了,解决问题的能力也提高了不少。