服务端渲染Nuxt.js

NuxtJs

前言:学完 NuxtJs 发现太好用了,完全不需要担心乱七八槽的配置,全部自己生成,很良心有没有♥。项目中一般 React 都是纯手配,可以看成手动挡的车🚗,除了需要完全自己配置而且概念比较多,Vue 好一点,插件都是官方指定比较稳定容易上手👆,没有选择恐惧症😱,尤其是 cli 比较好用,可类比为半自动档的车,但是如果你用完 NuxtJs 会发现完全是自动挡。身为开发者的车手如果技术老成肯定喜欢手动挡去开赛车飙技术,新手肯定是喜欢自动挡滴了 。当你学会了 NuxtJs 框架,恭喜你点亮了全部 Vue 技能。🍾

一、为什么需要服务端渲染

大多数单页面(SPA)应用都只是有一个div入口标签,如果项目只是内部或固定的人群使用是没问题的,但是如果涉及到新闻等类似面向普通用户的网站,就必须的考虑到 SEO,Nuxt.js 的服务端渲染就是用来解决 Vue 项目的 SEO 问题。虽然 Nuxt.js 也能创建单页面应用。

二、什么是 SSR ( 服务端渲染 )

SSR 即服务器渲染,就是 (Vue) 页面在服务器端渲染完生成 html 文件,浏览器访问时就将 html 文件直接传递给浏览器。因为数据在服务端就渲染完成,所以减少了浏览器的 HTTP 请求,看不见接口更加安全,减少了首屏加载时间。

三、安装

安装 Node 的前提下:

C:\Users\hpzhan\Desktop\test>npx create-nuxt-app app

create-nuxt-app v2.15.0
✨  Generating Nuxt.js project in app
? Project name app_test
? Project description 这是一个关于nuxt.js的项目(默认为(My beautiful Nuxt.js project))
? Author name Condor Hero
? Choose programming language JavaScript
? Choose the package manager Npm
? Choose UI framework Element
? Choose custom server framework None (Recommended)
? Choose Nuxt.js modules Axios
? Choose linting tools ESLint
? Choose test framework None
? Choose rendering mode Universal (SSR)
? Choose development tools jsconfig.json (Recommended for VS Code)
Installing packages with npm  

#安装依赖成功
�  Successfully created project app_test

  To get started:

        cd app
        npm run dev

  To build & start for production:

        cd app
        npm run build
        npm run start

进入项目文件cd app,通过 npm run dev 来启动项目,然后浏览器输入 http://localhost:3000/,就可以看到结果。

安装 Nuxt.js

四、Nuxt目录结构详讲

其实每个目录里面都有一个 README.md文件,清楚的告诉大家每个目录的作用。

|-- .nuxt                            // Nuxt自动生成,临时的用于编辑的文件,build
|-- assets                           // 用于组织未编译的静态资源入LESS、SASS 或 JavaScript
|-- components                       //Vue组件放置的地方
|-- layouts                          // 布局目录,用于组织应用的布局组件,文件名比较固定,不可更改。
|-- middleware                       // 用于存放中间件,类似路由钩子的作用
|-- pages                            // 用于存放写的页面,我们主要的工作区域
|-- plugins                          // 用于存放项目的第三方插件,例如element UI
|-- static                           // 用于存放静态资源文件,比如图片
|-- store                            // 用于组织应用的Vuex 状态管理。
|-- .editorconfig                    // 开发工具编辑器格式配置
|-- .eslintrc.js                     // ESLint的配置文件,用于检查代码格式
|-- .gitignore                       // 配置git不上传的文件
|-- nuxt.config.json                 // 用于组织Nuxt.js应用的个性化配置,已覆盖默认配置,对单页面应用类似是index.html和webpack.config.js两个文件的集成。
|-- package-lock.json                // npm自动生成,用于package依赖包固定版本的,yarn也有相同的操作
|-- package.json                     // npm包管理配置文件

统一代码风格工具——editorConfig

.editorconfig 文件,初次见到他是在用 Vue-cli 的时候,那时候没有深究怎么用的,里面的配置就是简单的看看,这次又见到了,结果用的时候完全不起作用,不起作用学习里面的语法可学不进去,而且官网 www.editorconfig.org 也是简单到看不懂😅。终于捯饬了一下午才发现需要安装插件😔,哎还以为编辑器自动支持。

在平常的项目开发过程中,每个开发者喜欢的编码风格是不一样的,就以缩进为例,有的人喜欢两个空格缩进,有的人喜欢四个空格的缩进,项目团队的人如果少的话,这么做是问题不大的。但是在多人合作开发项目。统一代码风格就显得十分有必要。editorConfig 有部分 ESLint 功能的感觉。

有些编辑器默认支持 editorConfig,如 webstorm;而有些编辑器则需要安装 editorConfig 插件,如 ATOM、Sublime、VS Code 等,在对应的插件市场直接搜索安装就行了。

EditorConfig for VS Code

这时候 .editorconfig 文件就能起作用了,编辑器的行为会与. editorconfig 文件中定义的一致,并且其优先级比编辑器自身的设置要高。

当打开一个文件时,EditorConfig 插件会在打开文件的目录和其每一级父目录查找.editorconfig文件,直到有一个配置文件里面有 root=true 才停止继续向上索引。

EditorConfig 的配置文件是从上往下读取的并且最近的 EditorConfig 配置文件会被最先读取,最近的配置文件中的配置项拥有优先权。如果 .editorconfig 文件没有进行某些配置,则使用编辑器默认的设置。

看看官网 https://editorconfig.org/ 有哪些配置规则:

符号 作用
* 匹配除/之外的任意字符串
** 匹配任意字符串
? 匹配任意单个字符
[name] 匹配name中的任意一个单一字符
[!name] 匹配不存在name中的任意一个单一字符
{s1,s2,s3} 匹配给定的字符串中的任意一个(用逗号分隔)
{num1..num2} 匹配num1到num2之间的任意一个整数, 这里的num1和num2可以为正整数也可以为负整
indent_style 设置缩进风格(tab是硬缩进,space为软缩进)
indent_size 用一个整数定义的列数来设置缩进的宽度,如果indent_style为tab,则此属性默认为tab_width
tab_width 用一个整数来设置tab缩进的列数。默认是indent_size
end_of_line 设置换行符,值为lf、cr和crlf
charset 设置编码,值为latin1、utf-8、utf-8-bom、utf-16be和utf-16le,不建议使用utf-8-bom
trim_trailing_whitespace 设为true表示会去除换行行首的任意空白字符。
insert_final_newline 设为true表示使文件以一个空白行结尾
root 表示是最顶层的配置文件,发现设为true时,才会停止查找.editorconfig文件

注意:所有的属性和值都是忽略大小写的. 解析时它们都是小写的
在来看官网一个例子:


# top-most EditorConfig file
root = true

# 使用Unix-style 换行符,并且每个文件以换行结束
[*]
end_of_line = lf
# CR:Carriage Return,对应ASCII中转义字符\r,表示回车
# LF:Linefeed,对应ASCII中转义字符\n,表示换行
# CRLF:Carriage Return & Linefeed,\r\n,表示回车并换行,windows的记事本换行规则
insert_final_newline = true

# 可以使用通配符匹配多个文件
# 设置默认编码为utf-8
[*.{js,py}]
charset = utf-8

# py文件 4 个空格缩进
[*.py]
indent_style = space
indent_size = 4

#  使用Tab缩进
[Makefile]
indent_style = tab

# lib目录下的js使用2个空格缩进
[lib/**.js]
indent_style = space
indent_size = 2

# 配置 package.json 或 .travis.yml文件 设置其为2个空格缩进
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

end_of_line 设置设置换行符规则,我们需要只要/r ,/n ,/r/n的区别和不同:

英文名 中文名 编辑器缩写 ASCII
carriage return 回车 CR \n
linefeed 换行 LF \r
carriage return & linefeed 回车换行 CRLF \r\n

https://www.ruanyifeng.com/blog/2006/04/post_213.html

文章中的电传打印机,这种终端不能独立使用,必须连接到一台主机,相当于显示器+键盘。这里有个Teletype Model 37 的演示视频

项目开发的时候,例如.gitignore.editorconfig等文件一旦配置完成几乎不需要改动,留着就有一点碍眼了,如何在 VSCode 目录栏中隐藏这些文件。


按图改下编辑器字体的大小按 ctrl+s 保存,这会在项目中生成 .vscode目录,里面有一个 settings.json文件,输入files.exclude会感应出出如下 JSON:

{
    "files.exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.hg": true,
        "**/CVS": true,
        "**/.DS_Store": true
    }
}

需要隐藏的文件类似添加就 OK 了。这时别忘了先在 .gitignore文件里面忽略.vscode 目录。

package.js文件

{
    "name": "condor",
    "version": "1.0.0",
    "description": "My marvelous Nuxt.js project",
    "author": "Condor Hero",
    "private": true,
    "scripts": {
        "dev": "nuxt",
        "build": "nuxt build",
        "start": "nuxt start",
        "generate": "nuxt generate"
    },
    "dependencies": {
        "nuxt": "^2.0.0"
    },
    "devDependencies": {},
    "config":{
        "nuxt":{
          "host":"127.0.0.1",
          "port":"8888"
        }
    }
}

主要是 scripts 字段里面的内容:

指令 描述
nuxt 开启一个监听3000端口的服务器,同时提供热加载功能
nuxt build 构建整个应用,压缩合并JS和CSS文件(用于生产环境)
nuxt start 开启一个生产模式的服务器(必须先运行nuxt build命令)
nuxt generate 构建整个应用,并为每一个路由生成一个静态页面(用于静态服务器)

其中通过 config 字段能解决 ip 占用问题,自定义端口和 IP。

nuxt.config.js

nuxt.config.js 如果对照单页面应用的话,就是综合了 index.html 和 webpack.config.js 两个文件。只捡一些重要的讲:

head 配置竟然还专门有一个网站。😀
https://vue-meta.nuxtjs.org/

很明显 nuxt.js 内置了这个插件,如果想自己安装也是可以的

 npm i vue-meta
plugins 配置要结合 plugins 目录使用,主要是为 Vue 插件提供服务的

此处以 element-ui 为例:

npm install -D element-ui

文件根目录 plugins/ 目录下创建相应的插件文件,我创建一个名为 element-ui.js 的文件,内容如下:

import Vue from 'vue';
import ElementUI from 'element-ui';

Vue.use(ElementUI);

在nuxt.config.js中,添加配置:

css:[
    'element-ui/lib/theme-chalk/index.css'
],
plugins:[
    '~/plugins/element-ui'
]

这时已经配置完成,可直接在 page 目录里面直接使用了。常见的插件还有 axios ,如果你不使用官方提供的话。可自行在此处配置,不过一般推荐使用 NuxtJs 官方推荐的 @nuxtjs/axios 插件。

使用 modules 来配置 @nuxtjs/axios

第一步安装:

npm i -D @nuxtjs/axios

第二步在 nuxt.config.js 文件的 modules 字段配置:

modules: [
    '@nuxtjs/axios'
]

这时可通过可在组件内直接使用 this.$axios 访问 axios 的实例。如果你需要通过注册拦截器或者改变全局设置来定制化axios, 你需要在 plugins 字段配置,同时在 plugins 目录下创建一个文件,我创建的文件名为 axios.js ,然后在添加到 nuxt.config.js 文件 plugins 字段里面:

plugins: [
    '~/plugins/element-ui',
    '@/plugins/axios'
]

axios.js 的文件内容为:

export default function (ctx) {
    const { $axios, redirect } = ctx;


    /* $axios发送ajax两种方式1、$axios.$get 2、$axios.get */
    console.log($axios.defaults.headers);
    console.log($axios.defaults.baseURL);
    console.log($axios.defaults.timeout);
    console.log($axios.setToken);


    /* 源码提供三个特别有用的函数 */
    /* setBaseURL(baseURL) {
        this.defaults.baseURL = baseURL
    };
    setHeader(name, value, scopes = 'common') {
        for (let scope of Array.isArray(scopes) ? scopes : [scopes]) {
            if (!value) {
                delete this.defaults.headers[scope][name];
                return
            }
            this.defaults.headers[scope][name] = value
        }
    };
    setToken(token, type, scopes = 'common') {
        const value = !token ? null : (type ? type + ' ' : '') + token
        this.setHeader('Authorization', value, scopes)
    }; */



    // 拦截器主要支持下面几个
    /* onRequest(config)
    onResponse(response)
    onError(err)
    onRequestError(err)
    onResponseError(err) */


    $axios.onRequest(config => {
        console.log('Making request to ' + config.url)
    });

    $axios.onError(error => {
        const code = parseInt(error.response && error.response.status)
        if (code === 400) {
            redirect('/400')
        };
    });
};

参考:

完整的 nuxt.config.js


