HOME/Articles/

Vue后端渲染之Nuxt.js

Article Outline

最近看大佬们闲聊说起页面优化,都觉得后端渲染效果立竿见影,就来学习一波NuxtJS。

<!--more-->

新建Nuxt项目

可以直接使用官方提供的[create-nuxt-app](https://github.com/nuxt/create-nuxt-app)来进行项目创建:


npx create-nuxt-app <项目名>

经过一些配置项的选择之后等待完成安装即可

另外也可以手动创建项目,创建好package.json:


{

  "name": "my-app",

  "scripts": {

    "dev": "nuxt"

  }

}

然后运行:


npm install --save nuxt

然后即可创建pages文件夹,创建一个pages/index.vue,启动项目npm run dev即可在http://localhost:3000访问到

目录结构

目录结构都是一些约定大于配置的东西,只有一些注意项:

  • components文件夹下的组件不会像页面组件那样有asyncData方法的特性

  • nuxt.config.js文件用于组织Nuxt.js应用的个性化配置,以便覆盖默认配置

另外还有约定的别名:

  • ~@ srcDir

  • ~~@@``rootDir

默认情况下,srcDirrootDir 相同。

路由

一直在强调,NuxtJS约定大于配置的,在pages文件夹下创建的组件都会被视为页面组件,根据嵌套关系和文件命名会自动生成路由配置文件

假设现有:


pages/

--| user/

-----| index.vue

-----| one.vue

--| index.vue

自动生成的路由配置如下:


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'

    }

  ]

}

当有以下划线作为前缀的Vue文件时,会被视为动态路由,如:_id.vue

另外一提,Nuxt提供了一个新的路由跳转组件<nuxt-link>, 比起Vue<router-link>新增了一些功能,建议使用<nuxt-link>

来看一个动态路由和路由参数校验的例子:

假设现在在Pages/About/下有一个_id.vue组件为:


<template>

    <div class="id">

        dynamic router params.id: {{id}}

    </div>

</template>



<script>

    export default {

        name: "dynamicId",

        asyncData({params}) {

            return {

                id: params.id // 在asyncData里面取到id作为本组件的data

            }

        },

        validate ({ params }) { // Nuxt.js提供了validate()方法可以让你在动态路由组件中定义参数校验方法。

            // 假设id不能大于100

            return Number(params.id) <= 100;

        }

    }

</script>

当我们访问http://localhost:3000/about/100时一切良好,当访问http://localhost:3000/about/200时提示页面找不到。

后续还有嵌套路由和动态嵌套路由,基本原理跟Vue是一模一样的

自动生成的router文件可以查看.nuxt/router.js

路由的全局守卫

关于全局守卫,完全可以当做一个插件来进行引用,我们可以建立:/plugins/router.js


export default ({app}) => {

    app.router.beforeEach((to,form,next) => {

        console.log(to)

        next();

    });

}

然后在nuxt.config.js中增加配置:


plugins: [

    '@/plugins/element-ui',

    '@/plugins/router' // 增加全局守卫插件

],

layouts

默认使用的是layouts/default.vue,可以理解为这个相当于Vue中的App.vue,我们可以通过修改这个组件来添加一些全局的东西。

另外也可以新增新的布局文件,比如我们可以新增一个layouts/about.vue:

layouts/about.vue:


<template>

    <div>

        <h1>This is About Page</h1>

        <nuxt />

    </div>

</template>



<script>

    export default {

        name: "about"

    }

</script>

然后在pages/About/index.vue中指定布局即可


<template>

    <div class="aboutIndex">

        pages/About/index.vue

    </div>

</template>



<script>

    export default {

        layout: "about" // 指定使用的布局

    }

</script>

定义了布局之后,任何指定该布局的页面都会使用该布局

当然也可以自定义404页面:

我们可以创建一个特殊的layouts/error.vue,虽然此文件放在 layouts 文件夹中, 但应该将它看作是一个 页面(page),且error.vue不需要包含<nuxt/>标签。

异步数据

asyncData()

asyncData()方法可以在设置组件的数据之前能一步获取或者处理数据,最关键的是其支持异步数据.

等待异步结果返回之后,Nuxt.js会将asyncData返回的数据融合组件data方法返回的数据一并返回给当前组件。

asyncData方法支持多种异步写法,返回一个Promise或者async/await都是可以的

返回一个Promise


<script>

    let p = new Promise((resolve) => {

        setTimeout(() => {

            resolve("DeeJay");

        }, 3000)

    });

    export default {

        asyncData ({params}) {

            return p.then(res => {

                return {

                    name: res,

                    id: params.id

                }

            })

        },

    }

