前文中我们已经搭建了一个最基本的 SRR 框架, 页面还只能有一个页面, 本文我们来继续加路由吧。
本文对应的代码为: 静态路由 commit 和 动态路由 commit
静态路由改造
前文中我们提到 SSR 的后台 bundle 需要为每个请求去创建一个新的 vue app 实例以防止数据错乱。对于路由道理也是一样的, SSR 版的路由也需要由工厂函数来创建。
-
我们把一般的 Vue 项目的路由像前文的 app 一样做个工厂化的改造:
// router/index.js import Vue from 'vue' import Router from 'vue-router' import Home from '../views/Home.vue' import Test from '../views/Test.vue' Vue.use(Router) + export function createRouter() { return new Router({ mode: 'history', routes: [ { path: '/home', component: Home }, { path: '/test', component: Test }, ] }) }
-
接着修改 app.js
import Vue from 'vue' import App from './App.vue' + import { createRouter } from './router/index' // 导出一个工厂函数,用于创建新的 // 应用程序、router 和 store 实例 export function createApp () { // create the router instance + const router = createRouter() const app = new Vue({ // 根实例简单的渲染应用程序组件。 + router, render: h => h(App) }) + return { app, router } }
-
前面2个修改比较简单,
entry-server.js
和server.js
的修改稍微解释下,server bundle 在渲染的时候是需要根据用户请求的路径来渲染的,所以我们要从用户 request 里取出这个路径传给 server bundle。// server.js // *** 省略的代码 server.get('*', (req, res) => { const app = createApp({ + url: req.url // node server 拿到请求的url并传个 app 的工厂函数 }) // render 壳子 html, app 里是带了路由信息的 renderer.renderToString(app, context, (err, html) => { // *** 省略的代码 }) // *** 省略的代码 })
// entry-server.js import { createApp } from './app' export default context => { const { app, router } = createApp() + router.push(context.url) // 路由信息设置到 app 里去 return app }
entry-server
在生成 app 时, 使用用户的访问的url
做了路由导航,renderer
渲染的出来的壳子页面也就是用户访问的那个了。
完整代码在这个commit 上,如果需要请读者自行尝试。
动态路由
SSR 的动态路由,和非 SSR 版本基本类似。
-
首先添加
syntax-dynamic-import
plugin 来支持动态导入的语法。// .babelrc { "plugins": [ "syntax-dynamic-import" ] }
- 修改 router 文件使用动态导入
// router/index.js - import Home from '../views/Home.vue' - import Test from '../views/Test.vue' + const Home = () => import('../views/Home.vue') + const Test = () => import('../views/Test.vue')
-
采用动态 router 后, 在后台渲染 app 和 前台
mount
app 前, 我们都需要等待动态组件的加载, 即等待router.onReady
的触发。(因为我们需要等待组件的异步加载好)// client-bundle.js import { createApp } from './app' const { app, router } = createApp() + router.onReady(() => { app.$mount('#app') })
前台 bundle 改动较小,等待 onReady 即可.
// server-bundle.js import { createApp } from './app' + export default context => { + return new Promise((resolve, reject) => { const { app, router } = createApp() router.push(context.url) + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + // 匹配不到的路由,执行 reject 函数,并返回 404 + if (!matchedComponents.length) { + reject({ code: 404 }) + } + // Promise 应该 resolve 应用程序实例,以便它可以渲染 + resolve(app) + }, reject) }) }
后台 bundle 的工厂函数的返回值变成了一个 Promise, 连带反应就是 Server.js 也需要修改。
// server.js - const app = createApp({ - url: req.url - }) + createApp({ + url: req.url + }).then(app => { + // *** + })
完整的代码,参看 commit.
总结
SSR 的 router 接入总体还是挺容易实现和理解的,和非 SSR 版本的差别不大,就不多做解释了。
系列文章: