记一个同构问题
yinguangyao opened this issue · comments
问题描述
ATM BillPayment 上线后,local 在 live 环境发现输入框的日期默认是 07-22 的,于是报了问题。
我看了一下代码,进来页面的时候,我默认设置的是当天的日期,不可能是 07-22 的。在本地跑一下发现是正常的,但是打开 live 后不管怎么刷新页面,日期就是停留在 07.22 这天。
问题定位
于是,我先怀疑这里是否有问题。问了一下 QA,上次发布 live 的日期是不是 07.22,他说是的。
我打开 UAT 环境的看了一下,发现停留在了 07.23,也刚好是那天发布了 UAT。
看起来时间都停留在了发布的那天。
第一步,肯定是先翻自己的代码。这段逻辑是放到 vuex 的 state 里面的。
虽然是个闭包,但正常情况下,我们刷新浏览器也会重新计算 date。可是这里没有重新计算。
问题思考
为什么页面上的日期会停留在发布服务的那天呢?
页面上的日期和服务有什么关系呢?
…...
是不是服务端渲染的问题呢?
这就不得不讲一下模块和同构了。
闭包 & 模块
当我们在不同模块中引入同一个模块 m,那么这个 m 被执行了几次呢?
一次?还是引入几次就执行几次?
答案是一次。为什么只执行了一次呢?
如果你用立即执行函数做过简单的模块,那么下面这段代码就豁然开朗。
在 webpack 中,加载模块也是进行了缓存,可以查看 webpack 将文件编译之后,他们是如何实现 require 的。
很显然,在 webpack 中对第一次引入的模块进行了缓存,后续读取都是从缓存中来的。webpack 中解析模块代码使用的还是 eval
。
而在 Node 中,CommonJS 的原理也是类似,只是解析代码这块儿是用 C++ 来实现的。这里是源码地址:https://github.com/nodejs/node/blob/a6a3684984/lib/internal/modules/cjs/loader.js
这说明了什么呢?我们的 state 模块,不管是被引入多少次,那么都只会被加载一次。
同构
在 Nuxt 中,服务端渲染是通过 vue-server-renderer 的 renderToString 方法来实现的。
一般情况下,SPA 应用对 SEO 不友好,首页加载比较慢,所以才出现了服务端渲染。
但服务端渲染带来的是无法保留 SPA 无刷新切换页面的体验。
而在实际场景下,往往只需要对首页进行服务端渲染,其他页面保持 SPA 的体验,所以有了同构应用的诞生。
同构的意思是,一份代码,既可以跑在浏览器端,又可以跑在服务端。
同构的一个核心是在于实现路由同构,比如我们使用 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 来帮助了解:
那么如何解决这个问题呢?其实也很简单,我们只需要将日期放到 state 函数里面就行了,每次创建 Store 的时候都会执行 State 函数重新计算。
为什么在浏览器刷新生效?
在浏览器端,刷新的时候 JS 文件都会被重新加载,模块也会被重新执行,日期自然是当前最新的日期。而在 Node 端,这些模块都被加载到了内存里面,所以不会重新执行了。
runInNewContext
但是在本地 dev 模式下,即使把日期放到外面,刷新后也会起效。这是因为本地开发模式开启了 runInNewContext
。
nuxt 会从 nuxt.config.js 里面读取了 runInNewContext
判断是否在新的上下文中运行。
这个 runInNewContext
是什么呢?这个 Node 提供的 vm 模块中的一个方法,它可以提供一个 JS 沙盒,允许你在新的上下文中运行代码,从而实现和当前上下文的隔离。有点儿类似于 eval
。
Nuxt 底层依然使用了 vue-server-renderer 中的 createBundleRenderer
方法,里面有个 runInNewContext
选项,会判断是否创建一个新的上下文去运行。
官方的解释是,当访问页面的时候,会判断是否使用 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 }
]
]
}
},
感觉平时源码看多了,解决问题的能力也提高了不少。