基于 Vue.js创建服务端渲染 (SSR) 应用:Nuxt.js 介绍

本文由清尘发表于2019-05-02 19:56最后修改于2019-05-03属于javascript分类

Github: https://github.com/shine130/shine-nuxt

Tgit私有仓库

HTTP:https://git.code.tencent.com/testpro/shine-nuxt.git
SSH:git@git.code.tencent.com:testpro/shine-nuxt.git


===============

Nuxt.js:路由与视图

创建与运行基于 Nuxt.js 框架的 Vue.js 项目

进入想要保存项目的目录下执行

npx create-nuxt-app shine-nuxt

用这个工具创建应用的时候会问我们几个问题 .. Project name .. 项目的名字 ..

Project description ,项目的描述 ..

是否要使用服务端框架 .. 暂时我们先选择 none ..

要使用哪个 UI 框架 .. 我们可以先用一下 bootstrap ..

然后是 Choose rendering mode ,选择渲染模式 .. Universal 就是使用了服务器渲染的应用,Single Page App,单页面应用不使用服务器渲染 ..

这里选择这个 Universal ..

是否使用 axios 模块 .. 它是一个 Http 客户端,可以发送 http 请求 .. 选择 yes ..

是否使用 eslint … 是的 …

要不要使用 prettier .. 可以试一下 ..

Author name .. 项目作者的名字 ..

选择包管理工具 .. 可以用一下 npm ..

create-nuxt-app 会根据我们的回答去创建一个基于 Nuxt 框架的 Vue.js 项目 ..

进入刚才生成的项目目录运行

npm run dev

它会创建一个服务器 http://localhost:3000/

页面上出现了一个错误 … 用编辑器打开项目
找到 pages .. index.vue 这个文件 ..
刚才在页面上看到的提示是因为项目里用了 prettier .. 提示我们要删除掉 style 标签下面的这个回车 ..
保存一下文件 .. 再回到浏览器 .. 页面上不再出现错误提示 .. 现在你看到的就是 Nuxt 项目的欢迎界面 .. 这个界面就是 pages 目录下面的 index.vue 里面定义的 ..

路由:自动生成路由

Nuxt 可以根据 pages 目录里的 vue 文件自动给我们生成对应的路由 .. 比如我现在想添加一个 posts 页面,可以显示内容的列表 ..

新建pages/posts/index.vue

<template>
  <div class="container">
    <h1 class="display-1">List</h1>
  </div>
</template>

这时打开http://localhost:3000/posts 就可以看到刚刚新建的List页面

在项目里面,有个 .nuxt 目录 .. 打开里面的 router.js …

浏览到文件的底部 .. 这个 routes 里面的东西就是应用的路由 .. 你会看到有一个叫 posts 的路由,地址是 /posts .. 对应的组件是这个 ..

复制一下这个组件的名字,再搜索一下 ..

你会发现这个组件对应的就是 pages… posts 里面的这个 index.vue ..

新建一个pages/posts/hello.vue

<template>
  <div class="container">
    <h1 class="display-1">Hello</h1>
  </div>
</template>

访问http://localhost:3000/posts/hello 就可以看到刚才新建的hello页面

路由链接:nuxt-link

修改默认的首页pages/index.vue ,加上链接

<template>
  <div class="container">
    <h1 class="display-1">
      <nuxt-link to="/posts">shine</nuxt-link>
    </h1>
  </div>
</template>
......

这时访问http://localhost:3000/ 点击链接可以跳转到List页面,只是List页面上的文字List也被首页的样式影响了。

在pages/index.vue上 . 找到 style .. 可以给它添加一个 scoped .. 意思是这里的样式只在当前这个组件上有效 ..

......
<style scoped>

</style>
......

刷新一下浏览器再次点击首页链接,这次打开的时候, 首页上的样式就不会影响到这个页面了

动态路由

Vue 应用的路由里的动态部分,我们可以创建一些名字里面用下划线开头的目录或者文件 .. 比如我打算创建一个路由,地址是 /posts/id .. posts 后面的 id 是地址里的动态部分..

新建一个文件pages/posts/_id.vue

