万字长文助你搞懂现代网页开发中常见的10种渲染模式
如何使您的网站呈现最佳状态?这个问题有很多答案,本文介绍了当前框架中应用最广泛的十种渲染设计模式,让您能够选择最适合您的方式。
近年来,网络开发的迅速演变,尤其是在前端开发领域。这种转变主要归功于无数涌现的框架和技术,它们旨在简化和增强构建引人入胜的用户界面的过程。然而,由于现有框架的丰富多样以及不断涌现的新框架,跟上前端趋势已成为一项艰巨的任务。对于新手来说,很容易感到不知所措,仿佛迷失在广阔的选择海洋中。
渲染是前端开发的核心挑战,它将数据和代码转化为可见且可交互的用户界面。虽然大多数框架以类似的方式应对这一挑战,通常比之前的方法更简洁,但也有一些框架选择了全新的解决方案。在本文中,我们将研究流行框架中使用的十种常见渲染模式,通过这样做,无论是初学者还是专家都将获得对新旧框架的扎实基础理解,同时也能对解决应用程序中的渲染问题有新的见解。
在本文的结尾,您将会:
对于当今网页开发中最常见的渲染模式有基本的了解了解不同渲染模式的优势和劣势了解在你的下一个大项目中使用哪种渲染模式和框架什么是UI渲染模式?在前端开发的背景下,渲染是将数据和代码转换为对最终用户可见的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如下
代码语言:javascript复制/* 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;
}
1、静态网站静态网站是最原始、最基本、最直接的UI渲染方法。它通过简单地编写HTML、CSS和JavaScript来创建网站。一旦代码准备好,它会被上传为静态文件到托管服务(如Netlify),并指向一个域名。通过URL请求时,静态文件会直接提供给用户,无需服务器端处理。静态网站渲染非常适合没有交互性和动态内容的静态网站,比如落地页和文档网站。
优点
非常简单快速廉价(无服务器)SEO友好缺点
不适用于数据频繁变动的情况(动态数据)不适用于互动应用程序没有直接的数据库连接当数据发生变化时,需要手动更新和重新上传相关框架
HugoJekyllHTML/CSS/纯JavaScript(无框架)Demo (HTML/CSS/JavaScript)
代码语言:javascript复制
Cryptocurrency Price App
BTC
- Binance:
- Kucoin:
- Bitfinex:
- Crypto.com:
代码语言:javascript复制//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价格。
请注意,在使用静态网站时,每个币种的价格页面必须手动编写。
2、多页面应用程序(MPAs)这种渲染模式是为了处理我们网站上的动态数据而出现的解决方案,并导致了今天许多最大、最受欢迎的动态Web应用程序的创建。在MPA中,渲染由服务器完成,服务器会重新加载以基于当前底层数据(通常来自数据库)生成新的HTML,以响应浏览器发出的每个请求。这意味着网站可以根据底层数据的变化而改变。最常见的用例是电子商务网站、企业应用程序和新闻公司博客。
优点
简单直接处理动态数据非常出色SEO友好良好的开发者体验高度可扩展的缺点
适度支持用户界面的交互性由于多次重新加载而导致用户体验差昂贵的(需要服务器)相关框架
Express 和 EJS (node.js)Flask (Python)Spring boot (java)Demo (ExpressandEJS)
代码语言:javascript复制npm i express and ejs代码语言:javascript复制
Cryptocurrency Price App
<%- ID %>
- Binance:<%- allPrices.binance[0][ID] %>
- Kucoin:<%- allPrices.kucoin[0][ID] %>
- Bitfinex:<%- allPrices.bitfinex[0][ID] %>
- Crypto.com:<%- allPrices.crypto_com[0][ID] %>
代码语言:javascript复制// 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"));注意:在这里,每个页面都将由服务器自动生成,不同于静态网站,静态网站需要手动编写每个文件。
3、单页应用程序(SPA)单页应用程序(SPA)是2010年代创建高度交互式Web应用程序的解决方案,至今仍在使用。在这里,SPA通过从服务器获取HTML外壳(空白HTML页面)和JavaScript捆绑包来处理渲染到浏览器。在浏览器中,它将控制权(水合)交给JavaScript,动态地将内容注入(渲染)到外壳中。在这种情况下,渲染是在客户端(CSR)上执行的。使用JavaScript,这些SPA能够在不需要完整页面重新加载的情况下对单个页面上的内容进行大量操作。它们还通过操作URL栏来创建多个页面的幻觉,以指示加载到外壳上的每个资源。常见的用例包括项目管理系统、协作平台、社交媒体Web应用、交互式仪表板或文档编辑器,这些应用程序受益于SPA的响应性和交互性。
优点
高度互动在浏览多个页面时,用户体验无缝衔接手机友好缺点
由于JavaScript捆绑包过大,加载时间较慢SEO能力差由于客户端上的代码执行,存在高安全风险可扩展性差相关框架
ReactAngularVueDemo (ReactandReact-router)
代码语言:javascript复制// pages/index.jsx
import { Link } from "react-router-dom";
export default function Index() {
return (
Cryptocurrency Price App
-
Bitcoin
-
Ethereum
-
Ripple
-
Cardano
);
}
//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 (
{ID}
{isLoading ? (
Loading...
) : Object.keys(marketPrices).length > 0 ? (
-
{exchange}: {marketPrices[exchange][0][ID]}
{Object.keys(marketPrices).map((exchange) => (
))}
) : (
No data available.
)}
);
}
//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 (
);
}
// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Index from "./pages";
import Price from "./pages/Price";
const router = createBrowserRouter([
{
path: "/",
element:
},
{
path: "/price/:id",
element:
},
]);
function App() {
return (
<>
>
);
}
export default App;4、静态网站生成(SSG)静态网站生成(SSG)是一种利用构建网站的原始静态网站模式的渲染模式。在构建过程中,从源代码中预先构建和渲染了所有可能的网页,生成静态HTML文件,然后将其存储在存储桶中,就像在典型静态网站的情况下原始上传静态文件一样。对于基于源代码可能存在的任何路由的请求,将向客户端提供相应的预构建静态页面。因此,与SSR或SPA不同,SSG不依赖于服务器端渲染或客户端JavaScript来动态渲染内容。相反,内容是提前生成的,并且可以被缓存和高性能地传递给用户。这适用于中度交互的网站,其数据不经常更改,例如作品集网站、小型博客或文档网站。
优点
SEO友好快速加载页面高性能提高安全性(由于代码既不在客户端上运行也不在服务器上运行)缺点
有限互动数据更改后需要重新构建和重新上传相关框架
Nextjs (默认情况下)GatsbyHugoJekyllDemo (Nextjs)
代码语言:javascript复制// 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 (
Toggle Mode
);
}
// 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 (
{ID}
{Object.keys(allPrices).length > 0 ? (
-
{exchange}: {allPrices[exchange][0][ID]}
{Object.keys(allPrices).map((exchange) => (
))}
) : (
No data available.
)}
);
}
//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 (
{allPrices && Object.keys(allPrices).length > 0 ? (
) : (
No data available.
)}
);
}
//page.js
import Link from "next/link";
export default function Index() {
return (
Cryptocurrency Price App
-
Bitcoin
-
Ethereum
-
Ripple
-
Cardano
);
}5、服务器端渲染(SSR)服务器端渲染(SSR)是一种渲染模式,它结合了多页面应用(MPA)和单页面应用(SPA)的能力,以克服两者的局限性。在这种模式下,服务器生成网页的HTML内容,填充动态数据,并将其发送给客户端进行显示。在浏览器上,JavaScript可以接管已经渲染的页面,为页面上的组件添加交互性,就像在SPA中一样。SSR在将完整的HTML交付给浏览器之前,在服务器上处理渲染过程,而SPA完全依赖于客户端JavaScript进行渲染。SSR特别适用于注重SEO、内容传递或具有特定可访问性要求的应用,如企业网站、新闻网站和电子商务网站。
优点
适度互动SEO友好快速加载时间对动态数据的良好支持缺点
复杂的实施成本(需要服务器)相关框架
Next.jsNuxt.jsDemo (Nextjs)
在NEXT.js上实现SSR的代码与SSG演示几乎相同。这里,唯一的变化在于 getCurrentPrice 函数。使用带有 no-cache 选项的fetch API,页面将不会被缓存;相反,服务器将需要在每个请求上创建一个新页面。
代码语言:javascript复制//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;
}6、增量静态生成(ISG)增量静态生成是一种生成静态网站的方法,它结合了静态网站生成的优点,能够更新和重新生成网站的特定页面或部分,而无需重建整个网站。增量静态生成允许自动增量更新,从而减少了重建整个应用程序所需的时间,并通过仅在必要时从服务器请求新数据,更有效地利用服务器资源。这对于国际多语言网站、企业网站和发布平台网站非常实用。
优点
静态网站的实时自动更新支持性价比高SEO友好良好的性能和可扩展性缺点
实施中的复杂性不适用于高度动态的数据应用相关框架
Next.jsNuxt.jsDemo (Nextjs)
在NEXT.js上实现ISR的代码与SSG演示几乎相同。唯一的变化在于 getCurrentPrice 函数。使用fetch API并使用指定条件的选项从服务器获取数据,当满足我们定义的条件时,页面将自动更新。在这里,我们说底层数据应该每60秒进行验证,并且UI应该根据数据中的任何变化进行更新。
代码语言:javascript复制//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;
}7、部分水合部分水合是客户端渲染(CSR)框架中用于解决加载时间缓慢问题的一种技术。使用这种技术,CSR框架将选择性地首先渲染和水合具有交互性的网页的最重要部分,而不是整个页面。最终,当满足特定条件时,较不重要的交互组件可以通过水合来实现其交互性。通过优先处理关键或可见组件的水合,而推迟处理非关键或在折叠区域下的组件的水合,它可以更有效地利用资源,并通过优先处理关键或可见组件的水合来加快初始页面渲染速度。部分水合可以使任何具有多个交互组件的复杂CSR或SPA受益。
优点
由于减少了初始的JavaScript捆绑包,加载时间更快性能提升了优化的搜索引擎优化资源效率缺点
增加的复杂性和代码不一致的用户界面可能性相关框架
ReactVueDemo (React)
代码语言:javascript复制//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 (
{ID}
{isLoading ? (
Loading...
) : Object.keys(marketPrices).length > 0 ? (
-
{exchange}: {marketPrices[exchange][0][ID]}
{Object.keys(marketPrices).map((exchange) => (
))}
) : (
No data available.
)}
{inViewport ? (
// Render the interactive component only when it's in the viewport
) : (
// Render a placeholder or non-interactive version when not in the viewport
)}