用一种很难绷的方式解决了 mathlive 在思源中的导入报错

问题描述

“数学增强”插件(siyuan-plugin-math-enhance),是一个对于推公式非常友好的插件,但是有一个点让我非常难受:在每次开启插件时都会有下面这样的报错

image.png

虽然这个报错本身并不会导致什么,但是对于插件开发者来说有时候会很烦躁,比如想让用户看一下控制台的红色报错输出,但是被发了张这个就很难绷,因此我还是想着手解决它(正好最近也在优化这个插件的一些其他功能,最近在推公式就顺手改改)

问题分析

首先我们看到这个报错的栈,问题出在这里

image.png

从 node-modules 中找到 mathlive.js,经过搜索找到了它本来的样子

image.png

这个函数是干什么的呢?它的名字叫 getFileUrl,但是起手一个 new Error(),在查询了之后发现它其实是想通过报错的调用栈来定位到自身脚本所在的绝对位置。调用这个函数的地方是这样写的

image.png

首先找 globalThis.document.currentScript.src,如果没有找到就调用 getFileUrl 这个函数找。好的,那我们能不能直接避免调用它,比如这个插件原本进行了设置

MathfieldElement.fontsDirectory = null;
MathfieldElement.soundsDirectory = null;

但是没有用,因为 gScriptUrl 是直接在最开始的 var Mathlive = (() => { 里写的,也就是说只要有 import("mathlive") 就一定会运行到这个函数。那么为什么会找不到 globalThis.document.currentScript.src 呢?主要原因是思源的插件调用机理是把插件导出的 index.js 文件作为一串文本然后通过 eval 运行,回到错误栈上就是这里

image.png

所以它是一定找不到运行路径的,也就一定会运行 getFileUrl 这个函数……吗?

等等,那为什么不在引入 mathlive 的时候直接给它指定一个路径呢?我检查了下 gScriptUrl 的引用情况,只要把 fontsDirectorysoundsDirectory 设置为 null,就不会再用到这个变量,而 mathlive 的机制就是只要引入就不用管了,创建了 <math-field> 标签的块之后就会自动渲染 mathlive 前端,所以完全可以在 onload 中动态引入。

问题解决

说干就干,最后形成了这样的代码:

async function mathliveDynamicImport() {
    // 准备一个假的 "currentScript" 对象
    // 这个 src 应该是指向你的插件最终被加载的 JS 文件的 URL
    // 在思源中,它通常是 /plugins/你的插件名/index.js
    const fakeCurrentScript = {
    src: `${window.location.origin}/plugins/siyuan-plugin-math-enhance/index.js`, // <-- 把 'siyuan-plugin-math-enhance' 换成你插件的真实名称
    };

    // 保存原始的 document.currentScript 属性描述符
    const originalCurrentScript = Object.getOwnPropertyDescriptor(document, 'currentScript');

    // **【核心步骤】**:在导入 mathlive 之前,用猴子补丁篡改 document.currentScript
    Object.defineProperty(document, 'currentScript', {
    get: () => fakeCurrentScript,
    configurable: true, // 必须是可配置的,以便我们之后可以恢复它
    });

    // console.log('Monkey patch applied. Importing MathLive...');

    try {
    // 在这个被修改过的环境中动态导入 mathlive
    // 当 mathlive 内部的 IIFE 运行时,它访问 document.currentScript 将会得到我们的 fakeCurrentScript 对象
    // 它的 .src 属性是一个有效的 URL,于是 `||` 左边为真,getFileUrl() 就不会被执行!
    // 在 import 内部添加一个特殊的注释
    await import(/* webpackMode: "eager" */ 'mathlive');
    globalThis.MathfieldElement.fontsDirectory = null;
    globalThis.MathfieldElement.soundsDirectory = null;
    // console.log('MathLive imported successfully.');

    } catch (error) {
    console.error('Failed to import MathLive:', error);

    } finally {
    // **【重要】**:无论成功还是失败,都必须恢复原始的 document.currentScript
    // 以免对思源本身或其他插件造成意想不到的副作用
    // console.log('Restoring original document.currentScript...');
    if (originalCurrentScript) {
        Object.defineProperty(document, 'currentScript', originalCurrentScript);
    } else {
        // 如果原始就不存在,就删除我们添加的
        delete (document as any).currentScript;
    }
    }
}

简单来说就是先保存目前的 globalThis.document.currentScript,再生成一个假的,然后通过 import('mathlive') 动态导入 mathlive 库,最后给原本的 globalThis.document.currentScript 恢复回来。

但是它还有一个问题,就是动态导入的库在用 webpack 打包的时候会生成一个独立的 chunk 文件,如果不修改 webpack.config.js 把这个 chunk 文件一起引入的话还是会导入出错,但是 webpack 提供了一个“魔法注释”,即在 import 的时候通过添加注释来让动态导入的包不再独立生成文件,也就是

await import(/* webpackMode: "eager" */ 'mathlive');

至此,这个调用报错就不会再出现了,清爽的控制台,真好看

image.png

至于说为什么难绷,主要就是很有那种屎山上打补丁的感觉,用一种很难受但是有效的方法实现了自己想要的功能

  • JavaScript

    JavaScript 一种动态类型、弱类型、基于原型的直译式脚本语言,内置支持类型。它的解释器被称为 JavaScript 引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在 HTML 网页上使用,用来给 HTML 网页增加动态功能。

    736 引用 • 1307 回帖 • 2 关注
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28442 引用 • 119754 回帖
  • 插件
    116 引用 • 752 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...