http http  模块用于创建HTTP服务器或客户端。
createServer()方法创建一个HTTP服务器,该方法返回http.Server实例。http.request()和http.get()用于创建HTTP客户端,返回http.ClientRequest实例。
创建HTTP服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const  server = http.createServer ()server.on ('request' , (req, res ) =>  {   if (req.method  === 'GET' ){     res.write ('GET:' )     res.end ('hello world' )   }else  if (req.method  === 'POST' ){     req.on ('data' , (chunk ) =>  {       console .log (chunk.toString ());     })     res.write ('POST:' )     res.end ('hello world' )   } }) server.listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }); 
 
创建HTTP客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 http.get ('http://localhost:8888' , (res ) =>  {   let  data = '' ;   res.on ('data' , (chunk ) =>  {     data += chunk;   })   res.on ('end' , () =>  {     console .log (data.toString ());      console .log ('已获取完毕' );   }) }) const  req = http.request ({  host : 'localhost' ,   port : 8888 ,   path : '/' ,   method : 'POST' ,   headers : {     'Content-Type' : 'application/json'    } }, (res ) =>  {   let  data = '' ;   res.on ('data' , (chunk ) =>  {     data += chunk;   })   res.on ('end' , () =>  {     console .log (data.toString ());      console .log ('已获取完毕' );   }) }) req.write (JSON .stringify ({"name" : "tom" })); req.end ();  
 
更多基本用法,之前已经记录过了:初识NodeJS—http模块 
反向代理 直接访问 :浏览器直接请求最终提供资源的服务器,中间没有经过任何的 http 代理服务器。正向代理(forward proxy) :代理的对象是客户端,代理服务器代替客户端(浏览器)去访问目标服务器,获取资源返回,并不直接面向最终提供资源的服务器。代理服务器可以对外隐藏客户端细节,代理服务器会对外屏蔽掉客户端信息。通常需要客户端主动配置代理服务器的信息。反向代理(reverse proxy) :代理的对象是服务端,客户端(浏览器)不需要任何配置。反向代理服务器代替服务端(服务器)接收请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时反向代理服务器和普通的服务器对外就是一个服务器,客户端不需要知道内部网络的信息。也称为透明代理 。
反向代理作用: 
保证内网的安全: 防止外部网络直接访问内网 
负载均衡: 将请求分发到多个后端服务器上,避免某个服务器过载,提高整体性能和可用性 
缓存和性能优化: 将请求的结果和静态资源缓存起来,减少对服务器的请求,还可以资源进行压缩、合并、优化等操作 
高可用: 当某个服务器出现故障时,可以自动将请求转发到其他服务器上 
域名和路径重写: 将请求的域名和路径重写,转发到其他服务器上,提高了灵活性和可维护性 
 
通过http模块可以直接实现反代,但通常用第三方库,例如:http-proxy-middleware 
先写好一个简单的后端服务器:
server.js 1 2 3 4 5 http.createServer ((req, res ) =>  {   res.end ('101010101' ) }).listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }) 
 
写好反代规则:
config.js 1 2 3 4 5 6 7 8 9 10 11 module .exports  = {  server : {     proxy : {              '/api' : {         target : 'http://localhost:8888' ,          changeOrigin : true ,        }     }   } } 
 
再配置反向代理服务器:
proxy.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const  http = require ('http' )const  { createProxyMiddleware } = require ('http-proxy-middleware' )const  url = require ('url' )const  config = require ('./config' )const  proxyList = config.server .proxy http.createServer ((req, res ) =>  {   const  pathname = url.parse (req.url ).pathname ;      if  (Object .keys (proxyList).includes (pathname)) {          const  proxy = createProxyMiddleware (config.server .proxy [pathname]);     proxy (req, res);     return ;   }   res.end ('hello world' ) }).listen (80 , () =>  {   console .log ('Server is running on http://localhost:80' ); }) 
 
结果:
1 2 3 [HPM] Proxy created: /  -> http://localhost:8888 请求 http://localhost/ 返回 hello world 请求 http://localhost/api 返回 101010101 
 
