从零到一打造前端内存监控 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_382395235 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__5 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.5 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~6 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华7 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子9 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅9 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni10 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学11 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端