普通视图

发现新文章,点击刷新页面。
今天 — 2025年8月23日技术

Vue3的渲染秘密:从同步批处理到异步微任务

2025年8月23日 11:39

面试里,当面试官把两段看似「都是改 5 次数据」的代码摆在你面前,却问「渲染了几次?」,如果你只回答「改了 5 次所以 5 次」,那大概率就踩坑了。本文用 100 行代码把同步批处理与异步微任务的底层机制拆开讲透,让你以后遇到同类问题直接秒答。

一、手写一个带合并渲染的 Component

需求

  1. 修改数据时触发 render
  2. 同步多次修改只触发一次 render

实现思路

利用 微任务队列 把同一次事件循环里的多次 set 合并到下一帧执行。

class Component {
  data = { name: '' }
  _pending = false          // 标记位:是否有未 flush 的修改

  constructor() {
    // 通过 Proxy 拦截所有属性写入
    this.data = new Proxy(this.data, {
      set: (target, key, value) => {
        target[key] = value
        this.scheduleRender()
        return true
      }
    })
  }

  scheduleRender() {
    if (this._pending) return   // 已在队列中,跳过
    this._pending = true
    queueMicrotask(() => {
      this.render()
      this._pending = false
    })
  }

  render() {
    console.log(`render - name: ${this.data.name}`)
  }
}

// 测试
const com = new Component()
com.data.name = '张三'
com.data.name = '李四'
com.data.name = '王五'
setTimeout(() => com.data.name = '巷子', 0)

输出顺序:

render - name: 王五      // 第一帧批处理
render - name: 巷子      // setTimeout 宏任务

核心原理:queueMicrotask 把多次写操作合并到同一微任务阶段,只执行一次 render

二、Vue 中同步 vs 异步赋值到底渲染几次?

代码一:同步 for 循环

<script setup>
import { ref } from 'vue'
const rCount = ref(0)
for (let i = 1; i <= 5; ++i) {
  rCount.value = i
}
</script>

渲染次数:2 次

  1. 初始挂载:渲染 0
  2. 批处理队列:5 次赋值被合并,最终渲染 5

Vue 内部使用 异步队列(queueJob) 收集同步变更,下一事件循环统一 flush。同一代码块内无论改多少次,都只走一次 DOM diff。

代码二:setTimeout 异步循环

<script setup>
import { ref } from 'vue'
const rCount = ref(0)
for (let i = 1; i <= 5; ++i) {
  setTimeout(() => rCount.value = i, 0)
}
</script>

渲染次数:6 次

  1. 初始挂载:渲染 0
  2. 每个 setTimeout 回调都是一个独立宏任务,Vue 的批处理无法跨任务合并,于是 5 次回调触发 5 次独立渲染。

总结

同步代码 → 全部进入同一批处理队列 → 1 次渲染

异步代码 → 每次回调独立任务 → n 次渲染

如何在vue项目中封装自己的全局message组件?一步教会你!

作者 折果
2025年8月23日 11:33

 我都使用过element plus 或者ant design Vue这些UI组件库,像element的全局message组件是如何通过一个ElMessage.success(),.warning(),.error()方法调用UI弹框显示,我们明明没有在template区域注入组件,这是为什么呢?答案就在下面!

这里因个人习惯,我使用的是typescript。

一、先定义message类型和实例类型。

import { createVNode } from 'vue';
import type { VNode } from 'vue'
import type { App } from 'vue';

// 定义通知类型
export type NotificationType = 'info' | 'success' | 'warning' | 'error';

// 定义通知选项
export interface NotificationOptions {
  message: string;
  type?: NotificationType;
  duration?: number;
  onClose?: () => void;
}

// 定义通知实例
interface NotificationInstance {
  id: number;
  vnode: VNode;
  container: HTMLDivElement;
}

二、创建容器实例和通知列表、id计数器,每一个通知实例都有一个唯一的id。

// 通知容器
let notificationContainer: HTMLElement | null = null;
// 通知实例列表
const notifications: NotificationInstance[] = [];
// ID计数器
let seed = 0;

三、创建容器,自定义样式模板

// 创建通知容器
const createNotificationContainer = () => {
  if (notificationContainer) return;

  notificationContainer = document.createElement('div');
  notificationContainer.className = 'global-notification-container';
  document.body.appendChild(notificationContainer);

  // 添加样式
  const style = document.createElement('style');
  style.textContent = `
    .global-notification-container {
      position: fixed;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 9999;
      width: fit-content;
      max-width: 1000px;
    }
    
    .global-notification {
      margin-bottom: 16px;
      padding: 12px 20px;
      border-radius: 4px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      display: flex;
      align-items: center;
      transform: translateY(-100%);
      opacity: 0;
      transition: all 0.3s ease;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      min-width: 300px;
      justify-content: center;
    }
    
    .global-notification.show {
      transform: translateY(0);
      opacity: 1;
    }
    
    .global-notification.info {
      background-color: #e6f7ff;
      border: 1px solid #91d5ff;
      color: #333;
    }
    
    .global-notification.success {
      background-color: #f6ffed;
      border: 1px solid #b7eb8f;
      color: #333;
    }
    
    .global-notification.warning {
      background-color: #fffbe6;
      border: 1px solid #ffe58f;
      color: #333;
    }
    
    .global-notification.error {
      background-color: #fff2f0;
      border: 1px solid #ffccc7;
      color: #333;
    }
    
    .global-notification-icon {
      margin-right: 12px;
      font-size: 16px;
      line-height: 20px;
    }
    
    .global-notification-content {
      flex: 1;
      font-size: 14px;
      line-height: 20px;
      text-align: center;
    }
    
    .global-notification-close {
      margin-left: 12px;
      cursor: pointer;
      font-size: 14px;
      line-height: 20px;
      color: #999;
    }
    
    .global-notification-close:hover {
      color: #333;
    }
    
    .global-notification.info .global-notification-icon {
      color: #1890ff;
    }
    
    .global-notification.success .global-notification-icon {
      color: #52c41a;
    }
    
    .global-notification.warning .global-notification-icon {
      color: #faad14;
    }
    
    .global-notification.error .global-notification-icon {
      color: #ff4d4f;
    }
  `;
  document.head.appendChild(style);
};

四、创建单个通知,自定义HTML模板

// 创建单个通知
const createNotification = (options: NotificationOptions) => {
  const id = seed++;
  const type = options.type || 'info';
  const duration = options.duration === undefined ? 4500 : options.duration;

  // 创建容器元素
  const container = document.createElement('div');
  container.className = 'global-notification';

  // 创建通知内容
  const icons = {
    info: 'ℹ️',
    success: '✅',
    warning: '⚠️',
    error: '❌'
  };

  container.innerHTML = `
    <div class="global-notification-icon">${icons[type]}</div>
    <div class="global-notification-content">${options.message}</div>
    <div class="global-notification-close">×</div>
  `;

  container.classList.add(type);

  // 添加到容器
  notificationContainer?.appendChild(container);

  // 触发进入动画
  setTimeout(() => {
    container.classList.add('show');
  }, 10);

  // 绑定关闭事件
  const closeBtn = container.querySelector('.global-notification-close');
  const close = () => {
    container.classList.remove('show');
    setTimeout(() => {
      container.remove();
      options.onClose?.();
    }, 300);
  };

  closeBtn?.addEventListener('click', close);

  // 自动关闭
  if (duration > 0) {
    setTimeout(close, duration);
  }

  // 保存实例
  const instance: NotificationInstance = {
    id,
    vnode: createVNode('div'),
    container
  };

  notifications.push(instance);

  return instance;
};

五、导出方法

// 提供不同类型的通知方法
export const notification = {
  open: (options: NotificationOptions) => {
    createNotificationContainer();
    return createNotification(options);
  },

  info: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'info',
      ...options
    });
  },

  success: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'success',
      ...options
    });
  },

  warning: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'warning',
      ...options
    });
  },

  error: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'error',
      ...options
    });
  },

  // 关闭所有通知
  closeAll: () => {
    notifications.forEach(instance => {
      instance.container.remove();
    });
    notifications.length = 0;
  }
};

六、导出自定义插件

// Vue插件安装方法
export default {
  install(app: App) {
    app.config.globalProperties.$notification = notification;
    app.provide('notification', notification);
  }
};

七、注册插件

import globalMessage from './utils/global-message'
app.use(globalMessage)

八、所有代码

// src/utils/global-message.ts
import { createVNode } from 'vue';
import type { VNode } from 'vue'
import type { App } from 'vue';

// 定义通知类型
export type NotificationType = 'info' | 'success' | 'warning' | 'error';

// 定义通知选项
export interface NotificationOptions {
  message: string;
  type?: NotificationType;
  duration?: number;
  onClose?: () => void;
}

// 定义通知实例
interface NotificationInstance {
  id: number;
  vnode: VNode;
  container: HTMLDivElement;
}

// 通知容器
let notificationContainer: HTMLElement | null = null;
// 通知实例列表
const notifications: NotificationInstance[] = [];
// ID计数器
let seed = 0;

// 创建通知容器
const createNotificationContainer = () => {
  if (notificationContainer) return;

  notificationContainer = document.createElement('div');
  notificationContainer.className = 'global-notification-container';
  document.body.appendChild(notificationContainer);

  // 添加样式
  const style = document.createElement('style');
  style.textContent = `
    .global-notification-container {
      position: fixed;
      top: 20px;
      left: 50%;
      transform: translateX(-50%);
      z-index: 9999;
      width: fit-content;
      max-width: 1000px;
    }
    
    .global-notification {
      margin-bottom: 16px;
      padding: 12px 20px;
      border-radius: 4px;
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
      display: flex;
      align-items: center;
      transform: translateY(-100%);
      opacity: 0;
      transition: all 0.3s ease;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
      min-width: 300px;
      justify-content: center;
    }
    
    .global-notification.show {
      transform: translateY(0);
      opacity: 1;
    }
    
    .global-notification.info {
      background-color: #e6f7ff;
      border: 1px solid #91d5ff;
      color: #333;
    }
    
    .global-notification.success {
      background-color: #f6ffed;
      border: 1px solid #b7eb8f;
      color: #333;
    }
    
    .global-notification.warning {
      background-color: #fffbe6;
      border: 1px solid #ffe58f;
      color: #333;
    }
    
    .global-notification.error {
      background-color: #fff2f0;
      border: 1px solid #ffccc7;
      color: #333;
    }
    
    .global-notification-icon {
      margin-right: 12px;
      font-size: 16px;
      line-height: 20px;
    }
    
    .global-notification-content {
      flex: 1;
      font-size: 14px;
      line-height: 20px;
      text-align: center;
    }
    
    .global-notification-close {
      margin-left: 12px;
      cursor: pointer;
      font-size: 14px;
      line-height: 20px;
      color: #999;
    }
    
    .global-notification-close:hover {
      color: #333;
    }
    
    .global-notification.info .global-notification-icon {
      color: #1890ff;
    }
    
    .global-notification.success .global-notification-icon {
      color: #52c41a;
    }
    
    .global-notification.warning .global-notification-icon {
      color: #faad14;
    }
    
    .global-notification.error .global-notification-icon {
      color: #ff4d4f;
    }
  `;
  document.head.appendChild(style);
};

// 创建单个通知
const createNotification = (options: NotificationOptions) => {
  const id = seed++;
  const type = options.type || 'info';
  const duration = options.duration === undefined ? 4500 : options.duration;

  // 创建容器元素
  const container = document.createElement('div');
  container.className = 'global-notification';

  // 创建通知内容
  const icons = {
    info: 'ℹ️',
    success: '✅',
    warning: '⚠️',
    error: '❌'
  };

  container.innerHTML = `
    <div class="global-notification-icon">${icons[type]}</div>
    <div class="global-notification-content">${options.message}</div>
    <div class="global-notification-close">×</div>
  `;

  container.classList.add(type);

  // 添加到容器
  notificationContainer?.appendChild(container);

  // 触发进入动画
  setTimeout(() => {
    container.classList.add('show');
  }, 10);

  // 绑定关闭事件
  const closeBtn = container.querySelector('.global-notification-close');
  const close = () => {
    container.classList.remove('show');
    setTimeout(() => {
      container.remove();
      options.onClose?.();
    }, 300);
  };

  closeBtn?.addEventListener('click', close);

  // 自动关闭
  if (duration > 0) {
    setTimeout(close, duration);
  }

  // 保存实例
  const instance: NotificationInstance = {
    id,
    vnode: createVNode('div'),
    container
  };

  notifications.push(instance);

  return instance;
};

// 提供不同类型的通知方法
export const notification = {
  open: (options: NotificationOptions) => {
    createNotificationContainer();
    return createNotification(options);
  },

  info: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'info',
      ...options
    });
  },

  success: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'success',
      ...options
    });
  },

  warning: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'warning',
      ...options
    });
  },

  error: (message: string, options?: Omit<NotificationOptions, 'message' | 'type'>) => {
    return notification.open({
      message,
      type: 'error',
      ...options
    });
  },

  // 关闭所有通知
  closeAll: () => {
    notifications.forEach(instance => {
      instance.container.remove();
    });
    notifications.length = 0;
  }
};

