阅读视图

发现新文章,点击刷新页面。

大厂面试:与HTML相关的基础知识点解析

前言

在前端开发的面试中,HTML 是最基础也是最重要的知识点之一。

尤其是大厂的前端面试,往往会围绕 HTML 的核心概念和语义化标签进行深入考察。

本文将带你梳理一些常见的 HTML 面试问题,并结合实际应用场景进行解析。


Q1 :HTML5 是什么,为什么重要?

HTML5 是 HTML 的最新版本标准,它不仅增强了网页结构的表现能力,还引入了丰富的语义化标签和原生功能(如音视频支持、本地存储等),使开发者能够更高效地构建现代 Web 应用。

在 HTML5 中,我们通常会在文档开头使用:

<!DOCTYPE html>

这行代码的作用是告诉浏览器这是一个 HTML5 文档,确保浏览器以标准模式渲染页面。


Q2 : HTML 标签有哪些分类?


1.按布局需求,可分为行内元素标签和块级元素标签:

1.1 行内元素(Inline Elements)

  • 特点:默认情况下,行内元素不会独占一行,宽度由内容决定,不能设置宽高。
  • 常见标签
    • <a>:超链接
    • <span>:文本容器,常用于样式控制

✅ 小技巧:调试的时候可以给元素添加背景色,以便观察布局效果。

示例:

<span style="background-color: yellow;">这是一个 span</span>
<a href="#">这是一个链接</a>

1.2 块级元素(Block-level Elements)

  • 特点:每个块级元素独占一行,默认宽度为父容器的 100%,可以设置宽高。
  • 常见标签
    • <div>:通用容器
    • <ul> / <li>:无序列表
    • <p>:段落
    • <header> / <footer> / <nav>:语义化标签

示例:

<div style="background-color: lightblue;">这是一个 div</div>
<p>这是一个段落</p>

2. 按功能分,可分为语义化标签、表格标签、表单标签等...

除了布局相关的分类外,HTML 标签还可以根据其功能进行划分,尤其是一些新增的语义化标签,在大厂面试中经常被问到。

2.1 语义化标签(Semantic Tags)

HTML5 引入了许多具有明确含义的语义化标签,这些标签比传统的 <div> 更具可读性和可维护性,也有利于 SEO 和爬虫抓取。

标签 含义
<header> 页面或区块的头部
<main> 页面主要内容
<footer> 页面或区块的底部
<nav> 导航区域
<section> 内容区块
<article> 独立文章内容
<aside> 侧边栏或辅助信息

示例:

<header>
  <h1>网站标题</h1>
  <nav>
    <ul>
      <li><a href="#home">首页</a></li>
      <li><a href="#about">关于我们</a></li>
    </ul>
  </nav>
</header>
<main>
  <article>
    <h2>文章标题</h2>
    <p>这是文章内容。</p>
  </article>
</main>
<footer>
  <p>© 2025 版权所有</p>
</footer>

2.2 表格相关标签

表格用于展示结构化的数据,虽然现在不常用作布局工具,但在数据展示方面依然不可或缺。

  • <table>:定义表格
  • <tr>:表格行
  • <td>:单元格
  • <th>:表头单元格(加粗居中显示)

示例:

<table border="1">
  <tr>
    <th>姓名</th>
    <th>年龄</th>
  </tr>
  <tr>
    <td>张三</td>
    <td>28</td>
  </tr>
</table>

2.3 表单相关标签

表单是用户与网页交互的重要方式,常用于登录、注册等功能。

  • <form>:表单容器
  • <input>:输入框(文本、密码、复选框等)
  • <select> / <option>:下拉选择框
  • <textarea>:多行文本输入

示例:

<form action="/submit" method="post">
  <label for="username">用户名:</label>
  <input type="text" id="username" name="username"><br><br>

  <label for="color">喜欢的颜色:</label>
  <select id="color" name="color">
    <option value="red">红色</option>
    <option value="blue">蓝色</option>
  </select><br><br>

  <input type="submit" value="提交">
</form>

3. 结语

HTML 虽然是前端三大核心技术之一中最基础的部分,但其重要性不容忽视。掌握 HTML 的基本结构、标签分类以及语义化应用,不仅能帮助你写出更清晰、易于维护的代码,还能在大厂面试中脱颖而出。

在准备面试时,建议多动手实践,尝试用语义化标签重构传统 <div> 布局,理解不同标签的行为差异,并关注 HTML5 新增的功能特性,如 Canvas、SVG、本地存储等,这些都是进阶面试题的重要考点。


📌 小贴士:

  • 在调试页面时,可以给元素添加背景颜色,快速识别布局结构。
  • 使用语义化标签提升代码可读性,有助于团队协作和搜索引擎优化(SEO)。

希望这篇文章能帮助你在 HTML 面试中游刃有余,顺利拿下心仪的大厂 offer!

Cesium基础(四):部署离线地图和地形资源

