在服务端把 Vue实例渲染为纯文本字符串
mkdir vue-ssr
cd vue-ssr
npm init -y
npm install vue vue-server-renderer
// server.js
const vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const app = new Vue({
template: `<div id="app">
<h1>{{msg}}</h1>
</div>`,
data: {
msg: 'xxx'
}
})
renderer.renderToString(app, (err, html)=> {
if(err) throw err
console.log(html)
})
// 运行
node server.js
将渲染结果发送到客户端
// 安装 web服务
npm install express nodemon --save
// 加载并启动服务 server.js
const Vue = require('vue')
const renderer = require('vue-server-renderer').createRenderer()
const express = require('express')
const server = express()
server.get('/', (req, res) => {
const app = new Vue({
template: `<div>
<h1>{{ msg }}</h1>
</div>`,
data: {
msg: 'xxx'
}
})
renderer.renderToString(app, (err, html) => {
if(err) {
return res.status(500).end('Internal Server Error.')
}
// 为html 设置 UTF-8编码
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// res.end(html)
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', () => { // 一个端口对应一个程序,总共有 65535个端口
console.log('server running at port 3000.')
})
// 使用nodemon 运行
npx nodemon server.js
//index.tempalte.html
<body>
<!--vue-ssr-outlet--> // html会插入到这个位置
</body>
// server.js
const fs = require('fs')
const renderer = require('vue-server-renderer').createRenderer({
template: fs.readFileSync('./index.template.html', 'utf-8')
})
// ...
res.end(html) // 此时发送的html, 是与模板结合后的
// server.js 多加一个参数
renderer.renderToString(app, {
title: '使用外部数据',
meta: `<meta name="description" content="vue ssr 插入外部数据">`
},(err, html) => { // ... })
// index.tempalte.html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{{{ meta }}} // html 使用 {{{ }}}
<title>{{ title }}</title> // 字符使用 {{ }}
服务端渲染只是将实例渲染成字符串,并没有实现客户端交互的功能。
服务端入口——> 打包——>渲染——>生成HTML
客户端入口——> 打包——>接管已生成的HTML ——> 激活交互功能
// 1. src/App.vue
<template>
<div id="app">
<h1> {{ msg }}</h1>
<p>
<input type="text" v-model="msg"/>
</p>
<button @click="onClick">点击</button>
</div>
</template>
<script>
export default {
name: 'App',
data(){
return {
msg: 'xxx'
}
},
methods: {
onClick(){
console.log('lagou ~')
}
}
}
</script>
// 2. src/app.js
/**
* 通用入口文件
* 纯客户端时,负责创建实例,并挂载到DOM。SSR,责任转移到纯客户端的entry 文件
* */
import Vue from 'vue'
import App from './App.vue'
// 导出一个工厂函数,用于创建新的应用程序、router、store 实例
export default createApp(){
const app = new Vue({
render: h => h(app)
})
return { app }
}
// 3. entry-client.js
/**
* 客户端入口文件
* 负责创建应用程序,并挂载到DOM
*
* */
import { createApp } from './app'
// 客户端特定引导逻辑...
const { app } = createApp()
// 挂载到 App.vue 中的 id="app"
app.$mount('#app')
// 4. entry-server.js
/**
* 服务端入口文件
*
* */
import { createApp } from './app'
export default context => {
const { app } = createApp()
// 服务端路由处理、数据预取...
return app
}
// 1. 生产依赖
vue Vue.js核心库
vue-server-renderer Vue服务端渲染工具
express 基于Node的Web服务框架
cross-en 通过npm sripts 设置的跨平台环境变量
// 2. 安装开发依赖
webpack
webpack-cli
webpack-merge
webpack-node-externals // 排除webpack中的node模块
@babel/core
@babel/plugin-transform-runtime
@babel/preset-env
babel-loader
css-loader
url-loader
file-loader
rimraf // 基于Node封装的跨平台 rm -rf 工具,清除之前的dist
vue-loader
vue-template-compiler
friendly-errors-webpack-plugin // webpack错误提示
// 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"
},
// server.js
// 在服务端将vue实例渲染为字符串
const Vue = require('vue')
const fs = require('fs')
const express = require('express')
// 加载打包资源
const template = fs.readFileSync('./index.template.html', 'utf-8')
const serverBundle = require('./dist/vue-ssr-server-bundle.json')
const clientManifest = require('./dist/vue-ssr-client-manifest.json')
const renderer = require('vue-server-renderer').createBundleRenderer(serverBundle, { // 打包后的启动 createRenderer ——> createBundleRenderer, 并加载打包后的文件
template,
clientManifest
})
// 启动服务
const server = express()
// 查找静态资源,处理返回
server.use('/dist/', express.static('./dist'))
server.get('/', (req, res) => {
renderer.renderToString({ // 打包中自动创建的实例
title: '打包运行',
meta: `<meta name="description" content="vue ssr">`
},(err, html) => {
if (err) {
console.dir(err, 'err')
return res.status(500).end('Internal Server Error.')
}
// 为html 设置 UTF-8编码
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(html)
})
})
server.listen('8080', () => {
console.log('server running at port 8080.')
})
注意: 使用 【SSR + 客户端混合】时,浏览器可能会更改一些特殊的HTML结构。如 <table>会自动注入<tbody>。 为能够正确匹配,请确保在模板中写入有效的HTML。
问题:路由、数据预取、每次构建都要重启服务
开发模式下,需要不断的重新生成打包文件,重新生成 renderer 成为核心操作。
// package.json
"scripts": {
"start": "cross-env NODE_ENV=production node server.js",
"dev": "node server.js"
},
/* 此时 代码 vue-ssr 调试 没有顺利进行 */
webpack ——> watch()
使用 webpack-dev-middleware 插件
使用 webpack-hot-middleware 插件
使用Vue-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,
render: h => h(App)
})
return {
app,
router
}
}
使用 vue-meta 插件
服务端不支持异步获取数据,需要 preload 或 prefetch, 放到 vuex 中。