// Vue插件安装方法
export default {
  install(app: App) {
    app.config.globalProperties.$notification = notification;
    app.provide('notification', notification);
  }
};

效果展示

编辑

编辑

CSS 布局小技巧:用 padding 撑开 div 不香吗?

作者 复苏季风
2025年8月23日 11:20

在 CSS 中,利用padding撑开div而不直接设置width的写法有以下几个显著优势:

image.png

当不设置固定`width`时,元素宽度会默认随内容自然扩展,
配合`padding`可以在内容周围创建空间,
同时保持元素宽度与内容动态匹配。
这在处理不确定长度的内容(如文本、动态加载的内容)时非常实用,避免内容溢出或留白过多。
  1. 简化响应式设计
    不固定宽度的元素会自动适应父容器的可用空间,配合padding可以在不同屏幕尺寸下保持一致的内边距比例,无需为不同断点重复设置宽度,减少代码冗余。

  2. 避免盒模型计算问题
    在默认的box-sizing: content-box下,width不包含paddingborder。如果设置固定width再添加padding,实际宽度会超出预期(需要手动计算调整)。而利用padding撑开元素时,宽度会自动包含内边距(配合box-sizing: border-box效果更佳),避免计算错误。

  3. 灵活的比例控制
    结合padding-top/bottom的百分比值(相对于父元素宽度),可以创建固定比例的容器(如 16:9 的视频框),这种技巧在响应式布局中常用于保持元素的宽高比。

示例代码:

/* 利用padding撑开元素,不设置固定width */
.box {
  padding: 20px;
  box-sizing: border-box; /* 确保padding不增加总宽度 */
  background: #f0f0f0;
}

先说优点:这波操作有点东西

最让开发者心动的莫过于自适应内容宽度这个特性。想象一下你要做一个标签组件,标签里的文字可能是 "热门",也可能是 "新品上市",如果傻乎乎地写死 width,要么文字溢出要么留白太多。但用 padding 就不一样了,设置padding: 5px 10px后,标签会像有弹性的气球,内容多长它就多大,完美贴合内容尺寸,从此和 "内容溢出" 警告说拜拜。

在响应式设计中,这种写法更是救星。现在的设备屏幕尺寸五花八门,从手机到平板再到电脑,固定 width 的元素在小屏幕上可能直接撑破容器。而不设 width 只加 padding 的元素,会乖乖地根据父容器宽度调整自己的大小,内边距比例还能保持一致。这意味着你不用在媒体查询里反复修改 width 值,少写 N 行代码的快乐谁懂啊!

还有个容易被忽略的好处是避免盒模型计算灾难。默认情况下 CSS 的盒模型是content-box,width 不包含 padding 和 border。假设你设置width: 200px; padding: 20px,实际宽度会变成 240px,这种 "算不准" 的情况经常导致布局错位。但用 padding 撑开 div 时,配合box-sizing: border-box,元素的总宽度会自动包含 padding,再也不用拿着计算器算来算去,妈妈再也不用担心我数学不好了。

特别值得一提的是比例控制小技巧。当你设置padding-top: 56.25%时(16:9 的比例),元素会形成一个固定比例的容器,这在视频播放器、图片占位符等场景中简直是神器。无论父容器怎么缩放,这个容器都能保持完美比例,比固定 width+height 的方式灵活多了。

再谈缺点:不是万能钥匙

当然这种写法也不是没有短板。最明显的问题是宽度无法精确控制。如果你的设计稿里某个元素必须是 300px 宽,那用 padding 撑开就很难保证精度,因为内容长度会直接影响最终宽度。这时候强行用 padding 反而会让你陷入 "调整 padding 值试错" 的循环,反而不如直接写 width 来得痛快。

在某些特殊布局中还可能遇到父容器宽度依赖问题。当元素设置padding-top百分比值时,这个百分比是相对于父元素宽度计算的。如果父元素没有明确宽度,就可能出现 "子元素比父元素还宽" 的尴尬情况,这种时候调试起来能让你怀疑人生。

还有个容易踩的坑是嵌套布局复杂化。当你在一个用 padding 撑开的元素里再嵌套同样布局的子元素时,子元素的宽度会继承父元素的 "自适应特性",有时候会出现意想不到的宽度叠加效果。比如父元素 padding 导致宽度变大,子元素的 padding 又在此基础上叠加,最后可能超出预期尺寸。

另外在极端内容场景下表现不佳。如果内容少到只有一个字,元素可能会窄得像条线;如果内容超长且不换行,又会无限拉伸容器宽度。这时候需要配合min-width、max-width或word-break等属性才能解决,反而增加了代码复杂度。

总结:合适的才是最好的

其实没有绝对好或坏的布局方式,只有适合不适合的场景。当你需要做按钮、标签、徽章等小组件,或者要实现响应式比例容器时,padding 撑开 div 的写法绝对值得一试;但如果遇到精确尺寸要求的元素,或者复杂的多层嵌套布局,固定 width 可能更靠谱。

记住 CSS 布局的真谛:没有银弹,只有权衡。下次写布局时不妨多试试不同的方法,或许会发现更多有趣的小技巧。最后留个小问题:你在项目中用过 padding 撑开 div 的写法吗?遇到过哪些有趣的问题?欢迎在评论区分享你的经历~

Redux vs Redux Toolkit 从传统 Redux 优雅升级

作者 海海思思
2025年8月23日 10:51

Redux Toolkit (RTK) 是 Redux 官方推荐的、用于高效、简洁地编写 Redux 逻辑的现代工具集。它旨在解决 Redux 原始写法中常见的“样板代码过多”、“配置复杂”和“新手入门门槛高”等问题,通过提供一组经过良好实践验证的工具和抽象,让开发者能够更轻松地构建 Redux 应用。

Redux Toolkit 核心目标

  1. 减少样板代码 (Boilerplate): 自动生成 action creators、action types 和 immutable state updates。
  2. 提供合理的默认配置: 内置了 redux-thunk(异步逻辑)和 immer(不可变更新),开箱即用。
  3. 简化 Store 配置: 通过 configureStore 简化 store 创建过程。
  4. 包含现代 JS 工具: 集成了 createSlicecreateAsyncThunk 等高级 API。

Redux Toolkit vs. 传统 Redux 的优势

特性/方面 传统 Redux Redux Toolkit (RTK) 优势说明
代码量 大量样板代码 (types, action creators, reducers) 极大减少样板代码 createSlice 自动创建 types, actions, reducer,代码更简洁、易维护。
状态更新 手动编写 immutable 逻辑 (展开运算符) 使用 immer,可直接写“可变”逻辑 开发者无需手动处理深拷贝,逻辑更直观,减少错误。
Store 配置 手动组合 reducers,手动应用中间件 configureStore 自动合并 reducer,配置 devtools 配置更简单、更健壮,默认开启 Redux DevTools。
异步逻辑 依赖中间件 (如 redux-thunk),需手动集成 内置 createAsyncThunk,更结构化处理异步 createAsyncThunk 自动生成 pending/fulfilled/rejected actions,逻辑更清晰。
不可变性 开发者完全负责 内置 immer,自动处理不可变更新 降低因错误的 mutable 操作导致 bug 的风险。
学习曲线 相对陡峭,概念多 更平缓,API 设计更符合直觉 新手更容易上手,核心概念集中在 createSliceconfigureStore

实际业务案例:用户管理模块

假设我们需要构建一个用户管理模块,功能包括:

  • 获取用户列表 (异步)
  • 添加用户 (异步)
  • 更新用户状态 (同步)
  • 删除用户 (同步)

1. 传统 Redux 写法 (繁琐)

// actionTypes.js
export const FETCH_USERS_REQUEST = 'FETCH_USERS_REQUEST';
export const FETCH_USERS_SUCCESS = 'FETCH_USERS_SUCCESS';
export const FETCH_USERS_FAILURE = 'FETCH_USERS_FAILURE';
export const ADD_USER = 'ADD_USER';
export const UPDATE_USER_STATUS = 'UPDATE_USER_STATUS';
export const DELETE_USER = 'DELETE_USER';

// actions.js
import * as api from '../api/userApi';
import {
  FETCH_USERS_REQUEST,
  FETCH_USERS_SUCCESS,
  FETCH_USERS_FAILURE,
  ADD_USER,
  UPDATE_USER_STATUS,
  DELETE_USER,
} from './actionTypes';

// 同步 Actions
export const updateUserStatus = (userId, status) => ({
  type: UPDATE_USER_STATUS,
  payload: { userId, status },
});

export const deleteUser = (userId) => ({
  type: DELETE_USER,
  payload: { userId },
});

// 异步 Actions (Thunk)
export const fetchUsers = () => {
  return (dispatch) => {
    dispatch({ type: FETCH_USERS_REQUEST });
    api
      .getUsers()
      .then((users) => {
        dispatch({ type: FETCH_USERS_SUCCESS, payload: users });
      })
      .catch((error) => {
        dispatch({ type: FETCH_USERS_FAILURE, error: error.message });
      });
  };
};

export const addUser = (userData) => {
  return (dispatch) => {
    // 假设 API 返回新用户对象
    api
      .createUser(userData)
      .then((newUser) => {
        dispatch({ type: ADD_USER, payload: newUser });
      })
      .catch((error) => {
        // 通常需要 dispatch 一个错误 action 或在组件中处理
        console.error('Failed to add user:', error);
      });
  };
};

// reducer.js
import {
  FETCH_USERS_REQUEST,
  FETCH_USERS_SUCCESS,
  FETCH_USERS_FAILURE,
  ADD_USER,
  UPDATE_USER_STATUS,
  DELETE_USER,
} from './actionTypes';

const initialState = {
  users: [],
  loading: false,
  error: null,
};

const userReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_USERS_REQUEST:
      return {
        ...state,
        loading: true,
        error: null,
      };
    case FETCH_USERS_SUCCESS:
      return {
        ...state,
        loading: false,
        users: action.payload, // 假设 payload 是用户数组
      };
    case FETCH_USERS_FAILURE:
      return {
        ...state,
        loading: false,
        error: action.error,
      };
    case ADD_USER:
      return {
        ...state,
        users: [...state.users, action.payload], // 手动 immutable
      };
    case UPDATE_USER_STATUS:
      return {
        ...state,
        users: state.users.map((user) =>
          user.id === action.payload.userId
            ? { ...user, status: action.payload.status }
            : user
        ),
      };
    case DELETE_USER:
      return {
        ...state,
        users: state.users.filter((user) => user.id !== action.payload.userId),
      };
    default:
      return state;
  }
};

export default userReducer;

// store.js
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import userReducer from './reducers/userReducer';

const rootReducer = combineReducers({
  users: userReducer,
  // ... 其他 reducers
});

const store = createStore(
  rootReducer,
  applyMiddleware(thunk)
);

export default store;

问题:代码量大,分散在多个文件,action types 容易出错,reducer 中的 immutable 逻辑繁琐。

2. Redux Toolkit 写法 (简洁高效)

// features/users/userSlice.js
import { createSlice, createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import * as api from '../../api/userApi';

// 使用 Entity Adapter 管理标准化的实体状态 (推荐用于列表)
export const usersAdapter = createEntityAdapter();

// 定义异步 thunk
export const fetchUsers = createAsyncThunk(
  'users/fetchUsers', // action type 前缀
  async (_, { rejectWithValue }) => {
    try {
      const response = await api.getUsers();
      return response.data; // 返回的数据会成为 action.payload
    } catch (error) {
      return rejectWithValue(error.message); // 处理错误
    }
  }
);

export const addUser = createAsyncThunk(
  'users/addUser',
  async (userData, { rejectWithValue }) => {
    try {
      const response = await api.createUser(userData);
      return response.data; // 新用户对象
    } catch (error) {
      return rejectWithValue(error.message);
    }
  }
);

// 创建 slice
const usersSlice = createSlice({
  name: 'users',
  initialState: usersAdapter.getInitialState({
    // adapter 管理 entities 和 ids
    loading: 'idle', // 'idle' | 'pending' | 'succeeded' | 'failed'
    error: null,
  }),
  reducers: {
    // 同步 reducer - 使用 Immer,可直接“修改” state
    updateUserStatus(state, action) {
      const { userId, status } = action.payload;
      // 直接“修改”实体,immer 会自动处理不可变性
      const user = state.entities[userId];
      if (user) {
        user.status = status; // 这行代码在底层会被转换为不可变更新
      }
    },
    // Entity Adapter 提供的预置 reducer
    // removeOne, updateOne 等已由 adapter 提供,无需手动写
  },
  extraReducers: (builder) => {
    // 处理异步 thunk 生成的 actions
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = 'pending';
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = 'succeeded';
        // 使用 adapter 的 setAll 来替换所有用户
        usersAdapter.setAll(state, action.payload);
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = 'failed';
        state.error = action.payload; // 使用 rejectWithValue 时 payload 是错误信息
      })
      // 处理 addUser
      .addCase(addUser.fulfilled, (state, action) => {
        // 使用 adapter 的 addOne 添加新用户
        usersAdapter.addOne(state, action.payload);
      })
      // 可以添加 .addCase(addUser.rejected, ...) 处理失败
      ;
  },
});

