整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

深入学习下现代网页开发中的10种渲染模式

载说明:原创不易,未经授权,谢绝任何形式的转载

如何使您的网站呈现最佳状态?这个问题有很多答案,本文介绍了当前框架中应用最广泛的十种渲染设计模式,让您能够选择最适合您的方式。

近年来,网络开发的迅速演变,尤其是在前端开发领域。这种转变主要归功于无数涌现的框架和技术,它们旨在简化和增强构建引人入胜的用户界面的过程。然而,由于现有框架的丰富多样以及不断涌现的新框架,跟上前端趋势已成为一项艰巨的任务。对于新手来说,很容易感到不知所措,仿佛迷失在广阔的选择海洋中。

渲染是前端开发的核心挑战,它将数据和代码转化为可见且可交互的用户界面。虽然大多数框架以类似的方式应对这一挑战,通常比之前的方法更简洁,但也有一些框架选择了全新的解决方案。在本文中,我们将研究流行框架中使用的十种常见渲染模式,通过这样做,无论是初学者还是专家都将获得对新旧框架的扎实基础理解,同时也能对解决应用程序中的渲染问题有新的见解。

在本文的结尾,您将会:

  • 对于当今网页开发中最常见的渲染模式有基本的了解
  • 了解不同渲染模式的优势和劣势
  • 了解在你的下一个大项目中使用哪种渲染模式和框架

什么是UI渲染模式?

在前端开发的背景下,渲染是将数据和代码转换为对最终用户可见的HTML。UI渲染模式是指实现渲染过程可以采用的各种方法。这些模式概述了不同的策略,用于描述转换发生的方式以及呈现出的用户界面。正如我们很快会发现的那样,根据所实现的模式,渲染可以在服务器上或浏览器中进行,可以部分或一次性完成。

选择正确的渲染模式对开发人员来说至关重要,因为它直接影响到Web应用程序的性能、成本、速度、可扩展性、用户体验,甚至开发人员的体验。

在本文中,我们将介绍下面列出的前十种渲染模式:

  • 1、静态网站(Static Site)
  • 2、多页面应用(Multi-Page Applications(MPA))
  • 3、单页应用程序(Single Page Applications (with Client Side Rendering CSR))
  • 4、服务器端渲染(erver Side Rendering (SSR))
  • 5、静态网站生成(Static Site Generation (SSG))
  • 6、增量静态生成(Incremental Static Generation (ISG))
  • 7、部分水合(Partial Hydration)
  • 8、Island Architectur
  • 9、Resumability
  • 10、 SSR

在每个案例中,我们将研究渲染模式的概念、优点和缺点、使用案例、相关的框架,并提供一个简单的代码示例来阐明观点。

代码示例

  • 第一页将显示可用的货币类型
  • 第二页将显示从Coingecko API获取的特定币种在不同交易所的价格。
  • 第二页还将提供深色和浅色模式。
  • 各种框架的实施可能会有轻微的差异。

所有示例的全局CSS如下

/* style.css or the name of the global stylesheet */
h1,
h2 {
 color: purple;
 margin: 1rem;
}

a {
 color: var(--text-color);
 display: block;
 margin: 2rem 0;
}

body {
 font-family: Arial, sans-serif;
 background-color: var(--background-color);
 color: var(--text-color);
}

.dark-mode {
 --background-color: #333;
 --text-color: #fff;
}

.light-mode {
 --background-color: #fff;
 --text-color: #333;
}
.toggle-btn{
   background-color: yellow;
   padding: 0.3rem;
   margin: 1rem;
   margin-top: 100%;
   border-radius: 5px;
}

静态网站

静态网站是最原始、最基本、最直接的UI渲染方法。它通过简单地编写HTML、CSS和JavaScript来创建网站。一旦代码准备好,它会被上传为静态文件到托管服务(如Netlify),并指向一个域名。通过URL请求时,静态文件会直接提供给用户,无需服务器端处理。静态网站渲染非常适合没有交互性和动态内容的静态网站,比如落地页和文档网站。

优点

  • 非常简单
  • 快速
  • 廉价(无服务器)
  • SEO友好

缺点

  • 不适用于数据频繁变动的情况(动态数据)
  • 不适用于互动应用程序
  • 没有直接的数据库连接
  • 当数据发生变化时,需要手动更新和重新上传

相关框架

  • Hugo
  • Jekyll
  • HTML/CSS/纯JavaScript(无框架)

Demo (HTML/CSS/JavaScript)

<!-- index.html -->
<!DOCTYPE html>
<html>
 <head>
   <title>Cryptocurrency Price App</title>
   <link rel="stylesheet" href="style.css" />
 </head>

 <body>
   <h1>Cryptocurrency Price App</h1>
   <ol>
     <li><a href="./btcPrice.html">Bitcoin </a></li>
     <li><a href="./ethPrice.html">Ethereum </a></li>
     <li><a href="./xrpPrice.html">Ripple </a></li>
     <li><a href="./adaPrice.html">Cardano </a></li>
   </ol>
</body>
</html>

<!-- btcPrice.html -->
<!DOCTYPE html>
<html lang="en">
 <head>
   <meta charset="UTF-8" />
   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
   <title>Document</title>
   <link rel="stylesheet" href="style.css" />
 </head>
 <body>
   <h2>BTC</h2>
   <ul>
     <li id="binance">Binance:</li>
     <li id="kucoin">Kucoin:</li>
     <li id="bitfinex">Bitfinex:</li>
     <li id="crypto_com">Crypto.com:</li>
   </ul>
   <script src="fetchPrices.js"></script>
   <button class="toggle-btn">Toggle Mode</button>
   <script src="darkMode.js"></script>
 </body>
</html>
//fetchPrices.js
const binance = document.querySelector("#binance");
const kucoin = document.querySelector("#kucoin");
const bitfinex = document.querySelector("#bitfinex");
const crypto_com = document.querySelector("#crypto_com");

// Get the cryptocurrency prices from an API
let marketPrices = { binance: [], kucoin: [], bitfinex: [], crypto_com: [] };

