从零到一打造前端内存监控 SDK,并发布到 npm ——基于 TypeScript + Vite + ECharts的解决方案

前言:

在开发 内存监控工具 或类似中间件时,我们通常会面临两个需求:

1. SDK 供开发者集成使用

  • 功能封装:SDK 封装了完整的内存监控功能,包括数据采集、分析、报告生成等
  • 通用性:支持多种环境(浏览器、Node.js、现代前端工具链)
  • 易集成 :开发者只需 import MemoryMonitor from 'memory-monitor-sdk' 即可开始使用
  • 类型安全:提供完整的 TypeScript 类型定义,支持智能提示和类型检查

2. 独立 HTML 可视化页面

  • 数据展示:提供直观的图表和统计信息,让开发者快速了解内存使用情况
  • 交互功能:支持文件上传、数据导出、实时图表展示等
  • 独立运行:不依赖特定项目环境,可直接在浏览器中打开使用
  • 演示效果:方便向团队展示监控工具的效果和价值

为什么同时做 SDK 和 HTML 可视化页面?

  • 解耦设计:SDK 专注数据采集和业务逻辑,HTML 页面专注数据展示和用户体验
  • 复用性高:SDK 可以在多个项目中复用,HTML 页面可以直接演示效果
  • 快速迭代:独立 HTML 页面方便测试和演示,加速开发流程
  • 用户体验友好:开发者可以直接通过浏览器查看监控结果,降低上手门槛
  • 完整解决方案:从数据采集到结果展示,提供端到端的解决方案

核心思想 :SDK 是"数据采集引擎",HTML 页面是"结果可视化前端"。两者结合,可以同时满足 集成开发结果展示 的需求。


一、项目架构设计

1. 项目结构

复制代码
memory-monitor-sdk/
├── src/
│   ├── index.ts              # SDK 入口文件
│   ├── memory-monitor.ts     # 核心监控类
│   └── index.html            # 可视化页面
├── dist/                     # 构建输出目录
│   ├── memory-monitor.es.js  # ES 模块格式
│   ├── memory-monitor.umd.js # UMD 格式
│   ├── memory-monitor.cjs.js # CommonJS 格式
│   ├── memory-monitor.d.ts   # 类型定义文件
│   └── index.html            # 压缩后的可视化页面
├── package.json              # 项目配置
├── tsconfig.json             # TypeScript 配置
└── vite.config.ts            # Vite 构建配置
└── README.md     # 使用说明文档

2. 核心功能模块

(1) SDK 核心功能
- 内存使用情况实时监控
- 峰值变化检测和告警
- 页面路由跟踪
- 数据持久化和导出
- 详细的报告生成
- 可拖拽的 DOM 显示面板
typescript 复制代码
/**
 * 内存监控工具
 * 用于检测内存泄漏和性能问题(主要在 PC 上调试,模拟手机环境)
 */
declare const getCurrentPages: any; // 临时声明,TS 不报错

interface MemoryInfo {
    usedJSHeapSize: number
    totalJSHeapSize: number
    jsHeapSizeLimit: number
}

class MemoryMonitor {
    private static instance: MemoryMonitor
    private memoryLogs: Map<number, MemoryInfo> = new Map()
    private intervalId: any = null
    private lastMemoryMB: number = 0
    private peakThresholdMB: number = 20 // 峰值变化阈值(MB)
    private appStartTime: number = Date.now() // 应用启动时间
    private currentPage: string = 'unknown' // 当前页面
    private pageHistory: Map<number, string> = new Map() // 页面历史记录
    private memoryPageLogs: Map<number, string> = new Map() // 内存记录时的页面信息

    // 模拟手机内存上限(默认 300MB)
    private SIMULATED_LIMIT_MB = 300

    // DOM 显示相关
    private displayElement: HTMLElement | null = null
    private isDisplayVisible: boolean = false
    private shouldShowDisplay: boolean = false

    static getInstance(): MemoryMonitor {
        if (!MemoryMonitor.instance) {
            MemoryMonitor.instance = new MemoryMonitor()
        }
        return MemoryMonitor.instance
    }

    /**
     * 开始监控内存使用情况
     * @param intervalMs 监控间隔(毫秒,默认2秒)
     * @param simulatedLimitMB 模拟手机内存上限(默认 300MB)
     * @param peakThresholdMB 峰值变化阈值(MB,默认20MB)
     * @param showDisplay 是否显示DOM面板(开发环境默认true)
     */
    startMonitoring(intervalMs: number = 2000, simulatedLimitMB: number = 300, peakThresholdMB: number = 20, showDisplay: boolean = true) {
        if (this.intervalId) {
            clearInterval(this.intervalId)
        }
        this.SIMULATED_LIMIT_MB = simulatedLimitMB
        this.peakThresholdMB = peakThresholdMB
        this.appStartTime = Date.now() // 重置启动时间

        // 存储显示设置,在第一次采集时创建DOM
        this.shouldShowDisplay = showDisplay && process.env.NODE_ENV === 'development'

        this.intervalId = setInterval(() => {
            this.logMemoryUsage()
        }, intervalMs)

        console.log(`🔍 内存监控已启动(采样间隔: ${intervalMs}ms,模拟上限: ${this.SIMULATED_LIMIT_MB}MB,峰值阈值: ${peakThresholdMB}MB)`)
        if (this.shouldShowDisplay) {
            console.log('📱 DOM面板将在首次采集到内存数据时创建')
        }
    }

    /**
     * 设置当前页面
     * @param pageName 页面名称
     */
    setCurrentPage(pageName: string) {
        this.currentPage = pageName
        // 记录页面变化历史
        this.pageHistory.set(Date.now(), pageName)

        // 只保留最近100条页面记录
        if (this.pageHistory.size > 100) {
            const oldestKey = Math.min(...this.pageHistory.keys())
            this.pageHistory.delete(oldestKey)
        }
    }

    /**
     * 获取应用运行时长(分钟)
     */
    private getAppRuntimeMinutes(): number {
        return Math.round((Date.now() - this.appStartTime) / 1000 / 60)
    }

    /**
     * 获取应用运行时长(秒)
     */
    private getAppRuntimeSeconds(): number {
        return Math.round((Date.now() - this.appStartTime) / 1000)
    }

    /**
     * 格式化运行时长显示
     */
    private formatRuntime(): string {
        const totalSeconds = this.getAppRuntimeSeconds()
        const minutes = Math.floor(totalSeconds / 60)
        const seconds = totalSeconds % 60
        return `${minutes}分${seconds}秒`
    }

    /**
     * 获取当前页面信息
     */
    private getCurrentPageInfo(): string {
        // 尝试从不同来源获取页面信息
        if (typeof window !== 'undefined') {
            // 浏览器环境
            if (window.location) {
                const pagePath = window.location.pathname || window.location.href
                if (pagePath !== this.currentPage) {
                    this.currentPage = pagePath
                    // 记录页面变化
                    this.pageHistory.set(Date.now(), pagePath)
                }
                return pagePath
            }
            // uni-app 环境
            if (typeof getCurrentPages === 'function') {
                const pages = getCurrentPages()
                if (pages.length > 0) {
                    const currentPage = pages[pages.length - 1]
                    const pageRoute = (currentPage as any).route || (currentPage as any).$page?.route || 'unknown'
                    if (pageRoute !== this.currentPage) {
                        this.currentPage = pageRoute
                        // 记录页面变化
                        this.pageHistory.set(Date.now(), pageRoute)
                    }
                    return pageRoute
                }
            }
        }
        return this.currentPage
    }

    /**
     * 获取指定时间点的页面信息
     * @param timestamp 时间戳
     */
    private getPageAtTime(timestamp: number): string {
        // 优先从内存记录时的页面信息中获取
        if (this.memoryPageLogs.has(timestamp)) {
            return this.memoryPageLogs.get(timestamp) || 'unknown'
        }

        // 如果没有精确匹配,查找最接近的页面记录
        const pageKeys = Array.from(this.pageHistory.keys()).sort()
        let closestPage = this.currentPage

        for (let i = 0; i < pageKeys.length; i++) {
            if (pageKeys[i] <= timestamp) {
                closestPage = this.pageHistory.get(pageKeys[i]) || this.currentPage
            } else {
                break
            }
        }

        return closestPage
    }

    /**
     * 停止监控
     */
    stopMonitoring() {
        if (this.intervalId) {
            clearInterval(this.intervalId)
            this.intervalId = null
        }
        this.removeDisplayElement()
        console.log('⏹️ 内存监控已停止')
    }

    /**
     * 创建DOM显示元素
     */
    private createDisplayElement() {
        if (typeof window === 'undefined' || this.displayElement) {
            return
        }

        // 创建主容器
        const container = document.createElement('div')
        container.id = 'memory-monitor-display'
        container.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            background: rgba(0, 0, 0, 0.8);
            color: #fff;
            font-family: 'Courier New', monospace;
            font-size: 14px;
            border-radius: 6px;
            padding: 8px 12px;
           
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
            backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            user-select: none;
            cursor: move;
            transition: all 0.3s ease;
        `

        // 创建内容区域
        const content = document.createElement('div')
        content.id = 'memory-monitor-content'
        content.style.cssText = `
            text-align: center;
            font-weight: bold;
        `

        // 添加到页面
        container.appendChild(content)
        document.body.appendChild(container)
        this.displayElement = container
        this.isDisplayVisible = true

        // 添加拖拽功能
        this.addDragFunctionality(container, container)

     
    }

    /**
     * 移除DOM显示元素
     */
    private removeDisplayElement() {
        if (this.displayElement && this.displayElement.parentNode) {
            // 清理拖拽事件监听器
            const dragListeners = (this.displayElement as any)._dragListeners
            if (dragListeners) {
                // 清理鼠标事件
                dragListeners.handle.removeEventListener('mousedown', dragListeners.onMouseDown)
                document.removeEventListener('mousemove', dragListeners.onMouseMove)
                document.removeEventListener('mouseup', dragListeners.onMouseUp)
                
                // 清理触摸事件
                dragListeners.handle.removeEventListener('touchstart', dragListeners.onTouchStart)
                document.removeEventListener('touchmove', dragListeners.onTouchMove)
                document.removeEventListener('touchend', dragListeners.onTouchEnd)
            }
            
            this.displayElement.parentNode.removeChild(this.displayElement)
            this.displayElement = null
            this.isDisplayVisible = false
        }
    }


    /**
     * 添加拖拽功能(支持移动端触摸)
     */
    private addDragFunctionality(element: HTMLElement, handle: HTMLElement) {
        let isDragging = false
        let startX = 0
        let startY = 0
        let startLeft = 0
        let startTop = 0

        // 设置拖拽句柄样式
        handle.style.cursor = 'move'
        handle.style.userSelect = 'none'
        handle.style.touchAction = 'none' // 禁用默认触摸行为

        // 开始拖拽
        const startDrag = (clientX: number, clientY: number) => {
            isDragging = true
            startX = clientX
            startY = clientY
            const rect = element.getBoundingClientRect()
            startLeft = rect.left
            startTop = rect.top
            
            // 添加拖拽时的样式
            element.style.transition = 'none'
            
            // 添加拖拽状态类名,用于CSS选择器
            element.classList.add('memory-monitor-dragging')
        }

        // 拖拽移动
        const dragMove = (clientX: number, clientY: number) => {
            if (!isDragging) return

            const deltaX = clientX - startX
            const deltaY = clientY - startY

            element.style.left = (startLeft + deltaX) + 'px'
            element.style.top = (startTop + deltaY) + 'px'
            element.style.right = 'auto'
        }

        // 结束拖拽
        const endDrag = () => {
            if (isDragging) {
                isDragging = false
                // 恢复过渡效果
                element.style.transition = 'all 0.3s ease'
                // 移除拖拽状态类名
                element.classList.remove('memory-monitor-dragging')
            }
        }

        // 鼠标事件(桌面端)
        const onMouseDown = (e: MouseEvent) => {
            // 只有左键点击才开始拖拽
            if (e.button === 0) {
                startDrag(e.clientX, e.clientY)
                // 只阻止默认行为,不阻止事件冒泡
                e.preventDefault()
            }
        }

        const onMouseMove = (e: MouseEvent) => {
            if (isDragging) {
                dragMove(e.clientX, e.clientY)
                e.preventDefault()
            }
        }

        const onMouseUp = (e: MouseEvent) => {
            if (isDragging) {
                endDrag()
                e.preventDefault()
            }
        }

        // 触摸事件(移动端)
        const onTouchStart = (e: TouchEvent) => {
            if (e.touches.length === 1) {
                const touch = e.touches[0]
                startDrag(touch.clientX, touch.clientY)
                // 只阻止默认行为,不阻止事件冒泡
                e.preventDefault()
            }
        }

        const onTouchMove = (e: TouchEvent) => {
            if (isDragging && e.touches.length === 1) {
                const touch = e.touches[0]
                dragMove(touch.clientX, touch.clientY)
                e.preventDefault()
            }
        }

        const onTouchEnd = (e: TouchEvent) => {
            if (isDragging) {
                endDrag()
                e.preventDefault()
            }
        }

        // 绑定事件
        handle.addEventListener('mousedown', onMouseDown)
        handle.addEventListener('touchstart', onTouchStart)
        document.addEventListener('mousemove', onMouseMove)
        document.addEventListener('touchmove', onTouchMove, { passive: false })
        document.addEventListener('mouseup', onMouseUp)
        document.addEventListener('touchend', onTouchEnd)

        // 存储事件监听器以便后续清理
        ;(element as any)._dragListeners = {
            onMouseDown,
            onMouseMove,
            onMouseUp,
            onTouchStart,
            onTouchMove,
            onTouchEnd,
            handle
        }
    }

    /**
     * 更新DOM显示内容
     */
    private updateDisplayContent(usedMB: number, totalMB: number, usagePercent: number, warningLevel: string, message: string) {
        if (!this.displayElement || !this.isDisplayVisible) {
            return
        }

        const content = this.displayElement.querySelector('#memory-monitor-content')
        if (!content) return

        // 根据警告级别设置颜色
        let statusColor = '#4CAF50' // 正常 - 绿色
        
        switch (warningLevel) {
            case 'danger':
                statusColor = '#F44336' // 危险 - 红色
                break
            case 'warning':
                statusColor = '#FF9800' // 警告 - 橙色
                break
            case 'caution':
                statusColor = '#FFC107' // 注意 - 黄色
                break
            case 'peak':
                statusColor = '#9C27B0' // 峰值 - 紫色
                break
        }

        // content.innerHTML = `
        //     <div style="color: ${statusColor};">
        //         ${usedMB}MB / ${this.SIMULATED_LIMIT_MB}MB
        //     </div>
        //     <div style="color: ${statusColor}; font-size: 12px; margin-top: 2px;">
        //         ${usagePercent}%
        //     </div>
        // `
        content.innerHTML = `
        <div style="color: ${statusColor};">
            ${usedMB}MB  ${usagePercent}%  
        </div>
      
    `
    }

