从壹开始前后端分离 [ 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 调用 layoutpage 的中间件方法。调用 validate 方法,在这一步可以对请求参数进行校验,或是对第一步中服务器下发的数据进行校验,如果校验失败,将抛出 404 页面。

调用 fetchasyncData 方法,这两个方法都会在组件加载之前被调用,它们的职责各有不同, asyncData 用来异步的进行组件数据的初始化工作,而 fetch 方法偏重于异步获取数据后修改 Vuex 中的状态。


2、每次修改文件,都会触发热 webpack 的[HMR] 热加载,因为 Nuxt.js集成了如下模块: Vue-Router, Vue-Meta 和 Vuex (仅在使用 Vuex 状态树配置项 时引入)。 这样的好处在于,不需要手工配置依赖,每次当我们修改文件,webpack 就会自动保存,Nuxt.js 使用 Webpack 和 vue-loader 、 babel-loader 来处理代码的自动化构建工作(如打包、代码分层、压缩等等)。

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

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返回的数据就是多了个静态页面(字符串形式)。

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

我又一次老生常谈的说了一遍,还是感觉不是很清晰,看来自己的功底还是不行呀,如果有爱好 nuxt 或者 做过 SSR 的小伙伴,欢迎联系,咱们一起讨论下,今天呢,接着昨天的工作,把详情页渲染出来吧~~~

零、今天要完成紫色的部分

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

一、动态路由实现详情页布局设计

经过昨天的首页渲染,大家不知道使用起来怎么样,不仅可以配置每一页的 head 信息( TDK head),还可以对整体进行配置,虽然中间引入了 plugins 插件机制,不过也是很好的做了封装,特别是路由这一块,大家是不是发现已经完全不用配置了,Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置,为我们减少了很大的工作量,今天咱们就继续对详情页进行配置。

什么是动态路由

昨天呢,咱们开发了首页,通过地址直接可以访问,但是在开发过程中,肯定会有这样的页面:通过不同的 id 加载不同的详情页面,这些页面虽然是一个,但是 URL 地址却是多个,所以我们就说这个路由是动态的,还记得咱们在第一个项目中的时候,是怎么配置的么?我们通过页面接收参数来实现动态路由

 {
      path: "/Content/:id",
      name: "Content",
      component: Content
    },
从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

在 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" }
    ],
从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

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、刷新页面查看结果

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

苍天呀,不是吧,报错了?!如果你看到这个错误,恭喜你比较幸运,可能你会进一步的了解到 nuxt 是如何渲染的。

4、点击 浏览器后退 ,返回到首页,发现更加崩溃

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

不仅刚刚的详情页不见了,就连我们的首页数据也出错了!虽然这上面有数据,但是这个是浏览器缓存的,而不是我们真实的数据,这个时候着急的小伙伴,一定会很着急,稳住,我们能赢!

首次运行服务端渲染,然后开始客户端渲染

这个时候,如果你刷新首页,发现一切正常,不仅如何,如果你刷新详情页,数据也能出现,不信你可以试试,那这是为什么呢?

原因就在于我们刷新页面,或者新窗口打开等等,都是新开了一个服务,我们的页面为了实现 SEO 先进行的是服务端渲染,讲整个页面的字符串发送过来,然后点击链接去详情页的时候,我们就开始走客户端渲染了,之所以页面会报错,就是我们存在跨域的问题。

你可能会问,问什么第一次不存在,因为第一次是服务端渲染呀,服务端是不存在跨域问题的,只有 js 请求才会存在跨域的问题,到这里,通过这个错误你是不是了解到了一点儿,这个错误也是我故意放出来的,就是为了让大家更清楚的了解到 nuxt 是如何进行渲染的。这也能说的通,为什么第一次刷新首页有数据,从详情页返回过来,报错的原因了,因为第二次渲染已经交给客户端了。

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

解决办法很简单,还是在我们 .net core api 中 CORS 跨域配置我们的端口就行,然后一切正常了。

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

相信这个时候你对 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 中,查看我们都加载了那些文件

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

这些文件咱们在文章顶部都讲到了,这里说下 初始页面,它是直接将 html 返回给我们的前端渲染,这个很好理解

2、点击到详情页

从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构...
image

我们发现这个我们的网络请求,并没有继续打包 build 走服务端渲染,而是仅仅请求了一个接口,返回了 json 数据,从这里大家应该就能看的处理,这就是所谓的双端渲染模式。

三、总结

好啦,今天就暂时说到这里了,通过详情页的添加,大家会切身体会到 nuxt 的渲染模式,是如何在服务端和客户端之间来回切换渲染的,这三篇文章大家要多看看,才能了解其中的内涵,加油鸭~~

四、Github

https://github.com/anjoy8/Blog.Vue.Nuxt