vite-plugin-compile-progress.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. export function progressTrackingPlugin() {
  2. let server
  3. const moduleStats = {
  4. total: 0,
  5. processed: 0,
  6. pending: new Set()
  7. }
  8. return {
  9. name: 'progress-tracking',
  10. apply: 'serve',
  11. enforce: 'pre',
  12. configureServer(_server) {
  13. server = _server
  14. },
  15. async transform(code, id) {
  16. if (server && id) {
  17. moduleStats.total++
  18. moduleStats.pending.add(id)
  19. server.ws.send('progress:update', {
  20. total: moduleStats.total,
  21. processed: moduleStats.processed,
  22. current: id
  23. })
  24. await server.waitForRequestsIdle(id)
  25. moduleStats.processed++
  26. moduleStats.pending.delete(id)
  27. server.ws.send('progress:update', {
  28. total: moduleStats.total,
  29. processed: moduleStats.processed,
  30. current: null
  31. })
  32. }
  33. },
  34. transformIndexHtml: {
  35. order: 'pre',
  36. handler(html, ctx) {
  37. return {
  38. html,
  39. tags: [
  40. {
  41. tag: 'style',
  42. children: `
  43. #vite-progress-bar {
  44. position: fixed;
  45. top: 0;
  46. left: 0;
  47. width: 0%;
  48. height: 3px;
  49. background: #646cff;
  50. transition: width 0.3s ease;
  51. z-index: 9999;
  52. opacity: 1;
  53. }
  54. #vite-progress-bar.complete {
  55. opacity: 0;
  56. transition: opacity 0.5s ease;
  57. }
  58. #vite-progress-status {
  59. position: fixed;
  60. top: 10px;
  61. right: 10px;
  62. background: rgba(0, 0, 0, 0.8);
  63. color: white;
  64. padding: 8px 12px;
  65. border-radius: 4px;
  66. font-size: 12px;
  67. font-family: monospace;
  68. z-index: 10000;
  69. display: none;
  70. }
  71. `,
  72. injectTo: 'head'
  73. },
  74. {
  75. tag: 'div',
  76. attrs: { id: 'vite-progress-bar' },
  77. injectTo: 'body-prepend'
  78. },
  79. {
  80. tag: 'div',
  81. attrs: { id: 'vite-progress-status' },
  82. children: 'Loading...',
  83. injectTo: 'body-prepend'
  84. },
  85. {
  86. tag: 'script',
  87. attrs: { type: 'module' },
  88. children: `
  89. if (import.meta.hot) {
  90. // 进度追踪状态
  91. let serverProgress = 0;
  92. let resourcesCompleted = 0;
  93. let estimatedTotal = 0;
  94. let isLoading = true;
  95. let completionTimer = null;
  96. let lastResourceTime = Date.now();
  97. let hasServerActivity = false;
  98. let serverActivityTimer = null;
  99. // DOM 元素引用
  100. const progressBar = document.getElementById('vite-progress-bar');
  101. const statusDisplay = document.getElementById('vite-progress-status');
  102. // 改进的预估算法
  103. function estimateResourceCount() {
  104. const scripts = document.querySelectorAll('script[src]').length;
  105. const links = document.querySelectorAll('link[href]').length;
  106. const styleSheets = document.querySelectorAll('link[rel="stylesheet"]').length;
  107. const preloadLinks = document.querySelectorAll('link[rel="preload"], link[rel="modulepreload"]').length;
  108. const baseEstimate = scripts + links + styleSheets + preloadLinks;
  109. const conservativeEstimate = Math.max(baseEstimate + 8, 12);
  110. return conservativeEstimate;
  111. }
  112. // 检测服务端活动
  113. function detectServerActivity() {
  114. hasServerActivity = true;
  115. if (serverActivityTimer) clearTimeout(serverActivityTimer);
  116. // 如果2秒内没有新的服务端活动,认为服务端编译已完成或无需编译
  117. serverActivityTimer = setTimeout(() => {
  118. if (serverProgress === 0) {
  119. console.log('No server compilation detected - using client-only mode');
  120. hasServerActivity = false;
  121. updateProgress();
  122. }
  123. }, 2000);
  124. }
  125. // 在 checkCompletion 函数中进一步优化完成条件
  126. function checkCompletion() {
  127. const now = Date.now();
  128. const timeSinceLastResource = now - lastResourceTime;
  129. // 添加详细的调试日志
  130. console.log('=== checkCompletion Debug ===');
  131. console.log('hasServerActivity:', hasServerActivity);
  132. console.log('serverProgress:', serverProgress);
  133. console.log('resourcesCompleted:', resourcesCompleted);
  134. console.log('estimatedTotal:', estimatedTotal);
  135. console.log('timeSinceLastResource:', timeSinceLastResource);
  136. console.log('isLoading:', isLoading);
  137. // 如果已经不在加载状态,直接返回
  138. if (!isLoading) {
  139. console.log('⚠️ Not loading anymore, exiting checkCompletion');
  140. return;
  141. }
  142. // 根据服务端活动情况调整完成条件
  143. if (!hasServerActivity) {
  144. console.log('Checking client-only completion conditions...');
  145. // 客户端模式下的完成条件更加宽松
  146. if (resourcesCompleted >= estimatedTotal * 0.8 && timeSinceLastResource > 1000) {
  147. console.log('✅ Client-only loading complete (80% threshold)');
  148. forceComplete();
  149. return;
  150. }
  151. // 如果进度超过95%且1秒无新资源,立即完成
  152. if (resourcesCompleted >= estimatedTotal * 0.95 && timeSinceLastResource > 1000) {
  153. console.log('✅ Client-only loading complete (95% threshold)');
  154. forceComplete();
  155. return;
  156. }
  157. // 如果进度超过97%且0.5秒无新资源,立即完成
  158. if (resourcesCompleted >= estimatedTotal * 0.97 && timeSinceLastResource > 500) {
  159. console.log('✅ Client-only loading complete (97% threshold)');
  160. forceComplete();
  161. return;
  162. }
  163. console.log('❌ Client-only conditions not met');
  164. } else {
  165. console.log('Checking mixed mode completion conditions...');
  166. // 有服务端编译时的完成条件 - 添加多个完成条件
  167. if (serverProgress >= 1.0 && timeSinceLastResource > 1500) {
  168. console.log('✅ Server + client loading complete (1.5s timeout)');
  169. forceComplete();
  170. return;
  171. }
  172. // 添加基于资源完成度的提前完成条件
  173. if (serverProgress >= 1.0 && resourcesCompleted >= estimatedTotal * 0.95 && timeSinceLastResource > 800) {
  174. console.log('✅ Server + client loading complete (95% resources + 0.8s)');
  175. forceComplete();
  176. return;
  177. }
  178. // 如果服务端完成且资源接近完成,更短的等待时间
  179. if (serverProgress >= 1.0 && resourcesCompleted >= estimatedTotal * 0.98 && timeSinceLastResource > 600) {
  180. console.log('✅ Server + client loading complete (98% resources + 0.6s)');
  181. forceComplete();
  182. return;
  183. }
  184. console.log('❌ Mixed mode conditions not met');
  185. console.log('serverProgress >= 1.0:', serverProgress >= 1.0);
  186. console.log('timeSinceLastResource > 1500:', timeSinceLastResource > 1500);
  187. console.log('resourcesCompleted >= estimatedTotal * 0.95:', resourcesCompleted >= estimatedTotal * 0.95);
  188. }
  189. // 超时强制完成 - 进一步缩短超时时间
  190. if (timeSinceLastResource > 2000) {
  191. console.log('⏰ Force completing due to timeout (2s)');
  192. forceComplete();
  193. return;
  194. }
  195. // 关键修复:如果条件未满足,重新调度检查
  196. // 根据当前状态动态计算下次检查的时间间隔
  197. let nextCheckDelay;
  198. if (hasServerActivity && serverProgress >= 1.0) {
  199. // 服务端已完成,更频繁地检查
  200. const remainingTimeFor1500 = Math.max(1500 - timeSinceLastResource, 0);
  201. const remainingTimeFor800 = Math.max(800 - timeSinceLastResource, 0);
  202. if (resourcesCompleted >= estimatedTotal * 0.95) {
  203. nextCheckDelay = Math.max(remainingTimeFor800, 200);
  204. } else {
  205. nextCheckDelay = Math.max(remainingTimeFor1500, 300);
  206. }
  207. } else if (!hasServerActivity) {
  208. // 客户端模式,根据资源完成度调整检查频率
  209. if (resourcesCompleted >= estimatedTotal * 0.97) {
  210. nextCheckDelay = Math.max(500 - timeSinceLastResource, 200);
  211. } else if (resourcesCompleted >= estimatedTotal * 0.95) {
  212. nextCheckDelay = Math.max(1000 - timeSinceLastResource, 300);
  213. } else {
  214. nextCheckDelay = Math.max(1000 - timeSinceLastResource, 500);
  215. }
  216. } else {
  217. // 服务端还在处理中,较长的检查间隔
  218. nextCheckDelay = 500;
  219. }
  220. // 确保最小检查间隔
  221. nextCheckDelay = Math.max(nextCheckDelay, 200);
  222. console.log('⏳ Rescheduling completion check in:', nextCheckDelay, 'ms');
  223. console.log('=== End checkCompletion Debug ===');
  224. // 重新调度下次检查
  225. if (completionTimer) clearTimeout(completionTimer);
  226. completionTimer = setTimeout(checkCompletion, nextCheckDelay);
  227. }
  228. // 强制完成
  229. function forceComplete() {
  230. if (!isLoading) return;
  231. isLoading = false;
  232. progressBar.style.width = '100%';
  233. statusDisplay.style.display = 'none';
  234. progressBar.classList.add('complete');
  235. console.log('Loading completed!');
  236. setTimeout(() => {
  237. progressBar.style.display = 'none';
  238. }, 1000);
  239. }
  240. // 更新进度条和状态显示
  241. function updateProgress() {
  242. if (!progressBar || !statusDisplay || !isLoading) return;
  243. let totalProgress;
  244. if (!hasServerActivity && serverProgress === 0) {
  245. // 无服务端编译活动,100%依赖客户端资源
  246. const clientProgress = estimatedTotal > 0 ?
  247. Math.min(resourcesCompleted / estimatedTotal, 1.0) : 0;
  248. totalProgress = clientProgress * 100;
  249. console.log(\`Client-only mode: \${resourcesCompleted}/\${estimatedTotal} resources\`);
  250. } else {
  251. // 有服务端编译活动,使用混合权重
  252. const serverWeight = serverProgress < 1.0 ? 0.6 : 0.3;
  253. const clientWeight = serverProgress < 1.0 ? 0.4 : 0.7;
  254. const serverPart = serverProgress * serverWeight;
  255. const clientPart = estimatedTotal > 0 ?
  256. Math.min(resourcesCompleted / estimatedTotal, 1.0) * clientWeight : 0;
  257. totalProgress = (serverPart + clientPart) * 100;
  258. console.log(\`Mixed mode: Server \${Math.round(serverProgress * 100)}%, Client \${resourcesCompleted}/\${estimatedTotal}\`);
  259. }
  260. // 确保进度不会倒退
  261. const currentWidth = parseFloat(progressBar.style.width) || 0;
  262. totalProgress = Math.max(totalProgress, currentWidth);
  263. // 更新进度条
  264. progressBar.style.width = Math.min(totalProgress, 100) + '%';
  265. // 更新状态显示
  266. statusDisplay.style.display = 'block';
  267. const mode = hasServerActivity ? 'Mixed' : 'Client-only';
  268. statusDisplay.textContent =
  269. \`Loading... \${Math.round(totalProgress)}% (\${mode}: \${resourcesCompleted}/\${estimatedTotal})\`;
  270. console.log(\`Progress: Total \${Math.round(totalProgress)}%\`);
  271. // 如果接近完成,开始检测完成状态
  272. if (totalProgress >= 85) {
  273. if (completionTimer) clearTimeout(completionTimer);
  274. completionTimer = setTimeout(checkCompletion, 800);
  275. }
  276. }
  277. // 监听服务端编译进度
  278. import.meta.hot.on('progress:update', (data) => {
  279. detectServerActivity();
  280. serverProgress = data.total > 0 ? data.processed / data.total : 0;
  281. updateProgress();
  282. });
  283. // 使用 PerformanceObserver 监控资源加载完成
  284. if (window.PerformanceObserver) {
  285. const completedResources = new Set();
  286. const observer = new PerformanceObserver((list) => {
  287. for (const entry of list.getEntries()) {
  288. if (entry.entryType === 'resource') {
  289. const isRelevantResource =
  290. entry.initiatorType === 'script' ||
  291. entry.initiatorType === 'link' ||
  292. entry.initiatorType === 'css' ||
  293. entry.name.includes('.js') ||
  294. entry.name.includes('.css') ||
  295. entry.name.includes('.ts');
  296. if (isRelevantResource && !completedResources.has(entry.name)) {
  297. completedResources.add(entry.name);
  298. resourcesCompleted++;
  299. lastResourceTime = Date.now();
  300. // 更保守的总数调整策略
  301. if (resourcesCompleted > estimatedTotal * 0.95) {
  302. estimatedTotal = Math.max(estimatedTotal, resourcesCompleted + 1);
  303. }
  304. console.log(\`Resource completed: \${entry.name} (type: \${entry.initiatorType})\`);
  305. updateProgress();
  306. }
  307. }
  308. }
  309. });
  310. observer.observe({ entryTypes: ['resource'] });
  311. }
  312. // 初始化
  313. function initialize() {
  314. estimatedTotal = estimateResourceCount();
  315. lastResourceTime = Date.now();
  316. console.log(\`Estimated total resources: \${estimatedTotal}\`);
  317. // 显示初始状态
  318. if (statusDisplay) {
  319. statusDisplay.style.display = 'block';
  320. statusDisplay.textContent = \`Initializing... (0/\${estimatedTotal} resources)\`;
  321. }
  322. updateProgress();
  323. // 启动服务端活动检测
  324. setTimeout(() => {
  325. if (!hasServerActivity) {
  326. console.log('No server activity detected, switching to client-only mode');
  327. updateProgress();
  328. }
  329. }, 1000);
  330. }
  331. // 页面加载完成后的处理
  332. function handlePageLoad() {
  333. setTimeout(() => {
  334. if (resourcesCompleted > 0) {
  335. estimatedTotal = Math.max(resourcesCompleted + 1, estimatedTotal);
  336. console.log(\`Final estimated total: \${estimatedTotal}\`);
  337. }
  338. updateProgress();
  339. // 页面加载完成后,更积极地检测完成状态
  340. setTimeout(() => {
  341. if (isLoading && resourcesCompleted >= estimatedTotal * 0.8) {
  342. console.log('Page load complete, forcing finish');
  343. forceComplete();
  344. }
  345. }, 1000);
  346. }, 500);
  347. }
  348. // 根据文档状态初始化
  349. if (document.readyState === 'loading') {
  350. document.addEventListener('DOMContentLoaded', initialize);
  351. document.addEventListener('load', handlePageLoad);
  352. } else {
  353. setTimeout(initialize, 0);
  354. if (document.readyState === 'complete') {
  355. setTimeout(handlePageLoad, 100);
  356. } else {
  357. document.addEventListener('load', handlePageLoad);
  358. }
  359. }
  360. // HMR 更新时重置状态
  361. import.meta.hot.on('vite:beforeUpdate', () => {
  362. console.log('HMR update detected, maintaining progress tracking...');
  363. });
  364. }
  365. `,
  366. injectTo: 'body'
  367. }
  368. ]
  369. }
  370. }
  371. }
  372. }
  373. }