打开 Obsidian 的开发者工具,我们可以发现其文件结构比较简单,功能都被压缩在几个源码文件里,而主要内容就是 app.js。
追踪 Obsidian 源码的目的是研究其插件实现原理,借助 vscode,才得以实现。
逆向思路
0x00 根据 API 进行反推
Obsidian 前端源码是做了混淆的,并且全局的 require 函数和插件里的 require 函数也不相同。在控制台使用的 require 是 electron 提供的 require,无法像插件里那样 require('obsidian')来获取 API。因此我们可以直接查询一个 API 关键词来获取,例如 AbstractTextComponent
。得到以下源码
var p99 = {};
n.r(p99),
n.d(p99, {
AbstractTextComponent: () => JA,
App: () => SW,
BaseComponent: () => YA,
ButtonComponent: () => XA,
ColorComponent: () => aL,
Component: () => OM,
DropdownComponent: () => rL,
EditableFileView: () => _O,
Editor: () => TL,
EditorSuggest: () => dH,
Events: () => rD,
ExtraButtonComponent: () => ZA,
FileManager: () => FH,
FileSystemAdapter: () => tA,
FileView: () => qO,
FuzzySuggestModal: () => _H,
HoverPopover: () => WN,
ItemView: () => zO,
Keymap: () => JT,
MarkdownPreviewRenderer: () => uT,
MarkdownPreviewSection: () => aT,
MarkdownPreviewView: () => $F,
MarkdownRenderChild: () => oT,
MarkdownRenderer: () => GF,
MarkdownSourceView: () => LN,
MarkdownView: () => FN,
Menu: () => OL,
MenuItem: () => IL,
MenuSeparator: () => FL,
MetadataCache: () => PA,
Modal: () => eD,
MomentFormatComponent: () => iL,
Notice: () => iD,
Platform: () => Mw,
Plugin: () => fD,
PluginSettingTab: () => mD,
PopoverState: () => DN,
PopoverSuggest: () => fR,
Scope: () => ZT,
Setting: () => KA,
SettingTab: () => ET,
SliderComponent: () => oL,
SuggestModal: () => UH,
TAbstractFile: () => oA,
TFile: () => sA,
TFolder: () => lA,
TextAreaComponent: () => nL,
TextComponent: () => eL,
TextFileView: () => PN,
ToggleComponent: () => QA,
ValueComponent: () => $A,
Vault: () => cA,
View: () => sL,
ViewRegistry: () => $N,
Workspace: () => lV,
WorkspaceContainer: () => rH,
WorkspaceFloating: () => rV,
WorkspaceItem: () => eH,
WorkspaceLeaf: () => lH,
WorkspaceParent: () => tH,
WorkspaceRibbon: () => nV,
WorkspaceRoot: () => iV,
WorkspaceSidedock: () => iH,
WorkspaceSplit: () => nH,
WorkspaceTabs: () => aH,
WorkspaceWindow: () => oV,
addIcon: () => YM,
apiVersion: () => bW,
arrayBufferToBase64: () => z,
arrayBufferToHex: () => W,
base64ToArrayBuffer: () => V,
debounce: () => aw,
editorEditorField: () => hO,
editorInfoField: () => dO,
editorLivePreviewField: () => fO,
editorViewField: () => pO,
finishRenderMath: () => UC,
fuzzySearch: () => Xw,
getAllTags: () => UD,
getBlobArrayBuffer: () => cw,
getIcon: () => GM,
getIconIds: () => XM,
getLinkpath: () => HD,
hexToArrayBuffer: () => j,
htmlToMarkdown: () => PL,
iterateCacheRefs: () => zD,
iterateRefs: () => qD,
livePreviewState: () => FB,
loadMathJax: () => NC,
loadMermaid: () => IC,
loadPdfJs: () => AC,
loadPrism: () => OC,
moment: () => PB,
normalizePath: () => Je,
parseFrontMatterAliases: () => MM,
parseFrontMatterEntry: () => EM,
parseFrontMatterStringArray: () => SM,
parseFrontMatterTags: () => TM,
parseLinktext: () => VD,
parseYaml: () => AB,
prepareFuzzySearch: () => Kw,
prepareQuery: () => Gw,
prepareSimpleSearch: () => ek,
removeIcon: () => $M,
renderMatches: () => nk,
renderMath: () => RC,
renderResults: () => tk,
request: () => Fw,
requestUrl: () => Iw,
requireApiVersion: () => kW,
resolveSubpath: () => KD,
sanitizeHTMLToDom: () => FM,
setIcon: () => KM,
sortSearchResults: () => Zw,
stringifyYaml: () => LB,
stripHeading: () => WD,
stripHeadingForLink: () => GD,
});
追踪 n.r
和 n.d
函数,可以找到
function n(i) {
var r = t[i];
if (void 0 !== r)
return r.exports;
var o = t[i] = {
exports: {}
};
return e[i].call(o.exports, o, o.exports, n),
o.exports
}
n.n = e=>{
var t = e && e.__esModule ? ()=>e.default : ()=>e;
return n.d(t, {
a: t
}),
t
}
,
n.d = (e,t)=>{
for (var i in t)
n.o(t, i) && !n.o(e, i) && Object.defineProperty(e, i, {
enumerable: !0,
get: t[i]
})
}
,
n.g = function() {
if ("object" == typeof globalThis)
return globalThis;
try {
return this || new Function("return this")()
} catch (e) {
if ("object" == typeof window)
return window
}
}(),
n.o = (e,t)=>Object.prototype.hasOwnProperty.call(e, t),
n.r = e=>{
"undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
value: "Module"
}),
Object.defineProperty(e, "__esModule", {
value: !0
})
}
直接阅读比较困难,不过看到 __esModule
,估计是 webpack 生成的模块化相关的代码,上 Google 直接把代码粘上去,也的确能印证这段代码是 webpack 生成的标准代码,除了变量名因为每次编译可能发生变化之外,结构是固定的。
因此我们可以发觉 p99
变量应该就是暴露 API 相关的内容。顺着变量往下找。
PS: 这里的 p99 并不是原始代码,而是利用 VScode 的 rename 功能把变量重命名,方便后面进行查找。
0x01 追踪
可以返现这个变量就在一个地方使用了
var pD99 = {
obsidian: p99,
"@codemirror/autocomplete": a,
"@codemirror/collab": s,
"@codemirror/commands": l,
"@codemirror/language": o,
"@codemirror/lint": c,
"@codemirror/search": u,
"@codemirror/state": e,
"@codemirror/text": e,
"@codemirror/view": t,
"@lezer/common": i,
"@lezer/lr": h,
"@lezer/highlight": r
}
, dD = {
"@codemirror/closebrackets": a,
"@codemirror/comment": l,
"@codemirror/fold": o,
"@codemirror/gutter": t,
"@codemirror/highlight": o,
"@codemirror/history": l,
"@codemirror/matchbrackets": o,
"@codemirror/panel": t,
"@codemirror/rangeset": e,
"@codemirror/rectangular-selection": t,
"@codemirror/stream-parser": o,
"@codemirror/tooltip": t
}
这么一看,终于找到了 obsidian
这个包是怎么获取的了,显然就是在这里定义的,那么是怎么实现的 require 函数呢?我们接着往下看
e.prototype.loadPlugin = function(e) {
return g(this, void 0, Promise, (function() {
var t, n, i, r, o, a, s, l;
return v(this, (function(c) {
switch (c.label) {
case 0:
return this.isEnabled() ? (t = this.plugins[e]) ? [2, t] : (n = this.manifests[e]) ? [4, this.app.vault.adapter.read(n.dir + "/" + aD)] : [2, null] : [2];
case 1:
if (i = c.sent(),
localStorage.getItem("debug-plugin") || (i = i.replace(vD, "")),
r = e,
o = function(e) {
return dD.hasOwnProperty(e) && console.error(new Error("[CM6][".concat(r, '] Using a deprecated package: "').concat(e, '".\n').concat(lD))),
dD[e] || pD99[e] || rt(e)
}
,
s = {
exports: a = {}
},
function(e, t) {
return window.eval("(function anonymous(require,module,exports){".concat(e, "\n})\n//# sourceURL=").concat(t, "\n"))
}(i, "plugin:" + encodeURIComponent(e))(o, s, a),
!(l = (a = s.exports || a).default || s.exports))
throw new Error("Failed to load plugin " + e + ". No exports detected.");
if (!((t = new l(this.app,n))instanceof fD))
throw new Error("Failed to load plugin " + e);
return this.plugins[e] = t,
[4, t.load()];
case 2:
return c.sent(),
[4, t.loadCSS()];
case 3:
return c.sent(),
[2, t]
}
}
))
}
))
}
loadPlugin
看来就是加载插件的源码了,这个方法名也没有混淆,很方便找。
这个 g
和 v
按照刚才的方法进行学习,可以了解是跟 Promise 异步处理相关的代码,或者将 async 语法编译为 Promise 之后生成的代码。主要看内部。
function(e, t) {
return window.eval("(function anonymous(require,module,exports){".concat(e, "\n})\n//# sourceURL=").concat(t, "\n"))
}(i, "plugin:" + encodeURIComponent(e))(o, s, a)
0x02 结果分析
原来是通过 eval
来实现的代码加载。这个结果与在 devtool 中查看到的源码 plugin:obsidian-day-planner
源码一致,就是在插件编译后的源码前后添加了 function anonymous(require,module,exports){
和 //# sourceURL=
。这里的 o
、s
、a
,分别对应了 require
、module
、exports
函数或对象。也就是说插件里的 require 方法,完全不是 electron 提供的 require 方法。通过这种方法,即能向模块提供私有 API,同时也能够污染插件内的 require,避免其调用可提供 API 之外的包,保证安全性。
而在插件的开发过程中,插件一般用 Typescript 进行编写,import { BaseComponent } from 'obsidian'
这种方法,一方面 npm 提供了 obsidian 的 npm 包,内置 API 的类型声明,同时编译后对 const BaseComponent = require('obisidan').BaseComponent
也能正常运行,开发体验和运行都得到了保障
0x03 推广到 siyuan
思源目前的挂件系统就是 iframe 实现,做的还比较粗糙,我们后续提供插件系统可以借鉴这种方式,来为开发者提供此种开发体验。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于