普通视图

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

CSS 迎来重大升级:Chrome 137 支持 if () 条件函数,样式逻辑从此更灵活

作者 大知闲闲i
2025年7月3日 16:26

一、CSS if () 函数:前端样式编程的新范式

从 Chrome 137 版本开始,CSS 正式引入if()条件函数,这是继 CSS 变量、网格布局后又一革命性特性。该函数允许在样式声明中直接编写条件逻辑,彻底改变了传统 CSS 依赖媒体查询、属性选择器实现条件渲染的模式。

核心语法与逻辑结构

property: if(
  condition-1: value-1;
  condition-2: value-2;
  /* 更多条件... */
  else: default-value);
  • 条件类型:支持media()(媒体查询)、supports()(功能检测)、style()(样式值判断)三种条件表达式
  • 执行逻辑:按条件顺序从上至下判断,满足首个条件即返回对应值,全部不满足则返回else默认值

二、三大条件表达式深度解析

1. media ():替代传统 @media 查询

场景:根据屏幕宽度动态调整导航栏布局

nav {
  display: if(
    media(min-width: 768px): flex; /* 平板以上横排 */
    else: block); /* 移动端竖排 */
  
  background-color: if(
    media(hover: hover): rgba(0,0,0,0.9); /* 支持悬停时半透明 */
    else: #000); /* 触摸设备纯黑 */
}

优势

  • 条件逻辑与样式声明同屏展示,避免代码碎片化
  • 支持嵌套在任意属性中,比@media更精准控制单一属性

2. supports ():智能兼容处理

场景:根据浏览器能力自动切换渲染方案

.hero-section {
  backdrop-filter: if(
    supports(backdrop-filter: blur(20px)): blur(20px); /* 新浏览器毛玻璃 */
    else: none); /* 旧浏览器无效果 */
  
  background: if(
    supports(backdrop-filter: blur(20px)): rgba(255,255,255,0.3); /* 配合毛玻璃半透明 */
    else: #fff); /* 纯背景色 */
}

最佳实践

  • 优先检测supports()再处理样式,避免冗余代码
  • 可结合style()实现「能力检测 + 状态判断」复合逻辑

3. style ():基于变量的状态响应

场景:根据元素数据属性动态切换主题色

<button class="btn" data-state="primary">提交</button>
<button class="btn" data-state="secondary">取消</button>

.btn {
  --state: attr(data-state);
  color: if(
    style(--state: primary): white;
    style(--state: secondary): #333;
    else: #666);
  
  background: if(
    style(--state: primary): #409EFF;
    style(--state: secondary): #F5F7FA;
    border: if(
      style(--state: secondary): 1px solid #DCDFE6;
      else: none);
}

技术要点

  • 通过attr()获取元素属性值并赋值给 CSS 变量
  • style()可直接比较变量值,实现类似 JavaScript 的if (state === 'primary')逻辑

三、三大应用场景实战

1. 响应式布局优化:卡片网格自适应

.grid-container {
  display: grid;
  grid-template-columns: if(
    media(min-width: 1200px): repeat(4, 1fr); /* 大屏4列 */
    media(min-width: 768px): repeat(3, 1fr); /* 平板3列 */
    else: repeat(2, 1fr)); /* 手机2列 */
  
  gap: if(
    media(hover: hover): 24px; /* 鼠标设备宽间距 */
    else: 16px); /* 触摸设备窄间距 */
}

对比传统方案

  • 传统方案需写 3 个@media块,现合并为 1 个属性声明
  • 直接控制grid-template-columns属性,避免全局布局切换

2. 交互状态动态样式:按钮加载态

<button class="action-btn" data-loading="false">提交</button>




.action-btn {
  --loading: attr(data-loading);
  cursor: if(
    style(--loading: true): wait; /* 加载中显示沙漏 */
    else: pointer); /* 常态显示小手 */
  
  opacity: if(
    style(--loading: true): 0.6; /* 加载中半透明 */
    else: 1);
  
  &::before {
    content: if(
      style(--loading: true): '加载中...'; /* 加载中显示文字 */
      else: '提交');
  }
}

动态效果

  • 通过 JS 修改data-loading属性,样式自动响应
  • 无需额外类名切换,保持 HTML 结构简洁

3. 暗黑模式智能适配

<html data-theme="light">
  <body class="page">...</body>
</html>




.page {
  --theme: attr(data-theme);
  color: if(
    style(--theme: dark): #f5f5f5; /* 暗黑模式浅色文字 */
    else: #333); /* 亮色模式深色文字 */
  
  background: if(
    style(--theme: dark): #121212; /* 暗黑模式深色背景 */
    else: #f9f9f9); /* 亮色模式浅色背景 */
  
  /* 智能适配系统主题 */
  background: if(
    media(prefers-color-scheme: dark) and not(style(--theme: light)): #121212;
    else: #f9f9f9);
}

逻辑拆解

  1. 优先使用元素自身data-theme属性
  2. 若未指定主题,自动检测系统暗黑模式
  3. 所有逻辑集中在一个选择器中,维护成本降低 50%

四、兼容性与最佳实践

1. 浏览器支持策略

  • 当前支持:Chrome 137+、Edge 137+(需开启实验性特性)

  • 降级方案

    .fallback {
      /* 基础样式(旧浏览器生效) */
      color: #333;
      
      /* 新特性增强(新浏览器覆盖) */
      color: if(style(--theme: dark): #f5f5f5; else: #333);
    }
    

2. 性能优化要点

  • 避免多层嵌套

    /* 推荐 */
    font-size: if(media(min-width: 768px): 18px; else: 16px);
    
    /* 不推荐(嵌套过深影响解析性能) */
    font-size: if(media(min-width: 1200px): if(media(hover: hover): 20px; else: 18px); else: 16px);
    
  • 条件顺序优化:将高频条件放在前面

3. 工程化集成建议

  • 结合 CSS 变量管理

    :root {
      --mobile-breakpoint: 768px;
    }
    
    .component {
      width: if(media(min-width: var(--mobile-breakpoint)): 50%; else: 100%);
    }
    
  • 配合 PostCSS 插件:在低版本浏览器中自动转换if()为传统语法

五、未来展望:CSS 逻辑编程的新纪元

if () 函数的引入标志着 CSS 从「声明式样式」向「逻辑式样式」的重要跨越。未来可能延伸的特性包括:

  1. 循环函数:如for()实现动态生成样式

  2. 数学表达式:支持calc()与条件的组合运算

  3. 函数嵌套:在条件中调用自定义 CSS 函数

对于前端开发者,掌握 if () 将成为必备技能 —— 它不仅简化样式逻辑,更推动 CSS 向完整「样式编程语言」演进,让界面动态性与维护性达到新高度。

IEEE 754 双精度浮点数标准,最大整数和最大的数字

2025年7月3日 16:02

基础:

  1. 所有的数字,都是用 64位双精度浮点数 表示,其内存结构分为3部分

[1位符号位][11位指数位][52位尾数位] 来存储的

符号位:决定数字的正负(0是正数,1是负数)

指数位:表示2的幂次(采用偏移码表示,实际指数 = 存储值 - 1023)

范围:-1022 到 1023(特殊值 0 和 2047用于表示0和无穷大)

尾数位/有效数字(52 bits + 隐含位)

  • 关键点:实际精度是 53 bits(52位显式存储 + 1位隐含的"1") 采用"隐含前导1"的表示法(normalized numbers)

综上: 数值的计算公式为:

image.png

最大整数Number.MAX_SAFE_INTEGER

2^53 -1

最大值:Number.MAX_VALUE

image.png

在Vue项目中启用HTTPS本地开发

作者 Oriel
2025年7月3日 15:51

现代前端开发中,启用HTTPS本地环境越来越重要。本文将详细介绍如何在Vue项目中配置HTTPS开发服务器,使用mkcert工具生成可信证书,解决跨域问题并模拟真实生产环境。


为什么需要HTTPS本地开发?

  1. 使用浏览器新特性(如地理位置API)
  2. 解决第三方登录(OAuth)的localhost限制
  3. 测试Service Worker和PWA功能
  4. 避免混合内容警告(Mixed Content)
  5. 模拟真实生产环境行为

完整配置步骤

1️⃣ 安装mkcert证书工具

# 查看系统架构
uname -m

# 根据架构下载(示例为Apple Silicon)
curl -LO https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-darwin-arm64
chmod +x mkcert-v1.4.4-darwin-arm64
sudo mv mkcert-v1.4.4-darwin-arm64 /usr/local/bin/mkcert

# 验证安装
mkcert --version  # 应输出 v1.4.4

2️⃣ 创建并安装本地CA

# 安装本地证书颁发机构
mkcert -install

✅ 系统钥匙串将出现名为 "mkcert development CA" 的证书

3️⃣ 生成域名证书

# 为自定义域名和本地地址生成证书
mkcert your.domain.com "*.your.domain.com" localhost 127.0.0.1 ::1

# 重命名证书文件
mv your.domain.com+4-key.pem localhost-key.pem
mv your.domain.com+4.pem localhost-cert.pem

4️⃣ Vue项目配置

// vue.config.js
const fs = require('fs');
const path = require('path');

module.exports = {
  devServer: {
    https: {
      key: fs.readFileSync(path.resolve(__dirname, 'localhost-key.pem')),
      cert: fs.readFileSync(path.resolve(__dirname, 'localhost-cert.pem'))
    },
    port: 8443,  // 推荐HTTPS端口
    host: 'your.domain.com',
    headers: {
      'Access-Control-Allow-Origin': '*' // 解决跨域问题
    }
  },
  // 重要:配置Webpack使用正确的主机名
  chainWebpack: config => {
    config.plugin('define').tap(args => {
      args[0]['process.env'].BASE_URL = '"https://your.domain.com:8443"';
      return args;
    });
  }
}

5️⃣ 配置本地hosts文件

# /etc/hosts 添加
127.0.0.1    your.domain.com
::1          your.domain.com

6️⃣ 启动项目并验证

npm run serve

访问 your.domain.com:8443
🔒 浏览器地址栏应显示安全锁标志


高级配置技巧

解决Chrome证书错误

若出现 NET::ERR_CERT_INVALID 错误:

  1. 打开 chrome://flags/#allow-insecure-localhost
  2. 将选项设为 Enabled

跨域请求配置

// 在axios全局配置
import axios from 'axios';
axios.defaults.baseURL = 'https://your.domain.com:8443';
axios.defaults.withCredentials = true;

自动重定向HTTP

// src/main.js
if (process.env.NODE_ENV === 'development') {
  if (window.location.protocol === 'http:') {
    window.location.href = `https://${window.location.host}${window.location.pathname}`;
  }
}

常见问题解决方案

问题现象 解决方案
ERR_OSSL_EVP_UNSUPPORTED 升级Node到v18+ 或设置环境变量 export NODE_OPTIONS=--openssl-legacy-provider
证书不被信任 在钥匙串中双击CA证书,设置为"始终信任"
HMR热更新失效 在vue.config.js中添加 client: { webSocketURL: 'wss://your.domain.com:8443/ws' }
iOS真机测试 手机安装CA证书:mkcert -CAROOT 找到rootCA.pem发送到手机安装

优化建议

  1. 添加环境检测 - 自动切换开发/生产环境
// .env.development
VUE_APP_API_BASE=https://your.domain.com:8443
  1. 脚本自动化 - 创建证书生成脚本
#!/bin/bash
mkcert -install
mkcert your.domain.com "*.your.domain.com" localhost 127.0.0.1 ::1
mv your.domain.com+4-key.pem localhost-key.pem
mv your.domain.com+4.pem localhost-cert.pem
echo "证书已更新!"
  1. Git忽略证书 - 避免证书提交
# .gitignore
localhost-key.pem
localhost-cert.pem

通过本文配置,你的Vue项目将获得:

  • ✅ 浏览器信任的HTTPS连接
  • ✅ 自定义域名开发环境
  • ✅ 避免跨域问题的API调用
  • ✅ 接近生产环境的开发体验
  • ✅ 支持移动设备真机测试

安全提示:切勿将生成的localhost-*.pem证书文件提交到版本控制或用于生产环境!此方案仅适用于本地开发。

前端架构模式哪家强?MVC vs MVVM 深度剖析,附三大框架 Vue、React、Angular 实战

作者 極光未晚
2025年7月3日 15:43

前端架构模式哪家强?MVC vs MVVM 深度剖析,附三大框架 Vue、React、Angular 实战

在前端开发的江湖中,架构模式犹如大侠们的绝世武功秘籍,选对了便能在代码的江湖中披荆斩棘,MVC 与 MVVM 便是其中备受瞩目的两大 “神功”。今天,咱们就来深入剖析这两种架构模式,再结合 Vue、React、Angular 三大框架的实战案例,看看谁才是最适合你的 “武功秘籍”。

MVC:经典的 “三板斧”

MVC 即 Model - View - Controller,是一种历史悠久且广泛应用的架构模式,就像江湖中流传已久的经典武功,有着清晰的招式套路。

Model:数据的 “藏经阁”

Model 层负责管理应用程序的数据和业务逻辑,它就像是一个庞大的藏经阁,存储着各种珍贵的 “数据秘籍”。在一个电商应用中,商品信息、用户订单数据等都归 Model 管。它不仅存储数据,还负责处理数据的增删改查等操作,保障数据的一致性和完整性。以下是一个简单的 JavaScript 示例,模拟一个管理用户数据的 Model:

// 用户数据Model
const userModel = {
    users: [],
    addUser: function (user) {
        this.users.push(user);
    },
    getUserById: function (id) {
        return this.users.find(user => user.id === id);
    }
};

View:用户的 “视觉盛宴”

View 层专注于将 Model 中的数据呈现给用户,是用户与应用交互的可视化界面。它就像是一场精心编排的视觉盛宴,通过 HTML、CSS 和 JavaScript(用于动态交互效果),把数据以美观、易用的方式展示出来。比如电商应用的商品列表页面、购物车页面等,用户看到的各种界面元素都是 View 层的杰作。以下是一个简单的 HTML 和 JavaScript 结合的 View 示例,用于展示用户列表:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>User View</title>
</head>
<body>
    <ul id="userList"></ul>
    <script>
        const userList = document.getElementById('userList');
        const renderUsers = function (users) {
            userList.innerHTML = '';
            users.forEach(user => {
                const li = document.createElement('li');
                li.textContent = `ID: ${user.id}, Name: ${user.name}`;
                userList.appendChild(li);
            });
        };
    </script>
</body>
</html>

Controller:协调的 “武林盟主”

Controller 层起着桥梁的作用,连接着 View 和 Model。它接收用户在 View 层的操作,比如点击按钮、提交表单等,然后根据这些操作决定调用 Model 层的哪些方法来处理数据,最后再根据 Model 层返回的结果,决定如何更新 View 层。它如同武林盟主,协调各方,确保整个应用的流畅运行。在电商应用里,当用户点击 “加入购物车” 按钮时,Controller 会捕获这个事件,调用 Model 层添加商品到购物车的方法,然后通知 View 层更新购物车的显示。以下是结合前面 Model 和 View 示例的 Controller 代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>User MVC Example</title>
</head>
<body>
    <ul id="userList"></ul>
    <button id="addUserButton">Add User</button>
    <script>
        // 用户数据Model
        const userModel = {
            users: [],
            addUser: function (user) {
                this.users.push(user);
            },
            getUserById: function (id) {
                return this.users.find(user => user.id === id);
            }
        };
        const userList = document.getElementById('userList');
        const renderUsers = function (users) {
            userList.innerHTML = '';
            users.forEach(user => {
                const li = document.createElement('li');
                li.textContent = `ID: ${user.id}, Name: ${user.name}`;
                userList.appendChild(li);
            });
        };
        const userController = {
            init: function () {
                const addUserButton = document.getElementById('addUserButton');
                addUserButton.addEventListener('click', () => {
                    const newUser = { id: Date.now(), name: 'New User' };
                    userModel.addUser(newUser);
                    renderUsers(userModel.users);
                });
                renderUsers(userModel.users);
            }
        };
        userController.init();
    </script>
</body>
</html>

MVC 的优缺点

MVC 的优点十分显著,它的职责划分清晰,使得团队开发时,不同成员可以专注于自己负责的层,提高开发效率。而且代码的可维护性和可扩展性也较好,就像一座结构稳固的大厦,后续修改和添加功能都相对容易。不过,它也并非完美无缺。随着应用变得复杂,Controller 可能会变得臃肿不堪,就像一个承担过多任务的武林盟主,力不从心。而且,Model 和 View 之间的同步需要手动处理,这无疑增加了开发的工作量和出错的概率。

MVC 在框架中的体现:以 Angular 为例

在 Angular 框架中,MVC 模式有着典型的应用。在 Angular 应用里,服务(Service)常常扮演着 Model 的角色,负责数据的获取和处理。例如,创建一个获取用户信息的服务:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
    providedIn: 'root'
})
export class UserService {
    private apiUrl = 'https://example.com/api/users';
    constructor(private http: HttpClient) {}
    getUsers(): Observable<any[]> {
        return this.http.get<any[]>(this.apiUrl);
    }
}

视图模板(Template)就是 View 层,通过 HTML 结合 Angular 的指令(Directive),将数据展示给用户。比如,使用*ngFor指令可以方便地遍历数据列表并展示在页面上:

<ul>
    <li *ngFor="let user of users">{{ user.name }}</li>
</ul>

控制器(Controller)则是连接两者的纽带,在 Angular 中,组件(Component)承担了部分 Controller 的职责。组件接收用户在视图上的操作,调用服务中的方法来处理数据,然后更新视图。例如,在一个用户登录组件中:

import { Component, OnInit } from '@angular/core';
import { UserService } from '../user.service';
@Component({
    selector: 'app - user - login',
    templateUrl: './user - login.component.html',
    styleUrls: ['./user - login.component.css']
})
export class UserLoginComponent implements OnInit {
    users: any[] = [];
    constructor(private userService: UserService) {}
    ngOnInit(): void {
        this.userService.getUsers().subscribe(users => {
            this.users = users;
        });
    }
}

MVVM:进化的 “新派武功”

MVVM 即 Model - View - ViewModel,是在 MVC 基础上发展而来的一种新架构模式,如同新派武功,融合了前辈的精华并有所创新。

Model:初心未改的数据担当

在 MVVM 中,Model 层的职责与 MVC 中的 Model 类似,依然负责管理数据和业务逻辑。它就像一位坚守岗位的老侠客,不管江湖格局如何变化,始终守护着数据的 “江湖”。同样以用户数据为例,Model 的代码可以如下:

const userModel = {
    users: [],
    addUser: function (user) {
        this.users.push(user);
    },
    getUserById: function (id) {
        return this.users.find(user => user.id === id);
    }
};

View:专注展示的 “门面担当”

View 层同样负责呈现数据给用户,不过在 MVVM 中,它与 Model 层的耦合度更低,更加专注于展示功能。它就像一家店铺的精美橱窗,只负责将商品(数据)以吸引人的方式展示出来,而不关心商品是如何进货(数据如何获取和处理)的。一个简单的 HTML 视图示例如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>User View</title>
</head>
<body>
    <ul id="userList"></ul>
</body>
</html>

ViewModel:神奇的 “数据粘合剂”

ViewModel 是 MVVM 的核心创新之处,它就像一位神奇的粘合剂大师,将 View 和 Model 巧妙地连接在一起。ViewModel 负责暴露 Model 中的数据,使其能方便地被 View 使用,同时处理 View 的交互逻辑。它还通过数据绑定机制,实现了 View 和 Model 之间的自动同步。也就是说,当 Model 中的数据发生变化时,ViewModel 会自动通知 View 更新显示;反之,当用户在 View 上进行操作导致数据变化时,ViewModel 也会自动更新 Model。比如在一个实时聊天应用中,当收到新消息时(Model 数据变化),ViewModel 会自动让 View 更新聊天记录的显示;用户发送新消息(View 操作),ViewModel 会将新消息数据更新到 Model 中。以下是一个简单的 MVVM 示例代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>MVVM Example</title>
</head>
<body>
    <ul id="userList"></ul>
    <button id="addUserButton">Add User</button>
    <script>
        const userModel = {
            users: [],
            addUser: function (user) {
                this.users.push(user);
            },
            getUserById: function (id) {
                return this.users.find(user => user.id === id);
            }
        };
        const userViewModel = {
            users: userModel.users,
            addUser: function () {
                const newUser = { id: Date.now(), name: 'New User' };
                userModel.addUser(newUser);
            }
        };
        const userList = document.getElementById('userList');
        const renderUsers = function () {
            userList.innerHTML = '';
            userViewModel.users.forEach(user => {
                const li = document.createElement('li');
                li.textContent = `ID: ${user.id}, Name: ${user.name}`;
                userList.appendChild(li);
            });
        };
        const addUserButton = document.getElementById('addUserButton');
        addUserButton.addEventListener('click', userViewModel.addUser);
        const observer = new MutationObserver(() => {
            renderUsers();
        });
        const targetNode = userViewModel;
        const config = { attributes: true, childList: true, subtree: true };
        observer.observe(targetNode, config);
        renderUsers();
    </script>
</body>
</html>

MVVM 的优缺点

MVVM 最大的优势在于数据绑定带来的高效开发体验。开发人员无需手动频繁地更新 View 以同步 Model 的数据变化,大大减少了代码量和出错的可能性,开发效率大幅提升。而且,由于 View 和 Model 通过 ViewModel 解耦,使得代码的可测试性和可维护性都得到了增强。当然,它也有一些小麻烦。对于初学者来说,理解 ViewModel 和数据绑定机制可能需要花费一些时间和精力,就像学习新派武功需要先领悟其独特的内功心法。另外,如果在应用中过度使用数据绑定,可能会对性能产生一定影响。

MVVM 在框架中的体现:以 Vue 和 React 为例

Vue 中的 MVVM 实现

Vue 是 MVVM 模式的典型代表,数据绑定是其核心特性之一。在 Vue 中,通过data选项定义的数据就是 Model 层的数据。例如:

new Vue({
    el: '#app',
    data: {
        message: 'Hello, Vue!'
    }
});
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>Vue MVVM Example</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <p>{{ message }}</p>
    </div>
</body>
</html>

当message数据发生变化时,视图会自动更新。Vue 还通过v - model指令实现了双向数据绑定,特别适用于表单元素。例如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>Vue v - model Example</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <input v - model="message">
        <p>{{ message }}</p>
    </div>
    <script>
        new Vue({
            el: '#app',
            data: {
                message: ''
            }
        });
    </script>
</body>
</html>

这样,输入框的值会与message数据实时同步,用户输入会更新message,message的变化也会实时显示在输入框中。

React 中的 MVVM 体现

React 虽然没有像 Vue 那样直接实现双向数据绑定,但通过单向数据流和状态管理机制,也体现了 MVVM 的思想。在 React 中,组件的状态(state)可以看作是 Model 层的数据。例如:

import React, { useState } from'react';
import ReactDOM from'react - dom';
function MyComponent() {
    const [count, setCount] = useState(0);
    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
}
ReactDOM.render(<MyComponent />, document.getElementById('root'));
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF - 8">
    <title>React MVVM Example</title>
    <script src="https://unpkg.com/react@17/umd/react.development.js"></script>
    <script src="https://unpkg.com/react - dom@17/umd/react - dom.development.js"></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

这里的count就是数据,setCount函数用于更新数据。当count变化时,组件会重新渲染,视图也随之更新,这类似于 MVVM 中 Model 变化自动更新 View 的机制。虽然 React 本身没有内置双向数据绑定,但通过一些库(如 Formik 等)可以实现类似的双向绑定效果,用于处理表单等场景。

MVC 与 MVVM 对比总结

关注点分离

MVC 中,Controller 承担了较多的职责,包括协调 Model 和 View 的交互以及处理用户输入等,View 与 Controller 和 Model 的联系较为紧密。而 MVVM 更加明确地将视图和模型的逻辑分离,ViewModel 专注于处理视图的展示逻辑和与模型的交互,View 相对更加独立,对 Model 的依赖较少。

数据绑定机制

MVC 中通常需要手动在 Controller 中更新 View,而 MVVM 强调数据绑定,模型的变化可以自动反映在视图上,减少了大量手动更新视图的代码,开发效率更高。

代码结构与维护

MVC 的代码结构在大型项目中可能会显得较为复杂,Controller 容易变得臃肿,导致维护难度增加。MVVM 的代码结构相对更加清晰,逻辑层次分明,使得代码的维护和扩展更加容易。

测试难度

MVC 中 Controller 和 View 的耦合度较高,可能会增加测试的难度,需要更多地考虑两者之间的交互。MVVM 由于 ViewModel 的存在,使得测试更加容易聚焦和隔离,可以方便地对 Model、ViewModel 和 View 进行单独测试。

结语

MVC 和 MVVM 各有千秋,MVC 作为经典架构模式,在传统 Web 应用开发等领域依然有着广泛的应用,其清晰的职责划分对于理解和构建应用有很大帮助。而 MVVM 作为后起之秀,凭借数据绑定等特性,在现代前端开发,尤其是单页应用(SPA)和移动应用开发中大放异彩,大大提升了开发效率和用户体验。

在实际项目中,选择 MVC 还是 MVVM,需要根据项目的具体需求、团队的技术栈以及开发者对不同模式的熟悉程度来决定。就像大侠们选择武功秘籍,适合自己的才是最好的。希望通过本文的介绍,你能对 MVC 和 MVVM 有更深入的理解。

老板突然发问 公司OA系统突然无法登录 我慌了

作者 poloma
2025年7月3日 15:40

前言

有一天,老板突然登录后台管理系统,说系统卡住了,无法登录,让我马上和运维排查问题

从点击登录到卡住,返回请求错误码足足等待了超过30S,老板挂面,我可能有被裁风险

image.png

排查问题

点击登录以后,前端请求后端的接口,这里的流程是前端通过跨域请求实现的。 方式是通过 nginx 服务器代理请求后端的接口的。

我们定位到 nginx 处理跨域请求的时候的配置上。

Nginx 在使用 proxy_pass 进行域名代理时,可能会出现缓存问题,导致后端服务更新后客户端无法及时获取最新内容。这通常由多种因素引起,包括 Nginx 自身缓存、上游服务器缓存头设置或浏览器缓存策略等诸多因素,我们一个个进行排查

一,确认缓存的来源

在解决问题前,需先确定缓存位置:

  1. 浏览器缓存:通过开发者工具(如 Chrome 的 Network 面板)检查响应头中的 Cache-Control 和 Expires
  2. Nginx 代理缓存:检查 Nginx 配置是否启用了 proxy_cache 或相关指令。
  3. 上游服务器缓存:检查后端应用是否设置了缓存头。

很容易我们就排除了浏览器的缓存和上游服务器后端的缓存,前端浏览器这边 Cache-Control Expires 已经设置好响应头,同时后端应用也设置好了缓存头

接下来我们将问题定位到运维的位置

二、禁用 Nginx 代理缓存

在处理跨域请求(CORS)时,Nginx 需要同时解决两个关键问题:跨域头配置禁用缓存。以下是针对这两个需求的配置方案:

一、基础配置:跨域头 + 禁用缓存

server {
    listen 80;
    server_name your-domain.com;
    
    # 处理跨域请求
    location /api/ {
        # 代理配置
        proxy_pass http://backend-api-server/;  # 后端 API 服务器地址
        
        # 跨域头配置
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE' always;
        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' always;
        
        # 允许携带凭证(如 cookies)
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        
        # OPTIONS 请求直接返回 204
        if ($request_method = 'OPTIONS') {
            return 204;
        }
        
        # 禁用缓存配置
        proxy_no_cache $cookie_nocache $arg_nocache $arg_comment;
        proxy_cache_bypass $cookie_nocache $arg_nocache $arg_comment;
        
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
        
        # 其他代理常用配置
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

二、高级配置:针对不同请求类型优化

1. 静态资源与动态 API 差异化处理
server {
    # 静态资源(允许缓存)
    location /static/ {
        proxy_pass http://backend-static/;
        # 默认缓存策略(根据需要调整)
        expires 7d;
    }
    
    # API 请求(禁用缓存)
    location /api/ {
        proxy_pass http://backend-api/;
        
        # 跨域头
        add_header 'Access-Control-Allow-Origin' '*' always;
        # ... 其他 CORS 头
        
        # 禁用缓存
        proxy_no_cache 1;
        proxy_cache_bypass 1;
        add_header Cache-Control "no-cache, no-store, must-revalidate" always;
        add_header Pragma "no-cache" always;
        add_header Expires "0" always;
    }
}
2. 仅对特定 API 禁用缓存
location /api/no-cache/ {
    proxy_pass http://backend/;
    
    # 禁用缓存
    proxy_no_cache $arg_nocache;
    proxy_cache_bypass $arg_nocache;
    
    # 动态添加 nocache 参数来控制是否缓存
    # 例如:/api/no-cache/data?nocache=1
}
3. 针对常用写死的域名,让每次请求都进行 DNS 查询

企业微信截图_17514472935097.png

http {
    resolver 8.8.8.8 valid=10s;

    server {
        listen 80;

        location / {
            set $upstream_host a.b.com;
            proxy_pass https://$upstream_host;
            proxy_set_header Host $upstream_host;
        }
    }
}

三、验证配置是否生效

1. 检查响应头
curl -I http://your-domain.com/api/data

# 预期输出:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE
Access-Control-Allow-Headers: DNT,X-Mx-ReqToken,...
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0
2. 浏览器开发者工具验证

在 Chrome/Firefox 中:

  1. 打开开发者工具 → Network 面板
  2. 发送请求并检查响应头
  3. 确认没有 (from cache) 标记

通过以上配置,可确保 Nginx 在处理跨域请求时正确禁用缓存,同时满足 CORS 安全要求。根据具体业务需求调整配置细节,平衡性能与安全性。

三、 常见问题与解决方案

缓存仍未完全禁用

  • 问题原因

    • 上游服务器设置了缓存头,覆盖了 Nginx 的配置
    • CDN 缓存未清除
    • 浏览器强缓存
  • 解决方案

    • 使用 proxy_ignore_headers Cache-Control Expires 忽略上游缓存头
    • 若使用 CDN,在 CDN 控制台刷新缓存
    • 要求用户使用 Ctrl+F5(Windows)或 Command+Shift+R(Mac)强制刷新

Nginx 配置生效部分

修改配置后需重启或重载 Nginx:

sudo nginx -s reload

总结

前端针对这种问题需要提前做预防措施,为了避免

预防措施

  1. 开发阶段
  • 使用代理服务器处理跨域(如 Vite 的 server.proxy
  • 在浏览器开发者工具中始终启用「Disable cache」
  1. 生产环境
  • 对静态资源使用版本化(如 app.js?v=1.0.1
  • 为 API 请求设置合理的缓存策略(如频繁更新的数据使用 no-cache
  • 定期审查服务器和 CDN 的缓存配置
解决方案总结
问题类型 解决方案
跨域错误 1. 修改服务器 Nginx 配置,添加正确的 CORS 头
2. 使用代理服务器(如 Vite/Webpack 代理)
3. 确保请求配置与服务器允许的范围一致
浏览器缓存 1. 在开发者工具中禁用缓存
2. 添加响应头 Cache-Control: no-cache
3. 请求 URL 添加随机参数
服务器缓存 1. 修改后端代码,禁用缓存头
2. 检查 Nginx 配置,确保正确设置 proxy_no_cache
CDN 缓存 1. 在 CDN 控制台刷新缓存
2. 调整 CDN 缓存规则(如设置 Cache-Control: no-cache

至此完结

常见简单的知识点

2025年7月3日 15:37

在编程中,?? 是一个 空值合并运算符(Nullish Coalescing Operator) ,主要用于提供默认值。它的作用如下:

语法:

leftExpression ?? rightExpression

行为:

  • 如果 leftExpression 的值为 null 或 undefined,则返回 rightExpression(即默认值)。
  • 否则,直接返回 leftExpression 的值。

示例:

const value1 = null ?? 'default';      // 输出: 'default'(因为左侧是 null)
const value2 = undefined ?? 'fallback'; // 输出: 'fallback'(因为左侧是 undefined)
const value3 = 0 ?? 42;                // 输出: 0(因为左侧不是 null/undefined)
const value4 = '' ?? 'hello';          // 输出: ''(因为左侧不是 null/undefined)

与 || 的区别:

  • || 运算符会对左侧的 假值(falsy) (如 0''falsenullundefined)触发默认值。
  • ?? 仅对 null 或 undefined 触发默认值,更精确。
const a = 0 || 42;   // 输出: 42(因为 0 是假值)
const b = 0 ?? 42;   // 输出: 0(因为 0 不是 null/undefined)

在 Vue 3 中,unref 是一个 响应式工具函数,用于获取一个响应式引用(Ref)的 内部值。如果传入的参数本身不是 Ref,则直接返回该参数。

作用

unref 的作用可以理解为:

  • 如果传入的是 Ref 对象(如 ref() 创建的),则返回它的 .value
  • 如果传入的不是 Ref,则直接返回原值。

源码实现

function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? ref.value : ref;
}

使用场景

  1. 简化 Ref 和普通值的访问

    • 在不确定某个变量是 Ref 还是普通值时,可以用 unref 安全地获取值。
    • 避免手动判断 isRef 再取值。
  2. 在组合式函数(Composables)中处理参数

    • 允许函数同时接受 Ref 或普通值,提高灵活性。

示例

基本用法
import { ref, unref } from 'vue';

const count = ref(1);
const num = 2;

console.log(unref(count)); // 输出: 1(相当于 count.value)
console.log(unref(num));   // 输出: 2(直接返回 num)
在组合式函数中使用
import { ref, unref, computed } from 'vue';

// 该函数可以接受 Ref<number> 或 number
function double(value) {
  const unwrapped = unref(value); // 安全取值
  return unwrapped * 2;
}

const a = ref(3);
const b = 4;

console.log(double(a)); // 输出: 6(a 是 Ref)
console.log(double(b)); // 输出: 8(b 是普通值)
与 toRef 对比
  • toRef:将响应式对象的属性转换为 Ref
  • unref:从 Ref 中提取值(反向操作)。
import { reactive, toRef, unref } from 'vue';

const state = reactive({ foo: 1 });
const fooRef = toRef(state, 'foo');

console.log(unref(fooRef)); // 输出: 1

注意事项

  • unref 不会解除深层响应式(如 reactive 对象),它仅处理 Ref
  • 如果需要深度解包(如嵌套 Ref),可以使用 toRaw 或第三方工具(如 vue-utils 的 deepUnref)。

总结

unref 是 Vue 3 响应式系统中一个轻量级的工具函数,主要用于:

  1. 统一处理 Ref 和普通值。

  2. 在组合式函数中增加参数灵活性。
    它的存在让代码更简洁,避免重复的 isRef 判断。

在 Vue 3 的响应式系统中,toRaw 和 toRef 是两个用途完全不同的工具函数,它们的核心区别如下:

1. toRaw:获取原始非响应式对象

作用
  • 返回一个响应式对象(reactive 或 readonly 创建的)的 原始普通对象(剥离所有响应式特性)。
  • 对 ref 对象,返回其 .value 的原始值(如果 .value 是响应式对象)。
使用场景
  • 需要直接操作原始数据,避免响应式开销(如性能敏感场景)。
  • 临时修改数据但不想触发响应式更新。
示例
import { reactive, toRaw } from 'vue';

const obj = reactive({ foo: 1 });
const rawObj = toRaw(obj); // 原始对象 { foo: 1 }

console.log(rawObj === obj); // false(rawObj 是非响应式的普通对象)

// 修改原始对象不会触发响应式更新
rawObj.foo = 2; 
console.log(obj.foo); // 2(值变化,但不会触发视图更新)
注意事项
  • 对 ref 对象,toRaw(ref) 等价于 toRaw(ref.value)

2. toRef:将响应式对象的属性转换为 Ref

作用
  • 为响应式对象(reactive)的某个属性创建一个 关联的 Ref 引用
  • 修改 Ref 会同步到原始对象,反之亦然。
使用场景
  • 需要将响应式对象的某个属性单独作为 Ref 传递,保持响应式关联。
  • 在组合式函数中解构属性时保持响应性。
示例
import { reactive, toRef } from 'vue';

const state = reactive({ foo: 1 });
const fooRef = toRef(state, 'foo'); // 创建 foo 的 Ref

// 修改 Ref 会同步到原对象
fooRef.value = 2;
console.log(state.foo); // 2

// 修改原对象也会更新 Ref
state.foo = 3;
console.log(fooRef.value); // 3
与 ref 的区别
  • ref(1) 创建一个独立的 Ref,与原对象无关。
  • toRef(state, 'foo') 创建的 Ref 和原对象的 foo 属性保持双向绑定。

核心区别对比

函数 作用对象 返回值类型 是否保持响应式关联 典型用途
toRaw reactive/readonly 原始普通对象 ❌ 完全剥离响应式 获取原始数据,避免响应式开销
toRef reactive 对象的属性 Ref ✅ 双向同步 解构属性并保持响应性

结合使用的场景

import { reactive, toRef, toRaw } from 'vue';

const state = reactive({ foo: { bar: 1 } });

// 将响应式对象的属性转为 Ref
const fooRef = toRef(state, 'foo'); 

// 获取 Ref 的原始值(非响应式)
const rawFoo = toRaw(fooRef.value); 
console.log(rawFoo); // { bar: 1 }(普通对象)

总结

  • toRaw:用于“降级”响应式对象,获取原始数据(非响应式)。
  • toRef:用于“升级”响应式对象的属性为 Ref,保持响应式关联。
  • 两者互补,分别处理响应式系统的不同层级需求。

在 JavaScript 中,不同类型的循环有不同的 停止(中断)方式,它们的用途和特性也有显著区别。以下是详细对比和完整示例:

1. 循环类型对比

循环方法 适用对象 能否被停止? 停止方式 返回值 特点
forEach 数组 ❌ 不能直接停止 抛出异常(不推荐) undefined 简洁,但无法中断
map 数组 ❌ 不能停止 新数组 返回新数组,不改变原数组
for...in 对象(枚举属性) ✅ 可以用 break break / return - 遍历键名(包括原型链属性)
for...of 可迭代对象(数组、字符串等) ✅ 可以用 break break / return - 遍历值(忽略原型链属性)
for 通用 ✅ 可以用 break break / return - 灵活控制循环条件

2. 完整示例代码

(1) forEach:无法直接停止

const arr = [1, 2, 3];
arr.forEach(item => {
  console.log(item);
  if (item === 2) {
    // 无法直接停止!只能通过抛出异常(不推荐)
    throw new Error('强行停止');
  }
});
// 输出: 1, 2, Error

(2) map:无法停止,始终返回新数组

const arr = [1, 2, 3];
const newArr = arr.map(item => {
  console.log(item);
  return item * 2;
});
console.log(newArr); // [2, 4, 6]

(3) for...in:遍历对象键名,可用 break 停止

const obj = { a: 1, b: 2, c: 3 };
for (const key in obj) {
  console.log(key, obj[key]); // 输出键名和值
  if (key === 'b') break; // 停止循环
}
// 输出: a 1, b 2

(4) for...of:遍历可迭代对象的值,可用 break 停止

const arr = [1, 2, 3];
for (const item of arr) {
  console.log(item);
  if (item === 2) break; // 停止循环
}
// 输出: 1, 2

(5) for 循环:经典循环,完全可控

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
  if (arr[i] === 2) break; // 停止循环
}
// 输出: 1, 2

3. 如何选择循环方式?

场景 推荐循环方式 原因
需要中途停止循环 for / for...of / for...in 支持 break 和 return
遍历数组并返回新数组 map 简洁,自动返回新数组
遍历数组但不需要返回值 forEach 语法简单,但无法中断
遍历对象属性(包括继承属性) for...in 遍历键名,但需用 hasOwnProperty 过滤原型链属性
遍历可迭代对象(数组、字符串等) for...of 直接遍历值,比 for...in 更适合数组

4. 特殊情况处理

forEach 模拟中断(不推荐)

const arr = [1, 2, 3];
try {
  arr.forEach(item => {
    console.log(item);
    if (item === 2) throw new Error('Stop');
  });
} catch (e) {
  if (e.message !== 'Stop') throw e;
}
// 输出: 1, 2

for...of + return(在函数中使用)

function findTarget(arr, target) {
  for (const item of arr) {
    if (item === target) return item; // 直接返回并停止循环
  }
  return null;
}
console.log(findTarget([1, 2, 3], 2)); // 2

总结

  • 需要中断循环:优先使用 forfor...of 或 for...in + break
  • 遍历数组并返回新数组:用 map
  • 遍历对象属性:用 for...in(注意过滤原型链属性)。
  • forEach 和 map 无法中断,但 map 会返回新数组。

Reflect 在 Vue 3 中的作用及常用 API 方法

Reflect 是 ES6 引入的一个内置对象,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy 处理器方法一一对应。在 Vue 3 的响应式系统中,Reflect 被广泛使用来实现代理行为。

Reflect 的核心作用

  1. 提供操作对象的标准方法:替代一些传统的 Object 方法
  2. 与 Proxy 配合使用:Proxy 的 trap 通常需要调用对应的 Reflect 方法来完成默认行为
  3. 更规范的返回值:相比传统方法,Reflect 方法有更一致的返回值(如成功返回 true,失败返回 false)

Vue 3 中常用的 Reflect API

1. Reflect.get(target, propertyKey[, receiver])

获取对象属性的值

const obj = { foo: 42 };
console.log(Reflect.get(obj, 'foo')); // 42

2. Reflect.set(target, propertyKey, value[, receiver])

设置对象属性的值

const obj = {};
Reflect.set(obj, 'foo', 123);
console.log(obj.foo); // 123

3. Reflect.has(target, propertyKey)

检查对象是否具有某属性

const obj = { foo: 1 };
console.log(Reflect.has(obj, 'foo')); // true
console.log(Reflect.has(obj, 'bar')); // false

4. Reflect.deleteProperty(target, propertyKey)

删除对象属性

const obj = { foo: 1, bar: 2 };
Reflect.deleteProperty(obj, 'foo');
console.log(obj); // { bar: 2 }

5. Reflect.ownKeys(target)

获取对象所有自身属性键(包括不可枚举和Symbol属性)

const obj = {
  [Symbol('id')]: 123,
  name: 'John'
};
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]
const obj = {
  [Symbol('id')]: 123,
  name: 'John'
};
console.log(Reflect.ownKeys(obj)); // ['name', Symbol(id)]

Vue 3 中使用 Reflect 的案例

案例1:响应式系统中的使用

Vue 3 的响应式系统大量使用 Reflect 与 Proxy 配合:

const reactiveHandler = {
  get(target, key, receiver) {
    track(target, key); // 依赖追踪
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    if (result && oldValue !== value) {
      trigger(target, key); // 触发更新
    }
    return result;
  }
  // 其他trap...
};

function reactive(obj) {
  return new Proxy(obj, reactiveHandler);
}

const state = reactive({ count: 0 });

案例2:组合式API中的使用

import { reactive, watchEffect } from 'vue';

const user = reactive({
  name: 'Alice',
  age: 25
});

// 使用Reflect进行属性操作
function updateUser(key, value) {
  if (Reflect.has(user, key)) {
    Reflect.set(user, key, value);
  } else {
    console.warn(`Property ${key} does not exist`);
  }
}

watchEffect(() => {
  console.log('User updated:', Reflect.ownKeys(user).map(k => `${k}: ${user[k]}`));
});

updateUser('age', 26); // 触发更新
updateUser('email', 'alice@example.com'); // 警告

案例3:自定义Ref实现

import { customRef } from 'vue';

function useDebouncedRef(value, delay = 200) {
  let timeout;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      }
    };
  });
}

// 使用
const text = useDebouncedRef('hello');

案例4:响应式工具函数

import { reactive, toRefs } from 'vue';

function useFeature() {
  const state = reactive({
    x: 0,
    y: 0,
    // 计算属性
    get distance() {
      return Math.sqrt(this.x ** 2 + this.y ** 2);
    }
  });

  function updatePosition(newX, newY) {
    // 使用Reflect批量更新
    Reflect.set(state, 'x', newX);
    Reflect.set(state, 'y', newY);
  }

  return {
    ...toRefs(state),
    updatePosition
  };
}

// 使用
const { x, y, distance, updatePosition } = useFeature();

Reflect 在 Vue 3 中的优势

  1. 与 Proxy 完美配合:每个 Proxy trap 都有对应的 Reflect 方法
  2. 更安全的操作:相比直接操作对象,Reflect 方法提供了更规范的错误处理
  3. 元编程能力:为 Vue 的响应式系统提供了底层支持
  4. 一致性:所有 Reflect 方法都返回布尔值表示操作是否成功

总结

Vue 3 的响应式系统深度依赖 Reflect API 来实现其核心功能。通过 Reflect 与 Proxy 的组合,Vue 能够:

  • 拦截对象操作
  • 跟踪依赖关系
  • 触发更新通知
  • 提供一致的响应式行为

理解 Reflect 的这些用法有助于更好地理解 Vue 3 的响应式原理,并在需要时实现更高级的自定义响应式逻辑。

跨域是什么?跨域的具体解决方案?

2025年7月3日 15:30

跨域:当页面运行中请求的url地址的源和当前页面url地址的源不相同时,就发生了跨域(也叫跨源)

浏览器同源策略会对跨域请求有限制,其中对ajax的限制最为严格:不允许ajax访问跨域资源

源 = 协议 + 主机 + 端口

跨域的解决方案

代理(适用开发环境跨域)

通过配置开发服务器,来进行拦截转发,例如vue项目,可以在vue.config.js中配置

// 配置代理解决、vue.config.js
devServer: {
  proxy: {
    "/api": {
      target: "http://your-backend-api.com", // 目标接口域名
      changeOrigin: true, // 是否启用代理
      pathRewrite: { "^/api": "" } // 重写路径,去掉/api
    }
  }
}

/api 表示开发服务器会拦截路径以/api开头的请求,并转发请求到target的地址,开发服务器拿到响应后,交给浏览器,在浏览器看来这次请求并没有跨域,所以允许ajax访问资源

可以看出,浏览器同源策略只检查协议,主机和端口号,而并不限制端口号后面的路径path,所以代理正是利用path中的信息来瞒过浏览器的限制

Nginx反向代理(适用生产环境跨域)

Nginx反向代理也是利用Nginx服务器转发请求,从而跳过浏览器的限制

server {
  listen 80;
  server_name frontend.com;
  location /api/ {
    proxy_pass http://your-backend-api.com;
    add_header Access-Control-Allow-Origin *;
  }
}
CORS

CORS(跨域资源共享)是后端服务器要进行一些处理。服务器在响应头里增加一些属性,就可以让浏览器允许ajax访问相应资源。CORS对于不同的跨域请求有三种处理模式

1.简单请求

请求方法 请求头
POST/GET/HEAD 没有自定义的请求头. 并且如果请求头中有Content-Type属性,只能是“text/plain”,"multipart/form-data","application/x-www-form-urlencoded"中的一种

对于简单请求,后端响应需在响应头中加入属性:

Access-Control-Allow-Origin:请求的源 (如果是*号,则代表允许所有源)

2.需要预检的请求

如果不满足上面的简单请求判断条件,那就是需要预检的请求 对于需要预检的请求,浏览器在发送真正的请求前,先发送一个预检请求,用于向服务器确认是否允许实际请求

OPTIONS /api/data HTTP/1.1 //预检请求的请求方式为OPTIONS
Host: example.com  //请求的主机
Origin: https://your-site.com //发起请求的源
Access-Control-Request-Method: POST //实际请求将使用的 HTTP 方法
Access-Control-Request-Headers: Content-Type,a,b //实际请求将携带的自定义头字段

服务器如果允许实际请求,则需在响应头中添加以下属性

Access-Control-Allow-Origin: https://your-site.com      //实际请求的源
Access-Control-Allow-Methods: POST, GET, OPTIONS        //需包含实际请求的请求方法
Access-Control-Allow-Headers: Content-Type, a, b  //需包含实际请求的头字段
Access-Control-Max-Age: 86400  // 可选:缓存预检结果的秒数,表示在这个时间内不必再发起预检请求可以直接请求

随后浏览器就可以发真实请求,与简单请求相同,需要响应头中设置

Access-Control-Allow-Origin:请求的源

3.携带身份凭证的请求

前端配置: 默认情况下,ajax的跨域不会附带cookie,需要前端增加配置

fetch: credentials: 'include'

fetch('https://api.example.com/data', {
  method: 'GET',
  credentials: 'include', // 必须设置为 include 以携带 Cookie
  headers: {
    'Content-Type': 'application/json',
  },
})
.then(response => response.json())
.catch(error => console.error('Error:', error));

xhr: xhr.withCredentials = true

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 关键配置,携带 Cookie
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log(xhr.responseText);
  }
};
xhr.send();

服务器配置

Access-Control-Allow-Origin: https://your-frontend.com  // 必须明确指定域名,不能为 *
Access-Control-Allow-Credentials: true  //明确允许凭证cookie
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS  // 允许的方法
Access-Control-Allow-Headers: Content-Type, Authorization  // 允许的请求头

简单请求和需要预检的请求都有可能携带凭证,无论是哪一种,响应头都需要显式声明允许凭证

Access-Control-Allow-Credentials: true //明确允许凭证cookie

Access-Control-Allow-Origin:请求的源 (不能是*)

JSONP(兼容老系统)

JSONP是在没有CORS之前的一种解决方案,通过生成一个script标签,并把src设置为要请求的地址

<script src="http://crossdomin.com/api/user"></script>

并且需要后端返回响应为一个函数,并将前端需要的数据作为函数的参数传入,这样浏览器请求script资源后,会执行js文件,也就会执行这个函数,前端在这个函数里就可以拿到需要的数据。

缺点:只支持GET请求

欢迎指正!

🧠“一次奇怪的 JS/TS 报错,背后竟是分号惹的祸”

2025年7月3日 15:29

引子:看似无害的一行代码,却让整个程序崩溃

在学习 TypeScript 的时候,我曾因为少写了一个分号,触发了一个让人摸不着头脑的编译错误:

类型“Card[]”不能分配给类型“number”

更诡异的是,这行代码乍看之下完全没有问题:

  shuffle() {
    for(let i = 0; i < this.cards.length; i++) {
      const targetIndex:number = this.getRandom(0, this.cards.length)
      [this.cards[i], this.cards[targetIndex]] = [this.cards[targetIndex], this.cards[i]]
    }
  }

但是 TypeScript 却报错了,怎么回事?明明语法也没问题,我们不是常说 JS 的分号是“可选”的嘛?

这次,它可真的不是。


一、JS 的分号真的是“可选”的吗?

很多人(包括以前的我)都习惯不写分号。因为在 JavaScript 中,确实有一种机制叫做 ASI(Automatic Semicolon Insertion,自动分号插入) ,它会在大多数情况下帮我们补上遗漏的分号。

像下面这样的代码,在不加分号的情况下也能正常运行:

let a = 1
let b = 2
console.log(a + b)

JavaScript 会在每一行后面“想当然地”补上分号。但这并不意味着它总能理解你的意图。 事实上,在某些情况下,ASI 会失效,甚至让代码逻辑彻底跑偏。


二、数组解构 + 缺失分号 = 地狱级 Bug

我们再回到那段引起错误的代码:

const targetIndex: number = this.getRandom(0, this.cards.length)
[this.cards[i], this.cards[targetIndex]] = [this.cards[targetIndex], this.cards[i]]

表面上是没问题的,但 JS 引擎的解释却可能出乎意料:

const targetIndex: number = this.getRandom(...) [this.cards[i], ...]

没错,JavaScript 把第二行开头的 [ 误以为是上一行函数调用的下标访问。它以为你在写:

this.getRandom(...)[...]

而不是一个新的解构赋值语句。

这就是 ASI 的一个坑点:当下一行以 [ 开头时,它不会自动插入分号


三、除了 [,还有哪些坑?

除了数组开头,JS 的 ASI 机制还有一些常见的“踩雷点”:

  • 下一行以 [ 开头(数组、解构)
  • 下一行以 ( 开头(函数调用、立即执行函数)
  • 上一行以 ++-- 结尾(自增/自减)

比如

const x = 123
[x, y] = [y, x]

JS 实际上会尝试当成一行执行:

const x = 123[x, y] = [y, x]

这当然是非法语法,也会抛出莫名其妙的错误。


四、怎么避免这种问题?

其实很简单:不要赌 JS 会自动帮你加分号,关键地方自己加!

写代码的时候,我们往往觉得“少个分号没什么大不了”,但有时候,这一丢,就像埋下了一颗“定时炸弹”,在你最不希望出错的时候,炸了出来。

规范很重要。少些分号看起来“优雅”,但出了问题你可是要多花两小时去 debug 的。

写好每一行代码,从一个分号开始 ✨

面试官:React Diff 算法原理?我:三个假设 + O(n) 复杂度,手撕给你看!

作者 Kincy
2025年7月3日 15:25

🧠 系列前言:

面试题千千万,我来帮你挑重点。每天一道,通勤路上、蹲坑时、摸鱼中,技术成长不设限!本系列主打幽默 + 深度 + 面霸必备语录,你只管看,面试场上稳拿 offer!

💬 面试官发问:

"React 的 Virtual DOM diff 算法是怎么实现的?为什么要用 key?三个假设是什么?"

哎呀,这题经典得像老北京炸酱面,每次面试都会遇到。Virtual DOM diff,听起来高大上,其实就是 React 的"比较专家",专门负责找出新旧虚拟 DOM 的差异。

🎯 快答区(初级背诵版)

React 的 diff 算法基于三个假设:同层级比较、不同类型元素会产生不同的树、通过 key 来标识列表元素。算法复杂度从 O(n³) 优化到 O(n),主要通过单层遍历、类型判断和 key 匹配来实现高效更新。

面试官:嗯,还行,但我想听点你背不出来的内容 😏

🧠 深入理解:Diff 算法的江湖传说

📌 1. 为什么需要 Diff 算法?

想象一下,你有一个超大的购物清单,每次修改都要重新写一遍

// 旧清单
const oldList = ['苹果', '香蕉', '橙子', '葡萄'];
// 新清单(只是把香蕉换成了芒果)
const newList = ['苹果', '芒果', '橙子', '葡萄'];

笨方法:撕掉重写 → 浪费纸张 → 环保局找上门
聪明方法:找出差异,只改"香蕉"→"芒果" → 省时省力

React 的 diff 算法就是这个聪明方法,它要在新旧两棵虚拟 DOM 树中找出最小的变化,然后只更新需要变化的部分。

🎯 2. 传统 Diff 的噩梦:O(n³) 复杂度

如果要完美比较两棵树的差异,传统算法需要:

  1. 遍历树 A 的每个节点:O(n)
  2. 遍历树 B 的每个节点:O(n)
  3. 计算编辑距离:O(n)

总复杂度:O(n³)

对于 1000 个节点的应用,就是 10 亿次操作! React 团队一看:这样下去,用户要等到花儿都谢了!

🧠 3. React 的三个"天才假设"

React 团队经过观察发现,实际开发中:

假设 1:同层级比较

不同层级的节点很少会移动

// 这种情况很少发生
<div>
  <span>Hello</span>  // 从这里
</div>
<p>
  <span>Hello</span>  // 移动到这里
</p>

所以 React 只比较同一层级的节点,跨层级直接删除重建

假设 2:不同类型 = 不同树

不同类型的元素会产生不同的树

// 从 div 变成 span
<div>Hello</div>  →  <span>Hello</span>

React 直接删除旧树,创建新树,不做进一步比较!

假设 3:Key 是列表的身份证

开发者可以通过 key 来暗示哪些子元素是稳定的

// 有了 key,React 就知道谁是谁
{items.map(item => 
  <Item key={item.id} data={item} />
)}

基于这三个假设,React 把 O(n³) 优化成了 O(n)

🎢 4. Diff 算法的三大战场

战场1:Tree Diff(树级别)

// 情况1:同类型节点
<div className="old">         <div className="new">
  <span>Hello</span><span>Hello</span>
</div>                        </div>
// 结果:只更新 className

// 情况2:不同类型节点  
<div>Hello</div>  →  <span>Hello</span>
// 结果:删除 div,创建 span

战场2:Component Diff(组件级别)

// 情况1:同类型组件
<MyComponent name="old" />  →  <MyComponent name="new" />
// 结果:更新 props,可能触发重新渲染

// 情况2:不同类型组件
<ComponentA />  →  <ComponentB />
// 结果:卸载 A,挂载 B

战场3:Element Diff(元素级别)

这是最复杂的战场,主要处理列表更新

// 老列表
['A', 'B', 'C', 'D']
// 新列表  
['A', 'C', 'D', 'B']

没有 key 的情况

  • React 按顺序比较
  • B→C,C→D,D→B,最后插入 B
  • 需要 3 次更新 + 1 次插入

有 key 的情况

  • React 知道 B 只是移动了位置
  • 直接移动 B 到末尾
  • 只需要 1 次移动操作

🔍 5. Key 的选择艺术

❌ 错误示范

// 用 index 当 key(面试官最爱问的反面教材)
{items.map((item, index) => 
  <Item key={index} data={item} />
)}

问题:当列表顺序改变时,key 和内容的对应关系就乱了!

✅ 正确示范

// 用稳定的唯一标识当 key
{items.map(item => 
  <Item key={item.id} data={item} />
)}

🎨 6. Diff 算法的核心实现逻辑

function diff(oldVNode, newVNode) {
  // 1. 节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    return { type: 'REPLACE', newVNode };
  }
  
  // 2. 文本节点,比较内容
  if (typeof newVNode === 'string') {
    if (oldVNode !== newVNode) {
      return { type: 'TEXT', newVNode };
    }
    return null;
  }
  
  // 3. 同类型元素,比较属性和子节点
  const propsPatches = diffProps(oldVNode.props, newVNode.props);
  const childrenPatches = diffChildren(oldVNode.children, newVNode.children);
  
  if (propsPatches.length || childrenPatches.length) {
    return { type: 'UPDATE', propsPatches, childrenPatches };
  }
  
  return null;
}

🎯 7. 列表 Diff 的核心算法

React 使用了一个聪明的双指针算法

function diffChildren(oldChildren, newChildren) {
  let oldStartIdx = 0, newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 头头比较
    if (sameVnode(oldChildren[oldStartIdx], newChildren[newStartIdx])) {
      // 相同节点,继续比较
      diff(oldChildren[oldStartIdx], newChildren[newStartIdx]);
      oldStartIdx++;
      newStartIdx++;
    }
    // 尾尾比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newEndIdx])) {
      diff(oldChildren[oldEndIdx], newChildren[newEndIdx]);
      oldEndIdx--;
      newEndIdx--;
    }
    // 头尾比较
    else if (sameVnode(oldChildren[oldStartIdx], newChildren[newEndIdx])) {
      // 需要移动节点
      moveNode(oldChildren[oldStartIdx], 'after', oldChildren[oldEndIdx]);
      oldStartIdx++;
      newEndIdx--;
    }
    // 尾头比较
    else if (sameVnode(oldChildren[oldEndIdx], newChildren[newStartIdx])) {
      moveNode(oldChildren[oldEndIdx], 'before', oldChildren[oldStartIdx]);
      oldEndIdx--;
      newStartIdx++;
    }
    // 都不匹配,查找表
    else {
      findAndMove();
    }
  }
  
  // 处理剩余节点
  handleRemainingNodes();
}

💬 面试中可以抛出的装 X 语录

  • "React 的 diff 算法是一种启发式算法,通过合理的假设将复杂度从 O(n³) 降到 O(n)。"
  • "Key 不仅是性能优化的手段,更是帮助 React 理解组件身份的重要线索。"
  • "Same level comparison 是 React diff 的核心思想,体现了工程化中的权衡艺术。"
  • "双指针算法在列表 diff 中的应用,让我看到了算法在实际场景中的优雅实现。"

(说完记得停顿两秒,喝一口水,看面试官点头)

✅ 总结一句话

React Diff 算法 = 基于三个假设的启发式算法,通过同层比较、类型判断和 key 匹配,实现了 O(n) 复杂度的高效 DOM 更新,它是 React 性能优化的核心基石。

🔚 系列结尾:明日继续爆料!

明天继续来一道面试题,咱们聊聊 React Hooks 的原理和闭包陷阱(别看 Hooks 用得溜,原理可能比你想象的更有趣🪝)。

📌 点赞 + 收藏 + 关注系列,让你成为面霸不是梦!

React vs Vue:谁才是轻量级框架的真命天子?

作者 極光未晚
2025年7月3日 15:23

React vs Vue:谁才是轻量级框架的真命天子?

前端江湖的框架之争从未停歇,当团队面临技术选型,Vue 和 React 谁更轻量成为高频争议话题。其实这就像问跑车和越野车谁更快 —— 脱离场景谈结论都是耍流氓,咱们从更多维度掰开揉碎了看!

一、框架核心:代码体积的原始较量

Vue 3 的代码就像精简版瑞士军刀,未压缩的生产环境版本仅约 22.6KB,gzip 压缩后直接 “瘦身” 到 6.4KB 左右 。在 HTML 中引入 Vue 3 简直像请了个轻装上阵的帮手:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Vue示例</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
</head>
<body>
  <div id="app">{{ message }}</div>
  <script>
    const app = Vue.createApp({
      data() {
        return {
          message: 'Hello, Vue!'
        }
      }
    });
    app.mount('#app');
  </script>
</body>
</html>

反观 React,其核心库 React 和 React DOM 合体后,未压缩体积约 100KB,gzip 压缩后仍有 32KB 左右。使用 React 时,除了引入库,还得借助 Babel 处理 JSX 语法:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>React示例</title>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    ReactDOM.render(
      <div>Hello, React!</div>,
      document.getElementById('root')
    );
  </script>
</body>
</html>

仅从核心库体积看,Vue 确实更轻盈,但这只是冰山一角。

二、运行时刻:性能占用的隐形战场

Vue 的 “聪明更新”

Vue 通过模板静态分析实现 “精准打击”。以下面的 Vue 组件为例:

<template>
  <div>
    <h1>固定标题</h1>
    <p>{{ dynamicText }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      dynamicText: '初始文本'
    }
  },
  methods: {
    updateText() {
      this.dynamicText = '更新后的文本';
    }
  }
}
</script>

当调用updateText方法时,Vue 能识别<h1>是静态内容,只重新渲染<p>标签,大幅减少计算量。

React 的 “严格管控”

React 依靠虚拟 DOM 的 diff 算法,但在复杂组件树中容易触发 “连带反应”。看这个 React 组件示例:

import React, { useState } from'react';
const ParentComponent = () => {
  const [count, setCount] = useState(0);
  return (
    <div>
      <ChildComponent />
      <button onClick={() => setCount(count + 1)}>点击计数</button>
    </div>
  );
};
const ChildComponent = () => {
  return <p>我是子组件</p>;
};
export default ParentComponent;

每次点击按钮更新count时,即使ChildComponent与count无关,也可能因父组件重新渲染而触发自身重新渲染,在大型项目中,这种情况可能导致性能损耗。

三、生态依赖:隐形的体积膨胀剂

Vue 的生态像个默契的小团队,官方库 Vue Router、Vuex 与框架无缝衔接。以 Element UI 为例,按需引入按钮组件仅需:

import { Button } from 'element-ui';
export default {
  components: {
    ElButton: Button
  }
}

React 的生态则像个大型集市,引入 Redux 全家桶(Redux、Redux - Thunk、Redux - Persist 等)时,代码量和体积会显著增加。配置 Redux 基本架构如下:

// store.js
import { createStore, applyMiddleware } from'redux';
import thunk from'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);
export default store;

如果依赖管理不当,React 项目很容易 “发福”。

四、跨平台开发:多端适配的能力比拼

Vue 的跨端方案

Vue 通过 uni-app、Taro 等框架实现跨平台开发。以 uni-app 为例,编写一次代码,就能同时发布到微信小程序、H5、APP 等多个平台。比如开发一个简单的计数器应用:

<template>
  <view>
    <text>{{ count }}</text>
    <button @click="increment">+1</button>
  </view>
</template>
<script>
export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};
</script>

这种 “一套代码,多端运行” 的模式,极大减少了开发成本,对于中小团队快速拓展多端业务十分友好。

React 的跨端表现

React 在跨平台领域也有 React Native 和 Expo 等方案。以 React Native 开发移动端应用为例,使用原生组件渲染,能实现接近原生应用的性能:

import React, { useState } from'react';
import { View, Text, Button } from'react-native';
const App = () => {
  const [count, setCount] = useState(0);
  return (
    <View>
      <Text>{count}</Text>
      <Button title="增加" onPress={() => setCount(count + 1)} />
    </View>
  );
};
export default App;

不过,React Native 在环境配置、原生模块集成等方面相对复杂,上手难度较高,但在打造高性能移动端应用时,优势明显。

五、开发者社区:资源与支持的对比

Vue 社区

Vue 社区以 “友好、活跃” 著称,官方文档详细且易于理解,新手指南手把手教学。在技术论坛和问答平台上,关于 Vue 的问题总能快速得到解答。例如在 Vue 的官方论坛,开发者们会分享各种实战经验、插件使用技巧,还有许多优质的开源项目模板可供参考,帮助新手快速成长。

React 社区

React 社区凭借 Facebook 的支持和庞大的开发者群体,资源极其丰富。GitHub 上每天都有大量与 React 相关的项目更新,从复杂的状态管理库到炫酷的动画效果库应有尽有。但由于生态庞大,新手可能会在海量资源中迷失方向,需要花费更多时间筛选适合自己项目的工具和方案。

六、与其他技术栈融合:扩展性的差异

Vue 的融合

Vue 与 TypeScript、Sass 等技术的融合非常自然。例如在 Vue 项目中使用 TypeScript,只需简单配置,就能利用其静态类型检查功能提升代码质量和可维护性:

<template>
  <div>{{ message }}</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  data() {
    return {
      message: 'Hello, Vue with TypeScript!'
    };
  }
});
</script>