// 导出 actions (同步和异步生成的)
export const { updateUserStatus } = usersSlice.actions;

// 导出 reducer
export default usersSlice.reducer;

// selectors - 使用 adapter 的 selectors
export const {
  selectAll: selectAllUsers,
  selectById: selectUserById,
  selectIds: selectUserIds,
} = usersAdapter.getSelectors((state) => state.users);

// 也可以创建自定义 selector
export const selectUsersLoading = (state) => state.users.loading;
export const selectUsersError = (state) => state.users.error;

// store.js (RTK 版本)
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './features/users/userSlice';

export const store = configureStore({
  reducer: {
    users: usersReducer,
    // ... 其他 slices
  },
  // devTools: true, // 默认开启
  // middleware: getDefaultMiddleware => getDefaultMiddleware(), // 默认包含 thunk, immer
});

// 通常还会导出 RootState 和 AppDispatch 类型 (TypeScript)
// export type RootState = ReturnType<typeof store.getState>
// export type AppDispatch = typeof store.dispatch

优势总结 (通过案例体现)

  1. 代码量锐减

    • 传统: actionTypes.js, actions.js (多个文件), reducer.js, store.js
    • RTK: 主要逻辑集中在 userSlice.js 一个文件 (createSlice + createAsyncThunk),store.js 极简。
  2. 自动处理样板

    • createSlice 自动根据 reducersextraReducers 中的 case 生成 action types 和 action creators。
    • createAsyncThunk 自动为 pending, fulfilled, rejected 状态生成 action types 和 creators。
  3. Immer 简化不可变更新

    • updateUserStatus reducer 中,直接写 user.status = status,而不是复杂的展开运算符。RTK 内部使用 Immer 库将其转换为不可变更新。
  4. 结构化异步处理

    • createAsyncThunk 将异步逻辑封装得更好,extraReducers 集中处理其生成的三种状态,逻辑清晰。
  5. Entity Adapter 优化实体管理

    • 对于用户列表这种常见场景,createEntityAdapter 提供了 setAll, addOne, updateOne, removeOne 等高效且标准化的 CRUD 操作,避免手动编写 mapfilter
  6. 简化 Store 配置

    • configureStore 自动合并 reducer,自动应用 redux-thunkimmer 中间件,并默认开启 Redux DevTools 扩展,无需手动配置。

结论

Redux Toolkit 并非取代 Redux,而是构建在 Redux 之上的一套最佳实践和工具集。它极大地提升了开发效率和代码可维护性,同时保留了 Redux 的核心原则(单一状态树、可预测的状态更新)。对于新项目,强烈推荐直接使用 Redux Toolkit,它可以让你专注于业务逻辑,而不是 Redux 的样板代码和配置细节。上面的用户管理案例清晰地展示了从“繁琐”到“简洁优雅”的转变。

物联网数据大屏开发效率翻倍:Vue + DataV + ECharts 的标准化模板库

作者 小码编匠
2025年8月23日 10:28

前言

数据可视化已成为企业决策、业务监控不可或缺的一环。一个直观、动态、可定制的数据大屏项目,能够极大地提升信息传递效率,帮助团队快速把握业务动态。

本文将推荐一个基于Vue、DataV、Echarts框架构建的数据大屏项目,该项目不仅支持数据动态刷新渲染,还提供图表自由替换、全局样式调整等强大功能,为开发提供一个高效、灵活的数据展示解决方案。

项目介绍

项目是一个集数据展示、图表渲染、动态更新于一体的数据大屏项目。

它利用Vue3的响应式特性,结合DataV丰富的可视化组件库和Echarts强大的图表绘制能力,实现了数据的实时渲染与动态展示。

项目支持全屏展示,通过精心设计的布局和交互,确保用户能够在大屏上获得最佳的数据浏览体验。同时,项目提供详细的文档和示例,便于开发快速上手和定制开发。

项目功能

1、数据动态刷新

通过Vue的组件化开发,实现了数据的实时获取与动态渲染,确保大屏数据始终与业务数据保持同步。

2、图表自由替换

项目内部图表可自由替换,支持Echarts官方社区中的各类图表案例,满足不同场景下的数据展示需求。

3、全局样式调整

通过全局CSS文件,开发者可以轻松调整整个项目的样式,包括字体、颜色、布局等,实现快速定制。

4、组件复用

通过封装可复用的图表组件,减少了重复代码,提高了开发效率。例如,任务通过率与任务达标率模块即使用了同一图表组件,仅通过传入不同参数实现差异化展示。

5、响应式设计

项目采用scale缩放方案,支持不同分辨率屏幕的适配,确保在不同设备上都能获得良好的展示效果。

项目说明

主要文件