cesium离线部署

  引言:前面文章主要介绍了cesium的初始化,这篇文章主要介绍cesium怎么在断网条件下进行开发和调试,具体内容包括地图与地形的下载,切片和部署。最后会在断网条件下进行初始cesium。

cesium影像瓦片

  cesium的初始化成功后的地球,看似是一个完整的球体,但是仔细观察镜头在推进的时候能看到图层表面是有一些缝隙的,就像一张张图片拼接成的一个能观察的现实地球,这些图片就是瓦片图。AI搜索的cesium瓦片图解释是这样: Cesium的瓦片图是其实现大规模地理数据高效加载与渲染的核心技术,主要分为影像瓦片和地形瓦片两大类,同时支持三维模型瓦片(3D Tiles)。其实,这些影像瓦片是通过将地图按照层级、行号、列号切分为小图块,以四叉树结构组织存储。Cesium通过ImageryProvider加载这些瓦片。具体实现如下:

离线化部署
  1. 瓦片图下载:下载全能地图下载器进行破解,因为很多地图资源的下载都是收费的,所以你懂的,[网盘链接](pan.baidu.com/s/11f5a6tsM… 提取码: 25yn)。下载解压后,右键注册机以管理员身份运行得到注册码,双击imaps.exe,将注册码填写就可以愉快的下载瓦片图了。具体操作如下:

3.PNG   有其余需要下载的,由于篇幅有限,就不一一介绍了,请自己摸索。下载成功的资源目录如下:

image.png