    /**
     * 记录当前内存使用情况
     */
    private async logMemoryUsage() {
        const timestamp = Date.now()

        let usedMB = 0
        let totalMB = 0
        let limitMB = this.SIMULATED_LIMIT_MB
        let usagePercent = 0

        try {
            // 桌面 Chrome 才能获取 performance.memory
            if ("memory" in performance) {
                const memory = (performance as any).memory as MemoryInfo
                usedMB = Math.round(memory.usedJSHeapSize / 1024 / 1024)
                totalMB = Math.round(memory.totalJSHeapSize / 1024 / 1024)

                usagePercent = Math.round((usedMB / limitMB) * 100)

                // 使用Map存储,自动去重时间戳
                this.memoryLogs.set(timestamp, memory)

                // 同时记录当前页面信息
                const currentPage = this.getCurrentPageInfo()
                this.memoryPageLogs.set(timestamp, currentPage)
            } else {
                console.warn("⚠️ 当前环境不支持 JS 内存检测,仅支持 PC Chrome")
                return
            }
        } catch (err) {
            console.error("内存检测失败:", err)
            return
        }

        // 只保留最近100条记录,使用Map的size属性
        if (this.memoryLogs.size > 100) {
            const oldestKey = Math.min(...this.memoryLogs.keys())
            this.memoryLogs.delete(oldestKey)
            this.memoryPageLogs.delete(oldestKey)
        }

        // 检测峰值变化(跳过第一次检测,因为这是基准值)
        const memoryChange = this.lastMemoryMB === 0 ? 0 : Math.abs(usedMB - this.lastMemoryMB)
        const isPeakChange = this.lastMemoryMB !== 0 && memoryChange >= this.peakThresholdMB

        // 状态判断
        let warningLevel = "normal"
        let emoji = "🟢"
        let message = "正常!内存使用率健康,无需担心!"
        let shouldLog = false

        if (usagePercent >= 80) {
            warningLevel = "danger"
            emoji = "🚨"
            message = "危险!内存使用率过高,立即处理!"
            shouldLog = true
        } else if (usagePercent >= 60) {
            warningLevel = "warning"
            emoji = "⚠️"
            message = "警告!内存使用率较高,需要优化!"
            shouldLog = true
        } else if (usagePercent >= 40) {
            warningLevel = "caution"
            emoji = "🟡"
            message = "注意!内存使用率中等,开始监控!"
            shouldLog = false
        }

        // 峰值变化检测
        if (isPeakChange) {
            shouldLog = true
            if (warningLevel === "normal") {
                warningLevel = "peak"
                emoji = "📈"
                message = `峰值变化!内存使用变化 ${memoryChange}MB,可能存在大量对象创建`
            }
        }

        // 更新DOM显示(开发环境)
        if (this.isDisplayVisible) {
            this.updateDisplayContent(usedMB, totalMB, usagePercent, warningLevel, message)
        } else if (this.shouldShowDisplay && !this.displayElement) {
            // 第一次采集到数据时创建DOM元素
            this.createDisplayElement()
            this.updateDisplayContent(usedMB, totalMB, usagePercent, warningLevel, message)
        }

        // 只在需要时打印日志
        if (shouldLog) {
            const logMethod =
                warningLevel === "danger" ? "error" :
                    warningLevel === "warning" ? "warn" :
                        warningLevel === "peak" ? "warn" :
                            "log"

            const runtimeFormatted = this.formatRuntime()
            const currentPage = this.getCurrentPageInfo()

            const logData: any = {
                used: `${usedMB}MB`,
                total: `${totalMB}MB`,
                limit: `${limitMB}MB (模拟手机上限)`,
                usage: `${usagePercent}%`,
                status: message,
                level: warningLevel,
                runtime: runtimeFormatted,
                page: currentPage
            }

            if (isPeakChange) {
                logData.change = `+${memoryChange}MB`
                logData.timestamp = new Date(timestamp).toLocaleTimeString()
            }

            console[logMethod](`${emoji} 内存使用情况:`, logData)

            // 内存过高时自动生成详细报告
            if (warningLevel === "danger" || warningLevel === "warning") {
                const reportTitle = warningLevel === "danger" ? '🚨 内存危险 - 自动生成详细报告' : '⚠️ 内存警告 - 自动生成详细报告'
                console.group(reportTitle)
                this.generateDetailedReport(warningLevel)
                console.groupEnd()
            }
        }

        // 更新上次内存值
        this.lastMemoryMB = usedMB
    }

    /**
     * 获取内存使用趋势
     */
    getMemoryTrend() {
        if (this.memoryLogs.size < 2) {
            return { trend: 'insufficient_data', message: '数据不足,无法分析趋势' }
        }

        const sortedKeys = Array.from(this.memoryLogs.keys()).sort()
        const first = this.memoryLogs.get(sortedKeys[0])!
        const last = this.memoryLogs.get(sortedKeys[sortedKeys.length - 1])!

        if (first.usedJSHeapSize === 0) {
            return { trend: 'insufficient_data', message: '初始内存为0,无法分析趋势' }
        }

        const growth = last.usedJSHeapSize - first.usedJSHeapSize
        const growthPercent = (growth / first.usedJSHeapSize) * 100

        if (growthPercent > 10) {
            return {
                trend: 'increasing',
                message: `内存使用增长 ${growthPercent.toFixed(2)}%,可能存在内存泄漏`,
                growth: `${Math.round(growth / 1024 / 1024)}MB`
            }
        } else if (growthPercent < -5) {
            return {
                trend: 'decreasing',
                message: `内存使用减少 ${Math.abs(growthPercent).toFixed(2)}%`,
                growth: `${Math.round(growth / 1024 / 1024)}MB`
            }
        } else {
            return {
                trend: 'stable',
                message: `内存使用稳定,变化 ${growthPercent.toFixed(2)}%`,
                growth: `${Math.round(growth / 1024 / 1024)}MB`
            }
        }
    }

    /**
     * 生成内存报告
     */
    generateReport() {
        const trend = this.getMemoryTrend()
        const sortedKeys = Array.from(this.memoryLogs.keys()).sort()
        const currentMemory = sortedKeys.length > 0 ? this.memoryLogs.get(sortedKeys[sortedKeys.length - 1]) : null

        console.group('📋 内存监控报告')
        console.log('当前内存使用:', currentMemory ? {
            used: `${Math.round(currentMemory.usedJSHeapSize / 1024 / 1024)}MB`,
            total: `${Math.round(currentMemory.totalJSHeapSize / 1024 / 1024)}MB`,
            limit: `${this.SIMULATED_LIMIT_MB}MB (模拟手机上限)`
        } : '无数据')

        console.log('内存趋势:', trend.message)
        console.groupEnd()

        return {
            currentMemory,
            trend
        }
    }

    /**
     * 生成详细报告(包含运行时长、页面信息、峰值变化等)
     * @param level 报告级别:'warning' | 'danger'
     */
    generateDetailedReport(level: 'warning' | 'danger' = 'warning') {
        const trend = this.getMemoryTrend()
        const sortedKeys = Array.from(this.memoryLogs.keys()).sort()
        const currentMemory = sortedKeys.length > 0 ? this.memoryLogs.get(sortedKeys[sortedKeys.length - 1]) : null
        const peakChanges = this.getPeakChanges()
        const runtimeFormatted = this.formatRuntime()
        const currentPage = this.getCurrentPageInfo()

        // 根据级别选择不同的日志方法和图标
        const logMethod = level === 'danger' ? 'error' : 'warn'
        const statusIcon = level === 'danger' ? '🚨' : '⚠️'

        const currentUsedMB = currentMemory ? Math.round(currentMemory.usedJSHeapSize / 1024 / 1024) : 0
        const currentTotalMB = currentMemory ? Math.round(currentMemory.totalJSHeapSize / 1024 / 1024) : 0
        const currentUsagePercent = Math.round((currentUsedMB / this.SIMULATED_LIMIT_MB) * 100)

        console[logMethod](`${statusIcon} 应用运行状态:`, {
            // 应用运行信息
            runtime: runtimeFormatted,
            currentPage: currentPage,
            startTime: new Date(this.appStartTime).toLocaleString(),
            // 当前内存状态
            memory: {
                used: `${currentUsedMB}MB`,
                total: `${currentTotalMB}MB`,
                limit: `${this.SIMULATED_LIMIT_MB}MB (模拟手机上限)`,
                usage: `${currentUsagePercent}%`,
                status: level === 'danger' ? '危险' : '警告'
            }
        })

        console[logMethod](`${statusIcon} 内存趋势分析:`, trend.message)

        if (peakChanges.length > 0) {
            console[logMethod](`${statusIcon} 峰值变化记录:`, peakChanges.map(change => ({
                time: change.timeFromStart,
                runtime: change.runtimeFromStart,
                page: change.page,
                change: `${change.change > 0 ? '+' : ''}${change.change}MB`,
                memory: `${change.memory}MB`
            })))
        } else {
            console.log('✅ 无显著峰值变化')
        }

        // 所有内存采样记录
        const recentLogs = sortedKeys.map(key => {
            const memory = this.memoryLogs.get(key)!
            const page = this.memoryPageLogs.get(key) || 'unknown'

            // 计算从应用启动到该记录的时间
            const runtimeFromStartMs = key - this.appStartTime
            const runtimeMinutes = Math.floor(runtimeFromStartMs / 1000 / 60)
            const runtimeSeconds = Math.floor((runtimeFromStartMs % (1000 * 60)) / 1000)
            const runtimeFromStart = `${runtimeMinutes}分${runtimeSeconds}秒`

            return {
                time: new Date(key).toLocaleTimeString(),
                runtime: runtimeFromStart,
                page: page,
                used: `${Math.round(memory.usedJSHeapSize / 1024 / 1024)}MB`
            }
        })
        console[logMethod](`${statusIcon} 内存采样记录:`, recentLogs)

        // 添加保存数据提示
        console.log('💾 保存数据命令: memoryMonitor.saveDataToFile()')
        console.log('📊 可视化命令: 打开 memory-data/index.html 查看图表')

        return {
            runtime: runtimeFormatted,
            currentPage,
            currentMemory,
            trend,
            peakChanges,
            recentLogs,
            level
        }
    }

