普通视图

发现新文章,点击刷新页面。
今天 — 2025年1月18日首页

如何优雅地处理第三方网站高清 Logo 显示?

作者 程普
2025年1月18日 10:46

今天正式发布了我的第一个新标签页插件:Next Idea NewTab

og.webp

这是一个为开发者、独立开发者量身打造的新标签页插件,主要功能分3个模块:

  • 精选的优质开源项目,分析项目潜在价值,帮助开发者找到Idea
  • 实时更新的 GitHub Trending,不错过当下热门项目
  • 开发者工具箱,高效便捷

有了这个插件,每次打开浏览器都能查找新 idea,一下子缩短了信息获取路径。

经常用掘金官方插件的朋友都觉得中间的 GitHub Trending 很眼熟。没错,掘金,你被我致敬了!感谢过去5年掘金一直在新标签页为我提供信息🫡

这篇博客的主要目的不是介绍插件,是要讲一个插件里出现的一个 bug。

bug描述

在插件的右侧,是一大批工具的展示,每个工具都需要显示 logo,问题恰恰出现在 logo 的展示上。

logo 最初的展示方案是,手动从第三方网站里找到高清 logo 地址,如果没有高清 logo,则使用第三方服务获取 logo。这个方案在我测试的时候没发现问题,上线后有用户反馈页面上出现 bug:

bug-2.webp

我第一反应就知道和网络稳定性有关。

解决这个 bug 的思路也简单:

  • 找到高清的 Logo
  • 做好回退方案

表演一下AI编程

启动 AI,发送 “我的产品里要展示不同网站的高清 logo,并且要做好回退方案,给我写一个 React 组件”。

AI 就开发分析如何找到高清 logo:

  • 加载 https://${domain}/apple-touch-icon.png
  • 利用 Google、DuckDuckGo 等第三方服务抓取

并且提供了一份组件代码:

import React, { useState } from 'react';

const WebsiteLogo = ({ domain, size = 32, className = "" }) => {
  const [imgSrc, setImgSrc] = useState(`https://${domain}/apple-touch-icon.png`);
  const [fallbackIndex, setFallbackIndex] = useState(0);
  
  const fallbackSources = [
    `https://${domain}/apple-touch-icon.png`,
    `https://${domain}/apple-touch-icon-precomposed.png`,
    `https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
    `https://icons.duckduckgo.com/ip3/${domain}.ico`,
    `https://${domain}/favicon.ico`,
  ];

  const handleError = () => {
    const nextIndex = fallbackIndex + 1;
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex);
      setImgSrc(fallbackSources[nextIndex]);
    }
  };

  return (
    <img 
      src={imgSrc}
      alt={`${domain} logo`}
      width={size}
      height={size}
      onError={handleError}
      className={`inline-block ${className}`}
      style={{ objectFit: 'contain' }}
    />
  );
};

export default WebsiteLogo;

这个组件已经实现了自动处理错误和降级方案,并且可以自定义大小和样式,看起来已经可行了。

但是咱们可是程序员,哪能被 AI 糊弄,继续鞭打,要求在加载状态添加占位符、logo 展示要有过渡效果、提供兜底的回退方案,并且每个回退方案只给1秒加载时间,超出即当作失败,去找下一个回退方案。

这样一份非常健壮的第三方网站 logo 展示的组件就实现好了,代码我放在开源项目 nextjs-15-starter 了,仓库的演示网站也能体验效果。

核心处理方法在这里:

  useEffect(() => {
    let timeoutId

    if (isLoading) {
      timeoutId = setTimeout(() => {
        handleError()
      }, timeout)
    }

    return () => {
      if (timeoutId) {
        clearTimeout(timeoutId)
      }
    }
  }, [imgSrc, isLoading])

  const handleError = () => {
    const nextIndex = fallbackIndex + 1
    if (nextIndex < fallbackSources.length) {
      setFallbackIndex(nextIndex)
      setImgSrc(fallbackSources[nextIndex])
      setIsLoading(true)
    } else {
      setHasError(true)
      setIsLoading(false)
    }
  }

现在组件就完成了如下任务:

  • 多重备选图标源,确保最大程度显示成功
  • 加载状态显示占位符
  • 超时处理机制
  • 优雅的降级显示(使用域名首字母)
  • 可自定义大小和样式

有了这个组件就能轻松解决不同网站的 favicon 格式不一、图标无法加载、加载超时等等痛点,希望同样有 logo 展示需求的朋友用起来!

关于我

🧑‍💻独立开发|⛵️出海|Next.js手艺人
🛠️今年致力于做独立产品和课程

欢迎在以下平台关注我:

昨天 — 2025年1月17日首页

Next.js 13入门 - 第一篇

作者 DevinJohw
2025年1月16日 23:40

Create a Next.js App

pnpm create next-app@13.4

CSS没有代码提示

当VSCode安装了PostCSS Language Support这个extension后,会导致.css 文件的代码提示功能失效,解决方法把css的Language mode换成tailwindcss。最好把这个设置配置在WorkSpace Settings中。

"files.associations": {
  "*.css": "tailwindcss"
},

Unknown at rule @tailwindcss(unknownAtRules) · tailwindlabs tailwindcss · Discussion #13881

Routing

app routes和pages routes有一个很明显的区别,就是pages routes文件夹中的所有文件都会对外暴露,但是app routes不会,它只会暴露特定的文件,比如page.tsx

假设有这样的文件结构:

app/users
├── new
│   └── page.tsx
├── page.tsx
└── test.css

只有page.tsx对外暴露,test.css没有。

因为这一点使用app router便可以将自己的组件放到到/app/components中了,只要组件名不是那些特定的文件名便不会被暴露。

Navigation - Link

在HTML中使用<a> 跳到其他路由会导致页面的刷新,页面刷新会带来不好的用户体验,可以使用Next.js提供的<Link>组件完成路由调整

<main>
  <div>Hello World</div>
  <Link href="/users">Users</Link>
</main>

Client Components and Server Components

Client Components VS Server Components

  • Large bundles → Smaller bundles

  • Resource intensive, Resource Heavy → Resource efficient

  • No SEO → SEO

  • Less secure → More secure

    Sensitive Data we have in our components or their dependencies, Like API keys, will be exposed to the client.

上面是Server Components的好处,但是Server Components也有缺点,下面是Server Components不能做的,这些功能只有Client Component能做:

  1. Listen to browser events

    比如,Server Components无法给<button> 绑定事件

  2. Access browser APIs

  3. Maintain state

  4. Use effects

In real world applications, we often use a mixture of server and client components.

比如下面的一些组件:

  1. Navbar
  2. Sidebar
  3. ProductList
  4. ProductCard
  5. Pagination
  6. Footer

这些Components在 Next.js 项目中都可以定义为Server Components,而且Next.js中所有组件默认都是Server Components。但是这导致问题,我们一定会希望某些组件处理事件的,这是一个网址最基本的功能,怎么做呢?

比如,这里有个ProductCard component:

function ProductCard() {
  return (
    <div>
      <button onClick={() => {
        console.log("clicked")
      }}>Click</button>
    </div>
  )
}

export default ProductCard

这段代码会导致下面的错误:

Unhandled Runtime Error

[ Server ] Error: Event handlers cannot be passed to Client Component props.
  <div onClick={function onClick} children=...>
               ^^^^^^^^^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.

解决方式有两种

第一种直接将整个Component变成Client Component,直接在最上面添加"use client",这个组件就变成了Client Component了:

'use client'

function ProductCard() {
  return (
    <div>
      <button onClick={() => {
        console.log("clicked")
      }}>Click</button>
    </div>
  )
}
export default ProductCard

这通常不是好的做法,因为将整个组件变成了Client Component之后,便失去了所有Server Component的好处了,就回到了之前写React的样子,对于Client Component还有一点需要注意,当一个组件是Client Componen后,其有文件依赖的子组件将全部变成Client Component(这一点我说的很微妙,我会另外开一篇文章讲这个)。

好的做法是仅仅在必须使用Client Component的地方使用,所以这里可以只将<button>另外提取出一个Component。然后再放到ProductCard中:

// /components/AddToCard.tsx
'use client'

function AddToCard() {
  return (
    <button onClick={() => {
      console.log("Added to cart")
    }}>Add to cart</button>
  )
}
export default AddToCard
// /components/ProductCard.tsx
import AddToCard from "@/app/components/AddToCard"

function ProductCard() {
  return (
    <div>
      <AddToCard />
    </div>
  )
}
export default ProductCard

Data Fetching

Fetch Data有两种方式,一种是在Client Side,还有一种是在Server Side。

Client Side去Fetch Data的方式,通常是先使用useState去declare要fetch的Data,然后在useEffect中去fetch,拿到后再存到state中,当然也可以用其他的Tools,像React Query。

Client Side Data fetching的缺点很明显,就是会多一层Fetch,我们总是先拿到一个没有数据的Component,然后再发送一个请求数据(Extra roundtrip to server),然后再执行render。这个方式无论是时间消耗或者内存消耗都会比Server Side Data Fetching更多。而且我觉得Server Side Data Fetch更简单。

下面是个Server Side Data Fetching,使用很多人知道的公共API, JSON placeholder请求users

interface User {
  id: number;
  name: string;
}

async function UsersPage() {
  const response = await fetch("<https://jsonplaceholder.typicode.com/users>");
  const users: User[] = await response.json();

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <div key={user.id}>{user.name}</div>
        ))}
      </ul>
    </div>
  );
}
export default UsersPage;

可以看到我们不需要useState也不需要useEffect,可以直接在组件中请求数据,然后将数据展示出来,可以看到浏览器直接拿到这样有数据的component。

2025-01-16 23.02.08.gif

Caching

Server Side Fetching还有一个好处,就是可以Caching,对于一个Web application,获取数据有以下方式:

  1. Memory
  2. File System
  3. Network

从上往下的请求速度变慢,Caching是将原本Network中的Data存到Memory中,Next.js扩张了fetch函数,默认会cache相同url产生的数据,这个数据再构建的时候便产生了,后续如果数据更新,不会看到数据的更新。这是其中的一种缓存策略,还有其他两种常用的的缓存策略,需要配置fetch:

 const response = await fetch("<https://jsonplaceholder.typicode.com/users>", {
    cache: "no-store",
  }); // 不缓存任何数据

const response = await fetch("<https://jsonplaceholder.typicode.com/users>", {
next: {
  revalidate: 10,
}
}); // 请求产生生成10s内不再请求数据

Static and Dynamic Rendering

Static Page在构建时就把数据写死,以后请求都将是相同的数据,比如

 <div>{new Date().toLocaleTimeString()}</div>

这个tag,如果当前page是个static page,那么后续请求的数据都只是构建时的数据。

那么,问题来了,Next.js是如何确定一个Page是不是Static Page的?

在开发模式下,所有的数据都是Dynamic的,但是到生产环境不一定,生产环境某些pages被Next.js视为Static的,比如这个page中使用了缓存的fetch,这个Page将是static的。

Rendering in Next.js

一个页面在Next.js中被展示出来,上面描述的那些方式,下图将这些方式总结出来:

2025-01-16 23.27.31.gif

Reference

本笔记来自油管博主Code with mosh的Next.js13课程:

codewithmosh.com/p/mastering…

❌
❌