普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月3日首页

css实现酷炫的边框流光(流动)旋转平移效果

作者 Nyingchi_X
2025年7月3日 18:00

最近为了迎合ai科技感的热度,产品突发奇想想把后台系统整得酷炫一点,比如什么接口请求的时候输入框带点酷炫的旋转效果,请求结果框移入内边框可旋转巴拉巴拉一堆“酷炫的效果”,然后就开始拿一堆酷炫的视屏效果企图让我实现她天马行空的想法,为了满足产品这个刁钻的需求,我把ai都问烂了,最终整理出了下面四种产品比较满意的流光效果(此处效果是从项目抽离出来的案例,具体的应用大家可可根据各自的场景自行使用,公司项目有保密要求我也不好展示,大家就凑活凑活,不好看也别嫌弃)

iShot_2025-04-24_01.29.38.mp4.gif

一、案例中使用到的参数

@property 声明 :

  • 这是一个较新的CSS特性,允许开发者明确定义自定义属性的类型和行为
  • 它比普通的CSS变量( --variable )提供了更多的控制能力
  • 这里关键的一句是:syntax: "";,如果你不声明他为角度类型的变量,那么 turn 这个单位将不能被 conic-gradient 在变化中读取。
@property --border-gradient-angle {
  syntax: '<angle>'; //指定此属性的值必须是角度类型
  inherits: true; // 表示此属性的值会从父元素继承
  initial-value: 0turn; //设置属性的初始值为0圈(即0度), 1turn单位表示一个完整的圆(360度)
} 

linear-gradient(线性渐变):

  • 由两种或多种颜色沿一条直线进行线性过渡的图像