<template>
  <div class="container">
    <h1 class="display-1">
      Post {{$route.params.id}}
    </h1>
  </div>
</template>

访问http://localhost:3000/posts/3 可以看到输出的页面

验证路由里的参数:validate

Nuxt 里面提供了一个验证方法,可以验证路由里的参数 .. 比如你可以规定参数值的类型必须是数字,或者字符串 ..

在这个 posts .. _id.vue 里面 .. 添加一组 script .. 在默认的导出的东西里面,添加一个 validate .. 这个方法可以验证路由里的参数 …

方法的参数里面,把 params 解构出来 .. 方法 return 的是 true 或者 false .. 如果 return false .. 就会显示 404 页面 ..

先写一个正则表达式 .. 意思就是字符串里面都是数字 .. 用一下 test 方法,要测试的是 params 里的 id 参数的值 …

......
<script>
  export default {
    validate({ params }) {
      return /^\d+$/.test(params.id)
    }
  }
</script>

回到浏览器预览一下 .. 访问一下 //localhost:3000/posts/3 .. 页面会正常显示,因为路由地址里的 id 参数的值是个数字 ..

再试一下,http://localhost:3000/posts/shine 让 id 参数的值是一个字符串 .. 这次会显示默认的 404 页面 ..

视图:应用模板

Nuxt 应用的模板,就是应用页面的主要的框架,比如它的 head 标签里的内容,主体的内容等等。如果你想定制一下这个默认的模板,可以在应用的根目录下面,创建一个 html 文件,名字是 app.html ..

如果你想定制页面的这个基本的结构,可以修改一下这个 app.html …

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

然后需要停止一下运行的应用 … 重新再运行一下 ..

改变应用的页面布局可以定制一下应用的 Layouts,就是布局.. 如果页面不特别指定要使用的布局,会使用一个默认的布局 .. 修改这个默认的布局,在项目下面的 layouts 目录里面,添加一个 default.vue ..

修改这个default.vue

<template>
  <div class="container">
    <nuxt />
  </div>
</template>

现在修改一下posts/index.vue,去掉container布局,因为我们已经在默认布局里面加入了container布局

<template>
  <div>
    <h1 class="display-1">List</h1>
  </div>
</template>

现在访问一下http://localhost:3000/posts,和之前的效果是一样的

再修改一下默认的布局,现在给布局添加一个导航

<template>
  <div>
    <nav class="navbar navbar-light bg-light">
      <div class="container">
        <a href="" class="navbar-brand">shine</a>
      </div>
    </nav>
    <div class="container">
      <nuxt />
    </div>
  </div>
</template>

<style>
.navbar-brand {
  letter-spacing: 2px;
}
</style>

访问一下 http://localhost:3000/posts 可以看到刚才增加的导航

视图:自定义布局

页面不指定要使用的布局就会使用默认的布局 .. 下面我们可以再去创建一个自定义的布局 … 放在项目的 layouts 目录的下面 .. 名字可以是 fullscreen.vue …

<template>
  <div>
    <nuxt />
  </div>
</template>

访问一下 http://localhost:3000/posts/hello
hello 是我们之前创建过的一个页面,这个页面没有特别指定要使用的布局,所以它会使用默认的布局 … 打开这个页面 … 添加一组 script …

......
<script>
  export default {
    layout: 'fullscreen'
  }
</script>

再次访问http://localhost:3000/posts/hello 就会用我们自定义的布局,没有导航栏。

Nuxt.js:异步数据

搭建假的 RESTful 接口(json-server)

全局安装json-server

npm install json-server --global

在项目的assets目录下新建一个db.json(内容可以在 https://resources.ninghao.net/demo/posts.json 找到)
在项目的根目录下执行

json-server --watch assets/db.json --port 3333

这样就会在http://localhost:3333/ 创建一个服务接口

载入页面初始数据(asyncData)

载入页面组件之前,会调用 asyncData 这个方法,你可以在这个方法里请求页面的初始数据。比如在 posts/index.vue 这个组件里面 ..

<template>
  <div>
    <h1 class="display-1">List</h1>
    <div v-for="post in posts" :key="post.id">
      {{post.title}}
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  async asyncData(){
    let {data} = await axios.get('http://localhost:3333/posts')
    return {posts:data}
  }
}
</script>

