【vue服务端渲染】—— NuxtJS基础

Nuxt.js 是一个基于 Vue.js 生态开发的一个第三方服务端渲染框架,通过它我们可以轻松构建现代化的服务端渲染应用,本章主要学习下 NuxtJS基础知识

NuxtJS 介绍

NuxtJS 是什么

Nuxt.js 是一个基于 Vue.js 的第三方开源服务端渲染应用框架,它可以帮我们轻松的实现同构应用。
通过对客户端/服务端基础架构的抽象组织,Nuxt.js 主要关注的是应用的UI渲染
我们的目标是创建一个灵活的应用框架,你可以基于它初始化新项目的基础结构代码,或者在已有 Node.js 项目中使用 Nuxt.js。
Nuxt.js 预设了利用 Vue.js 开发服务端渲染的应用所需要的各种配置。
除此之外,我们还提供了一种命令叫:nuxt generate,为基于 Vue.js 的应用提供生成对应的静态站点的功能。
我们相信这个命令所提供的功能,是向开发集成各种微服务(Microservices)的 Web 应用迈开的一步。
作为框架,Nuxt.js 为客户端/服务端这种典型的应用架构模式提供了许多有用的特性,例如异步数据加载、中间件支持、布局支持等非常实用的功能。

NuxtJS 特性

  • 基于 Vue.js
    • Vue、Vue Router、Vuex、Vue SSR
  • 自动代码分层
  • 服务端渲染
  • 强大的路由功能,支持异步数据
  • 静态文件服务
  • ES2015+ 语法支持
  • 打包和压缩 JS 和 CSS
  • HTML 头部标签管理
  • 本地开发支持热加载
  • 集成 ESLint
  • 支持各种样式预处理器: SASS、LESS、 Stylus 等等
  • 支持 HTTP/2 推送

NuxtJS 框架是如何运作的

NuxtJS 框架是如何运作

Nuxt.js 集成了以下组件/框架,用于开发完整而强大的 Web 应用:

  • Vue.js
  • Vue Router
  • Vuex
  • Vue Server Renderer
    压缩并 gzip 后,总代码大小为:57kb (如果使用了 Vuex 特性的话为 60kb)。

另外,Nuxt.js 使用 Webpackvue-loaderbabel-loader 来处理代码的自动化构建工作(如打包、代码分层、压缩等等)。

NuxtJS 渲染流程

下图阐述了Nuxt.js应用一个完整的服务器请求到渲染(或用户通过<nuxt-link>切换路由渲染页面)的流程:

NuxtJS 框架是如何运作

NuxtJS 使用方式

  • 初始项目
  • 已有的 Node.js 服务端项目
    • 直接把 Nuxt 当作一个中间件集成到 Node Web Server 中
  • 现有的 Vue.js 项目
    • 非常熟悉 Nuxt.js
    • 至少百分之 10 的代码改动

初始化 NuxtJS

官方文档

Nuxt 提供了两种方式用来创建项目:

  • 使用 create-nuxt-app 脚手架工具
  • 手动创建项目

手动创建项目

nuxtJs_demo

  • 项目准备工作

    • 创建项目,安装依赖

      1
      2
      3
      4
      5
      6
      7
      8
      # 创建示例项目 
      mkdir nuxtJs_demo
      # 进入示例项目目录中
      cd nuxtJs_demo
      # 初始化 package.json 文件
      npm init -y
      # 安装 nuxt
      npm innstall nuxt --save
    • package.json文件的scripts中新增:

      1
      2
      3
      "scripts": {
      "dev": "nuxt"
      }

      上面的配置使得我们可以通过运行npm run dev来运行nuxt

  • 创建页面并启动项目

    • 创建 pages 目录,pages 是固定写法,不可更改

      1
      mkdir pages
    • 创建页面pages/index.vue

    1
    2
    3
    <template>
    <h1>Hello world!</h1>
    </template>
    • 运行命令,启动项目
    1
    npm run dev

    在浏览器中通过 http://localhost:3000 看到具体页面。

    需要注意:Nuxt.js 会监听 pages 目录中的文件更改,因此在添加新页面时无需重新启动应用程序。

