一、渲染一个Vue实例

mkdir vue-ssr
cd vue-ssr
npm init -y
npm i vue vue-server-renderer
新建server.js

const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
	template:`
		<div id="app">
			<h1>{{message}}</h1>
		</div>
	`
	data:{
		message:'hello ssr'
	}
})
renderer.renderToString(app, (err, html) => {
	if(err) throw err
	console.log(html)
})

node server.js 可以正常输出

二、结合到Web服务中

npm i express

const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const express = require('express')
const server = express() // 创建server实例
// get请求网站根路径时设置一个处理函数
server.get('/', (req,res)=>{
	const app = new Vue({
		template:`
			<div id="app">
				<h1>{{message}}</h1>
			</div>
		`
		data:{
			message:'hello ssr'
		}
	})
	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">
				</head>
				<body>
				 ${html}
				</body>
				</html>
		`)
	})
})

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

nodemon server.js
浏览器输入localhost:3000 输出内容乱码 响应中正确 设置响应头解决
也可以在res.end 中放入完整的html片段

三、使用HTML模板

新建 index.template.html

并且要添加注释

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

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

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

</html>

const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer({
	template: fs.readFileSync('./index.template.html', 'utf-8') // 默认是二进制数据,新增utf-8是按照编码转成字符
})
const express = require('express')
const fs = require('fs')
const server = express() // 创建server实例
// get请求网站根路径时设置一个处理函数
server.get('/', (req,res)=>{
	const app = new Vue({
		template:`
			<div id="app">
				<h1>{{message}}</h1>
			</div>
		`
		data:{
			message:'hello ssr'
		}
	})
	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(html)
	})
})

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

四、在模板中使用外部数据

renderer.renderToString(app, {
	title:'hello ssr',
	meta: `
        <meta name="description" content="hello ssr">
      `,
},(err, html) => {
	if(err) {
		return	res.status(500).end('Internal Server Error')
	}
	res.setHeader('Content-Type', 'text/html; charset=utf8')
	res.end(html)
})
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  {{ meta }} // 这样会以字符串方式输出 不是标签的形式
  {{{ meta }}} // 这样vue就不会对其处理了
  <title>{{title}}</title>
</head>

重新启动服务 nodemon server.js

五、构建配置-基本思路

服务端渲染只是把vue实例处理成纯静态的html字符串。对于vue实例中需要客户端交互的动态的功能它本身是没有提供的
在这里插入图片描述

六、构建配置-源码结构

参考 https://ssr.vuejs.org/zh/guide/structure.html#%E4%BD%BF%E7%94%A8-webpack-%E7%9A%84%E6%BA%90%E7%A0%81%E7%BB%93%E6%9E%84
新建src文件夹 下新建App.vue
将server.js中template模板、data、methods等都拿入其中

template>
  <div id="app">
	{{message}}
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      message: 'hello ssr'
    }
  },
  methods: {
    onClick () {
      console.log('Hello World!')
    }
  }
}
</script>

<style>

</style>

新建src下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 客户端入口
客户端 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)安装生产依赖

npm i vue vue-server-renderer express cross-env

说明
vueVue.js 核心库
vue-server-rendererVue 服务端渲染工具
express基于 Node 的 Web 服务框架
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

说明
webpackwebpack 核心包
webpack-cliwebpack 的命令行工具
webpack-mergewebpack 配置信息合并工具
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处理图片资源

八、构建配置-webpack配置文件

配置文件及打包命令

(1)初始化 webpack 打包配置文件

build

├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件

webpack.base.config.js

  • 公共配置
/**
 * 公共配置
 */
 // 处理.vue资源的一个插件
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)
// process.env.NODE_ENV 拿到环境变量中的 NODE_ENV
const isProd = process.env.NODE_ENV === 'production'

module.exports = {
  mode: isProd ? 'production' : 'development',
  output: {
    path: resolve('../dist/'), // 打包结果输出到项目的dist目录中,没有的话会自动创建出来
    publicPath: '/dist/', // 用来设定打包结果的文件的加载或者说请求路径,打包结果这些文件请求路径的前缀都已dist开头。目的是为了防止打包出来的结果文件和本身的路由的路径产生冲突
    filename: '[name].[chunkhash].js'// hash文件内容发生改变那么打包结果的文件名也会发生变化,这样可以强制浏览器重新请求新的资源
  },
  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(), // 打包.vue资源所必须要用的插件
    new FriendlyErrorsWebpackPlugin() // 友好的webpack错误日志输出
  ]
}

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' // 这边相对路径相对的是当前打包所处的路径 即vue-ssr
  },

  module: {
    rules: [
      // ES6 转 ES5   服务端nodejs本身就是支持ES6的
      {
        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()
  ]
})

  • 服务端打包配置
/**
 * 服务端打包配置
 */
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"
  },

删除dist目录 rm -rf dist

十、构建配置-启动应用

https://ssr.vuejs.org/zh/guide/bundle-renderer.html#%E4%BD%BF%E7%94%A8%E5%9F%BA%E6%9C%AC-ssr-%E7%9A%84%E9%97%AE%E9%A2%98

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, {
	template,
	clientManifest // 客户端打包出来的资源的构建清单
})

const server = express() // 创建server实例
// get请求网站根路径时设置一个处理函数
server.get('/', (req,res)=>{
	// 此处不用创建vue实例了,因为在entry-server.js中已创建出来
	// const app = new Vue({
	//   template:`
	//     <div id="app">
	//       <h1>{{message}}</h1>
	//     </div>
	//   `
	//   data:{
	//     message:'hello ssr'
	//   }
	// })
	renderer.renderToString({
	title:'hello ssr',
	meta: `
        <meta name="description" content="hello ssr">
    `,
	},(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(3000, () => {
  console.log('server running at port 3000.

十一、构建配置-解析渲染流程

server.js中
renderer 在渲染时会来加载serverBundle(require(’./dist/vue-ssr-server-bundle.json’))的入口文件 server-bundle.js
通过它就得到 在entry-server里面createApp()创建的vue实例了,然后把vue实例进行了渲染
然后把渲染的结果注入到了template模板中。最后把这个数据发送给了客户端

服务端渲染后,客户端如何渲染或者说是激活服务端渲染的内容的呢
组要把客户端打包出来的脚本注入到页面中
createBundleRenderer里还配置个clientManifest(require(’./dist/vue-ssr-client-manifest.json’))
这是客户端打包资源的构建清单
在这里插入图片描述
publicpath 对应的客户端打包中出口中的publicpath
all 客户端所有打包构建出来的资源名称
initial server renser在渲染的时候会把initial里的资源自动的注入到index.template.html模板页面的

之后

async 存储异步资源的资源信息,比如代码中加载的异步资源文件 异步模块等
modules是对原始的模块的依赖信息说明
注入进来如何工作呢 参考以下链接
客户端激活:https://ssr.vuejs.org/zh/guide/hydration.html
在 entry-client.js 中,我们用下面这行挂载(mount)应用程序:
app.$mount(’#app’)

十二、构建配置开发模式-基本思路 、提取处理模块、update更新函数、处理模块文件、服务端监视打包、把数据写入内存中(npm i webpack-dev-middleware --save-dev)、客户端构建、热更新(npm i --save-dev webpack-hot-middleware)

"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"
  },
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 renderer
let onReady
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, {
    template,
    clientManifest
  })
} else {
  // 开发模式 -> 监视打包构建 -> 重新生成 Renderer 渲染器
  onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {
    renderer = createBundleRenderer(serverBundle, {
      template,
      clientManifest
    })
  })
}

const render = async (req, res) => {
  try {
    const html = await renderer.renderToString({
      title: 'hello ssr',
      meta: `
        <meta name="description" content="hello ssr">
      `,
      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)
  }
)

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

新建build/setup-dev-server.js

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) // 通过webpack创建的一个编译器
 	// serverCompiler.watch({},(err,stats) => { // stats 是构建结果模块相关的一些信息对象
	// 	if(err) throw err // wabpack本身的一些错误,直接终端程序运行
	// 	if(stats.hasErrors()) return // 判断打包源代码中是否有错误,但不会中断程序退出
	// 	 serverBundle = JSON.parse(
	// 	     fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8') // 这是读取物理磁盘中的文件
	// 	   )
	// 	   update()
	//   })

 const serverDevMiddleware = devMiddleware(serverCompiler, {
   logLevel: 'silent' // 关闭日志输出,由 FriendlyErrorsWebpackPlugin 处理
 })
 // 每当编译结束后 serverCompiler.hooks.done 触发这个钩子
 serverCompiler.hooks.done.tap('server', () => {
   serverBundle = JSON.parse(
   	// 读取内存中文件
     serverDevMiddleware.fileSystem.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.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')
   )
   update()
 })
 server.use(hotMiddleware(clientCompiler, {
   log: false // 关闭它本身的日志输出
 }))

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

十三、编写通用应用注意事项

参考:https://ssr.vuejs.org/zh/guide/universal.html

十四、路由处理-配置vueRouter

参考:https://ssr.vuejs.org/zh/guide/routing.html#%E4%BD%BF%E7%94%A8-vue-router-%E7%9A%84%E8%B7%AF%E7%94%B1
npm i vue-router
新建src/pages 新建相应页面
新建src/router/index.js

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

Vue.use(VueRouter)
// 这边和创建vue实例的思路一样,之所以创建这个函数的原因,是为了避免数据交叉请求带来的状态的污染
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 }
}

十六、路由处理-适配服务端入口

https://ssr.vuejs.org/zh/guide/routing.html#%E4%BD%BF%E7%94%A8-vue-router-%E7%9A%84%E8%B7%AF%E7%94%B1

现在我们需要在 entry-server.js 中实现服务器端路由逻辑 (server-side routing logic)
entry-server.js

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

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

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

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

十七、路由处理-服务端server适配

// 服务端路由设置为 *,意味着所有的路由都会进入这里

十八、路由处理-适配客户端入口

/**
 * 客户端入口
 */
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>
</template>

还有preload
prefetch 在浏览器空闲时加载。不稳定,可能失败,对页面不影响

二十、管理页面Head内容

参考:https://ssr.vuejs.org/zh/guide/head.html

https://vue-meta.nuxtjs.org

npm i vue-meta

src/app.js中

import VueMeta from 'vue-meta'
Vue.use(VueMeta)
Vue.mixin({
  metaInfo: {
    titleTemplate: '%s - hello ssr'
  }
})

https://vue-meta.nuxtjs.org/guide/ssr.html#add-vue-meta-to-the-context

entry-server.js:

  const meta = app.$meta()
  context.meta = meta

index.template.html

<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>

home

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

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

<style>

</style>

二十一、数据预取和状态管理-思路分析

https://ssr.vuejs.org/zh/guide/data.html

二十二、数据预取和状态管理-数据预取

https://ssr.vuejs.org/zh/guide/data.html#%E6%95%B0%E6%8D%AE%E9%A2%84%E5%8F%96%E5%AD%98%E5%82%A8%E5%AE%B9%E5%99%A8-data-store

npm i vuex

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)
      }
    }
  })
}

src/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 - hello 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>

npm run dev
此时发现页面中没有数据,数据一闪而过

二十三、数据预取和状态管理-将预取数据同步到客户端

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')
})

Logo

前往低代码交流专区

更多推荐