|
|
@@ -0,0 +1,111 @@
|
|
|
+/**
|
|
|
+ * 路由组件预取
|
|
|
+ *
|
|
|
+ * 菜单对应的路由组件是异步 chunk(import('@/views/' + component)),
|
|
|
+ * 点击菜单时才会第一次下载并执行这个 chunk,一次往往要几百毫秒。
|
|
|
+ * 这段时间里页面完全没有反馈,过渡动画也被推迟,表现就是「点击后卡住」。
|
|
|
+ *
|
|
|
+ * 这里做两件事:
|
|
|
+ * 1. 用户把鼠标移到菜单上时,提前把目标组件加载好(悬停预取);
|
|
|
+ * 2. 页面空闲后,把侧边栏里可见的菜单组件依次预取,等用户点过去时已经就绪。
|
|
|
+ *
|
|
|
+ * 预取只是提前发起同一个 import(),webpack 会复用同一个加载 promise,
|
|
|
+ * 所以即使预取还没完成,点击也不会比原来更慢。
|
|
|
+ */
|
|
|
+const prefetched = new Set();
|
|
|
+
|
|
|
+/**
|
|
|
+ * 找到路径对应的路由记录
|
|
|
+ * @param {Object} router vue-router 实例
|
|
|
+ * @param {String} path 路由路径
|
|
|
+ */
|
|
|
+function findRouteRecord(router, path) {
|
|
|
+ if (!router || typeof router.getRoutes !== 'function') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ const records = router.getRoutes();
|
|
|
+ const target = path.replace(/\/+$/, '') || '/';
|
|
|
+ return (
|
|
|
+ records.find((item) => item.path === target) ||
|
|
|
+ records.find((item) => (item.path || '').replace(/\/+$/, '') === target) ||
|
|
|
+ null
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 预取指定路径的路由组件
|
|
|
+ * @param {Object} router vue-router 实例
|
|
|
+ * @param {String} path 路由路径
|
|
|
+ */
|
|
|
+export function prefetchRouteComponent(router, path) {
|
|
|
+ if (!router || !path) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const key = path.replace(/\/+$/, '') || '/';
|
|
|
+ if (prefetched.has(key)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const record = findRouteRecord(router, key);
|
|
|
+ const loader = record && record.components && record.components.default;
|
|
|
+ // 只有异步组件(返回 Promise 的工厂函数)才需要预取
|
|
|
+ if (typeof loader !== 'function') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ prefetched.add(key);
|
|
|
+ try {
|
|
|
+ const result = loader();
|
|
|
+ if (result && typeof result.catch === 'function') {
|
|
|
+ result.catch(() => {
|
|
|
+ // 预取失败时允许下次重试
|
|
|
+ prefetched.delete(key);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ prefetched.delete(key);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 从链接地址里解析出路由路径
|
|
|
+ * @param {String} href 链接地址
|
|
|
+ * @param {String} base 路由 base
|
|
|
+ */
|
|
|
+export function resolveRoutePath(href, base) {
|
|
|
+ if (
|
|
|
+ !href ||
|
|
|
+ href.startsWith('http') ||
|
|
|
+ href.startsWith('//') ||
|
|
|
+ href.startsWith('#')
|
|
|
+ ) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ let path = href.split('?')[0].split('#')[0];
|
|
|
+ if (base && base !== '/' && path.startsWith(base)) {
|
|
|
+ path = '/' + path.slice(base.length);
|
|
|
+ }
|
|
|
+ return path;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 空闲时依次预取侧边栏里可见的菜单路由组件
|
|
|
+ * @param {Object} router vue-router 实例
|
|
|
+ * @param {Number} limit 最多预取多少个
|
|
|
+ * @param {Number} stagger 每个之间的间隔(毫秒)
|
|
|
+ */
|
|
|
+export function prefetchVisibleMenuRoutes(router, limit = 4, stagger = 800) {
|
|
|
+ if (!router || typeof document === 'undefined') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const links = document.querySelectorAll('.el-menu a[href]');
|
|
|
+ const base = router.options.base;
|
|
|
+ const paths = [];
|
|
|
+ for (let i = 0; i < links.length; i++) {
|
|
|
+ const path = resolveRoutePath(links[i].getAttribute('href'), base);
|
|
|
+ if (path && paths.indexOf(path) === -1) {
|
|
|
+ paths.push(path);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ paths.slice(0, limit).forEach((path, index) => {
|
|
|
+ setTimeout(() => prefetchRouteComponent(router, path), index * stagger);
|
|
|
+ });
|
|
|
+}
|