vue-ssr的使用

本文详细介绍了Vue服务器端渲染(SSR)的实现步骤,包括初始化项目、渲染Vue实例、与服务器集成、文件结构、依赖安装、Webpack配置、客户端激活、VueRouter集成、Vue-meta头管理以及数据预取和状态管理。通过实例展示了如何在服务器端渲染Vue应用,并处理客户端的交互和数据同步。
摘要由CSDN通过智能技术生成

简单的初始化体验

安装

yarn init --yes
yarn add vue vue-server-renderer

渲染一个 Vue 实例

server.js

// 第 1 步:创建一个 Vue 实例
const Vue = require('vue')
const app = new Vue({
  template: `<div>{{ msg }}</div>`,
  data: {
    msg: 'Hello World',
  },
})

// 第 2 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer()

// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
  if (err) throw err
  console.log(html)
  // => <div data-server-rendered="true">Hello World</div>
})

// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer.renderToString(app).then(html => {
  console.log(html)
}).catch(err => {
  console.error(err)
})

与服务器集成

yarn add express

server.js

const Vue = require('vue')
const server = require('express')()

const renderer = require('vue-server-renderer').createRenderer()

server.get('*', (req, res) => {
  const app = new Vue({
    template: `<div>{{ msg }}</div>`,
    data: {
      msg: '前端你好',
    },
  })

  renderer.renderToString(app, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error')
    }
    res.setHeader('Content-type', 'text/html; charset=utf8')
    res.end(`
      <!DOCTYPE html>
      <meta charset="UTF-8">
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>
    `)
  })
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

使用一个单独的页面模板维护html

创建一个index.template.html
注意 注释 – 这里将是应用程序 HTML 标记注入的地方

<!DOCTYPE html>
<meta charset="UTF-8">
<html lang="en">
  <head>
    <meta charset="UTF-8">
    {{{ meta }}}
    <title>{{ title }}</title>
  </head>
  <body>
    <!--vue-ssr-outlet-->
  </body>
</html>
const Vue = require('vue')
const server = require('express')()
const fs = require('fs')

const renderer = require('vue-server-renderer').createRenderer({
  template: fs.readFileSync('./index.template.html', 'utf-8')
})
const context = {
  title: 'vue ssr',
  meta: `
    <meta name="keyword" content="vue,ssr">
    <meta name="description" content="vue srr demo">
  `,
}


server.get('*', (req, res) => {
  const app = new Vue({
    template: `<div>{{ msg }}</div>`,
    data: {
      msg: '前端你好',
    },
  })

  renderer.renderToString(app, context, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error')
    }
    res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8
    res.end(html)
  })
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

Vue ssr构建

当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。
所以需要为每个请求创建一个新的根 Vue 实例,如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)
Alt

服务器上的数据响应
因为实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 (“pre-fetching” data) - 这意味着在我们开始渲染时,我们的应用程序就已经解析完成其状态。也就是说,将数据进行响应式的过程在服务器上是多余的,所以默认情况下禁用。禁用响应式数据,还可以避免将「数据」转换为「响应式对象」的性能开销。

组件生命周期钩子函数
由于没有动态更新,所有的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染 (SSR) 过程中被调用。这就是说任何其他生命周期钩子函数中的代码(例如 beforeMount 或 mounted),只会在客户端执行。应该避免在 beforeCreate 和 created 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer。

访问特定平台(Platform-Specific) API
通用代码不可接受特定平台的 API,因此如果你的代码中,直接使用了像 window 或 document,这种仅浏览器可用的全局变量,则会在 Node.js 中执行时抛出错误,反之也是如此。

对于共享于服务器和客户端,但用于不同平台 API 的任务(task),建议将平台特定实现包含在通用 API 中,或者使用为你执行此操作的 library。例如,axios 是一个 HTTP 客户端,可以向服务器和客户端都暴露相同的 API。

对于仅浏览器可用的 API,通常方式是,在「纯客户端 (client-only)」的生命周期钩子函数中惰性访问 (lazily access) 它们。

