Nodejs开发进阶C-HTTP

我们前面已经提到,nodejs是为Web应用开发而生的。所以,和一般的通用性开发平台不同,在设计之初,nodejs就内置了对标准网络协议的支持,包括了TCP、UDP、HTTP以及WS,当然其中的重点和核心就是HTTP,也就是我们在本章节中需要重点探讨的内容,nodejs的其他网络特性,笔者有机会另著文阐述。

HTTP协议和Nodejs

关于HTTP协议的内容,笔者另有一篇博文涉及的更加深入,这里就不再特别展开陈述。

这里想要特别说明的是,nodejs是将对http协议的支持,放在一个技术体系的核心的位置上的。所以其内置的不仅仅是简单的对http的支持,还包括了更加底层的网络协议TCP/UDP的支持。另外,考虑到Web应用的丰富性和多样性,还涉及到了扩展的网络协议如HTTPS、HTTP2和WebSocket等方面的内容。

在当前LTS版本(V20),相关http协议、网络和Web应用相关的功能模块包括(来自nodejs官方API文档):

  • HTTP: http协议支持模块
  • HTTPS: https协议支持模块
  • HTTP/2: http/2协议支持模块
  • TLS (SSL): 安全套件字层相关模块,支持HTTPS/2的底层技术
  • WS(WebSocket): WebSocket协议支持模块
  • URL: 标准URL结构
  • Query Strings: 查询字符串模块

可以看到,其相关HTTP协议支持的功能设计还是比较完善的,对于最新的Web技术的跟踪也是比较快的。

HTTP模块和类

熟悉http协议和相关设计的开发者都了解,http使用经典的客户端/服务器部署模式和请求/响应业务模型。所以在nodejs中,http模块的设计和实现,就围绕这些内容展开。

首先我们来看看http模块中的内容,就是在其中定义的类,包括:

  • Agent: 客户端代理对象
  • ClientRequest: 客户端请求对象
  • Server: http服务器类
  • ServerResponse: 服务端响应对象
  • IncomingMessage: 入站消息对象
  • OutgoingMessage: 出站消息对象
  • http: 主http类,包含一些常量和快速方法

笔者认为,这样的设计和规划,非常简洁清晰,贴合http协议的原理和定义,易于理解和使用。理论上而言,我们可以仅基于nodejs的http模块,构建一个完整的Web应用(Web部分)。但实际在工程实际上,我们一般会引入一些所谓的"框架"系统,它们会基于Web应有的类型模式、普通流程和最佳实践,提供更加规范和一致的开发体验,并增加常用和必要的扩展功能,熟练掌握和使用,通常可以大大提高开发的效率,并规避由于不规范和错误的技术应用所带来的,特别是涉及性能和安全性等方面问题的风险,我们后面会进一步讨论相关的内容。

但无疑基础也是非常重要的,下面我们就先以一些常用的使用场景为例,结合http模块和相关功能的实现,重点分析其中比较重要和常用的内容。

HTTP请求(客户端)

一般的Web应用开发都是针对作为服务器的角色的。比如Java的SpringBoot、PHP等等。如果需要作为HTTP客户端,则需要其他的组件或者技术。以前的Web应用需要作为客户端的场景并不多,但现在的Web应用系统越来越复杂,还有微服务概念和技术的流行,Web应用对其他HTTP服务的访问能力,也是HTTP服务本身的一个重要功能。

nodejs内置的HTTP模块,本身就内置了request对象,可以作为http客户端来使用。下面的示例代码展示了其常规应用方式:

js 复制代码
    const http = require('node:http');

    const postData = JSON.stringify({
      'msg': 'Hello World!',
    });

    const options = {
      hostname: 'www.google.com',
      port: 80,
      path: '/upload',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(postData),
      },
    };

    const req = http.request(options, (res) => {
      console.log(`STATUS: ${res.statusCode}`);
      console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
      res.setEncoding('utf8');
      res.on('data', (chunk) => {
        console.log(`BODY: ${chunk}`);
      });
      res.on('end', () => {
        console.log('No more data in response.');
      });
    });

    req.on('error', (e) => {
      console.error(`problem with request: ${e.message}`);
    });

    // Write data to request body
    req.write(postData);
    req.end();

