从壹开始先后端分离 [ vue + .netcore 补充教程 ] 二九║ Nuxt实战:异步实现数据双端渲染

回顾

哈喽你们好!又是元气满满的周~~~二哈哈,不知道你们中秋节过的如何,立刻又是国庆节了,博主我将经过三天的时间,给你们把项目二的数据添上(这里强调下,填充数据不是最重要的,最重要的是要配合着让你们明白 nuxt.js 是如何一步步实现服务端渲染的),虽然说是基于 Nuxt 的,可是数据源仍是咱们的老数据,就是 .net core api 的数据,和上一个项目中使用的是同样的,这里要说下,有的小伙伴说想要个人数据,这里暂时说抱歉,由于里边有一些个人私人的记录,还有一些网站密码啥的,我本身懒得一条一条的删除了,这里就不放出来了,多多包涵,不过接口地址能够随便使用http://apk.neters.club,有源代码和数据库表结构,相信你们仍是能够搞定滴,由于是三天,因此今天咱们就会把首页给处理出来,这里有俩个点,第一,为何是三天呢,由于博主三天后要放假了哈哈( 这里要给你们说下心得,坚持学习和坚持写博客是彻底不一样的时间量,天天我光写博客的时间最少三个小时,加上工做的九个小时,天天我至少须要12个小时,因此若是想快速学习,建议仍是要好好的写博客 ),第二,数据获取和以前的 vue 有点儿差异,虽然都是基于 axios ,可是 nuxt 框架作了必定的封装,并且仍是异步的,ASync/Await ,你们看我今天的标题也能看的出来,至于为何会是异步的呢,先留个神秘,你们看完今天的讲解应该就知道是为何了~javascript

书接上文,上周我们说到了nuxt 的运行原理《二八║ Nuxt 基础:面向源码研究Nuxt.js》,不知道你们是否看了呢,主要经过源码的分析,来重点说明 Nuxt 是如何实现服务端渲染的,我的感受写的比较羞涩难懂,我也是在慢慢的润色,尽可能修改为通俗易懂的给你们展现,写成人话。这里我要说句题外话,你们有时间的话,仍是应该把后端的注意力拿出来一点儿点儿放到前端了,之前我也是一个老后端,一直想着各类持久化ORM哪一个更帅,各类框架哪一个性价比更高,可是一直也技术平平,反而忽略了这两年的前端发展,通过这一个月的学习,我发现前端技术居然发展如此之快,竟超出个人想象,有点儿追赶挑战后端的意思了,多语言化的发展,更有助于一个程序员的发展( 这个欢迎来喷,只会一种语言的话,嗯~ 会有局限性┑( ̄Д  ̄)┍ ),哈哈这个扯的有点儿远了。php

对于昨天的文章,总结来讲,nuxt 的核心就是在 vue.js 的基础上,封装了双端渲染模式(服务端和客户端),结合页面html片断缓存,来实现 SSR ,最终解决首屏快速渲染和 SEO 的问题,核心就是在如何实现双端渲染上,你们以前的教程中,对 Vue 的 SPA 很熟练了,这三天我们就慢慢的研究下,如何实现双端渲染的,这个也就是我讲 nuxt 的核心 —— 渲染。css

 

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

 

 

1、重点温习框架中的几个部分文件 —— 铺垫

一、nuxt.config.js 文件

项目的核心文件,做为一个配置文件,它配合着 app.html 一块儿担当着以前咱们的 index.html 的做用,只不过这里是更偏重于配置,对全局配置起到一个十分重要的做用,像咱们的 webconfig 同样html

//这些配置在你的项目里可能不必定都有,可是我都会提到
module.exports = {
  cache: {},
  css: [
    // 加载一个 node.js 模块
    //  'hover.css/css/hover-min.css',
    //  // 一样加载一个 node.js 模块,不过咱们定义所需的预处理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 项目中的 CSS 文件
    //  '~assets/css/main.css',
    //  // 项目中的 Sass 文件
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],
  // 默认 true
  dev: process.env.NODE_ENV !== 'production',//不是生产环境

  // 建立环境变量
  env: {},

  // 配置 Nuxt.js 应用生成静态站点的具体方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },
  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: '老张的哲学',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },
  performance: {
    gzip: false,
    prefetch: true
  },
  // 引入 Vue.use 的插件
  plugins: [],
  // 默认当前路径
  rootDir: process.cwd(),
  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 导航(如按浏览器的返回按钮)时能够获取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目标页面子组件少于两个
        if (to.matched.length < 2) {
          // 滚动至页面顶部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 若是目标页面子组件中存在配置了scrollToTop为true
          position = { x: 0, y: 0 }
        }
        // 若是目标页面的url有锚点,  则滚动至锚点所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 扩展路由
    extendRoutes: () => {},

    // 默认同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 文件监控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

 