NuxtJS 路由

Nuxt.js 会根据 pages 目录中的所有*.vue文件生成应用的路由配置

基础路由

  • 假设 pages 的目录结构如下:

    ├── pages
        ├── user
            ├── index.vue
            ├── one.vue
        ├── index.vue
    
  • 那么,Nuxt.js 自动生成的路由配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router: { 
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}

此处的 user 目录名称,就是路由名称,浏览器访问地址为:http://localhost:3000/user,即默认访问 pages/user/index.vue 页面

路由导航

  • a 标签
    它会刷新整个页面,不要使用
1
2
<!-- a 链接,刷新导航,走服务端渲染 -->
<a href="/">首页</a>
  • <nuxt-link/>组件
    Vue Router 中的组件用法一样
1
2
3
4
5
<!-- router-link 导航链接组件 -->
<router-link to="/">首页</router-link>

<!-- nuxt-link 导航链接组件 -->
<nuxt-link to="/">首页</nuxt-link>
  • 编程式导航

和 Vue Router 中的 编程式导航 用法一样

1
2
<!-- 编程式导航 -->
<button @click="onClick">首页</button>
1
2
3
4
5
methods: {
onClick () {
this.$router.push('/')
}
}

动态路由

官方文档

在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的Vue文件或目录

以下目录结构:
├── pages
├── _slug
├── comments.vue
├── index.vue
├── users.vue
├── _id.vue
├── index.vue

Nuxt.js 生成对应的路由配置表为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
router: { 
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
}
]
}

通过上面代码可以发现名称为users-id的路由路径带有:id?参数(和vue一样可以通过$route.params.id动态获取路由参数),表示该路由是可选的。如果想将它设置为必选的路由,需要在users/_id目录内创建一个index.vue文件。

嵌套路由

官方文档:

可以通过 vue-router 的子路由创建 Nuxt.js 应用的嵌套路由。

创建内嵌子路由,你需要添加一个 Vue 文件,同时添加一个与该文件同名的目录用来存放子视图组件。

注意: 别忘了在父组件(*.vue 文件)内增加<nuxt-child/>用于显示子视图内容。

以下目录结构:
├── pages
├── users.vue
├── _id.vue
├── index.vue
├── users.vue

Nuxt.js 自动生成的路由配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
router: { 
routes: [
{
path: '/users',
component: 'pages/users.vue',
children: [
{
path: '',
component: 'pages/users/index.vue', name: 'users'
},
{
path: ':id',
component: 'pages/users/_id.vue', name: 'users-id'
}
]
}
]
}

pages/users.vue中,书写子路由出口,代码示例如下:

1
2
<!-- 子路由出口 -->
<nuxt-child />

自定义路由配置

官方文档

在项目根目录下,新建 nuxt.config.js 文件,它是 Nuxt.js 的配置文件。

注意:修改配置文件的内容以后,需要重新启动项目

  • base

应用程序的基URL。例如,如果整个单页应用程序在/app/,则base应使用该值’/app/‘
nuxt.config.js 文件中进行配置,代码示例如下:

1
2
3
4
5
module.exports = {
router: {
base: '/app'
}
}

base设置后,Nuxt.js 也将添加到文档头中<base href=""/>

  • extendRoutes

扩展Nuxt.js创建的路由

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
router: {
// routes: 一个数组,路由配置表
// resolve:解析路由组件路径
extendRoutes(routes, resolve) { // 自定义路由表
routes.push({
name: '/hello',
path: 'hello',
component: resolve(__dirname, 'pages/user/about.vue')
})
}
}
}

如果要对路线进行排序,可以使用sortRoutes(routes) 函数:

1
2
3
4
5
6
7
8
9
10
11
import { sortRoutes } from '@nuxt/utils'
module.exports = {
router: {
extendRoutes(routes, resolve) {
// Add some routes here ...

// and then sort them
sortRoutes(routes)
}
}
}

