Vue.js 的服务端渲染方法,配置过程、原理说明与操作示例

本篇目录

说明

Vue 服务端渲染指南 里详细介绍了配置服务端渲染的方法,唯一的问题是对于「对前端一无所知」的人来说,看起来费劲,要不停地试验才知道是怎么回事,这里记录下折腾过程。

使用服务端端渲染原因

Vue 默认是一个单页应用,返回给浏览器的是一个「空白的 html」,然后通过 js 代码执行,完成浏览器内的页面渲染。 这种方式最大的问题是「对 SEO 极度不友好」,搜索引擎的爬虫通常不会执行页面中的 js 代码,在爬虫看来 vue 页面是一个没有任何内容的空 html 文件。

浏览器渲染完成后的vue页面:

浏览器渲染完成后的vue页面

爬虫看到的 vue 页面,这样的页面会被搜索引擎认定为空页面,不收录,无法检索:

<!DOCTYPE html>
<html lang="">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width,initial-scale=1">
        <link rel="icon" href="/favicon.ico">
        <title>
            03-webpack
        </title>
        <link href="/css/app.9b8ecd1c.css" rel="preload" as="style">
        <link href="/js/app.32a63f2e.js" rel="preload" as="script">
        <link href="/js/chunk-vendors.8d312411.js" rel="preload" as="script">
        <link href="/css/app.9b8ecd1c.css" rel="stylesheet">
    </head>
    <body>
        <noscript>
            <strong>
                We're sorry but 03-webpack doesn't work properly without JavaScript enabled.
                Please enable it to continue.
            </strong>
        </noscript>
        <div id="app">  <!--  浏览器执行 js 代码时,更新这个 div 的内容  -->
        </div>
        <script src="/js/chunk-vendors.8d312411.js">
        </script>
        <script src="/js/app.32a63f2e.js">
        </script>
    </body>

</html>

Vue.js 服务端渲染思路

Vue.js 服务端渲染思路是:在浏览器与 vue 编译生成的文件之间,架设一台能执行 js 代码的 node server,node server 将 js 的执行结果以 html 文本的方式返回给浏览器。

客户端渲染方式:编译打包后的 vue 文件原封不动的下发到浏览器,浏览器完成本地渲染。纯粹的文件下发,中间只需要架设一台处理静态文件请求的 web server。

   +--------------+                            +-------------+
   |              |                            |             |
   |   vue files  | -----   web server   --->  |  vue files  |
   |              |       for static file      |             |
   +--------------+                            +-------------+

       服务器端                                    浏览器     

服务端渲染方式(简称 SSR,Server Side Render):架设一台能够执行 js 代码的 node server,将 js 执行后的内容下发给浏览器。

   +--------------+                            +--------------+        +-------------+
   |              |                            |              |        |             |
   |   vue files  | -----   web server   --->  |  node server | -----> |  html files |
   |              |       for static file      |              |        |             |
   +--------------+          optional          +--------------+        +-------------+
                                                                                             
       服务器端                                   node server              浏览器     

所以,如果使用服务端渲染,需要多写一段 node server 代码。

服务端渲染配置后的项目结构

先看一下完成后的目录结构:

├── README.md
├── babel.config.js
├── build.sh
├── dist               <--   编译后打包的发布文件
│   ├── client          <--  完整的客户端渲染的文件,可以直接发布到浏览器 
│   │   ├── css
│   │   ├── favicon.ico
│   │   ├── img
│   │   ├── index.html
│   │   └── js
│   └── server          <-- 用于服务端渲染的文件
│       ├── css
│       ├── favicon.ico
│       ├── img
│       ├── index.html
│       ├── js
│       └── ssr-manifest.json
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
├── server.js          <-- 执行 vue.js 的 node server 代码
├── src                <-- 项目源码
│   ├── App.vue       
│   ├── assets
│   │   └── logo.png
│   ├── components
│   │   ├── EchoInput.vue
│   │   └── HelloWorld.vue
│   ├── entry-client.js   <-- 使用客户端渲染时的应用入口,通过 webpack 指定 
│   └── entry-server.js   <-- 使用服务端渲染时的应用入口,通过 webpack 指定 
└── vue.config.js       <-- vue 项目配置,配置了两种打包方式,一种为客户端渲染打包,一种为服务端渲染打包