React 的融合

React 与 GraphQL、RxJS 等技术结合,能构建强大的数据驱动型应用。以 React 结合 GraphQL 为例,通过 Apollo Client 库,可以轻松实现高效的数据请求和管理:

import React from'react';
import { ApolloClient, InMemoryCache, ApolloProvider, gql } from '@apollo/client';
const client = new ApolloClient({
  uri: 'https://your - graphql - endpoint.com',
  cache: new InMemoryCache()
});
const GET_DATA = gql`
  query {
    // 具体查询语句
  }
`;
const App = () => (
  <ApolloProvider client={client}>
    {/* 应用组件 */}
  </ApolloProvider>
);
export default App;

七、项目场景:轻量性的终极裁判

小型项目用 Vue 就像开电瓶车逛胡同,轻便灵活。假设开发一个简单的待办事项应用,Vue 实现起来轻松惬意:

<template>
  <div>
    <input v-model="newTask" placeholder="添加任务">
    <button @click="addTask">添加</button>
    <ul>
      <li v-for="(task, index) in tasks" :key="index">{{ task }}</li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      newTask: '',
      tasks: []
    }
  },
  methods: {
    addTask() {
      if (this.newTask) {
        this.tasks.push(this.newTask);
        this.newTask = '';
      }
    }
  }
}
</script>

大型企业级项目中,React 则像重型卡车,虽然自身 “吨位” 大,但能通过组件化架构高效运输复杂业务。例如搭建一个电商后台管理系统,通过 React 的组件拆分和状态管理,可以轻松应对海量数据和复杂交互。

Vue 在核心库和部分场景下确实更轻盈,但 React 凭借强大的扩展性和生态,在合理使用时同样能实现轻量高效。技术选型就像相亲,没有绝对完美的框架,只有最适合项目需求和团队基因的选择。下次选型时,不妨带着这些对比思路,亲自试驾一番!

Java后端项目的前端基础Vue补充学习篇(一)

作者 阑梦清川
2025年7月3日 15:21

1.前言

最近在学习程序员鱼皮大佬的相关的项目教程,但是觉得自己太菜了,后端的这个很多的技术栈都不是很理解,对于前端的一些东西,也是完全不懂,之前在学习这个用户中心项目的时候了解过一些这个相关的内容,但是当时这个用户中心项目怎么起都起不来,后面也就放弃了,但是当时的那个项目基本上都是前后端完成了的;

这次在这个程序员鱼皮的编程导航上面学习的是关于聚合搜索和这个在线OJ的这个相关的内容,这个里面的一些技术我不是很了解,所以打算学习一下;

但是这两个项目都是使用的vue进行前端的设计,虽然我可以直接跳过去,但是我知道即使我拉去别人的代码,这个环境配置的问题也很难正确的运行起来,而且这个前后端的联调之类的,肯定是需要我知道这个前端的基础的,因此我决定还是学习一下这个相关的vue的内容把,毕竟用到的还是很多的,也是早晚的事情,肯定是逃脱不掉的;