动态路由页面上的初始数据

修改pages/posts/_id.vue

<template>
  <div class="container">
    <h1 class="display-1">
     {{post.title}}
    </h1>
    <div>
      {{post.description}}
    </div>
  </div>
</template>

<script>
import axios from "axios";

export default {
  validate({params}){
    return /^\d+$/.test(params.id)
  },

  async asyncData({params}){
    let {data} = await axios.get(`http://localhost:3333/posts/${params.id}`)
    return {post:data}
  }

}
</script>

然后访问一下 http://localhost:3000/posts/1 页面上会显示这个 id 的具体的内容 ..

处理错误:显示错误页面

访问一个不存在的页面 http://localhost:3000/posts/60 会出现一个错误

可以处理一下这个页面

修改一下pages/posts/_id.vue

......
<script>
import axios from "axios";

export default {
  validate({params}){
    return /^\d+$/.test(params.id)
  },

  async asyncData({params,error}){
    try {
    let {data} = await axios.get(`http://localhost:3333/posts/${params.id}`)
    return {post:data}

    }catch(e){
      error({statusCode:404,message:'Post not found.'})
    }
  }

}
</script>

重新访问 http://localhost:3000/posts/60 会显示一个404错误页面

重新设计内容页面

修改列表页 pages/posts/index.vue

<template>
  <div>
    <h1 class="display-1 my-5">List</h1>
    <div class="row justify-content-center">
    <div class="col-md-6" v-for="post in posts" :key="post.id">
      <nuxt-link :to="{name:'posts-id',params:{id:post.id}}">
        <div class="card my-3">
          <img :src="post.imageUrl" :alt="post.title" class="card-img-top">
          <div class="cart-body">
            <h5 class="card-title">{{post.title}}</h5>
            <h6 class="card-subtitle mb-2 text-black-50">{{post.author}}</h6>
          </div>
        </div>
      </nuxt-link>
    </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  async asyncData(){
    let {data} = await axios.get('http://localhost:3333/posts')
    return {posts:data}
  }
}
</script>

修改内容页 pages/posts/_id.vue

<template>
  <div class="my-5">
    <h1 class="display-1 my-5">
     {{post.title}}
    </h1>
    <img :src="post.imageUrl" :alt="post.title" class="rounded w-100 mb-3">
    <div>
      {{post.description}}
    </div>
  </div>
</template>

<script>
import axios from "axios";

export default {
  validate({params}){
    return /^\d+$/.test(params.id)
  },

  async asyncData({params,error}){
    try {
    let {data} = await axios.get(`http://localhost:3333/posts/${params.id}`)
    return {post:data}

    }catch(e){
      error({statusCode:404,message:'Post not found.'})
    }
  }

}
</script>


在导航栏添加菜单项目

修改layouts/default.vue

<template>
  <div>
    <nav class="navbar navbar-light bg-light">
      <div class="container">
        <nuxt-link :to="{name:'index'}" class="navbar-brand">
          shine
        </nuxt-link>
        <ul class="navbar-nav mr-auto">
          <li class="nav-item">
            <nuxt-link :to="{name:'posts'}" class="nav-link">Posts</nuxt-link>
          </li>
        </ul>
      </div>
    </nav>
    <div class="container">
      <nuxt />
    </div>
  </div>
</template>

<style>
.navbar-brand {
  letter-spacing: 2px;
  font-weight: bold;
}
.navbar-light .navbar-nav .nuxt-link-exact-active,
.navbar-light .navbar-nav .nuxt-link-exact-active:focus{
  color: red;
}
</style>

页面标题

修改pages/posts/index.vue

......
import axios from 'axios';

export default {
  async asyncData(){
    let {data} = await axios.get('http://localhost:3333/posts')
    return {posts:data}
  },

  head(){
    return {
      title:'Posts'
    }
  }

}

用同样的方法修改pages/index.vue 的标题

export default {
  components: {
    Logo
  },

  head(){
    return {
      title:'shine'
    }
  }

}

