普通视图

发现新文章,点击刷新页面。
今天 — 2025年5月20日掘金 前端

vue3中的form表单层级嵌套问题

2025年5月19日 23:52

先上代码

parent.vue

<script setup>
import { ref, reactive } from "vue";
import TaskList from "./ChildForm.vue";

const formRef = ref();
const formData = reactive({
  projectName: "",
  manager: "",
  tasks: [],
});

const rules = reactive({
  projectName: [
    { required: true, message: "项目名称不能为空", trigger: "blur" },
    { min: 3, max: 50, message: "长度在3到50个字符", trigger: "blur" },
  ],
  manager: [{ required: true, message: "负责人不能为空", trigger: "change" }],
});

const validateTasks = (rule, value, callback) => {
  if (formData.tasks.length === 0) {
    callback(new Error("至少需要添加一个任务"));
  } else {
    callback();
  }
};

const submit = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      console.log("提交数据:", formData);
    }
  });
};
</script>

<template>
  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
    <el-form-item label="项目名称" prop="projectName">
      <el-input v-model="formData.projectName" />
    </el-form-item>

    <el-form-item label="负责人" prop="manager">
      <el-select v-model="formData.manager">
        <el-option label="张三" value="zhangsan" />
        <el-option label="李四" value="lisi" />
      </el-select>
    </el-form-item>

    <el-form-item prop="tasks" :rules="[{ validator: validateTasks }]">
      <TaskList v-model="formData.tasks" />
    </el-form-item>

    <el-button type="primary" @click="submit">提交</el-button>
  </el-form>
</template>

child.vue

<script setup>
import { ref, computed, reactive } from "vue";

