我的Astro即时聊天应用

1,135 阅读5分钟

phy-chat 即时聊天应用

一开始由于个人原因,用不了微信等聊天工具,想着是否有网页版的即时聊天应用呢,随着网上搜索了一大片,很多都是聊天室之类的,1v1 的那种很少,于是就想着自己做一个吧。于是在 github 上也搜到了相关的代码库,他是使用 next 写的,而后端服务采用的都是 serverless,数据库采用 upstash 的 redis,消息推送使用 pusher,鉴权登陆采用的 next/auth,使用的是 github 登陆。不过这个是比较简单的版本,缺少截图后,可以发送图片,删除聊天信息等功能,于是我在其进行了一些拓展。

Astro

在一次机缘巧合下,国内突然有个说脏话的 gpt 突然火了一阵,而我也想着我自己的 chat 能加入 gpt 来聊天,于是,我也去看了下这个仓库的源码,发现他使用的框架是我所没见过的,而且运行速度也很快,对比我的那个 next 的 chat 来说,速度快了不止 1 倍,好家伙,这真是引起了我极大的兴趣,查看了下 Astro 的官网,其官网的介绍是这样的: 01.png 至此开始我的 chat 从 next 架构迁移到 Astro 架构上

迁移过程

起初遇到最大的难题是,next 版本的 chat 的 auth 模块,他是依赖于 next/auth,而 Astro 并没有被集成到 auth 里面去,这就有点强绑定了,在 next/auth 的仓库下却有一个 pr,是 Astro 的集成相关提交,但是一直没被合进去,时间点还是 22 年的 11 月,看来只能放弃使用 Astro 的 auth 集成了,不过项目中的上层 ui 使用的是 solidjs,solid-start 也是服务端渲染框架,只是还在 beta,但是他却拥有 auth 的集成,以后提交 pr 的时候,别人也会看重你是否是足够厉害才会允许你合进来,毕竟一个是 solid 的官方进行 auth 的开发,Astro 的 auth 只是个人的开发。 最大的问题解决后,后面其他模块包都没遇到强绑定的问题,迁移起来也很简单。

Astro 使用的个人感受

在迁移的过程中,也会对于一部分代码进行 Astro 的翻译,主要翻译的是每个页面的布局,还有一些在需要服务端进行的数据处理。对于 Astro 的分层架构语法,这开发体验真的很友好。

  • 服务端运行
---
import MyComponent from "./MyComponent.astro";
const name = "Astro";
---
  • 类 JSX 表达式
<MyComponent templateLiteralNameAttribute={`MyNameIs${name}`} />
  • css 样式写在 style 标签里
  • 客户端运行的 js 写在 script 标签里

这种分层的模板语法,可读性高,上手简单。全局的 Astro 对象 api 设计也是很友好,像请求上下文之类的...

Astro 群岛模式

“Astro 群岛“指的是静态 HTML 中的交互性的 UI 组件。一个页面上可以有多个岛屿,并且每个岛屿都被独立呈现。你可以将它们想象成在一片由静态(不可交互)的 HTML 页面中的动态岛屿。来源官方文档的解释。 02.png

群岛模式也是我喜欢 Astro 的一个重要因素,将页面中静态的部分放在 astro 文件中,动态的我们开启群岛模式,client:load。这种模式与传统的 ssr 相比,更加灵活,使得原本提倡静态为主的,也能使得你的页面更多的交互性。

Astro 3.0 View Transitions

Astro 3.0 版本后 viewTransitions 已经不再是实验特性,而我自己也添加了该特性在我的 chat 里,添加了该特性后,可以定义哪个部分在页面切换是不需要变化的,类似于 vue 中的 keepAlive 缓存,这就在页面过渡的时候更偏向于 spa,而且跳转页面的加载速度也能更快。

03.gif

solidjs 的使用

对于 solidjs,我个人觉得他的性能真的很厉害,无虚拟 dom,相信用过 react 的再去上手 solidjs,我觉得问题都不大,而且也没有什么 useEffect 的依赖项,这是最好的,useEffect 真的是另我头疼,你不知道他何时会出现问题。最重要的一点是不会 reRender,solidjs 他能追踪到 dom 级别的更新,细粒度很细,起初我还写了一个这样的 demo 去看他能否也能追踪到 dom 的更新,是在 for 循环体里做的操作。

import { render } from "solid-js/web";
import { createSignal, For } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(1);
  const [arr, setArr] = createSignal([1,2,3,4,5,6,7])

  const increment = async () => {
    setArr((prev) => prev.slice(0,2))
  };

  return (
    <>
    <button type="button" onClick={increment}>
      {count()}
    </button>
    <div>{count()}</div>
    <For each={arr()}>
      {(item) => <div>
        <div>{item}</div>
        <div>{Math.random()}</div>
      </div>}
    </For>
    </>
  );
}

render(() => <Counter />, document.getElementById("app")!);

04.gif

