ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

小程序文件被静默过滤?无依赖文件过滤机制与排查指南

小程序文件被静默过滤?无依赖文件过滤机制与排查指南 开发小程序最糟心的事情可能不是需求变更而是本地跑得好好的一发版就崩。我上个月就遇到一次某业务页面在微信开发者工具里怎么点都没事真机预览也正常结果正式版发完用户一进页面就白屏vConsole 里躺着一句module services/activity.js is not defined。我打开项目目录services/activity.js明明就在那儿路径一个字都没写错。这种问题十有八九和微信开发者工具从2022年起引入的无依赖文件过滤机制有关——这个机制本意是帮你把没用的文件从代码包里剔掉但一旦误判它比任何 bug 都隐蔽。这篇文章我不打算只讲怎么改一行代码而是把这套机制的运行逻辑、典型错误场景、完整排查链路都拆开来讲。不管你是刚装好微信开发者工具的小白还是已经接手过中大型项目的老人只要你的项目里出现过文件存在却找不到模块的诡异报错这篇内容都能给你一套可复现的排查思路。1. 一次发布后的幽灵报错文件明明在线上却说找不到1.1 本地正常、上传就崩的完整症状先把当时的现象还原一下。那个项目用的是原生小程序框架目录结构大概是miniprogram/ app.js app.json pages/ index/ activity/ services/ activity.js report.js utils/ format.js报错发生在线上正式版页面路径是pages/activity/index用户打开后直接白屏。vConsole 里的关键信息有两行Error: module services/activity.js is not defined at require(services/activity.js) at pages/activity/index.js:24:5我当时的第一个反应是检查路径。页面里的写法是这样的const activityService require(../../services/activity.js);路径从pages/activity/往上跳两级到miniprogram/services/activity.js完全没问题。打开代码仓库文件也在。用开发者工具重新编译模拟器里一切正常真机预览也正常。只有线上正式版报错。这里有个很关键的细节报错信息里显示的是require(services/activity.js)而不是源码里的require(../../services/activity.js)。这说明代码在被上传前经过了一次打包处理相对路径被转化成以miniprogram/为根目录的模块路径。换句话说报错发生在编译产物层面而不是源码层面。1.2 为什么本地预览和线上表现不一致很多人不理解为什么同一个项目本地模拟器、真机预览、正式版三个形态会表现得不一样。原因在于它们走的代码生成链路不同。本地模拟器调试时开发者工具直接基于当前项目目录运行文件系统里有什么就用什么require(../../services/activity.js)能命中自然正常。真机预览时工具会把项目编译成代码包并上传到微信后台这一步会触发代码包构建流程依赖过滤机制开始介入。预览包和正式包的构建流程基本一致如果预览包构建时被过滤真机预览也会报错。正式版上传时同样的构建流程再来一遍只是代码包被标记为正式版。所以本地正常、预览正常、线上崩并不是一个稳定的判断条件。更准确的描述是本地可能正常构建后可能失败失败与否取决于依赖分析结果。我之前遇到的情况是预览包侥幸没触发问题正式包因为改动后依赖关系变了才暴露出来。真正的分水岭是从 2022 年起微信开发者工具的默认行为变成了上传时过滤无依赖文件。以前这个功能可能是可选项默认关闭之后版本开始默认开启或者过滤规则变得更严格。一旦某个文件被判为无依赖它就不会进入代码包线上自然找不到。2. 无依赖文件过滤的运行逻辑工具靠什么判断文件有依赖2.1 依赖分析的入口和静态分析规则要理解过滤机制为什么会误伤首先得知道它怎么判断有依赖。微信开发者工具的构建系统会从几个固定入口出发遍历整个项目的引用关系构建出一张依赖图。入口大致包括app.js及其require、import的所有模块app.json中pages字段声明的所有页面subPackages/subpackages字段声明的分包及其页面页面.json中usingComponents声明的自定义组件workers字段指定的 Worker 脚本页面.wxml中引用的模板、wxs模块页面.wxss中通过import引用的样式文件遍历的方式是静态分析也就是基于代码文本解析而不是真的把代码运行一遍。工具看到require(../../services/activity.js)这样的写法能从字符串字面量直接解析出目标文件于是把它标记为有依赖。看到require(someVariable)时因为someVariable的值只有在运行期才知道静态分析无能为力这个路径上的所有文件都无法被关联。遍历结束后没有被任何入口触及、也没有被任何有依赖文件引用的文件就会被打上无依赖标记。在上传代码包时这些文件被过滤掉不进包。2.2 过滤发生的时间点和设置入口过滤发生在代码包构建阶段只要你使用开发者工具的上传、预览、真机调试等需要生成代码包的功能都会执行。单纯的本地编译调试不一定会走完整过滤这也是本地正常、上传崩的重要原因。如果你想去验证是不是这个机制导致的可以在开发者工具里找设置入口。以我手头的稳定版为例路径是右上角详情按钮 → 本地设置往下翻能看到上传时过滤无依赖文件之类的勾选项。对应的配置文件是项目根目录下的project.config.json里面有类似这样的字段{ setting: { ignoreUploadUnusedFiles: true } }字段名在不同版本里略有差异但含义一致为true时表示忽略上传无依赖文件也就是开启过滤。注意忽略的对象是无依赖文件不是忽略这个功能。很多同学第一次看到这行配置容易理解反。2.3 这个机制要解决什么问题平心而论这个机制不是闲得没事找事。小程序代码包有严格的体积限制一个项目里经常混着各种不该上线的文件node_modules里没被引用的依赖、设计稿、临时调试脚本、旧版本残留文件、__MACOSX这种系统生成物。如果没有过滤机制这些垃圾文件全都会被打进代码包白白占用空间上传也慢。正常的过滤应该是精准的把没被引用的垃圾文件剔掉让代码包体积更小。但问题是工具对依赖的判断完全基于静态分析而真实项目里的引用方式远比静态分析能覆盖的复杂。于是误判就产生了有用的文件被当成垃圾无声无息地丢了。3. 最容易踩的几类典型场景3.1 动态 require 路径静态分析的第一个盲区最典型、也最容易被复现的场景就是动态拼接路径的require。比如下面这种写法const serviceName getCurrentServiceName(); const service require(./services/ serviceName .js);运行的时候没有任何问题serviceName是activity就能加载到services/activity.js。但静态分析不会去跟踪serviceName的值它只看到require(./services/ serviceName .js)无法确定目标文件于是这个文件不会被标记为依赖。更隐蔽的版本是在循环里动态加载const pageList [home, list, detail]; pageList.forEach(name { require(./pages/ name /index.js); });这种代码在很多老项目里都有当初是图省事想用一份配置驱动多个页面。结果就是构建时pages/home/index.js、pages/list/index.js、pages/detail/index.js全部没有被静态分析命中如果它们又恰好没有被app.json的pages字段显式声明那就真的被当成无依赖文件过滤掉了。我见过最离谱的一个项目登录后的页面跳转是拿后端返回的字符串拼路径动态require。后端配置一改某个页面在所有已经发布的新版本里就永远打不开了排查了整整两天才找到原因。3.2 配置驱动和运行时注册的文件除了动态require还有一类场景是文件本身在配置里提到过但代码引用是运行期才发生的。举个例子有些团队喜欢做组件注册表// component-registry.js module.exports { user-card: ./components/user-card/user-card, order-card: ./components/order-card/order-card, coupon-card: ./components/coupon-card/coupon-card };然后在某个页面里用遍历的方式动态加载组件。工具分析component-registry.js时只看到它export了一个普通对象对象里的字符串看起来像路径但其实没有被require调用静态分析不会把这些路径解析成依赖。于是components/coupon-card下的所有文件都可能被过滤掉。再比如有些项目里用wx.loadSubpackage做分包预加载分包入口写在app.json里没问题但如果某个分包页面里的组件是通过运行期wx.nextTick动态塞进usingComponents的也可能出现类似问题。动态注册这种玩法在原生小程序里本来就偏 Hack但在 2022 年的过滤机制下风险被放大得很明显。3.3 分包、主包边界与构建产物的隐性依赖还有一种更微妙的情况牵扯到主包和分包的关系。假设services/report.js放在主包目录下它没有被主包的任何页面直接require但是某个分包页面引用了它。理论上工具在遍历分包页面时会把这个文件标记为有依赖但如果分包页面的引用本身是动态的、或者在分包内部又被二次转发依赖关系就可能断掉。举一个真实的例子某个项目的分包页面packageA/pages/result/index.js里这么写const report require(../../services/report.js);../../services/report.js从分包跳出了分包根目录指向主包目录。这种跨分包边界的相对路径引用工具做依赖分析时容易处理不准。最后的结果是services/report.js在主包侧没有被正常引用在分包侧又因为路径越界没有被算进依赖被判为无依赖过滤掉了。另外如果你的项目有自定义构建步骤比如先用 TypeScript 编译、再把产物拷贝到miniprogram/目录那么工具的静态分析面对的是编译后的产物而不是源码里的引用关系。一旦构建脚本产生了dist目录下多出来的文件却没有被任何编译入口引用它们大概率会被过滤。这类问题在原生小程序 自定义构建链路的项目里尤其常见。3.4 WXML/WXS 模板间接引用WXML里的模板引用和WXS模块引用是又一个容易忽略的维度。小程序支持在 WXML 里写wxs src../../utils/helper.wxs modulehelper /然后在页面里用helper.format(...)做数据格式化。如果helper.wxs没有被任何 JS 文件require它的依赖关系完全靠 WXML 解析来识别。大多数情况下工具能识别但如果wxs的路径是动态拼出来的虽然 WXML 里一般不支持动态路径但有些人会用数据绑定配合{{ }}来写或者你用了自定义构建插件改了路径就可能导致wxs文件被漏掉。样式文件同理。import在wxss里是支持相对路径的如果某个wxss文件只被另一个wxss引用而那个wxss又只被一个动态加载的页面引用依赖链一旦断掉样式文件直接消失。表现是线上页面样式错乱不仔细看还以为是 CSS 写错了。4. 一次完整的排查链路从报错到确认文件被过滤4.1 先确认报错类型和模块路径遇到文件存在却找不到模块的报错先别急着改代码。把 vConsole 里或真机调试的完整报错信息截下来重点关注两件事报错里写的模块路径是什么、以及这个路径对应源码里的哪个文件。如果报错信息里是module services/activity.js is not defined这种以项目根目录开头的路径基本可以确定文件已经被打进代码包后再查找失败了。接下来就要怀疑构建阶段把它剔了出去。如果报错信息里是module ../../services/activity.js is not defined那更可能是路径解析或者文件确实没提交到代码仓库的问题。两者的排查方向完全不同。4.2 巧用开关做第一轮定位最直接的验证方式是找到无依赖文件过滤的开关临时把它关掉。在开发者工具详情→本地设置里找到上传时过滤无依赖文件选项取消勾选。然后在project.config.json里确认对应的setting字段变成了关闭状态{ setting: { ignoreUploadUnusedFiles: false } }保存设置后重新预览或上传看看报错是否消失。如果消失说明问题就出在过滤机制如果还报错那得继续排查真实文件缺失或者路径错误。这一步是整个排查链路里性价比最高的一步几分钟就能得出结论。注意关闭过滤后代码包会变大所以这个操作只能用来验证生产环境不建议长期关闭。4.3 用全局搜索还原依赖关系一旦锁定是过滤机制的问题就要搞清楚为什么工具认为这个文件无依赖。手工还原依赖关系最直接的方法是全局搜索。在项目目录里搜索报错文件的文件名比如搜索services/activity看看到底有没有任何地方通过静态字符串引用了它。搜索结果如果是空的或者只有你自己写的动态拼接代码那结论就清楚了工具没扫到依赖。接下来沿着谁应该引用它的思路向上反推。比如services/activity.js是被pages/activity/index.js引用的那么pages/activity/index.js有没有被app.json的pages字段声明有没有被其他页面通过navigateTo引用如果入口本身没问题再看引用的写法是不是动态路径。这一步做完基本能从代码逻辑层面判断出过滤到底是对还是错。如果引用方式正常那是工具的判断逻辑有缺陷如果是动态引用导致的那就是自己的代码埋了雷。4.4 在入口文件加一行临时引用做最终验证我习惯用一个特别直接的验证方法在app.js顶部加一行临时代码显式require目标文件。// 临时验证代码确保该文件有依赖 require(./services/activity.js);保存后重新预览如果报错消失就证明这个文件在构建时确实是因为没有被任何静态依赖引用而被过滤的。因为你在入口处显式require了它依赖分析图里它就变成了有依赖文件会被保留。这个方法尤其适合验证那些我明明引用了啊的情况。如果你加了这行临时代码之后问题依旧那说明过滤不是唯一原因后面可能还藏着模块缓存、路径大小写、文件编码之类的问题。验证完后记得把临时代码删掉。我之前见过有同事验证完忘了删直接在app.js里留了一堆临时require被代码评审的时候点名批评。5. 修复方案与防再犯的操作清单5.1 把动态引用改成静态引用映射最干净的修复方案是消灭动态require把所有可能用到的模块变成静态可解析的对象映射。拿前面那个服务名拼接的例子来说改造方式如下// 正确写法显式声明所有可能用到的模块 const services { activity: require(./services/activity.js), report: require(./services/report.js), user: require(./services/user.js) }; // 使用时直接取 const service services[serviceName];这样每个模块都在代码里被显式require了静态分析一定能扫到依赖关系。代价是要把业务里可能用到的模块列全新增模块时需要记得同步维护这张表。但对于小程序这种不太可能无限动态扩展场景的项目来说这个代价完全可以接受。5.2 显式声明入口app.json、分包配置和 usingComponents有些文件不是靠require引用的而是靠配置声明引用的。这种文件你要确保它在配置里被显式登记过。页面文件必须在app.json或subPackages里声明自定义组件必须在页面的usingComponents里声明Worker 文件必须在app.json的workers字段里声明跨分包引用的模块尽量在app.json里把它归属于某个分包不要用../../跨出去引用我见过一个团队为了避免跨分包路径问题把所有需要共享的代码抽到miniprogram_npm或者主包common/目录下然后白名单化处理。虽然啰嗦一点但至少不会线上白屏。5.3 关闭过滤的兜底做法不建议长期使用如果项目实在改不动比如老项目里有大量历史遗留的动态引用临时止损的办法就是关闭无依赖过滤。在project.config.json里设{ setting: { ignoreUploadUnusedFiles: false } }或者直接在设置界面取消勾选。这样做的代价是代码包体积可能会增大很多一些本该被过滤的垃圾文件也会被打包。我的态度是用来应急可以长期不可取。因为过滤机制只会越来越严格逃避不是办法。一个更折中的思路是关掉过滤后把代码包体积和文件列表跑出来再反过来清理真正无用的文件。相当于先让工具不要删然后自己动手删掌握主动权。5.4 在构建流水线里加文件完整性检查小程序项目的工程化程度在逐年提升很多团队已经把构建搬到了 CI 上用miniprogram-ci或者其他工具做上传。但大多数 CI 流程只关心能不能编译成功不关心哪些文件被过滤掉了。这里建议加一道检查在构建结束后校验项目里的关键文件是否出现在产物文件清单里。最简单的做法是写一个 Node.js 脚本扫描项目中的所有.js、.wxml、.wxss文件用正则或 AST 解析出所有静态引用的相对路径逐一确认目标文件存在并输出未被任何入口引用的疑似文件清单。脚本里的核心逻辑大概是这样const fs require(fs); const path require(path); // 读取入口文件列表app.js 所有页面 js 组件 js // 对每个文件做静态依赖收集 // 最后和项目里存在的文件做差集不用做得很复杂重点是让文件即将被过滤这件事在 CI 阶段就暴露出来而不是等线上白屏了才去查。5.5 团队规范与代码评审的配合依赖过滤误伤本质上是一个可防不可治的问题。一旦代码到了线上再想恢复被过滤的文件只能重新发版影响面不可控。所以防再犯的手段里最重要的其实是规范和评审。在团队的代码规范里明确加两条禁止使用变量拼接路径调用require或import新增业务模块时必须在对应的页面配置或入口文件中显式声明代码评审时顺着这两条规则去查基本能拦下 90% 的雷。另外如果你的项目里用了 ESLint可以开启import/no-dynamic-require规则让工具本身就去拦截动态require的写法。我在实际排查这类问题时的体会是头三次遇到文件被过滤都觉得是路径写错了直到自己把project.config.json里的开关拨来拨去、亲眼看到过滤前后的代码包体积变化才真正建立起对这套机制的敏感度。后面再遇到类似情况我的第一反应从文件去哪了变成这个文件被谁依赖排查速度完全不一样。最后再分享一个小技巧每次发版前在开发者工具的上传弹窗里留意一下代码包体积。如果某次改动的功能逻辑很少代码包却明显变小了就要多留个心眼——可能不是优化成功而是某些文件被静默过滤掉了。
返回列表