const props = defineProps({
  modelValue: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmits(["update:modelValue"]);

const taskRules = reactive({
  name: [{ required: true, message: "任务名称不能为空", trigger: "blur" }],
  priority: [{ required: true, message: "请选择优先级", trigger: "change" }],
});

const tasks = computed({
  get: () => props.modelValue,
  set: (value) => emit("update:modelValue", value),
});

const addTask = () => {
  tasks.value.push({ name: "", priority: "medium" });
};

const removeTask = (index) => {
  tasks.value.splice(index, 1);
};
</script>

<template>
  <div class="task-list">
    <el-button @click="addTask">添加任务</el-button>

    <el-form
      v-for="(task, index) in tasks"
      :key="index"
      :model="task"
      :rules="taskRules"
      class="task-form"
    >
      <el-form-item prop="name">
        <el-input v-model="task.name" placeholder="任务名称" />
      </el-form-item>

      <el-form-item prop="priority">
        <el-select v-model="task.priority">
          <el-option label="高" value="high" />
          <el-option label="中" value="medium" />
          <el-option label="低" value="low" />
        </el-select>
      </el-form-item>

      <el-button @click="removeTask(index)">删除</el-button>
    </el-form>
  </div>
</template>

<style scoped>
.task-form {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 10px;
}
</style>

效果如下:

image.png

一、组件结构设计原理

  1. 数据流向设计父组件通过v-model实现数据双向绑定:
// ProjectForm.vue
const formData = reactive({
  tasks: [] // 自动同步到子组件
})

// TaskList.vue
const tasks = computed({
  get: () => props.modelValue,
  set: (v) => emit('update:modelValue', v)
})

2. 验证责任划分

  • 父组件:验证任务列表非空
  • 子组件:验证单个任务字段

二、分步实现流程

步骤1:父组件基础验证

// ProjectForm.vue
const validateTasks = (_, __, callback) => {
  formData.tasks.length === 0 
    ? callback(new Error('至少需要1个任务')) 
    : callback()
}

步骤2:子组件独立验证

// TaskList.vue
const taskRules = {
  name: { 
    required: true, 
    trigger: 'blur',
    message: '任务名称必填' 
  }
}

步骤3:动态prop绑定

<el-form-item 
  :prop="`tasks[${index}].name`"
  :rules="taskRules.name">
  <el-input v-model="item.name"/>
</el-form-item>

三、验证联动机制

  1. 提交时联合验证
// ProjectForm.vue
const submit = () => {
  formRef.value.validate().then(() => {
    taskListRef.value.validate().then(() => {
      // 全部验证通过
    })
  })
}

2. 实时错误反馈

// TaskList.vue
watch(() => props.modelValue, () => {
  formRef.value?.validate()
}, { deep: true })

四、异常处理方案

// 统一错误捕获
try {
  await Promise.all([
    formRef.value.validate(),
    taskListRef.value.validate()
  ])
} catch (errors) {
  console.error('验证失败:', errors)
}

一文讲清什么是A记录,CNAME记录,NS记录?

2025年5月19日 23:40

什么是A记录(Address Record)

简而言之,A记录是域名到ip地址的映射关系

例如,如果有一个网站example.sun.com,可能需要将其指向服务器的IP地址192.0.1.1。在DNS配置中添加一条A记录,将example.sun.com指向192.0.1.1即可实现

什么是CNAME记录(Canonical Name Record

也称为别名记录。它允许您为一个域名创建别名。

例如,如果您设置了example.sun.com作为www.dns.com的CNAME,这意味着所有访问example.sun.com的请求实际上都会被路由到www.dns.com。

为什么需要CNAME

简化了DNS记录的管理

例如,如果有好多个域名同时指向一个ip地址

www.cc.com → 1.1.1.1
www.xx.com → 1.1.1.1
www.kk.com → 1.1.1.1

那麽。当换服务器ip的时候,就需要对每个域名重新配置

而如果用CNAME的话呢

www.cc.com → www.cxk.com → 1.1.1.1
www.xx.com → www.cxk.com → 1.1.1.1
www.kk.com → www.cxk.com → 1.1.1.1

那么就只需要改动www.cxk.com映射的ip地址

CNAME的应用场景是什么

CDN是CNAME的主要应用场景

随着网站访问量越来越多,服务器顶不住了,就需要找CDN提供商购买CDN加速服务,这个时候他们要求你的域名做个CNAME指向他们给你的一个域名叫www.dd.cdn.com

www.dd.com → www.dd.cdn.com

当用户访问www.dd.com的时候,本地DNS会获得CDN提供的CNAME域名:www.dd.com,然后再次向DNS调度系统发出请求,通过DNS调度系统的智能解析,把离客户端地理位置最近的(或者相对负载低的,主要看CDN那边智能解析的策略)CDN提供商的服务器IP返回给本地DNS,然后再由本地DNS回给客户端,让用户就近取到想要的资源(如访问网站),大大降低了延迟。

3d66c87c40b30616357b79c0dbe75ed.jpg

本地运营商的DNS服务器怎么知道一个域名的授权(权威)服务器是哪台?这个域名应该在哪里取解析呢?

首先公司会去找运营商买域名,比如CDN公司买了cdn.com这个一级域名,那么本地运营商会做一个NS记录,即匹配到这个cdn.com后缀的域名都会到CDN服务提供商的DNS服务器做解析,即到权威服务器做解析。

什么是 NS 记录?

NS 记录,即域名服务器记录,用于指定域名应该由哪个 DNS 服务器来进行解析。简单来说,当用户在浏览器中输入一个域名时,NS 记录会告诉互联网中的 DNS 查询系统,应该向哪个 DNS 服务器询问解析结果。NS 记录中的 IP 地址即为负责解析该域名的 DNS 服务器的 IP 地址。

参考文章

Step - 3

作者 烛阴
2025年5月19日 23:33

Task

Using the max and step functions, paint only those pixels whose normalized x-coordinate is less than 0.25 or greater than 0.75.

使用maxstep函数,仅绘制那些标准化坐标下 x 坐标小于0.25或大于等于0.75

Requirements

The shader should avoid using branching or conditional statements in its code, and instead rely on the step and max functions to determine the color of each pixel.

着色器应避免在其代码中使用分支或条件语句,而是依靠stepmax函数来确定每个像素的颜色。

Theory

GLSL 中的函数max用于返回两个输入值中的最大值。它接受两个参数,并返回两个值中较大的一个。以下是该max函数在 GLSL 中的详细解释:

函数

float max(float x, float y);

  • 如果x大于或等于y,则函数返回x
  • 如果y大于x,则函数返回y

示例用法

float a = 5.0;

float b = 3.0;

float result = max(a, b); // will be 5.0

Answer

uniform vec2 iResolution;

void main() {
  // Normalized pixel coordinates (from 0 to 1)
  vec2 uv = gl_FragCoord.xy / iResolution.xy;

  vec3 color = vec3(1.0, 0.3, 0.3);
  float t1 = 1.0 - step(0.25, uv.x);
  float t2 = step(0.75, uv.x);

  gl_FragColor = vec4(color * max(t1, t2), 1.0);
}

效果

image.png

练习

Step

最后

如果你觉得这篇文章有用,记得点赞、关注、收藏,学Shader更轻松!!

我开源了一个基于 Tiptap 实现一个和功能丰富的协同编辑器 🚀🚀🚀

作者 Moment
2025年5月19日 22:35
一个基于 Tiptap 和 Next.js 构建的现代化协同文档编辑器,集成了丰富的编辑能力与多人实时协作功能,支持插件扩展、主题切换与持久化存储。适合团队写作、教育笔记、在线文档平台等场景。 项目地
昨天 — 2025年5月19日掘金 前端

「译文」我买不起奔驰 🚗,但是奔驰买得起 Oxlint ⚓️ 和 Rolldown ⚡️!

2025年5月19日 21:08

前言

今天刷推特看到的一篇博客,作者是奔驰技术团队!

尤雨溪转发的推特

往期精彩推荐

正文

在当今快速发展的 JavaScript 生态系统中,现代工具发挥着关键作用,不仅影响我们的编码方式,还影响我们花费在编码上的时间和金钱。

JavaScript 生态系统中工作时,我们可以利用各种工具来提升质量、一致性和效率。今天,我们将重点介绍两款出色的工具,它们在实现重大里程碑的同时,仍然是大多数用例的直接替代品。

我们将讨论 捆绑器代码检查器,因为它们可能会成为开发或生产构建的瓶颈。

JavaScript 开发中,捆绑器 采用单个代码模块(无论是以 ESM 还是 CommonJS 编写),并将它们组装成一个有凝聚力的、更大的输出,例如完整的应用程序或可重用的库。

对于 Web 应用而言,即使使用 HTTP/2,此过程也能提升加载速度和运行时效率。对于库而言,提前打包可以减少使用方项目重新打包的需要,并提升执行性能。

相反,JavaScript 中的 linting 涉及检查代码以捕获潜在的错误、风格不一致以及不良的编码习惯。linter 是一种根据一组定义的规则或标准扫描代码库的工具,它有助于确保代码一致性、增强可读性,并在常见错误成为实际问题之前将其预防。

它是如何变化的?

多年来,各种构建工具层出不穷,包括 WebpackTurbopackRspackVite。其中,Vite 凭借其灵活性、用户友好性以及与各种项目的无缝兼容性,在 JavaScript 社区中得到了广泛的采用。

Vite 底层使用了两个打包器:esbuildRollup。您可以在这里找到更多详细信息,因此我们在此不再赘述。Vite 优秀的开源团队在 VoidZero 的支持下,推出了一款基于 Rust 的全新打包器,旨在取代 esbuildRollup。这款新的打包器保留了 Rollup 的 API,同时显著提升了性能并进行了许多其他改进。

在代码检查方面,ESLint 一直以来都是 JavaScript 代码中发现问题的首选工具。然而,VoidZero 也推出了一款基于 Rust 的替代方案 Oxlint,它的性能得到了提升,运行速度比 ESLint 快 50 到 100 倍。

补充一点,需要记住的是,仅仅迁移到 Rust 并不会自动提升速度。许多此类工具也借此机会考察了其他开源项目的架构,找出了设计瓶颈,并根据我们目前所了解的现实情况,为未来做出更可持续的决策。

它对我们有何影响?

这些新工具更令人印象深刻的是,它们可以直接作为替代品。无需重构代码,也无需花时间思考如何集成所有功能。

Mercedes-Benz.io,我们的前端本质上是微前端,所以我们无法仅仅测试完整构建版本并查看其改进程度。尽管如此,我仍然好奇这些工具会给我们带来多少时间和金钱上的影响,而金钱不一定是机器时间,主要是工程时间。

我从一些团队中挑选了几个代码库,一些团队拥有较多的微前端,而另一些团队拥有较少的微前端,并运行了 4 种类型的测试:

  1. 使用 Rolldown 进行直接替换
  2. 使用 SASS-EmbeddedRolldown 进行直接替换
  3. 使用 SASS-Embedded + Lightning CSSRolldown 进行直接替换
  4. 使用 Oxlint 替代 ESLint

由于我已经在测试它,所以我决定采用两个可用于生产的替代品,如上所示。

Sass-EmbeddedSass 包的替代方案。它支持与 Sass 相同的 JS API,并且由同一团队维护。然而,Sass-Embedded 是一个 JavaScript 封装器,封装了原生 Dart 可执行文件。这意味着它通常速度更快,尤其是在处理大型 Sass 编译时。

LightningCSS 是一款用 Rust 编写的超快速 CSS 解析器、转换器、打包器和压缩器。它是一款能够编译、优化和转换 CSS 的工具,旨在比其他传统工具更快、更高效。

让我们来看看数字

在您测试的所有代码库中,平均结果显示:

  1. 仅使用 Rolldown,构建时间减少了 23%,最高可达 33%。
  2. 使用 Rolldown + Sass-Embedded,构建时间减少了 25%,最高可达 33%。
  3. 使用 Rolldown + Sass-Embedded + Lightning CSS,构建时间减少了 34%,最高可达 38%。

在 linting 方面,转向 Oxlint 后,平均减少了 71%,最高减少了 97%。

这只是这些工具的开始。Rolldown 仍在开发中,但已经经过社区的大量测试,而 Oxlint 目前处于 Beta 阶段。这两款工具都计划进行更多改进。

这真是太神奇了:我们谈论的不是重构代码,也不是花时间理解和尝试适应新工具。我们谈论的是一些可以让我们的 CI 流程减少 64% 的嵌入式替换(仅指构建 + linting)。

假设大约有 100 个项目,每个项目平均每年有 600 次提交,每次提交都需要构建和 lint。这意味着每年可以节省大约 500 个小时,差不多 21 天。由于机器成本难以计算,因此不计入机器成本,我们可以想象工程成本,包括生产力、专注度,以及能否顺利进入流程。

这种节省不仅能显著提高开发效率,还能让团队有更多时间专注于功能开发和创新,从而提升整体项目的质量和响应速度。

最后

原文地址:www.mercedes-benz.io/blog/2025-0…

原推特地址:x.com/boshen_c/st…

往期精彩推荐

Three.js 完全学习指南(二)场景、相机、渲染器基础

作者 鲫小鱼
2025年5月19日 18:27

场景、相机、渲染器基础

场景(Scene)详解

场景是 Three.js 中所有 3D 对象的容器,它定义了整个 3D 空间。让我们深入了解场景的配置和使用。

场景示例

图 2.1: 包含多个几何体的场景示例

1. 场景基础配置

import * as THREE from 'three';

// 创建场景
const scene = new THREE.Scene();

// 设置背景色
scene.background = new THREE.Color(0x000000); // 黑色背景

// 添加雾效果
scene.fog = new THREE.Fog(0x000000, 10, 100); // 颜色、近平面、远平面

// 设置场景环境
scene.environment = new THREE.CubeTextureLoader().load([
    'px.jpg', 'nx.jpg',
    'py.jpg', 'ny.jpg',
    'pz.jpg', 'nz.jpg'
]);

场景环境贴图

图 2.2: 使用环境贴图的场景效果

2. 场景管理

// 添加对象到场景
scene.add(mesh);

// 从场景中移除对象
scene.remove(mesh);

// 获取场景中的所有对象
const objects = scene.children;

// 遍历场景中的对象
scene.traverse((object) => {
    if (object.isMesh) {
        // 处理网格对象
    }
});
graph TD
    A[场景] --> B[几何体]
    A --> C[光源]
    A --> D[相机]
    A --> E[辅助对象]
    B --> F[网格]
    B --> G[线条]
    C --> H[环境光]
    C --> I[平行光]
    E --> J[网格辅助]
    E --> K[坐标轴]

图 2.3: 场景对象层级结构

3. 场景优化

// 设置场景自动更新
scene.autoUpdate = true;

// 手动更新场景
scene.updateMatrixWorld(true);

// 清理场景
function disposeScene() {
    scene.traverse((object) => {
        if (object.geometry) {
            object.geometry.dispose();
        }
        if (object.material) {
            if (Array.isArray(object.material)) {
                object.material.forEach(material => material.dispose());
            } else {
                object.material.dispose();
            }
        }
    });
}

相机(Camera)详解

Three.js 提供了多种相机类型,每种类型都有其特定的用途。

1. 透视相机(PerspectiveCamera)

透视相机模拟人眼视角,是最常用的相机类型。

透视相机效果

图 2.4: 透视相机的渲染效果

// 创建透视相机
const camera = new THREE.PerspectiveCamera(
    75, // 视角(FOV)
    window.innerWidth / window.innerHeight, // 宽高比
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 5, 10);

// 设置相机朝向
camera.lookAt(0, 0, 0);

// 更新相机参数
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

2. 正交相机(OrthographicCamera)

正交相机没有透视效果,适合用于 2D 场景或等距视图。

正交相机效果转存失败,建议直接上传图片文件

图 2.5: 正交相机的渲染效果

// 创建正交相机
const camera = new THREE.OrthographicCamera(
    -10, // 左
    10,  // 右
    10,  // 上
    -10, // 下
    0.1, // 近平面
    1000 // 远平面
);

// 设置相机位置
camera.position.set(0, 0, 10);
camera.lookAt(0, 0, 0);

3. 相机控制

使用 OrbitControls 实现相机控制:

相机控制效果

图 2.6: 使用 OrbitControls 的相机控制效果

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);

// 配置控制器
controls.enableDamping = true; // 启用阻尼效果
controls.dampingFactor = 0.05;
controls.screenSpacePanning = false;
controls.minDistance = 10;
controls.maxDistance = 500;
controls.maxPolarAngle = Math.PI / 2;

// 在动画循环中更新控制器
function animate() {
    requestAnimationFrame(animate);
    controls.update();
    renderer.render(scene, camera);
}

渲染器(Renderer)详解

渲染器负责将场景和相机的内容绘制到屏幕上。

1. 基础配置

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true, // 抗锯齿
    alpha: true,     // 透明背景
    precision: 'highp' // 精度
});

// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);

// 设置像素比
renderer.setPixelRatio(window.devicePixelRatio);

// 设置输出编码
renderer.outputEncoding = THREE.sRGBEncoding;

// 启用阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

渲染器效果转存失败,建议直接上传图片文件

图 2.7: 不同渲染效果的对比

2. 高级配置

// 设置渲染器参数
renderer.setClearColor(0x000000, 1); // 设置清除颜色
renderer.setClearAlpha(1); // 设置清除透明度

// 配置阴影
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 配置后期处理
const composer = new THREE.EffectComposer(renderer);
const renderPass = new THREE.RenderPass(scene, camera);
composer.addPass(renderPass);

后期处理效果

图 2.8: 使用后期处理的效果

3. 性能优化

// 设置渲染器参数
renderer.powerPreference = 'high-performance';
renderer.precision = 'highp';

// 自动清理
function disposeRenderer() {
    renderer.dispose();
    renderer.forceContextLoss();
    renderer.domElement.remove();
}

// 处理窗口大小变化
window.addEventListener('resize', () => {
    const width = window.innerWidth;
    const height = window.innerHeight;

    camera.aspect = width / height;
    camera.updateProjectionMatrix();

    renderer.setSize(width, height);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

实战:创建一个完整的 3D 场景

让我们结合以上知识,创建一个完整的 3D 场景:

完整场景示例

图 2.9: 完整的 3D 场景示例

import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';

// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a1a);

// 创建相机
const camera = new THREE.PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    0.1,
    1000
);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);

