vue-hackernews-2.0 源码解读(适合入门)(二)

书接上回

我们看下 entry-server.js 主要做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import { createApp } from "./app";

const isDev = process.env.NODE_ENV !== "production";

//此导出的函数将由`bundleRenderer`调用。
//这是我们执行数据预取以确定
//实际渲染应用程序之前的状态。
//由于数据获取是异步的,因此该函数有望
//返回解析为应用实例的Promise。

// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default (context) => {
return new Promise((resolve, reject) => {
const s = isDev && Date.now();
const { app, router, store } = createApp();

const { url } = context;
const { fullPath } = router.resolve(url).route;

if (fullPath !== url) {
return reject({ url: fullPath });
}

//设置路由位置
// set router's location
router.push(url);

//等待路由可能的异步钩子
// wait until router has resolved possible async hooks
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();
// no matched routes
if (!matchedComponents.length) {
return reject({ code: 404 });
}
//路由上匹配的组件唤醒fetchData钩子
//一个预置钩子匹配一个状态返回promise
//当解析完成且状态更新
// Call fetchData hooks on components matched by the route.
// A preFetch hook dispatches a store action and returns a Promise,
// which is resolved when the action is complete and store state has been
// updated.
// 使用Promise.all执行匹配到的Component的asyncData方法,即预取数据
Promise.all(
matchedComponents.map(
({ asyncData }) =>
asyncData &&
asyncData({
store,
route: router.currentRoute,
})
)
)
.then(() => {
isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`);
//解析了所有prefetch钩子后,渲染app所需要的state充满了store,

//在渲染上下文中公开状态,并让请求处理程序
//内联HTML响应中的状态。这允许客户端
//存储以获取服务器端状态,而不必重复
//在客户端上获取初始数据。

// After all preFetch hooks are resolved, our store is now
// filled with the state needed to render the app.
// Expose the state on the render context, and let the request handler
// inline the state in the HTML response. This allows the client-side
// store to pick-up the server-side state without having to duplicate
// the initial data fetching on the client.

// 把vuex的state设置到传入的context.initialState上
context.state = store.state;
// 返回state, router已经设置好的Vue实例app
resolve(app);
})
.catch(reject);
}, reject);
});
};

entry-server.js 的主要工作:

0.返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,

将 vue 实例通过 Promise 返回。 context 一般包含 当前页面的 url。

1.手动路由切换到请求的 url,即’/‘

2.找到该路由对应要渲染的组件,并调用组件的 asyncData 方法来预取数据

3.同步 vuex 的 state 数据至传入的 context.initialState 上,

后面会把这些数据直接发送到浏览器端与客户端的 vue 实例进行数据(状态)同步,

以避免客户端首屏重新加载数据(在客户端入口文件 entry-client.js)

还记得 index.template.html 被设置到 template 属性中吗?

此时 Vue 渲染器内部就会将 Vue 实例渲染进我们传入的这个 html 模板,那么 Vue render 内部是如何知道把 Vue 实例插入到模板的什么位置呢?

1
2
3
<body>
<!--vue-ssr-outlet-->
</body>

就是这里,这个<!--vue-ssr-outlet-->Vue 渲染器就是根据这个自动替换插入,所以这是个固定的 placeholder。

如果改动,服务端渲染时会有错误提示:Error: Content placeholder not found in template.

接下来,Vue 渲染器会回调 callback 方法,我们回到 server.js

1
2
3
4
5
6
7
8
function render (req, res) {

···
renderer.renderToString(context, (err, html) => {
res.end(html)
···
})
}

此时只需要将渲染好的 html 写入 http 响应体就结束了,浏览器客户端就可以看到页面了。

接下来我们看看服务端数据预取的实现

服务端渲染时的数据预取流程

上文提到,服务端渲染时,会手动将路由导航到请求地址即’/‘下,然后调用该路由组件的 asyncData 方法来预取数据

那么我们看看路由配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// /src/router/index.js
Vue.use(Router);
// route-level code splitting
const createListView = (id) => () =>
System.import("../views/CreateListView").then((m) => m.default(id));
const ItemView = () => System.import("../views/ItemView.vue");
const UserView = () => System.import("../views/UserView.vue");
export function createRouter() {
return new Router({
mode: "history",
scrollBehavior: () => ({ y: 0 }),
routes: [
{ path: "/top/:page(\\d+)?", component: createListView("top") },
{ path: "/new/:page(\\d+)?", component: createListView("new") },
{ path: "/show/:page(\\d+)?", component: createListView("show") },
{ path: "/ask/:page(\\d+)?", component: createListView("ask") },
{ path: "/job/:page(\\d+)?", component: createListView("job") },
{ path: "/item/:id(\\d+)", component: ItemView },
{ path: "/user/:id", component: UserView },
{ path: "/", redirect: "/top" },
],
});
}

地址’/‘是做了 redirect 到’/top’,其实就是默认地址就是到 top 页面,在看第一条路由配置,’/top’路由对应的组件是 createListView(‘top’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// /src/views/CreateListView.js
import ItemList from "./ItemList.vue";

const camelize = (str) => str.charAt(0).toUpperCase() + str.slice(1);

// This is a factory function for dynamically creating root-level list views,
// since they share most of the logic except for the type of items to display.
// They are essentially higher order components wrapping ItemList.vue.
export default function createListView(type) {
return {
name: `${type}-stories-view`,
//从store中取值
asyncData({ store }) {
return store.dispatch("FETCH_LIST_DATA", { type });
},

title: camelize(type),
//创建itemlist的节点,渲染节点
render(h) {
return h(ItemList, { props: { type } });
},
};
}

Vuex state 状态变更流程

asyncData 方法被调用,通过 store.dispatch 分发了一个数据预取的事件,接下来我们可以看到通过 FireBase 的 API 获取到 Top 分类的数据,然后又做了一系列的内部事件分发,保存数据状态到 Vuex store,获取 Top 页面的 List 子项数据,最后处理并保存数据到 store.

最后数据就都保存在 store 这里了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import Vue from "vue";
import Vuex from "vuex";
import actions from "./actions";
import mutations from "./mutations";
import getters from "./getters";

Vue.use(Vuex);

export function createStore() {
return new Vuex.Store({
state: {
activeType: null,
itemsPerPage: 20,
items: {
/* [id: number]: Item */
},
users: {
/* [id: string]: User */
},
lists: {
top: [
/* number */
],
new: [],
show: [],
ask: [],
job: [],
},
},
actions,
mutations,
getters,
});
}

然后将开始通过 Render 函数创建 HTML

1
2
3
4
5
// /src/views/CreateListView.js
render (h) {
console.log(`createListView render`)
return h(ItemList, { props: { type }})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /src/views/ItemList.vue
···
<template>
<div class="news-view">
<div class="news-list-nav">
<router-link v-if="page > 1" :to="'/' + type + '/' + (page - 1)">< prev</router-link>
<a v-else class="disabled">< prev</a>
<span>{{ page }}/{{ maxPage }}</span>
<router-link v-if="hasMore" :to="'/' + type + '/' + (page + 1)">more ></router-link>
<a v-else class="disabled">more ></a>
</div>
<transition :name="transition">
<div class="news-list" :key="displayedPage" v-if="displayedPage > 0">
<transition-group tag="ul" name="item">
<item v-for="item in displayedItems" :key="item.id" :item="item">
</item>
</transition-group>
</div>
</transition>
</div>
</template>
···

这样创建完 HTML Body 部分,前面提到的 Vue 渲染器会自动把这部分内容插入 index.template.html 中,替换对应的,然后就又回到前面的流程了,server.js 将整个 html 写入 http 响应体,浏览器就得到了整个 html 页面,整个首次访问过程完成。

暂时先这样