文件/目录 作用/功能
main.js 主目录文件,引入 ECharts/DataV 等依赖
utils 工具函数与 mixins 函数(如自适应适配逻辑)
views/index.vue 项目主结构文件
views/其余文件 界面各区域组件(按位置命名,如 center.vue
assets 静态资源目录(Logo、背景图片等)
assets/style.scss 全局通用 CSS 文件
assets/index.scss Index 界面的专属 CSS 文件
components/echart 所有 ECharts 图表组件(按位置命名)
common/... 全局封装的 ECharts 和屏幕适配插件(如 flexible 替代方案)

项目使用

1、启动项目

依赖要求:Node.js + pnpm

步骤:

下载项目后,在主目录运行 pnpm install 拉取依赖。

启动命令:npm run serve(或通过 vue-cli 启动)。

手动全屏:按 F11

问题处理:若编译提示缺少 DataV 依赖,手动安装:

npm install @jiaminghi/data-view  # 或 yarn add @jiaminghi/data-view

2、封装组件渲染图表

基础组件:所有图表基于 common/echart/index.vue 封装,支持:

动态数据监听(使用防抖函数优化性能)。

屏幕尺寸变动自适应。

默认样式:配置文件路径 common/echart/theme.json

组件参数:

参数名 类型 作用
id String 图表唯一标识(非必填,默认使用 $el
className String 自定义样式类名(非必填)
options Object ECharts 配置项(必填)
height String 图表高度(建议填写)
width String 图表宽度(建议填写)

3、动态渲染图表

案例路径:components/echart/[图表名]/chart.vue

核心逻辑

<template>
  <div>
    <Echart :options="options" id="id" :height="height" :width="width" />
  </div>
</template>
<script>
import Echart from '@/common/echart';
export default {
  data() { return { options: {} } },
  components: { Echart },
  props: {
    cdata: { type: Object, default: () => ({}) }
  },
  watch: {
    cdata: {
      handler(newData) {
        this.options = { /* ECharts 配置 */ };
      },
      immediate: true,
      deep: true
    }
  }
};
</script>

4、复用图表组件

案例:中间部分的"任务通过率"与"任务达标率"模块

实现方式:

父组件传递参数(如 views/center.vue):

<centerChart :id="rate[0].id" :tips="rate[0].tips" :colorObj="rate[0].colorData" />
<centerChart :id="rate[1].id" :tips="rate[1].tips" :colorObj="rate[1].colorData" />
<script>
export default {
  data() {
    return {
      rate: [
        { id: "centerRate1", tips: 60, /* 其他配置 */ },
        { id: "centerRate2", tips: 40, colorData: { /* 颜色配置 */ } }
      ]
    };
  }
};
</script>

子组件接收参数(如 components/echart/center/centerChartRate.vue):

<script>
export default {
  props: ['id', 'tips', 'colorObj'],
  // 根据参数动态渲染图表
};
</script>

5、更换边框

方法:使用 DataV 自带边框组件,替换 views/[区域].vue 中的对应标签:

<dv-border-box-1></dv-border-box-1>  <!-- 边框类型1 -->
<dv-border-box-2></dv-border-box-2>  <!-- 边框类型2 -->

更多边框:参考 DataV 官网文档

6、更换图表

路径:直接修改 components/echart/[图表名] 下的文件。

参考资源:ECharts 官方示例库

7、Mixins 解决自适应适配

文件:utils/resizeMixins.js

应用:注入到 common/echart/index.vue,实现对 this.chart 的功能扩展(如窗口变化时重绘图表)。

8、屏幕适配方案

版本:1.5 版本弃用 flexible + rem,改用 CSS3 scale 缩放。

逻辑:

基准尺寸:1920px * 1080px

同比例屏幕:100% 填充。

非同比例屏幕:自动计算比例居中填充,不足部分留白。

代码路径:src/utils/userDraw.js

9、请求数据(建议)

工具:Axios

全局配置示例(main.js):

import axios from 'axios';
Vue.prototype.$http = axios.create({
  timeout: 20000,
  baseURL: 'http://172.0.0.1:8080'  
  // 后端接口地址
});

项目效果

项目源码

源码托管在代码仓库中,可以通过拉取master分支的代码获取最新版本。项目结构清晰,包含主目录文件、工具函数、界面组件、静态资源等多个目录和文件。

大家可以根据项目文档和示例代码,快速上手并定制开发。在开发过程中,建议按照功能区域重命名文件,以便于管理和维护。

Gitee:gitee.com/MTrun/big-s…

总结

一个基于Vue、DataV、Echarts框架构建的数据大屏项目,具有数据动态刷新、图表自由替换、全局样式调整等强大功能。

通过模块化设计和高度可定制的特性,项目满足了不同场景下的数据展示需求。同时,项目还提供了丰富的文档和示例代码,便于大家快速上手和定制开发。

关键词

数据大屏、Vue、DataV、Echarts、动态刷新、图表替换、响应式设计、项目源码

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

TypeScript在前端的实践:类型系统助力大型应用开发

作者 艾小码
2025年8月23日 07:43

导读:在大型前端项目中,TypeScript的静态类型系统已成为提升代码质量和协作效率的核心工具。它能在编译阶段拦截约15%-30%的运行时错误,并通过类型约束优化架构设计。本文从类型基础到框架集成,解析TS在工程中的落地实践,并客观评估其成本与收益。

一、大型应用为何需要TypeScript

当代码量超过万行时,动态类型的JavaScript难以维护。TS通过静态类型检查在编译阶段捕获90%以上的类型错误,显著降低维护成本。例如:

  • Airbnb采用TS后维护成本下降38%
  • GitHub客户端在300万行代码规模下仍高效迭代
    核心价值在于:错误前移、代码自文档化、重构安全性。

二、核心类型系统实战

基础类型与字面量类型
基础类型(string/number/boolean)提供基本约束,字面量类型精确限定取值范围:

type ButtonSize = 'small' | 'medium' | 'large'; // 消灭魔法字符串

接口(Interface) vs 类型别名(Type Alias)

  • 接口:定义对象结构,支持继承与合并,适合API响应和组件Props
  • 类型别名:灵活处理联合类型、交叉类型和工具类型
    最佳实践:优先用interface定义对象,用type处理复杂类型操作。

泛型(Generics)应用场景
实现组件和函数的类型复用,避免重复定义:

// 泛型函数:输入输出类型关联
function identity<T>(arg: T): T { return arg; }  

// 泛型约束:确保类型满足条件
function mergeById<T extends HasID>(items: T[]) { ... }

开源库中泛型使用率高达78%,是构建通用工具的核心。

三、函数与类的类型实践

函数类型安全
通过签名约束参数和返回值,支持重载应对复杂场景:

type EventHandler<T> = (event: T) => void; // 精确事件类型
const handleClick: EventHandler<MouseEvent> = (e) => { ... }; // 自动推断e属性

类与接口结合
通过访问控制符(public/private)和接口实现多态:

interface ClockInterface { setTime(d: Date): void; }
class Clock implements ClockInterface { 
  constructor(public currentTime: Date) {} 
  setTime(d: Date) { this.currentTime = d; } // 强制实现接口
}

四、React/Vue项目集成指南

组件Props类型定义

  • React:使用interface定义Props,结合React.FC类型
  • Vue:在defineComponent中用PropType声明复杂类型
// React示例
interface ButtonProps { 
  label: string; 
  type?: 'primary' | 'danger'; 
}
const Button: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>;

// Vue示例
defineComponent({
  props: { user: Object as PropType<User> } // 强制User结构
});

Hooks类型规范

  • useState显式声明初始类型:const [user, setUser] = useState<User | null>(null);
  • useEffect依赖项需类型一致,避免隐式any

五、第三方库类型处理

官方类型声明
93%的npm Top 1000库提供@types/*包,安装即获提示:

npm install @types/react @types/lodash --save-dev

自定义声明文件
为无类型库编写.d.ts文件:

declare module 'custom-lib' {
  export function calculate(data: number[]): number;
}

六、开发体验提升与成本分析

核心优势

  • 智能提示:IDE基于类型推导属性和方法,加速编码
  • 重构安全:重命名变量时依赖处自动报错
  • 协作效率:类型定义作为团队契约,减少沟通成本。

潜在成本

  • 学习曲线:高级类型(如条件类型infer)需额外学习
  • 集成成本:旧项目迁移需逐步推进,建议从新模块入手
  • 编译配置:需维护tsconfig.json(如开启strictNullChecks)。

结语
TypeScript通过类型系统将运行时错误转化为编译时提示,使代码成为自解释的文档。尽管需投入学习与配置成本,但其在维护性、协作效率和重构安全性上的回报,已使其成为大型前端工程的必选项。

Vue2知识点详细回顾(以及自己的一些思考和解答)-2

作者 GHOME
2025年8月23日 03:57

2.关于组件

2.1 组件传值(通信)的方式

2.1.1 父传后代

  1. 父组件引入子组件,绑定数据:(孙组件无法直接使用,需要子组件再传递给孙组件)
<!-- 父组件引入子组件,绑定数据 -->
<Child :str1="str1"></Child>

<!-- 子组件通过props接收 -->
props:{
    str1:{
        type: String,  //字符串类型
        default: '',   //默认值
    }
}

这种方式 子组件不能直接修改父组件的数据,例如:this.str1 = '321' //错误

  1. 子组件直接使用父组件的数据
this.$parent.str1

这种方式 子组件可以直接修改父组件的数据,例如:this.$parent.str1 = '321' //正确

  1. 依赖注入(父组件可以直接向某个后代组件传值,无需逐级传递)
// 父组件
<script>
export default {
    provide() {
        return {
            val1: '这是父组件的依赖内容'
        }
    }
}
</script>

// 任意的后代组件使用(缺点就是有点难以找到这个值是再哪个父组件中定义的)
<script>
export default {
    inject: ['val1'],
}
</script>

2.1.2 后代传父

  1. 通过触发事件传递数据
// 父组件给子组件设置一个监听事件来触发自己的函数
<h1>{{ str2 }}</h1>
<Child @change='getChildVal'></Child>

<script>
export default {
    // 省略...
    data(){
        return {
            str2: '',
        }
    },
    methods: {
        getChildVal(val){
            this.str2 = val
        }
    }
}
</script>


// 子组件通过点击按钮触发父组件的函数
<button @click="sendChildVal">按钮</button>

<script>
export default {
    data(){
        return {
            childVal: '这里是子组件的数据',
        }
    },
    methods: {
        sendChildVal(){
            this.$emit('change',this.childVal) //触发'change'事件,参数是this.childVal
        }
    }
}
</script>
  1. 父组件直接拿到子组件的数据(通过DOM)
<Child ref='child'></Child>

// 使用时:this.$refs.child.xxx

2.1.3 兄弟组件之间传值

同样是可以通过自定义事件(的监听和触发)来进行传值

2.2 父组件如何直接修改子组件的值

父组件中可以先为子组件设置ref,拿到它的DOM来操作:

<Child ref='child'></Child>

使用 this.$refs.child.xxx 去修改

2.3 子组件如何直接修改父组件的值

子组件中可以使用 this.$parent.xxx 去修改

2.4 如何找到父组件

this.$parent

2.5 如何找到根组件(一般为App.vue中的id为'app'的div)

this.$root

2.6 keep-alive

keep-alive是用来缓存当前组件的!它的核心价值在于提升用户体验和性能。常用于Tab页的切换,下面写一个例子:

// App.vue
<template>
    <div id="app">
        <nav>
            <!-- 点击跳转到不同用户的主页 -->
            <router-link to="/user_home?user_id=1">用户1</router-link>
            <router-link to="/user_home?user_id=2">用户2</router-link>
        </nav>
        <keep-alive>
            <!-- 被缓存的部分 -->
            <router-view/>
        </keep-alive>
    </div>
</template>

那么当我们第一次点击“用户1”的时候,跳转到“/user_home?user_id=1”路径时会正常执行生命周期来发送请求、渲染页面等。(会额外增加2个生命周期activateddeactivated,进入时只会额外执行activated);

当我们处于“用户1”中的时候,点击“用户2”跳转到“/user_home?user_id=2”时,原本的组件会执行deactivated,仅仅只是“失活”而不是被“销毁”。

因此,后续我们再点击“用户1”的时候就会发现“/user_home?user_id=1”路径的内容已经被缓存了,不会再执行前四个生命周期 而只会执行activated再次被激活。

2.7 slot/插槽

2.7.1 匿名插槽:插槽没有名字

当我们的组件只有一个地方需要做区分时,直接使用匿名插槽会比较方便,比如:

// 带插槽的组件 List.vue
<div class="list-box">
    <div>内容内容</div>
    <slot></slot>
</div>

// 在A页面中引用组件,并插入一行提示在 List的内容下方插槽中
<div>
    <List>
        <p>提示提示</p>
    </List>
</div>

2.7.2 具名插槽:插槽有名字

还是用List组件打比方,假设我这次既要在内容上方加标题又要在内容下方加提示,这时候就需要给每个插槽起名字:

// 带插槽(在不同位置)的组件 List.vue
<div class="list-box">
    <slot name="title"></slot>
    <div>内容内容</div>
    <slot name="tips"></slot>
</div>

// 在A页面中引用组件,并分别插入 标题 和 提示
<div>
    <List>
        <template #title>
            <h1>标题</h1>
        </template>
        <template #tips>
            <p>提示提示</p>
        </template>
    </List>
</div>

2.7.3 作用域插槽:传值

假设在组件List.vue中的data还定义了一些数据 value1,value2 需要在A页面中接收数据来对插槽的具体内容进行编辑,就需要用到作用域插槽:

// 带插槽的组件 List.vue,并且我会往“提示”的插槽传两个参数
<div class="list-box">
    <slot name="title"></slot>
    <div>内容内容</div>
    <slot name="tips" :value1="value1" :value2="value2"></slot>
</div>

// 在A页面中引用组件,并分别插入 标题 和 提示,在 提示 插槽中拿到参数
<div>
    <List>
        <template #title>
            <h1>标题</h1>
        </template>
        
        <!-- 传过来的参数是一个对象,用{value1,value2}进行解构 -->
        <template #tips="{value1,value2}">
            <p>提示提示</p>
            <p>{{ value1 }}</p>
            <p>{{ value2 }}</p>
        </template>
    </List>
</div>

2.8 provide/inject

provide/inject ===> 依赖注入

我是跟着这位老师复习的,我这里会根据我自己的相关情况补充知识点和总结知识点。 www.bilibili.com/video/BV1ef…

ServiceWorker 报 MIME 类型错误

作者 Bottle414
2025年8月23日 02:18

发生背景

笔者写前端埋点 SDK 时遇到问题,sw 文件是写在 sdk 里的,注册逻辑也写在 sdk 里了;但最终 sdk 是在 demo 里用的,导致 demo 里一直显示:

SecurityError: Failed to register a ServiceWorker for scope ('http://localhost:5173/') with script ('http://localhost:5173/sw.js'): 、The script has an unsupported MIME type ('text/html').

image.png

笔者的 sw 引入:

// sdk/src/xxx.ts

const registerServiceWorker = () => {
        if ('ServiceWorker' in window) {
            const swURL = './sw.js'
            navigator.serviceWorker
                .register(swURL)
                .then((reg) => console.log('SW registered:', reg))
                .catch((err) => console.error(err))
        }
    }

解决过程

按理来说 sw.js 应该要塞到 demo 的 ./public 文件夹里,笔者试了,的确成功了。但笔者不能让 sdk 使用者自己复制 sw.js 然后粘贴到自己的项目里,笔者不喜欢这个方案。

去网上查资料,看到有个评论说 可能是 service worker 文件没找到 导致的,笔者觉得他说的对!

想起刚刚看的资料,资料作者在vite里配置了路径,也许我也可以试试。

于是笔者打开 vite 的文档,找到静态资源处理那栏(因为正确放置位置是 public 下面,所以笔者把它当静态资源看),找到了这样的信息:

image.png

解决方案

于是笔者改为使用ESM动态导入,就像这样:

    const registerServiceWorker = () => {
        ...
            const swURL = new URL('./sw.js', import.meta.url).href
        ...
    }

image.png

锵锵,解决哩!要是你遇到了和我一样的麻烦,不妨试一试😋☝️

资料: Service Worker 使用指南service worker用的可能不多,但在很多时候却有着不可替代的作用,现在很 - 掘金

前端工程化的范式革命:从 Webpack 的“全量打包”到 Vite 的“按需编译”

作者 今禾
2025年8月23日 00:25

引言:我们为何需要构建工具?

在 2010 年代初,前端开发还停留在“三剑客”时代:HTML、CSS、JavaScript 各自为政,项目结构简单,代码量小,部署方式原始。开发者只需将几个 .html.css.js 文件上传至服务器即可上线。

但随着单页应用(SPA)的兴起、React/Vue/Angular 等框架的普及,以及 TypeScript、JSX、Sass、模块化等现代开发范式的广泛应用,前端项目变得日益复杂。一个典型的现代前端项目可能包含:

  • 数百个模块文件(.tsx, .vue, .svelte
  • 多种非 JavaScript 资源(.styl, .less, .svg, .woff2
  • 第三方依赖(node_modules 中成百上千个包)
  • 多环境配置(开发、测试、预发、生产)
  • 类型系统(TypeScript)
  • 热更新、HMR、Source Map、Tree Shaking 等高级特性

在这种背景下,手动管理这些资源和流程已完全不可行。于是,构建工具(Build Tool)应运而生——它不再只是一个“打包器”,而是整个前端工程化体系的中枢神经系统

而在这场工程化演进中,WebpackVite 分别代表了两个时代的巅峰:

  • Webpack 是“兼容性优先、功能完备”的工业级解决方案;
  • Vite 是“速度优先、现代浏览器原生能力驱动”的轻量级革命。

本文将带你从最底层的浏览器机制、模块系统、编译原理、网络协议出发,深入剖析 Webpack 与 Vite 的设计哲学、实现机制、性能差异与适用场景,助你真正理解“构建工具”背后的本质。


第一部分:前端工程化的核心问题——我们到底在解决什么?

1.1 工程化的本质:抽象与自动化

前端工程化的核心目标是:让开发者专注于业务逻辑,而非构建流程

为此,我们需要解决一系列“非功能性需求”:

问题类别 具体挑战 解决方案
模块化 浏览器原生不支持 import/export(早期) 构建工具模拟模块系统
语法转换 TSX、JSX、Sass 等无法被浏览器直接执行 Babel、esbuild、PostCSS 等编译器
依赖管理 如何解析 import 'lodash'?如何处理别名? 模块解析器(Resolver)
资源处理 图片、字体、SVG 如何引用? File Loader、URL Loader
开发体验 修改代码后需手动刷新? 热更新(HMR)、开发服务器
性能优化 首屏加载慢?Bundle 过大? 代码分割、懒加载、Tree Shaking
环境适配 开发环境 vs 生产环境? 环境变量、多配置
兼容性 需要支持 IE11? Polyfill、降级编译

这些需求共同构成了一个“工程化一揽子方案”(Engineering Suite)。

而构建工具,正是这个方案的核心引擎


1.2 构建流程的抽象模型

我们可以将现代构建工具的工作流程抽象为一个编译流水线(Pipeline):

[源码][解析][转换][依赖分析][打包/编译][优化][输出]

更具体地,可以分为以下几个阶段:

  1. 入口分析(Entry Resolution)
    main.jsx 开始,确定构建的起点。

  2. 模块解析(Module Resolution)
    解析 import 语句,找到每个模块的物理路径(支持别名、扩展名省略等)。

  3. 加载器处理(Loader Processing)
    对不同类型的文件应用不同的“加载器”(Loader),如:

    • .tsxbabel-loader → JavaScript
    • .stylstylus-loader → CSS
    • .pngfile-loader/assets/logo.abc123.png
  4. 依赖图构建(Dependency Graph Construction)
    递归分析所有模块的依赖关系,形成一棵有向无环图(DAG)。

  5. 打包或按需编译(Bundling vs On-Demand Compilation)

    • Webpack:将整个依赖图打包成一个或多个 bundle
    • Vite:仅在浏览器请求时,按需编译单个模块
  6. 插件介入(Plugin Hooks)
    在构建的各个生命周期阶段插入自定义逻辑,如生成 HTML、注入环境变量。

  7. 输出与优化(Output & Optimization)
    将结果写入磁盘或内存,进行压缩、混淆、Source Map 生成等。


第二部分:Webpack 的“全量打包”范式

2.1 Webpack 的设计哲学:Everything is a Module

Webpack 的核心思想是:一切皆模块(Everything is a Module)。
这意味着:

  • .js 文件是模块
  • .css 文件是模块(通过 css-loader
  • .png 图片是模块(通过 file-loader
  • 甚至 .json.graphql 都可以是模块

这种设计使得 Webpack 能够统一处理所有资源,实现“静态资源即模块”的抽象。


2.2 Webpack 的工作流程深度解析

我们以一个典型的 React + TypeScript 项目为例,入口文件为 src/main.tsx

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.styl'; // Stylus 文件

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

Webpack 的构建流程如下:

阶段 1:入口解析与模块加载

  • Webpack 从 entry: './src/main.tsx' 开始
  • 读取文件内容
  • 根据文件扩展名(.tsx)匹配对应的 loader

阶段 2:Loader 链式处理

Webpack 对 .tsx 文件应用 loader 链:

// webpack.config.js
{
  test: /\.tsx?$/,
  use: ['babel-loader', 'ts-loader'] // 顺序:从右到左
}

实际执行顺序:

  1. ts-loader:将 TypeScript 编译为 JavaScript(含 JSX)
  2. babel-loader:将 ES6+ 语法降级为 ES5,处理 Decorators、Class Properties 等

最终输出标准 JavaScript 代码。

阶段 3:依赖图递归构建

Webpack 会递归分析每个模块的 import 语句,构建依赖图:

main.tsx
├── react (npm 包)
├── react-dom (npm 包)
├── App.tsx
│   ├── components/Button.tsx
│   ├── utils/api.ts
│   └── styles/App.styl
│       └── stylus-loader → CSS
└── index.styl
    └── stylus-loader → CSS

这棵依赖树会存储在内存中,记录每个模块的:

  • 原始代码
  • 编译后代码
  • 依赖列表
  • 导出内容

阶段 4:打包与代码生成

Webpack 将所有模块打包成一个或多个 bundle 文件。其核心机制是:

  • 每个模块被包裹在一个 IIFE(立即执行函数)中
  • 通过 __webpack_require__ 函数模拟 CommonJS 模块系统
  • 所有模块被放入一个大的对象中,由入口模块触发执行

生成的 bundle 结构如下:

// bundle.js
(function(modules) {
  // 模拟 require
  function __webpack_require__(moduleId) {
    // 缓存、加载、执行模块
  }

  // 模块定义:moduleId -> moduleFactory
  var modules = {
    "./src/main.tsx": function(module, exports, __webpack_require__) {
      // 编译后的 main.tsx 代码
    },
    "./src/App.tsx": function(module, exports, __webpack_require__) {
      // 编译后的 App.tsx 代码
    },
    // ... 其他模块
  };

  // 启动入口
  __webpack_require__("./src/main.tsx");

})({/* modules object */});

