简单的初始化体验
安装
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)
服务器上的数据响应
因为实际的渲染过程需要确定性,所以我们也将在服务器上“预取”数据 (“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 结构,比如会在
实现开发模式自动打包和热更新
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')
})