从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
上期回顾
说接上文《二九║ Nuxt实战:异步实现数据双端渲染》,昨天咱们通过项目二的首页数据处理,简单了解到了 nuxt 异步数据获取的作用,以及亲身体验了几个重要页面的意义,整篇文章也一直在往如何实现服务端渲染的方向讲解,因为我个人感觉这个是一个重点,如果是只会如何使用的话,大家就可以走马观花的看看就行了,昨天呢,遗留了几个问题,我也想了想,还没有想好如何通过浅显的话来概括,如果要是搬出来教科书似的讲解,感觉又不是很清晰,我就在以后的领悟中补充吧,这里就先说下其中的三个问题:
1、我们通过 dev 编译,生成的 .nuxt 临时文件夹(我个人感觉他就像我们 .net core 中的 bin 文件夹),.nuxt 目录为 npm run dev或者是npm run build 后才生成,两个操作都执行了 build() 方法,用于存放 Nuxt.js 的核心库文件,如果你将一个老项目的 .nuxt 文件夹覆盖一个新项目的 .nuxt 文件夹,新项目正常运行,按照老的项目路由规则之类的都可以正常访问。例如,你可以在这个目录下找到 server.js
文件,描述了 Nuxt.js 进行服务端渲染的逻辑,流程是:调用 nuxtServerInit
方法,当请求打入时,最先调用的即是 nuxtServerInit
方法,可以通过这个方法预先将服务器的数据保存,如已登录的用户信息等。另外,这个方法中也可以执行异步操作,并等待数据解析后返回。Middleware
层,经过第一步后,请求会进入 Middleware
层,在该层中有三步操作:读取 nuxt.config.js
中全局 middleware
字段的配置,并调用相应的中间件方法 匹配并加载与请求相对应的 layout
调用 layout
和 page
的中间件方法。调用 validate
方法,在这一步可以对请求参数进行校验,或是对第一步中服务器下发的数据进行校验,如果校验失败,将抛出 404 页面。
调用 fetch
及 asyncData
方法,这两个方法都会在组件加载之前被调用,它们的职责各有不同, asyncData
用来异步的进行组件数据的初始化工作,而 fetch
方法偏重于异步获取数据后修改 Vuex 中的状态。
2、每次修改文件,都会触发热 webpack 的[HMR] 热加载,因为 Nuxt.js集成了如下模块: Vue-Router, Vue-Meta 和 Vuex (仅在使用 Vuex 状态树配置项 时引入)。 这样的好处在于,不需要手工配置依赖,每次当我们修改文件,webpack 就会自动保存,Nuxt.js 使用 Webpack 和 vue-loader 、 babel-loader 来处理代码的自动化构建工作(如打包、代码分层、压缩等等)。
4、在 network 中,当有一个请求过来时,服务器会新建一个vue实例,渲染(render)出需要显示的页面的html,把得到的页面以字符串的形式返回给客户端。同时把相关的js文件也返回(首次请求时返回vue的runtime、webpack的runtime和app.js等文件,非首次请求返回按需加载的js文件),返回的js文件和单页面应用(SPA)返回的差不多
app.js:基本就是你实际编写的那个app.vue(.vue或.js),没这个页面跑不起来,该页面应该提供了跟app应用相关的公共方法,脚本里也明确配置了跟路由相关的信息
vendor.js:vue-cli全家桶默认配置里面这个chunk就是将所有从node_modules/里require(import)的依赖都打包到这里,所以这个就是所有node_modules/下的被require(import)的js文件
manifest.js: 最后一个chunk,被注入了webpackJsonp的定义及异步加载相关的定义(webpack调用CommonsChunkPlugin处理后模块管理的核心,因为是核心,所以要第一个进行加载,不然会报错),该文件确定是跟路由相关的配置信息,其中明确包含了路由的路径,和版本号,但是暂时不明白为何前端输出会保留该配置(大概是做一些页面动态切换效果或者是预加载的时候使用,但是页面的预加载已经在ssr 输出的html 已经包含了)
然后还有一些 pages_index.js文件,布局 layouts_blog.js文件等:default.js(跟dis/layout/default.js一致,是载入了使用的layout)
。浏览器接收到这些文件后,通过js文件把静态页面的字符串hydrate成可以交互的应用。和SPA相比,SSR返回的数据就是多了个静态页面(字符串形式)。
我又一次老生常谈的说了一遍,还是感觉不是很清晰,看来自己的功底还是不行呀,如果有爱好 nuxt 或者 做过 SSR 的小伙伴,欢迎联系,咱们一起讨论下,今天呢,接着昨天的工作,把详情页渲染出来吧~~~
零、今天要完成紫色的部分
一、动态路由实现详情页布局设计
经过昨天的首页渲染,大家不知道使用起来怎么样,不仅可以配置每一页的 head 信息( TDK head),还可以对整体进行配置,虽然中间引入了 plugins 插件机制,不过也是很好的做了封装,特别是路由这一块,大家是不是发现已经完全不用配置了,Nuxt.js 依据 pages
目录结构自动生成 vue-router 模块的路由配置,为我们减少了很大的工作量,今天咱们就继续对详情页进行配置。
什么是动态路由
昨天呢,咱们开发了首页,通过地址直接可以访问,但是在开发过程中,肯定会有这样的页面:通过不同的 id 加载不同的详情页面,这些页面虽然是一个,但是 URL 地址却是多个,所以我们就说这个路由是动态的,还记得咱们在第一个项目中的时候,是怎么配置的么?我们通过页面接收参数来实现动态路由
{
path: "/Content/:id",
name: "Content",
component: Content
},
在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。
以下目录结构:
pages/
--| _slug/
-----| comments.vue -----| index.vue --| users/
-----| _id.vue --| index.vue
Nuxt.js 生成对应的路由配置表为:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue' },
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue' } ]
}
你会发现名称为 users-id
的路由路径带有 :id?
参数,表示该路由是可选的。如果你想将它设置为必选的路由,需要在 users/_id
目录内创建一个 index.vue
文件。
添加博客详情页
1、在 pages 文件夹中,添加 blog 文件夹,然后添加 _id.vue 页面
这个时候,我们看我们的临时编译文件 .nuxt 中 router.js 已经动态的增加上了上边添加的路由
return new Router({
mode: 'history', base: '/',
linkActiveClass: 'nuxt-link-active',
linkExactActiveClass: 'nuxt-link-exact-active',
scrollBehavior,
routes: [
{
path: "/blog/:id?",
component: _66cb1a63,
name: "blog-id" },
{
path: "/",
component: _70e72bdd,
name: "index" }
],
2、编辑 _id.vue 文件,实现数据获取
<template>
<div class="post-page">
<h1 class="title">{{data.btitle}}</h1>
<p class="createTime">{{data.bCreateTime }}</p>
<div v-html="data.bcontent" ></div>
</div>
</template>
<script> import Vue from "vue";
export default {
layout: "blog",
validate ({ params }) { // 校验文章id是否为数字
return /^\d+$/.test(params.id);
}, async asyncData ({ params, error }) { // 获取文章详情
let data = {}; try {
data = await Vue.http.get(`blog/${params.id}`); return {
data: data
};
} catch (e) { //error({ statusCode: 404, message: "出错啦" });
}
},
fetch ({ store, params }) {},
data () { return {
comments: []
};
},
head () {//设置页面 head 信息
return {
title: `${this.data.btitle}`,
meta: [
{
name: "description",
content: this.data.btitle
}
]
};
},
filters: {
timeFormat: function (time) { if (!time) return ""; return time;
}
},
mounted () {},
components: {
}
}; </script>
//导入样式
<style lang="css"> @import "../../static/vue-blog-sq.css"; </style>
是不是很简单,直接添加页面内容,就可以实现路由渲染,直接就可以访问了,不过这里可能会有一个坑,如果你运气好的话,会碰上,运气不好,就过去了。
3、刷新页面查看结果
苍天呀,不是吧,报错了?!如果你看到这个错误,恭喜你比较幸运,可能你会进一步的了解到 nuxt 是如何渲染的。
4、点击 浏览器后退 ,返回到首页,发现更加崩溃
不仅刚刚的详情页不见了,就连我们的首页数据也出错了!虽然这上面有数据,但是这个是浏览器缓存的,而不是我们真实的数据,这个时候着急的小伙伴,一定会很着急,稳住,我们能赢!
首次运行服务端渲染,然后开始客户端渲染
这个时候,如果你刷新首页,发现一切正常,不仅如何,如果你刷新详情页,数据也能出现,不信你可以试试,那这是为什么呢?
原因就在于我们刷新页面,或者新窗口打开等等,都是新开了一个服务,我们的页面为了实现 SEO 先进行的是服务端渲染,讲整个页面的字符串发送过来,然后点击链接去详情页的时候,我们就开始走客户端渲染了,之所以页面会报错,就是我们存在跨域的问题。
你可能会问,问什么第一次不存在,因为第一次是服务端渲染呀,服务端是不存在跨域问题的,只有 js 请求才会存在跨域的问题,到这里,通过这个错误你是不是了解到了一点儿,这个错误也是我故意放出来的,就是为了让大家更清楚的了解到 nuxt 是如何进行渲染的。这也能说的通,为什么第一次刷新首页有数据,从详情页返回过来,报错的原因了,因为第二次渲染已经交给客户端了。
解决办法很简单,还是在我们 .net core api 中 CORS 跨域配置我们的端口就行,然后一切正常了。
相信这个时候你对 nuxt 的渲染有了一点理解了吧,如果还不是很清晰,请往下看
二、SSR 同构知多少
SSR 用通过同构的方法解决了上面问题。我们先说一下 SSR 的具体表现,比如我们现在有一个列表页,列表中每一行对应一个详情页,那么如果直接用浏览器访问列表页时,服务器返回数据和 html 融合后的页面,浏览器拿到页面直接渲染,这就省去了先请求 js 再由 js 发起数据请求的过程,页面渲染的同时请求js,js加载完成后绑定事件;从列表页中点击某一条到详情页的时候,和普通的全栈 Ajax 一样,先请求 js 再由 js 发起数据请求,然后填充数据渲染页面。如果将详情页的链接复制出来,直接在新浏览中访问,那么详情页会直接返回数据和 html 融合后的页面(服务端渲染),渲染的同时请求详情页 js,最后再绑定事件。这个“服务器端拼接 html 和 html 是由同样的页面和组件完成的,这种前后端采用同样的结构在不同的环境中产出同样的 html 的方案称之为“同构”。
什么叫前后端同构?
为了解决某些问题(比如SEO、提升渲染速度等)vue 提供了2个方法在服务端生成一个HTML文本格式的字符串。在得到了这个HTML格式的字符串之后,通常会将其组装成一个页面直接返回给用户的浏览器。
到这里,服务端的活已经干完了,然后就是浏览器这边干活。
浏览器拿到HTML文本后,立刻进行渲染将内容呈现给用户。然后加载页面所需的 .js 文件,然后执行 JavaScript 脚本,然后开始初始化 vue 组件
到这里问题就来了。vue 初始化组件后会执行组件内所有 render () 方法,然后生成虚拟DOM的树形结构,然后在适当的时候将虚拟dom写到浏览器的真实 dom 中。因为 vue 总是根据虚拟 dom 来生成真实dom,所以最后会把服务器端渲染好的HTML全部替换掉。
上面这个事情说不是问题确实也不是问题,无非就是用户看到页面然后“闪现”一下。说是问题还真是个问题,产品会拿着这毛病从用户体验的角度在各种场合和你死磕半个月。磕累了你索性把服务端渲染关了,然后运营又拿着SEO的问题准备和你开始撕逼了。
为了解决这些问题,他们在 .renderToString(element) 方法中提供了一个 checksum 机制。前后端同构就是保证前端和后端的dom结构一致,不会发生重复渲染。
什么叫 首屏渲染?
简单的说就是 vue 在浏览器内存中第一次生成的虚拟 dom 树。切记是虚拟 dom ,而不是浏览器的dom。
了解 vue 的应该知道,所有 vue组件都有一个 render() 方法(如果使用function方式编写的组件会把function里的所有代码都塞到 render() 方法中去)。当 render( element, container, [callback] )方法执行时,会执行以下步骤:
1. 所有组件的会先进行初始化(es6执行构造函数)。
2. 所有组件的 render () 方法会被调用一次,完成这个过程后会得到一颗虚拟的 dom 树。
3. vue 会将虚拟dom转换成浏览器dom,完成后调用组件的 componentDidMount() 方法告诉你已经装载到浏览器上了。
在上面这个过程成中,步骤2完成后即为完成 vue 的首屏渲染。结合 checksum 机制步骤3有可能不会执行。
当组件状态发生变更时( setState() 生命周期函数被调用)或者 父组件渲染时(父组件的 render() 方法被调用),当前组件的 render() 方法都会被执行,都有可能会导致虚拟dom变更,但是这些变更和首屏渲染没任何关系了。
在我们的项目中,查看是如何渲染的
1、在我们的首页中,首次加载,在 network 中,查看我们都加载了那些文件
这些文件咱们在文章顶部都讲到了,这里说下 初始页面,它是直接将 html 返回给我们的前端渲染,这个很好理解
2、点击到详情页
我们发现这个我们的网络请求,并没有继续打包 build 走服务端渲染,而是仅仅请求了一个接口,返回了 json 数据,从这里大家应该就能看的处理,这就是所谓的双端渲染模式。
三、总结
好啦,今天就暂时说到这里了,通过详情页的添加,大家会切身体会到 nuxt 的渲染模式,是如何在服务端和客户端之间来回切换渲染的,这三篇文章大家要多看看,才能了解其中的内涵,加油鸭~~