作者 | Brilliant Open Web团队

编辑 | Brilliant Open Web团队

近日谷歌发表了一篇关于谷歌搜索引入 service worker 的文章,文章详细介绍了引入过程中一些有意思的问题和解决方案,能帮助读者了解和使用 service worker,因此对原文进行了翻译,方便大家一起来学习和借鉴。原文链接:https://web.dev/google-search-sw

背景

您在使用 Google 的搜索任意话题时,都将立即获得一个相关且有用的结果页。您可能没有注意到的是,在某些特定的应用场景当中,结果列表的呈现是由一项功能强大的技术实现的,那就是 service worker。将 service worker 应用到谷歌搜索而不带来负面的性能影响,需要数十个来自不同团队的工程师合力协作。接下来要讲的就是这样一个关于推动什么技术落地、如何测量其影响以及为其做了什么权衡的故事。

探索 service worker 的主要原因

为一个 web app 添加 service worker,就像给您的站点做一个结构上的改变,需要有着一系列清晰的目标。有几个关键原因让谷歌搜索团队去探索如何引入 service worker。

service worker 是一段位于 web app 和服务端之间的代码,而运行这段代码需要一定的花销,所以需要确保 service worker 所做的能够带来在缓存或功能上足够大的收益,这样才能抵消产生的花销(Chrome Dev Summit 2018 的一个分享(https://www.youtube.com/watch?v=25aCD5XL1Jk)很好地深入探索了这个想法)。所以开启 service worker 之旅的第一步,是充分了解想实现的目标,然后收集一套完整的指标以便衡量目标的完成程度。

有限的搜索结果缓存

谷歌搜索团队发现,用户有时会短时间内多次去搜同一个词。与其每次都向后端发出请求并获得一样的结果,不如通过缓存在本地响应这些重复的请求。

同时也不能忽视结果的实时性。有时用户重复搜索同一个词是因为这是一个动态变化的话题,这样才能够看到最新的结果。搜索团队通过 service worker 可以在细粒度上控制本地结果缓存的生命周期,协调速度和实时性,以达到他们认为能最好地服务用户的平衡状态。

有意义的离线体验

另外,谷歌搜索团队想提供有意义的离线体验。当用户想了解一个话题时,他们会直接打开谷歌主页并开始搜索,而不会担心网络连接是否有效。如果没有 service worker,在离线的情况下访问谷歌搜索页面会跳转到浏览器的标准网络错误页面,用户需要在网络连接恢复之后再返回到这个页面重新搜索。而如果有 service worker,就可以向用户提供一个定制的离线 HTML 响应页面并允许他们立刻输入搜索内容。

在离线时搜索结果都是不可用的,但是 service worker 通过 background sync api(https://developers.google.com/web/updates/2015/12/background-sync),能够延迟搜索的执行,等网络恢复就立刻将关键词发送到谷歌服务器进行搜索。

更智能的 JavaScript 缓存和服务

另一个动机是为了优化 JavaScript 代码的缓存和载入,这些 JavaScript 代码是模块化的,为搜索结果页提供各种功能特性。在没有 service worker 的情况下,打包 JavaScript 有许多好处,所以搜索团队不想简单地完全停止拼合。

搜索团队预计,通过使用 service worker 在运行时对细粒度 JavaScript 代码块进行版本控制和缓存,他们可以减少缓存混乱的数量并确保将来复用的 JavaScript 被有效缓存。service worker 中的逻辑可以分析一个 JavaScript 的 HTTP 请求所需要包含哪些 JavaScript 模块,然后在本地找出这些已被缓存的、被有效拆分的模块拼合起来以完成这个请求。这节省了用户的带宽,提高了总体响应能力。

success:平均下来,被 service worker 处理的重复请求减少了一半新的 JavaScript 的下载量,而这直接让延迟的用户交互减少 6%。

使用 service worker 缓存 JavaScript 还有一些性能上的好处,在 Chrome 中,被存储和复用的 JavaScript 是被解析并用字节代码表示的(https://v8.dev/blog/code-caching-for-devs#use-service-worker-caches),因此在运行时只需要完成更少的处理工作,就能执行页面中的 JavaScript。

问题和解决方案

以下是实现团队既定目标需要克服的几个障碍。虽然其中一些挑战是特定于谷歌搜索的,但是更多的适用于可能正在考虑部署 service worker 的各种网站。

问题:service worker 开销

在谷歌搜索运行 service worker 的最大挑战和阻碍,是确保它不会增加用户的感知延迟。谷歌搜索非常重视性能,已经阻止使用一些会为给定的用户群带来几十毫秒额外延迟的新功能。

当团队在最早的实验中开始收集性能数据时,往往会出现问题。搜索结果页面的 navigation 请求(https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests)响应返回的 HTML 是动态的,会根据运行在谷歌搜索 web 服务器的逻辑而变化很大。目前 service worker 没办法复制这个逻辑并马上返回缓存的 HTML,最多只能将 navigation 请求发送到后端 web 服务器。

如果没有 service worker,这个网络请求能够立即发出。而当 service worker 被注册后,它总是需要启动并获得机会去运行 fetch event handlers(https://developers.google.com/web/fundamentals/primers/service-workers/#cache_and_return_requests),即使这些 fetch handlers 不会对请求作任何处理。所以启动和运行 service worker 的时间是在每次导航的基础上增加的纯粹开销:

这让 service worker 在验证其他好处前就具有很大的延迟劣势。另外,在用实机进行测试时,团队发现  service worker 的启动时间分布比较分散,一些低端移动设备启动 service worker 的时间几乎与网络请求结果页 HTML 所需的时间相同。

解决方案:使用 navigation preload

让谷歌搜索团队能够继续向前推进的一个最重要的特性是 navigation preload(https://developers.google.com/web/updates/2017/02/navigation-preload)。对于任何需要网络来响应 navigation 请求的 service worker 来说,navigation preload 对性能的影响非常关键。它会提示浏览器在 service worker 启动的同时立即发送 navigation 请求:

只要 service worker 在网络响应返回前完成启动,就意味着 service worker 不会引入任何延迟开销。

搜索团队还需要避免在低端移动设备使用 service worker,因为在低端移动设备 service worker 的启动时间可能超过进行 navigation 请求的时间。由于没有硬性而方便的规则用于定义低端移动设备,他们提出了检查设备内存(https://developers.google.com/web/updates/2017/12/device-memory)的启发式方法,内存少于 2 GB 的都会被认为是低端设备,因为这些设备 service worker 的启动时间会是不可接受的。

另一个考虑因素是可用的存储空间大小,因为可能需要几兆字节来缓存将来会使用的完整资源集。navigator.storage 接口(https://developers.google.com/web/updates/2017/08/estimating-available-storage-space)允许谷歌搜索页面提前确定,缓存数据的尝试是否会因存储空间分配失败而面临失败的风险。

这样,搜索团队就可以使用多个条件来决定是否使用 service worker:如果用户使用支持 navigation preload 的浏览器访问谷歌搜索页,并且所用设备拥有至少 2 GB 内存和足够的存储空间,那么就会注册 service worker(https://developers.google.com/web/updates/2017/08/estimating-available-storage-space)。否则就不会使用 service worker,但用户仍然拥有和以前一样的谷歌搜索体验。

这种选择性注册的一个好处是能够推出一个更小、更高效的 service worker。以现代的浏览器为目标来运行 service worker,消除了旧浏览器所需的 transpilation 和 polyfills 产生的开销。这最终从 service worker 实现的总大小中减少了大约 8 KB 的未压缩 JavaScript 代码。

问题:service worker 作用域

当搜索团队进行了足够多的延迟实验,并且相信 navigation preload 为他们提供了一个可行、无延迟的途径来使用 service worker 时,一些实际问题就开始出现。其中一个问题与 service worker 的作用域规则(https://developers.google.com/web/ilt/pwa/introduction-to-service-worker#registration_and_scope)有关。service worker 的作用域基于 URL 前缀确定,决定了它可以控制哪些页面。对于运行单个 web app 的域,这不是问题,因为通常只需使用最大作用域 / 的 service worker,它可以控制域下的所有页面。但是谷歌搜索的网址结构要复杂一些。

如果指定 service worker 的作用域为最大的 /,它最终能够控制任何在 www.google.com(或同等区域)下的页面,而该域下有许多与谷歌搜索无关的 URL。一个更合理、更具限制性的作用域是 /search,这至少会消除与搜索结果完全无关的 URL。

不幸的是,/search URL 被不同类型的谷歌搜索共享,由 URL 查询参数来决定显示特定类型的搜索结果。其中一些类型使用了与传统 web 搜索结果页面完全不同的代码库,例如,图像搜索和购物搜索都是在 /search URL 下使用不同的查询参数来提供服务的,但是这些搜索类型目前还没有准备提供自己的 service worker 体验。

解决方案:开发一个分发和路由框架

虽然已经有一些提议(https://github.com/w3c/ServiceWorker/issues/1373)允许使用比 URL 前缀更强大的方式去决定 service worker 的作用域,但谷歌搜索团队一直在尝试部署一个对所控制页面子集不起任何作用的 service worker。

为了解决这个问题,谷歌搜索团队构建了一个定制的调度和路由框架,该框架可以被配置去检查诸如客户端页面查询参数之类的条件,并根据这些条件来确定要执行的特定代码路径。与硬编码规则不同,该系统是灵活的,允许那些共享 URL 空间的团队,如图像搜索和购物搜索,在他们决定使用 service worker 时将自己 service worker 的逻辑放入其中。

虽然这个定制的解决方案是谷歌内部的,但是相同的原则可以应用于任何包含不同逻辑并在同一 URL 下 的 web app 的域。

问题:个性化结果和指标

用户可以使用他们的谷歌账号登陆谷歌搜索,也可以根据自己的特定帐户数据自定义搜索结果体验。登录用户由特定的cookies(https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies)标识,这是一个被广泛支持的标准。

使用 cookies 的一个缺点是,在 service worker 中不能访问 cookies,因此无法检查它的值,并确保它不会因用户注销或切换账户而发生改变。(已经在努力让 service worker 能够访问 cookies(https://developers.google.com/web/updates/2018/09/asynchronous-access-to-http-cookies#welcome_service_workers),但是目前还是实验性的(https://developers.google.com/web/updates/2018/09/asynchronous-access-to-http-cookies#origin-trial),并没有被广泛支持)

如果实际登录的用户和 service worker 认为目前登录的用户不匹配,会导致错误的个性化搜索结果、指标和日志记录。对于谷歌搜索团队来说,这些都是严重的问题。

解决方案:使用 postMessage 发送 cookies

谷歌搜索团队没有等待实验性 API 的发布以获得在 service worker 对 cookies 的直接访问,而是采用了一种权宜之计:每当被 service worker 控制的页面被加载时,相关的 cooikes 会被读取并通过 postMessage()(https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage)发送到 service worker。

service worker 根据预期值检查目前 cookies 的值,如果不匹配,就按照一定步骤清除存储中特定的用户数据,并重新加载搜索结果页而不会有不正确的个性化设置。

虽然 service worker 将数据重置成基础值所采取的具体步骤是由谷歌搜索的需求决定的,但对于需要获得浏览器 cookies 个性化数据的其他开发者来说,这一通用做法可能会很有用。

问题:实验与动态性

前面提到,在默认使用新代码和功能前,谷歌搜索团队在很大程度上依赖于在生产环境中运行实验和在现实中效果的测试。而对于严重依赖缓存数据的静态 service worker 来说,这会有一些挑战,因为选择用户进入与离开实验通常需要与后端服务器进行通信。

解决方案:动态生成 service worker 脚本

团队采用的解决方案是使用一个动态生成的 service worker 脚本,由 web 服务器为每个用户定制,而不是提前生成单个静态 service worker 脚本。会影响 service worker 行为或者网络请求的实验信息通常直接包含在这个定制的 service worker 脚本中,并且通过组合传统技术,如 cookies 和在注册有 service worker 的 URL 使用更新的代码,来改变用户有效的体验集合。

使用动态生成的 service worker 脚本还可以更容易地为那些需要避免的、 service worker 实现中存在的致命 bug 提供安全出口。另外动态 service worker 响应可以是一个 no-op 实现(https://stackoverflow.com/questions/33986976/how-can-i-remove-a-buggy-service-worker-or-implement-a-kill-switch/38980776#38980776),这样能够有效地为部分或所有当前用户禁用 service worker。

问题:协同更新

现实中所有 service worker 部署面临的最大挑战之一是,在优先使用缓存与确保用户能快速获得被部署到生产环境的关键更新和更改,两者之间提供一个合理的折中方案。正确的平衡取决于许多因素:

  • 您的 web app 是不是一个能持续使用的单页应用(https://en.wikipedia.org/wiki/Single-page_application),用户一直保持打开而不导航到新页面

  • 后端 web 服务器的更新部署周期是是什么

  • 普通用户是否能够容忍稍微过时的 web app 版本,或者实时性是不是最重要的

在进行 service worker 实验时,谷歌搜索团队确保实验运行在一系列预定的后端更新中,以保证指标和用户体验更接近于现实。

注意:要记住推动 service worker 落地不是一次性的部署,您需要有一个针对自身生产基础设施的流程,以确保更新能够随着时间的推移平滑地进行。

解决方案:平衡实时性和缓存利用

在测试了许多不同的配置项后,谷歌搜索团队发现以下设置在实时性和缓存利用之间提供了正确的平衡。

service worker 脚本 URL 响应的 header 包含 Cache-Control: private, max-age=1500 (1500 秒,25 分钟),并在注册 service worker 时将 updateViaCache 参数设置为 all(https://developers.google.com/web/updates/2018/06/fresher-sw#updateviacache),以确保 header 生效。

正如您所想象的那样,谷歌搜索 web 后端是一个大型的、分布在全球的服务器集,需要有尽可能接近 100% 的运行时间。因此以滚动的方式来部署会影响 servicer worker 脚本内容的更改。

如果用户命中了一个已更新的后端,然后快速导航到另一个后端没有更新的页面,那么他们会在 service worker 的版本之间多次切换。因此,通知浏览器在上次检查的25分钟后,只需要检查更新后的脚本,就不会有明显的负面影响。选择加入这个措施的好处是显著减少了动态生成 service worker 脚本的端点接收的流量。

此外,service worker 脚本的 HTTP 响应设置了 ETag header,确保在经过25分钟后进行更新检查时,如果在此期间没有部署任何 service worker 更新,则服务器可以使用 HTTP 304(https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304) 有效地响应。

虽然谷歌搜索 web app 的某些交互确实使用单页应用风格的导航(即通过 History API(https://developer.mozilla.org/en-US/docs/Web/API/History_API#Adding_and_modifying_history_entries)),但在大多数情况下,谷歌搜索是一个使用“真实”导航的传统 web app。这一点帮助团队确定了使用这两个选项:clients.claim()(https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim) 和 skipWaiting()(https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_the_waiting_phase),能够有效加速更新 service worker 生命周期。在谷歌搜索的界面上点击通常会导航到新的 HTML 文档,调用 skipWaiting() 可以确保更新的 service worker 有机会在安装后立即处理这些新的 navigation 请求。类似地,调用 clients.claim() 意味着更新的 service worker 将有机会在激活后有机会控制哪些已打开的、不受控制的谷歌搜索页面。

谷歌搜索团队所采用的方法不一定适用于所有人,这是他们对各种选项组合经过认真仔细的 A/B 测试并直到找出最适合的方案的结果。对于那些后端基础设施允许快速部署更新的开发人员来说,可能更倾向于让浏览器始终忽略 HTTP 缓存(https://developers.google.com/web/updates/2018/06/fresher-sw#whats_changing),尽可能频繁地检查是否有更新的 service worker 脚本。如果您正在构建一个用户可能会长时间保持打开状态的单页应用,那么使用 skipwaiting() 可能不是正确选择,因为如果您允许新的 service worker 在有保持打开的客户端的情况下激活,这样可能会有缓存不一致(https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#skip_the_waiting_phase)的风险。

关键要点

默认情况下,service worker 不是性能无关

为您的 web app 添加 service worker 意味着插入一段额外的 JavaScript 代码,这些代码需要在您的 web app 响应被接收前加载与执行。如果这些响应来自于本地缓存而不是网络,那么由于从缓存优先获得的性能优势,运行 service worker 的开销通常可以忽略不计。但是,如果您知道 service worker 在处理 navigation 请求时总是需要经过网络,那么使用 navigation preload 对于性能至关重要。

Service worker 是渐进增强的

现在对 service worker 支持的情况比去年好很多。所有现代浏览器目前至少都支持一些 service worker 特性(https://jakearchibald.github.io/isserviceworkerready/),但不幸的是,一些先进的特性如 background sync 和 navigation preload,还没有得到普遍推广。因此仍然有必要对您需要的特定子集的特性做检查,并只在有这些特性的时候才注册 service worker。

同样,如果您也通过实验知道了低端设备会因为 service worker 的额外开销而表现很差,您也可以避免在低端设备注册 service worker。

总之,您应该继续将 service worker 视为一个渐进式的增强(https://en.wikipedia.org/wiki/Progressive_enhancement),当所有的先决条件都满足时,它能被添加到 web app 中,提升体验和总体加载性能。

一切以数据指标为准

唯一能够弄清楚 service worker 对用户体验是否有正面或负面影响的方法是实验和测量结果。

设置有意义的度量的具体细节取决于您正在使用的分析程序,以及您在部署设置中如何正常进行实验。这个例子(https://developers.google.com/web/showcase/2016/service-worker-perf)根据 Google I/O web app 对 service worker 的使用经验,详细介绍了一种使用 Google Analytics 收集指标的方法。

非目标

虽然许多 web 开发社区通常将 service worker 和 PWA(https://developers.google.com/web/progressive-web-apps/) 联系在一起,但是开发“谷歌搜索 PWA”并不是团队的最初目标。谷歌搜索 web app 目前不通过 web app manifest(https://developers.google.com/web/fundamentals/web-app-manifest/) 提供 metadata,也不建议用户将其添加到主屏幕(https://developers.google.com/web/fundamentals/app-install-banners/)。搜索团队目前对用户通过传统的入口进入谷歌搜索这一方式感到满意。

与其试图将谷歌搜索 web 体验转化为与已安装应用程序相同的效果,不如将重点放在最初的推广上,逐步增强现有网站。

总结

以上就是原文的全部内容,文章对谷歌搜索引入 service worker 的原因、问题和解决方案进行了详细的介绍,向读者展示了谷歌搜索团队是如何谨慎地引入新技术和巧妙地解决遇到的问题。虽然文章中的使用场景是谷歌搜索,但是对想学习和使用 service worker 的开发者还是非常具有借鉴意义的,希望读者能够从中取得收获,使用 service worker 进一步提高自己的 web app 的体验。

另外推荐一本开源书籍《PWA 应用实战》。该书由百度 Web 生态团队撰写与分享,记录了团队过去两年积累的 PWA 方面的经验,欢迎对 Web 和 PWA 有浓厚兴趣的读者加入我们,一起来维护这本书。

Brilliant Open Web 

BOW(Brilliant Open Web)团队,是一个专门的Web技术建设小组,致力于推动 Open Web 技术的发展,让Web重新成为开发者的首选。

BOW 关注前端,关注Web;剖析技术、分享实践;谈谈学习,也聊聊管理。

关注 OpenWeb开发者,让我们一起推动 OpenWeb技术的发展!

OpenWeb开发者

ID:BrilliantOpenWeb

技术连接世界,开放赢得未来

谷歌浏览器未发送任何数据_将 service worker 引入谷歌搜索相关推荐

  1. 点赞封面未发送已删除_连朋友圈封面都点赞的人,到底是哪些人

    大多数人的暗恋都有这样的敏感和小心翼翼: 喜欢一个人的时候,会不厌其烦地一遍一遍地刷TA的朋友圈,每一条都细细研究. 只是从不评论从不点赞,默默截图. 但是朋友圈的封面图有点特别,只有特意去看朋友圈的 ...

  2. 点赞封面未发送已删除_“每日优鲜APP发送商业短信案”宣判 法院认定退订费平台负担|退订|隐私政策|优鲜|资费...

    封面新闻记者 粟裕 王女士注册使用"每日优鲜APP"后,"每日优鲜APP"向其发送了含有每日优鲜推广内容的商业短信,王女士按照短信指引回复"N&quo ...

  3. 点赞封面未发送已删除_微信表白新功能:有种喜欢,是给你朋友圈封面点赞

    微信最成功的设计,就是朋友圈封面点赞. 你的朋友圈封面被多少人点赞过,按照概率,这个次数的五分之一,应该就是喜欢过你的人. 有读者说:大叔,为什么他又赞了我朋友圈封面? 其实答案很简单. 给你朋友圈封 ...

  4. 电脑连接网络后显示服务器未发送任何数据因而无法载入该网页的解决办法

    最近科学上网,结果上完网后连不上学校vpn了,但是自己的电脑能连自己的热点.看了很多解决办法,发现了这个对自己这种情况有用的办法.给跟我情况有用的同学提供一点帮助. 1,可能是代理服务器的问题,找到自 ...

  5. 利用python从网页查找数据_利用Python模拟淘宝的搜索过程并对数据进行可视化分析...

    数据挖掘入门与实战 公众号: datadw 本文讲述如何利用Python模拟淘宝的搜索过程并对搜索结果进行初步的数据可视化分析. 搜索过程的模拟:淘宝的搜索页面有两种形式, 一种形式是, 2019/2 ...

  6. Service Worker

    Service Worker 随着前端快速发展,应用的性能已经变得至关重要,关于这一点大佬做了很多统计.你可以去看看. 如何降低一个页面的网络请求成本从而缩短页面加载资源的时间并降低用户可感知的延时是 ...

  7. 缓存存在那些位置?缓存位置可分Service Worker、Memory Cache、Disk Cache、Push Cache四种

    从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络. Service Worker Memory Cache Disk Cache Push Cache Se ...

  8. websphere mq 查看队列中是否有数据_全网最全的 “消息队列”

    消息队列的使用场景 以下介绍消息队列在实际应用常用的使用场景.异步处理.应用解耦.流量削锋和消息通讯四个场景. 1]异步处理:场景说明:用户注册后,需要发注册邮件和注册短信. 引入消息队列后架构如下: ...

  9. 科学价值 社交关系 大数据_服务的价值:数据科学和用户体验研究美好生活

    科学价值 社交关系 大数据 A crucial part of building a product is understanding exactly how it provides your cus ...

  10. 谷歌浏览器32位安装包_谷歌浏览器发布紧急安全更新修复Blink内核中的任意代码执行漏洞...

    上月底谷歌浏览器推送紧急安全更新对浏览器漏洞进行修复,当时谷歌浏览器博客并未公布漏洞的具体细节信息. 蓝点网当时也在文章中称通常这种不公布漏洞的更新,都是比较严重的问题因此只有等多数用户修复后才会公开 ...

最新文章

  1. 日常运维管理技巧十六(iftop网卡流量监控工具)(转载)
  2. 文本读取写入易错问题
  3. 树莓派4视频输出接口_树莓派第四代来啦!4G内存、支持双屏4K输出和H265硬解
  4. ATL服务器:用 Visual C++创建的高性能的Web应用程序和XML Web 服务
  5. 用 Go 构建一个区块链 ---- Part 1: 基本原型
  6. centos rpm 安装 perl_Linux【常用软件安装篇】
  7. 理解 Linux 中 `ls` 的输出
  8. http通道连接mysql_通过http tunnel连接mysql
  9. 1.3编程基础之算术表达式与顺序执行 19 AXB问题
  10. 炸掉卡西欧991CNX
  11. 北京飞马贸易借沟通CTBS实现总部与分公司同步做帐
  12. android 4.4.2海信电视,ROOT海信电视Android4.0的详细步骤
  13. 微服务架构师的道、法、术
  14. Unity Rendering Mode
  15. 通用计算机按其规模速度和功能,电脑基础知识详解
  16. 百度地图总结第三篇之定位(我的位置)
  17. 三分法求点到抛物线的最短距离
  18. HTML表单元素Emil和密码
  19. 【学习笔记】OFDM的原理和技术介绍以及仿真结果分析附代码--MATLAB
  20. 共享白板程序源代码_教育白板代码共享

热门文章

  1. GD32VF103启动流程分析
  2. 文本分割器TXTSpliter
  3. 网络蚂蚁(netants) v1.25 中文版 绿色
  4. java numberformat_NumberFormat(数字格式化类)
  5. 【Java 8 in Action】Stream
  6. Linux Vi 文本编辑器常用命令
  7. HeapSnap工具原理及其应用
  8. 计算机黑屏但是有鼠标,电脑桌面黑屏怎么解决 电脑黑屏怎么办 - 云骑士一键重装系统...
  9. Android友盟分享(微信简单集成)
  10. 俄罗斯方块c语言教程codeblocks,C语言俄罗斯方块修改结尾