Chilfish's avatar

Chilfish

2023 Logs 下半年

Chilfish
diary

07-25 环境变量

关于环境变量 .envdotenv 的问题,纯浏览器客户端的 JavaScript 没有 Node.js 的 process 概念,因此无法使用 process.meta.env 等变量

Vite 提供了一个解决方案,它内置了 dotenv,可以将 .env 文件的内容以明文形式复制到引用中 (import.meta.env.VITE_xxx),这样在 vite build 后生成的 JavaScript 代码中可以访问这些变量。然而,不推荐将敏感信息放在客户端代码中,因为在浏览器端无法保密。(Vue 本身还是运行在客户端的,所以打包后同理)

使用反代

如果非要使用这些变量,可以启动一个 Node 服务端,客户端通过 fetch('/api') 发送请求,服务端路由解析后携带密钥请求目标 API,并将结果返回给客户端。这就需要实现一个反向代理 (Reverse Proxy) 来转发请求。使用像 Nuxt、Next 等支持客户端/服务端代码共享的框架会更方便

以下是一个使用 Express 实现简单反向代理的示例代码:

const express = require('express')
const fetch = require('node-fetch')
require('dotenv').config()
const app = express()
app.get('/api', async (req, res) => {
try {
const response = await fetch(`https://api.example.com/id=${req.id}`, {
headers: {
Authorization: process.env.API_KEY,
},
})
const data = await response.json()
res.json(data)
}
catch (error) {
res.status(500).json({ error: 'Internal Server Error' })
}
})
app.listen(3000, () => {
console.log('Proxy server is running on port 3000')
})

在上面的示例中,当客户端访问 /api 时,Express 服务器会将请求转发到 https://api.example.com,并将响应返回给客户端

07-30 Serverless function

今天弹了个通知说之前用的腾讯云代理居然是 3 个月免费,后续按天收费…这只好转战到 Vercel 了

原来的那个用的是 Express 的代理中间件来转发的,但 Vercel 运行 Express 的方式有些奇怪。因为通常来说是要有 pnpm build 这样的指令来来部署运行 HTTP 服务的,但对于启动一个服务来说,只需要 node express.js 就行了。所以对于 Vercel 来说,并不能直接启动 node 服务,这是由它来接管了。它规定在 /api 目录下的文件都属于 Serverless Function,它以下面的形式来对请求作处理:

import type { VercelRequest, VercelResponse } from '@vercel/node'
export default function (request: VercelRequest, response: VercelResponse) {
const { name = 'World' } = request.query
response.send(`Hello ${name}!`)
}

这样就要把原先的 express 方法转换过来了,照着文档 using express with vercel 改了一下就成这样了 (详见 Chilfish/proxy-ai):

const app = require('express')()
const { createProxyMiddleware } = require('http-proxy-middleware')
app.use(
'/*',
createProxyMiddleware({
target: 'https://api.openai.com',
changeOrigin: true,
pathRewrite: (path, req) => {
// 移除 '/api' 前缀,将请求路径重写为 '/*'
return path.replace('/api', '')
},
onProxyReq: (proxyReq, req, res) => {
// 移除 'x-forwarded-for' 和 'x-real-ip' 头,以确保不传递原始客户端 IP 地址等信息
proxyReq.removeHeader('x-forwarded-for')
proxyReq.removeHeader('x-real-ip')
},
onProxyRes(proxyRes, req, res) {
proxyRes.headers['Access-Control-Allow-Origin'] = '*'
},
}),
)
module.exports = app

这里不能用 app.listen,必须要有导出能处理 (req, res) 的函数/对象,也不能放在 /express.js 然后 "build": node express.js,这样 vercel 就因为一直在 listen 而不能结束 build 而无法部署

08-23 Weibo-Archiver

刷到了个 vite-monkey 的 vite 插件,突然就给来灵感了,这就能用 vue/vite 的那一套去开发油猴插件。于是就干起来了,记录在了 dev/weibo

09-05 首个 Issues 😶‍🌫️

也是在改着 Uniapp 的启动模板 ↓ 的时候,遇到了个很难受的热加载的问题,即便它用的是 vite。于是花点时间控制变量法排查问题并解决后,尝试着去提了一个 issue,第二天就有回应了 🥳

这还是首次为其它仓库提个比较有用的 issue hhh

09-06 Uniapp is bad 😣

emmmm 还是不可避免的 Uniapp 了,虽然还是蛮不想深究的,毕竟是在是太麻烦了。使用的是 uni-vitesse 作为启动模板

