<![CDATA[王小明博客]]>https://tytcn.cn/RSS for NodeThu, 30 Nov 2023 09:46:09 GMT60<![CDATA[西双版纳无聊之旅]]>前些天作为电灯泡,去了一趟云南,不知道是不是钱没花到位,感觉没什么意思,甚至连天气都不如深圳暖和。唯一有意思的地方可能就是星光夜市,来来往往很多漂亮的小姐姐😁

⬇️⬇️⬇️ 当了几天的电灯泡 [抠鼻]

1701315520907.jpg

⬇️⬇️⬇️ 演员出场前抱抱,太暖了,我也要抱抱 (不是

1701315520891.jpg

⬇️⬇️⬇️ 要不要抓一只回家呢

1701315520900.jpg

⬇️⬇️⬇️ 这猴子比照片中看着小多了,可以说比巴掌大不了多少

1701315520883.jpg

⬇️⬇️⬇️ 说禁止大象表演了,就领出来逛了一圈

⬇️⬇️⬇️ 偶然远远望见一个“待完工”的楼房,特地步行几十分钟过去看看

1701315520866.jpg

]]>
https://tytcn.cn/blog/e0L2TzcRqS9vhttps://tytcn.cn/blog/e0L2TzcRqS9vThu, 30 Nov 2023 04:03:45 GMT
<![CDATA[nginx 层面缓存 rss 请求]]>从数据库中一下取出全站所有博文的全文、所有标签,然后还要把所有文章的 md 格式文本转成 html,是比较浪费性能的。(目前博文没有做分页,50篇以内都不会考虑做分页)

之前听说有些 rss 阅读器会高频访问 rss 源,考虑到这个问题,我的 rss 是不提供全文的,仅提供 title & description。今天突然想到,可以从 nginx 层面缓存一下,频繁访问也没事,nginx 直接吐出去,不用重新去数据库拿,好像挺好的。

以下是关键的 config, 完整内容请移步 github nginx.conf

  # 作 key 时忽略查询参数
  map $uri $noargs_uri {
    ~^(?P<path>[^?]*)(?:\?.*)?$  $path;
  }

  upstream nextjs_upstream {
    server                       127.0.0.1:3000;
    keepalive                    500;
  }

  proxy_cache_path              /var/cache/nginx levels=1:2 keys_zone=nextjs_cache:10m max_size=1g inactive=60m use_temp_path=off;

  server {
    # ...

    location / {
      # ...
    }

    # rss 源缓存
    location /rss.xml {
      # ...

      proxy_cache                nextjs_cache;
      # 缓存一小时
      proxy_cache_valid          200 302 1h;
      proxy_cache_valid          404 1m;
      # 忽略 upstream Cache-Control, nginx 强制缓存
      proxy_ignore_headers       Cache-Control;
      # proxy_cache_key 忽略 search params
      proxy_cache_key            "$scheme$request_method$host$noargs_uri";
    }
  }
]]>
https://tytcn.cn/blog/UePDIoS0CldOhttps://tytcn.cn/blog/UePDIoS0CldOWed, 29 Nov 2023 18:10:25 GMT
<![CDATA[博客问题汇总]]>一些说明
  • ~~网站这么慢的原因~~

  • 不存在了, 搬到国内了, 速度上来了

  • 懒得搞 api, 直接使用的 rpc 形式的 server action, 导致页面 cache-control 用不上, 始终是 no-store, 页面没法静态化, revalidate 失效;

    • 这个问题不想解决, 因为没人给我写后端, 而 next api 太孱弱, 新开一个 nest 项目又太麻烦, rpc 又太香了。。。
  • 选用了 aws 海外版, 只部署了首尔一个节点, ssl 握手就要花好几秒。穷就一个字, 我只说一次;

    • 这个问题理论上换到国内部署就能解决, 而由于 aws 有 amplify 便于部署, 所以也不想解决。。。
  • 偶尔会显示 服务器错误 是什么鬼

  • 大概率是超出了数据库最大连接数;

  • 数据库免费时长只有 750小时, 所以只开了一个实例;

  • 因为选用了免费的 aws mysql db.t3.micro 数据库, 1gb ram, 2 vCPUs, 最大连接数为 60, 而 Next.js Link viewport prefetch 导致并发请求, 由此导致数据库并发连接过高, 超出 max_connections;

  • 服务端对 5xx 错误做了过滤, 统一显示 "服务器错误, 请稍后再试";

  • ~~待采取的措施是: unstablecache + 一个很长的过期时间 + 修改内容后自动更新相应的 unstablecache 的 tag;~~

  • 已采取措施: 前台页面加 unstable_cache + 一个适当的过期时间, 以减少访问数据库;

  • 参考 prisma 的建议, connection_limit 改成 1

  • 取消使用 next/link prefetch

