http & 浏览器
状态码
-
1XX 通知
-
2XX 成功
-
200 操作成功
-
201 服务器创建了新的资源
-
204 资源存在,但不返回内容
-
-
3XX 重定向
-
301 永久重定向 搜索引擎(SEO)会永久删除旧地址
-
302 临时重定向 搜索引擎会使用新地址但是保存旧地址 (不推荐使用)
-
303 查看其它位置 请求返回一个响应文档的URI
-
304 未修改/强缓存 和上次请求相比没有修改内容,类似204 响应主题为空
-
307 临时重定向 类似302,但不允许将原本的POST请求重定向到GET上
-
308 永久重定向 类似301,但不允许将原本的POST请求重定向到GET上
-
-
4XX 客户端错误
- 400 请求错误 通用
- 401 没有授权 访问需要权限的地址但没有授权
- 403 服务器拒绝访问 按时资源存在,但不想处理 (服务器封ID,爬虫太多等)
- 404 资源不存在
- 408 请求超时 服务器等待请求超时
- 429 请求次数太多 请求太过频繁,超出限制
-
5XX 服务端错误
- 500 服务端错误
- 501 服务端不支持此http特性
- 502 网关错误
- 503 服务器无法处理,可能是资源不足(服务器超载/停机维护)
- 504 请求网关超时
HTTP2 和HTTP1.1 有什么区别
- HTTP/2 采用的是二进制格式,比HTTP\1.1 的文本协议解析起来更高效,错误率更低
- HTTP/2 是完全多路服用的,并非有序并阻塞的(线段阻塞)--只需要一个链接即可实现并行
- HTTP/2 使用报头压缩,减小开销
- HTTP/2 服务器可以将响应主动推送到客户端缓存中
HTTP缓存
强缓存
- 浏览器控制:后端把需要的资源发过来,并附带发送过期时间,浏览器每次去缓存看一下时间到没到
- expires:date:响应
expiress字段,后端直接把过期的时间发过来。 - cache-control:max-age:后端发送有效时长,浏览器自己倒计时。
- 优先 cache-control
- expires:date:响应
协商缓存(对比缓存)
- 后端发需要的资源发过来,附带上他的标识,浏览器下次请求附带表示,后端对比表示,如果相同就返回304 表示资源没有变化。
- 通过最后修改时间:last-Modified
- 浏览器第一次请求时,给浏览器一个最后的修改时间(
last-Modified) - 浏览器请求时带着自己存的最后修改时间(
if-Modified-Since) - 后端拿到后和现在的最后修改时间对比,没有变化就返回304,时间一样就按照第一次请求时正常返回。
- 浏览器拿到新的
last-modified并更新自己的If-Modified-Since
- 浏览器第一次请求时,给浏览器一个最后的修改时间(
- 通过标识字符串ETag
- 响应时携带
ETag - 请求时携带
If-None-Match - 服务器对比 新的
ETag和If-None-Match是否相同。
- 响应时携带
- 优先使用 ETag
- 通过最后修改时间:last-Modified
URL输入浏览器后,发生了什么
- 输入地址后:
- DNS解析:浏览器对域名进行解析,将域名地址转化为ip地址。
- 建立TCP连接:请求信息(SYN)、确认信息(SYN/ACK)、握手结束(ACK)
- 浏览器向web服务器发送http请求
- 服务器处理响应、将数据返回给浏览器
- 关闭TCP连接(FIN)(ACK)(FIN)(ACK)
- 浏览器解析资源
浏览器的渲染流程
- 基本概念:
- DOM:浏览器将HTML解析成的树形结构,简称DOM
- CSSOM:浏览器将CSS解析成树形结构,简称CSSOM
- Render Tree:将DOM和CSSOM合并后生成的Render Tree
- Layout:计算出Render Tree每个结点的具体位置
- Paintion:通过显卡,将Layout后的节点内容分别呈现到屏幕上
- 渲染基本流程:
- HTML->DOM:从服务器获取HTML文件,并将HTML文件转化为节点对象,并将节点对象连接起来,形成
DOM(文档对象模型)树 - CSS-CSSOM:浏览器在构建DOM时会遇到连接CSS的
<link>标签,会向服务器请求CSS文件,并将CSS文件转化为节点对象,并将节点对象连接起来形成CSSOM(CSS对象模型)树 - DOM+CSSOM->Render Tree:将DOM和CSSOM进行匹配,并且捕获可见内容(除了display:none之类的不可见内容),形成
Render Tree渲染树。 - Layout:获取渲染树的结构、节点位置和大小,依据盒子模型(每个元素都用一个盒子来表示)这些盒子在页面上进行排列和嵌套。
- Paintion:Layout后浏览器将渲染树以像素的形式渲染到页面
- HTML->DOM:从服务器获取HTML文件,并将HTML文件转化为节点对象,并将节点对象连接起来,形成
- 注意:
- DOM可以进行部分解析,而CSSOM不能进行部分解析
- 浏览器先获取JS文件还是CSS文件需要根据实际情况判断,但是浏览器会先构建CSSOM
- CSSOM的构建会阻塞渲染,JS的执行也会阻塞DOM,JS的执行要放在CSSOM构建完成后(因为JS既可以操作DOM,又可以操作CSSOM)。
- 整体流程:
- 获取HTML -> 构建DOM(部分) ->获取CSS/JS文件 -> 构建CSSOM -> 解析运行JS文件 -> 继续构建DOM -> 形成Render Tree -> Layout -> Paintion
回流(reflow)与重绘(repaint)
回流 Reflow
- 当Render Tree中的一部分(或者全部)因为元素的尺寸、布局、隐藏等改变而需要重新构建,称之为回流(reflow)。
- 至少发生一次:页面在第一次加载的时候,会构建一次Render Tree,发生回流。
- 在回流的时候,浏览器会使渲染树中受到影响的部分失效,并且重新构造这部分渲染树。
- 完成回流后,浏览器会重新绘制影响的部分到屏幕中,该过程成为重绘。
重绘 Repaint
- 当Render Tree中的一些元素需要更新属性,而这些属性值是影响元素的外观、风格,而不会影响布局,比如
background-color。称之为重绘(repaint)
区别
- 回流必将引起重绘,重绘不一定引起回流。
- 页面布局和几何属性(
width、height、margin、padding、display)等发生改变时就会产生回流。
优化
- 回流比重绘代价更高,回流的花销和Render Tree有多少节点需要重新构建有关系。
- 浏览器自身的优化:
- 浏览器会维护一个队列,会把所有引起回流、重绘的操作放到这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会执行队列,进行一个批处理。让多次回流、重绘变成一次回流和重绘。
- CSS 优化
- 使用
transform代替top - 使用
visibility(重绘) 代替display:none(回流) - 避免使用table布局(很小的改动可能引起整个table的重新布局)
- 尽可能在DOM树的最末端改变class,(减少回流影响的元素)
- 避免设置多层内联样式,CSS选择符从右向左查找,避免节点层级过多。
- 将动画效果用到
position为absolute或者fixed的元素上,避免影响其他元素布局。 - 控制动画使用
requestAnimationFrame - CSS的GPU加速可以让
transform、opacity这些动画不会引起回流重绘。但是其他属性还是会引起回流和重绘.
- 使用
- JS 优化
- 频繁操作样式,最好是一次兄重写style属性,或者将样式列表定义为class 一次性更改className
- 避免频繁操作DOM,创建documentFragment,在它上面应用到所有DOM操作,最后再把他添加到文档。
- 避免频繁读取会引发回流/重绘的属性,如果多次使用,可以先用变量缓存起来。
TCP的三次握手和四次挥手是什么
- 三次握手:为了保证能建立一个安全可靠的连接
- 客户端向服务器发送报文,SYN(表示发起新的连接)
- 客户端收到后向客户发送一个确认消息 ACK (表示可以收到客户端消息)
- 客户端将收到的确认消息ACK返回给客户端 (表示可以收到服务端数据)
- 四次握手:
- 客户端向服务器发送 FIN (表示想要断开连接)
- 服务器端可能还未发送完需要发送的响应,所以只能先返回确认收到 客服端向服务器发送的FIN,但是并不表示服务器已经准备好断开连接。
- 等待服务器完全发送完数据,会向客户端发送一个自己已经准备好断开的FIN报文。
- 客户端收到服务器的FIN报文后知道服务器要断开连接,之后就可以断开连接了。
对websocket的理解
- WebScoket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽,并达到实时通讯,它建立在TCP之上,同HTTP一样通过TCP来传输数据
- websocket和http最大的不同之处
- WebSocket是一种双向通讯协议,在建立连接后,WebSocket 服务器和客户端都能主动向对方发送或者接受数据
- WebSocket需要类似TCP的客户端和服务器端通过握手链接,链接后才能相互通信。
- 传统响应模式的处理办法:轮询
- 客户端通过一定的时间间隔频繁的向服务器请求数据,来保证实时更新。会造成大量的资源浪费。
跨域
什么是跨域
- 浏览器的同源策略限制
- 要求协议相同、主机(域名)
host相同、端口号port相同
如何解决跨域
-
JSONP : Json padding : callback(json)
- 在网页上添加一个
script标签向服务器请求json数据,服务器将参数放在指定名字的回调函数中返回回来 - 缺点:只支持get不支持post
- 在网页上添加一个
-
CORS :W3C标准 从根本上解决跨域问题
- 服务端设置响应头 Access-Cotrol-Allow-Origin
-
webpack 的本地代理 (开发时)
-
浏览器请求代理服务器,代理服务器请求服务器,服务器响应代理服务器,代理服务器响应浏览器
-
devServer 中配置 proxy
devServer:{ port:8080, proxy:{ '/api':{ traget:'http://' } } }
-
-
通过websocket协议
- 浏览器和服务器全双工通信,无关http,但是需要通过http协议建立连接
Options 参数
- 在正式跨域请求之前,浏览器会根据需要,发起一个"PreFlight"(也就是
option)请求,用来让服务端返回允许的方法(如get、post)、被夸云访问的源Origin,还有是否需要Credentials(认证信息) - 简单请求不会触发"PreFlight" (除了复杂请求的就是简单请求)
- 复杂请求会触发"PreFlight"
- 使用
put或者delete方法 - 发送
json格式的数据(content-type:application/json) - 请求中带有自定义的请求头
- 使用
- 为什么复杂请求需要options参数
- 复杂请求可能对服务器数据产生副作用,对数据进行修改。
CDN
什么是CDN
- 内容分发网络
- CDN加速需要依靠各个网络节点,会从多个CDN服务器中选出最近的节点(缓存服务器)返回资源
- 好处:主要是加速静态资源的访问
Webpack
什么是Webpack
- webpack是一个模块化打包工具,可以分析代码的依赖关系,通过转换形成文件依赖更为简单的项目文件
- webpack的核心:Entry(入口)、Output(出口)、loader、plugins、mode
有什么用
- 代码转换:将浏览器不能直接运行的代码转换成可以运行的代码 (TS、SCSS)
- 文件优化:压缩JS、CSS、HTML代码,压缩合并图片(小图片base64)
- 代码分割:提取多个页面的公共代码,提取首屏不需要的代码让其异步加载
- 模块合并:将多个模块和文件合并为一个文件,减少文件复杂的依赖关系
- 热刷新:提高开发效率
- 代码校验:检测是否规范(ESLint)
常见的 loader
- style-loader : 将编译完成的样式挂载到style标签上
- css-loader : 用于识别.css文件,配合style-loader使用
- less-loader :处理less文件
- sass-loader:处理sass/scss文件
- postcss-loader:补充css样式各种浏览器内核前缀、将px转化为vm、处理css兼容性问题
- file-loader:打包静态资源 除了html/css/js 的其他资源
- url-loader:可以将小图片改为date-URL (base64)格式,减少http请求次数
- html-loader::处理html中的img资源
- ts-loader:打包编译ts文件
- eslint-loader: 需排除第三方库(node_modules) 需配置package.json
- babel-loader:将ES6转化为ES5语法 解决js兼容问题 @babel/core @babel/preset-env babel-polyfill (解决全局变量:Map、Set、Promise之类的新特性)
- thread-loader: 多进程打包 进程启动也有开销,项目大的时候可以使用
常见的 plugin
- html-webpack-plugin:打包HTML文件、自动引入js、css等资源
- mini-css-extract-plugin:将css提取成单独文件,而不是内嵌在js中
- optimize-css-assets-webpack-plugin:压缩css体积
- workbox-webpack-plugin :渐进式网络开发应用程序(可离线访问)
mode的配置 devServer
devServer: {
// 项目构建后路径
contentBase: resolve(__dirname, 'build'),
// 启动 gzip 压缩
compress: true,
// 端口号
port: 3000,
proxy:{
'/api':{
traget:'http://'
}
},
// 自动打开浏览器
open: true
}
webpack 性能优化
-
HMR : 热模块替换hot module replacement
devServer:{ //开启HMR热替换 hot:true; } -
source-map :映射源文件的错误信息、方便开发
devtool: 'source-map' 会生成map格式的文件,里面包含映射关系的代码 devtool: 'inline-source-map' 不会生成map格式的文件,包含映射关系的代码会放在打包后生成的代码中 devtool: 'inline-cheap-source-map' 一是将错误只定位到行,不定位到列。升打包构建的速度。 devtool: 'inline-cheap-module-source-map' module会映射loader和第三方库 devtool: 'eval' 用eval的方式生成映射关系代码,效率和性能最佳。但是当代码复杂时,提示信息可能不精确。 开发环境:'cheap-module-eval-source-map' 生产环境:'cheap-module-source-map' -
oneOf : 将loader放入
oneOf:[]中 每个类型的文件只会被一个loader匹配 -
babel缓存:第二次构建会读之前的缓存 在
babel-loader中添加cacheDirectory:true -
tree-shaking:删除js代码死区的代码 (默认开启)
1. 只对 ES Module 起作用 不对commonjs起作用 因为commonjs是动态到处的 2. babel默认转换为 commonjs 需要配置 .babelrc的{module:false} 和 .package.json 的 {sideEffects:false} 才可以 tree-shaking -
lazy loading :webpack 异步引入模块 加速首页访问速度
// 将代码打包为不同的 chunk optimization:{ splitChunks:{ chunks:'all' } } -
thread-loader: 多进程打包 进程启动也有开销,项目大的时候可以使用
{
loader:'thread-loader',
options:{
worker:2 //两个进程
}
}
- PWA:workbox-webpack-plugin 渐进式网络开发应用程序(可离线访问)
webpack 的运行时代码
-
通过 webpack_modules_ 维护一个所有模块的数组
-
通过 webpack_require_ 手动实现加载一个模块
-
通过 webpack_require_(0) 运行第一个模块(入口模块)
// 将模块放入 _webpack_modules__ const _webpack_modules__ = [ ()=>{ `模块0 入口模块`}, (module,_webpack_require__)=>{`模块1 第一个模块`} ] // _webpack_require__ 手动实现commonjs的模块加载 const _webpack_require__ = (modulesId){ // 创建模块 const module={exports:{}} //调用执行 _webpack_modules__ 中管理的模块 const m = _webpack_modules__[modulesId](module,_webpack_require__) return module.exports; } // _webpack_require__(0) 运行入口模块 _webpack_require__(0)
前端性能优化
懒加载
-
使用异步加载资源。当资源被使用或即将被使用时再进行加载。
-
通过 setTimeout 或者 setInterval 设置加载延迟
-
条件触发 :当触发某些条件后再开始加载某些资源
-
事件监听:可视区域 (项目多时浪费资源,加载后依旧监听事件)
// 先将路径保存到自定义属性 data-src 中 <img data-src='1.jpg'> if(imageTop<window.innerHeight){ // 将 data-src 赋值给 src 属性,开始加载图片 const dataSrc = image.getAttribute('data-src') image.setAttribute('src',dataSrc) } -
IntersectionObserver API (浏览器API)(交叉观察)
// callback 会在目标元素(DOM节点) 能看见和不能看见的时候各触发一次 const observer = new intersectionObserver(callback) //开启观察 DOM节点 oberver.observe(DOM节点) //结束观察 DOM节点 observer.unobserve(DOM节点) //观察每一个image images.forEach(image=>{ observer.observe(image) }) //callback 自带接受一个数组参数,存放每个oberverDOM元素的观察信息 const callback = entries =>{ entries.forEach(entry=>{ if(entry.isIntersection){ // isIntersection 属性为是否交叉 true/fase const image = entry.target // target 属性为当前DOM元素 const dataSrc = image.getAttribute('data-src') image.setAttribute('src',dataSrc) //加载后取消观察 observer.unobserve(image) } }) } -
img标签 loading='lazy' 属性
<img src='1.jpg' loading='lazy'> // loading = 'eager | lazy'
预加载
- 再需要资源之前提前将资源加载好,例如:通过 new Image() 并设置其src属性来下载图片,当时用到相同的src就会从缓存中读取,而不是重新下载
懒加载 和 预加载 的区别
- 两种技术的本质:
- 预加载是提前加载。预加载则会增加服务器前端压力。
- 懒加载是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用
节流
-
设置节流阀,让事件再某个时间段内只能执行一次 (一般为先执行,再限制)
// 主要功能 let mainFun = () => { console.log('主要功能') } // 包裹功能的节流函数 function throttle(fn,time){ let canActive = true return function(){ if(canActive){ canActive = false setTimeout(()=>{ //如果需要原始的this对象,可以通过apply绑定 mainFun() canActive = true },time) } } } //将主要功能通过节流函数包裹 let mainFunThrottle = throttle(mainFun, 3000) //测试节流 setInterval(() => { mainFunThrottle() // 每3秒触发一次 }, 1000)
防抖
-
在一段时间内多次触发事件仅仅执行一次 (一般为先限制,后执行)
// 主要功能 let mainFun = () => { console.log('主要功能') } //防抖函数 let antiShake = function (fn, time) { let hasTimer return function () { if (hasTimer) { clearTimeout(hasTimer) // 如果存在,删除旧的,创建新的,按照新的计时 } hasTimer = setTimeout(() => { mainFun() }, time) } } //将主要功能通过防抖函数包裹 let mainFunAntiShake = antiShake(mainFun, 1000) let i = 0 let timeId = setInterval(() => { mainFunAntiShake() //仅仅触发1次 if (i < 30) { i++ } else { clearInterval(timeId) } }, 100)
节流 和 防抖 的区别
- 区别:
- 节流:通常为限执行,后限制,在规定时间内触发多次只有第一条生效
- 防抖:通常为先限制,后执行,在规定时间内触发多次只有最后一条生效
- 注意:
- 注意包裹后因为setTimeout引起的this指向改变问题,可以通过 apply(context , arguments) 改变
CDN加速
- 通过cdn加速静态资源访问
webpack性能优化
- 开发优化:
- HMR : 热模块替换hot module replacement
- source-map :映射源文件的错误信息、方便开发
- oneOf : 将loader放入
oneOf:[]中 每个类型的文件只会被一个loader匹配 - babel缓存:第二次构建会读之前的缓存 在
babel-loader中添加cacheDirectory:true - thread-loader: 多进程打包 进程启动也有开销,项目大的时候可以使用
- 生产优化:
- lazy loading :webpack 异步引入模块 加速首页访问速度
- 代码体积优化:
- tree-shaking:删除js代码死区的代码 (默认开启)
- js代码默认压缩
- mini-css-extract-plugin:将css提取成单独文件,而不是内嵌在js中
- optimize-css-assets-webpack-plugin:压缩css体积
- html-webpack-plugin:打包压缩HTML文件、自动引入js、css等资源
- http请求:
- url-loader:减少http请求次数
首屏加载优化
- 图片:
- 懒加载
- 精灵图
- url-loader (base64)
- Gzip压缩
- 首屏服务端渲染SSR
- CDN加速
- 合理使用浏览器缓存
SEO优化
-
合理的title标签
<title>标题</title> -
description
<meta name='Description' Content='网页简述'> -
keywords
<meta name='Keywords' Content='关键字1,关键字2,关键字3,'> -
语义化HTML
<header></header> <nav></nav> <article></article> <aside></aside> <section></section> <footer></footer> <p></p> <strong></strong> -
图片添加
alt属性 -
a标签加title属性
前端网络安全
XSS 是什么 如何解决
-
XSS:Cross Site Scripting 跨站脚本攻击
- 向网页注入恶意指令代码
- 例如:注入HTML 注入JavaScript 甚至服务端语言
-
如何解决
-
过滤 & 转义
-
虚拟DOM也可以防止XSS
-
CSP(内容安全策略)
-
在 HTML 的 Head 中添加如下 Meta 标签,将在符合 CSP 标准的浏览器中使非同源的 script 不被加载执行。
<meta http-equiv="Content-Security-Policy" content="script-src 'self'"> -
CSP 有两种策略类型:
'Content-Security-Policy' 对不安全的资源会进行阻止执行 'Content-Security-Policy-Report-Only' 只会进行数据上报,不会有实际的阻止。 -
注意: 如果页面很多可以通过统一设置HTTP Header
Content-Security-Policy:srcipt-src 'self' '*.qq.com'
-
-
CSRF 是什么 如何解决
- CSRF 攻击全称跨站请求伪造(Cross-site Request Forgery)
- 攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF的另一个特征是,攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用Cookie中的信息。
- 自动防御策略:浏览器同源策略(跨域)
- 主动防御:通过Token验证,后端不要再GET接口做用户操作
前端模块化
什么是前端模块化,有什么好处
-
产生原因:前端页面复杂度越来越高,规模越来越大,不同代码之间的相互依赖显得非常不清楚,多人开发十分不方便。
-
模块化是开发的一种管理方式,将不同功能的代码和资源分别作为一个模块。
-
模块化的好处
- 解决命名冲突问题
- 提高代码复用性
- 提高代码的可维护性
- 提高代码灵活性,按需导入,灵活使用
- 有利于多人共同开发
常见的前端模块化规范
- AMD
- CMD
- Commonjs
- ESM(ES6模块化规范)
CommonJS
-
Node 应用采用的CommonJS模块化规范,每个文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类都是私有的,对其他文件不可见。在服务器端,模块加载是运行时同步加载。在浏览器端,模块需要提前编译打包(不能再浏览器中直接使用CommonJS规范)
-
特点:
- 模块中的代码有自己的作用域,不会污染全局
- 模块可以被多次加载,但只在第一次运行,运行后会缓存起来,之后直接读缓存结果。想要模块重新运行,必须清除缓存。
- 模块加载顺序就是其再代码中出现的顺序
-
基本用法
-
暴露模块
module.exports = value 或 exports.xxx = value -
引入模块
const xxx = require('aaa') // 如果是第三方模块 aaa 为模块名即可 // 如果是自定义模块 aaa 为模块文件路径
-
-
本质:
- CommonJS中
mondule代表当前模块,是一个对象。他的exports属性是对外接口,加载模块加载的是该模块的module.exports属性 require的基本功能是用于加载模块文件:读入并执行一个JavaScript文件,然后返回该模块的exports对象- CommonJS的就加载机制:输入的是被输出的值的浅拷贝,也就是说,模块内部的变化影响不到输出后的值(原始类型,会被缓存),可以通过清除缓存或者使用函数引用(闭包)
- CommonJS中
ES6 模块化(ESM)
-
ES6 模块化的设计思想是尽量的静态化,在编译时就能确定模块的依赖关系,以及输入和输出的变量,是官方的语言层面的模块化设计,可以再浏览器中直接使用。
-
基本用法:
-
导入导出
// 通过 export 导出 export const name= 'zhangsan' export let age = 15 // 通过 import 导入 (通过 as 起别名) import {name,age as Age} from './xxx.js' // 通过 export default 默认导出 (只能有一个默认导出) export default function(){} // 通过 import 导入 import myFn from './xxx.js'
-
-
本质:
-
export语句输出的接口是与其对应值的动态绑定,通过该接口可以获取到模块内部实时的数据 -
ES6 模块化为官方语言层面的模块化设计,可以在浏览器中直接使用,是未来的趋势
<script type='module' src='xxx.js'></script>
-
ESM 和 CommonJS的区别
- CommonJS 模块输出的是一个值的拷贝,ESM输出的是值的只读引用
- CommonJS 是值的拷贝,将值缓存,缓存之后无法改变,除非清理缓存
- ESM 是动态只读引用,不会缓存值,可以实时获取模块内部数据变化
- CommonJS 的require() 是同步加载模块,ESM的import是异步加载模块
- CommonJS 是运行时加载,ESM是编译时加载或者静态加载
- CommonJS加载有缓存,ESM加载没有缓存
- CommonJS 加载的是整个模块,ESM 可以单独加其中一些方法(按需导入)
- CommonJS模块中的this指向当前模块(module.exports) ,ESM中指向undefined