vite-plugin-compile-progress.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. // DOM 元素引用
  96. const progressBar = document.getElementById('vite-progress-bar');
  97. const statusDisplay = document.getElementById('vite-progress-status');
  98. // 预估资源总数
  99. function estimateResourceCount() {
  100. const scripts = document.querySelectorAll('script[src]').length;
  101. const links = document.querySelectorAll('link[href]').length;
  102. const styleSheets = document.querySelectorAll('link[rel="stylesheet"]').length;
  103. const preloadLinks = document.querySelectorAll('link[rel="preload"], link[rel="modulepreload"]').length;
  104. // 基于现有资源数量和经验值估算
  105. const baseEstimate = scripts + links + styleSheets + preloadLinks;
  106. const dynamicEstimate = Math.max(baseEstimate * 1.5, 10); // 至少10个资源
  107. return Math.floor(dynamicEstimate);
  108. }
  109. // 更新进度条和状态显示
  110. function updateProgress() {
  111. if (!progressBar || !statusDisplay) return;
  112. // 服务端进度权重70%,客户端资源进度权重30%
  113. const serverWeight = 0.7;
  114. const clientWeight = 0.3;
  115. const serverPart = serverProgress * serverWeight;
  116. const clientPart = estimatedTotal > 0 ?
  117. (resourcesCompleted / estimatedTotal) * clientWeight : 0;
  118. const totalProgress = Math.min((serverPart + clientPart) * 100, 100);
  119. // 更新进度条
  120. progressBar.style.width = totalProgress + '%';
  121. // 更新状态显示
  122. if (isLoading && totalProgress < 100) {
  123. statusDisplay.style.display = 'block';
  124. statusDisplay.textContent =
  125. \`Loading... \${Math.round(totalProgress)}% (\${resourcesCompleted}/\${estimatedTotal} resources)\`;
  126. } else if (totalProgress >= 100) {
  127. isLoading = false;
  128. statusDisplay.style.display = 'none';
  129. progressBar.classList.add('complete');
  130. // 1秒后隐藏进度条
  131. setTimeout(() => {
  132. progressBar.style.display = 'none';
  133. }, 1000);
  134. }
  135. console.log(\`Progress: Server \${Math.round(serverProgress * 100)}%, Resources \${resourcesCompleted}/\${estimatedTotal}, Total \${Math.round(totalProgress)}%\`);
  136. }
  137. // 监听服务端编译进度
  138. import.meta.hot.on('progress:update', (data) => {
  139. serverProgress = data.total > 0 ? data.processed / data.total : 0;
  140. updateProgress();
  141. });
  142. // 使用 PerformanceObserver 监控资源加载完成
  143. if (window.PerformanceObserver) {
  144. const completedResources = new Set();
  145. const observer = new PerformanceObserver((list) => {
  146. for (const entry of list.getEntries()) {
  147. if (entry.entryType === 'resource') {
  148. const isRelevantResource =
  149. entry.initiatorType === 'script' ||
  150. entry.initiatorType === 'link' ||
  151. entry.initiatorType === 'css' ||
  152. entry.name.includes('.js') ||
  153. entry.name.includes('.css') ||
  154. entry.name.includes('.ts');
  155. if (isRelevantResource && !completedResources.has(entry.name)) {
  156. completedResources.add(entry.name);
  157. resourcesCompleted++;
  158. // 动态调整预估总数
  159. if (resourcesCompleted > estimatedTotal * 0.8) {
  160. estimatedTotal = Math.max(estimatedTotal, resourcesCompleted + 5);
  161. }
  162. console.log(\`Resource completed: \${entry.name} (type: \${entry.initiatorType})\`);
  163. updateProgress();
  164. }
  165. }
  166. }
  167. });
  168. observer.observe({ entryTypes: ['resource'] });
  169. }
  170. // 监控动态导入
  171. const originalImport = window.import;
  172. if (originalImport) {
  173. window.import = async function(specifier) {
  174. console.log(\`Dynamic import started: \${specifier}\`);
  175. try {
  176. const result = await originalImport.call(this, specifier);
  177. console.log(\`Dynamic import completed: \${specifier}\`);
  178. return result;
  179. } catch (error) {
  180. console.log(\`Dynamic import failed: \${specifier}\`);
  181. throw error;
  182. }
  183. };
  184. }
  185. // 初始化
  186. function initialize() {
  187. estimatedTotal = estimateResourceCount();
  188. console.log(\`Estimated total resources: \${estimatedTotal}\`);
  189. // 显示初始状态
  190. if (statusDisplay) {
  191. statusDisplay.style.display = 'block';
  192. statusDisplay.textContent = \`Initializing... (0/\${estimatedTotal} resources)\`;
  193. }
  194. updateProgress();
  195. }
  196. // 页面加载完成后的处理
  197. function handlePageLoad() {
  198. // 给一些时间让所有资源完成加载
  199. setTimeout(() => {
  200. if (resourcesCompleted < estimatedTotal * 0.5) {
  201. // 如果完成的资源太少,调整预估值
  202. estimatedTotal = Math.max(resourcesCompleted + 2, 5);
  203. console.log(\`Adjusted estimated total to: \${estimatedTotal}\`);
  204. }
  205. updateProgress();
  206. }, 1000);
  207. }
  208. // 根据文档状态初始化
  209. if (document.readyState === 'loading') {
  210. document.addEventListener('DOMContentLoaded', initialize);
  211. document.addEventListener('load', handlePageLoad);
  212. } else {
  213. setTimeout(initialize, 0);
  214. if (document.readyState === 'complete') {
  215. setTimeout(handlePageLoad, 100);
  216. } else {
  217. document.addEventListener('load', handlePageLoad);
  218. }
  219. }
  220. // 错误处理
  221. window.addEventListener('error', (event) => {
  222. console.log(\`Resource load error: \${event.filename || 'unknown'}\`);
  223. });
  224. // HMR 更新时重置状态
  225. import.meta.hot.on('vite:beforeUpdate', () => {
  226. console.log('HMR update detected, maintaining progress tracking...');
  227. });
  228. }
  229. `,
  230. injectTo: 'body'
  231. }
  232. ]
  233. }
  234. }
  235. }
  236. }
  237. }