vite-plugin-preloader 1.1.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +340 -195
- package/dist/index.d.mts +36 -3
- package/dist/index.d.ts +36 -3
- package/dist/index.js +244 -205
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +234 -205
- package/dist/index.mjs.map +1 -1
- package/package.json +80 -81
package/dist/index.d.mts
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
3
|
interface PreloadRoute {
|
|
4
|
+
/** 路由路径,如 /dashboard。含 :param 的动态路由会被自动过滤 */
|
|
4
5
|
path: string;
|
|
6
|
+
/** 可选。组件文件路径,不填则自动推断为 @/views/{path}/index.vue */
|
|
5
7
|
component?: string;
|
|
8
|
+
/** 备注说明,仅用于日志显示 */
|
|
6
9
|
reason?: string;
|
|
10
|
+
/** 优先级,数字越小越优先加载,默认 2 */
|
|
7
11
|
priority?: number;
|
|
8
12
|
}
|
|
13
|
+
type StatusPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
14
|
interface PreloaderOptions {
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 要预加载的路由列表,支持字符串或对象格式
|
|
17
|
+
* 含 :param 的动态路由段会被自动过滤
|
|
18
|
+
* 默认 []
|
|
19
|
+
*/
|
|
20
|
+
routes?: (string | PreloadRoute)[];
|
|
21
|
+
/**
|
|
22
|
+
* 页面加载完成后延迟多少毫秒再触发状态显示
|
|
23
|
+
* 默认 2000ms
|
|
24
|
+
*/
|
|
11
25
|
delay?: number;
|
|
26
|
+
/**
|
|
27
|
+
* 是否开启调试日志与 window.__preloaderDebug 调试工具
|
|
28
|
+
* 默认:开发环境 true,生产环境 false
|
|
29
|
+
*/
|
|
12
30
|
debug?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 是否在页面角落显示预加载状态提示
|
|
33
|
+
* 默认 true
|
|
34
|
+
*/
|
|
35
|
+
showStatus?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 状态提示的显示位置
|
|
38
|
+
* 默认 'bottom-right'
|
|
39
|
+
*/
|
|
40
|
+
statusPosition?: StatusPosition;
|
|
41
|
+
/**
|
|
42
|
+
* 要排除预加载的路由路径列表(精确匹配或前缀匹配)
|
|
43
|
+
* 例:['/404', '/login', '/admin']
|
|
44
|
+
*/
|
|
45
|
+
exclude?: string[];
|
|
13
46
|
}
|
|
14
47
|
|
|
15
|
-
declare function preloaderPlugin(
|
|
48
|
+
declare function preloaderPlugin(userOptions?: PreloaderOptions): Plugin;
|
|
16
49
|
|
|
17
|
-
export { type PreloadRoute, type PreloaderOptions, preloaderPlugin as default };
|
|
50
|
+
export { type PreloadRoute, type PreloaderOptions, type StatusPosition, preloaderPlugin as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
|
|
3
3
|
interface PreloadRoute {
|
|
4
|
+
/** 路由路径,如 /dashboard。含 :param 的动态路由会被自动过滤 */
|
|
4
5
|
path: string;
|
|
6
|
+
/** 可选。组件文件路径,不填则自动推断为 @/views/{path}/index.vue */
|
|
5
7
|
component?: string;
|
|
8
|
+
/** 备注说明,仅用于日志显示 */
|
|
6
9
|
reason?: string;
|
|
10
|
+
/** 优先级,数字越小越优先加载,默认 2 */
|
|
7
11
|
priority?: number;
|
|
8
12
|
}
|
|
13
|
+
type StatusPosition = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
14
|
interface PreloaderOptions {
|
|
10
|
-
|
|
15
|
+
/**
|
|
16
|
+
* 要预加载的路由列表,支持字符串或对象格式
|
|
17
|
+
* 含 :param 的动态路由段会被自动过滤
|
|
18
|
+
* 默认 []
|
|
19
|
+
*/
|
|
20
|
+
routes?: (string | PreloadRoute)[];
|
|
21
|
+
/**
|
|
22
|
+
* 页面加载完成后延迟多少毫秒再触发状态显示
|
|
23
|
+
* 默认 2000ms
|
|
24
|
+
*/
|
|
11
25
|
delay?: number;
|
|
26
|
+
/**
|
|
27
|
+
* 是否开启调试日志与 window.__preloaderDebug 调试工具
|
|
28
|
+
* 默认:开发环境 true,生产环境 false
|
|
29
|
+
*/
|
|
12
30
|
debug?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 是否在页面角落显示预加载状态提示
|
|
33
|
+
* 默认 true
|
|
34
|
+
*/
|
|
35
|
+
showStatus?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* 状态提示的显示位置
|
|
38
|
+
* 默认 'bottom-right'
|
|
39
|
+
*/
|
|
40
|
+
statusPosition?: StatusPosition;
|
|
41
|
+
/**
|
|
42
|
+
* 要排除预加载的路由路径列表(精确匹配或前缀匹配)
|
|
43
|
+
* 例:['/404', '/login', '/admin']
|
|
44
|
+
*/
|
|
45
|
+
exclude?: string[];
|
|
13
46
|
}
|
|
14
47
|
|
|
15
|
-
declare function preloaderPlugin(
|
|
48
|
+
declare function preloaderPlugin(userOptions?: PreloaderOptions): Plugin;
|
|
16
49
|
|
|
17
|
-
export { type PreloadRoute, type PreloaderOptions, preloaderPlugin as default };
|
|
50
|
+
export { type PreloadRoute, type PreloaderOptions, type StatusPosition, preloaderPlugin as default };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// vite-plugin-preloader - 智能路由预加载插件
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
3
4
|
var __defProp = Object.defineProperty;
|
|
4
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
9
|
var __export = (target, all) => {
|
|
8
10
|
for (var name in all)
|
|
@@ -16,6 +18,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
20
|
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
|
|
21
31
|
// src/index.ts
|
|
@@ -26,261 +36,290 @@ __export(index_exports, {
|
|
|
26
36
|
module.exports = __toCommonJS(index_exports);
|
|
27
37
|
|
|
28
38
|
// src/runtime.ts
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const sortedRoutes = [...PRELOAD_ROUTES].sort((a, b) => a.priority - b.priority)
|
|
61
|
-
|
|
62
|
-
for (const route of sortedRoutes) {
|
|
63
|
-
await this.preloadSingle(route)
|
|
64
|
-
await this.sleep(100)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
this.stats.endTime = Date.now()
|
|
68
|
-
this.isPreloading = false
|
|
69
|
-
|
|
70
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
71
|
-
console.log(\`\u{1F389} [\u9884\u52A0\u8F7D] \u5B8C\u6210! \u8017\u65F6 \${this.stats.endTime - this.stats.startTime}ms\`)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async preloadSingle(route) {
|
|
76
|
-
if (this.preloadedRoutes.has(route.path)) return
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const startTime = Date.now()
|
|
80
|
-
let componentPath = route.component
|
|
81
|
-
|
|
82
|
-
// \u5728\u5F00\u53D1\u73AF\u5883\u4E2D\uFF0CVite \u4F1A\u5904\u7406\u6A21\u5757\u89E3\u6790
|
|
83
|
-
// \u6211\u4EEC\u9700\u8981\u4F7F\u7528\u6B63\u786E\u7684\u6A21\u5757 ID
|
|
84
|
-
if (componentPath.startsWith('@/')) {
|
|
85
|
-
// \u5BF9\u4E8E Vite\uFF0C@ \u522B\u540D\u5E94\u8BE5\u5728\u6784\u5EFA\u65F6\u5C31\u88AB\u89E3\u6790
|
|
86
|
-
// \u4F46\u5728\u8FD0\u884C\u65F6\u6211\u4EEC\u9700\u8981\u4F7F\u7528\u5B9E\u9645\u7684\u8DEF\u5F84
|
|
87
|
-
componentPath = componentPath.replace('@/', '/src/')
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// \u786E\u4FDD\u8DEF\u5F84\u4EE5 / \u5F00\u5934\uFF08\u76F8\u5BF9\u4E8E\u9879\u76EE\u6839\u76EE\u5F55\uFF09
|
|
91
|
-
if (!componentPath.startsWith('/') && !componentPath.startsWith('./')) {
|
|
92
|
-
componentPath = '/' + componentPath
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
96
|
-
console.log(\`\u{1F50D} [\u9884\u52A0\u8F7D] \u5C1D\u8BD5\u52A0\u8F7D: \${componentPath}\`)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const module = await import(/* @vite-ignore */ componentPath)
|
|
100
|
-
const loadTime = Date.now() - startTime
|
|
101
|
-
|
|
102
|
-
this.preloadedRoutes.add(route.path)
|
|
103
|
-
this.stats.completed++
|
|
104
|
-
|
|
105
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
106
|
-
console.log(\`\u2705 [\u9884\u52A0\u8F7D] \${route.path} (\${loadTime}ms) - \${route.reason}\`)
|
|
107
|
-
}
|
|
108
|
-
} catch (error) {
|
|
109
|
-
this.stats.failed++
|
|
110
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
111
|
-
console.error(\`\u274C [\u9884\u52A0\u8F7D] \${route.path} \u5931\u8D25:\`, error)
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
sleep(ms) {
|
|
117
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
isPreloaded(path) {
|
|
121
|
-
return this.preloadedRoutes.has(path)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getStats() {
|
|
125
|
-
return {
|
|
126
|
-
...this.stats,
|
|
127
|
-
preloadedPaths: Array.from(this.preloadedRoutes),
|
|
128
|
-
isPreloading: this.isPreloading
|
|
39
|
+
var STATUS_STYLES = {
|
|
40
|
+
"bottom-right": "bottom:16px;right:16px",
|
|
41
|
+
"bottom-left": "bottom:16px;left:16px",
|
|
42
|
+
"top-right": "top:16px;right:16px",
|
|
43
|
+
"top-left": "top:16px;left:16px"
|
|
44
|
+
};
|
|
45
|
+
function buildRuntimeScript(config) {
|
|
46
|
+
const posStyle = STATUS_STYLES[config.statusPosition] ?? STATUS_STYLES["bottom-right"];
|
|
47
|
+
const configJson = JSON.stringify(config);
|
|
48
|
+
const statusUiCode = config.showStatus ? `
|
|
49
|
+
function showStatus(){
|
|
50
|
+
var links=document.querySelectorAll('link[data-preloader]');
|
|
51
|
+
if(!links.length)return;
|
|
52
|
+
var el=document.createElement('div');
|
|
53
|
+
el.id='__preloader_ui__';
|
|
54
|
+
el.style.cssText='position:fixed;${posStyle};z-index:2147483647;'
|
|
55
|
+
+'background:rgba(15,15,15,0.82);color:#fff;font-size:12px;line-height:1.5;'
|
|
56
|
+
+'padding:7px 14px;border-radius:20px;pointer-events:none;'
|
|
57
|
+
+'transition:opacity 0.4s ease;font-family:system-ui,-apple-system,sans-serif;'
|
|
58
|
+
+'backdrop-filter:blur(4px);box-shadow:0 2px 8px rgba(0,0,0,0.3);';
|
|
59
|
+
el.textContent='\u26A1 \u6B63\u5728\u4F18\u5316 '+links.length+' \u4E2A\u9875\u9762...';
|
|
60
|
+
document.body&&document.body.appendChild(el);
|
|
61
|
+
var done=0,total=links.length;
|
|
62
|
+
function tick(){
|
|
63
|
+
done++;
|
|
64
|
+
el.textContent=done>=total?'\u2705 \u9875\u9762\u4F18\u5316\u5B8C\u6210\uFF08'+total+'\u4E2A\uFF09':'\u26A1 \u4F18\u5316\u4E2D '+done+'/'+total+'...';
|
|
65
|
+
if(done>=total){
|
|
66
|
+
setTimeout(function(){
|
|
67
|
+
el.style.opacity='0';
|
|
68
|
+
setTimeout(function(){el&&el.parentNode&&el.parentNode.removeChild(el);},420);
|
|
69
|
+
},2200);
|
|
129
70
|
}
|
|
130
71
|
}
|
|
72
|
+
links.forEach(function(l){l.addEventListener('load',tick);l.addEventListener('error',tick);});
|
|
73
|
+
setTimeout(function(){el&&el.parentNode&&el.parentNode.removeChild(el);},9000);
|
|
131
74
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
75
|
+
if(document.readyState==='loading'){
|
|
76
|
+
document.addEventListener('DOMContentLoaded',function(){setTimeout(showStatus,C.delay);});
|
|
77
|
+
}else{
|
|
78
|
+
setTimeout(showStatus,C.delay);
|
|
79
|
+
}` : "";
|
|
80
|
+
const debugCode = config.debug ? `
|
|
81
|
+
console.group('%c\u{1F680} vite-plugin-preloader v2','color:#4ade80;font-weight:700');
|
|
82
|
+
console.log('\u5DF2\u6CE8\u5165 '+C.routes.length+' \u4E2A <link rel=\\"prefetch\\"> \u6807\u7B7E\uFF08\u6D4F\u89C8\u5668\u539F\u751F\u8C03\u5EA6\uFF09');
|
|
83
|
+
C.routes.forEach(function(r){
|
|
84
|
+
var ok=r.chunkPath;
|
|
85
|
+
console.log((ok?'\u2705 ':'\u26A0\uFE0F ')+r.path+(ok?' \u2192 '+r.chunkPath:' (chunk \u672A\u5339\u914D\uFF0C\u4EC5\u5F00\u53D1\u73AF\u5883\u6709\u6548)')+(r.reason?' | '+r.reason:''));
|
|
86
|
+
});
|
|
87
|
+
console.groupEnd();
|
|
88
|
+
window.__preloaderDebug={
|
|
89
|
+
routes:C.routes,
|
|
90
|
+
check:function(p){return C.routes.some(function(r){return r.path===p&&!!r.chunkPath;})},
|
|
91
|
+
help:function(){console.log('__preloaderDebug: .routes | .check(path)');}
|
|
92
|
+
};
|
|
93
|
+
console.log('%c\u{1F6E0} \u8C03\u8BD5: window.__preloaderDebug','color:#94a3b8;font-size:11px');` : "";
|
|
94
|
+
if (!statusUiCode && !debugCode) return "";
|
|
95
|
+
return `<script>/* vite-plugin-preloader v2 - status/debug */
|
|
96
|
+
(function(){
|
|
97
|
+
var C=${configJson};
|
|
98
|
+
var conn=navigator.connection||navigator.mozConnection||navigator.webkitConnection;
|
|
99
|
+
if(conn&&(conn.saveData||['slow-2g','2g'].indexOf(conn.effectiveType)>=0)){
|
|
100
|
+
if(C.debug)console.warn('[\u9884\u52A0\u8F7D] \u68C0\u6D4B\u5230\u7701\u6D41/\u4F4E\u901F\u7F51\u7EDC\uFF0C\u8DF3\u8FC7\u72B6\u6001\u663E\u793A');
|
|
101
|
+
return;
|
|
156
102
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
window.usePreloader = () => ({
|
|
163
|
-
start: () => preloader.start(),
|
|
164
|
-
isPreloaded: (path) => preloader.isPreloaded(path),
|
|
165
|
-
getStats: () => preloader.getStats()
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
})();`;
|
|
103
|
+
${debugCode}
|
|
104
|
+
${statusUiCode}
|
|
105
|
+
})();
|
|
106
|
+
</script>`;
|
|
107
|
+
}
|
|
169
108
|
|
|
170
109
|
// src/generator.ts
|
|
110
|
+
var import_vite = require("vite");
|
|
111
|
+
var import_node_path = __toESM(require("path"));
|
|
171
112
|
var CodeGenerator = class {
|
|
172
113
|
constructor(options) {
|
|
173
114
|
this.options = options;
|
|
115
|
+
/** @ 别名对应的相对路径(相对于 viteRoot),默认 'src' */
|
|
116
|
+
this.srcAlias = "src";
|
|
117
|
+
this.viteRoot = process.cwd();
|
|
174
118
|
}
|
|
175
119
|
/**
|
|
176
|
-
*
|
|
120
|
+
* 从 Vite resolvedConfig 中读取项目根目录和 @ 别名
|
|
121
|
+
* 必须在 configResolved hook 中调用
|
|
177
122
|
*/
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
123
|
+
setViteConfig(config) {
|
|
124
|
+
this.viteRoot = config.root;
|
|
125
|
+
const alias = config.resolve?.alias;
|
|
126
|
+
const entries = Array.isArray(alias) ? alias : Object.entries(alias ?? {}).map(([find, replacement]) => ({ find, replacement }));
|
|
127
|
+
const atEntry = entries.find(
|
|
128
|
+
(a) => a.find === "@" || a.find instanceof RegExp && a.find.source === "^@/"
|
|
129
|
+
);
|
|
130
|
+
if (atEntry && typeof atEntry.replacement === "string") {
|
|
131
|
+
const rel = (0, import_vite.normalizePath)(import_node_path.default.relative(this.viteRoot, atEntry.replacement));
|
|
132
|
+
if (rel) this.srcAlias = rel;
|
|
133
|
+
}
|
|
182
134
|
}
|
|
183
135
|
/**
|
|
184
|
-
*
|
|
136
|
+
* 将用户路由配置解析为内部 ResolvedRoute 列表
|
|
137
|
+
* 同时过滤:含 :param 的动态路由段、exclude 列表匹配项
|
|
185
138
|
*/
|
|
186
|
-
|
|
139
|
+
resolveRoutes() {
|
|
140
|
+
if (!Array.isArray(this.options.routes) || this.options.routes.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const { exclude } = this.options;
|
|
187
144
|
return this.options.routes.map((route) => {
|
|
188
145
|
if (typeof route === "string") {
|
|
189
|
-
const componentPath2 = this.inferComponentPath(route);
|
|
190
146
|
return {
|
|
191
147
|
path: route,
|
|
192
|
-
component:
|
|
148
|
+
component: this.inferComponentPath(route),
|
|
193
149
|
reason: "\u81EA\u52A8\u63A8\u65AD\u7684\u9884\u52A0\u8F7D\u9875\u9762",
|
|
194
150
|
priority: 2
|
|
195
151
|
};
|
|
196
152
|
}
|
|
197
|
-
const componentPath = route.component || this.inferComponentPath(route.path);
|
|
198
153
|
return {
|
|
199
154
|
path: route.path,
|
|
200
|
-
component:
|
|
201
|
-
reason: route.reason
|
|
202
|
-
priority: route.priority
|
|
155
|
+
component: route.component ?? this.inferComponentPath(route.path),
|
|
156
|
+
reason: route.reason ?? "\u7528\u6237\u914D\u7F6E\u7684\u9884\u52A0\u8F7D\u9875\u9762",
|
|
157
|
+
priority: route.priority ?? 2
|
|
203
158
|
};
|
|
204
|
-
})
|
|
159
|
+
}).filter((route) => {
|
|
160
|
+
if (/:\w+/.test(route.path)) return false;
|
|
161
|
+
if (exclude.length > 0 && exclude.some(
|
|
162
|
+
(e) => route.path === e || route.path.startsWith(e.replace(/\/$/, "") + "/")
|
|
163
|
+
)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}).sort((a, b) => a.priority - b.priority);
|
|
205
168
|
}
|
|
206
169
|
/**
|
|
207
|
-
*
|
|
170
|
+
* 根据 Rollup OutputBundle 中的 facadeModuleId 为各路由匹配实际 chunk 文件路径
|
|
171
|
+
* 仅在生产构建的 generateBundle hook 中调用
|
|
208
172
|
*/
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
173
|
+
fillChunkPaths(routes, bundle) {
|
|
174
|
+
const chunkMap = /* @__PURE__ */ new Map();
|
|
175
|
+
for (const [fileName, chunkOrAsset] of Object.entries(bundle)) {
|
|
176
|
+
if (chunkOrAsset.type !== "chunk") continue;
|
|
177
|
+
const chunk = chunkOrAsset;
|
|
178
|
+
if (!chunk.facadeModuleId) continue;
|
|
179
|
+
chunkMap.set((0, import_vite.normalizePath)(chunk.facadeModuleId), "/" + fileName);
|
|
180
|
+
}
|
|
181
|
+
return routes.map((route) => {
|
|
182
|
+
const absPath = (0, import_vite.normalizePath)(this.resolveComponentAbsPath(route.component));
|
|
183
|
+
const chunkPath = chunkMap.get(absPath);
|
|
184
|
+
return chunkPath ? { ...route, chunkPath } : route;
|
|
185
|
+
});
|
|
221
186
|
}
|
|
222
187
|
/**
|
|
223
|
-
*
|
|
188
|
+
* 生成 <link> 标签字符串(注入到 </body> 前)
|
|
189
|
+
* 生产:<link rel="prefetch"> 指向实际 chunk(浏览器空闲时拉取,完全不阻塞主线程)
|
|
190
|
+
* 开发:<link rel="modulepreload"> 指向源文件(Vite dev server 处理解析)
|
|
224
191
|
*/
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
192
|
+
generateLinkTags(routes, isBuild) {
|
|
193
|
+
const tags = [];
|
|
194
|
+
for (const route of routes) {
|
|
195
|
+
if (isBuild) {
|
|
196
|
+
if (!route.chunkPath) continue;
|
|
197
|
+
tags.push(
|
|
198
|
+
`<link rel="prefetch" href="${route.chunkPath}" as="script" crossorigin data-preloader="${encodeURIComponent(route.path)}">`
|
|
199
|
+
);
|
|
200
|
+
} else {
|
|
201
|
+
const devPath = route.component.startsWith("@/") ? route.component.replace("@/", `/${this.srcAlias}/`) : route.component;
|
|
202
|
+
tags.push(
|
|
203
|
+
`<link rel="modulepreload" href="${devPath}" data-preloader="${encodeURIComponent(route.path)}">`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
229
206
|
}
|
|
230
|
-
|
|
231
|
-
return `@/views/${pathSegments.join("/")}/index.vue`;
|
|
207
|
+
return tags.join("\n");
|
|
232
208
|
}
|
|
233
209
|
/**
|
|
234
|
-
*
|
|
210
|
+
* 生成可选的状态 UI 和调试工具脚本
|
|
211
|
+
* showStatus=false 且 debug=false 时返回空字符串(0 运行时开销)
|
|
235
212
|
*/
|
|
236
|
-
|
|
237
|
-
return
|
|
238
|
-
|
|
239
|
-
|
|
213
|
+
generateRuntimeScript(routes, isBuild) {
|
|
214
|
+
return buildRuntimeScript({
|
|
215
|
+
delay: this.options.delay,
|
|
216
|
+
debug: this.options.debug,
|
|
217
|
+
showStatus: this.options.showStatus,
|
|
218
|
+
statusPosition: this.options.statusPosition,
|
|
219
|
+
isBuild,
|
|
220
|
+
routes: routes.map((r) => ({
|
|
221
|
+
path: r.path,
|
|
222
|
+
chunkPath: r.chunkPath ?? null,
|
|
223
|
+
reason: r.reason
|
|
224
|
+
}))
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
// --------------------------------------------------------------------------
|
|
228
|
+
// 私有工具方法
|
|
229
|
+
// --------------------------------------------------------------------------
|
|
230
|
+
/** 根据路由路径推断 Vue 组件路径(@/views/{path}/index.vue) */
|
|
231
|
+
inferComponentPath(routePath) {
|
|
232
|
+
const cleanPath = routePath.replace(/^\//, "").replace(/\/$/, "");
|
|
233
|
+
return `@/views/${cleanPath}/index.vue`;
|
|
234
|
+
}
|
|
235
|
+
/** 将 @/ 别名路径或相对路径解析为绝对路径,用于与 facadeModuleId 比对 */
|
|
236
|
+
resolveComponentAbsPath(componentPath) {
|
|
237
|
+
if (componentPath.startsWith("@/")) {
|
|
238
|
+
return import_node_path.default.join(this.viteRoot, this.srcAlias, componentPath.slice(2));
|
|
239
|
+
}
|
|
240
|
+
if (import_node_path.default.isAbsolute(componentPath)) {
|
|
241
|
+
return componentPath;
|
|
242
|
+
}
|
|
243
|
+
return import_node_path.default.join(this.viteRoot, componentPath);
|
|
240
244
|
}
|
|
241
245
|
};
|
|
242
246
|
|
|
243
247
|
// src/index.ts
|
|
244
|
-
function preloaderPlugin(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
debug:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
showStatus: true,
|
|
253
|
-
// 默认显示状态
|
|
254
|
-
statusPosition: "bottom-right",
|
|
255
|
-
// 默认右下角
|
|
256
|
-
...options
|
|
257
|
-
// 用户配置覆盖默认配置
|
|
248
|
+
function preloaderPlugin(userOptions = {}) {
|
|
249
|
+
const options = {
|
|
250
|
+
routes: Array.isArray(userOptions.routes) ? userOptions.routes : [],
|
|
251
|
+
delay: userOptions.delay ?? 2e3,
|
|
252
|
+
debug: userOptions.debug ?? process.env.NODE_ENV !== "production",
|
|
253
|
+
showStatus: userOptions.showStatus ?? true,
|
|
254
|
+
statusPosition: userOptions.statusPosition ?? "bottom-right",
|
|
255
|
+
exclude: userOptions.exclude ?? []
|
|
258
256
|
};
|
|
257
|
+
const generator = new CodeGenerator(options);
|
|
258
|
+
let resolvedRoutes = [];
|
|
259
|
+
let isBuild = false;
|
|
259
260
|
return {
|
|
260
261
|
name: "vite-plugin-preloader",
|
|
261
|
-
//
|
|
262
|
+
// post 确保在其他插件(如 @vitejs/plugin-vue)处理完后执行
|
|
262
263
|
enforce: "post",
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
264
|
+
// SSR 构建时不注入客户端预加载脚本
|
|
265
|
+
apply(_, { isSsrBuild }) {
|
|
266
|
+
return !isSsrBuild;
|
|
267
|
+
},
|
|
268
|
+
// ✅ 正确读取 viteConfig:alias、root、command
|
|
269
|
+
configResolved(config) {
|
|
270
|
+
isBuild = config.command === "build";
|
|
271
|
+
generator.setViteConfig(config);
|
|
272
|
+
resolvedRoutes = generator.resolveRoutes();
|
|
273
|
+
if (options.debug) {
|
|
274
|
+
const total = Array.isArray(userOptions.routes) ? userOptions.routes.length : 0;
|
|
275
|
+
const filtered = total - resolvedRoutes.length;
|
|
276
|
+
console.log(
|
|
277
|
+
`\u{1F680} [\u9884\u52A0\u8F7D\u63D2\u4EF6 v2] \u5DF2\u542F\u7528\uFF0C${resolvedRoutes.length} \u4E2A\u8DEF\u7531\u5F85\u9884\u52A0\u8F7D` + (filtered > 0 ? `\uFF08\u5DF2\u8FC7\u6EE4 ${filtered} \u4E2A\u52A8\u6001/\u6392\u9664\u8DEF\u7531\uFF09` : "")
|
|
278
|
+
);
|
|
279
|
+
if (!isBuild) {
|
|
280
|
+
console.log(
|
|
281
|
+
' \u5F00\u53D1\u6A21\u5F0F\uFF1A\u6CE8\u5165 <link rel="modulepreload">\uFF0C\u751F\u4EA7\u6784\u5EFA\u540E\u5207\u6362\u4E3A <link rel="prefetch">'
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
// ✅ 生产构建阶段:扫描 bundle,为路由匹配真实 chunk hash 路径
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
288
|
+
generateBundle(_opts, bundle) {
|
|
289
|
+
if (!isBuild) return;
|
|
290
|
+
resolvedRoutes = generator.fillChunkPaths(resolvedRoutes, bundle);
|
|
291
|
+
if (options.debug) {
|
|
292
|
+
const matched = resolvedRoutes.filter((r) => r.chunkPath).length;
|
|
293
|
+
const unmatched = resolvedRoutes.filter((r) => !r.chunkPath);
|
|
294
|
+
console.log(
|
|
295
|
+
`\u{1F4E6} [\u9884\u52A0\u8F7D\u63D2\u4EF6 v2] bundle \u626B\u63CF\u5B8C\u6210\uFF1A${matched}/${resolvedRoutes.length} \u4E2A\u8DEF\u7531\u6210\u529F\u5339\u914D chunk`
|
|
296
|
+
);
|
|
297
|
+
unmatched.forEach((r) => {
|
|
298
|
+
console.warn(
|
|
299
|
+
` \u26A0\uFE0F \u672A\u627E\u5230 chunk\uFF1A${r.path} \u2192 ${r.component}\uFF08\u7EC4\u4EF6\u8DEF\u5F84\u53EF\u80FD\u4E0D\u5339\u914D\uFF09`
|
|
300
|
+
);
|
|
301
|
+
});
|
|
267
302
|
}
|
|
268
303
|
},
|
|
269
|
-
//
|
|
304
|
+
// ✅ 注入 <link> 标签 + 可选运行时脚本到 </body> 前
|
|
270
305
|
transformIndexHtml(html) {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
306
|
+
const linkTags = generator.generateLinkTags(resolvedRoutes, isBuild);
|
|
307
|
+
const runtimeScript = generator.generateRuntimeScript(
|
|
308
|
+
resolvedRoutes,
|
|
309
|
+
isBuild
|
|
310
|
+
);
|
|
311
|
+
const inject = [linkTags, runtimeScript].filter(Boolean).join("\n");
|
|
312
|
+
if (!inject) return html;
|
|
313
|
+
if (html.includes("</body>")) {
|
|
314
|
+
return html.replace("</body>", `${inject}
|
|
315
|
+
</body>`);
|
|
316
|
+
}
|
|
317
|
+
return html + "\n" + inject;
|
|
274
318
|
},
|
|
275
|
-
//
|
|
319
|
+
// HMR:vite.config 变更时触发完整刷新
|
|
276
320
|
handleHotUpdate(ctx) {
|
|
277
321
|
if (ctx.file.includes("vite.config")) {
|
|
278
|
-
|
|
279
|
-
console.log("\u{1F504} [\u9884\u52A0\u8F7D\u63D2\u4EF6] \u914D\u7F6E\u5DF2\u66F4\u65B0");
|
|
280
|
-
}
|
|
281
|
-
ctx.server.ws.send({
|
|
282
|
-
type: "full-reload"
|
|
283
|
-
});
|
|
322
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
284
323
|
return [];
|
|
285
324
|
}
|
|
286
325
|
return void 0;
|