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

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

一、前情提要

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

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

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

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。

  • 思源笔记

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

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

    23020 引用 • 92599 回帖

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • 你好啊,为什么我粘贴下去不行呢?还是调用外部浏览器,我是纯纯小白,能详细说一下操作流程吗,谢谢

    2 回复
  • 其他回帖
  • 因为这个还没有改成可以直接执行的

  • Nofood

    666,感谢大嘴哥哥制作并分享。而且还写了详细的注释,方便新手学习。同时得益于"代码片段"的功能,小白用户下载粘贴到 工作空间\data\snippets 就能使用了。

    除了内部打开网页还有快速移动、自定义随机题头图等等功能。太赞了 👍

    js 片段.gif

    2 回复
  • leolee 1

    做成插件了

    1 回复
  • 查看全部回帖