跳到主要内容

HTML 流式传输

· 阅读需 1 分钟
1adybug
子虚伊人
import { createServer } from "http"

const server = createServer(async (request, response) => {
response.writeHead(200, {
"Content-Type": "text/html; charset=utf-8",
"Transfer-Encoding": "chunked",
})

response.write(`
<!DOCTYPE html>
<html>
<body>
<h2 id="loading">Loading...</h2>
`)

const response2 = await fetch("https://dog.ceo/api/breeds/image/random")
const data = await response2.json()

response.write(`
<img src="${data.message}" alt="random dog" />
<script>
document.getElementById("loading").remove()
</script>
</body>
</html>
`)

response.end()
})

server.listen(3000, () => console.log("Server is running on http://localhost:3000"))

:last-child 与 :last-of-type 的区别

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

:last-child:last-of-type 的区别:

  1. :last-child 选择器

    • 选择父元素中最后一个子元素
    • 无论这个子元素是什么类型
    • 如果最后一个子元素不匹配选择器,则不会被选中
  2. :last-of-type 选择器

    • 选择父元素中最后一个指定类型的子元素
    • 只关注特定类型的元素
    • 即使是最后一个,但不是指定类型也不会被选中

举个例子:

<div>
<p>第一段</p>
<p>第二段</p>
<span>一个span</span>
</div>
  • div p:last-child 不会选中任何元素(因为最后一个子元素是 <span>
  • div p:last-of-type 会选中第二个 <p> 元素
  • div span:last-of-type 会选中最后的 <span>

简单来说:

  • :last-child 更严格,要求是最后一个子元素
  • :last-of-type 更灵活,只要是同类型的最后一个元素
提示

:last-of-type 只关注元素类型,不关注类名、ID 等属性,所以不会受到这些属性的影响。

比如:

<div>
<p class="red">第一段</p>
<p>第二段</p>
<p>第三段</p>
</div>

div p.red:last-of-type 不会选中任何元素,因为 p:last-of-type 已经选中最后一个 <p> 元素,但是这个元素没有 red 类名。

调整导入顺序

· 阅读需 2 分钟
1adybug
子虚伊人
  1. 安装插件

    npm i @ianvs/prettier-plugin-sort-imports glob -D
  2. 写入配置文件 prettier.config.mjs

    // @ts-check

    import { readFileSync } from "fs"
    import { parse } from "path"

    import { globSync } from "glob"

    /**
    * 数组去重
    * @template T - 数组的元素类型
    * @param {T[]} array - 输入的数组
    * @return {T[]} 新数组
    */
    function unique(array) {
    return Array.from(new Set(array))
    }

    const jsExts = [".js", ".jsx", ".ts", ".tsx", ".cjs", ".mjs", ".cts", ".mts", ".vue"]

    const assetExts = unique(
    globSync("**/*", {
    ignore: ["node_modules/**"],
    withFileTypes: true,
    cwd: import.meta.dirname,
    })
    .filter(path => path.isFile() && !jsExts.some(ext => path.name.toLowerCase().endsWith(ext)))
    .map(path => parse(path.name).ext.slice(1))
    .filter(ext => ext !== ""),
    )

    const assetExtsRegStr = `\\.(${assetExts.join("|")}|${assetExts.join("|").toUpperCase()})`

    const assetQueryRegStr = "(\\?[a-zA-Z0-9]+)?"

    const namespaces = unique(
    unique(
    globSync("**/package.json", {
    withFileTypes: true,
    cwd: import.meta.dirname,
    })
    .filter(path => path.isFile())
    .map(path => path.fullpath()),
    )
    .map(path => JSON.parse(readFileSync(path, "utf8")))
    .map(json =>
    Object.keys({
    ...json.dependencies,
    ...json.devDependencies,
    ...json.peerDependencies,
    ...json.optionalDependencies,
    }))
    .flat()
    .filter(dep => dep.startsWith("@"))
    .map(dep => dep.split("/")[0].slice(1)),
    )

    /**
    * @type {import("prettier").Options}
    */
    const config = {
    semi: false,
    tabWidth: 4,
    arrowParens: "avoid",
    printWidth: 160,
    plugins: ["@ianvs/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
    importOrder: [
    "<BUILTIN_MODULES>",
    `^@(${namespaces.join("|")})/`,
    "<THIRD_PARTY_MODULES>",
    "",
    `^@.+?(?<!${assetExtsRegStr}${assetQueryRegStr})$`,
    `^\\.{1,2}/.+?(?<!${assetExtsRegStr}${assetQueryRegStr})$`,
    "",
    `^@.+?${assetExtsRegStr}${assetQueryRegStr}$`,
    `^\\.{1,2}/.+?${assetExtsRegStr}${assetQueryRegStr}$`,
    ],
    importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
    importOrderTypeScriptVersion: "5.0.0",
    importOrderCaseSensitive: true,
    }

    export default config
    注意

    prettier-plugin-tailwindcss 插件必须放置在 @ianvs/prettier-plugin-sort-imports 的后面

  3. 配置 package.json

    {
    "scripts": {
    "lint": "prettier --write ."
    }
    }
  4. 运行 npm run lint 即可调整导入顺序。

body 设置为 overflow:scroll 时,antd Modal 打开后会被改为 overflow:hidden

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

在某个项目中,为了让每个页面的宽度一致,不因滚动条而发生突变,设置 bodyoverflow: scroll

然而发现 antdModal 组件打开后,body 会被改为 overflow: hidden,导致页面滚动条消失。这是因为 antdModal 组件会在打开时给 body 添加一个 overflow: hidden 的样式,关闭时再移除,这会导致页面的宽度发生变化,观感不好

但是某些页面的 Modal 打开后又会自动添加 padding-right,不会让页面宽度发生变化,这是因为 antdModal 组件在打开时会判断页面是否有滚动条,如果有滚动条,会给 body 添加一个 padding-right,这样页面的宽度就不会发生变化

获取页面滚动条的宽度也很简单:

const width = document.documentElement.offsetWidth - document.documentElement.clientWidth

clientWidth、offsetWidth、scrollWidth 三者的区别

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

clientWidth

  • 表示元素的内部宽度
  • 包括内容区(content)和内边距(padding)
  • 不包括边框(border)、外边距(margin)和滚动条
  • 对于内联元素,clientWidth 总是返回 0

例如一个元素样式如下:

.element {
width: 100px;
padding: 10px;
border: 5px solid black;
}

clientWidth = 120px (content 100px + padding 20px)

offsetWidth

  • 表示元素的布局宽度
  • 包括内容区(content)、内边距(padding)和边框(border)
  • 不包括外边距(margin)
  • 包括滚动条宽度(如果有)

继续上面的例子:

offsetWidth = 130px (content 100px + padding 20px + border 10px)

scrollWidth

  • 表示元素内容的完整宽度,包括因超出元素宽度而不可见的部分
  • 如果元素内容没有超出可视区域,则等于 clientWidth
  • 如果内容超出可视区域,则等于实际内容宽度加上内边距

举个例子:

// 如果一个容器宽度为 200px,但内容实际宽度为 300px
const container = document.querySelector(".container")

console.log(container.clientWidth) // 200px

console.log(container.scrollWidth) // 300px

这些属性的主要应用场景:

  • clientWidth:计算元素的可视内容区域
  • offsetWidth:获取元素实际占用的布局空间
  • scrollWidth:检测内容是否溢出,实现横向滚动功能

总结

在实际开发中如何选择:

  1. 需要判断元素是否需要滚动时,比较 scrollWidthclientWidth
  2. 需要获取元素实际占用空间时,使用 offsetWidth
  3. 需要获取元素可视内容区域时,使用 clientWidth

需要注意的是,这些值都是只读的,如果需要修改元素尺寸,应该使用 CSS 的 widthpadding 等属性。

修改 nvm 源为淘宝镜像

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

仅限 windows 版,一共两种方法:

修改文件

nvm 的安装路径下,找到 settings.txt 文件,设置 node_mirrornpm_mirror 为国内镜像地址,在文件末尾加入:

阿里云镜像

node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://npmmirror.com/mirrors/npm/

腾讯云镜像

node_mirror: http://mirrors.cloud.tencent.com/npm/
npm_mirror: http://mirrors.cloud.tencent.com/nodejs-release/

命令行

阿里云镜像

nvm npm_mirror https://npmmirror.com/mirrors/npm/
nvm node_mirror https://npmmirror.com/mirrors/node/

腾讯云镜像

nvm npm_mirror http://mirrors.cloud.tencent.com/npm/
nvm node_mirror http://mirrors.cloud.tencent.com/nodejs-release/

在右键菜单中添加“在终端中打开”

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

添加

新建文本文档,将以下内容复制到文本文档中:

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Directory\Background\shell\wt]
@="在此处打开 Windows Terminal"
"Icon"="C:\\Users\\lenovo\\Pictures\\terminal.ico"

[HKEY_CLASSES_ROOT\Directory\Background\shell\wt\command]
@="wt.exe -d \"%V\""

[HKEY_CLASSES_ROOT\Directory\shell\wt]
@="在此处打开 Windows Terminal"
"Icon"="C:\\Users\\lenovo\\Pictures\\terminal.ico"

[HKEY_CLASSES_ROOT\Directory\shell\wt\command]
@="wt.exe -d \"%V\""

C:\\Users\\lenovo\\Pictures\\terminal.ico 替换为你的 Windows Terminal 程序或者 .ico 文件图标路径。

注意

由于 Windows Terminal 经常会升级,所以如果使用 Windows Terminal 的绝对路径作为图标,可能会导致图标失效。

使用 UTF-16 LE 编码格式保存文件,将文件后缀名改为 .reg,双击运行即可

删除

新建文本文档,将以下内容复制到文本文档中:

Windows Registry Editor Version 5.00

[-HKEY_CLASSES_ROOT\Directory\Background\shell\wt]
[-HKEY_CLASSES_ROOT\Directory\shell\wt]

同样使用 UTF-16 LE 编码格式保存文件,将文件后缀名改为 .reg,双击运行即可

HTTP 状态码

· 阅读需 2 分钟
1adybug
子虚伊人
状态码类别状态码英文说明中文说明
1xx 信息性100Continue继续。客户端应继续请求
101Switching Protocols切换协议。服务器同意切换协议
2xx 成功200OK请求成功。一般用于 GET 与 POST 请求
201Created已创建。成功请求并创建了新的资源
204No Content无内容。服务器处理成功,但未返回内容
3xx 重定向301Moved Permanently永久移动。请求的资源已永久移动到新 URI
302Found临时移动。请求的资源临时移动到新 URI
304Not Modified未修改。资源未改变,可使用缓存
4xx 客户端错误400Bad Request错误请求。请求语法错误
401Unauthorized未授权。需要身份验证
403Forbidden禁止。服务器拒绝请求
404Not Found未找到。服务器找不到请求的资源
405Method Not Allowed方法禁用。不允许使用该请求方法
429Too Many Requests请求过多。用户在给定时间内发送了太多请求
5xx 服务器错误500Internal Server Error服务器内部错误
502Bad Gateway错误网关。服务器作为网关收到无效响应
503Service Unavailable服务不可用。服务器暂时过载或维护
504Gateway Timeout网关超时。服务器作为网关未及时响应

在 Express 中实现断点续传

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

import express from "express"

const app = express()

app.get("/video", async (request, response) => {
const filename = "demo.mp4"
const { size } = await stat(filename)
response.setHeader("Content-Type", "video/mp4")
const range = request.headers.range

if (!range) {
response.setHeader("Content-Length", size)
response.status(200)
createReadStream(filename).pipe(response)
return
}

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
response.setHeader("Content-Range", `bytes ${start}-${end}/${size}`)
response.setHeader("Accept-Ranges", "bytes")
response.setHeader("Content-Length", chunksize)
response.status(206)
createReadStream(filename, { start, end }).pipe(response)
})

app.listen(4567)

在 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,
}