记一次 Taro 2.x 老项目生产环境白屏的踩坑实录

341 阅读8分钟

大家好,今天不聊高大上的架构,继续跟大家分享一个最近遇到的,堪称“考古级”的踩坑故事。一句话总结就是:一个五年前的 Taro 小程序项目,如何在生产环境给了我一个大大的“惊喜” 😵。

故事的开端:来自远古的召唤

事情是这样的,我们手头一个已经稳定运行了五年并且没有任何迭代更新的小程序项目,突然因为业务发展,客户需要进行一波优化和功能迭代。这个项目技术栈非常“经典”:Taro 2.2.1

我熟练地从代码仓库拉下项目,yarn install 一气呵成,然后 yarn dev:weapp 启动!微信开发者工具里,项目跑得非常欢快,开发丝滑,功能一切正常。当时的我,嘴角微微上扬,心想:“廉颇老矣,尚能饭否?看来问题不大嘛!” 😎

然而,前端开发的“墨菲定律”永远不会缺席。当我信心满满地执行 yarn build:weapp,将构建后的代码上传到微信开发者平台,并发布为体验版后,诡异的事情发生了——打开体验版小程序,映入眼帘的是一片纯洁的白色,白得那么彻底,仿佛在嘲笑我的天真

白屏!对于前端开发者来说,这绝对是最糟糕的场景之一。本地开发一切正常,生产构建后白屏,这通常意味着一场恶战的开始。

没办法,开干吧!我调出了 vConsole,定睛一看,两条鲜红的错误信息赫然在目:

b884cb50d4ed2b57e52a62a9cdcd9e5b.jpg

  1. Error during evaluating file "app.js": TypeError: Function(...) is not a function
  2. Error during evaluating file "components/custom/address/index.js": TypeError: f.default.mark is not a function

看到这两个错误,我的第一反应是: Babel 编译语法在“作怪”!

排查之路:在迷雾中摸索

虽然心里有了初步判断,但作为一名严谨的工程师,我还是按照标准流程走了一遍排查。

🤔 **第一回合:依赖重装 **

我重新构建、上传,结果,熟悉的白屏,熟悉的配方...

虽然在项目初始我就发现,项目的 .npmrc 文件里配置的还是旧的淘宝源 registry.npm.taobao.org。出于良好的工程习惯,我顺手将其更新为了新的镜像地址 registry.npmmirror.com

修改完配置后,我执行了 yarn install。虽然我没有删除 yarn.lock 文件,但 yarn 在安装时检测到包的来源(resolved 字段)发生了变化,因此对 yarn.lock 文件进行了大量更新。

实际上正是这个操作,成为了问题的导火索! lock 文件的大量变动,意味着许多依赖(尤其是子依赖和孙子依赖)的版本已经发生了漂移。

🤔 第二回合:怀疑环境

“难道是微信开发者工具或者基础库版本的问题?” 我仔细核对了项目的 project.config.json 文件,检查了开发者工具的设置,特别是【详情】->【本地设置】中的调试基础库版本,确保其与线上环境保持一致。我还尝试切换了几个不同的基础库版本进行测试。

同时还将 【本地设置】- 中的 【将JS编译成ES5】【启用多核心编译】【使用SWC编译脚本文件】等配置进行切换测试

然而,小程序依旧白屏,错误信息坚挺地显示在 vConsole 中。看来,问题也不在这里。

🤔 第三回合:回归错误本身

排除了外部因素,是时候静下心来,仔细分析错误信息本身了。

  • TypeError: Function(...) is not a function: 这个错误比较模糊,但通常指向一些底层 polyfill 的缺失或冲突。
  • TypeError: f.default.mark is not a function: 这个错误就非常具有指向性了!任何和 Babel 打过交道的同学,看到 mark 这个词,DNA 都会动一下。它几乎是 regenerator-runtime 的代名词,而 regenerator-runtime 正是 Babel 用来转换 async/await 语法的核心工具。

其实结论已经很清晰了:问题出在 Babel 编译上! 由于依赖版本发生了变化,新的 Babel 相关插件与项目旧的 Babel 配置产生了冲突,导致构建后的代码中,async/await 语法没有被正确地转换为小程序环境可以识别和执行的代码。

柳暗花明:Babel 配置大作战

既然锁定了问题范围,接下来就是修改 Babel 配置了。

第一步:分析并修改 config/index.js

我打开了 Taro 项目的配置文件 config/index.js,找到了 babel 相关的配置。Taro 2.x 的配置还是比较直观的。

// config/index.js
const config = {
  // ...
  weapp: {
    // ...
    babel: {
      sourceMap: true,
      presets: [
        ['env', {
          modules: false
        }]
      ],
      plugins: [
        'transform-decorators-legacy',
        'transform-class-properties',
        'transform-object-rest-spread',
        ['transform-runtime', { // 问题可能出在这里
          "helpers": false,
          "polyfill": false,
          "regenerator": true, // 这个配置很可疑
          "moduleName": "babel-runtime"
        }]
      ]
    }
  }
}

根据错误分析,我需要调整 Babel 对 async/await 的处理方式。我决定引入一个更现代、更专门的插件来处理它,并避免 transform-runtime 的干扰。

首先,安装这个专门的插件:

yarn add --dev babel-plugin-transform-async-to-generator

然后,修改 config/index.jsbabel 配置:

// config/index.js (修改后)
const config = {
  // ...
  weapp: {
    // ...
    babel: {
      sourceMap: true,
      presets: [
        ['env', {
          modules: false
        }]
      ],
      plugins: [
        'transform-decorators-legacy',
        'transform-class-properties',
        'transform-object-rest-spread',
        // +++ 新增插件,专门处理 async/await
        'transform-async-to-generator', 
        ['transform-runtime', {
          "helpers": false,
          "polyfill": false,
          // --- 将 regenerator 交给上面的插件处理,避免冲突
          "regenerator": false, 
          "moduleName": "babel-runtime"
        }]
      ]
    }
  }
}

