移动web性能优化从入门到进阶--进阶篇

1.资源合并与压缩

启用GZIP

gzip是GNUzip的缩写,最先用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器)必须共同支持gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如Apache,Nginx,IIS一样支持gzip。 gizp流程javascript

  • 浏览器请求url,并在request header中设置属性accept-encoding:gzip。
  • 服务器支持gzip,response headers返回包含content-encoding:gzip。
  • 开启gzip能够达到80%的压缩率,即1MB的文件下载下来只须要200K,大大减小传输效率,是一项很是重要的资源压缩手段。

Nginx中开启gzip: css

图片描述

升级HTTP/2.0

HTTP/2是HTTP协议自1999年HTTP 1.1发布后的首个更新,主要基于SPDY协议(是Google开发的基于TCP的应用层协议,用以最小化网络延迟,提高网络速度,优化用户的网络使用体验)。 优化原理: 根据上文中说的资源合并问题,浏览器能够同时创建有限个TCP链接,而每一个链接都要通过慢启动三次握手链接创建,HTTP1.1为了解决这个问题推出了keep-alive,即保持链接不被释放,可是真正的这些链接下载资源是一个线性的流程:一个资源的请求响应返回后,下一个请求才能发送。这被称为线头阻塞,为了完全解决此问题,HTTP2.0带来了多路复用html

图片描述
HTTP2.0的其余新特性也有助于页面的打开速度:

合并资源 vs 并行加载资源?

如今回过头来探讨一下上文说的资源合并问题,有了HTTP2.0以后,咱们是否还须要合并资源,目前看须要遵循下面的原则:前端

  • 中止合并文件 在HTTP/1.1中,CSS,JavaScript被压缩到了一个文件,图片被合并到了一张雪碧图上。合并CSS、JavaScript和图片极大地减小了HTTP的请求数,在HTTP/1.1中能得到显著的性能提高。 可是,在HTTP/2.0中合并文件再也不是一个好的办法。虽然合并依然能够提升压缩率,但它带来了代价高昂的缓存失效。即便有一行代码改变了,整个文件就要从新打包压缩,浏览器也会强制从新加载新的文件。vue

  • 尽可能不要在HTML里内联资源 非特殊的代码(rem适配代码,上报代码等)以外,尽可能不要使用内联资源,在极端状况下,这确实可以减小给定网页的HTTP请求数。可是,和文件合并同样,HTTP/2优化时你不该该内联文件。内联意味着浏览器不能缓存单个的资源。若是你将全部页面使用的CSS声明嵌入了每个HTML文件,这些文件每次都要从服务端获取。这致使用户在访问任何页面时都要传输额外的字节。java

  • 合并域名 拆分域名是让浏览器创建更多TCP链接的一般手段,浏览器限制了单个服务器的链接数量,可是经过将网站上的资源切分到几个域上,你能够得到额外的TCP链接,可是每一个拆分的域名都会带来额外的DNS查询、握手,新链接的创建,根据HTTP2.0多路复用的原则:HTTP2采用多路复用是指,在同一个域名下,开启一个TCP的connection,每一个请求以stream的方式传输,域名的合并能够带来更多的多路复用,以下图在chrome的Network面板中查看HTTP2.0,注意protocol和ConnectID相同则表示启用复用: react

    图片描述

合理使用icon类图片base64化

<img src="data:image/gif;base64,R0..."/>
复制代码

在页面使用的背景类图片icon类图片,很少且比较小的状况下,能够把图片转成base64编码嵌入到html页面或者CSS文件中,这样能够减小页面的HTTP请求数。须要注意的是,要保证图片较小,通常超过5kb的就不推荐base64嵌入显示了。为何是5kb?。 同时,采用Webpack的url-loader能够帮咱们在不影响代码可读性的状况下,解决base64字符串问题。android

Icon Font

IconFont技术起源于Web领域的Web Font技术,它是把一些简单的图标制做成字体,而后让图标变成和字体同样使用,Icon 的设计和使用在近几年的发展中,也经历了由当初的 img 方案 到现现在的 svg 方案,有如下优势:webpack

  • 字体是矢量的,因此能够随意改变大小。
  • 由于它是字体,因此全部字体的css均可以使用,好比font-size,color,background,opacity等。
  • 减小图片请求数。
  • iconfont没有兼容性问题,IE6,Android2.3都可以兼容。