修改详情页pages/posts/_id.vue的标题

......
  head(){
    return {
      title:this.post.title
    }
  }
......

添加 dev-server 命令

现在你在页面上的看到的这些内容是一个假的 Rest 接口提供的 .. 用的是一个叫 json-server 的工具 .. 先停止一下 ..

这个工具可以作为项目的开发依赖 .. 安装一下 json-server … –save-dev 保存在开发依赖里面 ..

安装到项目的开发依赖

npm install json-server --save-dev

修改package.json 在scripts里面增加一个”dev-server”

......
  "scripts": {
  "dev": "nuxt",
  "dev-server": "./node_modules/.bin/json-server --watch assets/db.json --port 3333",
......

启动json-server

npm run dev-server

Nuxt.js:数据管理

Vuex:添加 Store

在 Nuxt 项目里面,有个 store 目录,里面存储的就是应用的 Store .. 你可以根据应用的需求去创建一些 Store .. 比如我们先添加一个演示用的 Store .. 在这个目录的下面,新建一个 demo.js ..

export const state = () => ({
  pageName:'Vuex'
})

重新启动一下项目,在浏览器上可以先检查一下应用的 Store ..

打开开发者工具 … Vue … 再打开 Vuex 看一下
这里会提示应用里面已经有了 Vuex Store .. 这里有个状态叫 demo .. 里面有个 pageName 属性 .. 值是 Vuex .. 这个状态就是在 demo.js 这个 store 里面添加的 ..

Vuex:使用 State

新建一个文件pages/demo/vuex.vue

<template>
  <div class="container">
    <h1 class="display-1 my-5">{{pageName}}</h1>
    <span class="badge badge-pill badge-primary">Count {{count}}</span>
  </div>
</template>

<script>
export default {
  computed:{
    pageName(){
      return this.$store.state.demo.pageName
    },
    count(){
      return this.$store.state.demo.count
    }
  }
}
</script>


修改store/demo.js

export const state = () => ({
  pageName:'Vuex',
  count:0
})

用浏览器访问 http://localhost:3000/demo/vuex 页面上会显示一个小标签 .. 这里的数字 0 就是 Demo Store 里的 count 这个 State 的值 .. 现在 Demo 的 State 里面有个 count .. 还有一个 pageName ..

Vuex:提交 Mutations

Mutations 是用来修改 State 用的一些东西… 比如我想在用户按了页面上这个小标签以后,可以让 count 这个 State 的值增加 1 .. 这样就可以去添加一个 Mutation,做的事情就是让 count 的值加上
1 ..

修改一下store/demo.js

export const state = () => ({
  pageName:'Vuex',
  count:0
})

export const mutations = {
  add(state){
    state.count++
  }
}

修改pages/demo/vuex.vue

<template>
  <div class="container">
    <h1 class="display-1 my-5">{{pageName}}</h1>
    <span @click="add" class="badge badge-pill badge-primary">Count {{count}}</span>
  </div>
</template>

<script>
export default {
  computed:{
    pageName(){
      return this.$store.state.demo.pageName
    },
    count(){
      return this.$store.state.demo.count
    }
  },
  methods:{
    add(){
      this.$store.commit('demo/add')
    }
  }
}
</script>

访问http://localhost:3000/demo/vuex 点击count小标签就会增加1

你会发现这个 Muation 的 payload 没有定义 .. 回来再编辑一下 add muation .. muation 的第二个参数就是 payload, payload 就是 muation 里面可以使用的数据 ..
比如我们让 add 这个 mutation 的 payload 叫 number ..
然后让 state.count 等于 state.count 加上这个 number 的值 ..

修改store/demo.js

export const state = () => ({
  pageName:'Vuex',
  count:0
})

export const mutations = {
  add(state,number){
    state.count = state.count + number
  }
}

回到 vuex 这个组件 .. 修改一下 add 方法里面 commit 的这个 mutation .. 给它一个 payload,比如数字 2 ..

修改pages/demo/vuex.vue

<template>
  <div class="container">
    <h1 class="display-1 my-5">{{pageName}}</h1>
    <span @click="add" class="badge badge-pill badge-primary">Count {{count}}</span>
  </div>
</template>

<script>
  export default {
    computed: {
      pageName() {
        return this.$store.state.demo.pageName
      },
      count() {
        return this.$store.state.demo.count
      }
    },
    methods: {
      add() {
        this.$store.commit('demo/add', 2)
      }
    }
  }
</script>

再回到浏览器试一下 .. 按一下页面上的小标签 .. 现在每按一次,count 的值会加上 2 ..

这个 Muation 的 Payload 是数字 2 .. Type 是 demo/add ..

Vuex:指派 Actions

在应用的组件里面可以指派要执行的动作 .. 就是 Action .. 在动作里面可以提交修改,也就是 commit mutation .. Action 可以是异步的, mutation 只能是同步的 ..

修改store/demo.js

export const state = () => ({
  pageName:'Vuex',
  count:0
})

export const mutations = {
  add(state,number){
    state.count = state.count + number
  }
}

export const actions = {
  addAction(context,number){
    context.commit('add',number)
  }
}

修改pages/demo/vuex.vue

<template>
  <div class="container">
    <h1 class="display-1 my-5">{{pageName}}</h1>
    <span @click="addAction" class="badge badge-pill badge-primary">Count {{count}}</span>
  </div>
</template>

<script>
export default {
  computed:{
    pageName(){
      return this.$store.state.demo.pageName
    },
    count(){
      return this.$store.state.demo.count
    }
  },
  methods:{
    add(){
      this.$store.commit('demo/add',2)
    },
    addAction(){
      this.$store.dispatch('demo/addAction',3)
    }
  }
}
</script>


访问一下http://localhost:3000/demo/vuex 按一下小标签 .. 每按一次,标签上的数字都会增加 3 ..

动作里面可以包含异步的动作 .. 我们可以使用一个简单的 setTimeout 模拟一下 .. 在这个 addAction 动作里面 .. 用一个 setTimeout … 等待时间是 1000 毫秒 .. 就是 1 秒钟的时间 ..
1 秒以后,要执行的就是提交一个 add 修改 ..

修改store/demo.js

export const state = () => ({
  pageName:'Vuex',
  count:0
})

export const mutations = {
  add(state,number){
    state.count = state.count + number
  }
}

export const actions = {
  addAction(context,number){
    setTimeout(() => {
      context.commit('add', number)
    },1000);
  }
}

再试一下 .. 按一下小标签 .. 执行了组件里的 addAction 方法 .. 这个方法里指派了一个 demo/addAction 动作 .. 带的 payload 是数字 3 ..

addAction 这个动作会等待 1 秒钟 .. 然后才会 commit 一个 add 修改 .. 这个 add 修改的是 store 里的 count 这个 state .. 它会让 count 的值加上修改里的 payload 的值
..

用 fetch 方法载入 store 数据

页面显示之前会执行 fetch 方法,在这个方法里我们可以请求后端服务接口,得到数据以后可以提交一个修改 .. 比如这个列表页面,现在是直接在组件里请求服务接口,得到数据以后把数据作为组件的状态 .. 下面我们可以使用 vuex
的方式改造一下 ..

新建一个store/post.js

export const state = () => ({
  list:[]
})

export const mutations = {
  setList(state,list){
    state.list = list
  }
}

修改pages/posts/index.vue

<template>
  <div>
    <h1 class="display-1 my-5">List</h1>
    <div class="row justify-content-center">
    <div class="col-md-6" v-for="post in posts" :key="post.id">
      <nuxt-link :to="{name:'posts-id',params:{id:post.id}}">
        <div class="card my-3">
          <img :src="post.imageUrl" :alt="post.title" class="card-img-top">
          <div class="cart-body">
            <h5 class="card-title">{{post.title}}</h5>
            <h6 class="card-subtitle mb-2 text-black-50">{{post.author}}</h6>
          </div>
        </div>
      </nuxt-link>
    </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  // async asyncData(){
  //   let {data} = await axios.get('http://localhost:3333/posts')
  //   return {posts:data}
  // },
  async fetch({store}){
    const response = await axios.get('http://localhost:3333/posts')
    store.commit('post/setList',response.data)
  },
  computed:{
    posts(){
      return this.$store.state.post.list
    }
  },
  head(){
    return {
      title:'Posts'
    }
  }

}
</script>