阶段 5:插件系统介入

Webpack 提供了丰富的 生命周期钩子(Hooks),插件可以在任意阶段介入:

钩子 时机 典型用途
compile 构建开始 初始化
make 模块构建开始 自定义模块处理
emit 输出资源前 生成 HTML、注入资源
done 构建完成 打包分析、通知

例如,HtmlWebpackPluginemit 阶段生成 index.html 并自动注入 <script src="bundle.js">


2.3 Webpack Dev Server:开发环境的模拟

webpack-dev-server 是一个基于 Express 的 HTTP 服务器,其核心机制是:

  1. 启动一个本地服务器(默认 localhost:8080
  2. 将打包后的资源存储在 内存文件系统memory-fs)中
  3. 浏览器请求 /bundle.js 时,直接从内存返回
  4. 支持 HMR(Hot Module Replacement):
    • 通过 WebSocket 通知浏览器哪些模块已更新
    • 浏览器下载新模块并替换,无需刷新页面

2.4 Webpack 的性能瓶颈:为什么越来越慢?

随着项目规模增长,Webpack 的性能问题日益凸显:

1. 冷启动慢

  • 原因:必须完整构建依赖图,编译所有模块
  • 影响:大型项目冷启动可能超过 1 分钟

2. 热更新延迟

  • 修改一个文件 → 触发重新构建 → 重新打包 → HMR 推送
  • 即使只改一行代码,也可能导致整个 bundle 重建

3. 内存占用高

  • 整个依赖图常驻内存
  • 复杂项目内存占用可达 1GB+

4. 配置复杂

  • 需要手动配置 entryoutputloaderspluginsresolve
  • 学习成本高,易出错

第三部分:Vite 的“按需编译”范式

3.1 Vite 的设计哲学:Leverage Native ESM

Vite 的核心思想是:利用现代浏览器原生支持 ES Modules(ESM)的能力,避免不必要的打包

其口号是:“Instant Server Start, Lightning-Fast HMR”。


3.2 原生 ESM 的浏览器支持

现代浏览器(Chrome 61+, Firefox 60+, Safari 10.1+, Edge 16+)均已支持:

<script type="module" src="/src/main.jsx"></script>

这意味着:

  • 浏览器原生支持 import / export
  • 模块可以按需加载,无需预先打包
  • 支持动态导入 import() 实现懒加载

Vite 正是基于这一事实,颠覆了传统打包模型


3.3 Vite 的开发服务器工作原理

Vite 在开发模式下不进行打包,而是启动一个基于 Koa 的轻量级服务器(默认 5173 端口)。

当浏览器请求 index.html 时:

<!-- index.html -->
<script type="module" src="/src/main.jsx"></script>

Vite 服务器的处理流程如下:

浏览器请求 /src/main.jsx
     ↓
Vite 拦截请求
     ↓
Vite 读取文件
     ↓
使用 esbuild 将 .tsx 编译为 JS
     ↓
返回编译后的 JS 模块(Content-Type: application/javascript)
     ↓
浏览器解析 import 语句,继续请求 /src/App.jsx
     ↓
Vite 继续按需编译...

这种方式称为“按需编译”(On-Demand Compilation)或“即时编译”(JIT Compilation)。


3.4 为什么 Vite 极快?三大核心优势

1. 冷启动:仅启动服务器,无需构建

  • Webpack:分析依赖 → 编译 → 打包 → 启动服务器(耗时)
  • Vite:启动 Koa 服务器 + 预构建依赖(极快)

2. 编译速度:esbuild vs Babel

工具 语言 速度 特点
esbuild Go ⚡️ 极快(10-100x Babel) 单线程、并行编译、内置 minify
Babel JavaScript 🐢 较慢 插件生态丰富、可调试

Vite 使用 esbuild 编译 TS、JSX、CSS,速度远超 Babel。

3. 依赖预构建(Pre-bundling)

node_modules 中的包多为 CommonJS 或 UMD 格式,无法直接通过 ESM 导入。

Vite 在启动时使用 esbuild 将这些依赖预构建为 ESM 格式:

# 预构建后
node_modules/.vite/deps/
  react.js
  react-dom.js
  lodash.js

浏览器通过 /node_modules/.vite/deps/react.js 访问。


3.5 Vite 的生产构建:Rollup 驱动

虽然开发模式下不打包,但生产环境仍需打包以优化性能。

Vite 使用 Rollup 作为生产构建器,原因:

  • Rollup 更适合库和应用打包
  • Tree Shaking 更彻底
  • 输出更小的 bundle
// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['react', 'react-dom'],
          ui: ['antd']
        }
      }
    }
  }
}

第四部分:Webpack 与 Vite 的深度对比

维度 Webpack Vite
核心理念 全量打包,兼容优先 按需编译,速度优先
开发启动 慢(需构建整个依赖图) 快(<1s,仅启动服务器)
热更新 HMR,有一定延迟 基于 ESM,近乎实时
编译器 Babel(JS)、ts-loader(TS) esbuild(TS/JSX/CSS)
兼容性 ✅ 支持 IE11(通过 Babel + Polyfill) ❌ 仅支持现代浏览器(ESM)
生态 🌍 极其丰富(10,000+ 插件) 📈 快速增长(兼容 Rollup 插件)
配置 复杂,需手动配置 简洁,开箱即用
定制性 极强(Tapable 钩子系统) 较强(基于 Rollup 插件)
适用场景 大型企业项目、需兼容旧浏览器 新项目、现代浏览器环境

4.1 兼容性:IE11 的“最后一公里”

  • Vite 不支持 IE11,因为:

    • 不支持 <script type="module">
    • 不支持 import/export
    • 不支持 fetchPromise 等现代 API
  • Webpack 可通过以下方式支持 IE11

    // babel.config.js
    presets: [
      ['@babel/preset-env', {
        targets: { ie: '11' },
        useBuiltIns: 'usage', // 按需注入 polyfill
        corejs: 3             // 使用 core-js 3
      }]
    ]
    

结论:若需支持 IE11,必须使用 Webpack。


4.2 生态与插件系统对比

场景 Webpack Vite
React + TS ✅ 完美支持 ✅ 开箱即用
Vue 3 ✅(官方推荐)
Angular ❌(无官方支持)
自定义 Loader ✅ 丰富生态 ⚠️ 有限(依赖 Rollup 插件)
微前端 ✅ Module Federation ⚠️ 需额外配置

4.3 性能实测(中等项目:500+ 模块)

操作 Webpack Vite
冷启动 32s 0.9s
修改组件文件 3.5s 后更新 0.15s 内更新
生产构建 50s 14s(esbuild)
内存占用 900MB 180MB

Vite 在开发体验上具有数量级优势


第五部分:如何选择?决策框架

选择 Webpack 如果:

  • 项目需支持 IE11 或旧版浏览器
  • 已有大型 Webpack 项目,迁移成本高
  • 需要高度定制化构建流程(如特殊打包策略)
  • 使用 Angular、Ember 等非主流框架
  • 团队熟悉 Webpack 生态

选择 Vite 如果:

  • 新项目,目标用户使用现代浏览器
  • 追求极致开发体验(快!)
  • 使用 React、Vue、Svelte 等现代框架
  • 希望减少配置,快速上手
  • 希望利用 esbuild 加速生产构建

第六部分:工程化一揽子方案设计(Vite 示例)

// vite.config.js
import { defineConfig } from 'vite';          // 1. 引入 Vite 的配置函数
import react from '@vitejs/plugin-react';     // 2. 引入 React 插件
import path from 'path';                      // 3. 引入 Node.js path 模块

export default defineConfig({                 // 4. 导出 Vite 配置对象
  plugins: [                                  // 5. 插件数组:启用 React 支持
    react()
  ],
  server: {                                   // 6. 开发服务器配置
    port: 5173,                               //    - 启动端口
    open: true,                               //    - 启动后自动打开浏览器
    proxy: {                                  //    - 开发环境代理
      '/api': 'http://localhost:3000'         //      将 /api 请求代理到后端服务
    }
  },
  resolve: {                                  // 7. 路径解析配置
    alias: {                                  //    - 路径别名
      '@': path.resolve(__dirname, 'src')     //      @ 指向 src 目录
    }
  },
  build: {                                    // 8. 生产构建配置
    outDir: 'dist',                           //    - 输出目录
    sourcemap: false,                         //    - 不生成 source map(生产环境)
    minify: 'esbuild',                        //    - 使用 esbuild 压缩(更快)
    rollupOptions: {                          //    - Rollup 高级选项
      output: {
        manualChunks: {                       //      手动代码分割
          vendor: ['react', 'react-dom'],     //        将 React 相关打包为 vendor.js
          ui: ['antd']                        //        将 UI 库打包为 ui.js
        }
      }
    }
  }
});

代码逐行解释:

  1. import { defineConfig } from 'vite';
    引入 Vite 提供的 defineConfig 函数,用于定义配置对象并获得 TypeScript 类型提示。

  2. import react from '@vitejs/plugin-react';
    引入官方提供的 React 插件,用于支持 JSX 语法和 React 特性。

  3. import path from 'path';
    引入 Node.js 内置的 path 模块,用于处理文件路径。

  4. export default defineConfig({ ... });
    使用 defineConfig 包裹配置对象,并将其导出为默认模块。

  5. plugins: [ react() ]
    配置插件列表。react() 返回一个插件对象,Vite 会使用它来处理 React 相关的编译。

  6. server: { ... }
    开发服务器配置:

    • port: 指定服务器监听的端口号。
    • open: 设置为 true 时,启动后自动在默认浏览器中打开应用。
    • proxy: 配置开发环境代理,解决跨域问题。所有以 /api 开头的请求都会被转发到 http://localhost:3000
  7. resolve: { alias: { ... } }
    配置模块解析规则:

    • alias: 定义路径别名。'@': path.resolve(__dirname, 'src') 表示在代码中使用 @/components/Button 等价于 src/components/Button,简化长路径引用。
  8. build: { ... }
    生产构建配置:

    • outDir: 指定构建输出的目录,默认是 dist
    • sourcemap: 是否生成 source map 文件。生产环境通常设为 false 以减小包体积。
    • minify: 指定压缩工具。'esbuild''terser' 快得多。
    • rollupOptions: 传递给底层 Rollup 打包器的高级选项。
      • output.manualChunks: 手动进行代码分割,将指定的依赖打包到独立的 chunk 中,有助于浏览器缓存优化。