动静分离 动静分离是指将动态资源和静态资源分开处理和分发,以提高服务器的性能和安全性。
动态资源: 指每次请求时动态生成的资源,例如:API接口、动态页面等。静态资源: 指长时间不会变化的资源,例如:html、css、js、图片、视频、音频等。
静态资源通常使用GET请求获取,mime 模块可以根据文件后缀名获取MIME类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import  http from  'http' ;import  fs from  'fs' ;import  url from  'url' ;import  path from  'path' ;import  mime from  'mime' ;const  __filename = url.fileURLToPath (import .meta .url )const  __dirname = path.dirname (__filename)http.createServer ((req, res ) =>  {   const  { pathname } = url.parse (req.url , true );   if  (req.method  === 'GET' ) {          if  (req.url .startsWith ("/public" )) {       const  filePath = path.normalize (__dirname + pathname);       console .log (filePath)       if  (!fs.existsSync (filePath)) {         res.statusCode  = 404 ;         res.end ('404' );         return ;       }       res.writeHead (200 , {                  'Content-Type' : mime.getType (pathname)       })       return  fs.createReadStream (filePath).pipe (res);            } else  {       return  res.end ('GET:hello world' )     }   } else  if  (req.method  === 'POST' ) {     return  res.end ('POST:hello world' )   }   res.statusCode  = 404 ;   return  res.end ('404' ); }).listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }) 
 
常见MIME类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 -   文本文件:   -   text/plain:纯文本文件   -   text/html:HTML 文件   -   text/css:CSS 样式表文件   -   text/javascript:JavaScript 文件   -   application/json:JSON 数据 -   图像文件:   -   image/jpeg:JPEG 图像   -   image/png:PNG 图像   -   image/gif:GIF 图像   -   image/svg+xml:SVG 图像 -   音频文件:   -   audio/mpeg:MPEG 音频   -   audio/wav:WAV 音频   -   audio/midi:MIDI 音频 -   视频文件:   -   video/mp4:MP4 视频   -   video/mpeg:MPEG 视频   -   video/quicktime:QuickTime 视频 -   应用程序文件:   -   application/pdf:PDF 文件   -   application/zip:ZIP 压缩文件   -   application/x-www-form-urlencoded:表单提交数据   -   multipart/form-data:多部分表单数据 
 
邮件服务 nodemailer 是一个简单易用的NodeJS邮件发送库。文档 
QQ邮件服务文档 ,需开启POP3/SMTP,并生成授权码。
createTransport() 创建邮件传输器对象,transport.sendMail() 发送邮件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import  nodemailer from  'nodemailer' import  http from  'http' import  url from  'url' import  email from  './email.json'  assert { type : "json"  }const  transport = nodemailer.createTransport ({  service : "qq" ,    port : 587 ,    host : 'smtp.qq.com' ,    secure : true ,       auth : {     user : email.user ,      pass : email.pass     } }) http.createServer ((req, res ) =>  {   const  { pathname } = url.parse (req.url , true );   if  (req.method  === 'POST'  && pathname === '/email' ) {     let  data = '' ;     req.on ('data' , (chunk ) =>  {       data += chunk.toString ();     })     req.on ('end' , () =>  {       const  { to, subject, text } = JSON .parse (data);       transport.sendMail ({         from : email.user ,         to,         subject,         text       }).then (() =>  {         res.end ('发送成功' )       }).catch ((err ) =>  {         res.end ('发送失败' )       })     })     res.end ('hello world' )   } else  {     res.statusCode  = 404 ;     res.end ('404' );   } }).listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }) 
 
express 
使用http模块写服务端太麻烦了,所以有了express等 web 应用框架。
 
express 是一个最小且灵活的 Node.js Web 应用程序框架,提供了路由、中间件等功能,支持模板引擎。
基本用法之前已经记录过了:Express框架 
快速回顾 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 import  express from  'express' import  path from  'path' import  url from  'url' const  __filename = url.fileURLToPath (import .meta .url )const  __dirname = path.dirname (__filename)const  app = express ()app.get ('/txt' , (req, res ) =>  {      console .log (req.method );   console .log (req.url );   console .log (req.httpVersion );   console .log (req.headers );      console .log (req.path );   console .log (req.query );   console .log (req.ip );   console .log (req.get ('host' ));      res.send ('hello get' ) }) app.get ('/user/:id' , (req, res ) =>  {   console .log (req.params .id )   res.send (req.params .id ) }) app.use (express.static (path.resolve (__dirname, 'public' ))) app.use (express.json ())  app.use (express.urlencoded ({ extended : true  }))  app.post ('/login' , (req, res ) =>  {   console .log (req.body )   res.send ('hello post' ) }) const  logger  = (req, res, next ) => {  console .log ('logger' )   next () } const  router = express.Router ();router.get ('/list' , logger, (req, res ) =>  {      res.json ({     code : 0 ,     msg : 'ok' ,     data : [1 , 2 , 3 ]   }) }) app.use ('/api' , router) app.listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }) 
 
