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

本篇目录

说明

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

SSR 项目快速创建过程

如果没有安装 vue cli,先完成安装:

$ npm install -g @vue/cli    # vue 命令安装方法

创建项目:

$ vue create ssr-mode

进入项目,安装依赖包:

$ cd ssr-mode
$ npm install path fs express @vue/server-renderer webpack webpack-manifest-plugin  webpack-node-externals [email protected]

编写路由文件,src/router.js:

import { createRouter } from 'vue-router'

const routes = [
    // 指定路由对应的 vue 组件
    { path: '/helloworld', component: ()=> import('./components/HelloWorld')},  // 懒加载,初始化首屏时按需加载
]

export default function (history) {
    return createRouter({
        history,
        routes
    })
}

编写客户端执行入口,src/entry-client.js:

import { createSSRApp } from 'vue'
import { createWebHistory } from 'vue-router'
import createRouter from './router.js'
import App from './App'

const app = createSSRApp(App)
const router = createRouter(createWebHistory())

app.use(router)

router.isReady().then(()=>{  // 使用懒加载后,等待路由解析完成
    app.mount('#app')
})

编写服务端渲染 入口,src/entry-server.js:

import { createSSRApp } from 'vue'
import { createMemoryHistory } from 'vue-router' //服务端渲染使用这个
import createRouter from './router.js'
import App from './App.vue'

export default function () {
    const app = createSSRApp(App)
    const router = createRouter(createMemoryHistory())

    app.use(router)

    return {
        app, router
    }
}

配置 webpack,新建文件 vue.config.js(vue的配置文件),配置 webpack:

/*
 * vue.config.js
 * Copyright (C) 2021 lijiaocn <[email protected] wechat:lijiaocn>
 *
 * Distributed under terms of the GPL license.
 */

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

module.exports = {
// webpack 用途 js 代码打包:https://webpack.docschina.org/guides/getting-started/
    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
            })
        )
    }
}

在项目根目录下添加服务端代码 server.js :

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

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

const server = express()

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, router } = createApp()

    await router.push(req.url)
    await router.isReady()

    const appContent = await renderToString(app)
    //console.log("appContent: ", appContent)

    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)

修改 package.json,增加 ssr 的打包命令:

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

编译打包运行:

$ npm run serve

如果遇到错误,对比下 packge.json,看下依赖的包版本是否相同:

{
  "name": "ssr-mode",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "lint": "vue-cli-service lint",
    "serve": "npm run build && node ./server.js",
    "build": "npm run build:client && npm run build:server",
    "build:client": "vue-cli-service build --dest dist/client",
    "build:server": "SSR=1 vue-cli-service build --dest dist/server"
  },
  "dependencies": {
    "@vue/server-renderer": "^3.2.26",
    "core-js": "^3.6.5",
    "express": "^4.17.2",
    "fs": "^0.0.1-security",
    "path": "^0.12.7",
    "vue": "^3.0.0",
    "vue-router": "^4.0.12",
    "webpack": "^4.46.0",
    "webpack-manifest-plugin": "^4.0.2",
    "webpack-node-externals": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

使用服务端端渲染原因

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 代码。

Vue 服务端渲染配置方法

以下是 vue 官方文档 服务端渲染指南 给出的配置方法。

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

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

├── 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 项目配置,配置了两种打包方式,一种为客户端渲染打包,一种为服务端渲染打包

特别注意,使用服务端渲染时,需要两个代码入口,一个是 entry-client.js 作为浏览器端的执行入口,一个是 entry-server.js 作为服务器端入口。 使用服务端渲染不等于不需要客户端js,交互动作还是需要客户端 js 执行,所以一共需要两个入口。

创建 vue 项目

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

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

编写客户端入口

src/entry-client.js,和非 SSR 模式的区别的是使用 createSSRApp 创建:

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

const app = createSSRApp(App)

app.mount('#app')

编写服务端入口

src/entry-server.js,服务端入口不能使用 mount 等涉及 dom 操作的指令,因为服务端渲染环境是 node.js 没有浏览器里的 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 如果写成下面的样式,编译的时候会报错找不到 document:

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

createApp(App).mount('#app')   // 服务端渲染时,不能用这种方式
/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/lijiao/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/lijiao/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/lijiao/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:2344:41)
    at __webpack_require__ (/Users/lijiao/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:21:30)
    at Object.0 (/Users/lijiao/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:93:18)
    at __webpack_require__ (/Users/lijiao/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:21:30)
    at /Users/lijiao/Work/lijiaocn/workspace/studys/study_vue/03-ssr/03-webpack/dist/server/js/app.1f9d4b66.js:85:18
    at Object.<anonymous> (/Users/lijiao/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

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

在项目根目录中创建文件 vue.config.js,它是 vue cli 默认使用的配置文件,支持的配置项见 vue.config.js

下面是 vue 给出的构建配置示例,主要就是增加了 webpack 打包规则,根据环境变量 process.env.SSR 的情况分别指定客户度入口和服务端入口:

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 中增加 build:client 和 build:server,分别打包客户端和服务端文件:

{
  "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:server
npm run build:client

dis 中会生成两个目录:

# ls dist
client server

编写服务端 Server 代码

在项目的根目录创建 server.js,下面是 vue 提供的示例,把服务端渲染完后的 appContent 填充到 /dist/client/index.html :

const 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)

安装新引入的依赖:

npm install path fs express @vue/server-renderer

启动 server:

node ./server.js

服务端渲染结果

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

服务端渲染的vue页面

查看网页源代码会发现 SSR 模式和客户端模式的区别。

SSR 中使用 vue-router

参考 路由和代码分离

参考

  1. 李佶澳的博客
  2. 服务端渲染指南
  3. Vue Cli
  4. Vue SSR:编写通用的代码
  5. vue.config.js
  6. 构建配置示例
  7. webpack getting started
  8. VueJs: Server Side Render with Vue-Router
  9. 路由和代码分离

推荐阅读

赞助商广告

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

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