第七部分:结语

7.1 结语:工具的选择是哲学的体现

Webpack 与 Vite 的差异,本质上是两种工程哲学的碰撞:

  • Webpack 代表了“防御性编程”:为兼容一切可能的环境,构建一个完整的沙箱。
  • Vite 代表了“前瞻性设计”:拥抱现代标准,轻装上阵,追求极致效率。

最终,工具的选择不在于“谁更好”,而在于“谁更适合你的场景”。
理解底层原理,才能做出明智决策。

前端必看:img标签不可忽视的width与height属性

作者 末日码农
2025年8月23日 00:12

在前端web开发时,有两种方式在页面中放置图片:一种是使用img标签,一种是使用CSS背景图片

如果一张图片只是用来装饰,没有其他含义,那么更推荐使用CSS背景图片来实现。

当我们使用img标签在页面中嵌入图片的时候,可能很多人都只是在CSS中指定图片的width属性,以此来使图片加载后保持正常的宽高比而不会变形。

这似乎是一种比较完美的写法,width属性可以指定图片的宽度,而高度浏览器会根据图片宽高比进行自动计算。这种方式在图片不确定的情况下是比较通用的写法,但带来的问题是页面的内容可能会发生偏移

因为图片加载是异步的,在未加载时,浏览器无法知晓图片的尺寸,img标签的高度就是0。在图片加载完成后,浏览器根据图片的尺寸计算出了高度,img的高度不再是0,此时图片后方的内容就会被强制向下偏移,这也会触发页面布局的重新计算。

这会带来两个影响:

  1. 如果页面图片较多,对浏览器的性能开销就会变大;
  2. 会打断用户当前正在浏览的内容,用户视觉体验不友好。

如果说现在的硬件性能都已经过剩,那么第二点也是不容忽视的问题。

你可以在Chrome浏览器的性能测试工具中看到布局偏移这一项指标,并也会检测出导致的原因:Unsized image element

具体有哪些最佳实践方案呢?

我们分两种情况来说:

情况1:已知图片尺寸

我们应该在img标签上设置正确的图片尺寸,这往往是现在很多开发这容易忽视的地方。

<img src="xxx.jpg" width="200" height="100" alt="">

然后在CSS中,指定图片在页面中应该渲染的尺寸。

img {
  width: 100%;
  height: auto;
}

这样,在图片加载之前,浏览器就能得到img标签应该渲染的尺寸,预留出正确的空间。

img的宽度由CSS中的width: 100%;来决定,高度设置为auto,浏览器会根据img标签上的width="200" height="100"得到图片的宽高比2:1,从而计算出高度宽度*1/2

如果你不想在img上设置尺寸的话,你也可以在CSS中手动指定图片比例。

img {
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 1;
}

情况2:未知图片尺寸

没有图片的尺寸也就不可能知道图片的比例,所以就不可能有上面如此完美的解决方案。

你只能尽可能的减小布局偏移带来的视觉影响,预估一个图片尺寸。这样在图片未加载的时候,能够让用户提前知道这个区域可能会有一张图片。

img {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1; /* 根据实际情况预估 */
}

你也可以在图片区域提前渲染一个占位符,比如:占位图片,加载动画等,来提升用户的使用体验。

好了,如果你正在做前端页面优化方面的工作,不妨试一试吧,在提升Chrome性能指标的同时,可能也会带来用户体验上的提升。

如何同时打开多个 Chrome 呢?

作者 楷鹏Dev
2025年8月23日 00:08

哈喽,我是楷鹏。

今天想要分享 Chrome 的一个小技巧,可以一次性打开多个干净独立的 Chrome,让你的开发更丝滑。

开头做个小调查,你平时开发的时候,会使用哪些浏览器呢?

  • Chrome
  • Firefox
  • Safari
  • 其他

我平时开发的时候,主力就是使用 Chrome。

Chrome 的 DevTools 功能非常强大,满足前端开发调试的绝大数需求。

但是长期来有一个困扰我的问题,就是我的日常使用和开发是耦合在一起的。

比如,我的 Chrome 会装载很多的插件:

Chrome Extensions

这些插件会影响我的开发,因为他们可能在页面中会插入 HTML 或者 CSS 代码,以及会产生很多额外的请求,干扰我的正常开发调试。

比如下面侧边栏的插件 HTML:

Chrome Layer Tab

此时的选择,要么是开启无痕窗口,要么是换另外一个浏览器。

这两种方式都不错,但无痕窗口还是使用同一个 Chrome 实例,并且重新打开无痕窗口,所有的状态都会被清空。

另外一种方式是换另外一个浏览器,我曾经尝试过,但是后来又放弃了,换一个浏览器就相当于换一种全新的开发环境,需要重新适应界面、操作习惯等等,真的很别扭。

最近学到了另一种新方式,就是可以通过使用不同的用户数据目录,来创建不同的 Chrome 实例。

运行命令:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_1"

你就可以创建一个全新的 Chrome 实例,并且这个实例的配置、插件、历史记录等都是独立的。

Create Chrome Instance

甚至在 Dock 栏,你还可以看到两个 Chrome 图标:

Chrome Instances in Dock

这个新创建的 Chrome 实例,完全可以看作是一个全新的 Chrome 浏览器。

你可以修改主题,来和其他 Chrome 实例区分开来:

Modify Theme

或者登录不同的账号等等操作,这是完全属于你的第二 Chrome。

通过运行这条命令,理论上你可以创建无限个 Chrome 实例,只需要修改 --user-data-dir 参数即可,比如:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_2"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_3"
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_4"
......

不过平时实际使用的时候,我一般使用两个 Chrome 实例,来回切换,一个用于网站浏览,一个用于开发调试。

在开发调试的时候,每次打开项目再打开新的 Chrome 会有一点点烦躁,所以你可以考虑将这条命令写入到你的前端项目 package.json 的脚本中:

  "scripts": {
    "dev": "next dev --turbopack",
    "open-chrome": "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --args --user-data-dir=/tmp/ChromeNewProfile http://localhost:3000",
    "dev:chrome": "npm run open-chrome && npm run dev"
  },

这样你就可以通过 npm run dev:chrome 来打开 Chrome 实例,并且自动运行 next dev 命令。

Windows PowerShell 用户可以使用:

 "scripts": {
   "dev": "next dev --turbopack",
   "open-chrome": "powershell -Command \"Start-Process 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe' -ArgumentList '--user-data-dir=D:\\temp\\ChromeNewProfile', 'http://localhost:3000'\"",
   "dev:chrome": "npm run open-chrome && npm run dev"
 },

如果你希望打开 Chrome 实例的时候,同时打开 localhost:3000 页面来看到页面效果,可以在命令后面直接添加 http://localhost:3000

{
  "scripts": {
    "dev": "next dev",
    "dev:chrome": "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir=\"/tmp/chrome_user_dir_1\" http://localhost:3000 && npm run dev"
  }
}

好了,这就是本期的全部内容,如果对你有帮助,欢迎点赞、收藏、转发。

我是楷鹏,我们下期再见。

SVG 适合静态图,Canvas 适合大数据?图表库的场景选择

2025年8月22日 23:20

图表库为何偏爱 Canvas 而非 SVG?

在前端开发中,图表库的选择是面试中常被问及的问题。尤其是在涉及到高性能图形渲染时,Canvas 和 SVG 之间的权衡更是核心考点。今天,我们就来深入探讨一下,为什么现在主流的图表库(如 ECharts、AntV G2 等)大多采用 Canvas 方案,而不是 SVG。

💡 SVG 的 GPU 加速特点

SVG Logo

SVG (Scalable Vector Graphics) 是一种基于 XML 的矢量图形格式,它在浏览器中渲染时,也可以利用 GPU 进行加速。然而,SVG 的 GPU 加速存在一些局限性。

⚠️ 有限的自动加速

SVG 的 GPU 加速并非总是由开发者控制,而是很大程度上依赖于浏览器的“心情”。

  • 浏览器说了算: 对于一些简单的 SVG 图形,浏览器可能觉得“小意思”,直接用 CPU 渲染就足够了,根本不会启用 GPU 加速。这就像你让一个经验丰富的厨师切菜,如果只是切几片黄瓜,他可能直接用手切,而不是去启动一台全自动切菜机。
  • 复杂场景才可能触发: 只有当 SVG 场景变得非常复杂,比如包含成千上万个图形元素、复杂的变换(旋转、缩放、倾斜)或者动画效果时,浏览器才有可能“大发慈悲”地启用 GPU 加速。但即便如此,这种加速的触发机制也并不完全可控,它取决于浏览器的内部实现和判断。你无法像控制电灯开关一样,明确地告诉浏览器:“嘿,给我用 GPU 加速!”

🔄 依赖浏览器优化

SVG 的 GPU 加速效果,就像“看人下菜碟”,不同的浏览器有不同的表现。

  • “因人而异”的性能: SVG 的 GPU 加速在很大程度上依赖于浏览器的优化策略。这意味着,同一个 SVG 图形在 Chrome、Firefox 或 Safari 等不同浏览器上的性能表现可能会大相径庭。这就像你请了不同的装修队来装修房子,即使是同样的图纸,最终的效果和效率也可能不一样。
  • 特性优化不一致: 某些现代浏览器可能会对特定的 SVG 特性(比如滤镜效果、渐变等)进行专门优化,并启用 GPU 加速。但其他浏览器可能就没有这样的“特殊待遇”。这就像有些手机支持某个游戏的专属优化模式,而其他手机则没有。

🔧 Canvas 的 GPU 加速特点

Canvas

Canvas 是 HTML5 提供的一个绘图 API,它允许开发者通过 JavaScript 直接在网页上绘制图形。与 SVG 不同,Canvas 在 GPU 加速方面拥有更明确的控制权和更高的性能潜力。。

✨ 更明确的控制

Canvas 就像一个“听话”的画板,你可以精确地控制它如何利用 GPU。

  • “指哪打哪”的加速: 在 Canvas 中,开发者可以通过一些特定的技术手段,比如结合 WebGL(基于 OpenGL ES 的 JavaScript API),明确地请求 GPU 加速。WebGL 允许你直接操作 GPU 的底层能力,进行高性能的 3D 图形渲染。这就像你拥有了一台专业的摄影机,可以精确地调整光圈、快门、ISO,而不是只能使用傻瓜模式。
  • 2D 图形也受益: 即使是 2D 图形,一些浏览器也提供了对 Canvas 的硬件加速支持。开发者可以通过特定的浏览器设置或使用图形库来启用它。这意味着,你可以更直接地控制是否使用 GPU 加速,以及如何优化图形渲染以充分利用 GPU 的性能。这就像你不仅能控制专业摄影机,还能控制普通相机的各种参数,让它拍出更好的照片。

🚀 高性能图形渲染

Canvas 结合 GPU 加速,能够实现非常惊人的图形渲染性能,尤其是在处理复杂场景时。。

  • “飞沙走石”般的渲染速度: Canvas 结合 GPU 加速,可以实现非常高性能的图形渲染,特别适用于那些需要大量图形元素、复杂动画或实时交互的场景,比如大型游戏、复杂的数据可视化和流畅的动画效果。GPU 的并行处理能力,就像一支训练有素的军队,可以同时处理大量的图形任务,快速绘制出海量的图形并进行复杂的图形变换。这就像一个大型工厂的流水线,每个环节都高效运转,大大提升了生产效率。
  • 数据可视化利器: 在数据可视化领域,Canvas 和 WebGL 的组合更是大放异彩。它们可以实现大规模数据的实时渲染和交互,为用户提供极其流畅的体验。想象一下,你在一个股票交易平台上,实时查看成千上万只股票的K线图,或者在一个大数据分析工具中,拖拽、缩放、筛选海量数据点,而这一切都能够丝滑流畅地进行,这背后很可能就是 Canvas 和 WebGL 的功劳。

CPU vs GPU

⚖️ 总结与选择

总而言之,Canvas 在利用 GPU 硬件加速方面通常具有更明确的控制和更高的性能潜力,而 SVG 的 GPU 加速则更多地依赖于浏览器的自动优化,且加速的范围和效果相对有限。

在实际应用中,选择使用 SVG 还是 Canvas,并考虑 GPU 加速,需要根据具体的应用场景、性能需求和开发技术栈来综合决定。

  • SVG 适用场景: 如果你的图形是静态的、数量不多、不需要频繁交互或复杂动画,并且对可访问性、SEO 有较高要求,那么 SVG 可能是更好的选择。它基于 DOM,易于操作和样式化。
  • Canvas 适用场景: 如果你需要处理大量图形、实现复杂动画、游戏开发、大规模数据可视化,或者对性能有极高要求,那么 Canvas 结合 GPU 加速将是你的不二之选。它提供了像素级的控制,性能更优。

