12-服务端渲染SSR

780 阅读13分钟

一、理解SSR概念

传统web开发

传统web开发,网页内容在服务端渲染完成,一次性传输到浏览器。

打开页面查看源码,浏览器拿到的是全部的dom结构

问题:服务器端渲染需要消耗更多的服务器端资源(cpu、内存等)

单页应用 Single Page App

单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染。

打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容。

单页应用的问题:

  • seo问题,对搜索引擎不友好
  • 首屏加载时间较长

服务端渲染 Server Side Render

ssr解决方案,后端返回完整的首屏dom结构,前端拿到的内容包括首屏及完整spa结构,应用激活后依然按照spa方式运行,这种页面渲染方式被称为服务端渲染(Server Side Render)

ssr是一种折中方法,利用vue语法编写程序,还能在服务端渲染html内容。

二、实现Vue SSR

创建工程

vue create ssr

安装相关依赖

 npm install vue-server-renderer express -D

启动脚本

创建一个express服务,将vue ssr集成进来,在根目录下创建server/ssr.js

// express
const express = require('express')
// vue
const Vue = require('vue')
// vue-server-renderer
const { createRenderer } = require('vue-server-renderer')

// 创建express实例
const app = express()

// 创建渲染器
const renderer = createRenderer()
// 待渲染页面
const vm = new Vue({
  data: { name: 'sofiya' },
  template: '<div>{{name}}</div>'
})

// express路由
app.get('/', async (req, res) => {
  const html = await renderer.renderToString(vm)
  res.send(html)
})

app.listen(3000, () => {
  console.log('渲染服务器成功')
})

路由

router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

// 导出工厂函数,可以返回新的Router实例
// 对于服务器,面对的是多个用户,不管面对的是单个用户还是多个用户,每次都应该是一个全新的router实例,这样对于不同的用户请求,不容易产生状态的污染
// 每个请求一个单独实例,避免状态污染
export default function createRouter () {
  return new VueRouter({
    mode: 'history',
    routes
  })
}

构建

对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」 然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。

1.vue创建实例

main.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import store from './store'

Vue.config.productionTip = false

// context是服务器传递给vue实例的参数对象
export function createApp (context) {
  // 1.获取router实例
  const router = createRouter()
  // 2.创建vue实例
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return { app, router }
}

2.服务端入口

在src下创建entry-server.js

// 这个文件将来在服务器执行——只负责渲染首屏内容
// 给服务器提供一个方法,可以根据接收url设置路由地址,然后返回创建vue实例
import { createApp } from './main.js'

export default context => {
  return new Promise((resolve, reject) => {
    // 获取vue、router实例
    const { app, router } = createApp(context)
    // 跳转至首屏
    router.push(context.url)
    // onReady完成时,异步任务都会结束
    router.onReady(() => {
      resolve(app)
    }, reject)
  })
}

3.客户端入口

在src下创建entry-client.js

// 这个文件将来在浏览器执行
// 挂载创建vue实例
import { createApp } from './main.js'

// 创建vue实例
const { app, router } = createApp()

// 路由就绪,执行挂载(激活过程)
router.onReady(() => {
  app.$mount('#app')
})

整体顺序是这样的:用户request=> express=> main.js=>router

webpack配置

1.安装相关依赖

npm install webpack-node-externals lodash.merge -D

2.vue.config.js

// 两个插件分别负责打包客户端和服务端
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
// 外置化,用于优化打包速度和体积
const nodeExternals = require('webpack-node-externals')
const merge = require('lodash.merge')
// 根据传入环境变量决定入口文件和相应配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === 'node'
const target = TARGET_NODE ? 'server' : 'client'
module.exports = {
  css: {
    extract: false
  },
  outputDir: './dist/' + target,
  configureWebpack: () => ({
    // 将 entry 指向应用程序的 server / client 文件
    entry: `./src/entry-${target}.js`,
    // 对 bundle renderer 提供 source map 支持
    devtool: 'source-map',
    // target设置为node使webpack以Node适用的方式处理动态导入,
    // 并且还会在编译Vue组件时告知`vue-loader`输出面向服务器代码。
    target: TARGET_NODE ? 'node' : 'web',
    // 是否模拟node全局变量
    node: TARGET_NODE ? undefined : false,
    output: {
      // 此处使用Node风格导出模块
      libraryTarget: TARGET_NODE ? 'commonjs2' : undefined
    },
    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 外置化应用程序依赖模块。可以使服务器构建速度更快,并生成较小的打包文件。
    externals: TARGET_NODE
      ? nodeExternals({
        // 不要外置化webpack需要处理的依赖模块。
        // 可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, // 还应该将修改`global`(例如polyfill)的依赖模块列入白名单
        whitelist: [/\.css$/]
      })
      : undefined,
    optimization: {
      splitChunks: undefined
    },
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 服务端默认文件名为 `vue-ssr-server-bundle.json`
    // 客户端默认文件名为 `vue-ssr-client-manifest.json`。
    plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin()]
  }),
  chainWebpack: config => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        merge(options, {
          optimizeSSR: false
        })
      })
  }
}