</script>

async/await写法:


<script>

    let p = new Promise((resolve) => {

        setTimeout(() => {

            resolve("DeeJay");

        }, 3000)

    });

    export default {

        async asyncData ({params}) {

            let res = await p;

            return {

                name: res,
打开新页
                id: params.id

            }

        },

    }

</script>

此外asyncData还提供了callback写法,对于第二个参数callback,我们可以打印出来为:


// 内部的callback实现,所以我们第一个参数如果为null则说明成功,如果不为null则说明在做错误处理

function (err, data) {

    if (err) {

      context.error(err);

    }



    data = data || {};

    resolve(data);

}

具体的用法就是在Promise写法或者async/await写法中不进行return,直接调用callback即可:


<script>

    let p = new Promise((resolve) => {

        setTimeout(() => {

            resolve("DeeJay");

        }, 3000)

    });

    export default {

        async asyncData ({params}, cb) {

            let res = await p;

            cb(null, {

                name: res,

                id: params.id

            });

            // return {

            //     name: res,

            //     id: params.id

            // }

        },

    }

</script>

这种写法多用于错误处理的情况

asyncData中的第一个参数,即上下文对象

上文提到的asyncData中的第一个参数,即为上下文对象,其内部详细的属性见这里

我们一般都只通过解构获取想要获得的对象,比如req res params以及error

错误处理

错误处理有2种方法:

  1. 上文提到的上下文对象中的error即为我们作为错误处理的方法

  2. 使用callback

使用例子:

context.error:


<script>

    let p = new Promise((resolve, reject) => {

        setTimeout(() => {

            reject();

        }, 3000)

    });

    export default {

        async asyncData ({params, error}) {

            return p.then(res => {

                return {

                    name: res,

                    id: params.id

                }

            }).catch(() => {

                error({ statusCode: 404, message: 'Error msg!' })

            })

        },

    }

</script>

callback:


<script>

    let p = new Promise((resolve, reject) => {

        setTimeout(() => {

            reject();

        }, 3000)

    });

    export default {

        async asyncData ({params}, callback) {

            return p.then(res => {

                return {

                    name: res,

                    id: params.id

                }

            }).catch(() => {

                callback({ statusCode: 404, message: 'Error msg!' })

            })

        },

    }

</script>

插件

对于第三方的插件,直接npm install之后在组件内部引用使用即可

但是对于Vue的插件,需要在/plugins/下建立一个js文件,比如我们要引入element-ui

先建立plugins/element-ui.js:


// plugins/element-ui.js

import Vue from 'vue'

import Element from 'element-ui'

import locale from 'element-ui/lib/locale/lang/en'

Vue.use(Element, { locale })

然后还需要在nuxt.config.js下进行配置:


// nuxt.config.js

export default {

    // balabala 其他配置

    plugins: [

        '@/plugins/element-ui',

    ],

}

Nuxt中使用Vuex

依旧是约定大于配置:Nuxt会自动找到/store目录,进行引用Vuex等工作

在此只介绍Nuxt官方推荐使用方式

/store目录下的每个.js文件会被转换成为状态树指定命名的子模块 (当然,index是根模块)

state的值应该始终是function,为了避免返回引用类型,会导致多个实例相互影响。

来看具体的使用例子:


// /store/index.js

export const state = () => ({

    counter: 0

});



export const mutations = {

    increment(state) {
打开新页
        state.counter++;

    }

};

再创建一个子模块/store/otherModule.js


// /store/otherModule.js

export const state = () => ({

    otherCounter: 0

});



export const mutations = {

    add(state, number) {

        state.otherCounter += number;

    }

};

此时nuxt自动生成的Store为:


new Vuex.Store({

  state: () => ({

    counter: 0

  }),

  mutations: {

    increment (state) {

      state.counter++

    }

  },

  modules: {

    otherModule: {

      namespaced: true,

      state: () => ({

        otherCounter: 0

      }),
      mutations: {

        add(state, number) {

            state.otherCounter += number;

        }

      }

    }

  }

})

在组件内部我们就可以进行使用了:


<template>

    <div class="id">

        <div>dynamic router params.id: {{id}}</div>

        <div>counter: {{counter}}</div>

        <div>otherCounter: {{otherCounter}}</div>

        <el-button @click="addCounter">click to plus one to counter</el-button>

        <el-button @click="addOtherCounter">click to plus one to otherCounter</el-button>

    </div>