第二步:引入 regenerator-runtime Polyfill

Babel 只是个“翻译官”,它将 async/await 翻译成了 Generator 函数。但小程序这个“运行环境”本身可能不认识这些 Generator 函数,需要一个“说明书”或者叫“运行时环境 (runtime)”。这个运行时就是 regenerator-runtime

我们需要手动安装并引入它。

首先,安装依赖:

yarn add regenerator-runtime

然后,在小程序的入口文件 src/app.tsx 的最顶部,引入这个运行时。这至关重要,必须确保它在所有业务代码之前被加载。

// src/app.tsx

// +++ 在文件顶部引入 regenerator-runtime
import 'regenerator-runtime/runtime';

import Taro, { Component, Config } from '@tarojs/taro';
import Index from './pages/index';

import './app.scss';

// ... class App extends Component ...

第三步:重新构建,见证奇迹

完成以上两步操作后,我深吸一口气,执行了 yarn build:weapp。 构建完成后,再次上传到体验版。 这一次,打开小程序......熟悉的首页终于出现了!白屏问题解决!🎉🚀

刨根问底:为什么以前好好的,现在不行了?

问题解决了,但真正的思考才刚刚开始。为什么一个稳定运行了五年的项目,在不做任何业务代码修改的情况下,仅仅是重新构建就出问题了?

这背后其实是前端技术生态演进和环境变化的综合结果。

1. 环境的“漂移”

  • NPM 源(Registry)的更换:这是本次问题的直接导火索。更换源地址后,即使包名和版本号不变,yarn.lock 文件中记录包来源的 resolved 字段也会更新。这会导致 Yarn 重新解析依赖树,并可能拉取到与旧源上略有差异的子依赖版本,从而打破了原有的依赖平衡。
  • Node.js 版本升级:我本地的 Node.js 环境早已不是五年前的 v12/v14,而是最新的 LTS 版本。新版的 Node.js 和 npm/yarn 在处理依赖解析和构建过程时,可能与旧版本存在细微差异。
  • 小程序运行环境升级:微信小程序的底层 JS 引擎也在不断更新迭代。新的引擎对代码规范和 Polyfill 的要求可能更加严格。

2. Babel 生态系统的演进

babel-runtime 及其相关插件在过去几年也发生了巨大变化。老版本的 Babel 插件可能会“隐式”地包含 regenerator-runtime 的实现,而新版本的生态系统则更倾向于让开发者“显式”地引入和管理这些 Polyfill,以实现更好的模块化和按需加载。

3. 问题的技术核心

我们可以通过一个流程图来直观地理解整个过程:

graph TD
    subgraph "修复后的流程"
        A3["源代码 (包含 async/await)"] --> B3{Taro 2.x 编译};
        B3 -- "修改 config/index.js" --> C3["使用 'transform-async-to-generator'"];
        C3 --> D3["编译后代码 (正确的 Generator 转换)"];
        G["在 app.tsx 中 import 'regenerator-runtime'"] --> E3;
        D3 --> E3["小程序新版 JS 引擎"];
        E3 --> F3["✅ 运行正常"];
    end
    
    subgraph "新环境 (现在)"
        A2["源代码 (包含 async/await)"] -- "1. 更换 npm 源" --> Y{yarn.lock 文件更新};
        Y -- "2. 导致依赖版本漂移" --> C2["新版 Babel 插件 (在新的 Node 环境下)"];
        A2 --> B2{Taro 2.x 编译};
        B2 --> C2;
        C2 --> D2["编译后代码 (未正确处理 regenerator)"];
        D2 --> E2["小程序新版 JS 引擎"];
        E2 --> F2["❌ 报错白屏: 'f.default.mark is not a function'"];
    end
    
    subgraph "旧环境 (几年前)"
        A["源代码 (包含 async/await)"] --> B{Taro 2.x 编译};
        B --> C["旧版 Babel 插件 (依赖被 lock 文件锁定)"];
        C --> D["编译后代码 (内置了 regenerator 实现)"];
        D --> E["小程序旧版 JS 引擎"];
        E --> F["✅ 运行正常"];
    end

简单来说,就是我们开发环境的“工具链”(Node.js, npm 源, Babel 插件版本)升级了,但项目的“蓝图”(Babel 配置文件)却没有更新,导致生产出来的“零件”(编译后的代码)与最新的“发动机”(小程序运行环境)不匹配。

我们的修复方案,本质上是更新了“蓝图”,并手动提供了那个缺失的核心“零件” (regenerator-runtime)

总结与反思

这次“考古”经历虽然折腾,但也带来了宝贵的经验:

  1. 敬畏老项目:对于长期未维护的老项目,任何环境变动都可能引发未知问题,哪怕只是更换一个 npm 源。
  2. 锁定环境:如果条件允许,使用 Docker 等技术将开发、构建环境(包括 Node 版本和 npm 源)完全固化下来,是避免此类问题的最佳实践。
  3. 定期维护:技术债迟早要还。与其等项目出问题被动修复,不如定期投入资源进行技术栈的升级和维护。
  4. 扎实基础:无论框架如何变迁,对 JavaScript 运行机制、Babel 编译原理、包管理工具工作方式等基础知识的理解,才是解决复杂问题的金钥匙。

希望这次的踩坑分享能对大家有所帮助,尤其是在维护老旧项目时,如果遇到了类似的白屏问题,不妨从 Babel 配置和 Polyfill 的角度去尝试一下。

好了,今天的分享就到这里。我要去给那个五年老项目继续添砖加瓦了!👨‍💻