当前位置: 首页 > 工具软件 > Vue-SSR > 使用案例 >

深入学习vue-ssr2

顾鸣
2023-12-01

前面我们已经对vue的服务端渲染有了一定的认识,并且对vue-ssr的构建配置有了一个基本的思路。前置知识参见:从0-1学习vue-srr深入学习vue-ssr

接下来我们根据前面讲述的基本思路开始学习如何进行一系列的操作,将页面赋予动态交互的能力,并且具备完成企业级项目开发的能力。

PS:本案例源代码仅供参考: 源码地址

一、项目源码结构

一个项目,肯定要有自己的目录,即源码存放的结构,这一点在官网中也已经介绍的很清楚了,这里不再赘述,官网介绍参见:源码结构

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

我们根据官网介绍,依次创建 App.vue app.js entry-client.js entry-server.js

另外我们还需要创建 index.template.html 模板文件和 server.js 文件

App.vue 中:

<template>
	<div id="app">
		<h1>{{ message }}</h1>
		<input type="text" v-model="message">
		<button @click="myClick">按钮</button>
	</div>
</template>

<script>
	export default {
		name: 'App',
		data() {
			return {
				message: 'hello vue-ssr'
			}
		},
		methods: {
			myClick() {
				console.log('hello vue-ssr')
			}
		}
	}
</script>

index.template.html 中:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>spades-vue-ssr</title>
</head>
<body>
	<!-- 下面这行注释,就是动态数据替换的地方 -->
	<!--vue-ssr-outlet-->
</body>
</html>

其他文件中的代码参见官网

二、安装依赖及配置打包

这里需要安装的依赖比较多,没必要死记硬背,需要的时候过来查看文档或者去官网查资料。但这里的介绍一定要搞清楚,头脑中有个印象,不然到时候查资料都不知道如何去查~

PS: 请考虑下你的webpack版本,支持去踩 webpack5 的坑,不过这里使用的是 webpack4webpack-cli3

1、安装生产依赖

npm i vue vue-server-renderer express cross-env
说明
vuevue.js 核心库
vue-server-rendereVue 服务端渲染工具
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处理图片资源

3、配置文件

初始化 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/preprocessors.html
			// 例如处理 Less 资源
			// {
			// 	test: /\.less$/,
			// 	use: [
			// 		'vue-style-loader',
			// 		'css-loader',
			// 		'less-loader'
			// 	]
			// },
		]
	},
	plugins: [
		new VueLoaderPlugin(),
		new FriendlyErrorsWebpackPlugin()
	]
}

webpack.client.config.js

/**
* 客户端打包配置
*/
const webpack = require('webpack')
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: '/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 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
  	// 并且还会在编译 Vue 组件时,
  	// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
  	target: 'node',

  	// 对 bundle renderer 提供 source map 支持
  	devtool: 'source-map',

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

  	// https://webpack.js.org/configuration/externals/#function
  	// https://github.com/liady/webpack-node-externals
  	// 外置化应用程序依赖模块。可以使服务器构建速度更快,
  	// 并生成较小的 bundle 文件。
  	externals: nodeExternals({
    	// 不要外置化 webpack 需要处理的依赖模块。
    	// 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件,
    	// 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单
    	allowlist: /\.css$/
  	}),

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

4、配置打包命令

package.json文件中的 scripts 中配置打包命令

"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"
},

5、运行测试

在运行测试前我们需要完善一下 server.js 中的内容

// 加载 vue
const Vue = require('vue')
// 导入 fs 模块
const fs = require('fs')
// 加载通过 webpack 打包好的 vue-ssr-server-bundle.json 文件
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
// 加载通过 webpack 打包好的 vue-ssr-client-bundle.json 文件
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
// fs 模块读取得到的 buffer 二进制流,需要转换为字符串
const template = fs.readFileSync('./index.template.html', 'utf-8')
// 加载 vue-server-renderer 中的 createBundleRenderer 方法
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, {
	template, // (可选)页面模板
	runInNewContext: false, // 推荐
	clientManifest // (可选)客户端构建 manifest
})
// 创建 Vue 实例
// 将 express 加载进来
const express = require('express')
// 创建一个 server 实例
const server = express()
// 挂载处理静态资源的中间件 第一个参数是请求前缀,第二个参数是具体路径
server.use('/dist', express.static('./dist'))