// 创建渲染器
const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
document.getElementById('app').appendChild(renderer.domElement);

// 创建控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

// 添加网格
const gridHelper = new THREE.GridHelper(10, 10);
scene.add(gridHelper);

// 添加环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

// 添加平行光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(5, 5, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);

// 创建一个立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
    color: 0x00ff00,
    metalness: 0.3,
    roughness: 0.4
});
const cube = new THREE.Mesh(geometry, material);
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);

// 创建地面
const planeGeometry = new THREE.PlaneGeometry(10, 10);
const planeMaterial = new THREE.MeshStandardMaterial({
    color: 0x808080,
    side: THREE.DoubleSide
});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.position.y = -0.5;
plane.receiveShadow = true;
scene.add(plane);

// 动画循环
function animate() {
    requestAnimationFrame(animate);

    // 更新控制器
    controls.update();

    // 旋转立方体
    cube.rotation.x += 0.01;
    cube.rotation.y += 0.01;

    // 渲染场景
    renderer.render(scene, camera);
}

// 开始动画
animate();

性能优化建议

  1. 场景优化

    • 使用适当的几何体复杂度
    • 及时清理不需要的对象
    • 使用对象池管理频繁创建的对象
  2. 相机优化

    • 设置合适的视锥体范围
    • 使用适当的相机类型
    • 优化控制器参数
  3. 渲染器优化

    • 使用适当的像素比
    • 启用必要的渲染特性
    • 及时释放资源

练习

  1. 实现相机的自动旋转
  2. 添加多个光源并观察效果
  3. 实现场景的昼夜变化
  4. 添加后期处理效果

下一步学习

在下一章中,我们将学习:

  • 几何体的创建和使用
  • 材质系统的详细配置
  • 纹理的应用
  • 对象的变换和组合

Flutter核心机制图解说明

作者 90后晨仔
2025年5月19日 18:19

一、三棵树协作流程详解

1. 架构关系示意图
[用户代码] → Widget树(声明式配置)
       ↓ 创建
Element树(生命周期管理)
       ↓ 绑定