更新:后续发现实际上这个 Static 目录是没有问题的,只是因为图片的文件名包含了非 UniCode 字符,导致了编译错误…… 只要改为英文就能解决了……🫥

还没写很多,有一个点是 Static 目录在不同平台的表现是不一样的,需要像下面这样来检测…

export const platform = uni.getSystemInfoSync().uniPlatform as
| 'mp-weixin'
| 'web'
/**
* 静态资源的目录,h5 与小程序对于绝对路径的解释不一
*/
export const staticDir = platform === 'web' ? '/src/static' : '/static'

而组件库方面,由于小程序的很多特性,只能用一些定制的组件库,挑来挑去还是使用 uview-plus 好了。而图标还是使用 UnoCSS 的方案

也还是再写再看好了…重点还不在这,这就是纯恶心人的东西 🤐

09-12 Nitro yes 🥳, Express no 😫

想着还有一个没填的坑是数据库那边的,于是就为 todo 启动了 MySQL+Express+TS 后端了。而且毕竟这学期还有一个 Spring Boot 要写,就顺便了,加上之后再填的后台管理系统,基本就填完了之前的小坑

ts + Node 第一个遇到的重拳是,要怎么运行啊…然后是打包 (?) 被 ts-node 的配置搞砸的情景还历历在目,就因为它默认是 CJS 而不是 ESM 的,特别麻烦。而像是 nodemon 这类的开发启动又依赖于 ts-node,总不能 tsc -w 再 nodemon 它吧…好吧这个坑还是得淌,毕竟之前就跳过了 node ts 的这一部分了 hhh

但期间想着打包的事情的时候,emmm 因为前端带来的习惯了,总想着打包。前端打包是为了压缩传输的体积和混淆代码,但在服务端就完全没必要了吧?后来我想的是,索然不用压缩,但可以转译为 js,并去掉开发依赖。这样一来服务器就能直接 node 运行,和不用下一堆生产版用不上的依赖了

说回来就是,正好在看着 VueUse 的源码,就看了看它是怎么打包发库的。发现它是先运行一个 /scripts/build.ts 脚本作预处理,再在里面调用 pnpm build:rollup 最终打包的。而我注意到它是直接运行 ts 的,用的是 tsx 工具,它直接是支持 ESM 的 ts 和 node,实在是大神器 hhh

在框框直写路由的时候,注意到 npm:mysql 这个包全传统回调写法实在是太不舒服了 (而且也没什么更新了),于是找到了个现代一点的替代品 mysql2。更实用和 Promised🥳 数据库服务商选的是免费的 TiDB 了,勉强用用还是可以的。其他很多方案还是要境外信用卡才能办理…

期间抱着超可维护性地想法,这抽离那聚合地,大致地写好了第一版。并终于地加了 auth 的路由中间件 (之前写的路由完全没往这里想,想着到时候再写好了 hhh,导致一直对中间件有种神秘的感觉 (写 Plugin 也是一样的感觉)),用的是 jose 库来写 JWS,即带签名的 JWT,一定程度上防止 Token 被伪造 (被偷了那还是没办法的)

终于,总算是写好了初版,但最大的问题就是,要怎么部署啊… Vercel + Express 虽然可行,但由于很多 js 历史遗留的问题,很难配置好在 Serverless Function 中用 ts (可以用,但就是一堆麻烦问题)。自建服务器感觉太麻烦,于是就找啊找一堆 Express Hosted Provider,还是很难方便免费地部署 express 上去…

想了再想,还是决定重构到 Nitro 上好了,它也是一个后端框架,支持部署到十几个服务商。记录在了 todo-ender/#1

我还是因为知道他是 Nuxt Server 部分的底层实现之一才了解到的,而且 unjs 这个组织十分神奇,Nuxt 依赖了许多他们的工具库。而且他们还将很多功能都抽离成单独的库,例如 Nitro 的网络服务依赖于 h3,监听 server 是他们的 listhen,useFetch 是 ofetch 等等

所以 Nitro 就相当于是 Nuxt 的 /server 部分,所以改写起来十分地舒畅 hhh。且得益于 “框架” 的特性 (总所周知 Express 还不算是一个框架),有很多像是基于文件的约定、hooks 等等,都可以很方便地利用起来

其中之一就是使用了统一的错误处理,它允许统一处理应用中抛出的错误 Nitro#errorHandler。这样我就这么想了,主要是一次次地写调用数据库等时的 try-catch 实在是太重复工作了,到时改起来会很麻烦,于是我干脆就不写 catch 了,在 nitroErrorHandler 中来处理这些抛出的错误,来统一错误信息和处理

