Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
載說明:原創(chuàng)不易,未經(jīng)授權(quán),謝絕任何形式的轉(zhuǎn)載
如何使您的網(wǎng)站呈現(xiàn)最佳狀態(tài)?這個(gè)問題有很多答案,本文介紹了當(dāng)前框架中應(yīng)用最廣泛的十種渲染設(shè)計(jì)模式,讓您能夠選擇最適合您的方式。
近年來,網(wǎng)絡(luò)開發(fā)的迅速演變,尤其是在前端開發(fā)領(lǐng)域。這種轉(zhuǎn)變主要?dú)w功于無數(shù)涌現(xiàn)的框架和技術(shù),它們旨在簡(jiǎn)化和增強(qiáng)構(gòu)建引人入勝的用戶界面的過程。然而,由于現(xiàn)有框架的豐富多樣以及不斷涌現(xiàn)的新框架,跟上前端趨勢(shì)已成為一項(xiàng)艱巨的任務(wù)。對(duì)于新手來說,很容易感到不知所措,仿佛迷失在廣闊的選擇海洋中。
渲染是前端開發(fā)的核心挑戰(zhàn),它將數(shù)據(jù)和代碼轉(zhuǎn)化為可見且可交互的用戶界面。雖然大多數(shù)框架以類似的方式應(yīng)對(duì)這一挑戰(zhàn),通常比之前的方法更簡(jiǎn)潔,但也有一些框架選擇了全新的解決方案。在本文中,我們將研究流行框架中使用的十種常見渲染模式,通過這樣做,無論是初學(xué)者還是專家都將獲得對(duì)新舊框架的扎實(shí)基礎(chǔ)理解,同時(shí)也能對(duì)解決應(yīng)用程序中的渲染問題有新的見解。
在本文的結(jié)尾,您將會(huì):
在前端開發(fā)的背景下,渲染是將數(shù)據(jù)和代碼轉(zhuǎn)換為對(duì)最終用戶可見的HTML。UI渲染模式是指實(shí)現(xiàn)渲染過程可以采用的各種方法。這些模式概述了不同的策略,用于描述轉(zhuǎn)換發(fā)生的方式以及呈現(xiàn)出的用戶界面。正如我們很快會(huì)發(fā)現(xiàn)的那樣,根據(jù)所實(shí)現(xiàn)的模式,渲染可以在服務(wù)器上或?yàn)g覽器中進(jìn)行,可以部分或一次性完成。
選擇正確的渲染模式對(duì)開發(fā)人員來說至關(guān)重要,因?yàn)樗苯佑绊懙絎eb應(yīng)用程序的性能、成本、速度、可擴(kuò)展性、用戶體驗(yàn),甚至開發(fā)人員的體驗(yàn)。
在本文中,我們將介紹下面列出的前十種渲染模式:
在每個(gè)案例中,我們將研究渲染模式的概念、優(yōu)點(diǎn)和缺點(diǎn)、使用案例、相關(guān)的框架,并提供一個(gè)簡(jiǎn)單的代碼示例來闡明觀點(diǎn)。
所有示例的全局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;
}
靜態(tài)網(wǎng)站是最原始、最基本、最直接的UI渲染方法。它通過簡(jiǎn)單地編寫HTML、CSS和JavaScript來創(chuàng)建網(wǎng)站。一旦代碼準(zhǔn)備好,它會(huì)被上傳為靜態(tài)文件到托管服務(wù)(如Netlify),并指向一個(gè)域名。通過URL請(qǐng)求時(shí),靜態(tài)文件會(huì)直接提供給用戶,無需服務(wù)器端處理。靜態(tài)網(wǎng)站渲染非常適合沒有交互性和動(dòng)態(tài)內(nèi)容的靜態(tài)網(wǎng)站,比如落地頁和文檔網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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實(shí)現(xiàn)的應(yīng)用程序。下面是應(yīng)用程序。
第一頁:顯示所有可用的虛擬幣
第2頁:從Coingecko API獲取的不同交易所的BTC價(jià)格。
請(qǐng)注意,在使用靜態(tài)網(wǎng)站時(shí),每個(gè)幣種的價(jià)格頁面必須手動(dòng)編寫。
這種渲染模式是為了處理我們網(wǎng)站上的動(dòng)態(tài)數(shù)據(jù)而出現(xiàn)的解決方案,并導(dǎo)致了今天許多最大、最受歡迎的動(dòng)態(tài)Web應(yīng)用程序的創(chuàng)建。在MPA中,渲染由服務(wù)器完成,服務(wù)器會(huì)重新加載以基于當(dāng)前底層數(shù)據(jù)(通常來自數(shù)據(jù)庫(kù))生成新的HTML,以響應(yīng)瀏覽器發(fā)出的每個(gè)請(qǐng)求。這意味著網(wǎng)站可以根據(jù)底層數(shù)據(jù)的變化而改變。最常見的用例是電子商務(wù)網(wǎng)站、企業(yè)應(yīng)用程序和新聞公司博客。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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"));
注意:在這里,每個(gè)頁面都將由服務(wù)器自動(dòng)生成,不同于靜態(tài)網(wǎng)站,靜態(tài)網(wǎng)站需要手動(dòng)編寫每個(gè)文件。
單頁應(yīng)用程序(SPA)是2010年代創(chuàng)建高度交互式Web應(yīng)用程序的解決方案,至今仍在使用。在這里,SPA通過從服務(wù)器獲取HTML外殼(空白HTML頁面)和JavaScript捆綁包來處理渲染到瀏覽器。在瀏覽器中,它將控制權(quán)(水合)交給JavaScript,動(dòng)態(tài)地將內(nèi)容注入(渲染)到外殼中。在這種情況下,渲染是在客戶端(CSR)上執(zhí)行的。使用JavaScript,這些SPA能夠在不需要完整頁面重新加載的情況下對(duì)單個(gè)頁面上的內(nèi)容進(jìn)行大量操作。它們還通過操作URL欄來創(chuàng)建多個(gè)頁面的幻覺,以指示加載到外殼上的每個(gè)資源。常見的用例包括項(xiàng)目管理系統(tǒng)、協(xié)作平臺(tái)、社交媒體Web應(yīng)用、交互式儀表板或文檔編輯器,這些應(yīng)用程序受益于SPA的響應(yīng)性和交互性。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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;
靜態(tài)網(wǎng)站生成(SSG)是一種利用構(gòu)建網(wǎng)站的原始靜態(tài)網(wǎng)站模式的渲染模式。在構(gòu)建過程中,從源代碼中預(yù)先構(gòu)建和渲染了所有可能的網(wǎng)頁,生成靜態(tài)HTML文件,然后將其存儲(chǔ)在存儲(chǔ)桶中,就像在典型靜態(tài)網(wǎng)站的情況下原始上傳靜態(tài)文件一樣。對(duì)于基于源代碼可能存在的任何路由的請(qǐng)求,將向客戶端提供相應(yīng)的預(yù)構(gòu)建靜態(tài)頁面。因此,與SSR或SPA不同,SSG不依賴于服務(wù)器端渲染或客戶端JavaScript來動(dòng)態(tài)渲染內(nèi)容。相反,內(nèi)容是提前生成的,并且可以被緩存和高性能地傳遞給用戶。這適用于中度交互的網(wǎng)站,其數(shù)據(jù)不經(jīng)常更改,例如作品集網(wǎng)站、小型博客或文檔網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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>
);
}
服務(wù)器端渲染(SSR)是一種渲染模式,它結(jié)合了多頁面應(yīng)用(MPA)和單頁面應(yīng)用(SPA)的能力,以克服兩者的局限性。在這種模式下,服務(wù)器生成網(wǎng)頁的HTML內(nèi)容,填充動(dòng)態(tài)數(shù)據(jù),并將其發(fā)送給客戶端進(jìn)行顯示。在瀏覽器上,JavaScript可以接管已經(jīng)渲染的頁面,為頁面上的組件添加交互性,就像在SPA中一樣。SSR在將完整的HTML交付給瀏覽器之前,在服務(wù)器上處理渲染過程,而SPA完全依賴于客戶端JavaScript進(jìn)行渲染。SSR特別適用于注重SEO、內(nèi)容傳遞或具有特定可訪問性要求的應(yīng)用,如企業(yè)網(wǎng)站、新聞網(wǎng)站和電子商務(wù)網(wǎng)站。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Nextjs)
在NEXT.js上實(shí)現(xiàn)SSR的代碼與SSG演示幾乎相同。這里,唯一的變化在于 getCurrentPrice 函數(shù)。使用帶有 no-cache 選項(xiàng)的fetch API,頁面將不會(huì)被緩存;相反,服務(wù)器將需要在每個(gè)請(qǐng)求上創(chuàng)建一個(gè)新頁面。
//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;
}
增量靜態(tài)生成是一種生成靜態(tài)網(wǎng)站的方法,它結(jié)合了靜態(tài)網(wǎng)站生成的優(yōu)點(diǎn),能夠更新和重新生成網(wǎng)站的特定頁面或部分,而無需重建整個(gè)網(wǎng)站。增量靜態(tài)生成允許自動(dòng)增量更新,從而減少了重建整個(gè)應(yīng)用程序所需的時(shí)間,并通過僅在必要時(shí)從服務(wù)器請(qǐng)求新數(shù)據(jù),更有效地利用服務(wù)器資源。這對(duì)于國(guó)際多語言網(wǎng)站、企業(yè)網(wǎng)站和發(fā)布平臺(tái)網(wǎng)站非常實(shí)用。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo (Nextjs)
在NEXT.js上實(shí)現(xiàn)ISR的代碼與SSG演示幾乎相同。唯一的變化在于 getCurrentPrice 函數(shù)。使用fetch API并使用指定條件的選項(xiàng)從服務(wù)器獲取數(shù)據(jù),當(dāng)滿足我們定義的條件時(shí),頁面將自動(dòng)更新。在這里,我們說底層數(shù)據(jù)應(yīng)該每60秒進(jìn)行驗(yàn)證,并且UI應(yīng)該根據(jù)數(shù)據(jù)中的任何變化進(jìn)行更新。
//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)框架中用于解決加載時(shí)間緩慢問題的一種技術(shù)。使用這種技術(shù),CSR框架將選擇性地首先渲染和水合具有交互性的網(wǎng)頁的最重要部分,而不是整個(gè)頁面。最終,當(dāng)滿足特定條件時(shí),較不重要的交互組件可以通過水合來實(shí)現(xiàn)其交互性。通過優(yōu)先處理關(guān)鍵或可見組件的水合,而推遲處理非關(guān)鍵或在折疊區(qū)域下的組件的水合,它可以更有效地利用資源,并通過優(yōu)先處理關(guān)鍵或可見組件的水合來加快初始頁面渲染速度。部分水合可以使任何具有多個(gè)交互組件的復(fù)雜CSR或SPA受益。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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 位于頁面底部,只有當(dāng)它進(jìn)入視口時(shí)才會(huì)被激活。
島嶼架構(gòu)是Astro框架開發(fā)者倡導(dǎo)的一種有前途的UI渲染模式。Web應(yīng)用程序在服務(wù)器上被劃分為多個(gè)獨(dú)立的小組件,稱為島嶼。每個(gè)島嶼負(fù)責(zé)渲染應(yīng)用程序UI的特定部分,并且它們可以獨(dú)立地進(jìn)行渲染。在服務(wù)器上被劃分為島嶼后,這些多個(gè)島嶼包被發(fā)送到瀏覽器,框架使用一種非常強(qiáng)大的部分加載形式,只有帶有交互部分的組件由JavaScript接管并啟用其交互性,而其他非交互式組件保持靜態(tài)。最常見的用例是構(gòu)建內(nèi)容豐富的網(wǎng)站。Astro是構(gòu)建專注于內(nèi)容的網(wǎng)站的不錯(cuò)選擇,例如博客、作品集和文檔網(wǎng)站。Astro的島嶼架構(gòu)模式可以幫助提高這些網(wǎng)站的性能,尤其是對(duì)于網(wǎng)絡(luò)連接較慢的用戶來說。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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是一個(gè)以重用性為核心的全新渲染方式的元框架。該渲染模式基于兩種主要策略:
在服務(wù)器上序列化應(yīng)用程序和框架的執(zhí)行狀態(tài),并在客戶端上恢復(fù)。
水合
這段來自Qwik文檔的摘錄很好地介紹了可重用性。
監(jiān)聽器 - 在DOM節(jié)點(diǎn)上定位事件監(jiān)聽器并安裝它們,使應(yīng)用程序具有交互性。組件樹 - 構(gòu)建表示應(yīng)用程序組件樹的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。應(yīng)用程序狀態(tài) - 恢復(fù)在服務(wù)器上存儲(chǔ)的任何獲取或保存的數(shù)據(jù)。總體而言,這被稱為水合。所有當(dāng)前的框架都需要這一步驟來使應(yīng)用程序具有交互性。
水合作用之所以昂貴,有兩個(gè)原因:
在序列化中, Qwik 顯示了在服務(wù)器上開始構(gòu)建網(wǎng)頁的能力,并在從服務(wù)器發(fā)送捆綁包后繼續(xù)在客戶端上執(zhí)行構(gòu)建,節(jié)省了其他框架重新初始化客戶端的時(shí)間。
就懶加載而言, Qwik 將通過極度懶加載來確保Web應(yīng)用程序盡快加載,只加載必要的JavaScript捆綁包,并在需要時(shí)加載其余部分。 Qwik 可以在開箱即用的情況下完成所有這些操作,無需進(jìn)行太多開發(fā)者配置。
這適用于復(fù)雜的博客應(yīng)用和企業(yè)網(wǎng)站的發(fā)布。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
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",
},
],
};
流式服務(wù)器端渲染(Streaming SSR)是一種相對(duì)較新的用于渲染W(wǎng)eb應(yīng)用程序的技術(shù)。流式SSR通過將應(yīng)用程序的用戶界面分塊在服務(wù)器上進(jìn)行渲染。每個(gè)塊在準(zhǔn)備好后立即進(jìn)行渲染,然后流式傳輸?shù)娇蛻舳恕?蛻舳嗽诮邮盏綁K時(shí)顯示和填充它們。這意味著客戶端在應(yīng)用程序完全渲染之前就可以開始與其進(jìn)行交互,無需等待。這提高了Web應(yīng)用程序的初始加載時(shí)間,尤其適用于大型和復(fù)雜的應(yīng)用程序。流式SSR最適用于大規(guī)模應(yīng)用,如電子商務(wù)和交易應(yīng)用程序。
優(yōu)點(diǎn)
缺點(diǎn)
相關(guān)框架
Demo
很遺憾,我們的應(yīng)用程序不夠復(fù)雜,無法提供一個(gè)合適的例子。
在本文中,我們探討了當(dāng)今前端網(wǎng)頁開發(fā)中最流行的十種UI渲染模式。在這個(gè)過程中,我們討論了每種方法的優(yōu)勢(shì)、局限性和權(quán)衡。然而,重要的是要注意,沒有一種適用于所有情況的渲染模式或普遍完美的渲染方法。每個(gè)應(yīng)用都有其獨(dú)特的需求和特點(diǎn),因此選擇合適的渲染模式對(duì)于開發(fā)過程的成功至關(guān)重要。
由于文章內(nèi)容篇幅有限,今天的內(nèi)容就分享到這里,文章結(jié)尾,我想提醒您,文章的創(chuàng)作不易,如果您喜歡我的分享,請(qǐng)別忘了點(diǎn)贊和轉(zhuǎn)發(fā),讓更多有需要的人看到。同時(shí),如果您想獲取更多前端技術(shù)的知識(shí),歡迎關(guān)注我,您的支持將是我分享最大的動(dòng)力。我會(huì)持續(xù)輸出更多內(nèi)容,敬請(qǐng)期待。
星 Galaxy Fold 和 Surface Duo 以及華為mate X等系列折疊屏手機(jī)問世至今已有三年多的時(shí)間。此后,三星 Galaxy Z Fold 3 和 Galaxy Z Flip 3、華為mate X2S、榮耀magic V系列等手機(jī)均已上市。可折疊設(shè)備可供購(gòu)買,目前正在被消費(fèi)者使用,隨之而來的是我們作為開發(fā)人員可以開始探索這種新型設(shè)備和響應(yīng)式設(shè)計(jì)的下一個(gè)發(fā)展的機(jī)會(huì)。
這些 Web 平臺(tái)功能與現(xiàn)有概念(例如視口和媒體查詢)集成,因此開發(fā)人員和設(shè)計(jì)人員可以花更多時(shí)間思考如何利用兩個(gè)顯示器來創(chuàng)建增強(qiáng)體驗(yàn),而不是學(xué)習(xí)一組新代碼來構(gòu)建它們。
雙屏和可折疊設(shè)備只是響應(yīng)式設(shè)計(jì)的下一步,因此它們被視為另一個(gè)響應(yīng)式設(shè)計(jì)目標(biāo),我們可以使用媒體功能為其設(shè)計(jì)樣式。我們今天已經(jīng)使用媒體功能和查詢來定位臺(tái)式機(jī)、平板電腦和手機(jī),現(xiàn)在我們擁有 CSS Viewport Segments 媒體功能來定位我們的可折疊和雙屏設(shè)備。
視口分段媒體查詢可以有兩個(gè)值。第一個(gè)是horizontal-viewport-segments,這表示設(shè)備鉸鏈垂直且視口被硬件鉸鏈拆分或折疊成列時(shí)的設(shè)備狀態(tài)。
當(dāng)horizonal-viewport-segment鉸鏈處于垂直折疊姿勢(shì)時(shí),目標(biāo)是設(shè)備。
為了專門為這種方向的可折疊設(shè)備提供樣式,我們將編寫以下內(nèi)容:
@media (horizontal-viewport-segments: 2) {
// Styles specific to the device in this orientation
}
整數(shù)表示設(shè)備方向中存在的視口數(shù)量。當(dāng)設(shè)備像一本書一樣處于垂直折疊姿勢(shì)時(shí),我們?cè)谒椒较蛴袃蓚€(gè)不同的視口,在垂直方向只有一個(gè)視口。
我們還可以結(jié)合我們的媒體查詢來定位雙屏設(shè)備和某些視口寬度,以提供特定的樣式:
@media (horizontal-viewport-segments: 2) and (min-width: 540px) {
body {
background: yellow;
}
}
我們的視口分段媒體功能的第二個(gè)值是vertical-viewport-segments,這是設(shè)備鉸鏈水平時(shí)設(shè)備的狀態(tài),并且硬件鉸鏈將我們的視口分成行。
vertical-viewport-segments目標(biāo)設(shè)備處于水平折疊姿勢(shì)。
要定位在這個(gè)方向旋轉(zhuǎn)的設(shè)備,我們將使用以下代碼:
@media (vertical-viewport-segments: 2) {
// Styles specific to the device in this orientation
}
在某些情況下,您可能無法或不想使用 CSS 媒體查詢來檢測(cè)您的用戶是否在可折疊設(shè)備上,這就是 JavaScript API 的用武之地。最初,提出了一個(gè)名為 Windows Segments Enumeration 的全新 API ,但在開發(fā)者社區(qū)通過原始試驗(yàn)獲得反饋后,在現(xiàn)有的Visual Viewport API 草案規(guī)范的基礎(chǔ)上構(gòu)建更有意義。
視口段表示位于彼此相鄰的單獨(dú)顯示器上的窗口區(qū)域。要檢測(cè)雙屏設(shè)備,您可以使用以下代碼查詢 segments 屬性:
const segments = window.visualViewport.segments;
此查詢返回的值將是一個(gè)數(shù)組DOMRects,指示有多少視口。如果只有一個(gè)視口段,則查詢將返回null,并以這種方式實(shí)現(xiàn)以防止將來出現(xiàn)兼容性問題,以免開發(fā)人員開始使用visualViewport.segments[0]針對(duì)單屏設(shè)備。
在雙屏設(shè)備上,查詢將返回 2 DOMRects,表示當(dāng)瀏覽器窗口跨越折疊時(shí)可用的 2 個(gè)視口。
我們存儲(chǔ)在segments常量中的這個(gè)值是查詢屬性時(shí)設(shè)備狀態(tài)的不可變快照,如果瀏覽器窗口調(diào)整大小或設(shè)備旋轉(zhuǎn),之前檢索到的視口段不再有效,需要查詢?cè)俅瓮ㄟ^調(diào)整大小或方向事件(或兩者)。
如果您調(diào)整瀏覽器窗口的大小以僅跨越一個(gè)顯示區(qū)域,我們將觸發(fā)調(diào)整大小事件。
如果您旋轉(zhuǎn)設(shè)備,這將觸發(fā)調(diào)整大小和方向事件,您可以使用這些事件再次查詢屬性以獲取瀏覽器顯示區(qū)域的當(dāng)前狀態(tài)。
window.addEventListener("resize", function() {
const segments = window.visualViewport.segments;
console.log(segments.length); *// 1*
});
CSS 媒體功能和 JavaScript 段屬性都將檢測(cè)雙屏設(shè)備,但 JavaScript 屬性最好在沒有使用 CSS 時(shí)使用,當(dāng)您在 Canvas2D 和 WebGL 中處理對(duì)象時(shí)可能會(huì)發(fā)生這種情況。例如,您正在開發(fā)的游戲可以同時(shí)利用兩個(gè)屏幕。
除了 CSS 媒體功能之外,還引入了六個(gè)新的 CSS 環(huán)境變量,以幫助開發(fā)人員計(jì)算顯示區(qū)域的幾何形狀,計(jì)算鉸鏈區(qū)域在被 Surface Duo 等物理硬件功能遮擋時(shí)的幾何形狀,以及它們還可用于幫助將內(nèi)容放置在每個(gè)顯示區(qū)域的邊界內(nèi)。
六個(gè)新的環(huán)境變量如下:
x和位置表示由分隔每個(gè)視口段的硬件功能創(chuàng)建的y二維網(wǎng)格,坐標(biāo)0,0從左上段開始。
當(dāng)您的設(shè)備處于垂直折疊姿勢(shì)且視口并排時(shí),左側(cè)的視口段將由 表示env(viewport-segment-width 0 0),而右側(cè)的視口段將由 表示env(viewport-segment-width 1 0)。如果您將設(shè)備轉(zhuǎn)換為水平折疊姿勢(shì),視口堆疊,頂部將由 表示env(viewport-segment-height 0 0),底部視口由表示env(viewport-segment-height 0 1)。
使用env(viewport-segment-width)andenv(viewport-segment-width)時(shí),除了索引之外,我們還可以設(shè)置一個(gè)后備值,如下所示:
env(viewport-segment-width 0 0, 100%);
但是這個(gè)額外的后備值是可選的,由作者自行決定,如果他們想包含它。
當(dāng)您的設(shè)備的鉸鏈被硬件功能遮擋時(shí),您可以使用提供的環(huán)境變量來計(jì)算它。
我們可以使用環(huán)境變量計(jì)算設(shè)備鉸鏈。
在我們的示例中,我們有一個(gè)處于垂直姿勢(shì)的設(shè)備,并且想要找到鉸鏈寬度,這樣就不會(huì)遮擋任何內(nèi)容。我們將從左顯示器的右視口段中減去右顯示器的左視口段:
calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0));
我們可以使用 CSS 環(huán)境變量在顯示區(qū)域邊界內(nèi)放置內(nèi)容,如果您想將內(nèi)容直接放置在鉸鏈或折疊處,這些特別有用。
在下面的示例中,我們將在左側(cè)第一個(gè)顯示區(qū)域的鉸鏈上直接放置圖像。該區(qū)域是視口的右側(cè)部分,因此我們將使用viewport-segment-right以下代碼放置它:
img {
max-width: 400px;
}
@media (horizontal-viewport-segments: 2) {
img {
position: absolute;
left: env(viewport-segment-right 0 0);
}
}
如果我們?cè)?Surface Duo 模式下在 Edge 開發(fā)人員工具中模擬我們的屏幕,我們將獲得以下布局:
最初使用環(huán)境變量將圖像放置在我們的布局中會(huì)將其放置在錯(cuò)誤的顯示區(qū)域中。
這不是我們想要的。圖像應(yīng)位于左側(cè)的顯示區(qū)域中。
因?yàn)閳D像是使用屬性絕對(duì)定位的left,所以圖像的左邊緣最終與viewport-segment-right顯示區(qū)域?qū)R。
然后,我們需要從環(huán)境變量中減去圖像的寬度,以使圖像與正確的鉸鏈邊緣對(duì)齊:
img {
max-width: 400px;
}
@media (horizontal-viewport-segments: 2) {
img {
position: absolute;
left: calc(env(viewport-segment-right 0 0) - 400px);
}
}
從視口段中減去圖像寬度會(huì)將其沿左側(cè)顯示中的鉸鏈放置。
現(xiàn)在我們將圖像放置在我們想要的位置。有關(guān)如何沿鉸鏈對(duì)齊項(xiàng)目的其他示例,您可以查看這個(gè)簡(jiǎn)單的盒子演示。打開Edge Developer Tools>Device Emulation然后選擇Surface Duo并確保您Duo emulation處于校正方向姿勢(shì)。
作為一個(gè)在做飯時(shí)經(jīng)常使用手機(jī)的人,當(dāng)我在我的雙屏設(shè)備上時(shí)會(huì)適應(yīng)的食譜網(wǎng)站會(huì)非常有幫助。讓我們來看看如何考慮為它調(diào)整一個(gè)單獨(dú)的食譜頁面。
我想考慮我將如何分塊我的主要內(nèi)容。通常情況下,我至少會(huì)看到食譜標(biāo)題、制作的份量、烹飪需要多長(zhǎng)時(shí)間、一張或多張圖片、配料以及制作菜肴的步驟。
當(dāng)我畫出我的線框時(shí),我得到以下信息:
桌面上食譜頁面的標(biāo)準(zhǔn)布局
我希望我的標(biāo)題和食譜詳細(xì)信息在最頂部,然后是一個(gè)占據(jù)整個(gè)內(nèi)容寬度的圖像,然后是成分列表和食譜步驟。我不想堆疊后兩個(gè)內(nèi)容組,因?yàn)槿绻叶询B它們,成分列表的右側(cè)會(huì)有很多空白,所以我希望步驟坐在成分旁邊,給我兩列圖片下方。
我知道我想如何在普通桌面屏幕上布置這個(gè)食譜,并且有多種方法可以對(duì)這個(gè)布局進(jìn)行編碼和對(duì)內(nèi)容進(jìn)行分組,但我如何對(duì)其進(jìn)行分組,以及我想在雙屏上實(shí)現(xiàn)什么布局在我編碼之前需要考慮設(shè)備。根據(jù)我為桌面視圖所做的草圖,我可以使用 flexbox 和 CSS Grid 的組合來實(shí)現(xiàn)我想要的布局,我將成分和步驟分組到一個(gè) flex 容器中。但是讓我勾勒一下我希望我的頁面如何在雙屏上顯示。
垂直折疊位置的可折疊設(shè)備上的理想布局通過顯示屏將內(nèi)容分開,因此不會(huì)被鉸鏈遮擋。
如果我想在布局上有更大的靈活性,那么我不能將我的成分和步驟分組到一個(gè) flex 容器中,否則,無論圖像沒有進(jìn)入哪一列,都會(huì)有很大的空白。
如果我只在這個(gè)布局中使用 flexbox,它會(huì)產(chǎn)生一些我想避免亂用的間距。
我將在桌面和雙屏布局中只使用 CSS Grid。所以,讓我們構(gòu)建我們的內(nèi)容。
<main>
<section class="recipe">
<div class="recipe-meta">
… <!—Contains our recipe title, yield and servings -->
</div>
<img src="imgs/pasta.jpg" alt="Pasta carbonara photographed from above on a rustic plate" />
<div class="recipe-details__ingredients">
…<!— Contains our ingredients list -->
</div>
<div class="recipe-details__preparation">
… <!— Contains our list of steps to put the ingredients together -->
</div>
</section>
</main>
接下來,讓我們構(gòu)建頁面的結(jié)構(gòu)。我要定義我的網(wǎng)格:我只想要三列,并且我希望它們是容器的相等部分。
.recipe {
display: grid;
grid-template-columns: repeat(3, 1fr);
接下來,我將定義我的行,并且我將使用grid-auto-rowswith minmax,這樣我的行是最小的,175px但可以增長(zhǎng)到最大內(nèi)容高度的最大值。
grid-auto-rows: minmax(175px, max-content);
然后我將添加更多屬性: my grip-gap、我的最大內(nèi)容寬度和一個(gè)邊距,以使我的布局在頁面上居中。
grid-gap: 1rem;
max-width: 64rem;
margin: 0 auto;
}
然后,我將把我的內(nèi)容放入我定義的網(wǎng)格中:
.recipe-meta {
grid-column: 1 / 4;
}
.recipe-meta p {
margin: 0;
}
img {
width: 100%;
grid-column: 1 / 4;
}
.recipe-details__ingredients {
grid-row: 3;
}
.recipe-details__preparation {
grid-column: 2 / 4;
grid-row: 3;
}
這將根據(jù)我的草圖為我提供布局:
布局在桌面上按預(yù)期呈現(xiàn)
偉大的!但是我的雙屏布局呢?讓我們深入了解我們的horizontal-viewport媒體功能和雙屏網(wǎng)格。
首先,這是我現(xiàn)在在雙屏上的布局:
在沒有實(shí)現(xiàn)任何雙屏代碼的情況下,如果用戶想要將瀏覽器跨過兩個(gè)顯示器,那么頁面將是這樣的。
如果我們向下滾動(dòng):
如果用戶選擇跨越兩個(gè)顯示器,則內(nèi)容會(huì)被鉸鏈遮擋。
不是很好。我們的內(nèi)容被鉸鏈遮住了,所以讓我開始重新定義我的網(wǎng)格。
對(duì)于我的網(wǎng)格列,我仍將使用三列,但我希望一列占據(jù)左側(cè)的第一個(gè)視口段,另外兩列占據(jù)右側(cè)視口段,因此我將使用我的 CSS環(huán)境變量env(viewport-segment-width 0 0)告訴瀏覽器,對(duì)于我的第一列,我希望它占據(jù)第一個(gè)顯示區(qū)域的整個(gè)視口。
@media (horizontal-viewport-segments: 2) {
/* Body styles for smaller screens */
body {
font: 1.3em/1.8 base, 'Playfair Display', serif;
margin: 0;
}
.recipe {
grid-template-columns: env(viewport-segment-width 0 0 1fr 1fr;
grid-template-rows: repeat(2, 175px) minmax(175px, max-content);
}
}
對(duì)于我的行,我希望在放置上更靈活一點(diǎn),所以我將重復(fù)兩行175px,這是關(guān)于帶有配方標(biāo)題、產(chǎn)量和時(shí)間信息的容器的高度,之后的行應(yīng)該匹配我最初在網(wǎng)格中定義的內(nèi)容。
如果我在 DevTools 中檢查我的設(shè)計(jì),我可以看到我在配方容器上設(shè)置的width和margin最初將我想要與我的視口段對(duì)齊的網(wǎng)格線推到正確的視口段中。
添加我的代碼后,我的內(nèi)容不再被遮擋,但仍需要一些間距調(diào)整。
要重置它,我將重置我的marginand max-width。
@media (horizontal-viewport-segments: 2) {
.recipe {
grid-template-columns: env(viewport-segment-width 0 0) 1fr 1fr;
grid-template-rows: repeat(2, 175px) minmax(175px, max-content);
margin: 0;
max-width: 100%;
}
}
重置我的邊距和填充會(huì)掩蓋右側(cè)顯示中的內(nèi)容。
現(xiàn)在我要把我的內(nèi)容放在網(wǎng)格中并調(diào)整我的布局。
.recipe-meta {
grid-column: 1 / 2;
padding: 0 2rem;
}
img {
grid-column: 2 / 4;
grid-row: 1 / 3;
width: 100%;
height: 100%;
object-fit: cover;
/* necessary to keep the image within the grid lines */
}
.recipe-details__ingredients {
grid-row: 2;
padding: 0 2rem;
}
.recipe-details__preparation {
grid-column: 2 / 4;
grid-row: 3;
padding: 0 2rem 0 3rem;
}
我已經(jīng)對(duì)內(nèi)容應(yīng)用了填充,除了我決定要跨越整個(gè)視口的圖像。對(duì)于圖像下方的內(nèi)容,由于從物理鉸鏈下方開始的網(wǎng)格線的性質(zhì),我想添加額外的填充,因此它看起來左側(cè)的填充與其他帶有填充的項(xiàng)目相同。如果我不添加額外的,它會(huì)落得太靠近鉸鏈。因?yàn)槲乙呀?jīng)有一個(gè) grid-gap1rem并且我想將 padding 加倍,所以我將添加3rem而不是4rem為我們提供雙屏設(shè)備上的最終布局:
我可以重新添加尺寸更合適的填充來顯示內(nèi)容,因此它不會(huì)在帶有物理鉸鏈的設(shè)備上被遮擋。
只需對(duì)我們的 CSS 進(jìn)行一些小的調(diào)整并使用其中一項(xiàng)新的媒體功能,我們就有了一個(gè)適應(yīng)雙屏設(shè)備的布局。要查看體驗(yàn),請(qǐng)前往此處的 Edge 演示站點(diǎn)或基于 Chromium 的瀏覽器,然后打開瀏覽器開發(fā)人員工具以查看 Surface Duo 仿真。如果您在 Chrome 中打開該站點(diǎn),請(qǐng)確保在 下啟用了實(shí)驗(yàn)性網(wǎng)絡(luò)平臺(tái)功能標(biāo)志chrome://flags,以便演示正確顯示。
為了確保我們考慮到小型單屏設(shè)備,我為手機(jī)布局選擇的代碼使用了 flexbox 并將所有內(nèi)容放在一個(gè)列中:
@media (max-width: 48rem) {
body {
font: 1.3em/1.8 base, 'Playfair Display', serif;
}
.recipe-details {
display: flex;
flex-direction: column;
}
}
默認(rèn)情況下,這些雙屏 API 在 Microsoft Edge 和 Android 上的 Edge 中可用,從版本 97 開始。這些計(jì)劃很快就會(huì)出現(xiàn)在其他 Chromium 瀏覽器中,但具體時(shí)間尚未確定。要在 Chrome 中啟用這些 API,請(qǐng)轉(zhuǎn)到chrome://flags并啟用實(shí)驗(yàn)性網(wǎng)絡(luò)平臺(tái)功能。
雖然這些是相對(duì)較新的設(shè)備,但許多現(xiàn)在已經(jīng)進(jìn)入第二代和第三代,因此公司正在投資它們。如果您無法使用物理設(shè)備,最好的測(cè)試方法是使用瀏覽器開發(fā)工具。我已經(jīng)在仿真工具和 Surface Duo 上測(cè)試了我的網(wǎng)站,Duo 的仿真工具似乎是相同的。我的設(shè)計(jì)在設(shè)備上的外觀與在 DevTools 中的外觀相同。它使構(gòu)建和設(shè)計(jì)雙屏設(shè)備就像開發(fā)桌面和單屏移動(dòng)設(shè)備一樣容易。
如果您使用的是不支持這些 API 的桌面或設(shè)備,則可以為 Visual Viewport Segments 屬性提供一個(gè) polyfill。CSS 媒體查詢沒有 API。目前,市場(chǎng)上的雙屏設(shè)備都是基于安卓的,這些API計(jì)劃在安卓上可用的基于Chromium的瀏覽器中。
如果可折疊設(shè)備上的瀏覽器不支持這些功能,您可以使用 polyfill 或確保您的網(wǎng)站在小單屏上仍能很好地呈現(xiàn),因?yàn)橛脩艨梢造`活選擇如何在雙屏上顯示網(wǎng)站屏幕設(shè)備。他們可以跨兩個(gè)顯示器跨越一個(gè)網(wǎng)站,或者他們可以選擇讓它跨一個(gè)顯示器,如果他們選擇后者,它將像在平板電腦或手機(jī)上一樣顯示。即使您的網(wǎng)站沒有雙屏實(shí)現(xiàn),用戶仍然可以選擇單顯示視圖。雙屏 API 提供了一種方法來逐步增強(qiáng)擁有設(shè)備的用戶的體驗(yàn)。
雙屏設(shè)備只是響應(yīng)式設(shè)計(jì)的下一個(gè)發(fā)展方向。如果您有 PWA 或網(wǎng)站,可用的 API 可以無縫集成到您現(xiàn)有的代碼庫(kù)中。還有其他方法可以為雙屏設(shè)備構(gòu)建應(yīng)用程序,您可以在Surface Duo 文檔https://docs.microsoft.com/en-us/dual-screen/中查看這些方法。這是在網(wǎng)絡(luò)上進(jìn)行布局的激動(dòng)人心的時(shí)刻,雙屏提供了獲得更多創(chuàng)意的機(jī)會(huì)。
為幫助到一部分同學(xué)不走彎路,真正達(dá)到一線互聯(lián)網(wǎng)大廠前端項(xiàng)目研發(fā)要求,首次實(shí)力寵粉,打造了《30天挑戰(zhàn)學(xué)習(xí)計(jì)劃》,內(nèi)容如下:
HTML/HTML5,CSS/CSS3,JavaScript,真實(shí)企業(yè)項(xiàng)目開發(fā),云服務(wù)器部署上線,從入門到精通
共4大完整的項(xiàng)目開發(fā) !一行一行代碼帶領(lǐng)實(shí)踐開發(fā),實(shí)際企業(yè)開發(fā)怎么做我們就是怎么做。從學(xué)習(xí)一開始就進(jìn)入工作狀態(tài),省得浪費(fèi)時(shí)間。
從學(xué)習(xí)一開始就同步使用 Git 進(jìn)行項(xiàng)目代碼的版本的管理,Markdown 記錄學(xué)習(xí)筆記,包括真實(shí)大廠項(xiàng)目的開發(fā)標(biāo)準(zhǔn)和設(shè)計(jì)規(guī)范,命名規(guī)范,項(xiàng)目代碼規(guī)范,SEO優(yōu)化規(guī)范
從藍(lán)湖UI設(shè)計(jì)稿 到 PC端,移動(dòng)端,多端響應(yīng)式開發(fā)項(xiàng)目開發(fā)
這些內(nèi)容在《30天挑戰(zhàn)學(xué)習(xí)計(jì)劃》中每一個(gè)細(xì)節(jié)都有講到,包含視頻+圖文教程+項(xiàng)目資料素材等。只為實(shí)力寵粉,真正一次掌握企業(yè)項(xiàng)目開發(fā)必備技能,不走彎路 !
過程中【不涉及】任何費(fèi)用和利益,非誠(chéng)勿擾 。
如果你沒有添加助理老師微信,可以添加下方微信,說明要參加30天挑戰(zhàn)學(xué)習(xí)計(jì)劃,來自!老師會(huì)邀請(qǐng)你進(jìn)入學(xué)習(xí),并給你發(fā)放相關(guān)資料。
30 天挑戰(zhàn)學(xué)習(xí)計(jì)劃 Web 前端從入門到實(shí)戰(zhàn) | arry老師的博客-艾編程
這篇文章中,我將分享21個(gè)HTML技巧,包括代碼片段,可以提升你的編碼技能。
讓我們立即開始吧。
(本文視頻講解:java567.com)
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。