RenderObject树(布局/绘制)
2. 协作流程步骤
  1. 初始化阶段

    • runApp() 触发根Widget创建
    • 生成对应的根Element(RenderObjectElement
    • Element创建关联的RenderObject
  2. 构建阶段

    • Widget树通过build()方法递归构建
    • Element树通过inflateWidget方法逐层创建子元素
    • RenderObject树执行createRenderObject初始化渲染对象
  3. 更新阶段

    • 当Widget发生变更时: a. Element树对比新旧Widget类型 b. 类型相同 → 更新现有Element配置(update()) c. 类型不同 → 销毁旧Element,创建新Element
  4. 布局阶段

    • RenderObject执行layout()方法
    • 父节点向子节点传递约束条件(Constraints)
    • 子节点返回布局尺寸(Size)
  5. 绘制阶段

    • 生成Layer树提交给Skia引擎
    • 通过OpenGL/Vulkan进行GPU渲染

二、Platform Channel架构解析

1. 通信层级结构
[Flutter层] - Dart代码
   │
   ├── MethodChannel (方法调用)
   ├── EventChannel (事件流)
   └── BasicMessageChannel (基础消息)
           │
           |
[Native层] - 平台原生代码
   │
   ├── Android (Java/Kotlin)
   └── iOS (Objective-C/Swift)
2. 数据流向示意图
Flutter → 序列化为二进制 → 平台通道 → 反序列化为原生类型 → Native处理
       ← 序列化返回数据 ←          ← 原生返回结果 ←
3. 核心组件说明表
组件 功能特点 典型使用场景
MethodChannel 支持异步方法调用与返回值 调用相机/获取地理位置
EventChannel 建立持续事件流(类似观察者模式) 传感器数据监听/实时定位更新
BasicMessageChannel 基础消息传递(支持自定义编解码器) 简单数据交换/二进制传输

三、Key机制工作原理图示

1. LocalKey复用逻辑
Widget树重建前:
Item1(Key:A) - Item2(Key:B) - Item3(Key:C)

Widget树重建后:
Item2(Key:B) - Item3(Key:C) - Item1(Key:A)

Element树保持:
ElementB ↔ ElementC ↔ ElementA(仅位置变化)
2. GlobalKey定位原理
           ┌───────────┐
           │ GlobalKey │
           └─────┬─────┘
                 │
           ┌─────▼─────┐
           │ Element树  │
           └─────┬─────┘
                 │
           ┌─────▼─────┐
           │ 获取RenderObject │
           └───────────┘

四、状态管理数据流模型

1. Provider架构模型
[ChangeNotifier] ← 数据更新
       │
       ├─── notifyListeners()
       │
[Consumer] → 局部刷新
       │
[Selector] → 精准刷新
2. GetX响应式流程
[Rx变量] → 数据变更
       │
       ├─── 自动触发更新
       │
[Obx组件] → 重建依赖部件
       │
[GetBuilder] → 手动控制刷新

五、混合开发通信时序图

1. MethodChannel调用流程
Flutter端               Native端
  │                        │
  │  invokeMethod('getInfo')│
  │───────────────────────>│
  │                        ├── 执行原生代码
  │                        │
  │     result(data)       │
  │<───────────────────────│
  │                        │
2. EventChannel事件流
Flutter端               Native端
  │                        │
  │   receiveBroadcast()   │
  │───────────────────────>│
  │                        ├── 注册监听器
  │                        │
  │     event(data)        │
  │<───────────────────────│(持续推送)
  │                        │

六、性能优化关键路径

1. 渲染优化路线
减少Widget重建 → 优化Element复用 → 降低RenderObject计算 → 精简Layer树
      ↑               ↑                  ↑
   const构造      Key精准控制        布局边界标记(RepaintBoundary)
2. 内存管理策略
图片缓存控制 → 及时销毁监听 → 避免闭包泄漏 → 使用Isolate计算
   ↑               ↑              ↑             ↑
LRU策略       dispose()清理    DevTools检测   compute()函数

通过以上文字图解,开发者可以建立清晰的架构认知:

  1. 三棵树机制:理解声明式UI的核心工作原理
  2. 平台交互:掌握混合开发的数据通信脉络
  3. 状态管理:构建可维护的响应式架构
  4. 性能优化:定位关键瓶颈实施精准优化

建议结合Flutter DevTools的以下功能进行验证:

  • Widget Inspector:实时查看三棵树状态
  • Timeline:分析渲染流水线性能
  • Memory:检测内存泄漏与溢出

跟着文档学VUE3(四)- 类与样式绑定

2025年5月19日 18:18

Class 与 Style 的绑定技巧

关键词:Vue3、class 绑定、style 绑定、动态样式、响应式 UI

在 Vue 开发中,我们经常需要根据组件状态动态地控制元素的类名和内联样式。Vue 提供了非常强大的 :class:style 指令,不仅可以绑定字符串,还能绑定对象或数组,实现灵活的 UI 样式切换。

🧱 一、绑定 class 的多种方式

💡 基本原理:

  • class 是 HTML 元素的一个 attribute。

  • 在 Vue 中,使用 v-bind:class 或简写为 :class 来动态绑定 class。

  • Vue 对 class 绑定做了增强,支持传入:

    • 字符串
    • 对象
    • 数组

✅ 1. 使用对象绑定 class

适用于需要根据布尔值来切换 class 的场景。

// 直接绑定一个对象
<div :class="classObject"></div>
// 内联字面量
<div
  class="static
  :class="{ active: isActive, 'text-danger': hasError }"
></div>

data() {
  return {
    isActive: true,
    hasError: false,
    classObject: { active: true, 'text-danger': false }
  }
}

💡 小贴士:
  • 如果类名是烤串命名(kebab-case),需要用引号包裹,如 'text-danger'
  • 可以将对象提取成一个计算属性,提升可读性和复用性

✅ 2. 使用数组绑定 class

适用于需要绑定多个 class 名字的情况。

📌 动态条件渲染?

可以结合三元表达式或嵌套对象来实现:

<div :class="[isActive ? activeClass : '', errorClass]"></div>
<div :class="[{ [activeClass]: isActive }, errorClass]"></div>

data() {
  return {
    activeClass: 'active',
    errorClass: 'text-danger',
    isActive: true,
  }
}

// 🧾渲染结果
<div class="active text-danger"></div>

✅ 3. 在组件上使用 class

单根元素组件:

当你给组件添加 class 时,这些类会自动合并到组件的根元素上。

多根元素组件:

如果组件有多个根元素,则需显式指定哪个元素接收 class

示例代码
<!-- 子组件模板 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />

<p class="baz boo">Hi!</p>

🎨 二、绑定 style 内联样式的高级用法

💡 基本原理:

  • :style 支持绑定 JavaScript 对象,用于动态设置内联样式。
  • 支持 camelCase 和 kebab-case 的 key 写法。

✅ 1. 使用对象绑定 style

// 推荐使用 camelCase, 如果是kebab-cased,则key需要对应它在css中的实际名称
<div :style="{ 'font-size': fontSize + 'px' }"></div>

// 也可以直接绑定一个对象
<div :style="styleObject"></div>
data() { 
    return { 
        styleObject: { color: 'red', fontSize: '13px' } 
        } 
 }

✅ 2. 使用数组绑定多个样式对象

可以传入多个样式对象,后面的样式会覆盖前面的:

 
// 也可以绑定一个包含多个样式对象的数组
<div :style="[baseStyles, overridingStyles]"></div>

✅ 3. 自动处理浏览器前缀

  • 如果在css中使用了需要浏览器特殊前缀的css,Vue在运行时会检查该属性是否支持在当前浏览器中使用,如果不支持,会自动加前缀
  • 可以提供多个不同前缀的值,只会渲染浏览器支持的最后一个值
// 不需要特别前缀的浏览器,最后会渲染为display: flex
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

【CodeBuddy】一句话开发一个完整项目之:响应式栅格布局生成器

作者 Jimaks
2025年5月19日 18:10
前言 在响应式网页设计领域,栅格系统是构建页面骨架的核心工具。传统开发中需手动编写重复的CSS代码,本项目通过实现可视化栅格生成器,将布局配置转化为可交互操作,提升前端开发效率。工具采用原生HTML/

uniapp(微信小程序、App)中获取音频文件时长duration

2025年5月19日 17:44

1.需求介绍

  • 在uniapp中获取上传的音频文件的时长,根据时长来校验提交规则。

2.实现

2.1 在微信小程序中获取音频文件时长。

根据 uni.createInnerAudioContext()来获取音频文化时长,点击可直达该api的官方文档说明。

2.1.1 不兼容的写法
  • 基于 uni.createInnerAudioContext() 封装的方法
  • ios微信小程序中可以获取到音频文件时长,在android微信小程序中就获取不到音频文件时长。
  const getAudioDurationByWx = (filePath) => {
    return new Promise((resolve, reject) => {
        // 创建并返回内部 audio 上下文 `innerAudioContext` 对象。
      const audioContext = uni.createInnerAudioContext();
      audioContext.src = filePath

      let retryCount = 0 // 当前重试获取音频文件时长的次数
      const maxRetries = 3 // 最大次数重试获取音频文件时长的次数

      const checkDuration = () => {
        if (audioContext.duration > 0) {
          const duration = audioContext.duration // 获取音频文件的时长
          audioContext.destroy() // 获取到音频文件的时长后, 直接销毁 `innerAudioContext`对象
          resolve(duration) // 音频文件的时长返回出去,供外面使用。
        } else if (retryCount < maxRetries) { // 重新试着获取时长。
          retryCount++
          setTimeout(checkDuration, 500) // 每500ms检查一次
        } else {
          audioContext.destroy(); // 超过了最大重试获取音频文件时长的次数后,直接销毁 `innerAudioContext`对象
          reject(new Error('无法获取音频时长'))
        }
      }

      // 等待一段时间后开始获取时长。
      setTimeout(() => {
        checkDuration()
      }, 100)

    // 监听`innerAudioContext`对象的错误回调方法
      audioContext.onError((err) => {
        audioContext.destroy()
        reject(err)
      })
    })
  }
2.1.2 微信小程序中的兼容写法
  • 参考博客,点击也可直达文档。
  • 如下列出代码
const getAudioDurationByWx = (filePath) => {
  return new Promise((resolve, reject) => {
    const audioContext = uni.createInnerAudioContext()
    audioContext.volume = 0; // 音量0
    audioContext.autoplay = true; // 自动播放
    audioContext.src = filePath

    audioContext.onTimeUpdate(() => {
      if (audioContext.duration > 0) {
        const duration = audioContext.duration
        audioContext.destroy()
        resolve(duration)
      } else {
        audioContext.destroy()
        reject('无法获取音频时长')
      }
      audioContext.destroy(); // 销毁实例
    })

    audioContext.onError((err) => {
      audioContext.destroy()
      reject(err)
    })
  })
}

2.2 在Android中获取音频文件时长

2.2.1 在Android中如下获取

  • 注意: 因为是音频文件上传,记得要去manifest.json中配置文件上传权限
const getAudioDurationByApp = (filePath) => {
  return new Promise((resolve, reject) => {
    try {
      if(plus.os.name.toLowerCase() === 'android') {
        const MediaMetadataRetriever = plus.android.importClass('android.media.MediaMetadataRetriever')
        const retriever = new MediaMetadataRetriever()

        retriever.setDataSource(filePath)
        const duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
        const durationInSeconds = Math.floor(parseInt(duration) / 1000)

        retriever.release()
        resolve(durationInSeconds)
      }
    } catch (e) {
      console.error('获取音频时长失败:', e)
      reject(e)
    }
  })
}

最后

  • 如有不正之处,请佬不吝赐教,万分感谢。
  • 祝大家向上!

前端表单提交请求的跨域机制深度解析:从浏览器同源策略到实战解决方案

作者 龚思凯
2025年5月19日 17:41
通过对表单提交跨域行为的全面解析,我们可以更清晰地认识到:浏览器的安全限制并非阻碍创新,而是为了构建更可靠的网络环境。合理利用跨域解决方案,既能满足业务需求,又能保障用户数据安全,这正是现代 Web

🧠 面试官:一行 v-model 背后发生了什么?你真知道吗?

作者 JiangJiang
2025年5月19日 17:41

面试官推了推眼镜:“我们项目用的是 Vue,对吧?”

“是的,我们主要用 Vue 3。”

“那我问个简单点的,v-model 是怎么实现的?”

“呃,它是语法糖吧……就是帮我们绑定 value 和监听 input……”

“那你知道 Vue 3 的 v-model 和 Vue 2 有什么不同?它做了什么?组件里又该怎么用?”

你愣了一下,心里咯噔:这事儿我还真没深究过……

很多人对 v-model 的印象还停留在“它帮我们双向绑定”,但到底怎么帮的、帮了哪些事、Vue 3 做了哪些变化、为什么要变化,真正能讲清楚的人并不多。

今天我们就深入拆解一下,一行 v-model 背后到底发生了什么事


🕰️ Vue 2:v-model 的“前世”

先来看 Vue 2:

<Child v-model="form.name" />

这个语法糖等价于:

<Child :value="form.name" @input="val => form.name = val" />

你看到这里可能会点点头:“对,就是绑定 value,然后监听 input 事件。”

确实没错,但:

组件里怎么接这个 valueinput

在 Vue 2 的组件里这么写:

// Child.vue
export default {
  props: ['value'],
  methods: {
    updateValue(newVal) {
      this.$emit('input', newVal)
    }
  }
}

组件内部要接收一个叫 value 的 prop,并触发一个叫 input 的事件。

这种方式虽然约定俗成,但也有明显的缺陷:

  • value 容易和其他 prop 名冲突
  • 所有 v-model 都只能绑定一个字段
  • 不支持多个 v-model(比如分别绑定 titlecontent

于是,Vue 3 来了。

🌱 Vue 3:v-model 的“今生”

在 Vue 3 中,还是这行代码:

<Child v-model="form.name" />

它被编译成了下面这样:

<Child modelValue="form.name" @update:modelValue="val => form.name = val" />

这就是 Vue 3 对 v-model 的标准行为:

  • 传值:通过 modelValue prop
  • 回传:通过 update:modelValue 事件

对应组件内部这样写:

// Child.vue
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

function update(val) {
  emit('update:modelValue', val)
}

优势是啥?

  • 组件更好维护,避免了 value 的语义冲突
  • 支持多个 v-model 绑定
  • 结构更清晰,统一使用 modelValue 开头

Vue 3 用 modelValue 替代 value,避免了跟表单原生属性冲突,增强组件语义的明确性。具名 v-model 使得多字段双向绑定更灵活。

小Tip

直接通过 modelValue 这个 prop 绑定,不用 v-model:

<Child :modelValue="xxx" @update:modelValue="yyy" />

但应该没人会这么干,毕竟v-model 这么简洁好用。

🧩 进阶 v-model:多个绑定 + 自定义命名

接下来我们来看看,当你需要更灵活地绑定多个属性或命名时,Vue 3 又是如何优雅应对的。

Vue 3 为组件通信带来了更多灵活性,其中一个重要升级就是:支持多个 v-model自定义绑定的 prop 名称

多个字段:一个组件绑定多个 prop

比如我们有个弹窗组件,既要绑定标题,又要绑定内容:

<Modal v-model:title="form.title" v-model:content="form.content" />

这行代码最终会被编译成:

<Modal 
  :title="form.title" 
  :content="form.content" 
  @update:title="val => form.title = val" 
  @update:content="val => form.content = val"
/>

在子组件内部:

// Modal.vue
defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])