二、视图模板

默认的 html 模版: 应用根目录下的 app.html 文件, 若是你没有找到这个文件, 则采用默认的模版,固然你也能够本身新增,配置,前端

这个更像是咱们以前的 index.html 页面,它配合着 nuxt.config.js 一块儿做为项目承载页面,更偏重于模板,把 <div id='app'></div> 挂载,变成了填充的方式。vue

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

 

三、layouts 布局目录

 

能够修改该目录下的 default.vue 来修改默认布局 , 这个就是相似于咱们以前的 app.vue 页面,java

<template>
  <div class="layout-default">
    <cl-header></cl-header>
    <nuxt class="layout-main"/>
    <cl-footer></cl-footer>
    <div class="layout-bg"></div>
  </div>
</template>

 

其中 <nuxt/> 是必需的,页面的主体内容会显示在这里 (相似于根节点的 <router-view/>)node

此外还能够在目录下新增 error.vue 做为错误页面,具体的写法能够参考官方文档webpack

 

四、pages 页面 路由

路由, 约定大于配置, 支持动态, 嵌套, 动态嵌套路由, 过渡效果和中间件,经过文件夹目录名称, 组件名称, 生成路由配置,默认的 transitionName 为 page, 可在 assets 中添加全局的过渡效果,ios

在匹配页面以前执行;

页面组件其实是 Vue 组件,只不过 Nuxt.js 为这些组件添加了一些特殊的配置项(对应 Nuxt.js 提供的功能特性)以便你能快速开发通用应用。

<template>
  <h1 class="red">Hello {{ name }}!</h1>
</template>

<script>
export default {
  asyncData (context) {
    // called every time before loading the component
    return { name: 'World' }
  },
  fetch () {
    // The fetch method is used to fill the store before rendering the page
  },
  head () {
    // Set Meta Tags for this Page
  },
  // and more functionality to discover
  ...
}
</script>

<style>
.red {
  color: red;
}
</style>

 

nuxt.config.js --> 执行middleware --> 匹配布局 --> 匹配页面

 

用于存放页面级别的组件,nuxt 会根据该目录下的页面结构生成路由

好比上图中的页面结构,会生成这样的路由配置:

const _7b01ffaa = () => import('..\\pages\\post\\index.vue' /* webpackChunkName: "pages_post_index" */).then(m => m.default || m)
const _2b7fe492 = () => import('..\\pages\\post\\_id.vue' /* webpackChunkName: "pages_post__id" */).then(m => m.default || m)
const _4f14dfca = () => import('..\\pages\\index.vue' /* webpackChunkName: "pages_index" */).then(m => m.default || m)

export function createRouter () {
  return new Router({
    mode: 'history',
    base: '/',
    linkActiveClass: 'nuxt-link-active',
    linkExactActiveClass: 'nuxt-link-exact-active',
    scrollBehavior,
    routes: [
        {
            path: "/post",
            component: _7b01ffaa,
            name: "post"
        },
        {
            path: "/post/:id",
            component: _2b7fe492,
            name: "post-id"
        },
        {
            path: "/",
            component: _4f14dfca,
            name: "index"
        }
    ],
    
    
    fallback: false
  })
}

 

五、使用插件 plugins 文件夹

若是项目中还须要引入其余的第三方插件,能够直接在页面中引入,这样在打包的时候,会将插件打包到页面对应的 js 里面,但要是别的页面也引入了一样的插件,就会重复打包。若是没有须要分页打包的需求,这时候能够配置 plugins,而后在根目录的 nuxt.config.js 中添加配置项 build.vendor 和 plugins,这里的 plugins 属性用来配置 vue.js 插件,也就是 能够用 Vue.user() 方法 的插件

默认只须要 src 属性,另外还能够配置 ssr: false,让该文件只在客户端被打包引入,若是是像 axios 这种第三方 (不能 use) 插件,只须要在 plugins 目录下建立 axios.js,而后在 build.vendor 中添加配置 (不须要配置 plugins)

