搭建本人的 SSR、动态站点生成(SSG)及封装 Vue.js 组件库
搭建本人的 SSR
一、渲染一个 Vue 实例
mkdir vue-ssr
cd vue-ssr
npm init -y
npm i vue vue-server-renderder
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: '拉钩教育'}
})
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.js
const 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 模板
- 创立 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.js
const 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.png - 源码构造
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) 装置生产依赖
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 解决图片资源
- 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"
}
- 启动利用
erver.js
const 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.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()
// 申请前缀,应用 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 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.js
module.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.watchFile
chokidar.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' // 热更新模式下确保统一的 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()})
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' // 热更新模式下确保统一的 hash
const hotMiddleware = require('webpack-hot-middleware')
server.use(hotMiddleware(clientCompiler, {log: false // 敞开它自身的日志输入}))
七、编写通用利用注意事项
八、路由解决
- 配置 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
}
- 将路由注册到根实例
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'
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.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 适配
咱们的服务器代码应用了一个 * 处理程序,它承受任意 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)
})
// ...
- 适配客户端入口
须要留神的是,你依然须要在挂载 app 之前调用 router.onReady,因为路由器必须要提前解析路由配置中的异步组件,能力正确地调用组件中可能存在的路由钩子。这一步咱们曾经在咱们的服务器入口 (server entry) 中实现过了,当初咱们只须要更新客户端入口 (client entry):
// entry-client.js
import {createApp} from './app'
const {app, router} = createApp()
router.onReady(() => {app.$mount('#app')
})
- 解决实现
路由进口:
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>
八、治理页面
- 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: '对于'}
}
九、数据预取和状态治理
- 思路剖析
在服务器端渲染 (SSR) 期间,咱们实质上是在渲染咱们应用程序的 ” 快照 ”,所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,须要先预取和解析好这些数据。
另一个须要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,须要获取到与服务器端应用程序完全相同的数据 – 否则,客户端应用程序会因为应用与服务器端应用程序不同的状态,而后导致混合失败。
为了解决这个问题,获取的数据须要位于视图组件之外,即搁置在专门的数据预取存储容器 (data store) 或 ” 状态容器 (state container))” 中。首先,在服务器端,咱们能够在渲染之前预取数据,并将数据填充到 store 中。此外,咱们将在 HTML 中序列化(serialize) 和内联预置 (inline) 状态。这样,在挂载 (mount) 到客户端应用程序之前,能够间接从 store 获取到内联预置 (inline) 状态。
- 数据预取
npm install 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}) { // 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>
- 将数据预取同步到客户端
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))
// 这个 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.js
import {createApp} from './app'
const {app, router, store} = createApp()
if (window.__INITIAL_STATE__) {store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {app.$mount('#app')
})