搭建自己的 SSR

Study Notes

Vue SSR 介绍

SSR 官方文档

什么是服务器端渲染 (SSR)?

  • 官方文档
  • Vue SSR(Vue.js Server-Side Rendering) 是 Vue.js 官方提供的一个服务端渲染(同构应用)解决方案
  • 使用它可以构建同构应用
  • 还是基于原有的 Vue.js 技术栈

:::tip 官方介绍

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

:::

使用场景

在对你的应用程序使用服务器端渲染 (SSR) 之前,你应该问的第一个问题是,是否真的需要它。

技术层面:

  • 更快的首屏渲染速度
  • 更好的 SEO

业务层面:

  • 不适合管理系统
  • 适合门户资讯类网站,例如企业官网、知乎、简书等
  • 适合移动网站

如何实现 Vue SSR

基于 Vue SSR 官方文档提供的解决方案

官方方案具有更直接的控制应用程序的结构,更深入底层,更加灵活,同时在使用官方方案的过程中,也会对 Vue SSR 有更加深入的了解。

该方式需要你熟悉 Vue.js 本身,并且具有 Node.js 和 webpack 的相当不错的应用经验。

Nuxt.js 开发框架

NUXT 提供了平滑的开箱即用的体验,它建立在同等的 Vue 技术栈之上,但抽象出很多模板,并提供了一些额外的功能,例如静态站点生成。通过 Nuxt.js 可以快速的使用 Vue SSR 构建同构应用。

Vue SSR 基本使用

项目 demo

安装

yarn

yarn add vue vue-server-renderer

npm

npm i vue vue-server-renderer

:::warning 注意

  • 推荐使用 Node.js 版本 6+。
  • vue-server-renderer 和 vue 必须匹配版本。
  • vue-server-renderer 依赖一些 Node.js 原生模块,因此只能在 Node.js 中使用。我们可能会提供一个更简单的构建,可以在将来在其他「JavaScript 运行时(runtime)」运行。

:::

渲染一个 Vue 实例

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 :

安装 express

yarn
yarn add express
npm
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

不过使用中文时,会出现中文乱码

如何解决中文乱码

  • meta 设置编码字符集为 utf-8
// 返回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 对象,允许模板插值中的组件动态地注册数据。

此外,模板支持一些高级特性,例如:

  • 在使用 *.vue 组件时,自动注入「关键的 CSS(critical CSS)」;
  • 在使用 clientManifest 时,自动注入「资源链接(asset links)和资源预加载提示(resource hints)」;
  • 在嵌入 Vuex 状态进行客户端融合(client-side hydration)时,自动注入以及 XSS 防御。

完整实例代码

/**
 * @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」会发送给浏览器,用于混合静态标记。

搭建自己的 SSR_第1张图片

使用 webpack 的源码结构

现在我们正在使用 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

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-client.js:

客户端 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-server.js:

服务器 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;
};
server.js
/**
 * @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
yarn add vue vue-server-renderer express cross-env
npm
npm i vue vue-server-renderer express cross-env
说明
vue Vue.js 核心库
vue-server-renderer Vue 服务端渲染工具
express 基于 Node 的 Web 服务框架
cross-env 通过 npm scripts 设置跨平台环境变量

安装开发依赖

yarn
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
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` 文件中的 `