这样在打包的时候,就会把 axios 打包到 vendor.js 中。

 

2、配置页面,实现首页的数据获取 —— 异步

一、在 static 文件中,新增样式 vue-blog-sq.css 文件

提醒,这里的文件,不会被打包,因此会在页面中呈现原有格式,若是想每次都被打包压缩,须要写到 assets 资源文件夹中

 

二、在 components 中,新建 layout 文件夹,而后新增页头页脚 组件

这个很简单,就是普通的 *.vue 组件写法,你们能够自行下载浏览

 

三、在 layouts 页面布局文件夹中,新增 blog.vue 布局

提醒:之后也能够单给用户增长布局,好比 user.vue 

<template>
  <div class="layout-default">
    <cl-header></cl-header>
    <nuxt class="layout-main"/>//注意,<nuxt />,必须有,相似一个 <router-view/>
    <cl-footer></cl-footer>
  </div>
</template>

<script type="text/javascript">
  import clHeader from "~/components/layout/header.vue";
  import clFooter from "~/components/layout/footer.vue";
  export default {
    data () {
      return {};
    },
    mounted () {
    },
    components: {
      clHeader,
      clFooter
    }
  };
</script>

很简单的一个布局入口,是否是很像咱们以前的 App.vue 中的 路由容器 —— <router-view /> ,可是又比其更丰富,由于咱们以前的 app.vue 只能有一个入口,可是 nuxt 能够提供多个自定义 模板layouts,更方便。

 

四、根目录新增 config 文件夹,添加 index.js ,做为咱们之后的配置文件,相似 .net core api 中的 appsetting.json 文件

const config = {
  //开发环境配置,开发的时候
    development: {
         //api: "http://localhost:58427/api/",
        api: "http://apk.neters.club/api/"
    },
  //生产环境配置,部署的时候
    production: {
        api: ""
    }
};
//获取当前环境变量,是 production或者development
module.exports = config[process.env.NODE_ENV];

 

 

五、在 plugins 插件中,新增 server_site 文件夹,而后添加 http.js 和 index.js

 

一、为何要使用插件?

咱们能够在应用中使用第三方模块,一个典型的例子是在客户端和服务端使用 axios 作 HTTP 请求。

首先咱们须要安装 npm 包:

npm install --save axios

而后在页面内能够这样使用:

<template>
  <h1>{{ title }}</h1>
</template>

<script>
import axios from 'axios'

export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  }
}
</script>

有一个值得注意的问题是,若是咱们在另一个页面内也引用了 axios,那么在应用打包发布的时候 axios 会被打包两次,而实际上咱们只须要打包一次。这个问题能够经过在 nuxt.config.js 里面配置 build.vendor 来解决:

module.exports = {
  build: {
    vendor: ['axios']
  }
}

通过上面的配置后,咱们能够在任何页面里面引入 axios 而不用担忧它会被重复打包。

 

二、为何要分 server_site服务端 与 client_site客户端 插件

有些插件可能只是在浏览器里使用,因此你能够用 ssr: false 变量来配置插件只从客户端仍是服务端运行。

举个栗子:

nuxt.config.js:

module.exports = {
  plugins: [
    { src: '~/plugins/vue-notifications', ssr: false }//设置ssr 为false
  ]
}
plugins/vue-notifications.js://定义一个插件

import Vue from 'vue'
import VueNotifications from 'vue-notifications'

Vue.use(VueNotifications)

一样地,若是有些脚本库你只想在服务端使用,在 Webpack 打包 server.bundle.js 文件的时候会将 process.server 变量设置成 true

 

三、配置服务端 http.js 和 index.js 的内容

// http.js 封装 axios,防止多处打包
import Axios from "axios";
import config from "~/config";//引入配置文件

// 实例化 axios()
const http = Axios.create({
    baseURL: config.api,//根url
    timeout: 8000,
    validateStatus: function (status) {
        return status >= 200;
    }
});

// 定义错误异常方法
function LogicError (message, code, data) {
    this.message = message;
    this.code = code;
    this.data = data;
    this.name = "LogicError";
}

LogicError.prototype = new Error();
LogicError.prototype.constructor = LogicError;

// http 的request 请求
http.interceptors.request.use((data, headers) => {
    return data;
});

// http 的response 命令,失败的调用上边的失败异常方法
http.interceptors.response.use(response => {
    const data = response.data;
    switch (data.success) {
        case true:
            return data.data;
        default:
            throw new LogicError(data.msg);
    }
}, err => {
    throw new LogicError("网络请求错误");
});