// 在请求根路径的时候,我们将渲染好的 vue-ssr 发送给客户端浏览器
server.get('/', (req, res) => {
	renderer.renderToString((err, html) => {
		if(err) {
			res.end(err)
		}
		// 如果没有错误,我们这里就得到了渲染的Vue实例的字符串形式
        // 发送渲染好的内容前,设置响应头当中的 Content-Type, 解决乱码问题
        // res.setHeader('Content-Type', 'text/html; charset=utf8')
		res.end(html)
	})
})

// 开启端口监听
server.listen(3000, () => {
	console.log('server running at port 3000.')
})

运行并测试:

npm run build:client
npm run build:server
npm run build

我们可以看到打包的内容已经正确的输出到dist目录中

6、启动应用

运行命令 nodemon server.js , 打开网页访问 http://localhost:3000/

我们看到页面显示内容是没问题的;

然后在input输入框中修改内容,双向绑定的h1标签中的内容也是随之改变的;

我们点击按钮,在控制台输出了 hello vue-ssr

此时,我们的页面已经具备了动态交互的能力,服务端渲染+客户端渲染(建议你还是要去看一下官网对这一部分的介绍:客户端激活)的学习也已经大工告成。但是,如果在企业的实战开发中,这些显然是不够的,仅仅具备这些知识是无法胜任企业级实战的各种复杂的业务以及逻辑的实现,接下来,我们就继续学习其他更深入的一些知识。

三、深入开发模式

在进行企业级开发的时候,我们需要不断的开发新页面和修改原页面的结构与逻辑功能,意味着我们需要不断的需要重新打包,重新启动服务等一系列重复的操作。为了提高我们的开发效率,我们需要深入学习开发模式的构建,以及热更新的配置等知识。

1、基本思路

其实思路很简单,首先我们需要判断是否是生产环境,如果是生产环境,按照我们已经完成的构建步骤就可以了;如果是开发模式,需要单独抽取出来,监听文件的变化,当文件变化了之后让工具自动去打包构建,重启服务。这里我们要做的是处理好判断的条件和自动打包构建重启服务。

2、开发模式和生产模式的分离

package.json 中的 script 中增加运行命令,start为生产模式,dev为开发模式

"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"
},

3、server.js 的实现

// 加载 vue
const Vue = require('vue')
// 导入 fs 模块
const fs = require('fs')
// 将 express 加载进来
const express = require('express')
// 创建一个 server 实例
const server = express()
// 挂载处理静态资源的中间件 第一个参数是请求前缀,第二个参数是具体路径
server.use('/dist', express.static('./dist'))
// 加载 vue-server-renderer 中的 createBundleRenderer 方法
const { createBundleRenderer } = require('vue-server-renderer')
// 加载模块处理函数
const setupDevServer = require('./build/setup-dev-server')
	
// 判断当前是否是生产环境
const isProd = process.env.NODE_ENV === 'production'
let renderer, onReady
if (isProd) {	// 生产环境打包--按照之前的处理逻辑
	// 加载通过 webpack 打包好的 vue-ssr-server-bundle.json 文件
	const serverBundle = require('./dist/vue-ssr-server-bundle.json')
	// 加载通过 webpack 打包好的 vue-ssr-client-bundle.json 文件
	const clientManifest = require('./dist/vue-ssr-client-manifest.json')
	// fs 模块读取得到的 buffer 二进制流,需要转换为字符串
	const template = fs.readFileSync('./index.template.html', 'utf-8')
	
	renderer = createBundleRenderer(serverBundle, {
		template, // (可选)页面模板
		runInNewContext: false, // 推荐
		clientManifest // (可选)客户端构建 manifest
	})
} else {	// 开发环境打包
	// 监视打包构建 -> 重新生成 renderer 渲染器
	onReady = setupDevServer(server, (serverBundle, template, clientManifest) => {	// 监视打包构建 之后执行回调函数
		// 基于打包构建后的结果,重新生成 renderer 渲染器
		renderer = createBundleRenderer(serverBundle, {
			template, // (可选)页面模板
			runInNewContext: false, // 推荐
			clientManifest // (可选)客户端构建 manifest
		})
	})
}

const render = (req, res) => {
	renderer.renderToString((err, html) => {
		if(err) {
			res.end(err)
		}
		// 如果没有错误,我们这里就得到了渲染的Vue实例的字符串形式
        // 发送渲染好的内容前,设置响应头当中的 Content-Type, 解决乱码问题
        res.setHeader('Content-Type', 'text/html; charset=utf8')
		res.end(html)
	})
}

// 在请求根路径的时候,我们将渲染好的 vue-ssr 发送给客户端浏览器
server.get('/', isProd 
	? render 
	: async (req, res) => {
		await onReady
		// 等待有了 renderer 渲染器以后,调用 render 进行渲染
		render(req, res)
	}
)