脚本配置

1.安装相关依赖

npm i cross-env -D

2.修改package.json文件

"scripts": {
    "serve": "vue-cli-service serve",
    "build:client": "vue-cli-service build",
    "build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build --mode server",
    "build": "npm run build:server && npm run build:client",
    "lint": "vue-cli-service lint"
}

3.执行打包

npm run build

宿主文件

直接修改public/index.html即可

<!-- vue-ssr-outlet -->这是约定,服务端渲染的插槽,必须这么写,挂载就有目标了。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <!-- vue-ssr-outlet -->
  </body>
</html>

修改服务器启动文件

server/ssr.js

// 加载本地文件
const fs = require('fs')

// 处理url
const path = require('path')
const express = require('express')
const app = express()
// 获取绝对路径

const resolve = dir => {
  return path.resolve(__dirname, dir)
}
// 第 1 步:开放dist/client目录,关闭默认下载index页的选项,不然到不了后面路由
app.use(express.static(resolve('../dist/client'), { index: false }))
// 第 2 步:获得一个createBundleRenderer
const { createBundleRenderer } = require('vue-server-renderer')
// 第 3 步:导入服务端打包文件
const bundle = require(resolve('../dist/server/vue-ssr-server-bundle.json'))
// 第 4 步:创建渲染器
const template = fs.readFileSync(resolve('../public/index.html'), 'utf-8')
const clientManifest = require(resolve('../dist/client/vue-ssr-client- manifest.json'))
const renderer = createBundleRenderer(bundle, {
  runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
  template, // 宿主文件
  clientManifest // 客户端清单
})
app.get('*', async (req, res) => {
  console.log(req.url)
  // 设置url和title两个重要参数
  const context = {
    title: 'ssr test',
    url: req.url
  }
  const html = await renderer.renderToString(context)
  res.send(html)
})
const port = 3001
app.listen(port, function () {
  // eslint-disable-next-line no-console
  console.log(`server started at localhost:${port}`)
})

整合Vuex

  • store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 导出工厂函数
export function createStore () {
  return new Vuex.Store({
    state: {
      count: 108
    },
    mutations: {
      add (state) {
        state.count += 1
      },
      // 加一个初始化
      init (state, count) {
        state.count = count
      }
    },
    actions: {
      getCount ({ commit }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            commit('init', Math.random() * 198)
            resolve()
          }, 1000)
        })
      }
    }
  })
}
  • main.js

挂载store

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

Vue.config.productionTip = false

// context是服务器传递给vue实例的参数对象
export function createApp (context) {
  // 1.获取router实例
  const router = createRouter()
  // 2. 获取vuex实例
  const store = createStore()
  // 3. 创建vue实例
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return { app, router, store }
}

获取数据

如果应用依赖于一些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据。

  • 组件中
export default {
  asyncData ({ store, route }) {
    return store.dispatch('getCount')
  }
}
  • 服务端数据预取(entry-server.js)
// 这个文件将来在服务器执行——只负责渲染首屏内容
// 给服务器提供一个方法,可以根据接收url设置路由地址,然后返回创建vue实例
import { createApp } from './main.js'

