Vue 服务端渲染指南 里详细介绍了配置服务端渲染的方法,唯一的问题是对于「对前端一无所知」的人来说,看起来费劲,要不停地试验才知道是怎么回事,这里记录下折腾过程。
如果没有安装 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 vue-router@4
编写路由文件,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 页面,这样的页面会被搜索引擎认定为空页面,不收录,无法检索:
<!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 代码的 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 官方文档 服务端渲染指南 给出的配置方法。
先看一下完成后的目录结构:
├── 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 命令创建项目,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:编写通用的代码 中有详细的说明。
在 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 & 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 & 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"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">>,</span> <span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">>></span> <span class="o">{</span>
<span class="kd">private</span> <span class="nc">ValueState</span><span class="o"><</span><span class="nc">Long</span><span class="o">></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"><</span><span class="nc">Long</span><span class="o">>(</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"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">></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"><</span><span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">>></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"><</span><span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">>></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"><</span><span class="nc">Click</span><span class="o">></span> <span class="n">clicks</span> <span class="o">=</span> <span class="o">...</span>
<span class="nc">DataStream</span><span class="o"><</span><span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">>></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"><</span><span class="nc">Click</span><span class="o">,</span> <span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">>>()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Tuple2</span><span class="o"><</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Long</span><span class="o">></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">-></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 & 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 是一个 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.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,在浏览器中打开页面地址:
查看网页源代码会发现 SSR 模式和客户端模式的区别。
参考 路由和代码分离