    /**
     * 保存数据到JSON文件
     */
    async saveDataToFile() {
        try {
            const sortedKeys = Array.from(this.memoryLogs.keys()).sort()
            const data = sortedKeys.map(key => {
                const memory = this.memoryLogs.get(key)!
                const page = this.memoryPageLogs.get(key) || 'unknown'
                const usedMB = Math.round(memory.usedJSHeapSize / 1024 / 1024)
                const totalMB = Math.round(memory.totalJSHeapSize / 1024 / 1024)
                
                // 计算从应用启动到该记录的时间
                const runtimeFromStartMs = key - this.appStartTime
                const runtimeMinutes = Math.floor(runtimeFromStartMs / 1000 / 60)
                const runtimeSeconds = Math.floor((runtimeFromStartMs % (1000 * 60)) / 1000)
                const runtimeFromStart = `${runtimeMinutes}:${runtimeSeconds.toString().padStart(2, '0')}`
                
                return {
                    timestamp: key,
                    time: new Date(key).toLocaleTimeString(),
                    runtime: runtimeFromStart,
                    page: page,
                    used: usedMB,
                    total: totalMB,
                    usage: Math.round((usedMB / this.SIMULATED_LIMIT_MB) * 100)
                }
            })

            const sessionData = {
                sessionId: this.generateSessionId(),
                startTime: this.appStartTime,
                endTime: Date.now(),
                duration: this.formatRuntime(),
                totalSamples: data.length,
                peakChanges: this.getPeakChanges(),
                trend: this.getMemoryTrend(),
                data: data,
                config: {
                    simulatedLimitMB: this.SIMULATED_LIMIT_MB,
                    peakThresholdMB: this.peakThresholdMB
                }
            }

            // 在浏览器环境中,使用下载方式保存文件
            if (typeof window !== 'undefined') {
                const blob = new Blob([JSON.stringify(sessionData, null, 2)], { type: 'application/json' })
                const url = URL.createObjectURL(blob)
                const a = document.createElement('a')
                a.href = url
                a.download = `memory-data-${sessionData.sessionId}.json`
                document.body.appendChild(a)
                a.click()
                document.body.removeChild(a)
                URL.revokeObjectURL(url)
                
                console.log(`💾 内存数据已保存: memory-data-${sessionData.sessionId}.json`)
                console.log(`📊 包含 ${data.length} 个采样点,运行时长 ${sessionData.duration}`)
            }

            return sessionData
        } catch (error) {
            console.error('保存数据失败:', error)
            return null
        }
    }

    /**
     * 生成会话ID
     */
    private generateSessionId(): string {
        const now = new Date()
        const dateStr = now.toISOString().slice(0, 19).replace(/[-:T]/g, '')
        const randomStr = Math.random().toString(36).substr(2, 6)
        return `${dateStr}_${randomStr}`
    }

    /**
     * 判断是否需要保存数据
     * 只要有数据记录就保存
     */
    shouldSaveData(): boolean {
        // 只要有数据记录就保存
        return this.memoryLogs.size > 0
    }

    /**
     * 清理监控数据
     */
    clearLogs() {
        this.memoryLogs.clear()
        this.memoryPageLogs.clear()
        this.pageHistory.clear()
        this.lastMemoryMB = 0
        this.appStartTime = Date.now() // 重置启动时间
        console.log('🧹 监控数据已清理')
    }

    /**
     * 手动显示DOM面板
     */
    showDisplay() {
        if (process.env.NODE_ENV === 'development') {
            this.createDisplayElement()
        }
    }

    /**
     * 手动隐藏DOM面板
     */
    hideDisplay() {
        this.removeDisplayElement()
    }

    /**
     * 切换DOM面板显示状态
     */
    toggleDisplayPanel() {
        if (this.isDisplayVisible) {
            this.hideDisplay()
        } else {
            this.showDisplay()
        }
    }

    /**
     * 获取峰值变化记录
     */
    getPeakChanges() {
        const changes: Array<{
            timestamp: number;
            change: number;
            memory: number;
            runtimeFromStart: string;
            page: string;
            timeFromStart: string;
        }> = []
        const sortedKeys = Array.from(this.memoryLogs.keys()).sort()

        for (let i = 1; i < sortedKeys.length; i++) {
            const prevKey = sortedKeys[i - 1]
            const currKey = sortedKeys[i]
            const prevMemory = this.memoryLogs.get(prevKey)!
            const currMemory = this.memoryLogs.get(currKey)!

            const change = Math.round((currMemory.usedJSHeapSize - prevMemory.usedJSHeapSize) / 1024 / 1024)

            // 只记录增加20MB以上的变化,不记录减少的变化
            if (change >= 20) {
                // 计算从应用启动到峰值变化的时间
                const runtimeFromStartMs = currKey - this.appStartTime
                const runtimeMinutes = Math.floor(runtimeFromStartMs / 1000 / 60)
                const runtimeSeconds = Math.floor((runtimeFromStartMs % (1000 * 60)) / 1000)
                const runtimeFromStart = `${runtimeMinutes}分${runtimeSeconds}秒`

                // 获取当时所在页面
                const page = this.getPageAtTime(currKey)

                // 格式化时间显示
                const timeFromStart = new Date(currKey).toLocaleTimeString()

                changes.push({
                    timestamp: currKey,
                    change,
                    memory: Math.round(currMemory.usedJSHeapSize / 1024 / 1024),
                    runtimeFromStart,
                    page,
                    timeFromStart
                })
            }
        }

        return changes
    }
}

// 导出单例实例
export const memoryMonitor = MemoryMonitor.getInstance()