从运行结果来看,我删除了后面的项,保留前2项,他也不会重新运行 random,由此可证明,他的追踪是dom级别的,真的是太厉害了👍,能把该做的事情做对,绝不会把不该做的也做了(指react不是

部署

一顿操作下来,本地环境基本上就解决了,接下来就需要部署到正式环境了,首先选择的是vercel及netlify部署,这2者都是用到对应的适配器进行打包的,配置如下:

const envAdapter = () => {
  switch (process.env.OUTPUT) {
    case 'vercel':
      return vercel({
        edgeMiddleware: true,
      });
    case 'netlify':
      return netlify({
        edgeMiddleware: true,
      });
    default:
      return node({ mode: 'standalone' });
  }
};

然后只要在部署平台上环境变量配置好,就可以顺利启动了,而且还有cicd的效果,很方便。

vercel及netlify的弊端

然而vercel或者netlify对于chatgpt而言,会失去打字机的效果,打字机的效果是源于sse(Server-sent events),而我的接口肯定是利用中间层去请求openapi接口的,问题就出现在这里,后端端点api不支持sse,会导致gpt的回答是一下子全部回来的。效果如下:

fld6q-sm1p9.gif

sealos

所以只能尝试别的部署方案,然而看到了国内有个支持容器化部署的平台sealos,用k8s做的,但是目前项目并没有将他打包成镜像的配置,而且对于docker也不是很熟悉,在网上搜索了大量资料后,也终于捣鼓出来了,打包成镜像的Dockerfile如下:

FROM node:alpine as builder
WORKDIR /usr/src
COPY . .

FROM node:alpine
WORKDIR /usr/src
RUN npm install -g pnpm
COPY --from=builder /usr/src ./
COPY --from=builder /usr/src/hack ./
RUN pnpm install
ENV HOST=0.0.0.0
ENV PORT=3000 
ENV NODE_ENV=production
EXPOSE $PORT
CMD ["/bin/sh", "docker-entrypoint.sh"]

起初并没有copy全部项目到容器里,只是将项目打包后的dist文件夹copy进去,然而Astro里有个坑,如果容器里没有env文件,他自己打包后的产物,配置的环境变量是为变量,并不是具体值,这应该是Vite打包带来的,因为区别于webpack的取值。

// webpack
const env = process.env
// vite
const env = import.meta.env

webpack这么取值是可以在打包后直接依赖于外部的env文件取得具体值,而vite并不行,他打包后是直接将env替换为具体值,怪不得在定义环境变量时,只有在服务端能获取到环境变量,客户端获取不了,这种行为明显是不安全的行为,若需要在客户端获取环境变量,需要为该环境变量的命名加上Public前缀才行。所以为了解决此,有2个方案,一个是在容器启动时,动态将dist目录下的每个文件搜索到的环境变量都替换为相对应的值,这种方案虽然可行,但是不知为啥,脚本运行会报sed: bad option in substitution expression这个错。

#!/bin/sh

# Your API Key for OpenAI
openai_api_key=$OPENAI_API_KEY
nextauth_url=$NEXTAUTH_URL
nextauth_secret=$NEXTAUTH_SECRET
upstash_redis_rest_url=$UPSTASH_REDIS_REST_URL
upstash_redis_rest_token=$UPSTASH_REDIS_REST_TOKEN
github_id=$GITHUB_ID
github_secret=$GITHUB_SECRET
pusher_app_id=$PUSHER_APP_ID
public_next_pusher_app_key=$PUBLIC_NEXT_PUSHER_APP_KEY
pusher_app_secret=$PUSHER_APP_SECRET
public_next_pusher_cluster=$PUBLIC_NEXT_PUSHER_CLUSTER
github_access_token=$GITHUB_ACCESS_TOKEN
public_owner_email=$PUBLIC_OWNER_EMAIL


for file in $(find ./dist -type f -name "*.mjs"); do
  sed "s/({}).OPENAI_API_KEY/\"$openai_api_key\"/g;
  s/({}).NEXTAUTH_URL/\"$nextauth_url\"/g;
  s/({}).NEXTAUTH_SECRET/\"$nextauth_secret\"/g;
  s/({}).UPSTASH_REDIS_REST_URL/\"$upstash_redis_rest_url\"/g;
  s/({}).UPSTASH_REDIS_REST_TOKEN/\"$upstash_redis_rest_token\"/g;
  s/({}).GITHUB_SECRET/\"$github_secret\"/g;
  s/({}).GITHUB_ID/\"$github_id\"/g;
  s/({}).PUSHER_APP_ID/\"$pusher_app_id\"/g;
  s/({}).PUBLIC_NEXT_PUSHER_APP_KEY/\"$public_next_pusher_app_key\"/g;
  s/({}).PUSHER_APP_SECRET/\"$pusher_app_secret\"/g;
  s/({}).PUBLIC_NEXT_PUSHER_CLUSTER/\"$public_next_pusher_cluster\"/g;
  s/({}).GITHUB_ACCESS_TOKEN/\"$github_access_token\"/g;
  s/({}).PUBLIC_OWNER_EMAIL/\"$public_owner_email\"/g" $file > tmp
  mv tmp $file
done

rm -rf tmp

所以我才用了第二个方案,在容器启动时,动态生成.env文件,再进行build打包命令,这样打包的产物就自己能够将所有的环境变量都写入进去了。至此,我以为我应该能成功部署上去了。结果,又迎来新的问题,由于服务器是国内的,调用openapi接口并不成功,得需要一个代理才可以。

csb

在这之前,我还将代码在codesanbox运行,他是支持sse的,因为打包的适配器是用node,而不是vercel这种自定义的,所以效果不错,只是在跑dev的情况下,github的第三方登陆调用并不成功,auth的问题,也遇到很多问题,像不可信主机[UntrustedHost],需要配置后才可以。那dev环境不可以,那生产环境是不是就可以了,于是我将他打包,并且用node去启动服务,再进行访问,成功运行,打字机效果也完美生效!!!

录屏2023-10-31+14.49.46.gif

总结

最后感谢 HappyChat 带来我的前端视野的开拓,让我能认识到 Astro 及 solidjs,现在新的框架出了很多,虽然用什么都是用,也不是非得去卷什么,在如今的 react 时代,不得不去感叹,大家都在避免 react 的缺点,创造更适合现在的框架。最后如果大家想去体验 chat 的话,可以去这里体验一下,需要 github 登陆,如果添加了我为好友,还能开启 gpt 的脏话体验,当然,这个 key 用完了就没了,还请不要滥用。代码仓库在这里 👉github