日志

  • 将 Next.js 项目从 Vercel 迁移到 Cloudflare

    将 Next.js 项目从 Vercel 迁移到 Cloudflare

    一起逃离 Vercel 拥抱 Cloudflare 吧

    Vercel 再次调价之后,性价比越来越低,每个月 $20 的额度根本扛不住什么访问量;而且建站所需的各种服务(数据库,KV 等)也欠缺,所以我觉得是时候迁离 Vercel,投奔赛博菩萨 Cloudflare 的怀抱了。

    不过由于 Cloudflare 平台的整体架构,其 Edge Runtime 和 Serverless runtime 都跟 Vercel,或者说标准架构存在一些不同之处,所以迁移的过程中往往需要我们做一些工作。本篇博客就来分享这些经验。

    Cloudflare 云服务

    首先,Cloudflare 不仅提供 Serverless / Edge 托管,更提供一整套几乎是必须的服务器组件:

    1. SQLite 接口的关系型数据库 D1,速度很快,第三方工具很多
    2. 形似 Redis 的 KV 数据库
    3. 存储服务 R2,兼容 AWS S3
    4. Queue、Durable Object 等几乎所有服务器长线运营必须的工具

    而且以上大部分都包含慷慨的免费额度,当你的产品度过极早期,需要更多额度时,$5/月也够用很久。总之,对比 Vercel 万国造且每个都要独立付费,当然是 Cloudflare 更好用。

    (当然,也会越来跟 Cloudflare 绑定越来越深,这方面见仁见智吧。)

    接下来,Cloudflare 更推荐我们使用他们家的 Worker 平台。以我的经验,使用 Worker 而不是 Pages 有以下好处:

    1. 实时日志,方便我们查看运行时错误和 debug
    2. 支持 cron trigger,可以方便的执行一些自动化操作
    3. 以及更好的缓存策略,比如预渲染

    自然,Worker 需要更多的工作,我们必须整体迁移到 OpenNext 才行。

    迁移到 OpenNext

    首先,请参考官方文档:https://opennext.js.org/cloudflare/get-started

    接下来,我也会捋一遍迁移过程,并分享我的经验。

    安装 @opennextjs/cloudflare

    这个适配器会帮我们在 Cloudflare 上运行我们的 Next.js 应用。

    pnpm install @opennextjs/cloudflare@latest

    安装 Wrangler

    Wrangler 是 Cloudflare 提供的命令行工具,可以帮我们完成很多工作,也是上面适配器的必备工具。

    pnpm install --save-dev wrangler@latest

    创建 wrangler 配置文件

    这个配置文件会影响到最后的部署和其它云服务使用。我建议大家使用 JSON 格式,因为语法更熟悉。

    {
      "$schema": "node_modules/wrangler/config-schema.json",
      "main": ".open-next/worker.js",
      "name": "<应用名称>",
      "compatibility_date": "<最近的日期>",
      "compatibility_flags": [
        "nodejs_compat",
        "global_fetch_strictly_public",
      ],
      "account_id": "<你的 account id>",
      "assets": {
        "directory": ".open-next/assets",
        "binding": "ASSETS",
      },
      "services": [
        {
          "binding": "WORKER_SELF_REFERENCE",
          "service": "<应用名称>",
        },
      ],
      "r2_buckets": [
        {
          "binding": "NEXT_INC_CACHE_R2_BUCKET",
          "bucket_name": "<BUCKET_NAME>",
        },
      ],
      "vars": {
        "NEXT_PUBLIC_SITE_URL": "<你的网站域名>"
      }
    }

    添加 open-next.config.ts 配置文件

    在根目录添加 open-next.config.ts 配置文件。

    import { defineCloudflareConfig } from "@opennextjs/cloudflare";
    import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
     
    export default defineCloudflareConfig({
      incrementalCache: r2IncrementalCache,
    });

    添加 .dev.vars 文件

    在根目录添加 .dev.vars 文件,告诉 Next.js 它应该使用哪一个 .env 文件。

    NEXTJS_ENV=development

    环境变量是比较难处理的一项工作。首先,线上使用的环境变量通常分两部分,一部分就是普通的变量,应该直接放在 wrangler.jsoncvars 字段里;另一部分是需要加密的比如各种 apiKey,需要通过 wrangler 工具放到 secrets 里。

    线上的环境变量必须保存在 wrangler.jsonc 里。这里我们只需要把本地开发环境所需的变量放在 .env.development,覆盖线上的即可。本地密钥也可以放在 .env 文件里。

    更新 package.json

    需要给 package.json 加上以下命令,方便开发和部署。

    "build": "next build",
    "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
    "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
    "upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
    "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts",

    其中,deploy 用来完成主动部署,upload 用来部署开发分支,cf-typegen 用来生成描述文件,让 IDE 的代码补全更强力。

    添加静态资源缓存

    创建 /public/_headers 文件,添加以下内容,让 Cloudflare CDN 默认缓存所有静态资源,加速网站访问。

    /_next/static/*
      Cache-Control: public,max-age=31536000,immutable

    移除 pages 相关内容

    从代码中移除 export const runtime = "edge";

    并卸载掉 @cloudflare/next-on-pages

    忽略掉 .open-next 和 .wrangler

    .gitignore 添加更多的忽略项。如果使用 ESLint,也可能需要添加。

    .open-next
    .next
    .wrangler

    本地开发

    修改 next.config.ts,增加 @opennextjs/cloudflare 提供的适配器。之后,你就可以使用 Cloudflare 提供的开发环境了。

    import type { NextConfig } from "next";
     
    const nextConfig: NextConfig = {
      /* config options here */
    };
     
    export default nextConfig;
     
    import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
    initOpenNextCloudflareForDev();

    【可选】移除首页的预渲染缓存

    如果你的首页需要加载远程数据,那么可能需要手动避免首页被预渲染,否则你可能会面对一个静态的首页。解决方案并不复杂,只需要给首页的 page.tsx 里添加下面的语句即可:

    // 完全不缓存,每次都重新渲染并加载
    export const dynamic = 'force-dynamic';
    // 或者如果不希望完全动态,只是希望数据不缓存
    // export const revalidate = 0;

    完成第一次部署

    接下来,建议大家执行 pnpm run deploy 完成第一次部署,这样会在 Cloudflare 里添加一个新的 worker,然后我们才好添加 secrets。

    关联 GitHub 仓库

    找到刚才创建的 Worker,在设置面板里找到“构建”,即可关联到我们的 GitHub 仓库。在构建配置里:

    1. 删除“构建命令”(Build command)
    2. 部署命令(Deploy comment)为 pnpm run deploy
    3. 非生产分支部署命令(Non-production branches deploy comment)为 pnpm run upload

    部署完成

    至此,迁移完成,后面正常推代码就可以触发自动部署了。

    总结

    其实还有一些工作要做,比如配置缓存,配置可见性,等等。大家可以根据需要来操作。

    希望这篇文章对大家所有帮助。如果大家对 Vercel,Cloudflare,Next.js 有任何问题或想法,欢迎留言讨论。

  • React Native + Expo 入门级实战开发多平台应用 WhiteScreen:3. 深入开发,完成应用主体

    React Native + Expo 入门级实战开发多平台应用 WhiteScreen:3. 深入开发,完成应用主体

    感谢剪辑同学的努力工作,第三集上线。

    油管地址:https://youtu.be/0Ix-Y-MPQY0

    B站地址:https://www.bilibili.com/video/BV1CxnJzfEAk/

    继续分享 React Native+Expo 应用开发,帮大家补上全栈开发的最后一块拼图:移动应用开发。

    在这个系列视频里,我会用一个入门级小应用 WhiteScreen 作为范例,介绍如何使用 React Native+Expo 开发移动应用,并上传到应用商店。虽然国内市场基本被几个超级应用垄断,但是海外的广阔天地仍然大有可为。而 React Native 可以最大限度的利用我们已有的技术积累,让我们快速完成原型开发与需求验证,是全栈开发的不二之选。

    这期是第三次课,主要介绍:

    1. 使用 Zustand 管理状态,在页面间同步数据
    2. 使用原生组件进行布局
    3. 实现简单动画
    4. 实现本地数据存储于使用

    希望能给大家带来更多的可能性,在这个不那么容易的年代,让大家都有更多的选择余地与发展空间。

    希望大家留下宝贵的一键三连分享收藏,让更多的人能看到我的视频。

    有任何问题和建议,欢迎留言弹幕一起讨论。

    谢谢大家!

  • 【视频教程】React Native + Expo 入门级实战开发多平台应用 WhiteScreen:1. 移动应用开发现状+项目简介+技术栈简介

    【视频教程】React Native + Expo 入门级实战开发多平台应用 WhiteScreen:1. 移动应用开发现状+项目简介+技术栈简介

    前几天我在博客 感谢赞助商 Mizu Financial,重启我的自媒体之路 提到,因为工作调整,下一阶段我要捡起自媒体,在新的工作开始之前,提升一下自身的品牌价值。所以,我开启了新系列视频教程的录制。

    现在新视频终于上线,敬请观看。

    油管地址:https://youtu.be/YEPvihSIQkw

    B站地址:https://www.bilibili.com/video/BV1ZgHoztE4v/

    (更多…)
  • 跟风吐槽一下小米

    跟风吐槽一下小米

    前两天小米市场部总经理王某被辞退,在数码媒体圈甚嚣尘上。我不认识这位,不便评价。不过我跟小米有些过节,所以赶紧跟风吐槽一下。开篇名义:我能不买小米就不买小米,因为我早就认为小米市场部很垃圾。

    我曾经不止一次说过,我不会买小米,也不会买魅族。这两家在我这里的问题是一样的:市场部人员不够诚心,欺骗老同事。

    (更多…)
  • 解决 React Native + Expo 面对 Google Play 的 16KB memory page 问题

    解决 React Native + Expo 面对 Google Play 的 16KB memory page 问题

    最近开始尝试开发 App,倒不是什么复杂的大项目,只是把朋友网站上的功能移植到移动端。技术栈仍然是 React Native + Expo,不过以前只做过 iOS,这次连 Android 一起做。

    那么自然的,这次就要踩 React Native Android 的坑。以后会分享所有相关的知识体验和坑,今天先分享最近两天花了不少时间解决的 Google Play 16KB memory page 问题。

    我们的应用提交到 Google Play 后,原本一切正常,前两天突然收到 Google 的政策通知:

    为确保您的应用能在最新版 Android 上正常运行,Google Play 要求以 Android 15 及更高版本为目标平台的应用支持 16 KB 内存页面大小。

    自 2026年5月30日起,如果您的应用更新不支持 16 KB 内存页面大小,您将无法发布相应更新。

    您的最新正式版应用不支持 16 KB 内存页面大小。

    嗯,必须承认,看到这个问题我一头雾水。不过好在我也不需要把它理解透彻,只要知道该怎么改就好。可惜的是,Gemini 对这个问题没什么了解,我只好去阅读 Google 的文档,得到的结论是:

    我需要修改 /android/app/build.gradle 其中的配置 useLegacyPackaging 将其改成 true

    android {
        packagingOptions {
            jniLibs {
                useLegacyPackaging true
            }
        }

    不过我使用的是最新版 expo prebuild 生成的 Android 项目,所以这个配置本身依赖 app.json 的配置,那么理论上,我只需要添加下面这行:

    {
      "expo": {
        "android": {
          "useLegacyPackaging": true
        }
      }
    }

    于是我改好配置重新打包上传,结果还是不行。认真阅读 app bundle 详细信息,发现错误位于 base/lib/arm64-v8a/librnskia.sobase/lib/x86_64/librnskia.so ,很明显,这是 @shopify/react-native-skia 包,也就是我们的绘图依赖。

    因为我的项目里用到 Expo,所以我一般用 Expo 安装依赖,安装的版本也由 Expo 决定。目前版本的 Expo 要求的 @shopify/react-native-skia 版本是 v2.0.0-next.4,在 GitHub issues 里搜索一下,发现这个版本果然不支持 16KB Memory page,而修复的版本是 v2.0.6。

    按照我的习惯,有新不用旧。于是直接升级到 2.2.9,然后应用就挂了……于是降级到 v2.0.7(2.0 版本的最高版本),测试没问题。打包上传,终于解决了 16KB 警告。

    简单总结一下:

    1. 不同平台有不同要求,不过大多可能和 RN 无关,通过项目配置就能解决
    2. React Native 由于跨平台,跨运行时,依赖之间的关系很复杂,不能乱升级,尽量控制小版本,只升补丁版本
  • 感谢赞助商 Mizu Financial,重启我的自媒体之路

    感谢赞助商 Mizu Financial,重启我的自媒体之路

    感谢 Mizu Financial 成为本站的新赞助商,也帮助我重新拾起自媒体之路。今年由于种种原因,我的博客和视频直播几乎彻底中断。最近终于有了一些富裕时间,在开启下一份职业生涯之前,我准备先把自媒体捡回来。

    介绍下赞助商:Mizu Financial 是一家硅谷初创企业,为北美和亚洲的中小型传统企业提供稳定币与比特币的财务管理 SaaS 服务,帮助客户在传统财务系统中安全接入数字资产,实现对加密资金的透明管理与自动化对账。公司成立于 2025 年 3 月,创始团队成员包括资深硅谷Web2/Web3 投资人、前 Meta 工程师、CMU 博士等连续创业者。

    接下来,我会尽量在白天保持直播,写各种自己的和甲方的项目,把以前挖的各种坑填上,并保持博客周更,以对得起赞助商的支持。

    (更多…)
  • 【视频】从浏览器渲染机制理解 Web 性能

    【视频】从浏览器渲染机制理解 Web 性能

    群里有同学问:

    如何优化网页性能?比如做了个网页,觉得打开速度比较慢,该怎么优化?

    这是个好问题,也是个很难回答的问题,因为涉及到的内容很多,一句两句说不清楚。所以我特地做了这个视频,详细讲解浏览器渲染的每一步,有哪些影响因素,以及我们可以针对性采取什么样的优化策略。

    开始觉得可能要做 10+p 幻灯片,讲个 10+ 分钟;结果幻灯片做了 29p,讲了 40+ 分钟。自荐给大家,希望大家认真观看,能够在日后面对网页初始加载优化时,能够游刃有余。

    从中我们也可以找到那道经典面试题:“当我们输入 URL,然后按下回车之后,都会发生什么”的答案。可谓一举两得。

    视频用到的 slidev 放在这里:

    当然,时间有限,能力有限,这个视频只关注 web 前端及部分 web 全栈的内容,以及我们力有所及能够优化的部分,可能有些内容没有覆盖到。所以,如果你有疑问、或者觉得有问题,请留言。我很乐意跟大家讨论这些问题,也很希望能给视频打补丁。

    如果你觉得我做得不错,也请给我一键三连,转发给更多需要的人。提前感谢支持。

  • 聊聊减肥

    聊聊减肥

    最近几年比较懒,博客上基本都是技术分享,很少聊生活相关。因为技术分享有人看,但中年胖子的日常说实话我自己都不想看。不过最近有一些变化,我的体重终于在自己的努力下,降至 105kg 附近,所以今天想聊聊减肥。

    2020 年的时候,糖尿病复发,开始打利拉鲁肽,于是食量大减,办了小区健身房的卡,每天半小时有氧+4×3随机力量(哪台机器没人就练哪台),体重慢慢下降,到年底稳定在 106~108 之间。

    2021 年,疫情反复,小区健身房终于扛不住,破产清算,于是我好不容易形成的健身习惯就泡汤了。虽然也在家里健身环+Just Dance,但总归运动量有限。我的体重也满满增加,到年底已经涨回 111。

    今天情况也不好。在利拉鲁肽的影响之下,我的胃口已经很小了,跟我老婆接近,比 9 岁的儿子还小。每天上班,遛狗两次,按说消耗也不算低,但是体重仍然稳步上涨,直到 113.9。

    后来去医院查身体构成,发现肌肉量降得很厉害,四肢骨骼肌已经降到正常偏下,难怪不吃不喝还会变胖。此时正好小区旁边新开了一家健身房,于是我决定:

    1. 进行力量训练,增肌
    2. 看看减重门诊,看看医生有没有什么好建议

    新健身房专营私教,课巨贵,次卡 300/节,办年卡 1w4。先把美团羊毛薅完,接着犹豫再三之后,我选择办了张 30 次卡开始训练。必须得说,贵是贵了点,但是物有所值。

    1. 教练很专业,排课循序渐进,而且刚好卡着我的极限,虽然每次都练得死去活来,但是身体状态提升很快。
    2. 因为全私教,所以不需要抢器械,也不需要排队,效率很高。
    3. 教练帮忙纠正了我的一些不规范动作,避免受伤和运动损害。
    4. 虽然我自恃当过运动员,“哥们儿练过”,但其实 20 年过去,我的身体条件发生了巨大变化,当年的经验很多时候只是负担,必须重新学习、重新摸索,有教练在,大大缩短了这一过程。

    总之,在教练帮助下,我的身体状态恢复很快,各方面力量纬度都在快速提升,体脂肉眼可见的下降。

    另一方面,我去医院减重门诊看了几次。体验不太好,减重门诊的大夫是个干瘦老头,非常目标导向,他建议我练完健身连一个茶叶蛋都不要吃,原话是:“你一天少吃一个鸡蛋,就少长半两。不要小看半两,一年就是 18 斤,两年你体重就降到 100kg 以下了……“

    熟悉我的人都知道,我的低血糖反应很严重,各种不舒服。而且吃太少也很影响训练效果,所以我擅自调整了药量,配合训练。不过我也得到好几个有价值的信息:

    1. 利拉鲁肽最多可以打 3ml,我每天 1.5ml 其实量偏小
    2. 二甲双胍我应该每次吃 0.5mg,我现在吃少了

    所以我现在的安排是:

    1. 每周二、四晚上训练,偶尔周六加练
    2. 周一开始,到周四,算训练时间,利拉鲁肽打 1.2ml;周五到周日,打 1.8ml
    3. 训练的晚上,二甲双胍吃 0.25mg
    4. 每周休息 1~2 天,没有力量练习的时间,用 jo姐 的踏步操进行有氧训练

    我从 8 月回来广州,慢慢摸索慢慢坚持,体重逐步下降,现在基本能稳定在 105kg。乐观估计到年底应该可以下探到 104kg。

    在利拉鲁肽的控制下,我日常食欲很低,虽然还是爱吃,但是吃的很少,很容易饱。健身期间也不再优先吃蔬菜,会尽量吃优质蛋白质。

    无糖饮料对血糖的影响很小,所以可以放心喝,不过也不建议多喝,能喝白开水最好,其次是淡茶水。但是无糖不等于无热量,比如奶茶、拿铁咖啡,因为含有很多奶,所以热量也不小,尽量少喝。

    同时,甜味感和热量也没有必然联系。水果中的西瓜,因为含水量巨高,所以大体上可以放心吃,血糖影响和实际热量都不大——因为你通常吃不了那么多。但是香蕉的热量就很高,枣也很高,常被当作减肥食物的牛油果也很高,能不吃尽量不要吃。

    我们的骨骼肌无法产生热量,而且在日常活动中会消耗很多热量,所以通过力量练习提升骨骼肌,增加基础代谢是非常好的减肥手段。相比于难以测算的日常有氧,力量练习对减重帮助更大。

    其实我最初的目标没这么高,原想着能稳到 110 以下就可以。所以现在非常满意,准备续卡。也分享出来,供其他有减肥需要的同学参考。

  • 函数,栈,try…catch,以及异步

    函数,栈,try…catch,以及异步

    前两天《记一个 `try…catch` 异步函数的坑》发出后,有同学表示对最后一句不解:

    异步函数的调用可能并不在当前栈,也无法被 try…catch 在当前栈捕获到错误。

    要解释清楚,一两行是不够的,于是写这篇文章展开解释一下。(这篇文章是基础向。)

    函数与栈

    首先来看这样一段代码:

    function a() {
      // ...
    }
    
    function b() {
      // 代码1 
      a();
      // 代码2
    }
    
    function c() {
      // 代码3
      b()
      // 代码4
    }
    
    c();

    它会怎么执行呢?简单来说是这样:

    1. 构建一个栈
    2. c 推入栈,开始执行 代码3
    3. b 推入栈,开始执行 代码1
    4. 将 a 推入栈,开始执行
    5. a 执行完,出栈
    6. 开始执行 代码 2
    7. b 执行完,出栈
    8. 开始执行 代码 4
    9. c 执行完,出栈

    栈是一个先入后出的数据结构,你可以把它当成一个桶,先放进去的东西会被后放进去的东西压在下面,需要先把上层的东西,也就是后放进去的东西拿出来才能拿到先放进去的东西。

    发生错误

    如果代码执行中发生错误,就会在错误处中断,并逐个出栈。此时我们不仅能看到错误本身,还能看到错误发生处的完整栈信息,对 debug 很有帮助。

    try...catch

    异步函数出现之前,try...catch 只能捕获当前栈的错误。比如,同样是上面那段代码,稍微改动一下:

    // 之前定义 a b c 的代码不变
    
    function d() {
      throw new Error('oh my god');
    }
    
    try {
      c();
    } catch (e) {
      console.error(e);
    }
    
    d();

    这段代码里,c() 执行时如果有错误,可以被 try...catch 捕获到;但是 d() 位于另一个栈(或同一个栈的另一个堆叠),就无法捕获。

    异步 callback & Promise

    Promise 成为规范被纳入标准之前,我们处理异步操作时需要使用回调(callback)。比如侦听事件:

    try {
      $('.btn').on('click', function (e) {
        console.log('hello');
      });
    } catch (e) {
      console.error(e);
    }

    请注意:这里添加侦听函数的代码,也就是 $().on() 的部分,是在当前栈执行的;而用户点击后执行的操作,即 console.log('hello') 的部分,是在将来某个事件调用栈里执行的。所以,如果后面的函数里有问题,那么如上面所示的代码是无法捕获到错误的。

    Promise 也一样。比如下面这段代码:

    new Promise((resolve) => {
      // 执行完后回调
      func0(resolve);
    })
      .then(() => {
        return func1();
      })
      .then(() => {
        return func2();
      });

    这里的 func0func1func2 三个函数都是在不同调用栈里执行的,所以如果你在最外面 try...catch,无法捕获到错误。

    这样会给我们编写代码带来一些困难。比如,有时候我们需要同时发起好几个网络请求,有些会成功有些会失败。我们并不知道每个失败请求对应的构造函数是怎么执行的,只能依靠请求内容进行判断,就比较麻烦。

    这个问题,在异步函数中得到了解决。

    异步函数 async function

    这个变化主要是 await 带来的。

    前篇文章所说,在不使用 await 的情况下,try...catch 只会捕获当前调用栈的错误。对于异步函数来说,它在当前栈会返回 Promise 实例,然后就顺利结束,不会抛出任何错误。所以 try...catch 也无法捕获任何错误。

    但是添加 await 之后,情况就不同了。这个时候,try...catch 会捕获整个 Promise 里所有的错误,即使函数位于不同调用栈,也可以正确捕获并且显示在一起,方便调试。