跳到主要内容

在 hono 中实现断点续传

· 阅读需 1 分钟
1adybug
子虚伊人
import { createReadStream } from "fs"
import { stat } from "fs/promises"
import { Readable } from "stream"

import { Hono } from "hono"

const app = new Hono()

function nodeToWebStream(nodeStream: Readable) {
return new ReadableStream({
start(controller) {
// 处理数据块
nodeStream.on("data", chunk => controller.enqueue(chunk))

// 处理流结束
nodeStream.on("end", () => controller.close())

// 处理错误
nodeStream.on("error", error => controller.error(error))
},
cancel() {
nodeStream.destroy()
},
})
}

app.get("/video", async c => {
const filename = "demo.mp4"
const { size } = await stat(filename)
const headers = new Headers()
headers.set("Content-Type", "video/mp4")
const range = c.req.header("Range")

if (!range) {
headers.set("Content-Length", String(size))
return c.newResponse(nodeToWebStream(createReadStream("demo.mp4")), {
status: 200,
headers,
})
}

const parts = range.replace(/bytes=/, "").split("-")
const start = parseInt(parts[0])
const end = parts[1] ? parseInt(parts[1]) : size - 1
const chunksize = end - start + 1
headers.set("Content-Range", `bytes ${start}-${end}/${size}`)
headers.set("Accept-Ranges", "bytes")
headers.set("Content-Length", String(chunksize))
return c.newResponse(nodeToWebStream(createReadStream(filename, { start, end })), { status: 206, headers })
})

export default {
port: 4567,
fetch: app.fetch,
}

使用 Tailwind CSS 选择元素

· 阅读需 1 分钟
1adybug
子虚伊人

在 Tailwind 中经常会有一些奇怪的类名,比如 w-[calc(100vw_-_64px)],这种类名是无法直接使用 document.querySelector 来选择的,需要进行一些处理,将类名的中 [](): 替换为 \\[\\]\\(\\)\\:,然后使用 document.querySelector 来选择。

className.replace(/([\[\]\(\):])/g, "\\$1")

AbortSignal 的用法

· 阅读需 1 分钟
1adybug
子虚伊人

对于 AbortSignal,我们常见的用法应该是用于取消 fetch 请求:

const controller = new AbortController()
const signal = controller.signal

fetch(url, { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error))

// 取消请求
controller.abort()

但是 AbortSignal 还有其他的用法,比如用于取消事件监听:

useEffect(() => {
const controller = new AbortController()
const signal = controller.signal
window.addEventListener("click", () => console.log("click"), { signal })
return () => {
controller.abort()
}
}, [])

当然,更高效的用法是取消多个事件监听

AbortSignal 类还有两个静态方法 AbortSignal.timeoutAbortSignal.any

AbortSignal.timeout 用于设置超时时间,在指定时间后会自动取消:

const signal = AbortSignal.timeout(1000)

类似于:

const controller = new AbortController()
const signal = controller.signal
setTimeout(() => controller.abort(), 1000)

AbortSignal.any 用于多个 AbortSignal 任意一个触发时取消:

const controller = new AbortController()
const controller2 = new AbortController()
const signal = AbortSignal.any([controller.signal, controller2.signal])

Nebula Graph 中的连续匹配

· 阅读需 2 分钟
1adybug
子虚伊人

先看两组 Cypher 语句:

MATCH p = (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
RETURN count(p)
MATCH (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
MATCH p = (u:`user`)-[w:watched]->(m:`movie`)
RETURN count(p)

通过运行结果可知两组语句的结果是一样的,可知连续匹配的作用域就是上一个匹配的结果。 uwm 是上一个匹配的结果,所以第二组语句中的 count(p) 与第一组语句中的 count(p) 是一样的。

如果我们把第二组语句改为:

MATCH (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
MATCH p = (u:`user`)-[w2:watched]->(m:`movie`)
RETURN count(p)

那么此时,只有 um 是继承了上一次匹配的结果,w2 是重新匹配的结果,所以 count(p) 的结果就不一样了,也就是对于每一组 um 重新匹配了 w2 这条边,那么 count(p) 的结果必然是大于或者等于第一组语句的结果。

MATCH (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
WITH u, m
MATCH p = (u:`user`)-[w:watched]->(m:`movie`)
RETURN count(p)

此时如果我们使用 WITH,那么 w 依然被丢失了,在第二次匹配中的 w 也相当于重新匹配的结果,所以 count(p) 的结果也是大于或者等于第一组语句的结果。

如果我们做以下改动也是错误的:

MATCH (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
WITH u, m
MATCH p = (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
RETURN count(p)

因为第一次匹配的结果并没有保证 um 之间的组合是独一无二的,也就是结果的列表中可能会存在系统的 um 组合,所以我们应该加一个语句:

MATCH (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
WITH u, m, count(w) as c
MATCH p = (u:`user`)-[w:watched]->(m:`movie`)
WHERE w.rate > 4
RETURN count(p)

通过 count(w) 就可以让 um 之间的组合是独一无二的

在 react 中手动触发 change 事件

· 阅读需 2 分钟
1adybug
子虚伊人

react 中存在很多受控组件,比如 inputtextarea 等,一般来说,受控组件将会是如下写法:

import { FC } from "react"

const App: FC = () => {
const [value, setValue] = useState("")

return <input value={value} onChange={e => setValue(e.target.value)} />
}

export default App

这种写法在 react 内部使用没有任何问题,但是对于外部使用者来说却存在很多问题,比如:

import { FC, useState } from "react"

const App: FC = () => {
const [value, setValue] = useState("")

return (
<div>
<div>
<label htmlFor="input">input:</label>
<input id="input" value={value} onChange={e => setValue(e.target.value)} />
</div>
<div>value:{value}</div>
</div>
)
}

export default App

当我们使用如下代码来更改 input 的值:

input.value = "123456"

却看到输入框的值发生了更改,下方的文本却没有任何变化,说明我们更改 input 的值的行为并没有触发 react 内部的 change 事件,那我们来手动触发 changeinput 事件:

const changeEvent = new Event("change", { bubbles: true })
const inputEvent = new Event("input", { bubbles: true })

input.dispatchEvent(changeEvent)
input.dispatchEvent(inputEvent)

可以看到,下方文本任然没有任何变化,Google 一番终于找到了正确答案 Simulate React On-Change On Controlled Components

const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set

set?.call(input, "123456")

const inputEvent = new Event("input", { bubbles: true })

input.dispatchEvent(inputEvent)

下方的文本成功同步!

在 Clash 中添加全局规则

· 阅读需 3 分钟
1adybug
子虚伊人

有时候我们希望某些网站不走代理,这时候就需要添加自定义规则,右击订阅,选择扩展脚本:

// Define main function (script entry)

/**
* @param {object} config
* @param {string[]} config.rules
* @param {string} profileName
*/
function main(config, profileName) {
config.rules.unshift("DOMAIN-SUFFIX,axshare.com,DIRECT")
config.rules.unshift("DOMAIN-SUFFIX,bing.com,DIRECT")
config.rules.unshift("DOMAIN-SUFFIX,codesandbox.io,DIRECT")
config.rules.unshift("DOMAIN-SUFFIX,csbops.io,DIRECT")
config.rules.unshift("DOMAIN-SUFFIX,csb.app,DIRECT")
config.rules.unshift("DOMAIN-SUFFIX,gallerycdn.vsassets.io,节点选择")
config.rules.unshift("DOMAIN-SUFFIX,gallery.vsassets.io,节点选择")
config.rules.unshift("DOMAIN-SUFFIX,marketplace.visualstudio.com,节点选择")
return config
}

DOMAIN 是指定域名,DOMAIN-SUFFIX 是指定域名后缀,DIRECT节点选择 都是规则内的分组。

规则类型匹配逻辑匹配目标性能等级关键注意事项
DOMAIN精确域名匹配域名仅匹配完全相同的域名字符串。
DOMAIN-SUFFIX域名后缀匹配域名匹配指定后缀及其所有子域名。
DOMAIN-KEYWORD域名关键词匹配域名匹配域名中任何位置出现的关键词,可能产生误匹配。
IP-CIDR目标 IPv4 地址段目标 IPv4默认触发 DNS 解析。
IP-CIDR6目标 IPv6 地址段目标 IPv6默认触发 DNS 解析。
GEOIP目标 IP 地理位置目标 IP基于 MaxMind 数据库;默认触发 DNS 解析。
SRC-IP-CIDR源 IPv4 地址段源 IPv4主要用于网关模式,区分局域网内不同设备。
DST-PORT目标端口目标端口基于 TCP/UDP 目标端口进行匹配。
SRC-PORT源端口源端口基于 TCP/UDP 源端口进行匹配。
PROCESS-NAME进程名称进程名操作系统相关;适用于 Windows, macOS, Linux, FreeBSD。
PROCESS-PATH进程路径进程路径操作系统相关;比进程名更精确,但可移植性差。
IPSETLinux IP 集目标 IP低 (内核)仅 Linux 可用;需要 ipset 工具;性能极高。

有时候我们希望某些规则对于所有的订阅都生效,这时候我们可以右击 全局扩展脚本,选择编辑文件:

// Define main function (script entry)

/**
* @param {object} config
* @param {string[]} config.rules
* @param {string} profileName
*/
function main(config, profileName) {
/** @type string | undefined */
const name = config["proxy-groups"]?.at(0)?.name
config.rules.unshift(`DOMAIN-SUFFIX,neo4j.com,${name}`)
return config
}

这里的意思找到订阅的第一个分组(一般都是通用的代理规则),然后将这条规则添加到这个分组中。

如果需要对于脚本进行调试,可以添加 console.log,在保存脚本以后,会在 全局扩展脚本 下方出现一个日志按钮,点击以后会输出脚本的日志。

stopPropagation 和 stopImmediatePropagation 之间的区别

· 阅读需 2 分钟
1adybug
子虚伊人

JavaScript 中,stopPropagationstopImmediatePropagation 是两个用于控制事件传播的方法。它们之间有一些关键的区别。

stopPropagation

stopPropagation 方法用于阻止事件冒泡到下一个元素,但允许其他事件处理程序在当前元素上继续执行。例如,如果你在一个按钮的点击事件中调用 stopPropagation,点击事件将不会冒泡到按钮的父元素,但该按钮上的其他点击事件处理程序仍然会被执行。

element.addEventListener("click", function (event) {
event.stopPropagation()
console.log("Button clicked")
})

stopImmediatePropagation

stopImmediatePropagation 方法不仅阻止事件冒泡,还阻止当前元素上所有后续的事件处理程序执行。这意味着一旦调用了 stopImmediatePropagation,当前元素上的其他事件处理程序将不会被触发。

element.addEventListener("click", function (event) {
event.stopImmediatePropagation()
console.log("Button clicked")
})

element.addEventListener("click", function (event) {
console.log("This will not be logged")
})

何时使用

  • 使用 stopPropagation 当你只想阻止事件冒泡但仍希望当前元素上的其他事件处理程序执行时。
  • 使用 stopImmediatePropagation 当你希望完全停止事件传播并阻止当前元素上的所有后续事件处理程序执行时。

了解这两个方法的区别可以帮助你更好地控制事件流,从而实现更复杂的用户交互逻辑。

修改函数的名称

· 阅读需 1 分钟
1adybug
子虚伊人

函数的名称在定义时已经被确定,比如:

function foo() {}

// 打印 "foo"
console.log(foo.name)

然而,匿名函数或者箭头函数的名称是空字符串:

const foo = (() => function () {})()

// 打印 ""
console.log(foo.name)

之所以要写成 (() => function () {})() 这么奇怪的形式,是应为使用 varletconst 定义的变量名会赋值给函数的名称:

const foo = function () {}

// 打印 "foo"
console.log(foo.name)

const bar = () => 0

// 打印 "bar"
console.log(bar.name)

变量定义时,属性名也会被赋值给函数的名称:

const obj = {
foo: function () {},
bar: () => 0,
}

// 打印 "foo"
console.log(obj.foo.name)

// 打印 "bar"
console.log(obj.bar.name)

然而,后续的赋值操作不会影响函数的名称:

const obj = {}

obj.foo = function () {}

// 打印 ""
console.log(obj.foo.name)

const name = "bar"
obj[name] = () => 0

// 打印 ""
console.log(obj.bar.name)

然而,更有趣的是,定义对象字面量时,动态的属性名会被赋值给属性名:

const name = "foo"

const obj = {
[name]: function () {},
}

// 打印 "foo"
console.log(obj[name].name)

最后,如果你想修改函数的名称,可以使用 Object.defineProperty

const foo = function () {}
Object.defineProperty(foo, "name", { value: "bar" })

// 打印 "bar"
console.log(foo.name)

在 Markdown 中使用 Prettier

· 阅读需 1 分钟
1adybug
子虚伊人

prettier 也支持 Markdown 文件的格式化,但是有以下注意点:

  1. 中文用户可以安装插件 prettier-plugin-lint-md
  2. JavaScript 代码块语言标识需要改为 javascript 或者 jsTypeScript 改为 typescript 或者 ts,驼峰命名的语言标识需要改为全小写,否则 prettier 无法识别
  3. tabWidth 要设置为 2,否则 Markdown 文件中的文本缩进会有问题

使用 Grid 布局实现的 Flow 组件

· 阅读需 1 分钟
1adybug
子虚伊人
.container {
display: grid;
grid-template-columns: repeat(auto-fill, 100px); /* 固定宽度为 100px */
grid-auto-rows: 100px; /* 固定高度为 100px */
gap: 10px; /* 网格间距 */
}

太牛了