image.png

  1. nginx部署: 在这里我就不介绍怎么在windows安装部署nginx了,请自行百度安装。安装后的目录如下: 捕获.PNG   打开conf文件夹,打开nginx.conf文件,新增一个server,配置代码如下。其中root对应的是文件名称,下面的是为例解决跨域访问的问题
 server {
        listen       81;
        server_name  127.0.0.1;
       
        location / {
            root   satellite;
            autoindex on;
            autoindex_exact_size off;
            autoindex_localtime on;
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Credentials true;
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header Access-Control-Allow-Methods 'GET,POST,OPTIONS';  
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

  之后双击nginx.exe,会出现cmd一闪而过,打开浏览器输入127.0.0.1:81出现如图所示即表示部署成功。 image.png

  1. 配置模板路径: 使用UrlTemplateImageryProvider,访问加载瓦片图,具体路径 如:http://127.0.0.1:81/{z}/{x}/{y}.jpg。具体实现如下:
<script setup>
import * as cesium from "cesium";
import { onMounted } from 'vue';
import { useCesium } from "@/hooks/useCesium";
let viewer = null
const map1 = new cesium.UrlTemplateImageryProvider({
    url: 'http://127.0.0.1:81/{z}/{x}/{y}.jpg'
})

onMounted(() => {
    const earth = document.querySelector("#earth");
    viewer = useCesium(earth);
    viewer.imageryLayers.addImageryProvider(map1)
})
</script>

<template>
    <div class="content">
        <div class="earth" id="earth"></div>
    </div>
</template>

<style lang="scss" scoped>
.content {
    width: 100%;
    height: 100%;
    position: relative;
    .earth {
        width: 100%;
        height: 100%;
    }
}
</style>

cesium地形瓦片

  1.下载: 地形瓦片的离线部署比较麻烦,因为需要自己切割,所以需要在地理空间数据云上下载数据。www.gscloud.cn/search 选择好需要下载的数据级,这里选择DEM数字高程数据中的GDEMV3 30M分辨率数字高程数据。

捕获.PNG   然后检索想要下载的地区,这里作者选择的是北京市昌平区。点击检索结果最右侧的下载按钮下载,实际下载的数据是绿色框框住的面积。

image.png   2.cesiumLab处理: 将解压后的ASTGTMV003_N40E115_dem.tif文件上传,具体操作如下:等待处理完成。 捕获.PNG   处理好的地形瓦片:

image.png   3. nginx部署: 具体代码如下:

server {
        listen       888;
        server_name  127.0.0.1;
       
        location / {
            root   terrain;
            autoindex on;
            autoindex_exact_size off;
            autoindex_localtime on;
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Credentials true;
            add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
            add_header Access-Control-Allow-Methods 'GET,POST,OPTIONS'; 
        }
     
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

捕获.PNG   4.配置模板路径: 使用静态方法cesium.CesiumTerrainProvider.fromUrl,访问http://127.0.0.1:888加载地形资源。实现与效果如下:

<script setup>
import * as cesium from "cesium";
import { onMounted } from "vue";
import { useCesium } from "@/hooks/useCesium";
let viewer = null
// 加载离线地图
const map1 = new cesium.UrlTemplateImageryProvider({
    url: 'http://127.0.0.1:81/{z}/{x}/{y}.jpg'
})
onMounted(async () => {
    const earth = document.querySelector("#earth");
    viewer = useCesium(earth);
    viewer.imageryLayers.addImageryProvider(map1)
    // 加载离线地形
   try{
    const terrainProvider = await cesium.CesiumTerrainProvider.fromUrl('http://127.0.0.1:888');
    viewer.terrainProvider = terrainProvider;
   }catch(e){
       console.log(e)
   }
});
</script>
<template>
    <div class="earth" id="earth"></div>
</template>

<style lang="scss" scoped>
.earth {
    width: 100%;
    height: 100%;
}
</style>

image.png

总结

  cesium影像瓦皮和地形瓦片的部署已经实现,回顾前几篇文章,发现几乎没有新的API用到,最麻烦的也就是nginx部署,实际应用中可以直接整合到后端代码中,如果前端再进行桌面化开发,那么就不用nginx也可以实现,作者已经使用tauri + rust实现了集成。具体实现等有时间再聊聊,今天就到此为止。下一篇聊cesium实体的创建,敬请期待!(觉得对你有帮助的话请点赞+收藏,有问题下方留言。)

npm link本地测试React组件库报错“Invalid hook call”?从多实例到pnpm依赖的完整排查指南

最近开发了一个React拖拽排序组件库,计划通过npm link到本地测试时,遇到了报错:Warning: Invalid hook call. Hooks can only be called inside of the body of a function component.经过多轮排查,最终发现到React多实例冲突pnpm对peerDependencies的隐式安装是核心原因,以下是完整解决流程,供同类问题参考

一、问题初判:React版本兼容性?

测试项目使用Next 15(App Router)报错,初步怀疑版本冲突:

  • Next 15 App Router默认依赖React 19,但组件库用了React 18.3.1
  • 尝试放宽版本限制:将组件库package.jsonreact的版本范围改为"^18.3.1 || ^19.0.0",但测试仍报错
  • 结论:版本兼容性非主因(组件库并没有使用与react18强相关的特性,后来使用了一个react18的项目也不行)

二、排查方向:组件库依赖配置问题

查阅React官方文档(Invalid Hook Call警告)和关键Issue(github.com/facebook/re… ,提示可能是React多实例Hooks未在函数组件内调用导致。因为组件代码没有问题所以排除后者,重点排查多实例:

1. 验证多实例的关键方法

  • 现象辅助判断:同时出现Invalid hook callCannot read properties of null (reading 'useState'),是典型多实例特征(不同React实例的Hooks上下文不共享)

  • 打包产物检查

    • 构建后搜索组件库代码,确认仅在入口有import React from 'react',其他位置无重复引入或者明显的function useState(){}定义等
    • 使用rollup-plugin-visualizer分析打包依赖,确认React未被打包进组件库

2. 修复尝试:配置peerDependencies与外部化

  • peerDependencies声明:在组件库package.json中,将reactreact-dom标记为peerDependencies(告知用户需自行安装)
  • Rollup外部化配置:在vite.config.ts中,通过build.rollupOptions.external将React相关模块排除在打包外:["react", "react-dom", "react/jsx-runtime"] 关键:包括react/jsx-runtime
  • 结果:仍报错

三、关键突破:React路径指向异常

尝试暴力验证:将组件库打包后的import React from 'react'改为测试项目中React的绝对路径(如import React from '/path/to/test-project/node_modules/react'),测试项目运行正常

  • 结论:实锤组件库与测试项目引用了不同路径的React实例

四、终极原因:pnpm对peerDependencies的隐式安装

进一步排查依赖管理工具pnpm的特性:

  • pnpm默认行为:pnpm 10默认开启autoInstallPeers官方文档)(pnpm9也是),会自动安装peerDependencies到当前项目的node_modules中(这篇文章stackoverflow.com/questions/7… 有误导性)
  • 问题触发场景:组件库依赖了motion,而motionpeerDependencies中声明了react,pnpm因autoInstallPeers=true,会隐式为组件库安装一个独立的React实例
  • 验证方法:执行pnpm why react(显示哪个包依赖了react导致的下载),输出显示React由motion的peer依赖触发安装

五、最终解决方案

通过配置pnpm禁用自动安装peer依赖,确保组件库与测试项目共享同一React实例:

  1. 在组件库根目录创建.npmrc文件,添加:auto-install-peers = false
  2. 删除lock文件,重新安装依赖(pnpm i),可以看到pnpm-lock.yaml中开头有一行autoInstallPeers: false
  3. 重新build组件库

总结与避坑指南

  • 核心原则:本地测试组件库时,确保组件库与测试项目共享同一React实例(路径、版本完全一致)

  • 关键配置

    • 组件库必须声明reactpeerDependencies,避免打包时包含React
    • Rollup/Vite需外部化reactreact-domreact/jsx-runtime(避免打包)
    • pnpm用户需检查auto-install-peers配置(默认开启,可能导致隐式安装独立实例)

tips:npm link和pnpm link的区别

  1. npm link:先npm link将组件库注册到全局,再在测试项目npm link 包名引用全局链接,即通过全局node_modules建立软链接
  2. pnpm link:直接将组件库路径硬链接到测试项目的node_modules(需显式指定路径pnpm link 组件库路径),无需经过全局node_modules

希望这篇记录能帮到遇到类似问题的开发者

Koa2 跨域实战:`withCredentials`场景下响应头配置全解析

第一章 跨域问题本质:同源策略与凭证携带限制

1.1 同源策略的核心规则

同源策略(Same-Origin Policy)是浏览器最核心的安全机制之一,其定义为:协议、域名、端口三者完全一致才视为同源。当浏览器发起跨域请求(如前端域名https://web.example.com访问后端https://api.example.com)时,会受到以下限制:

  • 默认禁止读取响应内容(如 JSON 数据)
  • 默认禁止携带凭证(如 Cookie、HTTP 认证头)
  • 需通过 CORS 响应头显式授权

1.2 withCredentials的特殊限制

当前端通过以下方式显式要求携带凭证时:

// Fetch API
fetch('https://api.example.com/data', { credentials: 'include' });

// XMLHttpRequest
xhr.withCredentials = true;

浏览器会对服务端响应头施加更严格的验证规则:

  1. Access-Control-Allow-Origin禁止使用*
    必须返回具体的源(如https://web.example.com),否则浏览器将拦截响应,报错:
    The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'

  2. 必须返回Access-Control-Allow-Credentials: true
    显式告知浏览器允许携带凭证,否则凭证不会被发送。

  3. 预检请求(OPTIONS)的特殊要求
    非简单请求(如 PUT/DELETE 方法、自定义请求头)需先发送 OPTIONS 请求验证权限,服务端必须正确响应以下头:

    Access-Control-Allow-Methods: POST, GET, OPTIONS
    Access-Control-Allow-Headers: Content-Type, Authorization
    

第二章 Koa2 核心解决方案:动态源与中间件配置

2.1 手动处理 CORS 响应头(无中间件)

通过 Koa 中间件手动解析请求源并动态设置响应头,适合轻量级项目或需要高度定制化的场景。

2.1.1 基础实现:白名单校验

const Koa = require('koa');
const app = new Koa();

// 允许的源白名单(生产环境建议从环境变量读取)
const ALLOWED_ORIGINS = new Set([
  'https://web.example.com',
  'https://admin.example.com:8080' // 包含端口的完整源
]);

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin'); // 获取前端请求的源
  
  // 校验源是否合法
  if (ALLOWED_ORIGINS.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true'); // 必须设置为true
    ctx.set('Access-Control-Expose-Headers', 'Authorization, X-Total-Count'); // 允许前端读取的自定义头
  } else {
    // 不允许的源返回403或不设置Access-Control-Allow-Origin(浏览器自动拒绝)
    if (ctx.method === 'OPTIONS') {
      ctx.status = 403;
      return;
    }
  }

  // 处理预检请求
  if (ctx.method === 'OPTIONS') {
    ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    ctx.set('Access-Control-Max-Age', '86400'); // 预检结果缓存24小时
    ctx.status = 204; // 成功响应预检请求
    return;
  }

  await next();
});

// 示例路由
app.use(async ctx => {
  if (ctx.path === '/api/data') {
    ctx.body = { data: 'Authenticated response' };
    ctx.set('Authorization', 'Bearer xxx'); // 配合Access-Control-Expose-Headers使用
  }
});

app.listen(3000);

2.1.2 进阶场景:通配符匹配(需谨慎)

允许同一域名下的所有子域名(如*.example.com),可通过正则表达式实现:

const ALLOWED_ORIGIN_REGEX = /^https?://(?:\w+.)?example.com$/;

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (origin && ALLOWED_ORIGIN_REGEX.test(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true');
  }
  // ...处理预检请求
});

警告:通配符匹配可能引入安全风险,仅建议在可控环境(如同一域名下的子系统)使用。

2.2 使用koa-cors中间件简化配置

koa-cors是 Koa 官方推荐的 CORS 中间件,支持动态源、凭证配置及预检请求处理,代码量可减少 50% 以上。

2.2.1 基础配置

const Koa = require('koa');
const cors = require('koa-cors');
const app = new Koa();

app.use(cors({
  origin: function(ctx) {
    // 动态返回允许的源,优先匹配白名单
    const allowed = ['https://web.example.com', 'https://admin.example.com'];
    const origin = ctx.get('Origin');
    if (allowed.includes(origin)) {
      return origin;
    }
    // 开发环境允许本地调试(需限制在开发阶段)
    if (process.env.NODE_ENV === 'development' && origin.includes('localhost')) {
      return origin;
    }
    return false; // 禁止的源返回false,浏览器会忽略该头
  },
  credentials: true, // 允许携带凭证
  maxAge: 86400, // 预检缓存时间
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  headers: ['Content-Type', 'Authorization', 'X-Custom-Header']
}));

app.listen(3000);

2.2.2 配置简写:环境变量驱动

app.use(cors({
  origin: ctx => ctx.get('Origin') || '*', // 生产环境需移除*,此处仅为示例
  credentials: true,
  // 从环境变量读取允许的方法和头
  methods: process.env.ALLOWED_METHODS.split(','),
  headers: process.env.ALLOWED_HEADERS.split(',')
}));

第三章 生产环境最佳实践:安全与性能优化

3.1 安全加固策略

3.1.1 严格限制允许的源

  • 禁止使用* :无论是否携带凭证,生产环境必须使用白名单

  • 白名单来源

    • 前端正式环境域名(如https://example.com
    • 移动端 API 域名(如api.example.com
    • 第三方合作平台域名(需提前审核)

3.1.2 防范 CSRF 攻击

  • 启用SameSite Cookie 属性:

    ctx.cookies.set('sessionId', 'xxx', {
      sameSite: 'strict', // 严格模式,禁止跨站发送Cookie
      secure: true, // 仅通过HTTPS传输
      httpOnly: true // 禁止JS读取Cookie
    });
    
  • 验证Referer头(配合 CORS 白名单):

    app.use(async (ctx, next) => {
      const referer = ctx.get('Referer');
      if (referer && !ALLOWED_ORIGINS.has(new URL(referer).origin)) {
        ctx.throw(403, 'Invalid referer');
      }
      await next();
    });
    

3.1.3 限制请求方法与头

  • 仅允许必要的 HTTP 方法(如GET/POST),禁止危险方法(如PUT/DELETE直接暴露在外网)

  • 严格控制允许的请求头,避免开放*

    // 错误示例(开放所有头,存在安全风险)
    ctx.set('Access-Control-Allow-Headers', '*');
    
    // 正确示例(仅允许必要头)
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    

3.2 性能优化

3.2.1 预检请求缓存

通过Access-Control-Max-Age头设置预检结果缓存时间(单位:秒),避免重复 OPTIONS 请求:

ctx.set('Access-Control-Max-Age', '86400'); // 缓存24小时

3.2.2 静态资源与 API 分离

  • 静态资源(JS/CSS/ 图片)通过 CDN 分发,设置Access-Control-Allow-Origin: *(无需凭证)
  • API 接口单独部署,严格限制允许的源并启用凭证校验

第四章 典型场景解决方案

4.1 前后端分离开发(本地调试)

前端环境:

  • 开发服务器地址:http://localhost:8080
  • 后端 API 地址:http://localhost:3000

Koa2 配置:

const ALLOWED_ORIGINS = new Set([
  'http://localhost:8080', // 前端开发地址
  'http://127.0.0.1:8080' // 兼容不同本地IP访问
]);

// 开发环境允许动态添加源(仅用于调试)
if (process.env.NODE_ENV === 'development') {
  ALLOWED_ORIGINS.add(ctx.get('Origin')); // 临时信任首次请求的源
}

前端请求:

fetch('http://localhost:3000/api/data', {
  credentials: 'include', // 携带本地Cookie(如登录态)
});

4.2 多前端应用共享 API(如 Web+App + 小程序)

允许的源列表:

const ALLOWED_ORIGINS = new Set([
  'https://web.example.com', // Web端
  'https://app.example.com', // 移动端App
  'https://miniprogram.example.com' // 小程序
]);

差异化配置:

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (ALLOWED_ORIGINS.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    
    // 根据不同源返回不同响应头
    if (origin.includes('miniprogram')) {
      ctx.set('Access-Control-Allow-Headers', 'Content-Type'); // 小程序仅需基础头
    } else {
      ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Web端需要认证头
    }
  }
  await next();
});

4.3 第三方应用授权访问(需严格审核)

场景:

允许经过认证的第三方应用(如partner.example.com)访问 API,但需限制 IP 来源。

实现方案:

app.use(async (ctx, next) => {
  const origin = ctx.get('Origin');
  const clientIp = ctx.ip; // 获取客户端IP
  
  // 校验源与IP绑定关系
  if (origin === 'https://partner.example.com' && clientIp === '192.168.1.100') {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', 'true');
  }
  await next();
});

第五章 常见问题与排错指南

5.1 浏览器报错:Access-Control-Allow-Origin缺失

可能原因:

  1. 服务端未返回Access-Control-Allow-Origin
  2. 源校验失败,服务端未设置任何允许的源

解决方案:

  • 通过浏览器开发者工具(F12)查看响应头,确认是否存在该头
  • 检查白名单是否包含当前请求的源(包括端口)

5.2 凭证未携带(Cookie 未发送到服务端)

可能原因:

  1. Access-Control-Allow-Credentials未设置为true
  2. 服务端返回的Access-Control-Allow-Origin与请求的源不一致(如缺少端口)

解决方案:

// 确保同时设置两者
ctx.set('Access-Control-Allow-Origin', 'https://web.example.com:8080');
ctx.set('Access-Control-Allow-Credentials', 'true');

5.3 预检请求(OPTIONS)失败

可能原因:

  1. 未处理 OPTIONS 请求,返回 404 或其他错误状态码
  2. Access-Control-Allow-Methods未包含实际请求的方法(如 POST 请求但仅允许 GET)

解决方案:

// 手动处理OPTIONS请求
app.use(async (ctx, next) => {
  if (ctx.method === 'OPTIONS') {
    ctx.status = 204;
    ctx.set('Access-Control-Allow-Methods', 'GET, POST'); // 包含实际使用的方法
    return;
  }
  await next();
});

第六章 完整项目示例:Koa2 + Vue3 跨域实战

6.1 项目结构

koa2-api/
├── src/
│   ├── app.js               # Koa主文件
│   ├── middleware/
│   │   └── cors.js          # CORS中间件
│   └── routes/
│       └── api.js           # API路由
├── package.json
└── config/
    └── cors.js              # CORS配置文件

6.2 配置文件(config/cors.js

module.exports = {
  allowedOrigins: new Set([
    'https://vue.example.com', // Vue生产环境
    'http://localhost:8080'    // Vue开发环境
  ]),
  credentials: true,
  maxAge: 86400
};

6.3 CORS 中间件(middleware/cors.js

const { allowedOrigins, credentials, maxAge } = require('../config/cors');

module.exports = async (ctx, next) => {
  const origin = ctx.get('Origin');
  if (allowedOrigins.has(origin)) {
    ctx.set('Access-Control-Allow-Origin', origin);
    ctx.set('Access-Control-Allow-Credentials', String(credentials));
    ctx.set('Access-Control-Max-Age', String(maxAge));
  }

  if (ctx.method === 'OPTIONS') {
    ctx.set('Access-Control-Allow-Methods', 'GET, POST');
    ctx.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    ctx.status = 204;
    return;
  }

  await next();
};

6.4 主文件(src/app.js

const Koa = require('koa');
const cors = require('./middleware/cors');
const apiRouter = require('./routes/api');

const app = new Koa();

app.use(cors);
app.use(apiRouter.routes());

app.listen(3000, () => {
  console.log('Koa2 API server running on port 3000');
});

6.5 前端(Vue3)请求示例

<template>
  <button @click="fetchData">获取数据</button>
</template>

<script setup>
import { ref } from 'vue';
import axios from 'axios';

const data = ref('');

const fetchData = async () => {
  try {
    const response = await axios.get('https://api.example.com/api/data', {
      withCredentials: true, // 携带Cookie
      headers: {
        Authorization: `Bearer ${localStorage.getItem('token')}`
      }
    });
    data.value = response.data;
  } catch (error) {
    console.error('跨域请求失败:', error);
  }
};
</script>

第七章 未来趋势:跨域方案的演进

7.1 HTTP2 与跨域优化

HTTP2 的多路复用特性可减少跨域请求的性能损耗,配合Alt-Svc头实现域名切换:

Alt-Svc: h2=":443"; ma=2592000

7.2 边缘计算与 CORS 卸载

通过 CDN 边缘节点处理 CORS 头,减轻源站压力:

# 示例Nginx配置
location /api/ {
  proxy_pass http://koa-server;
  add_header Access-Control-Allow-Origin $http_origin;
  add_header Access-Control-Allow-Credentials true;
}

7.3 同站策略(SameSite)替代跨域

通过调整 Cookie 的SameSite属性,将跨域请求转为同站请求(需前后端共享域名):

// 后端设置
ctx.cookies.set('sessionId', 'xxx', {
  sameSite: 'lax', // 允许跨站GET请求携带Cookie
  domain: '.example.com' // 共享同一域名
});

结语

withCredentials场景下,Koa2 通过动态源校验、中间件封装及安全策略配置,可有效解决跨域凭证携带问题。核心原则是:严格限制允许的源,避免滥用通配符,始终遵循最小权限原则。通过结合环境变量、配置文件及安全加固措施,既能满足开发效率需求,又能保障生产环境的安全性。未来随着浏览器策略的升级,建议持续关注 CORS 规范的演进,采用更高效的同站策略或边缘计算方案优化跨域体验。

React 闭包陷阱攻防:函数式编程思想的应用

在 React 开发中,闭包陷阱是一个常见且令人头疼的问题。当我们在 useEffectuseCallbacksetTimeoutsetInterval 或其他异步回调中使用 stateprops 时,这些函数会捕获其定义时作用域内的变量值。如果这些值后续发生了变化,而闭包本身没有被重新创建(例如,useEffect 的依赖项数组不正确或为空),那么回调函数内部引用的依然是陈旧的数据,导致各种难以预料的 Bug。

函数式编程思想,尤其是纯函数的概念,为我们提供了优雅的解决方案来应对这些挑战。本文将重点介绍两种广泛应用且充分体现纯函数优势的方法:函数式更新useReducer

闭包陷阱的核心问题

简单来说,当一个函数(回调函数)在另一个函数(例如组件函数或 useEffect 的 setup 函数)内部定义时,它会“记住”其外部作用域的变量。如果外部变量更新了,但这个回调函数没有重新创建以捕获新的值,它就会继续使用旧的值。

function MyComponent() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 假设这个 effect 只在组件挂载时运行一次
    const intervalId = setInterval(() => {
      // 问题:这里的 count 是 effect 创建时捕获的初始值 0
      // 即使外部 count 已经通过 setCount 更新,这里仍然是 0
      console.log('Stale count:', count);
      // setCount(count + 1); // 这样做会导致 count 永远是 1
    }, 1000);

    return () => clearInterval(intervalId);
  }, []); // 空依赖数组,effect 只运行一次

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

1. 函数式更新 (Functional Updates)

React 的 setState 函数(来自 useState Hook)提供了一种强大的机制来避免闭包陷阱:函数式更新。你可以传递一个函数给 setState,这个函数的参数是当前最新的状态值,返回值是新的状态。

原理与纯函数特性

当你使用 setCount(prevCount => prevCount + 1) 时:

  1. React 会确保传递给你的回调函数 (prevCount => ...) 的 prevCount 参数始终是最新的状态值,无论这个 setCount 调用是在哪个闭包中。
  2. 你传递给 setState 的回调函数 prevState => newState 本身就是一个纯函数。它接收当前状态,计算并返回新状态,不依赖外部变量,也没有副作用。

代码示例

import React, { useState, useEffect } from 'react';

function IntervalCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      // 正确:使用函数式更新
      // prevCount 总是最新的 count 值
      setCount(prevCount => prevCount + 1);
    }, 1000);

    // 清理函数
    return () => clearInterval(intervalId);
  }, []); // 依赖数组为空,setInterval 只设置一次,但 setCount 依然能获取最新状态

  return <h1>Count: {count}</h1>;
}

