服务端渲染改造
在编写好正常的客户端渲染逻辑代码后,就可以针对项目的首屏开启服务端渲染改造了。在本项目中,主要是针对 home 页面的改造。主要步骤概括如下:
-
基于服务端渲染逻辑和客户端渲染逻辑改造 main.js。
-
跑通正常的客户端渲染开发和生产构建流程。
-
创建 Node.js 服务端 server.js 逻辑,结合 Vite 跑通基于服务端渲染的开发流程。
-
改造 Node.js 服务端 server.js 逻辑,跑通服务端渲染生产构建流程。
-
配置 package.json 中定义的命令,以完成改造。
main.js改造
修改 main.js,使其同时支持服务端和客户端两种渲染逻辑,改造代码如下:
...
export function createApp() {
// 如果使用服务端渲染,需要将createApp替换为createSSRApp
const app = createSSRApp(App)
// 路由
const router = createRouter()
// store
const store = createStore()
app.use(router)
app.use(store)
// 将根实例以及路由暴露给调用者
return { app, router, store }
}
entry-client.js和entry-server.js
entry-client.js 和 entry-server.js 这两个文件分别作为客户端渲染和服务端渲染的入口,在后面的 server.js 中会被调用,其主要内容和10.2节一致,这里单独讲一下需要后端利用 Vuex 获取数据的逻辑,其区别如下:
entry-server.js
...
export async function render(url, manifest) {
const { app, router, store } = createApp()
// 设置默认的路由,/默认走home路由
router.push(url)
// 等待路由加载完成
await router.isReady()
// 获取首屏需要的异步数据store
await getAsyncData(router,store, true)
// store中已经存放了数据 提供渲染出html字符串
const ctx = {}
ctx.state = store.state
const html = await renderToString(app, ctx)
//处理需要预加载的链接
const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
return [html, preloadLinks, store]
}
上述代码中用到的 getAsyncData 方法需要在 home 组件中配置
entry-client.js:
if(window.__INIT_STATE__) {
// 当使⽤ template 时, context.state 将作为 window.__INIT_STATE__ 状态⾃动嵌⼊到最终的 HTML
// 在客户端挂载到应⽤程序之前, store 就应该获取到状态:
store.replaceState(window.__INIT_STATE__._state.data)
}
服务端获取的数据会注入 HTML 模板中挂在 window.INIT_STATE 对象中,在这里可以直接拿到,这样在浏览器端二次渲染时直接使用即可。
home.vue改造
在 home.vue 中,主要的改造点是:提供静态 asyncData 方法获取首屏数据,改造最近热映、正在热映、榜单三个模块对应的 nowplayList、recentplayList、rankList 数据放入 Vuex 的 store 中,其核心代码如下:
export default {
// 定义静态方法
asyncData({store}) {
return store.dispatch('getHomeMovieData')
},
...
setup(){
const store = useStore()
// 从store获取数据
let nowplayList = computed(()=> store.state.nowplayList)
let recentplayList = computed(()=> store.state.recentplayList)
let rankList = computed(()=> store.state.rankList)
return {
nowplayList,
recentplayList,
rankList
}
}
}
由于 asyncData 方法只会在服务端渲染时使用,和本书的组件并无太多逻辑关系,因此定义成静态方法更合适,对应的 store 页要修改部分逻辑。
store改造
新建一个 action,用来异步请求 home 页面所需要的数据,其核心代码如下:
actions: {
async getHomeMovieData(context, obj){
// 请求正在热映数据
let nowplayList = await service.get(configapi.nowmovie,{
start:0,
count:50,
})
// 请求最近热映数据
let recentplayList = await service.get(configapi.recentmovie,{
start:0,
count:50,
})
// 请求榜单映数据
let rankList = await service.get(configapi.toprank,{
start:0,
count:10,
})
context.commit('setHomeData',{
nowplayList,
recentplayList,
rankList
})
}
}
然后创建对应的 mutations 对数据进行处理,其核心代码如下:
/*
* 设置首屏数据
*/
setHomeData(state, obj){
state.rankList = obj.rankList.subject_collection_items || [];
state.nowplayList = obj.nowplayList.subject_collection_items || [];
// 将数据进行分组
let toArray = (data)=>{
let n = 10 // 10个一组
let len = data.length
let num = len % n == 0 ? len/n : Math.floor(len/n)+1
let res = []
for (var i = 0 ; i < num ; i++) {
res.push(data.slice(i*n,i*n+n))
}
return res
}
state.recentplayList = toArray(obj.recentplayList.subject_collection_items || []);
},
这些改造相当于将原来在 home.vue 的逻辑挪到了 actions 和 mutations 中,为服务端渲染提供支持。
在 store 文件下新建 getAsyncData.js,提供给 server.js 调用,其核心代码如下:
// 调用当前匹配到的组件中的asyncData钩子,预取数据
export const prefetchData = (
components,
router,
store
) => {
// 过滤出有asyncData静态方法的组件
const asyncDatas = components.filter(
(i) => typeof i.asyncData === "function"
);
return Promise.all(
asyncDatas.map((i) => {
return i.asyncData({ router: router.currentRoute.value, store });
})
);
};
// ssr自定义钩子
export const getAsyncData = (
router,
store,
isServer
) => {
return new Promise(async (resolve) => {
const { matched } = router.currentRoute.value;
// 当前路由匹配到的组件
const components = matched.map((i) => {
return i.components.default;
});
// 如果有Vuex的modules,可以选择动态注册modules
// registerModules(components, router, store);
if (isServer) {
// 预取数据
await prefetchData(components, router, store);
}
resolve();
});
};
其中,如果 registerModules 方法使用的是 Vuex 的 modules,并且需要动态添加时可以调用 registerModules,这里没有使用,就不调用了。getAsyncData.js 主要是一个桥梁,提供给 server.js 可以调用组件和 store 的能力,为了服务端获取数据服务。
server.js改造
服务端渲染的核心能力是利用 Node.js 提供渲染首屏 HTML 的服务,所以可以利用 Express 框架来开启一个 Node.js 服务,需要安装 Express:
cnpm i express -S
在 index.html 同级创建 server.js,其内容和10.2节类似,主要改动如下代码:
// 调用entry-server.js的render方法
const [appHtml, preloadLinks, store] = await render(url, manifest)
// 将服务端获取的数据注入到html页面中
const state = ("<script>window.__INIT_STATE__" + "=" + serialize(store, { isJSON: true }) + "</script>");
// 组装html
const html = template
.replace(`<!--preload-links-->`, preloadLinks)
.replace(`<!--app-html-->`, appHtml)
.replace(`<!--app-store-->`, state)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
上面的代码中,和之前的区别主要是调用 entry-server.js 方法获取数据,然后将数据注入生成的 html 字符串中,供客户端渲染使用。同时,利用 Express 提供静态资源的服务,主要配置如下:
// index false表示匹配不到静态文件时 不做处理交给后面逻辑
app.use('/js', express.static(resolve('dist/client/js'),{index: false}));
app.use('/css', express.static(resolve('dist/client/css'),{index: false}));
app.use('/assets', express.static(resolve('dist/client/assets'),{index: false}));
app.use('/json', express.static(resolve('dist/client/json'),{index: false}));
app.use('/favicon', express.static(resolve('dist/client/favicon'),{index: false}));
至此,整个服务端改造完成。本次改造的内容基本上是参照之前章节 Vite 服务端渲染章节所介绍的步骤和内容,在此基础上添加利用 Vuex 来获取服务端渲染首屏数据的逻辑,这部分逻辑的整体流程总结如图12-10所示。