希望通过这篇文章,你对 Canvas 和 SVG 在 GPU 加速方面的特点有了更深入的理解,在前端面试中也能游刃有余地回答相关问题!

Node.js全栈基石(壹)

作者 Face
2025年8月22日 22:59

Node.js 基础

Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。这里要敲黑板划重点了,JavaScript 运行时环境。 构建在 Chrome 的 V8 引擎之上。Node.js 是异步事件驱动单线程模型。由于 Node.js 异步非阻塞的特性,因此适用于 I/O 密集型的应用场景。需要注意的是,Node.js 是单线程模型,需要避免 CPU 的耗时操作。 Node.js 并不是语言,而是一个 JavaScript 运行时环境它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。

大纲

  • 什么是 Node.js
  • Node.js 环境搭建
  • CommonJS 规范
  • CommonJS 原理

Node.js 特性:

  • 开源、跨平台的 JavaScript 运行时环境
  • 构建在 chrome 的 V8 引擎上
  • 事件驱动、非阻塞 I/O,适用于 IO 密集型应用
  • 单线程模型

Node.js 并非是一门语言,只是一个 JavaScript 的运行时环境

  1. 什么是 JS 运行时环境
  2. 为什么 JS 需要特别的运行时环境
  3. JS 引擎是什么
  4. v8 引擎是什么

JS 无处不在

  • JS 代码在浏览器中如何被执行

浏览器内核

  • Gecko
  • Trident 微软 ie4 —— ie11
  • WebKit 苹果 khtml 用于 Safari
  • Blink WebKit 一个分支 Google 开发
浏览器 内核 JavaScript 引擎 User Agent 关键词
Chrome/Edge Blink V8 AppleWebKit/537.36
Safari WebKit JavaScriptCore AppleWebKit/
Firefox Gecko SpiderMonkey Gecko/或 Firefox/
旧版 IE Trident Chakra(旧) Trident/

事实上,我们经常说的浏览器内核指的是浏览器的排版引擎排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样板引擎。是浏览器的核心组件,负责将网页代码(HTML、CSS、JavaScript)转换为用户可视化的页面。

渲染引擎工作过程

2025-08-18-22-56-55-image.png

如上图:

  • HTML 和 CSS 经过对应的 Parser 解析之后,会形成对应的 DOM Tree 和 CSS Tree;
  • 它们经过附加合成之后,会形成一个 Render Tree,同时生成一个 Layout 布局,最终通过浏览器的渲染引擎帮助我们完成绘制,展现出平时看到的 Hmtl 页面;
  • 在 HTML 解析过程中,如果遇到了<script src='xxx'>,会停止解析 HTML,而优先去加载和执行 JavaScript 代码(此过程由 JavaScript 引擎完成)
  • 因为 JavaScript 属于高级语言(Python、C++、Java),所以 JavaScript 引擎会先把它转换成汇编语言,再把汇编语言转换成机器语言(二进制 010101),最后被 CPU 所执行。

JS 引擎

为什么需要 JavaScript 引擎呢?

  • 事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的;

  • 但是 CPU 只认识自己的指令集,实际上是机器语言,才能被 CPU 所执行;

  • 所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行;

    比较常见的 JavaScript 引擎有哪些呢?

引擎名称 所属浏览器/环境 特点
SpiderMonkey Firefox 首个 JavaScript 引擎(由 Brendan Eich 开发),逐步引入 JIT 优化。
Chakra 旧版 Edge(已弃用) 微软开发,曾支持异步编译,现被 V8 取代。
V8 Chrome、Edge、Node.js Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出。高性能,首创 JIT 分层编译(Ignition + TurboFan),支持 WebAssembly。
JavaScriptCore Safari、iOS 应用 苹果开发(原名 Nitro),WebKit 中的 JavaScript 引擎,注重能效比,优化移动端性能。
Hermes React Native Facebook 专为移动端优化,减少内存占用,提升启动速度。

JS → 汇编 → 机器语言 → CPU (指令集)

  • SpiderMonkey js 引擎. Firefox 使用
  • V8 js 引擎. Chrome、edge、Node.js 使用
  • javaScriptCore js 引擎. Safari、ios 使用,优化移动端
  • Hermes js 引擎. react-native 使用, facebook 移动端优化

JavaScript引擎浏览器内核之间的联系和不同

2025-08-18-23-01-41-image.png

  • 实际上 Webkit 是有两部分组成:
    1. WebCore:负责 HTML,CSS 解析,布局,渲染等操作;
    2. JavaScriptCore(JScore):用于解析和执行 JS 代码; JavaScriptCore(JScore)是 Webkit 中默认的 JS 引擎。另外一个强大的 JavaScript 引擎就是 V8 引擎。

v8 引擎

V8 引擎作为现代 Web 生态的核心技术支撑(Chrome、Node.js、Deno 等均依赖其运行),其设计融合了高性能编译、内存优化与安全增强等关键技术

官方对 V8 引擎的定义:

  • 支持语言:V8 是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等;
  • (译:V8 可以运行 JavaScript 和 WebAssembly 引擎编译的汇编语言等)
  • 跨平台:它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,
  • ARM 或 MIPS 处理器的 Linux 系统上运行;
  • 嵌入式:V8 可以独立运行,也可以嵌入到任何 C ++应用程序中;

v8 原理

2025-08-18-23-04-34-image.png 其中的 Parse(解析器)lgnition(解释器)TurboFan(优化编译器)都是 V8 引擎的内置模块

console.log("hello world");

function sum(num1, num2) {
return num1 + num2;
}
  1. Parse 模块会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;
    • 如果函数没有被调用,那么是不会被转换成 AST 的;
    • Parse 的 V8 官方文档:Parser
  2. lgnition 模块Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码);
    • 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
    • 如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
    • Ignition 的 V8 官方文档:Ignition
  3. TurboFan 模块TurboFan 是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码;
    • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;
    • 但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
    • TurboFan 的 V8 官方文档:TurboFan

上面是 JavaScript 代码的执行过程,事实上V8 的内存回收也是其强大的另一个原因

  • Orinoco模块:负责垃圾回收,将程序中不需要的内存回收
  • Orinoco的 V8 官方文档:Orinoco

编程语言会大体分为两大类

  • 解释性语言:运行效率相对较低(e.g. JavaScript)
  • 编译性语言:运行效率相对较高(e.g. C++)

上述情况对应的是 JavaScript 解释性语言的大体执行流程,但编译型语言往往不是。比如 C++,例如系统内的某些应用程序用 C++编写的,它们在执行的时候会直接转化为机器语言(二进制格式 010101),并交给 CPU 统一执行,这样的运行效率自然相对较高了些。

v8 引擎也对解释性的编程语言做了一个优化,就是上面的TurboFan 优化编译器,如果一个 JavaScript 函数被多次调用,那么它就会经过TurboFan转成优化后的机器码,交由 CPU 执行,提高代码的执行性能

浏览器和 Node.js 架构区别

2025-08-19-21-11-10-image.png

  1. 在 Chrome 浏览器中
    • V8 引擎只是其中一小部分,用来辅助 JavaScript 代码的运行
    • 还有一些浏览器的内核用来负责 HTML 解析、布局、渲染等相关工作
    • 中间层和操作系统( 网卡、硬盘、显卡……)
      • 比如发送网络请求,中间层会调用操作系统中的网卡
      • 读取一些本地文件,中间层会调用操作系统中的硬盘
      • 浏览器页面的渲染工作,中间层会调用操作系统中的显卡
  2. 在 Node.js 中
    • V8 引擎
    • 中间层(libUv) 包括 EventLoop
    • 操作系统( 网卡、硬盘、显卡…)

Node.js 和 浏览器的区别

相同:

  • 都可以运行 JavaScript 代码,都支持 JS 的基本语法 不同:
  • node 服务端没有 BOM、DOM。node 不能使用 BOM、DOM 方法
  • 浏览器端是不能操作文件的,没有内置模块。浏览器端不可以使用 JS 操作文件

Node.js 架构

  • JavaScript 代码会经过 V8 引擎,在通过 Node.js 的 bindings(Node.js API),将任务派发到 Libuv 的 EventLoop(事件循环)
  • Libuv提供了事件循环、文件系统读写、网络 IO、线程池等内容
  • Libuv是使用 C 语言实现的库,运行效率相对较高,适用于 IO 密集型应用

Node.js 代码主要分为三个部分,分别是 C、C++、JavaScript

  1. JS 代码就是我们平时在使用的那些 JS 模块。比如,fs模块,http模块,path模块等
  2. C++ 代码主要分三个部分。
    • 第一部分主要是封装Libuv和第三方库的C++代码,比如netfs这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装,暴露给开发者使用
    • 第二部分是不依赖Libuv和第三方库的C++代码,比如 Buffer模块的实现
    • 第三部分 C++ 代码是 V8 本身的代码
  3. C 语言代码主要是包括Libuv和第三方库的代码,它们都是纯 C 语言实现的代码

Node.js 中主要各部分实现:

2025-08-19-21-55-24-image.png

Node.js 与 JavaScript 分层

2025-08-19-21-57-03-image.png从下往上梳理:

  • 最下面一层是脚本语言规范(Spec),由于我们讲的是 Node.js,所以这里最下层只写 ECMAScript。
  • 再往上一层就是对于该规范的实现,如 JavaScript、JScript 以及 ActionScript 等都属于对 ECMAScript 的实现。
  • 再往上一层是执行引擎,JavaScript 常见的引擎有 V8、SpiderMonkey、QuickJS 等,解释 js 代码。Node.js 的引擎是 V8。
  • 再往上一层是运行时环境,比如基于 V8 封装的运行时环境有 Chromium、Node.js、Deno、CloudFlare Workers 等等。而我们所说的 Node.js 就是在运行时环境这一层。

总之,以后出去千万别再说 Node.js 语言这个词啦,要说 Node.js 运行时,编程语言还是 javascript

Node 历史

Node.js 的历史可以追溯到 2009 年,它的诞生和发展深刻影响了现代 Web 开发。以下是其关键阶段的梳理:

  1. 2009 年:Node.js 的诞生
    • 创始人:Ryan Dahl,一位对服务器性能瓶颈不满的开发者。
    • 灵感来源:受 **V8 引擎(Chrome 的 JavaScript 引擎)**启发,Dahl 希望用 JavaScript 实现高性能、事件驱动的服务器端开发。
    • 首次发布:2009 年 11 月,Node.js 在 JSConf EU 大会亮相,核心思想是非阻塞 I/O事件循环,解决了传统服务器(如 Apache)的并发处理瓶颈。
  2. 早期发展与争议(2010-2014)
    • npm 的诞生:2010 年,Isaac Schlueter 创建了 Node 包管理器 npm,迅速成为生态系统的基石(现托管超 200 万个包)。
    • 社区增长:Express.js(2010)、Socket.IO(2010)等框架涌现,推动 Node.js 在实时 Web 应用中的应用。
    • 管理争议:Joyent 公司主导 Node.js 期间,社区对开发进度不满,导致io.js 分叉(2014 年),采用更开放的治理模式。

Node.js 能做什么

  1. 服务端开发:用于做服务器端开发 web 服务器
  2. 工具:可以基于 node 构建一些工具,构建工作流,比如 npm,webpack,gulp,less,sass 等 vue-cli
  3. 开发工具或者客户端应用:开发桌面应用程序(借助 node-webkit、electron 等框架实现),比如:vscode、语雀等
  4. 同构:SSR,借助 Node.js 完成服务端渲染+前后端同构
  5. serverless
  6. 流式 SSR
  7. 游戏
  8. npm、yarn,pnpm 工具成为前端开发使用最多的工具;
  9. 爬虫

Node.js 环境搭建

如何选择 Node.js 版本

下载地址:Download | Node.js LTS VS Current Current 版本:当前最新的版本。 LTS 版本:Long Term Support,即长期维护版本。 说明参照:Release

Node.js 版本可以分为三个阶段:CurrentActive LTSMaintenance

当一个奇数版本发布后,最近的一个偶数版本会立即进入 LTS 维护阶段,一直持续 18 个月。LTS 维护阶段结束以后,进入 12 个月的 Maintenance 阶段。

注意:奇数版本不包含 Active LTSMaintenance 这两个阶段。

三个阶段主要做的事情

  • Current 版本:包含了主分支上非重大的更新。
  • Active LTS 版本:经过 LTS 团队审核的新功能、错误修复和更新,已被确定为适用于发布线且稳定。
  • Maintenance 版本:关键错误修复和安全更新。

建议:由于偶数版本获得的支持时间比较长,推荐在生产环境中使用偶数版本Node.js

安装 Node.js

确定安装的软件版本后,只需要下载操作系统对应的软件即可。 本教程安装的是 MacOS 系统 V20.8.0 版本。

Node.js 版本管理器 - nvm