log4js node服务端常用的日志收集工具:winston 、log4js 、bunyan 、npmlog 等
log4js  较为灵活、配置多样但轻量,可以将日志输出到文件、控制台、邮件等。文档 
log4js.configure() 配置日志记录器、分类,log4js.getLogger() 获取指定分类的日志记录器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import  express from  'express' import  log4js from  'log4js' log4js.configure ({   appenders : {          out : {       type : "stdout" ,       layout : {         type : "colored"        }     },          file : {       type : "file" ,              filename : "server.log"      }   },      categories : {          default : {              appenders : ["out" , "file" ],              level : "debug"      }   }, }); const  logger = log4js.getLogger ("default" )const  app = express ()const  LoggerMiddleware  = (req, res, next ) => {  logger.debug (`[${req.method} ] ${req.url} ` )      next () } app.use (LoggerMiddleware ) app.get ('*' , (req, res ) =>  {   res.send ('hello world' ) }) app.listen (8888 , () =>  {   console .log ('Server is running on http://localhost:8888' ); }) 
 
日志分级Level 日志分级可以将不同级别的日志在控制台中采用不同的颜色,也可以帮助在生产时有选择地生成不同级别的日志。
log4js默认的日志分级有9级,也可以自定义级别。
默认日志级别,value为优先级,从低到高 1 2 3 4 5 6 7 8 9 10 11 Level .addLevels ({  ALL : { value : Number .MIN_VALUE , colour : 'grey'  },   TRACE : { value : 5000 , colour : 'blue'  },   DEBUG : { value : 10000 , colour : 'cyan'  },   INFO : { value : 20000 , colour : 'green'  },   WARN : { value : 30000 , colour : 'yellow'  },   ERROR : { value : 40000 , colour : 'red'  },   FATAL : { value : 50000 , colour : 'magenta'  },   MARK : { value : 9007199254740992 , colour : 'grey'  },    OFF : { value : Number .MAX_VALUE , colour : 'grey'  }, }); 
 
在categories.default.level中配置日志等级,只会输出级别相等或更高级别的日志。
ALL OFF 这两个等级并不会直接在业务代码中使用。其它级别分别对应 Logger 实例的七个方法。在调用这些方法的时候,就相当于为这些日志定了级。
logger.trace() 详细信息 
logger.debug() 调试信息 
logger.info() 一般信息 
logger.warn() 警告信息 
logger.error() 错误信息 
logger.fatal() 致命信息 
logger.mark() 标记信息 
 
自定义级别:  levels属性用于定义自定义日志级别,或重新定义现有日志级别
1 2 3 4 5 6 7 8 log4js.configure ({   levels : {     custom : {       value : 10000 ,        colour : "blue" ,      }   }, }); 
 
日志分类Category 除了日志分级,还可以对日志进行分类Category ,两个维度对日志进行区分。
log4js.configure(config) 传入配置对象,用于配置日志记录器、分类、输出方式等。config下有两个属性:
appenders :用于配置日志输出方式,可以配置多个输出方式,例如:控制台、日志文件、邮件等。 
categories :用于配置日志分类,每个分类可以设置多个appender,也可以配置日志等级。 
 
log4js.getLogger(category)获取指定分类的日志记录器。
若不传入,则会使用default分类配置。 
若传入不存在的分类名,也使用default,但输出时仍然是传入的分类名。 
 
配置分类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 log4js.configure ({   appenders : {     console : {       type : "console" ,     },     out : {       type : "stdout" ,     },     err : {       type : "stderr" ,     },   },   categories : {          default : {              appenders : ["console" ],                     level : "debug" ,      },     access : {       appenders : ["out" ],       level : "info" ,     },     error : {       appenders : ["err" ],       level : "error" ,              enableCallStack : true ,     },   } }) 
 
使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const  defaultLogger = log4js.getLogger () defaultLogger.debug ("Time:" , new  Date ()); const  otherLogger = log4js.getLogger ("other" )otherLogger.debug ("Time:" , new  Date ()); const  accessLogger = log4js.getLogger ("access" )accessLogger.debug ("Time:" , new  Date ());  accessLogger.info ("Time:" , new  Date ()); const  errorLogger = log4js.getLogger ("error" )errorLogger.error ("Time:" , new  Error ("error" )); 
 