请注意,考虑到如果第三方 library 不是以上面的通用用法编写,则将其集成到服务器渲染的应用程序中,可能会很棘手。你可能要通过模拟 (mock) 一些全局变量来使其正常运行,但这只是 hack 的做法,并且可能会干扰到其他 library 的环境检测代码。

自定义指令
大多数自定义指令直接操作 DOM,因此会在服务器端渲染 (SSR) 过程中导致错误。有两种方法可以解决这个问题:

推荐使用组件作为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。

如果你有一个自定义指令,但是不是很容易替换为组件,则可以在创建服务器 renderer 时,使用 directives 选项所提供"服务器端版本(server-side version)"。

const renderer = createRenderer({
  directives: {
    example (vnode, directiveMeta) {
      // 基于指令绑定元数据(metadata)转换 vnode
    }
  }
})

文件结构

src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器

App.vue

<template>
  <div id="app">
    <div>{{ msg }}</div>
    <input v-model="msg">
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      msg: '前端你好111',
    }
  },
  methods: {
    handleClick () {
      console.log('Hello Front')
    },
  },
}
</script>

<style>

</style>

app.js

import Vue from 'vue'
import App from './App.vue'

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
  const app = new Vue({
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app }
}

entry-client.js

import { createApp } from './app'

// 客户端特定引导逻辑……

const { app } = createApp()

// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

entry-server.js

import { createApp } from './app'

export default context => {
  const { app } = createApp()
  return app
}

安装依赖

yarn add cross-env
yarn add -D webpack webpack-cli webpack-merge webpack-node-externals @babel/core @babel/plugin-transform-runtime @babel/preset-env babel-loader css-loader url-loader file-loader rimraf vue-loader vue-template-compiler friendly-errors-webpack-plugin

配置webpack打包文件

webpack.base.config.js

/**
 * 公共配置
 */
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const resolve = file => path.resolve(__dirname, file)

const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',
  output: {
    path: resolve('../dist/'),
    publicPath: '/dist/',
    filename: '[name].[chunkhash].js'
  },
  resolve: {
    alias: {
      // 路径别名,@ 指向 src
      '@': resolve('../src/')
    },
    // 可以省略的扩展名
    // 当省略扩展名的时候,按照从前往后的顺序依次解析
    extensions: ['.js', '.vue', '.json']
  },
  devtool: isProd ? 'source-map' : 'cheap-module-eval-source-map',
  module: {
    rules: [
      // 处理图片资源
      {
        test: /\.(png|jpg|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          },
        ],
      },

      // 处理字体资源
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/,
        use: [
          'file-loader',
        ],
      },

      // 处理 .vue 资源
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },

      // 处理 CSS 资源
      // 它会应用到普通的 `.css` 文件
      // 以及 `.vue` 文件中的 `<style>` 块
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      },
      
      // CSS 预处理器,参考:https://vue-loader.vuejs.org/zh/guide/pre-processors.html
      // 例如处理 Less 资源
      // {
      //   test: /\.less$/,
      //   use: [
      //     'vue-style-loader',
      //     'css-loader',
      //     'less-loader'
      //   ]
      // },
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new FriendlyErrorsWebpackPlugin()
  ]
}

webpack.client.config.js

/**
 * 客户端打包配置
 */
const { merge } = require('webpack-merge')
const baseConfig = require('./webpack.base.config.js')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = merge(baseConfig, {
  entry: {
    app: './src/entry-client.js'
  },

  module: {
    rules: [
      // ES6 转 ES5
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            cacheDirectory: true,
            plugins: ['@babel/plugin-transform-runtime']
          }
        }
      },
    ]
  },

  // 重要信息:这将 webpack 运行时分离到一个引导 chunk 中,
  // 以便可以在之后正确注入异步 chunk。
  optimization: {
    splitChunks: {
      name: "manifest",
      minChunks: Infinity
    }
  },

  plugins: [
    // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。
    new VueSSRClientPlugin()
  ]
})

webpack.server.config.js

/**
 * 服务端打包配置
 */
