2025-03-14 00:35:02
ESLint v9.0.0 是 ESLint 的一个主要版本,它有几个重大变化,其中最大的变化是其配置文件和插件生态系统的使用,可以通过官方网站的 迁移到 v9.x 文档了解如何迁移。
这里有一个关于 ESLint V9 的扁平化配置的 npm 包,内置了一些个人常用的 ESLint 配置,这也是我在 GitHub 上发布的一个开源项目。如果它对您有帮助,请 给它一个 Star !
一款现代化的扁平 ESLint 配置,适用于 ESLint V9 ,由 @chengpeiquan 精心打造。
使用此 ESLint 配置仅需三步:
settings.json
启用自动 Lint(参考:🛠 VS Code 配置)这个快速指南可以作为入门辅助,避免遗漏关键步骤 🚀 。
使用常用的包管理器安装该包:
npm install -D eslint @bassist/eslint-config
注意: 需要 ESLint 版本 >= 9.0.0
,以及 TypeScript 版本 >= 5.0.0
。
如果使用的是 pnpm
,建议在项目根目录添加 .npmrc
文件,并包含以下配置,以更顺利地处理 peer 依赖:
shamefully-hoist=true
auto-install-peers=true
如果仍在使用 ESLint v8,请参考旧版(已不再维护)包:@bassist/eslint。
在项目根目录创建 eslint.config.js
文件:
// eslint.config.js
import { imports, typescript } from '@bassist/eslint-config'
// 导出一个包含多个配置对象的数组
export default [...imports, ...typescript]
然后在 package.json
中添加 "type": "module" :
{
"type": "module",
"scripts": {
"lint": "eslint src",
"lint:inspector": "npx @eslint/config-inspector"
}
}
运行 npm run lint
以检查代码,或运行 npm run lint:inspector
在 http://localhost:7777
查看可视化的 ESLint 配置。
对于 TypeScript 配置文件(例如
eslint.config.ts
),需要 额外的设置 。
# 为 Node.js 提供运行时 TypeScript 和 ESM 支持
npm install -D jiti
为了增强类型安全性,可以使用 defineFlatConfig
:
// @ts-check
import { defineFlatConfig, imports, vue } from '@bassist/eslint-config'
export default defineFlatConfig([
...imports,
...vue,
// 添加更多自定义配置
{
// 为每个配置提供名称,以便在运行 `npm run lint:inspector` 时,
// 可以在可视化工具中清晰展示
name: 'my-custom-rule/vue',
rules: {
// 例如:默认情况下,该规则是 `off`
'vue/component-tags-order': 'error',
},
ignores: ['examples'],
},
])
在 VS Code 工作区的 settings.json
添加以下配置,以启用自动 Lint 修复:
{
"editor.formatOnSave": true,
"eslint.useFlatConfig": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always",
"source.fixAll.prettier": "always"
}
}
定义 ESLint 配置,可选支持 Prettier 和 Tailwind CSS。
API 类型声明:
/**
* 定义 ESLint 配置,可选支持 Prettier 集成。
*
* @param configs 基础 ESLint 配置数组。
* @param options - 配置选项。
*
* @returns 最终的 ESLint 配置数组。
*/
declare const defineFlatConfig: (
configs: FlatESLintConfig[],
options?: DefineFlatConfigOptions,
) => FlatESLintConfig[]
选项类型声明:
interface DefineFlatConfigOptions {
/**
* 指定用于加载 `.prettierrc` 配置的工作目录。
*
* 配置文件应为 JSON 格式。
*
* @default process.cwd()
*/
cwd?: string
/**
* 如果 `prettierEnabled` 设为 `false`,则所有与 Prettier 相关的规则和配置都将被忽略,
* 即使提供了 `prettierRules` 也不会生效。
*
* @default true
*/
prettierEnabled?: boolean
/**
* 默认情况下,会从当前工作目录读取 `.prettierrc`,并且 `.prettierrc` 文件必须是 JSON 格式。
*
* 如果配置文件不是 JSON 格式,或者使用了不同的文件名,可以将其转换为 JSON 规则后传入。
*
* 读取自定义配置后,会与默认的 ESLint 规则合并。
*
* @see https://prettier.io/docs/configuration.html
*/
prettierRules?: PartialPrettierExtendedOptions
/**
* Tailwind CSS 规则默认启用。如果它们影响了项目,可以通过该选项禁用。
*
* @default true
*/
tailwindcssEnabled?: boolean
/**
* 如果需要覆盖 Tailwind CSS 配置,可以传入相应的选项。
*
* 如果想要合并配置,可以导入 `defaultTailwindcssSettings`,手动合并后再传入。
*
* 如果传入空对象 `{}`,则会使用默认设置。
*/
tailwindcssSettings?: TailwindcssSettings
}
createGetConfigNameFactory
是一个灵活的工具函数,用于生成 ESLint 配置命名工具。它可以快速拼接配置名称,确保命名空间一致,并便于组织和管理复杂的规则集。
API 类型声明:
/**
* 一个灵活的工具函数,用于生成 ESLint 配置命名工具。
* 它可以快速拼接配置名称,确保命名空间一致,并便于组织和管理复杂的规则集。
*
* @param prefix - 表示配置名称前缀的字符串。
* @returns 一个函数,该函数会将提供的名称片段与指定的前缀拼接在一起。
*/
declare const createGetConfigNameFactory: (
prefix: string,
) => (...names: string[]) => string
使用示例:
import {
createGetConfigNameFactory,
defineFlatConfig,
} from '@bassist/eslint-config'
const getConfigName = createGetConfigNameFactory('my-prefix')
export default defineFlatConfig([
{
name: getConfigName('ignore'), // --> `my-prefix/ignore`
ignores: ['**/dist/**', '**/.build/**', '**/CHANGELOG.md'],
},
])
为什么要使用它?
这个工具在构建可复用的 ESLint 配置或维护复杂项目的规则集时尤其有用。
这些是一些常用的配置,如果有额外需求,欢迎提交 PR!
格式化规则默认启用,不会单独导出。如需自定义配置,请通过 defineFlatConfig API 的 options
传入。
.prettierrc
和 .prettierignore
的内容,并添加到 ESLint 规则中。tailwind.config.js
作为 Tailwind CSS 配置文件传入。--ext
CLI 选项已被移除 (#16991) 。详细更新内容请参考 CHANGELOG 。
2025-02-16 00:42:33
五年前买的阿里云 ECS 这个月底到期,前天准备续费的时候发现买个新的更划算,不仅价格差不多,还多了 1GB 内存,那还续个屌…… 服务器上要迁移的东西不多,影响不大,所以就直接买个新的了。
由于老的服务器上部署的大多是前端项目(数据是连 Serverless 的 API 操作的,不在这台机器上),并且基本都是用 Docker 部署的,所以迁移工作都比较简单,在源码仓库上修改 Workflow 的目标机器 IP 和 SSH Key ,重新运行一次 CI 打包,就可以把新的镜像推送到新的服务器上了。
其他的像 SSL 证书, Nginx 配置,都是拷贝过去后重启一下 Nginx 就搞定,等服务都起来了,去 DNS 解析那里把域名指向新机器的 IP 就迁移完了,都问题不大。
除了有一个 Nest 服务,因为连了 SQLite ,迁移后出现了一点问题。
问题倒不是出在 SQLite 上,用 Docker 连接这种嵌入式数据库,都是通过 Volumes 挂载到宿主机器上的,所以访问的数据库文件路径是宿主机器上的路径,知道了这一点,把 SQLite 数据迁移到新服务器上也很简单,并且在新机器上直接用 SQLite 查询数据,也都没问题。
至于在 Docker 里使用 SQLite 本身,只需要在 Dockerfile 里,在安装 libc6-compat 的时候记得一起安装 sqlite 就可以。
# Dockerfile
# Use the official Node.js image as the base image
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat sqlite
# Set the working directory inside the container
WORKDIR /app
# ...
但是 Docker 容器运行后,访问接口却挂了,通过 docker logs
查询容器的日志,发现启动后这里有个报错:
[Nest] 1 - 02/15/2025, 1:23:01 PM LOG [InstanceLoader] ScheduleModule dependencies initialized +1ms
[Nest] 1 - 02/15/2025, 1:23:01 PM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (1)...
Error: Could not locate the bindings file. Tried:
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/build/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/build/Debug/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/build/Release/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/out/Debug/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/Debug/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/out/Release/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/Release/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/build/default/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/compiled/18.20.6/linux/x64/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/addon-build/release/install-root/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/addon-build/debug/install-root/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/addon-build/default/install-root/better_sqlite3.node
→ /app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/lib/binding/node-v108-linux-x64/better_sqlite3.node
at bindings (/app/node_modules/.pnpm/[email protected]/node_modules/bindings/bindings.js:126:9)
at new Database (/app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/lib/database.js:48:64)
at BetterSqlite3Driver.Database [as sqlite] (/app/node_modules/.pnpm/[email protected]/node_modules/better-sqlite3/lib/database.js:11:10)
at BetterSqlite3Driver.createDatabaseConnection (/app/node_modules/.pnpm/[email protected][email protected][email protected][email protected]_@[email protected][email protected]_/node_modules/typeorm/driver/better-sqlite3/BetterSqlite3Driver.js:88:41)
at async BetterSqlite3Driver.connect (/app/node_modules/.pnpm/[email protected][email protected][email protected][email protected]_@[email protected][email protected]_/node_modules/typeorm/driver/sqlite-abstract/AbstractSqliteDriver.js:171:35)
at async DataSource.initialize (/app/node_modules/.pnpm/[email protected][email protected][email protected][email protected]_@[email protected][email protected]_/node_modules/typeorm/data-source/DataSource.js:136:9)
[Nest] 1 - 02/15/2025, 1:23:04 PM ERROR [TypeOrmModule] Unable to connect to the database. Retrying (2)...
Error: Could not locate the bindings file. Tried:
在 Node 服务端程序连接 SQLite 是用了 better-sqlite3 这个库,它是 Node.js 中速度最快、最简单的 SQLite 库,在 Nestjs 里也是支持用 TypeORM 来基于这个库操作 SQLite 。
和普通的依赖包直接引入 dist 产物开箱即用不一样,它还需要编译一次原生绑定文件,默认情况下,它会尝试在安装时自动构建原生模块,对比本地在 node_modules 里的目录文件,和 npmjs 上的发布文件列表,会发现线上的发布版少了 better_sqlite3.node
这个文件,提示的报错信息也是少了这个文件。
在 better-sqlite3 的 package.json 里,可以看到 install
脚本就是执行这个安装后编译 Node.js 模块的操作。
{
"scripts": {
"install": "prebuild-install || node-gyp rebuild --release",
"build-release": "node-gyp rebuild --release",
"build-debug": "node-gyp rebuild --debug",
"rebuild-release": "npm run lzz && npm run build-release",
"rebuild-debug": "npm run lzz && npm run build-debug",
"test": "mocha --exit --slow=75 --timeout=5000",
"benchmark": "node benchmark",
"download": "bash ./deps/download.sh",
"lzz": "lzz -hx hpp -sx cpp -k BETTER_SQLITE3 -d -hl -sl -e ./src/better_sqlite3.lzz"
}
}
这个 npm install
脚本,是 npm 的 生命周期 之一,当执行 npm install 时触发(其它包管理器如 pnpm install 也会触发)。如果 npm 包的根目录下有一个名为 binding.gyp 的文件,当没有自定义 install 或 preinstall 脚本时, npm 将默认使用 node-gyp rebuild 命令对 binding.gyp 进行编译。
node-gyp 是 Node.js 官方提供的跨平台命令行工具,用于编译 Node.js 的原生插件模块。
可以看到 better-sqlite3 的根目录下,也是存在一个 binding.gyp 文件,所以在 install 依赖的时候,better-sqlite3 会尝试使用 prebuild-install
来下载已编译好的二进制文件,如果没有找到匹配的文件,则会退回使用 node-gyp rebuild --release
来手动编译源代码。
better-sqlite3 这行 install 命令的意思是:
prebuild-install:
node-gyp rebuild --release:
但以上虽然是理论上的预期方案和兜底方案,但不知道为什么在 CI 机器上居然都没有执行成功,导致最后缺少了这个编译好的二进制文件。
既然确认问题就是因为少了这个编译的 Node.js 模块,那么可以尝试手动 build 一下,主动生成 better_sqlite3.node
文件。
因此在 Dockerfile 里,通过 pnpm i
之类安装项目依赖这一步的后面,添加如下代码:
# Dockerfile
# ...
RUN apk add --update --no-cache python3 build-base gcc && ln -sf /usr/bin/python3 /usr/bin/python
RUN cd node_modules/better-sqlite3 && pnpm build-release
# ...
手动安装编译需要的依赖,并手动执行 better-sqlite3 的构建脚本,主动生成运行程序需要的 better_sqlite3.node
文件,然后就一切恢复正常了!
之前 CI 构建没问题的时候是基于 Ubuntu 22.04.5 ,现在构建失败的时候是基于 Ubuntu 24.04.1 ,只能说 CI 机器的新系统环境少了一些预装的依赖,导致 node-gyp build 没有执行成功。
这次自己的主服务器迁移是从旧机器的 CentOS 7 迁移到新机器的 Debian 12 ,在选择 Debian 之前,还一度先装了 Ubuntu 24 ,然后看到一些建议说作为主服务器还是稳定优先,建议用 Debian ,所以就重新装了系统。
没想到话音刚落就在 CI 机器踩了 Ubuntu 的坑,害,对 Linux 这些系统版本不太熟,还是得多多学习呀。
2025-01-28 02:45:58
本来这篇文章的标题按惯例应该是《年终总结:2024 年的一些回顾和 2025 年的一些小规划》 ,但 2024 年刚好也是我开始独立博客的第十年,想顺便回顾一下十年时间自己经历和变化,所以换了这个不起眼的标题哈哈哈。
最初也想过用《独立博客的十年》或者是一些其它类似的就事论事的标题,但总觉得不够好,后来想到这十年时间刚好覆盖了我从一个产品运营到前端开发工程师的转变、从大厂光鲜到创业公司更好玩的从容、从广州去深圳又回到广州的生活见识,尽管工作和生活经历了很多变化,但对我个人的内心深处来说,似乎没有受到过多的影响,始终知道自己喜欢的是什么,不会因为外界的干扰而改变。
所以最后脑海里总是停留在《英雄本色》这部电影里,不论是它的中文名,还是英文名《A Better Tomorrow》,或者是它的主题曲《当年情》,都感觉很符合我想要的那种感觉,所以最终才决定用《本色十年》这个标题。
随着年龄的增长,我越来越觉得所谓的本色,就是一个人内心深处最真实的样子,不会因为外界的干扰而改变,也不会因为时间的流逝而改变。
十年前,也就是 2014 年,我的好友吴庸吴老师在 他的博客 上给我加了一个友情链接,他给我配的文案是:
诗人、贝斯手、出色的厨子、编辑、切得一手好图的前端、曾经放荡过的旅行家
十年后,2024 年,这个文案还在他博客上挂着,挂了整整十年。
再看看我在 2023 年刚入职现在这家公司的时候(飞牛私有云 fnOS),在公司同学录里的自我介绍:
纹了一条花臂,钟爱 Blackwork Tattoo 风格,第一个文身是我的琴;
养了三只猫,从 2016 年到现在,超粘人,喜欢抱着我的花臂睡觉;
自己跟自己玩的贝斯手,常用五弦的 MusicMan Neck-Through Bass;
从 2018 年留长发至今,已过肩快及腰,喜欢听摇滚乐 / 新金属 / 核;
家庭主厨,小红书的潮汕美食博主 @底迪 ,擅长粤菜和潮汕菜。
虽然有点变化,但不多,什么都可以变,但热爱的东西不变。
情绪和性格方面,这十年过得还算乐观,依然是个内向的人,依然独来独往,至于优点和缺点,好像也是维持了至少十年前的状态没啥变化。
优点嘛,想了想,比如:会做饭、喜欢做家务、能坚持文字阅读、情绪还算稳定、做事还算细心… 都是一些独处的能力?好像也都是一些只要是个人都可以学会的东西…
缺点倒是挺多的,比如:不会抽烟、不会喝酒、不会打牌、不会打麻将、不会桌游、不会唱歌、不会打篮球、不会踢足球、不会炒股、不爱八卦、不会开摩托、不会骑电动车、不会开车… 相对于新时代对一个普通人的要求,我好像什么都不会,可以说社交方面还真的就蛮需要这些技能的。
要学的话貌似也不难,但主要的阻力是自己不愿意,因为做自己不喜欢的事情很痛苦,就拿开车来说,因为从小家里很穷,出行只有自行车,后来有机会坐车的时候都是从潮州开到广州的大巴车,每次都几乎坐到吐,很害怕车的味道,打车有时候也会遇到那种味道,说不上来是什么味,很难受,心理阴影面积很大,所以一直到现在,我都不喜欢坐车,每次坐进车里还没开就会有一种心理排斥。
洪金宝在《奇谋妙计五福星》里的这段台词,简直就是为我量身定做,笑死。
很多人转码农都是基于 “混口饭吃” ,说直白点就是趁年轻多赚点,仅此而已,但我是在考虑很久尝试很久确定自己是真的喜欢才转行的。
在做产品运营的时候,最早是为了在拿不到排期的时候能解决自己的需求上线而尝试自己实现,写着写着感觉做前端挺有意思的,又从前端慢慢接触到其它更多的领域,读了很多计算机大佬的故事,并且也看着很多前辈都是五六十岁还在写代码,感受到如果真的喜欢,这就是一个能玩一辈子的事情,计算机的世界太广阔了,想怎么玩都行。
最重要的是:这一行很适合我这种独来独往的内向人士,不像以前要出差、要去接触各种玩家、媒体,反正只要自己乐意,可以从起床直接写代码写到睡觉,不用跟什么人打交道!
我在知乎上回答过两个关于职业咨询的问题,有几段话虽然是对提问者说的话,但实际上也是在人生十字路口的时候会对自己说的话。
一个是关于是否要转岗的:
在 “大转岗” 这个事情上面,单纯的喜欢是远远不够的,如果想在某个岗位走的更深更远,靠着一份 “喜欢” ,是支撑不了你很多年的,你至少需要上升到 “热爱” 这个层次。
我这里的 “大转岗” 是指 “产品转运营” / “运营转技术” 这种直接脱离原来核心能力的转岗; “小转岗” 一般是 “社区运营转直播运营” / “内容运营转新媒体运营” 这种原来的经验还可以大幅度复用的转岗。
“小转岗” 很正常,但是 “大转岗” ,大部分人其实都不会有很多次大转岗的机会,因为:虽然说种一棵树最好时间是十年前,其次是现在,但是这棵树要从树苗长成大树,它是需要时间的,如果没有足够的热爱去支撑你不断提升自己,那么很可能两三年就觉得又想换个岗位做一下,等到你毕业 10 年了,人家在那个岗位上已经是个 10 年经验的大佬,而你在当前的岗位,可能依然是一个只有 2、3 年经验的中级专员或工程师。
就像我喜欢某类型的电影,我可能就是那段时间觉得很喜欢而已(曾经漫威电影必看,到现在压根不看了);但是我热爱的事情,比如摇滚乐、下厨、养猫,这些事情能够让我从十几岁到现在,还是十年如一日的保持着高度的热情。
另一个是如何选择适合自己的公司:
这种工作内容拖久了,实际上对你下一份工作所需要的经验沉淀、业绩沉淀,起不到什么帮助,工作越久,需要的工作经验是深耕,而不是广而不精。
目前你还有一个优势是,已经回到了家乡,哪怕今年疫情影响工作比较难找,但是家在身边,总归比其他人能撑得住,我认真建议你先别着急接一些奇奇怪怪的 Offer ,好好考虑一下自己的兴趣和未来的发展方向。
做自己喜欢的事情是最好,哪怕有时加班到半夜,也会是一种目标接近完成的兴奋感,而不是说好烦啊怎么还没搞完我不想上班了的丧。
特别是那句 “哪怕有时加班到半夜,也会是一种目标接近完成的兴奋感” ,最近一年在狂赶 fnOS 的需求时,总会在开发完的时候冒出来和我击掌。
这十年来影响过我帮助过我的人很多,展示一个 Acknowledgement 在这里肯定放不下哈哈哈哈,我主要单独提一下对我在 “入门、成长、坚持” 这三个阶段影响比较大的人,没有提及的大佬们请不要介意,我一样心存感激!
“入门” 阶段的影响力应该属于初代淫贼三人组…… (后面不同时期有不同的淫贼 N 人组… )
插个词语释义: “淫贼” 是我对好友们的昵称,代表这个人人品端正、性格随和、乐观有趣、落落大方、有自己的独立人格、开得起玩笑、不会过于严肃、在一定程度上聊得来,是一个非常褒义的词语哈哈哈!
三人组分别是:产品大佬吴庸吴老师、技术大佬振权(网名 phpbug )和家辉(是的,真的姓张!)。
2014 年那会因为一些项目合作,和他们仨对接很频繁,也因为他们当时都有自己的独立博客,在他们的影响下我也尝试自己搞了起来。
在此之前完全没搞过自己的网站哈哈哈哈,也是第一次购买了自己的域名,学着很多技术大佬那样,实名制走江湖(像:阮一峰 ruanyifeng.com 、张鑫旭 zhangxinxu.com ),所以也用了自己的姓名拼音注册了域名,幸亏当时用了自己的名字,不然这些年各种中二的网名改来改去都不知道该叫什么了…
还记得最早是用新浪的 SAE 托管的,后来越来越慢,而且免费用户极度不友好,就逐步迁移到阿里云用到现在(在 2018 年迁移后的第一篇博客 《 世界,您好! 》 有说过这个事情,刚看了一下,当时竟然还是用 Windows 做的服务… 不堪回首),说来这次迁移可以说是绝对正确的选择,现在工作的服务也都是阿里云的,契合度 100% 。
在成长阶段里,前端大佬丰神对我的影响很大,除了请教过他不少问题外,他在我刚起步的时候对我说过一句话印象特别深刻,那就是 “不要只学会实现功能,还要了解实现原理” 。
那个时候我刚好处于 ”想实现 A 功能,就去搜包含 A 功能的 demo ,改改代码放到自己的网页上跑起来“ 的阶段,功能实现是实现了,但不知道为什么就实现了,所谓的代码能跑就行。
这句话在自学的过程中对我影响很大,当了解了实现原理之后,就会懂得如何举一反三去做更多的东西!哪怕没有亲自写过的也能知道个大概,以后遇到类似功能也有印象应该往哪个方向去查资料。
另外还有小毅 @chawyehsu ,当我还在用 jQuery 写 HTML 页面的时候,跟我分享了 Vue.js ,也就从那个时候开始慢慢知道了还有 Node.js 、 Webpack 等前端工程化的一些东西,以及来自 React / Vue 在当年完全没接触过的全新开发模式,还有不知道从哪年开始被他影响了开始在 GitHub 上活跃,在开源上真的学到了很多在公司里学不到的东西!(Btw: 他在 GitHub 也很活跃,熟悉很多开发语言,目前休息 ing ,年后有公司 OR 猎头挖人的话可以联系聊聊!)。
这一点我要感谢从小影响我长大的黄家驹先生和 Beyond 乐队,他们的歌给人努力、乐观、坚强的感染力,并且人生真的没有污点、一直言行一致地传达着积极向上的精神。
就像《Beyond日记之莫欺少年穷》的这个片段(右下角可以先暂停 BGM 再看)。
还有之前某天在凌晨三点多的时候,想起小时候看过的一个香港广告《生有限 活无限》,凭着记忆里的画面关键词搜了出来,竟然是 2000 年拍的,可以说是最喜欢的一个广告片,整整 24 年都没有忘记里面的画面和文案。
回来讲讲我的 2024 年,虽然这一年很忙,但还是偶尔在 GitHub 上提交一些有的没的,毕竟之前开源的项目也有一些用户反馈,时不时跟进一下,另外主要就是对博客做了一次改版,这一波也是贡献了不少活跃度在里面(截图生成自 GitHub Contributions )。
前段时间还在博客上线了一个 开源项目 的栏目,记录了一些由我创建或维护的项目,虽然没有大型项目,但有一些教程或者工具的受欢迎程度还可以,如果觉得不错,欢迎点个 Star 支持一下!
其他的事情今年没什么时间搞,主力还是在开发公司的 飞牛私有云 fnOS 的 Web 生态,可能很多朋友在公测期间就已经用上了,我家里的 NAS 也是装着我们的系统,用自己开发的作品影响着自己的生活!
如果不了解 NAS ,也可以看我之前写的《 千元预算组装入门 NAS 设备 分享 NAS 的硬件基础知识 》 一文。
刚好放假前用 git-fame 跑了一下代码贡献度,发现我竟然是贡献度最高的,有点惊喜啊哈哈!不过作为 Core Team 的第一批成员,确实参与到了很多需求里,也学到了很多东西,感谢团队!
这个产品 2025 年会正式上线,到时候欢迎大家来体验!
关于 2024 年和独立博客的十周年回顾,就写到这里吧,祝大家新年快乐!
2024-11-25 23:58:02
最近博客改版也顺便改了部署方式,页面访问也检查了重定向配置等等,看起来似乎没什么问题,但还是收到了一个反馈 RSS 订阅源报错的情况( issue 见 #370 ,订阅源见 feed.xml ,感谢 @AsanZhang 的反馈 )。
反馈在 RSS 聚合软件里提示订阅报错了,我自己也尝试了确实不行,奇了怪了!
在浏览器直接访问 XML ,发现 Network 里 Failed 了,控制台还报了个错误信息:
net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK)
截图如下:
这个报错有点眼熟啊!想起前段时间给博客加搜索功能的时候,一开始想做全文搜索,结果部署后也遇到类似的报错,本地 build 完预览没事,线上就跪了。
但那次因为是把所有文章都处理到一个 JSON 文件里,但因为文章里有很多代码块等内容,引起问题的原因比较多,例如可能破坏了数据结构、文件本身也很大,所以做搜索的时候最后决定去掉全文,改成了只搜标题,解决了当时遇到的问题,但没想到在 RSS 这里还是遇到类似的情况了。
那会还在本地 Docker 部署对比了,但本地也正常,愣是没怀疑到线上多了一层 Nginx 可能是个坑。
由于对 Nginx 并没有过多的深入使用,常年处于基础的转发配置阶段,所以直接请教 GPT 帮我解决。
原来的配置是这样子,比较早期的默认配置:
server_names_hash_bucket_size 128;
client_header_buffer_size 32k;
large_client_header_buffers 4 32k;
client_max_body_size 50m;
sendfile on;
tcp_nopush on;
keepalive_timeout 60;
tcp_nodelay on;
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
fastcgi_buffer_size 64k;
fastcgi_buffers 4 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 256k;
按照 GPT 的描述, proxy_buffer_size
、 proxy_buffers
和 proxy_busy_buffers_size
这些参数用于调整代理缓冲区的大小, fastcgi_buffer_size
、 fastcgi_buffers
和 fastcgi_busy_buffers_size
这些参数用于调整 FastCGI 缓冲区的大小,另外还建议我新增 Proxy 缓冲区相关配置。
打开 nginx.conf 文件,修改配置如下:
fastcgi_connect_timeout 300;
fastcgi_send_timeout 300;
fastcgi_read_timeout 300;
-fastcgi_buffer_size 64k;
+fastcgi_buffer_size 128k;
-fastcgi_buffers 4 64k;
+fastcgi_buffers 4 128k;
-fastcgi_busy_buffers_size 128k;
+fastcgi_busy_buffers_size 256k;
-fastcgi_temp_file_write_size 256k;
+fastcgi_temp_file_write_size 512k;
+proxy_buffer_size 128k;
+proxy_buffers 8 128k;
+proxy_busy_buffers_size 256k;
+proxy_temp_file_write_size 512k;
调整完这些配置后,重启 Nginx 服务以应用更改。
sudo nginx -s reload
现在确实解决了,成功订阅!
2024-11-10 01:34:05
最近对博客进行了一次技术栈迁移,其中对 Markdown 的解析渲染支持也从 Markdown-it 系列迁移至 Unifiedjs 系列,在 Unified 的工作流程里,又包含了处理 Markdown 的 Remarkjs 系列以及处理 HTML 的 Rehypejs 系列。
在博客里, Markdown Parser 的整个工作流程都是自己管理的,包括不同结果的输出,例如:提供给 RSS 订阅用的 HTML ,提供给列表和搜索用的 Metadata ,以及提供给详情页作为 React 组件渲染内容用的 JSX ,这些过程并不算复杂,事实上进展确实是很顺利,但是在我以为即将大功告成之际,突然发现渲染出来的内容少了一个东西:我的视频呢?
改版之前是使用 Markdown-it 作为技术栈, Markdown 代码与 HTML 代码的相处非常和谐,对于没有 Markdown 原生语法支持的 HTML 标签,都可以直接编写 HTML 代码进行渲染,内嵌视频最初就是这样子实现的。
像这样,在 Markdown 里直接编写 HTML 代码,即可直接输出 HTML 。
<video
src="https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4"
poster="https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1"
title="山景房里的三只猫"
controls
preload="auto"
/>
但改版后,原本应该渲染为视频的地方,都只剩下一个 <p></p>
标签,很明显是在 Markdown 代码转换过程中被过滤了。
像我这种 Parser 流程比较长,中间处理环节还是动态变化的情况,很怕这些奇怪的问题,但还好 Unified 的设计非常清晰,先了解一下实现原理,更方便找到问题的原因。上面提到了 Unified 包含了 Remark 和 Rehype 两个系列的工作流,因为在 Unified 生态的工作过程中,都是基于 AST 语法树工作,可以简单地理解为:
以上流程可以反过来,也就是先处理 HTML 再还原为 Markdown ,如果是这种流程,中间插件需要更换为 rehype-remark 。
名词解释:
MDAST —— Markdown Abstract Syntax Tree , Markdown 抽象语法树
HAST —— Hypertext Abstract Syntax Tree ,超文本抽象语法树
了解了工作流程,就可以分三个阶段排查问题了,要么就是在 Remark 环节把 HTML 代码屏蔽了,要么就是 Rehype 环节有问题,要么就是中间的 AST 转换抛弃了这部分代码。
此时 Parser 里的处理器插件是这么启用的:
const processor = unified()
.use(remarkPlugins) // Markdown to MDAST
.use(remarkRehypePlugins) // MDAST to HAST
.use(rehypePlugins) // HAST to HTML
.use(reactPlugins) // HTML to JSX
const file = await processor.process(markdown)
这里的每一个 Plugins 变量都是一个数组,会根据我的构建场景动态启用插件(相关源码见:core/parser ),例如:
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
import remarkStringify from 'remark-stringify'
import { type PluggableList } from 'unified'
const remarkPlugins: PluggableList = [
[remarkParse], // e.g. [plugin, pluginOptions]
[remarkGfm],
[remarkStringify],
]
因此先仅启用 remarkPlugins ,发现一切正常,继续启用 remarkRehypePlugins ,就出问题了, Markdown 里的 <video />
标签被过滤了。
所以我在 remark-rehype 的文档里找到了关于 HTML 标签过滤的说明:
因为在 markdown 中支持 HTML 是一项繁重的任务(性能和包大小) ,而且并不总是需要的,要同时使用两者,您还必须配置
allowDangerousHtml: true
选项。 —— 详见 When should I use this?
原因被定位到了就很好解决,目前是找到了这些解决方案,可以根据需要处理。
根据 remark-rehype 的文档,仅需开启该选项即可支持将 Markdown 里的 HTML 代码作为半标准节点嵌入 HAST 中 raw
。
import remarkRehype from 'remark-rehype'
import { type PluggableList } from 'unified'
const remarkRehypePlugins: PluggableList = [
[remarkRehype, { allowDangerousHtml: true }],
]
注意:除了该插件需要开启该选项之外,像我的博客还使用了 rehype-stringify 插件,它也需要一起开启该选项。
由于我还使用了 rehype-sanitize 用于对 HTML 内容的清理,因此仅开启该选项在我的博客里并不能直接达到目的,还要在 Sanitize 进行放行,并且平时写 React 组件的习惯上,我对 dangerouslySetInnerHTML 的使用非常克制,有一些代码洁癖让我不喜欢这个方案,因此我放弃了它。
在 remark-rehype 的文档里,描述 allowDangerousHtml
部分提及到了另外一个插件: rehype-raw 。
这个插件很适合希望支持渲染嵌入在 Markdown 里的 HTML(需要传递 allowDangerousHtml: true
给 remark-rehype ),它可以获取 Markdown 里的 HTML 字符串并将它们作为实际节点包含到 HAST 中。
在开启 allowDangerousHtml 选项时, Markdown 里的 HTML 代码仅作为半标准节点嵌入 HAST 中 raw
属性,但配合这个插件,可以将原始的 HTML 字符串解析为标准的 HAST 节点。
处理过程需要依赖一个完整的 HTML 解析器(详见 parse5 ),它将完全按照浏览器解析的方式重新创建抽象语法树,同时保持原始数据和位置信息完好无损。
注意在使用过程中的插件顺序:
import remarkRehype from 'remark-rehype'
import rehypeStringify from 'rehype-stringify'
import rehypeRaw from 'rehype-raw'
import { type PluggableList } from 'unified'
const remarkRehypePlugins: PluggableList = [
[remarkRehype, { allowDangerousHtml: true }],
]
const rehypePlugins: PluggableList = [
[rehypeStringify, { allowDangerousHtml: true }],
[rehypeRaw],
]
这个方案处理过程比较繁重,但这是支持不受信任内容的唯一方法,除非类似那种内容完全由用户提交的场景,否则在内容可控的场景下,都不推荐使用这个方案。
这是一个最轻巧的解决方案,几乎没有多余的处理成本。
因为我的博客文章详情页最终是通过 JSX 进行渲染(可参考 markup/renderer ),因此完整的处理过程是:Markdown > MDAST > HAST > HTML > JSX
,在最后一个环节使用 rehype-react 的时候,可以将 HTML 代码转换为 React 组件需要的 JSX 代码。
import rehypeReact, { type Options as RehypeReactOptions } from 'rehype-react'
import { a, img } from './components'
import { Fragment, jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'
import { type PluggableList } from 'unified'
const components = {
a, // e.g. `<a />` --> Next.js `<Link />`
img, // e.g. `<img />` --> Next.js `<Image />`
} as unknown as RehypeReactOptions['components']
const rehypeReactOptions = {
Fragment,
components, // e.g. Record<tagName, componentName>
ignoreInvalidStyle: true,
jsx,
jsxs,
passKeys: true,
passNode: true,
development: false,
} satisfies RehypeReactOptions
const reactPlugins: PluggableList = [[rehypeReact, rehypeReactOptions]]
所以我想到了一个方案,使用 Markdown 内置的图片语法,将视频链接放在原本需要放图片链接的位置,然后在转 JSX 的过程中,判断 URL 结尾的扩展名将视频 URL 分配给 Video 组件。

// components.tsx
const video = async (props: React.VideoHTMLAttributes<HTMLVideoElement>) => {
// 组件里的其它逻辑
// ...
return <video {...props} />
}
export const img = async (props: React.ImgHTMLAttributes<HTMLImageElement>) => {
// 判断常用的视频文件扩展名,将其转发给视频组件渲染
if (props?.src?.endsWith('.mp4')) {
return <video {...props} />
}
// 组件里的其它逻辑
// ...
return <img {...props} />
}
事实上在文章详情里实现很完美,但我考虑到了 RSS 订阅源里的 HTML 代码并没有得到解决,并且这种方式无法配置视频的 poster
属性,所以这个方案也被我放弃了。
这个方案是在 GitHub Remarkjs Discussions 里搜索时找到了几个讨论,提供了很棒的灵感!
Remark 提供了这方面的插件支持,仅需安装 remark-directive 插件,这是对 Markdown 指令语法提案 的实现(这个提案很有意思,值得阅读!),可以使用和 Markdown 十分接近的语法实现一些自定义功能。
看到这里的读者应该不会陌生,很多知名的静态生成器项目都支持自定义指令,例如:
简单说一下实现方案,最终是通过编写一个 Remark 插件实现自定义指令,以 :::video
的语法,在 Markdown 内容里配置视频的 src
、 poster
、 title
属性。
源码在 plugins/remark-video ,这里贴的代码在未来可能会有变化。
先安装依赖,由于不需要在运行时使用,所以统一安装到 devDependencies
里。
pnpm add -D remark-directive unist-util-visit @types/mdast
这些插件的作用:
插件 | 作用 | 写本文时使用的版本号 |
---|---|---|
remark-directive | 添加对通用指令的支持 | ^3.0.0 |
unist-util-visit | 遍历 AST 语法树节点,导出了一个 visit 方法 |
^5.0.0 |
@types/mdast | 为 TypeScript 提供插件主要参数的类型 | ^4.0.4 |
考虑到需要配置的参数如 src
和 poster
的 URL 都比较长,用 leafDirective
语法会比较难维护,因此选择了 containerDirective
语法,按照约定,从上往下分为三行内容,分别是 src
、 poster
以及 title
:
:::video
https://example.com/video-src.mp4
https://example.com/video-poster.jpg
A video title
:::
其他的视频播放器属性,由指令插件统一管理,因此不需要在 Markdown 里自定义配置。
按照 README 的例子,很快就能编写一个自定义插件了,这里就不赘述具体的过程,看代码和注释即可。
import { type Root } from 'mdast'
import { visit } from 'unist-util-visit'
import { isArray, isObject, isString } from '@bassist/utils'
// For the `src` and `poster` attributes
interface LinkNode {
type: 'link'
url: string
children?: unknown[]
[key: string]: unknown
}
// For the `title` attribute
interface TextNode {
type: 'text'
value: string
[key: string]: unknown
}
interface VideoDirectiveNodeChildren {
children: (TextNode | LinkNode | unknown)[]
}
interface VideoDirectiveNode {
type: 'containerDirective'
name: 'video'
children: VideoDirectiveNodeChildren[]
[key: string]: unknown
}
interface HyperScriptData {
hName?: string
hProperties?: {
[key: string]: unknown
}
[key: string]: unknown
}
/**
* With container directive
*
* @example
*
* ```md
* :::video
* src
* poster
* title
* :::
* ```
*/
const isVideoNode = (i: unknown): i is VideoDirectiveNode => {
if (!isObject(i)) return false
const children = i?.children?.[0]?.children
return (
i.type === 'containerDirective' &&
i.name === 'video' &&
isArray(children) &&
children.length > 0
)
}
const isLinkNode = (i: unknown): i is LinkNode => {
return isObject(i) && i.type === 'link' && isString(i.url) && !!i.url
}
const isTextNode = (i: unknown): i is TextNode => {
return isObject(i) && i.type === 'text' && isString(i.value) && !!i.value
}
const isValidChildNode = (i: unknown) => isLinkNode(i) || isTextNode(i)
/**
*
* @description
*
* I have customized a compilation process in Markdown Parser,
* so not all HTML codes are allowed to be rendered.
*
* When Markdown is being converted to AST,
* many HTML tags will be discarded, and the same is true for Video.
*
* In order to uniformly implement custom rendering content,
* this plugin implements the ability of `video` directive.
*
* One more important thing, since rehype-sanitize is enabled,
* remember to configure the options to allow
* rendering of video tags and attributes.
*
* @example
*
* Enter the following into the markdown file:
*
* ```md
* :::video
* https://example.com/video-src.mp4
* https://example.com/video-poster.jpg
* A video title
* :::
* ```
*
* Compile and output a Video tag:
*
* ```html
* <video
* src="https://example.com/video-src.mp4"
* poster="https://example.com/video-poster.jpg"
* title="Hello World"
* />
* ```
*
* @returns Transformer
*/
const remarkVideo = () => {
return (tree: Root) => {
// Prevents the following judgment from being inferred as never
visit(tree, (node: unknown) => {
if (!isVideoNode(node)) return
const [srcNode, posterNode, titleNode] = node.children[0].children
.map((i) => {
if (isLinkNode(i)) return i
if (isTextNode(i)) {
i.value = i.value.replace(/\n/g, '').trim()
if (i.value) return i
}
return undefined
})
.filter(isValidChildNode)
const src = srcNode.url
const poster = posterNode.url
const title = titleNode.value
const data = (node.data || (node.data = {})) as HyperScriptData
data.hName = 'video'
data.hProperties = {
src,
poster,
title,
controls: true,
preload: 'auto',
className: 'w-full aspect-video rounded-lg',
}
})
}
}
export default remarkVideo
在我的博客项目里,是在 core/parser 里启用插件(也就是最终提供给 unified().use()
使用 ),在使用的过程中,如果启用了另外一个 rehype-sanitize 插件,还需要在该插件的选项里配置 tagNames
和 attributes
的白名单列表。
import remarkDirective from 'remark-directive'
import remarkVideo from './plugins/remark-video'
import { type PluggableList } from 'unified'
const remarkPlugins: PluggableList = [
[remarkParse],
[remarkDirective],
[remarkVideo],
]
const rehypePlugins: PluggableList = [
// ...
[
rehypeSanitize,
{
// No need `user-content-` prefix
clobberPrefix: '',
// https://github.com/syntax-tree/hast-util-sanitize#tagnames
tagNames: [...toArray(defaultSchema.tagNames), 'video'],
// https://github.com/syntax-tree/hast-util-sanitize#attributes
attributes: {
...(defaultSchema.attributes || {}),
video: ['src', 'poster', 'controls', 'preload', 'className'],
},
},
],
// ...
]
// ...
这就是这段 Markdown 指令渲染出来的效果(当然,不包括下面的标题展示,那是我另外包裹了一层 figure
标签,详见 parser/components ,在转换为 JSX 的时候处理的)。
:::video
https://cdn.chengpeiquan.com/video/my-cats-in-mountain-view-room.mp4
https://cdn.chengpeiquan.com/img/2022/12/20221231235941.jpg?x-oss-process=image/interlace,1
山景房里的三只猫
:::
2024-04-06 21:55:00
很久前配合 《前端工程化:基于 Vue.js 3.0 的设计与实践》 一书在 TypeScript 章节里讲解的内容,提供了一个很干净的 demo (见 hello-node ),这里的 “干净” 是指除了必要的基础技术栈外,没有过多的第三方依赖,一直运行良好。
当然在教程里还是主动引导读者自己从零开始创建这个 Hello 项目,这也带来了这个假期遇到的一个读者反馈的问题。
前几天在 GitHub Issue 的评论区,有位读者和我反馈说在运行 npm run build
时出现类似下方的报错,无法正确编译(见 #193 (comment) )。
遇到反馈的问题,首先是要先复现问题,于是先把仓库里的演示项目拉下来跑了一下,依然可以正常运行,但因为 “自己的代码自己清楚” ,马上联想到一个区别,就是读者自己创建的项目,依赖可能都是最新版,而我的演示项目由于 package.json 和 package-lock.json 里的版本号已有指定,因此 node_modules 下安装好的依赖可能并不完全一样,所以在演示仓库的项目里,这个错误没有被触发。
因此我把 node_modules 和 package-lock.json 文件删除,再重新安装依赖,确实,现在演示项目也无法通过编译了,还好日志很清晰,报错是来自 node_modules/@types/node/globals.d.ts
这个文件:
➜ hello-node git:(main) ✗ npm run build
> @learning-vue3/[email protected] build
> tsc src/ts/index.ts --outDir dist --target es6
node_modules/@types/node/globals.d.ts:6:76 - error TS2792: Cannot find module 'undici-types'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?
6 type _Request = typeof globalThis extends { onmessage: any } ? {} : import("undici-types").Request;
~~~~~~~~~~~~~~
node_modules/@types/node/globals.d.ts:7:77 - error TS2792: Cannot find module 'undici-types'. Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?
7 type _Response = typeof globalThis extends { onmessage: any } ? {} : import("undici-types").Response;
~~~~~~~~~~~~~~
// ...
由于这个项目是很入门的演示项目,主要为了演示 Common JS 模块和 ES Module 模块的开发,以及一些 TypeScript 语法的入门,并没有涉及到 Node.js API 的操作,因此也没有主动去安装 @types/node
这个包。
这里顺便补充个说明:
@types/node
包主要是为 TypeScript 提供 Node.js API 的类型定义,如果在项目里调用了 Node.js 的 API ,则需要显式安装它,使 TypeScript 可以识别到这些 API 。
所以 @types/node
这个包只能是第三方依赖带进来一并被安装的,为了方便排查,重新克隆了一个演示项目的原版,并通过 npm list @types/node
查看可以正常 build
时的依赖版本号,以及是哪个包引入的这个依赖。
➜ hello-node-original git:(main) ✗ npm list @types/node
@learning-vue3/[email protected] /Users/chengpeiquan/Documents/projects/demo/h2
└─┬ [email protected]
└── @types/[email protected]
此时正常 build
的 @types/node
版本号是 18.11.0
,是从 ts-node
引入的。
同样的命令在有问题的项目下运行,得到不同的版本号 20.12.5
。
➜ hello-node git:(main) ✗ npm list @types/node
@learning-vue3/[email protected] /Users/chengpeiquan/Documents/projects/demo/hello-node
└─┬ [email protected]
└── @types/[email protected]
查看项目 node_modules/ts-node
目录下的 package.json 文件,看到 ts-node
对 @types/node
的依赖版本号是设置为 *
号,也就是通配符(下面是关键信息的列举,非全部)。
{
"name": "ts-node",
"version": "10.9.2",
"peerDependencies": {
"@types/node": "*"
}
}
通配符版本号是指允许任何版本的依赖项,会安装最新可用版本,这也是为什么删除了 node_modules 目录和 package-lock.json 文件后,重新安装依赖后版本变化这么大的原因。
关于这个 undici-types
依赖,查看了 @types/node
的 package.json 文件,确实在后面的版本里引入其作为 dependencies
依赖,而之前的版本并没有,在 GitHub 溜达了一圈,原因可能来自 Node.js Undici 的这个 issue )。
原因查明,解决方案就好办了,这里提供两个有效的解决方案。
由于 demo 的报错主要来自第三方库的代码检查( TypeScript 默认会检查所有代码),在实际的项目开发中为了节省编译时间和跳过源码之外的问题报错,通常会启用 skipLibCheck
选项通知 TypeScript 跳过这些依赖库的类型检查(扩展名为 .d.ts
的文件),从而只检查开发者编写的源代码。
这也是为什么写了那么久的 TypeScript 从来没有遇到这种问题的原因,因为在实际项目里一直都是跳过对第三方库的检查啊哈哈哈。
选择这个方案的话,如果是走 CLI 选项编译,可以在命令里添加一个 --skipLibCheck
选项:
// package.json
{
"scripts": {
"build": "tsc src/ts/index.ts --outDir dist --target es6 --skipLibCheck"
}
}
如果是通过 tsconfig.json 配置编译选项,则是添加在 compilerOptions
里:
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "./dist",
"skipLibCheck": true
}
}
关于 skipLibCheck 选项 的更多说明可以在 TypeScript 官网文档上查阅。
除了 skipLibCheck
,还有一个解决方案,还记得错误日志吗?在错误日志里给出了两个解决方案的建议:
Did you mean to set the 'moduleResolution' option to 'node', or to add aliases to the 'paths' option?
由于这是一个第三方库的报错,因此 paths
方案不适用(该方案适合对源码目录下的文件配置 Alias 别名)。
因此可以通过另外一个建议,添加 --moduleResolution
选项。
// package.json
{
"scripts": {
"build": "tsc src/ts/index.ts --outDir dist --target es6 --moduleResolution node"
}
}
也可以成功解决编译问题,同理,也可以在 tsconfig.json 里配置该选项:
// tsconfig.json
{
"compilerOptions": {
"target": "es6",
"module": "es6",
"outDir": "./dist",
"moduleResolution": "Node"
}
}
关于 moduleResolution 选项 的更多说明可以在 TypeScript 官网文档上查阅。