2.工程创建

2.1vue-cli脚手架创建

我看程序员鱼皮的这个项目里面目前主要使用的还是这个脚手架创建的这个方式,但是官网上面针对于这个脚手架目前是维护模式了,更加推荐我们使用这个vite进行这个项目的创建;

image-20250703140607644

我选择的是尚硅谷的教程,因此尚硅谷的老师针对于这个教程,主要也是使用的这个vite,也就是下面介绍的这个第二种方式进行这个项目的创建的,但是脚手架的方式大家可以去官网上面进行查阅,也是可以的,后面我尝试去学习聚合搜索和在线判题系统的时候,也是会写一写自己的这个笔记的,搭建项目的这个流程,自己的这个理解之类的,到时候还是使用的这个脚手架的方式进行的这个项目的创建;

2.2vite创建

vite和webpack是比较类似的,但是这个打包的速度是非常的快速的:如下图所示

image-20250703141939414

可以看到,这个vite进行项目创建的时候,这个速度是很快的;

2.3项目创建

image-20250703142925185

下面的这个是项目的启动方式,dev是我们的这个项目里面的一个文件夹里面的东西,文件夹的名字叫做package.json

image-20250703144400998

跳转之后,这个进入的就是我们的vue默认的这个界面

image-20250703144430691

顺便说一下,这个默认打开这个初始化的项目之后,这个里面其实是没有这个终端的,这个时候如果你想要输入这个npm的命令,肯定是需要把这个终端调用出来的,这个vscode之前我使用不是很多,即使是使用的话,也是有这个Python的这个直接运行显示出来这个终端的,这个我看尚硅谷的老师很快的就把这个终端调用出来的,我自己也是第一次知道这个方法,帮助一下可能和我一样这个编程的基础不是很好的同学:

使用ctrl+反引号(数字键1左边的那个按键)就可以把这个终端调用出来,然后就可以把输入这个npm run dev命令 把这个项目跑起来了,跑起来了之后,我们就可以点击这个给出来的这个链接直接进入这个前端的页面,在浏览器里面看到这个初始的效果拉;

2.4关于插件的安装

为什么使用这个vscode进行这个前端的开发,因为这个vue在vscode里面精心准备了这个对应的插件,非常的舒服,但是我看的这个尚硅谷老师的教程的笔记里面使用的是下面的这两个插件:

image-20250703145902058

但是我找了很久都没有找到,后来发现这个插件已经被放弃了,换成了新的插件,大家需要注意一下:上面的两个插件都是集成在了下面的这个插件里面

image-20250703145950868

其实这个在弹幕里面也是有说的,但是我嫌浙江华海金额个弹幕太乱了,所以把这个弹幕关闭了,所以我觉得有些时候这个弹幕还是很有用的;

下面的这个就是集成的新的插件,安装这个新的即可:

image-20250703150146767

2.5代码分析

image-20250703150346183

下面的这个是我们的mount指向的这个内容,因此大家也应该发现了,这个index。html里面可以没有其他的,但是这个id和这个script是一定需要有的;

image-20250703150501592

2.6如何把这个脚手架停下来

