SSR 官方文档
:::tip 官方介绍
Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。
:::
在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。
官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对 Vue SSR 有更加深入的了解。
该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。
NUXT 提供了平滑的开箱即用的体验,它建立在同等的 Vue 技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。
项目 demo
yarn add vue vue-server-renderer
npm i vue vue-server-renderer
:::warning 注意
:::
server.js
/**
* @author Wuner
* @date 2020/9/8 16:13
* @description
*/
const Vue = require('vue');
// 第 1 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer();
// 第 2 步:创建一个 Vue 实例
const app = new Vue({
template: `{{ message }}`,
data: {
message: 'Hello World',
},
});
// 第 3 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
if (err) throw err;
console.log(html);
// => Hello World
});
// 在 2.5.0+,如果没有传入回调函数,则会返回 Promise:
renderer
.renderToString(app)
.then((html) => {
console.log(html);
})
.catch((err) => {
console.error(err);
});
在 Node.js 服务器中使用时相当简单直接,例如 Express :
yarn add express
npm i express
在 Web 服务中渲染 Vue 实例:
/**
* @author Wuner
* @date 2020/9/8 16:13
* @description
*/
const Vue = require('vue');
// 第 1 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer();
// 第2步:创建 service
const service = require('express')();
service.get('/', (req, res) => {
// 第 3 步:创建一个 Vue 实例
const app = new Vue({
template: `
{{ message }}`,
data: {
message: 'Hello World',
},
});
// 第 4 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, (err, html) => {
// 异常时,抛500,返回错误信息,并阻止向下执行
if (err) {
res.status(500).end('Internal Server Error');
return;
}
// 返回HTML
res.end(`
Hello
${html}
`);
});
});
// 绑定并监听指定主机和端口上的连接
service.listen(3000, () =>
console.log(`service listening at http://localhost:3000`),
);
启动服务
node service.js
此时可以看到页面展示着 Hello World
不过使用中文时,会出现中文乱码
// 返回HTML
res.end(`
Hello
${html}
`);
// 设置响应头,解决中文乱码
res.setHeader('Content-Type', 'text/html;charset=utf8');
小编承接外包,有意者可加
QQ:1944300940
微信号:wxid_g8o2y9ninzpp12
当你在渲染 Vue 应用程序时,renderer 只从应用程序生成 HTML 标记 (markup)。在这个示例中,我们必须用一个额外的 HTML 页面包裹容器,来包裹生成的 HTML 标记。
为了简化这些,你可以直接在创建 renderer 时提供一个页面模板。多数时候,我们会将页面模板放在特有的文件中,例如 index.template.html:
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Titletitle>
head>
<body>
body>
html>
::: warning 注意
注释 – 这里将是应用程序 HTML 标记注入的地方。
:::
然后,我们可以读取和传输文件到 Vue renderer 中:
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8'),
});
// 返回HTML, 该html的值 将是注入应用程序内容的完整页面
res.end(html);
模板还支持简单插值。给定如下模板:
<html>
<head>
<title>{{ title }}title>
{{{ meta }}}
head>
<body>
body>
html>
我们可以通过传入一个"渲染上下文对象",作为 renderToString 函数的第二个参数,来提供插值数据:
const context = {
title: 'vue ssr demo',
metas: `
`,
};
// 第 4 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, context, (err, html) => {
// 异常时,抛500,返回错误信息,并阻止向下执行
if (err) {
res.status(500).end('Internal Server Error');
return;
}
// 返回HTML, 该html的值 将是注入应用程序内容的完整页面
res.end(html);
});
也可以与 Vue 应用程序实例共享 context 对象,允许模板插值中的组件动态地注册数据。
此外,模板支持一些高级特性,例如:
/**
* @author Wuner
* @date 2020/9/8 16:13
* @description
*/
const Vue = require('vue');
// 第 1 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8'),
});
// 第2步:创建 service
const service = require('express')();
const context = {
title: 'vue ssr demo',
metas: `
`,
};
service.get('/', (req, res) => {
// 设置响应头,解决中文乱码
res.setHeader('Content-Type', 'text/html;charset=utf8');
// 第 3 步:创建一个 Vue 实例
const app = new Vue({
template: `
{{ message }}`,
data: {
message: 'Hello World',
},
});
// 第 4 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, context, (err, html) => {
// 异常时,抛500,返回错误信息,并阻止向下执行
if (err) {
res.status(500).end('Internal Server Error');
return;
}
// 返回HTML, 该html的值 将是注入应用程序内容的完整页面
res.end(html);
});
});
// 绑定并监听指定主机和端口上的连接
service.listen(3000, () =>
console.log(`service listening at http://localhost:3000`),
);
项目 demo
到目前为止,我们还没有讨论过如何将相同的 Vue 应用程序提供给客户端。为了做到这一点,我们需要使用 webpack 来打包我们的 Vue 应用程序。事实上,我们可能需要在服务器上使用 webpack 打包 Vue 应用程序,因为:
通常 Vue 应用程序是由 webpack 和 vue-loader 构建,并且许多 webpack 特定功能不能直接在 Node.js 中运行(例如通过 file-loader 导入文件,通过 css-loader 导入 CSS)。
尽管 Node.js 最新版本能够完全支持 ES2015 特性,我们还是需要转译客户端代码以适应老版浏览器。这也会涉及到构建步骤。
所以基本看法是,对于客户端应用程序和服务器应用程序,我们都要使用 webpack 打包 - 服务器需要「服务器 bundle」然后用于服务器端渲染(SSR),而「客户端 bundle」会发送给浏览器,用于混合静态标记。
现在我们正在使用 webpack 来处理服务器和客户端的应用程序,大部分源码可以使用通用方式编写,可以使用 webpack 支持的所有功能。同时,在编写通用代码时,有一些 事项 要牢记在心。
一个基本项目可能像是这样:
src
├── components
│ ├── Foo.vue
│ ├── Bar.vue
│ └── Baz.vue
├── App.vue
├── app.js # 通用 entry(universal entry)
├── entry-client.js # 仅运行于浏览器
└── entry-server.js # 仅运行于服务器
app.js
是我们应用程序的「通用 entry」。在纯客户端应用程序中,我们将在此文件中创建根 Vue 实例,并直接挂载到 DOM。但是,对于服务器端渲染(SSR),责任转移到纯客户端 entry 文件。app.js
简单地使用 export 导出一个 createApp
函数:
/**
* @author Wuner
* @date 2020/9/8 17:51
* @description
*/
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 只需创建应用程序,并且将其挂载到 DOM 中:
/**
* @author Wuner
* @date 2020/9/8 17:55
* @description
*/
import { createApp } from './app';
// 客户端特定引导逻辑……
const { app } = createApp();
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app');
服务器 entry 使用 default export 导出函数,并在每次渲染中重复调用此函数。此时,除了创建和返回应用程序实例之外,它不会做太多事情 - 但是稍后我们将在此执行服务器端路由匹配 (server-side route matching) 和数据预取逻辑 (data pre-fetching logic)。
/**
* @author Wuner
* @date 2020/9/8 17:57
* @description
*/
import { createApp } from './app';
export default (context) => {
const { app } = createApp();
return app;
};
/**
* @author Wuner
* @date 2020/9/8 16:13
* @description
*/
const Vue = require('vue');
// 第 1 步:创建一个 renderer
const renderer = require('vue-server-renderer').createRenderer({
template: require('fs').readFileSync('./index.template.html', 'utf-8'),
});
// 第2步:创建 service
const service = require('express')();
const context = {
title: 'vue ssr demo',
metas: `
`,
};
service.get('/', (req, res) => {
// 设置响应头,解决中文乱码
res.setHeader('Content-Type', 'text/html;charset=utf8');
// 第 3 步:创建一个 Vue 实例
const app = new Vue({
template: `
{{ message }}`,
data: {
message: 'Hello World',
},
});
// 第 4 步:将 Vue 实例渲染为 HTML
renderer.renderToString(app, context, (err, html) => {
// 异常时,抛500,返回错误信息,并阻止向下执行
if (err) {
res.status(500).end('Internal Server Error');
return;
}
// 返回HTML, 该html的值 将是注入应用程序内容的完整页面
res.end(html);
});
});
// 绑定并监听指定主机和端口上的连接
service.listen(3000, () =>
console.log(`service listening at http://localhost:3000`),
);
项目 demo
yarn add vue vue-server-renderer express cross-env
npm i vue vue-server-renderer express cross-env
包 | 说明 |
---|---|
vue | Vue.js 核心库 |
vue-server-renderer | Vue 服务端渲染工具 |
express | 基于 Node 的 Web 服务框架 |
cross-env | 通过 npm scripts 设置跨平台环境变量 |
yarn add 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 -D
npm i 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 -D
包 | 说明 |
---|---|
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 | 处理图片资源 |
build
├── webpack.base.config.js # 公共配置
├── webpack.client.config.js # 客户端打包配置文件
└── webpack.server.config.js # 服务端打包配置文件
webpack.base.config.js
/**
* @author Wuner
* @date 2020/9/25 12:10
* @description 公共配置
*/
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` 文件中的 `