首页 文章 精选 留言 我的

精选列表

搜索[互动课堂],共7835篇文章
优秀的个人博客,低调大师

网易云课堂 Service Worker 运用与实践

前言 本文首先会简单介绍下前端的常见缓存方式,再引入Service Worker的概念,针对其原理和如何运用进行介绍。然后基于google推出的第三方库Workbox,在产品中进行运用实践,并对其原理进行简要剖析。 作者:刘放 编辑:Ein 前端缓存简介 先简单介绍一下现有的前端缓存技术方案,主要分为http缓存和浏览器缓存。 http缓存 http缓存都是第二次请求时开始的,这也是个老生常谈的话题了。无非也是那几个http头的问题: Expires HTTP1.0的内容,服务器使用Expires头来告诉Web客户端它可以使用当前副本,直到指定的时间为止。 Cache-Control HTTP1.1引入了Cathe-Control,它使用max-age指定资源被缓存多久,主要是解决了Expires一个重大的缺陷,就是它设置的是一个固定的时间点,客户端时间和服务端时间可能有误差。 所以一般会把两个头都带上,这种缓存称为强缓存,表现形式为: Last-Modified / If-Modified-Since Last-Modified是服务器告诉浏览器该资源的最后修改时间,If-Modified-Since是请求头带上的,上次服务器给自己的该资源的最后修改时间。然后服务器拿去对比。 若资源的最后修改时间大于If-Modified-Since,说明资源又被改动过,则响应整片资源内容,返回状态码200; 若资源的最后修改时间小于或等于If-Modified-Since,说明资源无新修改,则响应HTTP 304,告知浏览器继续使用当前版本。 Etag / If-None-Match 前面提到由文件的修改时间来判断文件是否改动,还是会带来一定的误差,比如注释等无关紧要的修改等。所以推出了新的方式。 Etag是由服务端特定算法生成的该文件的唯一标识,而请求头把返回的Etag值通过If-None-Match再带给服务端,服务端通过比对从而决定是否响应新内容。这也是304缓存。 浏览器缓存 Storage 简单的缓存方式有cookie,localStorage和sessionStorage。这里就不详细介绍他们的区别了,这里说下通过localStorage来缓存静态资源的优化方案。 localStorage通常有5MB的存储空间,我们以微信文章页为例。 查看请求发现,基本没有js和css的请求,因为它把全部的不需要改动的资源都放到了localStorage中: 所以微信的文章页加载非常的快。 前端数据库 前端数据库有WebSql和IndexDB,其中WebSql被规范废弃,他们都有大约50MB的最大容量,可以理解为localStorage的加强版。 应用缓存 应用缓存主要是通过manifest文件来注册被缓存的静态资源,已经被废弃,因为他的设计有些不合理的地方,他在缓存静态文件的同时,也会默认缓存html文件。这导致页面的更新只能通过manifest文件中的版本号来决定。所以,应用缓存只适合那种常年不变化的静态网站。如此的不方便,也是被废弃的重要原因。 PWA也运用了该文件,不同于manifest简单的将文件通过是否缓存进行分类,PWA用manifest构建了自己的APP骨架,并运用Servie Worker来控制缓存,这也是今天的主角。 Service Worker Service Worker本质上也是浏览器缓存资源用的,只不过他不仅仅是Cache,也是通过worker的方式来进一步优化。 他基于h5的web worker,所以绝对不会阻碍当前js线程的执行,sw最重要的工作原理就是: 1、后台线程:独立于当前网页线程; 2、网络代理:在网页发起请求时代理,来缓存文件。 兼容性 可以看到,基本上新版浏览器还是兼容滴。之前是只有chrome和firefox支持,现在微软和苹果也相继支持了。 成熟程度 判断一个技术是否值得尝试,肯定要考虑下它的成熟程度,否则过一段时间又和应用缓存一样被规范抛弃就尴尬了。 所以这里我列举了几个使用Service Worker的页面: 淘宝 网易新闻 考拉 所以说还是可以尝试下的。 调试方法 一个网站是否启用Service Worker,可以通过开发者工具中的Application来查看: 被Service Worker缓存的文件,可以在Network中看到Size项为from Service Worker: 也可以在Application的Cache Storage中查看缓存的具体内容: 如果是具体的断点调试,需要使用对应的线程,不再是main线程了,这也是webworker的通用调试方法: 使用条件 sw 是基于 HTTPS 的,因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。如果是本地调试的话,localhost是可以的。 而我们刚好全站强制https化,所以正好可以使用。 生命周期 大概可以用如下图片来解释: 注册 要使用Service Worker,首先需要注册一个sw,通知浏览器为该页面分配一块内存,然后sw就会进入安装阶段。 一个简单的注册方式: (function() { if('serviceWorker' in navigator) { navigator.serviceWorker.register('./sw.js'); } })() 当然也可以考虑全面点,参考网易新闻的注册方式: "serviceWorker" in navigator && window.addEventListener("load", function() { var e = location.pathname.match(/\/news\/[a-z]{1,}\//)[0] + "article-sw.js?v=08494f887a520e6455fa"; navigator.serviceWorker.register(e).then(function(n) { n.onupdatefound = function() { var e = n.installing; e.onstatechange = function() { switch (e.state) { case "installed": navigator.serviceWorker.controller ? console.log("New or updated content is available.") : console.log("Content is now available offline!"); break; case "redundant": console.error("The installing service worker became redundant.") } } } }). catch(function(e) { console.error("Error during service worker registration:", e) }) }) 前面提到过,由于sw会监听和代理所有的请求,所以sw的作用域就显得额外的重要了,比如说我们只想监听我们专题页的所有请求,就在注册时指定路径: navigator.serviceWorker.register('/topics/sw.js'); 这样就只会对topics/下面的路径进行优化。 installing 我们注册后,浏览器就会开始安装sw,可以通过事件监听: //service worker安装成功后开始缓存所需的资源 var CACHE_PREFIX = 'cms-sw-cache'; var CACHE_VERSION = '0.0.20'; var CACHE_NAME = CACHE_PREFIX+'-'+CACHE_VERSION; var allAssets = [ './main.css' ]; self.addEventListener('install', function(event) { //调试时跳过等待过程 self.skipWaiting(); // Perform install steps //首先 event.waitUntil 你可以理解为 new Promise, //它接受的实际参数只能是一个 promise,因为,caches 和 cache.addAll 返回的都是 Promise, //这里就是一个串行的异步加载,当所有加载都成功时,那么 SW 就可以下一步。 //另外,event.waitUntil 还有另外一个重要好处,它可以用来延长一个事件作用的时间, //这里特别针对于我们 SW 来说,比如我们使用 caches.open 是用来打开指定的缓存,但开启的时候, //并不是一下就能调用成功,也有可能有一定延迟,由于系统会随时睡眠 SW,所以,为了防止执行中断, //就需要使用 event.waitUntil 进行捕获。另外,event.waitUntil 会监听所有的异步 promise //如果其中一个 promise 是 reject 状态,那么该次 event 是失败的。这就导致,我们的 SW 开启失败。 event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { console.log('[SW]: Opened cache'); return cache.addAll(allAssets); }) ); }); 安装时,sw就开始缓存文件了,会检查所有文件的缓存状态,如果都已经缓存了,则安装成功,进入下一阶段。 activated 如果是第一次加载sw,在安装后,会直接进入activated阶段,而如果sw进行更新,情况就会显得复杂一些。流程如下: 首先老的sw为A,新的sw版本为B。 B进入install阶段,而A还处于工作状态,所以B进入waiting阶段。只有等到A被terminated后,B才能正常替换A的工作。 这个terminated的时机有如下几种方式: 1、关闭浏览器一段时间; 2、手动清除Service Worker; 3、在sw安装时直接跳过waiting阶段 //service worker安装成功后开始缓存所需的资源 self.addEventListener('install', function(event) { //跳过等待过程 self.skipWaiting(); }); 然后就进入了activated阶段,激活sw工作。 activated阶段可以做很多有意义的事情,比如更新存储在Cache中的key和value: var CACHE_PREFIX = 'cms-sw-cache'; var CACHE_VERSION = '0.0.20'; /** * 找出对应的其他key并进行删除操作 * @returns {*} */ function deleteOldCaches() { return caches.keys().then(function (keys) { var all = keys.map(function (key) { if (key.indexOf(CACHE_PREFIX) !== -1 && key.indexOf(CACHE_VERSION) === -1){ console.log('[SW]: Delete cache:' + key); return caches.delete(key); } }); return Promise.all(all); }); } //sw激活阶段,说明上一sw已失效 self.addEventListener('activate', function(event) { event.waitUntil( // 遍历 caches 里所有缓存的 keys 值 caches.keys().then(deleteOldCaches) ); }); idle 这个空闲状态一般是不可见的,这种一般说明sw的事情都处理完毕了,然后处于闲置状态了。 浏览器会周期性的轮询,去释放处于idle的sw占用的资源。 fetch 该阶段是sw最为关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作。 所有的缓存部分,都是在该阶段,这里举一个简单的例子: //监听浏览器的所有fetch请求,对已经缓存的资源使用本地缓存回复 self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { //该fetch请求已经缓存 if (response) { return response; } return fetch(event.request); } ) ); }); 生命周期大概讲清楚了,我们就以一个具体的例子来说明下原生的serviceworker是如何在生产环境中使用的吧。 举个栗子 我们可以以网易新闻的wap页为例,其针对不怎么变化的静态资源开启了sw缓存,具体的sw.js逻辑和解读如下: 'use strict'; //需要缓存的资源列表 var precacheConfig = [ ["https://static.ws.126.net/163/wap/f2e/milk_index/bg_img_sm_minfy.png", "c4f55f5a9784ed2093009dadf1e954f9"], ["https://static.ws.126.net/163/wap/f2e/milk_index/change.png", "9af1b102ef784b8ff08567ba25f31d95"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-download.png", "1c02c724381d77a1a19ca18925e9b30c"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-login-dark.png", "b59ba5abe97ff29855dfa4bd3a7a9f35"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-refresh.png", "a5b1084e41939885969a13f8dbc88abd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon-video-play.png", "065ff496d7d36345196d254aff027240"], ["https://static.ws.126.net/163/wap/f2e/milk_index/icon.ico", "a14e5365cc2b27ec57e1ab7866c6a228"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.eot", "e4d2788fef09eb0630d66cc7e6b1ab79"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.svg", "d9e57c341608fddd7c140570167bdabb"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.ttf", "f422407038a3180bb3ce941a4a52bfa2"], ["https://static.ws.126.net/163/wap/f2e/milk_index/iconfont_1.woff", "ead2bef59378b00425779c4ca558d9bd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/index.5cdf03e8.js", "6262ac947d12a7b0baf32be79e273083"], ["https://static.ws.126.net/163/wap/f2e/milk_index/index.bc729f8a.css", "58e54a2c735f72a24715af7dab757739"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-bohe.png", "ac5116d8f5fcb3e7c49e962c54ff9766"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-mail.png", "a12bbfaeee7fbf025d5ee85634fca1eb"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-manhua.png", "b8905b119cf19a43caa2d8a0120bdd06"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-open.png", "b7cc76ba7874b2132f407049d3e4e6e6"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-app-read.png", "e6e9c8bc72f857960822df13141cbbfd"], ["https://static.ws.126.net/163/wap/f2e/milk_index/logo-site.png", "2b0d728b46518870a7e2fe424e9c0085"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_no_pic.png", "aef80885188e9d763282735e53b25c0e"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_pc.png", "42f3cc914eab7be4258fac3a4889d41d"], ["https://static.ws.126.net/163/wap/f2e/milk_index/version_standard.png", "573408fa002e58c347041e9f41a5cd0d"] ]; var cacheName = 'sw-precache-v3-new-wap-index-' + (self.registration ? self.registration.scope : ''); var ignoreUrlParametersMatching = [/^utm_/]; var addDirectoryIndex = function(originalUrl, index) { var url = new URL(originalUrl); if (url.pathname.slice(-1) === '/') { url.pathname += index; } return url.toString(); }; var cleanResponse = function(originalResponse) { // If this is not a redirected response, then we don't have to do anything. if (!originalResponse.redirected) { return Promise.resolve(originalResponse); } // Firefox 50 and below doesn't support the Response.body stream, so we may // need to read the entire body to memory as a Blob. var bodyPromise = 'body' in originalResponse ? Promise.resolve(originalResponse.body) : originalResponse.blob(); return bodyPromise.then(function(body) { // new Response() is happy when passed either a stream or a Blob. return new Response(body, { headers: originalResponse.headers, status: originalResponse.status, statusText: originalResponse.statusText }); }); }; var createCacheKey = function(originalUrl, paramName, paramValue, dontCacheBustUrlsMatching) { // Create a new URL object to avoid modifying originalUrl. var url = new URL(originalUrl); // If dontCacheBustUrlsMatching is not set, or if we don't have a match, // then add in the extra cache-busting URL parameter. if (!dontCacheBustUrlsMatching || !(url.pathname.match(dontCacheBustUrlsMatching))) { url.search += (url.search ? '&' : '') + encodeURIComponent(paramName) + '=' + encodeURIComponent(paramValue); } return url.toString(); }; var isPathWhitelisted = function(whitelist, absoluteUrlString) { // If the whitelist is empty, then consider all URLs to be whitelisted. if (whitelist.length === 0) { return true; } // Otherwise compare each path regex to the path of the URL passed in. var path = (new URL(absoluteUrlString)).pathname; return whitelist.some(function(whitelistedPathRegex) { return path.match(whitelistedPathRegex); }); }; var stripIgnoredUrlParameters = function(originalUrl, ignoreUrlParametersMatching) { var url = new URL(originalUrl); // Remove the hash; see https://github.com/GoogleChrome/sw-precache/issues/290 url.hash = ''; url.search = url.search.slice(1) // Exclude initial '?' .split('&') // Split into an array of 'key=value' strings .map(function(kv) { return kv.split('='); // Split each 'key=value' string into a [key, value] array }) .filter(function(kv) { return ignoreUrlParametersMatching.every(function(ignoredRegex) { return !ignoredRegex.test(kv[0]); // Return true iff the key doesn't match any of the regexes. }); }) .map(function(kv) { return kv.join('='); // Join each [key, value] array into a 'key=value' string }) .join('&'); // Join the array of 'key=value' strings into a string with '&' in between each return url.toString(); }; var hashParamName = '_sw-precache'; //定义需要缓存的url列表 var urlsToCacheKeys = new Map( precacheConfig.map(function(item) { var relativeUrl = item[0]; var hash = item[1]; var absoluteUrl = new URL(relativeUrl, self.location); var cacheKey = createCacheKey(absoluteUrl, hashParamName, hash, false); return [absoluteUrl.toString(), cacheKey]; }) ); //把cache中的url提取出来,进行去重操作 function setOfCachedUrls(cache) { return cache.keys().then(function(requests) { //提取url return requests.map(function(request) { return request.url; }); }).then(function(urls) { //去重 return new Set(urls); }); } //sw安装阶段 self.addEventListener('install', function(event) { event.waitUntil( //首先尝试取出存在客户端cache中的数据 caches.open(cacheName).then(function(cache) { return setOfCachedUrls(cache).then(function(cachedUrls) { return Promise.all( Array.from(urlsToCacheKeys.values()).map(function(cacheKey) { //如果需要缓存的url不在当前cache中,则添加到cache if (!cachedUrls.has(cacheKey)) { //设置same-origin是为了兼容旧版本safari中其默认值不为same-origin, //只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息 var request = new Request(cacheKey, {credentials: 'same-origin'}); return fetch(request).then(function(response) { //通过fetch api请求资源 if (!response.ok) { throw new Error('Request for ' + cacheKey + ' returned a ' + 'response with status ' + response.status); } return cleanResponse(response).then(function(responseToCache) { //并设置到当前cache中 return cache.put(cacheKey, responseToCache); }); }); } }) ); }); }).then(function() { //强制跳过等待阶段,进入激活阶段 return self.skipWaiting(); }) ); }); self.addEventListener('activate', function(event) { //清除cache中原来老的一批相同key的数据 var setOfExpectedUrls = new Set(urlsToCacheKeys.values()); event.waitUntil( caches.open(cacheName).then(function(cache) { return cache.keys().then(function(existingRequests) { return Promise.all( existingRequests.map(function(existingRequest) { if (!setOfExpectedUrls.has(existingRequest.url)) { //cache中删除指定对象 return cache.delete(existingRequest); } }) ); }); }).then(function() { //self相当于webworker线程的当前作用域 //当一个 service worker 被初始注册时,页面在下次加载之前不会使用它。 claim() 方法会立即控制这些页面 //从而更新客户端上的serviceworker return self.clients.claim(); }) ); }); self.addEventListener('fetch', function(event) { if (event.request.method === 'GET') { // 标识位,用来判断是否需要缓存 var shouldRespond; // 对url进行一些处理,移除一些不必要的参数 var url = stripIgnoredUrlParameters(event.request.url, ignoreUrlParametersMatching); // 如果该url不是我们想要缓存的url,置为false shouldRespond = urlsToCacheKeys.has(url); // 如果shouldRespond未false,再次验证 var directoryIndex = 'index.html'; if (!shouldRespond && directoryIndex) { url = addDirectoryIndex(url, directoryIndex); shouldRespond = urlsToCacheKeys.has(url); } // 再次验证,判断其是否是一个navigation类型的请求 var navigateFallback = ''; if (!shouldRespond && navigateFallback && (event.request.mode === 'navigate') && isPathWhitelisted([], event.request.url)) { url = new URL(navigateFallback, self.location).toString(); shouldRespond = urlsToCacheKeys.has(url); } // 如果标识位为true if (shouldRespond) { event.respondWith( caches.open(cacheName).then(function(cache) { //去缓存cache中找对应的url的值 return cache.match(urlsToCacheKeys.get(url)).then(function(response) { //如果找到了,就返回value if (response) { return response; } throw Error('The cached response that was expected is missing.'); }); }).catch(function(e) { // 如果没找到则请求该资源 console.warn('Couldn\'t serve response for "%s" from cache: %O', event.request.url, e); return fetch(event.request); }) ); } } }); 这里的策略大概就是优先在Cache中寻找资源,如果找不到再请求资源。可以看出,为了实现一个较为简单的缓存,还是比较复杂和繁琐的,所以很多工具就应运而生了。 Workbox 由于直接写原生的sw.js,比较繁琐和复杂,所以一些工具就出现了,而Workbox是其中的佼佼者,由google团队推出。 简介 在 Workbox 之前,GoogleChrome 团队较早时间推出过 sw-precache 和 sw-toolbox 库,但是在 GoogleChrome 工程师们看来,workbox 才是真正能方便统一的处理离线能力的更完美的方案,所以停止了对 sw-precache 和 sw-toolbox 的维护。 使用者 有很多团队也是启用该工具来实现serviceworker的缓存,比如说: 淘宝首页 网易新闻wap文章页 百度的Lavas 基本配置 首先,需要在项目的sw.js文件中,引入Workbox的官方js,这里用了我们自己的静态资源: importScripts( "https://edu-cms.nosdn.127.net/topics/js/workbox_9cc4c3d662a4266fe6691d0d5d83f4dc.js" ); 其中importScripts是webworker中加载js的方式。 引入Workbox后,全局会挂载一个Workbox对象 if (workbox) { console.log('workbox加载成功'); } else { console.log('workbox加载失败'); } 然后需要在使用其他的api前,提前使用配置 //关闭控制台中的输出 workbox.setConfig({ debug: false }); 也可以统一指定存储时Cache的名称: //设置缓存cachestorage的名称 workbox.core.setCacheNameDetails({ prefix:'edu-cms', suffix:'v1' }); precache Workbox的缓存分为两种,一种的precache,一种的runtimecache。 precache对应的是在installing阶段进行读取缓存的操作。它让开发人员可以确定缓存文件的时间和长度,以及在不进入网络的情况下将其提供给浏览器,这意味着它可以用于创建Web离线工作的应用。 工作原理 首次加载Web应用程序时,Workbox会下载指定的资源,并存储具体内容和相关修订的信息在indexedDB中。 当资源内容和sw.js更新后,Workbox会去比对资源,然后将新的资源存入Cache,并修改indexedDB中的版本信息。 我们举一个例子: workbox.precaching.precacheAndRoute([ './main.css' ]); indexedDB中会保存其相关信息 这个时候我们把main.css的内容改变后,再刷新页面,会发现除非强制刷新,否则Workbox还是会读取Cache中存在的老的main.css内容。 即使我们把main.css从服务器上删除,也不会对页面造成影响。 所以这种方式的缓存都需要配置一个版本号。在修改sw.js时,对应的版本也需要变更。 使用实践 当然了,一般我们的一些不经常变的资源,都会使用cdn,所以这里自然就需要支持域外资源了,配置方式如下: var fileList = [ { url:'https://edu-cms.nosdn.127.net/topics/js/cms_specialWebCommon_js_f26c710bd7cd055a64b67456192ed32a.js' }, { url:'https://static.ws.126.net/163/frontend/share/css/article.207ac19ad70fd0e54d4a.css' } ]; //precache 适用于支持跨域的cdn和域内静态资源 workbox.precaching.suppressWarnings(); workbox.precaching.precacheAndRoute(fileList, { "ignoreUrlParametersMatching": [/./] }); 这里需要对应的资源配置跨域允许头,否则是不能正常加载的。且文件都要以版本文件名的方式,来确保修改后Cache和indexDB会得到更新。 理解了原理和实践后,说明这种方式适合于上线后就不会经常变动的静态资源。 runtimecache 运行时缓存是在install之后,activated和fetch阶段做的事情。 既然在fetch阶段发送,那么runtimecache 往往应对着各种类型的资源,对于不同类型的资源往往也有不同的缓存策略。 缓存策略 Workbox提供的缓存策划有以下几种,通过不同的配置可以针对自己的业务达到不同的效果: Stale While Revalidate 这种策略的意思是当请求的路由有对应的Cache缓存结果就直接返回, 在返回Cache缓存结果的同时会在后台发起网络请求拿到请求结果并更新Cache缓存,如果本来就没有Cache缓存的话,直接就发起网络请求并返回结果,这对用户来说是一种非常安全的策略,能保证用户最快速的拿到请求的结果。 但是也有一定的缺点,就是还是会有网络请求占用了用户的网络带宽。可以像如下的方式使用State While Revalidate策略: workbox.routing.registerRoute( new RegExp('https://edu-cms\.nosdn\.127\.net/topics/'), workbox.strategies.staleWhileRevalidate({ //cache名称 cacheName: 'lf-sw:static', plugins: [ new workbox.expiration.Plugin({ //cache最大数量 maxEntries: 30 }) ] }) ); Network First 这种策略就是当请求路由是被匹配的,就采用网络优先的策略,也就是优先尝试拿到网络请求的返回结果,如果拿到网络请求的结果,就将结果返回给客户端并且写入Cache缓存。 如果网络请求失败,那最后被缓存的Cache缓存结果就会被返回到客户端,这种策略一般适用于返回结果不太固定或对实时性有要求的请求,为网络请求失败进行兜底。可以像如下方式使用Network First策略: //自定义要缓存的html列表 var cacheList = [ '/Hexo/public/demo/PWADemo/workbox/index.html' ]; workbox.routing.registerRoute( //自定义过滤方法 function(event) { // 需要缓存的HTML路径列表 if (event.url.host === 'localhost:63342') { if (~cacheList.indexOf(event.url.pathname)) return true; else return false; } else { return false; } }, workbox.strategies.networkFirst({ cacheName: 'lf-sw:html', plugins: [ new workbox.expiration.Plugin({ maxEntries: 10 }) ] }) ); Cache First 这个策略的意思就是当匹配到请求之后直接从Cache缓存中取得结果,如果Cache缓存中没有结果,那就会发起网络请求,拿到网络请求结果并将结果更新至Cache缓存,并将结果返回给客户端。这种策略比较适合结果不怎么变动且对实时性要求不高的请求。可以像如下方式使用Cache First策略: workbox.routing.registerRoute( new RegExp('https://edu-image\.nosdn\.127\.net/'), workbox.strategies.cacheFirst({ cacheName: 'lf-sw:img', plugins: [ //如果要拿到域外的资源,必须配置 //因为跨域使用fetch配置了 //mode: 'no-cors',所以status返回值为0,故而需要兼容 new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 40, //缓存的时间 maxAgeSeconds: 12 * 60 * 60 }) ] }) ); Network Only 比较直接的策略,直接强制使用正常的网络请求,并将结果返回给客户端,这种策略比较适合对实时性要求非常高的请求。 Cache Only 这个策略也比较直接,直接使用 Cache 缓存的结果,并将结果返回给客户端,这种策略比较适合一上线就不会变的静态资源请求。 举个栗子 又到了举个栗子的阶段了,这次我们用淘宝好了,看看他们是如何通过Workbox来配置Service Worker的: //首先是异常处理 self.addEventListener('error', function(e) { self.clients.matchAll() .then(function (clients) { if (clients && clients.length) { clients[0].postMessage({ type: 'ERROR', msg: e.message || null, stack: e.error ? e.error.stack : null }); } }); }); self.addEventListener('unhandledrejection', function(e) { self.clients.matchAll() .then(function (clients) { if (clients && clients.length) { clients[0].postMessage({ type: 'REJECTION', msg: e.reason ? e.reason.message : null, stack: e.reason ? e.reason.stack : null }); } }); }) //然后引入workbox importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js'); workbox.setConfig({ debug: false, modulePathPrefix: 'https://g.alicdn.com/kg/workbox/3.3.0/' }); //直接激活跳过等待阶段 workbox.skipWaiting(); workbox.clientsClaim(); //定义要缓存的html var cacheList = [ '/', '/tbhome/home-2017', '/tbhome/page/market-list' ]; //html采用networkFirst策略,支持离线也能大体访问 workbox.routing.registerRoute( function(event) { // 需要缓存的HTML路径列表 if (event.url.host === 'www.taobao.com') { if (~cacheList.indexOf(event.url.pathname)) return true; else return false; } else { return false; } }, workbox.strategies.networkFirst({ cacheName: 'tbh:html', plugins: [ new workbox.expiration.Plugin({ maxEntries: 10 }) ] }) ); //静态资源采用staleWhileRevalidate策略,安全可靠 workbox.routing.registerRoute( new RegExp('https://g\.alicdn\.com/'), workbox.strategies.staleWhileRevalidate({ cacheName: 'tbh:static', plugins: [ new workbox.expiration.Plugin({ maxEntries: 20 }) ] }) ); //图片采用cacheFirst策略,提升速度 workbox.routing.registerRoute( new RegExp('https://img\.alicdn\.com/'), workbox.strategies.cacheFirst({ cacheName: 'tbh:img', plugins: [ new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 20, maxAgeSeconds: 12 * 60 * 60 }) ] }) ); workbox.routing.registerRoute( new RegExp('https://gtms01\.alicdn\.com/'), workbox.strategies.cacheFirst({ cacheName: 'tbh:img', plugins: [ new workbox.cacheableResponse.Plugin({ statuses: [0, 200] }), new workbox.expiration.Plugin({ maxEntries: 30, maxAgeSeconds: 12 * 60 * 60 }) ] }) ); 可以看出,使用Workbox比起直接手撸来,要快很多,也明确很多。 原理 目前分析Service Worker和Workbox的文章不少,但是介绍Workbox原理的文章却不多。这里简单介绍下Workbox这个工具库的原理。 首先将几个我们产品用到的模块图奉上: 简单提几个Workbox源码的亮点。 通过Proxy按需依赖 熟悉了Workbox后会得知,它是有很多个子模块的,各个子模块再通过用到的时候按需importScript到线程中。 做到按需依赖的原理就是通过Proxy对全局对象Workbox进行代理: new Proxy(this, { get(t, s) { //如果workbox对象上不存在指定对象,就依赖注入该对象对应的脚本 if (t[s]) return t[s]; const o = e[s]; return o && t.loadModule(`workbox-${o}`), t[s]; } }) 如果找不到对应模块,则通过importScripts主动加载: /** * 加载前端模块 * @param {Strnig} t */ loadModule(t) { const e = this.o(t); try { importScripts(e), (this.s = !0); } catch (s) { throw (console.error(`Unable to import module '${t}' from '${e}'.`), s); } } 通过freeze冻结对外暴露api Workbox.core模块中提供了几个核心操作模块,如封装了indexedDB操作的DBWrapper、对Cache Storage进行读取的Cache Wrapper,以及发送请求的fetchWrapper和日志管理的logger等等。 为了防止外部对内部模块暴露出去的api进行修改,导致出现不可预估的错误,内部模块可以通过Object.freeze将api进行冻结保护: var _private = /*#__PURE__*/Object.freeze({ DBWrapper: DBWrapper, WorkboxError: WorkboxError, assert: finalAssertExports, cacheNames: cacheNames, cacheWrapper: cacheWrapper, fetchWrapper: fetchWrapper, getFriendlyURL: getFriendlyURL, logger: defaultExport }); 总结 通过对Service Worker的理解和Workbox的应用,可以进一步提升产品的性能和弱网情况下的体验。有兴趣的同学也可以对Workbox的源码细细评读,其中还有很多不错的设计模式和编程风格值得学习。 -END-

优秀的个人博客,低调大师

安全课堂:云安全七项最佳实践

云技术的使用在给组织带来诸多好处的同时,也带来了很多风险。今天我们就为大家准备了在云时代进行数据保护的七项小建议。学习如何在云时代保护自己,免受网络泄露和大规模攻击。 一、保护代码 保护代码百分之分属于企业自己的责任,而且黑客会持续不断地寻求攻破企业应用程序的途径。未经彻底测试和完全保护的代码更容易受到黑客的攻击和破坏。一定要将安全做为软件开发生命周期的一部分:包括库文件测试,插件扫描等等。 二、创建访问管理策略 登录是用户进入云平台的钥匙,所以一定要有一个可靠的访问管理策略,特别是关于那些建立在临时访问基础上的授权。将所有应用程序和云环境集成到以企业级动态目录(AD)或轻量目录访问协议(LDAP)为中心的身份验证模型,有助于将这一过程作为双重身份验证。 三、实施补丁管理办法 不打补丁的软件和系统可能会导致重大问题,所以要通过对定期更新系统流程进行梳理来保证环境安全。可以考虑开发重要性检查程序,在部署到真实环境前对所有更新进行检测,确认其不会对系统构成破坏或创建漏洞。 四、日志管理 日志检查应该是组织安全制度的重要组成部分。如今的日志已经成为了一种强大的安全工具,远比那些所谓的合规标准要有用得多。利用日志数据可以对恶意活动进行监控。 五、建立安全工具包 没有哪一款软件能够应付所有的安全需求。你必须实施能够覆盖堆栈中所有任务的深度防御策略,实施IP Tables、web应用防火墙、防病毒、入侵检测、加密和日志管理。 六、了解最新信息 好好利用互联网这个丰富的信息资源库,搜索并关注行业内发生的数据泄露和攻击,时刻对可能造成影响的最新漏洞信息保持了解。 七、了解云服务提供商的安全模式 最后,要了解云服务提供商的安全架构,并以此制定相应的计划。网络攻击一定还会发生,安全漏洞也都将会得到确认。 本文作者:佚名 来源:51CTO

优秀的个人博客,低调大师

苹果6跟淘云互动连接不上怎么回事

在iPhone主屏幕上点击打开设置图标,进入到设置页面,在页面上方找到蜂窝移动网络,点击打开 在打开的蜂窝移动网络页面中,滑动蜂窝移动数据右侧的开关按钮,关闭蜂窝数据,然后再打开,看看是否能够连接上 在iPhone主屏幕上,由下往上滑动屏幕,打开快捷菜单,打开飞行模式飞一会儿,然后再关闭,尝试一下是否可以连上网络 长按iPhone的电源键,也就是power键,直到屏幕显示滑动来关机,关机10秒左右,再长按电源键,重新开机,进入iPhone主页面之后,再打开蜂窝移动网络,尝试一下是否可以连上了 关闭iPhone,打开iPhone的卡槽,把SIM卡取下来,然后再重新安装回去,再打开iPhone,连接蜂窝数据,看是否可以连接上了 如果以上方法不行,可以先借用别人iPhone 6的SIM插入到自己的手机当中,建议是使用相同运营商的卡,比如自己使用中国移动,就借用中国移动的SIM,如果换上卡之后,可以正常上网,那么就是自己的SIM问题 如果以上方法都不能解决,那么只能尝试一下还原网络设置,在手机主屏幕上点击打开设置图标,进入iPhone设置页面,往下翻页,可以找到通用栏目,点击打开 进入通用页面,在页面的最下方可以看到还原栏目,点击打开 在还原设置页面当中,找到还原网络设置,点击打开提示:该还原设置仅仅重置了网络,对iPhone上的其他数据不会造成任何丢失,尽可放心步骤阅读10然后再重启iPhone,重新打开蜂窝移动网络,看一下是否能够正常连接了一、关蜂窝数据(1)打iPhone6 plus手机——设置——设置页面——蜂窝移网络——点击打即(2)进入蜂窝移网络页面——滑蜂窝移数据右侧关按钮——关闭蜂窝数据——再打蜂窝数据——看否能够连接网(3)打iPhone6plus主屏幕——由往滑屏幕打快捷菜单——打飞行模式飞——再关闭——看否能连二、重启iPhone6plus手机:按iPhone电源键关机——关机10秒左右——按电源键重新机——进入iPhone6plus主页面——打蜂窝移网络否连三、原手机网络设置:点击设置图标——设置页面——通用栏目——点击打——进入通用页面——原栏目点击打——原设置页面——原网络设置——点击打注意:该原设置重置网络其数据造影响输入前设置密码——重启iPhone6plus手机——重新打蜂窝移网络-看否能够连接网络.

优秀的个人博客,低调大师

杨老师课堂之JavaSe 部分面试题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kese7952/article/details/80535100 Java基础面试题 Java基础面试题 1.简述 path 和 classpath 的区别 2.请说说你对 JVM 的理解 3.char 型变量中能不能存贮一个中文汉字?请说出理由 4.简述 break、continue 和 return 语句的区别。 5.请简述方法重写和方法重载的区别? 6.请简述 Error 和 Exception 有什么区别? 7.请简述 synchronized 和 java.util.concurrent.locks.Lock 的异同点 8.进程和线程之间有什么不同 9.请简述装箱和拆箱的概念。 10.请简述 Hashtable 和 HashMap 的区别。 11.请简述使用泛型的优点。 12.简述 TCP/IP 协议的层次结构 1.简述 path 和 classpath 的区别 path:path 环境变量是系统环境变量中的一种,它用于保存一系列可执行文件的路径, 每个路径之间以分号分隔。当在命令行窗口运行一个可执行文件时,操作系统首先会在当前 目录下查找是否存在该文件,如果不存在会继续在 path 环境变量中定义的路径下去寻找这 个文件,如果仍未找到,系统会报错。 classpath:classpath 环境变量也是系统环境变量中的一种,它用于保存一系列类(.class) 文件的路径。当 Java 虚拟机需要运行一个类时,会先在 classpath 环境变量中所定义的路径 下去寻找所需的 class 文件。如果 classpath 的路径最后有分号,则在 classpath 中找不到时, 再到当前路径查找;如果最后无分号,则只在 classpath 设置的路径中查找。 2.请说说你对 JVM 的理解 ​ JVM 是 JavaVirtual Machine 的缩写,全称是 Java 虚拟机。Java 语言的一个非常重要的 特性就是跨平台性,而 Java 虚拟机是实现这一特性的关键。不同的操作系统需要使用不同 版本的虚拟机,这种方式使得 Java 语言能够“一次编写,到处运行”。Java 语言编译程序只 需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 Java 虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。 3.char 型变量中能不能存贮一个中文汉字?请说出理由 ​ char 型变量中能不能存贮一个中文汉字?请说出理由。 char 型变量是用来存储 Unicode 编码的字符的, Unicode 编码字符集中包含了汉字,所以,char 型变量中当然可以存储汉字啦。不过,如果某个特殊的汉字没有被包含在 Unicode 编码 字符集中,那么,这个 char 型变量中就不能存储这个特殊汉字。补充说明:Unicode 编码占 用两个字节,所以 char 类型的变量也是占用两个字节。 4.简述 break、continue 和 return 语句的区别。 break 语句:在 switch 条件语句和循环语句中都可以使用 break 语句。当它出现在 switch 条件语句中时,作用是终止某个 case 并跳出 switch 结构。当它出现在循环语句中,作用是 跳出循环语句,执行后面的代码 continue 语句:continue 语句用在循环语句中,它的作用是终止本次循环,执行下一次循 环。 return 语句:return 语句可以用于从当前的方法中退出,返回到该调用的方法的语句处, 继续执行。return 返回一个值给调用该方法的语句,返回值的数据类型必须与方法的声明中 的返回值的类型一致。 5.请简述方法重写和方法重载的区别? 方法重写(Overriding)和方法重载(Overloading)是 Java 多态性的不同表现。 重写 Overriding 是父类与子类之间多态性的一种表现,重载 Overloading 是一个类中多态性的一 种表现。 如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Overriding)。 子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义 如同被“屏蔽”了。 如果在同一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类 型,则称为方法的重载(Overloading) 6.请简述 Error 和 Exception 有什么区别? Error 表示恢复不是不可能但很困难的情况下的一种严重问题,比如说内存溢出,仅靠修 改程序本身是不能处理这样的情况的。 Exception 表示程序本身可以处理的错误,在开发 Java 程序中进行的异常处理,都是针 对 Excption 类及其子类。在 Exception 类的众多子类中有一个特殊的 RuntimeException 类, 该类及其子类用于表示运行时异常,除了此类,Exception 类下所有其它的子类都用于表示 编译时异常。 7.请简述 synchronized 和 java.util.concurrent.locks.Lock 的异同点 相同点:Lock 能完成 synchronized 所实现的所有功能。 不同点:总的来说,Lock 更加灵活。Lock 有比 synchronized 更精确的线程予以和更好的 性能。synchronized 会自动释放锁,但是 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。synchronized 修饰方法时表示同一个对象在不同的线程中表现为同步队列。如 果实例化不同的对象 那么 synchronized 就不会出现同步效果了。 8.进程和线程之间有什么不同 在一个操作系统中,每个独立执行的程序都可以称为一个进程,也就是“正在运行的程 序”。而在进程中还可以有多个执行单元同时执行,这些执行单元可以看作程序执行的一条 条线索,被称为线程。 Java 运行环境是一个包含了不同的类和程序的单一进程。线程可以被 称为轻量级进程。线程需要较少的资源来创建和驻留在进程中,并且可以共享进程中的资源。 9.请简述装箱和拆箱的概念。 ​ 包装类和基本数据类型在进行转换时,引入了装箱和拆箱的概念,其中装箱是指将基本 数据类型的值转为引用数据类型,反之,拆箱是指将引用数据类型的对象转为基本数据类型。 10.请简述 Hashtable 和 HashMap 的区别。 ​ Hashtable 继承自 Dictionary 类,而 HashMap 是 Java1.2 引进的 Map 接口的一个实现 HashMap允许将null作为一个entry的key或者value,而Hashtable不允许还有就是, HashMap 把 Hashtable 的 contains 方法去掉了,改成 containsvalue 和 containsKey。 ​ 最大的不同是, Hashtable 的方法是线程安全的,而 HashMap 不是,在多个线程访问 Hashtable 时,不需要 自己为它的方法实现同步,而 HashMap 就必须为之提供外同步。 11.请简述使用泛型的优点。 (1)提高了 Java 程序的类型安全,在编译时期解决错误,避免程序在运行时期发生错 误。 (2)消除强制类型转换。 (3)泛型可以替代 Object 类型的参数和变量的使用,带来性 能的大幅提高并增加代码的可读性。 12.简述 TCP/IP 协议的层次结构 ​ TCP/IP 协议的层次结构比较简单,共分为四层,分别是应用层、传输层、网络层和链 路层。其中链路层是用于定义物理传输通道,通常是对某些网络连接设备的驱动协议,例如 针对光纤、双绞线提供的驱动。 ​ 网络层是整个 TCP/IP 协议的核心,它主要用于将传输的数 据进行分组,将分组数据发送到目标计算机或者网络。运输层主要使网络程序进行通信,在 进行网络通信时,可以采用 TCP 协议,也可以采用 UDP 协议。应用层主要负责应用程序的 协议,例如 HTTP 协议、FTP 协议等。 作者: 杨校 出处: https://blog.csdn.net/kese7952 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。 本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 如有问题, 可邮件(397583050@qq.com)咨询。

优秀的个人博客,低调大师

杨老师课堂之ArrayList集合常用方法解析

ArrayList集合常用方法的解析 1.概述 ​ 在前面我们学习了数组,数组可以保存多个元素,但在某些情况下无法确定到底要保存多少个元素,此时数组将不再适用,因为数组的长度不可变。例如,要保存一个学校的学生,由于不停有新生来报道,同时也有学生毕业离开学校,这时学生的数目很难确定。为了保存这些数目不确定的元素,JDK中提供了一系列特殊的类,这些类可以存储任意类型的元素,并且长度可变,统称为集合。在这里,我们先介绍ArrayList集合,其他集合在后续课程中学习。 ​ ArrayList集合是程序中最常见的一种集合,它属于引用数据类(类)。在ArrayList内部封装了一个长度可变的数组,当存入的元素超过数组长度时,ArrayList会在内存中分配一个更大的数组来存储这些元素,因此可以将ArrayList集合看作一个长度可变的数组。 1.1集合的创建 创建集合的常用格式在此说明一下: 导包:import java.util.ArrayList; 创建对象:与其他普通的引用数据类型创建方式完全相同,但是要指定容器中存储的数据类型: ArrayList<要存储元素的数据类型>变量名 = new ArrayList<要存储元素的数据类型>(); 1、集合中存储的元素,只能为<>括号中指定的数据类型元素; 2、“<要存储元素的数据类型>”中的数据类型必须是引用数据类型,不能是基本数据类型; 我们通过举几个例子,来明确集合的创建方式: 存储String类型的元素 ArrayList list = new ArrayList(); 存储int类型的数据 ArrayList list = new ArrayList(); 存储Phone类型的数据 ArrayListlist = new ArrayList(); 1.2 集合中常用方法 ​ 接下来,我们来学习下ArrayList集合提供的一些常用方法,如下表: 通过代码演示上述方法的使用:ArrayListDemo01.java package cn.javabs.list.demo; import java.util.ArrayList; public class ArrayListDemo01 { public static void main(String[] args) { // 创建ArrayList集合 ArrayList<String> list = new ArrayList<String>(); // 向集合中添加元素 list.add("stu1"); list.add("stu2"); list.add("stu3"); list.add("stu4"); // 获取集合中元素的个数 System.out.println("集合的长度:" + list.size()); // 取出并打印指定位置的元素 System.out.println("第1个元素是:" + list.get(0)); System.out.println("第2个元素是:" + list.get(1)); System.out.println("第3个元素是:" + list.get(2)); System.out.println("第4个元素是:" + list.get(3)); } } 代码运行后截图: 1.3 集合中常用方法 通过集合遍历,得到集合中每个元素,这是集合中最常见的操作。集合的遍历与数组的遍历很像,都是通过索引的方式,集合遍历方式如下:ArrayListDemo02.java package cn.javabs.list.demo; import java.util.ArrayList; public class ArrayListDemo02 { public static void main(String[] args) { //创建ArrayList集合 ArrayList<Integer> list = new ArrayList<Integer>(); //添加元素到集合 list.add(13); list.add(15); list.add(22); list.add(29); //遍历集合 for (int i = 0; i < list.size() ; i++) { //通过索引,获取到集合中每个元素 int n = list.get(i) ; System.out.println(n); } } 上述代码中,第5行定义了一个可以存储int元素的集合;第7-10行,实现将int类型数值存储到集合中; ​ 第12-16行,实现遍历集合元素。这里要强调一点,get方法返回值的类型为集合中元素的类型。 1.4 集合中的常用方法补充 ArrayList集合提供的一些常用方法,如下表: boolean add(int index, Object obj) 功能:在集合中指定index位置,添加新元素obj 功能说明:假设集合list中有元素[“java”,“javaEE”],当使用add(1,“javaWeb”)后,集合list中的元素为[“java”,“javaWeb”,“JavaEE”]。 Object set(int index, Object obj) 功能:用指定元素obj替代集合中指定index位置的元素 功能说明:假设集合list中有元素[“java”,“javaEE”],当使用set(0,“javaWeb”)后,集合list中的元素为[“javaWeb”,“JavaEE”]。 Object remve(int index) 功能:从集合中删除指定index处的元素,返回该元素 功能说明:假设集合list中有元素[“java”,“javaEE”],当使用remove(0)后,集合list中的元素为[“JavaEE”],返回值为“java”。 void clear() 功能:清空集合中所有元素 功能说明:假设集合list中有元素[“java”,“javaEE”],当使用clear()后,集合list中的元素为空[ ]。 作者: 杨校 出处:https://blog.csdn.net/kese7952 分享是快乐的,也见证了个人成长历程,文章大多都是工作经验总结以及平时学习积累,基于自身认知不足之处在所难免,也请大家指正,共同进步。 本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 如有问题, 可邮件(397583050@qq.com)咨询。

优秀的个人博客,低调大师

Microduino王镇山:将物联网带进课堂

创意过剩的时代,每个人都有造物的诉求。但各方面的门槛让很多人永远停留在幻想的层面,只有极少数的人一直走在将梦想‘造’进现实的路上。我们称之为极客。 但极客何其少,唯有读书高。随着硬件厂商这两年在国内的兴起,创客文化在国内日益浓郁。如何最大限度地降低创客们的开发成本,成了很多极客去思考的问题,王镇山的Microduino就是在这样的思考中得出的结果。 创客不一定是极客 据王镇山介绍,microduino有两个特性——尺寸小、模块化。开发者只需将自己的功能模块拼接起来,就可以组成一个硬件解决方案。操作起来远没有极客般令人不明觉厉。如今,Microduino已经推出50余款硬件产品。用Microduino做项目的开发者在海外有1万多名,而国内相对较少。 当王镇山回忆起自己的读书时代,他坦言道,在学校做项目的时候,很难从中得到成就感。究其原

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册