vite-plugin-preloader 1.1.2 → 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 -5
- package/dist/index.d.ts +36 -5
- package/dist/index.js +244 -258
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +234 -258
- package/dist/index.mjs.map +1 -1
- package/package.json +80 -81
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,314 +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
|
-
|
|
61
|
-
// \u663E\u793A\u72B6\u6001\u6307\u793A\u5668
|
|
62
|
-
this.showStatus()
|
|
63
|
-
|
|
64
|
-
const sortedRoutes = [...PRELOAD_ROUTES].sort((a, b) => a.priority - b.priority)
|
|
65
|
-
|
|
66
|
-
for (const route of sortedRoutes) {
|
|
67
|
-
await this.preloadSingle(route)
|
|
68
|
-
this.updateStatus()
|
|
69
|
-
await this.sleep(100)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
this.stats.endTime = Date.now()
|
|
73
|
-
this.isPreloading = false
|
|
74
|
-
|
|
75
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
76
|
-
console.log(\`\u{1F389} [\u9884\u52A0\u8F7D] \u5B8C\u6210! \u8017\u65F6 \${this.stats.endTime - this.stats.startTime}ms\`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// \u9690\u85CF\u72B6\u6001\u6307\u793A\u5668
|
|
80
|
-
this.hideStatus()
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async preloadSingle(route) {
|
|
84
|
-
if (this.preloadedRoutes.has(route.path)) return
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const startTime = Date.now()
|
|
88
|
-
let componentPath = route.component
|
|
89
|
-
|
|
90
|
-
// \u5728\u5F00\u53D1\u73AF\u5883\u4E2D\uFF0CVite \u4F1A\u5904\u7406\u6A21\u5757\u89E3\u6790
|
|
91
|
-
// \u6211\u4EEC\u9700\u8981\u4F7F\u7528\u6B63\u786E\u7684\u6A21\u5757 ID
|
|
92
|
-
if (componentPath.startsWith('@/')) {
|
|
93
|
-
// \u5BF9\u4E8E Vite\uFF0C@ \u522B\u540D\u5E94\u8BE5\u5728\u6784\u5EFA\u65F6\u5C31\u88AB\u89E3\u6790
|
|
94
|
-
// \u4F46\u5728\u8FD0\u884C\u65F6\u6211\u4EEC\u9700\u8981\u4F7F\u7528\u5B9E\u9645\u7684\u8DEF\u5F84
|
|
95
|
-
componentPath = componentPath.replace('@/', '/src/')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// \u786E\u4FDD\u8DEF\u5F84\u4EE5 / \u5F00\u5934\uFF08\u76F8\u5BF9\u4E8E\u9879\u76EE\u6839\u76EE\u5F55\uFF09
|
|
99
|
-
if (!componentPath.startsWith('/') && !componentPath.startsWith('./')) {
|
|
100
|
-
componentPath = '/' + componentPath
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
104
|
-
console.log(\`\u{1F50D} [\u9884\u52A0\u8F7D] \u5C1D\u8BD5\u52A0\u8F7D: \${componentPath}\`)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const module = await import(/* @vite-ignore */ componentPath)
|
|
108
|
-
const loadTime = Date.now() - startTime
|
|
109
|
-
|
|
110
|
-
this.preloadedRoutes.add(route.path)
|
|
111
|
-
this.stats.completed++
|
|
112
|
-
|
|
113
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
114
|
-
console.log(\`\u2705 [\u9884\u52A0\u8F7D] \${route.path} (\${loadTime}ms) - \${route.reason}\`)
|
|
115
|
-
}
|
|
116
|
-
} catch (error) {
|
|
117
|
-
this.stats.failed++
|
|
118
|
-
if (PRELOAD_OPTIONS.debug) {
|
|
119
|
-
console.error(\`\u274C [\u9884\u52A0\u8F7D] \${route.path} \u5931\u8D25:\`, error)
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
showStatus() {
|
|
125
|
-
if (!PRELOAD_OPTIONS.showStatus || !document.body) return
|
|
126
|
-
|
|
127
|
-
// \u6DFB\u52A0\u6837\u5F0F
|
|
128
|
-
if (!document.getElementById('preloader-styles')) {
|
|
129
|
-
const style = document.createElement('style')
|
|
130
|
-
style.id = 'preloader-styles'
|
|
131
|
-
const position = PRELOAD_OPTIONS.statusPosition.replace('-', ': 20px; ') + ': 20px;'
|
|
132
|
-
style.textContent = \`
|
|
133
|
-
.preloader-status {
|
|
134
|
-
position: fixed; \${position}
|
|
135
|
-
background: rgba(0,0,0,0.8); color: white; padding: 8px 16px;
|
|
136
|
-
border-radius: 6px; font-size: 12px; z-index: 9999;
|
|
137
|
-
pointer-events: none; font-family: system-ui;
|
|
138
|
-
transition: opacity 0.3s ease;
|
|
139
|
-
}
|
|
140
|
-
.preloader-status.fade-out {
|
|
141
|
-
opacity: 0;
|
|
142
|
-
}
|
|
143
|
-
\`
|
|
144
|
-
document.head.appendChild(style)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// \u521B\u5EFA\u72B6\u6001\u5143\u7D20
|
|
148
|
-
this.statusElement = document.createElement('div')
|
|
149
|
-
this.statusElement.className = 'preloader-status'
|
|
150
|
-
this.updateStatus()
|
|
151
|
-
document.body.appendChild(this.statusElement)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
updateStatus() {
|
|
155
|
-
if (!this.statusElement) return
|
|
156
|
-
this.statusElement.textContent = \`\u{1F504} \u6B63\u5728\u4F18\u5316\u9875\u9762... \${this.stats.completed}/\${this.stats.total}\`
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
hideStatus() {
|
|
160
|
-
if (!this.statusElement) return
|
|
161
|
-
this.statusElement.classList.add('fade-out')
|
|
162
|
-
setTimeout(() => {
|
|
163
|
-
if (this.statusElement && this.statusElement.parentNode) {
|
|
164
|
-
this.statusElement.parentNode.removeChild(this.statusElement)
|
|
165
|
-
}
|
|
166
|
-
}, 300)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
sleep(ms) {
|
|
170
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
isPreloaded(path) {
|
|
174
|
-
return this.preloadedRoutes.has(path)
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
getStats() {
|
|
178
|
-
return {
|
|
179
|
-
...this.stats,
|
|
180
|
-
preloadedPaths: Array.from(this.preloadedRoutes),
|
|
181
|
-
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);
|
|
182
70
|
}
|
|
183
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);
|
|
184
74
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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;
|
|
209
102
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
window.usePreloader = () => ({
|
|
216
|
-
start: () => preloader.start(),
|
|
217
|
-
isPreloaded: (path) => preloader.isPreloaded(path),
|
|
218
|
-
getStats: () => preloader.getStats()
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
})();`;
|
|
103
|
+
${debugCode}
|
|
104
|
+
${statusUiCode}
|
|
105
|
+
})();
|
|
106
|
+
</script>`;
|
|
107
|
+
}
|
|
222
108
|
|
|
223
109
|
// src/generator.ts
|
|
110
|
+
var import_vite = require("vite");
|
|
111
|
+
var import_node_path = __toESM(require("path"));
|
|
224
112
|
var CodeGenerator = class {
|
|
225
113
|
constructor(options) {
|
|
226
114
|
this.options = options;
|
|
115
|
+
/** @ 别名对应的相对路径(相对于 viteRoot),默认 'src' */
|
|
116
|
+
this.srcAlias = "src";
|
|
117
|
+
this.viteRoot = process.cwd();
|
|
227
118
|
}
|
|
228
119
|
/**
|
|
229
|
-
*
|
|
120
|
+
* 从 Vite resolvedConfig 中读取项目根目录和 @ 别名
|
|
121
|
+
* 必须在 configResolved hook 中调用
|
|
230
122
|
*/
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
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
|
+
}
|
|
235
134
|
}
|
|
236
135
|
/**
|
|
237
|
-
*
|
|
136
|
+
* 将用户路由配置解析为内部 ResolvedRoute 列表
|
|
137
|
+
* 同时过滤:含 :param 的动态路由段、exclude 列表匹配项
|
|
238
138
|
*/
|
|
239
|
-
|
|
139
|
+
resolveRoutes() {
|
|
140
|
+
if (!Array.isArray(this.options.routes) || this.options.routes.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
const { exclude } = this.options;
|
|
240
144
|
return this.options.routes.map((route) => {
|
|
241
145
|
if (typeof route === "string") {
|
|
242
|
-
const componentPath2 = this.inferComponentPath(route);
|
|
243
146
|
return {
|
|
244
147
|
path: route,
|
|
245
|
-
component:
|
|
148
|
+
component: this.inferComponentPath(route),
|
|
246
149
|
reason: "\u81EA\u52A8\u63A8\u65AD\u7684\u9884\u52A0\u8F7D\u9875\u9762",
|
|
247
150
|
priority: 2
|
|
248
151
|
};
|
|
249
152
|
}
|
|
250
|
-
const componentPath = route.component || this.inferComponentPath(route.path);
|
|
251
153
|
return {
|
|
252
154
|
path: route.path,
|
|
253
|
-
component:
|
|
254
|
-
reason: route.reason
|
|
255
|
-
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
|
|
256
158
|
};
|
|
257
|
-
})
|
|
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);
|
|
258
168
|
}
|
|
259
169
|
/**
|
|
260
|
-
*
|
|
170
|
+
* 根据 Rollup OutputBundle 中的 facadeModuleId 为各路由匹配实际 chunk 文件路径
|
|
171
|
+
* 仅在生产构建的 generateBundle hook 中调用
|
|
261
172
|
*/
|
|
262
|
-
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
+
});
|
|
274
186
|
}
|
|
275
187
|
/**
|
|
276
|
-
*
|
|
188
|
+
* 生成 <link> 标签字符串(注入到 </body> 前)
|
|
189
|
+
* 生产:<link rel="prefetch"> 指向实际 chunk(浏览器空闲时拉取,完全不阻塞主线程)
|
|
190
|
+
* 开发:<link rel="modulepreload"> 指向源文件(Vite dev server 处理解析)
|
|
277
191
|
*/
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
}
|
|
282
206
|
}
|
|
283
|
-
|
|
284
|
-
return `@/views/${pathSegments.join("/")}/index.vue`;
|
|
207
|
+
return tags.join("\n");
|
|
285
208
|
}
|
|
286
209
|
/**
|
|
287
|
-
*
|
|
210
|
+
* 生成可选的状态 UI 和调试工具脚本
|
|
211
|
+
* showStatus=false 且 debug=false 时返回空字符串(0 运行时开销)
|
|
288
212
|
*/
|
|
289
|
-
|
|
290
|
-
return
|
|
291
|
-
|
|
292
|
-
|
|
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);
|
|
293
244
|
}
|
|
294
245
|
};
|
|
295
246
|
|
|
296
247
|
// src/index.ts
|
|
297
|
-
function preloaderPlugin(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
debug:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
showStatus: true,
|
|
306
|
-
// 默认显示状态
|
|
307
|
-
statusPosition: "bottom-right",
|
|
308
|
-
// 默认右下角
|
|
309
|
-
...options
|
|
310
|
-
// 用户配置覆盖默认配置
|
|
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 ?? []
|
|
311
256
|
};
|
|
257
|
+
const generator = new CodeGenerator(options);
|
|
258
|
+
let resolvedRoutes = [];
|
|
259
|
+
let isBuild = false;
|
|
312
260
|
return {
|
|
313
261
|
name: "vite-plugin-preloader",
|
|
314
|
-
//
|
|
262
|
+
// post 确保在其他插件(如 @vitejs/plugin-vue)处理完后执行
|
|
315
263
|
enforce: "post",
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
+
}
|
|
320
284
|
}
|
|
321
285
|
},
|
|
322
|
-
//
|
|
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
|
+
});
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
// ✅ 注入 <link> 标签 + 可选运行时脚本到 </body> 前
|
|
323
305
|
transformIndexHtml(html) {
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
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;
|
|
327
318
|
},
|
|
328
|
-
//
|
|
319
|
+
// HMR:vite.config 变更时触发完整刷新
|
|
329
320
|
handleHotUpdate(ctx) {
|
|
330
321
|
if (ctx.file.includes("vite.config")) {
|
|
331
|
-
|
|
332
|
-
console.log("\u{1F504} [\u9884\u52A0\u8F7D\u63D2\u4EF6] \u914D\u7F6E\u5DF2\u66F4\u65B0");
|
|
333
|
-
}
|
|
334
|
-
ctx.server.ws.send({
|
|
335
|
-
type: "full-reload"
|
|
336
|
-
});
|
|
322
|
+
ctx.server.ws.send({ type: "full-reload" });
|
|
337
323
|
return [];
|
|
338
324
|
}
|
|
339
325
|
return void 0;
|