export default context => {
  return new Promise((resolve, reject) => {
    // 获取vue、router实例
    const { app, router, store } = createApp(context)
    // 跳转至首屏
    router.push(context.url)
    // onReady完成时,异步任务都会结束
    router.onReady(() => {
      // 获取匹配的路由组件数组
      const matchedComponents = router.getMatchedComponents()
      // 若无匹配则抛出异常
      if (!matchedComponents.length) {
        return reject(new Error({ code: 404 }))
      }
      // 对所有匹配的路由组件调用可能存在的`asyncData()`
      Promise.all(
        matchedComponents.map(Component => {
          if (Component.asyncData) {
            return Component.asyncData({
              store,
              route: router.currentRoute
            })
          }
        })
      )
        .then(() => {
        // 所有预取钩子 resolve 后,
        // store 已经填充入渲染应用所需状态
        // 将状态附加到上下文,且 `template` 选项用于 renderer 时,
        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
          context.state = store.state
          resolve(app)
        })
        .catch(reject)
    }, reject)
  })
}
  • 客户端在挂载到应用程序之前,store就应该获取到状态(entry-client.js
// 这个文件将来在浏览器执行
// 挂载创建vue实例
import { createApp } from './main.js'

// 创建vue实例
const { app, router, store } = createApp()

// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态自动嵌入到最 终的 HTML
// 在客户端挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}
// 路由就绪,执行挂载(激活过程)
router.onReady(() => {
  app.$mount('#app')
})
  • 客户端数据预取处理(main.js
// 确保客户端每个组件如果有asyncData,要执行它
Vue.mixin({
  beforeMount () {
    const { asyncData } = this.$options
    if (asyncData) {
      // 将获取数据操作分配给 promise
      // 以便在组件中,我们可以在数据准备就绪后
      // 通过运行 `this.dataPromise.then(...)` 来执行其他任务
      this.dataPromise = asyncData({
        store: this.$store,
        route: this.$roure
      })
    }
  }
})

总结

  • 优点
    1. seo
    2. 首屏内容到达时间短
  • 缺点
    1. 负载大
    2. 开发条件限制
    3. 构建部署:nodejs环境
  • 技术选型
    1. 首屏内容到达时间重要程度如何?
    2. seo是否是重要需求?仅有少量营销页面需要seo,考虑预渲染pre-render
    3. 已经完成spa项目,重构量很大怎么办?可以考虑Puppeteer
    4. 高流量情况是否做好充足服务器负载准备、缓存策略制定

三、nuxt.js实现ssr

Nuxt.js 是一个基于 Vue.js 的通用应用框架。

nuxt特性

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

nuxt安装

  • 安装
npx create-nuxt-app <项目名>
  • 选项

  • 运行项目

npm run dev

路由

1.路由生成

Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置。

生成的路由文件在.nuxt/router.js

2.导航

文件:layout/default.vue

<template>
  <div>
    <nuxt-link to="/">
      首页
    </nuxt-link>
    <NLlink to="/admin">
      管理
    </NLlink>
    <n-link to="/cart">
      购物车
    </n-link>
    <Nuxt />
  </div>
</template>

以上3种效果等同

3.动态路由

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

pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue
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? 参数,表示该路由是可选的。如果你想将它设置为必选的路由,需要在 users/_id 目录内创建一个 index.vue 文件。

4.嵌套路由

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

Warning: 别忘了在父组件(.vue文件) 内增加 用于显示子视图内容。

pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue
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'
        }
      ]
    }
  ]
}

5.配置路由

要扩展 Nuxt.js 创建的路由,我们需要在nuxt.config.js修改:

// nuxt.config.js
export default {
  router: {
    extendRoutes (routes, resolve) {
      routes.push({
        name: "foo",
        path: "/foo",
        component: resolve(__dirname, "pages/custom.vue")
}); }
} }

视图

Nuxt.js 应用中为指定的路由配置数据和视图,包括应用模板、页面、布局和HTML头部等内容。

布局

  • 默认布局

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

<template>
  <nuxt/>
</template>

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

  • 自定义布局

layouts 目录中的每个文件 (顶级) 都将创建一个可通过页面组件中的 layout 属性访问的自定义布局。

假设我们要创建一个 博客布局 并将其保存到layouts/blog.vue:

<template>
  <div>
    <div>我的博客导航栏在这里</div>
    <nuxt/>
  </div>
</template>

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

<template>
<!-- Your template -->
</template>
<script>
export default {
  layout: 'blog'
  // page component definitions
}
</script>
  • 错误页面

你可以通过编辑 layouts/error.vue 文件来定制化错误页面.

警告: 虽然此文件放在 layouts 文件夹中, 但应该将它看作是一个 页面(page).

这个布局文件不需要包含 标签。你可以把这个布局文件当成是显示应用错误(404,500等)的组件。

默认的错误页面源码在 这里.

