likes
comments
collection
share

一步教你如何在浏览器中解析小程序异常

作者站长头像
站长
· 阅读数 16

没错,只需要安装了谷歌插件后就可以实现一键解析小程序异常并查看其源码,一步搞定!

效果图如下:

一步教你如何在浏览器中解析小程序异常

代码地址在 👇👇👇👇👇👇👇👇👇,喜欢的话可以点个 star 哦

github地址

实现思路

实现思路其实很简单,就是通过谷歌插件上传微信小程序线上的 sourceMap 文件,再将报错信息传给服务端去接解析返回源码和错误信息就完事了。

一步教你如何在浏览器中解析小程序异常

谷歌插件实现

关于谷歌插件的实现,本人使用的是开源的 rollup + typescript开发谷歌插件模板

由于原模板不支持 CSS Modulessass,可以通过以下代码实现:

npm install node-sass postcss -D
// rollup.config.js
import postcss from 'rollup-plugin-postcss'

export default {
    ...,
    postcss({
      autoModules: true,
      modules: false
    }),
}

根目录下新建 typeing.d.ts 文件

// typeing.d.ts
declare module "*.scss";
declare module "*.css";

// tsconfig.json
"include": [
    "typings.d.ts"
]

上传微信小程序线上SourceMap

我们点击 We 分析页面下载线上版本的 SourceMap 文件 发现下载 Source Map ZIP 包其实是通过调用wedata.weixin.qq.com/mp2/cgi/rep… 接口并携带 cookie 来下载的。

一步教你如何在浏览器中解析小程序异常

我们通过插件来获取指定域名下的 cookie 并将其传给服务端,在服务端下载微信小程序的线上 SourceMap 文件。

let cookieValue = ''
// 获取多个cookie,并设置到当前插件页面下
chrome.cookies.getAll({ domain: 'wedata.weixin.qq.com' }, function (cookie) {
    cookie.forEach(function (c) {
        cookieValue += (c.name + '=' + c.value + ';')
    });
    cookieValue = cookieValue
});

DOM 中插入【解析】按钮的实现

通过页面操作和 network ,我们能知道 We 分析的异常错误列表是通过wedata.weixin.qq.com/mp2/cgi/rep… 接口获取的。知道这个后我们就好办了,我们可以通过插件监听 GetWxaJsErrInfoSummary 接口请求完成后给 DOM 插入一个新的按钮用来实现错误解析。

一步教你如何在浏览器中解析小程序异常

在插件的 background 里通过 chrome.webRequest 来监听请求的完成,然后再通过 sendMessage 来通知 content。

// background.ts
chrome.webRequest.onCompleted.addListener(
    function (details) {
        if (details.url == 'https://wedata.weixin.qq.com/mp2/cgi/reportdevlog/GetWxaJsErrInfoSummary') {
            chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
                chrome.tabs.sendMessage(tabs[0].id!, {
                    type: 2,
                    message: 'GetWxaJsErrInfoSummary接口调用完成'
                }, function (response) {
                });
            });
        }
    },
    { urls: ['https://*/*', 'http://*/*'] },
);

// content.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    const detailContainer = document.querySelectorAll('.detail__container')
    const cloneButton = document.querySelectorAll('.detail__link_clone')
    cloneButton.forEach(item => {
        item.remove();
    })

    if (detailContainer.length > 0) {
        detailContainer.forEach((el, index: number) => {
            var sourceNode = document.querySelector(".detail__link"); // 获得被克隆的节点对象   
            var clonedNode = sourceNode?.cloneNode(true) as HTMLElement;
            clonedNode.innerText = '解析'
            clonedNode.setAttribute('date-index', String(index))
            clonedNode.setAttribute('class', 'detail__link detail__link_clone')
            el.appendChild(clonedNode)
            clonedNode?.addEventListener('click', (e) => {
                e.stopPropagation()
                // 从 DOM 中获取报错信息
                reportError((el as HTMLElement).innerText)
            })
            // 这里是返回给 background 的内容
            sendResponse('success')
        })
    }
})

以上代码就完成了监听 GetWxaJsErrInfoSummary 接口请求完成后向 DOM 插入解析按钮的实现。

源码展示的实现

通过接口获取到源码文件如何优雅的展示出来呢?通过 highlight.js + pre 标签来实现