这段代码稍显繁复,是因为它展示了很多技术细节,也贴近真实的使用场景,要考虑很多实际的问题。我们简单的分析一下其中的要点:

  • 完全基于HTTP协议的原理和过程
  • http请求操作的核心,是先基于一些配置和选项,创建request对象
  • 这些选项,主要包括请求的地址、端口、请求方式(此处为POST)、头信息(格式、内容类型长度等等)
  • 同时需要编写一个响应处理程序,其参数就是响应对象
  • 响应对象其实是一个读取流(read stream),可以使用数据流的方式来处理响应内容,这里是拼接chunk
  • 请求的内容,也可以使用写入流的方式,依次写入并使用end方法结束
  • 可能需要编写处理错误的程序,请求的过程如果出现错误,将会触发error事件

HTTPS

和互联网早期的蛮荒和简陋相比,现在Web应用的发展已经相当成熟。一个明显的标注就是网络传输的安全机制已经比较完善,体现在HTTPS协议的标准化和全面化。

和一般的想象不同,nodejs专门提供了HTTPS模块来实现和处理相关的功能。笔者觉得可能是由于其继承自TLS的体系,而且需要在里面实现很多密码学相关的处理。所以索性使用一个独立的模块。

但使用HTTPS来进行http请求的方式,却和http模块基本上没有什么差别。其他可能的差异在于:

  • 需要提供客户端的证书和密钥(如果要对客户端进行验证的话)
  • 可能需要处理和SSL相关的错误和问题,如可选的服务端签名和证书的验证等

所以,我们在使用HTTPS的请求功能是,最常见的错误信息就是证书类型的错误了,比如自签名证书,证书过期等等。特别是在开发和测试阶段,我们不需要那么严格的安全策略,可以选择暂时忽略这些错误。这个操作可以通过设置环境变量,或者修改进程参数的方式来实现:

js 复制代码
    // 启动前设置环境变量
    set NODE_TLS_REJECT_UNAUTHORIZED = 0
    
    // 在程序中修改进程参数
    process.env.NODE_TLS_REJECT_UNAUTHORIZED = 0;

简单方法

前面已经看到,在nodejs中实现http请求的过程,虽然清晰明确,却略显繁复。比如对于非常简单的GET请求,也需要提供一个URL地址和相关的设置。为了方便代码编写,http模块提供了快捷的get方法,方便编写get请求的操作(注意简单方法没有POST请求)。

下面的参考代码,可以让我们很好的理解这一设定:

js 复制代码
    https
    .get('https://encrypted.google.com/', (res) => {
      console.log('statusCode:', res.statusCode);
      console.log('headers:', res.headers);

      res.on('data', (d) => {
        process.stdout.write(d);
      });

    }).on('error', (e) => {
      console.error(e);
    }); 

fetch

如果我们熟悉前端开发,我们就知道浏览器环境中,编程异步发起HTTP请求(AJAX)的技术基础是XMLHttpRequest (XHR),后来又提供了一个较新的API名为fetch。相对XHR,fetch比较容易使用,最重要的是它可以使用promise方式来处理异步请求,支持HTTP2和更多的HTTP方法和请求头,已经接近于一个比较理想的标准的HTTP客户端模块了。

有趣的是,在nodejs环境中,也提供了一个fetch对象可以直接使用。但经过资料查证,笔者发现,它和浏览器中的fetch API并不是同一个事物。但是,它们都遵循WHATWG Fetch API规范,使用方式确实是基本相同的。nodejs中的fetch对象是基于Undici(Node.js社区开发的HTTP客户端),而浏览器中的则是由其开发者自行实现。nodejs fetch提供的功能稍稍多于标准fetch。但需要注意的是,fetch并不是标准nodejs模块,不需要引用,技术文档中也没有相关的章节。

相对而言,fetch的使用方式就比request或者XHR简单多了,下面是其参考示例代码:

js 复制代码
    const url = 'https://httpbin.org/post'
    const data = {
        x: 1920,
        y: 1080,
    };
    const customHeaders = {
        "Content-Type": "application/json",
    }

    fetch(url, {
        method: "POST",
        headers: customHeaders,
        body: JSON.stringify(data),
    })
    .then((response) => response.json())
    .then((data) => {
        console.log(data);
    })
    .catch(error=>{
        // something wrong 
    });
    
    // 同步形式
    async function logMovies() {
         const response = await fetch("http://example.com/movies.json");
         const movies = await response.json();
         console.log(movies);
    }