日志落盘Appender Appender 用于配置日志输出方式
默认Appenders: 若不进行任何配置,日志将输出到标准输出,但也定义了默认日志级别OFF,这意味着实际上不会输出任何日志。
自定义Appenders: 传入一个对象,key为Appender名,value为Appender配置。
常用的Appender.type:
console:通过V8的console输出到控制台 
stdout:通过标准输出到控制台 
stderr:通过标准错误输出到控制台 
file:输出为指定日志文件 
dateFile:按照特定日期滚动生成多个日志文件 
multiFile 动态配置不同category下或者不同变量控制下,落盘到不同文件 
smtp 将日志输出到邮件 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 log4js.configure ({      appenders : {     out : {               type : "stdout" ,                      layout : {         type : "colored"                                             }     },     file : {              type : "file" ,              filename : "server.log" ,              maxLogSize : 1024  * 1024  * 10 ,               backups : 3 ,                      encoding : "utf-8" ,               compress : true ,                      keepFileExt : true ,               fileNameSep : "-" ,             },     date : {       type : "dateFile" ,       filename : "app.log" ,              pattern : "yyyy-MM-dd" ,             },     multi : {       type : "multiFile" ,        base : "logs/" ,               property : "categoryName" ,        extension : ".log" ,               timeout : 0 ,            },     error : {              type : "logLevelFilter" ,              appender : "file" ,                     level : "error" ,     },   },      categories : {          default : {              appenders : ["out" , "error" ],              level : "debug"      },          date : {       appenders : ["out" , "date" ],       level : "debug"      },          multi : {       appenders : ["out" , "multi" ],       level : "debug"      },   }, }); const  defaultLogger = log4js.getLogger ()const  dateLogger = log4js.getLogger ('date' )const  multiLogger = log4js.getLogger ('multi' )defaultLogger.debug ('Time:' , new  Date ())  defaultLogger.error ('Time:' , new  Error ('error' ))  dateLogger.debug ('Time:' , new  Date ())  multiLogger.debug ('Time:' , new  Date ())  
 
对于日志滚动和日期日志,先创建一个原名日志文件,当发生滚动或日期变更时,重命名该文件为原名+序号 或原名+日期 ,然后创建一个新的原名日志文件继续写入,依次循环。
其它接口 log4js.isConfigured() 返回boolean,表示log4js.configure()先前是否成功调用(getLogger()也会隐式调用configure())
1 2 3 console .log (log4js.isConfigured ()) log4js.getLogger ()  console .log (log4js.isConfigured ()) 
 
log4js.shutdown(cb) 接受一个回调,当 log4js 关闭所有附加程序并完成写入日志事件时将调用该回调。当程序退出时使用此选项,以确保所有日志都写入文件、套接字已关闭等。
1 2 3 log4js.shutdown (() =>  {   console .log ('log4js shutdown' ); }) 
 
log4js.addLayout(type, fn) 添加自定义布局,type为布局名,fn为布局处理函数。详见 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 log4js.addLayout ("json" , function  (config ) {   return  function  (logEvent ) {     return  JSON .stringify (logEvent) + config.separator ;   }; }); log4js.configure ({   appenders : {     out : { type : "stdout" , layout : { type : "json" , separator : ","  } },   },   categories : {     default : { appenders : ["out" ], level : "info"  },   }, }); const  logger = log4js.getLogger ("json-test" );logger.info ("this is just a test" ); 
 
HTTP标头 HTTP 标头 (header)允许客户端和服务器通过 HTTP 请求(request)或者响应(response)传递附加信息 
HTTP 标头由它的名称(不区分大小写)后跟随一个冒号(:),冒号后跟随它具体的值。该值之前的空格会被忽略。
根据不同的消息上下文,标头可以分为:
请求标头 包含有关要获取的资源或客户端或请求资源的客户端的更多信息。 
响应标头 包含有关响应的额外信息,例如响应的位置或者提供响应的服务器。 
表示标头 包含资源主体的信息,例如主体的 MIME 类型或者应用的编码/压缩方案。 
有效负荷标头 包含有关有效载荷数据表示的单独信息,包括内容长度和用于传输的编码。 
 
请求表头 HTTP请求头(HTTP request headers)是在HTTP请求中发送的元数据信息,用于描述请求的特性、内容和行为。
常见请求头: 
Accept :客户端能够处理的媒体类型,值:text/html、application/json等 
Accept-Encoding :客户端能够处理的编码方式,值:gzip、deflate等 
Accept-Language :客户端能够处理的语言,值:zh-CN、en-US等 
Connection :连接方式,值:keep-alive、close等 
Content-Type :请求体的MIME类型,值:text/html、application/json等 
Content-Length  请求体的长度,单位字节 
Host :请求的主机名,值:www.baidu.com、localhost:8888等 
Referer :请求来源,值:https://www.baidu.com、http://localhost:8888等 
User-Agent :客户端信息,值:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36等 
Cache-Control  缓存控制,值:no-cache、max-age=3600等 
Cookie  请求携带的cookie,值:name=tom;等 
Range  用于断点续传,值:bytes=0-1023等,指定第一个字节的位置和最后一个字节的位置 
Sec-Ch-Ua  浏览器的 UA 字符串,值:"Google Chrome";v="120"等 
Sec-Ch-Ua-Mobile  浏览器是否为移动端,值:?0、?1等 
Sec-Fetch-Dest  请求的目标资源类型,值:document、image等 
Sec-Fetch-Mode  请求的模式,值:navigate、cors等 
Sec-Fetch-Site  请求的站点类型,值:same-origin、cross-site等 
Sec-Ch-Ua-Platform  浏览器的平台,值:"Windows"、"macOS"等 
 
响应标头 HTTP响应头(HTTP response headers)是在HTTP响应中发送的元数据信息,用于描述响应的特性、内容和行为。
常见响应头: 
Content-Type :响应体的MIME类型,值:text/html、application/json等 
Content-Length :响应体的长度,单位字节 
Content-Encoding :响应体的编码方式,值:gzip、deflate等 
Content-Language :响应体的语言,值:zh-CN、en-US等 
Content-Disposition :响应体的附加信息,值:attachment; filename="filename.jpg",表示附件,浏览器会提示下载 
Location :重定向的地址,值:https://www.baidu.com 
Set-Cookie :设置cookie,值:name=tom;等 
Cache-Control :缓存控制,值:no-cache、max-age=3600等 
Expires :缓存过期时间,值:Wed, 21 Oct 2024 07:28:00 GMT 
Last-Modified :资源最后修改时间,值:Wed, 21 Oct 2024 07:28:00 GMT 
Etag :资源的唯一标识,例如:"5d8b9b4e-2a",用于缓存验证 
Access-Control-Allow-Origin :允许跨域的域名,值:*、http://localhost:8080等 
Access-Control-Allow-Methods :允许跨域的请求方法,值:GET、POST等 
Access-Control-Allow-Headers :允许跨域的请求头 ,值:Content-Type、Authorization等 
Access-Control-Allow-Credentials :是否允许跨域携带cookie,值:true、false等 
Access-Control-Max-Age :预检请求的有效期,单位秒,值:3600等 
Access-Control-Expose-Headers :允许跨域的响应头 ,值:Content-Type、Authorization等,若请求没有携带凭据(Cookie或认证信息),*才会被当作一个特殊的通配符,否则会被简单地当作标头名称。默认情况下,仅暴露CORS安全列表的响应标头  
Strict-Transport-Security :强制使用HTTPS,值:max-age=31536000; includeSubDomains,表示一年内所有子域名都必须使用HTTPS 
X-Frame-Options :防止网页被嵌入到iframe中,值:DENY、SAMEORIGIN等 
X-XSS-Protection :防止XSS攻击,值:1; mode=block、0等 
X-Content-Type-Options :防止MIME类型被修改,值:nosniff等 
Referrer-Policy :控制referer的发送,值:no-referrer、strict-origin-when-cross-origin等 
Content-Security-Policy :控制资源加载,值:default-src 'self'、script-src 'self'等 
X-Powered-By :服务器信息,值:Express、PHP/7.4.3等 
Server :服务器信息,值:nginx/1.18.0、Apache/2.4.46等 
Date :响应时间,值:Wed, 21 Oct 2024 07:28:00 GMT 
Connection :连接方式,值:keep-alive、close等 
Transfer-Encoding :传输编码,值:chunked、gzip等 
Vary :缓存策略,值:Accept-Encoding、User-Agent等 
 
CORS 跨域资源共享(Cross-Origin Resource Sharing,CORS)用于在浏览器 中实现跨域请求访问资源的权限控制。
当一个网页发起跨域请求时,浏览器会根据同源策略(Same-Origin Policy)进行限制。同源策略要求请求的源(协议、域名和端口 )必须与资源的源相同,否则请求会被浏览器拒绝。
AJAX请求相关-跨域 
express设置CORS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const  allowedOrigins = ["127.0.0.1:5500" , undefined ];app.use ((req, res, next ) =>  {   const  origin = req.headers .origin ;      if  (     (origin && allowedOrigins.some ((item ) =>  origin.includes (item))) ||     allowedOrigins.includes (origin)   ) {          res.header ('Access-Control-Allow-Origin' , origin);          res.header ('Access-Control-Allow-Headers' , "*, Origin, X-Requested-With, Content-Type, Accept, Authorization" );          res.header ('Access-Control-Allow-Methods' , 'PUT, POST, GET, DELETE, PATCH ,OPTIONS' );               res.header ("Access-Control-Allow-Credentials" , true );     if  (req.method .toLowerCase () == 'options' ) {       res.send (200 );      }     else  {       next ();     }   } else  {     res.status (403 ).send ('Forbidden' );   } }) 
 
跨域使用Cookie 
服务端 将Access-Control-Allow-Credentials设为 true  
客户端 请求时,设置XMLHttpRequest.withCredentials为 true  或 Fetch API 的 credentials 选项为 include  
 
1 2 3 4 5 fetch ('http://127.0.0.1:8888/api' , {  method : 'GET' ,   credentials : 'include' ,  }).then (res  =>  res.text ())   .then (data  =>  console .log (data)) 
 
注意: 此时服务端不能设置Access-Control-Allow-Origin为*,必须指定具体的域名,否则会报CORS错误
1 2 index.html:1 Access to fetch at 'http://127.0.0.1:8888/api' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy:  The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 
 
Cookie属性: 
1 2 3 4 5 6 7 8 9 10 11 12 13 1. 名称:Cookie的name。 2. 值:Cookie的value。 3. Domain: Cookie的域。如果设成xxx.com(一级域名),那么子域名x.xxx.com(二级域名),都可以使用xxx.com的Cookie。 4. Path:Cookie的路径。如果设为/,则同域名全部路径均可使用该Cookie。如果设为/xxx/,则只有路径为/xxx/可以使用该Cookie。 5. Expires / Max-Age:Cookie的超时时间。如果值为时间,则在到达指定时间后Cookie失效。如果值为Session(会话),Cookie会同Session一起失效,当整个浏览器关闭的时候Cookie失效。 6. Size:Cookie的大小。 7. HttpOnly:值为true时,Cookie只会在Http请求头中存在,不能通过doucment.cookie(JavaScript)访问Cookie。 8. Secure:值为true时,只能通过https来传输Cookie。 9. SameSite:   值为Strict,完全禁止第三方Cookie,跨站时无法使用Cookie。   值为Lax,允许在跨站时使用Get请求携带Cookie,下面有一个表格介绍Lax的Cookie使用情况。   值为None,允许跨站跨域使用Cookie,前提是将Secure属性设置为true。 10. Priority :Cookie的优先级。值为Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。 
 
一些额外的情况,可能还需要进行如下操作,才能跨域使用Cookie: 将Cookie的SameSite值设为None,Secure值改为true,并且使用https。
自定义响应头 默认情况下,仅暴露CORS安全列表的响应标头 
其它响应头包括自定义的,需要使用 Access-Control-Expose-Headers 配置。
只有当请求没有携带凭据(Cookie或认证信息),*才会被当作一个特殊的通配符,否则会被简单地当作标头名称。
预检请求 预检请求 (preflight request)是浏览器在发送真正的请求之前,先发送一个OPTIONS请求,用于检查服务器是否支持 CORS 即跨域资源共享。
存在以下情况时,浏览器会发送预检请求:
使用了自定义的请求头(非简单请求) 
使用了非简单请求方法(PUT、DELETE等) 
Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain之一 
 
Access-Control-Allow-Headers 如下设置允许自定义header、允许Content-Type为非默认值等:"*, Origin, X-Requested-With, Content-Type, Accept, Authorization"
SSE单工通信 Server-Sent Events (SSE)服务器发送事件 ,也称为事件流 ,用于服务器向客户端单向实时、持续地发送消息。属于单工通信。可以使用自定义事件(Custom Events)来发送具有特定类型的事件数据。
SSE 基于 HTTP  协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的单向实时数据推送。
Server-Sent Events 教程-阮一峰 
使用SSE需要设置响应头: 
1 2 3 res.set ('Content-Type' , 'text/event-stream' ); res.set ('Cache-Control' , 'no-cache' ); res.set ('Connection' , 'keep-alive' ); 
 
一个后端示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 app.get ('/sse' , (req, res ) =>  {      res.set ('Content-Type' , 'text/event-stream' );   res.set ('Cache-Control' , 'no-cache' );   res.set ('Connection' , 'keep-alive' );   let  i = 1 ;      setInterval (() =>  {          res.write ('event: test\n' );           res.write (`id: ${i++} \n` );           res.write ('retry: 3000\n' );           res.write (`data: ${JSON .stringify({         time: new  Date ().toLocaleString(),     })} \n\n` );   }, 1000 ); }); 
 
res.write发送消息:  每一次发送的信息,由若干个message 组成,每个message之间用\n\n分隔。 每个message内部由若干行 组成,每行格式:[field]: value\n,每行之间用\n分隔。
[field]可以取四个值:
event :事件类型名,默认为 message。 
id :事件的ID,也可以是每一条数据的编号。 
retry :出错的重连时间间隔,单位毫秒。 
data :事件的数据,必须是字符串,如果数据有多行,使用\n分隔。 
 
此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。 如 : This is a comment\n
id的作用:  每个message都应该有一个独特的id,浏览器用lastEventId属性读取id的值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
EventSource 前端使用EventSource(url, options)创建SSE对象,监听对应事件,获得服务端响应的数据。
本质是发送了一次GET请求,但是不会关闭连接,而是保持连接,等待服务端推送数据。
options 配置项: 
withCredentials Boolean,是否允许发送 Cookie 和 HTTP 认证信息。默认为 false。 
headers Object,要发送的请求头信息。 
retryInterval Number,与服务器失去连接后,重新连接的时间间隔。默认为 1000 毫秒。 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const  sse = new  EventSource ('http://localhost:8888/sse' , {  withCredentials : true   }); console .log ('连接中' , sse.readyState ); sse.addEventListener ('open' , () =>  {   console .log ('连接已建立' , sse.readyState );  }); sse.addEventListener ('test' , (e ) =>  {   console .log (e.type );    console .log (e.lastEventId );    console .log (JSON .parse (e.data ));     }); setTimeout (() =>  {  sse.close ();    console .log ('连接已关闭' , sse.readyState );  }, 4000 ); 
 
EventSource.readyState,表明连接的当前状态。
0 :相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。 
1 :相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。 
2 :相当于常量EventSource.CLOSED,表示连接已断,且不会重连。 
 
事件: 
open EventSource 对象已经和服务器建立了连接,并开始接收来自服务器的数据,此时 readyState 为 1。 
error 在建立连接或接收服务器数据时发生了错误。 
message 服务器发送了一个没有指定事件名的消息,此时 event.type 为 message。 
自定义事件名:服务器发送了一个指定事件名的消息,此时 event.type 为自定义事件名。 
 
WebSocket双工通信 WebSocket  是建立在单个 TCP  连接上的全双工通信协议 ,允许客户端和服务器之间进行实时双向通信。
WebSocket 教程-阮一峰 你不知道的 WebSocket-阿宝哥 
特点: 
建立在 TCP 协议之上,服务器端的实现比较容易。 
与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。 
数据格式比较轻量,性能开销小,通信高效。 
有状态的协议,之后通信时可以省略部分状态信息 
可以发送文本,也可以发送二进制数据。 
没有同源限制,客户端可以与任意服务器通信。 
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。 
 
客户端 WebSocket  对象用于创建 WebSocket 连接,通过该连接可以发送和接收数据。
1 2 3 4 const  ws = new  WebSocket ('ws://localhost:8080' );ws.onopen  = () =>  {   console .log ('Connected' ); }; 
 
readyState 属性
0 :相当于常量WebSocket.CONNECTING,表示连接还未建立,或者断线正在重连。 
1 :相当于常量WebSocket.OPEN,表示连接已经建立,可以通讯。 
2 :相当于常量WebSocket.CLOSING,表示连接正在关闭。 
3 :相当于常量WebSocket.CLOSED,表示连接已断,且不会重连。 
 
事件: 
open WebSocket 连接已建立。 
message 接收到服务器发送的数据。 
error 连接出错。 
close 连接关闭。 
 
方法: 
send(data) 向服务器发送数据。 
close() 关闭连接。 
 
请求和响应头 1、请求头: 
Upgrade  指定为websocket,表示要升级协议为 WebSocket(HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议) 
Connection  指定为Upgrade,表示要升级连接。 
Sec-WebSocket-Key  Base64编码的16字节随机字符串,用于验证服务器是否正确处理了握手请求。 
Sec-WebSocket-Version  WebSocket协议的版本号,当前为13。 
Sec-WebSocket-Extensions  指定扩展,如压缩等。 
 
1 2 3 4 5 6 7 8 9 10 11 12 GET ws://localhost:8080/ HTTP/1.1 Host: localhost:8080 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache Upgrade: websocket Origin: http://127.0.0.1:5500 Accept-Encoding: gzip, deflate, br, zstd Accept-Language: zh-CN,zh;q=0.9 Sec-WebSocket-Version: 13 Sec-WebSocket-Key: iXGVJiUgpjqmAIMXKj592w== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 
 
2、响应头: 
101 Switching Protocols  101 状态码,表示协议切换成功。 
Upgrade  指定为websocket,表示要升级协议为 WebSocket。 
Connection  指定为Upgrade,表示要升级连接。 
Sec-WebSocket-Accept  通过ws协议指定算法处理后的Sec-WebSocket-Key,表示ws已握手成功,后续消息收发使用ws协议。 
 
1 2 3 4 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: Ifs+JScoZK0aP3knDahEYlTi9Eg= 
 
服务端 ws 模块 是 Node.js 的 WebSocket 模块,用于创建 WebSocket 服务器。
1 2 pnpm i ws pnpm i @types/ws -D 
 
使用该模块,省去了手动处理 WebSocket 握手、消息解析等繁琐的工作,抽象出了各种事件,方便使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import  webSocket from  'ws' ; const  ws = new  webSocket.Server ({   port : 8080   }, () =>  {   console .log ('ws服务器启动成功' ); }); ws.on ('connection' , (client ) =>  {   console .log ('有客户端连接' );      client.on ('message' , (msg ) =>  {     console .log ('有消息' , msg);          client.send ('服务器收到了消息' );   }); }); 
 
webSocket服务事件: 
connection 客户端连接事件,参数为客户端对象。 
close 客户端关闭事件。 
error 服务器出错事件。 
headers 客户端请求头事件,参数为请求头对象。 
listening 服务器监听事件。 
 
client 客户端事件: 
message 客户端消息事件,参数为消息内容。 
close 客户端关闭事件。 
error 客户端出错事件。 
open 客户端打开事件。 
ping 客户端ping事件。 
pong 客户端pong事件。 
unexpected-response 客户端响应事件。 
upgrade 客户端协议升级事件。 
 
简易公共聊天室 后端:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import  webSocket from  'ws' ; const  ws = new  webSocket.Server ({   port : 8080   }, () =>  {   console .log ('ws服务器启动成功' ); }); ws.on ('connection' , (client ) =>  {   console .log ('新客户端连接' );      client.on ('message' , (msg ) =>  {     console .log ('新消息:' , msg.toString ());          ws.clients .forEach ((client ) =>  {       client.send (msg.toString ());     });   });      client.on ('close' , () =>  {     console .log ('有客户端断开连接' );   }); }); 
 
前端:
1 2 3 4 5 6 7 8 <body >   <div >      <ul  id ="list" > </ul >      <input  type ="text"  id ="input" >      <button  id ="send" > 发送</button >    </div >    <script  src ="index.js" > </script >  </body > 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const  ws = new  WebSocket ('ws://localhost:8080' );const  list = document .getElementById ('list' );const  input = document .getElementById ('input' );const  send = document .getElementById ('send' );send.addEventListener ('click' , () =>  {   if  (!input.value ) {     return ;   }   ws.send (input.value );   input.value  = '' ; }); ws.onmessage  = (event ) =>  {   const  li = document .createElement ('li' );   li.textContent  = event.data ;   list.appendChild (li); }; 
 
当 socket 连接长时间不使用、发生网络波动、弱网环境可能导致连接断开,通常还需要实现心跳检测,以保持连接、检测连接断开后重连。
在服务端或客户端实现均可,没有一个固定的实现方式,通常是在客户端使用定时器发送心跳包。