最近无论是在公司还是自己研究的项目,都一直在搞 H5 页面服务端渲染
方面的探索,因此本文来探讨一下服务端渲染的必要性以及其背后的原理。
To C
的营销H5
页面的典型特点是:
那么此时作为传统的CSR
渲染方式为什么就不合适了呢?
看了下面一小节,也许你就有答案了
我们分别来对比一下两者的DOM
渲染过程。
图片来源The Benefits of Server Side Rendering Over Client Side Rendering[1]
客户端渲染,需要先得到一个空的 HTML 页面
(这个时候页面已经进入白屏)之后还需要经历:
JavaScript
和CSS
几个过程才可以看到最后的页面。
特别是在复杂应用中,由于需要加载 JavaScript
脚本,越是复杂的应用,需要加载的 JavaScript
脚本就越多、越大,这就会导致应用的首屏加载时间
非常长,进而影响用户体验感。
相对于客户端渲染,服务端渲染在用户发出一次页面 url
请求之后,应用服务器返回的 html
字符串就是完备的计算好的,可以交给浏览器直接渲染,使得 DOM
的渲染不再受静态资源和 ajax
的限制。
但服务端渲染真的就那么好吗?
其实,也不是。
为了实现服务端渲染,应用代码中需要兼容服务端和客户端两种运行情况,对第三方库的要求比较高,如果想直接在 Node 渲染过程中调用第三方库,那这个库必须支持服务端渲染。对应的代码复杂度提升了很多。
由于服务器增加了渲染 HTML
的需求,使得原本只需要输出静态资源文件的 nodejs
服务,新增了数据获取的 IO
和渲染 HTML
的 CPU
占用,如果流量陡增,有可能导致服务器宕机,因此需要使用相应的缓存策略和准备相应的服务器负载。
对于构建部署也有了更高的要求,之前的SPA应用
可以直接部署在静态文件服务器上,而服务器渲染应用,需要处于 Node.js server
运行环境。
聊了这么多可能你对于服务端渲染的原理还不是很清楚,下面我就以Vue
服务端渲染为例来简述一下其原理:
这张图来自Vue SSR 指南[2]
原理解析参考如何搭建一个高可用的服务端渲染工程[3]
Source
为我们的源代码区,即工程代码。
Universal Appliation Code
和我们平时的客户端渲染的代码组织形式完全一致,因为渲染过程是在Node
端,所以没有DOM
和BOM
对象,因此不要在beforeCreate
和created
生命周期钩子里做涉及DOM
和BOM
的操作。
比客户端渲染多出来的app.js
、Server entry
、Client entry
的主要作用为:
app.js
分别给Server entry
、Client entry
暴露出createApp()
方法,使得每个请求进来会生成新的app
实例Server entry
和Client entry
分别会被webpack
打包成vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
Node
端会根据webpack
打包好的vue-ssr-server-bundle.json
,通过调用createBundleRenderer
生成renderer
实例,再通过调用renderer.renderToString
生成完备的html字符串
。
Node
端将render
好的html
字符串返回给Browser
,同时Node
端根据vue-ssr-client-manifest.json
生成的js
会和html
字符串hydrate
,完成客户端激活html
,使得页面可交互。
我们知道市面上实现服务端渲染一般有这几种方法:
next.js
/nuxt.js
的服务端渲染方案node
+vue-server-renderer
实现vue
项目的服务端渲染(也就是上面提到的)node
+React renderToStaticMarkup/renderToString
实现react
项目的服务端渲染ssr
(比如ejs
, jade
, pug
等)最近要改造的项目正好是 Vue
开发的,目前也考虑基于vue-server-renderer
将其改造为服务端渲染的。基于上面分析的原理,我从零一步步搭建了一个最小化的vue-ssr[4],大家有需要的可直接拿去用~
这里我贴几点需要注意的:
SSR
不存在单例模式我们知道Node.js
服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。所以每次用户请求都会创建一个新的 Vue
实例,这也是为了避免交叉请求状态污染的发生。
因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例:
// main.js
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
import createStore from "./store";
export default () => {
const router = createRouter();
const store = createStore();
const app = new Vue({
router,
store,
render: (h) => h(App),
});
return { app, router, store };
};
服务端代码与客户端代码构建的区别在于:
CSS
,服务器端渲染会自动将CSS
内置nodejs
环境nodejs
将所有代码一次性加载到内存中更有利于运行效率// vue.config.js
// 两个插件分别负责打包客户端和服务端
const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");
// 根据传⼊环境变量决定⼊⼝⽂件和相应配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false,
},
outputDir: "./dist/" + target,
configureWebpack: () => ({
// 将 entry 指向应⽤程序的 server / client ⽂件
entry: `./src/${target}-entry.js`,
// 对 bundle renderer 提供 source map ⽀持
devtool: "source-map",
// target设置为node使webpack以Node适⽤的⽅式处理动态导⼊,
// 并且还会在编译Vue组件时告知`vue-loader`输出⾯向服务器代码。
target: TARGET_NODE ? "node" : "web",
// 是否模拟node全局变量
node: TARGET_NODE ? undefined : false,
output: {
// 此处使⽤Node⻛格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined,
},
externals: TARGET_NODE
? nodeExternals({
allowlist: [/\.css$/],
})
: undefined,
optimization: {
splitChunks: undefined,
},
// 这是将服务器的整个输出构建为单个 JSON ⽂件的插件。
// 服务端默认⽂件名为 `vue-ssr-server-bundle.json`
// 客户端默认⽂件名为 `vue-ssr-client-manifest.json`。
plugins: [
TARGET_NODE ? new VueSSRServerPlugin() : new VueSSRClientPlugin(),
],
}),
chainWebpack: (config) => {
// cli4项⽬添加
if (TARGET_NODE) {
config.optimization.delete("splitChunks");
}
config.module
.rule("vue")
.use("vue-loader")
.tap((options) => {
merge(options, {
optimizeSSR: false,
});
});
},
};
正常服务端路由我们可能会这样写:
router.get("/", async (ctx) => {
ctx.body = await render.renderToString();
});
但这样打包后,启动server
你会发现样式没生效。这个问题我们需要通过promise
的方式来解决:
pp.use(async (ctx) => {
try {
ctx.body = await new Promise((resolve, reject) => {
render.renderToString({ url: ctx.url }, (err, data) => {
console.log("data", data);
if (err) reject(err);
resolve(data);
});
});
} catch (error) {
ctx.body = "404";
}
});
之所以事件没有生效是因为我们没有进行客户端激活
操作,也就是把客户端打包出来的clientBundle.js
挂载到HTML
上。
首先我们要在App.vue
的根结点加上app
的id
:
<template>
<!-- 客户端激活 -->
<div id="app">
<router-link to="/">foo</router-link>
<router-link to="/bar">bar</router-link>
<router-view></router-view>
</div>
</template>
<script>
import Bar from "./components/Bar.vue";
import Foo from "./components/Foo.vue";
export default {
components: {
Bar,
Foo,
},
};
</script>
然后通过vue-server-renderer
中的server-plugin
和client-plugin
分别生成vue-ssr-server-bundle.json
和vue-ssr-client-manifest.json
文件,也就是服务端映射和客户端映射。
最后在node
服务这里做下关联:
const ServerBundle = require("./dist/server/vue-ssr-server-bundle.json");
const template = fs.readFileSync("./public/index.html", "utf8");
const clientManifest = require("./dist/client/vue-ssr-client-manifest.json");
const render = VueServerRender.createBundleRenderer(ServerBundle, {
runInNewContext: false, // 推荐
template,
clientManifest,
});
这样就完成了客户端激活操作,也就支持了 css
和事件。
在服务端渲染生成 html
前,我们需要预先获取并解析依赖的数据。同时,在客户端挂载(mounted)之前,需要获取和服务端完全一致的数据,否则客户端会因为数据不一致导致混入失败。
为了解决这个问题,预获取的数据要存储在状态管理器(store)中,以保证数据一致性。
首先是创建store
实例,同时供客户端和服务端使用:
// src/store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default () => {
const store = new Vuex.Store({
state: {
name: "",
},
mutations: {
changeName(state) {
state.name = "cosen";
},
},
actions: {
changeName({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit("changeName");
resolve();
}, 1000);
});
},
},
});
return store;
};
将createStore
加入到createApp
中,并将store
注入到vue
实例中,让所有Vue
组件可以获取到store
实例:
import Vue from "vue";
import App from "./App.vue";
import createRouter from "./router";
+ import createStore from "./store";
export default () => {
const router = createRouter();
+ const store = createStore();
const app = new Vue({
router,
+ store,
render: (h) => h(App),
});
+ return { app, router, store };
};
在页面中使用store
:
// src/components/Foo.vue
<template>
<div>
Foo
<button @click="clickMe">点击</button>
{{ this.$store.state.name }}
</div>
</template>
<script>
export default {
mounted() {
this.$store.dispatch("changeName");
},
asyncData({ store, route }) {
return store.dispatch("changeName");
},
methods: {
clickMe() {
alert("测试点击");
},
},
};
</script>
如果用过nuxt
的同学肯定知道在nuxt
中有一个钩子叫asyncData
,我们可以在这个钩子发起一些请求,而且这些请求是在服务端发出的。
那我们来看下如何实现 asyncData
吧,在 server-entry.js
中我们通过 const matchs = router.getMatchedComponents()
获取到匹配当前路由的所有组件,也就是我们可以拿到所有组件的 asyncData
方法:
// src/server-entry.js
// 服务端渲染只需将渲染的实例导出即可
import createApp from "./main";
export default (context) => {
const { url } = context;
return new Promise((resolve, reject) => {
console.log("url", url);
// if (url.endsWith(".js")) {
// resolve(app);
// return;
// }
const { app, router, store } = createApp();
router.push(url);
router.onReady(() => {
const matchComponents = router.getMatchedComponents();
console.log("matchComponents", matchComponents);
if (!matchComponents.length) {
reject({ code: 404 });
}
// resolve(app);
Promise.all(
matchComponents.map((component) => {
if (component.asyncData) {
return component.asyncData({
store,
route: router.currentRoute,
});
}
})
)
.then(() => {
// Promise.all中方法会改变store中的state
// 把vuex的状态挂载到上下文中
context.state = store.state;
resolve(app);
})
.catch(reject);
}, reject);
});
};
通过 Promise.all
我们就可以让所有匹配到的组件中的asyncData
执行,然后修改服务端的store
了。而且也将服务端的最新store
同步到客户端的store
中。
上一步将state
存入context
后,在服务端渲染HTML
时,也就是渲染template
的时候,context.state
会被序列化到window.__INITIAL_STATE__
中:
可以看到,状态已经被序列化到 window.__INITIAL_STATE__
中,我们需要做的就是将这个 window.__INITIAL_STATE__
在客户端渲染之前,同步到客户端的 store
中,下面修改 client-entry.js
:
// 客户端渲染手动挂载到 dom 元素上
import createApp from "./main";
const { app, router, store } = createApp();
// 浏览器执行时需要将服务端的最新store状态替换掉客户端的store
if (window.__INITIAL_STATE__) {
// 激活状态数据
store.replaceState(window.__INITIAL_STATE__);
}
router.onReady(() => {
app.$mount("#app", true);
});
通过使用store
的replaceState
函数,将window.__INITIAL_STATE__
同步到store
内部,完成数据模型的状态同步。
[1]The Benefits of Server Side Rendering Over Client Side Rendering: https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8
[2]Vue SSR 指南: https://ssr.vuejs.org/zh/
[3]如何搭建一个高可用的服务端渲染工程: https://tech.youzan.com/server-side-render/
[4]vue-ssr: https://github.com/Cosen95/vue-ssr
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/GkcHIzcP5dDg6F1DBz-77w
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。