Express.js 基础

Express.js 是 Node.js 生态系统中最流行、最成熟的 Web 应用框架。它提供了极简且灵活的 API,使得构建 Web 服务器和 RESTful API 变得快速而简单。


一、Express 简介与安装

什么是 Express

Express 是一个基于 Node.js 的极简且灵活的 Web 应用程序框架,提供了一组强大的功能,适用于 Web 和移动应用程序的开发。它构建在 Node.js 的 HTTP 模块之上,简化了服务器端应用程序的开发过程。

安装 Express

在使用 Express 之前,需要先安装它。Express 通过 npm(Node Package Manager)进行安装和管理。

bash 复制代码
# 创建项目目录
mkdir my-express-app
cd my-express-app

# 初始化项目(创建 package.json)
npm init -y

# 安装 Express
npm install express

二、创建第一个 Express 应用

创建一个 Express 应用非常简单,只需要调用 express() 函数即可。这个函数返回一个 Express 应用实例,它是整个应用的核心。

最简单的 Express 应用

javascript 复制代码
// 引入 Express 模块
const express = require('express');

// 创建 Express 应用实例
const app = express();

// 定义路由
app.get('/', (req, res) => {
  res.send('Hello World!');
});

// 启动服务器,监听指定端口
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

express() 函数

express() 是一个工厂函数,用于创建 Express 应用实例。这个实例包含了所有 Express 的功能,包括路由、中间件、模板引擎等。

javascript 复制代码
const express = require('express');
const app = express();  // 创建应用实例

// app 对象现在包含了所有 Express 的方法和属性
console.log(typeof app);  // 'function'(实际上是一个函数对象)

app.listen() 方法

app.listen() 方法用于启动服务器并开始监听指定端口的 HTTP 请求。它是 Express 应用启动的关键方法。

基本语法:

javascript 复制代码
app.listen(port, callback)

参数说明:

  • port: 要监听的端口号(数字)
    • 端口号范围:有效端口号为 0-65535
    • 推荐端口:开发环境常用 3000、3001、8000、8080、5000 等
    • 不能使用的端口
      • 0-1023 :系统保留端口(需要管理员权限),常见的有:
        • 80:HTTP(通常需要 root 权限)
        • 443:HTTPS(通常需要 root 权限)
        • 22:SSH
        • 21:FTP
        • 25:SMTP
        • 3306:MySQL
        • 5432:PostgreSQL
      • 1024-49151:注册端口(IANA 注册),应避免与已知服务冲突
      • 49152-65535:动态/私有端口(相对安全,适合开发使用)
    • 注意事项 :如果端口已被占用,会抛出 EADDRINUSE 错误
  • callback: 服务器启动后执行的回调函数(可选)

示例:

javascript 复制代码
const express = require('express');
const app = express();

// 方式一:只指定端口
app.listen(3000);

// 方式二:指定端口和回调函数(推荐)
app.listen(3000, () => {
  console.log('服务器运行在 3000 端口');
});

// 方式三:使用环境变量配置端口
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 ${PORT} 端口`);
});

使用 Node.js HTTP 模块(底层方式):

Express 的 app.listen() 实际上是对 Node.js http.createServer() 的封装。你也可以直接使用 HTTP 模块:

javascript 复制代码
const express = require('express');
const http = require('http');

const app = express();
const server = http.createServer(app);

server.listen(3000, () => {
  console.log('服务器运行在 3000 端口');
});

三、路由基础

路由是 Express 应用的核心功能之一。它定义了应用如何响应客户端对不同 URL 路径和 HTTP 方法的请求。

什么是路由

路由是指确定应用程序如何响应客户端对特定端点的请求,该端点是 URI(或路径)和特定的 HTTP 请求方法(GET、POST、PUT、DELETE 等)的组合。

路由的基本结构

每个路由都有一个或多个处理函数,当路由匹配时执行:

javascript 复制代码
app.METHOD(path, handler)
  • app: Express 应用实例
  • METHOD: HTTP 请求方法(get、post、put、delete 等)
  • path: 服务器上的路径
  • handler: 路由匹配时执行的函数

HTTP 方法路由

Express 支持所有 HTTP 方法,最常用的是 GET、POST、PUT 和 DELETE。

app.get() - GET 请求

GET 请求用于获取资源,是最常用的 HTTP 方法。

javascript 复制代码
// 获取所有用户
app.get('/users', (req, res) => {
  res.json([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ]);
});

// 获取单个用户
app.get('/users/:id', (req, res) => {
  const userId = req.params.id;
  res.json({ id: userId, name: 'John' });
});

app.post() - POST 请求

POST 请求用于创建新资源。

javascript 复制代码
// 需要配置中间件解析请求体
app.use(express.json());

// 创建用户
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  // 创建用户的逻辑...
  
  res.status(201).json({
    id: 1,
    name: name,
    email: email,
    message: '用户创建成功'
  });
});

app.put() - PUT 请求

PUT 请求用于更新整个资源。

javascript 复制代码
app.use(express.json());

// 更新用户(完整更新)
app.put('/users/:id', (req, res) => {
  const userId = req.params.id;
  const { name, email } = req.body;
  
  // 更新用户的逻辑...
  
  res.json({
    id: userId,
    name: name,
    email: email,
    message: '用户更新成功'
  });
});

app.delete() - DELETE 请求

DELETE 请求用于删除资源。

javascript 复制代码
// 删除用户
app.delete('/users/:id', (req, res) => {
  const userId = req.params.id;
  
  // 删除用户的逻辑...
  
  res.status(204).send();  // 204 No Content
});

其他 HTTP 方法

Express 还支持其他 HTTP 方法:

javascript 复制代码
// PATCH - 部分更新
app.patch('/users/:id', (req, res) => {
  res.json({ message: '部分更新用户' });
});

// OPTIONS - 预检请求
// 使用场景:主要用于 CORS(跨域资源共享)场景
// - 当前端应用(如 http://localhost:3000)请求后端 API(如 http://localhost:8080)时
// - 当请求使用了 PUT、DELETE、PATCH 等非简单方法,或包含自定义请求头时
// 解决的问题:
// - 浏览器安全策略:浏览器会阻止跨域请求,除非服务器明确允许
// - 告知客户端服务器支持哪些 HTTP 方法和请求头
app.options('/users', (req, res) => {
  res.set('Allow', 'GET, POST, OPTIONS');
  res.send();
});

// HEAD - 获取响应头
app.head('/users', (req, res) => {
  res.end();
});

路由路径匹配

Express 支持多种路由路径匹配方式,包括字符串匹配、正则表达式匹配和参数匹配。

字符串路径匹配

最简单的路由路径是字符串匹配:

javascript 复制代码
// 精确匹配
app.get('/about', (req, res) => {
  res.send('关于页面');
});

// 匹配根路径
app.get('/', (req, res) => {
  res.send('首页');
});

路径参数匹配

使用 : 定义路径参数:

javascript 复制代码
// 单个参数
app.get('/users/:id', (req, res) => {
  res.json({ userId: req.params.id });
});

// 多个参数
app.get('/users/:userId/posts/:postId', (req, res) => {
  res.json({
    userId: req.params.userId,
    postId: req.params.postId
  });
});

可选参数

使用 ? 定义可选参数:

javascript 复制代码
// /users 和 /users/:id 都可以匹配
app.get('/users/:id?', (req, res) => {
  if (req.params.id) {
    res.json({ userId: req.params.id });
  } else {
    res.json({ message: '所有用户' });
  }
});

通配符匹配

使用 * 进行通配符匹配:

javascript 复制代码
// 匹配 /users/ 后面的所有路径
app.get('/users/*', (req, res) => {
  res.send('用户相关页面');
});

正则表达式匹配

使用正则表达式进行复杂匹配:

javascript 复制代码
// 只匹配数字 ID
app.get('/users/:id(\\d+)', (req, res) => {
  res.json({ userId: req.params.id });
});

// 匹配特定格式
app.get('/files/:filename(.*\\.(jpg|png|gif))', (req, res) => {
  res.send(`图片文件: ${req.params.filename}`);
});

多个处理函数

一个路由可以有多个处理函数(中间件),按顺序执行:

javascript 复制代码
// 验证函数
function validateUser(req, res, next) {
  const userId = req.params.id;
  if (!userId || isNaN(userId)) {
    return res.status(400).json({ error: '无效的用户 ID' });
  }
  next();  // 继续执行下一个处理函数
}

// 获取用户函数
function getUser(req, res) {
  const userId = req.params.id;
  res.json({ id: userId, name: 'John' });
}

// 使用多个处理函数
app.get('/users/:id', validateUser, getUser);

四、请求对象(req):获取请求数据

在 Express 中,每个路由处理函数都会接收请求对象(req),它包含了客户端发送的所有信息,包括请求头、请求参数、请求体等。

req.params - 路径参数

路径参数是 URL 路径中的动态部分,使用 : 定义,通过 req.params 对象访问。

基本用法

javascript 复制代码
// 定义路径参数
app.get('/users/:id', (req, res) => {
  console.log(req.params);  // { id: '123' }
  console.log(req.params.id);  // '123'
  
  res.json({
    userId: req.params.id,
    message: '获取用户信息'
  });
});

// 访问 /users/123
// req.params = { id: '123' }

多个路径参数

一个路由可以定义多个路径参数:

javascript 复制代码
app.get('/users/:userId/posts/:postId', (req, res) => {
  console.log(req.params);
  // 访问 /users/123/posts/456
  // req.params = { userId: '123', postId: '456' }
  
  res.json({
    userId: req.params.userId,
    postId: req.params.postId
  });
});

路径参数命名规则

路径参数的名称可以是字母、数字和下划线的组合:

javascript 复制代码
// 有效
app.get('/users/:id', handler);
app.get('/users/:userId', handler);
app.get('/users/:user_id', handler);

// 无效(不能包含特殊字符)
// app.get('/users/:user-id', handler);  // 错误

req.query - 查询参数

查询参数是 URL 中 ? 后面的键值对,通过 req.query 对象访问。查询参数用于过滤、排序、分页等操作。

基本用法

javascript 复制代码
// 访问 /search?q=express&page=1
app.get('/search', (req, res) => {
  console.log(req.query);  // { q: 'express', page: '1' }
  console.log(req.query.q);  // 'express'
  console.log(req.query.page);  // '1'
  
  res.json({
    query: req.query.q,
    page: req.query.page
  });
});

多个查询参数

javascript 复制代码
// 访问 /products?category=electronics&minPrice=100&maxPrice=500&sort=price
app.get('/products', (req, res) => {
  const { category, minPrice, maxPrice, sort } = req.query;
  
  console.log('分类:', category);
  console.log('最低价格:', minPrice);
  console.log('最高价格:', maxPrice);
  console.log('排序:', sort);
  
  // 使用查询参数进行过滤和排序...
  
  res.json({
    category: category,
    minPrice: minPrice,
    maxPrice: maxPrice,
    sort: sort
  });
});

数组查询参数

查询参数可以是数组:

javascript 复制代码
// 访问 /products?tags=javascript&tags=nodejs&tags=express
app.get('/products', (req, res) => {
  console.log(req.query.tags);  // ['javascript', 'nodejs', 'express']
  
  res.json({
    tags: req.query.tags
  });
});

查询参数的默认值

为查询参数提供默认值:

javascript 复制代码
app.get('/products', (req, res) => {
  const page = parseInt(req.query.page) || 1;  // 默认第 1 页
  const limit = parseInt(req.query.limit) || 10;  // 默认每页 10 条
  const sort = req.query.sort || 'id';  // 默认按 id 排序
  
  res.json({
    page: page,
    limit: limit,
    sort: sort
  });
});

req.body - 请求体

请求体包含 POST、PUT 等请求中发送的数据。需要中间件来解析(如 express.json())。

javascript 复制代码
// 需要先配置中间件
app.use(express.json());

app.post('/users', (req, res) => {
  console.log(req.body);  // { name: 'John', email: 'john@example.com' }
  console.log(req.body.name);  // 'John'
  
  res.json({
    id: 1,
    name: req.body.name,
    email: req.body.email
  });
});

req 的其他常用属性

req.headers - 请求头

请求头包含了客户端发送的所有 HTTP 头信息。

javascript 复制代码
app.get('/', (req, res) => {
  console.log(req.headers);
  console.log(req.headers['user-agent']);  // 浏览器信息
  console.log(req.headers['content-type']);  // 内容类型
});

req.method - HTTP 方法

获取请求的 HTTP 方法(GET、POST、PUT、DELETE 等)。

javascript 复制代码
app.use((req, res, next) => {
  console.log(`请求方法: ${req.method}`);
  console.log(`请求路径: ${req.path}`);
  next();
});

req.path - 请求路径

获取请求的路径部分(不包含查询字符串)。

javascript 复制代码
// 访问 /users/123?page=1
app.get('/users/:id', (req, res) => {
  console.log(req.path);  // '/users/123'
});

req.url - 完整 URL

获取请求的完整 URL(包含查询字符串)。

javascript 复制代码
// 访问 /users/123?page=1
app.get('/users/:id', (req, res) => {
  console.log(req.url);  // '/users/123?page=1'
});

req.ip - 客户端 IP 地址

获取客户端的 IP 地址。

javascript 复制代码
app.get('/', (req, res) => {
  console.log('客户端 IP:', req.ip);
  res.send(`你的 IP 地址是: ${req.ip}`);
});

路径参数 vs 查询参数

理解路径参数和查询参数的区别和使用场景很重要:

路径参数(req.params):

  • 用于标识资源(如用户 ID、文章 ID)
  • URL 的一部分,必需
  • 示例:/users/123(123 是用户 ID)

查询参数(req.query):

  • 用于过滤、排序、分页等操作
  • URL 的可选部分
  • 示例:/users?page=1&limit=10(page 和 limit 用于分页)

五、响应对象(res):发送响应

响应对象(res)代表 HTTP 响应,用于向客户端发送数据、设置响应头、设置状态码等。

res.send() - 通用响应方法

res.send() 是最灵活的响应方法,可以发送各种类型的数据。Express 会根据数据类型自动设置适当的 Content-Type

javascript 复制代码
app.get('/', (req, res) => {
  res.send('Hello World!');  // 发送字符串,Content-Type: text/html
});

app.get('/json', (req, res) => {
  res.send({ message: 'Hello' });  // 发送对象,Content-Type: application/json
});

app.get('/buffer', (req, res) => {
  res.send(Buffer.from('Hello'));  // 发送 Buffer,Content-Type: application/octet-stream
});

app.get('/api/users', (req, res) => {
  res.send([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ]);  // 发送数组,Content-Type: application/json
});

res.json() - JSON 响应

res.json() 专门用于发送 JSON 响应,会自动设置 Content-Typeapplication/json,并调用 JSON.stringify()

javascript 复制代码
app.get('/api/user', (req, res) => {
  res.json({
    id: 1,
    name: 'John Doe',
    email: 'john@example.com'
  });
});

app.get('/api/users', (req, res) => {
  res.json([
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ]);
});

app.get('/api/user/:id', (req, res) => {
  res.json({
    id: req.params.id,
    name: 'John Doe',
    profile: {
      age: 30,
      city: 'New York'
    },
    tags: ['developer', 'nodejs']
  });
});

res.status() - 设置状态码

res.status() 用于设置 HTTP 响应状态码,通常与其他响应方法链式调用。

基本用法

javascript 复制代码
// 成功响应(200)
app.get('/success', (req, res) => {
  res.status(200).json({ message: '成功' });
});

// 创建成功(201)
app.post('/users', (req, res) => {
  res.status(201).json({ id: 1, name: 'John' });
});

// 未找到(404)
app.get('/not-found', (req, res) => {
  res.status(404).json({ error: '资源未找到' });
});

// 服务器错误(500)
app.get('/error', (req, res) => {
  res.status(500).json({ error: '服务器内部错误' });
});

常用 HTTP 状态码

javascript 复制代码
// 2xx - 成功
res.status(200).json({ message: 'OK' });           // 成功
res.status(201).json({ message: 'Created' });       // 创建成功
res.status(204).send();                             // 无内容(常用于 DELETE)

// 4xx - 客户端错误
res.status(400).json({ error: 'Bad Request' });     // 请求错误
res.status(401).json({ error: 'Unauthorized' });   // 未授权
res.status(403).json({ error: 'Forbidden' });       // 禁止访问
res.status(404).json({ error: 'Not Found' });       // 未找到
res.status(409).json({ error: 'Conflict' });       // 冲突

// 5xx - 服务器错误
res.status(500).json({ error: 'Internal Server Error' });  // 服务器错误
res.status(503).json({ error: 'Service Unavailable' });     // 服务不可用

res.redirect() - 重定向

res.redirect() 用于将客户端重定向到另一个 URL。这对于页面跳转、URL 重写等场景非常有用。

javascript 复制代码
// 临时重定向(302)
app.get('/old-page', (req, res) => {
  res.redirect('/new-page');
});

// 永久重定向(301)
app.get('/old-url', (req, res) => {
  res.redirect(301, '/new-url');
});

// 重定向到外部 URL
app.get('/external', (req, res) => {
  res.redirect('https://www.example.com');
});

// 重定向到相对路径
app.get('/login', (req, res) => {
  // 登录成功后重定向到首页
  res.redirect('/');
});

app.get('/users/:id/edit', (req, res) => {
  // 编辑完成后重定向到用户详情页
  const userId = req.params.id;
  res.redirect(`/users/${userId}`);
});

res 的其他常用方法

res.set() / res.header() - 设置响应头

设置 HTTP 响应头。

javascript 复制代码
app.get('/', (req, res) => {
  res.set('Content-Type', 'text/html');
  res.set('X-Custom-Header', 'custom-value');
  res.send('<h1>Hello</h1>');
});

// 或者使用 res.header()
app.get('/api', (req, res) => {
  res.header('Content-Type', 'application/json');
  res.json({ message: 'Hello' });
});

res.get() - 获取响应头

获取已设置的响应头值。

javascript 复制代码
app.get('/', (req, res) => {
  res.set('X-Custom-Header', 'custom-value');
  console.log(res.get('X-Custom-Header'));  // 'custom-value'
  res.send('Hello');
});

设置 Cookie。

javascript 复制代码
app.get('/set-cookie', (req, res) => {
  res.cookie('username', 'john', { maxAge: 900000, httpOnly: true });
  res.send('Cookie 已设置');
});

res.write() 和 res.end() - 分块写入和结束响应

res.write():用于分块写入响应数据,可以多次调用 res.end():用于结束响应

javascript 复制代码
app.get('/chunked', (req, res) => {
  res.write('Hello');
  res.write(' ');
  res.write('World');
  res.end('!');  // 结束响应
});

注意事项:

  • res.write() 可以多次调用,但必须最后调用 res.end() 来结束响应
  • 如果使用 res.send()res.json(),它们会自动结束响应,不需要手动调用 res.end()

res.render() - 渲染模板

javascript 复制代码
// 需要配置模板引擎(如 EJS)
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  res.render('index', { title: '首页', users: [] });
});

res.download() - 下载文件

res.download() 用于将文件作为附件下载发送给客户端,会自动设置适当的响应头。客户端访问后,浏览器会弹出下载对话框,用户可以选择保存文件。

javascript 复制代码
app.get('/download', (req, res) => {
  res.download('/path/to/file.pdf', 'document.pdf');  // 第一个参数是文件路径,第二个参数是下载时的文件名(可选)
});

res.sendFile() - 发送文件

javascript 复制代码
const path = require('path');

app.get('/file', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

响应方法链式调用

多个响应方法可以链式调用:

javascript 复制代码
app.get('/api/user', (req, res) => {
  res
    .status(200)
    .set('X-Custom-Header', 'custom-value')
    .json({
      id: 1,
      name: 'John Doe'
    });
});

六、完整示例:RESTful API

下面是一个完整的 RESTful API 示例,展示了 Express 的各种功能的使用:

javascript 复制代码
const express = require('express');
const app = express();

// 配置中间件解析 JSON 请求体
app.use(express.json());

// 模拟数据存储
let users = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

// GET /users - 获取所有用户(支持分页和过滤)
app.get('/users', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const role = req.query.role;  // 可选的角色过滤
  
  res.status(200).json({
    success: true,
    page: page,
    limit: limit,
    count: users.length,
    data: users
  });
});

// GET /users/:id - 获取单个用户
app.get('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const user = users.find(u => u.id === userId);
  
  if (!user) {
    return res.status(404).json({
      success: false,
      error: '用户未找到'
    });
  }
  
  res.status(200).json({
    success: true,
    data: user
  });
});

// POST /users - 创建用户
app.post('/users', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({
      success: false,
      error: '缺少必要字段:name 和 email'
    });
  }
  
  const newUser = {
    id: users.length + 1,
    name: name,
    email: email
  };
  
  users.push(newUser);
  
  res.status(201).json({
    success: true,
    message: '用户创建成功',
    data: newUser
  });
});

// PUT /users/:id - 更新用户(完整更新)
app.put('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);
  
  if (userIndex === -1) {
    return res.status(404).json({
      success: false,
      error: '用户未找到'
    });
  }
  
  const { name, email } = req.body;
  users[userIndex] = { ...users[userIndex], name, email };
  
  res.status(200).json({
    success: true,
    message: '用户更新成功',
    data: users[userIndex]
  });
});

// DELETE /users/:id - 删除用户
app.delete('/users/:id', (req, res) => {
  const userId = parseInt(req.params.id);
  const userIndex = users.findIndex(u => u.id === userId);
  
  if (userIndex === -1) {
    return res.status(404).json({
      success: false,
      error: '用户未找到'
    });
  }
  
  users.splice(userIndex, 1);
  
  res.status(204).send();  // 204 No Content
});

// 获取用户的文章列表(路径参数 + 查询参数)
app.get('/users/:userId/posts', (req, res) => {
  const userId = req.params.userId;  // 路径参数
  const page = parseInt(req.query.page) || 1;  // 查询参数
  const limit = parseInt(req.query.limit) || 10;  // 查询参数
  const sort = req.query.sort || 'createdAt';  // 查询参数
  
  res.json({
    userId: userId,
    page: page,
    limit: limit,
    sort: sort,
    posts: []
  });
});

// 重定向示例
app.get('/home', (req, res) => {
  res.redirect('/');
});

app.get('/', (req, res) => {
  res.send('欢迎访问用户管理 API');
});

// 启动服务器
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

总结

Express.js 作为 Node.js 最流行的 Web 框架,提供了简洁而强大的 API 来构建 Web 应用和 RESTful API。本文深入探讨了 Express 的基础概念:

  1. Express 简介与安装:了解 Express 是什么以及如何安装
  2. 创建第一个 Express 应用 :通过 express() 创建应用实例,使用 app.listen() 启动服务器
  3. 路由基础 :使用 app.get()app.post()app.put()app.delete() 等方法定义路由,支持路径参数、查询参数等多种匹配方式
  4. 请求对象(req) :通过 req.params 获取路径参数,通过 req.query 获取查询参数,通过 req.body 获取请求体数据
  5. 响应对象(res) :使用 res.send()res.json()res.status()res.redirect() 等方法向客户端发送响应
  6. 完整示例:构建一个完整的 RESTful API,综合运用所学知识

参考资源

相关推荐
梨子同志7 小时前
Node.js HTTP 服务器开发
前端
码途潇潇7 小时前
数据大屏常用布局-等比缩放布局(Scale Laylout)-使用 CSS Transform Scale 实现等比缩放
前端·css
犬大犬小7 小时前
从头说下DOM XSS
前端·javascript·xss
绿鸳7 小时前
Socket.IO实时通信
前端
Cache技术分享7 小时前
273. Java Stream API - Stream 中的中间操作:Mapping 操作详解
前端·后端
我的div丢了肿么办7 小时前
echarts中appendData的详细讲解
前端·javascript·vue.js
JamesGosling6667 小时前
async/defer 执行顺序全解析:从面试坑到 MDN 标准
前端·javascript
喝咖啡的女孩7 小时前
Web Worker 前端多线程解析
前端