腾讯云技术社区-掘金主页持续为你们呈现云计算技术文章,欢迎你们关注!javascript
做者:villainthrcss
Service Worder 是用来代替 manifest,用来生成缓存的效果的。之前吭哧吭哧的学 manifest 的时候,就发现 MD 好难用。并且 MDN 特地告诉你,manifest 有毒,请不要乱用,保不定后面不支持。今儿,我看了下兼容性,呵呵~html
人生苦短,及时享乐,前端真坑,不敢乱学。前端
前方高能,若是以为生活没有趣味能够继续看下去,会让你的人生更没有趣味。若是以为凑合能过,请 ctrl/command + w
。java
继续~node
Service Worker 讲道理是由两部分构成,一部分是 cache,还有一部分则是 Worker。因此,SW(Service Worker) 自己的执行,就彻底不会阻碍当前 js 进程的执行,确保性能第一。那 SW 究竟是怎么工做的呢?git
咱们先来看看 SW 比较坑的地方,它的 lifecyclegithub
首先,SW 并非你网页加载就与生俱来的。若是,你须要使用 SW,你首先须要注册一个 SW,让浏览器为你的网页分配一块内存空间来。而且,你可否注册成功,还须要看你缓存的资源量决定(有可能失败,真的有可能)。若是,你须要缓存的静态资源所有保存成功,那么恭喜您,SW 安装成功。若是,其中有一个资源下载失败而且没法缓存,那么此次吊起就是失败的。不过,SW 是由重试机制的,这点也不算特别坑。web
当安装成功以后,此时 SW 就进入了激活阶段(activation)。而后,你能够选择性的检查之前的文件是否过时等。ajax
检查完以后,SW 就进入待机状态。此时,SW 有两种状态,一种是 active,一种是 terminated。就是激活/睡眠。激活是为了工做,睡眠则为了节省内存。这是一开始设计的初衷。若是,SW 已经 OK,那么,你网页的资源都会被 SW 控制,固然,SW 第一次加载除外。
简单的流程图,能够参考一下 google的:
上面简单介绍了 SW 的基本生命周期(实际上,都是废话),讲点实在的,它的兼容性咋样?
基本上手机端是能用的。
如今,开发一个网站没用 HTTPS,估计都没好意思放出本身的域名(太 low)。HTTPS 不只仅能够保证你网页的安全性,还可让一些比较敏感的 API 完美的使用。值得一提的是,SW 是基于 HTTPS 的,因此,若是你的网站不是 HTTPS,那么基本上你也别想了 SW。这估计形成了一个困难,即,我调试 SW 的时候咋办?
解决办法也是有的,使用 charles
或者 fildder
完成域名映射便可。
下面,咱们仔细介绍下,SW 的基本使用。
SW 其实是挂载到 navigator 下的对象。在使用以前,咱们须要先检查一下是否可用:
if ('serviceWorker' in navigator) {
// ....
}复制代码
若是可用,咱们就要使用 SW 进行路由的注册缓存文件了。不过,这里有点争议。啥时候开始执行 SW 的注册呢?上面说过,SW 就是一个网络代理,用来捕获你网页的全部 fetch 请求。那么,是否是能够这么写?
window.addEventListener('DOMContentLoaded', function() {
// 执行注册
navigator.serviceWorker.register('/sw.js').then(function(registration) {
}).catch(function(err) {
});
});复制代码
这样理解逻辑上是没有任何问题的,关键在于,虽然 SW 是 worker ,但浏览器的资源也是有限的,浏览器分配给你网页的内存就这么多,你再开个 SW(这个很大的。。。),没有 jank 才怪嘞,并且若是你网页在一开始加载的时候有动画展现的话,那么这种方式基本上就 GG 了。
另外,若是算上用户第一次加载,那么这个卡顿或者延时就很大了。
固然,W3C 在制定相关规范时,确定考虑到这点,实际上 SW 在你网页加载完成一样也能捕获已经发出的请求。因此,为了减小性能损耗,咱们通常直接在 onload 事件里面注册 SW 便可。GOOGLE Jeff Posnick
针对这个加载,专门讨论了一下,有兴趣的能够参考一下。(特别提醒,若是想要测试注册 SW 可使用隐身模式调试!!!)
那当我注册成功时,怎样查看我注册的 SW 呢?
这很简单,直接打开 chrome://inspect/#service-workers 就能够查看,在当前浏览器中,正在注册的 SW。另外,还有一个 chrome://serviceworker-internals,用来查看当前浏览器中,全部注册好的 SW。
使用 SW 进行注册时,还有一个很重要的特性,即,SW 的做用域不一样,监听的 fetch 请求也是不同的。
例如,咱们将注册路由换成: /example/sw.js
window.addEventListener('DOMContentLoaded', function() {
// 执行注册
navigator.serviceWorker.register('/example/sw.js').then(function(registration) {
}).catch(function(err) {
});
});复制代码
那么,SW 后面只会监听 /example
路由下的全部 fetch 请求,而不会去监听其余,好比 /jimmy
,/sam
等路径下的。
从这里开始,咱们就正式进入 SW 编程。记住,下面的部分是在另一个 js 中的脚本,使用的是 worker 的编程方法。若是,有同窗还不理解 worker 的话,能够先去学习一下,这样在后面的学习中才不会踩很深的坑。
监听安装 SW 的代码也很简单:
self.addEventListener('install', function(event) {
// Perform install steps
});复制代码
当安装成功后,咱们能使用 SW 作什么呢?
那就开始缓存文件了呗。简单的例子为:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('mysite-static-v1').then(function(cache) {
return cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js'
]);
})
);
});复制代码
此时,SW 会检测你制定文件的缓存问题,若是,已经都缓存了,那么 OK,SW 安装成功。若是查到文件没有缓存,则会发送请求去获取,而且会带上 cache-bust
的 query string,来表示缓存的版本问题。固然,这只针对于第一次加载的状况。当全部的资源都已经下载成功,那么恭喜你能够进行下一步了。你们能够参考一下 google demo。
这里,我简单说一下上面的过程,首先 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 开启失败。
不过,若是其中一个文件下载失败的话,那么此次你的 SW 启动就告吹了,即,若是其中有一个 Promise 是使用 reject 的话,那就表明着--您此次启动是 GG 的。那,有没有其余办法在保证必定稳定性的前提下,去加载比较大的文件呢?
有的,那你别返回 cache.addAll 就ok了。什么个意思呢?
就这样:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('mygame-core-v1').then(function(cache) {
// 不稳定文件或大文件加载
cache.addAll(
//...
);
// 稳定文件或小文件加载
return cache.addAll(
// core assets & levels 1-10
);
})
);
});复制代码
这样,第一个 cache.addAll
是不会被捕获的,固然,因为异步的存在,这毋庸置疑会有一些问题。好比,当大文件还在加载的时候,SW 断开,那么此次请求就是无效的。不过,你这样写原本就算是一个 trick,这种状况在制定方案的时候,确定也要考虑进去的。整个步骤,咱们能够用下图表示:
FROM GOOGLE
该阶段就是事关整个网页可否正常打开的一个阶段--很是关键。在这一阶段,咱们将学会,如何让 web 使用缓存,如何作向下兼容。
先看一个简单的格式:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});复制代码
首先看一下,第一个方法--event.respondWith
,用来包含响应主页面请求的代码。当接受到 fetch 请求时,会直接返回 event.respondWith
Promise 结果。咱们在 worker 中,捕获页面全部的 fetch 请求。能够看到 event.request
,这个就是 fetch 的 request 流。咱们经过 caches.match 捕获,而后返回 Promise 对象,用来进行响应的处理。你们看这段代码时,可能会有不少的疑惑,是的,一开始我看的时候也是,由于,根本没注释,有些 name
其实是内核自带的。上面的就有:
简单来讲,caches.match 根据 event.request
,在缓存空间中查找指定路径的缓存文件,若是匹配到,那么 response
是有内容的。若是没有的话,则再经过 fetch 进行捕获。整个流图以下:
OK,那如今有个问题,若是没有找到缓存,那么应该怎么作呢?
那怎么手动添加呢?
很简单,本身发送 fetch,而后使用 caches
进行缓存便可。不过,这里又涉及到另一个概念,Request 和 Response 流。这是在 fetch 通讯方式 很重要的两个概念。fetch 不只分装了 ajax,并且在通讯方式上也作了进一步的优化,同 node 同样,使用流来进行重用。众所周知,一个流通常只能使用一次,能够理解为喝矿泉水,只能喝一次,不过,若是我知道了该水的配方,那么我就能够量产该水,这就是流的复制。下面代码也基本使用到这两个概念,基本代码为:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}
// 由于 event.request 流已经在 caches.match 中使用过一次,
// 那么该流是不能再次使用的。咱们只能获得它的副本,拿去使用。
var fetchRequest = event.request.clone();
// fetch 的经过信方式,获得 Request 对象,而后发送请求
return fetch(fetchRequest).then(
function(response) {
// 检查是否成功
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 若是成功,该 response 一是要拿给浏览器渲染,而是要进行缓存。
// 不过须要记住,因为 caches.put 使用的是文件的响应流,一旦使用,
// 那么返回的 response 就没法访问形成失败,因此,这里须要复制一份。
var responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(function(cache) {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});复制代码
那么整个流图变为:
而里面最关键的地方就是 stream 这是如今浏览器操做数据的一个新的标准。为了不将数据一次性写入内存,咱们这里引入了 stream,至关于一点一点的吐。这个和 nodeJS 里面的 stream 是同样的效果。你用上述哪一个流图,这估计得取决于你本身的业务。
在 SW 中的更新涉及到两块,一个是基本静态资源的更新,还有一个是 SW.js 文件的更新。这里,咱们先说一下比较坑的 SW.js 的更新。
SW.js 的更新不只仅只是简单的更新,为了用户可靠性体验,里面仍是有不少门道的。
install
事件被触发waiting
状态。注意,此时并不存在替换activate
事件。整个流程图为:
若是上述步骤成功后,原来的 SW.js 就会被清除。可是,之前版本 SW.js 缓存文件没有被删除。针对于这一状况,咱们能够在新的 SW.js 里面监听 activate 事件,进行相关资源的删除操做。固然,这里主要使用到的 API 和 caches 有很大的关系(由于,如今全部缓存的资源都在 caches 的控制下了)。好比,我之前的 SW 缓存的版本是 v1
,如今是 v2
。那么我须要将 v1
给删除掉,则代码为:
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['v1'];
event.waitUntil(
// 遍历 caches 里全部缓存的 keys 值
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.includes(cacheName)) {
// 删除 v1 版本缓存的文件
return caches.delete(cacheName);
}
})
);
})
);
});复制代码
另外,我那么你不经仅能够用来做为版本的更新,还能够做为缓存目录的替换。好比,我想直接将 site-v1
的缓存文件,替换为 ajax-v1
和 page-v1
。则,咱们一是须要先在 install
事件里面将 ajajx-v1
和 page-v1
缓存套件给注册了,而后,在 activate 里面将 site-v1
缓存给删除,实际代码和上面实际上是同样的:
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['site-v1'];
event.waitUntil(
// 遍历 caches 里全部缓存的 keys 值
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheWhitelist.includes(cacheName)) {
// 删除 v1 版本缓存的文件
return caches.delete(cacheName);
}
})
);
})
);
});复制代码
OK,SW.js 更新差很少就是这样一块内容。
对于文件更新来讲,整个机制就显得很简单了。能够说,你想要一个文件更新,只须要在 SW 的 fetch
阶段使用 caches 进行缓存便可。实际操做也很简单,一开始咱们的 install
阶段的代码为:
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('mysite-static-v1').then(function(cache) {
return cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js'
]);
})
);
});复制代码
咱们只须要在这里简单的写下一下 prefetch 代码便可。
self.addEventListener('install', function(event) {
var now = Date.now();
// 事先设置好须要进行更新的文件路径
var urlsToPrefetch = [
'static/pre_fetched.txt',
'static/pre_fetched.html',
'https://www.chromium.org/_/rsrc/1302286216006/config/customLogo.gif'
];
event.waitUntil(
caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
var cachePromises = urlsToPrefetch.map(function(urlToPrefetch) {
// 使用 url 对象进行路由拼接
var url = new URL(urlToPrefetch, location.href);
url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
// 建立 request 对象进行流量的获取
var request = new Request(url, {mode: 'no-cors'});
// 手动发送请求,用来进行文件的更新
return fetch(request).then(function(response) {
if (response.status >= 400) {
// 解决请求失败时的状况
throw new Error('request for ' + urlToPrefetch +
' failed with status ' + response.statusText);
}
// 将成功后的 response 流,存放在 caches 套件中,完成指定文件的更新。
return cache.put(urlToPrefetch, response);
}).catch(function(error) {
console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
});
});
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching complete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})
);
});复制代码
当成功获取到缓存以后, SW 并不会直接进行替换,他会等到用户下一次刷新页面事后,使用新的缓存文件。
不过,这里请注意,我并无说,咱们更新缓存只能在 install
里更新,事实上,更新缓存能够在任何地方执行。它主要的目的是用来更新 caches 里面缓存套件。咱们提取一下代码:
// 找到缓存套件并打开
caches.open(CURRENT_CACHES.prefetch).then(function(cache) {
// 根据事先定义的路由开始发送请求
var cachePromises = urlsToPrefetch.map(function(urlToPrefetch) {
// 执行 fetch
return fetch(request).then(function(response) {
// 缓存请求到的资源
return cache.put(urlToPrefetch, response);
}).catch(function(error) {
console.error('Not caching ' + urlToPrefetch + ' due to ' + error);
});
});
// 使用 promise.all 进行所有捕获
return Promise.all(cachePromises).then(function() {
console.log('Pre-fetching complete.');
});
}).catch(function(error) {
console.error('Pre-fetching failed:', error);
})复制代码
如今,咱们已经拿到了核心代码,那有没有什么简便的办法,让咱们少写一些配置项,直接对每个文件进行文件更新教研。
有的!!!
还记得上面的 fetch
事件吗?咱们简单回顾一下它的代码:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});复制代码
实际上,咱们能够将上面的核心代码作一些变化直接用上:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
return response || fetchPromise;
})
})
);
});复制代码
这里比较难的地方在于,咱们并无去捕获 fetch(fetchRequest)... 相关内容。也就是说,这一块是彻底独立于咱们的主体业务的。他的 fetch 只是用更新文件而已。咱们可使用一个流图进行表示:
ok,关于文件的缓存咱们就介绍到这里。
如今,为了更好的用户体验,咱们能够作的更尊重用户一些。能够设置一个 button,告诉用户是否选择缓存指定文件。有同窗可能会想到使用 postmessage API,来告诉 SW 执行相关的缓存信息。不过事实上,还有更简单的办法来完成,即,直接使用 caches 对象。caches 和 web worker 相似。都是直接挂载到 window 对象上的。因此,咱们能够直接使用 caches 这个全局变量来进行搜索。那么该环节就不须要直接经过 SW,这个流程图能够画为:
代码能够参考:
document.querySelector('.cache-article').addEventListener('click', function(event) {
event.preventDefault();
var id = this.dataset.articleId;
// 建立 caches 套件
caches.open('mysite-article-' + id).then(function(cache) {
fetch('/get-article-urls?id=' + id).then(function(response) {
// 返回 json 对象
return response.json();
}).then(function(data) {
// 缓存指定路由
cache.addAll(data);
});
});
});复制代码
这里我就不赘述了,简单来讲就是更新一下缓存。
上面大体了解了一下关于 SW 的基本流程,不过说到底,SW 只是一个容器,它的内涵只是一个驻留后台进程。咱们想关心的是,在这进程里面,咱们能够作些什么?
最主要的应该有两个东西,缓存和推送。这里咱们主要讲解一下缓存。不过在SW 中,咱们通常只能缓存 POST
上面在文件更新里面也讲了几个更新的方式。简单来讲:
简单的情形上面已经说了,我这里专门将一下比较复杂的内容。
这种情形通常是用来装逼的,一方面检查请求,一方面有检查缓存,而后看两个谁快,就用谁,我这里直接上代码吧:
function promiseAny(promises) {
return new Promise((resolve, reject) => {
// 经过 promise 的 resolve 特性来决定谁快
promises = promises.map(p => Promise.resolve(p));
// 这里调用外层的 resolve
promises.forEach(p => p.then(resolve));
// 若是其中有一方出现 error,则直接挂掉
promises.reduce((a, b) => a.catch(() => b))
.catch(() => reject(Error("All failed")));
});
};
self.addEventListener('fetch', function(event) {
event.respondWith(
promiseAny([
caches.match(event.request),
fetch(event.request)
])
);
});复制代码
这里就和咱们在后台配置的 Last-Modifier || Etag 同样,询问更新的文件内容,而后执行更新:
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return fetch(event.request).then(function(response) {
cache.put(event.request, response.clone());
return response;
});
})
);
});复制代码
这应该是目前为止最佳的体验,返回的时候不会影响正在发送的请求,而接受到的新的请求后,最新的文件会替换旧的文件。(这个就是前面写的代码):
self.addEventListener('fetch', function(event) {
event.respondWith(
caches.open('mysite-dynamic').then(function(cache) {
return cache.match(event.request).then(function(response) {
var fetchPromise = fetch(event.request).then(function(networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
})
return response || fetchPromise;
})
})
);
});复制代码
接下来,咱们来详细了解一下关于 Cache Object 相关的内容。加深印象:
Cache 虽然是在 SW 中定义的,可是咱们也能够直接在 window 域下面直接使用它。它经过 Request/Response 流(就是 fetch)来进行内容的缓存。每一个域名能够有多个 Cache Object,具体咱们能够在控制台中查看:
而且 Cache Object 是懒更新,实际上,就能够把它比喻为一个文件夹。若是你不本身亲自更新,系统是不会帮你作任何事情的。对于删除也是同样的道理,若是你不显示删除,它会一直存在的。不过,浏览器对于每一个域名的 Cache Object 数量是有限制的,而且,会周期性的删掉一些缓存信息。最好的办法,是咱们本身管理资源,官方给出的建议是: 使用版本号进行资源管理。上面我也展现过,删除特定版本的缓存资源:
self.addEventListener('activate', function(event) {
var cacheWhitelist = ['v2'];
event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (cacheWhitelist.indexOf(key) === -1) {
return caches.delete(key);
}
}));
})
);
});复制代码
这里,咱们就能够将 Cache Object 理解为一个持久性数据库,那么针对于数据库来讲,简单的操做就是 CRUD。而 Cache Object 也提供了这几个接口,而且接口结果都是经过 Promise 对象返回的,成功返回对应结果,失败则返回 undefined:
http://foo.com/?value=bar
,咱们不会再搜索 ?value=bar
这几个字符。cache.match(request,{options}).then(function(response) {
//do something with the response
});复制代码
cache.matchAll(request,{options}).then(function(response) {
response.forEach(function(element, index, array) {
cache.delete(element);
});
});复制代码
cache.add(url).then(function() {
// 请求的资源被成功缓存
});
# 等同于
fetch(url).then(function (response) {
if (!response.ok) {
throw new TypeError('bad response status');
}
return cache.put(url, response);
})
.then(res=>{
// 成功缓存
})复制代码
this.addEventListener('install', function(event) {
event.waitUntil(
caches.open('v1').then(function(cache) {
return cache.addAll([
'/public/',
'/public/index.html',
'/public/style.css',
'/public/app.js'
]);
})
);
});复制代码
cache.put(request, response).then(function() {
// 成功缓存
});复制代码
cache.keys().then(function(keys) {
keys.forEach(function(request, index, array) {
cache.delete(request);
});
});复制代码
能够查看到上面的参数都共同的用到了 request
这就是 fetch 套件里面的请求流,具体,能够参考一下前面的代码。上面全部方法都是返回一个 Promise 对象,用来进行异步操做。
上面简单介绍了一下 Cache Object,但实际上,Cache 的管理方式是两级管理。即,最外层是 Cache Storage
,下一层是 Cache Object
。
浏览器会给每一个域名预留一个 Cache Storage(只有一个)。而后,剩下的缓存资源,所有都存在下面。咱们能够理解为,这就是一个顶级缓存目录管理。而咱们获取 Cache Object 的惟一途径,就是经过 caches.open() 进行获取。这里,咱们就能够将 open 方法理解为 没有已经存在的 Cache Object 则新建,不然直接打开
。它的相关操做方法也有不少:
caches.match(event.request).then(function(resp) {
return resp || fetch(event.request).then(function(r) {
caches.open('v1').then(function(cache) {
cache.put(event.request, r);
});
return r.clone();
});
});复制代码
caches.has('v1').then(function(hasCache) {
// 检测是否存在 Cache Object Name 为 v1 的缓存内容
if (!hasCache) {
// 没存在
} else {
//...
}
}).catch(function() {
// 处理异常
});复制代码
caches.open('v1').then(function(cache) {
cache.add('/index.html');
});复制代码
caches.delete(cacheName).then(function(isDeleted) {
// 检测是否删除成功
});
# 经过,能够经过 Promise.all 的形式来删除多个 cache object
Promise.all(keyList.map(function(key) {
if (cacheList.indexOf(key) === -1) {
return caches.delete(keyList[i]);
}
});复制代码
event.waitUntil(
caches.keys().then(function(keyList) {
return Promise.all(keyList.map(function(key) {
if (['v1','v2'].indexOf(key) === -1) {
return caches.delete(keyList[i]);
}
});
})
);复制代码
上面就是关于 Cache Storage 的全部内容。
这里放一张本身写的总结图吧:
相关推荐
React 同构思想
Vue.js先后端同构方案之准备篇——代码优化
Vue组件开发实践之scopedSlot的传递
此文已由做者受权腾讯云技术社区发布,转载请注明文章出处
原文连接:www.qcloud.com/community/a…
获取更多腾讯海量技术实践干货,欢迎你们前往腾讯云技术社区