我们的这个npm run dev之后,这个脚手架就是处于运行的这个状态了,这个时候,我们如果想要把这个脚手架停下来,这个时候可以使用ctrl+C,这个时候会问你确认删除吗,你选择yes即可,这个就是退出我们的这个项目,这个时候你可以输入其他的这个指令了;

image-20250703150719638

2.7总结

image-20250703151352739

Augment code + axure 一键生成前端代码

上一篇文章我们讲了怎么通过Augment code + Figma MCP生成前端代码,同时我也用了cursor,做了简单对比,结果如下: cursor + Figma MCP

  1. 速度快。
  2. 可以导出Figma文件中的图标到项目中。
  3. 整体还原度不咋地。
  4. 对话式,会不断询问。

Augment code + Figma MCP

  1. 速度慢。
  2. 还原度高。
  3. 基本不需要手动干预。
  4. 访问项目整体性好,能检测到项目是否已经配置依赖等等。

今天我们同样使用Augment code + axure来演示,通过axure设计文档直接生成前端项目,同时和cursor + axure生成前端项目做个对比。

一、axure导出设计文件到本地。

打开axure -> 发布 -> 生成html文件 -> 选择本地文件 -> 导出到本地,删除多余的html文件。

image.png

image.png

二、将文件放在初始化的项目中。

我使用的vue3,我初始化了vue3项目,将刚才生成的html文件和一些css/js文件拖到了src目录下面,放在html文件夹中。配置了elementPlus+scss(这些都可以交给Agument来做,但是有点慢,我就顺手配置了,它会检测整个项目,如果做了配置就不会再重新配置了)

image.png

三、发布指令,让Augment为我们生成页面。

这是我简单些的指令

1. 读取src/html文件夹里面的所有html文件,不包含子目录。
2. 根据html文件生成vue3+elementplus+scss的页面。
3. 配置路由跳转。
4. 封装公共组件。
5. icon使用iconfont,统一风格。

接下来就是Augment code吭哧吭哧写代的过程了,全程基本不需要干预,写完可能有一些错误,缺少图片的错误很常见,我们可以直接复制错误信息给我它,它会全局处理。

四、Augment + axure和cursor + axure区别

cursor + axure

  1. 速度快
  2. 需要不停地对话
  3. 还原度低
  4. 他会推荐直接将html转换vue文件。

Augment + axure

  1. 速度慢
  2. 全程基本不需要管
  3. 还原度高
  4. 对项目整体检测性好。

useAsyncState 异步更新数据,在异步回调调用成功后再更新值

2025年7月3日 15:10

场景描述

image.png

当有需要切换状态并同时调用接口的情况下,为了体验更好,希望点击tab的时候并不真正的更新tab的状态,要直到接口调用成功后再更新。

因此就期望有如下代码,让这种异步更新数据的操作更简单明了。

const [activeKey, setActiveKey, activeKeyRef] = useAsyncState('wait', async (data) => {
  await reloadAsync(data)
})

代码

代码不多,就不过多讲解了

function useAsyncState <T>(
  initData: T,
  callback?: (data?: T) => void | Promise<void>
): [
  T,
  (data: T) => Promise<void>,
  React.RefObject<T>
] {
  const [data, setData] = useState<T>(initData)
  const tempRef = useRef<T>(initData)

  const setDataProxy = async (data: T) => {
    tempRef.current = data
    await callback?.(data)
    setData(data)
  }

  return [data, setDataProxy, tempRef]
}

react 基础API

作者 Rubin93
2025年7月3日 15:03

1. useEffect

  1. 浏览器重新绘制之后触发
  2. 参数(setup,dependencies?)
  3. 异步执行
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

2. useLayoutEffect

  1. 浏览器重新绘制之前触发
  2. 参数(setup,dependencies?)
  3. 同步执行,会阻塞DOM渲染
  4. 挂载组件时执行setup,依赖更新时先执行cleanup,再执行setup

3. useReducer

const [state, dispatch] = useReducer(reducerFn, initiaValue, inituaValueFn?)

reducerFn:处理函数,参数:state, action,返回值:新的state
initiaValue:state默认值
inituaValueFn:默认值函数,用于初始化state,优先执行,非必填


const reducerfn = (state, action) => {
if (action==='inc') {
return state++
}
if (action==='dec') {
return state--
}
}
const [state, dispatch] = useReducer(reducerfn, -1, ()=> {
// 高于默认值的优先级
return 0
})

<Button onClick={() => dispatch('inc')}>增加</Button>
<Button onClick={() => dispatch('dec')}>减少</Button>

4. useSyncExternalStore

useSyncExternalStore 用于从外部存储(例如状态管理库、浏览器 API等)获取状态并在组件中同步显示。跟踪外部状态。

useSyncExternalStore 使用场景:

  1. 订阅外部 store 例如(redux,mobx,Zustand,jotai)
  2. 订阅浏览器AP| 例如(online,storage,location, history hash)等

抽离逻辑,编写自定义hooks

const res = useSyncExternalStore(subscribe, getSnapshot, getSeeverSnapShot?)
sybscribe:订阅数据源的变化,接受一个回调函数在数据源更新时调用此函数
getSnapShot:获取当前数据源的快照(当前信息)
getSeeverSnapShot:服务端使用

// hooks/useStorage.tsx
import { useSyncExternalStore } from 'react'
export const useStorage = (key: string, initiaValue: number) => {
const subscribe = (callback: () => void) => {
window.addEventListener('storage', callback)
return () => {
window.removeEventListener('storage', callback)
}
}
const getSnapShot = () => {
return localStorage.getItem(key) ? JSON.parse(localStorage.getItem(key)!) : initiaValue
}
const data = useSyncExternalStore(subscribe, getSnapShot)
const setData = (value: any) => {
localStorage.setItem(key, JSON.stringify(value))
// 手动触发eventStorage事件,通知订阅
window.dispatchEvent(new Event('storage'))
}
return [data, setData]
}

// views/page.tsx
import { useStorage } from "./hooks/useStorage";
function App() {
  const [token, setToken] = useStorage('token', 0)
  return (
    <>
      <h1>hooks useStorage</h1>
      <div>
        <p> token is {token} </p>
        <button onClick={() => setToken(token + 1)}> token ++ </button>
        <button onClick={() => setToken(token - 1)}> token -- </button>
      </div>
    </>
  )
}
export default App

5. useTransition

useTransition 用于优化用户界面响应性的 Hook,它允许你在不阻塞用户交互的前提下,优雅地处理状态更新带来的加载状态(如“挂起”或“过渡”)

useTransition 作用:

  • 状态更新通常是同步的,调用 setState 会立即重新渲染组件树,可能会导致页面卡顿,特别是在处理复杂计算或大量数据更新时。useTransition 将某些状态更新标记为“非紧急”(过渡态),从而优先处理更紧急的 UI 更新(比如输入框的响应) 。

useTransition 使用场景:

  1. 数据加载时的“pending”状态展示
  2. 页面切换时的平滑过渡
  3. 大型表单提交等场景

6. useDeferredValue

useDeferredValue 用于优化用户界面响应性的 Hook,它允许你在不阻塞渲染的前提下,延迟更新某些非紧急的状态值,从而保持 UI 的流畅性。

useDeferredValue 作用:

  • 延迟使用新的值进行渲染,优先显示旧值,直到浏览器空闲时再更新为新值。

useDeferredValue 使用场景:

  1. 搜索输入框的自动补全
  2. 列表滚动时的平滑更新
  3. 大量数据展示时的渐进式渲染

7. useDeferred 对比 useTransition

特性 useDeferredValue useTransition
控制对象 值(value) 状态更新(state update)
是否需要包装副作用
是否有 isPending 状态
适用场景 延迟更新某个值(如列表、文本) 控制状态更新优先级,显示 loading 状态

8. useImperativeHandle & forwardRef

用于自定义暴露子组件方法或属性给父组件调用的 Hook,它通常与 forwardRef 一起使用。

import { useImperativeHandle, forwardRef, useRef } from "react";