export default http;// 输出http
// 定义 http 插件,是一个全局变量
import Vue from "vue";
import http from "./http.js";// 引入http封装的axios

const install = function (VueClass, opts = {}) {

    // http method
    VueClass.http = http;
    VueClass.prototype.$http = http;
};
Vue.use(install);// 在vue 中,使用该插件

 

六、在 nuxt.config.js 中引用咱们的服务端插件

这样添加之后,咱们就能够全局使用请求命令了,打包的时候,也只会打包一个

提示:一、记得须要按照提示安装 axios npm install --save axios 

           二、引入的组件库必须配置 plugins, 可是有的组件库不支持 ssr.

 

七、设计修改 pages 下的 index.vue 页面,异步获取接口数据

 提示:这个就是文章开头提到的问题

一、为何要异步?

asyncData方法会在组件(限于页面组件,也就是pages 文件夹下的vue文件 )每次加载以前被调用。它能够在服务端或路由更新以前被调用。 在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你能够利用 asyncData方法来获取数据,Nuxt.js 会将 asyncData 返回的数据融合组件 data 方法返回的数据一并返回给当前组件。

注意:因为 asyncData方法是在组件 初始化 前被调用的,因此在方法内是没有办法经过  this 来引用组件的实例对象。

Nuxt.js 提供了几种不一样的方法来使用 asyncData 方法,你能够选择本身熟悉的一种来用:

  1. 返回一个 Promise, nuxt.js会等待该Promise被解析以后才会设置组件的数据,从而渲染组件.
  2. 使用 async 或 await (推荐使用)

返回 Promise

export default {
  asyncData ({ params }) {
    return axios.get(`https://my-api/posts/${params.id}`)
    .then((res) => {
      return { title: res.data.title }
    })
  }
}

 

使用 async或await

export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(`https://my-api/posts/${params.id}`)
    return { title: data.title }
  }
}

 


 

<!-- index.vue 页面 -->
<template> <div class="u-marginBottom40 js-collectionStream"> <div class="streamItem streamItem--section js-streamItem"> <div class="u-clearfix u-maxWidth1000 u-marginAuto"> <div class="row u-marginTop30 u-marginBottom20 u-sm-marginLeft20 u-sm-marginRight20 u-xs-marginTop0 u-xs-marginRight0 u-xs-marginLeft0"> <div v-for="post in blogs" :key="post.bID" class="postArticle postArticle--short is-withAccentColors"> <!--......--> </div> </div> </div> </div> </div> </template> <script> import Vue from "vue";//引入 vue 实例,获取全局变量 export default { layout: "blog",//这个就是咱们自定义的模板布局 async asyncData (ctx) {// asyncData() 异步获取数据方法 let blogs = []; try { console.log(1) const blogData = await Vue.http.get("Blog?page=1&bcategory=技术博文"); console.log(blogData); blogs = blogData; return { blogs: blogs, }; } catch (e) { //ctx.error({ statusCode: 500, message: "出错啦" });//自定义错误页面 } }, data () { return {}; }, head () {//针对每个页面,进行封装 head return { meta: [ { name: "description", content: "老张的哲学是我的博客,利用NUXT.js的服务端渲染方案" } ] }; }, mounted () {}, filters: {//过滤器,用来过滤时间字符串 timeFormat: function (time) { if (!time) return ""; return time; } }, methods: {}, components: { } }; </script>

 

八、启动项目,就能看到咱们的数据了

提示:若是报 less 错误,请安装 

  npm install less less-loader

 

 

 

3、页面是如何一步步加载数据的 ? —— 存疑

这里先给你们抛出几个问题:

一、咱们经过第一次编译的时候,生成 .nuxt 临时文件夹,是服务端渲染仍是客户端渲染?

 

二、咱们打开浏览器的 调试工具,发现每次修改,会都生成一些提示,固然这都是 webpack 的热加载,那这些又是什么含义呢?

 

 

三、既然是服务端和客户端一块儿渲染,咱们的 页面路由 是如何匹配到的呢?

四、打开咱们的页面 network 网络请求,发现有不少不知道的文件,都是怎样生成的呢?

 

其实这几个问题咱们在以前的都经过文字的形式说过,由于时间的问题,今天暂时就说到这里,明天我们再继续深刻研究这个问题,顺便填充下详情页的数据。

 

4、CODE

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