普通视图
Vue3的渲染秘密:从同步批处理到异步微任务
面试里,当面试官把两段看似「都是改 5 次数据」的代码摆在你面前,却问「渲染了几次?」,如果你只回答「改了 5 次所以 5 次」,那大概率就踩坑了。本文用 100 行代码把同步批处理与异步微任务的底层机制拆开讲透,让你以后遇到同类问题直接秒答。
一、手写一个带合并渲染的 Component
需求
- 修改数据时触发
render
- 同步多次修改只触发一次
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 次
- 初始挂载:渲染
0
- 批处理队列: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 次
- 初始挂载:渲染
0
- 每个
setTimeout
回调都是一个独立宏任务,Vue 的批处理无法跨任务合并,于是 5 次回调触发 5 次独立渲染。
总结
同步代码 → 全部进入同一批处理队列 → 1 次渲染
异步代码 → 每次回调独立任务 → n 次渲染
如何在vue项目中封装自己的全局message组件?一步教会你!
我都使用过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 不香吗?
在 CSS 中,利用padding
撑开div
而不直接设置width
的写法有以下几个显著优势:
当不设置固定`width`时,元素宽度会默认随内容自然扩展,
配合`padding`可以在内容周围创建空间,
同时保持元素宽度与内容动态匹配。
这在处理不确定长度的内容(如文本、动态加载的内容)时非常实用,避免内容溢出或留白过多。
-
简化响应式设计
不固定宽度的元素会自动适应父容器的可用空间,配合padding
可以在不同屏幕尺寸下保持一致的内边距比例,无需为不同断点重复设置宽度,减少代码冗余。 -
避免盒模型计算问题
在默认的box-sizing: content-box
下,width
不包含padding
和border
。如果设置固定width
再添加padding
,实际宽度会超出预期(需要手动计算调整)。而利用padding
撑开元素时,宽度会自动包含内边距(配合box-sizing: border-box
效果更佳),避免计算错误。 -
灵活的比例控制
结合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 的写法吗?遇到过哪些有趣的问题?欢迎在评论区分享你的经历~
一文读懂 Python 的 JSON 模块:从零到高手的进阶之路
Redux vs Redux Toolkit 从传统 Redux 优雅升级
Redux Toolkit (RTK) 是 Redux 官方推荐的、用于高效、简洁地编写 Redux 逻辑的现代工具集。它旨在解决 Redux 原始写法中常见的“样板代码过多”、“配置复杂”和“新手入门门槛高”等问题,通过提供一组经过良好实践验证的工具和抽象,让开发者能够更轻松地构建 Redux 应用。
Redux Toolkit 核心目标
- 减少样板代码 (Boilerplate): 自动生成 action creators、action types 和 immutable state updates。
-
提供合理的默认配置: 内置了
redux-thunk
(异步逻辑)和immer
(不可变更新),开箱即用。 -
简化 Store 配置: 通过
configureStore
简化 store 创建过程。 -
包含现代 JS 工具: 集成了
createSlice
、createAsyncThunk
等高级 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 设计更符合直觉 | 新手更容易上手,核心概念集中在 createSlice 和 configureStore 。 |
实际业务案例:用户管理模块
假设我们需要构建一个用户管理模块,功能包括:
- 获取用户列表 (异步)
- 添加用户 (异步)
- 更新用户状态 (同步)
- 删除用户 (同步)
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
优势总结 (通过案例体现)
-
代码量锐减:
-
传统:
actionTypes.js
,actions.js
(多个文件),reducer.js
,store.js
。 -
RTK: 主要逻辑集中在
userSlice.js
一个文件 (createSlice
+createAsyncThunk
),store.js
极简。
-
传统:
-
自动处理样板:
-
createSlice
自动根据reducers
和extraReducers
中的 case 生成 action types 和 action creators。 -
createAsyncThunk
自动为pending
,fulfilled
,rejected
状态生成 action types 和 creators。
-
-
Immer 简化不可变更新:
- 在
updateUserStatus
reducer 中,直接写user.status = status
,而不是复杂的展开运算符。RTK 内部使用 Immer 库将其转换为不可变更新。
- 在
-
结构化异步处理:
-
createAsyncThunk
将异步逻辑封装得更好,extraReducers
集中处理其生成的三种状态,逻辑清晰。
-
-
Entity Adapter 优化实体管理:
- 对于用户列表这种常见场景,
createEntityAdapter
提供了setAll
,addOne
,updateOne
,removeOne
等高效且标准化的 CRUD 操作,避免手动编写map
和filter
。
- 对于用户列表这种常见场景,
-
简化 Store 配置:
-
configureStore
自动合并 reducer,自动应用redux-thunk
和immer
中间件,并默认开启 Redux DevTools 扩展,无需手动配置。
-
结论
Redux Toolkit 并非取代 Redux,而是构建在 Redux 之上的一套最佳实践和工具集。它极大地提升了开发效率和代码可维护性,同时保留了 Redux 的核心原则(单一状态树、可预测的状态更新)。对于新项目,强烈推荐直接使用 Redux Toolkit,它可以让你专注于业务逻辑,而不是 Redux 的样板代码和配置细节。上面的用户管理案例清晰地展示了从“繁琐”到“简洁优雅”的转变。
物联网数据大屏开发效率翻倍:Vue + DataV + ECharts 的标准化模板库
前言
数据可视化已成为企业决策、业务监控不可或缺的一环。一个直观、动态、可定制的数据大屏项目,能够极大地提升信息传递效率,帮助团队快速把握业务动态。
本文将推荐一个基于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在前端的实践:类型系统助力大型应用开发
导读:在大型前端项目中,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
2.关于组件
2.1 组件传值(通信)的方式
2.1.1 父传后代
- 父组件引入子组件,绑定数据:(孙组件无法直接使用,需要子组件再传递给孙组件)
<!-- 父组件引入子组件,绑定数据 -->
<Child :str1="str1"></Child>
<!-- 子组件通过props接收 -->
props:{
str1:{
type: String, //字符串类型
default: '', //默认值
}
}
这种方式 子组件不能直接修改父组件的数据,例如:this.str1 = '321' //错误
- 子组件直接使用父组件的数据
this.$parent.str1
这种方式 子组件可以直接修改父组件的数据,例如:this.$parent.str1 = '321' //正确
- 依赖注入(父组件可以直接向某个后代组件传值,无需逐级传递)
// 父组件
<script>
export default {
provide() {
return {
val1: '这是父组件的依赖内容'
}
}
}
</script>
// 任意的后代组件使用(缺点就是有点难以找到这个值是再哪个父组件中定义的)
<script>
export default {
inject: ['val1'],
}
</script>
2.1.2 后代传父
- 通过触发事件传递数据
// 父组件给子组件设置一个监听事件来触发自己的函数
<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>
- 父组件直接拿到子组件的数据(通过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个生命周期activated
和deactivated
,进入时只会额外执行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 类型错误
发生背景
笔者写前端埋点 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').
笔者的 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 下面,所以笔者把它当静态资源看),找到了这样的信息:
解决方案
于是笔者改为使用ESM动态导入,就像这样:
const registerServiceWorker = () => {
...
const swURL = new URL('./sw.js', import.meta.url).href
...
}
锵锵,解决哩!要是你遇到了和我一样的麻烦,不妨试一试😋☝️
资料: Service Worker 使用指南service worker用的可能不多,但在很多时候却有着不可替代的作用,现在很 - 掘金
前端工程化的范式革命:从 Webpack 的“全量打包”到 Vite 的“按需编译”
引言:我们为何需要构建工具?
在 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)应运而生——它不再只是一个“打包器”,而是整个前端工程化体系的中枢神经系统。
而在这场工程化演进中,Webpack 与 Vite 分别代表了两个时代的巅峰:
- 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):
[源码] → [解析] → [转换] → [依赖分析] → [打包/编译] → [优化] → [输出]
更具体地,可以分为以下几个阶段:
-
入口分析(Entry Resolution)
从main.jsx
开始,确定构建的起点。 -
模块解析(Module Resolution)
解析import
语句,找到每个模块的物理路径(支持别名、扩展名省略等)。 -
加载器处理(Loader Processing)
对不同类型的文件应用不同的“加载器”(Loader),如:-
.tsx
→babel-loader
→ JavaScript -
.styl
→stylus-loader
→ CSS -
.png
→file-loader
→/assets/logo.abc123.png
-
-
依赖图构建(Dependency Graph Construction)
递归分析所有模块的依赖关系,形成一棵有向无环图(DAG)。 -
打包或按需编译(Bundling vs On-Demand Compilation)
- Webpack:将整个依赖图打包成一个或多个 bundle
- Vite:仅在浏览器请求时,按需编译单个模块
-
插件介入(Plugin Hooks)
在构建的各个生命周期阶段插入自定义逻辑,如生成 HTML、注入环境变量。 -
输出与优化(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'] // 顺序:从右到左
}
实际执行顺序:
-
ts-loader
:将 TypeScript 编译为 JavaScript(含 JSX) -
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 |
构建完成 | 打包分析、通知 |
例如,HtmlWebpackPlugin
在 emit
阶段生成 index.html
并自动注入 <script src="bundle.js">
。
2.3 Webpack Dev Server:开发环境的模拟
webpack-dev-server
是一个基于 Express 的 HTTP 服务器,其核心机制是:
- 启动一个本地服务器(默认
localhost:8080
) - 将打包后的资源存储在 内存文件系统(
memory-fs
)中 - 浏览器请求
/bundle.js
时,直接从内存返回 - 支持 HMR(Hot Module Replacement):
- 通过 WebSocket 通知浏览器哪些模块已更新
- 浏览器下载新模块并替换,无需刷新页面
2.4 Webpack 的性能瓶颈:为什么越来越慢?
随着项目规模增长,Webpack 的性能问题日益凸显:
1. 冷启动慢
- 原因:必须完整构建依赖图,编译所有模块
- 影响:大型项目冷启动可能超过 1 分钟
2. 热更新延迟
- 修改一个文件 → 触发重新构建 → 重新打包 → HMR 推送
- 即使只改一行代码,也可能导致整个 bundle 重建
3. 内存占用高
- 整个依赖图常驻内存
- 复杂项目内存占用可达 1GB+
4. 配置复杂
- 需要手动配置
entry
、output
、loaders
、plugins
、resolve
等 - 学习成本高,易出错
第三部分: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
- 不支持
fetch
、Promise
等现代 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
}
}
}
}
});
代码逐行解释:
-
import { defineConfig } from 'vite';
引入 Vite 提供的defineConfig
函数,用于定义配置对象并获得 TypeScript 类型提示。 -
import react from '@vitejs/plugin-react';
引入官方提供的 React 插件,用于支持 JSX 语法和 React 特性。 -
import path from 'path';
引入 Node.js 内置的path
模块,用于处理文件路径。 -
export default defineConfig({ ... });
使用defineConfig
包裹配置对象,并将其导出为默认模块。 -
plugins: [ react() ]
配置插件列表。react()
返回一个插件对象,Vite 会使用它来处理 React 相关的编译。 -
server: { ... }
开发服务器配置:-
port
: 指定服务器监听的端口号。 -
open
: 设置为true
时,启动后自动在默认浏览器中打开应用。 -
proxy
: 配置开发环境代理,解决跨域问题。所有以/api
开头的请求都会被转发到http://localhost:3000
。
-
-
resolve: { alias: { ... } }
配置模块解析规则:-
alias
: 定义路径别名。'@': path.resolve(__dirname, 'src')
表示在代码中使用@/components/Button
等价于src/components/Button
,简化长路径引用。
-
-
build: { ... }
生产构建配置:-
outDir
: 指定构建输出的目录,默认是dist
。 -
sourcemap
: 是否生成 source map 文件。生产环境通常设为false
以减小包体积。 -
minify
: 指定压缩工具。'esbuild'
比'terser'
快得多。 -
rollupOptions
: 传递给底层 Rollup 打包器的高级选项。-
output.manualChunks
: 手动进行代码分割,将指定的依赖打包到独立的 chunk 中,有助于浏览器缓存优化。
-
-
第七部分:结语
7.1 结语:工具的选择是哲学的体现
Webpack 与 Vite 的差异,本质上是两种工程哲学的碰撞:
- Webpack 代表了“防御性编程”:为兼容一切可能的环境,构建一个完整的沙箱。
- Vite 代表了“前瞻性设计”:拥抱现代标准,轻装上阵,追求极致效率。
最终,工具的选择不在于“谁更好”,而在于“谁更适合你的场景”。
理解底层原理,才能做出明智决策。
前端必看:img标签不可忽视的width与height属性
在前端web开发时,有两种方式在页面中放置图片:一种是使用img
标签,一种是使用CSS背景图片。
如果一张图片只是用来装饰,没有其他含义,那么更推荐使用CSS背景图片来实现。
当我们使用img
标签在页面中嵌入图片的时候,可能很多人都只是在CSS中指定图片的width
属性,以此来使图片加载后保持正常的宽高比而不会变形。
这似乎是一种比较完美的写法,width
属性可以指定图片的宽度,而高度浏览器会根据图片宽高比进行自动计算。这种方式在图片不确定的情况下是比较通用的写法,但带来的问题是页面的内容可能会发生偏移。
因为图片加载是异步的,在未加载时,浏览器无法知晓图片的尺寸,img
标签的高度就是0
。在图片加载完成后,浏览器根据图片的尺寸计算出了高度,img
的高度不再是0
,此时图片后方的内容就会被强制向下偏移,这也会触发页面布局的重新计算。
这会带来两个影响:
- 如果页面图片较多,对浏览器的性能开销就会变大;
- 会打断用户当前正在浏览的内容,用户视觉体验不友好。
如果说现在的硬件性能都已经过剩,那么第二点也是不容忽视的问题。
你可以在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 呢?
哈喽,我是楷鹏。
今天想要分享 Chrome 的一个小技巧,可以一次性打开多个干净独立的 Chrome,让你的开发更丝滑。
开头做个小调查,你平时开发的时候,会使用哪些浏览器呢?
- Chrome
- Firefox
- Safari
- 其他
我平时开发的时候,主力就是使用 Chrome。
Chrome 的 DevTools 功能非常强大,满足前端开发调试的绝大数需求。
但是长期来有一个困扰我的问题,就是我的日常使用和开发是耦合在一起的。
比如,我的 Chrome 会装载很多的插件:
这些插件会影响我的开发,因为他们可能在页面中会插入 HTML 或者 CSS 代码,以及会产生很多额外的请求,干扰我的正常开发调试。
比如下面侧边栏的插件 HTML:
此时的选择,要么是开启无痕窗口,要么是换另外一个浏览器。
这两种方式都不错,但无痕窗口还是使用同一个 Chrome 实例,并且重新打开无痕窗口,所有的状态都会被清空。
另外一种方式是换另外一个浏览器,我曾经尝试过,但是后来又放弃了,换一个浏览器就相当于换一种全新的开发环境,需要重新适应界面、操作习惯等等,真的很别扭。
最近学到了另一种新方式,就是可以通过使用不同的用户数据目录,来创建不同的 Chrome 实例。
运行命令:
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --user-data-dir="/tmp/chrome_user_dir_1"
你就可以创建一个全新的 Chrome 实例,并且这个实例的配置、插件、历史记录等都是独立的。
甚至在 Dock 栏,你还可以看到两个 Chrome 图标:
这个新创建的 Chrome 实例,完全可以看作是一个全新的 Chrome 浏览器。
你可以修改主题,来和其他 Chrome 实例区分开来:
或者登录不同的账号等等操作,这是完全属于你的第二 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"
}
}
好了,这就是本期的全部内容,如果对你有帮助,欢迎点赞、收藏、转发。
我是楷鹏,我们下期再见。
0基础进大厂,第22天 : CSS中的定位布局,你的.container还找不到位置吗?
SVG 适合静态图,Canvas 适合大数据?图表库的场景选择
图表库为何偏爱 Canvas 而非 SVG?
在前端开发中,图表库的选择是面试中常被问及的问题。尤其是在涉及到高性能图形渲染时,Canvas 和 SVG 之间的权衡更是核心考点。今天,我们就来深入探讨一下,为什么现在主流的图表库(如 ECharts、AntV G2 等)大多采用 Canvas 方案,而不是 SVG。
💡 SVG 的 GPU 加速特点
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 是 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 的功劳。
⚖️ 总结与选择
总而言之,Canvas 在利用 GPU 硬件加速方面通常具有更明确的控制和更高的性能潜力,而 SVG 的 GPU 加速则更多地依赖于浏览器的自动优化,且加速的范围和效果相对有限。
在实际应用中,选择使用 SVG 还是 Canvas,并考虑 GPU 加速,需要根据具体的应用场景、性能需求和开发技术栈来综合决定。
- SVG 适用场景: 如果你的图形是静态的、数量不多、不需要频繁交互或复杂动画,并且对可访问性、SEO 有较高要求,那么 SVG 可能是更好的选择。它基于 DOM,易于操作和样式化。
- Canvas 适用场景: 如果你需要处理大量图形、实现复杂动画、游戏开发、大规模数据可视化,或者对性能有极高要求,那么 Canvas 结合 GPU 加速将是你的不二之选。它提供了像素级的控制,性能更优。
希望通过这篇文章,你对 Canvas 和 SVG 在 GPU 加速方面的特点有了更深入的理解,在前端面试中也能游刃有余地回答相关问题!
Node.js全栈基石(壹)
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 的运行时环境
- 什么是 JS 运行时环境
- 为什么 JS 需要特别的运行时环境
- JS 引擎是什么
- 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)转换为用户可视化的页面。
渲染引擎工作过程
如上图:
- 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引擎
和浏览器内核
之间的联系和不同
- 实际上 Webkit 是有两部分组成:
-
WebCore
:负责 HTML,CSS 解析,布局,渲染等操作; -
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 原理
其中的
Parse(解析器)
、lgnition(解释器)
、TurboFan(优化编译器)
都是 V8 引擎的内置模块
console.log("hello world");
function sum(num1, num2) {
return num1 + num2;
}
-
Parse
模块会将 JavaScript 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;- 如果函数没有被调用,那么是不会被转换成 AST 的;
-
Parse
的 V8 官方文档:Parser
-
lgnition
模块Ignition 是一个解释器,会将 AST 转换成 ByteCode(字节码);- 同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition 会执行解释执行 ByteCode;
- Ignition 的 V8 官方文档:Ignition
-
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 架构区别
- 在 Chrome 浏览器中
- V8 引擎只是其中一小部分,用来辅助 JavaScript 代码的运行
- 还有一些浏览器的内核用来负责 HTML 解析、布局、渲染等相关工作
- 中间层和操作系统( 网卡、硬盘、显卡……)
- 比如发送网络请求,中间层会调用操作系统中的网卡
- 读取一些本地文件,中间层会调用操作系统中的硬盘
- 浏览器页面的渲染工作,中间层会调用操作系统中的显卡
- 在 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
- JS 代码就是我们平时在使用的那些 JS 模块。比如,
fs
模块,http
模块,path
模块等 - C++ 代码主要分三个部分。
- 第一部分主要是封装
Libuv
和第三方库的C++
代码,比如net
,fs
这些模块都会对应一个 C++ 模块,它主要是对底层的一些封装,暴露给开发者使用 - 第二部分是不依赖
Libuv
和第三方库的C++
代码,比如Buffer
模块的实现 - 第三部分 C++ 代码是 V8 本身的代码
- 第一部分主要是封装
- C 语言代码主要是包括
Libuv
和第三方库的代码,它们都是纯 C 语言实现的代码
Node.js 中主要各部分实现:
Node.js 与 JavaScript 分层
从下往上
梳理:
- 最下面一层是脚本语言规范(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 开发。以下是其关键阶段的梳理:
- 2009 年:Node.js 的诞生
- 创始人:Ryan Dahl,一位对服务器性能瓶颈不满的开发者。
- 灵感来源:受 **V8 引擎(Chrome 的 JavaScript 引擎)**启发,Dahl 希望用 JavaScript 实现高性能、事件驱动的服务器端开发。
- 首次发布:2009 年 11 月,Node.js 在 JSConf EU 大会亮相,核心思想是非阻塞 I/O和事件循环,解决了传统服务器(如 Apache)的并发处理瓶颈。
- 早期发展与争议(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 能做什么
- 服务端开发:用于做服务器端开发 web 服务器
- 工具:可以基于 node 构建一些工具,构建工作流,比如 npm,webpack,gulp,less,sass 等 vue-cli
- 开发工具或者客户端应用:开发桌面应用程序(借助 node-webkit、electron 等框架实现),比如:vscode、语雀等
- 同构:SSR,借助 Node.js 完成服务端渲染+前后端同构
- serverless
- 流式 SSR
- 游戏
- npm、yarn,pnpm 工具成为前端开发使用最多的工具;
- 爬虫
Node.js 环境搭建
如何选择 Node.js 版本
下载地址:Download | Node.js LTS VS Current Current 版本:当前最新的版本。 LTS 版本:Long Term Support,即长期维护版本。 说明参照:Release
Node.js 版本可以分为三个阶段:Current
、Active LTS
和Maintenance
当一个奇数版本发布后,最近的一个偶数版本会立即进入 LTS 维护阶段,一直持续 18 个月。LTS 维护阶段结束以后,进入 12 个月的 Maintenance
阶段。
注意:奇数版本
不包含 Active LTS
和 Maintenance
这两个阶段。
三个阶段主要做的事情
-
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 的缩写。
-
AMD
是Asynchronous Module Definition
,即异步模块定义 -
CMD
是Common Module Definition
,即一般模块定义,虽然 Common 也含通用意思,但这里将其译为“一般”是为了不与后面 UMD 冲突 -
UMD
则是Universal Module Definition
,即通用模块定义。
AMD
最开始在 require.js 中被使用,其首个提交是在 2009 年发出的。CMD
与 AMD
很类似,不同点在于: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() 访问此对象。exports
是module.exports
对应的引用。这是一个用于导出模块内容的通道exports = module.exports = {}; exports.name = "exports"; console.info(module.exports); // name 'exports'
-
require()
: 加载模块,访问模块导出的内容。运行时加载,是同步的,返回值是加载模块的module.exports
值,加载失败返回null
。require
是一个函数,这个函数有一个参数代表模块标识,它的返回值就是其所引用的外部模块所暴露的 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";
Node.js 的 module
对象下还挂载了个 module.exports
对象,其初始值指向 CommonJS 所定义的 exports
对象。而真正导出是 module.exports
,并不是 exports
.
启用 ESM 的两种方式
- 通过文件扩展名
.mjs
:将文件扩展名改为.mjs
,Node.js 会自动识别为ESM模块
// app.mjs export const hello = () => "Hello ESM";
- 在
package.json
中设置"type":"module"
{ "name": "ESM", "version": "1.0.0", "type": "module", "scripts": { "serve": "xxx serve", "build": "xxx build" }, }
CommonJS 源码解析
- 给 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
- 同时使用 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
- 访问包含随机语句的模块
// 模块导出 - 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
- 循环引用
// 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)
- 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
为什么直接赋值 exports
会失效
错误用法 ❌
// 错误!无法导出内容
exports = {
name: "foo",
method: function () {
/* ... */
},
};
- 上面代码中
exports
被重新赋值,但module.exports
仍然指向原始的空对象,导致导出失败。 -
exports
的本质:exports
是模块系统在运行时提供的一个引用变量,它默认指向module.exports
的内存地址。 -
直接赋值会切断引用:如果你直接对
exports
赋值(例如exports = ...
),相当于让exports
指向了一个新的对象,而原本的module.exports
不会受到影响。此时模块实际导出的仍然是旧的module.exports
(默认为空对象),就会导致 require 引入的始终是空对象
正确用法:
- 通过
module.exports
导出// 模块导出 - user.js const name = "lucy"; const age = 28; const printName = () => { console.log(name); };
- 修改
exports
的属性,如果希望保持使用 exports,可以通过添加属性的方式修改它(而不是直接赋值):
此时exports.name = "face"; exports.method0 = () => { console.log("method0"); };
exports
仍然指向原始的module.exports
,因此修改其属性是有效的。 - 底层原理
- 模块的导出始终以
module.exports
为准 - 初始化时,Node.js 会执行一下操作
var module = { exports: {}, }; var exports = module.exports; // 初始状态时两者指向同一个对象
- 因此,直接修改
exports = ...
会影响module.exports
- 因此,直接修改
- 模块的导出始终以
- 直接赋值给
exports
:❌ 无效(会切断与 module.exports 的关联)。 - 操作
module.exports
:✅ 正确且可靠。 - 修改
exports
的属性:✅ 正确(前提是保持引用)。