整合營銷服務商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          深入學習下現(xiàn)代網頁開發(fā)中的10種渲染模式

          載說明:原創(chuàng)不易,未經授權,謝絕任何形式的轉載

          如何使您的網站呈現(xiàn)最佳狀態(tài)?這個問題有很多答案,本文介紹了當前框架中應用最廣泛的十種渲染設計模式,讓您能夠選擇最適合您的方式。

          近年來,網絡開發(fā)的迅速演變,尤其是在前端開發(fā)領域。這種轉變主要歸功于無數(shù)涌現(xiàn)的框架和技術,它們旨在簡化和增強構建引人入勝的用戶界面的過程。然而,由于現(xiàn)有框架的豐富多樣以及不斷涌現(xiàn)的新框架,跟上前端趨勢已成為一項艱巨的任務。對于新手來說,很容易感到不知所措,仿佛迷失在廣闊的選擇海洋中。

          渲染是前端開發(fā)的核心挑戰(zhàn),它將數(shù)據(jù)和代碼轉化為可見且可交互的用戶界面。雖然大多數(shù)框架以類似的方式應對這一挑戰(zhàn),通常比之前的方法更簡潔,但也有一些框架選擇了全新的解決方案。在本文中,我們將研究流行框架中使用的十種常見渲染模式,通過這樣做,無論是初學者還是專家都將獲得對新舊框架的扎實基礎理解,同時也能對解決應用程序中的渲染問題有新的見解。

          在本文的結尾,您將會:

          • 對于當今網頁開發(fā)中最常見的渲染模式有基本的了解
          • 了解不同渲染模式的優(yōu)勢和劣勢
          • 了解在你的下一個大項目中使用哪種渲染模式和框架

          什么是UI渲染模式?

          在前端開發(fā)的背景下,渲染是將數(shù)據(jù)和代碼轉換為對最終用戶可見的HTML。UI渲染模式是指實現(xiàn)渲染過程可以采用的各種方法。這些模式概述了不同的策略,用于描述轉換發(fā)生的方式以及呈現(xiàn)出的用戶界面。正如我們很快會發(fā)現(xiàn)的那樣,根據(jù)所實現(xiàn)的模式,渲染可以在服務器上或瀏覽器中進行,可以部分或一次性完成。

          選擇正確的渲染模式對開發(fā)人員來說至關重要,因為它直接影響到Web應用程序的性能、成本、速度、可擴展性、用戶體驗,甚至開發(fā)人員的體驗。

          在本文中,我們將介紹下面列出的前十種渲染模式:

          • 1、靜態(tài)網站(Static Site)
          • 2、多頁面應用(Multi-Page Applications(MPA))
          • 3、單頁應用程序(Single Page Applications (with Client Side Rendering CSR))
          • 4、服務器端渲染(erver Side Rendering (SSR))
          • 5、靜態(tài)網站生成(Static Site Generation (SSG))
          • 6、增量靜態(tài)生成(Incremental Static Generation (ISG))
          • 7、部分水合(Partial Hydration)
          • 8、Island Architectur
          • 9、Resumability
          • 10、 SSR

          在每個案例中,我們將研究渲染模式的概念、優(yōu)點和缺點、使用案例、相關的框架,并提供一個簡單的代碼示例來闡明觀點。

          代碼示例

          • 第一頁將顯示可用的貨幣類型
          • 第二頁將顯示從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;
          }
          

          靜態(tài)網站

          靜態(tài)網站是最原始、最基本、最直接的UI渲染方法。它通過簡單地編寫HTML、CSS和JavaScript來創(chuàng)建網站。一旦代碼準備好,它會被上傳為靜態(tài)文件到托管服務(如Netlify),并指向一個域名。通過URL請求時,靜態(tài)文件會直接提供給用戶,無需服務器端處理。靜態(tài)網站渲染非常適合沒有交互性和動態(tài)內容的靜態(tài)網站,比如落地頁和文檔網站。

          優(yōu)點

          • 非常簡單
          • 快速
          • 廉價(無服務器)
          • SEO友好

          缺點

          • 不適用于數(shù)據(jù)頻繁變動的情況(動態(tài)數(shù)據(jù))
          • 不適用于互動應用程序
          • 沒有直接的數(shù)據(jù)庫連接
          • 當數(shù)據(jù)發(fā)生變化時,需要手動更新和重新上傳

          相關框架

          • 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實現(xiàn)的應用程序。下面是應用程序。

          第一頁:顯示所有可用的虛擬幣

          第2頁:從Coingecko API獲取的不同交易所的BTC價格。

          請注意,在使用靜態(tài)網站時,每個幣種的價格頁面必須手動編寫。

          多頁面應用程序(MPAs)

          這種渲染模式是為了處理我們網站上的動態(tài)數(shù)據(jù)而出現(xiàn)的解決方案,并導致了今天許多最大、最受歡迎的動態(tài)Web應用程序的創(chuàng)建。在MPA中,渲染由服務器完成,服務器會重新加載以基于當前底層數(shù)據(jù)(通常來自數(shù)據(jù)庫)生成新的HTML,以響應瀏覽器發(fā)出的每個請求。這意味著網站可以根據(jù)底層數(shù)據(jù)的變化而改變。最常見的用例是電子商務網站、企業(yè)應用程序和新聞公司博客。

          優(yōu)點

          • 簡單直接
          • 處理動態(tài)數(shù)據(jù)非常出色
          • SEO友好
          • 良好的開發(fā)者體驗
          • 高度可擴展的

          缺點

          • 適度支持用戶界面的交互性
          • 由于多次重新加載而導致用戶體驗差
          • 昂貴的(需要服務器)

          相關框架

          • 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"));

          注意:在這里,每個頁面都將由服務器自動生成,不同于靜態(tài)網站,靜態(tài)網站需要手動編寫每個文件。

          單頁應用程序(SPA)

          單頁應用程序(SPA)是2010年代創(chuàng)建高度交互式Web應用程序的解決方案,至今仍在使用。在這里,SPA通過從服務器獲取HTML外殼(空白HTML頁面)和JavaScript捆綁包來處理渲染到瀏覽器。在瀏覽器中,它將控制權(水合)交給JavaScript,動態(tài)地將內容注入(渲染)到外殼中。在這種情況下,渲染是在客戶端(CSR)上執(zhí)行的。使用JavaScript,這些SPA能夠在不需要完整頁面重新加載的情況下對單個頁面上的內容進行大量操作。它們還通過操作URL欄來創(chuàng)建多個頁面的幻覺,以指示加載到外殼上的每個資源。常見的用例包括項目管理系統(tǒng)、協(xié)作平臺、社交媒體Web應用、交互式儀表板或文檔編輯器,這些應用程序受益于SPA的響應性和交互性。

          優(yōu)點

          • 高度互動
          • 在瀏覽多個頁面時,用戶體驗無縫銜接
          • 手機友好

          缺點

          • 由于JavaScript捆綁包過大,加載時間較慢
          • SEO能力差
          • 由于客戶端上的代碼執(zhí)行,存在高安全風險
          • 可擴展性差

          相關框架

          • 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;

          靜態(tài)網站生成(SSG)

          靜態(tài)網站生成(SSG)是一種利用構建網站的原始靜態(tài)網站模式的渲染模式。在構建過程中,從源代碼中預先構建和渲染了所有可能的網頁,生成靜態(tài)HTML文件,然后將其存儲在存儲桶中,就像在典型靜態(tài)網站的情況下原始上傳靜態(tài)文件一樣。對于基于源代碼可能存在的任何路由的請求,將向客戶端提供相應的預構建靜態(tài)頁面。因此,與SSR或SPA不同,SSG不依賴于服務器端渲染或客戶端JavaScript來動態(tài)渲染內容。相反,內容是提前生成的,并且可以被緩存和高性能地傳遞給用戶。這適用于中度交互的網站,其數(shù)據(jù)不經常更改,例如作品集網站、小型博客或文檔網站。

          優(yōu)點

          • SEO友好
          • 快速加載頁面
          • 高性能
          • 提高安全性(由于代碼既不在客戶端上運行也不在服務器上運行)

          缺點

          • 有限互動
          • 數(shù)據(jù)更改后需要重新構建和重新上傳

          相關框架

          • 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)

          服務器端渲染(SSR)是一種渲染模式,它結合了多頁面應用(MPA)和單頁面應用(SPA)的能力,以克服兩者的局限性。在這種模式下,服務器生成網頁的HTML內容,填充動態(tài)數(shù)據(jù),并將其發(fā)送給客戶端進行顯示。在瀏覽器上,JavaScript可以接管已經渲染的頁面,為頁面上的組件添加交互性,就像在SPA中一樣。SSR在將完整的HTML交付給瀏覽器之前,在服務器上處理渲染過程,而SPA完全依賴于客戶端JavaScript進行渲染。SSR特別適用于注重SEO、內容傳遞或具有特定可訪問性要求的應用,如企業(yè)網站、新聞網站和電子商務網站。

          優(yōu)點

          • 適度互動
          • SEO友好
          • 快速加載時間
          • 對動態(tài)數(shù)據(jù)的良好支持

          缺點

          • 復雜的實施
          • 成本(需要服務器)

          相關框架

          • Next.js
          • Nuxt.js

          Demo (Nextjs)

          在NEXT.js上實現(xiàn)SSR的代碼與SSG演示幾乎相同。這里,唯一的變化在于 getCurrentPrice 函數(shù)。使用帶有 no-cache 選項的fetch API,頁面將不會被緩存;相反,服務器將需要在每個請求上創(chuàng)建一個新頁面。

          //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)生成(ISG)

          增量靜態(tài)生成是一種生成靜態(tài)網站的方法,它結合了靜態(tài)網站生成的優(yōu)點,能夠更新和重新生成網站的特定頁面或部分,而無需重建整個網站。增量靜態(tài)生成允許自動增量更新,從而減少了重建整個應用程序所需的時間,并通過僅在必要時從服務器請求新數(shù)據(jù),更有效地利用服務器資源。這對于國際多語言網站、企業(yè)網站和發(fā)布平臺網站非常實用。

          優(yōu)點

          • 靜態(tài)網站的實時自動更新支持
          • 性價比高
          • SEO友好
          • 良好的性能和可擴展性

          缺點

          • 實施中的復雜性
          • 不適用于高度動態(tài)的數(shù)據(jù)應用

          相關框架

          • Next.js
          • Nuxt.js

          Demo (Nextjs)

          在NEXT.js上實現(xiàn)ISR的代碼與SSG演示幾乎相同。唯一的變化在于 getCurrentPrice 函數(shù)。使用fetch API并使用指定條件的選項從服務器獲取數(shù)據(jù),當滿足我們定義的條件時,頁面將自動更新。在這里,我們說底層數(shù)據(jù)應該每60秒進行驗證,并且UI應該根據(jù)數(shù)據(jù)中的任何變化進行更新。

          //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框架將選擇性地首先渲染和水合具有交互性的網頁的最重要部分,而不是整個頁面。最終,當滿足特定條件時,較不重要的交互組件可以通過水合來實現(xiàn)其交互性。通過優(yōu)先處理關鍵或可見組件的水合,而推遲處理非關鍵或在折疊區(qū)域下的組件的水合,它可以更有效地利用資源,并通過優(yōu)先處理關鍵或可見組件的水合來加快初始頁面渲染速度。部分水合可以使任何具有多個交互組件的復雜CSR或SPA受益。

          優(yōu)點

          • 由于減少了初始的JavaScript捆綁包,加載時間更快
          • 性能提升了
          • 優(yōu)化的搜索引擎優(yōu)化
          • 資源效率

          缺點

          • 增加的復雜性和代碼
          • 不一致的用戶界面可能性

          相關框架

          • 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 位于頁面底部,只有當它進入視口時才會被激活。

          Island Architecture(Astro)

          島嶼架構是Astro框架開發(fā)者倡導的一種有前途的UI渲染模式。Web應用程序在服務器上被劃分為多個獨立的小組件,稱為島嶼。每個島嶼負責渲染應用程序UI的特定部分,并且它們可以獨立地進行渲染。在服務器上被劃分為島嶼后,這些多個島嶼包被發(fā)送到瀏覽器,框架使用一種非常強大的部分加載形式,只有帶有交互部分的組件由JavaScript接管并啟用其交互性,而其他非交互式組件保持靜態(tài)。最常見的用例是構建內容豐富的網站。Astro是構建專注于內容的網站的不錯選擇,例如博客、作品集和文檔網站。Astro的島嶼架構模式可以幫助提高這些網站的性能,尤其是對于網絡連接較慢的用戶來說。

          優(yōu)點

          • 性能(當今最快的框架之一)
          • 更小的捆綁尺寸
          • 易學易懂,易于維護
          • 良好的SEO表現(xiàn)
          • 良好的開發(fā)者體驗

          缺點

          • 有限互動
          • 由于組件數(shù)量極多,導致調試困難

          相關框架

          • 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>

          Resumability (withQwik)

          Qwik是一個以重用性為核心的全新渲染方式的元框架。該渲染模式基于兩種主要策略:

          在服務器上序列化應用程序和框架的執(zhí)行狀態(tài),并在客戶端上恢復。

          水合

          這段來自Qwik文檔的摘錄很好地介紹了可重用性。

          監(jiān)聽器 - 在DOM節(jié)點上定位事件監(jiān)聽器并安裝它們,使應用程序具有交互性。組件樹 - 構建表示應用程序組件樹的內部數(shù)據(jù)結構。應用程序狀態(tài) - 恢復在服務器上存儲的任何獲取或保存的數(shù)據(jù)。總體而言,這被稱為水合。所有當前的框架都需要這一步驟來使應用程序具有交互性。

          水合作用之所以昂貴,有兩個原因:

          • 框架必須下載與當前頁面相關的所有組件代碼。
          • 框架必須執(zhí)行與頁面上的組件相關聯(lián)的模板,以重建監(jiān)聽器位置和內部組件樹。

          在序列化中, Qwik 顯示了在服務器上開始構建網頁的能力,并在從服務器發(fā)送捆綁包后繼續(xù)在客戶端上執(zhí)行構建,節(jié)省了其他框架重新初始化客戶端的時間。

          就懶加載而言, Qwik 將通過極度懶加載來確保Web應用程序盡快加載,只加載必要的JavaScript捆綁包,并在需要時加載其余部分。 Qwik 可以在開箱即用的情況下完成所有這些操作,無需進行太多開發(fā)者配置。

          這適用于復雜的博客應用和企業(yè)網站的發(fā)布。

          優(yōu)點

          • 由于可恢復性而對網絡中斷具有彈性
          • 快速加載時間
          • 友好的搜索引擎優(yōu)化

          缺點

          • 復雜的實施
          • 更高的帶寬使用

          相關框架

          • 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)

          流式服務器端渲染(Streaming SSR)是一種相對較新的用于渲染Web應用程序的技術。流式SSR通過將應用程序的用戶界面分塊在服務器上進行渲染。每個塊在準備好后立即進行渲染,然后流式傳輸?shù)娇蛻舳恕?蛻舳嗽诮邮盏綁K時顯示和填充它們。這意味著客戶端在應用程序完全渲染之前就可以開始與其進行交互,無需等待。這提高了Web應用程序的初始加載時間,尤其適用于大型和復雜的應用程序。流式SSR最適用于大規(guī)模應用,如電子商務和交易應用程序。

          優(yōu)點

          • Performance
          • 實時更新

          缺點

          • 復雜性

          相關框架

          • Next.js
          • Nuxt.js

          Demo

          很遺憾,我們的應用程序不夠復雜,無法提供一個合適的例子。

          結束

          在本文中,我們探討了當今前端網頁開發(fā)中最流行的十種UI渲染模式。在這個過程中,我們討論了每種方法的優(yōu)勢、局限性和權衡。然而,重要的是要注意,沒有一種適用于所有情況的渲染模式或普遍完美的渲染方法。每個應用都有其獨特的需求和特點,因此選擇合適的渲染模式對于開發(fā)過程的成功至關重要。

          由于文章內容篇幅有限,今天的內容就分享到這里,文章結尾,我想提醒您,文章的創(chuàng)作不易,如果您喜歡我的分享,請別忘了點贊和轉發(fā),讓更多有需要的人看到。同時,如果您想獲取更多前端技術的知識,歡迎關注我,您的支持將是我分享最大的動力。我會持續(xù)輸出更多內容,敬請期待。

          參考文獻

          • 有關可恢復性的更多信息,請參閱文檔: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/

          星 Galaxy Fold 和 Surface Duo 以及華為mate X等系列折疊屏手機問世至今已有三年多的時間。此后,三星 Galaxy Z Fold 3 和 Galaxy Z Flip 3、華為mate X2S、榮耀magic V系列等手機均已上市。可折疊設備可供購買,目前正在被消費者使用,隨之而來的是我們作為開發(fā)人員可以開始探索這種新型設備和響應式設計的下一個發(fā)展的機會。

          這些 Web 平臺功能與現(xiàn)有概念(例如視口和媒體查詢)集成,因此開發(fā)人員和設計人員可以花更多時間思考如何利用兩個顯示器來創(chuàng)建增強體驗,而不是學習一組新代碼來構建它們。

          使用新的 CSS 媒體功能檢測可折疊設備

          雙屏和可折疊設備只是響應式設計的下一步,因此它們被視為另一個響應式設計目標,我們可以使用媒體功能為其設計樣式。我們今天已經使用媒體功能和查詢來定位臺式機、平板電腦和手機,現(xiàn)在我們擁有 CSS Viewport Segments 媒體功能來定位我們的可折疊和雙屏設備。

          horizontal-viewport-segments

          視口分段媒體查詢可以有兩個值。第一個是horizontal-viewport-segments,這表示設備鉸鏈垂直且視口被硬件鉸鏈拆分或折疊成列時的設備狀態(tài)。

          horizonal-viewport-segment鉸鏈處于垂直折疊姿勢時,目標是設備。

          為了專門為這種方向的可折疊設備提供樣式,我們將編寫以下內容:

          @media (horizontal-viewport-segments: 2) {
          // Styles specific to the device in this orientation
          }

          整數(shù)表示設備方向中存在的視口數(shù)量。當設備像一本書一樣處于垂直折疊姿勢時,我們在水平方向有兩個不同的視口,在垂直方向只有一個視口。

          我們還可以結合我們的媒體查詢來定位雙屏設備和某些視口寬度,以提供特定的樣式:

          @media (horizontal-viewport-segments: 2) and (min-width: 540px) {
             body {
                 background: yellow;
            }
          }

          vertical-viewport-segments

          我們的視口分段媒體功能的第二個值是vertical-viewport-segments,這是設備鉸鏈水平時設備的狀態(tài),并且硬件鉸鏈將我們的視口分成行。

          vertical-viewport-segments目標設備處于水平折疊姿勢。

          要定位在這個方向旋轉的設備,我們將使用以下代碼:

          @media (vertical-viewport-segments: 2) {
            // Styles specific to the device in this orientation
          }

          使用 JavaScript 檢測可折疊設備

          在某些情況下,您可能無法或不想使用 CSS 媒體查詢來檢測您的用戶是否在可折疊設備上,這就是 JavaScript API 的用武之地。最初,提出了一個名為 Windows Segments Enumeration 的全新 API ,但在開發(fā)者社區(qū)通過原始試驗獲得反饋后,在現(xiàn)有的Visual Viewport API 草案規(guī)范的基礎上構建更有意義。

          視口段屬性

          視口段表示位于彼此相鄰的單獨顯示器上的窗口區(qū)域。要檢測雙屏設備,您可以使用以下代碼查詢 segments 屬性:

          const segments = window.visualViewport.segments;

          此查詢返回的值將是一個數(shù)組DOMRects,指示有多少視口。如果只有一個視口段,則查詢將返回null,并以這種方式實現(xiàn)以防止將來出現(xiàn)兼容性問題,以免開發(fā)人員開始使用visualViewport.segments[0]針對單屏設備。

          在雙屏設備上,查詢將返回 2 DOMRects,表示當瀏覽器窗口跨越折疊時可用的 2 個視口。

          我們存儲在segments常量中的這個值是查詢屬性時設備狀態(tài)的不可變快照,如果瀏覽器窗口調整大小或設備旋轉,之前檢索到的視口段不再有效,需要查詢再次通過調整大小或方向事件(或兩者)。

          如果您調整瀏覽器窗口的大小以僅跨越一個顯示區(qū)域,我們將觸發(fā)調整大小事件。

          如果您旋轉設備,這將觸發(fā)調整大小和方向事件,您可以使用這些事件再次查詢屬性以獲取瀏覽器顯示區(qū)域的當前狀態(tài)。

          window.addEventListener("resize", function() {
             const segments = window.visualViewport.segments;
             console.log(segments.length); *// 1*
          });

          何時使用 JAVASCRIPT API 與 CSS 媒體功能來檢測 設備

          CSS 媒體功能和 JavaScript 段屬性都將檢測雙屏設備,但 JavaScript 屬性最好在沒有使用 CSS 時使用,當您在 Canvas2D 和 WebGL 中處理對象時可能會發(fā)生這種情況。例如,您正在開發(fā)的游戲可以同時利用兩個屏幕。

          使用 CSSenv()變量

          除了 CSS 媒體功能之外,還引入了六個新的 CSS 環(huán)境變量,以幫助開發(fā)人員計算顯示區(qū)域的幾何形狀,計算鉸鏈區(qū)域在被 Surface Duo 等物理硬件功能遮擋時的幾何形狀,以及它們還可用于幫助將內容放置在每個顯示區(qū)域的邊界內。

          六個新的環(huán)境變量如下:

          • env(viewport-segment-width <x> <y>);
          • env(viewport-segment-height <x> <y>);
          • env(viewport-segment-top <x> <y>);
          • env(viewport-segment-left <x> <y>);
          • env(viewport-segment-bottom <x> <y>);
          • env(viewport-segment-right <x> <y>);

          x和位置表示由分隔每個視口段的硬件功能創(chuàng)建的y二維網格,坐標0,0從左上段開始。

          當您的設備處于垂直折疊姿勢且視口并排時,左側的視口段將由 表示env(viewport-segment-width 0 0),而右側的視口段將由 表示env(viewport-segment-width 1 0)。如果您將設備轉換為水平折疊姿勢,視口堆疊,頂部將由 表示env(viewport-segment-height 0 0),底部視口由表示env(viewport-segment-height 0 1)

          使用env(viewport-segment-width)andenv(viewport-segment-width)時,除了索引之外,我們還可以設置一個后備值,如下所示:

          env(viewport-segment-width 0 0, 100%);

          但是這個額外的后備值是可選的,由作者自行決定,如果他們想包含它。

          計算鉸鏈寬度

          當您的設備的鉸鏈被硬件功能遮擋時,您可以使用提供的環(huán)境變量來計算它。

          我們可以使用環(huán)境變量計算設備鉸鏈。

          在我們的示例中,我們有一個處于垂直姿勢的設備,并且想要找到鉸鏈寬度,這樣就不會遮擋任何內容。我們將從左顯示器的右視口段中減去右顯示器的左視口段:

          calc(env(viewport-segment-left 1 0) - env(viewport-segment-right 0 0));

          使用 CSSenv()變量 放置內容

          我們可以使用 CSS 環(huán)境變量在顯示區(qū)域邊界內放置內容,如果您想將內容直接放置在鉸鏈或折疊處,這些特別有用。

          在下面的示例中,我們將在左側第一個顯示區(qū)域的鉸鏈上直接放置圖像。該區(qū)域是視口的右側部分,因此我們將使用viewport-segment-right以下代碼放置它:

          img {
            max-width: 400px;
          }
          
          @media (horizontal-viewport-segments: 2) {
            img {
                position: absolute;
                left: env(viewport-segment-right 0 0);
            }
          }

          如果我們在 Surface Duo 模式下在 Edge 開發(fā)人員工具中模擬我們的屏幕,我們將獲得以下布局:

          最初使用環(huán)境變量將圖像放置在我們的布局中會將其放置在錯誤的顯示區(qū)域中。

          這不是我們想要的。圖像應位于左側的顯示區(qū)域中。

          因為圖像是使用屬性絕對定位的left,所以圖像的左邊緣最終與viewport-segment-right顯示區(qū)域對齊。

          然后,我們需要從環(huán)境變量中減去圖像的寬度,以使圖像與正確的鉸鏈邊緣對齊:

          img {
             max-width: 400px;
          }
          
          @media (horizontal-viewport-segments: 2) {
             img {
                 position: absolute;
                 left: calc(env(viewport-segment-right 0 0) - 400px);
            }
          }

          從視口段中減去圖像寬度會將其沿左側顯示中的鉸鏈放置。

          現(xiàn)在我們將圖像放置在我們想要的位置。有關如何沿鉸鏈對齊項目的其他示例,您可以查看這個簡單的盒子演示。打開Edge Developer Tools>Device Emulation然后選擇Surface Duo并確保您Duo emulation處于校正方向姿勢。

          把它們放在一起:讓我們構建一個適應雙屏設備的食譜頁面

          作為一個在做飯時經常使用手機的人,當我在我的雙屏設備上時會適應的食譜網站會非常有幫助。讓我們來看看如何考慮為它調整一個單獨的食譜頁面。

          我想考慮我將如何分塊我的主要內容。通常情況下,我至少會看到食譜標題、制作的份量、烹飪需要多長時間、一張或多張圖片、配料以及制作菜肴的步驟。

          當我畫出我的線框時,我得到以下信息:

          桌面上食譜頁面的標準布局

          我希望我的標題和食譜詳細信息在最頂部,然后是一個占據(jù)整個內容寬度的圖像,然后是成分列表和食譜步驟。我不想堆疊后兩個內容組,因為如果我堆疊它們,成分列表的右側會有很多空白,所以我希望步驟坐在成分旁邊,給我兩列圖片下方。

          用于布局的 CSS 網格或 FLEXBOX?

          我知道我想如何在普通桌面屏幕上布置這個食譜,并且有多種方法可以對這個布局進行編碼和對內容進行分組,但我如何對其進行分組,以及我想在雙屏上實現(xiàn)什么布局在我編碼之前需要考慮設備。根據(jù)我為桌面視圖所做的草圖,我可以使用 flexbox 和 CSS Grid 的組合來實現(xiàn)我想要的布局,我將成分和步驟分組到一個 flex 容器中。但是讓我勾勒一下我希望我的頁面如何在雙屏上顯示。

          垂直折疊位置的可折疊設備上的理想布局通過顯示屏將內容分開,因此不會被鉸鏈遮擋。

          如果我想在布局上有更大的靈活性,那么我不能將我的成分和步驟分組到一個 flex 容器中,否則,無論圖像沒有進入哪一列,都會有很大的空白。

          如果我只在這個布局中使用 flexbox,它會產生一些我想避免亂用的間距。

          添加我們的內容

          我將在桌面和雙屏布局中只使用 CSS Grid。所以,讓我們構建我們的內容。

          <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>

          接下來,讓我們構建頁面的結構。我要定義我的網格:我只想要三列,并且我希望它們是容器的相等部分。

          .recipe {
          display: grid;
          grid-template-columns: repeat(3, 1fr);

          接下來,我將定義我的行,并且我將使用grid-auto-rowswith minmax,這樣我的行是最小的,175px但可以增長到最大內容高度的最大值。

          grid-auto-rows: minmax(175px, max-content);

          然后我將添加更多屬性: my grip-gap、我的最大內容寬度和一個邊距,以使我的布局在頁面上居中。

          grid-gap: 1rem;
          max-width: 64rem;
          margin: 0 auto;
          }

          然后,我將把我的內容放入我定義的網格中:

          .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ù)我的草圖為我提供布局:

          布局在桌面上按預期呈現(xiàn)

          偉大的!但是我的雙屏布局呢?讓我們深入了解我們的horizontal-viewport媒體功能和雙屏網格。

          使用媒體查詢和調整容器布局

          首先,這是我現(xiàn)在在雙屏上的布局:

          在沒有實現(xiàn)任何雙屏代碼的情況下,如果用戶想要將瀏覽器跨過兩個顯示器,那么頁面將是這樣的。

          如果我們向下滾動:

          如果用戶選擇跨越兩個顯示器,則內容會被鉸鏈遮擋。

          不是很好。我們的內容被鉸鏈遮住了,所以讓我開始重新定義我的網格。

          對于我的網格列,我仍將使用三列,但我希望一列占據(jù)左側的第一個視口段,另外兩列占據(jù)右側視口段,因此我將使用我的 CSS環(huán)境變量env(viewport-segment-width 0 0)告訴瀏覽器,對于我的第一列,我希望它占據(jù)第一個顯示區(qū)域的整個視口。

          @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);
          }
          
          }

          對于我的行,我希望在放置上更靈活一點,所以我將重復兩行175px,這是關于帶有配方標題、產量和時間信息的容器的高度,之后的行應該匹配我最初在網格中定義的內容。

          如果我在 DevTools 中檢查我的設計,我可以看到我在配方容器上設置的widthmargin最初將我想要與我的視口段對齊的網格線推到正確的視口段中。

          添加我的代碼后,我的內容不再被遮擋,但仍需要一些間距調整。

          要重置它,我將重置我的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%;
          }
          
          }

          重置我的邊距和填充會掩蓋右側顯示中的內容。

          現(xiàn)在我要把我的內容放在網格中并調整我的布局。

          .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;
          }

          我已經對內容應用了填充,除了我決定要跨越整個視口的圖像。對于圖像下方的內容,由于從物理鉸鏈下方開始的網格線的性質,我想添加額外的填充,因此它看起來左側的填充與其他帶有填充的項目相同。如果我不添加額外的,它會落得太靠近鉸鏈。因為我已經有一個 grid-gap1rem并且我想將 padding 加倍,所以我將添加3rem而不是4rem為我們提供雙屏設備上的最終布局:

          我可以重新添加尺寸更合適的填充來顯示內容,因此它不會在帶有物理鉸鏈的設備上被遮擋。

          只需對我們的 CSS 進行一些小的調整并使用其中一項新的媒體功能,我們就有了一個適應雙屏設備的布局。要查看體驗,請前往此處的 Edge 演示站點或基于 Chromium 的瀏覽器,然后打開瀏覽器開發(fā)人員工具以查看 Surface Duo 仿真。如果您在 Chrome 中打開該站點,請確保在 下啟用了實驗性網絡平臺功能標志chrome://flags,以便演示正確顯示。

          單屏響應式設計細節(jié)

          為了確保我們考慮到小型單屏設備,我為手機布局選擇的代碼使用了 flexbox 并將所有內容放在一個列中:

          @media (max-width: 48rem) {
          
             body {
                 font: 1.3em/1.8 base, 'Playfair Display', serif;
            }
          
             .recipe-details {
                 display: flex;
                 flex-direction: column;
            }
          
          }

          API 瀏覽器可用性和無設備測試

          默認情況下,這些雙屏 API 在 Microsoft Edge 和 Android 上的 Edge 中可用,從版本 97 開始。這些計劃很快就會出現(xiàn)在其他 Chromium 瀏覽器中,但具體時間尚未確定。要在 Chrome 中啟用這些 API,請轉到chrome://flags并啟用實驗性網絡平臺功能。

          雖然這些是相對較新的設備,但許多現(xiàn)在已經進入第二代和第三代,因此公司正在投資它們。如果您無法使用物理設備,最好的測試方法是使用瀏覽器開發(fā)工具。我已經在仿真工具和 Surface Duo 上測試了我的網站,Duo 的仿真工具似乎是相同的。我的設計在設備上的外觀與在 DevTools 中的外觀相同。它使構建和設計雙屏設備就像開發(fā)桌面和單屏移動設備一樣容易。

          如果您使用的是不支持這些 API 的桌面或設備,則可以為 Visual Viewport Segments 屬性提供一個 polyfill。CSS 媒體查詢沒有 API。目前,市場上的雙屏設備都是基于安卓的,這些API計劃在安卓上可用的基于Chromium的瀏覽器中。

          如果可折疊設備上的瀏覽器不支持這些功能,您可以使用 polyfill 或確保您的網站在小單屏上仍能很好地呈現(xiàn),因為用戶可以靈活選擇如何在雙屏上顯示網站屏幕設備。他們可以跨兩個顯示器跨越一個網站,或者他們可以選擇讓它跨一個顯示器,如果他們選擇后者,它將像在平板電腦或手機上一樣顯示。即使您的網站沒有雙屏實現(xiàn),用戶仍然可以選擇單顯示視圖。雙屏 API 提供了一種方法來逐步增強擁有設備的用戶的體驗。

          結束

          雙屏設備只是響應式設計的下一個發(fā)展方向。如果您有 PWA 或網站,可用的 API 可以無縫集成到您現(xiàn)有的代碼庫中。還有其他方法可以為雙屏設備構建應用程序,您可以在Surface Duo 文檔https://docs.microsoft.com/en-us/dual-screen/中查看這些方法。這是在網絡上進行布局的激動人心的時刻,雙屏提供了獲得更多創(chuàng)意的機會。

          為幫助到一部分同學不走彎路,真正達到一線互聯(lián)網大廠前端項目研發(fā)要求,首次實力寵粉,打造了《30天挑戰(zhàn)學習計劃》,內容如下:

          HTML/HTML5,CSS/CSS3,JavaScript,真實企業(yè)項目開發(fā),云服務器部署上線,從入門到精通

          • PC端項目開發(fā)(1個)
          • 移動WebApp開發(fā)(2個)
          • 多端響應式開發(fā)(1個)

          共4大完整的項目開發(fā) !一行一行代碼帶領實踐開發(fā),實際企業(yè)開發(fā)怎么做我們就是怎么做。從學習一開始就進入工作狀態(tài),省得浪費時間。

          從學習一開始就同步使用 Git 進行項目代碼的版本的管理,Markdown 記錄學習筆記,包括真實大廠項目的開發(fā)標準和設計規(guī)范,命名規(guī)范,項目代碼規(guī)范,SEO優(yōu)化規(guī)范

          從藍湖UI設計稿 到 PC端,移動端,多端響應式開發(fā)項目開發(fā)

          • 真機調試,云服務部署上線;
          • Linux環(huán)境下 的 Nginx 部署,Nginx 性能優(yōu)化;
          • Gzip 壓縮,HTTPS 加密協(xié)議,域名服務器備案,解析;
          • 企業(yè)項目域名跳轉的終極解決方案,多網站、多系統(tǒng)部署;
          • 使用 使用 Git 在線項目部署;

          這些內容在《30天挑戰(zhàn)學習計劃》中每一個細節(jié)都有講到,包含視頻+圖文教程+項目資料素材等。只為實力寵粉,真正一次掌握企業(yè)項目開發(fā)必備技能,不走彎路 !

          過程中【不涉及】任何費用和利益,非誠勿擾 。

          如果你沒有添加助理老師微信,可以添加下方微信,說明要參加30天挑戰(zhàn)學習計劃,來自!老師會邀請你進入學習,并給你發(fā)放相關資料。

          30 天挑戰(zhàn)學習計劃 Web 前端從入門到實戰(zhàn) | arry老師的博客-艾編程

          這篇文章中,我將分享21個HTML技巧,包括代碼片段,可以提升你的編碼技能。

          讓我們立即開始吧。

          (本文視頻講解:java567.com)


          主站蜘蛛池模板: 亚洲综合国产一区二区三区| 国产一区二区免费在线| 日韩免费无码一区二区视频| 国产在线精品一区二区不卡| 精品人妻无码一区二区三区蜜桃一| 国模丽丽啪啪一区二区| 日本一区二区三区日本免费| 伊人久久精品无码麻豆一区 | 国产SUV精品一区二区88L| 亚洲一区无码中文字幕乱码| 国产一区视频在线| 一夲道无码人妻精品一区二区| 国产成人精品a视频一区| 日本一区二区三区免费高清在线| 国模极品一区二区三区| 国产精品美女一区二区三区 | 无码国产精品一区二区免费I6| 亚拍精品一区二区三区| 亚洲一区免费在线观看| 国产福利一区二区三区在线观看| 亚洲国产精品第一区二区| 熟女精品视频一区二区三区| 无码人妻AV免费一区二区三区| 亚洲国产一区在线| 精品深夜AV无码一区二区老年| 51视频国产精品一区二区| 亚洲av乱码一区二区三区按摩| 精产国品一区二区三产区| 亚洲av成人一区二区三区观看在线 | 人妻无码第一区二区三区| 人妻少妇久久中文字幕一区二区| 亚洲福利视频一区| 国产精品无码一区二区在线观| 国产精品免费视频一区| 国产色精品vr一区区三区| 麻豆精品久久久一区二区| 国产精品制服丝袜一区| 日韩精品一区二区三区在线观看l 日韩精品一区二区三区毛片 | 国产一区二区三区日韩精品| 国产精品污WWW一区二区三区| 国产一区二区三区精品视频 |