通过 nvm 工具可以快速安装和使用不同版本的 node。 下载

// 下载命令
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
// 或者
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
nvm 对 windows 系统支持的不太好,只支持在部分情况下的 windows 中使用。因此,在 windows 中推荐使用 nvm-windows。
命令
// 选择版本
nvm use 16
// 查看列表
nvm ls
// 安装版本
nvm install 16
// 卸载版本
nvm uninstall 16
// 查看全部命令
nvm -h
// 检查版本是否正确
node -v
// 设置默认版本
nvm alias default 18.16.0

Node.js 版本管理器 - n

非常轻量级的 Node.js 版本管理器。和 nvm 类似,可以通过其提供的命令快速安装和使用不同版本的 node。 遗憾的是,n 也对 windows 操作系统支持的不太好,只可以在部分情况下的 windows 中使用。 下载

npm install -g n
命令
// 安装版本
n 20.8.0
// 查看列表
n ls
// 卸载版本
n rm 20.8.0
// 选择版本
n
// 查看全部命令
n -h
// 检查版本是否正确
node -v

CommonJS 模块化及源码解析

四大模块体系

IIFE之后,业界迸发出几类 JavaScript 的模块体系。其中最流行的四大体系分别为:

  • CommonJS,2009 年出现,模块规范发布
  • AMD
  • CMD
  • UMD

AMD、CMD、UMD

这里三大模块体系中,只有首字母不一样,而后两个字母则都是 Module Definition 的缩写。

  • AMDAsynchronous Module Definition,即异步模块定义
  • CMDCommon Module Definition,即一般模块定义,虽然 Common 也含通用意思,但这里将其译为“一般”是为了不与后面 UMD 冲突
  • UMD 则是 Universal Module Definition,即通用模块定义。

AMD 最开始在 require.js 中被使用,其首个提交是在 2009 年发出的。CMDAMD 很类似,不同点在于:AMD 推崇依赖前置、提前执行CMD 推崇依赖就近、延迟执行。CMD 是在推行 (Sea.js)[seajs.github.io/seajs/docs/] 中产生的,而 Sea.js 则是玉伯大佬多年前的作品。 UMD 是个“大一统”,在当时的野心是对 CommonJS、AMD 和 CMD 做兼容。

由于这三种模块方式与 Node.js 几乎没有关系,就不继续展开了。

CommonJS

CommonJS 模块规范发布于 2009 年,由 Mozilla 工程师 Kevin Dangoor 起草,他于当年 1 月发表了一篇文章《What Server-side JavaScript Needs》。注意这个时间,2009 年。嘿!这不巧了吗! 其实 AMD 这类也基本上是在 2009、2010 时间点出现的。以及其依赖的 V8 也都是相仿阶段出生的。我们说 2010 年前后几年是泛前端体系开始觉醒的两年是丝毫不怵的。 CommonJS 最初的主要目的是为除浏览器环境之外的 JavaScript 环境建立模块生态系统公约。继续注意这个词,“除浏览器之外的 JavaScript 环境”。答案是不是呼之欲出?其实 CommonJS 最初不叫 CommonJS,而是叫 ServerJS。后来觉得路走窄了没朋友,就把 Server 改成了 Common——把浏览器又给包括回来了。

按其说法,在 CommonJS 规范之下,你可以写:

  • 服务端 JavaScript 应用;
  • 命令行工具;
  • 桌面 GUI 应用;
  • 混合应用(Titanium,Adobe AIR……)。

CommonJS 缩写就是 CJS。所以,这个就是 Node.js 一直以来的模块规范。

CommonJS 规范

Node.js 应用由模块组成,默认采用的是 CommonJS 规范。即每个文件是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类等都是私有的,对其它文件不可见。 不过我们可以通过在模块中通过 exports 或者 module.exports 命令将内容导出,其它文件就可以通过 require 命令访问到这些被导出的内容。

Module

module对象是模块系统的核心, 每个文件都是一个模块,有自己的作用域,文件中定义的变量、类、函数都是私有的,都有一个独立的module对象,用于管理模块的导出、依赖和元信息 module是对当前模块对象的引用

  • module.exports用于定义模块导出的内容
  • 内含该模块元信息,比如一个id字段
  • 实际上,Node.js 中的module下还含了初始的exports对象
console.log(module)

{
  id: '.',
  path: '/Users//FeProjects/about-node/learn-node/module',
  exports: {},
  filename: '/Users/xxx/FeProjects/about-node/learn-node/module/node.js',
  loaded: false,
  children: [],
  paths: [
    '/Users/xxx/FeProjects/about-node/learn-node/module/node_modules',
    '/Users/xxx/FeProjects/about-node/learn-node/node_modules',
    '/Users/xxx/FeProjects/about-node/node_modules',
    '/Users/xxxx/FeProjects/node_modules',
    '/Users/xxx/node_modules',
    '/Users/node_modules',
    '/node_modules'
  ],
  [Symbol(kIsMainSymbol)]: true,
  [Symbol(kIsCachedByESMLoader)]: false,
  [Symbol(kIsExecuting)]: true
}

各模块中大家所使用的 module 对象就是该模块对应的 Module 类实例。它除了包含 exports 对象之外,还包含:

  • children:该模块通过 require() 加载的子模块数组。
  • filename:模块文件的绝对路径
  • id:模块的唯一标识符,通常是文件的完整路径
  • loaded:表示模块是否已加载完成
  • path: 模块所在路径
  • paths:模块的搜索路径数组(由当前文件路径逐级向上查找 node_modules)。
  • exports: 模块对外暴露的内容,其他模块通过 require() 访问此对象。exportsmodule.exports对应的引用。这是一个用于导出模块内容的通道
    exports = module.exports = {};
    exports.name = "exports";
    console.info(module.exports); // name 'exports'
    
  • require(): 加载模块,访问模块导出的内容。运行时加载,是同步的,返回值是加载模块的module.exports值,加载失败返回nullrequire 是一个函数,这个函数有一个参数代表模块标识,它的返回值就是其所引用的外部模块所暴露的 API。
// math.js
exports.add = function () {
var sum = 0,
i = 0,
args = arguments,
l = arguments.length;
while (i < 1) {
sum += args[i++];
}
return sum;
};

// increment.js
var add = require("math").add;
exports.increment = function (val) {
return add(val, 1);
};

// program.js
var inc = require("increment").increment;
var a = 1;
inc(a); // 2

module.id = "program";

image.png

Node.js 的 module 对象下还挂载了个 module.exports 对象,其初始值指向 CommonJS 所定义的 exports 对象。而真正导出是 module.exports,并不是 exports.

启用 ESM 的两种方式

  1. 通过文件扩展名.mjs:将文件扩展名改为.mjs,Node.js 会自动识别为 ESM模块
    // app.mjs
    export const hello = () => "Hello ESM";
    
  2. package.json中设置"type":"module"
    {
    "name": "ESM",
    "version": "1.0.0",
    "type": "module",
    "scripts": {
    "serve": "xxx serve",
    "build": "xxx build"
    },
    }
    

CommonJS 源码解析

  1. 给 exports 直接赋值,不能导出
// 模块导出 - user.js
const name = "face";
const age = 18;
const printName = () => {
console.info(name);
};
exports = { name, age };

// 模块导入 - index.js
const { name, age, printName } = require("./user.js");
console.info(name, age, printName); // undefined undefined undefined

// 入口文件
node ./index.js
  1. 同时使用 exports 和 module.exports
// 模块导出 - user.js
const name = 'lucy';
const age = 28;
const printName = () => {
    console.log(name);
};

module.exports = { name, age };
exports.printName = printName; // 给 exports 的属性赋值,可以导出

// 模块导入 - index.js
const { name, age, printName } = require('./user.js');

console.log(name, age, printName); // lucy 28 undefined

// 入口文件
node ./index.js
  1. 访问包含随机语句的模块
// 模块导出 - user.js
const height = Math.random();
module.exports = { height };

// 模块导入 - index.js
const { height: firstHeight } = require("./user");
console.log("🚀 ~ firstHeight:", firstHeight);
/* 一次使用,内容会被缓存,模块缓存机制,不会重新加载后续取缓存内容,重新 require 时会重新加载 */
const { height: secondHeight } = require("./user");
console.log("🚀 ~ secondHeight:", secondHeight);
// 🚀 ~ firstHeight: 0.227755537794758
// 🚀 ~ secondHeight: 0.227755537794758
  1. 循环引用
// moduleA.js
const name = "moduleA";
const path = "./moduleA.js";
const moduleB = require("./moduleB");
console.log("🚀 ~ moduleB.name:", moduleB.name);

module.exports = { name, path };

// moduleB.js
const name = "moduleB";
const path = "./moduleB.js";
const moduleA = require("./moduleA");
console.log("🚀 ~ moduleA.name:", moduleA.name);

module.exports = { name, path };

// 入口文件  node .\moduleA.js
// 🚀 ~ moduleA.name: undefined
// 🚀 ~ moduleB.name: moduleB
// (node:26784) Warning: Accessing non-existent property 'name' of module exports inside circular dependency
// (Use `node --trace-warnings ...` to show where the warning was created)
  1. CommonJS 模块输出的是一个值的拷贝, 浅拷贝
// user.js
let num = 1;
let user = { name: "face" };

exports.num = num; // 赋值, 是对变量 num 进行了拷贝, 拷贝的是 1 这个值
exports.user = user; // 赋值, 是对变量 user 进行了拷贝, 拷贝的是对象的引用地址

exports.addNum = () => {
num += 1; // 修改变量 num 的值
};
exports.getNum = () => {
console.log("🚀 ~ 模块内部修改 num:", num);
};

exports.setName = () => {
user.name = "exports"; // 修改对象 user 的属性 name
};
exports.getName = () => {
console.log("🚀 ~ 模块内部修改 user.name:", user.name);
};

// index.js
const a = require("./user");

console.log("🚀 ~ a 模块:", a);
console.log("🚀 ~ 初始 a.num :", a.num);
console.log("🚀 ~ 初始 a.user.name:", a.user.name);

a.addNum();
a.setName();

console.log("🚀 ~ 修改后 a.num:", a.num);
console.log("🚀 ~ 修改后 a.user.name:", a.user.name);

a.getNum();
a.getName();

/* 运行 node ./index.js */
// 🚀 ~ a 模块: {
//   num: 1,
//   user: { name: 'face' },
//   addNum: [Function (anonymous)],
//   getNum: [Function (anonymous)],
//   setName: [Function (anonymous)],
//   getName: [Function (anonymous)]
// }
// 🚀 ~ 初始 a.num : 1
// 🚀 ~ 初始 a.user.name: face
// 🚀 ~ 修改后 a.num: 1
// 🚀 ~ 修改后 a.user.name: exports
// 🚀 ~ 模块内部修改 num: 2
// 🚀 ~ 模块内部修改 user.name: exports

image_2.png

为什么直接赋值 exports 会失效

错误用法 ❌

// 错误!无法导出内容
exports = {
name: "foo",
method: function () {
/* ... */
},
};
  • 上面代码中 exports 被重新赋值,但 module.exports 仍然指向原始的空对象,导致导出失败。
  • exports的本质:exports是模块系统在运行时提供的一个引用变量,它默认指向 module.exports 的内存地址。
  • 直接赋值会切断引用:如果你直接对 exports 赋值(例如 exports = ...),相当于让 exports 指向了一个新的对象,而原本的 module.exports 不会受到影响。此时模块实际导出的仍然是旧的 module.exports(默认为空对象),就会导致 require 引入的始终是空对象

正确用法

  1. 通过 module.exports 导出
    // 模块导出 - user.js
    const name = "lucy";
    const age = 28;
    const printName = () => {
    console.log(name);
    };
    
  2. 修改exports的属性,如果希望保持使用 exports,可以通过添加属性的方式修改它(而不是直接赋值):
    exports.name = "face";
    exports.method0 = () => {
    console.log("method0");
    };
    
    此时 exports 仍然指向原始的 module.exports,因此修改其属性是有效的。
  3. 底层原理
    • 模块的导出始终以module.exports为准
    • 初始化时,Node.js 会执行一下操作
      var module = {
      exports: {},
      };
      var exports = module.exports; // 初始状态时两者指向同一个对象
      
      • 因此,直接修改exports = ... 会影响module.exports
  • 直接赋值给 exports:❌ 无效(会切断与 module.exports 的关联)。
  • 操作 module.exports:✅ 正确且可靠。
  • 修改 exports 的属性:✅ 正确(前提是保持引用)。

前端跨域难题终结者:从JSONP到CORS,一文搞定所有跨域问题!

2025年8月22日 22:41
前言:为什么会有跨域这个"坑"? 记得我刚学前端时,第一次遇到跨域问题,整个人都懵了。明明后端API已经调通了,前端代码也没问题,可浏览器就是报错: 相信很多前端开发者都经历过这种绝望。但别担心,今天
❌
❌