async function getCurrentPrice(market) {
 if (
   `${market}` === "binance" ||
   `${market}` === "kucoin" ||
   `${market}` === "crypto_com" ||
   `${market}` === "bitfinex"
 ) {
   marketPrices[market] = [];
   const res = await fetch(
     `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
   );
   if (res) {
     let data = await res.json();
     if (data) {
       for (const info of data.tickers) {
         if (info.target === "USDT") {
           let name = info.base;
           let price = info.last;
           if (`${market}` === "binance") {
             marketPrices.binance = [
               ...marketPrices.binance,
               { [name]: price },
             ];
           }
           if (`${market}` === "kucoin") {
             marketPrices.kucoin = [...marketPrices.kucoin, { [name]: price }];
           }
           if (`${market}` === "bitfinex") {
             marketPrices.bitfinex = [
               ...marketPrices.bitfinex,
               { [name]: price },
             ];
           }
           if (`${market}` === "crypto_com") {
             marketPrices.crypto_com = [
               ...marketPrices.crypto_com,
               { [name]: price },
             ];
           }
         }
       }
     }
   }
 }
}

async function findPrices() {
 try {
   const fetched = await Promise.all([
     getCurrentPrice("binance"),
     getCurrentPrice("kucoin"),
     getCurrentPrice("bitfinex"),
     getCurrentPrice("crypto_com"),
   ]);
   if (fetched) {
     binance ? (binance.innerHTML += `${marketPrices.binance[0].BTC}`) : null;
     kucoin ? (kucoin.innerHTML += `${marketPrices.kucoin[0].BTC}`) : null;
     bitfinex
       ? (bitfinex.innerHTML += `${marketPrices.bitfinex[0].BTC}`)
       : null;
     crypto_com
       ? (crypto_com.innerHTML += `${marketPrices.crypto_com[0].BTC}`)
       : null;
   }
 } catch (e) {
   console.log(e);
 }
}

findPrices();

//darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");

document.addEventListener("DOMContentLoaded", () => {
 const preferredMode = localStorage.getItem("mode");
 if (preferredMode === "dark") {
   document.body.classList.add("dark-mode");
 } else if (preferredMode === "light") {
   document.body.classList.add("light-mode");
 }
});
// Check the user's preferred mode on page load (optional)

function toggleMode() {
 const body = document.body;
 body.classList.toggle("dark-mode");
 body.classList.toggle("light-mode");

 // Save the user's preference in localStorage (optional)
 const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
 localStorage.setItem("mode", currentMode);
}

toggleBtn.addEventListener("click", () => {
 toggleMode();
});

上面的代码块展示了我们使用HTML/CSS/JavaScript实现的应用程序。下面是应用程序。

第一页:显示所有可用的虚拟币

第2页:从Coingecko API获取的不同交易所的BTC价格。

请注意,在使用静态网站时,每个币种的价格页面必须手动编写。

多页面应用程序(MPAs)

这种渲染模式是为了处理我们网站上的动态数据而出现的解决方案,并导致了今天许多最大、最受欢迎的动态Web应用程序的创建。在MPA中,渲染由服务器完成,服务器会重新加载以基于当前底层数据(通常来自数据库)生成新的HTML,以响应浏览器发出的每个请求。这意味着网站可以根据底层数据的变化而改变。最常见的用例是电子商务网站、企业应用程序和新闻公司博客。

优点

  • 简单直接
  • 处理动态数据非常出色
  • SEO友好
  • 良好的开发者体验
  • 高度可扩展的

缺点

  • 适度支持用户界面的交互性
  • 由于多次重新加载而导致用户体验差
  • 昂贵的(需要服务器)

相关框架

  • Express 和 EJS (node.js)
  • Flask (Python)
  • Spring boot (java)

Demo (ExpressandEJS)

npm i express and ejs
<!-- views/index.ejs -->
<!-- css file should be in public folder-->
<!DOCTYPE html>
<html>
 <head>
   <title>Cryptocurrency Price App</title>
   <link rel="stylesheet" href="style.css">
 </head>
 <body>
   <h1>Cryptocurrency Price App</h1>
   <ol>
     <li><a href="./price/btc">Bitcoin </a></li>
     <li><a href="./price/eth">Ethereum </a></li>
     <li><a href="./price/xrp">Ripple </a></li>
     <li><a href="./price/ada">Cardano </a></li>
   </ol>
 </body>
</html>

<!-- views/price.ejs -->
<!DOCTYPE html>
<html lang="en">
 <head>
   <title>Cryptocurrency Price App</title>
   <link rel="stylesheet" href="/style.css" />
 </head>
 <body>
   <h2><%- ID %></h2>
   <ul>
     <li id="binance">Binance:<%- allPrices.binance[0][ID] %></li>
     <li id="kucoin">Kucoin:<%- allPrices.kucoin[0][ID] %></li>
     <li id="bitfinex">Bitfinex:<%- allPrices.bitfinex[0][ID] %></li>
     <li id="crypto_com">Crypto.com:<%- allPrices.crypto_com[0][ID] %></li>
   </ul>

   <button class="toggle-btn">Toggle Mode</button>
   <script src="/darkMode.js"></script>

 </body>
</html>
// public/darkMode.js
const toggleBtn = document.querySelector(".toggle-btn");
document.addEventListener("DOMContentLoaded", () => {
 const preferredMode = localStorage.getItem("mode");
 if (preferredMode === "dark") {
   document.body.classList.add("dark-mode");
 } else if (preferredMode === "light") {
   document.body.classList.add("light-mode");
 }
});

// Check the user's preferred mode on page load (optional)
function toggleMode() {
 const body = document.body;
 body.classList.toggle("dark-mode");
 body.classList.toggle("light-mode");

 // Save the user's preference in localStorage (optional)
 const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
 localStorage.setItem("mode", currentMode);
}

toggleBtn.addEventListener("click", () => {
 toggleMode();
});

// utils/fetchPrices.js
async function getCurrentPrice(market) {
 let prices = [];
 if (
   `${market}` === "binance" ||
   `${market}` === "kucoin" ||
   `${market}` === "crypto_com" ||
   `${market}` === "bitfinex"
 ) {
   const res = await fetch(
     `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=bitcoin%2Cripple%2Cethereum%2Ccardano`
   );
   const data = await res.json();

   for (const info of data.tickers) {
     if (info.target === "USDT") {
       let name = info.base;
       let price = info.last;
       prices.push({ [name]: price });
     }
   }

   return prices;
 }
}

module.exports = getCurrentPrice;

//app.js. 
const getCurrentPrice = require("./utils/fetchPrices");
const express = require("express");
const ejs = require("ejs");
const path = require("path");

const app = express();

app.set("view engine", "ejs");
app.set("views", path.join(__dirname, "views"));
app.use(express.static("public"));
app.get("/", (req, res) => {
 res.render("index");
});

app.get("/price/:id", async (req, res) => {
 let { id } = req.params;
 let ID = id.toUpperCase();
 let allPrices;
 try {
   const fetched = await Promise.all([
     getCurrentPrice("binance"),
     getCurrentPrice("kucoin"),
     getCurrentPrice("bitfinex"),
     getCurrentPrice("crypto_com"),
   ]);
   if (fetched) {
     allPrices = {};
     allPrices.binance = fetched[0];
     allPrices.kucoin = fetched[1];
     allPrices.bitfinex = fetched[2];
     allPrices.crypto_com = fetched[3];
     console.log(allPrices);
     res.render("price", { ID, allPrices });
   }
 } catch (e) {
   res.send("server error");
 }
});

app.listen(3005, () => console.log("Server is running on port 3005"));

注意:在这里,每个页面都将由服务器自动生成,不同于静态网站,静态网站需要手动编写每个文件。

单页应用程序(SPA)

单页应用程序(SPA)是2010年代创建高度交互式Web应用程序的解决方案,至今仍在使用。在这里,SPA通过从服务器获取HTML外壳(空白HTML页面)和JavaScript捆绑包来处理渲染到浏览器。在浏览器中,它将控制权(水合)交给JavaScript,动态地将内容注入(渲染)到外壳中。在这种情况下,渲染是在客户端(CSR)上执行的。使用JavaScript,这些SPA能够在不需要完整页面重新加载的情况下对单个页面上的内容进行大量操作。它们还通过操作URL栏来创建多个页面的幻觉,以指示加载到外壳上的每个资源。常见的用例包括项目管理系统、协作平台、社交媒体Web应用、交互式仪表板或文档编辑器,这些应用程序受益于SPA的响应性和交互性。

优点

  • 高度互动
  • 在浏览多个页面时,用户体验无缝衔接
  • 手机友好

缺点

  • 由于JavaScript捆绑包过大,加载时间较慢
  • SEO能力差
  • 由于客户端上的代码执行,存在高安全风险
  • 可扩展性差

相关框架

  • React
  • Angular
  • Vue

Demo (ReactandReact-router)

// pages/index.jsx
import { Link } from "react-router-dom";
export default function Index() {
 return (
   <div>
     <h1>Cryptocurrency Price App</h1>
     <ol>
       <li>
         <Link to="./price/btc">Bitcoin </Link>
       </li>
       <li>
         <Link to="./price/eth">Ethereum </Link>
       </li>
       <li>
         <Link to="./price/xrp">Ripple </Link>
       </li>
       <li>
         <Link to="./price/ada">Cardano </Link>
       </li>
     </ol>
   </div>
 );
}

//pages/price.jsx
import { useParams } from "react-router-dom";
import { useEffect, useState, useRef, Suspense } from "react";
import Btn from "../components/Btn";

export default function Price() {
 const { id } = useParams();
 const ID = id.toUpperCase();
 const [marketPrices, setMarketPrices] = useState({});
 const [isLoading, setIsLoading] = useState(true);
 const containerRef = useRef(null);

 function fetchMode() {
   const preferredMode = localStorage.getItem("mode");
   if (preferredMode === "dark") {
     containerRef.current.classList.add("dark-mode");
   } else if (preferredMode === "light") {
     containerRef.current.classList.add("light-mode");
   }
 }

 useEffect(() => {
   fetchMode();
 }, []);

 async function getCurrentPrice(market) {
   const res = await fetch(
     `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
   );
   const data = await res.json();
   const prices = [];
   for (const info of data.tickers) {
     if (info.target === "USDT") {
       const name = info.base;
       const price = info.last;
       prices.push({ [name]: price });
     }
   }
   return prices;
 }

 useEffect(() => {
   async function fetchMarketPrices() {
     try {
       const prices = await Promise.all([
         getCurrentPrice("binance"),
         getCurrentPrice("kucoin"),
         getCurrentPrice("bitfinex"),
         getCurrentPrice("crypto_com"),
       ]);
       const allPrices = {
         binance: prices[0],
         kucoin: prices[1],
         bitfinex: prices[2],
         crypto_com: prices[3],
       };
       setMarketPrices(allPrices);
       setIsLoading(false);
       console.log(allPrices); // Log the fetched prices to the console
     } catch (error) {
       console.log(error);
       setIsLoading(false);
     }
   }

   fetchMarketPrices();
 }, []);

 return (
   <div className="container" ref={containerRef}>
     <h2>{ID}</h2>
     {isLoading ? (
       <p>Loading...</p>
     ) : Object.keys(marketPrices).length > 0 ? (
       <ul>
         {Object.keys(marketPrices).map((exchange) => (
           <li key={exchange}>
             {exchange}: {marketPrices[exchange][0][ID]}
           </li>
         ))}
       </ul>
     ) : (
       <p>No data available.</p>
     )}
     <Btn container={containerRef} />
   </div>
 );
}

