搭建本人的SSR、动态站点生成(SSG)及封装 Vue.js 组件库
搭建本人的SSR
一、渲染一个Vue实例

mkdir vue-ssrcd vue-ssrnpm init -ynpm i vue vue-server-renderderserver.jsconst Vue = require('vue')const renderer = require('vue-server-renderer').createRenderer()const app = new Vue({  template: `    <div id="app">      <h1>{{message}}</h1>    </div>  `,  data: {    message: '拉钩教育'  }})renderer.renderToString(app, (err, html) => {  if (err) throw err  console.log(html)})node server.js,运行后果:<div id="app" data-server-rendered="true"><h1>拉钩教育</h1></div>data-server-rendered="true"这个属性是为了未来客户端渲染激活接管的接口

二、联合到Web服务器中

server.jsconst Vue = require('vue')const express = require('express')const renderer = require('vue-server-renderer').createRenderer()const server = express()server.get('/', (req, res) => {  const app = new Vue({    template: `      <div id="app">        <h1>{{message}}</h1>      </div>    `,    data: {      message: '拉钩教育'    }  })  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>      <html lang="en">      <head>        <meta charset="UTF-8">        <meta name="viewport" content="width=device-width, initial-scale=1.0">        <title>Document</title>      </head>      <body>        ${html}      </body>      </html>    `)  })})server.listen(3000, () => {  console.log('server running at port 3000...')})

三、应用HTML模板

  1. 创立HTML模板文件
<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <title>Document</title></head><body>  <!--vue-ssr-outlet--></body></html><!--vue-ssr-outlet-->是占位符,为了接管未来要渲染的变量,不能写错,不能有多余的空格2. js代码中的createRenderer办法指定模板文件server.jsconst Vue = require('vue')const express = require('express')const fs = require('fs')const renderer = require('vue-server-renderer').createRenderer({  // 这里指定模板文件  template: fs.readFileSync('./index.template.html', 'utf-8')})const server = express()server.get('/', (req, res) => {  const app = new Vue({    template: `      <div id="app">        <h1>{{message}}</h1>      </div>    `,    data: {      message: '拉钩教育'    }  })  renderer.renderToString(app, (err, html) => { // 此处的html参数是被模板文件解决过了的,能够间接输入到用户的页面上    if (err) {      return res.status(500).end('Internal Server Error.')    }    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码    res.end(html)  })})server.listen(3000, () => {  console.log('server running at port 3000...')})

四、在模板中应用内部数据

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 }}}  <title>{{ title }}</title></head><body>  <!--vue-ssr-outlet--></body></html>应用两个花括号能够数据内部数据变量,而标签也会进行本义后输入在页面上。此时能够应用三个花括号原样输入数据,不会对标签进行本义解决在js代码中给renderer.renderToString减少第二个参数为内部数据对象renderer.renderToString(app, {    title: '拉勾教育',    meta: `      <meta name="description" content="拉勾教育" >    `  }, (err, html) => {    if (err) {      return res.status(500).end('Internal Server Error.')    }    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码    res.end(html)  })

五、构建配置

  1. 基本思路
    1.png
  2. 源码构造
    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">    <h1>{{message}}</h1>    <h2>客户端动静交互</h2>    <div>      <input v-model="message">    </div>    <div>      <button @click="onClick">点击测试</button>    </div>  </div></template><script>export default {  name: 'App',  data: function () {    return {      message: '拉勾教育'    }  },  methods: {    onClick () {      console.log('Hello World!')    }  }}</script>

app.js 是咱们应用程序的「通用 entry」。在纯客户端应用程序中,咱们将在此文件中创立根 Vue 实例,并间接挂载到 DOM。然而,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js 简略地应用 export 导出一个 createApp 函数:

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 客户端 entry 只需创立应用程序,并且将其挂载到 DOM 中:import { createApp } from './app'// 客户端特定疏导逻辑……const { app } = createApp()// 这里假设 App.vue 模板中根元素具备 `id="app"`app.$mount('#app')entry-server.js 服务器 entry 应用 default export 导出函数,并在每次渲染中反复调用此函数。此时,除了创立和返回应用程序实例之外,它不会做太多事件 - 然而稍后咱们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。import { createApp } from './app'export default context => {  const { app } = createApp()  return app}
  1. 装置依赖
    (1) 装置生产依赖

npm i vue vue-server-renderer express cross-env
包 阐明
vue Vue.js外围库
vue-server-renderer Vue服务端渲染工具
express 基于Node的webpack服务框架
cross-env 通过npm scripts设置跨平台环境变量
(2) 装置开发依赖

npm i -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外围包
webpack-cli webpack的命令行工具
webpack-merge webpack配置信息合并工具
webpack-node-externals 排除webpack中的Node模块
rimraf 基于Node封装的一个跨平台rm -rf工具
friendly-errors-webpack-plugin 敌对的webpack谬误提醒
@babel/core
@babel/plugin-transform-runtime
@babel/preset-env
babel-loader Babel相干工具
vue-loader
vue-template-compiler 解决.vue资源
file-loader 解决字体资源
css-loader 解决CSS资源
url-loader 解决图片资源

  1. webpack配置文件及打包命令
    (1) 初始化webpack打包配置文件

build
|---webpack.base.config.js # 公共配置
|---webpack.client.config.js # 客户端打包配置文件
|---webpack.server.config.js # 服务端打包配置文件
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()  ]})5. 配置构建命令"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"  }
  1. 启动利用
erver.jsconst Vue = require('vue')const express = require('express')const fs = require('fs')const serverBundle = require('./dist/vue-ssr-server-bundle.json')const clientManifest = require('./dist/vue-ssr-client-manifest.json')const { static } = require('express')const template = fs.readFileSync('./index.template.html', 'utf-8')const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {  template,  clientManifest })const server = express()// 申请前缀,应用express中间件的static解决server.use('/dist', express.static('./dist'))server.get('/', (req, res) => {    renderer.renderToString({    title: '拉勾教育',    meta: `      <meta name="description" content="拉勾教育" >    `  }, (err, html) => {    if (err) {      return res.status(500).end('Internal Server Error.')    }    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码    res.end(html)  })})server.listen(3001, () => {  console.log('server running at port 3001...')})7. 解析渲染流程六、构建配置开发模式1. 基本思路生产模式间接渲染,开发模式监督打包构建,从新生成Renderer渲染器2. 提取解决模块server.jsconst 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()// 申请前缀,应用express中间件的static解决server.use('/dist', express.static('./dist'))const isProd = process.env.NODE_ENV === 'production'let rendererlet onReadyif (isProd) {  const serverBundle = require('./dist/vue-ssr-server-bundle.json')  const clientManifest = require('./dist/vue-ssr-client-manifest.json')  const { static } = require('express')  const template = fs.readFileSync('./index.template.html', 'utf-8')  renderer = createBundleRenderer(serverBundle, {    template,    clientManifest   })} else {  // 开发模式 -> 监督打包构建 -> 从新生成Renderer渲染器  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {    renderer = createBundleRenderer(serverBundle, {      template,      clientManifest     })  })}// render 是路由函数const render = (req, res) => {  // renderer是Vue SSR的渲染器  renderer.renderToString({    title: '拉勾教育',    meta: `      <meta name="description" content="拉勾教育" >    `  }, (err, html) => {    if (err) {      return res.status(500).end('Internal Server Error.')    }    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码    res.end(html)  })}server.get('/', isProd ? render : async (req, res) => {  // 期待有了Renderer渲染器当前,调用render进行渲染  await onReady  render()})server.listen(3001, () => {  console.log('server running at port 3001...')})build/setup-dev-server.jsmodule.exports = (server, callback) => {  let ready // ready就是promise中的resolve  const onReady = new Promise(r => ready = r)  // 监督构建 -> 更新 Renderer  let template  let serverBundle  let clientManifest    return onReady  }3. update更新函数const update = () => {  if (template && serverBundle && clientManifest) {    ready()    callback(serverBundle, template, clientManifest)  }}4. 解决模板文件// 监督构建 template -> 调用 update -> 更新 Renderer 渲染器const templatePath = path.resolve(__dirname, '../index.template.html')template = fs.readFileSync(templatePath, 'utf-8')update()// fs.watch、fs.watchFilechokidar.watch(templatePath).on('change', () => {  template = fs.readFileSync(templatePath, 'utf-8')  update()})5. 服务端监督打包// 监督构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器const serverConfig = require('./webpack.server.config')// serverCompiler是一个webpack编译器,间接监听资源扭转,进行打包构建const serverCompiler = webpack(serverConfig)serverCompiler.watch({}, (err, stats) => {  if (err) throw err  if (stats.hasErrors()) return  serverBundle = JSON.parse(    fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')  )  console.log(serverBundle)  update()})6. 把数据写到内存中// 监督构建 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.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')  )  update()})7. 客户端构建// 监督构建 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' // 热更新模式下确保统一的 hashconst clientCompiler = webpack(clientConfig)const clientDevMiddleware = devMiddleware(clientCompiler, {  publicPath: clientConfig.output.publicPath,  logLevel: 'silent' // 敞开日志输入,由 FriendlyErrorsWebpackPlugin 解决})clientCompiler.hooks.done.tap('client', () => {  clientManifest = JSON.parse(    clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')  )  update()})8. 热更新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' // 热更新模式下确保统一的 hashconst hotMiddleware = require('webpack-hot-middleware')server.use(hotMiddleware(clientCompiler, {  log: false // 敞开它自身的日志输入}))

七、编写通用利用注意事项
八、路由解决

  1. 配置Vue-Router
    router/index.js
import Vue from 'vue'import VueRouter from 'vue-router'import Home from '@/src/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('@/src/pages/About')      },      {        path: '*',        name: 'error404',        component: () => import('@/src/pages/404')      }    ]  })  return router // 千万别忘了返回router}
  1. 将路由注册到根实例
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 }}
  1. 适配服务端入口
    拷贝官网上提供的entry-server.js
// entry-server.jsimport { createApp } from './app'export default context => {  // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,    // 以便服务器可能期待所有的内容在渲染前,    // 就曾经准备就绪。  return new Promise((resolve, reject) => {    const { app, router } = createApp()    // 设置服务器端 router 的地位    router.push(context.url)    // 等到 router 将可能的异步组件和钩子函数解析完    router.onReady(() => {      const matchedComponents = router.getMatchedComponents()      // 匹配不到的路由,执行 reject 函数,并返回 404      if (!matchedComponents.length) {        return reject({ code: 404 })      }      // Promise 应该 resolve 应用程序实例,以便它能够渲染      resolve(app)    }, reject)  })}路由表里曾经配置过404页面了,所以不必额定判断404,而后将Promise改成async/await的模式,最终如下:// entry-server.jsimport { createApp } from './app'export default async context => {  // 因为有可能会是异步路由钩子函数或组件,所以咱们将返回一个 Promise,    // 以便服务器可能期待所有的内容在渲染前,    // 就曾经准备就绪。    const { app, router } = createApp()    // 设置服务器端 router 的地位    router.push(context.url)    // 等到 router 将可能的异步组件和钩子函数解析完    await new Promise(router.onReady.bind(router))    return app}
  1. 服务端server适配
    咱们的服务器代码应用了一个 * 处理程序,它承受任意 URL。这容许咱们将拜访的 URL 传递到咱们的 Vue 应用程序中,而后对客户端和服务器复用雷同的路由配置!

server.js解决

// ...// render 是路由函数const render =async (req, res) => {  // renderer是Vue SSR的渲染器  try {    const html = await renderer.renderToString({      title: '拉勾教育',      meta: `        <meta name="description" content="拉勾教育" >      `,      url: req.url    })    res.setHeader('Content-Type', 'text/html; charset=utf8') // 设置编码,避免乱码    res.end(html)  }catch(err) {    res.status(500).end('Internal Server Error.')  }}// 服务端路由匹配为*,意味着所有的路由都会进入这里server.get('*', isProd ? render : async (req, res) => {  // 期待有了Renderer渲染器当前,调用render进行渲染  await onReady  render(req, res)})// ...
  1. 适配客户端入口
    须要留神的是,你依然须要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,能力正确地调用组件中可能存在的路由钩子。这一步咱们曾经在咱们的服务器入口 (server entry) 中实现过了,当初咱们只须要更新客户端入口 (client entry):
// entry-client.jsimport { createApp } from './app'const { app, router } = createApp()router.onReady(() => {  app.$mount('#app')})
  1. 解决实现
    路由进口:
App.vue<div id="app">  <ul>    <li>      <router-link to="/">Home</router-link>    </li>    <li>      <router-link to="/about">About</router-link>    </li>  </ul>  <!-- 路由进口 -->  <router-view/></div>

八、治理页面

  1. Head 内容
    npm install vue-meta
    在src/app.js外面,减少代码
import VueMeta from 'vue-meta'Vue.use(VueMeta)Vue.mixin({  metaInfo: {    titleTemplate: '%s - 拉勾教育'  }})在entry-server.js的导出函数里,减少代码:const meta = app.$meta()context.meta = meta将meta数据注入到模板页面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>在vue页面中的利用:export default {  name: 'Home',  metaInfo: {    title: '首页'  }}export default {  name: 'About',  metaInfo: {    title: '对于'  }}

九、数据预取和状态治理

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

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

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

  1. 数据预取
npm install vuex创立src/store/index.jsimport 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}) { // async默认返回Promise        // return new Promise()        const { data } = await axios.get('https://cnodejs.org/api/v1/topics')        commit('setPosts', data.data)      }    }  })}

将容器注入到入口文件src/app.js

/** * 同构利用通用启动入口 */import Vue from 'vue'import App from './App.vue'import { createRouter } from './router/'import VueMeat from 'vue-meta'import { createStore } from './store'Vue.use(VueMeta)Vue.mixin({  metaInfo: {    titleTemplate: '%s - 拉勾教育'  }})// 导出一个工厂函数,用于创立新的// 应用程序、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 }}页面pages/Posts.vue,应用serverPrefetch办法在服务端发动异步申请。<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>
  1. 将数据预取同步到客户端
entry-server.js// entry-server.jsimport { 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))    // 这个rendered函数会在服务端渲染结束之后被调用    context.rendered = () => {      // Renderer会把 context.state 数据对象内联到页面模板中      // 最终发送到客户端的页面中会蕴含一段脚本:window.__INITIAL_STATE__ = context.state      // 客户端就要把页面中的 window.__INITIAL_STATE__ 拿进去填充到客户端 store 容器中       context.state = store.state    }    return app}entry-client.js// entry-client.jsimport { createApp } from './app'const { app, router, store } = createApp()if (window.__INITIAL_STATE__) {  store.replaceState(window.__INITIAL_STATE__)}router.onReady(() => {  app.$mount('#app')})