function updateTitle(val) {
  emit('update:title', val)
}
function updateContent(val) {
  emit('update:content', val)
}

这是不是有点 React “受控组件” 的味道?

多个字段独立传参、独立更新,逻辑更加清晰、组件更易维护,相比 Vue 2 的单字段绑定,灵活度提升明显。


自定义名称:不再被 modelValue 限制

你可能还想问:

v-model 一定只能绑定到 modelValue 吗?我能不能自定义名字?

当然可以。

Vue 3 的做法是使用具名 v-model 替代 Vue 2 中的 model 配置项,不再推荐如下写法:

// Vue 2 时代的写法
export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: ['checked'],
  emits: ['change']
}

现在我们直接用具名绑定:

<MySwitch v-model:checked="isOn" />

组件内部写法:

defineProps(['checked'])
const emit = defineEmits(['update:checked'])

function toggle() {
  emit('update:checked', !props.checked)
}

绑定字段名和事件名一目了然,不局限于 modelValue,也无需写死绑定逻辑。

✨ Vue 3.3 新特性:defineModel 语法糖

Vue 3.3 新增了 defineModel,主要解决了手动声明 modelValue prop 的冗余和类型声明繁琐问题。它让响应式状态声明更简洁,尤其对 TypeScript 友好。

它的用法很简单,帮你自动声明了 modelValue(或者你指定的其他名称)的 prop,且支持类型推断。

