vue-ssr的实现原理连载(四): vue-ssr中实现路由
上一篇文章基本已经实现了vue-ssr整理流程,接下来让我们一起来实现vue-ssr的路由实现:
首先在上一次项目的src文件下建立router文件。经过前面文章的了解,路由应该也是一个函数,每次调用都返回一个新的路由实例。
src/router.js
import Vue from "vue"; import VueRouter from "vue-router"; import Foo from "./components/Foo"; Vue.use(VueRouter); export default () => { const router = new VueRouter({ mode: "history", routes: [ { path: "/", component: Foo }, { path: "/bar", component: () => import("./components/Bar.vue") } ] }); return router; };
然后我们在man.js入口文件中引入这个路由函数,并在vue初始化函数中调用这个路由函数获取一个新的路由实例挂载在vue上
src/main.js
import Vue from "vue"; import App from "./App"; import createRouter from "./router"; // const vm = new Vue({ // el: "#app", // render: h => h(App) // }); // main.js是项目的入口文件,作用是提供vue的实例 // 将入口文件改造为一个函数,每次调用都返回一个vue的实例,这样做可以 1.根据客户端或者服务端来添加或不添加el; 2.每次调用都产生一个新的实例,服务端的根本要求 export default () => { // 每次调用vue实例生成函数的时候调用一次路由生成函数,生成一个路由实例,然后再new Vue的时候进行挂载 const router = createRouter(); const app = new Vue({ router, render: h => h(App) }); return { app }; };
修改src/App.vue, 配置router-link 和 router-view
<template> <div id="app"> <router-link to="/">Foo页面</router-link> <router-link to="/bar">Bar页面</router-link> <router-view></router-view> </div> </template> <script> export default { name: 'App', data() { return { }; } }; </script>
然后分别打包客户端和服务端,打开浏览器 localhost:3000 , 发现页面报错了:
原因是因为,我们服务中的路由 '/' 在被访问后,会调用 vue-server-renderer 的渲染器去渲染模板为字符串,但是渲染器并不知道该渲染哪个路由。
我们在serve.js中传入要渲染的url:
router.get("/", async ctx => { // css样式只能通过回调,同步的话会有问题 ctx.body = await new Promise((resolve, reject) => { render.renderToString({ url: "/" }, (err, data) => { if (err) reject(err); resolve(data); }); });
这样在server-entry.js中就能拿到传入的参数,这个参数其实就是context上下文。并且我们需要在返回需要服务端渲染的app实例的时候先跳转到指定的路由页面,需要router实例,可以再main.js中将router也一并导出:
src/amin.js
return { app, router };
src/server-entry.js
// content 上下文,是服务端传入的 export default context => { // 服务端会执行这个方法 const { app, router } = createApp(); // 返回的实例应该跳转到对应的路由 router.push(context.url); return app; };
重新打包后查看页面,显示正常,并且之前的报错也已经没有了。
到这里又会有一个问题,我们只是匹配了 / 路由,其他路由需要怎么处理呢?当我们访问bar后,页面明显是不对的。解决的方法就是在浏览器请求服务端的时候,服务端发现不是根路径,就直接返回给server-entry.js当前的请求路径。下面我们来写这个中间件
// 路由匹配中间件 app.use(async ctx => { // 根据请求路径去返回给server-entry url路径 ctx.body = await new Promise((resolve, reject) => { render.renderToString({ url: ctx.path }, (err, data) => { if (err) reject(err); resolve(data); }); }); });
打开浏览器发现bar页面可以正常访问了。
由于有异步组件的问题,server-entry.js 实例返回函数最好返回一个promise, 同时vue-router 提供了一个 onReady的方法,这个方法接收两个回调,第一个是成功的回调,第二个是失败的回调,即路由准备完成和路由准备失败:
// 调用当前这个文件产生一个vue的实例,并且需要导出给node服务端使用 // content 上下文,是服务端传入的 export default context => { // 服务端会执行这个方法 return new Promise((resolve, reject) => { const { app, router } = createApp(); // 返回的实例应该跳转到对应的路由 router.push(context.url); router.onReady(() => { resolve(app); }, reject); }); };
刷新浏览器一切正常,接下来还需要考虑一个问题,就是404页面的问题: 当我们访问一个不存在的路由的时候,页面还是正常渲染了。
vue-router也提供了一个方法 router.getMatchedCompoents() 这个方法返回当前路径对应匹配的路由数组。我们可以根据这个函数的返回来判断当前访问路径是否能匹配到路由,如果匹配不到的话就返回reject和一个状态码
router.onReady(() => { const matchs = router.getMatchedComponents(); if (matchs.length === 0) reject({ code: 404 }); resolve(app); }, reject);
然后再serve.js中可以通过错误处理来判断是够渲染正常的页面还是404页面
// 路由匹配中间件 app.use(async ctx => { try { // 根据请求路径去返回给server-entry url路径 ctx.body = await new Promise((resolve, reject) => { render.renderToString({ url: ctx.path }, (err, data) => { if (err) reject(err); resolve(data); }); }); } catch (err) { ctx.body = err.code; } });
打开浏览器,试着访问一个不存在的路由,页面正确的显示了404,再访问一个存在的路由 /bar 页面也正常的出现了。到此为止vue-ssr路由的简单实现也完成了。
本节源码可以查看我的github: https://github.com/Jasonwang911/vue-ssr/tree/master/step4
vue-ssr的实现原理连载(一): https://www.cnblogs.com/jasonwang2y60/p/11299503.html 源码: https://github.com/Jasonwang911/vue-ssr/tree/master/step1
vue-ssr的实现原理连载(二):https://www.cnblogs.com/jasonwang2y60/p/11300255.html 源码: https://github.com/Jasonwang911/vue-ssr/tree/master/step2
vue-ssr的实现原理连载(三):https://www.cnblogs.com/jasonwang2y60/p/11300255.html 源码: https://github.com/Jasonwang911/vue-ssr/tree/master/step3