// 在开发环境下自动启动监控
if (process.env.NODE_ENV === 'development') {
    setTimeout(() => {
        // 开发环境:3秒监控一次,适合快速发现问题,并显示DOM面板
        memoryMonitor.clearLogs()
        memoryMonitor.startMonitoring(3000, 300, 20, true) // 显示DOM面板
        
        // 显示使用提示
        console.log('🔍 内存监控已启动!')
        console.log('📱 DOM面板已创建(右上角浮动面板)')
        console.log('💾 手动保存: memoryMonitor.saveDataToFile()')
        console.log('📊 查看图表: 打开 memory-data/index.html')
        console.log('📋 生成报告: memoryMonitor.generateDetailedReport()')
        console.log('ℹ️ 自动保存条件: 内存使用率≥40% | 峰值变化 | 增长趋势 | 运行≥5分钟 | 内存≥150MB')
    }, 1000)

    // 页面刷新前自动保存数据(仅在检测到问题时)
    // if (typeof window !== 'undefined') {
    //     window.addEventListener('beforeunload', () => {
    //         // 检查是否有问题需要保存数据
    //         if (memoryMonitor.shouldSaveData()) {
    //             memoryMonitor.saveDataToFile()
    //         }
    //     })
    // }
}
(window as any).MemoryMonitor = MemoryMonitor;
export default memoryMonitor
(2) HTML 可视化功能
- 基于 ECharts 的图表展示
- 多文件数据加载
- 统计信息展示
- 数据导出功能
- 响应式设计
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>内存监控数据可视化</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
            overflow: hidden;
        }

        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }

        .header h1 {
            font-size: 2.5em;
            margin-bottom: 10px;
            font-weight: 300;
        }

        .header p {
            font-size: 1.1em;
            opacity: 0.9;
        }

        .controls {
            padding: 20px 30px;
            background: #f8f9fa;
            border-bottom: 1px solid #e9ecef;
            display: flex;
            gap: 15px;
            align-items: center;
            flex-wrap: wrap;
        }

        .file-input-wrapper {
            position: relative;
            display: inline-block;
        }

        .file-input {
            display: none;
        }

        .file-input-label {
            background: #007bff;
            color: white;
            padding: 10px 20px;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.3s ease;
            display: inline-block;
            font-weight: 500;
        }

        .file-input-label:hover {
            background: #0056b3;
            transform: translateY(-2px);
        }

        .btn {
            background: #28a745;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.3s ease;
        }

        .btn:hover {
            background: #218838;
            transform: translateY(-2px);
        }

        .btn-secondary {
            background: #6c757d;
        }

        .btn-secondary:hover {
            background: #5a6268;
        }

        .info-panel {
            background: #e3f2fd;
            padding: 15px;
            border-radius: 8px;
            margin-left: auto;
            min-width: 200px;
        }

        .info-item {
            display: flex;
            justify-content: space-between;
            margin-bottom: 5px;
        }

        .info-label {
            font-weight: 600;
            color: #1976d2;
        }

        .info-value {
            color: #333;
        }

        .chart-container {
            padding: 30px;
        }

        .chart-title {
            font-size: 1.5em;
            margin-bottom: 20px;
            color: #333;
            text-align: center;
        }

        .chart-wrapper {
            width: 100%;
            height: 500px;
            border: 1px solid #e9ecef;
            border-radius: 8px;
            padding: 20px;
            background: #fafafa;
        }

        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }

        .stat-card {
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            border-left: 4px solid #007bff;
        }

        .stat-title {
            font-size: 0.9em;
            color: #666;
            margin-bottom: 10px;
            text-transform: uppercase;
            letter-spacing: 1px;
        }

        .stat-value {
            font-size: 2em;
            font-weight: bold;
            color: #333;
            margin-bottom: 5px;
        }

        .stat-subtitle {
            font-size: 0.8em;
            color: #999;
        }

        .no-data {
            text-align: center;
            padding: 60px 20px;
            color: #666;
        }

        .no-data-icon {
            font-size: 4em;
            margin-bottom: 20px;
        }

        .loading {
            text-align: center;
            padding: 40px;
            color: #666;
        }

        .spinner {
            border: 4px solid #f3f3f3;
            border-top: 4px solid #007bff;
            border-radius: 50%;
            width: 40px;
            height: 40px;
            animation: spin 1s linear infinite;
            margin: 0 auto 20px;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .error {
            background: #f8d7da;
            color: #721c24;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
            border: 1px solid #f5c6cb;
        }

        .success {
            background: #d4edda;
            color: #155724;
            padding: 15px;
            border-radius: 8px;
            margin: 20px 0;
            border: 1px solid #c3e6cb;
        }

        @media (max-width: 768px) {
            .controls {
                flex-direction: column;
                align-items: stretch;
            }

            .info-panel {
                margin-left: 0;
                margin-top: 15px;
            }

            .chart-wrapper {
                height: 400px;
            }

            .stats-grid {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>🔍 内存监控数据可视化</h1>
            <p>实时内存使用情况分析和趋势展示</p>
        </div>

        <div class="controls">
            <div class="file-input-wrapper">
                <input type="file" id="fileInput" class="file-input" accept=".json" multiple>
                <label for="fileInput" class="file-input-label">
                    📁 选择JSON数据文件
                </label>
            </div>
            <button class="btn" onclick="loadSampleData()">📊 加载示例数据</button>
            <button class="btn btn-secondary" onclick="clearData()">🗑️ 清空数据</button>
            <button class="btn btn-secondary" onclick="exportData()">💾 导出数据</button>
            
            <div class="info-panel" id="infoPanel" style="display: none;">
                <div class="info-item">
                    <span class="info-label">会话ID:</span>
                    <span class="info-value" id="sessionId">-</span>
                </div>
                <div class="info-item">
                    <span class="info-label">运行时长:</span>
                    <span class="info-value" id="duration">-</span>
                </div>
                <div class="info-item">
                    <span class="info-label">采样点数:</span>
                    <span class="info-value" id="sampleCount">-</span>
                </div>
                <div class="info-item">
                    <span class="info-label">峰值变化:</span>
                    <span class="info-value" id="peakChanges">-</span>
                </div>
            </div>
        </div>

        <div class="chart-container">
            <div id="messageArea"></div>
            
            <div class="stats-grid" id="statsGrid" style="display: none;">
                <div class="stat-card">
                    <div class="stat-title">当前内存使用</div>
                    <div class="stat-value" id="currentMemory">-</div>
                    <div class="stat-subtitle">MB</div>
                </div>
                <div class="stat-card">
                    <div class="stat-title">最大内存使用</div>
                    <div class="stat-value" id="maxMemory">-</div>
                    <div class="stat-subtitle">MB</div>
                </div>
                <div class="stat-title">平均内存使用</div>
                    <div class="stat-value" id="avgMemory">-</div>
                    <div class="stat-subtitle">MB</div>
                </div>
                <div class="stat-card">
                    <div class="stat-title">内存使用率</div>
                    <div class="stat-value" id="usagePercent">-</div>
                    <div class="stat-subtitle">%</div>
                </div>
            </div>

            <div class="chart-title">内存使用趋势图</div>
            <div class="chart-wrapper">
                <div id="memoryChart" style="width: 100%; height: 100%;"></div>
            </div>
        </div>
    </div>
<script >

let currentData = null;
let chart = null;

// 初始化图表
function initChart() {
    const chartDom = document.getElementById('memoryChart');
    chart = echarts.init(chartDom);
    
    const option = {
        title: {
            text: '内存使用趋势',
            left: 'center',
            textStyle: {
                fontSize: 18,
                fontWeight: 'normal'
            }
        },
        tooltip: {
            trigger: 'axis',
            backgroundColor: 'rgba(0,0,0,0.8)',
            borderColor: '#333',
            textStyle: {
                color: '#fff'
            },
            formatter: function(params) {
                const data = params[0];
                const pointData = data.data;
                return `
                    <div style="padding: 10px;">
                        <div><strong>时间:</strong> ${pointData.time}</div>
                        <div><strong>运行时长:</strong> ${pointData.runtime}</div>
                        <div><strong>页面:</strong> ${pointData.page}</div>
                        <div><strong>内存使用:</strong> ${pointData.used}MB</div>
                        <div><strong>使用率:</strong> ${pointData.usage}%</div>
                    </div>
                `;
            }
        },
        legend: {
            data: ['内存使用', '内存限制'],
            top: 30
        },
        grid: {
            left: '3%',
            right: '4%',
            bottom: '3%',
            top: '15%',
            containLabel: true
        },
        xAxis: {
            type: 'category',
            boundaryGap: false,
            data: [],
            axisLabel: {
                rotate: 45,
                fontSize: 12
            }
        },
        yAxis: {
            type: 'value',
            name: '内存使用 (MB)',
            axisLabel: {
                formatter: '{value} MB'
            }
        },
        series: [
            {
                name: '内存使用',
                type: 'line',
                data: [],
                smooth: true,
                lineStyle: {
                    color: '#007bff',
                    width: 3
                },
                itemStyle: {
                    color: '#007bff'
                },
                areaStyle: {
                    color: {
                        type: 'linear',
                        x: 0,
                        y: 0,
                        x2: 0,
                        y2: 1,
                        colorStops: [{
                            offset: 0, color: 'rgba(0, 123, 255, 0.3)'
                        }, {
                            offset: 1, color: 'rgba(0, 123, 255, 0.1)'
                        }]
                    }
                }
            }
        ]
    };

    chart.setOption(option);
}

// 加载数据
function loadData(data) {
    currentData = data;
    
    // 更新信息面板
    document.getElementById('sessionId').textContent = data.sessionId || '-';
    document.getElementById('duration').textContent = data.duration || '-';
    document.getElementById('sampleCount').textContent = data.totalSamples || '-';
    document.getElementById('peakChanges').textContent = data.peakChanges ? data.peakChanges.length : '-';
    
    // 显示信息面板和统计卡片
    document.getElementById('infoPanel').style.display = 'block';
    document.getElementById('statsGrid').style.display = 'grid';
    
    // 计算统计数据
    const memoryData = data.data || [];
    const usedValues = memoryData.map(d => d.used);
    const currentMemory = usedValues[usedValues.length - 1] || 0;
    const maxMemory = Math.max(...usedValues);
    const avgMemory = Math.round(usedValues.reduce((a, b) => a + b, 0) / usedValues.length);
    const usagePercent = Math.round((currentMemory / data.config.simulatedLimitMB) * 100);
    
    // 更新统计卡片
    document.getElementById('currentMemory').textContent = currentMemory;
    document.getElementById('maxMemory').textContent = maxMemory;
    document.getElementById('avgMemory').textContent = avgMemory;
    document.getElementById('usagePercent').textContent = usagePercent;
    
    // 更新图表
    if (chart) {
        const xAxisData = memoryData.map(d => d.runtime);
        const seriesData = memoryData.map(d => ({
            value: d.used,
            time: d.time,
            runtime: d.runtime,
            page: d.page,
            used: d.used,
            usage: d.usage
        }));
        
        chart.setOption({
            xAxis: {
                data: xAxisData
            },
            series: [{
                data: seriesData
            }]
        });
    }
    
    showMessage('数据加载成功!', 'success');
}

// 文件输入处理
document.getElementById('fileInput').addEventListener('change', function(e) {
    const files = e.target.files;
    if (files.length > 0) {
        const file = files[0];
        const reader = new FileReader();
        reader.onload = function(e) {
            try {
                const data = JSON.parse(e.target.result);
                loadData(data);
            } catch (error) {
                showMessage('文件格式错误,请选择有效的JSON文件', 'error');
            }
        };
        reader.readAsText(file);
    }
});

// 加载示例数据
function loadSampleData() {
    const sampleData = {
        sessionId: 'sample_20241219_001',
        startTime: Date.now() - 300000, // 5分钟前
        endTime: Date.now(),
        duration: '5分0秒',
        totalSamples: 30,
        peakChanges: [
            {
                timestamp: Date.now() - 200000,
                change: 25,
                memory: 150,
                runtimeFromStart: '2分30秒',
                page: '/pages/index/index',
                timeFromStart: '14:30:15'
            }
        ],
        trend: {
            trend: 'increasing',
            message: '内存使用增长 15.5%,可能存在内存泄漏',
            growth: '20MB'
        },
        data: generateSampleMemoryData(),
        config: {
            simulatedLimitMB: 300,
            peakThresholdMB: 20
        }
    };
    
    loadData(sampleData);
}

// 生成示例内存数据
function generateSampleMemoryData() {
    const data = [];
    const baseMemory = 100;
    const startTime = Date.now() - 300000; // 5分钟前
    
    for (let i = 0; i < 30; i++) {
        const timestamp = startTime + (i * 10000); // 每10秒一个点
        const runtimeFromStartMs = timestamp - startTime;
        const runtimeMinutes = Math.floor(runtimeFromStartMs / 1000 / 60);
        const runtimeSeconds = Math.floor((runtimeFromStartMs % (1000 * 60)) / 1000);
        const runtime = `${runtimeMinutes}:${runtimeSeconds.toString().padStart(2, '0')}`;
        
        // 模拟内存增长趋势
        const growth = Math.sin(i * 0.2) * 20 + i * 2;
        const used = Math.round(baseMemory + growth + Math.random() * 10);
        
        data.push({
            timestamp: timestamp,
            time: new Date(timestamp).toLocaleTimeString(),
            runtime: runtime,
            page: i < 10 ? '/pages/index/index' : i < 20 ? '/pages/call/call' : '/pages/chat/chat',
            used: used,
            total: used + Math.round(Math.random() * 50),
            usage: Math.round((used / 300) * 100)
        });
    }
    
    return data;
}

// 清空数据
function clearData() {
    currentData = null;
    document.getElementById('infoPanel').style.display = 'none';
    document.getElementById('statsGrid').style.display = 'none';
    
    if (chart) {
        chart.setOption({
            xAxis: { data: [] },
            series: [{ data: [] }]
        });
    }
    
    showMessage('数据已清空', 'success');
}

// 导出数据
function exportData() {
    if (!currentData) {
        showMessage('没有数据可导出', 'error');
        return;
    }
    
    const dataStr = JSON.stringify(currentData, null, 2);
    const blob = new Blob([dataStr], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `memory-data-${currentData.sessionId}.json`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    
    showMessage('数据已导出', 'success');
}

// 显示消息
function showMessage(message, type) {
    const messageArea = document.getElementById('messageArea');
    messageArea.innerHTML = `<div class="${type}">${message}</div>`;
    setTimeout(() => {
        messageArea.innerHTML = '';
    }, 3000);
}

// 页面加载完成后初始化
window.addEventListener('load', function() {
    initChart();
    showMessage('欢迎使用内存监控数据可视化工具!请选择JSON数据文件或加载示例数据。', 'success');
});

// 响应式处理
window.addEventListener('resize', function() {
    if (chart) {
        chart.resize();
    }
});


</script>
   
</body>
</html>

二、TypeScript 配置详解

使用 TypeScript 时,我们需要配置 tsconfig.json 来保证类型安全和编译输出:

json 复制代码
{
  "compilerOptions": {
    "target": "es2017",                    // 编译到 ES2017,平衡兼容性和现代特性
    "module": "esnext",                    // 使用 ESNext 模块化,支持最新的模块语法
    "declaration": true,                   // 生成类型声明文件
    "declarationDir": "dist/types",        // 类型声明文件输出目录
    "outDir": "dist",                      // 编译输出目录
    "moduleResolution": "node",            // 使用 Node.js 模块解析策略
    "esModuleInterop": true,               // 启用 ES 模块互操作性
    "forceConsistentCasingInFileNames": true, // 强制文件名大小写一致
    "strict": true,                        // 启用所有严格类型检查
    "skipLibCheck": true,                  // 跳过库文件的类型检查
    "downlevelIteration": true             // 支持降级迭代
  },
  "include": ["src"],                      // 包含 src 目录下的所有文件
  "types": ["node"]                        // 包含 Node.js 类型定义
}

配置说明

  • target: "es2017":编译到 ES2017,支持 async/await、Object.values 等现代特性
  • module: "esnext":使用最新的 ES 模块语法,Vite 会进一步处理
  • declaration: true :生成 .d.ts 文件,供其他 TypeScript 项目使用
  • strict: true:启用严格模式,提高代码质量和类型安全
  • esModuleInterop: true:允许 CommonJS 和 ES 模块之间的互操作

三、Vite 打包工具配置

我们使用 Vite 来打包 SDK 和 HTML 页面。Vite 的主要优势:

  • 快速构建:基于 Rollup,支持现代 ES 模块化
  • 插件生态:丰富的插件系统,支持各种自定义需求
  • 开发体验:热重载、快速冷启动等开发特性
  • 生产优化:内置代码分割、压缩等优化功能

1. 完整配置解析

ts 复制代码
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import path from 'path';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import { minify } from 'html-minifier-terser';
import fs from 'fs';

export default defineConfig({
    build: {
        lib: {
            entry: path.resolve(__dirname, 'src/index.ts'), // SDK 入口文件
            name: 'MemoryMonitor',                          // 全局变量名(UMD 格式)
            fileName: (format) => `memory-monitor.${format}.js`, // 输出文件名模板
            formats: ['es', 'umd', 'cjs'],                 // 输出三种模块格式
        },
        outDir: 'dist',                                    // 输出目录
    },
    plugins: [
        // 生成 TypeScript 类型声明文件
        dts({
            insertTypesEntry: true,                        // 在 package.json 中插入 types 字段
        }),
        
        // 复制静态文件到输出目录
        viteStaticCopy({
            targets: [
                { src: 'src/index.html', dest: '.' },     // 复制 HTML 文件到 dist 根目录
            ],
        }),
        
        // 自定义插件:压缩 HTML 文件
        {
            name: 'html-minify',
            apply: 'build',                               // 仅在构建时应用
            closeBundle: async () => {                    // 在打包完成后执行
                const htmlPath = path.resolve(__dirname, 'dist/index.html');
                if (fs.existsSync(htmlPath)) {
                    const content = fs.readFileSync(htmlPath, 'utf-8');
                    const minified = await minify(content, {
                        collapseWhitespace: true,         // 压缩空白字符
                        removeComments: true,             // 移除注释
                        minifyCSS: true,                  // 压缩 CSS
                        minifyJS: true,                   // 压缩 JS
                        removeAttributeQuotes: false,     // 保留属性引号
                    });
                    fs.writeFileSync(htmlPath, minified, 'utf-8');
                }
            },
        },
    ],
});

2. 模块格式详解

1. ES Module (ESM)

特点

  • 官方标准模块,现代浏览器和 Node.js 都支持。
  • 语法原生支持 import / export
  • 支持静态分析,打包工具(Vite / Webpack / Rollup)可以进行 tree-shaking(去掉未用代码)。

示例

js 复制代码
import MemoryMonitor from 'memory-monitor-sdk';

MemoryMonitor.startMonitoring(2000, 300, 20, true);

优点

  • 静态依赖:打包工具可以提前分析依赖,提高优化效率。
  • 兼容现代前端生态:适合浏览器和现代 Node.js。
  • 支持 tree-shaking:减小打包体积。

缺点

  • 老旧浏览器不支持(IE 不支持)。
  • Node.js 需要在 .mjs 文件或 package.json 配置 "type": "module"

适用场景

  • 前端项目(Vue3 / React / Svelte 等现代框架)。
  • Node.js 14+ 的服务端项目。

2. UMD (Universal Module Definition)

特点

  • 通用模块格式,浏览器 + Node.js 都能用。
  • 通过 <script> 引入可以直接使用全局变量,也可以通过 CommonJS 或 AMD 引入。

示例

html 复制代码
<script src="memory-monitor.umd.js"></script>
<script>
  const monitor = new MemoryMonitor();
  monitor.startMonitoring(2000, 300, 20, true);
</script>

优点

  • 兼容性强:浏览器、AMD、CommonJS 环境都能用。
  • 零配置直接用:无需打包工具或 Node.js 环境支持。

缺点

  • 体积可能大:UMD 文件通常包含包装代码。
  • 不支持 tree-shaking:未使用的代码仍会打包进文件。

适用场景

  • 传统前端项目,直接在 HTML 页面上使用。
  • 插件 / SDK 发布给第三方使用(无需他们配置构建工具)。

3. CommonJS (CJS)

特点

  • Node.js 原生模块格式,使用 require / module.exports
  • 以同步加载为主(适合服务器环境)。

示例

js 复制代码
const MemoryMonitor = require('memory-monitor-sdk');

const monitor = new MemoryMonitor();
monitor.startMonitoring(2000, 300, 20, true);

优点

  • Node.js 原生支持,无需额外配置。
  • 使用广泛:很多老旧 Node.js 模块仍然是 CJS。

缺点

  • 不适合浏览器,需要打包工具(Webpack / Rollup)才能用。
  • 不支持静态分析,tree-shaking 不太好做。

适用场景

  • Node.js 服务端项目。
  • 构建工具内部或老旧模块生态。

🔹 总结对比表
模块格式 语法 运行环境 tree-shaking 使用场景
ESM import/export 浏览器 / Node.js 14+ ✅ 支持 前端现代框架,服务端现代 Node.js
UMD 全局变量 / AMD / CommonJS 浏览器 / Node.js ❌ 不支持 插件/SDK,兼容性需求高
CJS require/module.exports Node.js ❌ 不支持 服务器端 Node.js,老旧项目

四、自定义插件详解

Vite 插件基于 Rollup 插件系统,可以在构建流程的不同阶段执行自定义逻辑:

ts 复制代码
{
    name: 'plugin-name',           // 插件名称
    apply: 'build',                // 应用时机:'build' | 'serve' | 函数
    buildStart() {                 // 构建开始
        console.log('构建开始');
    },
    transform(code, id) {          // 转换每个模块
        // 处理模块内容
        return code;
    },
    generateBundle(options, bundle) { // 生成包内容前
        // 处理生成的包
    },
    closeBundle() {                // 构建完成后
        // 清理、压缩、生成报告等
    }
}

Vite 打包流程及插件钩子详解

1️⃣ 配置阶段(Config)

钩子:config / configResolved

  • 作用:修改 Vite 配置或读取配置信息

  • 发生时间:打包开始前,Vite 还没初始化

  • 适用场景

    • 动态修改 build.outDir
    • 根据环境变量修改配置
  • 注意:还没有生成任何文件

ts 复制代码
{
  name: 'my-plugin',
  config(config, env) {
    console.log('打包配置阶段', config, env)
  }
}

2️⃣ 构建阶段(Build Start)

钩子:buildStart

  • 作用:开始构建前,做一些初始化或检查

  • 发生时间:开始打包,Rollup 还没输出文件

  • 适用场景

    • 检查文件是否存在
    • 初始化缓存或数据
    • 报错提前终止打包
ts 复制代码
{
  name: 'my-plugin',
  buildStart() {
    console.log('构建开始')
  }
}

3️⃣ 文件生成阶段(Transform / Load / Resolve)

钩子:load / transform / resolveId

  • 作用:逐个模块处理 JS/TS/CSS/HTML 等文件

  • 发生时间:Rollup 开始解析和打包每个文件

  • 适用场景

    • 转换 TS/JS 文件内容
    • 内联 CSS 或图片资源
    • 修改模块路径
ts 复制代码
{
  name: 'my-plugin',
  transform(code, id) {
    if (id.endsWith('.js')) {
      return code.replace(/console\.log/g, '')
    }
  }
}

4️⃣ 生成输出阶段(Generate / Render Chunk)

钩子:generateBundle

  • 作用:Rollup 已经生成了输出内容,但还没写入磁盘

  • 发生时间 :打包文件还没保存到 dist

  • 适用场景

    • 修改输出文件内容
    • 添加额外文件或资源
    • 动态生成 HTML、LICENSE、banner
ts 复制代码
{
  name: 'my-plugin',
  generateBundle(options, bundle) {
    console.log('所有文件准备生成', Object.keys(bundle))
  }
}

5️⃣ 打包完成阶段(Close Bundle)

钩子:closeBundle ✅ 我们的插件用的就是这个

  • 作用:所有文件都生成并写入磁盘后,做最后处理

  • 发生时间dist 目录里已经有 index.htmlmemory-monitor.js 等文件

  • 适用场景

    • 压缩 HTML、CSS、JS 文件
    • 修改最终生成的文件内容
    • 上传到 CDN / 自动发布
ts 复制代码
{
  name: 'html-minify',
  closeBundle: async () => {
    const htmlPath = path.resolve(__dirname, 'dist/index.html')
    if (fs.existsSync(htmlPath)) {
      const content = fs.readFileSync(htmlPath, 'utf-8')
      const minified = await minify(content, { collapseWhitespace: true })
      fs.writeFileSync(htmlPath, minified, 'utf-8')
    }
  }
}

解释:

  • closeBundle 是最后一步,Vite 已经把 SDK 打包好、HTML 拷贝好
  • 我们可以安全地去修改 dist/index.html,不会影响打包流程

6️⃣ 总结流程图(文字版)
复制代码
配置阶段
  └─ config / configResolved
构建开始
  └─ buildStart
模块解析 & 转换
  ├─ resolveId
  ├─ load
  └─ transform
生成输出
  └─ generateBundle
打包完成
  └─ closeBundle ← 我们压缩 HTML 的位置

五、构建流程详解

1. 完整构建流程

开始构建 TypeScript 编译 生成类型声明文件 打包 SDK 为三种格式 复制 HTML 文件 压缩 HTML 文件 输出到 dist 目录 构建完成

2. 输出文件说明

SDK 文件

  • memory-monitor.es.js - ES 模块格式,现代浏览器和构建工具使用
  • memory-monitor.umd.js - UMD 格式,浏览器直接引入使用
  • memory-monitor.cjs.js - CommonJS 格式,Node.js 环境使用
  • memory-monitor.d.ts - TypeScript 类型定义文件

可视化页面

  • index.html - 压缩后的可视化页面,包含完整的图表和交互功能

3. 使用方式

作为 SDK 使用

js 复制代码
// ES 模块
import MemoryMonitor from 'memory-monitor-sdk';

// 开始监控
MemoryMonitor.startMonitoring(2000, 300, 20, true);

// 保存数据
MemoryMonitor.saveDataToFile();

作为可视化工具使用

html 复制代码
<!-- 直接打开 dist/index.html -->
<!-- 或通过 HTTP 服务器访问 -->

六、项目特色功能及效果图展示

1. SDK 核心特性

智能监控

  • 自动检测内存使用情况
  • 峰值变化告警(默认 20MB 阈值)
  • 页面路由自动跟踪
  • 运行时长统计

数据管理

  • 自动数据清理(保留最近 100 条记录)
  • 多种数据导出格式
  • 详细的报告生成
  • 会话数据持久化

开发体验

  • 可拖拽的 DOM 显示面板
  • 丰富的控制台日志
  • 完整的 TypeScript 类型支持
  • 开发环境自动启动

2. 可视化页面特性

图表展示

  • 基于 ECharts 的交互式图表
  • 内存使用趋势分析
  • 峰值变化标记
  • 响应式设计

数据交互

  • 多文件数据加载
  • 实时数据更新
  • 数据导出功能
  • 示例数据演示

用户体验

  • 现代化的 UI 设计
  • 移动端适配
  • 加载状态提示
  • 错误处理机制

3.实现效果图



八、发布到 NPM

bash 复制代码
1. **准备 package.json**
{
  "name": "memory-monitor-sdk",
  "version": "1.0.1",
  "description": "A powerful JavaScript memory monitoring SDK for Web and mobile applications. Features real-time monitoring, data visualization, automatic reporting, and memory leak detection.",
  "main": "dist/memory-monitor.es.js",
  "module": "dist/memory-monitor.es.js",
  "types": "dist/memory-monitor.d.ts",
  "files": [
    "dist",
    "README.md"
  ],
  "directories": {
    "example": "examples"
  },
  "scripts": {
    "build": "vite build",
    "dev": "vite",
    "preview": "vite preview",
    "type-check": "tsc --noEmit",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "memory",
    "monitoring",
    "performance",
    "memory-leak",
    "sdk",
    "typescript",
    "web-performance",
    "mobile-performance",
    "memory-analysis",
    "performance-tools",
    "browser-memory",
    "memory-visualization",
    "real-time-monitoring"
  ],
  "author": {
    "name": "huangql520",
    "url": "https://github.com/huangql520"
  },
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/huangql520/memoryDevelopmentSdk.git"
  },
  "bugs": {
    "url": "https://github.com/huangql520/memoryDevelopmentSdk/issues"
  },
  "homepage": "https://github.com/huangql520/memoryDevelopmentSdk#readme",
  "engines": {
    "node": ">=14.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead",
    "Chrome >= 60"
  ],
  "devDependencies": {
    "@types/node": "^24.5.2",
    "typescript": "^5.9.2",
    "vite": "^7.1.7",
    "vite-plugin-dts": "^4.5.4",
    "vite-plugin-static-copy": "^3.1.2",
    "html-minifier-terser": "^7.2.0"
  },
  "dependencies": {},
  "publishConfig": {
    "access": "public"
  }
}

确保必要字段完整(main / module / types / files / repository 等)。

  1. 构建项目
bash 复制代码
npm run build
  1. 提交代码到 Git
bash 复制代码
git add .
git commit -m "chore: prepare release"
  1. 更新版本号
bash 复制代码
npm version patch  # 或 minor / major
  1. 发布到 npm
bash 复制代码
npm publish --access public
  1. 安装使用
bash 复制代码
npm install memory-monitor-sdk

import MemoryMonitor from 'memory-monitor-sdk';
MemoryMonitor.startMonitoring(2000, 300, 20, true);

⚠️ 注意:npm 不允许覆盖已有版本,需要使用新的版本号;确保 Git 工作区干净,类型文件和 README 完整。


九、总结

SDK 核心特性

  • 智能监控、峰值告警
  • 页面路由跟踪
  • 会话数据持久化与导出
  • 可拖拽 DOM 面板

可视化页面特性

  • ECharts 图表交互
  • 多文件数据加载
  • 实时更新与导出
  • 响应式设计与移动端适配

架构优势

  • 模块化设计:SDK 与可视化页面分离
  • 多模块格式:兼容多种环境
  • 类型安全:完整 TypeScript 支持
  • 开发友好:热重载、详细日志

应用场景

  • 前端项目内存监控
  • 性能分析与报告生成
  • 团队协作与演示
  • 内存问题快速排查

这个项目展示了如何构建一个完整的监控工具,这种方案可以让开发者快速上手,既能在项目中通过 npm 直接集成 SDK,又能直接通过 HTML 页面查看内存监控数据,极大提升了开发体验。

相关推荐
weixin_456904272 小时前
离线下载npm包
前端·npm·node.js
低代码布道师2 小时前
少儿舞蹈小程序(19)地址列表功能实现及默认地址逻辑
前端·低代码·小程序
90后的晨仔2 小时前
Vue3 + TypeScript + Pinia 实战全解析
前端
90后的晨仔2 小时前
Vue 3 + TypeScript + Pinia 实战架构指南
前端
zhennann3 小时前
VonaJS多租户同时支持共享模式和独立模式
数据库·typescript·node.js·nestjs
妄小闲3 小时前
免费html网页模板 html5网站模板 静态网页模板
前端·html·html5
困惑阿三3 小时前
React 展示Markdown内容
前端·react.js·前端框架
lichong9514 小时前
【大前端++】Android studio Log日志高对比度配色方案
android·java·前端·json·android studio·大前端·大前端++
没头脑的男大4 小时前
如何把pdf转换的excell多个表格合并
java·前端·pdf