首页
归档
关于
友人帐
Search
1
Mac OS 终端利器 iTerm2 + Oh My Zsh
14,398 阅读
2
前端 History 路由及 API 反向代理的 Nginx 配置
10,723 阅读
3
解决 Xcode Command Line Tools 错误或无法安装问题
6,841 阅读
4
Mac下右键使用VSCode打开项目
4,076 阅读
5
BrowserHistory刷新页面404问题
3,602 阅读
码上世界
内容分享
生活印记
其他
登录
Search
小小孩
累计撰写
96
篇文章
累计收到
187
条评论
首页
栏目
码上世界
内容分享
生活印记
其他
页面
归档
关于
友人帐
搜索到
49
篇与
码上世界
的结果
2025-09-12
JavaScript 高性能现代网页截图
最近发现一款简单好用、功能强大的前端截图开发工具库 SnapDOM,现在可以彻底抛弃 html2canvas 了。SnapDOMSnapDOM是一款性能优越的网页截图工具,能够快速准确地将网页元素转换成多种图片格式,包括SVG、PNG、JPG、WebP以及Canvas元素。它支持复杂元素的截图,如CSS样式、伪元素、Shadow DOM等,并且能够高度还原网页的视觉效果。技术特性零依赖:轻量化(约8KB),纯原生 JavaScript 实现。渲染速度超快:全屏截图耗时仅0.8秒(比 html2canvas 快8倍);像素级还原:可以精准渲染复杂样式(渐变、阴影、SVG)和动态加载内容(比如懒加载图片、异步数据等);与 html2canvas和 dom-to-image相比,SnapDOM在速度和还原度上都有显著优势,尤其在处理大型元素时速度可达到html2canvas的93倍以上。将复杂DOM结构分解为可复用的矢量层和像素层,通过SVG转换和Canvas渲染分离,将处理时间从全量克隆模式缩短至分层处理模式对比对比项SnapDOMhtml2canvas截图速度✅ 超快❌ 慢高分辨率支持✅ 高清无模糊,甚至支持矢量❌ 缩放易失真滚动截图✅ 自动拼接长图❌ 需手动截取GPU加速✅ 异步渲染❌ 阻塞主线程CSS 支持✅ 绝大部分都支持❌ 大部分不支持跨域资源✅ 完美支持⚠️ 部分会失效使用安装npm i @zumer/snapdom简单截图实现import { snapdom } from '@zumer/snapdom'; const el = document.querySelector('#target'); const res = await snapdom(el); // 下载图片为指定格式 await res.download({ format: 'jpg', filename: '截图.jpg' });注意事项SnapDOM 各方面都很强,是新一代的网页截图库,完全可以在生产上使用,不过需要注意一下几点:图片跨域:有外部图片,需要得确保这些资源支持 CORS,否则会截图失败;iframe 限制:浏览器的安全限制,SnapDOM 不能截 iframe 里的内容;长页面截图:建议分块去截超长页面,否则可能会导致内存溢出。
2025年09月12日
4 阅读
0 评论
1 点赞
2024-11-11
前端项目路径别名终极解决方案
原生路径解决从 Node.js v12.19.0 开始,开发人员可以使用 Subpath Imports 在 npm 包中声明路径别名。这可以通过 package.json 文件中的 imports 字段来完成。不需要在 npm 上发布包。在任何目录中创建一个 package.json 文件就足够了。因此,这种方法也适用于私人项目。配置路径别名,假如有项目结构如下:my-project ├── src/ │ ├── components/ │ │ └── searchForm/ │ │ └── form/ │ │ └── index.ts │ ├── pages/ │ │ └── login/ │ │ └── about/ │ │ └── home/ │ └── mock/ │ └── api/ │ └── index.ts └── package.json我们可以在 package.json 中这么配置:{ "name": "my-awesome-project", "imports": { "#*": "./*" } }接下来就是愉快的使用了:import { SearchForm } from "#src/components/searchForm";
2024年11月11日
130 阅读
0 评论
0 点赞
2024-03-29
http 协议头 Content-Disposition 的作用
在响应中 Content-Disposition 是真正的作为消息头的一部分,它主要有两种用法:inline当它的值为 inline 时,表示响应的消息作为 HTML 页面的一部分(inline 是默认值)。假设你本身想要下载一个 PDF 文件,但是你将 Content-Disposition 的值设置为 inline 或者没设置,你的响应头对应如下:Content-Type: application/pdf Content-Disposition: inline; filename="example.pdf"此时浏览器不会去下载这个文件,而是直接在浏览器中去打开这个 PDF 文件。相信这样的场景大家会熟悉,我们经常去浏览一个学术完整的 PDF 链接时都是直接在浏览器中打开 PDF 文件,而不是下载到本地,这就是设置了 Content-Disposition: inline 的原因(即使没设置它的默认值也是这个)。attachment现在再来看下 Content-Disposition: attachment,这个是真正意义上的文件下载。还是以前面的响应头为例:Content-Type: application/pdf Content-Disposition: attachment; filename="example.pdf"此时,当服务器给客户端(通常是浏览器)响应时,它会有一个弹窗提示,提醒你保存文件。而保存的文件的默认名就是 filename 指定的值,当然该属性是非必须的。
2024年03月29日
718 阅读
4 评论
0 点赞
2024-02-27
LNMP Nginx 安装 brotli 模块
lnmp 安装wget https://soft.lnmp.com/lnmp/lnmp2.0.tar.gz -O lnmp2.0.tar.gz && tar zxf lnmp2.0.tar.gz && cd lnmp2.0 && ./install.sh lnmpnginx 安装 brotlicd /root git clone --recurse-submodules -j8 https://gitee.com/hipi/ngx_brotli.git cd ngx_brotli/deps/brotli mkdir out && cd out cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed .. cmake --build . --config Release --target brotlienc cd /root编辑lnmp安装包下的/root/lnmp2.0/lnmp.conf在 Nginx_Modules_Options=" 的引号内加上--add-module=/root/ngx_brotli保存./upgrade.sh nginx升级一下nginx,版本号填写当前版本号就行升级完成就支持broti了,
2024年02月27日
295 阅读
0 评论
0 点赞
2023-11-02
Nginx 安装 brotli 模块
前置条件cd /www/server git clone --recurse-submodules -j8 https://gitee.com/hipi/ngx_brotli.git cd ngx_brotli/deps/brotli mkdir out && cd out cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DCMAKE_C_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_CXX_FLAGS="-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -ffunction-sections -fdata-sections -Wl,--gc-sections" -DCMAKE_INSTALL_PREFIX=./installed .. cmake --build . --config Release --target brotlienc cd ../../../..安装方法 1echo "--add-module=/www/server/ngx_brotli" > /www/server/panel/install/nginx_configure.pl面板直接编译安装方法 2模块名称:ngx_brotli模块描述:ngx_brotli模块参数:--add-module=/www/server/ngx_brotli前置脚本不填配置# 启用 Brotli 压缩 brotli on; # 设置 Brotli 压缩级别 brotli_comp_level 6; # 设置启用压缩的最小文件大小 brotli_min_length 20; # 配置 Brotli 压缩的缓冲区大小 brotli_buffers 16 8k; # 指定要压缩的文件类型 brotli_types text/xml text/plain text/css application/javascript application/x-javascript application/rss+xml text/javascript image/tiff image/svg+xml application/json application/xml;
2023年11月02日
755 阅读
3 评论
0 点赞
2023-08-30
JS 深拷贝终结者 structuredClone
之前的 JS 数据深拷贝,都是要么递归要么是利用 MessageChannel 来实现深克隆,现在终结者来了,JS 原生支持的函数 structuredClone ,可以实现数据深拷贝StructuredClone APIstructuredClone 是结构化拷贝算法的实现,能够实现几乎对所有数据类型的深拷贝。语法structuredClone(value) structuredClone(value, { transfer })参数value被克隆的对象。可以是任何结构化克隆支持的类型。transfer 可选是一个可转移对象的数组,里面的 值 并没有被克隆,而是被转移到被拷贝对象上。限制但是也有一些限制不允许克隆Error、Function和DOM对象,如果对象中含有,将抛出DATA_CLONE_ERR异常。不保留RegExp 对象的 lastIndex 字段。不保留属性描述符,setters 以及 getters(以及其他类似元数据的功能)。例如,如果4. 一个对象用属性描述符标记为 read-only,它将会被复制为 read-write。不保留原形链。兼容性Chrome >= 98FireFox >= 94
2023年08月30日
362 阅读
1 评论
0 点赞
2023-08-02
自定义可以异步等待的 forEach
自定义可以异步等待的 forEachArray.prototype.canAwaitForEach = async function(cb,thisArg){ for(let i = 0; i<this.length;i++){ await cb.call(thisArg,this[i],i,this) } }
2023年08月02日
208 阅读
0 评论
0 点赞
2023-05-26
清除 npx 缓存
为何需要清除 npx 缓存在新建模板时,比如 npm init vue@3 , 会下载 create-vue 包在 npx 缓存中,并且有概率不会根据create-vue包的小版本更新而更新,导致运行 npm init vue@3 都是旧的模板代码。如果发现这种问题就需要删除 npx 缓存,重新获取 create-vue 包。如何删除 npx 缓存查看本地 npm 缓存文件的路径:npm config get cache然后打开上述命令返回的文件夹路径,删除_npx 文件夹即可清除;其它方式还可以借助 clear-npx-cache 实现:npx clear-npx-cache
2023年05月26日
865 阅读
0 评论
1 点赞
2023-02-28
扩展 vis-network 右键菜单
之前一直使用 antv/g6 作为拓扑图展示插件,但发现 antv/g6 在力导向布局拖动下展示效果很差,节点一直在抖,而且在大数据下,直接卡死。所以转换为 vis-network ,相比之下 vis-network 效果很流畅,展示效果也不错。 唯一不好就是 没有 G6 那么多插件,就自己根据 G6 的插件api封装形式,自己封装了一份 vis 的右键菜单插件插件代码function modifyCSS(dom, css) { if (dom) { for (let key in css) { dom.style[key] = css[key]; } } return dom; } export default class VisMenu { constructor(network, config = {}) { this.network = network; const defaultConfig = { offsetX: 6, offsetY: 6, handleMenuClick: undefined, getContent: (e) => { return ` <div class='aa'> <div>菜单项1</div> <div>菜单项2</div> </div> `; }, onHide() { return true; }, itemTypes: ["node", "edge", "canvas"], }; this.config = { ...defaultConfig, ...config }; const _this = this; // 初始化menuDom this._createMenu(); this.network.on("oncontext", function (params) { params.event.preventDefault(); _this._showMenu(params); }); } _createMenu() { const _this = this; const containerDom = _this.network.canvas.frame; modifyCSS(containerDom, { position: "relative" }); const menuDom = document.createElement("div"); menuDom.className = _this.config.className || "vis-contextmenu"; containerDom.appendChild(menuDom); modifyCSS(menuDom, { top: "0px", position: "absolute", visibility: "hidden", }); _this.menuDom = menuDom; _this.containerDom = containerDom; } _showMenu(params) { const _this = this; const { pointer } = params; const menuResParams = {}; const atNode = _this.network.getNodeAt({ x: pointer.DOM.x, y: pointer.DOM.y, }); const atEdge = _this.network.getEdgeAt({ x: pointer.DOM.x, y: pointer.DOM.y, }); if (atNode) { menuResParams.type = "node"; menuResParams.id = atNode; menuResParams.orgItemData = _this.network.body.data.nodes.get(atNode); } else if (atEdge) { menuResParams.type = "edge"; menuResParams.id = atEdge; const orgItemData = _this.network.body.data.edges.get(atEdge); const fromId = orgItemData.from; const toId = orgItemData.to; menuResParams.orgItemData = orgItemData; menuResParams.orgFromData = _this.network.body.data.nodes.get(fromId); menuResParams.orgToData = _this.network.body.data.nodes.get(toId); } else { menuResParams.type = "canvas"; } // 判断是否显示菜单 if (!_this.config.itemTypes.includes(menuResParams.type)) { this._hideMenu(); return false; } // 塞入menuDom const menuStr = _this.config.getContent(menuResParams); _this.menuDom.innerHTML = menuStr; const graphWidth = _this.containerDom.offsetWidth; const graphHeight = _this.containerDom.offsetHeight; // Vis 默认在父元素添加 position:relative 所以不需要计算 offsetTop,offsetLeft // const graphTop = _this.containerDom.offsetTop; // const graphLeft = _this.containerDom.offsetLeft; // const bbox = _this.menuDom.getBoundingClientRect(); // let x = pointer.DOM.x + graphLeft + _this.config.offsetX; // let y = pointer.DOM.y + graphTop + _this.config.offsetY; // // when the menu is (part of) out of the DOM // if (x + bbox.width > graphWidth) { // x = pointer.DOM.x - bbox.width - _this.config.offsetX + graphLeft; // } // if (y + bbox.height > graphHeight) { // y = pointer.DOM.y - bbox.height - _this.config.offsetY + graphTop; // } const bbox = _this.menuDom.getBoundingClientRect(); let x = pointer.DOM.x + _this.config.offsetX; let y = pointer.DOM.y + _this.config.offsetY; if (x + bbox.width > graphWidth) { x = pointer.DOM.x - bbox.width - _this.config.offsetX; } if (y + bbox.height > graphHeight) { y = pointer.DOM.y - bbox.height - _this.config.offsetY; } modifyCSS(_this.menuDom, { top: `${y}px`, left: `${x}px`, visibility: "visible", }); const handler = () => { _this._hideMenu(); }; _this.handler = handler; // 如果在页面中其他任意地方进行click, 隐去菜单 document.body.addEventListener("click", handler); if (_this.config.handleMenuClick) { const handleMenuClickWrapper = (evt) => { _this.config.handleMenuClick(evt.target, menuResParams, _this.network); }; _this.menuDom.addEventListener("click", handleMenuClickWrapper); _this.handleMenuClickWrapper = handleMenuClickWrapper; } } _removeMenuEventListener() { const _this = this; if (_this.handler) { document.body.removeEventListener("click", _this.handler); } if (_this.handleMenuClickWrapper) { _this.menuDom.removeEventListener("click", _this.handleMenuClickWrapper); } } _hideMenu() { if (this.menuDom) { modifyCSS(this.menuDom, { visibility: "hidden" }); } // 隐藏菜单后需要移除事件监听 this._removeMenuEventListener(); } destroy() { this._removeMenuEventListener(); if (this.menuDom) { this.containerDom.removeChild(this.menuDom); } } } 如何使用import { Network } from "vis-network"; import VisMenu from './visMenu' const myNetwork = new Network(DOM, data, {/*...options*/}); new VisMenu(myNetwork, { className: "my-network-menu", getContent(e) { return `<ul> <li>${e.type}</li> <li>菜单一</li> <li>菜单二</li> <li>菜单三</li> <li>菜单四</li> <ul>`; }, handleMenuClick(target, item, network) { console.log(target.innerText); }, itemTypes: ["node", "edge"], });
2023年02月28日
451 阅读
1 评论
0 点赞
2022-12-26
利用 MessageChannel 来实现深克隆
前言一般 JS 深度克隆有两种方法 ,一种是 JSON 序列与反序列,另一种是 lodash 提供的 deepClone,其中利用 JSON 克隆 有以下缺点属性值为函数和 undefined 的属性会丢失属性值为正则表达式的会变成{}属性值为时间对象的会变成时间字符串其实另一种方法来实现克隆,利用MessageChannel消息通道的数据传输也可以实现对象深拷贝。MessageChannel利用 MessageChannel() 构造函数返回一个新的 MessageChannel 对象,其中返回的对象中包含两个 MessagePort 对象。这就是两个通道,其中可以数据传输。具体实现如下:function deepClone(obj) { return new Promise((resolve, reject) => { const { port1, port2 } = new MessageChannel(); port1.postMessage(obj); port2.onmessage = (msg) => { resolve(msg.data); }; port2.onerror = (msg) => { reject(msg.data); }; }); }使用(async () => { function deepClone(obj) { return new Promise((resolve, reject) => { const { port1, port2 } = new MessageChannel(); port1.postMessage(obj); port2.onmessage = (msg) => { resolve(msg.data); }; port2.onerror = (msg) => { reject(msg.data); }; }); } const obj = { a: 1, b: /\d/, c: new Date(), d: undefined, }; const cloneObj = await deepClone(obj); console.log(cloneObj, cloneObj === obj); })();利用 MessageChannel 来实现克隆 可以解决 大部分 JSON 克隆的缺点,但还是有个缺点是函数不能拷贝。 如果你所需拷贝的对象含有内置类型并且不包含函数,就可以利用 MessageChannel 来实现克隆。如果函数也要拷贝, 还是使用lodash 通过递归处理的 deepClone 方法。
2022年12月26日
403 阅读
0 评论
0 点赞
1
2
...
5