// 子组件,必须配合 forwardRef
const Child_ = forwardRef((props, ref)=>{
  const Child_reff = useRef(null)
  const focusInput = () => { 
    Child_reff.current!.focus()
  }
  const selectInput = () => {
    Child_reff.current!.select()
  }
  const BlurInput = () => {
    Child_reff.current!.blur()
  }
  // 暴露方法给父组件
  useImperativeHandle(ref,() => ({
    focus: focusInput,
    select: selectInput,
    blur: BlurInput,
  }))
  return <input ref={Child_reff} type="text" />
})

// 父组件
function Parent_() {
  const inputRef = useRef(null);
  const handleFocus = () => {
    inputRef.current!.focus(); // 调用子组件方法
  };
  const handleSelect = () => {
    inputRef.current!.select(); // 调用子组件方法
  };
  const handleBlur = () => {
    inputRef.current!.blur(); // 调用子组件方法
  };
  return (
    <>
        <div>
          <h1>hooks useImperativeHandle forwardRef</h1>
          <p> useImperativeHandle  </p>
          <br />
          <Child_ ref={inputRef} />
          <button onClick={handleFocus}>输入框聚焦</button>
          <button onClick={handleSelect}>选中输入框文本</button>
          <button onClick={handleBlur}>输入框失焦</button>
      </div>
    </>
  )
}

export default Parent_

你一定要知道前端开发效率飞起来的神器:Emment

作者 一大树
2025年7月3日 15:01

在前端开发中,编写HTML、CSS和JavaScript代码时,重复的标签、属性和样式常常让人感到繁琐。Emment(前身为Zen Coding)是一款强大的代码缩写工具,通过简单的快捷键或缩写语法,能瞬间生成复杂的代码结构,大幅提升开发效率。无论是新手还是资深开发者,掌握Emment都能让你的编码体验“飞”起来!


一、Emment是什么?

Emment是一种基于CSS选择器的缩写引擎,它允许开发者通过简短的缩写快速生成HTML、CSS甚至部分JavaScript代码。其核心特点包括:

  • 语法简洁:类似CSS的选择器语法,易学易用。
  • 极速生成:输入缩写后按Tab键,立即展开为完整代码。
  • 跨平台支持:兼容VS Code、Sublime Text、WebStorm等主流编辑器。
  • 高度可扩展:支持自定义缩写和脚本。

示例
输入div.container>ul.list>li*3,按Tab键后生成:

<div class="container">
  <ul class="list">
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>

二、Emment核心语法详解

1. 元素与类/ID

缩写 展开结果
div <div></div>
div.header <div class="header"></div>
div#main <div id="main"></div>
p.text.red <p class="text red"></p>

2. 嵌套与子元素

  • >:子元素
    div>ul>li  →  
    <div>
      <ul>
        <li></li>
      </ul>
    </div>
    
  • +:兄弟元素
    h1+p  →  
    <h1></h1>
    <p></p>
    
  • ^:向上层级(返回父级)
    div>ul>li>a^span  →  
    <div>
      <ul>
        <li>
          <a href=""></a>
          <span></span>
        </li>
      </ul>
    </div>
    

3. 乘法与编号

  • *:重复生成元素
    ul>li*3  →  
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
    
  • $:自动编号
    ul>li.item$*3  →  
    <ul>
      <li class="item1"></li>
      <li class="item2"></li>
      <li class="item3"></li>
    </ul>
    

4. 属性与文本

  • []:添加属性
    a[href="https://example.com" target="_blank"]  →  
    <a href="https://example.com" target="_blank"></a>
    
  • {}:插入文本
    p{Hello, World!}  →  
    <p>Hello, World!</p>
    

5. 隐式标签

在特定上下文中可省略标签名(默认生成divp):

.container  →  <div class="container"></div>
#header>p  →  
<div id="header">
  <p></p>
</div>

三、Emment实战场景

1. 快速生成表单

输入以下缩写:

form#login>input[type="text" placeholder="Username"]+input[type="password" placeholder="Password"]+button{Login}

展开结果:

<form id="login">
  <input type="text" placeholder="Username">
  <input type="password" placeholder="Password">
  <button>Login</button>
</form>

2. 创建Bootstrap网格布局

输入:

.container>.row>.col-md-4*3>h3{Title $}+p{Content...}

展开结果:

<div class="container">
  <div class="row">
    <div class="col-md-4">
      <h3>Title 1</h3>
      <p>Content...</p>
    </div>
    <div class="col-md-4">
      <h3>Title 2</h3>
      <p>Content...</p>
    </div>
    <div class="col-md-4">
      <h3>Title 3</h3>
      <p>Content...</p>
    </div>
  </div>
</div>

3. 生成CSS样式

Emment也支持CSS缩写(需编辑器插件支持):

m10  →  margin: 10px;
bd1-s  →  border: 1px solid;

四、主流编辑器配置指南

1. VS Code

  1. 安装插件:“Emmet Abbreviation”(通常内置,无需额外安装)。
  2. 启用快捷键:在设置中搜索emmet.triggerExpansionOnTab并勾选。
  3. 自定义缩写:修改settings.json
    "emmet.extensionsPath": ["./path/to/custom-snippets.json"]
    

2. Sublime Text

  1. 安装**“Emmet”**插件(通过Package Control)。
  2. 绑定快捷键:在Preferences > Key Bindings中添加:
    { "keys": ["tab"], "command": "expand_abbreviation_by_tab", "context": [...]}
    

3. WebStorm/IntelliJ IDEA

  1. 启用Emmet:Preferences > Editor > Emmet
  2. 支持范围:HTML、CSS、JSX等文件类型。

五、进阶技巧

1. 自定义代码片段

在编辑器配置文件中添加自定义缩写(如VS Code的snippets.json):

{
  "html": {
    "snippets": {
      "react-component": "import React from 'react';\n\nconst ${1:ComponentName} = () => {\n  return ($2);\n};\n\nexport default ${1:ComponentName};"
    }
  }
}

2. 结合CSS预处理器

在Sass/Less中使用Emmet生成嵌套结构:

nav>ul>li*3>a[href="#"]nav {
  ul {
    li:nth-child(1) { a[href="#"] {} }
    li:nth-child(2) { a[href="#"] {} }
    li:nth-child(3) { a[href="#"] {} }
  }
}

3. 数学运算

在属性值中插入计算表达式:

.container{width: calc(100% - 20px*2)}  →  
<div class="container" style="width: calc(100% - 40px);"></div>

六、常见问题解决

1. 缩写不生效

  • 检查是否在支持的文件类型中(如HTML/CSS)。
  • 确认编辑器已正确安装Emmet插件。
  • 尝试手动触发(如VS Code中按Ctrl+E展开缩写)。

2. 符号冲突

  • 如果缩写中包含$等特殊字符,需用反斜杠转义:
    div.item\$\$  →  <div class="item$$"></div>
    

3. 扩展功能失效

  • 更新编辑器或插件到最新版本。
  • 检查是否有其他插件冲突(如同时安装了多个Emmet变体)。

七、总结

Emment通过简洁的缩写语法和强大的扩展能力,将前端开发中的重复劳动转化为“一键生成”的流畅体验。无论是快速搭建页面骨架、生成复杂表单,还是编写CSS样式,Emment都能让你事半功倍。

行动建议

  1. 从今天开始,在编写HTML时尝试使用Emment缩写。
  2. 自定义几个常用的代码片段(如React组件模板)。
  3. 分享Emment技巧给团队成员,共同提升效率!

学习资源

掌握Emment,让你的代码“飞”起来! 🚀

什么可以帮助你在前端面试中取得成功(以及什么可能会对你造成阻碍)

作者 MiyueFE
2025年7月3日 14:59

原文:《What Could Help You Succeed in a Frontend Interview (and What Might Hold You Back)》

作者:Dzmitry Ihnatovich

2025 年的前端市场堪称“地狱模式”——但也不是没活路。你只需要学会怎么“聪明地玩”。

最近我一直在人才市场里摸爬滚打。有些面试顺风顺水,有些嘛……就一言难尽了。

在经历了足够多的这类事情后(外加只要有机会就求爷爷告奶奶地要反馈),我开始注意到一些套路。

所以,我写这篇文章,送给所有正在前端求职路上挣扎的兄弟姐妹们。希望我踩过的这些坑能帮你省点时间、更聪明地准备,或者至少让你知道,你不是一个人在战斗。

咱们这就开整 👇

✅ 什么能让你脱颖而出

1. 扎实的基础 > 花哨的库

听着,我和下一个人一样,也喜欢搞个酷炫的 React 项目——但你知道什么最能亮瞎我的眼吗?

是那种真正懂行的人,他明白:

  • JavaScript (特别是 ES6+、async/await、闭包、类型)
  • JavaScript 的事件循环到底是怎么个事儿
  • 为什么 CSS 权重那么重要
  • ===== 的区别,可不只是多一个等号那么简单
  • 语义化 HTML 到底意味着啥

如果你能把这些给面试官讲得明明白白,那你已经赢在起跑线上了,加分加爆!

💡 面试官们可还惦记着原生 JS 和 CSS 的问题呢。别总指望框架替你扛所有。

2. 做的项目得有得聊

起初,我的作品集里全是些“经典”教程项目——待办事项列表、计算器,老三样了。但当我放进去一个自己捣鼓的个人小项目后,一切都变了。

面试官们开始追着我问:

  • 你的设计决策是咋想的?
  • 你的文件夹结构为啥这么搭?
  • 你是怎么处理那些犄角旮旯的边界情况的?
  • 下一步你打算怎么改进?

而我呢,对答如流,因为这玩意儿是我亲手拉扯大的,我能不关心吗?

💡 小贴士:项目不求大、不求完美——但求真实。一个你从零开始、能充满激情地聊起来的东西,就够了。

3. 学会“边想边说”

有件事我完全低估了:面试官们到底有多在乎你的思考过程

就算我没能给出完美的答案,仅仅是解释我如何着手解决问题,就帮我在面试中走得更远。我会这么说:

“我不是百分百确定,但我想我会先试试 X 方法,然后用 console.log 验证一下。”

或者:

“这个方案可能扩展性不太好,所以我们之后或许可以用 reducer 来重构一下。”

这种坦诚、透明的思考方式,一下子就建立起了信任。

💡 小贴士:就算心里没底,也要把你的思路说给他们听。沉默是金?在这里,沉默比不完美更糟糕。

4. 对技术生态保持好奇心

你不需要了解每一个新工具,但当我开始对这些东西表现出好奇时:

  • React Server Components
  • 性能优化
  • 测试
  • 部署 (Vercel/Netlify 那点基础玩意儿就行)

……我就变得与众不同了。即便我不是专家,面试官们也很欣赏我那种有兴趣在成长的态度。

💡 小贴士:保持好奇。哪怕只是能聊聊某个概念(比如 hydration 或 lazy loading),都能让你看起来更资深。

🚩 什么可能会拖你后腿

好了,咱们来聊聊那些不太顺利的事儿——这样你就能尽量避开这些雷区。

❌ 1. 没啥能拿得出手的公开作品 (前面说过了)

❌ 2. 简历上堆满了时髦词汇

曾几何时,我的简历上写着:

“精通 React, Vue, Angular, Svelte, Node, GraphQL, Tailwind, Webpack…”

你猜猜看,这些里面我真正在生产环境用过的有几个?可能就三个。结果呢?搬起石头砸自己的脚。

一次面试中,我被问到一个超细节的 Vue 问题。我当场石化。面试官不是故意的,但我确实误导了人家。

💡 小贴士:只列你真正用得溜的。深入掌握几个工具,比假装什么都会要强得多。

❌ 3. 提交的代码乱七八糟

当我急着完成那些带回家的编程作业时,我最终提交的代码虽然能“跑”,但一点也不整洁——变量名乱取、没注释、结构混乱。

我现在才意识到,大多数团队更关心你怎么写代码,而不是代码是否完美。他们在想:我愿意 review 或者维护这段代码吗?

💡 小贴士:永远要留出时间来整理你的代码。把它当成是别人要来维护的样子去读。

❌ 4. 没准备问题问他们

我以前总觉得“你有什么问题想问我们吗?”这个环节就是走个过场。大错特错!

现在我总会问这些问题:

  • “你们的代码审查流程是怎样的?”
  • “你们目前在处理哪些技术债?”
  • “在你们团队,设计师和开发是怎么协作的?”

这时候,面试才真正变成了一场对话。而且不止一次,面试官告诉我:

“这个问题问得好——从来没人问过这个。”

❌ 5. 对自己太苛刻

我有过在面试中突然想不起某个基础知识的经历。也有过好几天都确信自己搞砸了的时候。

但自怨自艾从来没用。真正有帮助的是做笔记,下次改进一小点,然后继续前进。

有时候,问题甚至不在你——团队匹配度、预算变动、时机……这些都不是你能控制的。

💡 提醒:被拒确实很难受,但也很正常。把它们当成反馈,而不是对你的审判。

❌ 6. 对前团队充满负能量

想让面试官对你失去兴趣,最快的方法之一就是开始吐槽你以前的同事或老板。

说些像这样的话:

“我上个团队根本不知道自己在干嘛” “那家公司简直一团糟”

不久前,我就是因为说前任经理让我尽可能在工作中使用 AI,而我对此很不爽,结果在文化契合度面试中挂了。我没有给出任何建设性的理由,所以……

……这会让我们想:你来我们这儿,会不会也成为一个问题? 即使你有过糟糕的经历,也要保持建设性

