diff --git a/package-lock.json b/package-lock.json index 60b7ff0..0fa5549 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "chrome-extension-router-demo", "version": "0.1.0", "dependencies": { + "@rrweb/all": "^2.0.0-alpha.18", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", @@ -21,7 +22,14 @@ "react-router-dom": "^6.30.2", "react-scripts": "5.0.1", "remark-gfm": "^1.0.0", + "rrweb": "^2.0.0-alpha.4", + "rrweb-player": "^1.0.0-alpha.4", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@types/chrome": "^0.1.36", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3" } }, "node_modules/@adobe/css-tools": { @@ -3066,6 +3074,39 @@ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", "license": "MIT" }, + "node_modules/@rrweb/all": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/@rrweb/all/-/all-2.0.0-alpha.18.tgz", + "integrity": "sha512-QJ1WxHfAdkDadu79/zrZt+yzco6ckPOXEVRMW/RIv+8BIFh3q/j89y9rPbg5BpmH662V5J0GB99f7BKIM+RMJg==", + "license": "MIT", + "dependencies": { + "@rrweb/packer": "^2.0.0-alpha.18", + "@rrweb/types": "^2.0.0-alpha.18", + "rrweb": "^2.0.0-alpha.18" + } + }, + "node_modules/@rrweb/packer": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/@rrweb/packer/-/packer-2.0.0-alpha.18.tgz", + "integrity": "sha512-rEXltE/gnflEv/NatVNPIp4/6EtVlPLSwzIbd4WPSMtQGvwZe5OfAAk/Y88HkbAwb1nHfdA3PtFGn2Epik4inQ==", + "license": "MIT", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.18", + "fflate": "^0.4.4" + } + }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/@rrweb/types/-/types-2.0.0-alpha.18.tgz", + "integrity": "sha512-iMH3amHthJZ9x3gGmBPmdfim7wLGygC2GciIkw2A6SO8giSn8PHYtRT8OKNH4V+k3SZ6RSnYHcTQxBA7pSWZ3Q==", + "license": "MIT" + }, + "node_modules/@rrweb/utils": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/@rrweb/utils/-/utils-2.0.0-alpha.18.tgz", + "integrity": "sha512-qV8azQYo9RuwW4NGRtOiQfTBdHNL1B0Q//uRLMbCSjbaKqJYd88Js17Bdskj65a0Vgp2dwTLPIZ0gK47dfjfaA==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", @@ -3449,6 +3490,12 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/svelte": { + "version": "1.0.13", + "resolved": "https://registry.npmmirror.com/@tsconfig/svelte/-/svelte-1.0.13.tgz", + "integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==", + "license": "MIT" + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3515,6 +3562,17 @@ "@types/node": "*" } }, + "node_modules/@types/chrome": { + "version": "0.1.36", + "resolved": "https://registry.npmmirror.com/@types/chrome/-/chrome-0.1.36.tgz", + "integrity": "sha512-BvHbuyGttYXnGt5Gpwa4769KIinKHY1iLjlAPrrMBS2GI9m/XNMPtdsq0NgQalyuUdxvlMN/0OyGw0shFVIoUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", @@ -3534,6 +3592,12 @@ "@types/node": "*" } }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmmirror.com/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "license": "MIT" + }, "node_modules/@types/eslint": { "version": "8.56.12", "resolved": "https://registry.npmmirror.com/@types/eslint/-/eslint-8.56.12.tgz", @@ -3596,6 +3660,23 @@ "@types/send": "*" } }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmmirror.com/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmmirror.com/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmmirror.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -3605,6 +3686,13 @@ "@types/node": "*" } }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmmirror.com/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "2.3.10", "resolved": "https://registry.npmmirror.com/@types/hast/-/hast-2.3.10.tgz", @@ -3735,15 +3823,24 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.9", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", @@ -4236,6 +4333,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmmirror.com/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==", + "license": "MIT" + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -5127,6 +5230,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.9.7", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.7.tgz", @@ -6390,8 +6502,7 @@ "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -8039,6 +8150,12 @@ "bser": "2.1.1" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -11842,6 +11959,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", @@ -14844,6 +14967,50 @@ "randombytes": "^2.1.0" } }, + "node_modules/rrdom": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/rrdom/-/rrdom-2.0.0-alpha.18.tgz", + "integrity": "sha512-fSFzFFxbqAViITyYVA4Z0o5G6p1nEqEr/N8vdgSKie9Rn0FJxDSNJgjV0yiCIzcDs0QR+hpvgFhpbdZ6JIr5Nw==", + "license": "MIT", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.18" + } + }, + "node_modules/rrweb": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/rrweb/-/rrweb-2.0.0-alpha.18.tgz", + "integrity": "sha512-1mjZcB+LVoGSx1+i9E2ZdAP90fS3MghYVix2wvGlZvrgRuLCbTCCOZMztFCkKpgp7/EeCdYM4nIHJkKX5J1Nmg==", + "license": "MIT", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.18", + "@rrweb/utils": "^2.0.0-alpha.18", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "mitt": "^3.0.0", + "rrdom": "^2.0.0-alpha.18", + "rrweb-snapshot": "^2.0.0-alpha.18" + } + }, + "node_modules/rrweb-player": { + "version": "1.0.0-alpha.4", + "resolved": "https://registry.npmmirror.com/rrweb-player/-/rrweb-player-1.0.0-alpha.4.tgz", + "integrity": "sha512-Wlmn9GZ5Fdqa37vd3TzsYdLl/JWEvXNUrLCrYpnOwEgmY409HwVIvvA5aIo7k582LoKgdRCsB87N+f0oWAR0Kg==", + "license": "MIT", + "dependencies": { + "@tsconfig/svelte": "^1.0.0", + "rrweb": "^2.0.0-alpha.4" + } + }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.18", + "resolved": "https://registry.npmmirror.com/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz", + "integrity": "sha512-hBHZL/NfgQX6wO1D9mpwqFu1NJPpim+moIcKhFEjVTZVRUfCln+LOugRc4teVTCISYHN8Cw5e2iNTWCSm+SkoA==", + "license": "MIT", + "dependencies": { + "postcss": "^8.4.38" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", diff --git a/package.json b/package.json index 3cc6a3a..22e9fae 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@rrweb/all": "^2.0.0-alpha.18", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.1", @@ -16,6 +17,8 @@ "react-router-dom": "^6.30.2", "react-scripts": "5.0.1", "remark-gfm": "^1.0.0", + "rrweb": "^2.0.0-alpha.4", + "rrweb-player": "^1.0.0-alpha.4", "web-vitals": "^2.1.4" }, "scripts": { @@ -41,5 +44,10 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@types/chrome": "^0.1.36", + "@types/react": "^19.2.9", + "@types/react-dom": "^19.2.3" } } diff --git a/public/favicon.ico b/public/favicon.ico index a11777c..4e0c6b0 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/manifest.json b/public/manifest.json index 23b5043..7106cf3 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,11 +1,14 @@ { "manifest_version": 3, - "name": "React Router Chrome Extension", + "name": "Testing Tools", "version": "1.0", - "description": "A Chrome Extension with React Router: User List + Actions Pages", + "description": "测试工具", "permissions": [ "storage", - "clipboardWrite" + "clipboardWrite", + "activeTab", + "scripting", + "tabs" ], "background": { "service_worker": "background.js" diff --git a/src/App.js b/src/App.js index acbd24c..55aeccd 100644 --- a/src/App.js +++ b/src/App.js @@ -2,6 +2,7 @@ import {HashRouter as Router, Routes, Route, NavLink} from 'react-router-dom'; import {useState, useEffect} from 'react'; import TimestampPage from './pages/TimestampPage'; // import ElectronicWoodenFishPage from "./pages/ElectronicWoodenFishPage"; +import RecordeReplayPage from './pages/RecordeReplayPage'; import './App.css'; // 导航项配置数组,便于后续添加 @@ -9,6 +10,7 @@ import './App.css'; const navItems = [ {path: '/', label: '时间戳', element: }, // {path: '/dzmy', label: '电子木鱼', element: }, + {path: '/recorde-replay', label: '录制与回放', element: }, ]; function App() { diff --git a/src/hooks/useRecorder.js b/src/hooks/useRecorder.js new file mode 100644 index 0000000..1558267 --- /dev/null +++ b/src/hooks/useRecorder.js @@ -0,0 +1,42 @@ +import {useRef, useState} from "react"; +import {record} from "rrweb"; + +export const useRecorder = () => { + // 存储录制事件 + const eventRef = useRef([]); + // 存储停止录制函数 + const stopFnRef = useRef(); + const [isRecording, setIsRecording] = useState(false); + + const startRecord = async () => { + if (isRecording) return; + + setIsRecording(true); + + // 启动录制 + stopFnRef.current = record({ + emit(event) { + // 存储数据 + eventRef.current.push(event); + } + }); + }; + + const stopRecord = () => { + if (!isRecording) return; + + setIsRecording(false); + + // 停止录制 + if (stopFnRef.current) { + stopFnRef.current(); + } + }; + + return { + startRecord, + stopRecord, + isRecording, + getEvents: () => eventRef.current + } +}; \ No newline at end of file diff --git a/src/pages/RecordeReplayPage.js b/src/pages/RecordeReplayPage.js new file mode 100644 index 0000000..80a08ea --- /dev/null +++ b/src/pages/RecordeReplayPage.js @@ -0,0 +1,62 @@ +import React from 'react'; +// import {sendRecordCommand} from '../services/bridge' +// import {storage} from "../services/storage"; +import {useRecorder} from "../hooks/useRecorder"; +import {downloadHtml} from "../utils/recordUtils"; + +const RecordeReplayPage = () => { + // const [isRecording, setIsRecording] = useState(false); + + const {startRecord, stopRecord, getEvents, isRecording} = useRecorder(); + + const handleStart = async () => { + try { + // await sendRecordCommand("START_RECORD"); + if (!isRecording) { + await startRecord(); + } else { + stopRecord(); + } + // await storage.set({isRecording: true, recordingStartTime: Date.now()}); + // console.log('events', events); + } catch (e) { + console.error("Error starting recording:", e); + } + } + + const handleStop = async () => { + try { + // await sendRecordCommand("STOP_RECORD"); + // const events =useRecorder().getEvents(); + // console.log('events', events); + // setIsRecording(false); + // await storage.remove('isRecording'); + stopRecord(); + downloadHtml(getEvents()); + // console.log('events', getEvents()); + } catch (e) { + console.error("Error stopping recording:", e) + } + }; + + return ( +
+

