一、创建一个Vue实例
先创建一个服务器入口文件server.js,安装Vue提供的服务端渲染的工具npm install vue vue-server-renderer --save 如果是客户端渲染会有<div id=”app”></div> 的标签,最终内容是通过JS动态渲染进去的,但对SEO不太友好,所以要用服务端渲染 先渲染一个Vue实例
const Vue = require('vue')
const app = new Vue({
template: '<div>Hello Vue SSR!</div>'
})
然后使用vue-server-renderer的createRenderer方法创建一个渲染器,再调用renderToString把Vue实例渲染成字符串返回
const renderer = require('vue-server-renderer').createRenderer()
renderer.renderToString(app, (err, html) => {
if(err) throw err;
console.log(html)
})
执行server.js node server.js 可以看到渲染后的html模板结果
接下来就是创建一个服务器,然后把html模板结果返回 下载express框架 npm install express --save 引入并执行express
const server = require('express')()
然后处理get请求
server.get('*', (req, res) => {}
所有的get请求都经过该中间件,里面创建一个Vue实例,它含有一个url属性返回请求路径,再把它渲染到页面
const app = new Vue({
data: {
url: req.url
},
template: '<div>当前访问的url是:{{url}}</div>'
})
然后把Vue实例用渲染器渲染到页面上,如果出错则报错,否则就返回HTML模板
renderer.renderToString(app, (err, html) => {
if(err) {
res.status(500).end('服务器内部错误')
} else{
res.end(`
<!DOCTYPE html>
<html lang="zh">
<meta charset="utf-8"></meta>
<head><title>Hello SSR</title></head>
<body>${html}</body>
</html>
`)
}
})
服务器运行在8080端口
server.listen(8080, () => {
console.log('服务器运行在8080端口')
})
访问8080端口,可以看到页面渲染成功
然后查看源代码,可以看到他是显示一个完整的HTML模板
Nuxt也是这样做的,只不过做了很多方法的封装
二、使用模板
单独新建一个index.template.html文件要想服务器返回的HTML模板插入到template文件,先在template.html文件添加一个固定的注释节点
<body>
</body>
引入template文件,那么渲染器就会把模板渲染到注释节点上去
const VueServerRenderer = require('vue-server-renderer')
const template = fs.readFileSync('./index.template.html', 'utf-8')
const renderer = VueServerRenderer.createRenderer({
template
})
所以就不用手写模板字符串了,不够优雅, 直接返回渲染后的模板
renderer.renderToString(app, (err, html) => {
if(err) {
res.status(500).end('服务器内部错误')
} else{
res.end(html)
}
})
此外还可以传入额外内容,例如定义一个上下文对象,保存title标题和meta元信息
const context = {
title: 'Vue SSR',
metas: `
<meta name="keyword" content="vue,ssr">
<meta name="description" content="vue ssr demo">
`
}
在渲染器第二个参数传入context
renderer.renderToString(app, context, (err, html) => {})
然后使用插值的方式插入模板文件
可以看到标题成功渲染,而元信息标签被转义后渲染,所以要使用{{{}}} 插值则原封不动地渲染
三、服务端渲染Vue项目的源码结构
vue-ssr官方图
app.js就是整个Vue程序的入口文件,客户端渲染的Vue2项目流程
import Vue from 'vue'
new Vue().$mount()
但是我们服务端Node.js是没有DOM这个概念的,只有ECMAscript,所以$mount挂载的代码应该移动到客户端里面去,所以存在两个入口:一个服务端入口(Server entry)和一个客户端入口(Client entry) 所以先定义一个App.vue组件
<template>
<div id="app">
<div class="demo" @click="onClick">
一段内容
</div>
</div>
</template>
<script>
export default {
methods: {
onClick() {
console.log('handleClick')
}
}
}
</script>
<style>
.demo {
width: 300px;
height: 300px;
background-color: orange;
}
</style>
app.js定义一个工厂函数返回一个渲染渲染App组件的Vue实例,但先不挂载到页面
import Vue from 'vue'
import App from './App.vue'
export function createApp() {
const app = new Vue({
render: h => h(App)
})
return { app }
}
这时回到客户端入口,引入并执行工厂函数,并把App组件挂载到#app节点即可
import { createApp } from './app.js'
const { app } = createApp()
app.$mount('#app')
服务端入口一样创建Vue实例,但是导出一个函数在每次请求时执行创建并返回一个新实例防止污染。
import { createApp } from "./app";
export default () => {
const { app } = createApp()
return app
}
中间通过webpack打包客户端入口打包到browser,服务端的打包到Node server,server bundle作用是渲染器根据它渲染出Vue的服务端渲染结果,他是一个静态内容字符串,插入到HTML里面,返回到浏览器端,所以他没有动态交互逻辑。此时就需要客户端打包后的结果经过hydrate(激活)动态逻辑。
先配置webpack配置 webpack.base是公共配置
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const resolve = (dir) => {
return path.resolve(__dirname, dir)
}
module.exports = {
output: {
filename: '[name].bundle.js',
path: resolve('../dist')
},
resolve: {
extensions: ['.js', '.vue']
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
},
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
new VueLoaderPlugin()
]
}
webpack.client是客户端特有配置,比如入口不一样
const path = require('path')
const { merge } = require('webpack-merge')
const base = require('./webpack.base')
const resolve = (dir) => {
return path.resolve(__dirname, dir)
}
module.exports = merge(base, {
entry: {
client: resolve('../src/entry-client.js')
}
})
webpack.server是服务端特有配置,它要配置和node风格匹配的相关输入和目标等
const path = require('path')
const { merge } = require('webpack-merge')
const base = require('./webpack.base')
const resolve = (dir) => {
return path.resolve(__dirname, dir)
}
module.exports = merge(base, {
entry: {
server: resolve('../src/entry-server.js')
},
target: 'node',
output: {
libraryTarget: 'commonjs2'
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.ssr.html',
template: resolve('../public/index.ssr.html'),
excludeChunks: ['server'],
minify: {
removeComments: false
}
})
]
})
运行npm run client:build打包得到dist目录下client.bundle.js客户端的代码 然后再运行npm run server:build得到服务端打包后的html文件和服务端的js代码,但server.bundle.js不会直接引入到index.ssr.html,而是在服务器中使用
四、客户端激活
用渲染器的createBundleRenderer方法可以把如css之类的样式插入到模板,第一个参数是打包后的server文件,然后配置打包后的html模板
const serverBundle = fs.readFileSync('./dist/server.bundle.js', 'utf-8')
const template = fs.readFileSync('./dist/index.ssr.html', 'utf-8')
const renderer = VueServerRenderer.createBundleRenderer(serverBundle, {
template
})
再把服务端渲染的结果返回到浏览器
server.get('*', (req, res) => {
renderer.renderToString().then(html => {
res.send(html)
})
})
此时可以看到html渲染完成 查看源代码可以看到是服务端渲染后的结果 但是css样式和点击事件不生效,那是因为html中没有引入相应的client打包后的js文件进行样式添加和交互逻辑的添加,所以需要进行客户端的激活。本质上是把客户端打包后的结果放在服务端生成的html文件上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue SSR</title>
</head>
<body>
<script src="./client.bundle.js"></script>
</body>
</html>
运行服务器可以看到client文件已被激活,但是还是不生效 原因是client.bundle.js不被视作为静态资源文件,所以需要配置静态资源
server.use(express.static(path.resolve(__dirname, 'dist')))
此时js文件能够正确加载,样式生效 点击事件也能正常触发
|