🧠 几个可能真能帮到你的最后小贴士

None

  • 练习模拟面试,特别是现场编码和“边想边说”的练习。
  • 为你的面试官准备几个聪明的问题(问问团队、代码审查流程、他们如何交付代码)。
  • 更新你的领英和 GitHub
  • 聊聊你的学习经历:训练营、自学、书籍、YouTube、博客文章……我们喜欢看到主动性。
  • 明智地使用 AI。它是个学习和调试的好工具——但如果你的回答听起来像 AI 生成的,而你又解释不清楚?那就是一个危险信号。

🚀 最后的想法

前端圈的变化简直快到飞起,哪还只是捣鼓界面那么简单哟——核心是你咋动脑子、咋往上冲,还有啊,咋跟小伙伴们并肩作战。

要是你能死磕这几点:

  • 基本功练得跟钢筋似的扎实
  • 撸起袖子干真项目不玩虚的
  • 聊天儿掏心窝子不打哑谜
  • 永远揣着颗想扒新知识的心

……那你这条路基本就稳了,妥妥的!

真不用逼自己当啥全能大神,只要随时支棱着、对啥都两眼放光、做人实在不飘,就超够用啦~

React笔记

作者 JulianNing
2025年7月3日 14:57

React Hooks

1. useState

用于在函数组件中添加状态管理功能。它让函数组件能够拥有自己的内部状态。

(1)基本语法

const [state, setState] = useState(initialValue);
  • state: 当前状态值
  • setState: 更新状态的函数
  • initialValue: 状态的初始值

(2)重要特性

  • 状态更新是异步的
function AsyncExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    console.log('点击前:', count); // 0
    setCount(count + 1);
    console.log('点击后:', count); // 仍然是 0,因为状态更新是异步的
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}
  • 函数式更新:当新状态依赖于前一个状态时,需使用函数式更新:
function Counter() {
  const [count, setCount] = useState(0);
  
  const incrementTwice = () => {
    // 错误方式:可能不会按预期工作
    // setCount(count + 1);
    // setCount(count + 1);
    
    // 正确方式:使用函数式更新
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementTwice}>增加2</button>
    </div>
  );
}
  • React 会对多个状态更新进行批处理:
function BatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  const handleClick = () => {
    // 这些更新会被批处理,只触发一次重新渲染
    setCount(c => c + 1);
    setFlag(f => !f);
  };
  
  console.log('渲染'); // 只会打印一次
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>更新</button>
    </div>
  );
}

2. useEffect

用于处理副作用,包括:请求数据、设置订阅、操作 DOM、定时器、清理资源。

(1)基本语法

useEffect(() => {
  console.log('组件挂载或更新');
  return () => {
    console.log('组件卸载或清理');
  };
}, [count]); // 依赖项改变时才执行

第二个参数是依赖数组: 空数组 []时,只在挂载时执行一次;有依赖项时,依赖项变化时执行;无依赖数组时,每次渲染都执行。

(2)在return 中,可以取消请求、清理定时器、移除事件监听器

// 取消请求
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    const abortController = new AbortController();
    
    const searchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortController.signal
        });
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('搜索失败:', error);
        }
      } finally {
        setLoading(false);
      }
    };
    
    searchData();
    
    // 清理函数:取消请求
    return () => {
      abortController.abort();
    };
  }, [query]);
  
  return (
    <div>
      {loading && <div>搜索中...</div>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

3. useContext

用于在组件树中跨层级传递数据,避免了逐层传递 props的问题。能够在不通过 props 的情况下,将数据传递给深层嵌套的组件。

  • Context: 上下文对象,用于存储共享数据
  • Provider: 提供者组件,用于提供数据
  • Consumer: 消费者组件,用于使用数据
import React, { createContext, useContext, useState } from 'react';

// 1. 创建 Context
const ThemeContext = createContext();

// 2. 创建 Provider 组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// 3. 在子组件中使用 Context
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <header style={{ 
      backgroundColor: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        切换到 {theme === 'light' ? '深色' : '浅色'} 模式
      </button>
    </header>
  );
}

function Content() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <main style={{ 
      backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '20px'
    }}>
      <p>当前主题: {theme}</p>
    </main>
  );
}

// 4. 在应用中使用
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

4. useRef

用于获取 DOM 节点或保存可变值,useRef 返回一个对象,该对象有一个 current 属性,可以通过修改 current 来存储任何值。与 useState 不同的是,修改 useRef 的值不会触发组件重新渲染。

function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

5. useMemo

可以缓存计算结果,只有在依赖项发生变化时才会重新计算。

(1)基本语法

import { useMemo } from 'react';

const memoizedValue = useMemo(() => {
  return expensiveCalculation(a, b);
}, [a, b]);

useMemo 接收两个参数:

  • 计算函数:返回需要缓存的值
  • 依赖数组:当数组中的值发生变化时,才会重新执行计算函数

6. useCallback

用于缓存函数,返回一个记忆化的回调函数,只有在依赖项发生变化时才会重新创建函数。

import { useCallback } from 'react';

const memoizedCallback = useCallback(() => {
  // 函数逻辑
}, [dependency1, dependency2]);

useCallback 接收两个参数:

  • 回调函数:需要缓存的函数
  • 依赖数组:当数组中的值发生变化时,才会重新创建函数

7. useReducer

useState 的替代方案,特别适用于复杂的状态逻辑管理。

import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
function Counter() {
  const initialState = { count: 0 };
  
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      case 'reset':
        return initialState;
      default:
        throw new Error();
    }
  }
  
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}

开发语音识别或录制系统音频时遇到的一些问题记录1:getDisplayMedia和getUserMedia

作者 DcTbnk
2025年7月3日 14:52

❓问题重述:

  • navigator.mediaDevices.getUserMedia({ audio: true })只能获取麦克风音频
  • navigator.mediaDevices.getDisplayMedia({ audio: true }):可以获取屏幕共享时的系统音频
  • ❌ 不能通过 getUserMedia({ audio: true }) 获取系统的输出音频(如扬声器、媒体播放声音)

🔍 为什么不能用 getUserMedia({ audio: true }) 获取“系统声音”?

这是因为:

🧱 安全策略 + 隐私保护限制

浏览器(Chrome、Edge 等)为了防止网页偷偷监听你的扬声器内容默认不允许网页访问用户设备的“扬声器输出”,也就是 系统音频(system audio output)

  • getUserMedia({ audio: true }) 只允许访问 麦克风设备
  • 不允许访问电脑“正在播放的声音”(比如你听的音乐、看的视频)
  • 即使你用 deviceId 设置输出设备 ID 也没用,浏览器只允许输入设备

🔍 为什么不能用 getUserMedia({ audio: true }) 获取“系统声音”?

这是因为:

🧱 安全策略 + 隐私保护限制

浏览器(Chrome、Edge 等)为了防止网页偷偷监听你的扬声器内容默认不允许网页访问用户设备的“扬声器输出”,也就是 系统音频(system audio output)

  • getUserMedia({ audio: true }) 只允许访问 麦克风设备
  • 不允许访问电脑“正在播放的声音”(比如你听的音乐、看的视频)
  • 即使你用 deviceId 设置输出设备 ID 也没用,浏览器只允许输入设备

✅ 那为什么 getDisplayMedia({ audio: true }) 可以获取系统音频?

这是个例外情况。

当你发起:

js
复制编辑
navigator.mediaDevices.getDisplayMedia({ video: true, audio: true })

浏览器会提示用户选择“共享整个屏幕 + 是否包含音频”,如果用户勾选了:

✅「共享此屏幕的声音」

这时候浏览器会:

  • 合成一个 包含系统音频 的 MediaStream
  • 允许你通过 getDisplayMedia() 获取该音频轨道

这是一种受控授权的特殊情况,仅在用户明确同意的屏幕共享场景下允许访问系统声音。

✅ 结论

API 调用 是否能获取系统声音 说明
getUserMedia({ audio: true }) 只能获取麦克风
getDisplayMedia({ video: true, audio: true }) ✅(用户勾选后) 可以获取系统音频(比如播放的视频声音)

从XHR到Fetch:Promise封装Ajax的奇幻之旅

作者 FogLetter
2025年7月3日 14:51

大家好,我是你们的前端小伙伴Fogletter!今天我要和大家分享一个前端开发中非常经典的话题——Ajax请求的演进史,以及如何用Promise封装传统的XHR对象来模拟现代Fetch API的效果。

一、Ajax:前端开发的里程碑

还记得2005年,Google在Gmail和Google Maps中大规模使用Ajax技术时,整个Web开发界为之震撼的场景吗?Ajax(Asynchronous JavaScript and XML)彻底改变了Web应用的交互方式,让我们告别了整页刷新的时代。

1.1 传统XHR的"原始社会"

在ES6之前,我们只能使用XMLHttpRequest对象(简称XHR)来进行异步请求。看看这段"考古代码":

const xhr = new XMLHttpRequest(); // 实例化
xhr.open('GET', 'https://api.github.com/users/fogletter/repos');
xhr.send(); // 发送请求

xhr.onreadystatechange = function() {
    if(xhr.readyState == 4){
        const data = JSON.parse(xhr.responseText);
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    }
}

这段代码有几个痛点:

  1. 回调地狱:当有多个依赖请求时,代码会形成金字塔形状
  2. 状态管理:需要手动检查readyState
  3. 错误处理:需要额外代码处理网络错误

1.2 readyState的五个阶段

XHR对象的状态变化很有意思,它经历了五个阶段:

  • 0 (UNSENT): 代理被创建,但尚未调用 open() 方法
  • 1 (OPENED): open() 方法已经被调用
  • 2 (HEADERS_RECEIVED): send() 方法已经被调用,并且头部和状态已经可获得
  • 3 (LOADING): 下载中;responseText 属性已经包含部分数据
  • 4 (DONE): 下载操作已完成

我们通常只关心阶段4,也就是请求完成的时候。

二、Promise:异步编程的救星

随着前端应用越来越复杂,回调地狱问题日益严重。ES6引入的Promise成为了解决这一问题的利器。

2.1 Promise的三种状态

Promise对象代表一个异步操作的最终完成(或失败)及其结果值。它有三种状态:

  1. pending: 初始状态,既不是成功,也不是失败
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

2.2 用Promise封装XHR

让我们用Promise来改造传统的XHR:

const getJSON = (url) => {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.send();
        xhr.onreadystatechange = function() {
            if(xhr.readyState == 4){
                if(xhr.status === 200) {
                    resolve(JSON.parse(xhr.responseText));
                } else {
                    reject(new Error(xhr.statusText));
                }
            }
        }
        xhr.onerror = function() {
            reject(new Error('Network Error'));
        }
    })
}

这样封装后,我们就可以像这样使用:

getJSON('https://api.github.com/users/fogletter/repos')
    .then(data => {
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    })
    .catch(error => {
        console.error('请求失败:', error);
    });

是不是清爽多了?这种链式调用的方式让代码更加线性化,易于理解和维护。

三、Fetch API:现代浏览器的原生支持

ES6不仅带来了Promise,还引入了更现代的Fetch API。Fetch基于Promise设计,提供了更强大、更灵活的功能。

3.1 Fetch的基本用法

fetch('https://api.github.com/users/fogletter/repos')
    .then(res => res.json())
    .then(data => {
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    });

Fetch API的优点:

  1. 语法简洁,更符合现代JavaScript风格
  2. 内置Promise支持,无需额外封装
  3. 提供了Request和Response对象,功能更强大
  4. 默认不会接收或发送cookies,安全性更好

3.2 结合async/await使用

ES8引入的async/await语法让异步代码看起来像同步代码一样直观:

document.addEventListener('DOMContentLoaded', async() => {
    try {
        const result = await fetch('https://api.github.com/users/fogletter/repos');
        const data = await result.json();
        document.getElementById('repos').innerHTML = 
            data.map(item => `<li>${item.name}</li>`).join('');
    } catch (error) {
        console.error('请求失败:', error);
    }
});

这种写法几乎消除了所有回调,代码可读性大大提高。

四、为什么还要学习XHR?

虽然Fetch API已经很优秀了,但学习XHR和Promise封装仍然很有必要:

  1. 兼容性考虑:一些老旧项目或浏览器可能需要XHR
  2. 理解底层原理:了解XHR有助于深入理解网络请求机制
  3. 特殊需求:如上传进度监控等,Fetch API支持还不够完善
  4. 面试必备:很多面试官喜欢考察对底层原理的理解

八、最佳实践建议

  1. 现代项目优先使用Fetch API:语法简洁,功能强大
  2. 需要兼容性时使用Promise封装XHR:保证代码风格一致
  3. 始终处理错误:不要忽略.catch或try/catch
  4. 合理设置超时:避免请求长时间挂起
  5. 考虑使用拦截器:统一处理请求和响应

九、总结

从前端的异步请求发展史中,我们可以看到JavaScript语言的不断进化:

  1. XHR时代:回调地狱,手动管理状态
  2. Promise封装:链式调用,代码更清晰
  3. Fetch API:原生Promise支持,语法更现代
  4. async/await:同步写法,异步效果

理解这个演进过程不仅能帮助我们写出更好的代码,还能在面试中展现出对前端技术的深刻理解。记住,技术总是在不断进步的,今天的Fetch API也许明天就会被更优秀的方案取代,但核心的异步编程思想是不变的。

希望这篇笔记能帮助大家更好地理解Ajax和Promise封装!如果有任何问题,欢迎在评论区留言讨论。

❌
❌