const SourceFile = () => {
    const { show, errorInfo, onClose } = props

    useEffect(() => {
        // 配置 highlight.js
        hljs.configure({
            languages: ['javascript'],
            ignoreUnescapedHTML: true
        })
    }, [])

    useEffect(() => {
        if (show) {
            const codes = document.querySelectorAll('pre .minicode')
            codes.forEach((el) => {
                // 让code进行高亮
                hljs.highlightElement(el as HTMLElement)
            })
        }
    }, [show])

    return <div>
        <pre className={style.code} dangerouslySetInnerHTML={{ __html: errorInfo.sourceContent }} >
        </pre>
    </div>
}

服务端实现

服务端主要实现的有两个功能:下载 SourceMap解析错误

下载 SourceMap 实现

通过链接下载 ZIP 包后就将其解压出来。

npm i decompress -D
const getSourceMap = function (cookie, dirId) {
    return new Promise((resolve, reject) => {
        var req = https.get('https://wedata.weixin.qq.com/mp2/cgi/reportdevlog/GetSourceMapDownload?index=0', {
            headers: {
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
                "cookie": cookie
            }
        }, function (res) {

            const zipFilePath = fileResolve('../zipFileDownload.zip')
            const writeStream = fs.createWriteStream(zipFilePath)

            res.on('data', function (chunk) {
                writeStream.write(chunk)
            })

            res.on('end', function (chunk) {
                writeStream.end(chunk)
            })

            writeStream.on('finish', async () => {
                try {
                    const decompressFolderPath = fileResolve(`../contents/${dirId}`) // decompress folder
                    await decompress(zipFilePath, decompressFolderPath)
                    const sourceMapFolder = fileResolve(`../mapFile/${dirId}`)
                    await exitsFolder(sourceMapFolder)
                    await deleteDir(sourceMapFolder)
                    mapDir(decompressFolderPath, async (pathName, fileName) => {
                        if (!fs.existsSync(`${sourceMapFolder}/${fileName}`)) {
                            rename(pathName, `${sourceMapFolder}/${fileName}`)
                            return
                        }
                        try {
                            const oldStats = await stat(pathName)
                            const newStats = await stat(`${sourceMapFolder}/${fileName}`)
                            if (oldStats.size > newStats.size) {
                                await rename(pathName, `${sourceMapFolder}/${fileName}`)
                            }
                        } catch (error) {
                            console.error('error1', error)
                        }
                    })
                    await unlink(zipFilePath)

                    resolve()
                } catch (error) {
                    console.error('error2', error)
                }
            })

        }).on('error', function (e) {
            console.log('error: ' + e.message)
            reject()
        })

        req.end()
    })
}

解析错误的实现

const analysis = async (errorStack, id) => {
    const tracey = new Stacktracey(errorStack)

    // 过滤系统报错
    const errorInfo = tracey.items.find(item => {
        return !(['WASubContext.js', 'WAServiceMainContext.js'].includes(item.fileName))
    }) || tracey.items[0]

    let { fileName } = errorInfo
    fileName = fileName.indexOf('appservice') != -1 ? fileName.replace('.js', '.map') : fileName.replace('.js', '.appservice.map')
    let sourceMapFileName = `../mapFile/${id}/app-service.map.map`
    if (fs.existsSync(path.resolve(__dirname, `../mapFile/${id}/${fileName}.map`))) {
        sourceMapFileName = `../mapFile/${id}/${fileName}.map`
    }

    console.log('path.resolve(__dirname, sourceMapFileName)', path.resolve(__dirname, sourceMapFileName))
    const sourceMapFileContent = await readFile(path.resolve(__dirname, sourceMapFileName))
    // 解析错误栈信息
    const sourceMapContent = JSON.parse(sourceMapFileContent);

    try {
        // 根据source map文件创建SourceMapConsumer实例
        const consumer = await new sourceMap.SourceMapConsumer(sourceMapContent);

        // 根据打包后代码的错误位置解析出源码对应的错误信息位置
        const originalPosition = consumer.originalPositionFor({
            line: errorInfo.line,
            column: errorInfo.column,
        });

        // 获取源码内容
        const sourceContent = consumer.sourceContentFor(originalPosition.source);

        console.log('originalPosition', originalPosition)

        // 返回解析后的信息
        return {
            sourceContent,
            ...originalPosition
        }
    } catch (error) {
        console.log('error', error)
        return Promise.reject(error)
    }
}