项目地址:ccr39/NicoNote: 从零开始编写的浏览器笔记脚本
故事的开始:
在浏览器上学习时时常要记笔记。
在预览网络上已有的各类插件后本人发现,这类插件往往已经长时间失去维护,或需要付费使用。
为什么不自己制作一个有类似功能的脚本呢?
在几乎没有任何理论知识的前提下本人决定实施这个构想。
一、开始前的准备
希望实现的功能
基本功能:
-
能够在网页上编写笔记,并保存在本地。可以在打开网页后显示对应笔记。 -
能够使用 Markdown 语法并实现实时渲染 $\LaTeX$ 。 -
能够导出与导入 Markdown 笔记。 -
能够调整笔记框的大小。
进阶功能:
- 美化笔记框背景。
- 可以根据网页中的某条内容链接到笔记的某一部分。
- 可以根据视频的进度链接到笔记。
- 自定义笔记框。
- (待思考)
实现方案
1、使用 Tampermonkey 编写基本主体脚本。这需要学习一部分网页前端知识。
2、使用开源的纯前端 Markdown 文字编辑器。
3、利用 Git 进行版本控制与脚本的发布。
二、让我们开始吧
尝试了解 Tampermonkey,发现官方文档没有进行汉化,因此翻阅了中文的油猴开发指南。
1、对头部元数据的理解
元数据 | 值 |
---|---|
@name( :zh-CN :zh-TW :en ) |
脚本的名字(简中,繁中,英文)。 |
@namespace |
一般为一个 URL 或自定义的内容,用于脚本管理器在@name 相同的情况下识别唯一的脚本 |
@version |
脚本的版本。 |
@author |
脚本作者。 |
@description( :zh-CN :zh-YW :en ) |
脚本的描述(简中,繁中,英文)。 |
@include |
脚本应该运行的页面, 可以使用通配符, 允许多个标签。 |
@match |
与@include相似,区别是不可以用通配符,更精确因此更为常用,允许多个标签。 |
@exclude |
排除URL,即使它们包含在@include或@match中 ,允许多个标签。 |
@icon @iconURL @defaulticon |
脚本的图标的 URL,会在脚本管理列表上显示。 |
@icon64 @icon64URL |
64*64 的图标文件的 URL ,可能会在选项中缩放。 |
@updateURL |
更新脚本的地址,该URL应该指向一个脚本文件,只需要定期更新该文件即可实现对脚本的更新。注意:只有存在@version标签才会去更新。 |
@downloadURL |
定义脚本的下载地址(一般指的是手动安装脚本时的文件)通常和 @updateURL 指向同一文件较常见。 |
@license |
用于声明脚本的许可证类型。许可证定义了其他人如何使用、修改和分发你的脚本。 |
@grant |
声明脚本会用到的特殊权限或功能。它决定了脚本是否可以使用某些特定的 API,或者是否可以访问用户脚本管理器提供的功能。可以存在多条 |
@require |
指向一个脚本文件,会在本脚本运行前加载并执行 |
@run-at |
定义脚本被注入的时间,与其他脚本处理相反,@run-at 定义了脚本要运行的第一可能时间。这意味着,使用@require 标记的脚本可能会在文档已加载后执行,因为获取所需脚本花费了很长时间。 |
@supportURL |
插件个人支持的地址,用于提交 issue 。 |
@homepage @homepageURL @website @source |
在“选项”页上用于从脚本名链接到给定页的作者主页。请注意,如果@namespace标记以“http://”开头,则其内容也将用于此操作。 |
@connect |
用于声明可以与那些外部的域名进行跨域通信,未声明的域名在脚本中将被禁止访问,允许多个标签。可以使用@connect * 允许所有域名(不推荐,可能带来安全风险)。 |
部分元数据的常用值
①关于@license
常见的许可证类型:
许可证 | 作用/特点 |
---|---|
MIT | 允许几乎任何使用,包括修改、分发和商业用途,只需保留原作者声明。 |
GNU GPL-3.0 | 允许修改和分发,但要求开源修改后的版本,适合需要保持自由传播的软件。 |
Apache-2.0 | 类似于 MIT,但要求保留通知文件,并保护专利权。 |
CC-BY-4.0 | 允许分享和修改,只需署名原作者,常用于非软件内容。 |
Unlicense | 放弃所有版权,允许任何用途。 |
WTFPL | Do What The Fuck You Want To Public License 中文译:你他妈的想干嘛就干嘛公共许可证。是一种不太常用的、极度放任的自由软件许可证。它的条款基本等同于贡献到公有领域。 |
Custom | 自定义声明,允许作者明确指定特殊的版权声明,例如:仅供个人使用,禁止修改传播 。 |
②@grant
的常用值
@grant 值 | 描述 |
---|---|
none |
不授予任何特殊权限,脚本与页面共享相同的全局作用域。 |
GM_addStyle |
向页面动态添加 CSS 样式。 |
GM_getValue |
从脚本的本地存储中获取数据。 |
GM_setValue |
将数据存储到脚本的本地存储中。 |
GM_deleteValue |
删除脚本本地存储中的数据。 |
GM_listValues |
获取所有存储值的键名列表。 |
GM_xmlhttpRequest |
发起跨域的 HTTP 请求(比原生的 XMLHttpRequest 更强大)。 |
GM_download |
下载文件到本地。 |
GM_notification |
在桌面弹出通知。 |
GM_openInTab |
在新标签页中打开一个链接。 |
GM_registerMenuCommand |
向用户脚本菜单添加自定义选项。 |
GM_unregisterMenuCommand |
删除自定义的菜单选项。 |
GM_setClipboard |
将内容复制到系统剪贴板。 |
GM_info |
提供有关用户脚本的信息(只读)。 |
③@run-at
的常用值
@run-at 值 | 描述 |
---|---|
document-start |
脚本会被尽可能快地注入 |
document-body |
当body元素存在是被注入 |
document-end |
当 DOMContentLoaded 事件被触发时或者之后注入 |
document-idle |
当 DOMContentLoaded 事件被触发后被注入 如果没有@run-at标签也是在此时注入 |
2、目前产生的问题(ChatGPT 解答)
Q.1:@downloadURL和@updateURL的区别
在编写油猴(Tampermonkey 或 Greasemonkey)脚本时,
@include
和@match
是用来定义脚本作用范围的元数据注释。它们的作用类似,但有一些关键区别:@include
用途:匹配指定的 URL 模式,让脚本在符合模式的页面上运行。
语法:支持通配符(*),简单易用。
*
:匹配任意字符。示例:
// 脚本会在所有以 "https://example.com/" 开头的页面上运行 @include https://example.com/*
匹配示例:
https://example.com/*
可以匹配:
https://example.com/page1
https://example.com/page2
通配符 *也可以放在中间或末尾,如:
https://*.example.com/*
匹配所有子域名。@match
用途:更严格地匹配 URL 模式,符合 URL 模式规范。
语法:不支持简单的 * 通配符定义所有,而是更精确的匹配规则。
但可以使用通配符
*
匹配子域名或协议。示例:
javascript复制代码// 脚本会在 https://example.com 的所有页面运行 @match https://example.com/*
匹配示例:
https://*.example.com/*
可以匹配:
https://sub.example.com/page
https://another.example.com/page
但不会匹配:
http://example.com
(因为协议不同)。关键区别
特性 @include
@match
灵活性 支持简单通配符,较宽松 严格匹配 URL,符合标准规范 安全性 容易出现误匹配 更严格,减少误匹配的风险 协议要求 可以省略协议 必须指定协议( http://
或https://
)兼容性 旧版脚本可能使用更多 推荐使用,符合现代浏览器扩展规范 使用建议
- 如果需要快速定义作用域,且 URL 规则较简单,可以使用
@include
。- 如果需要更安全、更准确的匹配,建议使用
@match
,特别是在现代浏览器扩展环境下。现代浏览器和 Tampermonkey 都更推荐使用
@match
,因为它的匹配规则符合浏览器扩展的标准。个人总结
两者用来将脚本应用到所有的页面,应分别如下写:
@match *://*/*
@include *
Q.2:什么是 DOMContentLoaded 事件?
DOMContentLoaded
是一个常见的 JavaScript 事件,用于监听 HTML 文档的加载状态。当浏览器解析完所有的 HTML 并构建完成 DOM 树时,就会触发DOMContentLoaded
事件,而无需等待其他资源(如图片、样式表和脚本文件)的加载完成。特点
- 触发时机:当 HTML 文档被完全加载和解析完成时(即 DOM 树构建完成)。
- 不等待其他资源:与
window.onload
不同,DOMContentLoaded
不需要等待图片、样式表、iframe 等资源加载完成后才触发。- 适用于操作 DOM:在该事件触发时,DOM 已经可用,因此适合初始化页面内容或操作 DOM 元素。
用法
监听
DOMContentLoaded
的方法有以下几种:1. 使用
addEventListener
document.addEventListener("DOMContentLoaded", function() { console.log("DOM 已完全加载并解析"); // 可以安全地操作 DOM 元素 document.body.style.backgroundColor = "lightblue"; });
2. 使用旧的
onreadystatechange
早期浏览器中,可以通过
readyState
属性判断文档是否加载完成:document.onreadystatechange = function() { if (document.readyState === "interactive") { console.log("DOM 已完全加载并解析"); } };
3. 直接在脚本末尾执行
如果你的脚本放在文档的底部(
</body>
标签之前),可以不显式监听DOMContentLoaded
,因为此时 DOM 已经解析完成:<!DOCTYPE html> <html> <head> <title>Example</title> </head> <body> <h1>Hello, World!</h1> <script> console.log(document.querySelector("h1").textContent); // "Hello, World!" </script> </body> </html>
DOMContentLoaded
和window.onload
的区别
特性 DOMContentLoaded
window.onload
触发时机 DOM 树构建完成(HTML 已加载并解析)。 页面所有资源(包括图片、样式等)加载完成。 是否等待外部资源 不等待外部资源加载完成。 等待所有外部资源加载完成。 使用场景 初始化 DOM 操作或页面功能。 确保所有资源可用(如图片尺寸)。 示例:两者对比
document.addEventListener("DOMContentLoaded", function() { console.log("DOM 已加载完成,但图片可能还在加载中"); }); window.addEventListener("load", function() { console.log("页面所有资源加载完成,包括图片和样式"); });
浏览器支持
DOMContentLoaded
是一个标准事件,现代浏览器(包括 IE9+)都完全支持。使用建议
- 推荐场景:如果你的脚本只需要操作 DOM 元素(如添加事件监听器或修改内容),优先使用
DOMContentLoaded
,因为它比window.onload
更早触发。- 性能优化:通过监听
DOMContentLoaded
,可以更快地响应用户交互,而无需等待所有资源加载完成。
Q.3:什么是DOM树?
DOM 树(Document Object Model Tree)是浏览器在加载和解析 HTML 文档时,根据 HTML 的层级结构生成的一种树形数据结构。它表示了文档的结构以及每个节点之间的关系,使得 JavaScript 可以通过 DOM API 操作页面内容和结构。
DOM 树的组成
DOM 树由以下几种节点组成,每个节点代表 HTML 文档的一部分:
- 文档节点(Document Node):
- 表示整个 HTML 文档的根节点。
- 通过 JavaScript 的
document
对象访问。- 元素节点(Element Nodes):
- 表示 HTML 的元素标签,例如
<div>
、<p>
。- 元素节点可以嵌套形成层级结构。
- 属性节点(Attribute Nodes):
- 表示 HTML 元素的属性,例如
id="example"
。- 属性是元素节点的一部分,但在 DOM 树中不是子节点,而是直接附属于对应的元素节点。
- 文本节点(Text Nodes):
- 表示元素或属性中的文本内容。
- 文本节点始终是叶节点,没有子节点。
- 注释节点(Comment Nodes):
- 表示 HTML 文档中的注释内容。
DOM 树的结构
以以下 HTML 代码为例:
<!DOCTYPE html> <html> <head> <title>Example</title> </head> <body> <h1 id="header">Hello, World!</h1> <p>Welcome to DOM.</p> </body> </html>
对应的 DOM 树结构如下:
Document └── html ├── head │ └── title │ └── "Example" └── body ├── h1 │ ├── Attribute: id="header" │ └── "Hello, World!" └── p └── "Welcome to DOM."
DOM 树的特点
- 层级关系:
- HTML 文档的嵌套结构会在 DOM 树中反映为父子关系。
- 每个 HTML 元素是一个节点,嵌套关系形成了树状层级。
- 节点类型:
- 可以通过
nodeType
属性查看节点类型,例如:
1
:元素节点3
:文本节点8
:注释节点- 可遍历:
- DOM 树中的节点可以通过 JavaScript 遍历,例如
parentNode
、childNodes
、nextSibling
等。常见操作
获取节点:
const header = document.getElementById("header"); // 获取 h1 元素 const paragraphs = document.getElementsByTagName("p"); // 获取所有 <p> 元素
2. **修改节点内容**: ```javascript const header = document.getElementById("header"); header.textContent = "Hello, DOM!";
添加新节点:
const newDiv = document.createElement("div"); newDiv.textContent = "This is a new div."; document.body.appendChild(newDiv);
删除节点:
const header = document.getElementById("header"); header.remove();
**DOM 树的作用** 1. **页面动态交互**:通过操作 DOM 树,可以动态修改页面的结构和内容。 2. **事件绑定**:JavaScript 事件处理器通过 DOM 树关联 HTML 元素。 3. **样式动态调整**:通过修改 DOM 节点的样式属性实现动态效果。 **总结** DOM 树是 HTML 文档在浏览器中的表示形式,它将 HTML 结构化为可以用 JavaScript 操作的对象模型。熟练掌握 DOM 树和相关操作是前端开发的基础。
2024/12/8
3、Hello World
在头部元数据下方标注了书写代码的地方。
(function() {
'use strict';
// Your code here...
})();
根据文档描述,脚本的书写应该使用JavaScript
。
利用alert()
弹出一个对话框,并输出 HelloWorld,并使用@include *
将其应用到所有页面。
// ==UserScript==
//@name a_simple_test
//@include *
// ==/UserScript==
(function() {
'use strict';
alert('HelloWorld')
// Your code here...
})();
这样,在打开任意网站后,都会弹出一个 HelloWorld 的对话框。
4、熟悉 JavaScript 的语法
在JavaScript 用法 | 菜鸟教程中了解JS的基本用法。
在此出现的问题:
Q.4:之前 HelloWorld 脚本中的alert('HelloWorld')
末尾没有分号,这种写法是否合法?
是合法的!在 JavaScript 中,分号(
;
)是可选的,因为 JavaScript 有一个自动分号插入(Automatic Semicolon Insertion, ASI)的机制。在大多数情况下,即使你省略了分号,JavaScript 引擎也会根据上下文自动插入。解释代码中的
alert('HelloWorld')
在以下代码中:
alert('HelloWorld')
即使没有写分号,JavaScript 引擎仍然能正确解析,因为:
alert('HelloWorld')
是一个独立的语句。ASI 机制会在行末自动插入分号,等效于:
alert('HelloWorld');
因此,这种省略分号的写法是合法的。
什么时候分号是必要的?
尽管 ASI 机制可以避免大部分语法错误,但在某些特定情况下,省略分号可能导致意外行为。例如:
情况 1:行首是
[
或(
的表达式let a = 5 [1, 2, 3].forEach(x => console.log(x))
在这种情况下,JavaScript 会将
[1, 2, 3]
视为数组访问器,而不是独立的数组字面量。解决方法:
let a = 5; [1, 2, 3].forEach(x => console.log(x));
情况 2:返回值与表达式在不同行
function test() { return { value: 42 } } console.log(test()) // undefined
在这种情况下,
return
语句后自动插入了分号,因此实际返回的是undefined
。解决方法:
function test() { return { value: 42 }; }
建议
- 始终使用分号:虽然 JavaScript 的 ASI 机制可以省略分号,但为了代码的可读性和避免潜在的错误,建议显式添加分号。
- 遵循团队规范:如果项目使用了某种代码风格(如 Airbnb 或 Standard),遵循其规范(例如 Standard JS 就不使用分号)。
- 工具辅助:使用代码检查工具(如 ESLint)和格式化工具(如 Prettier)来统一代码风格。
结论
在你的脚本中,省略
alert('HelloWorld')
的分号是合法的,且能正确运行。不过,养成显式添加分号的习惯会更安全、更规范。
5、(番外)为插件创作一个图标
使用 ChatGPT 绘制一个笔记本的图标,将其转换为64*64大小。
使用 SVG 在线编辑器 | 菜鸟工具 将其转化为矢量图形,并使其可以用代码表示出来。
发现 ChatGPT 绘制的图形不是纯色图形,会导致代码过长,故放弃使用 ChatGPT 绘制的图形,转而使用网络上公开的资源,使用 SVG 工具将其稍加修改。
接下来,将其转换为可以直接在@icon
后使用的格式,即 Data URI 格式。使用在线工具在线SVG转DataURL - UU在线工具自动转化。
6、创建一个唤出笔记本的标识
我们需要一个拥有以下功能的标识:
- 在浏览器页面的边框处,并始终位于页面最上方。
- 点击后会唤出笔记本。
- 拥有一些简单的动画效果。
- 随页面滚动时不改变标识的位置。
首先要知道如何使用 JavaScript 创建并插入 HTML 元素。
查阅了HTML DOM insertAdjacentHTML() 方法 - 菜鸟教程,HTML DOM querySelector() 方法 | 菜鸟教程。
document.querySelector("body").insertAdjacentHTML("afterend", "<div><p>Hello world</p></div>");
//表示在<body>元素后插入<div>元素。先用querySelector()获取<body>元素的document,再用insertAdjacentHTML(,)插入元素。
现在我们要将这个元素置于我们希望它存在的位置,为此,我们需要了解如何利用脚本改变元素的 CSS 样式,以及如何书写 CSS。
设置位置查阅的资料:CSS position 属性 | 菜鸟教程、CSS bottom 属性 | 菜鸟教程、使用css,如何把div设置为圆形-百度经验、CSS border 属性 | 菜鸟教程
document.querySelector("body").insertAdjacentHTML("afterbegin",'<div id="nicoBt"><p>helloworld</></div>');
//表示在<body>元素后插入<div>元素
var nicoButton = document.getElementById("nicoBt");
//var 表示定义变量,根据getElementById()利用id找到<div>元素
var nicoButtonStyle = "\
position: fixed;\
right: 1%;\
bottom:80%;\
width: 44px;\
height: 44px;\
border: 2px solid #D3D3D3;\
border-radius: 50%;\
background: rgb(0, 127, 255);\
z-index: 2147483647;\
";
//第一句表示依照浏览器边框定位,right与bottom定义位置,width与height定义长宽,border用于定义边框,border-radius用于实现圆形效果,z-index定义了元素的图层高度,设置一个很大的数字可以保证其置于其他图像上方。
//定义<div>元素的CSS样式,输入字符串时进行换行操作,在每行末尾加上\
//末尾的;可加可不加
nicoButton.setAttribute("style",nicoButtonStyle);
//setAttribute(,)可以改变元素属性,用来改变nicoButton的CSS样式
现在我们把这个
var htmlNicoButton = '\
<div id="nicoBt">\
<img src="图标URL">\
</div>\
';
document.querySelector("body").insertAdjacentHTML("afterbegin",htmlNicoButton);
//以下略
2024/12/9
position: absolute;/*使用相对位置*/
top: 50%;/*竖直方向上一半*/
left: 50%;/*水平方向上一半*/
transform: translate(-50%, -50%);/*x,y方向上均往回移动自身的一般*/
使用上面的方法把图像居中,参考:CSS3 transform 属性 | 菜鸟教程
发现存在<iframe>
时会添加很多的标识,希望只在主页面添加,加上判断:
if (window.self === window.top) {
document.querySelector("body").insertAdjacentHTML("afterend",htmlNicoButton);
}
参考了JavaScript 比较和逻辑运算符 | 菜鸟教程、Window self 属性 | 菜鸟教程
下面我们要让鼠标悬停在标识上方时有一定的反馈效果,这里利用了addEventListener()
设置两个效果,参考了HTML DOM addEventListener() 方法 | 菜鸟教程。
nicoButton.addEventListener("mouseover", function () { //光标移动到图标上,执行函数
nicoButton.style.width = "55px";
nicoButton.style.height = "55px";
//大小改变
nicoButton.style.cursor = "pointer";
//鼠标光标从箭头改编为小手
});
nicoButton.addEventListener("mouseout", function () { //光标移开
nicoButton.style.width = "48px";
nicoButton.style.height = "48px";
nicoButton.style.cursor = "";
//变回
});
7、创建一个容器
创建一个
var nicoNoteDiv = document.createElement("div");//这里用document.creatElement()创建元素。ps.比最开始进步了
nicoNoteDiv.setAttribute("id","nicoNoteDiv");
用点击事件呼出编辑框,并让图标不透明度降低(参考css中让元素消失的方法_transition visibility 无效-CSDN博客):
nicoButton.addEventListener("click", function () {
document.querySelector("html").appendChild(nicoNoteDiv);//用点击事件呼出编辑框,添加在 nicoBt 后面
nicoButton.style.opacity="0.3"; //图标不透明度降低
});
//这里用 elementA.appendChild(elemenetB):表明在A内最后添加B元素
下面我们来设计这个nicoNoteDiv
除了最基本的长宽位置之外,我们希望它可以允许自己通过拖拽调节大小和位置(似乎略有些麻烦)。
2024/12/10
2024/12/11 去医院了,还被坑了,详情请见:只身陷入心理治疗骗局!!深扒“SCIO全科生物反馈测疗仪”与“心理AI智能检测”底裤。 - 知乎
①拖拽调节位置
为了使其可以调节自身位置和大小,我们应该从两方考虑:
- CSS 角度,在鼠标靠近边缘时,光标应该发生变化。
- JS 角度,根据鼠标光标的坐标改变
的大小。为了实现上述操作,我们考虑把存放笔记本主体的
和调整大小的分开,外层仅仅实现拖拽功能,内层为主体。Q.5:在查询资料时发现的
() => {}
是什么意思?(e) => {}
是 JavaScript 中的箭头函数语法,是定义函数的一种简洁方式。它可以用来代替传统的function
声明。语法
(param1, param2, ...) => { // 函数体 }
分解理解
(e) => {}
- 箭头函数:
=>
是箭头函数的核心,表示这是一个函数。
- **参数
(e)
**:
e
是箭头函数的参数。它通常代表事件对象,尤其是在事件处理程序中,比如addEventListener
。
- **函数体
{}
**:
{}
包含函数执行的代码。
完整例子
以下是一个常见的使用场景:
传统函数写法
document.addEventListener("click", function(e) { console.log("鼠标点击了页面", e); });
箭头函数写法
document.addEventListener("click", (e) => { console.log("鼠标点击了页面", e); });
简化规则
参数简写:
如果只有一个参数,可以省略括号 ()
e => console.log(e);
如果没有参数或有多个参数,则必须使用括号。
() => console.log("无参数"); (x, y) => console.log(x + y);
函数体简写:
- 如果函数体只有一行代码,且直接返回值,可以省略 {} 和 return
x => x * x; // 返回 x 的平方
优势
- 简洁:语法更短,易读。
- 自动绑定上下文:箭头函数不会创建自己的
this
,而是继承自外层作用域。
例子:箭头函数绑定上下文
function Example() { this.name = "箭头函数示例"; document.addEventListener("click", () => { console.log(this.name); // 指向 Example 实例 }); } new Example();
对比传统函数
特性 箭头函数 普通函数 语法简洁 是 否 this
指向继承外层作用域 动态绑定,取决于调用位置 使用场景 回调函数、简单逻辑 需要动态绑定 this
的复杂场景箭头函数非常适合在事件处理、数组操作等回调中使用,但如果需要灵活绑定
this
,普通函数可能更适合。先实现较简单的拖拽:
首先我们应该想办法获取鼠标的光标坐标,可以利用
addEventListener
监听mousemove
事件,再将生成的MouseEvent
对象传入参数,提取clientX
和clientY
的信息。关于这一过程的具体原理 Chat GPT 回答如下:
在以下代码中:
document.addEventListener('mousemove', (event) => { const { clientX, clientY } = event; console.log(`鼠标X坐标: ${clientX}, 鼠标Y坐标: ${clientY}`);//console.log()用于把信息输出到工作台。${}表示在字符串中插入函数或变量。 });
事件参数
event
的捕捉和传递机制:- 事件监听器的注册:
document.addEventListener('mousemove', callback)
将事件监听器绑定到document
上,用于监听mousemove
事件。- 当用户移动鼠标时,浏览器会触发
mousemove
事件。
- 事件对象的生成:
- 浏览器在触发事件时,会生成一个
Event
对象。对于mousemove
事件,生成的对象是一个MouseEvent
实例,它继承了Event
的所有属性和方法,同时还包含鼠标相关的属性,比如clientX
和clientY
。
- 浏览器在触发事件时,会生成一个
- 事件对象的传递:
- 当事件触发时,浏览器会将生成的
MouseEvent
对象作为参数传递给监听器中定义的回调函数(event) => { ... }
。
- 当事件触发时,浏览器会将生成的
- 解构赋值:
- 在回调函数中,通过
const { clientX, clientY } = event;
使用了解构赋值,从event
对象中提取clientX
和clientY
属性值,代表鼠标指针相对于视口左上角的 X 和 Y 坐标。
- 在回调函数中,通过
- 输出结果:
- 提取到的坐标值会被插入到字符串中,并通过
console.log
输出到控制台。
- 提取到的坐标值会被插入到字符串中,并通过
总结: 浏览器自动生成事件对象并将其作为参数传递给事件回调函数。你只需定义回调函数的参数名称(这里是
event
),浏览器会负责传递这个对象。MouseEvent实例中还有那些属性?
MouseEvent
是由浏览器生成的事件对象,专门用于表示与鼠标操作相关的事件(例如click
、mousemove
、mousedown
等)。它继承自UIEvent
,并进一步继承自Event
。MouseEvent
实例包含许多有用的属性,用于描述鼠标事件的详细信息。以下是
MouseEvent
的主要属性:鼠标位置信息
- **
clientX
**:鼠标指针相对于浏览器视口(窗口)左边缘的 X 坐标。 - **
clientY
**:鼠标指针相对于浏览器视口(窗口)顶部的 Y 坐标。 - **
screenX
**:鼠标指针相对于屏幕左边缘的 X 坐标。 - **
screenY
**:鼠标指针相对于屏幕顶部的 Y 坐标。 - **
pageX
**:鼠标指针相对于整个文档左边缘的 X 坐标(包括页面滚动距离)。 - **
pageY
**:鼠标指针相对于整个文档顶部的 Y 坐标(包括页面滚动距离)。 - **
offsetX
**:鼠标指针相对于事件目标元素左边缘的 X 坐标。 - **
offsetY
**:鼠标指针相对于事件目标元素顶部的 Y 坐标。
鼠标按键信息
button
:- 触发事件的鼠标按键的代码:
0
:主按钮(通常是左键)1
:中间按钮(滚轮)2
:次按钮(通常是右键)
buttons
:- 一个按位标记的数字,表示当前按下的鼠标按钮:
1
:左键2
:右键4
:中键 (可以组合,如同时按下左键和右键时值为3
)
- 一个按位标记的数字,表示当前按下的鼠标按钮:
键盘修饰键状态
这些属性表示事件发生时是否按下了修饰键:
- **
altKey
**:true
表示Alt
键被按下。 - **
ctrlKey
**:true
表示Ctrl
键被按下。 - **
metaKey
**:true
表示Meta
键被按下(Mac 上为Command
键)。 - **
shiftKey
**:true
表示Shift
键被按下。
拖放和相关元素
relatedTarget
:- 对于某些事件(如
mouseover
和mouseout
),表示鼠标从哪个元素移入/移出。
- 对于某些事件(如
target
:- 事件的目标元素。
currentTarget
:- 当前正在处理事件的元素。
其他信息
- **
movementX
**:自上一次mousemove
事件以来,鼠标指针在 X 轴上的移动距离。 - **
movementY
**:自上一次mousemove
事件以来,鼠标指针在 Y 轴上的移动距离。 - **
detail
**:事件的点击次数(适用于click
和dblclick
等事件)。
方法
getModifierState(key)
:检查指定的修饰键(如Alt
、Shift
)是否被按下。
继承自
UIEvent
和Event
的属性- **
type
**:事件类型(如"click"
、"mousemove"
)。 - **
target
**:事件触发的目标元素。 - **
bubbles
**:true
表示事件会冒泡。 - **
cancelable
**:true
表示事件可以被取消。 - **
defaultPrevented
**:true
表示默认操作已被阻止。 - **
timeStamp
**:事件触发时的时间戳。
示例
document.addEventListener('mousemove', (event) => { console.log('clientX:', event.clientX); console.log('clientY:', event.clientY); console.log('screenX:', event.screenX); console.log('button:', event.button); console.log('altKey pressed:', event.altKey); });
这些属性可以帮助开发者获取鼠标的位置信息、按键状态和修饰键等详细信息,以便实现复杂的鼠标交互功能。
Q.6:什么是解构赋值?
在
const { clientX } = event;
和const clientX = event.clientX;
中,**clientX
** 最终的效果是等价的,它们都会从event
对象中提取clientX
的值并赋值给一个同名变量。但它们的语法和解构方式不同,具体区别如下:
解构赋值 (
const { clientX } = event;
)这是 ES6 解构赋值 的语法,用于从对象中提取属性并赋值给同名变量。
const { clientX } = event;
等价于:
const clientX = event.clientX;
优点:
可以同时提取多个属性:
const { clientX, clientY } = event;
上述写法相当于:
const clientX = event.clientX; const clientY = event.clientY;
语法更加简洁,适合处理多个属性时使用。
普通赋值 (
const clientX = event.clientX;
)这是更传统的方式,逐个访问对象属性并赋值。
const clientX = event.clientX;
优点:
- 更直观,适合只提取单个属性时。
总结
两种写法最终效果是等价的,区别在于语法形式和适用场景:
- 如果只需要提取单个属性,使用
const clientX = event.clientX;
和const { clientX } = event;
都可以。 - 如果需要提取多个属性,解构赋值更简洁且可读性更好。
例如:
// 解构赋值 const { clientX, clientY } = event; // 传统方式 const clientX = event.clientX; const clientY = event.clientY;
这里遇到了新的问题,最初设置
依靠的是百分比作为位置信息,在js操作中利用的是px,导致操作繁琐。现在尝试把之前的代码进行一定的修改,在一番折腾后写出了以下代码:
let offsetX = 0; let offsetY = 0; let nicoDivX =0; let nicoDivY =0; let ifDragging = false; //充当一个布尔变量,用于判断是否在抓取移动状态,let为局部变量 nicoNoteDiv.addEventListener("mousedown",(e) => {//"mousedown"即监测鼠标点下的事件。 ifDragging = true; nicoNoteDiv.style.cursor = "move"; offsetX = e.clientX; offsetY = e.clientY; //记录初始鼠标坐标(相对于左上角) nicoDivY = parseInt(nicoNoteDiv.style.top); nicoDivX = parseInt(nicoNoteDiv.style.right); //获取div的位置parseInt()会将字符串转化为数字(忽略最后的非数字部分) e.preventDefault(); //用来禁止鼠标拖拽时选中的功能 nicoNoteDiv.addEventListener('mousemove',(e)=>{if (ifDragging){nicoDragging(e)}}); //通过检测是否拖拽来启用拖拽函数 }); function nicoDragging(e) {//定义一个Dragging函数 语法:function 函数名(参数) {} const { clientX, clientY } = e; //获取当前鼠标坐标 nicoNoteDiv.style.top = `${nicoDivY+clientY-offsetY}px`; //${}表示在字符串中插入函数或变量,但要确保使用反引号`Code` nicoNoteDiv.addEventListener("mouseup",() =>{ifDragging = false;}); //松开鼠标,停止拖拽 };
但是事情并没有那么简单,在松开鼠标后这个玩意还在动,原因如下:
JavaScript 中,监听器(事件监听器)只要没有被明确移除或停止,就会一直监听事件。
当你使用诸如
addEventListener
方法在 DOM 元素上绑定一个事件监听器时,这个监听器会一直处于活动状态,直到满足以下条件之一:- 手动移除监听器:
可以使用removeEventListener
方法来移除监听器。例如:
const button = document.getElementById('myButton'); function handleClick(event) { console.log('Button clicked!'); } // 添加监听器 button.addEventListener('click', handleClick); // 移除监听器 button.removeEventListener('click', handleClick);
元素被移除:
如果绑定了监听器的 DOM 元素被从 DOM 中移除,监听器也会随之停止。一次性监听器:
如果在添加监听器时指定了{ once: true }
,监听器会在首次触发后自动移除。例如:
button.addEventListener('click', handleClick, { once: true });
父监听器与子监听器的关系
如果你指的是在父元素上添加的事件监听器(比如通过事件冒泡机制监听子元素的事件),这种父监听器也会一直监听,除非明确移除。事件冒泡意味着子元素触发的事件会逐层向上传递到父元素,因此父监听器会接收到事件。比如:
const parent = document.getElementById('parent'); // 父元素监听器 parent.addEventListener('click', (event) => { console.log('Parent clicked!', event.target); });
无论是直接点击父元素,还是点击子元素,父元素的监听器都会被触发,除非:
- 你使用
stopPropagation()
来阻止冒泡; - 或者通过
removeEventListener
手动移除父监听器。
总结
- 监听器会持续监听事件,直到手动移除或绑定的 DOM 元素被移除。
- 如果没有停止或移除,父监听器会一直监听其范围内的事件冒泡。
也就是说,由于
mousemove
在mousedown
内导致mousedmove
一直没法停止。把:nicoNoteDiv.addEventListener('mousemove',(e)=>{if (ifDragging){nicoDragging(e)}}); nicoNoteDiv.addEventListener("mouseup",() =>{ifDragging = false;});
插在外面就行了。
最终成品:
let offsetX = 0; let offsetY = 0; let nicoDivX =0; let nicoDivY =0; let ifDragging = false; //充当一个布尔变量,用于判断是否在抓取移动状态,let为局部变量 nicoNoteDiv.addEventListener("mousedown",(e) => {//"mousedown"即监测鼠标点下的事件。 ifDragging = true; offsetX = e.clientX; offsetY = e.clientY; //记录初始鼠标坐标(相对于左上角) nicoDivY = parseInt(nicoNoteDiv.style.top); nicoDivX = parseInt(nicoNoteDiv.style.right); //获取div的位置parseInt()会将字符串转化为数字(忽略最后的非数字部分) e.preventDefault(); //用来禁止鼠标拖拽时选中的功能 }); nicoNoteDiv.addEventListener('mousemove',(e)=>{ if(ifDragging){ const { clientX, clientY } = e; //获取当前鼠标坐标 nicoNoteDiv.style.top = `${nicoDivY+clientY-offsetY}px`; nicoNoteDiv.style.right = `${nicoDivX-clientX+offsetX}px`; //${}表示在字符串中插入函数或变量,但要确保使用反引号`Code ` } }); nicoNoteDiv.addEventListener("mouseup",() =>{ifDragging = false;}); //松开鼠标,停止拖拽
考虑到这个玩意可以应用到之前那个图标上,我们把它封装成一个函数。
由于光标太快会划出
element
,有时候mousedown
事件会停止监听,改变策略把mousemove
和mouseup
监听对象改为document
,即:document.addEventListener()
。这里有个新问题:
Q.7如何在拖拽的同时不触发点击?
方法一:通过
mousemove
判断是否发生拖拽可以在
mousedown
和mousemove
中记录鼠标的移动距离,并在mouseup
时判断是否是一次有效的拖拽操作。如果移动的距离超过一定阈值,就认为是拖拽而非点击。let isDragging = false; const element = document.querySelector("#draggable"); element.addEventListener("mousedown", (e) => { isDragging = false; // 重置拖拽状态 const startX = e.clientX; const startY = e.clientY; const onMouseMove = (e) => { if (Math.abs(e.clientX - startX) > 5 || Math.abs(e.clientY - startY) > 5) { isDragging = true; } }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); element.addEventListener("click", (e) => { if (isDragging) { e.preventDefault(); // 如果是拖拽,不触发点击 } });
方法二:使用
drag
和dragstart
事件如果使用 HTML5 的拖放 API,可以利用
drag
和dragstart
来区分拖拽和点击。let isDragging = false; const element = document.querySelector("#draggable"); element.addEventListener("dragstart", () => { isDragging = true; // 标记为拖拽 }); element.addEventListener("click", (e) => { if (isDragging) { e.preventDefault(); // 阻止点击事件 isDragging = false; // 重置状态 } });
方法三:直接在
click
中判断拖拽状态如果你的拖拽实现中没有其他复杂的状态,也可以简单地在
click
事件中检查。let isDragging = false; element.addEventListener("mousedown", () => { isDragging = false; }); element.addEventListener("mousemove", () => { isDragging = true; // 如果发生移动,标记为拖拽 }); element.addEventListener("mouseup", () => { setTimeout(() => (isDragging = false), 0); // 延迟重置状态 }); element.addEventListener("click", (e) => { if (isDragging) { e.preventDefault(); } });
方法四:CSS
pointer-events
阻止点击如果拖拽操作是通过 CSS 结合 JavaScript 实现,可以在拖拽时临时禁用
pointer-events
。const element = document.querySelector("#draggable"); element.addEventListener("mousedown", () => { element.style.pointerEvents = "none"; // 禁止点击 }); element.addEventListener("mouseup", () => { setTimeout(() => { element.style.pointerEvents = "auto"; // 恢复点击 }, 0); });
总结
最推荐的方法是 方法一,结合鼠标的移动距离判断是否是拖拽,因为它适用于大多数拖拽实现场景,且与 HTML5 拖放 API 或 CSS 的依赖性较低。
这里采用方法四,而 mouseup 的监听对象一定为 document 才有效,最后将right改为更易操作的的left,封装后的函数如下:
function letWeDrag(dragDiv){ let offsetX = 0; let offsetY = 0; let nicoDivX =0; let nicoDivY =0; let ifDragging = false; //充当一个布尔变量,用于判断是否在抓取移动状态,let为局部变量 dragDiv.addEventListener("mousedown",(e) => {//"mousedown"即监测鼠标点下的事件。 ifDragging = true; offsetX = e.clientX; offsetY = e.clientY; //记录初始鼠标坐标(相对于左上角) nicoDivY = parseInt(dragDiv.style.top); nicoDivX = parseInt(dragDiv.style.left); //获取div的位置parseInt()会将字符串转化为数字(忽略最后的非数字部分) e.preventDefault(); //用来禁止鼠标拖拽时选中的功能 }); document.addEventListener('mousemove',(e)=>{ if(ifDragging){ const { clientX, clientY } = e; //获取当前鼠标坐标 dragDiv.style.top = `${nicoDivY+clientY-offsetY}px`; dragDiv.style.left = `${nicoDivX+clientX-offsetX}px`; //${}表示在字符串中插入函数或变量,但要确保使用反引号`Code` dragDiv.style.pointerEvents = "none"; //禁止单击,防止拖拽同时点击。 } }); document.addEventListener("mouseup",() =>{ ifDragging = false; dragDiv.style.pointerEvents = "auto"; // 恢复点击 }); //松开鼠标,停止拖拽 }
由于将right换用left,为了使图标继续在右侧,我们应该获取屏幕的宽度。
Q.8:如何获取屏幕的宽度?
在 JavaScript 中,可以使用以下方法获取屏幕的 像素信息:
1. 获取屏幕的宽度和高度(
window.screen
)通过
window.screen
对象,可以获取屏幕的物理分辨率:console.log("屏幕宽度:" + window.screen.width + "px"); console.log("屏幕高度:" + window.screen.height + "px");
window.screen.width
:屏幕的宽度(像素)。window.screen.height
:屏幕的高度(像素)。
2. 获取可用工作区域的尺寸
window.screen.availWidth
和window.screen.availHeight
用于获取屏幕除去任务栏或工具栏后的可用尺寸:javascript复制代码console.log("可用宽度:" + window.screen.availWidth + "px"); console.log("可用高度:" + window.screen.availHeight + "px");
3. 获取浏览器窗口的尺寸
通过
window.innerWidth
和window.innerHeight
可以获取浏览器窗口的内容区域大小(不包含工具栏或边框):javascript复制代码console.log("窗口宽度:" + window.innerWidth + "px"); console.log("窗口高度:" + window.innerHeight + "px");
4. 获取设备像素比(
devicePixelRatio
)window.devicePixelRatio
返回设备像素比(DPR),表示物理像素与 CSS 像素的比值:console.log("设备像素比:" + window.devicePixelRatio);
例如:
- 如果
devicePixelRatio = 2
,表示 1 个 CSS 像素对应 2 个物理像素。 - 通常高分辨率设备(如 Retina 屏幕)会有较高的 DPR 值。
5. 获取页面滚动的像素位置
通过
window.scrollX
和window.scrollY
可以获取页面的水平和垂直滚动位置:console.log("水平滚动位置:" + window.scrollX + "px"); console.log("垂直滚动位置:" + window.scrollY + "px");
6. 获取元素的大小和位置
如果要获取某个 HTML 元素的宽度和高度,可以使用
getBoundingClientRect()
:const element = document.querySelector("#myElement"); const rect = element.getBoundingClientRect(); console.log(`元素宽度:${rect.width}px, 高度:${rect.height}px`); console.log(`元素位置:左=${rect.left}px, 上=${rect.top}px`);
总结
- 屏幕尺寸:
window.screen.width
和window.screen.height
- 可用区域:
window.screen.availWidth
和window.screen.availHeight
- 浏览器窗口:
window.innerWidth
和window.innerHeight
- DPR:
window.devicePixelRatio
- 滚动位置:
window.scrollX
和window.scrollY
这里我们用到
window.innerWidth
就可以了。这里我们还要保证窗口调节时,它的位置能一同调节,用以下函数,达到这一效果:
window.top.addEventListener('resize', () => {//监听顶层窗口调节大小事件 const element = document.getElementById("nicoBt"); const elementWidth = element.offsetWidth; const viewportWidth = window.innerWidth; element.style.left = (viewportWidth - elementWidth-10) + "px"; nicoNoteDiv.style.top = `${parseInt(nicoButton.style.top)}px`; nicoNoteDiv.style.left = `${parseInt(nicoButton.style.left)-parseInt(nicoNoteDiv.style.width)}px`; });
2024/12/12
②拖拽调节大小
只需要在CSS中添加:
resize: both;/*允许元素在x、y两个方向上调整大小*/ overflow: auto;/*显示滚动条*/
就可以实现简单的调整大小,参考css resize-CSDN博客。
注意的是,能正常调整的条件是:CSS样式中,元素的位置描述使用 top 与 left。
但是我们要让它有最大最小的限制,思路是通过监听鼠标,通过JS来限制大小。
document.addEventListener("mouseup",()=>{ if(parseInt(nicoNoteDiv.style.width)<200){ nicoNoteDiv.style.width="200px";}; if(parseInt(nicoNoteDiv.style.height)<40){ nicoNoteDiv.style.height="40px";} });
③组合起来!
由于这两者判定位置重合,因此我们应该使用一定的嵌套。
这时我们就要对 UI 进行一定的规划:也就是流出上方一条允许拖拽,其余部分无法拖拽。
让外层元素调节大小,内层元素负责拖动,这样一个东西就做好了!
8、引入 Vditor
JS 与 CSS 的引入
终于捣鼓完这个框框了,接下来我们要在这个容器中引入文本编辑器。
使用开源项目 Vditor 来作为文本编辑器 ,根据开发者文档尝试在 HTML 中引入 vditor 的 JS 与 CSS 。
<!-- ⚠️生产环境请指定版本号,如 https://unpkg.com/vditor@x.x.x/dist... --> <link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" /> <script src="https://unpkg.com/vditor/dist/index.min.js"></script>
把这行代码写入
中:document.querySelector("head").insertAdjacentHTML("afterbegin",headVditorCode);
Q.9:之前讲过用
@require
加载脚本,这里可以那样做吗?可以,且更推荐使用
@require
的方式,因为如果你在插入资源后立即使用 Vditor,可能会因为资源未加载完成而报错,并且动态插入 HTML 字符串不够直观,可能会增加代码阅读和维护的难度。但是**
@require
只能用来加载 JS 而不能用于 CSS **使用GM_addStyle
可以解决这一问题。// ==UserScript== // @name Vditor Integration with CSS // @namespace http://tampermonkey.net/ // @version 1.0 // @description Integrate Vditor // @author Your Name // @match *://*/* // @require https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js // @grant GM_addStyle // ==/UserScript== // 使用 GM_addStyle 插入 CSS 样式 GM_addStyle(` @import url('https://cdn.jsdelivr.net/npm/vditor/dist/index.css'); `);
如果你坚持使用动态插入方式,建议优化为分步加载,避免时序问题:
// 动态加载 CSS var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://unpkg.com/vditor/dist/index.css'; document.head.appendChild(link); // 动态加载 JS var script = document.createElement('script'); script.src = 'https://unpkg.com/vditor/dist/index.min.js'; script.onload = function () { // 确保 Vditor 加载完成后再初始化 new Vditor('editor', { height: 300, placeholder: 'Write something...', }); }; document.head.appendChild(script);
这种方式更易维护,且避免 HTML 字符串操作的潜在问题。
Vditor 的初始化
为了理解如何引入 Vditor ,我需要花一点时间来对文档进行理解:
首先,Vditor 定义了一个名为
Vditor
的类。这个类包含若干属性,最主要的是 id 和 options。
我们首先要创建一个这个”类“的”实例“,可以理解为所谓”实例“就是一个这个”类“的变量。
创建实例的语法为:
new 类名(参数1,参数2。。。)
在这个
Vditor
就应该写成:new Vditor(id,options,....)
这个id是一个字符串,而这个 options 里面包含了许多属性,我们把这些属性用大括号括起来。
另外 options 还有诸如
options.toolbar
options.counter
的子属性他们的书写方式文档中也有写。此外,我们这个类中还有一些”方法“
methods
供我们操作这些实例。第一次尝试引入:
const vditor = new Vditor("nicoVditor",{ placeholder: 'Write something here...', toolbar: ['emoji', 'headings', 'bold', 'italic', 'strike', 'link', 'list'], })
显示 nicoVditor Not found ,推断是初始化代码在 nicoVditor 被添加之前就已经运行,通过调整代码逻辑,成功解决该问题。
再次运行显示:
GET https://unpkg.com/vditor@3.10.8/dist/js/i18n/zh_CN.js net::ERR_ABORTED 404 (Not Found)
怀疑是脚本无权访问此文件,在元属性中添加
// @require https://unpkg.com/vditor@3.10.8/dist/js/i18n/zh_CN.js
成功运行。接下来就是简单的调试一下!
2024/12/13
9、尝试与修复 CDN 被 CSP 拦截的问题(待解决)
发现无法在 Github 中初始化,询问AI后得到以下解答:
这个错误消息表示,浏览器拒绝加载样式表
https://cdn.jsdelivr.net/npm/vditor/dist/index.css
,因为它违反了当前页面的内容安全策略(Content Security Policy,CSP)指令。具体来说,CSP 指令style-src 'unsafe-inline' github.githubassets.com
限制了可以加载样式表的来源,而jsdelivr.net
不在允许的来源列表中。可能导致该错误的原因
- 严格的内容安全策略:GitHub 页面设置了严格的 CSP,限制了可以加载样式表的来源,以提高安全性。
- 外部样式表来源不被允许:当前 CSP 只允许从
github.githubassets.com
加载样式表,而不允许从jsdelivr.net
加载。
搜索方案,发现某些项目可以绕过这一限制:guansss/webpack-monkey:一个 webpack 插件,用于使用现代工作流程开发您的用户脚本,具有 HMR、元生成等功能。
使用 webpack-monkey
用 npm 或 yarn(install 替换为 add) 均可。(超时挂梯子,给命令台走代理)
对该项目进行安装与初始化:
npm init -y npm install webpack webpack-cli webpack-dev-server webpack-monkey --save-dev
创建以下目录:
. ├── dist │ └── hello.user.js ├── src │ ├── index.js │ └── meta.js ├── webpack.config.js └── package.json
编写脚本:
- 在
src/index.js
中编写脚本逻辑(不包含元数据)
GM_log("Hello world!"); if (module.hot) { module.hot.monkeyReload(); }
- 在
src/meta.js
中定义脚本的元数据
module.exports = { name: "Hello world", version: "1.0.0", match: ["*://example.com/"], };
- 在
webpack.config.js
中配置webpack-monkey
const path = require("path") const { monkey } = require("webpack-monkey") // module.exports = monkey({ entry: './src/index.js',//输入文件 output: { path: path.resolve(__dirname, "dist"),//输出目录 }, })
配置构建脚本
在
package.json
中添加构建脚本,相当于添加了命令台指令:{ "scripts": { "dev": "webpack serve --mode development",//调试工具 "build": "webpack --mode production"//生成工具 } }
生成用户脚本
npm build
会在 dist 中生成用户脚本。
尝试绕过CSP
webpack-monkey 的原理是:通过将需要外部加载的脚本与CSS样表与油猴脚本一起打包到一个文件,从而解决了 CSP 的问题。
首先,安装用于打包 CSS 与 JS 的 loader:
npm install css-loader style-loader --save-dev#用于加载CSS npm install script-loader --save-dev#用于加载JS
在 Webpack 配置中,添加相关配置:
const path = require("path"); const { monkey } = require("webpack-monkey"); const TerserPlugin = require("terser-webpack-plugin"); // 引入 terser-webpack-plugin module.exports = monkey({ entry: './src/index.js',//输入文件 output: { path: path.resolve(__dirname, "dist"),//输出目录 }, module: { rules: [ { test: /\.css$/i, // 匹配所有 .css 文件 use: ["style-loader", "css-loader"], // 使用 style-loader 和 css-loader 处理 }, { test: /external-script\.js$/, // 匹配外部 JS 脚本 use: "script-loader", // 使用 script-loader 打包 }, ], }, });
将脚本和CSS下载到本地后,在 index.js 文件中引入本地的脚本:
import "./CSS/index.css"; import "./JS/index.min.js";
运行后生成了打包后的脚本,但无法运行,反思后认为原因如下:
webpack打包的js中又通过字符串操作引用了新的URL,而新的URL仍然不能绕过CSP。
并且不同的 JS 之间的相互依赖限制了运行顺序,运行顺序不当也会出现这种情况。
替代的解决方案
- 手动追踪所有的URL将他们修改并打包。缺点是耗时长且可维护性极低。(不可行)
- 使用拓展程序来手动禁用 CSP。
无法加载的其他未知原因与局限性分析
在某些页面不是因为 CSP 而无法初始化 Vditor (经过测试,所有搜索引擎的“搜索结果”页面都不能正常载入)原因暂时未知。
这类问题暴露出了油猴作为脚本运行器的局限性,或许我们可以开发浏览器插件,而不是油猴脚本。
总结就是,关于这个问题:
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
あたしは失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した
失敗した失敗した失敗した失敗した失敗した2024/12/13
10、储存编辑的文本
参考:HTML5 Web IndexedDB 数据库 | 菜鸟教程
Vditor 提供了把文件导出到本地的功能,但不能实现将笔记保存在当前网页的功能。
由于浏览器的网页存在严格的沙箱机制,因此我们无法利用浏览器脚本操作本地文件,自然也就不能利用脚本把文件保存在本地。
但是浏览器提供了 IndexedDB 这一机制,允许我们储大量结构化数据。它是异步的,可以存储较大的数据对象,适合需要存储大量数据或进行复杂查询的场景。因此我们甚至可以实现自动保存,历史版本管理的功能。
获取文本内容
Vditor 提供的工具中为我们提供了获取文本内容的 API:
方法 说明 exportJSON(markdown: string) 根据 Markdown 获取对应 JSON getValue() 获取 Markdown 内容 getHTML() 获取 HTML 内容 创建 IndexedDB
const request = indexedDB.open("VditorHistoryDB", 1);//创建版本号为1的IndexedDB let db;
初始化:
request.onupgradeneeded = e => { db = e.target.result; //表示成功打开的数据库实例。 if (!db.objectStoreNames.contains("history")) { db.createObjectStore("history", { keyPath: "id" }); //如果没有创建对象储存,则创建一个"history"对象储存。 //而每一条主字段的标识为id,也就是我们可以在之后给每个字段添加id,作为标识。IndexedDB 会根据这个属性对记录进行索引。 } };
这里的
onupgradeneeded
代表数据库初次创建或版本号更新时执行函数。**
db.objectStoreNames
**:一个类似数组的对象,列出了当前数据库中所有已存在的对象存储(表)。而**
contains("history")
**:检查是否已存在名为"history"
的对象存储。如果数据库已经创建了,则:
request.onsuccess = e => { db = e.target.result; };
onsuccess
表示成功加载执行函数。出现的问题:
onsuccess
无法正确触发既不出发
onsuccess
也不触发onerror
,只能触发onupgradeneeded
,且控制台没有输出,原因暂时未知,所以无法修复。这里采用替代方案每次进入都改变版本号来触发
onupgradeneeded
。将内容保存至数据库
这里使用数据库实例的
transaction()
方法创建一个事务:const transaction = db.transaction(storeNames, mode);
**
storeNames
**:- 可以是一个字符串(单个对象存储的名称)或数组(多个对象存储的名称)。
- 决定事务可以操作哪些对象存储。例如,
"history"
表示事务将操作名为"history"
的存储。
**
mode
**:- 事务的模式,定义了可以执行的操作:
"readonly"
:只读事务,只允许读取数据。"readwrite"
:读写事务,允许读取和修改数据。
通过
transaction.objectStore()
获取特定的存储,操作其中的数据。function saveContent() { const content = nicoVditor.getValue();//获取 Markdown 内容 const timestamp = Date.now(); // 时间戳作为 ID const transaction = db.transaction("history", "readwrite");//创建一个操作 "history" 存储的读写事务。 const store = transaction.objectStore("history");// 获取 "history" 对象存储 store.put({ id: timestamp, content: content, savedAt: new Date().toLocaleString() }); //向"history" 存储中放入一个id为时间戳,内容为 Markdown 内容,保存的时间为当前时间的内容。 //savedAt 是一个字段,用于记录保存的时间。而new Date().toLocaleString() 是 JavaScript 的方法,返回当前时间的本地化格式(例如 2024/12/15 10:30:45) transaction.oncomplete = () => { alert("内容已保存!"); }; transaction.onerror = (event) => { console.error("保存失败", event.target.error); }; }
最后在工具栏插入一个按钮:
{ name: 'nicoSave', tipPosition: 's', tip: '保存到浏览器', className: 'right', icon: ``, click () {saveContent()},}
出现的问题:
getValue()
未定义(已解决)原因是无法引入
method
脚本文件,引入后问题依旧存在,替代方案:document.querySelector("#nicoVditor > div.vditor-content > div.vditor-ir").innerHTML;//获取 HTML 内容
出错原因:在 saveContent() 函数中 Vditor 未被定义
解决方法:将 Vditor 作为参数传入函数中
返回历史版本
function showHistory(ifFirst) { const transaction = db.transaction("history", "readonly");//创建读取事务的实例 const store = transaction.objectStore("history");//获取对象储存的实例 const request = store.getAll(); request.onsuccess = (event) => { const history = event.target.result; // 确保在请求成功后访问 result 属性 }; history.sort((a, b) => b.id - a.id);// 按 ID(即时间戳)降序排序 if(ifFirst){document.querySelector("#nicoVditor > div.vditor-content > div.vditor-ir").innerHTML = history[0].content;ifFirst=true;} else{return history;} }
这里直接使用修改
.innerHTML
导致返回保存后无法被继续编辑,改用setValue()
重构代码点击之后呼出弹窗
function showHistoryDialog(history) { // 如果已有弹窗,先移除 //const existingDialog = document.querySelector("#history-dialog"); //if (existingDialog) existingDialog.remove(); // 创建弹窗容器 const dialog = document.createElement("div"); dialog.id = "history-dialog"; dialog.style.position = "fixed"; dialog.style.top = "50%"; dialog.style.left = "50%"; dialog.style.transform = "translate(-50%, -50%)"; dialog.style.background = "#fff"; dialog.style.border = "1px solid #ddd"; dialog.style.padding = "15px"; dialog.style.boxShadow = "0px 2px 10px rgba(0, 0, 0, 0.3)"; dialog.style.zIndex = "1000"; dialog.style.maxHeight = "300px"; dialog.style.overflowY = "auto"; // 弹窗标题 const title = document.createElement("h3"); title.innerText = "历史记录"; title.style.marginTop = "0"; dialog.appendChild(title); // 遍历历史记录,添加按钮 history.forEach((item, index) => { const button = document.createElement("button"); button.innerText = `${index + 1}. ${item.savedAt}`; button.style.display = "block"; button.style.marginBottom = "10px"; button.style.width = "100%"; // 点击按钮时写入内容 button.onclick = () => { document.querySelector("#nicoVditor > div.vditor-content > div.vditor-ir").innerHTML = item.content; console.log(`已恢复历史记录: ${item.savedAt}`); dialog.remove(); // 关闭弹窗 }; dialog.appendChild(button); }); // 关闭按钮 const closeButton = document.createElement("button"); closeButton.innerText = "关闭"; closeButton.style.marginTop = "10px"; closeButton.style.width = "100%"; closeButton.onclick = () => dialog.remove(); dialog.appendChild(closeButton); // 将弹窗插入到页面 document.body.appendChild(dialog); }
但是因为
showHistory
函数是异步(允许它之后的代码先运行)的,而你在调用showHistory
函数时期望立即获得结果。你需要确保在request.onsuccess
事件处理程序中处理结果。Q10.异步执行的函数如何获取返回值?
在 JavaScript 中,异步函数的返回值无法直接通过同步方式获取,因为异步操作(如网络请求、定时器等)需要时间完成。因此,需要通过以下几种方法来获取异步函数的返回值:
Promise
和then()
当异步函数返回一个 Promise 对象时,可以通过
.then()
方法来处理返回值。示例代码
function asyncFunction() { return new Promise((resolve) => { setTimeout(() => { resolve("异步返回值"); }, 1000); // 模拟异步操作,1秒后返回 }); } // 使用 then() 获取返回值 asyncFunction().then((result) => { console.log("获取到的返回值:", result); });
async/await
async/await
是对Promise
的语法糖,使得异步代码更像同步代码,更易于理解和编写。示例代码
async function asyncFunction() { return new Promise((resolve) => { setTimeout(() => { resolve("异步返回值"); }, 1000); // 模拟异步操作 }); } // 使用 async/await 获取返回值 async function getValue() { const result = await asyncFunction(); console.log("获取到的返回值:", result); } getValue();
注意:
await
只能在async
函数 中使用。await
会暂停当前async
函数的执行,直到异步操作完成。
- 回调函数
将一个回调函数作为参数传入异步函数,在异步操作完成后调用该回调函数并传入返回值。
示例代码
function asyncFunction(callback) { setTimeout(() => { const value = "异步返回值"; callback(value); // 调用回调函数,传入返回值 }, 1000); } // 使用回调函数获取返回值 asyncFunction((result) => { console.log("获取到的返回值:", result); });
- 结合
async/await
和Promise
的例子
当你调用一个异步函数,返回的是 Promise,你可以用
async/await
来等待它的完成。示例代码
function fetchData() { return new Promise((resolve) => { setTimeout(() => resolve("返回数据成功"), 1000); }); } async function getData() { const result = await fetchData(); console.log("最终获取的数据:", result); } getData();
- 立即执行异步函数 (IIFE)
有时你需要在代码块内立即调用
async
函数,可以使用 **立即执行函数表达式 (IIFE)**。示例代码
(async () => { const result = await asyncFunction(); console.log("立即执行异步函数的返回值:", result); })();
删除历史记录
使用 delate 方法删除历史记录:
const request = indexedDB.open("VditorHistoryDB", dbVersion); request.onsuccess = (event) => { const db = event.target.result; const transaction = db.transaction("history", "readwrite"); const store = transaction.objectStore("history"); const deleteRequest = store.delete(recordId); // 替换 recordId 为你要删除的记录的 ID deleteRequest.onsuccess = () => { console.log("记录已删除"); }; deleteRequest.onerror = (event) => { console.error("删除记录失败", event.target.error); }; };
2024/12/19
三、完成
大概耗时十天时间,我们从无到有完成了这个脚本的编写。
事实上,我认为这次尝试,是利用 AI 进行学习的一种方式。
过去人们大部分情况下只能从前到后,一步步到达想要的结果,但是 AI 让我们可以从后向前,并且在这个过程中一点点弄清楚这个东西的原理,这一点是以往人所达不到的。
最后我希望我自己可以继续维护这个脚本,不断为其添加新的功能。
赏--> 切换主题 | SCHEME TOOL - JS 角度,根据鼠标光标的坐标改变