添加使用命名视图的管线时,不要忘记添加相应的chunkNames已命名的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
router: {
extendRoutes(routes, resolve) {
routes.push({
path: '/users/:id',
components: {
default: resolve(__dirname, 'pages/users'), // or routes[index].component
modal: resolve(__dirname, 'components/modal.vue')
},
chunkNames: {
modal: 'components/modal'
}
})
}
}
}

NuxtJS 视图

视图

视图

在 NuxtJS 中页面结构一般由三部分组成:

  • 第一部分是最外层的文档页面,也就是单页面或者说服务端渲染的HTML页面。
  • 在HTML 页面里面包裹着 Layout布局组件(可选),相当于所有页面的父路由。
  • 再往里面是页面组件,每个页面组件有自己额外的成员方法,包括页面的子组件之类的可选内容。

模板

app模板用于为Nuxt.js应用程序创建文档的实际HTML框架,该应用程序为头部和主体注入内容以及变量。此文件是自动为您创建的,通常很少需要修改。通过创建一个app.html项目的源目录中的文件,默认情况下是根目录。

  • Nuxt.js使用的默认模板是:app.html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
<!-- 渲染的内容最终会注入到这里 -->
{{ APP }}
</body>
</html>

可以根据自己需求做修改:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
<head {{ HEAD_ATTRS }}>
{{ HEAD }}
</head>
<body {{ BODY_ATTRS }}>
<!-- 渲染的内容最终会注入到这里 -->
<h1>app.html</h1>
{{ APP }}
</body>
</html>

布局 Layout

当你想改变你的Nuxt.js应用程序的外观时,布局是一个很好的帮助。例如,您希望包含一个侧边栏或具有不同的移动和桌面布局。

  • 默认布局

可以通过 添加 layouts/default.vue 文件。这将用于所有未指定布局的页面。您只需要在布局中包含<Nuxt />呈现页面组件的组件

1
2
3
4
5

<template>
<!-- 页面出口,类似子路由出口 -->
<Nuxt />
</template>
  • 自定义布局

Nuxt.js 允许你扩展默认的布局,或在layout目录下创建自定义的布局。

可通过添加 layouts/default.vue 文件来扩展应用的默认布局。

提示: 别忘了在布局文件中添加组件用于显示页面的主体内容。

默认布局的源码如下:

1
2
3
<template>
<nuxt />
</template>

一旦使用,默认所有页面都会作用,不能取消,只能更改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- layouts/default.vue -->

<template>
<div>
<h1>layouts/default.vue 组件</h1>
<!-- 页面出口,类似于子路由出口 -->
<nuxt />
</div>
</template>

<script>
export default {
name: 'LayoutDefault'
}
</script>

然后我们必须告诉页面 (即pages/index.vue) 使用您的自定义布局:

1
2
3
4
5
6
7
8
9
10
11
<template>
<h1>Hello world!</h1>
</template>

<script>
export default {
name: 'HomePage',
// 默认 default 可修改为指定的布局页面
layout: 'default'
}
</script>

NuxtJS 异步数据

Nuxt.js 扩展了Vue.js,增加了一个叫asyncData的方法,使得我们可以在设置组件的数据之前能异步获取或处理数据。