举一个个性化错误页面的例子 layouts/error.vue:

<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' // 你可以为错误页面指定自定义的布局
}
</script>

页面

页面组件实际上是 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.js 为页面提供的特殊配置项:

  • asyncData:最重要的一个键, 支持 异步数据处理,另外该方法的第一个参数为当前页面组件的 上下文对象。
  • fetch: 与 asyncData 方法类似,用于在渲染页面之前获取数据填充应用的状态树(store)。不同的是 fetch 方法不会设置组件的数据。
  • head: 配置当前页面的 Meta 标签
  • layout: 指定当前页面使用的布局(layouts 根目录下的布局文件)。
  • loading: 如果设置为false,则阻止页面自动调用this.$nuxt.$loading.finish()this.$nuxt.$loading.start(),您可以手动控制它,请看例子,仅适用于在nuxt.config.js中设置loading的情况下。
  • transition: 指定页面切换的过渡动效。
  • scrollToTop: 布尔值,默认: false。 用于判定渲染页面前是否需要将当前页面滚动至顶部。这个配置用于 嵌套路由的应用场景。
  • validate: 校验方法用于校验 动态路由的参数。
  • middleware: 指定页面的中间件,中间件会在页面渲染之前被调用。

默认Meta标签

Nuxt.js 允许你在 nuxt.config.js 里定义应用所需的所有默认 meta 标签,在 head 字段里配置就可以了:

head: {
  meta: [
    { charset: 'utf-8' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' }
  ],
  link: [
    { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto' }
  ]
}

异步数据获取

asyncData 方法使得我们可以在设置组件数据之前异步获取或处理数据。

准备接口

  • 安装相关依赖
npm i koa-router koa-bodyparser -S
  • 接口文件server/api.js
// 此文件并非nuxt生成,它为演示项目提供数据服务接口
const Koa = require('koa')
const app = new Koa()
const bodyparser = require('koa-bodyparser')
const router = require('koa-router')({ prefix: '/api' })

// 设置cookie加密秘钥
app.keys = ['some secret', 'another secret']

const goods = [
  { id: 1, text: 'Web全栈架构师', price: 1000 },
  { id: 2, text: 'Python架构师', price: 1000 }
]

// 配置路由
// 获取产品列表
// http://localhost:8080/api/goods
router.get('/goods', (ctx) => {
  ctx.body = {
    code: 0,
    goods
  }
})

// 产品详情
router.get('/detail', (ctx) => {
  ctx.body = {
    code: 0,
    data: goods.find(good => good.id === ctx.query.id)
  }
})

// 登录
router.post('/login', (ctx) => {
  const user = ctx.request.body
  if (user.username === 'jerry' && user.password === '123') {
    // 将token存入cookie
    const token = 'a mock token'
    ctx.cookies.set('token', token)
    ctx.body = { code: 0, token }
  } else {
    ctx.body = { code: -1 }
  }
})

// 解析post数据并注册路由
app.use(bodyparser())
// 注册路由
app.use(router.routes())

app.listen(8090, () => console.log('api服务已启动'))

整合axios

  • 安装@nuxt/axios模块
npm i @nuxtjs/axios -S
  • 配置nuxt.config.js
modules: ['@nuxtjs/axios'],
axios: {
    proxy: true
},
proxy: {
    '/api': 'http://localhost:8090'
},

解决跨域,使用proxy代理

请求数据

async asyncData ({ $axios, error }) {
    // 注意:这里不能使用this,因为此刻组件实例还未创建
    // $axios由nuxtjs/axios模块注入
    // $get是axios模块封装的类fetch风格api
    const { code, goods } = await $axios.$get('/api/goods')
    console.log(code, goods)
    if (code === 0) {
      // 这里返回的数据将来会和data合并
      return { goods }
    }
    // 错误处理
    error({ statusCode: 400, message: '数据请求失败' })
}

中间件

中间件会在一个页面或一组页面渲染之前运行我们定义的函数,常用于权限控制、校验等任务。

  • 创建中间件middleware/auth.js
// 定义中间件,参数是nuxt提供的上下文对象
export default function ({ route, redirect, store }) {
  // 上下文中通过store访问vuex中的全局状态
  if (!store.state.user.token) {
    redirect('/login?redirect=' + route.path)
  }
}
  • 使用

【页面级】

// admin.vue——必须登录后才能访问admin页面
export default {
 // 页面级-中间件
 middleware: ['auth']
}

【路由级】

// nuxt.config.js
router: {
   middleware: ['auth']
}

每访问一个路由,都会先执行中间件

状态管理Vuex

Nuxt.js 会尝试找到src目录(默认是应用根目录)下的 store 目录,如果该目录存在,它将做以下的事情:

  1. 引用 vuex 模块
  2. vuex 模块 加到 vendors 构建配置中去
  3. 设置 Vue 根实例的 store 配置项

使用状态树两种方式

Nuxt.js 支持两种使用 store 的方式,你可以择一使用:

  • 模块方式store 目录下的每个 .js 文件会被转换成为状态树指定命名的子模块 (当然,index 是根模块)
export const state = () => ({
  token: ''
})

export const mutations = {
  init (state, token) {
    state.token = token
  }
}

export const getters = {
  isLogin (state) {
    return !!state.token
  }
}

export const actions = {
  login ({ commit, getters }, u) {
    // this.$axios由@nuxt/axios模块提供
    return this.$axios.$post('/api/login', u).then(({ token }) => {
      if (token) {
        commit('init', token)
      }
      return getters.isLogin
    })
  }
}
  • Classic(不建议使用): store/index.js返回创建Vuex.Store实例的方法。

案例:登录页面使用

<template>
  <div>
    <h1>用户登录</h1>
    <input v-model="user.username" type="text">
    <input v-model="user.password" type="password">
    <button @click="submit">
      登录
    </button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      user: {
        username: '',
        password: ''
      }
    }
  },
  methods: {
    submit () {
      this.$store.dispatch('user/login', this.user).then((res) => {
        if (res) {
          const redirect = this.$route.query.redirect || '/'
          this.$router.push(redirect)
        }
      })
    }
  }
}
</script>