回到内容列表页面 .. 页面上仍然会显示之前的这组内容列表 .. 不过现在这个页面上使用的数据并不是组件里的 State .. 而是 posts 这个 store 里的 list ..

删除内容项目的动作

store/post.js

import axios from 'axios'

export const state = () => ({
  list:[]
})

export const mutations = {
  setList(state,list){
    state.list = list
  }
}

export const actions = {
  async destroyAction(context,id){
    await axios.delete(`http://localhost:3333/posts/${id}`)
  }
}

修改pages/posts/index.vue

<template>
  <div>
    <h1 class="display-1 my-5">List</h1>
    <div class="row justify-content-center">
    <div class="col-md-6" v-for="post in posts" :key="post.id">
        <div class="card my-3">
          <nuxt-link :to="{name:'posts-id',params:{id:post.id}}">
          <img :src="post.imageUrl" :alt="post.title" class="card-img-top">
          <div class="cart-body">
            <h5 class="card-title">{{post.title}}</h5>
            <h6 class="card-subtitle mb-2 text-black-50">{{post.author}}</h6>
          </div>
            </nuxt-link>
            <div class="cart-footer">
              <button @click="destroyAction(post.id)" class="btn btn-link">Delete</button>
            </div>
        </div>
    </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  // async asyncData(){
  //   let {data} = await axios.get('http://localhost:3333/posts')
  //   return {posts:data}
  // },
  async fetch({store}){
    const response = await axios.get('http://localhost:3333/posts')
    store.commit('post/setList',response.data)
  },
  computed:{
    posts(){
      return this.$store.state.post.list
    }
  },
  methods:{
    destroyAction(id){
      this.$store.dispatch('post/destroyAction',id)
    }
  },
  head(){
    return {
      title:'Posts'
    }
  }

}
</script>

