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>
<h1>{{msg}}</h1>
</div>
`,
data() {
return {
msg: '牛批',
}
},
})
renderer.renderToString(app, (err, html) => {
if (err) throw err
console.log(html)
})
npm i express
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>
<h1>{{msg}}</h1>
</div>
`,
data() {
return {
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>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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 http://localhost:3000')
)
server.js
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
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>
<h1>{{msg}}</h1>
</div>
`,
data() {
return {
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(html)
})
})
server.listen(3000, () =>
console.log('Server running at http://localhost:3000')
)
index.template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
server.js
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
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>
<h1>{{msg}}</h1>
</div>
`,
data() {
return {
msg: '牛批',
}
},
})
renderer.renderToString(
app,
{
title: 'SSR',
meta: '<meta name="description" content="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 http://localhost:3000')
)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{{meta}}}
<title>{{title}}</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
server.js
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
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>
<h1>{{msg}}</h1>
<div>
<input v-model="msg"/>
</div>
<div>
<button @click="handleClick">测试</button>
</div>
</div>
`,
data() {
return {
msg: '牛批',
}
},
methods: {
handleClick() {
console.log('两横一竖')
},
},
})
renderer.renderToString(
app,
{
title: 'SSR',
meta: '<meta name="description" content="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 http://localhost:3000')
)
src/app.js
import Vue from 'vue'
import App from './App.vue'
export function createApp() {
const app = new Vue({
render: (h) => h(App),
})
return { app }
}
src/App.vue
<template>
<div id="app">
<h1>{{ msg }}</h1>
<div>
<input v-model="msg" />
</div>
<div>
<button @click="handleClick">测试</button>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
msg: '牛批',
}
},
methods: {
handleClick() {
console.log('两横一竖')
},
},
}
</script>
src/entry-client.js
import { createApp } from './app'
const { app } = createApp()
app.$mount('#app')
src/entry-server.js
import { createApp } from './app'
export default (context) => {
const { app } = createApp()
// 服务端的路由处理、数据预取...
return app
}
npm i cross-env # 可以通过 npm scripts 设置跨平台的环境变量,以区分打包环境
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
build/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()],
}
build/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(),
],
})
build/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"
},
}
测试
npm run build:client
npm run build:server
npm run build
server.js
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
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')
// 注意:换成了 createBundleRenderer
const renderer = require('vue-server-renderer').createBundleRenderer(
serverBundle,
{
template,
clientManifest,
}
)
const server = express()
// 注意:托管静态资源,应该和打包出口当中的 publicPath 保持一致
server.use('/dist', express.static('./dist'))
server.get('/', (req, res) => {
// !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
renderer.renderToString(
{
title: 'SSR',
meta: '<meta name="description" content="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 http://localhost:3000')
)
期望自动打包,热更新!
package.json
{
"scripts": {
"start": "cross-env NODE_ENV=production && node server.js",
"dev": "node server.js"
},
}
server.js
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const isProd = process.env.NODE_ENV === 'production'
let renderer
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 = require('vue-server-renderer').createBundleRenderer(
serverBundle,
{
template,
clientManifest,
}
)
} else {
// 开发 -> 监听打包构建 -> 重新生成 Renderer 渲染器
}
const server = express()
server.use('/dist', express.static('./dist'))
const render = (req, res) => {
// !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
renderer.renderToString(
{
title: 'SSR',
meta: '<meta name="description" content="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.get(
'/',
isProd
? render
: (req, res) => {
// 等待有了 Renderer 渲染器以后,调用 render 进行渲染
render()
}
)
server.listen(3000, () =>
console.log('Server running at http://localhost:3000')
)
提取处理模块
const fs = require('fs')
const Vue = require('vue')
const express = require('express')
const server = express()
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')
const isProd = process.env.NODE_ENV === 'production'
server.use('/dist', express.static('./dist'))
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 = (req, res) => {
// !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
renderer.renderToString(
{
title: 'SSR',
meta: '<meta name="description" content="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.get(
'/',
isProd
? render
: async (req, res) => {
// 等待有了 Renderer 渲染器以后,调用 render 进行渲染
await onReady()
render()
}
)
server.listen(3000, () =>
console.log('Server running at http://localhost:3000')
)
build/setup-dev-server.js
module.exports = function(server, callback) {
const onReady = new Promise()
// 监视构建 -> 更新 Renderer
return onReady
}
build/setup-dev-server.js
module.exports = function(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 渲染器
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
return onReady
}
npm i chokidar
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
module.exports = function(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, 'utf8')
update()
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
return onReady
}
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const resolve = (file) => path.resolve(__dirname, file)
module.exports = function(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, 'utf8')
update()
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
serverCompiler.watch({}, (err, stats) => {
if (err) throw err // Webpack 的错误
if (stats.hasErrors()) return // 打包的结果中自己源代码是否有错
serverBundle = JSON.parse(
fs.readFileSync(
resolve('../dist/vue-ssr-server-bundle.json'),
'utf-8'
)
)
update()
})
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
return onReady
}
npm i webpack-dev-middleware -D
const fs = require('fs')
const path = require('path')
const chokidar = require('chokidar')
const webpack = require('webpack')
const devMiddleware = require('webpack-dev-middleware')
const resolve = (file) => path.resolve(__dirname, file)
module.exports = function(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, 'utf8')
update()
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler) // 如何关闭日志输出?
serverCompiler.hooks.done.tap('server', () => {
// serverDevMiddleware.fileSystem.readFileSync // 旧版本写法
serverBundle = JSON.parse(
serverDevMiddleware.context.outputFileSystem.readFileSync(
resolve('../dist/vue-ssr-server-bundle.json'),
'utf-8'
)
)
console.log(serverBundle)
update()
})
/* 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'
)
)
update()
}) */
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
return onReady
}
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 resolve = (file) => path.resolve(__dirname, file)
module.exports = function(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, 'utf8')
update()
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler)
// console.log(serverDevMiddleware.context.outputFileSystem.readFileSync)
serverCompiler.hooks.done.tap('server', () => {
serverBundle = JSON.parse(serverDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-server-bundle.json'), 'utf-8'))
console.log(serverBundle)
update()
})
// 监视构建 clientManifest -> 调用 update -> 更新 Renderer 渲染器
const clientConfig = require('./webpack.client.config')
const clientCompiler = webpack(clientConfig)
const clientDevMiddleware = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath,
})
clientCompiler.hooks.done.tap('client', () => {
clientManifest = JSON.parse(clientDevMiddleware.context.outputFileSystem.readFileSync(resolve('../dist/vue-ssr-client-manifest.json'), 'utf-8'))
update()
})
// 将 clientDevMiddleware 挂载到 Express 服务中,提供对其内部内存中数据的访问,不然会 404
server.use(clientDevMiddleware)
return onReady
}
server.js
server.get(
'/',
isProd
? render
: async (req, res) => {
// 等待有了 Renderer 渲染器以后,调用 render 进行渲染
// onReady is not a function
await onReady
render(req, res)
}
)
npm i webpack-hot-middleware -D
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 = function(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, 'utf8')
update()
chokidar.watch(templatePath).on('change', () => {
template = fs.readFileSync(templatePath, 'utf8')
update()
})
// 监视构建 serverBundle -> 调用 update -> 更新 Renderer 渲染器
const serverConfig = require('./webpack.server.config')
const serverCompiler = webpack(serverConfig)
const serverDevMiddleware = devMiddleware(serverCompiler)
// console.log(serverDevMiddleware.context.outputFileSystem.readFileSync)
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,
})
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
}
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: '*',
name: 'error404',
component: () => import('@/pages/404'),
},
],
})
return router
}
src/app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
export function createApp() {
const router = createRouter()
const app = new Vue({
router, // 将路由注册到跟市里
render: (h) => h(App),
})
return { app, router }
}
src/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)
})
} */
export default async (context) => {
const { app, router } = createApp()
router.push(context.url)
await new Promise(router.onReady.bind(router))
return app
}
server.js
const fs = require('fs')
const express = require('express')
const server = express()
const { createBundleRenderer } = require('vue-server-renderer')
const setupDevServer = require('./build/setup-dev-server')
const isProd = process.env.NODE_ENV === 'production'
server.use('/dist', express.static('./dist'))
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 {
// !删除了 app 实例,因为会自动去 entry-server.js 里面找到实例
const html = await renderer.renderToString({
title: 'SSR',
meta: '<meta name="description" content="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 进行渲染
// onReady is not a function
await onReady
render(req, res)
}
)
server.listen(3000, () => console.log('Server running at http://localhost:3000'))
entry-client.js
import { createApp } from './app'
const { app, router } = createApp()
router.onReady(() => {
app.$mount('#app')
})
link 标签的 rel 属性值 preload 和 prefetch 的区别是什么
App.vue
<template>
<div id="app">
<ul>
<li>
<router-link to="/">Home</router-link>
</li>
<li>
<router-link to="/about">About</router-link>
</li>
</ul>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
msg: '牛批',
}
},
methods: {
handleClick() {
console.log('两横一竖')
},
},
}
</script>
让不同的页面拥有自己的 head
npm i vue-meta
src/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 - SSR 学习',
},
})
export function createApp() {
const router = createRouter()
const app = new Vue({
router, // 将路由注册到跟市里
render: (h) => h(App),
})
return { app, router }
}
src/entry-server.js
import { createApp } from './app'
export default async (context) => {
const { app, router } = createApp()
const meta = app.$meta()
router.push(context.url)
context.meta = meta
await new Promise(router.onReady.bind(router))
return app
}
index.template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{{meta.inject().meta.text()}}}
{{{meta.inject().title.text()}}}
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
src/pages/Home.vue
<template>
<div>Home</div>
</template>
<script>
export default {
name: 'Home',
metaInfo: {
title: '首页',
},
}
</script>
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'
export default {
name: 'Posts',
components: {},
data() {
return {
posts: [],
}
},
// !服务端渲染
// 只支持 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>
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 }) {
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 - SSR 学习',
},
})
export function createApp() {
const router = createRouter()
const store = createStore()
const app = new Vue({
router, // 将路由注册到跟市里
store,
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: 'Posts',
data() {
return {
// posts: [],
}
},
computed: {
...mapState(['posts']),
},
// !服务端渲染
// 只支持 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')
}, */
methods: {
...mapActions(['getPosts']),
},
// Vue SSR 为服务端渲染提供的一个生命周期函数
serverPrefetch() {
// 发起 action,返回 Promise
return this.getPosts()
},
}
</script>
问题:客户端没有此数据,和服务端数据同步失败
src/entry-server.js
import { createApp } from './app'
export default async (context) => {
const { app, router, store } = createApp()
const meta = app.$meta()
router.push(context.url)
context.meta = meta
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
}
src/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')
})
客户端用自己的数据和服务端进行比对,一比对发现一致,直接接管服务端数据进行使用,并混合进去和动态交互相关的功能,例如点击事件,双向数据绑定等等
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。