nuxtServerInit 方法

如果在状态树中指定了 nuxtServerInit 方法,Nuxt.js 调用它的时候会将页面的上下文对象作为第2个参数传给它(服务端调用时才会酱紫哟)。当我们想将服务端的一些数据传到客户端时,这个方法是灰常好用的。

export const actions = {
  nuxtServerInit ({ commit }, { app }) {
    // 登录后,服务端将token写入了cookie中,前端通过`cookie-universal-nuxt`取出token来
    const token = app.$cookies.get('token')
    if (token) {
      console.log('nuxtServerInit: token:' + token)
      commit('user/init', token)
    }
  }
}
  • 安装依赖模块
npm i -S cookie-universal-nuxt
  • 配置nuxt.config.js
modules: [
  'cookie-universal-nuxt'
],

注意:
必须定义到store/index.js文件中
nuxtServerInit仅在服务端执行

插件

Nuxt.js允许您在运行Vue.js应用程序之前执行js插件。这在您需要使用自己的库或第三方模块时特别有用。

插件只执行一次,时间点比较靠前

使用第三方插件plugin/element-ui.js

import Vue from 'vue'
import Element from 'element-ui'
import locale from 'element-ui/lib/locale/lang/en'

export default () => {
  Vue.use(Element, { locale })
}

axios请求拦截器plugin/interceptor.js

export default function ({ $axios, store }) {
  // 为$axios实例添加一个请求事件监听
  // onRequest是nuxtjs/axios模块提供的帮助方法
  $axios.onRequest((config) => {
    if (store.state.user.token) {
      config.headers.Authorization = 'Bearer ' + store.state.user.token
    }
    return config
  })
}

配置nuxt.config.js

plugins: [
    '@/plugins/element-ui',
    '@/plugins/interceptor'
]

发布部署

服务端渲染应用部署

部署 Nuxt.js 服务端渲染的应用不能直接使用 nuxt 命令,而应该先进行编译构建,然后再启动 Nuxt 服务,可通过以下两个命令来完成:

nuxt build
nuxt start

静态应用部署

Nuxt.js 可依据路由配置将应用静态化,使得我们可以将应用部署至任何一个静态站点主机服务商。

可利用下面的命令生成应用的静态目录和文件:

npm run generate

四、总结

  • 优点
  1. spa+ssr通用框架
  2. 约定路由
  3. 代码分层
  4. 优化到位
  • 缺点

若是ssr模式,前面需要考虑的因素都存在

  • nuxt流程图

学完nuxt,再来看nuxt执行的流程图就会一目了然了

五、参考

nuxt