export default <NitroErrorHandler> async function (error, event) {
const err = await myErrorHandler(error)
const res = event.node.res
res.setHeader('Content-Type', 'application/json')
res.statusCode = err.statusCode
res.end(JSON.stringify(err.cause))
}

其中有一个要注意的点是,如果是浏览器 UA 访问错误页面,它是会返回一个 HTML 的,而 curl 则是 json。这是因为它默认情况是视 UA 来处理 new Error 的返回体的,所以就要手动地将它转为 json 格式

我的做法是判断它的错误代码来分类的,好在 mysql2 和 jose 抛出的错误都有 code 字段,于是就能写成下面这样,然后再 switch case 就好了。这样就要写一个 creatMyError 之类的函数来抛出我们自定义的错误了

// the handled error codes
const errorCodes = [
'not_admin',
// mysql2 error codes
'ER_NO_SUCH_TABLE',
'ER_DUP_ENTRY',
// jose error codes
'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
'ERR_JWS_INVALID',
] as const

总体的体验下来就感觉 Nitro 实在是太又好了,Nuxt 生态圈实在是太 🥰🥰 了。之后就是拿 SB 写一遍来应付作业,和期待许久的 admin dashboard 了,todo 的前端也整合了过去 (见 09-16)

09-14 Kotlin 是幸福的 😎

还得是要学 Spring Boot 啊…但好在使用 Kotlin 是幸福无比的 😭

因为 Nest.js 和它实在太像了,数据库层面又让人联想到 Jetpack Room 的那一套,配置又梦回 Android,路由、MVC 等等,都把之前实践过的又整合了起来,想法十分地贯通

在数据库的实体类定义和解析方面有点小扯了一下,原因之一还是要找 Kotlin 的文档教程什么的还是有些绕,但还是找到了个专为 Kotlin 编写的 ORM:ktorm。使用 Kotlin 函数式的写法来写 SQL 操作,实在是太符合想象了,合鲤

再之后的,先鸽着了。回想这几星期并行这几个不同的项目,实在是有些难处理 hhh,但 antfu 简直是 😱😱

09-16 Nuxt is awesome 😭

怎么说呢,起因之一还是想配置一下 Vue 的自动路由,实在是不想新增一个页面就要去路由那里改一大串了,尝试简单手写一下基于文件的解析。虽然可以用,但拓展性实在是不强,许多 vue-router 都没法自定义。最后还是老实换回了 vite-plugin-pages 的文件路由库

这时候,todo 的后端也写得差不多了 (就是上面的 nitro 后端),正想整合一下,于是就很快糊了一个登录界面,和 /todo 的 meta: auth: true。并写了个前置守卫的中间件,也是能很成功地拦截

但这时遇到了个经典的大问题:跨域 hhh… 按下面 CORS 的方法成功地解决了一些问题,但是生产环境怎么办捏…虽然也能去后端配 cors 的列表,但想想还是太憋屈了,于是重构到了 Nuxt,真香 hhh。记录在了 learn-vue/#1

当然,有很多 ssr 的问题还是要单独解决,特判和 ClientOnly 一把嗦就好了 😊

09-16 CORS 跨域代理

解决跨域的方法之一就是搭建一个不在跨域范围内的反代服务器,要同协议、域名、端口,所以通常就会将它映射在子域名或是根域名 /proxy 下,来反向代理转发目标服务器。(见 #使用反代)

Vite 可以在配置文件中设置开发代理,详见 vite proxy。这样,fetch /api/hello 的时候就被代理到了 http://localhost:3003/hello

export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:3003',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
})

但这只适用于开发环境,部署后的 SPA 模式只是纯 HTML,并没有 node 环境,所以并不能转发请求。要么使用 Vite+SSR 来配置路由,或是在服务器中单独配置

基于此,我还是换到了 Nuxt),这样生产环境也能代理了

export default defineNuxtConfig({
routeRules: {
'/api/proxy/**': { proxy: `${PROXY_API}/**` },
},
})

09-19 Pull Request Merged 初体验

虽然早已广泛使用 pr 来管理自己项目的 commits,但还是第一次为别人的项目提交 pr😣😣,地址:v3-admin-vite/#133

这会正学着怎么写一个经典的后台管理系统,于是就找到了个看着还行的项目。在看源码的时候,被这拉风的代码风格给无语到了,立刻给它装了 eslint 试图美观一下,但还是挺难看的