在应用里我们创建的后端服务其实是一个演示用的 RESTful 接口 .. 用的是 json-server 创建的 .. assets 下面的 db.json ,这个文件就相当于是后端服务接口的数据库 ..
内容列表页面上显示的东西都在这个文件里面 .. 先复制一下这个文件里面的内容 .. 因为一会儿在前台执行删除动作的时候会从这个文件里面把内容删除掉 ..

回到内容列表页面 .. 按一下内容项目下面的这个 Delete 按钮 .. 执行了 destroyAction 方法,这个方法会指派一个 destroyAction 动作,在这个动作里面会请求服务接口,把某个内容项目删除掉 ..

刷新一下页面 .. 现在这个列表页面上就不会再显示之前被删除的内容项目了 .. 再回到 db.json 这个文件里面看一下 .. 你会发现,id 号是 1 的内容项目已经从这个文件里面删除掉了 ..

把刚才复制的内容粘贴过来 ..恢复一下这个文件

移除内容项目的修改

按了内容项目下面的删除按钮以后,会执行删除动作,把内容项目从后端服务那里删除掉 .. 现在我想让页面立即做出反应,去掉被删除掉的内容项目 ..

我们可以在执行了删除动作以后,重新向后端服务请求一个内容列表 .. 或者直接在 store 的 state 里面,把删除的内容项目从列表里面去掉 .. 下面我们可以试一下这种方法 ..

修改一下store/post.js

import axios from 'axios'

export const state = () => ({
  list:[]
})

export const mutations = {
  setList(state,list){
    state.list = list
  },
  removeItem(state,id){
    state.list = state.list.filter(item => {
      return item.id != id
    })
  }
}

export const actions = {
  async destroyAction(context,id){
    await axios.delete(`http://localhost:3333/posts/${id}`)
    context.commit('removeItem',id)
  }
}

回到前台页面 .. 按一下内容项目下面的 Delete .. 会执行 destroyAction 动作 .. 把内容项目从后端服务那里删除掉 .. 接着会 commit 一个 removeItem 修改,把内容项目从列表里面去掉