//components/Btn.jsx
export default function Btn({ container }) {
 function toggleMode() {
   container.current.classList.toggle("dark-mode");
   container.current.classList.toggle("light-mode");
   // Save the user's preference in localStorage (optional)
   const currentMode = container.current.classList.contains("dark-mode")
     ? "dark"
     : "light";
   localStorage.setItem("mode", currentMode);
 }
 // Check the user's preferred mode on page load (optional)
 return (
   <div>
     <button
       className="toggle-btn"
       onClick={() => {
         toggleMode();
       }}
     >
       Toggle Mode
     </button>
   </div>
 );
}

// App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Index from "./pages";
import Price from "./pages/Price";

const router = createBrowserRouter([
 {
   path: "/",
   element: <Index />,
 },
 {
   path: "/price/:id",
   element: <Price />,
 },
]);

function App() {
 return (
   <>
     <RouterProvider router={router}></RouterProvider>
   </>
 );
}

export default App;

静态网站生成(SSG)

静态网站生成(SSG)是一种利用构建网站的原始静态网站模式的渲染模式。在构建过程中,从源代码中预先构建和渲染了所有可能的网页,生成静态HTML文件,然后将其存储在存储桶中,就像在典型静态网站的情况下原始上传静态文件一样。对于基于源代码可能存在的任何路由的请求,将向客户端提供相应的预构建静态页面。因此,与SSR或SPA不同,SSG不依赖于服务器端渲染或客户端JavaScript来动态渲染内容。相反,内容是提前生成的,并且可以被缓存和高性能地传递给用户。这适用于中度交互的网站,其数据不经常更改,例如作品集网站、小型博客或文档网站。

优点

  • SEO友好
  • 快速加载页面
  • 高性能
  • 提高安全性(由于代码既不在客户端上运行也不在服务器上运行)

缺点

  • 有限互动
  • 数据更改后需要重新构建和重新上传

相关框架

  • Nextjs (默认情况下)
  • Gatsby
  • Hugo
  • Jekyll

Demo (Nextjs)

// components/Btn.js
export default function Btn({ container }) {
 function toggleMode() {
   container.current.classList.toggle("dark-mode");
   container.current.classList.toggle("light-mode");

   // Save the user's preference in localStorage (optional)
   const currentMode = container.current.classList.contains("dark-mode") ? "dark" : "light";
   localStorage.setItem("mode", currentMode);
 }

 // Check the user's preferred mode on page load (optional)

 return (
   <div>
     <button className="toggle-btn" onClick={() => {toggleMode()}}>
       Toggle Mode
     </button>
   </div>
 );
}

// components/Client.js
"use client";
import { useEffect, useRef } from "react";
import Btn from "@/app/components/Btn";
import { usePathname } from "next/navigation";

export default function ClientPage({ allPrices }) {
 const pathname = usePathname();
 let ID = pathname.slice(-3).toUpperCase();

 const containerRef = useRef(null);

 function fetchMode() {
   const preferredMode = localStorage.getItem("mode");
   if (preferredMode === "dark") {
     containerRef.current.classList.add("dark-mode");
   } else if (preferredMode === "light") {
     containerRef.current.classList.add("light-mode");
   }
 }

 useEffect(() => {
   fetchMode();
 }, []);

 return (
   <div className="container" ref={containerRef}>
     <h2>{ID}</h2>
     {Object.keys(allPrices).length > 0 ? (
       <ul>
         {Object.keys(allPrices).map((exchange) => (
           <li key={exchange}>
             {exchange}: {allPrices[exchange][0][ID]}
           </li>
         ))}
       </ul>
     ) : (
       <p>No data available.</p>
     )}
     <Btn container={containerRef} />
   </div>
 );
}


//price/[id]/page.js
import ClientPage from "../../components/Client";

async function getCurrentPrice(market) {
 const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
 );
 console.log("fetched");
 const data = await res.json();
 const prices = [];
 for (const info of data.tickers) {
   if (info.target === "USDT") {
     const name = info.base;
     const price = info.last;
     prices.push({ [name]: price });
   }
 }
 return prices;
}

export default async function Price() {
 async function fetchMarketPrices() {
   try {
     const prices = await Promise.all([
       getCurrentPrice("binance"),
       getCurrentPrice("kucoin"),
       getCurrentPrice("bitfinex"),
       getCurrentPrice("crypto_com"),
     ]);
     const allPrices = {
       binance: prices[0],
       kucoin: prices[1],
       bitfinex: prices[2],
       crypto_com: prices[3],
     };

     return allPrices;
     // Log the fetched prices to the console
   } catch (error) {
     console.log(error);
   }
 }

 const allPrices = await fetchMarketPrices();

 return (
   <div>
     {allPrices && Object.keys(allPrices).length > 0 ? (
       <ClientPage allPrices={allPrices} />
     ) : (
       <p>No data available.</p>
     )}
   </div>
 );
}

//page.js
import Link from "next/link";
export default function Index() {
 return (
   <div>
     <h1>Cryptocurrency Price App</h1>
     <ol>
       <li>
         <Link href="./price/btc">Bitcoin </Link>
       </li>
       <li>
         <Link href="./price/eth">Ethereum </Link>
       </li>
       <li>
         <Link href="./price/xrp">Ripple </Link>
       </li>
       <li>
         <Link href="./price/ada">Cardano </Link>
       </li>
     </ol>
   </div>
 );
}

服务器端渲染(SSR)

服务器端渲染(SSR)是一种渲染模式,它结合了多页面应用(MPA)和单页面应用(SPA)的能力,以克服两者的局限性。在这种模式下,服务器生成网页的HTML内容,填充动态数据,并将其发送给客户端进行显示。在浏览器上,JavaScript可以接管已经渲染的页面,为页面上的组件添加交互性,就像在SPA中一样。SSR在将完整的HTML交付给浏览器之前,在服务器上处理渲染过程,而SPA完全依赖于客户端JavaScript进行渲染。SSR特别适用于注重SEO、内容传递或具有特定可访问性要求的应用,如企业网站、新闻网站和电子商务网站。

优点

  • 适度互动
  • SEO友好
  • 快速加载时间
  • 对动态数据的良好支持

缺点

  • 复杂的实施
  • 成本(需要服务器)

相关框架

  • Next.js
  • Nuxt.js

Demo (Nextjs)

在NEXT.js上实现SSR的代码与SSG演示几乎相同。这里,唯一的变化在于 getCurrentPrice 函数。使用带有 no-cache 选项的fetch API,页面将不会被缓存;相反,服务器将需要在每个请求上创建一个新页面。

//price/[id]/page.js
async function getCurrentPrice(market) 
 const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
   { cache: "no-store" }
 );
 console.log("fetched");
 const data = await res.json();
 const prices = [];
 for (const info of data.tickers) {
   if (info.target === "USDT") {
     const name = info.base;
     const price = info.last;
     prices.push({ [name]: price });
   }
 }
 return prices;
}

增量静态生成(ISG)

增量静态生成是一种生成静态网站的方法,它结合了静态网站生成的优点,能够更新和重新生成网站的特定页面或部分,而无需重建整个网站。增量静态生成允许自动增量更新,从而减少了重建整个应用程序所需的时间,并通过仅在必要时从服务器请求新数据,更有效地利用服务器资源。这对于国际多语言网站、企业网站和发布平台网站非常实用。

优点

  • 静态网站的实时自动更新支持
  • 性价比高
  • SEO友好
  • 良好的性能和可扩展性

缺点

  • 实施中的复杂性
  • 不适用于高度动态的数据应用

相关框架

  • Next.js
  • Nuxt.js

Demo (Nextjs)

在NEXT.js上实现ISR的代码与SSG演示几乎相同。唯一的变化在于 getCurrentPrice 函数。使用fetch API并使用指定条件的选项从服务器获取数据,当满足我们定义的条件时,页面将自动更新。在这里,我们说底层数据应该每60秒进行验证,并且UI应该根据数据中的任何变化进行更新。

//price/[id]/page.js
async function getCurrentPrice(market) 
 const res = await fetch( `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`,
  { next: { revalidate: 60 } }
 );
 console.log("fetched");
 const data = await res.json();
 const prices = [];
 for (const info of data.tickers) {
   if (info.target === "USDT") {
     const name = info.base;
     const price = info.last;
     prices.push({ [name]: price });
   }
 }
 return prices;
}

部分水合

部分水合是客户端渲染(CSR)框架中用于解决加载时间缓慢问题的一种技术。使用这种技术,CSR框架将选择性地首先渲染和水合具有交互性的网页的最重要部分,而不是整个页面。最终,当满足特定条件时,较不重要的交互组件可以通过水合来实现其交互性。通过优先处理关键或可见组件的水合,而推迟处理非关键或在折叠区域下的组件的水合,它可以更有效地利用资源,并通过优先处理关键或可见组件的水合来加快初始页面渲染速度。部分水合可以使任何具有多个交互组件的复杂CSR或SPA受益。

优点

  • 由于减少了初始的JavaScript捆绑包,加载时间更快
  • 性能提升了
  • 优化的搜索引擎优化
  • 资源效率

缺点

  • 增加的复杂性和代码
  • 不一致的用户界面可能性

相关框架

  • React
  • Vue

Demo (React)

//pages/price.jsx
import { useParams } from "react-router-dom";
import React, { useEffect, useState, useRef, Suspense } from "react";
const Btn = React.lazy(() => import("../components/Btn"));
import getCurrentPrice from "../utils/fetchPrices";

export default function Price() {
 const { id } = useParams();
 const ID = id.toUpperCase();
 const [marketPrices, setMarketPrices] = useState({});
 const [isLoading, setIsLoading] = useState(true);
 const containerRef = useRef(null);

 // Wrapper component to observe if it's in the viewport
 const [inViewport, setInViewport] = useState(false);

 useEffect(() => {
   const observer = new IntersectionObserver((entries) => {
     const [entry] = entries;
     setInViewport(entry.isIntersecting);
   });

   if (containerRef.current) {
     observer.observe(containerRef.current);
   }

   return () => {
     if (containerRef.current) {
       observer.unobserve(containerRef.current);
     }
   };
 }, []);

 function fetchMode() {
   const preferredMode = localStorage.getItem("mode");
   if (preferredMode === "dark") {
     containerRef.current.classList.add("dark-mode");
   } else if (preferredMode === "light") {
     containerRef.current.classList.add("light-mode");
   }
 }

 useEffect(() => {
   fetchMode();
 }, []);

 useEffect(() => {
   async function fetchMarketPrices() {
     try {
       const prices = await Promise.all([
         getCurrentPrice("binance"),
         getCurrentPrice("kucoin"),
         getCurrentPrice("bitfinex"),
         getCurrentPrice("crypto_com"),
       ]);
       const allPrices = {
         binance: prices[0],
         kucoin: prices[1],
         bitfinex: prices[2],
         crypto_com: prices[3],
       };
       setMarketPrices(allPrices);
       setIsLoading(false);
       console.log(allPrices); // Log the fetched prices to the console
     } catch (error) {
       console.log(error);
       setIsLoading(false);
     }
   }

   fetchMarketPrices();
 }, []);

 return (
   <div className="container" ref={containerRef}>
     <h2>{ID}</h2>
     {isLoading ? (
       <p>Loading...</p>
     ) : Object.keys(marketPrices).length > 0 ? (
       <ul>
         {Object.keys(marketPrices).map((exchange) => (
           <li key={exchange}>
             {exchange}: {marketPrices[exchange][0][ID]}
           </li>
         ))}
       </ul>
     ) : (
       <p>No data available.</p>
     )}
     {inViewport ? (
       // Render the interactive component only when it's in the viewport
       <React.Suspense fallback={<div>Loading...</div>}>
         <Btn container={containerRef} />
       </React.Suspense>
     ) : (
       // Render a placeholder or non-interactive version when not in the viewport
       <div>Scroll down to see the interactive component!</div>
     )}
   </div>
 );
}

在上面的演示中,我们代码的交互组件 Btn 位于页面底部,只有当它进入视口时才会被激活。

Island Architecture(Astro)

岛屿架构是Astro框架开发者倡导的一种有前途的UI渲染模式。Web应用程序在服务器上被划分为多个独立的小组件,称为岛屿。每个岛屿负责渲染应用程序UI的特定部分,并且它们可以独立地进行渲染。在服务器上被划分为岛屿后,这些多个岛屿包被发送到浏览器,框架使用一种非常强大的部分加载形式,只有带有交互部分的组件由JavaScript接管并启用其交互性,而其他非交互式组件保持静态。最常见的用例是构建内容丰富的网站。Astro是构建专注于内容的网站的不错选择,例如博客、作品集和文档网站。Astro的岛屿架构模式可以帮助提高这些网站的性能,尤其是对于网络连接较慢的用户来说。

优点

  • 性能(当今最快的框架之一)
  • 更小的捆绑尺寸
  • 易学易懂,易于维护
  • 良好的SEO表现
  • 良好的开发者体验

缺点

  • 有限互动
  • 由于组件数量极多,导致调试困难

相关框架

  • Astro

Demo (Astro)

---
// components/Btn.astro
---

<div>
 <button class="toggle-btn"> Toggle Mode</button>
</div>
<script>
 const toggleBtn = document.querySelector(".toggle-btn");

 document.addEventListener("DOMContentLoaded", () => {
   const preferredMode = localStorage.getItem("mode");
   if (preferredMode === "dark") {
     document.body.classList.add("dark-mode");
   } else if (preferredMode === "light") {
     document.body.classList.add("light-mode");
   }
 });
 // Check the user's preferred mode on page load (optional)
 function toggleMode() {
   const body = document.body;
   body.classList.toggle("dark-mode");
   body.classList.toggle("light-mode");

   // Save the user's preference in localStorage (optional)
   const currentMode = body.classList.contains("dark-mode") ? "dark" : "light";
   localStorage.setItem("mode", currentMode);
 }

 toggleBtn.addEventListener("click", () => {
   toggleMode();
 });
</script>

---
// pages/[coin].astro

import Layout from "../layouts/Layout.astro";
import Btn from "../components/Btn.astro";
export async function getStaticPaths() {
 return [
   { params: { coin: "btc" } },
   { params: { coin: "eth" } },
   { params: { coin: "xrp" } },
   { params: { coin: "ada" } },
 ];
}

const { coin } = Astro.params;

async function getCurrentPrice(market) {
 const res = await fetch(
   `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
 );
 const data = await res.json();
 const prices = [];
 for (const info of data.tickers) {
   if (info.target === "USDT") {
     const name = info.base;
     const price = info.last;
     prices.push({ [name]: price });
   }
 }
 return prices;
}

async function fetchMarketPrices() {
 try {
   const prices = await Promise.all([
     getCurrentPrice("binance"),
     getCurrentPrice("kucoin"),
     getCurrentPrice("bitfinex"),
     getCurrentPrice("crypto_com"),
   ]);
   const allPrices = {
     binance: prices[0],
     kucoin: prices[1],
     bitfinex: prices[2],
     crypto_com: prices[3],
   };

   return allPrices;
   // Log the fetched prices to the console
 } catch (error) {
   console.log(error);
   return null;
 }
}

const allPrices = await fetchMarketPrices();
---

<Layout title="Welcome to Astro.">
 <div>
   <h2>{coin}</h2>
   {
     allPrices && Object.keys(allPrices).length > 0 ? (
       <ul>
         {Object.keys(allPrices).map((exchange) => (
           <li>
             {exchange}: {allPrices[exchange][0][coin]}
           </li>
         ))}
       </ul>
     ) : (
       <p>No data available.</p>
     )
   }
   <Btn />
 </div>
</Layout>

---
//pages/index.astro
import Layout from "../layouts/Layout.astro";
---

<Layout title="Welcome to Astro.">
 <main>
   <div>
     <h1>Cryptocurrency Price App</h1>
     <ol>
       <li>
         <a href="./btc">Bitcoin</a>
       </li>
       <li>
         <a href="./eth">Ethereum</a>
       </li>
       <li>
         <a href="./xrp">Ripple</a>
       </li>
       <li>
         <a href="./ada">Cardano</a>
       </li>
     </ol>
   </div>
 </main>
</Layout>

Resumability (withQwik)

Qwik是一个以重用性为核心的全新渲染方式的元框架。该渲染模式基于两种主要策略:

在服务器上序列化应用程序和框架的执行状态,并在客户端上恢复。

水合

这段来自Qwik文档的摘录很好地介绍了可重用性。

监听器 - 在DOM节点上定位事件监听器并安装它们,使应用程序具有交互性。组件树 - 构建表示应用程序组件树的内部数据结构。应用程序状态 - 恢复在服务器上存储的任何获取或保存的数据。总体而言,这被称为水合。所有当前的框架都需要这一步骤来使应用程序具有交互性。

水合作用之所以昂贵,有两个原因:

  • 框架必须下载与当前页面相关的所有组件代码。
  • 框架必须执行与页面上的组件相关联的模板,以重建监听器位置和内部组件树。

在序列化中, Qwik 显示了在服务器上开始构建网页的能力,并在从服务器发送捆绑包后继续在客户端上执行构建,节省了其他框架重新初始化客户端的时间。

就懒加载而言, Qwik 将通过极度懒加载来确保Web应用程序尽快加载,只加载必要的JavaScript捆绑包,并在需要时加载其余部分。 Qwik 可以在开箱即用的情况下完成所有这些操作,无需进行太多开发者配置。

这适用于复杂的博客应用和企业网站的发布。

优点

  • 由于可恢复性而对网络中断具有弹性
  • 快速加载时间
  • 友好的搜索引擎优化

缺点

  • 复杂的实施
  • 更高的带宽使用

相关框架

  • Qwik

Demo (Qwik)

//components/Btn.tsx
import { $, component$, useStore, useVisibleTask$ } from "@builder.io/qwik";

export default component$(({ container }) => {
 const store = useStore({
   mode: true,
 });
 useVisibleTask$(({ track }) => {
   // track changes in store.count
   track(() => store.mode);
   container.value.classList.toggle("light-mode");
   container.value.classList.toggle("dark-mode");
   // Save the user's preference in localStorage (optional)
   const currentMode = container.value.classList.contains("dark-mode")
     ? "dark"
     : "light";
   localStorage.setItem("mode", currentMode);
   console.log(container.value.classList);
 });

 return (
   <div>
     <button
       class="toggle-btn"
       onClick$={$(() => {
         store.mode = !store.mode;
       })}
     >
       Toggle Mode
     </button>
   </div>
 );
});

//components/Client.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";
import { useLocation } from "@builder.io/qwik-city";

import Btn from "./Btn";

export default component$(({ allPrices }) => {
 const loc = useLocation();
 const ID = loc.params.coin.toUpperCase();

 const containerRef = useSignal<Element>();

 useVisibleTask$(() => {
   if (containerRef.value) {
     const preferredMode = localStorage.getItem("mode");
     if (preferredMode === "dark") {
       containerRef.value.classList.add("dark-mode");
     } else if (preferredMode === "light") {
       containerRef.value.classList.add("light-mode");
     }
   }
 });

 return (
   <div class="container" ref={containerRef}>
     <h2>{ID}</h2>
     {Object.keys(allPrices).length > 0 ? (
       <ul>
         {Object.keys(allPrices).map((exchange) => (
           <li key={exchange}>
             {exchange}: {allPrices[exchange][0][ID]}
           </li>
         ))}
       </ul>
     ) : (
       <p>No data available.</p>
     )}
     <Btn container={containerRef} />
   </div>
 );
});

export const head: DocumentHead = {
 title: "Qwik",
};

// routes/price/[coin]/index.tsx
import { component$, useVisibleTask$, useSignal } from "@builder.io/qwik";

import { type DocumentHead } from "@builder.io/qwik-city";
import Btn from "../../../components/Btn";
import Client from "../../../components/Client";

export default component$(async () => {
 async function getCurrentPrice(market) {
   const res = await fetch(
     `https://api.coingecko.com/api/v3/exchanges/${market}/tickers?coin_ids=ripple%2Cbitcoin%2Cethereum%2Ccardano`
   );
   const data = await res.json();
   const prices = [];
   for (const info of data.tickers) {
     if (info.target === "USDT") {
       const name = info.base;
       const price = info.last;
       prices.push({ [name]: price });
     }
   }
   return prices;
 }

 async function fetchMarketPrices() {
   try {
     const prices = await Promise.all([
       getCurrentPrice("binance"),
       getCurrentPrice("kucoin"),
       getCurrentPrice("bitfinex"),
       getCurrentPrice("crypto_com"),
     ]);
     const allPrices = {
       binance: prices[0],
       kucoin: prices[1],
       bitfinex: prices[2],
       crypto_com: prices[3],
     };

     return allPrices;
     // Log the fetched prices to the console
   } catch (error) {
     console.log(error);
   }
 }

 const allPrices = await fetchMarketPrices();

 return (
   <div>
     {allPrices && Object.keys(allPrices).length > 0 ? (
       <Client allPrices={allPrices} />
     ) : (
       <p>No data available.</p>
     )}
   </div>
 );
});

export const head: DocumentHead = {
 title: "Qwik Flower",
};

//routes/index.tsx
import { component$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import { Link } from "@builder.io/qwik-city";
export default component$(() => {
 return (
   <>
     <div>
       <h1>Cryptocurrency Price App</h1>
       <ol>
         <li>
           <Link href="./price/btc">Bitcoin </Link>
         </li>
         <li>
           <Link href="./price/eth">Ethereum </Link>
         </li>
         <li>
           <Link href="./price/xrp">Ripple </Link>
         </li>
         <li>
           <Link href="./price/ada">Cardano </Link>
         </li>
       </ol>
     </div>
   </>
 );
});

export const head: DocumentHead = {
 title: "Welcome to Qwik",
 meta: [
   {
     name: "description",
     content: "Qwik site description",
   },
 ],
};

流式服务器端渲染(Streaming SSR)

流式服务器端渲染(Streaming SSR)是一种相对较新的用于渲染Web应用程序的技术。流式SSR通过将应用程序的用户界面分块在服务器上进行渲染。每个块在准备好后立即进行渲染,然后流式传输到客户端。客户端在接收到块时显示和填充它们。这意味着客户端在应用程序完全渲染之前就可以开始与其进行交互,无需等待。这提高了Web应用程序的初始加载时间,尤其适用于大型和复杂的应用程序。流式SSR最适用于大规模应用,如电子商务和交易应用程序。

优点

  • Performance
  • 实时更新

缺点

  • 复杂性

相关框架

  • Next.js
  • Nuxt.js

Demo

很遗憾,我们的应用程序不够复杂,无法提供一个合适的例子。

结束

在本文中,我们探讨了当今前端网页开发中最流行的十种UI渲染模式。在这个过程中,我们讨论了每种方法的优势、局限性和权衡。然而,重要的是要注意,没有一种适用于所有情况的渲染模式或普遍完美的渲染方法。每个应用都有其独特的需求和特点,因此选择合适的渲染模式对于开发过程的成功至关重要。

由于文章内容篇幅有限,今天的内容就分享到这里,文章结尾,我想提醒您,文章的创作不易,如果您喜欢我的分享,请别忘了点赞和转发,让更多有需要的人看到。同时,如果您想获取更多前端技术的知识,欢迎关注我,您的支持将是我分享最大的动力。我会持续输出更多内容,敬请期待。

参考文献

  • 有关可恢复性的更多信息,请参阅文档:https://qwik.builder.io/docs/concepts/think-qwik/ https://qwik.builder.io/docs/concepts/resumable/
  • 有关岛屿架构的更多信息,请参阅文档 https://docs.astro.build/en/getting-started/
  • 关于渲染的简要文档 https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic-rendering
  • 流式服务器端渲染(SSR) https://blog.logrocket.com/streaming-ssr-with-react-18/

星 Galaxy Fold 和 Surface Duo 以及华为mate X等系列折叠屏手机问世至今已有三年多的时间。此后,三星 Galaxy Z Fold 3 和 Galaxy Z Flip 3、华为mate X2S、荣耀magic V系列等手机均已上市。可折叠设备可供购买,目前正在被消费者使用,随之而来的是我们作为开发人员可以开始探索这种新型设备和响应式设计的下一个发展的机会。

这些 Web 平台功能与现有概念(例如视口和媒体查询)集成,因此开发人员和设计人员可以花更多时间思考如何利用两个显示器来创建增强体验,而不是学习一组新代码来构建它们。

使用新的 CSS 媒体功能检测可折叠设备

双屏和可折叠设备只是响应式设计的下一步,因此它们被视为另一个响应式设计目标,我们可以使用媒体功能为其设计样式。我们今天已经使用媒体功能和查询来定位台式机、平板电脑和手机,现在我们拥有 CSS Viewport Segments 媒体功能来定位我们的可折叠和双屏设备。

horizontal-viewport-segments

视口分段媒体查询可以有两个值。第一个是horizontal-viewport-segments,这表示设备铰链垂直且视口被硬件铰链拆分或折叠成列时的设备状态。

horizonal-viewport-segment铰链处于垂直折叠姿势时,目标是设备。

为了专门为这种方向的可折叠设备提供样式,我们将编写以下内容:

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

整数表示设备方向中存在的视口数量。当设备像一本书一样处于垂直折叠姿势时,我们在水平方向有两个不同的视口,在垂直方向只有一个视口。

我们还可以结合我们的媒体查询来定位双屏设备和某些视口宽度,以提供特定的样式:

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

vertical-viewport-segments

我们的视口分段媒体功能的第二个值是vertical-viewport-segments,这是设备铰链水平时设备的状态,并且硬件铰链将我们的视口分成行。

vertical-viewport-segments目标设备处于水平折叠姿势。

要定位在这个方向旋转的设备,我们将使用以下代码:

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

使用 JavaScript 检测可折叠设备

在某些情况下,您可能无法或不想使用 CSS 媒体查询来检测您的用户是否在可折叠设备上,这就是 JavaScript API 的用武之地。最初,提出了一个名为 Windows Segments Enumeration 的全新 API ,但在开发者社区通过原始试验获得反馈后,在现有的Visual Viewport API 草案规范的基础上构建更有意义。

视口段属性

视口段表示位于彼此相邻的单独显示器上的窗口区域。要检测双屏设备,您可以使用以下代码查询 segments 属性:

const segments = window.visualViewport.segments;

此查询返回的值将是一个数组DOMRects,指示有多少视口。如果只有一个视口段,则查询将返回null,并以这种方式实现以防止将来出现兼容性问题,以免开发人员开始使用visualViewport.segments[0]针对单屏设备。

在双屏设备上,查询将返回 2 DOMRects,表示当浏览器窗口跨越折叠时可用的 2 个视口。

我们存储在segments常量中的这个值是查询属性时设备状态的不可变快照,如果浏览器窗口调整大小或设备旋转,之前检索到的视口段不再有效,需要查询再次通过调整大小或方向事件(或两者)。

如果您调整浏览器窗口的大小以仅跨越一个显示区域,我们将触发调整大小事件。

如果您旋转设备,这将触发调整大小和方向事件,您可以使用这些事件再次查询属性以获取浏览器显示区域的当前状态。

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

何时使用 JAVASCRIPT API 与 CSS 媒体功能来检测 设备

CSS 媒体功能和 JavaScript 段属性都将检测双屏设备,但 JavaScript 属性最好在没有使用 CSS 时使用,当您在 Canvas2D 和 WebGL 中处理对象时可能会发生这种情况。例如,您正在开发的游戏可以同时利用两个屏幕。

使用 CSSenv()变量

除了 CSS 媒体功能之外,还引入了六个新的 CSS 环境变量,以帮助开发人员计算显示区域的几何形状,计算铰链区域在被 Surface Duo 等物理硬件功能遮挡时的几何形状,以及它们还可用于帮助将内容放置在每个显示区域的边界内。

六个新的环境变量如下:

  • 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和位置表示由分隔每个视口段的硬件功能创建的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%);

但是这个额外的后备值是可选的,由作者自行决定,如果他们想包含它。

计算铰链宽度

当您的设备的铰链被硬件功能遮挡时,您可以使用提供的环境变量来计算它。

我们可以使用环境变量计算设备铰链。

在我们的示例中,我们有一个处于垂直姿势的设备,并且想要找到铰链宽度,这样就不会遮挡任何内容。我们将从左显示器的右视口段中减去右显示器的左视口段:

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

使用 CSSenv()变量 放置内容

我们可以使用 CSS 环境变量在显示区域边界内放置内容,如果您想将内容直接放置在铰链或折叠处,这些特别有用。

在下面的示例中,我们将在左侧第一个显示区域的铰链上直接放置图像。该区域是视口的右侧部分,因此我们将使用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 开发人员工具中模拟我们的屏幕,我们将获得以下布局:

最初使用环境变量将图像放置在我们的布局中会将其放置在错误的显示区域中。

这不是我们想要的。图像应位于左侧的显示区域中。

因为图像是使用属性绝对定位的left,所以图像的左边缘最终与viewport-segment-right显示区域对齐。

然后,我们需要从环境变量中减去图像的宽度,以使图像与正确的铰链边缘对齐:

img {
   max-width: 400px;
}

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

从视口段中减去图像宽度会将其沿左侧显示中的铰链放置。

现在我们将图像放置在我们想要的位置。有关如何沿铰链对齐项目的其他示例,您可以查看这个简单的盒子演示。打开Edge Developer Tools>Device Emulation然后选择Surface Duo并确保您Duo emulation处于校正方向姿势。

把它们放在一起:让我们构建一个适应双屏设备的食谱页面

作为一个在做饭时经常使用手机的人,当我在我的双屏设备上时会适应的食谱网站会非常有帮助。让我们来看看如何考虑为它调整一个单独的食谱页面。

我想考虑我将如何分块我的主要内容。通常情况下,我至少会看到食谱标题、制作的份量、烹饪需要多长时间、一张或多张图片、配料以及制作菜肴的步骤。

当我画出我的线框时,我得到以下信息:

桌面上食谱页面的标准布局

我希望我的标题和食谱详细信息在最顶部,然后是一个占据整个内容宽度的图像,然后是成分列表和食谱步骤。我不想堆叠后两个内容组,因为如果我堆叠它们,成分列表的右侧会有很多空白,所以我希望步骤坐在成分旁边,给我两列图片下方。

用于布局的 CSS 网格或 FLEXBOX?

我知道我想如何在普通桌面屏幕上布置这个食谱,并且有多种方法可以对这个布局进行编码和对内容进行分组,但我如何对其进行分组,以及我想在双屏上实现什么布局在我编码之前需要考虑设备。根据我为桌面视图所做的草图,我可以使用 flexbox 和 CSS Grid 的组合来实现我想要的布局,我将成分和步骤分组到一个 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;
}

这将根据我的草图为我提供布局:

布局在桌面上按预期呈现

伟大的!但是我的双屏布局呢?让我们深入了解我们的horizontal-viewport媒体功能和双屏网格。

使用媒体查询和调整容器布局

首先,这是我现在在双屏上的布局:

在没有实现任何双屏代码的情况下,如果用户想要将浏览器跨过两个显示器,那么页面将是这样的。

如果我们向下滚动:

如果用户选择跨越两个显示器,则内容会被铰链遮挡。

不是很好。我们的内容被铰链遮住了,所以让我开始重新定义我的网格。

对于我的网格列,我仍将使用三列,但我希望一列占据左侧的第一个视口段,另外两列占据右侧视口段,因此我将使用我的 CSS环境变量env(viewport-segment-width 0 0)告诉浏览器,对于我的第一列,我希望它占据第一个显示区域的整个视口。

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

}

重置我的边距和填充会掩盖右侧显示中的内容。

现在我要把我的内容放在网格中并调整我的布局。

.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 的浏览器,然后打开浏览器开发人员工具以查看 Surface Duo 仿真。如果您在 Chrome 中打开该站点,请确保在 下启用了实验性网络平台功能标志chrome://flags,以便演示正确显示。

单屏响应式设计细节

为了确保我们考虑到小型单屏设备,我为手机布局选择的代码使用了 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 开始。这些计划很快就会出现在其他 Chromium 浏览器中,但具体时间尚未确定。要在 Chrome 中启用这些 API,请转到chrome://flags并启用实验性网络平台功能。

虽然这些是相对较新的设备,但许多现在已经进入第二代和第三代,因此公司正在投资它们。如果您无法使用物理设备,最好的测试方法是使用浏览器开发工具。我已经在仿真工具和 Surface Duo 上测试了我的网站,Duo 的仿真工具似乎是相同的。我的设计在设备上的外观与在 DevTools 中的外观相同。它使构建和设计双屏设备就像开发桌面和单屏移动设备一样容易。

如果您使用的是不支持这些 API 的桌面或设备,则可以为 Visual Viewport Segments 属性提供一个 polyfill。CSS 媒体查询没有 API。目前,市场上的双屏设备都是基于安卓的,这些API计划在安卓上可用的基于Chromium的浏览器中。

如果可折叠设备上的浏览器不支持这些功能,您可以使用 polyfill 或确保您的网站在小单屏上仍能很好地呈现,因为用户可以灵活选择如何在双屏上显示网站屏幕设备。他们可以跨两个显示器跨越一个网站,或者他们可以选择让它跨一个显示器,如果他们选择后者,它将像在平板电脑或手机上一样显示。即使您的网站没有双屏实现,用户仍然可以选择单显示视图。双屏 API 提供了一种方法来逐步增强拥有设备的用户的体验。

结束

双屏设备只是响应式设计的下一个发展方向。如果您有 PWA 或网站,可用的 API 可以无缝集成到您现有的代码库中。还有其他方法可以为双屏设备构建应用程序,您可以在Surface Duo 文档https://docs.microsoft.com/en-us/dual-screen/中查看这些方法。这是在网络上进行布局的激动人心的时刻,双屏提供了获得更多创意的机会。

为帮助到一部分同学不走弯路,真正达到一线互联网大厂前端项目研发要求,首次实力宠粉,打造了《30天挑战学习计划》,内容如下:

HTML/HTML5,CSS/CSS3,JavaScript,真实企业项目开发,云服务器部署上线,从入门到精通

  • PC端项目开发(1个)
  • 移动WebApp开发(2个)
  • 多端响应式开发(1个)

共4大完整的项目开发 !一行一行代码带领实践开发,实际企业开发怎么做我们就是怎么做。从学习一开始就进入工作状态,省得浪费时间。

从学习一开始就同步使用 Git 进行项目代码的版本的管理,Markdown 记录学习笔记,包括真实大厂项目的开发标准和设计规范,命名规范,项目代码规范,SEO优化规范

从蓝湖UI设计稿 到 PC端,移动端,多端响应式开发项目开发

  • 真机调试,云服务部署上线;
  • Linux环境下 的 Nginx 部署,Nginx 性能优化;
  • Gzip 压缩,HTTPS 加密协议,域名服务器备案,解析;
  • 企业项目域名跳转的终极解决方案,多网站、多系统部署;
  • 使用 使用 Git 在线项目部署;

这些内容在《30天挑战学习计划》中每一个细节都有讲到,包含视频+图文教程+项目资料素材等。只为实力宠粉,真正一次掌握企业项目开发必备技能,不走弯路 !

过程中【不涉及】任何费用和利益,非诚勿扰 。

如果你没有添加助理老师微信,可以添加下方微信,说明要参加30天挑战学习计划,来自!老师会邀请你进入学习,并给你发放相关资料。

30 天挑战学习计划 Web 前端从入门到实战 | arry老师的博客-艾编程

这篇文章中,我将分享21个HTML技巧,包括代码片段,可以提升你的编码技能。

让我们立即开始吧。

(本文视频讲解:java567.com)