export default {
    // 'spa': 没有服务器端渲染(只有客户端路由导航等)
    // 'universal': 同构应用程序(服务器端呈现+客户端路由导航等)
    mode: 'universal',
    /*
    ** Headers of the page
    */
    head: {
        // 页面标题
        title: process.env.npm_package_name || '',
        // 页面最终展示的标题,%s就是title的占位符
        titleTemplate:"%s - Hero",
        // html和body标签增加属性
        htmlAttrs: {
            lang: 'zh-CN',
            amp: true
        },
        bodyAttrs: {
            class: ['dark-mode', 'mobile']
        },
        // 元数据
        meta: [
            { charset: 'utf-8' },
            { name: 'viewport', content: 'width=device-width, initial-scale=1' },
            // 支持模板☞
            { names: 'template', content: 'Condor' , template:chunk=>`${chunk} - Hero`},
            // 注意:为了避免子组件中的meta标签不能正确覆盖父组件中相同的标签而产生重复的现象,
            // 建议利用 hid 键为meta标签配一个唯一的标识编号。不加hid,子和父相同时,不能覆盖父
            { hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
            // 单组件中可调用head方法,自定义此页面的head信息
            /* head () {
                return {
                    title: this.title,
                    meta: [
                        { hid: 'description', name: 'description', content: 'My custom description' }
                    ]
                }
            } */
        ],
        // <link>元素
        link: [
            { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
        ],
        // style元素,自动创建style标签
        style: [
            { cssText: '.foo { background-color: red }', type: 'text/css' }
        ],
        // script标签
        script: [
            { src: '/test.js', async: true, defer: true }
        ],
    },
    /*
    ** Customize the progress-bar color
    ** 页面最顶端加载进度条
    ** https://zh.nuxtjs.org/api/configuration-loading/#__layout
    */
    loading: { color: 'blue' , height: '5px'},
    /*
    ** Global CSS
    ** 引入全局的 CSS 文件,之后每个页面都会被引入。
    */
    css: [
        '@/assets/css/reset.css', //引入assets下的reset.css全局标签重置样式
        'element-ui/lib/theme-chalk/index.css'
    ],
    /*
    ** Plugins to load before mounting the App
    ** plugins 配置要结合 plugins 目录使用
    ** @和~都表示从根目录开始索引
    */
    plugins: [
        '~/plugins/element-ui',
        '@/plugins/axios'
    ],
    /*
    ** Nuxt.js dev-modules
    ** 构建模块
    */
    buildModules: [
        // Doc: https://github.com/nuxt-community/eslint-module
        // Simple usage
        '@nuxtjs/eslint-module',

        // With options
        // ['@nuxtjs/eslint-module', { /* module options */ }]
    ],
    // Using top level options
    eslint: {
        /* module options */
    },
    /*
    ** Nuxt.js modules
    ** modules是Nuxt.js扩展,里面放置的是NuxtJs推荐的插件
    ** 一般命名类似为'@nuxtjs/axios',
    */
    modules: [
        '@nuxtjs/axios'
    ],
    /*
    ** Build configuration
    ** 这个配置项用来配置 Nuxt.js 项目的构建规则,即 Webpack 的构建配置
    */
    build: {
        /*
        ** You can extend webpack config here
        */
        extend(config, ctx) {
        }
    }
}

五、Nuxt 的默认模版(app.html)和默认布局(default.vue)

在开发应用时,经常会用到一些公用的元素,比如网页的标题是一样的,每个页面都是一模一样的标题。这时候我们有两种方法:

  • 第一种方法是作一个公用的组件出来,公用组件更加灵活,但是每次都需要自己手动引入。
  • 第二种方法是修改默认模版。但是模板每个页面都会引入。

Nuxt 为我们提供了超简单的默认模版订制方法,需要在应用根目录下创建一个 app.html 的文件。在保持 nuxt.config.js 文件里创建的 index.html 不变的情况下,新增内容。记住设置重启下。

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
  <head {{ HEAD_ATTRS }}>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    <h1>每个页面都有我</h1>
    {{ APP }}
  </body>
</html>

{{xxx}}里面是读取 index.html 的默认设置。

和默认模板类似的功能还有默认布局,但是从名字上你就可以看出来,默认布局主要针对于页面的统一布局使用。它在位置根目录下的 layouts/default.vue。需要注意的是在默认布局里不要加入头部信息,只是关于<template>标签下的内容统一订制。

<template>
  <div>
    <h1>每个页面都有我</h1>
    <!-- nuxt标签相当于Vue项目中第一个router-view标签 -->
    <nuxt />
  </div>
</template>

这厮不需要重启服务器。layouts 目录还有一个重要的文件 error.vue:

当用户输入路由错误的时候,我们需要给他一个明确的指引,所以说在应用程序开发中404页面是必不可少的。Nuxt.js支持直接在默认布局文件夹里建立错误页面。

<template>
  <div class="container">
    <h1 v-if="error.statusCode === 404">页面不存在</h1>
    <h1 v-else>应用发生错误异常</h1>
    <nuxt-link to="/">首 页</nuxt-link>
  </div>
</template>

<script>
export default {
  props: ['error'],
  layout: 'blog' // 你可以为错误页面指定自定义的布局
  /*
  layouts 根目录下的所有文件都属于个性化布局文件,可以在页面组件中利用 layout 属性来引用。
   */
}
</script>

代码用 v-if 进行判断错误类型,返回不同的结果,需要注意的是这个错误变量 error 是需要在<script> 里 通过 props 接收的。

六、asyncData 方法获取数据

你可能想要在服务器端获取并渲染数据。Nuxt.js添加了asyncData方法使得你能够在渲染组件之前异步获取数据。asyncData方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,return 的东西和 data 函数的 return 用法类似。

注意:由于asyncData方法是在组件 初始化 前被调用的,所以在方法内是没有办法通过 this 来引用组件的实例对象。

<template>
    <div>
        <h1>{{ title }}</h1>
        <h2>My name is {{ name }}</h2>
        <nuxt-link to="/news">新闻</nuxt-link>
        <nuxt-link to="/about">关于</nuxt-link>
        <nuxt-link to="/shares">分享</nuxt-link>
    </div>
</template>
<script>
    export default {
        data(){
            return {
                title:"首页"
            }
        },
        asyncData({isDev, route, store, env, params, query, req, res, redirect, error}) {
            // 发送Ajax拿到数据return出去
            return { name : "Condor Hero"}
        },
    }
</script>

七、Vuex 状态树(内置)

Nuxt.js 会尝试找到应用根目录下的 store 目录,引用 vuex 模块,store 目录下的每个 .js 文件会自动被转换成为状态树指定命名的子模块 (当然,index 是根模块),一个简单的加法器。

store/index.js

export default {
    namespaced: true,
    state:{
        count:0
    },
    mutations:{
        ADD(state) {
            state.count++;
        },
        UNADD(state) {
            state.count--;
        }
    }
}

pages/index.vue

<template>
    <div>
        <h3>
            {{$store.state.count}}
        </h3>
        <el-button @click="$store.commit('ADD')">加加</el-button>
        <el-button @click="$store.commit('UNADD')">减减</el-button>
    </div>
</template>
加法器

除此之外,NuxtJs 还专门提供了一个 fetch 方法,用来修改状态树。

fetch 方法会在渲染页面前被调用,作用是填充状态树 (store) 数据,与 asyncData 方法类似,不同的是它不会设置组件的数据。

八、路由

NuxtJs 的路由和 vue-router 路由的用法和 API 几乎一摸一样。例如路由跳转:

<nuxt-link to="/news">新闻</nuxt-link>
<router-link to="/news">新闻</router-link>
<!--  虽然不建议这么做,但是router-link在nuxt项目中也能起作用 -->

对了,NuxtJs 路由没有 #,不是单页面应用,而是多页面。但是路由用法啥的没变。

重点来了,NuxtJs 路由表是自动生成。路由的用法没啥好讲的,主要涉及的知识会 Vue-router 就会,罗列下知识点:

  • 基础路由(简单的一层路由)
  • 动态路由(:id形式)
    在 Nuxt.js 里面定义带参数的动态路由,需要创建对应的以下划线作为前缀的 Vue 文件 或 目录。
  • 路由参数校验
// Nuxt.js 可以让你在动态路由组件中定义参数校验方法。
export default {
    validate({ params }) {
        // 如果校验方法返回的值不为 true或Promise中resolve 解析为false或抛出Error , Nuxt.js 将自动加载显示 404 错误页面或 500 错误页面。
        return true;
    }
}
  • 嵌套路由(文件嵌套模仿 vue-router 的 children)父组件必须设置<nuxt-child></nuxt-child>
  • 动态嵌套路由
  • 过渡动效(vue 的 transition)
  • 路由传参:动态路由参数 params和查询路由参数 query
  • 中间件(middleware对应 vue-router 的路由钩子)

主要讲下中间件也就是 vue-router 中的路由钩子。

每一个中间件应放置在 middleware/ 目录下。文件名的名称将成为中间件名称(middleware/auth.js将成为 auth 中间件)。路由鉴权的 auth 中间件是最常用的。

auth.js

export default function (ctx) {
    const { route , redirect} = ctx;
    console.log("☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆");
    redirect("/");
    console.log("☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆");
};

一个中间件接收 context 作为第一个参数,使用的位置有三个地方可选:

  • nuxt.config.js
module.exports = {
    router: {
        middleware: 'auth'
    }
}
  • layouts
<script>
export default {
    middleware: 'auth'
}
</script>
  • pages
<script>
export default {
    middleware: 'auth'
}
</script>

参考链接:使用dotenv统一管理node环境变量和配eslint
完。

2020年3月23日19点52分农历二月三十

推荐阅读更多精彩内容