如何使您的网站呈现最佳状态?这个问题有很多答案,本文介绍了当前框架中应用最广泛的十种渲染设计模式,让您能够选择最适合您的方式。
近年来,网络开发的迅速演变,尤其是在前端开发领域。这种转变主要归功于无数涌现的框架和技术,它们旨在简化和增强构建引人入胜的用户界面的过程。然而,由于现有框架的丰富多样以及不断涌现的新框架,跟上前端趋势已成为一项艰巨的任务。对于新手来说,很容易感到不知所措,仿佛迷失在广阔的选择海洋中。
渲染是前端开发的核心挑战,它将数据和代码转化为可见且可交互的用户界面。虽然大多数框架以类似的方式应对这一挑战,通常比之前的方法更简洁,但也有一些框架选择了全新的解决方案。在本文中,我们将研究流行框架中使用的十种常见渲染模式,通过这样做,无论是初学者还是专家都将获得对新旧框架的扎实基础理解,同时也能对解决应用程序中的渲染问题有新的见解。
在本文的结尾,您将会:
对于当今网页开发中最常见的渲染模式有基本的了解
了解不同渲染模式的优势和劣势
了解在你的下一个大项目中使用哪种渲染模式和框架
在前端开发的背景下,渲染是将数据和代码转换为对最终用户可见的HTML。UI渲染模式是指实现渲染过程可以采用的各种方法。这些模式概述了不同的策略,用于描述转换发生的方式以及呈现出的用户界面。正如我们很快会发现的那样,根据所实现的模式,渲染可以在服务器上或浏览器中进行,可以部分或一次性完成。
选择正确的渲染模式对开发人员来说至关重要,因为它直接影响到Web应用程序的性能、成本、速度、可扩展性、用户体验,甚至开发人员的体验。
在本文中,我们将介绍下面列出的前十种渲染模式:
1、静态网站(Static Site)
2、多页面应用(Multi-Page Applications(MPA))
3、单页应用程序(Single Page Applications (with Client Side Rendering CSR))
4、服务器端渲染(erver Side Rendering (SSR))
5、静态网站生成(Static Site Generation (SSG))
6、增量静态生成(Incremental Static Generation (ISG))
7、部分水合(Partial Hydration)
8、岛屿架构(Island Architectur)
9、可恢复框架(Resumability)
10、流式服务器端渲染 (Streaming SSR)
在每个案例中,我们将研究渲染模式的概念、优点和缺点、使用案例、相关的框架,并提供一个简单的代码示例来阐明观点。
第一页将显示可用的货币类型
第二页将显示从Coingecko API获取的特定币种在不同交易所的价格。
第二页还将提供深色和浅色模式。
各种框架的实施可能会有轻微的差异。
所有示例的全局CSS如下
/* style.css or the name of the global stylesheet */
h1,
h2 {
color: purple;
margin: 1rem;
}
a {
color: var(--text-color);
display: block;
margin: 2rem 0;
}
body {
font-family: Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
.dark-mode {
--background-color: #333;
--text-color: #fff;
}
.light-mode {
--background-color: #fff;
--text-color: #333;
}
.toggle-btn{
background-color: yellow;
padding: 0.3rem;
margin: 1rem;
margin-top: 100%;
border-radius: 5px;
}
静态网站是最原始、最基本、最直接的UI渲染方法。它通过简单地编写HTML、CSS和JavaScript来创建网站。一旦代码准备好,它会被上传为静态文件到托管服务(如Netlify),并指向一个域名。通过URL请求时,静态文件会直接提供给用户,无需服务器端处理。静态网站渲染非常适合没有交互性和动态内容的静态网站,比如落地页和文档网站。
优点
非常简单
快速
廉价(无服务器)
SEO友好
缺点
不适用于数据频繁变动的情况(动态数据)
不适用于互动应用程序
没有直接的数据库连接
当数据发生变化时,需要手动更新和重新上传
相关框架
Hugo
Jekyll
HTML/CSS/纯JavaScript(无框架)
Demo (HTML/CSS/JavaScript)
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h1>Cryptocurrency Price App</h1>
<ol>
<li><a href="./btcPrice.html">Bitcoin </a></li>
<li><a href="./ethPrice.html">Ethereum </a></li>
<li><a href="./xrpPrice.html">Ripple </a></li>
<li><a href="./adaPrice.html">Cardano </a></li>
</ol>
</body>
</html>
<!-- btcPrice.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<h2>BTC</h2>
<ul>
<li id="binance">Binance:</li>
<li id="kucoin">Kucoin:</li>
<li id="bitfinex">Bitfinex:</li>
<li id="crypto_com">Crypto.com:</li>
</ul>
<script src="fetchPrices.js"></script>
<button class="toggle-btn">Toggle Mode</button>
<script src="darkMode.js"></script>
</body>
</html>
//fetchPrices.js
const binance = document.querySelector("#binance");
const kucoin = document.querySelector("#kucoin");
const bitfinex = document.querySelector("#bitfinex");
const crypto_com = document.querySelector("#crypto_com");
// Get the cryptocurrency prices from an API
let marketPrices = { binance: [], kucoin: [], bitfinex: [], crypto_com: [] };
async function getCurrentPrice(market) {
if (
`${market}` === "binance" ||
`${market}` === "kucoin" ||
`${market}` === "crypto_com" ||
`${market}` === "bitfinex"
) {
marketPrices[market] = [];
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
);
if (res) {
let data = await res.json();
if (data) {
for (const info of data.tickers) {
if (info.target === "USDT") {
let name = info.base;
let price = info.last;
if (`${market}` === "binance") {
marketPrices.binance = [
...marketPrices.binance,
{ [name]: price },
];
}
if (`${market}` === "kucoin") {
marketPrices.kucoin = [...marketPrices.kucoin, { [name]: price }];
}
if (`${market}` === "bitfinex") {
marketPrices.bitfinex = [
...marketPrices.bitfinex,
{ [name]: price },
];
}
if (`${market}` === "crypto_com") {
marketPrices.crypto_com = [
...marketPrices.crypto_com,
{ [name]: price },
];
}
}
}
}
}
}
}
async function findPrices() {
try {
const fetched = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
if (fetched) {
binance ? (binance.innerHTML += `${marketPrices.binance[0].BTC}`) : null;
kucoin ? (kucoin.innerHTML += `${marketPrices.kucoin[0].BTC}`) : null;
bitfinex
? (bitfinex.innerHTML += `${marketPrices.bitfinex[0].BTC}`)
: null;
crypto_com
? (crypto_com.innerHTML += `${marketPrices.crypto_com[0].BTC}`)
: null;
}
} catch (e) {
console.log(e);
}
}
findPrices();
//darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
上面的代码块展示了我们使用HTML/CSS/JavaScript实现的应用程序。下面是应用程序。
第一页:显示所有可用的虚拟币
第2页:从Coingecko API获取的不同交易所的BTC价格。
请注意,在使用静态网站时,每个币种的价格页面必须手动编写。
这种渲染模式是为了处理我们网站上的动态数据而出现的解决方案,并导致了今天许多最大、最受欢迎的动态Web应用程序的创建。在MPA中,渲染由服务器完成,服务器会重新加载以基于当前底层数据(通常来自数据库)生成新的HTML,以响应浏览器发出的每个请求。这意味着网站可以根据底层数据的变化而改变。最常见的用例是电子商务网站、企业应用程序和新闻公司博客。
优点
简单直接
处理动态数据非常出色
SEO友好
良好的开发者体验
高度可扩展的
缺点
适度支持用户界面的交互性
由于多次重新加载而导致用户体验差
昂贵的(需要服务器)
相关框架
Express 和 EJS (node.js)
Flask (Python)
Spring boot (java)
Demo (ExpressandEJS)
npm i express and ejs
<!-- views/index.ejs -->
<!-- css file should be in public folder-->
<!DOCTYPE html>
<html>
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Cryptocurrency Price App</h1>
<ol>
<li><a href="./price/btc">Bitcoin </a></li>
<li><a href="./price/eth">Ethereum </a></li>
<li><a href="./price/xrp">Ripple </a></li>
<li><a href="./price/ada">Cardano </a></li>
</ol>
</body>
</html>
<!-- views/price.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<title>Cryptocurrency Price App</title>
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<h2><%- ID %></h2>
<ul>
<li id="binance">Binance:<%- allPrices.binance[0][ID] %></li>
<li id="kucoin">Kucoin:<%- allPrices.kucoin[0][ID] %></li>
<li id="bitfinex">Bitfinex:<%- allPrices.bitfinex[0][ID] %></li>
<li id="crypto_com">Crypto.com:<%- allPrices.crypto_com[0][ID] %></li>
</ul>
<button class="toggle-btn">Toggle Mode</button>
<script src="/darkMode.js"></script>
</body>
</html>
// public/darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
// utils/fetchPrices.js
async function getCurrentPrice(market) {
let prices = [];
if (
`${market}` === "binance" ||
`${market}` === "kucoin" ||
`${market}` === "crypto_com" ||
`${market}` === "bitfinex"
) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
);
const data = await res.json();
for (const info of data.tickers) {
if (info.target === "USDT") {
let name = info.base;
let price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
}
module.exports = getCurrentPrice;
//app.js.
const getCurrentPrice = require("./utils/fetchPrices");
const express = require("express");
const ejs = require("ejs");
const path = require("path");
const app = express();
app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.static("public"));
app.get("/", (req, res) => {
res.render("index");
});
app.get("/price/:id", async (req, res) => {
let { id } = req.params;
let ID = id.toUpperCase();
let allPrices;
try {
const fetched = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
if (fetched) {
allPrices = {};
allPrices.binance = fetched[0];
allPrices.kucoin = fetched[1];
allPrices.bitfinex = fetched[2];
allPrices.crypto_com = fetched[3];
console.log(allPrices);
res.render("price", { ID, allPrices });
}
} catch (e) {
res.send("server error");
}
});
app.listen(3005, () => console.log("Server is running on port 3005"));
注意:在这里,每个页面都将由服务器自动生成,不同于静态网站,静态网站需要手动编写每个文件。
单页应用程序(SPA)是2010年代创建高度交互式Web应用程序的解决方案,至今仍在使用。在这里,SPA通过从服务器获取HTML外壳(空白HTML页面)和JavaScript捆绑包来处理渲染到浏览器。在浏览器中,它将控制权(水合)交给JavaScript,动态地将内容注入(渲染)到外壳中。在这种情况下,渲染是在客户端(CSR)上执行的。使用JavaScript,这些SPA能够在不需要完整页面重新加载的情况下对单个页面上的内容进行大量操作。它们还通过操作URL栏来创建多个页面的幻觉,以指示加载到外壳上的每个资源。常见的用例包括项目管理系统、协作平台、社交媒体Web应用、交互式仪表板或文档编辑器,这些应用程序受益于SPA的响应性和交互性。
优点
高度互动
在浏览多个页面时,用户体验无缝衔接
手机友好
缺点
由于JavaScript捆绑包过大,加载时间较慢
SEO能力差
由于客户端上的代码执行,存在高安全风险
可扩展性差
相关框架
React
Angular
Vue
Demo (ReactandReact-router)
// pages/index.jsx
import { Link } from "react-router-dom";
export default function Index() {
return (
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link to="./price/btc">Bitcoin </Link>
</li>
<li>
<Link to="./price/eth">Ethereum </Link>
</li>
<li>
<Link to="./price/xrp">Ripple </Link>
</li>
<li>
<Link to="./price/ada">Cardano </Link>
</li>
</ol>
</div>
);
}
//pages/price.jsx
import { useParams } from "react-router-dom";
import { useEffect, useState, useRef, Suspense } from "react";
import Btn from "../components/Btn";
export default function Price() {
const { id } = useParams();
const ID = id.toUpperCase();
const [marketPrices, setMarketPrices] = useState({});
const [isLoading, setIsLoading] = useState(true);
const containerRef = useRef(null);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
useEffect(() => {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
setMarketPrices(allPrices);
setIsLoading(false);
console.log(allPrices); // Log the fetched prices to the console
} catch (error) {
console.log(error);
setIsLoading(false);
}
}
fetchMarketPrices();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{isLoading ? (
<p>Loading...</p>
) : Object.keys(marketPrices).length > 0 ? (
<ul>
{Object.keys(marketPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {marketPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
}
//components/Btn.jsx
export default function Btn({ container }) {
function toggleMode() {
container.current.classList.toggle("dark-mode");
container.current.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.current.classList.contains("dark-mode")
? "dark"
: "light";
localStorage.setItem("mode", currentMode);
}
// Check the user's preferred mode on page load (optional)
return (
<div>
<button
className="toggle-btn"
onClick={() => {
toggleMode();
}}
>
Toggle Mode
</button>
</div>
);
}
// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Index from "./pages";
import Price from "./pages/Price";
const router = createBrowserRouter([
{
path: "/",
element: <Index />,
},
{
path: "/price/:id",
element: <Price />,
},
]);
function App() {
return (
<>
<RouterProvider router={router}></RouterProvider>
</>
);
}
export default App;
静态网站生成(SSG)是一种利用构建网站的原始静态网站模式的渲染模式。在构建过程中,从源代码中预先构建和渲染了所有可能的网页,生成静态HTML文件,然后将其存储在存储桶中,就像在典型静态网站的情况下原始上传静态文件一样。对于基于源代码可能存在的任何路由的请求,将向客户端提供相应的预构建静态页面。因此,与SSR或SPA不同,SSG不依赖于服务器端渲染或客户端JavaScript来动态渲染内容。相反,内容是提前生成的,并且可以被缓存和高性能地传递给用户。这适用于中度交互的网站,其数据不经常更改,例如作品集网站、小型博客或文档网站。
优点
SEO友好
快速加载页面
高性能
提高安全性(由于代码既不在客户端上运行也不在服务器上运行)
缺点
有限互动
数据更改后需要重新构建和重新上传
相关框架
Nextjs (默认情况下)
Gatsby
Hugo
Jekyll
Demo (Nextjs)
// components/Btn.js
export default function Btn({ container }) {
function toggleMode() {
container.current.classList.toggle("dark-mode");
container.current.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.current.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
// Check the user's preferred mode on page load (optional)
return (
<div>
<button className="toggle-btn" onClick={() => {toggleMode()}}>
Toggle Mode
</button>
</div>
);
}
// components/Client.js
"use client";
import { useEffect, useRef } from "react";
import Btn from "@/app/components/Btn";
import { usePathname } from "next/navigation";
export default function ClientPage({ allPrices }) {
const pathname = usePathname();
let ID = pathname.slice(-3).toUpperCase();
const containerRef = useRef(null);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {allPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
}
//price/[id]/page.js
import ClientPage from "../../components/Client";
async function getCurrentPrice(market) {
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
export default async function Price() {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
}
}
const allPrices = await fetchMarketPrices();
return (
<div>
{allPrices && Object.keys(allPrices).length > 0 ? (
<ClientPage allPrices={allPrices} />
) : (
<p>No data available.</p>
)}
</div>
);
}
//page.js
import Link from "next/link";
export default function Index() {
return (
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link href="./price/btc">Bitcoin </Link>
</li>
<li>
<Link href="./price/eth">Ethereum </Link>
</li>
<li>
<Link href="./price/xrp">Ripple </Link>
</li>
<li>
<Link href="./price/ada">Cardano </Link>
</li>
</ol>
</div>
);
}
服务器端渲染(SSR)是一种渲染模式,它结合了多页面应用(MPA)和单页面应用(SPA)的能力,以克服两者的局限性。在这种模式下,服务器生成网页的HTML内容,填充动态数据,并将其发送给客户端进行显示。在浏览器上,JavaScript可以接管已经渲染的页面,为页面上的组件添加交互性,就像在SPA中一样。SSR在将完整的HTML交付给浏览器之前,在服务器上处理渲染过程,而SPA完全依赖于客户端JavaScript进行渲染。SSR特别适用于注重SEO、内容传递或具有特定可访问性要求的应用,如企业网站、新闻网站和电子商务网站。
优点
适度互动
SEO友好
快速加载时间
对动态数据的良好支持
缺点
复杂的实施
成本(需要服务器)
相关框架
Next.js
Nuxt.js
Demo (Nextjs)
在NEXT.js上实现SSR的代码与SSG演示几乎相同。这里,唯一的变化在于 getCurrentPrice 函数。使用带有 no-cache 选项的fetch API,页面将不会被缓存;相反,服务器将需要在每个请求上创建一个新页面。
//price/[id]/page.js
async function getCurrentPrice(market)
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
{ cache: "no-store" }
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
增量静态生成是一种生成静态网站的方法,它结合了静态网站生成的优点,能够更新和重新生成网站的特定页面或部分,而无需重建整个网站。增量静态生成允许自动增量更新,从而减少了重建整个应用程序所需的时间,并通过仅在必要时从服务器请求新数据,更有效地利用服务器资源。这对于国际多语言网站、企业网站和发布平台网站非常实用。
优点
静态网站的实时自动更新支持
性价比高
SEO友好
良好的性能和可扩展性
缺点
实施中的复杂性
不适用于高度动态的数据应用
相关框架
Next.js
Nuxt.js
Demo (Nextjs)
在NEXT.js上实现ISR的代码与SSG演示几乎相同。唯一的变化在于 getCurrentPrice 函数。使用fetch API并使用指定条件的选项从服务器获取数据,当满足我们定义的条件时,页面将自动更新。在这里,我们说底层数据应该每60秒进行验证,并且UI应该根据数据中的任何变化进行更新。
//price/[id]/page.js
async function getCurrentPrice(market)
const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
{ next: { revalidate: 60 } }
);
console.log("fetched");
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
部分水合是客户端渲染(CSR)框架中用于解决加载时间缓慢问题的一种技术。使用这种技术,CSR框架将选择性地首先渲染和水合具有交互性的网页的最重要部分,而不是整个页面。最终,当满足特定条件时,较不重要的交互组件可以通过水合来实现其交互性。通过优先处理关键或可见组件的水合,而推迟处理非关键或在折叠区域下的组件的水合,它可以更有效地利用资源,并通过优先处理关键或可见组件的水合来加快初始页面渲染速度。部分水合可以使任何具有多个交互组件的复杂CSR或SPA受益。
优点
由于减少了初始的JavaScript捆绑包,加载时间更快
性能提升了
优化的搜索引擎优化
资源效率
缺点
增加的复杂性和代码
不一致的用户界面可能性
相关框架
React
Vue
Demo (React)
//pages/price.jsx
import { useParams } from "react-router-dom";
import React, { useEffect, useState, useRef, Suspense } from "react";
const Btn = React.lazy(() => import("../components/Btn"));
import getCurrentPrice from "../utils/fetchPrices";
export default function Price() {
const { id } = useParams();
const ID = id.toUpperCase();
const [marketPrices, setMarketPrices] = useState({});
const [isLoading, setIsLoading] = useState(true);
const containerRef = useRef(null);
// Wrapper component to observe if it's in the viewport
const [inViewport, setInViewport] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const [entry] = entries;
setInViewport(entry.isIntersecting);
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => {
if (containerRef.current) {
observer.unobserve(containerRef.current);
}
};
}, []);
function fetchMode() {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.current.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.current.classList.add("light-mode");
}
}
useEffect(() => {
fetchMode();
}, []);
useEffect(() => {
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
setMarketPrices(allPrices);
setIsLoading(false);
console.log(allPrices); // Log the fetched prices to the console
} catch (error) {
console.log(error);
setIsLoading(false);
}
}
fetchMarketPrices();
}, []);
return (
<div className="container" ref={containerRef}>
<h2>{ID}</h2>
{isLoading ? (
<p>Loading...</p>
) : Object.keys(marketPrices).length > 0 ? (
<ul>
{Object.keys(marketPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {marketPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
{inViewport ? (
// Render the interactive component only when it's in the viewport
<React.Suspense fallback={<div>Loading...</div>}>
<Btn container={containerRef} />
</React.Suspense>
) : (
// Render a placeholder or non-interactive version when not in the viewport
<div>Scroll down to see the interactive component!</div>
)}
</div>
);
}
在上面的演示中,我们代码的交互组件 Btn 位于页面底部,只有当它进入视口时才会被激活。
岛屿架构是Astro框架开发者倡导的一种有前途的UI渲染模式。Web应用程序在服务器上被划分为多个独立的小组件,称为岛屿。每个岛屿负责渲染应用程序UI的特定部分,并且它们可以独立地进行渲染。在服务器上被划分为岛屿后,这些多个岛屿包被发送到浏览器,框架使用一种非常强大的部分加载形式,只有带有交互部分的组件由JavaScript接管并启用其交互性,而其他非交互式组件保持静态。最常见的用例是构建内容丰富的网站。Astro是构建专注于内容的网站的不错选择,例如博客、作品集和文档网站。Astro的岛屿架构模式可以帮助提高这些网站的性能,尤其是对于网络连接较慢的用户来说。
优点
性能(当今最快的框架之一)
更小的捆绑尺寸
易学易懂,易于维护
良好的SEO表现
良好的开发者体验
缺点
有限互动
由于组件数量极多,导致调试困难
相关框架
Astro
Demo (Astro)
---
// components/Btn.astro
---
<div>
<button class="toggle-btn"> Toggle Mode</button>
</div>
<script>
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
document.body.classList.add("dark-mode");
} else if (preferredMode === "light") {
document.body.classList.add("light-mode");
}
});
// Check the user's preferred mode on page load (optional)
function toggleMode() {
const body = document.body;
body.classList.toggle("dark-mode");
body.classList.toggle("light-mode");
// Save the user's preference in localStorage (optional)
const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
localStorage.setItem("mode", currentMode);
}
toggleBtn.addEventListener("click", () => {
toggleMode();
});
</script>
---
// pages/[coin].astro
import Layout from "../layouts/Layout.astro";
import Btn from "../components/Btn.astro";
export async function getStaticPaths() {
return [
{ params: { coin: "btc" } },
{ params: { coin: "eth" } },
{ params: { coin: "xrp" } },
{ params: { coin: "ada" } },
];
}
const { coin } = Astro.params;
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
return null;
}
}
const allPrices = await fetchMarketPrices();
---
<Layout title="Welcome to Astro.">
<div>
<h2>{coin}</h2>
{
allPrices && Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li>
{exchange}: {allPrices[exchange][0][coin]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)
}
<Btn />
</div>
</Layout>
---
//pages/index.astro
import Layout from "../layouts/Layout.astro";
---
<Layout title="Welcome to Astro.">
<main>
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<a href="./btc">Bitcoin</a>
</li>
<li>
<a href="./eth">Ethereum</a>
</li>
<li>
<a href="./xrp">Ripple</a>
</li>
<li>
<a href="./ada">Cardano</a>
</li>
</ol>
</div>
</main>
</Layout>
Qwik是一个以重用性为核心的全新渲染方式的元框架。该渲染模式基于两种主要策略:
在服务器上序列化应用程序和框架的执行状态,并在客户端上恢复。
水合
这段来自Qwik文档的摘录很好地介绍了可重用性。
监听器 - 在DOM节点上定位事件监听器并安装它们,使应用程序具有交互性。组件树 - 构建表示应用程序组件树的内部数据结构。应用程序状态 - 恢复在服务器上存储的任何获取或保存的数据。总体而言,这被称为水合。所有当前的框架都需要这一步骤来使应用程序具有交互性。
水合作用之所以昂贵,有两个原因:
框架必须下载与当前页面相关的所有组件代码。
框架必须执行与页面上的组件相关联的模板,以重建监听器位置和内部组件树。
在序列化中, Qwik 显示了在服务器上开始构建网页的能力,并在从服务器发送捆绑包后继续在客户端上执行构建,节省了其他框架重新初始化客户端的时间。
就懒加载而言, Qwik 将通过极度懒加载来确保Web应用程序尽快加载,只加载必要的JavaScript捆绑包,并在需要时加载其余部分。Qwik 可以在开箱即用的情况下完成所有这些操作,无需进行太多开发者配置。
这适用于复杂的博客应用和企业网站的发布。
优点
缺点
复杂的实施
更高的带宽使用
相关框架
Qwik
Demo (Qwik)
//components/Btn.tsx
import { $, component$, useStore, useVisibleTask$ } from "@builder.io/qwik";
export default component$(({ container }) => {
const store = useStore({
mode: true,
});
useVisibleTask$(({ track }) => {
// track changes in store.count
track(() => store.mode);
container.value.classList.toggle("light-mode");
container.value.classList.toggle("dark-mode");
// Save the user's preference in localStorage (optional)
const currentMode = container.value.classList.contains("dark-mode")
? "dark"
: "light";
localStorage.setItem("mode", currentMode);
console.log(container.value.classList);
});
return (
<div>
<button
class="toggle-btn"
onClick$={$(() => {
store.mode = !store.mode;
})}
>
Toggle Mode
</button>
</div>
);
});
//components/Client.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";
import { useLocation } from "@builder.io/qwik-city";
import Btn from "./Btn";
export default component$(({ allPrices }) => {
const loc = useLocation();
const ID = loc.params.coin.toUpperCase();
const containerRef = useSignal<Element>();
useVisibleTask$(() => {
if (containerRef.value) {
const preferredMode = localStorage.getItem("mode");
if (preferredMode === "dark") {
containerRef.value.classList.add("dark-mode");
} else if (preferredMode === "light") {
containerRef.value.classList.add("light-mode");
}
}
});
return (
<div class="container" ref={containerRef}>
<h2>{ID}</h2>
{Object.keys(allPrices).length > 0 ? (
<ul>
{Object.keys(allPrices).map((exchange) => (
<li key={exchange}>
{exchange}: {allPrices[exchange][0][ID]}
</li>
))}
</ul>
) : (
<p>No data available.</p>
)}
<Btn container={containerRef} />
</div>
);
});
export const head: DocumentHead = {
title: "Qwik",
};
// routes/price/[coin]/index.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";
import { type DocumentHead } from "@builder.io/qwik-city";
import Btn from "../../../components/Btn";
import Client from "../../../components/Client";
export default component$(async () => {
async function getCurrentPrice(market) {
const res = await fetch(
`https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
);
const data = await res.json();
const prices = [];
for (const info of data.tickers) {
if (info.target === "USDT") {
const name = info.base;
const price = info.last;
prices.push({ [name]: price });
}
}
return prices;
}
async function fetchMarketPrices() {
try {
const prices = await Promise.all([
getCurrentPrice("binance"),
getCurrentPrice("kucoin"),
getCurrentPrice("bitfinex"),
getCurrentPrice("crypto_com"),
]);
const allPrices = {
binance: prices[0],
kucoin: prices[1],
bitfinex: prices[2],
crypto_com: prices[3],
};
return allPrices;
// Log the fetched prices to the console
} catch (error) {
console.log(error);
}
}
const allPrices = await fetchMarketPrices();
return (
<div>
{allPrices && Object.keys(allPrices).length > 0 ? (
<Client allPrices={allPrices} />
) : (
<p>No data available.</p>
)}
</div>
);
});
export const head: DocumentHead = {
title: "Qwik Flower",
};
//routes/index.tsx
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { Link } from "@builder.io/qwik-city";
export default component$(() => {
return (
<>
<div>
<h1>Cryptocurrency Price App</h1>
<ol>
<li>
<Link href="./price/btc">Bitcoin </Link>
</li>
<li>
<Link href="./price/eth">Ethereum </Link>
</li>
<li>
<Link href="./price/xrp">Ripple </Link>
</li>
<li>
<Link href="./price/ada">Cardano </Link>
</li>
</ol>
</div>
</>
);
});
export const head: DocumentHead = {
title: "Welcome to Qwik",
meta: [
{
name: "description",
content: "Qwik site description",
},
],
};
流式服务器端渲染(Streaming SSR)是一种相对较新的用于渲染Web应用程序的技术。流式SSR通过将应用程序的用户界面分块在服务器上进行渲染。每个块在准备好后立即进行渲染,然后流式传输到客户端。客户端在接收到块时显示和填充它们。这意味着客户端在应用程序完全渲染之前就可以开始与其进行交互,无需等待。这提高了Web应用程序的初始加载时间,尤其适用于大型和复杂的应用程序。流式SSR最适用于大规模应用,如电子商务和交易应用程序。
优点
Performance
实时更新
缺点
复杂性
相关框架
Next.js
Nuxt.js
Demo
很遗憾,我们的应用程序不够复杂,无法提供一个合适的例子。
在本文中,我们探讨了当今前端网页开发中最流行的十种UI渲染模式。在这个过程中,我们讨论了每种方法的优势、局限性和权衡。然而,重要的是要注意,没有一种适用于所有情况的渲染模式或普遍完美的渲染方法。每个应用都有其独特的需求和特点,因此选择合适的渲染模式对于开发过程的成功至关重要。
由于文章内容篇幅有限,今天的内容就分享到这里,文章结尾,我想提醒您,文章的创作不易,如果您喜欢我的分享,请别忘了点赞和转发,让更多有需要的人看到。同时,如果您想获取更多前端技术的知识,欢迎关注我,您的支持将是我分享最大的动力。我会持续输出更多内容,敬请期待。
有关可恢复性的更多信息,请参阅文档:https://qwik.builder.io/docs/concepts/think-qwik/ https://qwik.builder.io/docs/concepts/resumable/
有关岛屿架构的更多信息,请参阅文档 https://docs.astro.build/en/getting-started/
关于渲染的简要文档 https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic-rendering
流式服务器端渲染(SSR) https://blog.logrocket.com/streaming-ssr-with-react-18/