const { merge } = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const baseConfig = require('./webpack.base.config.js')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(baseConfig, {
  // 将 entry 指向应用程序的 server entry 文件
  entry: './src/entry-server.js',

  // 这允许 webpack 以 Node 适用方式处理模块加载
  // 并且还会在编译 Vue 组件时,
  // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  target: 'node',

  output: {
    filename: 'server-bundle.js',
    // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
    libraryTarget: 'commonjs2'
  },

  // 不打包 node_modules 第三方包,而是保留 require 方式直接加载
  externals: [nodeExternals({
    // 白名单中的资源依然正常打包
    allowlist: [/\.css$/]
  })],

  plugins: [
    // 这是将服务器的整个输出构建为单个 JSON 文件的插件。
    // 默认文件名为 `vue-ssr-server-bundle.json`
    new VueSSRServerPlugin()
  ]
})

配置构建命令

package.json

"scripts": {
  "build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js",
  "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js",
  "build": "rimraf dist && npm run build:client && npm run build:server",
  "start": "cross-env NODE_ENV=production node server.js",
  "dev": "node server.js"
}

改造server.js

使用createBundleRenderer,引入打包后的文件

const Vue = require('vue')
const express = require('express')
const fs = require('fs')

const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
  runInNewContext: false,
  template, // (可选)页面模板
  clientManifest, // (可选)客户端构建 manifest
})
const context = {
  title: 'vue ssr',
  meta: `
    <meta name="keyword" content="vue,ssr">
    <meta name="description" content="vue srr demo">
  `,
}

const server = express()

server.use('/dist', express.static('./dist'))

server.get('*', (req, res) => {
  renderer.renderToString(context, (err, html) => {
    if (err) {
      return res.status(500).end('Internal Server Error')
    }
    res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8
    res.end(html)
  })
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

激活客户端

在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:

app.$mount('#app')

检查服务器渲染的输出结果,你会注意到应用程序的根元素上添加了一个特殊的属性:

<div id="app" data-server-rendered="true">

data-server-rendered 特殊属性,让客户端 Vue 知道这部分 HTML 是由 Vue 在服务端渲染的,并且应该以激活模式进行挂载,在没有 data-server-rendered 属性的元素上,还可以向 $mount 函数的 hydrating 参数位置传入 true,来强制使用激活模式(hydration):

app.$mount('#app', true)

为App.vue添加id

<template>
  <div id="app">
    // ...
  </div>
</template>

在开发模式下,Vue 将推断客户端生成的虚拟 DOM 树 (virtual DOM tree),是否与从服务器渲染的 DOM 结构 (DOM structure) 匹配。如果无法匹配,它将退出混合模式,丢弃现有的 DOM 并从头开始渲染。在生产模式下,此检测会被跳过,以避免性能损耗。
注意:浏览器可能会更改的一些特殊的 HTML 结构,比如会在

内部自动注入 ,由于 Vue 生成的虚拟 DOM (virtual DOM) 不包含 ,所以会导致无法匹配。所以请确保模板中写入有效的HTML

实现开发模式自动打包和热更新

const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const hotMiddleware = require('webpack-hot-middleware')

const resolve = file => path.resolve(__dirname, file)

module.exports = (server, callback) => {
  let ready
  const onReady = new Promise(r => ready = r)

  // 监视构建 -> 更新 Renderer

  let template
  let serverBundle
  let clientManifest

  const update = () => {
    if (template && serverBundle && clientManifest) {
      ready()
      callback(serverBundle, template, clientManifest)
    }
  }

  // 监视构建 template -> 调用 update -> 更新 Renderer 渲染器
  const templatePath = path.resolve(__dirname, '../index.template.html')
  template = fs.readFileSync(templatePath, 'utf-8')
  update()
  // fs.watch、fs.watchFile
  chokidar.watch(templatePath).on('change', () => {
    template = fs.readFileSync(templatePath, 'utf-8')
    update()
  })

  // 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
  const serverConfig = require('./webpack.server.config')
  const serverCompiler = webpack(serverConfig)
  const serverDevMiddleware = devMiddleware(serverCompiler, {
    // logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
  })
  serverCompiler.hooks.done.tap('server', () => {
    serverBundle = JSON.parse(
      serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
    )
    update()
  })

  // 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
  const clientConfig = require('./webpack.client.config')
  clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
  clientConfig.entry.app = [
    'webpack-hot-middleware/client?quiet=true&reload=true', // 和服务端交互处理热更新一个客户端脚本
    clientConfig.entry.app
  ]
  clientConfig.output.filename = '[name].js' // 热更新模式下确保一致的 hash
  const clientCompiler = webpack(clientConfig)
  const clientDevMiddleware = devMiddleware(clientCompiler, {
    publicPath: clientConfig.output.publicPath,
    // logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
  })
  clientCompiler.hooks.done.tap('client', () => {
    clientManifest = JSON.parse(
      clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
    )
    update()
  })
  server.use(hotMiddleware(clientCompiler, {
    log: false // 关闭它本身的日志输出
  }))

  // 重要!!!将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问
  server.use(clientDevMiddleware)

  return onReady
}

配置VueRouter

src/router/index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/pages/Home'

Vue.use(VueRouter)

export const createRouter = () => {
  const router = new VueRouter({
    mode: 'history', // 兼容前后端
    routes: [
      {
        path: '/',
        name: 'home',
        component: Home
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('@/pages/About')
      },
      {
        path: '/posts',
        name: 'post-list',
        component: () => import('@/pages/Posts')
      },
      {
        path: '*',
        name: 'error404',
        component: () => import('@/pages/404')
      }
    ]
  })

  return router
}

app.js

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

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
  const router = createRouter()
  const app = new Vue({
    router, // 把路由挂载到 Vue 根实例中
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app, router }
}

entry-server.js

// entry-server.js
import { createApp } from './app'

// 此处context来自于server.js中的renderer.renderToString
export default async context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
  const { app, router } = createApp()

  // 设置服务器端 router 的位置
  router.push(context.url)

  // 等到 router 将可能的异步组件和钩子函数解析完
  await new Promise(router.onReady.bind(router))

  return app
}

server.js

const Vue = require('vue')
const express = require('express')
const fs = require('fs')
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')

const server = express()

server.use('/dist', express.static('./dist'))

let renderer
let onReady
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {
  const serverBundle = require('./dist/vue-ssr-server-bundle.json')
  const template = fs.readFileSync('./index.template.html', 'utf-8')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  renderer = createBundleRenderer(serverBundle, {
    runInNewContext: false,
    template,
    clientManifest,
  })
} else {
  // 开发模式 => 监视打包构建 => 重新生成Renderer渲染器
  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
    renderer = createBundleRenderer(serverBundle, {
      runInNewContext: false,
      template,
      clientManifest,
    })
  })
}