示例:

<script setup lang="ts">
const model = defineModel({
  name: 'modelValue',
  type: String
})

const emit = defineEmits(['update:modelValue'])

function onInput(e: Event) {
  const target = e.target as HTMLInputElement
  emit('update:modelValue', target.value)
}
</script>

<template>
  <input :value="model" @input="onInput" />
</template>

这里:

  • defineModel 自动帮你定义了 modelValue 这个 prop
  • 变量 model 就是传入的值
  • 你仍然需要手动用 emit('update:modelValue', value) 来通知父组件

🧩 原理图解:这样理解最简单

可以把 v-model 看作是下面这个转换器:

[v-model="xxx"] 
===> 
:propName="xxx" 
@update:propName="val => xxx = val"

111.png

默认情况下,propName = modelValue

如果使用具名绑定 v-model:title,那 propName = title

只要记住这几步步,你就能秒懂任何形式的 v-model

⚠️ 需要注意的小细节

1. 在组件中使用 v-model,需要手动 emit

如果你只是传了 modelValue,却忘了触发 update:modelValue,那页面是不会更新的:

function onInput(e) {
  emit('update:modelValue', e.target.value)
}

Vue 没有帮你做双向绑定,它只是做了“语法转译”。

2. 不要在组件中直接修改 modelValue

// 错误写法
props.modelValue = 'newValue' // Vue 会报警告

要改只能 emit,保持数据流是从父向子传,子通过事件通知父。

🛠️ Vue 2 vs Vue 3 一眼对比

对比点 Vue 2 Vue 3
默认 prop 名 value modelValue
默认事件名 input update:modelValue
多个 v-model 不支持 ✅ 支持具名 v-model:xxx
自定义字段名 model 配置项 直接具名 v-model:xxx
类型推断 手动声明,类型弱 ✅ 支持 defineModel 语法糖
数据更新方式 手动 emit 同样手动 emit,无自动双绑

🎯 小总结

v-model 看似一行代码,背后其实做了三件事:

  1. v-model="x" 转为 :modelValue="x"
  2. 添加 @update:modelValue 监听;
  3. 要求子组件手动 emit 更新。

Vue 3 把 “value + input” 升级为更语义化的 “modelValue + update:modelValue”,不仅避免了命名冲突,还支持多个字段绑定和自定义名称。 但需要注意:

它只是“语法糖”,真正的双向更新还是得你在组件里手动触发事件。

掌握这几点,面试官再问你 v-model,你就能从容不迫地答出底层原理!

如果你觉得这篇文章对你有帮助,欢迎点赞 👍、收藏 ⭐、评论 💬 让我知道你在看! 后续我也会持续输出更多 前端打怪笔记系列文章,敬请期待!❤️

Nuxt 安装 tailwindcss 4.1

2025年5月19日 17:38

但是按照Nuxt模块安装,我的项目打包的时候会有冲突,@nuxtjs/tailwindcss 下的一个依赖库,与我的项目中的一个其他依赖库有版本冲突,然后我就换了一种方式安装tailwind,如下

用tailwind4.1官网指导

tailwindcss.com/docs/instal…

然后我发现以前的v3的一些用法和配置 v4.1 不一样了,参考文档:

[1]. tailwind.nodejs.cn/docs/theme#…

[2]. tailwind.nodejs.cn/docs/upgrad…

比如配置:

// 原来的使用@nuxtjs/tailwindcss
// tailwind.config.js
module.exports= {
    darkMode: 'class',
    theme:{
        screens:{
            xs:"614pX"
            sm:"1002px"
            md:"1022px"
            lg:"1092px"
            xl:"1280px"
        },
    extend:{
        colors:{
            dim:{
                50:"#5F99F7",
                100:"#5F99F7",
                200:"#38444d",
                300:"#202e3a",
                400:"#253341",
                500:"#5F99F7",
                600:"#5F99F7",
                700:"#192734",
                800:"#162d40",
                900:"#15202b",
            },
        },
        
    plugins:[
        require('@tailwindcss/forms'),
    ]
}


// 现在4.1, 不会寻找tailwind.config.js的配置了
// 可以在 /assets/css/tailwind.css中配置
@import "tailwindcss";

@theme {
    --color-dim-50: #5F99F7;
    --color-dim-100: #5F99F7;
    --color-dim-200: #38444D;
    --color-dim-300: #202E3A;
    --color-dim-400: #253341;
    --color-dim-500: #5F99F7;
    --color-dim-600: #5F99F7;
    --color-dim-700: #192734;
    --color-dim-800: #162D40;
    --color-dim-900: #15202B;


    --breakpoint-xs: 38.375rem;
    --breakpoint-sm: 62.625rem;
    --breakpoint-md: 63.875rem;
    --breakpoint-lg: 68.25rem;
    --breakpoint-xl: 80rem;
    --breakpoint-2xl: 96rem;
}

注意:如果你硬要使用tailwind.config.js怎么办?

从 Tailwind CSS v4.0 开始,官方将 PostCSS 插件逻辑移出主包,如果你尝试像旧版本一样使用:

ts
复制编辑
require("tailwindcss")()

就会报你看到的错误:

"It looks like you're trying to use tailwindcss directly as a PostCSS plugin..."

Error An error has occurred It looks like you're trying to use `tailwindcss` directly as a PostCSS plugin. The PostCSS plugin has moved to a separate package, so to continue using Tailwind CSS with PostCSS you'll need to install `@tailwindcss/postcss` and update your PostCSS configuration. Stack Trace Pretty Raw

我也不知道,有人知道的话,评论区快快告诉我!!!

面试官:讲一下HTPP缓存...

作者 momo06117
2025年5月19日 17:32

前言

不知道各位有没有在面试中被拷打HTTP缓存策略的经历,那到底什么是HTTP缓存? HTTP缓存具体有什么作用? HTTP缓存的策略是什么? 今天我们就来简单了解一下HTTP缓存

HTTP简介

HTTP缓存是一种在客户端或服务端临时存储资源(HTML,CSS,JS文件,图片等)的机制。每当浏览器请求相同资源的时候,不是直接从服务端获取,而是看本地有没有相同请求,如果有就直接从本地获取。如果资源更新,那下次请求时则从服务端获取并保存到本地。

这样做的好处有:

  • 减少请求次数,减轻服务端压力;
  • 减少网页响应时间,增加用户体验;
  • 减少网络带宽,减少用户流量消耗;

HTTP缓存流程

  1. 浏览器向服务端发送请求
  2. 服务端收到浏览器发送过来的请求,返回资源,并在响应头加上对应的缓存标识
  3. 浏览器接收到响应,根据响应报文中缓存标识(如Cache-Control,Etag,Last-Modified 与 If-Modified-Since等)做出对应的缓存策略
  4. 浏览器下次发送同样的请求,先在本地查找有没有对应的缓存,如果有直接返回结果

为了更加直白的演示,我们可以看一下简单的流程图:

image.png

HTTP缓存策略

根据缓存策略的不同,HTTP缓存策略可以分为强缓存和协商缓存。

强缓存

强缓存是指在浏览器请求资源时,优先判断本地缓存是否可用。若缓存未过期,则无需向服务端发送请求,直接返回本地的缓存。其核心依赖于响应头中的Cache Control和Expires字段

Cache Contorl: 通过指定有效时间实现浏览器精准缓存的关键字段,有以下几个常用指令

常用指令 作用
max-age 指定缓存过期的时间,以秒为单位。在该时间内,浏览器直接从本地缓存
pubcic 表示资源可以被中间任何代理服务器缓存,适用于不需要用户验证的公共资源
private 表示该资源只有客户端可以缓存,常用于用户私密资源
no-cache 强制浏览器在使用缓存前,必须先向服务器发送请求进行验证。
no-store 表示禁止任何缓存,每次请求资源都向服务端发送新的请求

Expires: 指定资源过期的绝对时间,但由于依赖服务器与客户端的时间同步,存在误差风险,逐渐被 Cache-Control 替代。

Expires: Wed, 15 Apr 2026 03:19:25 GMT

强缓存的优势在于极大地减少了网络请求的次数。若是过期时间到了,浏览器又会怎么处理呢?

协商缓存

当强缓存失效时,协商缓存发挥作用。浏览器就会携带缓存标签向服务端发送请求,确认资源是否发生变化。如果没有发生变化,则返回304状态,让浏览器使用本地缓存。如果发生变化,则返回200状态,重新返回资源,浏览器会将收到的资源重新缓存到本地。

协商缓存主要依靠 ETagLast-Modified 两组字段

ETag/If-None-Match: 服务端为资源生成的唯一指纹ETag,浏览器再次发送请求时携带If-None-Match,服务端通过比对过后,若资源未修改,返回304状态,浏览器直接使用本地缓存,若资源已修改,则返回新的资源。

Last-Modified/If-Modidied-Sence: 作用机制与ETag类似,不同的是Last—Modified通过是通过资源最后的修改时间进行比对。

两者对比

精确度上:ETag是通过服务端计算出来的hash值作为标识,而Last-Modified是通过时间作为标识且单位为秒,如果资源在一秒内连续变化,那么Last-Modified就不能准确得到最新的数据。

性能上: ETag是通过计算得出的hash值,无疑增加了服务端负载,而Last-Modified只是返回一个时间。在性能上Modified要优于ETag。

服务器需要同时考虑精准度和性能问题,服务器可依据资源特性灵活选择或组合使用 ETag 与 Last-Modified。

总结

通过协商缓存,即使强缓存失效,也能通过少量数据交换(仅验证标识)避免重复传输完整资源,在动态内容更新场景中尤为重要。

协商缓存和强缓存并非孤立存在,而是相互配合、分阶段发挥作用,共同构建高效的 HTTP 缓存体系。

缓存位置

那么从HTTP缓存的资源放在哪里呢? HTTP缓存资源的位置主要分为客户端缓存和服务端缓存。

客户端缓存

客户端缓存可以分为内存缓存和磁盘缓存。

  1. 内存缓存是位于浏览器运行时占用的内容空间中,是浏览器优先使用的缓存区域。其特点是读写速度特别快,适用于需要快速响应的资源。
  2. 磁盘缓存是将数据存储在用户的磁盘当中,相比内存缓存,磁盘缓存容量更大,可存储更多资源,且数据不会因浏览器关闭而丢失,能长期保存,不过读取速度比较慢。常用于资源比较大,不常更新的资源。

浏览器缓存

除了客户端缓存以外,服务端自身也会进行缓存用于减轻服务端的压力。服务端缓存可以分为服务器本地缓存和内容分发网络缓存(CDN)。

服务器本地缓存:服务器可在本地设置缓存机制,将频繁访问的数据或资源存储在内存(如使用 Redis 等内存数据库)或磁盘中。当接收到相同请求时,直接从本地缓存返回结果,减少数据库查询或文件读取的开销。

内容分发网络(CDN):CDN 是一种分布式的缓存系统,由分布在全球各地的边缘节点服务器组成。当用户请求资源时,CDN 会将请求分配到离用户最近的节点,该节点若缓存了对应资源,就直接返回给用户,大大缩短了网络传输距离,提升加载速度。

结语

HTTP 缓存是 Web 性能优化的 “基石”,看似简单却暗藏玄机。掌握这些面试常考点:

  • 强缓存和协商缓存的区别与配合
  • 常见缓存标识的作用
  • 缓存位置与适用场景

理解这些逻辑后,面试问到的时候就不会再手足无措了。

image.png

创作不易 礼貌集赞

使用 openapi-typescript-codegen 自动生成 openapi 接口文档的 TypeScript 类型

2025年5月19日 17:20

安装依赖

npm i openapi-typescript-codegen -D

配置生成脚本

这里配置了只生成接口数据类型的定义,需要生成每个 API 的请求封装需要修改 参数

"generate-api": "npx openapi-typescript-codegen --input http://xxx/v2/api-docs --output ./src/api/generated --exportCore false --exportServices false",

脚本中的 http://xxx/v2/api-docs 换成你接口文档的 JSON 数据源地址,可以在浏览器控制台找到

image.png

生成结果

执行 npm run generate-api 后就能看到一堆类型定义

image.png

使用

然后就可以愉快的使用自动生成的类型定义了

image.png

❌
❌