export default IntervalCounter;

常用场景

  • setInterval / setTimeout 回调:如上例,当需要在定时器回调中基于前一个状态更新状态时,函数式更新是首选,因为你不需要将 state 加入 useEffect 的依赖数组,避免了不必要的定时器重置。
  • 异步操作回调:在 fetch 或其他异步操作的 thencatch 回调中更新状态,如果该回调是在 useEffect 中定义且不希望因 state 变化而重新触发 useEffect
    useEffect(() => {
      fetchData().then(newData => {
        setData(prevData => ({ ...prevData, ...newData }));
      });
    }, []); // 假设 fetchData 和 setData 引用稳定
    
  • 复杂的事件处理器:当事件处理器需要在多次触发后累积或修改状态,而事件处理器本身通过 useCallback 缓存且不希望因依赖的状态变化而频繁重建。
    const handleScroll = useCallback(() => {
      // ... 一些计算
      setScrollPositions(prevPositions => [...prevPositions, window.scrollY]);
    }, []); // 如果 setScrollPositions 不依赖其他 state/props
    
  • 任何你希望在回调中安全地更新状态,而不必担心闭包捕获了旧状态的场景。

2. 使用 useReducer 管理复杂状态

对于更复杂的状态逻辑,或者当下一个状态依赖于前一个状态并且涉及到多个子值时,useReducer 是一个更强大的选择。