可以看到,在nodejs中使用fetch的要点如下:

  • 可以直接使用fetch对象(应该是模块级变量)
  • 可以使用基本相同的模式来使用Get和Post
  • Post方法,直接将内容注入body参数
  • 可以不分http和https
  • 可以使用then/catch方式调用,也可以使用async/await方式调用
  • 调用有几个阶段,它提供了内容编码的方法如json(),text()等等,并且可以链接处理
  • 可以使用一致的错误处理方式

显然,相比http的request方法,fetch使用更加灵活方便。

HTTP服务(Server)

使用http模块,创建一个标准的http服务,基本上就是两个步骤:

1 使用一个函数创建一个http server实例,函数的参数只有两个,就是请求和响应对象

这个函数用于基于请求信息,进行业务处理,并且使用响应对象发送响应信息

2 使用网络和端口配置信息,启动实例侦听

参考代码如下:

js 复制代码
    const server = http.createServer((req, res) => {
      const 
      ip   = res.socket.remoteAddress,
      port = res.socket.remotePort;
      
      res.end(`Your IP address is ${ip} and your source port is ${port}.`);
    }).listen(3000);

这样,一个最精简的Web应用服务程序,基本上就是这样架构的。其他各种丰富的功能和配置,都是根据业务和应用的需求,在这个架构上进行扩展开来的。

http模块

在了解了HTTP请求和服务的简单应用后,我们来了解初步了解一下http模块。作为Web应用开发支持的一个重要组成部分,nodejs提供了http模块,里面定义了很多和HTTP协议处理相关的对象和方法。

http对象结构

我们可以使用REPL来检查http对象的结构:

js 复制代码
> http
{
  _connectionListener: [Function: connectionListener],
  METHODS: [
    'ACL',        'BIND',        'CHECKOUT',
    'CONNECT',    'COPY',        'DELETE',
    'GET',        'HEAD',        'LINK',
    'LOCK',       'M-SEARCH',    'MERGE',
    'MKACTIVITY', 'MKCALENDAR',  'MKCOL',
    'MOVE',       'NOTIFY',      'OPTIONS',
    'PATCH',      'POST',        'PRI',
    'PROPFIND',   'PROPPATCH',   'PURGE',
    'PUT',        'REBIND',      'REPORT',
    'SEARCH',     'SOURCE',      'SUBSCRIBE',
    'TRACE',      'UNBIND',      'UNLINK',
    'UNLOCK',     'UNSUBSCRIBE'
  ],
  STATUS_CODES: {
    '100': 'Continue',
    '101': 'Switching Protocols',
    '102': 'Processing',
    '200': 'OK',
    '201': 'Created',
    '202': 'Accepted',
    '203': 'Non-Authoritative Information',
    '204': 'No Content',
    '205': 'Reset Content',
    '206': 'Partial Content',
    '207': 'Multi-Status',
    '208': 'Already Reported',
    '226': 'IM Used',
    '300': 'Multiple Choices',
    '301': 'Moved Permanently',
    '302': 'Found',
    '303': 'See Other',
    '304': 'Not Modified',
    '305': 'Use Proxy',
    '307': 'Temporary Redirect',
    '308': 'Permanent Redirect',
    '400': 'Bad Request',
    '401': 'Unauthorized',
    '402': 'Payment Required',
    '403': 'Forbidden',
    '404': 'Not Found',
    '405': 'Method Not Allowed',
    '406': 'Not Acceptable',
    '407': 'Proxy Authentication Required',
    '408': 'Request Timeout',
    '409': 'Conflict',
    '410': 'Gone',
    '411': 'Length Required',
    '412': 'Precondition Failed',
    '413': 'Payload Too Large',
    '414': 'URI Too Long',
    '415': 'Unsupported Media Type',
    '416': 'Range Not Satisfiable',
    '417': 'Expectation Failed',
    '418': "I'm a Teapot",
    '421': 'Misdirected Request',
    '422': 'Unprocessable Entity',
    '423': 'Locked',
    '424': 'Failed Dependency',
    '425': 'Too Early',
    '426': 'Upgrade Required',
    '428': 'Precondition Required',
    '429': 'Too Many Requests',
    '431': 'Request Header Fields Too Large',
    '451': 'Unavailable For Legal Reasons',
    '500': 'Internal Server Error',
    '501': 'Not Implemented',
    '502': 'Bad Gateway',
    '503': 'Service Unavailable',
    '504': 'Gateway Timeout',
    '505': 'HTTP Version Not Supported',
    '506': 'Variant Also Negotiates',
    '507': 'Insufficient Storage',
    '508': 'Loop Detected',
    '509': 'Bandwidth Limit Exceeded',
    '510': 'Not Extended',
    '511': 'Network Authentication Required'
  },
  Agent: [Function: Agent] { defaultMaxSockets: Infinity },
  ClientRequest: [Function: ClientRequest],
  IncomingMessage: [Function: IncomingMessage],
  OutgoingMessage: [Function: OutgoingMessage],
  Server: [Function: Server],
  ServerResponse: [Function: ServerResponse],
  createServer: [Function: createServer],
  validateHeaderName: [Function: hidden],
  validateHeaderValue: [Function: hidden],
  get: [Function: get],
  request: [Function: request],
  maxHeaderSize: [Getter],
  globalAgent: [Getter/Setter]
}

从这个结构,我们可以简单的分析一下nodejs如何来实现和处理http协议。可以分为以下几类。

HTTP协议相关

和HTTP协议相关的内容包括:

  • METHODS: http方法常数,但好像内容比http标准方法多啊
  • STATUS_CODES: 状态码常数,来自HTTP规范
  • IncomingMessage: 入站消息,就是作为逻辑的接收方,接收到的数据
  • OutgoingMessage: 出站消息,就是作为逻辑的发送方,发送的数据
  • validateHeaderName: 方法,用于验证HTTP头项目的名称是否有效
  • validateHeaderValue: 方法,用于验证HTTP头项目的值是否有效
  • setMaxIdleHTTPParsers: 方法,用于设置 HTTP 解析器的最大空闲数量,通常用于调节HTTP数据处理性能

HTTP客户端相关

作为HTTP客户端相关的:

  • Agent: 请求时使用的代理对象,也就是HTTP客户端对象
  • ClientRequest: 创建请求的方法和所创建的请求对象,它继承了OutgoingMessage,用于处理发送数据
  • request: 请求构造方法,可用于配置和发起HTTP请求,并直接通过回调函数处理响应信息
  • get: 快速请求方法,实际上是一个request特例和简化

HTTP服务端相关

作为HTTP服务端相关包括以下内容:

  • createServer:方法,用于创建服务器对象
  • Server: HTTP服务器类
  • ServerResponse: 在请求时,由服务器对象字段创建的,可以用于操作响应的响应对象

其中,Server类的核心属性和方法包括:

  • listen: 侦听方法,此处使用给定的绑定地址和端口,启动网络侦听
  • close/: 关闭连接,停止服务
  • requestListener: 这是调用creatServer的时候,设置的请求侦听处理回调程序,它会自动传入两个参数:request和response
  • request: 这是一个IncomingMessage对象实例,表示HTTP请求
  • response: 这是一个ServerResponse(继承了OutgoingMessage)子类的实例,这是一个OutgoingMessage对象的扩展,表示HTTP响应

作为IncomingMessage类的实例,request在服务请求侦听回调参数中的核心属性和方法包括:

  • headers: 请求的头信息
  • method: 请求方法
  • url: 请求地址
  • on("data",data=>{}): 接收数据事件和处理程序,发送数据可能是分阶段或者信息流,数据的默认形式是buffer
  • on("end", ()=>{}): 请求结束事件和处理程序,可以在请求结束时统一处理请求数据

其中,ServerResponse的核心属性和方法包括:

  • socket: 请求和响应使用的网络插座对象
  • socket.remoteAddress/remotePort: 远端的网络地址信息,即客户端IP地址和端口(请求端口可能是随机的)
  • statusCode/statusMessage: 响应状态码和文本信息
  • setHeader/setHeaders: 设置响应头信息
  • writeHeader: 发送响应状态码和响应头信息
  • write: 发送响应信息
  • end: 结束响应,参数是结束时传输的内容

从http对象的结构和文档上来看,涉及到的相关内容是非常庞杂的。作为一般的Web应用开发,我们可能通常不会用到非常细节的特性,只需要处理上述几个核心环节就可以了。

而且,在实际的开发组织工作中,我们也不会直接nodejs提供的这些非常基础和底层的http特性,而是使用一些功能更加完善,应用更加成熟的Web框架技术,如Express、fastify等等。虽然这些框架的技术基础应该也是nodejs http模块,但它们在其上面做了很多扩展和封装工作,比如路由,内容解析,自动编码等等,方便更加复杂的Web应用开发。笔者另有其他的章节讨论相关议题。

HTTP Trailers

trailer的原意是拖车,在HTTP协议中,它通常用于在HTTP响应中添加额外的头部信息,而且是在响应主体发送完毕后进行操作,添加的信息可以包括如Cookie或其他状态信息等等。

要使用Trailer,需要先在HTTP头部声明 Transfer-Encoding: trailers。然后就可以在body尾部,或者发送数据之后,声明并且设置Trailer了。另外,要想获取trailer的内容,通常只能在onEnd事件中处理。在http模块的message类中,就有trailers的相关属性和addTrailers等相关方,例如下面的代码:

js 复制代码
message.writeHead(200, { 'Content-Type': 'text/plain',
                         'Trailer': 'Content-MD5' });
message.write(fileData);
message.addTrailers({ 'Content-MD5': '7895bf4b8828b55ceaf47747b4bca667' });
message.end(); 

虽然HTTP协议设计了这个功能,看起来也可以使请求响应的过程处理更加灵活,但笔者尚未发现其很好的应用场景或者案例。

完整参考示例

为了让读者更好的理解前面的内容,和http模块的基本工作原理和相对完整的流程,笔者编写了下列的参考示例代码:

js 复制代码
    const 
    http = require("http"),
    HOST = "127.0.0.1",
    PORT = 8087;

    const doRequest = async()=>{
        const postData = JSON.stringify({
            name: "John",
            time: 0 | Date.now()/1000,
        });

        const options = {
            hostname: HOST,
            port: PORT,
            path: '/upload',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(postData),
            },
        };

        const req = http.request(options, (res) => {
            console.log(`STATUS: ${res.statusCode}`);
            console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
            res.setEncoding('utf8');
            let data =[];
            res.on('data', chunk => data.push(chunk));
            res.on('end', () => {
                console.log("Response:");
                console.log(data.join(""));
                console.log("Requet Ended.");
            });
        });

        req.on('error', (e) => {
        console.error(`problem with request: ${e.message}`);
        });

        // Write data to request body
        req.write(postData);
        req.end();
    };

    const handle = (req, res)=>{
        console.log("Client:", req.socket.remoteAddress, req.socket.remotePort);
        console.log("Url:", req.url);

        let body = [];
        req
        .on("data", data=>body.push(data))
        .on("end",()=>{
            let qobject = JSON.parse(Buffer.concat(body).toString());
            console.log("Request:\n",qobject);

            res.write('hello!\n');
            res.write("Welcome:" + qobject.name+ qobject.time);
            res.end();        
        });
    }; 

    // start Server
    setTimeout(()=>http
      .createServer(handle)
      .listen(PORT, HOST, null, ()=>{
        console.log("Server Started:", HOST, PORT);
      }),1000);

    // do request
    setInterval(doRequest,3000);

这里没有使用掘金的代码片段,是因为服务启动侦听后,端口会被占用,程序无法重复执行,:(。读者可以复制到自己的nodejs环境中直接执行,无需任何外部依赖。

HTTP/2

相对而言,HTTP/2是一个更新的协议,已经于2015年(基本上是和HTML5同时)发布。作为HTTP协议的第二个主要版本,笔者觉得它并不是一个HTTP协议的简单升级,而是从某种意义上是一个全新的设计,主要目的是解决一些HTTP协议的固有缺陷和问题,比如它包括下面的一些主要特性:

  • 多路复用:允许客户端和服务器同时发送多个请求和响应,而无需等待前一个请求/响应完成,可以显著提高网络吞吐量。
  • 头部压缩:使用HPACK算法来压缩HTTP头部,减少需要传输的数据量
  • 流量控制:使用流量控制来防止客户端/服务器的无效数据发送,避免和减缓网络拥塞
  • 优先级:允许客户端指定请求优先级,提供业务灵活性
  • 双向通信:允许客户端/服务器进行双向通信,能够提供业务灵活性和简化应用开发

nodejs有专门的http2模块,可以用于处理客户端、服务端和session等方面的问题。由于在现阶段,这个特性的使用并不是特别主流和广泛,我们只做一些概念性的探讨,不会过于深入。

HTTP2客户端

下面是一个使用HTTP2协议客户端发起请求的示例代码:

js 复制代码
const http2 = require('node:http2');
const fs = require('node:fs');
const client = http2.connect('https://localhost:8443', {
  ca: fs.readFileSync('localhost-cert.pem'),
});
client.on('error', (err) => console.error(err));

const req = client.request({ ':path': '/' });

// 以下内容基本同标准HTTP请求
...

可以看到,主要修改的地方,就是相对明确的提出了一个"连接后请求"的概念,并且强化了对HTTPS协议的要求。另外,这种两阶段请求的方式,应该可以在后续,在同一个域中重复发起请求,并且可以共享已经建立的连接,这样可以提高网络请求的性能。

HTTP2 Session

和传统的HTTP类不同的是,HTTP2的类有一个新的Session子类用于处理HTTP2 Session相关的功能。这也体现出HTTP2协议的一个很重要的特性,就是面向连接提供了很多扩展的特性,例如可以承载多个stream来进行数据的请求和响应处理。笔者认为,也可以简单的将session就理解成一个连接,在HTTP2中的连接是可以复用的。

Session继承自EventEmitter,所以它的所有方法都是事件回调方法。下面的代码,应该能够帮助我们理解其在服务端和客户端的一些应用方式:

js 复制代码
const http2 = require('node:http2');

// 服务端
const server = http2.createServer();
const expectedWindowSize = 2 ** 20;
server.on('connect', (session) => {
  // Set local window size to be 2 ** 20
  session.setLocalWindowSize(expectedWindowSize);
}); 

// 客户端
const client = http2.connect('https://example.org');

client.on('altsvc', (alt, origin, streamId) => {
  console.log(alt);
  console.log(origin);
  console.log(streamId);
}); 

在比较简单的应用场景中,涉及到session的使用并不多,而是直接使用HTTP2Stream,比较直观和简单。

HTTP2 Stream

在HTTP/2协议中,流是 HTTP/2 的核心概念,HTTP/2中的连接可以包含多个流,每个流对应一个HTTP请求或响应,是数据传输的基本单位。基于这个新的理念和设计,nodejs提供了HTTP2Stream类,和相关一系列属性和方法,来帮助操作这个数据流。在HTTP/2对象中,请求和响应对象,都是某种流的实例。

我们来看一段简单的代码:

js 复制代码
    const http2 = require('node:http2');
    const server = http2.createServer();
    server.on('stream', (stream) => {
      stream.respond({ ':status': 200 });
      stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => {
        if (err) throw err;
        pushStream.respond({ ':status': 200 });
        pushStream.end('some pushed data');
      });
      stream.end('some data');
    }); 

这段代码揭示了一些stream的特性和信息:

  • stream是在server的onStream事件中创建和获取的
  • HTTP2支持push数据,并使用pushStream实现
  • stream的一般用法,其实是和http的response是相同的

小结

本文的主要讨论内容是nodejs作为一个主要用于Web应用开发的技术平台,提供的HTTP协议支持的相关技术和模块。这是nodejs的一个核心特性,每一个Web应用开发者,都应该比较好的理解和掌握这一部分的知识和技能。

相关推荐
萌萌哒草头将军3 小时前
🚀🚀🚀React Router 现在支持 SRC 了!!!
javascript·react.js·preact
@ chen5 小时前
Spring Boot 解决跨域问题
java·spring boot·后端
转转技术团队6 小时前
转转上门隐私号系统的演进
java·后端
【本人】6 小时前
Django基础(二)———URL与映射
后端·python·django
Humbunklung7 小时前
Rust 模块系统:控制作用域与私有性
开发语言·后端·rust
WanderInk7 小时前
依赖对齐不再“失联”:破解 feign/BaseBuilder 错误实战
java·后端·架构
Adolf_19938 小时前
React 中 props 的最常用用法精选+useContext
前端·javascript·react.js
前端小趴菜058 小时前
react - 根据路由生成菜单
前端·javascript·react.js
喝拿铁写前端8 小时前
`reduce` 究竟要不要用?到底什么时候才“值得”用?
前端·javascript·面试
空の鱼8 小时前
js与vue基础学习
javascript·vue.js·学习