注意:在服务端渲染环境下,执行renderToString的时候,组件会被实例化,并且返回字符串形式的dom,这个过程react组件的生命周期只会执行到willmount之后的render。
3)我们写好一个html文件,大致如下。 当前已经渲染出了相应的节点字符串,后端需要返回html文本,内容应该包含标题,节点和最后需要加载的打包好的js,依次去替换html占位部分。
index.html
server/router.js
4)最后客户端js加载完成后,会运行react,并且执行同构方法ReactDOM.hydrate,而不是平时用的ReactDOM.render。
以下是首次渲染过程大致流程图,点击查看大图
css处理
现在我们已经完成了最核心的逻辑,但是有一个问题。 我发现在后端渲染组件的时候,style-loader会报错,style-loader会找到组件依赖的css,并在组件加载时,把style载入到html header中,但是我们在服务端渲染的时候,没有window对象,因此style-loader内部代码会报错。 服务端webpack需要移除style-loader,用其他方法代替,后来我把样式赋值给组件静态变量,然后通过服务端渲染一并返回给前端,但是有个问题,我只能拿到当前组件的样式,子组件的样式没办法拿到,如果要给子组件再添加静态方法,再想办法去取,那就太麻烦了。 后来我找到了一个库isomorphic-style-loader可以支持我们想要的功能,看了下它的源码和使用方法,通过高阶函数把样式赋值给组件,然后利用react的Context,拿到当前需要渲染的所有组件的样式,最后把style插入到html中,这样解决了子组件样式无法导入的问题。 但是我觉得有点麻烦,首先需要定义所有组件的高阶函数和引入这个库,然后在router之中需要写相关代码收集style,最后插入到html中。 后来我定义了一个ProcessSsrStyle方法,入参是style文件,逻辑是判断环境,如果是服务端把style加载到当前组件的dom中,如果是客户端就不处理(因为客户端有style-loader)。 实现和使用非常简单,如下:
ProcessSsrStyle.js
使用:
服务端返回html的内容如下,用户马上能够看到完整的页面样式,而当客户端react同构完成后,dom会被替换为纯dom,因为ProcessSsrStyle方法在客户端不会输出style,最终style-loader执行后header中也会有样式,,页面不会出现不一致的变化,对于用户来说这一切都是无感的。
至此,最核心的功能已经实现,但是在后来的开发中,我发现事情还并没有那么简单,因为开发环境似乎太不友好了,开发效率低,需要手动重启。
开发环境
先说说最初的开发环境如何工作:
npm run dev启动开发环境
webpack.client-dev.js打包服务端代码,代码会被打包到dist/server中
webpack.server-dev.js打包客户端代码,代码会被打包到dist/client中
启动服务端应用,端口9999
启动webpack-dev-server, 端口8888
webpack打包后,启动了两个服务,一个是服务端的app应用、端口为9999,一个是客户端的dev-server、端口为8888,dev-server会监听和打包client代码,可以在客户端代码更新的时候,实时热更新前端代码。 当访问localhost:9999时,server会返回html,我们的server返回的html中的js脚本路径是指向的dev-serve端口的地址,如下图。 也就是说,客户端的程序和服务端的程序被分别打包,并且运行两个不同的端口服务。
在生产环境下,因为不需要dev-server去监听和热更新,因此只一个服务就足够, 如下图,服务端注册静态资源文件夹:
server/app.js