原理与纯函数特性

const [state, dispatch] = useReducer(reducer, initialState);

  1. Reducer 函数的纯粹性reducer(currentState, action) 函数本身被设计为纯函数。它接收当前状态和描述操作的 action 对象,然后返回一个新的状态对象。它不修改原状态,也没有副作用。
  2. dispatch 函数的稳定性:React 保证 dispatch 函数的引用在组件的整个生命周期内是稳定的。这意味着你可以安全地将其传递给子组件或在 useEffectuseCallback 的回调中使用,而无需将其添加到依赖数组中(ESLint 插件通常会自动处理或允许忽略)。

代码示例

import React, { useReducer, useEffect } from 'react';

// 初始状态
const initialState = {
  count: 0,
  step: 1,
  lastAction: null,
};

// Reducer 纯函数
function counterReducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { ...state, count: state.count + state.step, lastAction: 'increment' };
    case 'decrement':
      return { ...state, count: state.count - state.step, lastAction: 'decrement' };
    case 'setStep':
      return { ...state, step: action.payload, lastAction: 'setStep' };
    case 'reset':
      return { ...initialState, lastAction: 'reset' }; // 重置到初始状态
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function ComplexCounter() {
  const [state, dispatch] = useReducer(counterReducer, initialState);

  useEffect(() => {
    // 示例:一个异步操作,完成后需要更新计数
    const simulateAsyncIncrement = () => {
      setTimeout(() => {
        // dispatch 是稳定的,可以直接在回调中使用
        // reducer 函数会接收到最新的 state
        dispatch({ type: 'increment' });
        console.log('Dispatched increment from timeout.');
      }, 1500);
    };

    if (state.count < 5 && state.lastAction !== 'increment') { // 仅在特定条件下触发
        simulateAsyncIncrement();
    }

    // 注意:如果 useEffect 逻辑依赖 state 中的某些值,
    // 应该将这些值加入依赖数组,以确保逻辑在它们变化时重新运行。
    // 这里为了演示 dispatch 的稳定性,假设异步操作的触发条件不频繁改变。
  }, [state.count, state.lastAction]); // 依赖 state 中的值

  return (
    <div>
      <p>Count: {state.count} (Step: {state.step})</p>
      <p>Last Action: {state.lastAction || 'None'}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
      <button onClick={() => dispatch({ type: 'setStep', payload: state.step + 1 })}>Increase Step</button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
    </div>
  );
}