RRWeb Recorder

+ + {/* 3. 选择标签页逻辑:默认为当前页,如果需要跨页,需先列出 chrome.tabs.query */} + +
+ {!isRecording ? ( + + ) : ( + + )} +
+
+ ); +}; + +export default RecordeReplayPage; diff --git a/src/services/bridge.js b/src/services/bridge.js new file mode 100644 index 0000000..ab9c62b --- /dev/null +++ b/src/services/bridge.js @@ -0,0 +1,56 @@ +// src/services/bridge.ts +import {record} from 'rrweb'; +import {isExtension} from '../utils/env'; + +let stopFn = null; +let events = []; + +/** + * 发送指令给录制器(Content Script 或 当前页面逻辑) + * @param action 'START_RECORD' | 'STOP_RECORD' + * @returns {Promise} + */ +export const sendRecordCommand = async (action) => { + if (isExtension()) { + // === 插件环境 === + try { + const [tab] = await chrome.tabs.query({active: true, currentWindow: true}); + if (tab?.id) { + // 使用 Promise 包装 sendMessage 以便统一 async/await 风格 + return new Promise((resolve, reject) => { + chrome.tabs.sendMessage(tab.id, {action}, (response) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + } else { + resolve(response); + } + }); + }); + } + } catch (e) { + console.error("Extension communication error:", e); + } + } else { + // === 网页环境 (Web Demo / Localhost 调试) === + console.log(`[Web Mode] Simulation: Sending ${action}`); + + if (action === 'START_RECORD') { + stopFn = record({ + emit(event) { + if (events.length > 1000) { + console.log('events length > 1000'); + stopFn(); + } + events.push(event); + } + }); + } else if (action === 'STOP_RECORD') { + if (stopFn != null) { + stopFn(); + } + console.log('[Web Mode] Simulation: Stopping recorder'); + console.log('[Web Mode] Simulation: Events:', events); + console.log('[Web Mode] Simulation: Events:', events.length); + } + } +}; \ No newline at end of file diff --git a/src/services/storage.js b/src/services/storage.js new file mode 100644 index 0000000..2a179f9 --- /dev/null +++ b/src/services/storage.js @@ -0,0 +1,40 @@ +// src/services/storage.ts +import {isExtension} from '../utils/env'; + +export const storage = { + set: async (items) => { + if (isExtension()) { + return chrome.storage.local.set(items); + } else { + // Web 兼容:写入 localStorage + Object.keys(items).forEach(key => { + localStorage.setItem(key, JSON.stringify(items[key])); + }); + return Promise.resolve(); + } + }, + + get: async (keys) => { + if (isExtension()) { + return chrome.storage.local.get(keys); + } else { + // Web 兼容:读取 localStorage + const result = {}; + const keyList = Array.isArray(keys) ? keys : [keys]; + keyList.forEach(key => { + const val = localStorage.getItem(key); + if (val) result[key] = JSON.parse(val); + }); + return Promise.resolve(result); + } + }, + + remove: async (key) => { + if (isExtension()) { + return chrome.storage.local.remove(key); + } else { + localStorage.removeItem(key); + return Promise.resolve(); + } + } +}; \ No newline at end of file diff --git a/src/utils/env.js b/src/utils/env.js new file mode 100644 index 0000000..836b527 --- /dev/null +++ b/src/utils/env.js @@ -0,0 +1,7 @@ +/** + * 判断当前是否处于 Chrome 插件环境 + * @returns {false|chrome.storage.LocalStorageArea} + */ +export const isExtension = () => { + return typeof chrome !== 'undefined' && chrome.storage && chrome.storage.local; +} \ No newline at end of file diff --git a/src/utils/recordUtils.js b/src/utils/recordUtils.js new file mode 100644 index 0000000..4f9f5fb --- /dev/null +++ b/src/utils/recordUtils.js @@ -0,0 +1,53 @@ +// import rrwebPlayer from 'rrweb-player'; + +export const downloadHtml = (events) => { + if (events.length === 0) return; + + // 1. 构建 HTML 模板字符串 + // 我们将 events 数据直接注入到 + + + + + `; + + // 2. 创建 Blob 并下载 + const blob = new Blob([htmlContent], {type: 'text/html'}); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `replay-${Date.now()}.html`; // 保存为 .html 文件 + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}; \ No newline at end of file