正研究着路由权限的时候,/src/router/permission.ts 五层嵌套……他是怎么能忍的啊,于是花点时间按尽早 return 结束的原则改了改。想着既然都花时间了,为什么不提个 pr 贡献一下?于是就这么干了,建个新的分支,然后提交 pr,第二天就 merge 了,还是挺开心的 🥳🥳

09-26 api 状态码统一 200

看了篇知乎讨论说,其实后端的 Api 返回的状态码都设为 200 会更好,详见我们应该将所有的 API 都返回 200 的状态码

09-28 Exposed: Kotlin SQL Framework

在写 Kotlin Spring Boot 的时候,厌倦了 JDBC 或是 MyBatis 的繁琐,于是就找了个 Kotlin 友好的 ORM 框架 [Exposed] 来用。它的特点是使用 Kotlin DSL 来写 SQL,这样就能在编译时检查 SQL 语句的正确性,而不是在运行时才发现错误

使用下来的感受还是,不用写 SQL,而是用 Kotlin 的语法来操作真的是舒服很多 🥳

10-10

试试推特卡片

TODO:试着能不能 SSG 出来,但又不大想在 Vuepress 上再折腾太多……

10 月……

翻了下,想不到10月都干嘛去了,似乎就只有 chilpost-sb 和机器学习这回事……

11-03 Win11😎😎

两年下来,自带的 512G 有些捉襟见肘了,于是下了个致态的 PC005 1T 的固态 (虽然贵上了几十块,但老哥们都说这个稳),感恩长江存储 🙏🙏 1T 只要三百多,真香

然后想着,既然都换硬盘了 (轻薄本就一个固态的接口),为什么不顺便体验下 win11😍 巧的是,在下 win11 镜像的时候,恰巧发了新版本

要把积累的经验都用起来,于是装的是全英文版的,地区、语言什么的都用英文地区,至少能避免很多非 UniCode 字符的问题

换新硬盘的最大感受就是什么都变快了,读写有 3|2.3 G/s,休眠和恢复、内存交换等等都特别快 🥳

再就是有很多 win11 特有的功能。WSL2 开启 networkingMode=mirrored 就能与本机公用网卡,它的 localhost 就是本机的,再也不用端口转发之类的了。当然 WSA 也能玩上了,不过官方版必须设地区为美国才能装

当然……新系统也有一大堆时不时出现的小问题

11-23 又一个油猴爬虫插件

辅导员突然找我接了个他朋友那边的外包 😹 要我写个脚本帮他按格式批量导出那后台管理系统的数据……因为这个系统也是外包买来的,所以就没定制这个功能。他说上头要让他导出几万份数据……

具体的工作就是,截图一个表单、下载表单的附件文件,然后分类整理,完全就是重复性动作 🫥

我一听,好啊又是爬虫导出是吧,vite-monkey 启动!

做起来用不了多久,但最大问题在于,给特定 DOM 元素截图保存。虽然有 html2canvas 再 toBlob(),因为它是 SPA + ElementUI,得等数据加载出来才能截图。一开始的想法是 fetch 这个 html 然后给 DOMparser 解析,但它的数据是异步加载的,所以就没办法了

然后想着插进一个不可见的 iframe,然后再等。这个做法是完美可行的,但最大的问题就是内存泄漏……即便 remove 了 iframe,但它的内容还是会留在内存里,导致一次次的截图就会越来越慢,最后直接卡死了。必须重启标签页才能释放内存……

虽然有第三方的 HTML 截图库,但这个要登陆才有结果,而且试着尝试破解鉴权,但还是无果

突然间地,想到了可以利用跨窗口通信,直接在新标签页打开,截图之后,通过 postMessage 传回来。因为直接关闭了这个标签页,这样就不用担心内存泄漏的问题了,而且也能很好地控制截图的时机

接下来要做地就只是优化下 UI 和交互。但还是没做到测试齐全,考虑周到,还是根据客户的使用提 issues 来改 bug😹😹 直接一个用户即测试 (Customer as a Tester,CaaT) 的模式 🤣

当然对面还是很友好很有梗的,也很满意,最后还跟着辅导员去面基吃饭了 🥳🥳

12-02 WSA with Magisk

想着都是 WSA 了,不 root 怎能忍。但看了眼,似乎挺麻烦的,还得重新编译打包

但好在强大的社区已经帮办做好了 WSABuilds,直接下载就能用了 🥳

虽然真机还没 root 过,但先试用一下吧