export default ComplexCounter;

常用场景

  • 复杂的状态对象:当你的组件状态是一个包含多个字段的对象,并且这些字段之间可能存在依赖关系或需要一起更新时。
  • 状态转换逻辑复杂:当状态的下一个值不仅仅是前一个值的简单修改,而是需要根据不同的“动作”(action)类型执行不同的计算逻辑。
  • 多个事件处理器更新同一状态:如果多个用户交互(如按钮点击、表单提交等)都会以不同方式影响同一块状态数据。
  • 状态逻辑需要跨组件共享或易于测试:Reducer 函数是纯函数,易于独立测试。结合 Context API,useReducer 可以用于管理全局或局部共享的状态。
  • 优化性能:由于 dispatch 函数引用稳定,传递给子组件时不会导致不必要的重渲染(如果子组件用了 React.memo)。

其他重要的辅助策略

虽然函数式更新和 useReducer 是解决闭包问题的核心函数式方法,但以下策略同样重要:

  1. 正确使用React Hooks的依赖数组 (useEffect, useCallback, useMemo): 这是React官方推荐的确保闭包捕获最新值的方式。务必将回调函数内部引用的所有会随时间变化的 propsstate 都列入依赖数组。ESLint 插件 eslint-plugin-react-hooks 对此有很大帮助。

  2. 使用 useRef 存储可变值 (作为“逃生舱口”): 在某些情况下,你可能确实需要一个在多次渲染之间保持不变的引用,并且其 .current 属性可以被修改而不会触发重新渲染。你可以用它来存储那些你想在回调中访问的最新值,而又不想让回调本身因为这些值的变化而重新创建(例如,避免频繁添加/移除事件监听器,但监听器内部需要最新状态)。

    const latestCountRef = useRef(count);
    useEffect(() => {
      latestCountRef.current = count; // 每次 count 变化时更新 ref
    });
    
    const handleClick = useCallback(() => {
      setTimeout(() => {
        console.log('Latest count via ref:', latestCountRef.current);
      }, 1000);
    }, []); // handleClick 只创建一次
    

    注意:修改 ref.current 不会触发组件重新渲染。