配置过程

用 vue 命令创建项目,cli 的用法见 Vue Cli

# npm install -g @vue/cli    # vue 命令安装方法
vue create hello-world

编写客户端渲染入口文件

src/entry-client.js,就是标准的 vue 入口:

import { createSSRApp } from 'vue'
import App from './App'

const app = createSSRApp(App)

app.mount('#app')

编写服务端渲染入口文件

src/entry-server.js,需要注意不能想 entry-client.js 那样使用 mount 等涉及dom 操作的指令:

import { createSSRApp } from 'vue'
import App from './App.vue'

export default function () {
    const app = createSSRApp(App)

    return {
        app
    }
}

由于 node server 和浏览器的 js 运行环境不是完全等同的,node server 中没有 window、document 等对象。使用服务端渲染时,不能使用会引发 dom 操作的指令。

如果 src/entry-server.js 写成下面的样式:

import { createApp } from ‘vue’ import App from ‘./App.vue’

createApp(App).mount(‘#app’) // 服务端渲染时,不能用这种方

编译的时候会报错找不到 document:

/workspace/studys/study_vue/03-ssr/03-webpack/node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.js:1589
        const res = document.querySelector(container);
ReferenceError: document is not defined
    at normalizeContainer (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.js:1589:21)
    at Object.app.mount (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/node_modules/@vue/runtime-dom/dist/runtime-dom.cjs.js:1510:27)
    at Module.b7ab (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:2344:41)
    at __webpack_require__ (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:21:30)
    at Object.0 (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:93:18)
    at __webpack_require__ (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:21:30)
    at /Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:85:18
    at Object.<anonymous> (/Users/bytedance/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:88:10)

Vue SSR:编写通用的代码 中有详细的说明。

编写 vue 代码

在 src/App.vue 以及 src/components 中完成 vue 代码,开发方式和客户端渲染时的开发方法相同,只需要注意遵守 Vue SSR:编写通用的代码 中的规范。 这里在 vue cli 生成的源码文件基础上,增加了一个带有交互的组件,用来验证服务端渲染后,组件交互的交互是否还能正常进行。

src/components/EchoInput.vue:

<template>
<div>
  <div>
    <span>Input:</span><input v-model="content" :placeholder=placeholder>
  </div>
  <div>
    <span>Echo:</span> <h2 id="本篇目录">本篇目录</h2>

<ul id="markdown-toc">
  <li><a href="#本篇目录" id="markdown-toc-本篇目录">本篇目录</a></li>
  <li><a href="#说明" id="markdown-toc-说明">说明</a></li>
  <li><a href="#使用场景" id="markdown-toc-使用场景">使用场景</a></li>
  <li><a href="#运算资源的获取" id="markdown-toc-运算资源的获取">运算资源的获取</a></li>
  <li><a href="#基本概念" id="markdown-toc-基本概念">基本概念</a></li>
  <li><a href="#操作接口" id="markdown-toc-操作接口">操作接口</a>    <ul>
      <li><a href="#processfunction" id="markdown-toc-processfunction">ProcessFunction</a></li>
      <li><a href="#datastream-api" id="markdown-toc-datastream-api">DataStream API</a></li>
      <li><a href="#sql--table-api" id="markdown-toc-sql--table-api">SQL &amp; Table API</a></li>
    </ul>
  </li>
  <li><a href="#最后" id="markdown-toc-最后">最后</a></li>
  <li><a href="#参考" id="markdown-toc-参考">参考</a></li>
</ul>

<h2 id="说明">说明</h2>

<p>学习资料是官网文档 <a href="https://flink.apache.org/flink-architecture.html" title="What is Apache Flink? ">What is Apache Flink? </a>,简单了解下使用场景和原理。</p>

<h2 id="使用场景">使用场景</h2>

<p>用于处理在「一段时间内」逐渐产生的数据,即数据流,数据流中的单个数据称为事件/event。</p>

<p>处理流式数据有两种思路:</p>

<ol>
  <li>等数据都生成后,对完整数据进行处理</li>
  <li>在数据生成过程中就开始处理,数据生成的同时进行处理</li>
</ol>

<p>方式1存在的问题:</p>

<ol>
  <li>需要等数据全部就绪,获得结果要等太久</li>
  <li>有些场景下,数据是永续生成的,没有终止,譬如日志</li>
</ol>

<p>永续生成没有截止的数据,flink 将其称为「Unbounded streams」,与之相对的是「Bounded streams」,如下图所示:</p>

<p><img src="/img/article/flink_stream.png" alt="unbounded streams 和 bounded streams" /></p>

<p>flink 是一个专门用于处理流式数据的开发框架,同时支持 unbounded streams 和 bounded streams。</p>

<h2 id="运算资源的获取">运算资源的获取</h2>

<p>flink 可以自行管理服务器的资源,也可以部署到其它资源调度系统中,从第三方资源调度系统申请资源,支持以下系统:</p>

<ol>
  <li>Hadpoop YARN</li>
  <li>Apache Mesos</li>
  <li>Kubernetes</li>
</ol>

<h2 id="基本概念">基本概念</h2>

<p>flink 有三个基本概念:</p>

<ol>
  <li>streams:即数据流</li>
  <li>state:流式数据处理系统的状态</li>
  <li>time:时间</li>
</ol>

<p>开发者基于 flink 开发运行在 flink 上的流式处理应用,stream 是应用的输入,应用处理事件的中间态是 state(即有状态服务),开发者在应用代码中事件处理的时间策略。</p>

<p>整个 flink 就是围绕 state 构建的,简单说就是如何保持住中间结果。</p>

<p>事件到达应用的顺序和它的产生顺序可能不一致,并且事件产生和到达之间有时延,所以需要设置事件处理的时间策略。flink 支持两种时间策略:</p>

<ol>
  <li>Event-time Mode:按照事件发生时间处理,无论事件到达情况怎样,统一按照事件发生顺序处理</li>
  <li>Processing-time Mode:按照事件的到达顺序处理,忽略事件的发生顺序</li>
</ol>

<p>方式1可以保证中间结果和实际情况一致,但是可能要过度等待,避免漏掉还在传输中的事件。</p>

<p>方式2收到事件时即处理,延迟低,但是中间输出的结果可能和实际不符。</p>

<p>为了协调方式1和方式2各自的优缺点,flink 提供了 Watermark Support 和 Late Data Handing。</p>

<ol>
  <li>Watermark Support:在 Event-time Mode 中,通过设置允许时差,协调延迟事件和结果准确性</li>
  <li>Late Data Handing:在 Processing-time Mode 中,设定「先发生后到达」的事件的处理策略</li>
</ol>

<h2 id="操作接口">操作接口</h2>

<p>flink 提供了三个层面的操作接口:</p>

<p><img src="/img/article/flink_api.png" alt="flink 操作接口" /></p>

<p>控制粒度最细的是 ProcessFunction,即编写事件的处理代码,直接操作到达的事件。</p>

<p>其次是 DataStream API,DataStream API 提供了一些汇聚函数。</p>

<p>最后是 SQL &amp; Table API,提供类似 SQL 的操作接口 。</p>

<h3 id="processfunction">ProcessFunction</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Matches keyed START and END events and computes the difference between
 * both elements' timestamps. The first String field is the key attribute,
 * the second String attribute marks START and END events.
 */</span>

<span class="kd">public</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">StartEndDuration</span>
    <span class="kd">extends</span> <span class="nc">KeyedProcessFunction</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;,</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;&gt;</span> <span class="o">{</span>

  <span class="kd">private</span> <span class="nc">ValueState</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;</span> <span class="n">startTime</span><span class="o">;</span>

  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">open</span><span class="o">(</span><span class="nc">Configuration</span> <span class="n">conf</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// obtain state handle</span>
    <span class="n">startTime</span> <span class="o">=</span> <span class="n">getRuntimeContext</span><span class="o">()</span>
      <span class="o">.</span><span class="na">getState</span><span class="o">(</span><span class="k">new</span> <span class="nc">ValueStateDescriptor</span><span class="o">&lt;</span><span class="nc">Long</span><span class="o">&gt;(</span><span class="s">"startTime"</span><span class="o">,</span> <span class="nc">Long</span><span class="o">.</span><span class="na">class</span><span class="o">));</span>
  <span class="o">}</span>

  <span class="cm">/** Called for each processed event. */</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">processElement</span><span class="o">(</span>
      <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">in</span><span class="o">,</span>
      <span class="nc">Context</span> <span class="n">ctx</span><span class="o">,</span>
      <span class="nc">Collector</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;&gt;</span> <span class="n">out</span><span class="o">)</span> <span class="kd">throws</span> <span class="nc">Exception</span> <span class="o">{</span>

    <span class="k">switch</span> <span class="o">(</span><span class="n">in</span><span class="o">.</span><span class="na">f1</span><span class="o">)</span> <span class="o">{</span>
      <span class="k">case</span> <span class="s">"START"</span><span class="o">:</span>
        <span class="c1">// set the start time if we receive a start event.</span>
        <span class="n">startTime</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">ctx</span><span class="o">.</span><span class="na">timestamp</span><span class="o">());</span>
        <span class="c1">// register a timer in four hours from the start event.</span>
        <span class="n">ctx</span><span class="o">.</span><span class="na">timerService</span><span class="o">()</span>
          <span class="o">.</span><span class="na">registerEventTimeTimer</span><span class="o">(</span><span class="n">ctx</span><span class="o">.</span><span class="na">timestamp</span><span class="o">()</span> <span class="o">+</span> <span class="mi">4</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">1000</span><span class="o">);</span>
        <span class="k">break</span><span class="o">;</span>
      <span class="k">case</span> <span class="s">"END"</span><span class="o">:</span>
        <span class="c1">// emit the duration between start and end event</span>
        <span class="nc">Long</span> <span class="n">sTime</span> <span class="o">=</span> <span class="n">startTime</span><span class="o">.</span><span class="na">value</span><span class="o">();</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">sTime</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
          <span class="n">out</span><span class="o">.</span><span class="na">collect</span><span class="o">(</span><span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">in</span><span class="o">.</span><span class="na">f0</span><span class="o">,</span> <span class="n">ctx</span><span class="o">.</span><span class="na">timestamp</span><span class="o">()</span> <span class="o">-</span> <span class="n">sTime</span><span class="o">));</span>
          <span class="c1">// clear the state</span>
          <span class="n">startTime</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
        <span class="o">}</span>
      <span class="k">default</span><span class="o">:</span>
        <span class="c1">// do nothing</span>
    <span class="o">}</span>
  <span class="o">}</span>

  <span class="cm">/** Called when a timer fires. */</span>
  <span class="nd">@Override</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onTimer</span><span class="o">(</span>
      <span class="kt">long</span> <span class="n">timestamp</span><span class="o">,</span>
      <span class="nc">OnTimerContext</span> <span class="n">ctx</span><span class="o">,</span>
      <span class="nc">Collector</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;&gt;</span> <span class="n">out</span><span class="o">)</span> <span class="o">{</span>

    <span class="c1">// Timeout interval exceeded. Cleaning up the state.</span>
    <span class="n">startTime</span><span class="o">.</span><span class="na">clear</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="datastream-api">DataStream API</h3>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// a stream of website clicks</span>
<span class="nc">DataStream</span><span class="o">&lt;</span><span class="nc">Click</span><span class="o">&gt;</span> <span class="n">clicks</span> <span class="o">=</span> <span class="o">...</span>

<span class="nc">DataStream</span><span class="o">&lt;</span><span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;&gt;</span> <span class="n">result</span> <span class="o">=</span> <span class="n">clicks</span>
  <span class="c1">// project clicks to userId and add a 1 for counting</span>
  <span class="o">.</span><span class="na">map</span><span class="o">(</span>
    <span class="c1">// define function by implementing the MapFunction interface.</span>
    <span class="k">new</span> <span class="nc">MapFunction</span><span class="o">&lt;</span><span class="nc">Click</span><span class="o">,</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;&gt;()</span> <span class="o">{</span>
      <span class="nd">@Override</span>
      <span class="kd">public</span> <span class="nc">Tuple2</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">&gt;</span> <span class="nf">map</span><span class="o">(</span><span class="nc">Click</span> <span class="n">click</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">click</span><span class="o">.</span><span class="na">userId</span><span class="o">,</span> <span class="mi">1L</span><span class="o">);</span>
      <span class="o">}</span>
    <span class="o">})</span>
  <span class="c1">// key by userId (field 0)</span>
  <span class="o">.</span><span class="na">keyBy</span><span class="o">(</span><span class="mi">0</span><span class="o">)</span>
  <span class="c1">// define session window with 30 minute gap</span>
  <span class="o">.</span><span class="na">window</span><span class="o">(</span><span class="nc">EventTimeSessionWindows</span><span class="o">.</span><span class="na">withGap</span><span class="o">(</span><span class="nc">Time</span><span class="o">.</span><span class="na">minutes</span><span class="o">(</span><span class="mi">30L</span><span class="o">)))</span>
  <span class="c1">// count clicks per session. Define function as lambda function.</span>
  <span class="o">.</span><span class="na">reduce</span><span class="o">((</span><span class="n">a</span><span class="o">,</span> <span class="n">b</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="nc">Tuple2</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">a</span><span class="o">.</span><span class="na">f0</span><span class="o">,</span> <span class="n">a</span><span class="o">.</span><span class="na">f1</span> <span class="o">+</span> <span class="n">b</span><span class="o">.</span><span class="na">f1</span><span class="o">));</span>
</code></pre></div></div>

<h3 id="sql--table-api">SQL &amp; Table API</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">userId</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">clicks</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="k">SESSION</span><span class="p">(</span><span class="n">clicktime</span><span class="p">,</span> <span class="n">INTERVAL</span> <span class="s1">'30'</span> <span class="k">MINUTE</span><span class="p">),</span> <span class="n">userId</span>
</code></pre></div></div>

<h2 id="最后">最后</h2>

<p>这里只简单了解下 flink 是干嘛的,至于怎么搭建、怎么使用,使用时注意些什么,以后有时间再研究。</p>

<h2 id="参考">参考</h2>

<ol>
  <li><a href="https://www.lijiaocn.com" title="李佶澳的博客">李佶澳的博客</a></li>
  <li><a href="https://flink.apache.org/flink-architecture.html" title="What is Apache Flink? ">What is Apache Flink? </a></li>
</ol>


  </div>
</div>
</template>

<script>
export default {
  name: "EchoInput",
  props: ['placeholder'],
  data () {
    return {
      content: "",
    }
  }
}
</script>

<style scoped>
input {
  width: 50em;
}
</style>

src/App.vue:

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
  <EchoInput placeholder="input some texts"></EchoInput>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import EchoInput from './components/EchoInput.vue'

export default {
  name: 'App',
  components: {
    HelloWorld,EchoInput
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

配置 webpack

在项目根目录中创建文件 vue.config.js,它是 vue cli 默认使用的配置文件,支持的配置项见 vue.config.js。 这里使用 vue 给出的构建配置示例,主要就是增加了 webpack 打包规则,根据环境变量 process.env.SSR 是否存在,分别使用客户端渲染、服务端渲染两种打包方式,并指定了不同的入口(entry-client.js/entry-serer.js):

webpack 是一个 js 代码打包工具,负责将源代码中的 js 代码压缩打包到指定的 js 文件中,详情见 webpack getting started

const { WebpackManifestPlugin } = require('webpack-manifest-plugin')
const nodeExternals = require('webpack-node-externals')
const webpack = require('webpack')

module.exports = {
  chainWebpack: webpackConfig => {
    // 我们需要禁用 cache loader,否则客户端构建版本会从服务端构建版本使用缓存过的组件
    webpackConfig.module.rule('vue').uses.delete('cache-loader')
    webpackConfig.module.rule('js').uses.delete('cache-loader')
    webpackConfig.module.rule('ts').uses.delete('cache-loader')
    webpackConfig.module.rule('tsx').uses.delete('cache-loader')

    if (!process.env.SSR) {
      // 将入口指向应用的客户端入口文件
      webpackConfig
        .entry('app')
        .clear()
        .add('./src/entry-client.js')
      return
    }

    // 将入口指向应用的服务端入口文件
    webpackConfig
      .entry('app')
      .clear()
      .add('./src/entry-server.js')

    // 这允许 webpack 以适合于 Node 的方式处理动态导入,
    // 同时也告诉 `vue-loader` 在编译 Vue 组件的时候抛出面向服务端的代码。
    webpackConfig.target('node')
    // 这会告诉服务端的包使用 Node 风格的导出
    webpackConfig.output.libraryTarget('commonjs2')

    webpackConfig
      .plugin('manifest')
      .use(new WebpackManifestPlugin({ fileName: 'ssr-manifest.json' }))

    // https://webpack.js.org/configuration/externals/#function
    // https://github.com/liady/webpack-node-externals
    // 将应用依赖变为外部扩展。
    // 这使得服务端构建更加快速并生成更小的包文件。

    // 不要将需要被 webpack 处理的依赖变为外部扩展
    // 也应该把修改 `global` 的依赖 (例如各种 polyfill) 整理成一个白名单
    webpackConfig.externals(nodeExternals({ allowlist: /\.(css|vue)$/ }))

    webpackConfig.optimization.splitChunks(false).minimize(false)

    webpackConfig.plugins.delete('preload')
    webpackConfig.plugins.delete('prefetch')
    webpackConfig.plugins.delete('progress')
    webpackConfig.plugins.delete('friendly-errors')

    webpackConfig.plugin('limit').use(
      new webpack.optimize.LimitChunkCountPlugin({
        maxChunks: 1
      })
    )
  }
}

安装引入的依赖:

npm install webpack-manifest-plugin  webpack-node-externals webpack

编译打包

在 package.json 中增加下面的命令,分别对应客户端渲染打包、服务端渲染打包:

{
  "scripts": {
    "start": "npm run build && node ./server.js",
    "build": "npm run build:client && npm run build:server",
    "lint": "vue-cli-service lint",
    "build:client": "vue-cli-service build --dest dist/client",
    "build:server": "SSR=1 vue-cli-service build --dest dist/server"
  }

要保证 npm run build 执行无错误。

vue.config.js 中引入的 package 如果没有安装,用下面的命令安装:

npm install webpack-manifest-plugin  webpack-node-externals webpack

编写服务端渲染代码

在项目的根目录创建 server.js,直接使用 vue 提供的示例

onst path = require('path')
const express = require('express')
const fs = require('fs')
const { renderToString } = require('@vue/server-renderer')
const manifest = require('./dist/server/ssr-manifest.json')

const server = express()

const appPath = path.join(__dirname, './dist', 'server', manifest['app.js'])
const createApp = require(appPath).default

server.use('/img', express.static(path.join(__dirname, './dist/client', 'img')))
server.use('/js', express.static(path.join(__dirname, './dist/client', 'js')))
server.use('/css', express.static(path.join(__dirname, './dist/client', 'css')))
server.use(
        '/favicon.ico',
        express.static(path.join(__dirname, './dist/client', 'favicon.ico'))
        )

server.get('*', async (req, res) => {
        const { app } = createApp()

        const appContent = await renderToString(app)

        fs.readFile(path.join(__dirname, '/dist/client/index.html'), (err, html) => {
                if (err) {
                throw err
                }

                html = html
                .toString()
                .replace('<div id="app">', `<div id="app">${appContent}`)
                res.setHeader('Content-Type', 'text/html')
                res.send(html)
                })
        })

console.log('You can navigate to http://localhost:8080')

server.listen(8080)

上述渲染逻辑就是把客户端渲染文件中的 app 部分,用服务端渲染完后的文本填充后,返回客户端,浏览器使用客户端渲染模式中的 js 文件。既实现了服务端渲染,又保留了浏览器中的交互动作。

安装新引入的依赖:

npm install path fs express @vue/server-renderer

服务端渲染结果

启动 node ./server.js,在浏览器中打开页面地址:

服务端渲染的vue页面

查看网页源代码会发现,网页中直接包含了文本信息,和客户端渲染模式下的文件不相同。

参考

  1. 李佶澳的博客
  2. 服务端渲染指南
  3. Vue Cli
  4. Vue SSR:编写通用的代码
  5. vue.config.js
  6. 构建配置示例
  7. webpack getting started

推荐阅读

赞助商广告

Copyright @2011-2019 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: [email protected],备注网站合作

友情链接:  李佶澳的博客  小鸟笔记  软件手册  编程手册  运营手册  爱马影视  网络课程  奇技淫巧  课程文档  精选文章  发现知识星球  百度搜索 谷歌搜索