// 开启端口监听
server.listen(3000, () => {
	console.log('server running at port 3000.')
})

4、提取出server.js 中的 setupDevServer 处理函数

const fs = require('fs')
const path = require('path')
// 加载第三方模块,用来监视文件变化
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
// 关于 webpack5 热更新这么配置会有问题
const hotMiddleware = require('webpack-hot-middleware')
// 自己封装一个 resolve 方法
const resolve = file => {
	return path.resolve(__dirname, file)
}

module.exports = (server, callback) => {
	let ready
	const onReady = new Promise(r => ready = r)
	// 处理逻辑---监视构建,更新 renderer
	let serverBundle, template, clientManifest
	const update = () => {
		if (serverBundle && template && clientManifest) {
			ready()
			callback(serverBundle, template, clientManifest)
		}
	}

	// 监视构建 template => 调用 update 函数 => 更新 renderer 渲染器
	const templatePath = resolve('../index.template.html')
	template = fs.readFileSync(templatePath, 'utf-8')
	update()
	// console.log(template)
	// 监听 template 的变化 推荐使用第三方包 chokidar 封装了 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)
	// 编译器自带有监视文件变化的方法---这个方法总是向物理磁盘读写数据
	// serverCompiler.watch({}, (err, stats) => {
	// 	if (err) throw err
	// 	if (stats.hasError) return
	// 	const serverBundleStr = fs.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
	// 	serverBundle = JSON.parse(serverBundleStr)
	// 	// console.log(serverBundle)
	// 	update()
	// })
	// 使用 中间件 文件保存在内存中---不再向物理磁盘读写数据
	const serverDevMiddleware = devMiddleware(serverCompiler, {
		logLevel: 'silent'	// 不打印日志 这里有个坑 5.0.0版本废弃了这个属性,3.7.2版本这个属性可以用
	})
	serverCompiler.hooks.done.tap('server', () => {
		// const serverBundleStr = serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')
		// 5.0.0版本上面的才好使 3.7.2版本下面这个读取方法可以用
		const serverBundleStr = serverDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8')	
		serverBundle = JSON.parse(serverBundleStr)
		update()
	})

	// 监视构建 clientManifest => 调用 update 函数 => 更新 renderer 渲染器
	const clientConfig = require('./webpack.client.config')
	clientConfig.plugins.push(new webpack.HotModuleReplacementPlugin())
	// api 的问题,为什么要用 clientConfig.entry.api
	clientConfig.entry.app = [
		'webpack-hot-middleware/client?quiet=true&reload=true',
		clientConfig.entry.app
	]
	clientConfig.output.filename = '[name].js'
	const clientCompiler = webpack(clientConfig)
	// 使用 中间件 文件保存在内存中---不再向物理磁盘读写数据
	const clientDevMiddleware = devMiddleware(clientCompiler, {
		publicPath: clientConfig.output.publicPath,
		logLevel: 'silent'	// 不打印日志 这里有个坑 5.0.0版本废弃了这个属性,3.7.2版本这个属性可以用
	})
	clientCompiler.hooks.done.tap('client', () => {
		const clientManifestStr = clientDevMiddleware.fileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8')	
		clientManifest = JSON.parse(clientManifestStr)
		// console.log(clientManifest)
		update()
	})
	server.use(hotMiddleware(clientCompiler, {
		log: false	// 关闭它本身的日志输出
	}))
	// 将 clientDevMiddleware 挂载到 express 服务器上 提供对内存中数据的访问
	server.use(clientDevMiddleware)

	return onReady
}

四、深入路由处理

上面的学习中,你可能注意到,在server.js 中我们使用了 / 来处理程序,它只能匹配到根路径的访问,而我们在实际开发中会有各种不同的路由页面,需要针对不同的路由进行不同的处理;在下面的学习中,我们将深入理解路由配置处理的相关知识。

vue-ssr 推荐使用 vue-router 的路由,你可以去看一下官网的使用介绍,这里不再赘述。在这里我们创建了pages文件夹并写了3个页面来模拟(404页面、首页、关于页),创建了router文件夹并在index.js文件中进行路由配置,配置方式和 vue-router 类似。

1、配置路由

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: '*',
				name: '404',
				component: () => import('@/pages/404')	// 路由懒加载
			}
		]
	})
	return router
}

2、注册路由实例

app.js中:

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

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

3、服务端适配

entry-server.js中,我们直接使用官网的代码,然后进行对应的修改。 官网代码

import { 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
}

server.js 中优化路由匹配和 render函数:

const render = async (req, res) => {
	try {
		const html = await renderer.renderToString({ url: req.url })
		// 发送渲染好的内容前,设置响应头当中的 Content-Type, 解决乱码问题
		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) => {
		await onReady
		// 等待有了 renderer 渲染器以后,调用 render 进行渲染
		render(req, res)
	}
)

4、客户端适配

官网中是这么说的:需要注意的是,你仍然需要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,才能正确地调用组件中可能存在的路由钩子。

所以,在 entry-client 中,需要进行如下改造:

import { createApp } from './app'
// 客户端特定引导逻辑……
const { app, router } = createApp()
// 这里 App.vue 模板中根元素一定要具有 `id="app"`
router.onReady(() => {
  	app.$mount('#app')
})

5、配置路由出口

app.vue 中 配置路由出口

<template>
	<div id="app">
		<h1>{{ message }}</h1>
		<input type="text" v-model="message">
		<button @click="myClick">按钮</button>
		<br><br><br>
		
		<router-link to="/">Home</router-link>
		<router-link to="/about">About</router-link>
		<!-- 这里是路由出口 -->
		<router-view />
	</div>
</template>

<script>
	export default {
		name: 'App',
		data() {
			return {
				message: 'hello vue-ssr'
			}
		},
		methods: {
			myClick() {
				console.log('hello vue-ssr')
			}
		}
	}
</script>

6、启动服务 npm run dev 访问页面 localhost: 3000

可以看到页面正常运行了,而且路由切换以及动态交互都是没问题的。

至此,我们的路由处理模块的学习就结束了,业务开发各有不同,但处理逻辑万变不离其宗,在实际开发中根据实际运用场景进行相应的修改就可以了。

PS: 推荐一个meta插件,用来管理页面Head内容,简单好用,这个案例中已用,最上面已经贴上了案例源码,在这不再赘述。 github地址 vue-meta官网

五、数据预取和状态管理

对于纯客户的渲染,客户端发送请求到服务器,服务器返回数据给客户端,客户端把返回的数据渲染到页面就大功告成,但是在服务端渲染中,这么做就会有一定的问题(客户端会重新渲染,导致两次渲染的问题)。官网中是这么描述的:在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据

接下来我们用一个文章列表(posts)组件,来学习一下数据预取和状态管理的相关知识。

1、解决思路

其实思路很简单,就是在服务端渲染之前就把数据取出来,放到外部容器中,这里容器官网推荐使用 vuex ;然后再把数据同步到客户端,就避免了客户端重新渲染的问题

2、安装依赖

首先要装一下axiosvuex

3、store容器的创建和配置

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: {
			async getPosts({ commit }) {
				const { data } = await axios({
					method: 'get',
					url: '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)
// 自定义 meta 显示模板
Vue.mixin({
	metaInfo: {
		titleTemplate: '%s - spades-vue-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 }
}

pages/posts.js 组件中:

<template>
	<div class="page-wrapper">
		<ul>
			<li v-for="item in posts" :key="item.id">{{item.title}}</li>
		</ul>
	</div>
</template>
<script>
	import { mapState, mapActions } from 'vuex'
	export default {
		name: 'PostsList',
		metaInfo() {
			return {
				title: '列表'
			}
		},
		computed: {
			...mapState(['posts'])
		},
		// Vue SSR 特殊为 服务端渲染提供的生命周期钩子函数
		serverPrefetch() {
			// 发起 action 返回 Promise
			return this.getPosts()
		},
		methods: {
			...mapActions(['getPosts'])
		}
	}
</script>

4、数据预取

服务端 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))
	// 在所有预取钩子(preFetch hook) resolve 后,
    // 我们的 store 现在已经填充入渲染应用程序所需的状态。
    // 当我们将状态附加到上下文,并且 `template` 选项用于 renderer 时,
    // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
    context.state = store.state
	return app
}

服务端 entry-client.js 中:

import { createApp } from './app'
const { app, router, store } = createApp()
// 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中。
// 而在客户端,在挂载到应用程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}
// 这里 App.vue 模板中根元素一定要具有 `id="app"`
router.onReady(() => {
  	app.$mount('#app')
})

6、运行测试

运行 npm run dev 访问 http://localhost:3000/posts 可以看到列表正确显示了。

至此,我们对 vue-ssr 的学习也结束了。在学习过程中如果有问题可以查看官网的介绍,所有知识点基本上都可以在官网找到相关介绍。

PS:接下来会抽时间学习下如何封装vue.js组件库,体验造轮子的快感…

 类似资料: