普通视图

发现新文章,点击刷新页面。
昨天以前首页

我的远程实习(六) | 一个demo讲清Auth.js国外平台登录鉴权👈|nextjs

作者 浪遏
2025年4月2日 23:00

前言

前些时候 ,远程实习要求实现鉴权登录 , 采用 Auth.js 可用于在 Nextjs 项目中实现登录鉴权 ,今天我写一个 demo , 为大家展示一下 auth 鉴权的数据流转 ~

我们首先介绍一下: NextAuth.js 是一个专为 Next.js 应用设计的灵活且易于使用认证库,它极大地简化了添加登录、注册、登出等认证功能的过程。该库以其灵活性和强大的功能而闻名,支持多种认证方式,包括电子邮件与密码、OAuth 2.0 提供商(如 Google、GitHub、Facebook 等),以及自定义提供商。

主要特点

  • 丰富的内置提供者:支持众多 OAuth 和 OpenID Connect 提供商,方便快捷地与第三方服务集成。
  • 会话管理:提供简明的 API 来处理用户会话,轻松获取当前用户的会话信息。
  • 数据库兼容性:支持与多种数据库集成,适用于存储用户数据,并能无缝对接无头 CMS 和自定义后端。
  • 多语言支持:根据用户的语言偏好显示错误消息及其他文本,增强用户体验。
  • 可定制页面:允许开发者创建符合应用设计风格的自定义登录、注册或错误页面。
  • 安全优先:采用一系列安全默认设置,帮助保护应用免受常见的安全威胁。
  • API 路由整合:利用 Next.js 的 API 路由机制来实现身份验证逻辑,让开发者可以自由创建用于登录、登出等操作的自定义端点。
  • 会话管理选项:提供选择,既可以通过 JSON Web Tokens (JWT) 实现无状态会话管理,也可以选择基于数据库的会话管理。
  • 适配器支持:为了满足将用户数据持久化到数据库的需求,NextAuth.js 提供了与不同数据库系统(如 Prisma、TypeORM 等)集成的适配器。

demo 效果展示

Gitee:gitee.com/luli1314520…

开源不易 , 点个小小赞支持下 ~

认证演示项目登录流程分析

项目结构

项目使用Next.js框架构建,采用App Router架构,主要目录结构:

  • /app - 前端页面和API路由
    • /app/auth/signin - 登录页面
    • /app/api/auth/[...nextauth] - NextAuth API路由
  • /auth - NextAuth配置
  • /services - 业务逻辑层
  • /models - 数据访问层
  • /types - 类型定义
  • /lib - 工具函数

认证方案

项目使用NextAuth.js实现第三方OAuth认证,支持以下登录方式:

  • Google OAuth登录
  • GitHub OAuth登录

认证数据流转图

┌─────────────┐      ┌─────────────────┐      ┌────────────────────┐
│             │      │                 │      │                    │
│  用户界面   │─────▶│  NextAuth API   │─────▶│  OAuth提供商       │
│  (客户端)   │◀─────│  (服务端路由)   │◀─────│  (Google/GitHub)   │
│             │      │                 │      │                    │
└─────────────┘      └─────────────────┘      └────────────────────┘
       │                     │                         │
       │                     │                         │
       ▼                     ▼                         │
┌─────────────┐      ┌─────────────────┐               │
│             │      │                 │               │
│  会话状态   │◀────▶│  数据存储       │◀──────────────┘
│  (JWT)      │      │  (用户信息)     │
│             │      │                 │
└─────────────┘      └─────────────────┘

数据流转详解

  1. 用户界面 → NextAuth API → OAuth提供商
    • 用户在前端界面点击登录按钮,触发认证流程
    • NextAuth API构建OAuth授权URL并重定向用户
    • 用户被引导至第三方OAuth提供商(Google/GitHub)进行身份验证
  2. OAuth提供商 → NextAuth API → 数据存储
    • 用户在OAuth提供商完成身份验证
    • 提供商重定向回NextAuth回调URL,附带授权码
    • NextAuth API使用授权码换取用户信息
    • 用户信息被保存到数据存储中
  3. NextAuth API → 会话状态
    • 认证成功后,NextAuth创建JWT令牌
    • JWT包含必要的用户信息和认证元数据
    • 令牌加密并存储在HTTP-only cookie中
  4. 会话状态 用户界面
    • 前端通过 useSession钩子或 getSession函数获取会话状态
    • 会话状态包含用户身份和权限信息
    • 用户界面根据会话状态调整显示内容(如显示用户资料)
  5. 会话状态 数据存储
    • JWT令牌中包含用户标识符(如UUID)
    • 可通过标识符从数据存储中获取完整用户信息
    • 会话更新时可能涉及数据存储的变更(如更新登录时间)

登录流程(前端到后端)

1. 前端登录入口

文件: app/auth/signin/page.tsx

登录页面是服务端组件,显示登录按钮并检查用户是否已登录:

export default async function SignInPage({
  searchParams,
}: {
  searchParams: { callbackUrl: string | undefined };
}) {
  const session = await auth();
  if (session) {
    return redirect(searchParams.callbackUrl || "/");
  }
  
  // 登录页面UI
}

此组件对应数据流转图中的用户界面(客户端),负责展示登录界面并检查会话状态。

2. 客户端登录按钮

文件: app/auth/signin/client.tsx

客户端组件,处理登录按钮点击事件:

export function SignInButton({ 
  provider, 
  callbackUrl,
  children
}: { 
  provider: string; 
  callbackUrl?: string;
  children: React.ReactNode;
}) {
  return (
    <button
      onClick={() => signIn(provider, { callbackUrl: callbackUrl || '/' })}
      className="..."
    >
      {provider === 'google' && <FaGoogle className="..." />}
      {provider === 'github' && <FaGithub className="..." />}
      <span>{children}</span>
    </button>
  );
}

此组件触发用户界面→NextAuth API的数据流,调用 signIn函数发起认证请求。

3. NextAuth API路由

文件: app/api/auth/[...nextauth]/route.ts

处理NextAuth的API请求:

import NextAuth from "next-auth";
import { authOptions } from "@/auth/config";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

此文件对应数据流转图中的NextAuth API(服务端路由),处理所有认证相关的HTTP请求,包括:

  • 认证请求(重定向到OAuth提供商)
  • 回调处理(接收OAuth提供商的回调)
  • 会话查询(前端获取会话状态)

4. NextAuth配置

文件: auth/config.ts

配置NextAuth认证选项,包括:

  • 认证提供商(Google、GitHub)
  • 登录回调
  • JWT处理
  • Session处理

关键代码:

export const authOptions: NextAuthConfig = {
  providers,
  pages: {
    signIn: "/auth/signin",
  },
  callbacks: {
    async signIn({ user, account, profile }) { ... },
    async redirect({ url, baseUrl }) { ... },
    async session({ session, token }) { ... },
    async jwt({ token, user, account }) { ... },
  },
};

这个配置文件定义了NextAuth API如何与OAuth提供商数据存储交互,以及如何处理会话状态

5. NextAuth实例

文件: auth/index.ts

创建NextAuth实例并导出相关函数:

import NextAuth from "next-auth";
import { authOptions } from "./config";

export const { handlers, signIn, signOut, auth } = NextAuth(authOptions);

这些导出的函数促成了数据流转图中的多个流程:

  • signIn:用户界面→NextAuth API
  • auth:NextAuth API→会话状态
  • signOut:用户界面→NextAuth API→会话状态(清除)

6. 用户信息处理

文件: services/user.ts

处理用户信息保存逻辑:

export async function saveUser(user: User) {
  try {
    const existUser = await findUserByEmail(user.email);
    if (!existUser) {
      await insertUser(user);
    } else {
      user.id = existUser.id;
      user.uuid = existUser.uuid;
      user.created_at = existUser.created_at;
    }
    return user;
  } catch (e) {
    throw e;
  }
}

此服务对应数据流转图中的NextAuth API→数据存储流程,负责将OAuth提供商返回的用户信息持久化到数据存储中。

7. 数据存储

文件: models/user.ts

示例项目使用内存数组存储用户信息,实际项目应使用数据库:

// 演示用简化版本,实际项目中应使用数据库
const users: User[] = [];

export async function findUserByEmail(email: string): Promise<User | null> { ... }
export async function findUserByUuid(uuid: string): Promise<User | null> { ... }
export async function insertUser(user: User): Promise<User> { ... }

此模块实现了数据流转图中的**数据存储(用户信息)**组件,提供用户数据的CRUD操作。

登录流程详解

  1. 用户点击登录按钮
    • 前端调用 signIn(provider)函数
    • NextAuth.js将用户重定向到第三方OAuth提供商(Google/GitHub)
    • 数据流:用户界面→NextAuth API→OAuth提供商
  2. 第三方认证
    • 用户在第三方平台完成认证
    • 第三方平台将用户重定向回应用的回调URL
    • 数据流:OAuth提供商→NextAuth API
  3. 处理回调
    • NextAuth.js API接收回调请求
    • 调用 jwt回调处理令牌
    • 保存用户信息到后端存储
    • 数据流:NextAuth API→数据存储NextAuth API→会话状态
  4. 创建会话
    • 调用 session回调构建会话信息
    • 返回包含用户信息的会话对象
    • 数据流:会话状态用户界面
  5. 完成登录
    • 用户被重定向到指定的回调URL或首页
    • 前端可通过 useSession钩子或 auth()函数访问会话信息
    • 数据流:会话状态用户界面会话状态数据存储

JWT与会话状态管理

JWT(JSON Web Token)在认证流程中扮演核心角色,对应数据流转图中的**会话状态(JWT)**节点:

  1. JWT创建
    • 用户成功认证后,NextAuth创建包含用户信息的JWT令牌
    • JWT中存储必要的用户信息(如UUID、头像URL等)
    • 数据流:NextAuth API→会话状态
  2. JWT存储
    • JWT令牌加密后存储在HTTP-only cookie中
    • 浏览器每次请求自动发送cookie,实现无状态认证
    • 数据流:会话状态用户界面
  3. 会话构建
    • 服务端通过 auth()函数解析JWT令牌获取会话信息
    • 客户端通过 useSession()钩子访问会话状态
    • 数据流:会话状态用户界面
  4. 会话与数据存储交互
    • 会话中的用户标识可用于从数据存储获取完整用户信息
    • 可通过会话中的用户ID更新数据存储中的用户信息
    • 数据流:会话状态数据存储

类型扩展

NextAuth类型扩展,支持JWT和Session中的自定义字段:

declare module "next-auth" {
  interface JWT {
    user?: {
      uuid?: string;
      nickname?: string;
      avatar_url?: string;
      created_at?: string;
    };
  }

  interface Session {
    user: {
      uuid?: string;
      email?: string | null;
      name?: string | null;
      nickname?: string | null;
      avatar_url?: string | null;
      created_at?: string | null;
    }
  }
}

用户类型定义,对应数据存储中的用户数据结构:

export interface User {
  id?: number;
  uuid?: string;
  email: string;
  created_at?: string;
  nickname: string;
  avatar_url: string;
  signin_type?: string;
  signin_ip?: string;
  signin_provider?: string;
  signin_openid?: string;
}

安全考虑

  1. 环境变量:OAuth客户端ID和密钥存储在环境变量中
  2. 重定向检查:验证重定向URL的合法性
  3. JWT令牌:使用JWT保存会话状态,加密存储防篡改
  4. 无密码存储:使用OAuth方式不需要存储用户密码
  5. HTTP-only Cookie:JWT存储在HTTP-only cookie中,防止JavaScript访问
  6. CSRF保护:NextAuth内置CSRF令牌验证机制

数据流转优化建议

  1. 数据库集成
    • 将内存存储替换为持久化数据库(MongoDB、PostgreSQL等)
    • 优化数据存储与NextAuth API的交互性能
  2. 令牌刷新机制
    • 实现OAuth访问令牌自动刷新功能
    • 延长用户会话有效期,减少重复登录
  3. 缓存层引入
    • 在数据存储与会话状态之间添加缓存层(如Redis)
    • 减轻数据库负担,提高频繁会话查询性能
  4. 前端状态管理
    • 优化前端会话状态管理,减少不必要的API调用
    • 实现优雅的会话过期处理和自动重新认证

参考

场景题:大文件上传 ?| 过总字节一面😱

作者 浪遏
2025年3月31日 22:24

前言

刚刚要吃饭,放下书包 , 适才掏出手机点开支付码 , 微信嘀嘟一声 , 划过一条消息 :过佬发来一张图片 , 我好奇地点开群聊消息 , 点入图片 , 不禁一惊 , 过佬出征字节了😱 , 面经如下 :

现在都流行问场景题呢👈 , 之前在掘金上看到个观点 : 在 AI 大行其道 の 今天 , 八股一问便有答案 , 也是说在一定程度上打破了信息差 ; 未来在传统八股方面会减弱 , 将会聚焦与场景化 , 各位掘友怎被看 ?

哈哈 , 不管怎么样😏 , 如果八股背成了八股 , 自然无用 , 利用八股解决实际问题 ,有何尝不是场景化地具体实现呢 ?

今天 , 开始一起研究下大文件上传吧 ~

我之前写过一篇文章关于大文件上传 , 主要是用于实践 , 很多细节没有挖掘哈~

大文件上传👈 | React + NestJs |分片、断点续传、秒传🚀 , 你是否知道 ???

现在进行深入拷打 ~

就是因为这篇文章 , 我也被拷打过😭 , 面经如下 :

完整的面试经过可以查看我之前写的一篇面经:

2025年2月凉面与热面(1)——杭州AI公司一二面

过总

先看过佬的面经

问题一 : 大文件上传以及应用场景优化

这个是十分大的问题 , 拆解下来就是应用场景性能优化 , 可以先泛泛谈之 ~

应用场景

这边举一些例子:

  • 云存储:用户向百度网盘、阿里云盘等上传大型视频、软件安装包等,利用大文件上传功能,即便网络不稳定也能成功上传。
  • 企业办公:设计师上传大型设计素材文件到公司共享服务器,或程序员上传项目部署包等。
  • 在线视频制作:创作者将本地高清视频素材上传到在线剪辑平台。

在这些例子里面 , 我有个点有深刻影响 🤡👈 —— 程序员上传项目部署包 , 我之前文章提到 (本地构建/手动上传/服务器运行) 的部署方案 , 在这个过程中 , 我就需要在本地打包上传服务器 , 这就是大文件上传 ~

我的远程实习(四)| Ailln叫我docker部署项目,我顺便填了以前的坑

性能优化

  • 网络层面:利用 CDN(内容分发网络),将文件切片缓存到离用户近的节点,加快传输速度。如腾讯云 CDN 可加速文件上传(旨在通过将网站内容缓存到全球各地的节点,实现快速、稳定和高效的访问体验)
  • 前端优化:使用多线程或 Web Workers 实现多切片并发上传。像 JavaScript 借助 Web Workers 可开启多个线程同时上传不同切片。
  • 后端优化:优化服务器存储和文件合并逻辑,采用分布式存储系统,如 Ceph,提升存储和处理能力。

问题二:断点续传,怎么确认上传完了

断点续传原理:在上传过程中记录已上传切片信息,网络中断恢复后,从上次中断处继续上传。例如,上传 100 个切片的文件,传到第 30 个时断网,恢复后从第 31 个开始。
确认上传完成方式

  • 计数确认:前端记录已上传切片数量,后端接收切片时也记录。当两端记录的已上传切片数都等于总分片数,确认上传完成。比如前端记录上传了 100 个切片中的 100 个,后端也接收了 100 个,即上传完毕。
  • 哈希校验:对完整文件计算哈希值(如 MD5、SHA - 1 等),上传过程中服务器对合并后的文件计算哈希值,两者相等则确认上传完成。例如上传一个软件安装包,本地计算其 MD5 值,服务器合并后计算 MD5 值与之比对。

问题三:用户上传到一半,重新刷新页面,要不要重新上传

不需要重新上传的情况:若前端和后端有完善的断点续传机制,且能记录已上传切片信息。比如前端使用 IndexedDB 存储已上传切片索引,后端数据库记录接收情况。刷新页面后,前端从** IndexedDB** 读取信息,和后端核对,接着传未上传切片。
需要重新上传的情况:如果没有有效的状态记录和断点续传机制,刷新页面后前端无法知晓已上传进度,可能会重新上传。例如简单的单线程上传脚本,没做任何状态保存,刷新后只能从头开始。

问题四:文件分片的 id 记录在哪

前端记录

  • 内存变量:在 JavaScript 中,可定义变量存储切片 id。如let uploadedChunkIds = [];,每上传一个切片,将其 id push 进数组。适用于简单页面,页面刷新数据丢失。
  • 本地存储:使用localStorageIndexedDB。如localStorage.setItem('chunkIds', JSON.stringify(uploadedChunkIds)); ,可长期保存,支持断点续传,适用于复杂大文件上传场景。

后端记录

  • 数据库:存入关系型数据库(如 MySQL)或非关系型数据库(如 MongoDB) 。以 MySQL 为例,建表记录文件 id、切片 id、上传状态等信息。方便服务器端管理和查询,支持多用户、大规模文件上传场景。
  • 缓存:如 Redis,可快速记录和查询切片 id 状态。适合高并发场景,能提升查询和处理速度。

以上就是过佬面经中的大文件上传了,接下来再来拷打一下 !!! 分别从不同角度拷打 , 可能答案相似之处甚多

我的

以下是对图中文件上传相关问题的详细深刻解释:

如何并发的执行文件上传

  • 原理:利用浏览器或运行环境的多线程、异步特性,同时发起多个文件切片的上传请求。比如在前端 JavaScript 中,可将每个切片的上传任务封装成 Promise,再借助 Promise.all 等方式并发执行。
  • 实现方式:先把大文件切片,如将一个 500MB 的文件切成 500 个 1MB 的切片。然后为每个切片创建上传任务,像使用 XMLHttpRequestFetch API 来发起请求。例如用 Fetch API 时,代码类似 fetch('/upload', { method: 'POST', body: chunk })chunk 为切片数据),最后通过 Promise.all 并发执行这些请求。
  • 注意事项:需控制并发数量,避免因过多并发请求耗尽网络资源或导致浏览器性能下降。可通过自定义队列等方式,设定最大并发数,如设置为 5,当正在执行的上传任务小于 5 时,才从任务队列中取出新切片进行上传。

一般的并发数是多少

  • 影响因素
    • 网络环境:在高速稳定的企业专线网络下,可适当提高并发数;而在普通家庭宽带或移动网络环境中,过高并发数可能导致网络拥塞,一般并发数不宜过高。
    • 服务器性能:服务器配置高、带宽充足,能承受较多并发请求;若服务器性能有限,过多并发会使其负载过高,影响服务稳定性。
    • 浏览器限制:不同浏览器对并发连接数有不同限制,比如 Chrome 浏览器对同一个域名的并发连接数一般限制在 6 - 8 个左右 。
  • 常见取值范围:通常在 3 - 10 之间。对于小型项目或对实时性要求不高的场景,3 - 5 较为合适;而在一些大型文件存储系统且网络和服务器条件较好时,可能会设置到 8 - 10 。但实际应用中需通过测试来确定最优并发数。

如果分片5片,其中两片传完了,接下来怎么办

  • 正常情况:继续按顺序或并发上传剩余的 3 片。若采用顺序上传,依次发起对剩余切片的上传请求;若采用并发上传且还有并发名额,将剩余切片的上传任务加入并发队列执行。
  • 异常处理:若在上传过程中发现已上传的两片存在问题(如通过 MD5 校验发现文件损坏 ),需重新上传这两片。同时,若上传过程中网络中断,需记录已上传切片的状态,待网络恢复后,根据记录继续上传剩余切片或重新上传有问题的切片。
  • 后端协作:后端需配合记录已成功接收的切片信息,以便前端在各种情况下能准确判断上传进度和下一步操作。比如后端可在数据库中记录每个切片的接收状态 。

文件切片大一点好,还是小一点好? 分片切多少怎么考虑

  • 切片大的优劣
    • 优点:切片数量少,上传任务调度开销小,合并操作相对简单,在网络稳定且带宽充足时,能快速完成上传。例如在企业内部高速网络环境下上传大型安装包。
    • 缺点:单个切片传输时间长,网络不稳定时,易因超时等问题导致整个切片重传,浪费时间和流量。
  • 切片小的优劣
    • 优点:单个切片传输快,网络波动影响小,便于实现断点续传,重传成本低。比如在网络状况复杂的公共 Wi - Fi 环境中上传文件。
    • 缺点:切片数量多,调度和管理开销大,后端合并操作也更复杂。
  • 考虑因素
    • 网络状况:网络稳定且带宽大,可适当增大切片;网络不稳定则宜采用较小切片。
    • 文件类型:对实时性要求高的文件(如视频流 ),小切片可减少卡顿;对完整性要求高的文件(如可执行程序 ),大切片可能更合适。
    • 服务器性能:服务器处理能力强、存储 I/O 性能高,可处理较多切片;若服务器性能有限,大切片可减轻其处理压力。

如何实现秒传?(MD5值比对)

  • 原理:利用文件内容的唯一性,通过计算文件的 MD5 值(一种哈希算法,能为文件生成唯一的固定长度字符串 )来标识文件。用户上传文件前,先计算其 MD5 值并发送给服务器,服务器在存储中查找是否有相同 MD5 值的文件。若有,直接将该文件与用户关联,实现秒传;若无,则开始正常上传流程。
  • 实现步骤
    • 前端计算:在前端使用相关库(如 js - md5 库 )计算文件的 MD5 值。
    • 服务器查找:服务器接收到 MD5 值后,在文件索引数据库中查询是否存在相同 MD5 值的文件记录。
    • 结果处理:若找到,向用户返回已存在文件的信息,完成秒传;若未找到,通知前端开始上传文件切片,并在上传完成后将新文件及其 MD5 值记录到数据库。
  • 安全性和局限性:MD5 算法存在碰撞概率(不同文件可能有相同 MD5 值 ),但概率极低。在实际应用中,可结合其他校验方式(如文件大小、文件头信息等 )提高准确性。

前端如何实现并发上传的 ?

大文件并发上传通常按以下步骤和方式实现:

1. 文件切片

先把大文件分割成若干较小的数据块,比如将一个 100MB 的文件按 1MB 大小切成 100 个切片。在前端,利用 JavaScript 的 Blob.prototype.slice 方法就能实现,示例代码如下:

function splitFile(file, chunkSize = 5 * 1024 * 1024) {
    const chunks = [];
    let start = 0;
    while (start < file.size) {
        const chunk = file.slice(start, start + chunkSize);
        chunks.push(chunk);
        start += chunkSize;
    }
    return chunks;
}

2. 并发控制策略

  • 基于 Promise.all 并发:将每个切片的上传请求封装成 Promise 对象,存进数组,再用 Promise.all 并发执行这些 Promise。举例:
const chunks = splitFile(file);
const promises = chunks.map((chunk, index) => {
    const formData = new FormData();
    formData.append('chunk', chunk);
    formData.append('index', index);
    return fetch('/upload', { method: 'POST', body: formData });
});
await Promise.all(promises);

不过,这种方式可能因同时发起过多请求,耗尽系统资源或造成网络拥塞。

  • 队列控制并发数:设定最大并发数,用队列管理切片上传。比如设置最大并发数为 5,上传队列里存放待上传切片,当前上传数小于 5 且队列有切片时,就取出切片上传。示例代码:
const MAX_CONCURRENT_UPLOADS = 5;
const uploadQueue = [];
let activeUploads = 0;

function enqueueUpload(file) {
    uploadQueue.push(file);
    processQueue();
}

function processQueue() {
    if (activeUploads < MAX_CONCURRENT_UPLOADS && uploadQueue.length > 0) {
        const file = uploadQueue.shift();
        activeUploads++;
        uploadFile(file).finally(() => {
            activeUploads--;
            processQueue();
        });
    }
}

3. 前端上传实现

  • 利用 XMLHttpRequest 或 Fetch API:通过这两个 API 发起切片上传请求。以 XMLHttpRequest 为例:
function uploadChunk(chunk, index) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/upload', true);
        xhr.onload = () => {
            if (xhr.status === 200) {
                resolve();
            } else {
                reject(new Error('Chunk upload failed'));
            }
        };
        xhr.onerror = () => {
            reject(new Error('Upload error'));
        };
        const formData = new FormData();
        formData.append('chunk', chunk);
        formData.append('index', index);
        xhr.send(formData);
    });
}

4. 后端处理

后端接收切片请求,可借助像 Express.js(Node.js)、Django(Python)等框架。以 Express.js 为例,示例代码如下:

const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('chunk'), (req, res) => {
    // 处理接收到的切片,可记录切片信息到数据库等
    res.status(200).send('Chunk received successfully');
});

5. 切片合并

所有切片都上传到后端后,要按顺序合并成原始文件。比如在 Node.js 里,使用 fs.appendFileSync 方法,先创建空文件,再把切片数据依次追加进去:

const fs = require('fs');
const path = require('path');

function mergeChunks(chunkPaths, outputPath) {
    const writeStream = fs.createWriteStream(outputPath);
    chunkPaths.forEach((chunkPath) => {
        const readStream = fs.createReadStream(chunkPath);
        readStream.pipe(writeStream, { end: false });
        readStream.on('end', () => {
            fs.unlinkSync(chunkPath); // 合并后删除临时切片文件
        });
    });
    writeStream.on('finish', () => {
        console.log('File merged successfully');
    });
}

此外,部分云存储服务(如阿里云 OSS 等)的 SDK 也提供了并发上传功能,像 Java SDK 用 taskNum 、Python SDK 用 num_threads 参数来控制并发数 ,使用时按对应 SDK 文档配置即可实现大文件并发上传。

除了采用 promise.all 并发上传 , 你还知道什么 ?

除了基于Promise.all并发外,还有以下并发控制策略:

自定义队列控制

  • 原理:维护一个任务队列和一个记录当前正在执行任务数量的变量。设定最大并发数,当有新任务时,先放入队列。若当前执行任务数小于最大并发数,从队列取出任务执行;任务完成后,减少当前执行任务数,并检查队列,若有剩余任务则继续取出执行。
  • 示例代码(JavaScript)
class TaskQueue {
    constructor(maxConcurrent) {
        this.maxConcurrent = maxConcurrent;
        this.currentCount = 0;
        this.queue = [];
    }
    addTask(taskFn) {
        return new Promise((resolve, reject) => {
            this.queue.push({ taskFn, resolve, reject });
            this.processQueue();
        });
    }
    processQueue() {
        if (this.currentCount >= this.maxConcurrent || this.queue.length === 0) {
            return;
        }
        const { taskFn, resolve, reject } = this.queue.shift();
        this.currentCount++;
        taskFn()
          .then(resolve)
          .catch(reject)
          .finally(() => {
                this.currentCount--;
                this.processQueue();
            });
    }
}
// 使用示例
const taskQueue = new TaskQueue(3);
const tasks = Array.from({ length: 10 }, (_, i) => () => new Promise((resolve) => setTimeout(() => {
    console.log(`Task ${i} completed`);
    resolve();
}, 1000 * (i + 1))));
const executeTasks = async () => {
    const promises = tasks.map(task => taskQueue.addTask(task));
    await Promise.all(promises);
    console.log('All tasks completed');
};
executeTasks();

生成器函数结合yield

  • 原理:利用生成器函数可以暂停和恢复执行的特性,配合yield关键字手动控制异步任务的执行顺序和并发情况。在生成器函数内部逐个生成异步任务,每次yield一个任务,等待其完成后再继续执行下一个任务。
  • 示例代码(JavaScript)
function* taskGenerator() {
    yield new Promise((resolve) => setTimeout(() => {
        console.log('Task 1 completed');
        resolve();
    }, 1000));
    yield new Promise((resolve) => setTimeout(() => {
        console.log('Task 2 completed');
        resolve();
    }, 1500));
    yield new Promise((resolve) => setTimeout(() => {
        console.log('Task 3 completed');
        resolve();
    }, 2000));
}
const runner = async function () {
    const gen = taskGenerator();
    let result;
    do {
        result = gen.next();
        if (!result.done) {
            await result.value;
        }
    } while (!result.done);
    console.log('All tasks in generator completed');
};
runner();

消息队列

  • 原理:将异步任务放入消息队列中,由多个消费者(可以是线程、进程等)从队列中获取任务并并行处理。消息队列会按照一定规则(如先进先出)分配任务给消费者,能处理大量异步任务且允许一定延迟。适用于后端系统处理高并发任务场景,像大型电商系统处理订单、物流等任务。
  • 示例(以RabbitMQ为例,Python语言)
import pika

# 连接RabbitMQ服务器
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 声明队列
channel.queue_declare(queue='task_queue')

# 生产者发送任务消息
for i in range(10):
    message = f"Task {i}"
    channel.basic_publish(exchange='', routing_key='task_queue', body=message)
print("Tasks sent to queue")

# 关闭连接
connection.close()

# 消费者接收并处理任务(另一个Python脚本示例)
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
channel.queue_declare(queue='task_queue')

def callback(ch, method, properties, body):
    print(f"Received and processed: {body}")

channel.basic_consume(queue='task_queue', on_message_callback=callback, auto_ack=True)
print('Waiting for tasks...')
channel.start_consuming()

限流算法

  • 令牌桶算法:系统按固定速率生成令牌放入桶中,桶有最大容量。请求到来时需从桶中获取令牌,有令牌则可继续处理,无令牌则请求被限流。比如设定桶容量为100个令牌,每秒生成10个令牌,若请求瞬间到来200个,只能处理100个,剩余100个等待新令牌生成。
  • 漏桶算法:请求像水一样注入漏桶,漏桶以固定速率流出水(处理请求),若注入速度过快,桶满后多余请求会被丢弃。可想象一个底部有小孔的桶,水不断注入但从小孔恒定流出,水注入太快就会溢出。
  • 应用场景:在高并发网络请求场景中,防止服务器因请求过多负载过高。如Web服务器对API接口请求进行限流,保护服务器稳定运行。

第三方库

  • p-limit:轻量级Promise并发控制库。可设置最大并发数,简单易用。示例代码:
const limit = require('p-limit')(2); // 设置最大并发数为2
const tasks = [
    () => new Promise((resolve) => setTimeout(() => { console.log('Task 1'); resolve(); }, 1000)),
    () => new Promise((resolve) => setTimeout(() => { console.log('Task 2'); resolve(); }, 1500)),
    () => new Promise((resolve) => setTimeout(() => { console.log('Task 3'); resolve(); }, 2000))
];
const runTasks = async () => {
    const results = await Promise.all(tasks.map(task => limit(task)));
};
runTasks();
  • async - pool:支持多种并发策略的Promise并发控制库,能灵活控制并发任务数量、处理任务队列等 。

番外

如何在前端实现文件的断点续传,并确保大文件安全可靠上传?

回答重点

为了在前端实现文件的断点续传,并确保大文件能够安全可靠地上传,我们需要以下关键技术和步骤:

  1. 文件分块上传 (Chunked Upload) :将大文件分成多个小块,每个小块可以独立上传,这样即便在上传过程中网络中断,我们也只需要重新上传未完成的小块,而不必重新上传整个文件。
  2. 断点续传标识 (Resume Identifier) :为了实现断点续传,我们需要一个唯一的标识符来标记已经上传的分块。通常可以通过文件的名字、大小和哈希值生成这样一个标识符。
  3. MD5 校验 (MD5 Checksum) :在上传每个分块之后,计算其 MD5 校验值,并在服务器端进行校验,确保分块在传输过程中没有被篡改或损坏。
  4. 并发上传 (Concurrent Uploads) :利用浏览器的并发上传能力,可以同时上传多个分块,提高上传速度。
  5. 进度监控 (Progress Monitoring) :利用 XMLHttpRequest 或 Fetch API 的进度事件,可以实时跟踪上传进度,并在前端界面上显示。
扩展知识
  1. 文件分块实现:我们可以利用 JavaScript 的 File 对象和 Blob.prototype.slice 方法将文件分成多个小块。例如:
const chunkSize = 5 * 1024 * 1024; // 每块5MB
const file = document.getElementById('fileInput').files[0];
const totalChunks = Math.ceil(file.size / chunkSize);

for (let i = 0; i < totalChunks; i++) {
    const start = i * chunkSize;
    const end = Math.min(file.size, start + chunkSize);
    const chunk = file.slice(start, end);
    // 这里可以执行上传操作
}
  1. 断点续传标识生成:通过文件的名字、大小和哈希值生成唯一标识符。例如:
function generateIdentifier(file) {
    return `${file.name}-${file.size}-${file.lastModified}`;
}
  1. MD5 校验:使用 js-md5 库或 Web Cryptography API 实现分块的 MD5 校验:
async function calculateMD5(fileChunk) {
    const arrayBuffer = await fileChunk.arrayBuffer();
    const hashBuffer = await crypto.subtle.digest('MD5', arrayBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}
  1. 并发上传:通过 Promise.all 同时上传多个分块提高上传速度:
const promises = [];
for (let i = 0; i < totalChunks; i++) {
    const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize);
    const promise = uploadChunk(chunk);
    promises.push(promise);
}
await Promise.all(promises);
  1. 进度监控:使用 XMLHttpRequest 或 Fetch API 的进度事件,可以更新前端界面的上传进度条。例如:
function uploadChunk(chunk) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/upload', true);
        xhr.upload.addEventListener('progress', (event) => {
            if (event.lengthComputable) {
                const percentComplete = (event.loaded / event.total) * 100;
                console.log(`Chunk upload: ${percentComplete}% complete`);
            }
        });
        xhr.onload = () => {
            if (xhr.status === 200) {
                resolve(xhr.responseText);
            } else {
                reject(new Error('Chunk upload failed'));
            }
        };
        xhr.send(chunk);
    });
}

怎么用 JS 实现大型文件上传?要考虑哪些问题?

在前端实现大型文件上传,需要考虑以下几个问题:

  • 分片上传:将大文件切割成多个小块进行上传,可以避免一次性上传大文件导致的上传时间过长,网络中断等问题。通常情况下,每个块大小为 1MB 左右。
  • 断点续传:由于网络等因素,上传过程中可能出现中断,此时需要能够从中断的地方恢复上传。
  • 并发上传:多个文件同时上传,需要对上传队列进行管理,保证上传速度和顺序。
  • 上传进度显示:及时显示上传进度,让用户知道上传进度和状态。

可以通过使用第三方库来实现大型文件上传,比如 Plupload、Resumable.js 等。

以下是一个使用 Plupload 实现大型文件上传的示例:

<!-- 引入 Plupload 的 JavaScript 和 CSS 文件 -->
<script type="text/javascript" src="plupload.full.min.js"></script>
<link rel="stylesheet" href="plupload.css">

<!-- 上传控件的容器 -->
<div id="uploader">
    <p>Your browser doesn't have Flash, Silverlight or HTML5 support.</p>
</div>
<!-- 初始化上传控件 -->
<script type="text/javascript">
var uploader = new plupload.Uploader({
    browse_button: 'uploader', // 上传控件的容器
    url: '/upload', // 上传文件的 URL
    multi_selection: false, // 是否允许同时上传多个文件
    filters: {
        max_file_size: '100mb', // 最大上传文件大小
        mime_types: [
            { title: 'Image files', extensions: 'jpg,jpeg,gif,png' },
            { title: 'Zip files', extensions: 'zip,rar' }
        ]
    },
    init: {
        // 添加文件到上传队列之前触发的事件
        BeforeUpload: function (up, file) {
            console.log('BeforeUpload:', file.name);
        },
        // 开始上传文件时触发的事件
        UploadFile: function (up, file) {
            console.log('UploadFile:', file.name);
        },
        // 上传进度改变时触发的事件
        UploadProgress: function (up, file) {
            console.log('UploadProgress:', file.percent);
        },
        // 上传成功时触发的事件
        FileUploaded: function (up, file, info) {
            console.log('FileUploaded:', file.name, info.response);
        },
        // 上传出错时触发的事件
        Error: function (up, err) {
            console.log('Error:', err.message);
        }
    }
});

// 初始化上传控件
uploader.init();
</script>

在后端,需要根据上传控件发送的请求,来实现文件的接收和存储。具体实现方式视具体情况而定,可以使用 SpringMVC、Express.js 等框架来实现。同时,也需要考虑上传文件大小限制、上传速度控制等问题。

vue react路由底层原理 | Hash & histroy 模式

作者 浪遏
2025年3月30日 00:30

一、前端路由原理

1. SPA(单页面应用)

单页面应用(Single Page Application)是一种只加载一次 HTML 页面并在用户与应用交互时动态更新页面内容的 Web 应用。浏览器在首次加载时会获取所需的 HTML、CSS 和 JavaScript 文件,之后所有的操作都通过 JavaScript 控制。这种模式非常适合现代 Web 应用,因为它可以提供流畅的用户体验。

我们结合 react 项目来理解就是 index.html 页面提供挂载点和 js 的引用

2. 路由的必要性

对于复杂的 SPA 应用,路由是不可或缺的。路由允许开发者在不同的页面之间切换,而无需重新加载整个页面。Vue 和 React 都提供了强大的路由功能,无论React 还是 Vue 的 router 都支持两种模式:Hash 模式和 History 模式。

我们先来看看 Hash 和 histroy 的核心差异

二、Hash 模式

1. 定义

Hash 模式是一种通过在 URL 中添加 # 符号来实现前端路由的方式。浏览器在检测到 # 后的路径变化时,不会重新发起请求,而是触发 onhashchange 事件。

2. 网页 URL 组成部分

  • location.protocol:协议(如 http:
  • location.hostname:主机名(如 127.0.0.1
  • location.host:主机(如 127.0.0.1:8001
  • location.port:端口号(如 8001
  • location.pathname:访问页面(如 01-hash.html
  • location.search:搜索内容(如 ?a=100&b=20
  • location.hash:哈希值(如 #/aaa/bbb

3. Hash 的特点

  • 无刷新跳转:Hash 变化不会触发页面刷新,因此不会重新加载资源。
  • 浏览器兼容性:Hash 模式兼容性较好,几乎所有现代浏览器都支持。
  • SEO 限制:Hash 模式下的 URL 不会被搜索引擎爬虫完全解析,因此不利于 SEO。
  • 灵活性:Hash 模式只能修改 # 后面的部分,因此只能跳转到同文档的 URL。

4. Hash 模式的应用场景

  • To B 系统:企业内部系统通常对 SEO 要求不高,Hash 模式简单易用,适合快速开发。
  • 小型项目:对于功能简单的项目,Hash 模式可以快速实现路由功能。

5. 案例与代码示例

以下是一个简单的 Hash 模式实现:

// 监听 hash 变化
window.addEventListener('hashchange', function() {
  console.log('Hash changed to:', location.hash);
  // 根据 hash 值加载对应的内容
  loadContent(location.hash);
});

// 初始加载
window.addEventListener('load', function() {
  console.log('Initial hash:', location.hash);
  loadContent(location.hash);
});

function loadContent(hash) {
  // 根据 hash 值加载内容
  if (hash === '#/home') {
    console.log('Loading home page...');
  } else if (hash === '#/about') {
    console.log('Loading about page...');
  } else {
    console.log('Loading default page...');
  }
}

三、History 模式

1. 定义

History 模式是 HTML5 提供的新特性,允许开发者直接更改前端路由,更新浏览器 URL 地址而不重新发起请求。

2. 与 Hash 模式的区别

通过一个例子来说明 Hash 和 History 模式在浏览器刷新时的区别:

  • 正常页面浏览
    • https://github.com/xxx 刷新页面
    • https://github.com/xxx/yyy 刷新页面
    • https://github.com/xxx/yyy/zzz 刷新页面
  • History 模式
    • https://github.com/xxx 刷新页面
    • https://github.com/xxx/yyy 前端跳转,不刷新页面
    • https://github.com/xxx/yyy/zzz 前端跳转,不刷新页面

3. History 的 API

  • history.pushState(data, title [, url]):向历史记录堆栈顶部添加一条记录。
  • history.replaceState(data, title [, url]):更改当前的历史记录。
  • history.state:存储 pushStatereplaceState 的数据。
  • window.onpopstate:响应 pushStatereplaceState 的调用。

4. History 的特点

  • 无刷新跳转:通过 pushStatereplaceState 实现无刷新跳转。
  • SEO 友好:URL 更加规范,适合搜索引擎爬虫解析。
  • 服务端支持:需要服务端配置,否则刷新页面会导致 404 错误。

5. History 模式的应用场景

  • To C 系统:面向用户的系统通常对 SEO 要求较高,History 模式更适合。
  • 大型项目:功能复杂的项目需要更灵活的路由管理,History 模式可以提供更好的用户体验。

6. 案例与代码示例

以下是一个简单的 History 模式实现:

// 使用 pushState 添加历史记录
function navigateTo(url) {
  history.pushState({}, '', url);
  loadContent(url);
}

// 监听 popstate 事件
window.addEventListener('popstate', function() {
  loadContent(location.pathname);
});

function loadContent(path) {
  // 根据路径加载内容
  if (path === '/home') {
    console.log('Loading home page...');
  } else if (path === '/about') {
    console.log('Loading about page...');
  } else {
    console.log('Loading default page...');
  }
}

// 初始加载
loadContent(location.pathname);

7. 服务端配置示例(Nginx)

为了支持 History 模式,需要在服务端配置中确保所有路由都指向主页面:

server {
  listen 80;
  server_name example.com;

  location / {
    try_files $uri /index.html;
  }
}

四、两者的选择

在实际项目中,选择 Hash 模式还是 History 模式需要根据具体需求和场景来决定:

  • To B 系统:推荐使用 Hash 模式,因为它相对简单,且对 URL 规范不敏感。
  • To C 系统:可以考虑使用 History 模式,但需要服务端支持。
  • 成本与收益:能用简单的解决方案就不要用复杂的,尽量平衡开发成本和用户体验。

五、性能优化与最佳实践

1. 性能优化

  • 减少不必要的渲染:通过路由守卫或懒加载减少不必要的组件渲染。
  • 缓存策略:合理使用缓存策略,减少网络请求。
  • 代码分割:将路由相关的代码进行分割,按需加载。

2. 最佳实践

  • 统一的路由管理:使用路由库(如 Vue Router 或 React Router)统一管理路由。
  • 路由验证:在路由切换时进行权限验证,确保用户只能访问授权的页面。
  • 错误处理:捕获路由切换时的错误,提供友好的错误提示。

六、总结

Hash 和 History 模式各有优缺点,了解它们的区别和适用场景是前端开发中的重要技能。本文对前端路由原理进行了深入探讨,希望能为大家提供一些帮助。通过合理选择和优化,可以为用户提供一个流畅且高效的用户体验。

参考资料

❌
❌