asyncData

  • 基本用法

    • 增加一个数据文件(static/data.json),NuxtJS默认在Web服务中将数据暴露出来,可以直接通过 http://localhost:3000/app/data.json 获取数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      {
      "posts": [
      {
      "id": 1,
      "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
      "body": "1-quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
      },
      {
      "id": 2,
      "title": "qui est esse",
      "body": "2-est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
      },
      {
      "id": 3,
      "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
      "body": "3-et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"
      },
      {
      "id": 4,
      "title": "eum et est occaecati",
      "body": "4-ullam et saepe reiciendis voluptatem adipisci\nsit amet autem assumenda provident rerum culpa\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\nquis sunt voluptatem rerum illo velit"
      },
      {
      "id": 5,
      "title": "nesciunt quas odio",
      "body": "5-repudiandae veniam quaerat sunt sed\nalias aut fugiat sit autem sed est\nvoluptatem omnis possimus esse voluptatibus quis\nest aut tenetur dolor neque"
      }
      ],
      "title": "文章内容"
      }
    • pages/index.vue

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      <template>
      <div>
      <h1>Hello Nuxt.js</h1>
      <div>{{title}}</div>
      <router-link to="/about">about</router-link>
      <Foo :foo ="foo" :posts="posts" />
      </div>
      </template>
      <script>
      import axios from 'axios'
      import Foo from '@/components/Foo.vue'
      export default {
      name:'homePage',
      components:{
      Foo
      },
      // 当想要动态页面内容有利于SEO或者是提升首屏渲染速度的时候,就可以在asyncData中发请求拿数据
      async asyncData(){
      console.log('asyncData')
      console.log(this); // undefined
      const res = await axios({
      method:'GET',
      url:'http://localhost:3000/app/data.json'
      })
      return res.data
      },
      // 如果是非异步数据或者普通数据,则正常的初始化到data中即可,asyncData中数据和data中数据最终会合并
      data(){
      return {
      foo:'bar-foo'
      }
      }
      }
      </script>
      • 它会将 asyncData 返回的数据融合组件 data 方法返回数据一并给组件

      视图

      • 调用时机:服务端渲染期间和客户端路由更新之前
        (1) 服务端渲染期间
        视图
        初次访问主页时候,我们可以看到 console.log(‘asyncData’) 执行的时机是在服务端(Nuxt SSR包裹起来在客户端),同时可以通过 console.log(this) 看到 this 返回值为undefined,因为它是在组件初始化之前被调用的。
        (2) 客户端路由更新之前
        视图
        服务端首屏渲染既能确保异步数据能在渲染到客户端之前已经被渲染好(提高首屏渲染速度,有利于SEO)。达到页面后又变成SPA客户端应用,同样可以被调用从而更新数据。

      • 只能在页面组件中使用,非页面组件(子组件)中使用会报错
        创建子组件:components/foo.vue

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        <template>
        <div>
        <h1>component foo page</h1>
        <div>{{foo}}</div>
        </div>
        </template>
        <script>
        export default {
        props:['posts'],
        // 只能在页面组件中使用,非页面组件中不能使用
        async asyncData(){
        return {
        foo:'bar'
        }
        }
        }
        </script>

        pages/index.vue使用组件Foo,会报错
        视图

        解决方式 :利用父组件向子组件传递数据的方式:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        export default {
        props:['foo','posts'],
        /*
        async asyncData(){
        return {
        foo:'bar'
        }
        }*/
        }

上下文对象 Context

可通过官方文档 API context 来了解该对象的所有属性和方法。

通过案例理解Context使用:

这里给这些文章标题加了链接,点击标题跳转到文章页面
视图

  • components/Foo.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     <template>
    <div>
    <h1>component foo page</h1>
    <ul>
    <li v-for="item in posts" :key="item.id">
    <nuxt-link :to="/article/+item.id">{{item.title}}</nuxt-link>
    </li>
    </ul>
    </div>
    </template>
    <script>
    export default {
    props:['foo','posts'],
    }
    </script>
  • pages/article/_id.vue

详情页面使用动态路由,通过对应的id,获取文章内容展示到页面,这里会有个问题,由于asyncData没有 this,无法获取到路由的参数id,那么此处就可以使用context

视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div>
<h1>article page</h1>
<h3>{{ article.body }}</h3>
</div>
</template>
<script>
import axios from 'axios'
export default {
name:'articlePage',
async asyncData(context){
console.log(context);
const res = await axios({
method:'GET',
url:'http://localhost:3000/app/data.json'
})
const id =Number(context.params.id)
return {
article:res.data.posts.find(item=>item.id === id)
}
}
}
</script>

NuxtJS 生命周期

视图