
前端模块化的最后一块拼图——浏览器中如何运行模块
前端模块化的演变包括CJS、AMD、CMD、UMD和ESM,现代浏览器支持ESM,webpack负责模块处理和打包,确保在浏览器中高效运行模块,支持代码拆分和Tree Shaking以优化性能。
本文探讨了在React中使用react-use-websocket库来实现WebSocket功能的最佳实践,包括如何封装WebSocket逻辑、处理连接状态、发送和接收消息,以及使用ArrayBuffer传输文件。此外,文中还介绍了Socket.IO的工作原理及其与WebSocket的区别,提供了Node.js后端服务器的示例代码,展示了如何实现低延迟的双向通信和进度反馈。
在 WebSocket 出现之前,如果我们想实现实时通信,比较常采用的方式是 Ajax 轮询,即在特定时间间隔(比如每秒)由浏览器发出请求,服务器返回最新的数据。这样做的问题有:
WebSocket协议的出现解决了上述问题:
由于上述特点,WS广泛应用于视频弹幕、在线聊天、音视频通话、实时定位等场景。
WebSocket已经成为HTML5标准之一,被目前所有主流浏览器支持,内部提供WebSocket()
API
WebSocket 是基于 TCP 的一种新的应用层网络协议。它实现了浏览器与服务器全双工通信,即允许服务器主动发送信息给客户端。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
WebSocket的特点如下:
直接向WS服务器发送请求切换协议的报文(默认就是HTTP的端口,也可以指定端口)
Connection: Upgrade
表示要升级协议,以及Upgrade: websocket
字段说明要切换到WS协议,以上报文创建工作由浏览器中的WebSocket
功能完成WebSocket协议使用自己的帧(frame)结构传递信息,一条WS消息可能被分成若干WS帧进行发送,在接收端重新组装。
WS帧结构如下图所示:
WS报文头和HTTP不同,不包括繁杂的Header信息,只包含基础的三项:
FIN
表示当前帧是否是当前消息的最后帧opcode
操作码,表示当前请求的类型
len
表示载荷的长度由于WebSocket是全双工、保持连接的机制,我们需要通过一定手段来确定连接正常没有断开或者服务是否可用,这就是心跳机制。
可以看见,上面的操作码中定义了用于心跳请求和响应的位,可以通过定时发送对应数据包来确定让对方知道自己在线且正常工作,确保通信有效。如果对方无法响应,便可以弃用旧连接,发起新的连接了。
在JS中我们可以简单地用setInterval()
创建定时器发送消息即可。
通过用Node.js原生的ws
库实现一个基础的WebSocket聊天室,理解WebSocket在实际使用过程中的调用流程和抽象模型。
首先初始化一个项目,这里使用 pnpm init
,并安装依赖 pnpm add ws
。
这里基于个人爱好,我在package.json 中写入了 "type": "module" 以启用ESM,如果你习惯使用CJS请留意代码中的导入部分。
在写代码之前,我们先阅读ws的文档和处理流程:
WebSocketServer
创建一个服务端实例,对于每个连接创建一个 WebSocket
实例,一个WebSocketServer
实例管理多个WebSocket
实例。WebSocketServer
通过connection
事件与WebSocket
实例连接,每个WebSocket
实例对应实际的WS连接。整个处理流程基于事件驱动,即在connection
事件中获取当前连接的 WebSocket
实例并给它绑定对应的事件处理器
connection
:连接事件,一切操作的起点,只有它绑定在Server上
socket
参数可以获得当前连接的WebSocket实例request
参数可以获得当前连接请求的所有HTTP报文信息,如Origin、Cookie等message
:接收到某个Socket传来的消息的事件,参数为data
,即消息的载荷,可以是文本、Buffer或二进制close
:服务关闭事件,由于WebSocket是全双工协议,在服务正式关闭时(任何一方*close()
***)双方都会收到关闭报文和对应事件**WebSocketServer
实例可以管理所有连接的Sockets(默认可以多连接),使用clients
属性获取与当前服务器连接的所有WebSocket
实例**(可以用来实现广播)**WebSocket
实例对应了一个WebSocket连接,常用的API如下:
Event
接口:一般包含了type
, data
, target
等属性可以调用
type
:事件属性,如message, close, connection等data
:有效载荷target
:一般对应WebSocket
实例url
属性:目标WS服务器地址send(data, [options], [callback])
方法:向对方发送信息(服务端也可以调用)close([ErrorCode], [data])
方法:关闭连接创建一个基础服务端和一个用于测试的客户端,代码如下:
// server.js
import { WebSocketServer, WebSocket } from "ws";
const server = new WebSocketServer({ port: 4567 });
function broadcast(msg, user, source) {
server.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN && client !== source) {
client.send(`[${user}] ${msg}`);
}
});
}
server.on('connection', (socket, req) => {
const user = req.headers.origin;
console.log(`[Server] connected: ${user}`);
socket.on('message', (msg) => {
console.log(`From ${user}: ${msg}`);
socket.send(`[Echo] You sent -> ${msg}`)
broadcast(msg, user, socket);
if (msg === 'bye') {
socket.close(0, "Bye");
}
});
socket.on('close', (code, reason) => {
console.log(`[Server] Connection closed with code: ${code} for ${reason}`);
});
})
console.log('WebSocket Server started at port 4567...')
// client.js
import { WebSocket } from "ws";
const socket = new WebSocket('ws://localhost:4567', { origin: ((Math.random()*10).toFixed(0)).toString() });
socket.addEventListener('open', (e) => {
console.log(`Connected to server: ${e.target.url}`)
socket.send('Hello Server from NodeJS Client!');
})
socket.addEventListener('message', (e) => {
// e.type=事件类型,open|message|close
console.log(e.data);
})
socket.addEventListener('close', (e) => {
console.log(`Type=${e.type}\nCode=${e.code}\nReason=${e.reason}`);
})
let count = 0;
setInterval(() => {
socket.send(count++);
}, 1000);
可以看到,在上面的代码中我们利用clients
属性手写实现了广播功能,所谓WS“不提供”的功能,这里我们能窥见一点WebSocket的本质属性——它只是一种服务协议,提供了一种全双工的沟通方式,而上层的调用和实现、需要什么功能则完全由开发者决定,就像HTTP一样是一种“基础设施”。
运行效果如下:
首先使用pnpm create vite
创建一个SPW应用,这里使用React+Typescript+SWC的方案,你也可以根据自己的喜好选择。然后按照流程pnpm install
安装依赖、pnpm run dev
启动开发服务器。
我们使用上面写的服务端作为WebSocket服务器,改写./src/App.tsx
,代码如下:
import { useEffect, useMemo, useRef, useState } from 'react'
import './App.css'
function App() {
const ws = useRef<WebSocket | null>(null);
const [connected, setConnected] = useState<boolean>(false);
const [host, setHost] = useState<string>('localhost:4567');
const [send, setSend] = useState<string>('');
const [messages, setMessages] = useState<{
category: 'message' | 'error' | 'info',
message: string
}[]>([]);
const handleConnect = () => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
messages.push({category: 'info', message: 'Already connected!'});
return;
}
ws.current = new WebSocket('ws://' + host);
ws.current.addEventListener('open', () => {
messages.push({category: 'info', message: 'Connected to ' + ws.current!.url});
setMessages([...messages])
setConnected(true);
});
ws.current.addEventListener('message', (event) => {
messages.push({category: 'message', message: event.data as string});
setMessages([...messages])
});
ws.current.addEventListener('close', () => {
messages.push({category: 'info', message: 'Disconnected from ' + ws.current!.url});
setMessages([...messages])
});
ws.current.addEventListener('error', () => {
messages.push({ category: 'error', message: 'Error Connection' });
setMessages([...messages])
});
};
const handleDisconnect = () => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.close();
setMessages([]);
setConnected(false);
}
}
const handleSendMessage = () => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(send);
}
}
const texts = useMemo(() => {
return messages.map(({ category, message }, index) => {
return <div className={category} key={index}>
{`[${category.toUpperCase()}] ${message}`}
</div>
})
}, [messages])
useEffect(() => {
return () => {
handleDisconnect();
}
}, [])
return (
<div>
<div className='flex-container'>
<input value={host} onChange={(e) => setHost(e.target.value)} />
<button disabled={connected} onClick={handleConnect}>Connect</button>
<button disabled={!connected} onClick={handleDisconnect}>Disconnect</button>
</div>
{
connected &&
<div className='flex-container sender'>
<input value={send} onChange={(e) => {setSend(e.target?.value)}} />
<button onClick={handleSendMessage}>Send</button>
<button onClick={() => setMessages([])}>Clear</button>
</div>
}
<div className='log-div'>
{texts}
</div>
</div>
)
}
export default App
部分CSS样式如下:
.flex-container {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.log-div {
background-color: aliceblue;
border-radius: 10px;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
margin: 20px 0;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: 5px 10px;
width: 100%;
}
.log-div div {
margin: 5px 0;
}
.log-div .info {
color: #888;
}
.log-div .error {
color: #ff0000;
}
.sender {
margin: 10px 0;
}
.sender input {
width: 500px;
}
从代码中我们可以看到我们使用了ref
管理WS实例(不会随视图渲染改变的变量),并使用几个state
来管理消息列表、连接状态和输入框缓存,最后将连接和发送操作封装为可调用的方法。
最终效果如下图所示,可以看到尽管来自同一域名,我们的WebSocket服务器也能区分不同会话。
react-use-websocket
在上面的开发过程中我们可以发现,建立WebSocket连接其实是一个可以抽象的逻辑,以供重复调用,在React中也就是抽象为一个Hook,每次需要用到WebSocket的时候就调用这个Hook。
这里我们使用一个名为react-use-websocket
的npm包,先安装它 pnpm add react-use-websocket
(如果你使用React17及以下请安装3.0.0版本)。使用封装好的包的好处是节省开发时间和提供更健壮和全面的封装,避免自己开发时没有考虑到的边界情况引发的Bugs(但不是绝对的)。
react-use-websocket
提供的API如下(Copy自官方文档),可以看到它直接为我们封装好了一系列的行为(如心跳重连、JSON封装等),将基础的ReadyState
属性、send
操作等暴露了出来:
type UseWebSocket = (
//Url can be return value of a memoized async function.
url: string | () => Promise<string>,
options: {
fromSocketIO?: boolean;
queryParams?: { [field: string]: any };
protocols?: string | string[];
share?: boolean;
onOpen?: (event: WebSocketEventMap['open']) => void;
onClose?: (event: WebSocketEventMap['close']) => void;
onMessage?: (event: WebSocketEventMap['message']) => void;
onError?: (event: WebSocketEventMap['error']) => void;
onReconnectStop?: (numAttempts: number) => void;
shouldReconnect?: (event: WebSocketEventMap['close']) => boolean;
reconnectInterval?: number | ((lastAttemptNumber: number) => number);
reconnectAttempts?: number;
filter?: (message: WebSocketEventMap['message']) => boolean;
retryOnError?: boolean;
eventSourceOptions?: EventSourceInit;
} = {},
shouldConnect: boolean = true,
): {
sendMessage: (message: string, keep: boolean = true) => void,
//jsonMessage must be JSON-parsable
sendJsonMessage: (jsonMessage: JsonValue, keep: boolean = true) => void,
//null before first received message
lastMessage: WebSocketEventMap['message'] | null,
//null before first received message. If message.data is not JSON parsable, then this will be a static empty object
lastJsonMessage: JsonValue | null,
// -1 if uninstantiated, otherwise follows WebSocket readyState mapping: 0: 'Connecting', 1 'OPEN', 2: 'CLOSING', 3: 'CLOSED'
readyState: number,
// If using a shared websocket, return value will be a proxy-wrapped websocket, with certain properties/methods protected
getWebSocket: () => (WebSocketLike | null),
}
使用react-use-websocket
改写后的代码如下:
import { useEffect, useMemo, useRef, useState } from 'react'
import useWebSocket from 'react-use-websocket';
import './App.css'
import { WebSocketLike } from 'react-use-websocket/dist/lib/types';
function App() {
const ws = useRef<WebSocketLike | null>(null);
const senderRef = useRef<HTMLInputElement>(null);
const [shouldConnect, setShouldConnect] = useState<boolean>(false);
const [host, setHost] = useState<string>('localhost:4567');
const [messages, setMessages] = useState<{
category: 'message' | 'error' | 'info',
message: string
}[]>([]);
const {
sendMessage,
lastMessage,
readyState,
getWebSocket,
} = useWebSocket("ws://"+host, {
onOpen: () => {
ws.current = getWebSocket();
},
shouldReconnect: () => shouldConnect,
}, shouldConnect);
const connected = readyState === 1;
useEffect(() => {
if (lastMessage) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data } = lastMessage
setMessages((prev) => {
return [...prev, { category: 'message', message: data as string }]
})
}
}, [lastMessage])
const texts = useMemo(() => {
return messages.map(({ category, message }, index) => {
return <div className={category} key={index}>
{`[${category.toUpperCase()}] ${message}`}
</div>
})
}, [messages])
const handleConnect = () => {
setShouldConnect(true);
}
const handleDisconnect = () => {
if (ws.current) {
ws.current.close();
}
}
useEffect(() => {
return () => {
handleDisconnect();
}
}, [])
return (
<div>
<div className='flex-container'>
<input value={host} onChange={(e) => setHost(e.target.value)} />
<button disabled={connected} onClick={handleConnect}>Connect</button>
<button disabled={!connected} onClick={handleDisconnect}>Disconnect</button>
</div>
{
connected &&
<div className='flex-container sender'>
<input ref={senderRef} />
<button onClick={() => {
if (senderRef.current) {
sendMessage(senderRef.current.value);
senderRef.current.value = '';
}
}}>Send</button>
</div>
}
<div className='log-div'>
{texts}
</div>
</div>
)
}
export default App
可以看到,我们使用了react-use-websocket
后由于用其暴露的属性进行了逻辑重写,包括使用useEffect
来更新消息队列、直接使用sendMessage()
来发送消息以及直接使用readyState
来判断连接状态。相较于之前的实现,封装后的代码显然减小了重复代码量。
需要注意的是,由于该Hook将WebSocket实例封装在了内部,外部采用状态驱动的函数式形式,因此无法通过新建实例的方式对同一WS地址进行断开和重连(即手动控制连接状态),只能依赖以下两种方式:
shouldConnect
和shouldReconnect
绑定到一个外部的connect
状态中,等待重连来重新建立连接(上面代码的方法)url
参数可以做到立刻新建****WebSocket
*** 实例并重连(推荐),实践中可以在重连后立刻清空状态,等待用户重新输入connect
操作暴露给用户WebSocket中使用ArrayBuffer传输文件,在react-use-websocket
中直接将文件用sendMessage()
传输出去即可,ws
接收到的参数中isBin
就是true
,如下图所示:
因此,使用ArrayBuffer传输文件的方案有下:
Blob
对象(File
是Blob
的子类)进行传输Socket.IO 不是 WebSocket实现。尽管它尽可能使用 WebSocket 作为****传输协议,但它一方面可能回退到其他传输方式(如Ajax轮询),另一方面其每条消息都被附上了额外的数据来实现自身逻辑,如轮询回退、自动重连等。
这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器,虽然Socket.io的传输协议使用了WebSocket,但它们在接口上并不是兼容的。
Socket.IO 是一个多语言、跨平台的库,可以在客户端和服务器之间实现 低延迟, 双向 和 基于事件的 通信。通常情况下,它建立在 WebSocket 协议之上,并提供额外的保证,例如回退到 HTTP 长轮询或自动重新连接。它提供以下额外特性:
广播和多路复用
socket.io默认工作在WebSocket上,在轮询模式中则回退到HTTP,但其报文内容是一致的。socket.io通过在报文负载中附上额外信息来确保自身额外功能的实现。
如socket.emit("hello", "world")
将作为单个 WebSocket 帧发送,其中包含42["hello","world"]
:
你可能注意到了上面有engine.io和socket.io两种不同的数据包类型标识符,这对应了Socket.io 两个不同的层:
底层通道 Engine.IO:负责建立服务器和客户端之间的低级连接,如WebSocket连接、轮询回退、断线检测(心跳机制)等,向上提供一个全双工连接的抽象
默认使用HTTP长轮询建立连接,后续尝试升级协议为WebSocket
HTTP 长轮询:由连续的 HTTP 请求组成,WebSocket的回退策略
GET
请求,用于从服务器接收数据POST
请求,用于向服务器发送数据Socket
实例这里的Socket概念和WebSocket类似,对应一个连接,其具有以下额外属性:
id
属性:每个新连接都分配有一个随机的 20 个字符的标识符handshake
属性:握手信息conn
属性:底层连接信息房间是在服务器端可见的客户端的集合,它包含一个或多个客户端。
socket.rooms()
可以查看当前Socket所在房间的集合socket.join(roomName: string)
可以使socket加入某个房间io.to(room).emit(event, ...args)
可以向房间内所有客户端广播事件
to()
支持级联,同时向多个房间发送socket
发送广播,则除了发送者外房间内所有客户端都会收到广播中间件函数是为每个传入连接执行的函数,Socket.io里的中间件和其他地方的中间件含义是相同的,通常用于登录检测等领域。
Socket.IO API 的灵感来自 Node.js EventEmitter,这意味着您可以在一侧发出事件并在另一侧注册侦听器
和普通的WebSocket一样,Socket.io也是基于事件驱动的双工通讯
io.on('connection', (socket) ⇒ {...})
是服务端一切事件的起点,需要在回调中给每个socket绑定事件socket.on(event: string, callback: (...args) ⇒ void)
是对Socket连接注册事件监听的方法
socket.once()
是签名和on()
相同的一次性监听函数socket.onAny(listener)
可用于监听任意事件disconnect
和 disconnecting
:在服务端发出的特殊事件,Socket 实例在断开连接时触发,payload
为断开原因event
是可以自定义的,只要不覆盖任何现有事件就可以是任何类型的字段,这对应了原版WebSocket的****type
*** 字段,但不同之处是原版WebSocket中只能使用send()
方法,type
固定为message
,但这里是可以完全自定义的socket.off(event: string)
用于移除指定监听器socket.emit(event: string, ...args: any)
是向对方发送事件的方法
socket.send(..args)
方法仍是支持的,将发送一个默认的message
事件JSON.stringify()
***,因为它会为您完成emit
中添加回调,只需要把最后一个参数设为回调函数,在服务端进行调用即可socket.broadcast.emit(event, ...args)
会向除发送者外的所有客户端发送消息待完成
import { Server } from "socket.io";
const io = new Server({
cors: 'http://127.0.0.1:5173/',
});
let progress = 0;
io.on('connection', (socket) => {
console.log(`[Server] Connection established with ${socket.id}`);
// 模拟进度
const timer = setInterval(() => {
if (progress < 100) {
socket.emit("progress", progress);
socket.send(`${progress}% 这是一条模拟日志`);
progress++;
} else {
socket.emit("progress", progress);
socket.send("Done");
progress = 0;
clearInterval(timer);
socket.disconnect();
}
}, 100);
// 基础回声
socket.on('message', (data) => {
console.log(`[Message] ${data}`);
socket.send(`[Echo] You sent -> ${data}`);
});
// 断开连接
socket.on('disconnect', (reason) => {
clearInterval(timer);
progress = 0;
console.log(`[Server] Connection with ${socket.id} closed with ${reason}`);
});
})
io.listen(4567);
在这个例子中我使用自定义事件progress
模拟了一个后台进程向前端输出日志和进度的过程。
需要注意的是,在前后端分离的情况下我们需要手动指定跨域,就像上面的cors: 'http://127.0.0.1:5173/',
一样
这里我们沿用上一个WebSocket的例子,先安装依赖 pnpm add socket.io-client
。
直接抽象出一个useSocket
Hook进行封装:
// useSocket.ts
/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
export default function useSocket(url: string, autoConnect=false) {
const socket = useRef(io(url, { autoConnect }));
const [isConnected, setIsConnected] = useState(socket.current.connected);
const [latestMessage, setLatestMessage] = useState<unknown>(null);
const [progress, setProgress] = useState<number>(0);
useEffect(() => {
socket.current.on("connect", () => {
setIsConnected(true);
setLatestMessage('Connected!')
});
socket.current.on("disconnect", () => {
setIsConnected(false);
alert('Disconnected!')
});
socket.current.on("message", (data) => {
setLatestMessage(`${Date.now()}: ${data as string}`);
});
socket.current.on("progress", (progress: number) => {
setProgress(progress);
});
return () => {
socket.current.off("connect");
socket.current.off("disconnect");
socket.current.off("message");
socket.current.off("progress");
}
}, [])
const handleSendMessage = (message: string) => {
socket.current.send(message);
}
return { isConnected, latestMessage, progress, sendMessage: handleSendMessage, socket: socket.current }
}
// App.tsx
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import './App.css'
import useSocket from './useSocket';
const inputTypes = ['text', 'file'];
function App() {
const senderRef = useRef<HTMLInputElement>(null);
const [inputType, setInputType] = useState<string>(inputTypes[0]);
const [host, setHost] = useState<string>('localhost:4567');
const { isConnected, latestMessage, progress, socket, sendMessage } = useSocket("http://" + host);
const [messages, setMessages] = useState<{
category: 'message' | 'error' | 'info',
message: string
}[]>([]);
useEffect(() => {
if (latestMessage) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
setMessages((prev) => {
return [...prev, { category: 'message', message: latestMessage as string }]
})
}
}, [latestMessage])
const texts = useMemo(() => {
return messages.map(({ category, message }, index) => {
return <div className={category} key={index}>
{`[${category.toUpperCase()}] ${message}`}
</div>
})
}, [messages])
const handleConnect = () => {
socket.connect();
}
const handleDisconnect = useCallback(() => {
socket.disconnect();
}, [socket])
useEffect(() => {
return () => {
handleDisconnect();
}
}, [handleDisconnect])
return (
<div>
<div className='flex-container'>
<input value={host} onChange={(e) => setHost(e.target.value)} />
<button disabled={isConnected} onClick={handleConnect}>Connect</button>
<button disabled={!isConnected} onClick={handleDisconnect}>Disconnect</button>
</div>
{
isConnected &&
<div className='flex-container sender'>
<select value={inputType} onChange={(e) => {setInputType(e.target.value)}}>
{inputTypes.map((type, index) => {
return <option key={index} value={type}>{type}</option>
})}
</select>
<div>{progress}%</div>
<input type={inputType} ref={senderRef} />
<button onClick={() => {
if (senderRef.current) {
if (inputType === 'text') sendMessage(senderRef.current.value);
senderRef.current.value = '';
}
}}>Send</button>
</div>
}
<div className='log-div'>
{texts}
</div>
</div>
)
}
export default App
最终运行效果如下:
作者: ChlorineC
创建于: 2023-07-08 17:47:00
更新于: 2025-02-12 15:41:00
版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
前端模块化的演变包括CJS、AMD、CMD、UMD和ESM,现代浏览器支持ESM,webpack负责模块处理和打包,确保在浏览器中高效运行模块,支持代码拆分和Tree Shaking以优化性能。
Bun 1.0是一个新的JavaScript运行时,作为Node.js的替代品,提供更快的包管理和开发解决方案。尽管目前在生产环境中尚不成熟,但它集成了多种工具,支持统一模块标准和Web API,具有显著的性能优势。Bun的包管理器速度比pnpm快4倍,支持原生JSX和TS,且具备构建工具的潜力。整体来看,Bun在运行时和工具链上均有加速效果,但仍需进一步发展以满足生产需求。
pnpm是一个高效且节省空间的前端包管理器,通过改进的非扁平node_modules目录和硬链接机制优化依赖管理。与npm和yarn相比,pnpm在性能和兼容性上表现优越,尤其在缓存情况下安装速度更快。pnpm的安装过程分为解析、目录结构计算和链接依赖项三个步骤,采用符号链接解决幽灵依赖问题,并通过硬链接机制减少硬盘占用。pnpm还支持monorepo,并提供了操作全局store的命令。
本文介绍了前端包管理器npm的发展历史、功能和重要性。npm不仅是一个包管理器,还是前端项目管理的基础,管理依赖、项目信息和脚本。文章详细探讨了npm的历史、模块化概念、依赖管理的演变,以及如何使用npm管理项目和发布自己的npm包。尽管npm在现代前端开发中逐渐被yarn和pnpm等新工具取代,但其核心思想和机制仍然对开发者有重要的学习价值。