todos

  • [x] 全站音乐(页面跳转音乐不断)
  • [x] 全站字幕(页面跳转字幕不断)
  • [x] ~~tagList 加个背景,与其他部分分隔开~~(不需要)
  • [x] 在自己的博文加上"编辑"按钮
  • [x] sitemap
  • https://nextjs.org/docs/app/api-reference/file-conventions/metadata/sitemap#generate-a-sitemap
  • https://claritydev.net/blog/nextjs-dynamic-sitemap-pages-app-directory
  • [x] 添加 robots
  • [x] 添加 rss (参考: https://taoshu.in/webfeed/lets-webfeed.html)
  • [x] 标题(h1-h6)增加 hash & id, 支持点击修改 url
  • [x] ~~新增 TODO/road map 页~~ (不需要了, 放在这个博文里)
  • [x] 优化深色模式的实现逻辑(现在初次渲染时闪屏太严重了)
  • [x] 部署问题 (用了 aws, 一键解决了)
  • [x] ~~nginx~~(不需要了)
  • [x] ssl
  • [x] 优化 a11y 及键盘导航
  • [x] 上传功能
  • [x] 非 admin 用户限制上传数量和文件大小
  • [x] 文件上传支持图片预览 (因为上传的主要内容就是图片)
  • [x] 上传弹窗的关闭按钮改为完成
  • [x] 支持粘贴式上传文件 (全站任意地方粘贴文件触发上传)
  • [ ] 支持拖拽式上传文件 (全站任意地方 drag and drop 触发上传)
  • [ ] 优化上传的交互 (如上传时的空状态等)
  • [x] react server component streaming with suspense
  • [x] ~~pwa 消息订阅与推送~~ (不用 pwa 了)
  • [x] 前端路由时, 页面 title 不会变化; ( 这儿说解决了, 解决个毛线… )
  • 解决方案: 额外添加了前端修改 title
  • [x] 内容页的编辑按钮直接打开编辑弹窗
  • [x] 博文编辑完成后 revalidateTag
  • [x] 需要配置 next-image config, 考虑下是否需要 next/image
  • [x] 尝试把 title 放到article 中 (主要是考虑 UI)
  • [x] 401 默认弹出登录弹窗
  • [x] 统一弹窗 UX (存在部分弹窗关闭按钮在左侧)
  • 目前统一在右上角, 除了博文编辑, 右上角放保存和预览按钮, 放不下关闭按钮了
  • [x] media 新增 loading 态
  • [x] 弹窗支持物理返回键 (物理返回键功能实现见 我给弹窗添加了支持物理返回键 一)
  • [x] 图片预览 (也支持物理返回键)
  • [x] 考虑一下网站搬到国内服务器
  • [x] s3 上的资源加缓存头
  • [x] 自动生成博文目录
  • [x] 固定图片尺寸 (可选方案: 尺寸信息放在 url 上)
  • [x] ~~添加谷歌收录 (国内域名本来就做了)~~
  • [x] 自动评估阅读时长
  • [x] ~~新增 about me 页 (不需要, 直接放博文里吧)~~
  • [x] 全站搜索功能 (当前支持博文搜索)
  • [x] 新增 404 500 页面
  • [x] ~~回复功能 (站点在国内, 估计上不了这个功能了)~~
  • [x] 一次性链接 (支持访问密码 + 访问时间限制 + 访问次数限制)
  • [x] 文章底部统一展示版权声明
  • [x] ~~资源管理页 (管理个毛线, 不需要了)~~
  • [x] 添加友链页面
  • [x] 添加留言页面 (单向, 给站长留言, 其他人看不到)
  • [ ] 自动生成描述文本 (真的需要吗?)
  • [ ] 编辑的缓存
  • [ ] 账号过期机制
  • [ ] pre > code 添加复制按钮
  • [ ] api 日志

bugs

  • [x] !!! 高危 !!! server action 缓存和 service worker 发生了化学反应, 具体原因待查; 表现是, 版本更新后, 本地访问到了旧的页面, 旧的页面引用旧的 js, 然而旧的 js 资源已经失效了, 导致页面崩溃
  • 不用 pwa 了, html 也不用缓存了, 所以不会有这个问题了
  • [x] tags 上面的数量,需要仅计算 published 的博客
  • [x] 不可见博客的推荐阅读,第一条就是它自己
  • [x] 博客编辑中,默认没有填充标签
  • md mui Autocomplete 自己有 bug, Chip 没加 key, 导致控制台一直报红色 warning, 很烦
  • [x] tag/hash 页 tagName 颜色有误
  • [x] mui Stack 注意换行问题
  • [x] 在 pwa 应用中通过 target _blank 打开站内链接偶现直接展示出了 post response
  • 原因: 不是 post response, 是 get response, 因为在 next.config.js 中为页面设置了 cache-control, 浏览器直接读取了本地缓存…
  • 解决方案: 页面不缓存了…
  • [x] ~~pwa 封面图背景色有误~~ (不用 pwa 就不管了)
  • [x] revalidateTag 更新内容后, 本地 pwa 没有更新, 导致刷新页面和前端路由切换页面时的数据不统一
  • 不用 pwa 了
  • [x] /blog/[hash] 页面缓存仍有问题
  • unstable_cache 博客修改之后要更新一堆的东西, 考虑一下别用了
  • 国外接口耗时太久, 站点放到国内就没有这个问题了
  • [x] 登录过期导致的 401 未能弹出登录弹窗
  • [x] 更新博客后的 router.refresh() not work
  • 国外接口耗时太久, 站点放到国内就没有这个问题了

解决方案

疑问

  • 数据库的连接是怎么回事? 是不是我只要从数据库取一次东西, 就会占用一个连接?
  • 答: 部分正确, 查询完成后, 会将连接释放到连接池中可供后续请求使用; 所以当有并发请求时,数据库连接可能会过高
]]>
https://tytcn.cn/blog/MlShKQJhLUEThttps://tytcn.cn/blog/MlShKQJhLUETMon, 27 Nov 2023 17:41:36 GMT
<![CDATA[休息一下,听会歌吧]]>
  • 聽悲傷的情歌 - 蘇星婕『聽悲傷的情歌 看傷人的戲 我還是會想起你』

    1. 《如果我是___,你会爱我吗》
    1. 《爱你》,但是骂你
    ]]>https://tytcn.cn/blog/Xo1SmCzkTxsbhttps://tytcn.cn/blog/Xo1SmCzkTxsbThu, 16 Nov 2023 13:49:39 GMT<![CDATA[Next.js 13 升 14 记录]]>前几天突然发现 Next.js 出到 14 了,那肯定要跟啊,就把博客升级了一下,前端业务代码很顺利,一天就搞定了,运维花了我大概三四天,痛苦😭特此记录

    Next 14 优缺点

    优点

    最大的优点就是,极大地提高了开发时体验。

    原来从运行 pnpm run dev 到页面可交互,大概需要 30+s(没算过,大致估计),但是升级 14 之后,20s内就能交互了。切换页面速度也有了很大提高,首次切换至一个新的动态路由,大概需要构建 10-20s,再次进入同一个动态路由的其他页面,就几乎是秒进了(之前每进一个路由都需要不短的构建时间)

    (由于项目需要使用 webpack,所以开发时很慢,swc 估计能快一些)

    缺点

    node 版本最低要求 18.17,腾讯云轻量服务器宝塔 centos 版本最高 7.9,不支持那么高的 node 版本,搞得我必须得重装系统。我这种 linux 二把刀,本地捣鼓了一天虚拟机后,云上重装 + 各种配置又花了两三天,见 hyper-v ubuntu 22.04 装机记录。加上 https + 数据库 + 数据库定时备份,之前宝塔都一键配置,到了这边都得自己弄,简直要了我半条命。

    P.S. 我本地装的虚拟机是可视化桌面,结果云上的却是命令行,幸好版本一致没出什么幺蛾子。

    P.P.S. 新机装好,第一时间试了线上 build,不出所料,还是 build 不动,一瞬间 cpu 飚到 97 还不止,然后就命令行都卡死了,ctrl+C 都无效了。。。

    升级过程

    前端业务代码

    前端业务代码其实没什么好说的,由于这个项目比较新,我时不时就会运行 pnpm up --latest,这次也差不多,直接更新依赖之后,哪里报错改哪里,一共也就不到十处报错,改了就好了,有部分从报错提示中看不出原因的,搜一搜也就改了。

    最隐晦的一条是,命令行报错 Cannot set properties of undefined (setting 'inTable'),没有其他任何提示,也没有报错上下文,最后搜到,原来是 remark-gfm 4.0 依赖了 unified 11, 其他大量生态没有更新该依赖,导致版本不兼容,只能回退到 remark-gfm 3.0 了。

    TypeError: Cannot read properties of undefined (reading 'inTable') in v4.0.0 发现相同的问题,然后报 Duplicate of xxx,然后一直追溯了五六篇 issues,一直到 Error: No overload matches this call after library update,感觉很有趣。

    其他的处理一方面参考 Next.js changelog,一方面报错的栈也会给出不少信息,所以可以说整个过程水到渠成,没有花费什么力气

    部署

    之前在宝塔上部署,数据库是一键生成的,自动备份是一键生成的,SSL 是一键申请部署自动续期的,nginx 是一键生成的。没了宝塔,什么都要自己弄。

    1. 数据库配置

    创建及新建用户就不说了;自动备份数据库是必须的,网上随便一搜一大把,也就不说了

    • 允许远程访问,ubuntu mysql设置远程访问是讲的比较完善的。其他回答,几乎 80% 都是说修改 /etc/mysql/conf.d/mysql.cnf 或其他类似文件的(这还是在我设置了 -csdn 的前提下)。 ini # /etc/mysql/my.conf # 这个地方设置 bind-address 是没有用的 bind-address=0.0.0.0 # 这里 include 了一个 mysqld.cnf 文件 # 其内包含了 bind-address=127.0.0.1 会把上面的配置覆盖掉 !includedir /etc/mysql/mysql.conf.d/
    1. nginx 配置

    我们知道 Next.js 起了一个服务,通常会给它配置 3000 端口,80/443 进来的流量,可以通过 nginx 来配置反向代理,同时,http 重定向、wwwnon-www重定向、httpsgzip,都可以交给 nginx

    抄着 chatgpt,搞了一份配置,仅供参考吧

    1. 本地虚拟机构建后发到云上并重启

      脚本 deploy.sh


    下一期讲讲重构到 Next.js Route Groups 的经过

    ]]>
    https://tytcn.cn/blog/F8Uy9eztfBxlhttps://tytcn.cn/blog/F8Uy9eztfBxlThu, 16 Nov 2023 00:50:57 GMT
    <![CDATA[hyper-v ubuntu 22.04 装机记录]]>设置中文及输入法

    截图已经是中文了,凑合着看吧,应该能猜到对应的英文

    1. 语言中添加中文
    • 没有中文就点选下图中的管理已安装的语言,弹窗中选择添加或删除语言...,以添加中文

    • 注意,键盘输入法系统选择IBus

      管理已安装语言.jpg

    1. 添加中文输入法 按下图所示,逐步添加即可

      键盘.jpg 添加输入源.jpg 汉语输入法.jpg

    常用软件

    tips: 有些需要 git clone github.com 的,如果拉不下来,可以试试换成 ssh 源,就是格式如 git@github.com:xxx/xxx.git这样的 获取 git ssh 地址示意图.jpg

    1. sudo apt install curl git

    2. zsh + oh my zsh

    先安装终端, 避免下面安装的软件还需要手动修改 .zshrc

    • sudo apt install zsh
    • sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
    • 上面的 oh my zsh 需要科学上网,你也可以搜索“ohmyzsh国内镜像安装”或者看看oh-my-zsh国内镜像安装 - VertexZzz
    1. zsh 推荐主题 & 插件

    主题插件可以之后安装

    • theme
      • powerlevel10k
        • git clone --depth=1 https://gitee.com/romkatv/powerlevel10k.git ${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/themes/powerlevel10k
    • plugins
      • zsh-syntax-highlighting
        • git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
      • zsh-autosuggestions
        • git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
    • config
      • ZSH_THEME="powerlevel10k/powerlevel10k"
      • plugins=(git extract history sudo zsh-autosuggestions zsh-syntax-highlighting)
      • Note that zsh-syntax-highlighting must be the last plugin sourced.
    1. 安装 nvm
    • curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
    • 安装完成后重启终端
    • 然后使用 nvm 安装需要的 node 版本
    1. 安装 pnpm
    • npm install --global pnpm
    1. 安装 tsx (用于直接运行 .ts 文件)
    • pnpm i -g tsx

    问题解决

    1. DNS 无法解析

    不知道怎么了,dns 解析突然出问题,apt install 不了了,git 也拉不了了,ping 任何外网都 ping 不通。google 一下各种牛鬼蛇神都出来了,最后终于找到一篇对症的,就是 Ubuntu-DNS解析问题

    1. 我们只需要编辑 /etc/systemd/resolved.conf 文件,修改下面的 DNS 值 # /etc/systemd/resolved.conf # 我们 DNS 解析使用 8.8.8.8 [Resolve] DNS=8.8.8.8
    2. 然后重启一下就 OK 了
      • systemctl restart systemd-resolved.service

    忠告

    多添加检查点,避免哪里出了问题要从头再来。。。

    多添加检查点.jpg

    ]]>
    https://tytcn.cn/blog/R2fDzG4uLvx4https://tytcn.cn/blog/R2fDzG4uLvx4Mon, 13 Nov 2023 04:28:51 GMT
    <![CDATA[我给弹窗添加了支持物理返回键 二]]>背景: 书接上文

    上文说到, 如果页面上多个弹窗并存时, popstate 事件会在所有弹窗中触发, 导致一个 popstate 事件关闭掉所有弹窗, 那我们的问题就是, 如何管理 history 栈, 让其逐个触发呢?

    实现思路

    既然所有的弹窗都在监听 popstate, 那我们维护一个队列, 弹窗出现时往队列放入一个特征值, popstate 事件时, 弹窗根据队列尾部的值, 决定自身是否需要响应 popstate;

    image.png

    show me your code

    首先我们实现一个管理 index 的机制

    export class IndexManager {
      private stack: number[] = []
    
      get latest() {
        // 是最大值 而非 最后一个元素, 因为 stack 不一定是 有序的
        return Math.max(...this.stack) ?? 0
      }
    
      push(n = this.latest) {
        this.stack.push(n)
      }
    
      drop(n = this.latest) {
        this.stack = this.stack.filter((k) => k !== n)
      }
    
      has(n: number) {
        return this.stack.includes(n)
      }
    }
    

    然后就是最终实现了:

    import { useListen } from './useListen'
    
    import { IndexManager } from '@/utils/IndexManager'
    
    import { useEventCallback } from '@mui/material'
    import { useRef } from 'react'
    import { useEvent } from 'react-use'
    
    let ignoreIdx = -1
    // 这个 manager 既非 stack, 也非 queue,
    // 叫 stack 只是随便叫的
    const stack = new IndexManager()
    
    /**
     * 该 hook 可用于任何需要物理返回键的地方;
     * 注释中的所有 modal 或 弹窗 都不仅限于 "弹窗",
     * 任何需要的地方都可以用,
     * 如 Dropdown/Drawer/Tooltip 等, 任何有 visible change 的地方都可以用;
     * (其实没有 visible change 的地方也可以用, 毕竟该 hook 只负责管理 history, 不干涉 ui)
     */
    export function useInjectHistory(
      open: boolean,
      /**
       * 如果需要在用户物理返回时关闭弹窗, 就在该方法中手动调用 modal.hide();
       * 如果拒绝关闭弹窗, 就别 hide() 并 throw Error;
       */
      onPopState: (e: PopStateEvent) => Promise<void>
    ) {
      // 弹窗维护各自的 index
      const IndexRef = useRef(-1)
    
      const finalOnPopState = useEventCallback((e: PopStateEvent) => {
        // 轮到我了吗? 没轮到就返回
        if (IndexRef.current !== stack.latest) {
          return
        }
        // 是上一个弹窗关闭而触发的 popstate 吗? 是就返回;
        // (并且把 ignoreIdx 标志位清掉, 使得下次 popstate 能正常生效)
        if (IndexRef.current === ignoreIdx) {
          ignoreIdx = -1
          return
        }
        stack.drop(IndexRef.current)
        onPopState(e).catch(() => {
          // 抛错时, 恢复 stack
          stack.push(IndexRef.current)
          window.history.pushState(null, '', window.location.href)
        })
      })
    
      useEvent('popstate', finalOnPopState)
    
      useListen(!!open, () => {
        if (open) {
          // modal index 初始化
          IndexRef.current = stack.latest + 1
          stack.push(IndexRef.current)
          window.history.pushState(null, '', window.location.href)
        } else {
          if (stack.has(IndexRef.current)) {
            // stack 内有该 modal index, 说明是其他地方手动调用 modal.hide, 而非物理返回所触发
            stack.drop(IndexRef.current)
            // 此时需要先清理 stack, 然后根据新 stack 设置标志位
            ignoreIdx = stack.latest
            // 设置标志位是为了避免下一个 modal 被此处触发的 popstate 关闭
            window.history.back()
          }
          // 不管怎么样, modal close 的时候都需要还原该 modal index
          IndexRef.current = -1
        }
      })
    }
    

    usage

    ``` typescript {4-6,14-16} function TempA() { const modal = useModal()

    useInjectHistory(modal.visible, () => { modal.hide() })

    return <> }

    function TempB() { const [open, setOpen] = useState(false)

    useInjectHistory(open, () => { setOpen(false) })

    return { setOpen(false) }} > body } ```

    ]]>
    https://tytcn.cn/blog/CLc499WyaT7Khttps://tytcn.cn/blog/CLc499WyaT7KWed, 08 Nov 2023 14:22:38 GMT
    <![CDATA[介绍一个学习 CSS 的网站]]>介绍一个学习 CSS 的网站, https://www.cssportal.com/, 站内涵盖了众多 CSS 现成、可配置的实践案例, 整个网站有一些广告, 但是不太影响使用。

    推荐示例

    1. 各种贝塞尔曲线动画效果演示

    动画演示.gif

    你还可以一键查看不同曲线的效果动画

    动画列表.png

    1. clip path 实现代码及效果

    clip-path.jpg

    1. 各种精美的 loading 图

    spin.gif

    下图可以看到只有一个 dom 元素, 很简洁, 也便于我们定制化

    spin.jpg

    目录

    categories.jpg

    ]]>
    https://tytcn.cn/blog/SWnQrBVd8y7yhttps://tytcn.cn/blog/SWnQrBVd8y7yWed, 08 Nov 2023 14:11:07 GMT
    <![CDATA[极简 js 入门]]>需要下载的东西:

    编辑器, 写代码用的
    sublime / notepad++ 等现代编辑器都行
    windows 自带的记事本不行

    • node js 的环境, 是一个命令行工具

    起步

    1. 新建一个项目目录

    最好起全英文名, 别带中文, 别带空格
    例如 hello-worldjs-testhello (分割单词使用中划线 下划线都行, 看自己喜好)

    1. 在刚刚那个目录里面新建一个 js 文件

    一般叫 index.js 或者 main.js
    用上面下载的 vscode, 别用 windows 自带的记事本

    1. 在里面写些代码并保存

    比如说

       console.log('hello world')
    
    1. 执行

    在项目目录下打开命令行工具, 执行 node index.js, 应该就能看到打印出来的 hello world

    进阶

    • 去网上找些案例, 跟着做一做
    • 把项目保存到 github(或 gitee), 并使用 git 管理项目
    • 使用 typescript

    有用的站点

    可以在 https://github.com 上面新建一个自己的账号, 代码可以保存在里面。上面说的 git 就可以用于管理 github 上的项目的。(如果实在没有梯子, https://gitee.com/ 也勉强能用)

    大佬

    大佬

    脚手架, 能帮助我们更快、更便捷地构建项目
    建议学习 js 半个月之后再接触

    一个前端的框架
    如果想要学习前端就可以学学
    建议学习 js 半个月之后再接触

    ]]>
    https://tytcn.cn/blog/9yQot2vnXJVihttps://tytcn.cn/blog/9yQot2vnXJViFri, 03 Nov 2023 07:54:41 GMT
    <![CDATA[奇奇怪怪的 Date]]>我们知道, Date 实例有一些 set 方法, 如设置月份 setMonth, 设置日期 setDate, 设置时分秒 setHours, 他们都可以接收零或者负数。

    我们知道, setMonth(0) 表示将 Date 实例的月份设置为第 0 个月, 即 "1月"

    const d = new Date('2023-06-06 06:06:06')
    d.setMonth(0)
    // 2023/1/6 06:06:06
    console.log(d.toLocaleString())
    

    我们也知道, setHours(0) 表示将 Date 实例的 hour 设置为第 0 小时, 即 "0点钟"

    const d = new Date('2023-06-06 06:06:06')
    d.setHours(0)
    // 2023/6/6 00:06:06
    console.log(d.toLocaleString())
    

    但, setDate(0) 并不会将日期设置为本月第 0 天(1 号), 而是上月的最后一天…

    const d = new Date('2023-06-06 06:06:06')
    d.setDate(0)
    // 2023/5/31 06:06:06
    console.log(d.toLocaleString())
    

    或许是因为 getDate() 返回值是 1-31?

    同理, 参数为 -1 就显然了

    ]]>
    https://tytcn.cn/blog/fYj4OYafYbOkhttps://tytcn.cn/blog/fYj4OYafYbOkSun, 29 Oct 2023 06:40:54 GMT
    <![CDATA[我们应该抵制不合理的募捐]]>今天看到知乎上一条提问: 如何看待「成都恶犬撕咬女童」事件家属发起200万募捐?, 里面有一条评论是:

    没问题啊,我看孩子可怜捐点钱怎么了?你觉得不行你不捐不就行了。至于孩子家里有钱没钱什么的关我捐钱的什么事?

    那么, 看待募捐, 能简单地说 我看孩子可怜捐点钱怎么了?你觉得不行你不捐不就行了 吗? 我认为不行。

    注意, 本文并不表示我赞同或者不赞同此次募捐, 本文并不是针对此次募捐的评论, 而是对 "募捐" 这一行为/概念的讨论。

    募捐, 对于整个社会来说, 并不仅仅事关 "发起募捐者" 和 "愿意捐款者" 两者, 同时, 还会消耗整个社会的 "同情心" 和 "注意力"。

    一方面, 募捐的泛滥, 会导致热心人士以及普通人(中立者)注意力的分散, 会导致真正迫切需要帮助的信息被淹没, 从而更难从社会得到妥善的帮助;

    另一方面, 每一次不合理的募捐, 都是对社会热心人士泼的一盆凉水, 是对同情心的消费。

    因此, 对于不必要(不合理)的募捐行为, 我们应该理直气壮地抵制。


    顺便, chatgpt 牛啊, 我把这篇文章发给他帮我润色, 下图左边是我原文, 右边是经过润色后的, 可以发现确实通顺了不少。(但是我就是不改, 诶, 就是玩儿)

    原文和经chatgpt润色后的文章的对比

    ]]>
    https://tytcn.cn/blog/MpgeqGBS893zhttps://tytcn.cn/blog/MpgeqGBS893zThu, 19 Oct 2023 03:12:40 GMT
    <![CDATA[想想 svg 会怎么做]]>对于一些活动页, 有时候设计那边会出一些奇思妙想的设计稿, 对于一些需要支持动态修改的、含有特殊效果的文本, 如果 css 不能实现, 我们也可以考虑拼接 svg. 如下图:

    活动设计稿节选

    产品需要动态配置活动起止日期, 而设计稿中有描边, css 中的 text-stroke 效果不好, 太生硬, 我们就可以在 react 中拼接 svg, 文本可以使用变量

    const svg = <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 180 24"
      width={`${180 * 6}px`}
      height={`${24 * 6}px`}
      fill="#BB7A09"
      style={{
        fontSize: "16px",
        lineHeight: "1.5",
        fontWeight: "500",
        textAnchor: "start",
      }}
    >
      <text
        x="4"
        y={24 / 2}
        stroke="white"
        strokeWidth="4.5px"
        strokeLinecap="round"
        strokeLinejoin="round"
        // dominantBaseline 需要设置到 text 上, safari 不会从父节点继承该属性
        dominantBaseline="central"
      >
        活动截止日期:11月31日 {/* 这儿可以按需使用变量 */}
      </text>
      <text x="4" y={24 / 2} dominantBaseline="central">
        活动截止日期:11月31日 {/* 这儿可以按需使用变量 */}
      </text>
    </svg>
    

    需要什么效果, 就可以拉着 UI 一起配合写出相应效果, 主打的就是一个灵活。更多的 svg 的奇思妙想, 可以看看 tympanus.net/codrops, 里面有很多花里胡哨天马行空的交互、UI.

    P.S. svg 当然也有劣势, 排版/换行什么的需要手动计算。但是我们只是在少量的地方自己写 svg, 大部分的 svg 直接从设计软件中导出来就能用, 所以无所谓。

    ]]>
    https://tytcn.cn/blog/5-4WU7eliMEdhttps://tytcn.cn/blog/5-4WU7eliMEdWed, 18 Oct 2023 03:45:44 GMT
    <![CDATA[记一次接入支付宝时 XSS 风险]]>
    • 不是支付宝的问题, 是我们自己业务的问题;
    • 由于 xss 风险出在桌面端支付宝, 下面就只说桌面端支付宝的情况了;

    前因后果

    目前业务上接入第三方支付的流程是, 前端把商品 id 给后端, 后端返回订单 id 和第三方支付 (支付宝) 相关信息: 一个字符串, 内容为 <form>xxx</form><script>form.submit()</script>, 需要注入到页面内并执行 form.submit(), form.submit() 会将当前页面跳转到支付宝的支付页面; 伪代码如下:

    ``typescript {1,9} // 商品页面 // 先有的后端接口, 后有的业务流程; 后端接口和业务流程没有前端参与评审; const { orderId, aliHtml } = await post(/api/order/${goodsId}) // 由于当前页面需要保留, 所以只能新页面打开, 并在新页面中处理 form.submit() 相关逻辑 window.open(/pay-redirect?aliHtml=${aliHtml}`, '_blank') // 监听订单状态 (后续在页面中提示订单状态) const result = await listenOrder(orderId)

    // 支付页面 (即 /pay-redirect) const { aliHtml } = searchParams injectIntoHtml(aliHtml) ```

    相信大家能看出来了, xss 漏洞就发生在 /pay-redirect 页面, 直接从 url 中提取字符串并作为 html 注入到页面中, 是不安全的, 恶意用户可以在参数中添加恶意代码, 并作为钓鱼链接发布, 普通用户点击后就会执行恶意代码。

    解决方案

    1. 也是最优方案: 修改业务流程 (后端修改接口), 将获取业务订单发起支付订单分为两个接口, 前端先获取业务订单, 再凭借业务订单来发起支付订单 (由于时间关系, 该方案没有被采纳);
    2. 先修改前端流程, 后续有时间再说: typescript {3} const subWindow = window.open('/pay-redirect', '_blank') const { orderId, aliHtml } = await post(`/api/order/${goodsId}`) // 接口获取到 aliHtml 后, 再通知 /pay-redirect 页面 notice(subWindow, { aliHtml }) // 监听订单状态 (后续在页面中提示订单状态) const result = await listenOrder(orderId)

    经验教训

    没有, 因为方案评审压根就没拉前端 [卑微]

    开玩笑啦, 不是吐槽, 其实还好, 之前一直都是这样, 后端自己内部评一下后端技术方案, 然后出接口文档, 然后前端按接口文档搭页面; 由于 90% 都是再常见不过的业务需求, 张三李四王二麻子谁来写都差不多, 接口文档也都大差不差, 所以一直没有什么问题, 只不过这次的支付可能就是那 10% ?

    所以可能经验教训是: 夜路走多了, 总会碰到鬼?

    ]]>
    https://tytcn.cn/blog/lKUa2x0AJV0Rhttps://tytcn.cn/blog/lKUa2x0AJV0RMon, 25 Sep 2023 08:20:00 GMT
    <![CDATA[个人站点搭建记录]]>

    个人最终选择的方案是, 买了个腾讯云轻量服务器, 3 年 288 元,1 核 2G,硬盘 60 GB, 送宝塔镜像, 本地构建 Next.js 项目, 再推送到服务器, 最后执行 ssh 重启。为什么如此选择? 那就要说说其他方案为什么不合适了。

    尝试一: 云上构建

    云上构建, 最开始我试了 github action 监听 push 后自动触发宝塔 webhook, webhook 执行 git pull 从 github 拉取最新的镜像, 执行构建。结果第一步就折戟了, 服务器拉不到 github, gfw 挡住了。。。

    既然 github 拉不到, 那 gitlab 总可以吧? 毕竟 gitlab 不需要梯子的。确实可以, 结果倒在了第二步: 服务器跑不动 Next.js 的构建。我重新试了一下, 空的 Next.js 项目可以 build 完成, 但是随便写点什么东西, 内存就爆了, 直接飙到 100%。

    尝试二: 本地 windows 构建

    我个人电脑是 windows, 云上是 centos, 其实这个方案我没尝试, 因为肯定失败: 项目依赖 sharp 进行图片相关优化, sharp 是需要在 install 的时候执行跟平台有关的 build 的, windows 上执行的 build, 放到 linux 上运行必然抛错。

    尝试三: 本地虚拟机构建

    所以我就本地装了一个跟服务器同一版本的 centos 镜像, 在里面执行构建, 构建完成后上传到服务器, 然后 ssh 执行项目 reload, 显然这样是可行的, 已经稳定运行了个把月了。部署脚本感觉没什么好说的, 愿意看的可以点 deploy.sh

    ]]>
    https://tytcn.cn/blog/lOIqZGcuWRJphttps://tytcn.cn/blog/lOIqZGcuWRJpMon, 04 Sep 2023 13:19:19 GMT
    <![CDATA[小秀一下站点评分哈哈哈]]>桌面端 lighthouse 评分

    桌面端 lighthouse 评分

    手机端 lighthouse 评分

    手机端 lighthouse 评分

    ]]>
    https://tytcn.cn/blog/-hg7STBVhTqthttps://tytcn.cn/blog/-hg7STBVhTqtSat, 02 Sep 2023 19:20:24 GMT
    <![CDATA[浏览器端 aws 文件上传]]>选择 aws 包

    我们去 google awa upload from browser, 总能看见一个包的身影 —— @aws-sdk/s3-request-presigner, 以及一些关键字 —— getSignedUrl or presign, 我遇到了各种各样的干扰:

    干扰一: deprecated

    如果我们搜这个包 @aws-sdk/s3-request-presigner, 会发现 google 第一条就是 This API Documentation is now deprecated, 经过测试, 要用的就是这个包, 估计那个 deprecated 只是说那个文档 deprecated…

    干扰二: 过时文档

    还有一些干扰文档, 如:

    // aws-sdk 已经过时了
    const AWS = require('aws-sdk')
    
    const s3 = new AWS.S3()
    
    const url = s3.getSignedUrl(/* ... */)
    

    aws-sdk 已经过时了, 我们应该用 @aws-sdk 系列包。

    排除干扰, 然后照着 @aws-sdk/s3-request-presigner 文档抄就可以了

    然而

    即使照着抄, 如果你使用的是 pnpm, 仍然会报 warning(虽然并不影响运行), 看 log 会发现它缺少一个包 @aws-sdk/signature-v4-crt, install 就好了。

    上码

    代码放这, 以供参考

    server side

    // server side
    import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
    import {
      S3Client,
      PutObjectCommand,
      ObjectCannedACL,
    } from "@aws-sdk/client-s3";
    
    import type { PutObjectCommandInput } from "@aws-sdk/client-s3";
    
    const s3Client = new S3Client({
      region: process.env.AWS_REGION,
      // 所需其他参数请参考 S3Client 文档
    });
    
    type RequestUploadConfig = Pick<File, "type" | "size">;
    
    export async function requestUpload(f: RequestUploadConfig) {
      // 我们可以在这里做鉴权及其他一些限制逻辑
      // ...
    
      const uploadParams: PutObjectCommandInput = {
        Bucket: process.env.AWS_BUCKET,
        /**
         * Key 实际上是上传的 pathname,
         * 但有一点需要注意, 不包括前缀 '/',
         * 即: 如果你的目标路径是 '/public/dir/file.txt',
         * 那 Key 应该是: 'public/dir/file.txt';
         */
        Key: uploadKey,
        // 我们可以限制用户上传的 type & size, 其他更多限制可以看文档
        ContentType: f.type,
        ContentLength: f.size,
        // 权限任选 public or private
        ACL: ObjectCannedACL.public_read,
      };
    
      const command = new PutObjectCommand(uploadParams);
    
      // 把预签名的 url 返回给客户端就好了
      return getSignedUrl(s3Client, command, { expiresIn: 3600 });
    }
    

    client side

    // client side
    const url = await requestUpload({
      type: f.type,
      size: f.size,
    });
    
    await fetch(url, {
      // 注意是 PUT, 而非 POST 或其他
      method: "PUT",
      body: f,
    }).then((res) => {
      if (!res.ok) {
        throw new Error(`上传失败: ${res.statusText}`);
      }
    });
    
    // ok 上传成功
    

    补充 Key 的问题

    上面说了 Key 不能带前缀 '/', 因为当你调用 aws 删除命令时, 带 '/' 的话不能成功删除, aws 也不会抛错, 会静默地失败。

    await new DeleteObjectCommand({
      Bucket: process.env.AWS_BUCKET,
      // 错误, 这样是删不掉的, 不能带 '/' 前缀
      Key: "/public/dir/file.txt",
      // 正确的 Key 应当是 'public/dir/file.txt'
    });
    
    ]]>
    https://tytcn.cn/blog/ztv4GbGwZZcthttps://tytcn.cn/blog/ztv4GbGwZZctThu, 31 Aug 2023 16:46:49 GMT
    <![CDATA[简述前端发展脉络]]>

    "前端是个娱乐圈, 成天发明各种概念。"
    "现在搞的服务端渲染, 那不就是炒 php 的冷饭?"
    "前端千辛万苦爬到山顶, 却发现 phper 早已等候多时。"

    本篇代码均为伪代码, 且不严谨不完善, 仅为示例

    1. 纯静态 html
       XXX 博客欢迎你
    
    1. 通用网关接口 CGI
    • 服务器上实现动态网页的通用协议。
    • 一次请求对应一个 CGI 脚本的执行,生成一个 HTML。
    • 不限语言, Perl、Shell 脚本、C 都行。
       print("""
         XXX 商城欢迎你
    
         <form onsubmit="/buy">
            <input name="count" />
            <button onclick="buy">买买买</button>
         </form>
       """)
    
    1. js & php
    • js: 客户端执行一些脚本, 一方面节约服务端资源, 另一方面提高用户体验 (不用刷新整个页面);
    • php: 更便捷地混排 server data + html + js;
       欢迎 <?php echo $_POST["fname"]; ?>! <br>
    
       <script>
       function plus(a, b) {
         alert(`和为 ${a + b}`)
       }
       <script>
    
       <input name="a" />
       <input name="b" />
       <button onclick="plus">相加</button>
    

    在此之前, 前端大多不能独立部署, 代码组织也受限于后端, "戴着镣铐跳舞"。

    1. ajax

    js 能从服务端动态获取数据; 标志着前端有能力从代码层面脱离后端独立部署, 独立完成需求;

       <script>
         function buy(count) {
           fetch(`/buy?count=${count}`)
         }
       </script>
    
       <input name="count" />
       <button onclick="buy">买买买</button>
    
    1. MVVM: react / vue / angular 等
    • 原则: 数据驱动视图;
       const unitPrice = 10
       const count = 0
    
       // 之前
       function onClick(newCount) {
         count = newCount
         const total = unitPrice * count
         countElement.innerText = `个数: ${count}`
         totalPriceElement.innerText = `总价: ${total}`
       }
    
       // 之后
       const total = unitPrice * count
    
       function onClick(newCount) {
         count = newCount
       }
    
       return (
         <div>
           <div>个数: {count}</div>
           <div>总价: {total}</div>
         </div>
       )
    
    1. (4.1) 前端路由 (单页面)

    前端越来越简单, 能做的事情越来越多, 干脆一把梭, "路由也交给前端处理吧"; 前端统一组织代码, 切换页面本质上是组件替换, 能保留子组件状态, 能实现页面转场动画, 跳转还很快, 简直"一本万利"。

       if (pathname === '/a') {
         return <PageA />
       }
       if (pathname === '/b') {
         return <PageB />
       }
    
    1. 服务端渲染 ssr
    • 单页面的问题: 首次渲染很重 (懒加载可以解决)、SEO 不友好;
    • (Node) 服务端渲染优点:
      • 有些东西在服务端就可以决定, 不需要发送给客户端, 降低传送的体积;
      • 服务端直出 html, SEO 友好;
      • 客户端、服务端语言一致, 类型可以共用;
       // server
       const config = await fetch('/config')
    
       if (config.aaa) {
         return <div>aaa</div>
       }
       if (config.bbb) {
         return <div>bbb</div>
       }
    
    1. rsc (react server component)

    即使如 ssr, 也需要生成一整个 html, 然后发给客户端, 如果我们能流式传输, 服务端一边处理, 一边响应客户端呢? 那速度不得起飞?

       // 真实代码远不是这样, 这写法甚至是错误的, 只为简述, 理解概念
       const aaa = await fetch('/api/aaa')
       const bbb = await fetch('/api/bbb')
       const ccc = await fetch('/api/ccc')
    
       return (
         <div>
           <div>{aaa}</div>
           <div>{bbb}</div>
           <div>{ccc}</div>
         </div>
       )
    
    ]]>
    https://tytcn.cn/blog/E_J4g1pSNTg2https://tytcn.cn/blog/E_J4g1pSNTg2Wed, 30 Aug 2023 03:54:29 GMT
    <![CDATA[linux 一步到位解除端口占用]]>查看占用端口的进程 id (即 PID)
    # 此处的 {PORT} 指你需要查看的端口号
    lsof -t -i:{PORT}
    
    # 示例
    lsof -t -i:3000
    

    终止进程

    # kill -15 让进程正常退出
    kill -15 {PID}
    
    # kill -9 表示强制中断, 不推荐使用, 只有当进程发生异常, 自身无法正常退出时, 才使用
    kill -9 {PID}
    

    一步到位

    kill -15 `lsof -t -i:{PORT}`
    

    如果你想终止某个命令

    假定命令名是 next-render-worker-xxx

    kill -15 $(ps aux | grep '[n]ext-render-worker-' | awk '{print $2}')

    ]]>
    https://tytcn.cn/blog/9FyGrXBK__jJhttps://tytcn.cn/blog/9FyGrXBK__jJSun, 27 Aug 2023 06:47:01 GMT
    <![CDATA[宝塔命令行重启 node 项目]]>1. 终止进程

    linux 一步到位解除端口占用

    # {PORT} 指项目运行的端口号
    kill -15 `lsof -t -i:{PORT}`
    

    2023-08-27 append: 需要额外清理旧的进程, 详见 记一次排查服务器内存占用爆满

    kill -15 $(ps aux | grep '[n]ext-render-worker-' | awk '{print $2}')

    2. 重新启动项目

    宝塔问答: 如何重启可视化 node 项目

    # {PROJECT_NAME} 指你自己待重启的项目名
    bash /www/server/nodejs/vhost/scripts/{PROJECT_NAME}.sh
    
    ]]>
    https://tytcn.cn/blog/LXf9GP8BHrb9https://tytcn.cn/blog/LXf9GP8BHrb9Sun, 27 Aug 2023 06:39:20 GMT
    <![CDATA[记一次排查服务器内存占用爆满]]>

    服务器内存使用率 90%+, 我一个 2G 的服务器就运行一个 Node 和一个 mysql, 不可能占用那么多啊, 平时都是 40% 不到的, 所以需要排查问题

    查看内存占用

    首先应该查看是谁占用了那么多内存

    top -c -o %MEM 可以查看当前系统实时视图

    • -c 表示显示完整信息, 不截断
    • -o %MEM 表示以 %MEM 排序

    一看就发现问题了 (忘了截图), 排最前面有六七个 next-render-worker-app / next-render-worker-pages, 每个都占用了 %5 左右的内存, 加起来占了一半多, 原来内存都被你们吃了!

    应该是 命令行重启 node 项目 的时候, 需要额外清理这些进程

    解决方案

    重启项目之前清理掉相应的进程

    kill -15 $(ps aux | grep '[n]ext-render-worker-' | awk '{print $2}')

    ]]>
    https://tytcn.cn/blog/7qvCFp-jBGQghttps://tytcn.cn/blog/7qvCFp-jBGQgSun, 27 Aug 2023 06:34:14 GMT
    <![CDATA[记一次愚蠢的缓存危机]]>问题

    今天改了 manifest.json, 推到服务器上后, 一直不生效。看了一下缓存头, Cache-Control: max-age:31536000, must-revalidate, 糟糕。。。

    原因

    • 下列所述 "过期" 是指 stale: 客户端综合计算 age + max-age + Date 得出的当前资源是否有效的状态;
    • 源服务器综合考虑 If-Modified-SinceIf-None-Match 后, 回复客户端是否过期 (如果已过期, 会在 body 中返回新的资源; 如果没过期, 就返回 304);
    • 下列所述 "不能" / "必须" / "应该" 都是指服务器如此要求, 但是否执行取决于客户端的实现, 你完全可以自己编译一个浏览器, 忽略请求头, 应该不犯法;
    • no-store 表示不能缓存
    • no-cache 表示可以缓存, 但客户端每次使用都必须与源服务器验证是否有变
    • must-revalidate 表示可以缓存, 但一旦过期, 就不能继续使用该资源, 而是必须与源服务器重新验证才能使用, 如果连不上源服务器, 那你就别用了 (注意, 平时没过期的时候, 客户端是不会去找服务端验证的)
    • stale-while-revalidate 表示即使缓存过期了也还能用, 只要你用了之后重新验证并更新缓存就好了, 但是如果验证出错了 (500 或 404 或其他什么错误), 那你下次就不应该再用那个过期的资源了
    • stale-if-error 表示即使缓存过期了也还能用, 之后重新验证并更新缓存就好了, 万一重新验证出错 (500 或 404 或其他什么错误), 你下次还能用旧的缓存
    • immutable 表示只要还没过期,你就尽管用, 不要来问我服务器, 肯定不会变

    不应变化的静态资源可以适用 must-revalidate, 例如不会变化的文件如 vconsole.min.js, 自带版本号的文件如 sensorsdata-20110513.min.js, 这类文件无需打包, 可以直接放在 public 静态目录中, 内容长期不会变化, 如果有变, 我们也会修改版本号, 所以可以适用长期缓存, 且客户端每次使用都无需重新验证。(这种情况其实也能用/更适合用 immutable)

    但是像一些可能有变、且不能随便修改文件名的文件, 千万不要使用 must-revalidate (即使要用, 也设一个短一点的 max-age)。如 favicon.ico, manifest.json 等, 应该用 no-cache, 每次用的时候必须验证是否有变。

    解决办法

    没什么好办法, 只能庆幸这是一个没人访问的私人站点。。。

    1. 手动清理浏览器缓存, 发现还是没用, 然后发现 Response Headers 中有另一个 header: X-Cache: HIT, 所以:

    2. 清理 nginx 缓存

    OK, 勉强搞定

    后记

    最后, 如果真的有用户访问的项目遇到了这样的问题, 我们没办法一个一个去清理用户的浏览器, 那么我们能怎么做呢?

    1. Deleting stored responses 说, 可以给服务器发个同 url 的 POST 请求

    One of the methods mentioned in the specification is to send a request for the same URL with an unsafe method such as POST, but that is usually difficult to intentionally do for many clients.

    1. 同样是 1 中的链接说的, 使用 Clear-Site-Data: "cache" 标头 (但是并非所有浏览器都支持该标头, 且该方法只能清空浏览器缓存, 无法处理中间缓存 intermediate caches)

    总结来说, 就是谨慎点, 别搞出这样的事, 给自己找麻烦。

    ]]>
    https://tytcn.cn/blog/tIUWujf42LL0https://tytcn.cn/blog/tIUWujf42LL0Sat, 26 Aug 2023 19:46:54 GMT
    <![CDATA[关于我]]>

    都是闲聊, hr 可以不用浪费时间在这里

    我是怎么走上前端的

    2017 年 (也可能 16 年底? 不记得了) 我开始自学前端, 那时候一股脑扎进来, 什么都不懂, 甚至都不懂找教程。那时候的目的甚至不是工作, 只是感觉要学点什么, 恰好前端写了就能看到结果, 就学了。

    当时买了一本红宝书《JavaScript 高级程序设计》, 照着敲, 忘了敲了多久了, 反正差不多敲完了一本书, 然后就到处找 demo 做, 其中有 2 个印象最深刻的, 一个是阿里某位大佬 (后来才知道是阿里的, 名字忘了) 的网站 http://www.fgm.cc/learn/ , 现在已经闭站了, 里面有很多入门级例子, 可以实现。另一个是 百度前端技术学院, 这是我唯一高度评价的百度的产品, 里面也有很多实战的项目, 并且由浅入深, 十分友好, 17 年及之前的题目都可称得上十分精良。可惜自从 vue 火了以后, 百度也想蹭一把热度, 强推自己的 mvvm 框架 san, 18 年 (也可能是 17 年年中?) 的整个站点项目全都跟 san 有关, 其后也慢慢消亡了。

    17 年 4 月份的时候实现了我的第一个博客, 可以说标志着我从此走上前端这条路

    2017年博客.jpg

    现在来说十分粗糙, 生成文章是靠"批处理"来做的, 也没有服务端的概念, 全部都是一股脑 html + css + js, 也几乎没有用库的概念, 几乎全部功能都是 js 手写的, 所以里面的 复制本文链接 其实是没用的, 因为我当时不知道怎么复制文本, 也没想过去网上搜索答案…

    我的前端路线摸索

    17 年 vue 刚开始火, 那时候就开始学 vue + webpack, 那时候到处是 webpack 的脚手架, 就尝试着自己从零搭脚手架, 用以学习 vue 和 webpack。但是我比较笨, vue 的 slot 总是搞不明白怎么用 (我的我的, 才知道 slot 是 web component 规范的一部分), @click="func" 还是 @click="func()" 总是搞混, 我这人很不喜欢背东西, 可是 vue 总是有层出不穷的东西让我背…

    后来 (忘了什么时候了) 接触到了 react 之后, 立刻就被 react 俘虏了, 这就是 js 啊, 没有任何新东西, 都是 js, 都是函数, 只有入参和出参。然后我就只做 react 了。

    18 年入职了第一家公司, 做 3d 相关的, 想搞一点 webVR, 把我招进去了, 那时候学上了全景, 开始用 krpano, 我用得很不爽, 根本就没有一点 js 的风格。我就寻找替代品, 找上了 three.js, 这才是前端该用的东西嘛。但是那家公司不喜欢 three.js, 所以次年我离开了那家公司, 入职了我的第二家公司, 做汽车全景内饰改装的。(顺带一提, 第一家公司在我离开后三四个月就倒闭了, 当然跟我的离开没有关系 hhh)

    19 年入职的第二家公司, 在我入职之前也是用 krpano 做全景, 我入职后也做了几个, 由于需要使用 xml 来书写 krpano 私有的语言, 也没有自动化工具, 效率奇低, 一天只能生成几辆车, 我入职之后, 自然就"不能惯着他们"了, 花了 2 个月重新实现了一整套, 从 three.js 前端到 python 自动化工具, 然后就舒服了, 只要电脑跑得过来, 车子随便你上, 三四个设计忙不过来了 hh

    说实话, 自从我实现了那一整套之后, 我的工作就变成了优化前端界面和 "自动化工具操控者", 收收文件, 点点鼠标, 无聊得紧, 我感觉他们不需要我了, 我的工作, 随便一个运营的同学来也能搞定, 还能比我搞得更精细更好。只是自那之后是"不可言说的三年", 所以 20 年我又在那家公司呆了一年, 混日子的一年。在那一年, 我意识到 three.js 其实不是前端, 而是数学, 而我, 承认我的数学不足以驾驭它, 因此, 转向普通前端吧。

    21 年, 入职了第三家公司, 普通的前端工作, 职责是为公司内部提供营销的技术支持。说得好听, 其实就是开发一些活动页, 无聊, 但有钱… 做的过程中, 我就意识到, react 很好, 可是貌似不太适合活动页, 单页面太重了… 可是没等我做什么, 仅仅呆了 4 个月, 公司裁员, 把我裁了, 因为 "双减", 而公司是当时青少年编程培训的领头羊, 首当其冲…

    被裁了之后, 跟着公司另一个项目组, 另立门户, 成为了现在这家公司的一员。基于之前的认识, 我决定用 next.js, 做服务端渲染, 并一直做到现在。

    我对前端的看法 (大言不惭)

    下面谈论的都是传统的前端 (像我这样的页面仔), 其他如编辑器/web 3d 等, 我搞不来, 就不妄言了。

    前端的现状 (我看到的)

    • 随着前端越来越复杂, 前端人员也需要掌握一定的部署知识 (不用太多, 太多就成运维了hh)。

    • 低代码在未来或许会吃掉一 (小) 部分低端岗位, 但不是现在。

    • 社会上仍然存在大量前端基础岗位 (月薪 20k 以下), 也就是传说中的 "页面仔 (指我自己)", 虽然面临了一定的 ai 带来的冲击, 但较为微弱, 大多是开发者出于提高自身效率的自发行为, 未有颠覆性的效率工具冲击现状。但同时由于前些年前端培训的大力发展, 人员供应过于充沛, 且受当前经济形势影响, 因此基础岗位竞争较为激烈。

    • 仍处于基础岗位上的开发者需要有危机意识, 一方面需要提高自身技术水平, 另一方面建议积极寻求跨界的机会, 如智能家居、车机等。

    前端的未来 (我认为的)

    • 短期 (未来 5 - 10 年) 来看, 社会发展、技术水平应该不会发展到, 不需要前端花力气优化、网页尺寸/性能无关紧要的地步。基于我个人的体验, 服务端渲染的页面性能、用户体验远好于传统单页面, 尤其是首屏体验。因此, 服务端渲染会在之后的几年越来越流行, 并成为顶峰时期的 jquery/php。(很难说是好还是坏)

    • 一方面服务端渲染对开发者的综合能力要求更高, 包括需要了解服务端和客户端的差异 (虽然是基础知识, 但之前其实不了解也能写前端), 包括组织代码也会比之前更复杂; 另一方面 ai 飞速发展; 两方综合之下, 未来几年将会减少大量前端基础岗位。

    • 随着社会的发展, wasm 将不再如现在这么 "重"; 同时随着语言的发展, 前端的门槛将越来越低, 其他语言的使用者将会更容易地 "侵入" 前端。"纯粹" 的前端将会在服务端渲染技术到达顶峰后, 面临长期的下坡路。未来是综合开发者的天下 (此处的 "综合" 并非是说技术上的 "全栈", 而是说综合性的、跨行业的)。

    ]]>
    https://tytcn.cn/blog/3EpPJTM2LwB_https://tytcn.cn/blog/3EpPJTM2LwB_Wed, 23 Aug 2023 17:32:10 GMT
    <![CDATA[如何优雅地处理 loading]]>让我们想象一个场景:

    “下班顺路买一斤包子带回来,如果看到卖西瓜的,买一个。”

    现在,让我们用 react 来实现一下:

    ```tsx {2,5,17} function GoHome() { const [loading, setLoading] = useState(false)

    const goForward = async () => { setLoading(true) const stuff = await lookAround() switch (stuff) { case '包子': await buy(2, '斤', '包子') break case '西瓜': await buy(1, '个', '包子') break default: break } setLoading(false) }

    // do something with loading }

    这样显然是不行的,因为异步方法可能会抛错,导致 `setLoading(false)` 被跳过,那么我们可能会:
    

    tsx {2,6} const goForward = async () => { setLoading(true) try { // … } finally { setLoading(false) } }

    这样实在是不够优雅。
    
    what about this:
    

    tsx {2,4} function GoHome() { const [loading, withLoading] = useLoading()

    const goForward = withLoading(async () => { const stuff = await lookAround() switch (stuff) { case '包子': await buy(2, '斤', '包子') break case '西瓜': await buy(1, '个', '包子') break default: break } })

    // do something with loading }

    以此延伸,我们通常会有这样的需求:点击按钮时发送请求,如果请求超过 500 ms, 再展示 `loading`
    

    tsx {2} function HelloWorld() { return }

    实现不复杂,直接看**全部代码**
    

    tsx import { clamp } from 'lodash-es' import { useCallback, useState } from 'react'

    const InfiniteTimeout = 2 ** 31 - 1

    /**

    • 同一个 withLoading 可以包装多个函数,

    • 同一个 withLoading 包装的所有函数全部执行完毕后才会关闭 loading; *

    • @usage

    • ```tsx

    • function Comp() {

    • const [ loading, withLoading ] = useLoading() *

    • const { data } = useSWR('xxx', withLoading(asyncTask1)) *

    • return <>

    • *

    • <Button

    • onClick={withLoading(asyncTask2)}

    • >

    • async task

    • }

    • ``` */ export function useLoading() { const [flag, setFlag] = useState(0)

      /**

    • @param fn 需要包装的函数

    • @param delayMs 延迟显示 loading (!!! 而非延迟执行函数) */ const withLoading = useCallback( ( fn: (…args: Arg) => Res | Promise, delayMs = 500 ) => async (…args: Arg) => { let timer = -1 if (delayMs > 0) { timer = +setTimeout(() => { timer = -1 setFlag((prev) => prev + 1) }, clamp(delayMs, 0, InfiniteTimeout)) } else { setFlag((prev) => prev + 1) } try { return await fn(…args) } finally { clearTimeout(timer) if (timer < 0) { setFlag((prev) => prev - 1) } } }, [] )

      return [ loading: flag > 0, withLoading, ] as const }

    ```


    后记:代码的实现很简单,但我感觉从代码风格上来说,由原来的命令式,改为了声明式,更 "react" 了,所以感觉很有意义,就此记录。~~(当然不会说是为了水一篇文了)~~

    ]]>
    https://tytcn.cn/blog/rhCL8zAniBX_https://tytcn.cn/blog/rhCL8zAniBX_Tue, 22 Aug 2023 14:07:51 GMT
    <![CDATA[我给弹窗添加了支持物理返回键 一]]>背景介绍

    我们知道, 对于 app 端的弹窗, 物理返回键可以关闭弹窗, 但是在 web 端, 按物理返回键, 直接就返回上一页了。

    返回键关闭弹窗, 无疑交互体验更好, 那么怎么实现呢?

    首先向大家推荐一个好用的弹窗管理库: NiceModal, 下列代码都是基于该库以及 mui (大多是伪代码, 当然可以不用库或者使用其他库)

    利弊分析及思路分析

    基本实现思路

    因为返回键会改变 history, 所以我们的实现思路肯定是通过监听 popstate 来关闭弹窗:

    • 打开弹窗时 history.pushState()
    • popstate 事件发生时关闭弹窗
    • 同时当我们手动关闭弹窗时, 需要 history.back() 恢复 history

    好处

    • 交互体验更好

    坏处

    • 由于需要 history.pushState()history.back(), 会破坏用户的浏览记录
    • 用户本来的浏览记录从 A 页面跳转 B 页面, 再返回 A 页面, 此时用户本来可以通过浏览器的 "前进 (forward)" 按钮回到 B 页面的, 如果我们在 A 页面执行 history.pushState() 的话, 用户就无法通过 back / forward 回到 B 页面了

    期望的调用方式

    暂且将我们的方法命名为 useInjectHistory

    // 声明
    const TestModal = NiceModal.create(() => {
      const modal = useModal();
    
      useInjectHistory(modal);
    
      return <Dialog>test modal</Dialog>;
    });
    
    // 调用
    NiceModal.show(TestModal);
    

    但是调用方需要有阻止弹窗关闭的能力, 也就是说 "弹窗关闭" 需要放在外部, 即:

    // 声明
    const TestModal = NiceModal.create(() => {
      const modal = useModal();
    
      useInjectHistory(modal, () => {
        modal.hide();
      });
    
      return <Dialog>test modal</Dialog>;
    });
    
    // 调用
    NiceModal.show(TestModal);
    

    实现思路

    export function useInjectHistory(
      modal: NiceModalHandler<Record<string, unknown>>,
      /**
       * 如果需要在用户物理返回时关闭弹窗, 就在该方法中手动调用 modal.hide();
       * 如果拒绝关闭弹窗, 就别 hide() 并 throw Error;
       */
      onPopState: (e: PopStateEvent) => Promise<void>
    ) {
      // mui 的 useEventCallback
      const finalOnPopState = useEventCallback((e: PopStateEvent) => {
        isTriggeredByPopStateRef.current = true;
        // 当 popstate 发生时, 如果 onPopState 抛错 (即调用方拒绝关闭弹窗),
        // 那么此处重新 history.pushState, 以恢复 history 栈
        onPopState(e).catch(() => {
          window.history.pushState(null, "", "#dialog");
        });
      });
    
      // react-use 的 useEvent (也可自行使用其他方法, 反正就是监听 popstate, 然后执行 onPopState)
      useEvent("popstate", finalOnPopState);
    
      // useListen 见下文所述
      useListen(modal.visible, () => {
        if (modal.visible) {
          // 弹窗打开时, push history
          window.history.pushState(null, "", "#dialog");
        } else if (!isTriggeredByPopStateRef.current)
          // 其他地方 (非 popstate 事件) 触发弹窗关闭时, 恢复 history 栈
          window.history.back();
      });
    }
    

    useListen 见本站文章 一个监听变量变化的语法糖


    2023-08-10 更新:

    上面的实现有一个问题: 如果我页面上多个弹窗并存时, popstate 事件会在所有弹窗中触发, 导致一个 popstate 关闭了所有弹窗, 如何解决这个问题呢? 且听下回分解

    ]]>
    https://tytcn.cn/blog/njC-AH0TX131https://tytcn.cn/blog/njC-AH0TX131Thu, 10 Aug 2023 08:20:12 GMT
    <![CDATA[一个监听变量变化的语法糖]]>由来

    在实践中, 我们(我)经常会遇到, 当某变量变化的时候, 执行其他方法(副作用), 通常我们使用 useEffect:

    function useHook() {
      const [varA] = useState();
    
      useEffect(() => {
        // do something
      }, [varA]);
    }
    

    但是如果执行的方法中包含其他响应式变量 varB, 我们需要 varB 的最新值, 但却不希望 varB 变化时触发 useEffect 重新执行, 那我们可能需要 hack:

    function useHook() {
      const [varA] = useState();
      const [varB] = useState();
    
      // mui 的 useEventCallback
      const effect = useEventCallback(() => {
        doSomething(varB);
      });
    
      useEffect(() => {
        effect();
      }, [varA, effect]);
    }
    

    实现

    因此我们可以写一个小小的语法糖来简化这一目的:

    export function useListen<T>(
      value: T,
      // prev 需要可能为 undefined, 因为初次渲染时, prev 就是 undefined
      callback: (next: T, prev: T | undefined) => void
    ) {
      const prevRef = useRef(value);
      const callbackRef = useRef(callback);
      callbackRef.current = callback;
    
      useEffect(() => {
        /**
         * useEffect 在 dev 环境会执行 2 遍, 此处避免该行为造成的影响;
         * 因为我们该方法的目的与 useEffect 单纯的 "执行幂等的副作用" 不同,
         * 该方法的目的就是变量改变的时候执行 callback, 并不要求 callback 幂等,
         * 所以不应该执行 2 遍;
         */
        if (value === prevRef.current) {
          return;
        }
        callbackRef.current(value, prevRef.current);
        prevRef.current = value;
      }, [value]);
    }
    

    用法

    ``` typescript {4} function Test() { const [varA, setVarA] = useState()

    useListen(varA, (next, prev) => { console.log({ next, prev }) })

    // … }

    ### 利弊分析
    
    #### 优势
    
    - 语义明晰
       - 就是为了监听变量变化而生
    
    - 免除各种冗余的格式代码
    
    #### 不足
    
    - 不应用于添加事件监听等
    

    typescript useListen(varA, () => { // 错误用法, 因为没有消除副作用 (removeEventListener) window.addEventListener('event-name', callback) }) ```

    ]]>
    https://tytcn.cn/blog/QGMtI1cUUbNBhttps://tytcn.cn/blog/QGMtI1cUUbNBSat, 05 Aug 2023 13:56:07 GMT
    <![CDATA[小计一次 CLS 优化]]>网站 CLS(Cumulative Layout Shift) 评分一直在 0.7+, lighthouse 评分一直在 80 分上下, 然后就尝试找哪里有问题。

    首先运行 lighthouse 评分, 找到 performance 下的 CLS 选项, 看到主要两处影响了 CLS 评分:

    1. 第一处是所有文章的列表, 是一个 MUI Grid 组件

    当初文章列表打算做响应式, 就用了 MUI 的 Grid 组件, 后来决定列表不响应了, 但是感觉 Gird 还能用于纵向的 space, 就懒得换了。 可是这是 MUI 啊, 难道 MUI 还能影响 CLS? 不, 我不相信!!!

    然而看 CLS 的截图, 明确指向了 Grid 组件, 之前看过 Grid 组件实现原理就是负 margin, 难道是负 margin的问题? 一搜关键字, "mui Grid CLS", "negative margin CLS", 好几个 issue 指向了它:

    1. 第二处是所有标签(tag)的列表, 是一个 object 元素

    由于我希望在任何地方, 标签(tag)都能点击跳转到标签详情页(即使在另一个大的 a 标签下面), & 我希望使用原生的 a 标签, 以保证在禁用 js 的情况下仍能跳转, 然而 a 标签不能嵌套, 查了查, 哦, 可以用一个 object 标签包裹住 Tag 组件, 就用了。

    CLS 截图指示 Tag 组件影响了评分, 可是它只是一个行内文本, 带了点 padding 和 margin, 它什么也没干啊, 它也没有负 margin, 为什么说它影响了评分啊, 不, 我不相信!!!

    可是突然回想起前些天处理 tailwind preflight.css 时, 发现 tailwind preflight 把 object 和 img 放在了一起, 并将他们的默认样式设置为了 display: block;, 会不会, object 和 img 有差不多的性质?

    一看 MDN, 果然:

    HTML <object> 元素(或者称作 HTML 嵌入对象元素)表示引入一个外部资源,这个资源可能是一张图片,一个嵌入的浏览上下文,亦或是一个插件所使用的资源。

    它引用一个外部资源, 那他必然不知道那个外部资源有多大, 而它里面的文字是标签的 name, 有长有短, 就必然导致 object 产生偏移。是啦, 说干就干, 标签 name 固定一下长度, object 也设置一下宽度, 搞定。

    重新跑一下 lighthouse, 99 分, 啊, 多么美丽的绿色(是不是…有哪里不对)

    ]]>
    https://tytcn.cn/blog/9M7HgvNI--R3https://tytcn.cn/blog/9M7HgvNI--R3Fri, 28 Jul 2023 17:05:36 GMT
    <![CDATA[HTML 邮件需要注意的点]]>优秀参考

    XHTML 1 规范与 HTML 4 的差异

    翻译自 https://www.w3.org/TR/xhtml1/

    1. 文档格式必须正确

      正确: 嵌套元素

       <p>这是强调 <em>段落</em>。</p>
    

    错误: 重叠元素

       <p>这是强调 <em>段落</p>。</em>
    
    1. 元素名和属性名必须小写

      li 而非 LI

    2. 非空元素必须要结束标签

      正确

      <p>这是一段。</p><p>这是另一段。</p>
      

      错误

      <p>这是一段。<p>这是另一段。
      
    3. 所有属性值必须使用引号

      正确

      <td rowspan="3">
      

      错误

      <td rowspan=3>
      
    4. 不支持属性最小化(Attribute Minimization)

      正确

      <input checked="checked" />
      

      错误

      <input checked />
      
    5. 空元素必须有结束标签

      正确

      <br /><hr />
      

      错误

      <br><hr>
      
    6. 处理属性值中的空白

      根据 XML 属性值归一化 的标准:

      • 去除前导和尾随空白字符
      • 词间的一个或多个空白字符(包括换行符)将替换为一个 space
    7. script 和 style 元素(其实在邮件中是用不上 script 的)

      在 XML 中, <& 被视为标记的开始, 而 &lt;&amp; 会被转义为 <&. 所以如果你不希望他们被转义, 可以用 CDATA 包裹他们, 或者引用外部文档

      <script type="text/javascript">
        <![CDATA[
        ... unescaped script content ...
        ]]>
      </script>
      
    8. SGML 排除项

      SGML 禁止某些元素嵌套, XML 中没有这些禁令, 但仍表示这些元素不应嵌套

      • a 禁止包含其他 a
      • pre 禁止包含其他 img, object, big, small, sub, sup
      • button 禁止包含其他 input, select, textarea, label, button, form, fieldset, iframe, isindex
      • label 禁止包含其他 label
      • form 禁止包含其他 form
    9. 元素的 idname 属性

      HTML 4 为 a, applet, form, frame, iframe, img, map 定义了 name 属性, 同时也有 id 属性, nameid 都是用作片段标识符

      而 XML 仅使用 id 作为标识符, 且必须文档内唯一(id不能重复), 因此出于兼容性考虑, 当上述元素需要定义标识符时, 必须使用 id(而非 name)

      同时需要注意, 上述元素的 name 属性在 XHTML 1.0 中已被正式弃用, 并将在 XHTML 的后续版本中删除。

    10. 属性枚举值

      HTML 4 中, 属性枚举值不区分大小写(如 inputtype 属性, 值 TEXT 等同于 text), 但在 XML 中区分大小写, XHTML 1 中定义为小写

    11. 16 进制实体引用(Entity references)必须使用小写

      &#xnn; 而非 &#XNN;

    12. 空元素的尾随 / 前面要加一个空格

      • <br /> 而非 <br/>
      • 使用 <br /> 而非 <br></br> (因为后者, 用户代理可能给出不确定的结果, 即不同浏览器的解析结果可能不一致)
    13. 内容模型(content model)非空的元素, 别使用 minimized form

      <p> </p> 而非 <p />

    14. 如果 scripts or style sheets 脚本中有用到 <, &, ]]>, -- 时, 使用外部脚本

      XML 允许用户代理删除注释, 因此你想将他们放在注释中来做兼容也是行不通的

    15. 避免在属性值中使用换行符和多个空白字符, 用户代理处理这些情况是不一致的

    16. 同时使用 langxml:lang 时, xml:lang 的优先级更高

    17. 片段标识符

      标识符用于在 URI 结尾来引用元素, 如 #foo 会引用到 标识符的值为 foo 的元素。

      • XML 中以 id 作为标识符, HTML 4 以 name 作为标识符, 建议对支持的元素, 同时使用 idname, 以做到最大兼容。
      • 需要注意, XHTML 1.0 中 a, applet, form, frame, iframe, img, mapname 属性已弃用, 并将在 XHTML 的后续版本中删除。
    18. 在属性值(和其他地方)中使用 & 符号需要转义 (存疑)

      文档表示, 当元素 href 属性带有 & 符号时, 需要转义, 如:

      • 正确: http://my.site.dom/cgi-bin/myscript.pl?class=guest&amp;name=user
      • 错误: http://my.site.dom/cgi-bin/myscript.pl?class=guest&name=user

      我是有些存疑的, 那样还能访问到正确的地址吗? 待测试

    19. 一些在 HTML 文档中合法的字符, 在 XML 文档中是非法的。

      例如, 在 HTML 中, Formfeed 字符 (U+000C) 被视为空白, 在 XHTML 中, 由于 XML 对字符的定义, 它是非法的。

    20. 撇号 不应该使用 &apos; 而应该使用 &#39; > - 命名字符引用 &apos; (撇号 U+0027: ') 是在 XML 1.0 中引入的, 但没有出现在 HTML 中。因此, 应该使用 &#39; 而不是 &apos;, 才能在 HTML 4 用户代理中按预期工作。 > - 我就遇到过, 产品表示部分用户收到的邮件标题直接出现了 &apos;, 体验非常不好, 而 &#39; 是否可以避免这个问题? 待测试

    ]]>
    https://tytcn.cn/blog/mz9sNneP9r8chttps://tytcn.cn/blog/mz9sNneP9r8cFri, 28 Jul 2023 17:04:18 GMT
    <![CDATA[操作异步函数的两点建议]]>这儿是工具函数
    async function sleepMs(n: number) {
      return new Promise((resolve) => {
        setTimeout(resolve, n);
      });
    }
    
    async function asyncFunc() {
      await sleepMs(500)
    }
    
    async function randomAsyncFunc() {
      await sleepMs(Math.random() * 1000)
    }
    

    忠告一: 异步函数如果有顺序要求, 则最好 await 之

    // 此处, 你能幸运地得到顺序打印的 1 2
    async function main() {
      asyncFunc().then(() => {
        console.log(1)
      })
    
      asyncFunc().then(() => {
        console.log(2)
      })
    }
    
    // 此处, 只有老天才能知道 1 2 的打印顺序了
    async function main() {
      randomAsyncFunc().then(() => {
        console.log(1)
      })
    
      randomAsyncFunc().then(() => {
        console.log(2)
      })
    }
    
    // 因此: 如果对顺序有要求, 最好 await 之
    async function main() {
      await randomAsyncFunc().then(() => {
        console.log(1)
      })
    
      await randomAsyncFunc().then(() => {
        console.log(2)
      })
    }
    

    忠告二: 谨慎在异步函数中操作引用变量

    async function anotherAsyncFunc() {
      const arr = []
    
      sleepMs(500).then(() => {
        // 在异步函数中操作引用变量需要谨慎 (这是错误用法)
        arr.push(42)
      })
    
      return arr
    }
    
    async function main() {
      const res = anotherAsyncFunc()
    
      // 这儿打印的结果是 []
      console.log(res)
    
      await sleepMs(1000)
      // 这儿打印的结果却是 [42]
      console.log(res)
    }
    

    先解释一下上面的代码, 就是由于 arr 是引用变量, 所以, 返回值 res 开始是空数组, 等睡了 500 ms 后, arr.push(42) 被执行, 导致返回值被修改, 此后 arr 变成了 [42]

    你可能会说, 谁会写出这么傻的代码, 我只能说有则改之无则加勉, 操作异步函数需要谨慎, 最好是等待其 await, 如果特殊场合不允许, 则尽量别在其内操作指针或执行有顺序要求的动作, 一定要这么做的话, 确保自己能 hold 住

    ]]>
    https://tytcn.cn/blog/xhh7Oqi2-JXRhttps://tytcn.cn/blog/xhh7Oqi2-JXRFri, 28 Jul 2023 16:18:01 GMT
    <![CDATA[注意, setTimeout 有最大延时值, 溢出就会被立即执行]]>我们有时候使用 setTimeout 会简单粗暴地使用外部传入的值, 如下:

    async function sleepMs(ms: number) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    

    这样通常情况下没问题, 但是如果输入的数值过大, 会出现意想不到的 bug.

    我们知道 js 中, Number.MAXSAFEINTEGER2^53 - 1, 但是 setTimeout 并不支持那么大的数。

    https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout#%E6%9C%80%E5%A4%A7%E5%BB%B6%E6%97%B6%E5%80%BC

    浏览器内部以 32 位带符号整数存储延时。这就会导致如果一个延时大于 2147483647 (即 2^31 - 1) 毫秒(大约 24.8 天)时就会溢出,导致定时器将会被立即执行。

    所以, 如果数值是外部传入的, 建议函数内做一个简单的判断 (过滤):

    async function sleepMs(ms: number): Promise<void> {
      // 至于如何处理 NaN, 则可以根据业务, 自己决定是抛错还是赋默认值
      if (Number.isNaN(ms)) {
        throw new Error("invalid input: NaN");
      }
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, clamp(ms, 0, 2 ** 31 - 1));
      });
    }
    
    ]]>
    https://tytcn.cn/blog/IClO74MRNFmShttps://tytcn.cn/blog/IClO74MRNFmSThu, 27 Jul 2023 17:02:53 GMT