const render = async (req, res) => {
  try {
    const html = await renderer.renderToString({
      title: 'vue ssr',
      meta: `
        <meta name="description" content="vue ssr">
      `,
      url: req.url
    })
    res.setHeader('Content-type', 'text/html; charset=utf8') // 设置响应编码utf8
    res.end(html)
  } catch (error) {
    res.status(500).end('Internal Server Error')
  }
  
}

server.get('*', isProd ? render : async (req, res) => {
  await onReady
  render(req, res)
})

server.listen(3000, () => {
  console.log('server running at port 3000')
})

客户端仍然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子

/**
 * 客户端入口
 */
 import { createApp } from './app'

 // 客户端特定引导逻辑……
 
 const { app, router } = createApp()
 
 router.onReady(() => {
   app.$mount('#app')
 })

App.vue

<template>
  <div id="app">
    <ul>
      <li>
        <router-link to="/">Home</router-link>
      </li>
      <li>
        <router-link to="/about">About</router-link>
      </li>
      <li>
        <router-link to="/posts">Posts</router-link>
      </li>
    </ul>

    <!-- 路由出口 -->
    <router-view/>
    <div>{{ msg }}</div>
    <input v-model="msg">
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      msg: '前端你好111',
    }
  },
  methods: {
    handleClick () {
      console.log('Hello Front')
    },
  },
}
</script>

<style>

</style>

使用vue-meta进行head管理

yarn add vue-meta

app.js

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router/'
import VueMeta from 'vue-meta'

Vue.use(VueMeta)

Vue.mixin({
  metaInfo: {
    titleTemplate: '%s - 拉勾教育'
  }
})

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
  const router = createRouter()
  const app = new Vue({
    router, // 把路由挂载到 Vue 根实例中
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app, router }
}