//渐变轴为 90 度,从左到右渐变 
background: linear-gradient(90deg, #6631ff, #a431ff, #ee89ff, #31daff, #316bff, #44ffdd, #6631ff);

conic-gradient(锥形渐变) :

    background: conic-gradient(
      // 指定渐变的起始角度--border-gradient-angle(这个值会在动画中变化,使得渐变可以随时间旋转),并设置设置渐变的中心点在元素的正中心(水平50%,垂直50%)
      from var(--border-gradient-angle) at 50% 50%,
      // 渐变中使用的颜色(注意首尾颜色相同(#6631ff),确保渐变过渡平滑)
      #6631ff,
      #a431ff,
      #ee89ff,
      #31daff,
      #316bff,
      #44ffdd,
      #6631ff
    );

animation(动画效果):

  • 通过改变 --border-gradient-angle 的值(从0turn到1turn),实现了渐变色彩的旋转效果
 animation: border-wave 5s linear infinite 0ms;

 @keyframes border-wave {
    0% {
      --border-gradient-angle: 0turn;
    }

    100% {
      --border-gradient-angle: 1turn;
    }
  }

filter

//- 高斯模糊效果,使元素产生15像素的模糊效果
filter: blur(10px);

二、实现效果代码

2.1 旋转流光

代码原理很简单,先实现一个旋转的流光背景设置一个内边距

image-20250423173016298.png 然后再在这个背景上叠加上元素,就可实现一个伪旋转边框的效果

2.2 平移流光

代码原理很简单,通过控制background-size实现一个平移的流光背景并设置一个内边距,然后再在这个背景上叠加上元素,就可实现一个伪边框的效果

image-20250423174610941.png

2.3 内边框流光

为了实现内边框旋转的效果此处需要通过伪元素::before创建一个旋转边框

未命名.png

再通过 filter: blur(10px);为这个旋转边框设置一个模糊效果

未命名 2.png

然后再给通过父元素设置overflow: hidden;使超出的边框隐藏

未命名 3.png

2.4 外边框流光

为了不让 filter: blur(10px);影响到子元素的效果,所以需要通过伪元素::before给父元素创建一个模糊的旋转元素 未命名 4.png

创建完成后覆盖上有背景色的子元素就可实现效果

ES2020 都有哪些新写法?

2025年7月3日 17:53

1、可选链操作符

// 传统写法
const street = user && user.address && user.address.street;

// ES2020
const street = user?.address?.street; // 任意一环不存在则返回 undefined

支持的场景:

  • 属性访问 obj?.prop
  • 动态属性 obj?.[expr]
  • 函数调用 func?.()

2、空值合并运算符

作用:精准判断 null/undefined(不包含其他假值如 0 或 '')。

// 传统写法
const value = input !== null && input !== undefined ? input : 'default';

// ES2020
const value = input ?? 'default'; // 仅在 input 为 null/undefined 时生效

对比 ||

const count = 0;
console.log(count || 10); // 10(0 是假值)
console.log(count ?? 10); // 0(精准判断)

3、动态导入

作用:按需异步加载模块。

// 传统静态导入
import module from 'module';

// ES2020 动态导入
button.addEventListener('click', async () => {
  const module = await import('./module.js');
  module.doSomething();
});

4、 BigInt 大整数类型

作用:表示超出 Number.MAX_SAFE_INTEGER 的整数。

Number.MAX_SAFE_INTEGER 是多少?

2^53 - 1 = 9007199254740991

技术背景:

JS使用 IEEE 754 标准的64位双精度浮点数表示所有数字(包括整数) 其中52位用于表示整数部分的尾数

5、Promise.allSettled()

获取所有Promise的结果(无论成功还是失败)

6、String.matchAll()

作用:高效遍历正则匹配的所有分组。

const str = 'test1test2';
const regex = /t(e)(st(\d?))/g;

// 传统写法:循环 exec
// ES2020
const matches = [...str.matchAll(regex)];
matches[0]; // ["test1", "e", "st1", "1", index: 0, ...]

七、globalThis

作用:统一全局对象访问(跨浏览器/Node.js 环境)。

// 传统环境判断
const global = typeof window !== 'undefined' ? window : global;

// ES2020
console.log(globalThis); // 浏览器: window, Node.js: global

八、模块新特性

1. import.meta

console.log(import.meta.url); // 文件 URL(如 "file:///path/to/module.js")

2. 导出命名空间

export * as utils from './utils.js'; // 将模块所有导出作为命名空间

九、for-in 机制标准化

明确规范 for-in 循环的遍历顺序(虽实际仍依赖引擎实现)

记一次replaceAll报错的问题

作者 笑看吳鉤
2025年7月3日 16:56

产生原因

公司大屏在国产机运行不正常,但是运行后台端都正常

排查问题

a.replaceAll() is not a function

查了下MDN

image.png

解决问题

可以使用replace去替换

image.png

//方法一 直接替换
const str = "Hello World, Hello Universe";
const newStr = str.replace(/Hello/g, "Hi");
console.log(newStr); // 输出: "Hi World, Hi Universe


//或者使用函数
function replaceAll(str, search, replacement) {
    let result = str;
    while (result.includes(search)) {
        result = result.replace(search, replacement);
    }
    return result;
}

const str = "Hello World, Hello Universe";
const newStr = replaceAll(str, "Hello", "Hi");
console.log(newStr); // 输出: "Hi World, Hi Universe"
//方法二   如果不兼容使用改造方法
if (!String.prototype.replaceAll) {
  String.prototype.replaceAll = function (newStr, oldStr) {
    return this.replace(new RegExp("oldStr", "g"), "newStr");
  };
}

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

常见简单的知识点

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 的响应式原理,并在需要时实现更高级的自定义响应式逻辑。

🧠“一次奇怪的 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 的。

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

从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封装!如果有任何问题,欢迎在评论区留言讨论。

tauri项目在windows上的c盘没有权限写入文件

作者 1024小神
2025年7月3日 10:46

在使用 Tauri 开发时,如果尝试在 C:\Program Files\ 这样的受保护系统目录写入或读取文件,Windows 会阻止操作(除非以管理员权限运行)。以下是解决方案:


1. 避免写入 Program Files(推荐)

Windows 对 Program Files 有严格的权限控制,普通应用不应在此目录写入数据。改用以下目录:

  • 用户数据目录(推荐):

    <pre>
    

    use tauri::api::path::{app_data_dir, resolve}; let app_data_path = app_data_dir(&tauri::Config::default()).unwrap(); std::fs::create_dir_all(&app_data_path).unwrap(); let icon_path = app_data_path.join("app.ico");

    <ul>
    <li>路径示例:<code>C:\Users\&lt;用户名&gt;\AppData\Roaming\&lt;你的应用名&gt;\app.ico</code></li>
    </ul>
    </li>
    <li>
    <p><strong>临时目录</strong></p>
    
    <pre>
    

    let temp_dir = std::env::temp_dir(); let icon_path = temp_dir.join("app.ico");


2. 如果必须写入 Program Files(不推荐)

方法 1:以管理员权限运行应用

  • 在 tauri.conf.json 中启用管理员权限:
    <pre>
    

    { "tauri": { "windows": [{ "webviewInstallMode": { "type": "offline" }, "runAsAdmin": true // 以管理员运行 }] } } 缺点:用户每次启动都会看到 UAC 弹窗,体验差。

方法 2:安装时修改目录权限

  • 用 NSIS 或 WiX 安装包脚本,在安装时赋予 Program Files\YourApp 可写权限(仍需谨慎)。

3. 检查文件是否存在(错误处理)

在读取文件前,先检查路径是否存在:

use std::path::Path;
if !Path::new(&icon_path).exists() {
  // 提供默认图标或报错
}

4. 开发时调试路径

在 main.rs 或事件处理中打印路径,确认是否正确:

println!("当前路径: {:?}", icon_path);

总结

  • 推荐方案:改用 %APPDATA% 或用户目录存储数据(Tauri 的 app_data_dir 已封装)。
  • 临时方案:以管理员运行(不推荐长期使用)。
  • 调试技巧:检查路径是否存在,打印日志定位问题。

如果仍有问题,可以提供更多代码片段(如 tauri.conf.json 和文件操作部分),我会进一步分析!

前端真的需要懂算法吗?聊聊感受

作者 ErpanOmer
2025年7月3日 10:39

image.png 在公司干了几年,带个小团队,零零总总也面试了上百个前端候选人了。说实话,有时候面完一天,感觉人都是麻的。

最让我头疼的是什么?就是“算法题”这个环节。

我经常遇到两种候选人。一种是一听算法题,就两手一摊,表情痛苦,说“哥,我天天写业务,真没准备这个”。另一种呢,正好相反,题目一出,眼睛一亮,不出三十秒,就把LeetCode上背得滚瓜烂熟的最优解,一字不差地敲了出来,然后一脸期待地看着我。

说实话,这两种,都不是我最想看到的。

这就引出了一个很多候选人都想问,但不敢问的问题:“你们这些面试官,到底怎么想的?你们明知道我们前端平时工作中,99%的时间都用不上这些,为什么非要折磨我们?”

今天,我就想站在桌子对面,跟大伙掏心窝子地聊聊,我们问算法题,到底图个啥。


首先,我得承认一件事:我们知道你工作中不怎么写算法

对,你没看错。

我心里门儿清,我团队里的小伙伴们,每天的工作是跟产品经理“吵架”,是跟UI设计师对像素,是封装React/Vue组件,是处理浏览器兼容性,是调CSS。我招你进来,也不是为了让你用动态规划来给按钮加border-radius的。

我们不会天真地以为,前端开发就是算法竞赛。如果你能把一个复杂的业务表单组件写得清晰、可维护、可扩展,在我眼里,这远比你徒手写一个红黑树要来得有价值。

所以,请你先放轻松。我们不是在考察你是不是一个“算法大神”。


那我们到底在看什么?——思路远比答案重要

既然不是看你会不会背最优解,那我们花这宝贵的20分钟,到底在考察什么?

其实,算法题只是一个“载体”,一个“媒介”。通过这个载体,我想看到的是这几样东西:

1. 你是怎么“解读”问题的(沟通与理解能力)

一个靠谱的工程师,拿到需求不会立刻动手。他会先问问题,搞清楚所有的边界和约束。

我出一道题:“写个函数,找出数组中第二大的数。”

  • 普通候选人:埋头就开始写代码。
  • 我欣赏的候选人:会先问我,“这个数组里会有重复的数字吗?会是无序的吗?会有负数吗?如果数组长度小于2怎么办?”

你看,这就是差距。我能通过这些问题,看出你是否严谨,是否有处理边界情况的意识。这个能力,在你将来面对产品经理那些模糊的需求时,至关重要。

2. 你的“思路”是否清晰(逻辑思维)

我最喜欢看到的,不是你直接写出最优解,而是你告诉我你的思考过程。

比如,你可以说:“我首先想到的,是一个最笨的办法,先排序,然后取倒数第二个。这个时间复杂度是O(n log n)。但感觉可以优化,我再想想……也许我只需要遍历一遍,用两个变量来维护最大值和第二大值,这样时间复杂度就降到O(n)了。”

这个“先暴力,再优化”的思考过程,在我看来,比你直接默写出最优解要加分得多。因为它展示了你的逻辑推理能力优化意识

3. 你的代码“品味”(工程素养)

算法题的代码量不大,但足以管中窥豹,看出一个人的代码“品味”。

你的变量是怎么命名的?a, b, c 还是 max, secondMax, current?

你有没有处理我刚才提到的那些边界情况?

你的代码有没有基本的缩进和格式?

这些细节,都反映了你平时的编码习惯。一个连算法题都写得乱七八糟的人,我很难相信他在业务项目里能写出整洁的代码。

4. 当你卡住时,你会怎么办?(抗压与学习能力)

我有时候会故意出一些有点难度的题。我不是为了让你难堪,而是想看看你卡住的时候,会有什么反应。

是直接放弃,说“不会”?还是会尝试跟我沟通,说“我卡在xxx了,能不能给点提示?”

我非常乐意给提示。我更想招一个能和我一起“协作”解决问题的人,而不是一个遇到困难就“躺平”的人。你面对一道题的态度,很可能就是你未来面对一个技术难题的态度。


给求职者的一些真心话

所以,聊了这么多:

  • 别光背题,没用。 我只要稍微改动一下题目条件,或者问你为什么这么写,背题的同学马上就露馅了。
  • 多练习“说” 。刷题的时候,试着把你的思路说出来,录下来自己听听,或者讲给朋友听。面试时的口头表达,和自己闷头做题是两回事。
  • 重点理解“为什么” 。不要满足于“这道题这么解”,要去理解它为什么要用双指针,为什么要用哈希表。理解了思路,才能举一反三。
  • 面试时,心态放平。 没做出最优解,真没关系。把你思考的过程、你的尝试、你的权衡都清晰地表达出来,你已经赢了很多人了。

我知道,让前端去卷算法,这个“游戏规则”本身就不那么公平。我们想找的是一个会思考、会沟通、有工程素养的“解决问题的人”。

算法题,只是恰好成了当前最方便、成本最低的考察工具而已。

希望这些“面试官的牢骚”,能让你稍微不那么焦虑一点。 你们怎么看?

JavaScript 数据扁平化方法大全

作者 绅士玖
2025年7月3日 10:31

前言

数据扁平化是指将多维数组转换为一维数组的过程。由于嵌套数据结构增加了访问和操作数据的复杂度,所以·我们可以将嵌套数据变成一维的数据结构,下面就是我搜集到的一些方法,希望可以给你带来帮助!!

1. 使用 Array.prototype.flat()(推荐)

ES2019 引入的专门方法:

const nestedArr = [1, [2, [3, [4]], 5]];

// 默认只扁平化一层
const flattened1 = nestedArr.flat();
console.log(flattened1); // [1, 2, [3, [4]], 5]

// 指定深度为2
const flattened2 = nestedArr.flat(2);
console.log(flattened2); // [1, 2, 3, [4], 5]

// 完全扁平化
const fullyFlattened = nestedArr.flat(Infinity);
console.log(fullyFlattened); // [1, 2, 3, 4, 5]

解析

  • flat(depth) 方法创建一个新数组,所有子数组元素递归地连接到指定深度
  • 参数 depth 指定要提取嵌套数组的结构深度,可选的参数,默认为1
  • 使用 Infinity 可展开任意深度的嵌套数组,Infinity 是一个特殊的数值,表示无穷大

2. 使用 reduce() 和 concat() 递归

function flatten(arr) {
  // 使用 reduce 方法遍历数组元素
  return arr.reduce((acc, val) => {
    // 如果当前元素是数组,则递归调用 flatten 继续展开,并拼接到累积数组 acc
    if (Array.isArray(val)) {
      return acc.concat(flatten(val));
    } 
    // 如果当前元素不是数组,直接拼接到累积数组 acc
    else {
      return acc.concat(val);
    }
  }, []); // 初始累积值是一个空数组 []
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. 递归处理嵌套数组

    • 遇到子数组时,递归调用 flatten(val) 继续展开,直到所有层级都被展开为单层。
  2. reduce 方法的作用

    • 遍历数组,通过 acc(累积值)逐步拼接结果,初始值设为 [](空数组)。
  3. Array.isArray(val) 检查

    • 判断当前元素是否为数组,决定是否需要递归展开。
  4. concat 拼接结果

    • 将非数组元素或递归展开后的子数组拼接到累积数组 acc 中。

3. 使用 concat() 和扩展运算符递归

function flatten(arr) {
  // 使用扩展运算符 (...) 展开数组的第一层,并合并成一个新数组
  const flattened = [].concat(...arr);

  // 检查当前展开后的数组中是否仍然包含嵌套数组
  // 如果存在嵌套数组,则递归调用 flatten 继续展开
  // 如果所有元素都是非数组类型,则直接返回展开后的数组
  return flattened.some(item => Array.isArray(item)) 
    ? flatten(flattened) 
    : flattened;
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // 输出: [1, 2, 3, 4, 5]

解析

  1. [].concat(...arr) 展开一层数组

    • 使用扩展运算符 ... 展开 arr 的最外层,并通过 concat 合并成一个新数组。
    • 例如:[].concat(...[1, [2, [3]]]) → [1, 2, [3]](仅展开一层)。
  2. flattened.some(Array.isArray) 检查嵌套

    • 使用 Array.prototype.some() 检查当前数组是否仍然包含子数组。
    • 如果存在,则递归调用 flatten 继续展开。
  3. 递归终止条件

    • 当 flattened 不再包含任何子数组时,递归结束,返回最终结果。

4. 使用 toString() 方法(仅适用于数字数组)

const nestedArr = [1, [2, [3, [4]], 5]];
const flattened = nestedArr.toString().split(',').map(Number);
console.log(flattened); // [1, 2, 3, 4, 5]

解析

  1. toString() 的隐式转换

    • JavaScript 的 Array.prototype.toString() 会自动展开嵌套数组,并用逗号连接所有元素。
    • 例如:[1, [2, [3]]].toString() → "1,2,3"
  2. split(',') 分割字符串

    • 将字符串按逗号拆分成字符串数组,但所有元素会是字符串类型(如 "2")。
  3. map(Number) 类型转换

    • 通过 Number 构造函数将字符串元素转换为数字类型。
    • 注意:如果原数组包含非数字(如 ['a', [2]]),结果会变成 [NaN, 2]

优缺点

  • 优点:代码极其简洁,适合纯数字的嵌套数组。

  • 缺点

    • 仅适用于数字数组(其他类型会被强制转换,如 true → 1null → 0)。
    • 无法保留原数据类型(如字符串 '3' 会被转成数字 3)。

适用场景

  • 快速展开纯数字的嵌套数组,且不关心中间过程的性能损耗(toString 和 split 会有临时字符串操作)。

5. 使用 JSON.stringify() 和正则表达式

function flatten(arr) {
  // 1. 使用 JSON.stringify 将数组转换为字符串表示
  //    例如:[1, [2, [3]], 'a'] → "[1,[2,[3]],\"a\"]"
  const jsonString = JSON.stringify(arr);

  // 2. 使用正则表达式移除所有的 '[' 和 ']' 字符
  //    例如:"[1,[2,[3]],\"a\"]" → "1,2,3,\"a\""
  const withoutBrackets = jsonString.replace(/[\[\]]/g, '');

  // 3. 按逗号分割字符串,生成字符串数组
  //    例如:"1,2,3,\"a\"" → ["1", "2", "3", "\"a\""]
  const stringItems = withoutBrackets.split(',');

  // 4. 尝试将每个字符串解析回原始数据类型
  //    - 数字会变成 Number 类型(如 "1" → 1)
  //    - 字符串会保留(如 "\"a\"" → "a")
  //    - 其他 JSON 可解析类型也会被正确处理
  return stringItems.map(item => {
    try {
      // 尝试 JSON.parse 解析(处理字符串、数字等)
      return JSON.parse(item);
    } catch (e) {
      // 如果解析失败(如空字符串或非法 JSON),返回原始字符串
      return item;
    }
  });
}

// 测试用例
const nestedArr = [1, [2, [3, [4]], 5, 'a', { b: 6 }];
console.log(flatten(nestedArr)); 
// 输出: [1, 2, 3, 4, 5, "a", { b: 6 }]

解析

  1. JSON.stringify 的作用

    • 将整个数组(包括嵌套结构)转换为 JSON 字符串,保留所有数据类型信息。
  2. 正则替换 /[[]]/g

    • 移除所有方括号字符 [ 和 ],只保留逗号分隔的值。
  3. split(',') 分割字符串

    • 生成一个字符串数组,但每个元素可能仍是被 JSON 字符串化的(如 ""a"")。
  4. JSON.parse() 尝试恢复数据类型

    • 通过 JSON.parse 将字符串转换回原始类型(数字、字符串、对象等)。
    • 使用 try-catch 处理不合法的 JSON 字符串(如空字符串或格式错误的情况)。

优缺点

  • 优点

    • 支持任意数据类型(数字、字符串、对象等)。
    • 能正确处理嵌套对象(如 { b: 6 })。
  • 缺点

    • 性能较低(涉及 JSON 序列化、正则替换、解析等操作)。
    • 如果原始数组包含特殊字符串(如 "[1]" ,可能会被错误解析。

适用场景

  • 需要处理混合数据类型(非纯数字)的嵌套数组。
  • 对性能要求不高,但需要代码简洁的场景。

6. 使用堆栈的非递归实现

function flatten(arr) {
  // 创建栈并初始化(使用扩展运算符浅拷贝原数组)
  const stack = [...arr];
  const result = [];
  
  // 循环处理栈中的元素
  while (stack.length) {
    // 从栈顶取出一个元素
    const next = stack.pop();
    
    if (Array.isArray(next)) {
      // 如果是数组,展开后压回栈中(保持顺序)
      stack.push(...next);
    } else {
      // 非数组元素,添加到结果数组前端(保持原顺序)
      result.unshift(next);
    }
  }
  
  return result;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 栈结构初始化

    • 使用扩展运算符 [...arr] 创建原数组的浅拷贝作为初始栈
    • 避免直接修改原数组
  2. 栈处理循环

    • 使用 while 循环处理栈直到为空
    • 每次从栈顶 pop() 一个元素进行处理
  3. 元素类型判断

    • 使用 Array.isArray() 检查元素是否为数组
    • 如果是数组则展开后重新压入栈
    • 非数组元素则添加到结果数组
  4. 顺序保持

    • 使用 unshift() 将元素添加到结果数组前端,当然这样比较费性能,可以改用 push() + reverse() 替代 unshift()
    • 确保最终结果的顺序与原数组一致

优缺点

  • 优点

    • 支持任意数据类型(不限于数字)
    • 可以处理深层嵌套结构(无递归深度限制)
    • 相比递归实现,不易导致栈溢出
  • 缺点

    • 使用 unshift() 导致时间复杂度较高(O(n²))
    • 需要额外空间存储栈结构
    • 相比原生 flat() 方法性能稍差
    • 无法控制扁平化深度(总是完全扁平化)

适用场景

  • 需要处理混合数据类型的深层嵌套数组
  • 需要避免递归导致的栈溢出风险

7. 使用 Array.prototype.some() 和扩展运算符

function flatten(arr) {
  // 循环检测数组中是否还包含数组元素
  while (arr.some(item => Array.isArray(item))) {
    // 使用扩展运算符展开当前层级的所有数组
    // 并通过concat合并为一层
    arr = [].concat(...arr);
  }
  return arr;
}

const nestedArr = [1, [2, [3, [4]], 5]];
console.log(flatten(nestedArr)); // [1, 2, 3, 4, 5]

解析

  1. 循环条件检测

    • 使用 arr.some() 方法检测数组中是否还存在数组元素
    • Array.isArray(item) 判断每个元素是否为数组
  2. 层级展开

    • 使用扩展运算符 ...arr 展开当前层级的数组
    • 通过 [].concat() 将展开的元素合并为新数组
  3. 迭代处理

    • 每次循环处理一层嵌套
    • 重复直到没有数组元素存在

性能比较

对于大多数现代应用:

  1. 优先使用 flat(Infinity)(最简洁且性能良好)
  2. 对于深度嵌套的大数组,考虑非递归的堆栈实现
  3. 递归方法在小数据集上表现良好且代码简洁
  4. 避免 toString() 方法除非确定只有数字数据

总结

JavaScript 提供了多种扁平化数组的方法,从简单的内置 flat() 方法到各种手动实现的递归、迭代方案。选择哪种方法取决于:

  • 运行环境是否支持 ES2019+
  • 数据结构的复杂程度
  • 对性能的要求
  • 代码可读性需求

在大多数现代应用中,flat(Infinity) 是最佳选择,因为它简洁、高效且语义明确。

如何丝滑使用JavaScript的装饰器?

作者 best666
2025年7月3日 10:21

在 JavaScript 里,装饰器(Decorators)是一种能对类、方法、属性的行为进行扩展或者修改的语法。它的核心原理是借助元编程,在不改变原有代码结构的前提下,为目标添加新功能。

基本概念

直接show code,现有如下代码,用来记录log日志:

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

装饰器函数参数解析

在 JavaScript 装饰器中,log 函数的三个参数分别代表:

  1. target:被装饰的类或原型对象。

    • 若装饰的是类方法,target 就是类的原型(prototype)。
    • 若装饰的是类,target 就是类本身。
  2. name:被装饰的方法或属性的名称(字符串类型)。

  3. descriptor:属性描述符对象(与 Object.defineProperty 中的描述符相同),包含以下属性:

    • value:被装饰的方法或属性的值(即原始函数)。
    • writable:是否可修改(布尔值)。
    • enumerable:是否可枚举(布尔值)。
    • configurable:是否可配置(布尔值)。

函数实现原理详解

log 装饰器的核心逻辑是替换原始方法,在执行前后添加日志:


    function log(target, name, descriptor) {
      // 1. 保存原始方法的引用
      const original = descriptor.value;

      // 2. 修改 descriptor.value 为新函数
      descriptor.value = function(...args) {
        // 3. 执行前置逻辑(打印入参)
        console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
        
        // 4. 执行原始方法并保存结果
        const result = original.apply(this, args);
        
        // 5. 执行后置逻辑(打印返回值)
        console.log(`方法 ${name} 返回:${result}`);
        
        // 6. 返回原始结果
        return result;
      };

      // 7. 返回修改后的描述符
      return descriptor;
    }

为什么要这样实现?

这种写法的关键点在于:

  1. 不改变原始方法的核心逻辑:通过包装原始方法,在不修改其代码的前提下添加新功能。

  2. 保留上下文(this

    • 使用 original.apply(this, args) 确保原始方法在调用时的 this 指向不变。
    • 若直接调用 original(args),可能导致 this 指向全局对象(非严格模式)或 undefined(严格模式)。
  3. 支持任意参数

    • 使用剩余参数 ...args 收集所有传入参数。
    • 使用 JSON.stringify(args) 将参数序列化为字符串(需注意无法处理函数或 undefined 类型的参数)。
  4. 遵循装饰器规范

    • 装饰器必须返回一个描述符对象(或新类)。
    • 通过修改 descriptor.value 替换原始方法。

应用示例

使用该装饰器的类方法会自动添加日志功能:


    class Calculator {
      @log
      add(a, b) {
        return a + b;
      }
    }

    const calc = new Calculator();
    calc.add(3, 4);

    // 输出:
    // 调用 add 方法,参数:[3,4]
    // 方法 add 返回:7

注意事项

  1. 参数序列化限制

    • JSON.stringify 无法处理函数或 undefined 参数,可能导致日志不完整。
    • 改进方案:使用 args.map(arg => String(arg)).join(', ') 或自定义序列化函数。
  2. 异步方法处理

    • 若原始方法返回 Promise,需使用 await 等待结果:

      
          descriptor.value = async function(...args) {
            // ...
            const result = await original.apply(this, args);
            // ...
          };
      
  3. 兼容性

    • 装饰器语法需 Babel 或 TypeScript 支持。

    • 确保项目配置中启用了装饰器(如 @babel/plugin-proposal-decorators)。

通过这种方式,装饰器实现了 ** 横切关注点(Cross-cutting Concerns)** 的分离,让日志、权限等功能与核心业务逻辑解耦。

下面介绍装饰器常见的应用场景:

1. 日志记录

装饰器能够在方法执行的前后添加日志,这样可以对函数的调用情况进行监控。

function log(target, name, descriptor) {
  const original = descriptor.value;
  descriptor.value = function(...args) {
    console.log(`调用 ${name} 方法,参数:${JSON.stringify(args)}`);
    const result = original.apply(this, args);
    console.log(`方法 ${name} 返回:${result}`);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a, b) {
    return a + b;
  }
}

// 使用示例
const calc = new Calculator();
calc.add(3, 4); // 控制台会输出调用信息和返回结果

2. 权限验证

可以在执行方法前对用户权限进行检查,防止未授权的访问。


    function auth(requiredRole) {
      return function(target, name, descriptor) {
        const original = descriptor.value;
        descriptor.value = function(...args) {
          if (this.userRole !== requiredRole) {
            throw new Error("权限不足");
          }
          return original.apply(this, args);
        };
        return descriptor;
      };
    }

    class AdminPanel {
      userRole = "admin";

      @auth("admin")
      deleteUser() {
        return "用户已删除";
      }
    }

3. 性能分析

装饰器能够对函数的执行时间进行测量,有助于性能优化。


    function benchmark(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = async function(...args) {
        const start = performance.now();
        const result = await original.apply(this, args);
        const end = performance.now();
        console.log(`${name} 方法执行耗时:${end - start}ms`);
        return result;
      };
      return descriptor;
    }

    class DataService {
      @benchmark
      async fetchData() {
        await new Promise(resolve => setTimeout(resolve, 1000));
        return { data: "大量数据" };
      }
    }

4. 自动绑定

在 React 等框架中,装饰器可以解决方法上下文丢失的问题。


    function autobind(target, name, descriptor) {
      const original = descriptor.value;
      return {
        configurable: true,
        get() {
          const bound = original.bind(this);
          Object.defineProperty(this, name, {
            value: bound,
            configurable: true,
            writable: true
          });
          return bound;
        }
      };
    }

    class Component {
      constructor() {
        this.state = { count: 0 };
      }

      @autobind
      increment() {
        this.state.count++;
      }
    }

5. 单例模式实现

装饰器可以确保一个类仅有一个实例。


    function singleton(constructor) {
      let instance;
      return function(...args) {
        if (!instance) {
          instance = new constructor(...args);
        }
        return instance;
      };
    }

    @singleton
    class AppState {
      constructor() {
        this.data = {};
      }
    }

    const state1 = new AppState();
    const state2 = new AppState();
    console.log(state1 === state2); // 输出 true

6. 类型检查

在运行时对函数参数的类型进行验证。


    function validateTypes(target, name, descriptor) {
      const original = descriptor.value;
      descriptor.value = function(...args) {
        const paramTypes = Reflect.getMetadata("design:paramtypes", target, name);
        args.forEach((arg, i) => {
          if (arg && paramTypes[i] && !(arg instanceof paramTypes[i])) {
            throw new TypeError(`参数 ${i} 类型错误,期望 ${paramTypes[i].name}`);
          }
        });
        return original.apply(this, args);
      };
      return descriptor;
    }

    class MathUtils {
      @validateTypes
      add(a: number, b: number) {
        return a + b;
      }
    }

7. 缓存机制

对函数的计算结果进行缓存,避免重复计算。


    function memoize(target, name, descriptor) {
      const original = descriptor.value;
      const cache = new Map();
      descriptor.value = function(...args) {
        const key = args.toString();
        if (cache.has(key)) {
          return cache.get(key);
        }
        const result = original.apply(this, args);
        cache.set(key, result);
        return result;
      };
      return descriptor;
    }

    class Fibonacci {
      @memoize
      calculate(n) {
        return n <= 1 ? n : this.calculate(n - 1) + this.calculate(n - 2);
      }
    }

装饰器使用注意要点

  • 要启用装饰器语法,需要在 Babel 或者 TypeScript 中进行配置。

  • 装饰器的执行顺序是从下往上,例如:

    
        @a
        @b
        method() {} // 先执行 b,再执行 a
    
    
  • 装饰器可以返回一个新的类或者修改原有的描述符(descriptor)。

装饰器的主要价值在于它遵循了开放 - 封闭原则,即对扩展开放,对修改封闭。它能让代码变得更加简洁,同时增强代码的可复用性。

贝塞尔曲线:让计算机画出丝滑曲线的魔法

作者 LeonGao
2025年7月3日 10:12

想象一下,如果你让计算机画一条曲线,它可能会像个刚学画画的孩子,画出的线条要么僵硬得像铁丝,要么歪歪扭扭如同毛毛虫。但有了贝塞尔曲线,计算机突然就像掌握了绘画技巧的艺术家,能画出从字体轮廓到动画路径的各种丝滑线条。今天我们就来揭开这个让计算机变身为 "曲线大师" 的秘密。

从点到线:贝塞尔曲线的底层逻辑

贝塞尔曲线的核心原理其实很简单:用几个控制点 "拉扯" 出一条平滑曲线。就像你用手指捏住绳子的几个点,轻轻一拉就能得到自然的弧线。这背后藏着一种叫 "插值" 的数学思想 —— 通过已知的点,算出中间该有的样子。

最基础的是一次贝塞尔曲线,说穿了就是直线。取两个点,比如 (0,0) 和 (100,100),连接它们的线段就是一次贝塞尔曲线。这时候你可能会说:"这有什么了不起?" 别急,精彩的在后面。

当我们增加到三个点时,就得到了二次贝塞尔曲线。想象中间那个点是个 "磁铁",它会把直线段往自己这边吸,形成一条优美的抛物线。三个点分工明确:起点和终点固定曲线的两端,中间的控制点则决定了曲线的弯曲程度 —— 离直线越远,曲线弯得越厉害,就像有人在中间用力拽了一把。

让曲线更灵活:高阶贝塞尔曲线

三次贝塞尔曲线是应用最广泛的,它有四个控制点:起点、终点和两个中间控制点。这两个中间控制点就像两个方向舵,能让曲线做出更复杂的转弯。你可以把它想象成一条被两个人从不同方向拉扯的绳子,最终形成的形状取决于两人用力的方向和大小。

更高阶的贝塞尔曲线原理类似,只是增加了更多控制点。但有趣的是,在实际应用中,我们很少用到五阶以上的曲线。这就像做菜,加太多调料反而会破坏原本的味道,三个到四个控制点已经能满足绝大多数设计需求了。

数学背后的小秘密

贝塞尔曲线的数学表达其实是一系列多项式的组合,但我们可以用更形象的方式理解:曲线上每个点的位置,都是由所有控制点按一定比例 "混合" 而成的

以三次贝塞尔曲线为例,想象有一辆小车从起点开往终点,行驶过程中会受到两个中间控制点的 "引力" 影响。刚出发时,起点的引力最大,小车几乎直线冲向第一个控制点;随着前进,第一个控制点的引力逐渐减弱,第二个控制点的引力逐渐增强;快到终点时,终点的引力变成主导,小车会从第二个控制点的方向平滑地驶入终点。整个过程就像一场精心编排的舞蹈,每个控制点都在特定时刻发挥着恰到好处的作用。

这种 "混合" 比例遵循着类似二项式展开的规律,每个控制点的影响力随曲线位置呈现平滑的增减变化,这正是曲线能保持连续光滑的关键。

用代码画出贝塞尔曲线

让我们用 JavaScript 来实践一下,通过 Canvas 绘制一条三次贝塞尔曲线:

// 获取画布元素
const canvas = document.getElementById('bezierCanvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
canvas.width = 600;
canvas.height = 400;
// 定义四个控制点
const startPoint = { x: 50, y: 200 };         // 起点
const controlPoint1 = { x: 200, y: 50 };      // 第一个控制点
const controlPoint2 = { x: 400, y: 350 };     // 第二个控制点
const endPoint = { x: 550, y: 200 };          // 终点
// 绘制辅助线和控制点(帮助理解)
ctx.strokeStyle = '#cccccc';
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
ctx.lineTo(controlPoint1.x, controlPoint1.y);
ctx.lineTo(controlPoint2.x, controlPoint2.y);
ctx.lineTo(endPoint.x, endPoint.y);
ctx.stroke();
// 绘制控制点标记
[startPoint, controlPoint1, controlPoint2, endPoint].forEach((point, index) => {
    ctx.fillStyle = index === 0 || index === 3 ? 'green' : 'red';
    ctx.beginPath();
    ctx.arc(point.x, point.y, 6, 0, Math.PI * 2);
    ctx.fill();
});
// 绘制贝塞尔曲线(这才是主角!)
ctx.strokeStyle = '#3366ff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(startPoint.x, startPoint.y);
// 核心API:绘制三次贝塞尔曲线
ctx.bezierCurveTo(
    controlPoint1.x, controlPoint1.y,
    controlPoint2.x, controlPoint2.y,
    endPoint.x, endPoint.y
);
ctx.stroke();

运行这段代码,你会看到一条蓝色的平滑曲线,旁边还有灰色的辅助线连接着四个控制点。绿色的是起点和终点,红色的是中间控制点。试着修改控制点的坐标值,你会发现曲线的形状会随之发生奇妙的变化 —— 这就是贝塞尔曲线的魅力所在。

动画中的贝塞尔魔法

在动画领域,贝塞尔曲线更是不可或缺的工具。当你看到一个物体先加速后减速的自然运动,或者一个元素平滑地转弯绕行时,很可能就是贝塞尔曲线在背后默默工作。

比如下面这个简单的动画示例,让一个小球沿着贝塞尔曲线运动:

const ball = document.getElementById('ball');
let time = 0;
function updateBallPosition() {
    // 计算当前时间在动画中的比例(0到1之间)
    time += 0.01;
    if (time > 1) time = 0;
    
    const t = time;
    // 三次贝塞尔曲线的位置计算公式(简化版)
    const cx = 3 * (1 - t) * (1 - t) * t * controlPoint1.x 
             + 3 * (1 - t) * t * t * controlPoint2.x 
             + t * t * t * endPoint.x 
             + (1 - t) * (1 - t) * (1 - t) * startPoint.x;
             
    const cy = 3 * (1 - t) * (1 - t) * t * controlPoint1.y 
             + 3 * (1 - t) * t * t * controlPoint2.y 
             + t * t * t * endPoint.y 
             + (1 - t) * (1 - t) * (1 - t) * startPoint.y;
             
    // 更新小球位置
    ball.style.left = `${cx}px`;
    ball.style.top = `${cy}px`;
    
    requestAnimationFrame(updateBallPosition);
}
// 开始动画
updateBallPosition();

这段代码通过不断计算小球在贝塞尔曲线上的位置,让它看起来像是沿着一条平滑的路径运动。你可以调整控制点的位置,让小球做出各种有趣的轨迹 —— 直线、弧线、S 形曲线,甚至是看似不可能的急转弯。

无处不在的贝塞尔曲线

贝塞尔曲线的应用远不止于此:从你手机上的图标设计到汽车的流线型车身,从字体的优美轮廓到地图上的路线规划,都能看到它的身影。每当你在屏幕上画出一条平滑的线条,或者看到一个自然流畅的动画时,不妨想一想:这背后是不是有贝塞尔曲线在施展魔法?

下次当你再看到那些令人赞叹的数字设计时,或许会对它们多一份理解和欣赏 —— 因为你知道,那些看似复杂的曲线背后,其实是几个控制点和一段精妙的数学逻辑共同谱写的优雅篇章。

CommonJS 与 ESM

作者 G等你下课
2025年7月3日 09:47

CommonJS 与 ESM

bible-2778631_1280.jpg

Node.js 自诞生以来发展出了两套模块系统:CommonJS (CJS) 和 ECMAScript Modules (ESM)。这两套系统在设计理念、语法和使用方式上都有显著差异,理解它们的特性和适用场景对现代 Node.js 开发至关重要。

一、CommonJS:Node.js 的传统模块系统

  1. 基本特性

    CommonJS是2009年随 Node.js 一起出现,其设计目的是问了满足服务端JavaScript的模块化需求

  2. 语法示例

    使用require() 导入模块,使用 module.exports 或 exports 导出

    // 导入模块
    const fs = require('fs');
    const { funcA } = require('./moduleA');
    
    // 导出模块
    module.exports = {
      foo: 'bar',
      doSomething: function() {}
    };
    
    // 或者
    exports.doSomething = function() {};
    
  3. 特性

    • 同步加载:模块在require()调用时立即加载并执行
    • 动态依赖:可根据条件动态引入不同模块
    • 缓存机制:模块首次加载后会被缓存,后续require()调用返回缓存实例,可以通过require.cache可访问缓存

二、ECMAScript Modules (ESM):JavaScript 标准模块系统

  1. 基本特性

    ECMAScript Modules (ESM),ES6 (2015) 语言标准的一部分,其目的是为了统一浏览器和服务端的模块系统

  2. 语法示例

    使用 import / export 语法 导入导出

    // 导入模块
    import fs from 'fs';
    import { funcA } from './moduleA.js';
    import * as utils from './utils.js';
    
    // 导出模块
    export const foo = 'bar';
    export function doSomething() {}
    
    // 默认导出
    export default function() {}
    
  3. 特性

    • 静态结构:导入导出必须在顶层,不能动态导入(除 import()
    • 异步加载:模块加载过程异步
    • 严格模式:默认启用严格模式
    • 文件扩展名:必须明确指定(如 .js.mjs

初识XPath——了解XML与HTML文档中的导航神器

作者 烛阴
2025年7月3日 09:08

引言

在Web开发和自动化测试中,常常需要定位和操作页面中的元素。传统上,我们用CSS选择器,但在某些复杂场景下,XPath是一种更强大、更灵活的工具。本文将带你由浅入深,了解XPath的基本概念和用法。


什么是XPath?

XPath(XML Path Language)是一种用于在XML文档中查找信息的语言。由于HTML是HTML5的标准变体,可视为XML的一种,只要遵守标准,同样适用XPath。

XPath的用途

  • 选择特定元素或一组元素
  • 计算元素的路径
  • 提取元素的内容或属性
  • 在自动化测试框架(如Selenium)中定位元素

XPath的基本结构

XPath表达式类似路径,用于从文档的根节点开始,逐层筛选目标。

例子

<html>
  <body>
    <div id="main">
      <h1>标题</h1>
      <p class="text">这是一段文字。</p>
    </div>
  </body>
</html>

对应的XPath:

  • 选择<h1>//h1
  • 选择<p>//p[@class='text']
  • 选择<div id="main">//div[@id='main']

常用的XPath表达式

表达式 描述 示例
/ 从根节点开始,绝对路径 /html/body/div
// 在文档中查找匹配的元素,不考虑层级 //p
. 当前节点 ./span
.. 父节点 ../div
@属性名 指定属性 //a[@href='https://']
* 任意元素 //*/a

结合条件过滤

  • [条件]:筛选出满足条件的元素
  • 例://div[@class='main']:选择class为main的div
  • 叠加过滤://ul/li[1]:第一个li元素

实战演练:用XPath定位元素

如果你安装了谷歌浏览器,可以安装Xpath测试器进行实战演练

screenshot_2025-07-02_19-02-00.png


小结

XPath是网页元素定位的重要工具,掌握其基础语法可以帮助你更高效地进行网页自动化、数据抓取与测试验证。


如果你喜欢本教程,记得点赞+收藏!关注我获取更多JavaScript开发干货。

跨域问题解决方案:开发代理

2025年7月3日 08:39

由于浏览器的同源策略,当开发环境中的前端应用试图与后端服务进行通信时,经常会遇到跨域问题,开发代理为我们提供了一种简单而有效的解决方案。

一、开发代理的概念与适用场景

(一)开发代理

开发代理是一种在开发环境中使用的代理机制,它允许前端应用在开发阶段绕过浏览器的同源策略。开发代理的核心思想是,将前端应用的请求转发到后端服务,从而解决跨域问题。

(二)适用场景

开发代理适用于以下场景:

  • 生产环境不发生跨域,但开发环境发生跨域:在生产环境中,前端应用和后端服务通常部署在同一域名下,不会发生跨域问题。但在开发环境中,前端应用和后端服务通常运行在不同的端口或域名下,会发生跨域问题。
  • 开发阶段的快速迭代:开发代理可以帮助开发者在开发阶段快速迭代,无需担心跨域问题。

二、如何配置开发代理

(一)Vue.js开发服务器代理配置

在Vue.js项目中,可以通过vue.config.js文件配置开发服务器的代理。以下是一个示例配置:

// vue.config.js
module.exports = {
  devServer: { // 配置开发服务器
    proxy: { // 配置代理
      "/api": { // 若请求路径以 /api 开头
        target: "http://dev.taobao.com", // 将其转发到 http://dev.taobao.com
        changeOrigin: true, // 允许跨域
      },
    },
  },
};

(二)配置说明

  • /api:这是代理的路径前缀。当请求路径以/api开头时,开发服务器会将请求转发到指定的target地址。
  • target:这是后端服务的地址。开发服务器会将请求转发到这个地址。
  • changeOrigin:这是一个可选配置项,当设置为true时,允许跨域。

(三)其他框架的代理配置

其他前端框架(如React.js、Angular.js)也提供了类似的代理配置功能。例如,在React.js项目中,可以通过package.json文件配置代理:

{
  "proxy": "http://dev.taobao.com"
}

三、开发代理的优势

(一)简单易用

开发代理的配置非常简单,只需在开发服务器的配置文件中添加几行代码即可。

(二)无需修改后端代码

开发代理在前端应用和后端服务之间起到了中间人的作用,无需修改后端代码即可解决跨域问题。

(三)适用于开发环境

开发代理主要适用于开发环境,不会影响生产环境的部署。

四、总结

开发代理是一种简单而有效的解决前端开发中跨域问题的方法。通过在开发服务器中配置代理,可以轻松解决开发环境中的跨域问题,无需修改后端代码。

Vue路由模式大揭秘:选对模式,页面跳转不再"迷路"!

2025年7月3日 08:26

大家好,我是小杨,一个干了6年前端的老兵。今天咱们聊聊Vue路由的模式问题,很多新手在配置路由时,往往对hashhistory模式傻傻分不清楚,结果部署上线后各种404、页面刷新白屏,甚至SEO不友好。今天我就带大家彻底搞懂Vue路由的几种模式,让你在项目里游刃有余!


一、Vue路由的两种核心模式

Vue Router默认支持两种路由模式:

  1. Hash模式mode: 'hash'
  2. History模式mode: 'history'

此外,还有Memory模式(主要用于非浏览器环境,比如SSR或移动端),但今天我们主要讨论前两种。


二、Hash模式:带#号的URL

1. 特点

  • URL里带#,比如 http://example.com/#/home
  • 不依赖服务器配置,刷新不会404
  • 兼容性好,IE9+都能跑

2. 原理

Hash模式利用的是浏览器的锚点(hash) 特性,#后面的变化不会触发页面刷新,但会触发hashchange事件,Vue Router监听这个事件来实现路由切换。

3. 适用场景

  • 静态网站托管(GitHub Pages、Netlify等)
  • 不想折腾服务器配置的情况
  • 需要兼容老浏览器的项目

4. 代码示例

const router = new VueRouter({
  mode: 'hash', // 默认就是hash,可以不写
  routes: [...]
})

三、History模式:优雅的URL

1. 特点

  • URL干净,比如 http://example.com/home
  • 依赖服务器配置,否则刷新会404
  • 需要后端支持(Nginx/Apache/Node.js等)

2. 原理

History模式利用HTML5的history.pushStatehistory.replaceStateAPI,让URL变化但不刷新页面,同时能记录浏览历史。

3. 适用场景

  • 需要SEO友好的项目
  • 企业级应用,追求专业URL风格
  • 能控制服务器配置的情况

4. 代码示例

const router = new VueRouter({
  mode: 'history', // 使用history模式
  routes: [...]
})

5. 服务器配置(避免刷新404)

Nginx配置

location / {
  try_files $uri $uri/ /index.html; # 所有路径都回退到index.html
}

Node.js(Express)

app.get('*', (req, res) => {
  res.sendFile(path.resolve(__dirname, 'dist', 'index.html'))
})

四、Memory模式:无URL变化的路由

1. 特点

  • URL不会变,完全由前端JS控制
  • 适用于非浏览器环境(如React Native、Electron、SSR)
  • 不会影响SEO,因为压根没有URL变化

2. 代码示例

const router = new VueRouter({
  mode: 'abstract', // Vue 2叫abstract,Vue 3叫memory
  routes: [...]
})

五、如何选择路由模式?

模式 适用场景 是否需要服务器配置 SEO友好 兼容性
Hash 静态托管、兼容老浏览器 ❌ 不需要 ❌ 不友好 IE9+
History 企业级应用、SEO优化 ✅ 需要 ✅ 友好 IE10+
Memory 非浏览器环境(SSR、Electron) ❌ 不需要 ❌ 不适用 所有环境

我的经验总结

  1. 个人博客/静态网站 → Hash模式(省事)
  2. 企业后台/电商网站 → History模式(专业)
  3. React Native/Electron → Memory模式(无URL需求)

六、常见坑点 & 解决方案

1. History模式刷新404?

  • 原因:服务器没正确配置回退到index.html
  • 解决:参考上面的Nginx/Node.js配置

2. Hash模式SEO差?

  • 解决:用服务端渲染(SSR)或预渲染(Prerender)

3. 微信内置浏览器兼容性问题?

  • 解决:强制使用Hash模式,避免微信的奇葩history兼容问题

七、总结

  • Hash模式:简单省事,适合静态网站
  • History模式:专业优雅,但需要服务器支持
  • Memory模式:非浏览器环境专属

选对模式,能让你的项目少踩很多坑!如果你在项目里遇到过路由的奇葩问题,欢迎在评论区分享,我帮你分析~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

路由守卫通关秘籍:这些钩子函数让你的页面跳转稳如老狗!

2025年7月3日 08:21

大家好,我是小杨,一个做了6年前端的老司机。今天我们来聊聊路由守卫这个在前端开发中特别实用的功能,它能帮我们控制页面的跳转流程,就像给网站请了个尽职的保安。

一、什么是路由守卫?

简单说,路由守卫就是页面跳转时的"安检门"。比如:

  • 用户没登录想进会员中心?拦住!
  • 页面数据没保存就想离开?弹窗提醒!
  • 普通员工想访问管理员页面?门都没有!

二、Vue路由的三大守卫钩子

1. 全局守卫 - 整个网站的保安队长

// 全局前置守卫(每次跳转前都会触发)
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !我.store.state.isLogin) {
    next('/login') // 去登录页
  } else {
    next() // 放行
  }
})

// 全局解析守卫(适合做权限校验)
router.beforeResolve(async to => {
  if (to.meta.requiresAdmin) {
    await 我.checkAdminRole() // 异步检查权限
  }
})

// 全局后置钩子(跳转完成后触发)
router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath) // 可以在这里做页面统计
})

2. 路由独享守卫 - 特定页面的专属安检

const routes = [
  {
    path: '/dashboard',
    component: Dashboard,
    beforeEnter: (to, from, next) => {
      if (!我.store.state.userInfo.vip) {
        next('/upgrade') // 非VIP跳转到升级页
      } else {
        next()
      }
    }
  }
]

3. 组件内守卫 - 组件自己的小门卫

export default {
  beforeRouteEnter(to, from, next) {
    // 注意!这里还不能用this
    next(vm => {
      vm.initData() // 通过vm访问组件实例
    })
  },
  
  beforeRouteUpdate(to, from, next) {
    // 当前路由改变但组件被复用时触发
    this.fetchData(to.params.id)
    next()
  },
  
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      if (confirm('有未保存的修改,确定离开吗?')) {
        next()
      } else {
        next(false) // 取消导航
      }
    } else {
      next()
    }
  }
}

三、React路由守卫实现方案

React Router没有内置守卫,但我们可以自己实现:

1. 高阶组件方式

function PrivateRoute({ component: Component, ...rest }) {
  return (
    <Route
      {...rest}
      render={props =>
        我.isAuthenticated ? (
          <Component {...props} />
        ) : (
          <Redirect to="/login" />
        )
      }
    />
  )
}

2. 自定义Hook方式

function useAuthGuard() {
  const history = useHistory()
  
  useEffect(() => {
    if (!我.store.getState().auth.isLogin) {
      history.replace('/login')
    }
  }, [history])
}

// 在需要守卫的组件中使用
function AdminPage() {
  useAuthGuard()
  return <div>管理员页面</div>
}

四、实战中的骚操作

  1. 动态路由加载:在beforeEach中按需加载路由
router.beforeEach(async (to) => {
  if (to.meta.requiresAdmin && !我.hasAdminRoute) {
    await 我.loadAdminRoutes() // 动态添加路由
    return to.fullPath // 重定向到目标页
  }
})
  1. 页面离开确认
beforeRouteLeave(to, from, next) {
  window.onbeforeunload = () => "数据可能丢失!" // 浏览器原生提示
  // ...其他逻辑
}
  1. 滚动行为控制
router.afterEach((to) => {
  if (to.meta.scrollToTop) {
    window.scrollTo(0, 0)
  }
})

五、常见坑点指南

  1. 死循环陷阱
// 错误示范!会导致无限循环
router.beforeEach((to, from, next) => {
  if (!isLogin) next('/login')
})

// 正确做法
router.beforeEach((to, from, next) => {
  if (!isLogin && to.path !== '/login') next('/login')
  else next()
})
  1. 异步操作处理
// 记得要调用next!
beforeRouteEnter(to, from, next) {
  fetchData().then(() => next()) // 别忘了next
}
  1. meta字段妙用
{
  path: '/admin',
  meta: {
    requiresAuth: true,
    requiredRole: 'admin'
  }
}

六、总结

路由守卫用得好,能帮我们实现:

  • ✅ 登录状态验证
  • ✅ 权限精细控制
  • ✅ 数据变更提示
  • ✅ 页面访问统计
  • ✅ 动态路由加载

记住守卫钩子的执行顺序:全局beforeEach → 路由beforeEnter → 组件beforeRouteEnter → 全局beforeResolve → 全局afterEach

希望这篇能帮到大家!如果有问题欢迎在评论区交流,我会把6年踩过的坑都分享出来~

⭐  写在最后

请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.

✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式

✅ 认为我部分代码过于老旧,可以提供新的API或最新语法

✅ 对于文章中部分内容不理解

✅ 解答我文章中一些疑问

✅ 认为某些交互,功能需要优化,发现BUG

✅ 想要添加新功能,对于整体的设计,外观有更好的建议

✅ 一起探讨技术加qq交流群:906392632

最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!

❌
❌