思源笔记折腾记录 - 思源内部打开网页

本贴最后更新于 853 天前,其中的信息可能已经时异事殊

一、前情提要

我们在之前已经实现了自定义菜单和自定义工具栏,虽然都比较绿皮,但是也确实是能够使用。

这次我们来整个复杂点的(为什么整之后就知道了)。

思源本来就能够打开网页,就像这样:

image

应该都能够看得出来,这是一个 iframe 块吧。

但是有的时候比如说你要对着网页做笔记啥的,这样好像有点不太方便,而且有些网站在 iframe 里面也没有办法正常登录。

所有我们这次来实现一个类似浏览器网页的功能。

就像这样:

image

二、实现过程

获取原型

首先呢,我们要知道页签(tab)是怎么渲染出来的。

看源码就知道大概是这样:

wnd.addTab(new tab(options))

但是思源并没有把这些暴露出来,那么我们首先就需要能够取到 wnd 和 Tab 的类。

这里鼓捣几个工具函数:

noobApi\util\layouts.js

import {展平树 as flattern } from "../util/common.js" export function getLayoutByElement(el) { let array = flattern(window.siyuan.layout.centerLayout) let target target = array.filter( item => { return item.id == el.firstChild.dataset.id } ) return target[0] } export function getClosestLayout(el) { if (getLayoutByElement(el)) { return getLayoutByElement(el) } else { return getLayoutByElement(el.parentElement) } } export function getWndParentElement(element) { let parent = element.parentElement if (parent.dataset && parent.dataset.type == "wnd") { return parent.parentElement } else { return getWndParentElement(parent) } }

这样我们就能够获取到 layout 了。

然后来获取 tab 的类:

function 获取tab原型() { let array = flatten(window.siyuan.layout.centerLayout) let tab = array.filter( item => { return item.headElement } )[0] if(tab){ return tab.constructor } }

没错基本所有的 tab 都有个 headElement,所以可以靠它来判断一下取到的是不是 tab,但是我们在打开思源的时候它不一定就有打开的标签页嘛,所以还要实现一个“如果拿不到结果我就穷追不舍”的函数。

这个时候可能会想到用 setTimeout 之类来搞,但是因为我们不知道什么时候这个函数会返回,所以咋整呢?

还记得之前我们使用过的 asyncawait 嘛?

如果你看了相关的资料的话,可能知道这俩跟 promise 有关。

JavaScript Promise 对象 | 菜鸟教程 (runoob.com)

这里我们就用 promise 来生成一个工具函数:

export function 重复执行直到返回(函数,间隔){ return new Promise((resolve, reject) => { if(!间隔){ 间隔 = 500 } let 工具函数 = setInterval(async() => { try{ let 执行结果 = await 函数() if(执行结果!==undefined){ clearInterval(工具函数) resolve(执行结果) } }catch(e){ clearInterval(工具函数) reject(e) } }, 间隔); }) }

这个函数会以传入的间隔不断执行传入的函数,直到取到的结果不是 undefined 为止。

其实这里有个坑,js 是一种非常神奇的语言嘛:

JS 中的 null 和 undefined,undefined 为啥用 void 0 代替? - 掘金 (juejin.cn)

不过这里我们假设你不会闲到蛋疼去支持 ie,也不会坑到胃疼去改 undefined,先就这么着吧。

然后我们就可以实现实际的功能了:

import { 重复执行直到返回 } from '../util/common.js' let tab export function 展平Layout() { let array = [] function flatten(tree) { if (tree.children && tree.children instanceof Array) { tree.children.forEach( item => { array.push(item) if (item.children && item.children instanceof Array) { flatten(item) } } ) } } flatten(window.siyuan.layout.centerLayout) return array } function 获取tab原型() { let array = 展平Layout() let tab = array.filter( item => { return item.headElement } )[0] if(tab){ return tab.constructor } } tab = await 重复执行直到返回(获取tab原型) class customTab extends tab{ constructor(options){ let {panel,title,icon,data} = options let docIcon = JSON.stringify({type:options.type,data:data}) super({panel,title,icon,docIcon}) this.type = options.type this.data = options.data } save(){ this.docIcon = JSON.stringify({type:this.type,data:this.data}) } } export {customTab as Tab} export {注册自定义tab} from './util/hack.js'
import { 展平Layout } from "../index.js"; let tab注册表= {} export const hackLayout=()=>{ let layouts = 展平Layout() layouts.forEach( layout=>{ if(layout.docIcon&&layout.docIcon.indexOf('type:')){ try { let {type,data} = JSON.parse(layout.docIcon) //已经挂载好的tab咱就不修改它了 if(tab注册表[type]&&!layout.inited){ let customTab = tab注册表[type] let tab = new customTab(data) tab.inited = true layout.parent.addTab(tab) layout.parent.removeTab(layout.id) } }catch(e){ console.error(e) } } } ) } export function 注册自定义tab(类型,构造函数){ tab注册表[类型]=构造函数 hackLayout() } document.addEventListener('mouseover',hackLayout) hackLayout()

这样之后,我们的工作就完成了。。。。。。。。额 三分之一?

实现仿浏览器的 tab 页

首先我们给笔记里面的链接元素绑定一下事件:

import noobApi from '../noobApi/index.js' import browserTab from './browserTab.js' //还记得这几个函数嘛?就在上面呢 let {getWndParentElement,getLayoutByElement} = noobApi.layouts.util document.addEventListener('click',onclick,true) function onclick(e) { if (e.target.dataset && e.target.dataset.type == 'a') { e.preventDefault() e.stopPropagation() //找到窗口对象 let wndElement = getWndParentElement(e.target) //用这个窗口对象拿到wnd let layout = getLayoutByElement(wndElement, siyuan.layout.centerLayout) //然后创建一个新的tab layout.addTab((new browserTab({ url: e.target.dataset.href, title: e.target.dataset.tilte || e.target.innerHTML }))) }

然后来实现一个 tab 类

import noobApi from '../noobApi/index.js' let { Tab, 注册自定义tab } = noobApi.layouts let { 核心api } = noobApi class iframeTab extends Tab { constructor(option) { let panel = ` <div class="fn__flex fn__flex-1 fn__flex-column"> <div class="fn__flex" style="padding: 4px 8px;position: relative"> <span style="opacity: 1" class="block__icon fn__flex-center btnBack" data-menu="true"> <svg><use xlink:href="#iconLeft"></use></svg> </span> <span style="opacity: 1" class="block__icon fn__flex-center btnForward" data-menu="true"> <svg ><use xlink:href="#iconRight"></use></svg> </span> <div class="fn__space"></div> <input class="b3-text-field fn__flex-1"> <span class="fn__space"></span> <span style="opacity: 1" class="block__icon fn__flex-center b3-tooltips b3-tooltips__w reload" aria-label="刷新"> <svg><use xlink:href="#iconRefresh"></use></svg> </span> <span style="opacity: 1" class="block__icon fn__flex-center b3-tooltips b3-tooltips__w debug fn__none" aria-label="反向链接"> <svg><use xlink:href="#iconLink"></use></svg> </span> <div id="searchHistoryList" data-close="false" class="fn__none b3-menu b3-list b3-list--background" style="position: absolute;top: 30px;max-height: 50vh;overflow: auto"></div> </div> <div class="fn__flex fn__flex-1 naive_ifrmeContainer" style="max-height:100%" > <webview class="fn__flex-1" style=" max-height:calc(100% - 200px)" src="${option.url}" data-src="" border="0" frameborder="no" framespacing="0" allowfullscreen="true" allowpopups="true" ></webview > <div class="fn__flex fn__flex-column browserBakclink" style="width:20%"> <div class="block__icons block__icons--active"> <div class="block__logo"> <svg><use xlink:href="#iconLink"></use></svg> 反向链接 </div> <span class="counter listCount fn__none">1</span> <span class="fn__space"></span> <label class="b3-form__icon b3-form__icon--small search__label"> <svg class="b3-form__icon-icon"><use xlink:href="#iconSearch"></use></svg> <input class="b3-text-field b3-text-field--small b3-form__icon-input" placeholder="Enter 搜索"> </label> <span class="fn__space"></span> <span data-type="refresh" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="刷新"><svg class=""><use xlink:href="#iconRefresh"></use></svg></span> <span class="fn__space"></span> <span data-type="min" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="最小化 Ctrl+W"><svg><use xlink:href="#iconMin"></use></svg></span> </div> <div class="backlinkList fn__flex-1"> <ul class="b3-list b3-list--background"> <li class="b3-list--empty">暂无相关内容</li> </ul> </div> </div> </div> </div> </div> ` super({ panel, title: option.title || '', icon: "naiveBrowser", type: "iframeTab", data: { url: option.url, title: option.title || '' } }) this.urlInputter = this.panelElement.querySelector("input") this.urlInputter.value = option.url this.frame = this.panelElement.querySelector("webview") if(!window.require){ this.frame.outerHTML = ` <iframe class="fn__flex-1" style=" max-height:calc(100% - 200px)" src="${option.url}" data-src="" border="0" frameborder="no" framespacing="0" allowfullscreen="true" allowpopups="true" ></iframe > ` this.frame=this.panelElement.querySelector('iframe') this.frame.reload =()=>{ this.frame.setAttribute("src",this.frame.getAttribute('src'))} } this.devButton = this.panelElement.querySelector(".debug") this.minimalButton = this.panelElement.querySelector('[data-type="min"]') this.backlinkListElement = this.panelElement.querySelector(".backlinkList .b3-list--background") this.history={ stack:[this.urlInputter.value], index:0 } //绑事件啊绑事件 this.挂载事件() this.findBacklinks() } 挂载事件() { this.浏览记录 = [] this.urlInputter.addEventListener("change", () => { console.log(this.frame) if (!this.urlInputter.value.startsWith("http://") && !this.urlInputter.value.startsWith("https://") && !this.urlInputter.value.startsWith("/")) { this.urlInputter.value = "https://" + this.urlInputter.value } this.tilte = "" this.frame.setAttribute("src", this.urlInputter.value) this.tilte = this.urlInputter.value document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = this.urlInputter.value this.data = { url: this.urlInputter.value ,title:this.tilte} this.history.stack.push(this.urlInputter.value) this.history.index+=1 this.save() this.findBacklinks() }) document.addEventListener('mousedown',()=>{this.showOverlay()}, true) document.addEventListener('mouseup',()=>{this.hideOverlay()}, true) this.bindButtonEvent() this.bindframeEvent() } hideOverlay(){ this.panelElement.querySelector('.ovelayer').remove() } showOverlay() { if(!this.panelElement.querySelector('.ovelayer')){ let div = document.createElement('div') div.setAttribute('style', `position:absolute;top:0;left:0;height:100%;width:80%`) div.setAttribute('class',"ovelayer") this.panelElement.appendChild(div) } } findBacklinks() { let url = this.urlInputter.value let { backlinkListElement } = this let stmt = `select * from spans` 核心api.sql({ stmt: stmt }, '', (data) => { let backlinkList = data.filter(item => { return /\[[\s\S]*?\]\([\s\S]*?\)/gm.test(item.markdown) && item.markdown.indexOf(`(${url})`) > 0 }) backlinkListElement.innerHTML = "" backlinkList.forEach( item => { backlinkListElement.innerHTML += `<li class="b3-list-item" draggable="true" data-node-id="20201225220955-bdl9x01" data-treetype="backlink" data-type="NodeListItem" data-subtype="u"> <span style="padding-left: 16px" class="b3-list-item__toggle"> <svg data-id="%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%981" class="b3-list-item__arrow fn__hidden"><use xlink:href="#iconRight"></use></svg> </span> <svg class="b3-list-item__graphic popover__block" data-id="20201225220955-bdl9x01"><use xlink:href="#iconListItem"></use></svg> <span class="b3-list-item__text"><a href="siyuan://blocks/${item.block_id}/">${item.content}</a></span> </li>` } ) }) } bindButtonEvent() { this.devButton.addEventListener('mousedown', () => { this.panelElement.querySelector(".browserBakclink").classList.remove("fn__none") this.devButton.classList.add("fn__none") },true) this.panelElement.querySelector('.reload').addEventListener('mousedown', () => { this.frame.reload() },true) this.minimalButton.addEventListener('mousedown',()=>{ this.panelElement.querySelector(".browserBakclink").classList.add("fn__none") this.devButton.classList.remove("fn__none") },true) this.panelElement.querySelector('.btnForward').addEventListener( 'mousedown',()=>{ if(this.history.index<this.history.stack.length-1){ this.history.index+=1 this.urlInputter.value = this.history.stack[this.history.index] this.frame.setAttribute('src',this.urlInputter.value) this.data = { url: this.urlInputter.value ,title:this.tilte} this.save() } } ) this.panelElement.querySelector('.btnBack').addEventListener( 'mousedown',()=>{ if(this.history.index>0){ this.history.index-=1 this.urlInputter.value = this.history.stack[this.history.index] this.frame.setAttribute('src',this.urlInputter.value) this.data = { url: this.urlInputter.value ,title:this.tilte} this.save() } } ) } bindframeEvent() { let { frame, urlInputter } = this this.findBacklinks() frame.addEventListener("dom-ready", () => { fetch(urlInputter.value).then( res => { return res.text() } ).then( text => { let tilte = text.match(/<title>(.*?)<\/title>/); if (tilte) { this.tilte = tilte document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = tilte } } ) }) frame.addEventListener('will-navigate', (e) => { e.preventDefault() const protocol = (new URL(e.url)).protocol if (protocol === 'http:' || protocol === 'https:') { frame.src = (e.url) } }) frame.addEventListener('page-title-updated', async (e) => { this.tilte = e.title document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = e.title }) } } 注册自定义tab("iframeTab", iframeTab) export default iframeTab

这里面其实就是绑事件的活儿了。

有关 webview 标签可以参考这里:

&lt;webview&gt; Tag | Electron (electronjs.org)

啊,可以看到在浏览器里面因为没有这玩意所以我们降了个记用 iframe。

if(!window.require){ this.frame.outerHTML = ` <iframe class="fn__flex-1" style=" max-height:calc(100% - 200px)" src="${option.url}" data-src="" border="0" frameborder="no" framespacing="0" allowfullscreen="true" allowpopups="true" ></iframe > ` this.frame=this.panelElement.querySelector('iframe') //因为iframe根本就没有reload方法嘛 this.frame.reload =()=>{ this.frame.setAttribute("src",this.frame.getAttribute('src'))} }

然后实现一下简单的浏览记录,实际上主要的就是弄个数组把记录放进去(其实这里你可以再抽象一下):

this.panelElement.querySelector('.btnForward').addEventListener( 'mousedown',()=>{ if(this.history.index<this.history.stack.length-1){ this.history.index+=1 this.urlInputter.value = this.history.stack[this.history.index] this.frame.setAttribute('src',this.urlInputter.value) this.data = { url: this.urlInputter.value ,title:this.tilte} //save方法就挂载在customTab的原型上啦 this.save() } } ) this.panelElement.querySelector('.btnBack').addEventListener( 'mousedown',()=>{ if(this.history.index>0){ this.history.index-=1 this.urlInputter.value = this.history.stack[this.history.index] this.frame.setAttribute('src',this.urlInputter.value) this.data = { url: this.urlInputter.value ,title:this.tilte} this.save() } } )

然后因为是在思源里面用嘛,所以加一个反向链接的获取(其实就是查一下表看看有没有包含这个链接的)

findBacklinks() { let url = this.urlInputter.value let { backlinkListElement } = this //没错通过sql接口我们可以查spans表的,不是只有blocks表能查询 let stmt = `select * from spans` 核心api.sql({ stmt: stmt }, '', (data) => { //这一步其实你可以在sql里查 let backlinkList = data.filter(item => { return /\[[\s\S]*?\]\([\s\S]*?\)/gm.test(item.markdown) && item.markdown.indexOf(`(${url})`) > 0 }) backlinkListElement.innerHTML = "" backlinkList.forEach( item => { backlinkListElement.innerHTML += `<li class="b3-list-item" draggable="true" data-node-id="${item.block_id}" data-treetype="backlink" data-type="NodeListItem" data-subtype="u"> <span style="padding-left: 16px" class="b3-list-item__toggle"> <svg data-id="%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%981" class="b3-list-item__arrow fn__hidden"><use xlink:href="#iconRight"></use></svg> </span> <svg class="b3-list-item__graphic popover__block" data-id="20201225220955-bdl9x01"><use xlink:href="#iconListItem"></use></svg> <span class="b3-list-item__text"><a href="siyuan://blocks/${item.block_id}/">${item.content}</a></span> </li>` } ) }) }

这么一通鼓捣之后:效果就像这样了:

浏览器页签

啊,因为这个东西的代码现在已经稍微有点复杂了,你从头实现一遍可能会有些麻烦,可以尝试去 github 下载一下看看吧。

leolee9086/snippets (github.com)

本文使用思源笔记编辑生成,更新于:2022-11-27。

  • 思源笔记

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

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

    24811 引用 • 102069 回帖

相关帖子

欢迎来到这里!

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

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