entry-server.js

// entry-server.js
import { createApp } from './app'

export default async context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
  const { app, router, store } = createApp()

  const meta = app.$meta()

  // 设置服务器端 router 的位置
  router.push(context.url)

  context.meta = meta

  // 等到 router 将可能的异步组件和钩子函数解析完
  await new Promise(router.onReady.bind(router))

  return app
}

index.template.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {{{ meta.inject().title.text() }}}
  {{{ meta.inject().meta.text() }}}
</head>

<body>
  <!--vue-ssr-outlet-->
</body>

</html>

Home.vue

<template>
  <div>
    <h1>Home Page</h1>
  </div>
</template>

<script>
export default {
  name: 'HomePage',
  metaInfo: {
    title: '首页'
  }
}
</script>

<style>

</style>

数据预取和状态管理

在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。

另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。

为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container)"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export const createStore = () => {
  return new Vuex.Store({
    state: () => ({
      posts: []
    }),

    mutations: {
      setPosts (state, data) {
        state.posts = data
      }
    },

    actions: {
      // 在服务端渲染期间务必让 action 返回一个 Promise
      async getPosts ({ commit }) {
        // return new Promise()
        const { data } = await axios.get('https://cnodejs.org/api/v1/topics')
        commit('setPosts', data.data)
      }
    }
  })
}

app.js

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

Vue.use(VueMeta)

Vue.mixin({
  metaInfo: {
    titleTemplate: '%s - ssr'
  }
})

// 导出一个工厂函数,用于创建新的
// 应用程序、router 和 store 实例
export function createApp () {
  const router = createRouter()
  const store = createStore()
  const app = new Vue({
    router, // 把路由挂载到 Vue 根实例中
    store, // 把容器挂载到 Vue 根实例中
    // 根实例简单的渲染应用程序组件。
    render: h => h(App)
  })
  return { app, router, store }
}

src/pages/Posts.vue

<template>
  <div>
    <h1>Post List</h1>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

<script>
// import axios from 'axios'
import { mapState, mapActions } from 'vuex'

export default {
  name: 'PostList',
  metaInfo: {
    title: 'Posts'
  },
  data () {
    return {
      // posts: []
    }
  },
  computed: {
    ...mapState(['posts'])
  },

  // Vue SSR 特殊为服务端渲染提供的一个生命周期钩子函数
  serverPrefetch () {
    // 发起 action,返回 Promise
    // this.$store.dispatch('getPosts')
    return this.getPosts()
  },
  methods: {
    ...mapActions(['getPosts'])
  }
  // 服务端渲染
  //     只支持 beforeCreate 和 created
  //     不会等待 beforeCreate 和 created 中的异步操作
  //     不支持响应式数据
  // 所有这种做法在服务端渲染中是不会工作的!!!
  // async created () {
  //   console.log('Posts Created Start')
  //   const { data } = await axios({
  //     method: 'GET',
  //     url: 'https://cnodejs.org/api/v1/topics'
  //   })
  //   this.posts = data.data
  //   console.log('Posts Created End')
  // }
}
</script>

<style>

</style>

entry-server.js

// entry-server.js
import { createApp } from './app'

export default async context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
    // 以便服务器能够等待所有的内容在渲染前,
    // 就已经准备就绪。
  const { app, router, store } = createApp()

  const meta = app.$meta()

  // 设置服务器端 router 的位置
  router.push(context.url)

  context.meta = meta

  // 等到 router 将可能的异步组件和钩子函数解析完
  await new Promise(router.onReady.bind(router))

  context.rendered = () => {
    // Renderer 会把 context.state 数据对象内联到页面模板中
    // 最终发送给客户端的页面中会包含一段脚本:window.__INITIAL_STATE__ = context.state
    // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿出来填充到客户端 store 容器中
    context.state = store.state
  }

  return app
}

entry-client.js

/**
 * 客户端入口
 */
import { createApp } from './app'

// 客户端特定引导逻辑……

const { app, router, store } = createApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.onReady(() => {
  app.$mount('#app')
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值