总结

通过拥抱函数式编程思想,特别是利用纯函数(如函数式更新的回调和 Reducer 函数)以及 React Hooks 提供的机制,我们可以有效地避免和解决闭包陷阱带来的问题。函数式更新和 useReducer 不仅使状态管理更加清晰、可预测,还因为其对纯粹性和不变性的强调,使得代码更易于理解、测试和维护。

在实际开发中,根据状态的复杂度和更新逻辑的特性,灵活选择最适合的策略,是编写高质量 React 应用的关键。

常用DOM

目录 获取DOM节点 通过ID查找节点 通过标签名查找节点 通过类名查找节点 通过CSS选择器查找单个节点 通过CSS选择器查找所有节点 通过关系获取节点 获取下一个兄弟节点 获取上一个兄弟节点

Flutter中的Key详解

在 Flutter 开发中,Key 是一个经常被提及但又容易被忽视的概念。它在 Widget 树的更新和状态管理中扮演着至关重要的角色。本文将详细介绍 Flutter 中 Key 的作用、类型、常见使

纯血鸿蒙开发之广告服务(1)

前言 大家好,我是青蓝逐码的云杰,今天我想来聊一聊学习一下鸿蒙的广告服务! Ads Kit(广告服务) Ads Kit(广告服务)依托华为终端平台与数据能力为您提供流量变现服务,帮助您解决流量变现的难

JavaScript原型链

在JavaScript中,原型链是一个非常重要的概念。它不仅决定了对象的继承机制,还影响了对象属性的查找过程。本文将详细介绍JavaScript中的原型链,包括它的基本概念、工作原理以及实际应用。

语法规范/错误/运算符/判断分支/注释

JavaScript 语法涵盖多方面。语法规范上,严格区分大小写,语句建议以分号结尾,代码会忽略多余空格与换行。常见错误有引号、括号不匹配,逗号、分号缺失等。运算符丰富,包括算术、比较、逻辑等。判断分
❌