</template>

<script>

    import { mapMutations } from 'vuex'

    export default {

        async asyncData ({params}) {

            return {

                id: params.id

            }

        },

        computed: {

            counter() {

                return this.$store.state.counter

            },

            otherCounter() {

      mutations: {

        add(state, number) {

            state.otherCounter += number;

        }

      }

    }

  }

})

在组件内部我们就可以进行使用了:


<template>

    <div class="id">

        <div>dynamic router params.id: {{id}}</div>

        <div>counter: {{counter}}</div>

        <div>otherCounter: {{otherCounter}}</div>

        <el-button @click="addCounter">click to plus one to counter</el-button>

        <el-button @click="addOtherCounter">click to plus one to otherCounter</el-button>

    </div>

</template>

<script>

    import { mapMutations } from 'vuex'

    export default {

        async asyncData ({params}) {

            return {

                id: params.id

            }

        },

        computed: {

            counter() {

                return this.$store.state.counter

            },

            otherCounter() {

                return this.$store.state.otherModule.otherCounter

            }

        },

        methods: {

            ...mapMutations({

                increment: "increment",

                add: 'otherModule/add',

            }),

            addCounter() {

                this.increment();

            },

            addOtherCounter() {

                this.add(1);

            }

        }

    }

</script>

Nuxt支持TypeScript

笔者在这里踩过坑,所以注明以下配置都是针对Nuxt 2.10及以上的版本进行切换到TS,对于低版本的,安装@nuxt/typescriptts-node即可

对于Nuxt 2.10及以上的版本来说,想要支持TypeScript 需要安装@nuxt/types @nuxt/typescript-build以及@nuxt/typescript-runtime

其中@nuxt/typescript-build@nuxt/typescript-runtime都已经集成了@nuxt/types,没必要进行单独安装,另外@nuxt/typescript-runtime是可选安装的。

@nuxt/typescript-build

如果只想对于layouts,components,plugins以及middlewares这几个文件夹下的做ts支持的话,我们只需要安装@nuxt/typescript-build即可

npm install --save-dev @nuxt/typescript-build

然后修改nuxt.config.js:

// nuxt.config.js
export default {
  buildModules: ['@nuxt/typescript-build']
}

最后创建一个tsconfig.json:

// tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "lib": [
      "esnext",
      "esnext.asynciterable",
      "dom"
    ],
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "allowJs": true,
    "sourceMap": true,
    "strict": true,
    "noEmit": true,
    "baseUrl": ".",
    "paths": {
      "~/*": [
        "./*"
      ],
      "@/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxt/types"
    ]
  },
  "exclude": [
    "node_modules"
  ]
}

这边要注明一点,如果最后是想要通过class API的风格进行编写组件的话,需要引入vue-property-decorator,所以在tsconfig.json中需要加上"experimentalDecorators": true,这个选项,否则会有报错提示。

@nuxt/typescript-runtime

@nuxt/typescript-runtime针对的是那些不会被webpack编译的文件,比如说nuxt.config,还有本地的模块以及serverMiddlewares等。

其内部使用了ts-node进行编译这些文件。

npm install @nuxt/typescript-runtime

安装完成后需要修改npm scripts:

"scripts": {
  "dev": "nuxt-ts",
  "build": "nuxt-ts build",
  "generate": "nuxt-ts generate",
  "start": "nuxt-ts start"
},

然后就可以进行开发了,对于组件内部的写法,有传统的optional API写法,也有Vue新出的composition API写法,以及使用vue-property-decoratorClass API

具体可以见官方的cookbook

笔者是习惯写class API的,放一个实例上来:

<template>
    <div id="App">
        <div>{{msg}}</div>
        <div>{{msg2}}</div>
    </div>
</template>

<script lang="ts">
    import {Vue, Component} from "vue-property-decorator";
    import { Context } from "@nuxt/types";

    let p = new Promise<string>((resolve => {
        setTimeout(() => {
            resolve("Hi, this is msg from Promise");
        }, 3000);
    }));

    @Component({
        asyncData(ctx: Context) {
            return p.then(res => {
                return {
                    msg2: res
                }
            })
        }
    })
    export default class App extends Vue {
        msg: string = "msg from data";
    }
</script>

其余SSR替代方案

由于已有Vue项目迁移到NuxtJS项目需要较大工作量的重构(二者的规则约定差别较大)

所以如果需要进行SSR的页面较少、页面内容实时性要求较低的话,可以考虑:

另附上相关阅读: