Skip to main content
Glama
caniuse-service.js17.3 kB
/** * Can I Use API 服務 * 動態查詢瀏覽器相容性資料 */ /** * Can I Use 服務 */ export class CanIUseService { // 官方 Can I Use GitHub 資料來源(主要) dataUrls = [ 'https://raw.githubusercontent.com/Fyrd/caniuse/main/data.json', 'https://cdn.jsdelivr.net/npm/caniuse-db@latest/data.json', 'https://raw.githubusercontent.com/nicxvan/caniuse/main/fulldata-json/data-2.0.json' ]; cachedData = null; cacheTime = 0; cacheDuration = 24 * 60 * 60 * 1000; // 24 小時 /** * 載入 Can I Use 資料(支援 fallback) */ async loadData() { // 檢查快取 if (this.cachedData && Date.now() - this.cacheTime < this.cacheDuration) { return this.cachedData; } let lastError = null; // 嘗試所有資料來源 for (const url of this.dataUrls) { try { const response = await fetch(url, { headers: { 'Accept': 'application/json', 'User-Agent': 'DevAdvisor-MCP/1.0' } }); if (!response.ok) { lastError = new Error(`Can I Use 資料載入失敗: ${response.status} (${url})`); continue; } this.cachedData = await response.json(); this.cacheTime = Date.now(); return this.cachedData; } catch (error) { lastError = error; console.warn(`嘗試載入 ${url} 失敗,嘗試下一個來源...`); } } // 所有來源都失敗 console.error('所有 Can I Use 資料來源都載入失敗:', lastError); throw new Error(`Can I Use 資料載入失敗: 所有資料來源都無法存取`); } /** * 搜尋功能 */ async searchFeature(query) { const data = await this.loadData(); const features = data.data || {}; const lowerQuery = query.toLowerCase(); const matches = []; for (const [id, feature] of Object.entries(features)) { let score = 0; // 完全匹配 ID if (id.toLowerCase() === lowerQuery) { score = 100; } // ID 包含查詢 else if (id.toLowerCase().includes(lowerQuery)) { score = 80; } // 標題匹配 else if (feature.title?.toLowerCase().includes(lowerQuery)) { score = 60; } // 描述匹配 else if (feature.description?.toLowerCase().includes(lowerQuery)) { score = 40; } // 關鍵字匹配 else if (feature.keywords?.toLowerCase().includes(lowerQuery)) { score = 30; } if (score > 0) { matches.push({ id, score }); } } // 按分數排序並返回 ID return matches .sort((a, b) => b.score - a.score) .slice(0, 10) .map(m => m.id); } /** * 取得功能的瀏覽器支援資料 */ async getFeatureSupport(featureId) { const data = await this.loadData(); const feature = data.data?.[featureId]; if (!feature) { // 嘗試搜尋 const matches = await this.searchFeature(featureId); if (matches.length > 0) { return this.getFeatureSupport(matches[0]); } return null; } const browsers = data.agents || {}; return { featureId, title: feature.title || featureId, description: feature.description || '', status: feature.status || 'other', usage: this.calculateGlobalUsage(feature, browsers), categories: feature.categories || [], browsers: this.parseBrowserSupport(feature.stats, browsers), notes: this.parseNotes(feature.notes_by_num), links: feature.links?.map((l) => ({ url: l.url, title: l.title })), mdnUrl: feature.mdn_url, specUrl: feature.spec }; } /** * 計算全球使用率 */ calculateGlobalUsage(feature, browsers) { // 簡化計算:基於主要瀏覽器的支援狀況 const stats = feature.stats || {}; let supportedUsage = 0; const majorBrowsers = ['chrome', 'firefox', 'safari', 'edge']; for (const browser of majorBrowsers) { const browserStats = stats[browser] || {}; const latestVersion = Object.keys(browserStats).pop(); if (latestVersion) { const support = browserStats[latestVersion]; if (support?.startsWith('y') || support === 'y') { supportedUsage += 25; // 每個主要瀏覽器約 25% } else if (support?.startsWith('a')) { supportedUsage += 15; } } } return Math.min(supportedUsage, 100); } /** * 解析瀏覽器支援資料 */ parseBrowserSupport(stats, browsers) { const browserList = ['chrome', 'firefox', 'safari', 'edge', 'ie', 'opera', 'ios_saf', 'android', 'samsung']; const result = {}; for (const browser of browserList) { const browserStats = stats?.[browser] || {}; const versions = Object.entries(browserStats); // 找到第一個支援的版本 let sinceVersion; let currentSupport = 'n'; let hasPartialSupport = false; for (const [version, support] of versions) { if (support.startsWith('y') && !sinceVersion) { sinceVersion = version; } if (support.startsWith('a')) { hasPartialSupport = true; } } // 取得最新版本的支援狀態 const latestVersion = versions[versions.length - 1]; if (latestVersion) { const [, support] = latestVersion; currentSupport = support.charAt(0) || 'u'; } result[browser] = { supported: currentSupport === 'y' || currentSupport === 'a', partialSupport: hasPartialSupport || currentSupport === 'a', sinceVersion, currentSupport }; } return result; } /** * 解析備註 */ parseNotes(notesByNum) { if (!notesByNum) return undefined; return Object.values(notesByNum).slice(0, 5); } /** * 檢查 API 在目標瀏覽器中的相容性 */ async checkCompatibility(featureId, targetBrowsers = { chrome: '90', firefox: '88', safari: '14', edge: '90' }) { const support = await this.getFeatureSupport(featureId); if (!support) { return { feature: featureId, globalSupport: 0, supported: [], notSupported: Object.keys(targetBrowsers), partialSupport: [], recommendation: `找不到 ${featureId} 的相容性資料,建議查詢 MDN 文件`, polyfillAvailable: false }; } const supported = []; const notSupported = []; const partialSupport = []; for (const [browser, targetVersion] of Object.entries(targetBrowsers)) { const browserSupport = support.browsers[browser]; if (!browserSupport) { notSupported.push(browser); continue; } if (browserSupport.supported) { if (browserSupport.sinceVersion) { const sinceNum = parseFloat(browserSupport.sinceVersion); const targetNum = parseFloat(targetVersion); if (sinceNum <= targetNum) { if (browserSupport.partialSupport) { partialSupport.push(`${browser} >= ${browserSupport.sinceVersion}`); } else { supported.push(`${browser} >= ${browserSupport.sinceVersion}`); } } else { notSupported.push(`${browser} < ${browserSupport.sinceVersion}`); } } else { supported.push(browser); } } else { notSupported.push(browser); } } // 生成建議 let recommendation = ''; if (notSupported.length === 0) { recommendation = '✅ 所有目標瀏覽器都支援此功能'; } else if (supported.length === 0) { recommendation = '❌ 此功能在目標瀏覽器中不受支援,需要 polyfill 或替代方案'; } else { recommendation = `⚠️ 部分瀏覽器不支援:${notSupported.join(', ')}。建議提供 fallback 或使用 polyfill`; } return { feature: support.title, globalSupport: support.usage, supported, notSupported, partialSupport, recommendation, polyfillAvailable: this.checkPolyfillAvailable(featureId), polyfillUrl: this.getPolyfillUrl(featureId) }; } /** * 檢查是否有 polyfill */ checkPolyfillAvailable(featureId) { const polyfillableFeatures = [ 'fetch', 'promise', 'array-includes', 'object-assign', 'string-includes', 'array-find', 'array-from', 'symbol', 'map', 'set', 'weakmap', 'weakset', 'proxy', 'reflect', 'intersectionobserver', 'resizeobserver', 'mutationobserver', 'customelements', 'shadowdom', 'template', 'dialog' ]; return polyfillableFeatures.some(f => featureId.toLowerCase().includes(f)); } /** * 取得 polyfill URL */ getPolyfillUrl(featureId) { // polyfill.io 服務 const polyfillIoFeatures = { 'fetch': 'fetch', 'promise': 'Promise', 'array-includes': 'Array.prototype.includes', 'object-assign': 'Object.assign', 'string-includes': 'String.prototype.includes', 'array-find': 'Array.prototype.find', 'array-from': 'Array.from', 'symbol': 'Symbol', 'map': 'Map', 'set': 'Set', 'weakmap': 'WeakMap', 'weakset': 'WeakSet', 'intersectionobserver': 'IntersectionObserver', 'resizeobserver': 'ResizeObserver', }; for (const [key, polyfillName] of Object.entries(polyfillIoFeatures)) { if (featureId.toLowerCase().includes(key)) { return `https://polyfill.io/v3/polyfill.min.js?features=${polyfillName}`; } } return undefined; } /** * 批次檢查多個功能的相容性 */ async checkMultipleFeatures(featureIds, targetBrowsers) { const reports = []; for (const featureId of featureIds) { const report = await this.checkCompatibility(featureId, targetBrowsers); reports.push(report); } return reports; } /** * 取得特定類別的功能列表 */ async getFeaturesByCategory(category) { const data = await this.loadData(); const features = data.data || {}; const matches = []; const lowerCategory = category.toLowerCase(); for (const [id, feature] of Object.entries(features)) { const categories = feature.categories || []; if (categories.some((c) => c.toLowerCase().includes(lowerCategory))) { matches.push(id); } } return matches; } /** * 取得所有可用的 API 類別 * 從 Can I Use 資料庫中提取所有唯一的類別 */ async getAllCategories() { const data = await this.loadData(); const features = data.data || {}; // 統計每個類別的功能數量 const categoryMap = new Map(); for (const [, feature] of Object.entries(features)) { const categories = feature.categories || []; for (const category of categories) { const count = categoryMap.get(category) || 0; categoryMap.set(category, count + 1); } } // 轉換為陣列並排序 const categories = Array.from(categoryMap.entries()) .map(([name, count]) => ({ name, count, description: this.getCategoryDescription(name) })) .sort((a, b) => b.count - a.count); // 按數量降序排列 return categories; } /** * 取得類別的描述 */ getCategoryDescription(category) { const descriptions = { 'CSS': 'CSS 相關功能,包括佈局、動畫、選擇器等', 'HTML': 'HTML 元素和屬性', 'JavaScript': 'JavaScript API 和語言特性', 'SVG': 'SVG 圖形相關功能', 'Canvas': 'Canvas 繪圖 API', 'Security': '安全性相關 API', 'Performance': '效能相關 API', 'Network': '網路請求相關 API', 'Storage': '資料儲存相關 API', 'Media': '媒體處理相關 API(音訊、視訊)', 'Graphics': '圖形渲染相關 API', 'DOM': 'DOM 操作相關 API', 'Events': '事件處理相關 API', 'Forms': '表單相關 API', 'Mobile': '行動裝置相關功能', 'Other': '其他功能', 'JS API': 'JavaScript API', 'Misc': '雜項功能', 'APIs': 'Web API', 'HTML5': 'HTML5 功能', 'CSS3': 'CSS3 功能' }; return descriptions[category] || `包含 ${category} 相關的 Web API 和功能`; } } /** * API 名稱到 Can I Use ID 的映射 * 用於從常見的 API 呼叫名稱查詢對應的 caniuse feature ID */ export const API_NAME_TO_CANIUSE = { // Fetch & Network 'fetch': 'fetch', 'AbortController': 'abortcontroller', 'Headers': 'fetch', 'Request': 'fetch', 'Response': 'fetch', // DOM APIs 'querySelector': 'queryselector', 'querySelectorAll': 'queryselector', 'classList': 'classlist', 'MutationObserver': 'mutationobserver', 'IntersectionObserver': 'intersectionobserver', 'ResizeObserver': 'resizeobserver', // Storage 'localStorage': 'namevalue-storage', 'sessionStorage': 'namevalue-storage', 'indexedDB': 'indexeddb', 'IndexedDB': 'indexeddb', // Media 'getUserMedia': 'stream', 'MediaRecorder': 'mediarecorder', 'AudioContext': 'audio-api', // Graphics 'canvas': 'canvas', 'getContext': 'canvas', 'WebGL': 'webgl', 'WebGL2': 'webgl2', // Async 'Promise': 'promises', 'Worker': 'webworkers', 'SharedWorker': 'sharedworkers', 'ServiceWorker': 'serviceworkers', // URL 'URL': 'url', 'URLSearchParams': 'urlsearchparams', // Events 'CustomEvent': 'customevent', 'BroadcastChannel': 'broadcastchannel', 'WebSocket': 'websockets', 'EventSource': 'eventsource', // File 'FileReader': 'fileapi', 'Blob': 'blobbuilder', 'File': 'fileapi', // Location & Notification 'geolocation': 'geolocation', 'Notification': 'notifications', // Intl 'Intl.DateTimeFormat': 'internationalization', 'Intl.NumberFormat': 'internationalization', // Crypto 'crypto.randomUUID': 'mdn-api_crypto_randomuuid', 'crypto.subtle': 'cryptography', // Animation 'requestAnimationFrame': 'requestanimationframe', 'animate': 'web-animation', // ES6+ 'Proxy': 'proxy', 'Symbol': 'es6', 'Map': 'es6', 'Set': 'es6', 'WeakMap': 'es6', 'WeakSet': 'es6', 'Array.from': 'array-from', 'Array.includes': 'array-includes', 'Object.assign': 'object-assign', 'Object.entries': 'object-entries', 'Object.values': 'object-values', 'String.padStart': 'pad-start-end', 'String.padEnd': 'pad-start-end', 'Array.flat': 'array-flat', 'Array.flatMap': 'array-flat', 'BigInt': 'bigint', }; /** * 從 API 名稱取得 Can I Use ID */ export function getCaniuseIdFromApiName(apiName) { // 直接匹配 if (API_NAME_TO_CANIUSE[apiName]) { return API_NAME_TO_CANIUSE[apiName]; } // 嘗試匹配部分名稱 for (const [key, value] of Object.entries(API_NAME_TO_CANIUSE)) { if (apiName.includes(key) || key.includes(apiName)) { return value; } } return null; } /** * 建立 Can I Use 服務實例 */ export function createCanIUseService() { return new CanIUseService(); } //# sourceMappingURL=caniuse-service.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mukiwu/dev-advisor-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server