2.浏览器加载原理优化

首屏资源优化

1)剥离首屏资源:

首屏的快速显示,能够大大提高用户对页面速度的感知,所以应尽可能针对首屏的快速显示作优化,基于联通3G网络平均338KB/s(2.71Mb/s),因此首屏资源不该超过1014KB,剥离首屏须要的资源,非首屏的资源单独合并,采用懒加载。这个原则适用上文的资源合并和加载中的场景。git

2)按需加载:

将不影响首屏的资源和当前屏幕资源不用的资源放到用户须要时才加载,能够大大提高重要资源的显示速度和下降整体流量,对于移动web端常见的多tab页面,Webpack的CodeSplitting帮助咱们更加便捷实现按需加载。

3)非首屏图片Lazyload:

不用多说,在目前流量费用还算比较高昂的状况下,帮助用户节省更多的流量能够避免用户的投诉,为了保证页面内容最小化,加速页面渲染,尽量节省首屏网络流量,页面中的图片资源推荐使用懒加载实现,在页面滚动时动态载入图片。

使用CDN

CDN是将源站内容分发至最接近用户的节点,使用户可就近取得所需内容,提升用户访问的响应速度和成功率。解决因分布、带宽、服务器性能带来的访问延迟问题,适用于站点加速、点播、直播等场景。 对于web页面来讲,将项目的js,css等静态资源存放在CDN是一个重要的优化手段,加入全部资源统一打包放在同一个域名下,很难达到用户就近获取的优点(目前最佳实践是html页面采用一个域名,静态资源文件采用CDN域名),所谓静态资源便是能够被浏览器缓存的资源,而对于html页面,因为是js和css等连接的入口,一般不采用缓存。经常使用的阿里云CDN腾讯云CDN都有开放接口,开发者能够按需选择。

预加载

此预加载主要分为两个部分,一种是采用原生浏览器支持的API来对页面的一些资源进行预先拉取或者加载,另外一种是经过本身写逻辑来加载一些重要的资源,当即下面内容的前提是要当即目前移动web常见的hybrid架构,webview外壳+H5页面:

图片描述

1) DNS预解析(dns-prefetch):

DNS 做为互联网的基础协议,其解析的速度彷佛很容易被网站优化人员忽视。如今大多数新浏览器已经针对DNS解析进行了优化,典型的一次DNS解析须要耗费 20-120 毫秒,减小DNS解析时间和次数是个很好的优化方式。DNS Prefetching 是让具备此属性的域名不须要用户点击连接就在后台解析,而域名解析和内容载入是串行的网络操做,因此这个方式能 减小用户的等待时间,提高用户体验 。 html <link rel="dns-prefetch" href="//haitao.nos.netease.com">

2) Preload 和 Prefetch:

二者都是以<link rel="preload"> 和 <link rel="prefetch">做为引入方式。

Preload

一个基本的用法是提早加载资源,告诉浏览器预先请求当前页须要的资源,从而提升这些资源的请求优先级,加载可是不运行,占用浏览器对同一个域名的并发数:

<link rel="preload" href="a.js" as="script" onload="preloadLoad()">
复制代码
Prefetch

用法是浏览器会在空闲的时候,下载资源, 并缓存起来。当有页面使用的时候,直接从缓存中读取。其实就是把决定是否和什么时间加载这个资源的决定权交给浏览器。

<link rel="prefetch" href="a.js">
复制代码

遗憾的是对于这两个接口,移动端的浏览器支持性很很差,这也是没有广泛推广开来的缘由。

图片描述
图片描述
图片描述
何时使用Preload,何时使用Prefetch能够总结以下:对于当前页面颇有必要的资源使用 preload,对于可能在未来的页面中使用的资源使用 prefetch。关于Preload 和 Prefetch能够 参考这里

另外还有PrerendersubresourcePreconnect属性,因为目前能支持到这些属性的机型太少,这里就不在赘述了。

3) 业务逻辑的预加载:

关于业务逻辑的预加载,在这里我能够举一个微信小程序的例子。小程序主要分为渲染层和逻辑层,逻辑层有iOS或者Android的JavaScriptcore来运行,渲染层由各自的webview组件负责渲染。咱们用户实际体验到的UI仍是跑在咱们的webview里面,这个和大多数H5页面的渲染用的是一个组件。可是为何咱们体验小程序会比H5页面要快不少?尤为是新开页面时?

图片描述
小程序在启动时,会预先加载全部页面逻辑代码进内存,在 a页面跳转至 b页面 时,能够在内存中直接运行而无需在发送资源请求,a页面的逻辑代码 Javascript 数据也不会从内存中消失。b页面甚至能够直接访问 a页面中的数据,整个坏境在一个大的上下文中。 固然这里你可能会有疑问?假如用户不会进入page2,那加载page2的逻辑代码岂不是浪费?这里就会牵扯到一个用户行为预测的问题,在小程序的架构中,整个逻辑代码是统一在一个包里,微信是统一将这些文件下载并加载到内存中,这可能会涉及到一些浪费,可是对于提速来说收益大于弊端的。固然小程序页提供出 分包策略来优化这些问题。

借助小程序的思路,咱们的移动web一样适用这种预加载优化逻辑:

预加载资源:

在多tab的单页应用中,咱们能够在用户打开首屏以后,预先加载其余tab的资源。例如用户进入时在推荐tab,这时就能够预先加载订单,个人 这两个tab的资源了,当用户点击订单时,页面的展示就会快一些。

图片描述

预加载数据:

预加载数据的时机最好是在空闲时,什么是空闲时呢?咱们分析一下打开一个H5页面的流程:

图片描述
从图上能够看到,利用闲时能够作的事情有不少,预加载数据是一个典型的优化手段,提早把新页面所须要的数据加载好,在新页面打开后,能够直接用数据来进行渲染,固然这里涉及到的跨页面数据通讯,咱们能够利用localStroage来实现。

预加载webview:

我利用闲时来作更多事情的前提是闲时够长,但这本书也不是一个很好的现象,尽可能的减小闲时,也是咱们须要作的一项优化,例如咱们来减小webview的加载时间,这就须要提早加载webview,此项优化大可能是由native端来完成:

  • 在APP启动后,就提早在内存中将webview加载好,而不是等到点击进入web页面时才去加载。
  • 建立一个webview的复用池,例如最多只存在3个webview,每次从池子里获取webview,达到复用的目的。

3.合理利用缓存

上文说了浏览器缓存的基础知识,既然是基础,那就说明必须掌握,下面来讲一些进阶篇的利用缓存来优化页面:

妙用localStorage

HTML5 LocalStorage能够看作是增强版的cookie,数据存储大小提高,有更好的弹性以及架构,能够将数据写入到本机的ROM中,还能够在关闭浏览器后再次打开时恢复数据,以减小网络流量,平常使用localStorage来优化咱们的页面大概有如下几种场景:

  • 缓存一些非实时更新的变量,例如某些闪屏的标志位信息,地理位置信息等等,取用方便,即存即用。
  • 使用localStorage缓存Js和css文的,为了提高页面的打开速度,或者是页面能够离线使用,有些页面会采用将静态资源文件直接缓存在localStorage中,当页面打开时将内容读取出并运行,使用此方法确实能够减小http请求,提速页面。
  • 在一些跨webview通讯的场景中,localStorage是兼容性最好的数据通讯方案,例如预加载的数据能够缓存在localStorage中,来实现各个页面的webview数据共享。
  • 须要注意的是,localStorage并非无限大的,针对每一个域名,PC端浏览器给localStorage分配的容量大概4.5m-5m,移动端相似微信等等的浏览器大概容量是2.5m-3m参考这里。因此在使用时须要作好异常捕获,让localstorage超出容量时,是没法在进行插入并报错,若是对容量有更高的要求,能够参考使用indexeddb。须要注意的是indexeddb的兼容性却不是很好,android4.4以前以及iOS7之前都没法使用。

老生常谈离线包

离线包技术能够说是并不算很新的技术了,各个业务都有在使用,也都有本身的一套hybrid离线包系统,关键点在于离线包的打包,同时对文件加密/签名,更新离线包(增量) ,安全教研以及容错机制等等,在这里列举一些大厂的离线包方案来参考:

Service Worker探索

提到缓存,那就不得不提近几年比较火的Service Worker了:

图片描述
做为一个比较新的技术,你们能够把 Service Worker 理解为一个介于客户端和服务器之间的一个代理服务器。在 Service Worker 中咱们能够作不少事情,好比拦截客户端的请求、向客户端发送消息、向服务器发起请求等等,其中最重要的做用之一就是离线资源缓存。 Service Worker的主要复杂点在于不断地对缓存策略的调整,笔者在这里就不过多展开,能够参考一下淘宝 Service worker实战

4.Nodejs服务端渲染(SSR)优化首屏时间

在先后端分离以后,后端语言的模板功能被弱化,整个页面的渲染基本上都由前端 js 动态渲染,但这样对于一些应用来讲是有缺陷的。好比须要 SEO 的,须要打开页面不用等待就能看到页面的,另外前端页面展现过分依赖js和css逻辑执行,在极端状况或者网络较差,手机性能低下(尤为在低端Android机型较为明显)时,白屏时间较长,这时服务端渲染便应用而生,至于为何是Nodejs,做为一个前端,难道还要用Java么。。?

为何会有服务端渲染?

若是你说服务端渲染和早期web框架,例如SSH,JSP servlet,PHP等等同样的话,那我只能说呵呵,目前的服务端渲染和早期的框架是有本质区别的:

  • Web 2.0时代最大的思想革命本质不是先后端分离,而是把网页看成独立的应用程序(app)。创建在先后端分离的基础上,后端只负责提供数据json格式,前端仍是负责页面交互逻辑,大多数的服务端渲染采用Nodejs层来进行数据组装,html拼接。
  • 重点在首屏!!首屏时间的优化,移动互联网时代的爆发,用户对网页性能的要求愈来愈高,但毕竟基于3G,4G网络,让用户更快的看到页面就能挽留更多的潜在商机。服务端只负责首屏的页面渲染,真正过了首屏,大多数的业务逻辑,页面交互,仍是须要有单独的前端来实现的。

如何实现?

若是你的项目用的是React或者是vue,那么下面两个现场的开源框架是不错的选择。

固然,你也能够本身实现一套本身的服务端渲染框架,通常须要关注这些问题:

  • 实现自定义的Node端的window上下文对象Cookie & Session等。
  • 远程数据的获取,通常采用Nodejs的http模块。
  • React采用ReactDOMServer调用renderToString(),Vue采用vue-server-renderer调用renderToString()。
  • Node端内存泄露和控制等问题

代码同构

使用Nodejs的服务端渲染的一大优点就是代码同构,这使得一个项目能够分别部署成走线上正常前端渲染版本,和走服务端渲染版本,这样能够更好的作到容灾机制,当任何一种分之挂掉以后,能够直接走另外一个版本,提升稳定性。这也同构的魅力所在!由于在同构直出宕掉的时候,还有前端渲染页面能够提供正常的服务。

图片描述

取舍

虽说服务端渲染这类优化确实能够提高必定的页面首屏时间,可是也是须要成本的,在前端开发接管了Node做为中间层时,须要额外的机器资源部署,而且一旦接触到后端,容灾机制,内存管理等性能指标都须要关注,这对于当前的业务系统架构可能须要有必定的调整,因此仍是要斟酌来使用。

5.渲染优化

终于回到咱们前端的老本家了,若是说前面的优化都是在框架,逻辑层面的优化,或者是参考后端,客户端的优化思路,那么真正涉及到UI渲染的优化才是咱们做为前端工程师的立身之本了。

何为渲染优化?

抛开首屏加速,真正让用户体验web页面的另外一个很重要的部分就是用户行为交互了,这包括用户的点击相应滚动流畅度动画是否卡顿流畅度等等,这些关于用户交互性的优化在已往的PC端可能不是很被重视,由于PC浏览器的性能要远远大于手机端,可是到了移动web就不同了,用户都但愿移动web能有PC端同样的性能。

为何一样的页面在iPhone里总比Android流畅?

目前主流的Android硬件配置能够说是甩iPhone几条街了,那为何高配置却得不到好的体验呢?关键两类机型的操做系统上的优化程度,其中一个缘由就是iOS操做系统采用执行率较高的Object-c语言,大部分硬件接口能够直接调用和运行,而Android则采用Java语言,由于虚拟机的存在,虽然跨平台性提高了,可是经过虚拟机在和系统硬件交互,执行效率就低了不少,固然这只是其中一个缘由。那么,咱们移动web主要优化的群体就是Android机型了。

16ms优化

目前大多数设备的屏幕刷新频率为60次/秒,每一帧所消耗的时间约为16ms(1000 ms / 60 = 16.66ms),这16ms就是渲染帧的时长,所谓渲染帧是指浏览器一次完整绘制过程,帧之间的时间间隔是DOM视图更新的最小间隔,但实际上,浏览器还有一些整理工做要作,所以开发者所作的全部工做须要在10ms内完成。 若是不能完成,帧率将会降低,网页会在屏幕上抖动,也就是一般所说的卡顿,这会对用户体验产生严重的负面影响。因此若是一个页面中有动画效果或者用户正在滚动页面,那么浏览器渲染动画或页面的速率也要尽量地与设备屏幕的刷新频率保持一致,以保证良好的用户体验。在这一个间隔内,浏览器可能须要作如下事情:

- 脚本执行(JavaScript):脚本形成了须要重绘的改动,好比增删 DOM、请求动画等
 - 样式计算(CSS Object Model):级联地生成每一个节点的生效样式。
 - 布局(Layout):计算布局,执行渲染算法
 - 重绘(Paint):各层分别进行绘制(好比 3D 动画)
 - 合成(Composite):合成各层的渲染结果
复制代码

16ms优化重绘和重排

在上面浏览器须要作的这些事情中,会引起不一样程度的重绘和重排,而重绘和重排正式影响流畅的重要因素:

  1. 部分渲染树(或者整个渲染树)须要从新分析而且节点尺寸须要从新计算,这被称为重排。

  2. 因为节点的几何属性发生改变或者因为样式发生改变,例如改变元素背景色时,屏幕上的部份内容须要更新,这样的更新被称为重绘。

重排和重绘代价是高昂的,它们会破坏用户体验,而且让UI展现很是迟缓,可是每次重排,必然会致使重绘,而每次重绘并不必定会发生重排,咱们须要在如下几种场景来减小重排的发生: 当页面布局和几何属性改变时就须要回流。下述状况会发生浏览器回流:

  1. 添加或者删除可见的DOM元素。
  2. 元素位置改变。
  3. 元素尺寸改变——边距、填充、边框、宽度和高度。
  4. 内容改变——好比文本改变或者图片大小改变而引发的计算值宽度和高度改变。
  5. 页面渲染初始化。
  6. 浏览器窗口尺寸改变——resize事件发生时。

使用 requestAnimationFrame:

提高动画流畅度的另外一个重要因素是让浏览器变得智能起来,好在浏览器给咱们提供了这个接口requestAnimationFrame,经过这个API,能够告诉浏览器某个JavaScript代码要执行动画,浏览器收到通知后,则会运行这些代码的时候进行优化,它会确保JS尽早在每一帧的开始执行,实现流畅的效果,而再也不须要开发人员烦心刷新频率的问题了:

function animationWidth() {
	  var div = document.getElementById('box');
	  div.style.width = parseInt(div.style.width) + 1 + 'px';

	  if(parseInt(div.style.width) < 200) {
	    requestAnimationFrame(animationWidth)
	  }
	}
	requestAnimationFrame(animationWidth);

复制代码

试试requestIdleCallback

requestIdleCallback的出现伴随着React 16 的Fiber特性,他的使用场景是当用户在作负责交互时,不但愿由于一些不重要的任务(如统计上报)致使用户感受到卡顿的话,就应该考虑使用了,由于requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态,可是须要注意的是不要在requestIdleCallback操做任何DOM,这违背了这个接口的设计原则。

requestIdelCallback(myNonEssentialWork);
	    function myNonEssentialWork (deadline) {
	    
	      // deadline.timeRemaining()能够获取到当前帧剩余时间
	      while (deadline.timeRemaining() > 0 && tasks.length > 0) {
	        doWorkIfNeeded();
	      }
	      if (tasks.length > 0){
	        requestIdleCallback(myNonEssentialWork);
	      }
	    }
复制代码

fragment元素的应用

在你使用dominnerHTML方法来插入大量dom节点时,不妨试试fragment,fragment文档片断是个轻量级的document对象,它的设计初衷就是为了完成这类任务——更新和移动节点。文档片断的一个便利的语法特性是当你附加一个片段到节点时,实际上被添加的是该片段的子节点,而不是片段自己。只触发了一次重排,并且只访问了一次实时的DOM。

列表滚动优化

长列表滚动在移动端是一种很是常见的交互模式,例如feeds流,图片流等等,这些列表的滚动流畅度优化对用户体验的提高是很是重要的,基于目前的优化思路,借助dom复用的方案,相似iOS的UITableView或者Android的recyclerview原理,在列表滚动时,只保证视窗区域内的dom节点存在,在有限的dom节点内实现滚动,而不在建立新的节点,在用户不断下拉翻页的过程当中,保证整个页面有限的dom元素来减小内存的消耗,原理以下图:

图片描述
复用的dom:
图片描述
采用这一个方案的前端是借助浏览器的onscroll事件来作逻辑处理,可是问题在于有些机型例如iOS的UIWebview下,onscroll不能实时触发,这就给优化带来了难题,由此引起出了模拟滚动:

  • 正常的滚动:咱们平时使用的scroll,包括上面讲的滚动都属于正常滚动,利用浏览器自身提供的滚动条来实现滚动,底层是由浏览器内核控制。
  • 模拟滚动:最典型的例子就是iscroll了,原理通常有两种:
    • 1).监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,而后采用requestanimationframe来在一个线型函数下不断的修改元素的transform来实现手指离开时的一段惯性滚动距离。
    • 2).监听滚动元素的touchmove事件,当事件触发时修改元素的transform属性来实现元素的位移,让手指离开时触发touchend事件,而后给元素一个css的animation,并设置好duration和function来实现手指离开时的一段惯性距离。

结论是若是要采用模拟滚动,能够解决onscroll不实时触发的问题,从而实现长列表的复用的优化,可是带来新的问题就是模拟滚动自己也是dom的重绘,增长额外的性能消耗,达到有优化效果并不理想,好在iOS的新版WKwebview解决了onscroll问题,让开发者有了更好的选择。

滚动函数防抖

当持续触发事件时,必定时间段内没有再触发事件,事件处理函数才会执行一次,若是设定的时间到来以前,又一次触发了事件,就从新开始延时。以下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。

function debounce(fn, wait) {
	    var timeout = null;
	    return function() {
	        if(timeout !== null) 
	                clearTimeout(timeout);
	        timeout = setTimeout(fn, wait);
	    }
	}
	// 处理函数
	function handle() {
	    console.log(Math.random()); 
	}
	// 滚动事件
	window.addEventListener('scroll', debounce(handle, 1000));
复制代码

合理使用GPU

动画卡顿是在移动web开发时常常遇到的问题,解决这个问题通常会用到CSS3硬件加速。硬件加速这个名字感受上很高大上,其实它作的事情能够简单归纳为:经过GPU进行渲染,解放CPU,咱们能够利用GPU的图形层,将负责的动画操做放在这个层,如何开启?

webkit-transform: translateZ(0);
复制代码

强制把须要动画的dom的对象 ,放置在GPU的layout层来缓存从而达到任何移动,大小变化都在这个层。 经过开启GPU硬件加速虽然能够提高动画渲染性能或解决一些棘手问题,但使用仍需谨慎,使用前必定要进行严谨的测试,不然它反而会大量占用浏览网页用户的系统资源,尤为是在移动端,肆无忌惮的开启GPU硬件加速会致使大量消耗内存,千万不要* {webkit-transform: translateZ(0);}

写在最后

本文在性能优化的基础上,将移动web的性能点逐步展开和深刻,内容比较多,指望各位开发者能真正实践并进行不断尝试,总之:

技术就是在于不断折腾,愿各位在踩坑的道路上一路顺风!

最后,向你们推荐一门慕课网的实战课程《移动Web APP开发之实战美团外卖》(当即学习),但愿小伙伴们能经过这门课程收获满满,祝你们学习进步。

相关文章
相关标签/搜索