前言
HackerNews是基于 HN 的官方 firebase API 、Vue 2.0 、vue-router 和 vuex 来构建的,使用服务器端渲染。
vue-hackernews项目,涉及知识点及技术栈非常全面,对于初学者来说,直接阅读该项目,极具挑战。这也是写这个项目解读的初衷,希望为阅读该项目提供一些指引。
结构概览
项目结构图上显示,有两个入口文件,entry-server.js 和 entry-client.js, 分别是服务端渲染和客户端渲染的实现入口,webpack 将两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle.
服务端:当 Node Server 收到来自Browser的请求后,会创建一个 Vue 渲染器 BundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件(即entry-server.js),并且执行它,而 server bundle 实现了数据预取并返回已填充数据的Vue实例,接下来Vue渲染器内部就会将 Vue 实例渲染进 html 模板,最后把这个完整的html发送到浏览器。
客户端:Browser收到HTML后,客户端加载了 client bundle(即entry-client.js) ,通过app.$mount('#app')
挂载Vue实例到服务端渲染的 DOM 上,并会和服务端渲染的HTML 进行Hydration(合并)
目录概览
|
|
本项目包含开发环境及生产环境,我们先学习开发环境。
开发环境的服务端渲染流程
让我们从node环境下执行命令开始。
然后发生了什么?我们来看一张图。
Tips:package.json解读
上述执行dev属性对应的脚本:node server
即node server.js
,即执行server.js
|
|
Tips
1.vue-server-renderer(Vue服务端渲染,同时支持prefetch、prerender特性)
2.webpack-dev-server(webpack-dev-middleware/webpack-hot-middleware)
3.此项目全面使用ES6语法,包括箭头函数,解构赋值,Promise等特性。
server.js
最终监听8080端口等待处理客户端请求,此时在浏览器访问localhost:8080
请求经由express路由接收后,执行处理逻辑:readyPromise.then(() => render(req, res))
沿着Promise的调用链处理:
开发环境下
1.调用setup-dev-server.js 模块,根据上图中webpack config文件实现入口文件打包,热替换功能实现。
最终通过回调把生成在内存中的server bundle传回。
2.创建渲染器,绑定server bundle,设置渲染模板,缓存等
3.依次装载一系列Express中间件,用来处理静态资源,数据压缩等
4.最后将渲染好的HTML写入http响应体,传回浏览器。
接下来分解解读下这几个的实现。
setup-dev-server
看一张server.js的模块依赖关系图,只看项目自文件依赖即可(黄色)
build/setup-dev-server.js
|
|
build/webpack.base.config.js
|
|
build/webpack.client.config.js
|
|
bulid/webpack.server.config.js
|
|
如上,基于 webpack config 的setup-dev-server
就到这里,接下来说创建渲染器
。
创建渲染器
|
|
创建渲染器时重点两件事:
1.绑定渲染用的server bundle至渲染器,这个bundle是在setup-dev-server.js中将服务端入口文件entry-server.js
打包生成的。
当渲染器调用renderer.renderToString
开始渲染时,会执行该入口文件的默认方法。
2.传入了一个html模板index.template.html
,这个模板稍后在服务端渲染时就会动态填充预取数据到模板中。
顺着readyPromise.then的调用链,接下来调用render方法
renderer.renderToString
方法内部会先调用入口模块entry-server.js
的默认方法,我们看下entry-server.js
主要做了什么
|
|
entry-server.js
的主要工作:
0.返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例通过 Promise 返回。 context 一般包含 当前页面的url。
1.手动路由切换到请求的url,即’/‘
2.找到该路由对应要渲染的组件,并调用组件的asyncData方法来预取数据
3.同步vuex的state数据至传入的context.initialState上,后面会把这些数据直接发送到浏览器端与客户端的vue 实例进行数据(状态)同步,以避免客户端首屏重新加载数据(在客户端入口文件entry-client.js)
Tips:下一章节我们会详细介绍这部分内容实现 稍后见于:
服务端渲染时的数据预取流程
还记得index.template.html
被设置到template
属性中吗?
此时Vue渲染器内部就会将Vue实例渲染进我们传入的这个html模板,那么Vue render内部是如何知道把Vue实例插入到模板的什么位置呢?
就是这里,这个<!--vue-ssr-outlet-->
Vue渲染器就是根据这个自动替换插入,所以这是个固定的placeholder。
如果改动,服务端渲染时会有错误提示:Error: Content placeholder not found in template.
接下来,Vue渲染器会回调callback方法,我们回到server.js
此时只需要将渲染好的html
写入http响应体就结束了,浏览器客户端就可以看到页面了。
接下来我们看看服务端数据预取的实现
服务端渲染时的数据预取流程
上文提到,服务端渲染时,会手动将路由导航到请求地址即'/'
下,然后调用该路由组件的asyncData方法来预取数据
那么我们看看路由配置
|
|
地址'/'
是做了redirect到'/top'
,其实就是默认地址就是到top页面,在看第一条路由配置,'/top'
路由对应的组件是createListView('top')
|
|
Tips: Vuex状态管理
1.dispatch对应Action,commit对应mutation
2.Action 类似于 mutation,不同在于:Action是异步事件,mutation是同步事件。
Vuex state状态变更流程
asyncData方法被调用,通过store.dispatch分发了一个数据预取的事件,接下来我们可以看到通过FireBase的API获取到Top分类的数据,然后又做了一系列的内部事件分发,保存数据状态到Vuex store,获取Top页面的List子项数据,最后处理并保存数据到store.
最后数据就都保存在store这里了。
然后将开始通过Render 函数创建HTML。
|
|
|
|
这样创建完HTML Body部分,前面提到的Vue渲染器会自动把这部分内容插入index.template.html中,替换对应的<!--vue-ssr-outlet-->
,然后就又回到前面的流程了,server.js将整个html写入http响应体,浏览器就得到了整个html页面,整个首次访问过程完成。
Tips:
后续更新内容规划:
1.生产环境下的服务端渲染逻辑流程
2.客户端渲染逻辑流程
3.客户端vue组件细节解读