多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

7年前的开源模拟城市OpenSC2K复活记:老依赖修复与前端工具链实战

7年前的开源模拟城市OpenSC2K复活记:老依赖修复与前端工具链实战 1. 项目全貌7 年前的开源模拟城市现在还能玩吗1.1 “老项目”到底老在哪先说清楚这次要折腾的 OpenSC2K 是个什么东西。简单说它是一个用 JavaScript 和 HTML5 Canvas 重写的经典模拟城市类游戏社区里常叫它“开源版模拟城市 2000”。整个项目以浏览器为运行载体通过生成随机地图、铺道路、拉电网、放住宅区和商业区复刻了当年在 486 电脑上玩城市建设的核心体验。不过这个项目最大的特点不是玩法而是“老”。代码仓库里的提交记录大部分停留在 7 年前依赖锁定在一批 2017 年前后的工具链上。你拿到手会发现 package.json 里写的是 webpack 2.x、node-sass 4.x、babel-loader 7.x这些在今天全部属于博物馆级别的版本。更麻烦的是当年的项目作者习惯用最新的 ES 语法和自定义的构建脚本很多写法在现在的浏览器和 Node 运行时里已经变了味。所以“重见天日”并不是双击一下就能跑而是要完成一次完整的“老代码考古”把旧依赖解析出来绕过 node-sass 的编译地狱让老 webpack 在现代化终端里继续服役最后还要搞定浏览器兼容问题。这个过程看着像是在浪费时间实际上是一次非常好的前端工具链历史课。1.2 为什么值得花时间把它跑起来有人问一个 7 年前的游戏项目有必要折腾吗我的判断是相当有必要尤其是对想深入理解前端工程化和游戏渲染的人来说。第一这个项目体积不大源码组织清晰核心模块包括地图生成、地形处理、道路自动连接、区块规划、Canvas 绘制调度。它不像现代框架项目那样层层抽象你能直接看到“数据变了画面怎么跟着变”的完整链路。第二它踩的坑是典型的“老依赖复活”问题几乎每个做维护的开发者都会遇到类似场景node-sass 编译失败、Webpack 插件不兼容、npm 版本冲突。把这些坑趟一遍以后再遇到老项目就不会慌。就算你对游戏本身没兴趣把它当作一个“必须要跑起来的前端实验项目”也完全值得。下文我会按实际操作顺序从环境准备、依赖修复、编译启动到浏览器调试完整走一遍同时把常见的报错和解决办法整理成速查表确保你拿到源码后能一步步跟下来。2. 准备一套能兼容的老环境2.1 先看清 package.json 再动手拿到源码后第一步绝对不是急着执行 npm install而是先把 package.json 从头到尾读一遍甚至可以用编辑器搜索一下这几个关键词scripts、engines、dependencies、devDependencies。我当时看到的依赖大致长这样下面是一个典型化改造后的示例{ scripts: { build: webpack, start: webpack-dev-server --content-base ./public, dev: webpack --watch }, dependencies: { lodash: ^4.17.4, jszip: ^3.1.3, uuid: ^3.0.1 }, devDependencies: { webpack: ^2.5.1, webpack-dev-server: ^2.4.5, babel-core: ^6.25.0, babel-loader: ^7.1.1, node-sass: ^4.5.3, sass-loader: ^6.0.6, css-loader: ^0.28.4, style-loader: ^0.18.2 } }这里有几个关键信息值得注意。scripts 里的 start 说明项目是用 webpack-dev-server 启动开发服务的不是纯静态文件服务器。devDependencies 中出现了 node-sass 4.x这是后来最容易爆雷的一个点。webpack 2.x 的配置写法与现在差异很大比如 loader 的写法是loader字段而不是rules数组里的useplugins 也需要手动实例化。另外还要留意仓库里有没有 package-lock.json。7 年前的项目很多只有 package.json这意味着你安装依赖时没法锁定历史上精确的版本同一个大版本下的最新小版本可能与老代码不兼容。更稳妥的做法是先不急着改依赖直接跑一次 npm install让报错告诉你哪里需要调整。2.2 用 Node 版本管理器锁住老版本最直接的一个坑是 Node 版本。现代 Node 22 相当于一个完全不同的运行时对老版本 Webpack 和 node-sass 的支持非常差。直接拿新 Node 跑旧项目会遇到类似下边这种报错Module build failed: Error: Node Sass does not yet support your current environment: Linux 64-bit with Unsupported runtime (107)这句话翻译过来就是node-sass 这个库不知道当前 Node 的 ABI所以它拒绝编译原生模块。node-sass 通过编译 C/C 绑定文件来提供功能而每个 Node 大版本都会改变内部的二进制接口node-sass 如果没有针对这个版本发布预编译产物就会在安装时或运行时触发构建流程一旦构建环境缺东缺西又会产生新的错误。解决办法是安装一个版本管理器把 Node 锁到老版本。Windows 上常见的是 nvm-windowsmacOS 和 Linux 环境则使用 nvm。这里以 nvm 为例安装完成后执行nvm install 14.21.3 nvm use 14.21.3 node -v为什么不推荐干脆用 Node 8 或者 Node 10因为太老的 Node 现在连 npm 本身也会报安全警告而且很多新版依赖的 API 不支持。Node 14 是兼具兼容性和可用性的妥协点至少能跑起 Webpack 2 的大部分功能且在安装依赖时不会像 Node 22 那样直接因为 OpenSSL 加密算法变化而崩掉。安装完 Node 14 后还要确认 npm 的版本不要过高。高版本 npm 在处理老 lockfile 时会有额外行为如果仓库里已经有 lockfile 但版本格式过老甚至会提示你重新生成。为了尽量减少变量建议执行npm -v如果版本在 7 以上且后续安装依赖时出现 lockfile 警告可以直接删除旧的 package-lock.json再用npm install --no-package-lock安装。后面我会在速查表里再提一次。2.3 处理依赖下载和 npm 警告切换完 Node 版本后进入项目目录执行npm install。这时候你会看到一堆警告比如 deprecated 警告、babel 插件提示、许可证信息等等。别急着慌这些大部分不影响运行真正致命的是安装过程的报错。如果下载特别慢可以考虑临时切换镜像源。我不建议把镜像源永久写进全局配置因为某些包在镜像源上的同步会有延迟或者缺失。临时安装时用这种方式更安全npm install --registryhttps://registry.npmjs.org如果网络确实不太行也可以在命令行里指定你常用的镜像地址只是不要把这个改动提交到仓库里去免得坑到后来人。安装过程中还有一个高频现象npm 会因为 node-sass 的安装脚本而尝试从外部下载一个二进制文件一旦下载不到就会报出类似 403 或者 ETIMEDOUT。此时先不要急着全盘重来可以先单独重试安装 node-sassnpm rebuild node-sass这一步在少数情况下能解决问题但如果多次尝试都失败我建议直接跳过按照下一节的方案把 node-sass 替换成 sass。3. 从源码到浏览器编译与启动全过程3.1 卸载 node-sass切换到现代 sass先说明替换原则node-sass 和 sass 对于 scss 文件的处理能力在绝大多数项目里是等价的区别在于 node-sass 是 C 实现编译速度快但对 Node 版本敏感sass 是纯 JavaScript 实现安装简单兼容性好。对这个老项目而言我们关注的不是极限编译速度而是“能不能跑起来”所以纯 JS 的 sass 反而是更合适的选择。具体操作分三步走。第一步修改 package.json 里的依赖声明把 node-sass 从 devDependencies 中移除加入sass: ^1.32.0。第二步重新运行 npm install让项目里只剩下 sass 的依赖树。第三步修改 webpack.config.js 里 sass-loader 的配置。老项目中 sass-loader 的配置通常是这样的{ test: /\.scss$/, loader: style-loader!css-loader!sass-loader }这里有一个很容易忽视的问题sass-loader 6.x 默认会寻找 node-sass如果找不到就报错。我们需要明确告诉它使用 sass 作为实现改成下面这种写法{ test: /\.scss$/, use: [ style-loader, css-loader, { loader: sass-loader, options: { implementation: require(sass) } } ] }注意这里我把 loader 从字符串形式改成了 use 数组形式。Webpack 2 对loader和use都能支持但 use 数组更清晰也方便后续调整顺序。改完配置后再执行 npm installnode-sass 带来的头号威胁就解除了。这里有个实操心得替换 node-sass 后编译出来的 CSS 可能和原来有细微差别例如某些颜色函数的舍入方式。对游戏项目来说这点差别肉眼几乎看不出但如果项目回归测试关注视觉快照就需要认真做一次像素级对比。我这个项目里没有遇到这类问题所以整体风险很低。3.2 锁定与 Webpack 2 匹配的 dev server替换完 node-sass 只是第一步。接下来会遇到的典型问题是 webpack-dev-server 启动失败。老项目里 devDependencies 里可能有webpack-dev-server: ^2.4.5但如果 npm install 时没有 lockfilenpm 可能会安装到 2.x 的最新版甚至是 3.x 或 4.x。新版 webpack-dev-server 与 Webpack 2 的插件机制完全不兼容启动时控制台会直接抛错。一个比较典型的报错是TypeError: Cannot read property EntryPlugin of undefined这种报错一般不是代码逻辑问题而是版本错位。解决办法很简单在 package.json 里把 webpack-dev-server 锁定到 2.x 的精确版本比如webpack-dev-server: 2.11.1安装时也建议用精确版本避免 npm 自动解析出更高版本npm install --save-dev webpack-dev-server2.11.1同理如果项目里的 webpack 版本没有锁死也建议把webpack: 2.5.1写成精确版本。老项目的依赖树非常脆弱任何一个小版本漂移都可能引发连锁报错。实操下来把 webpack 和 webpack-dev-server 都改成语义化版本里的具体数字能省很多排查时间。3.3 启动到本地端口和浏览器调试依赖装完、配置改完就到了激动人心的启动环节。执行npm start正常情况下webpack-dev-server 会开始编译控制台输出类似下面这样Project is running at http://localhost:8080/ webpack output is served from /此时打开浏览器访问 localhost:8080应该能看到游戏的初始界面。如果页面是白屏先按 F12 打开控制台看看有没有红色报错。有一个常见问题是运行时代码里用了较新的 ES 语法而项目没有对应的 polyfill。这种情况在后面章节细说。启动时如果再遇到类似Module not found: Cant resolve fs的报错不用太紧张这是有些包被判定了错误的运行环境。可以尝试给 webpack config 添加 node 字段的 polyfill 配置或者直接查源码看是在哪一行引用了 Node 自带模块按需 mock 掉。我当时的处理是用了一个最朴素的方案给 webpack.config.js 加上 node: { fs: empty }。Webpack 2 支持这种写法告诉编译器遇到 fs 模块时就当一个空模块处理免得整条编译链路断掉。这个方案不优雅但对于在浏览器里运行的老项目是投入产出比很高的应急办法。3.4 让老像素画面在新显示器上更清晰游戏跑起来后你会发现画面糊成一片。这不是游戏的 bug而是 7 年前的 Canvas 游戏没有针对现代高 DPI 屏幕做适配再加上 CSS 对像素画的缩放策略不同。问题的根源在于浏览器把 Canvas 画布内容默认当作普通图片做平滑缩放。解决办法有两个。一个是从绘制源头改代码把 Canvas 的实际像素尺寸提高同时保持 CSS 尺寸不变利用 window.devicePixelRatio 让绘制分辨率匹配屏幕物理像素。另一个是更省事的 CSS 方案给 canvas 元素加样式canvas { image-rendering: pixelated; }这个属性会让浏览器在图片放大时使用最近邻插值而不是平滑模糊插值从而保留像素画的锐度。如果 canvas 绘制代码里已经手动做了 scale 操作可能需要同时调整一下 scale 的计算逻辑。实测下来加一个image-rendering: pixelated就能解决 80% 的视觉模糊问题剩下的锐度问题可以通过修改绘制尺寸解决。4. 草根排查手册常见的十二个坑与解决办法4.1 依赖安装阶段的集中问题老项目依赖安装的坑九成集中在 node-sass、Python、构建工具链三者。先说说 node-sass 的经典报错。我在前文提到过 Unsupported runtime这个报错出现后第一反应该是查 Node 版本而不是去装 Python 或者 Visual Studio。如果你的 Node 确实已经切到 14但项目里 node-sass 版本还是 4.5.3理论上会有对应版本的预编译产物但如果你使用的是特殊版本的操作系统或者非 x64 架构还是有概率触发本地编译。本地编译会要求系统装了 Python 2.x 以及对应的 C 编译套件在 Windows 上尤其劝退。所以我一直推荐直接换 sass 而不是去跟 node-sass 硬扛。你可以把 node-sass 卸载干净然后用npm ls node-sass检查依赖树里还有没有间接依赖。如果有间接依赖确认它们是不是必须的可替换的尽量替换实在换不掉再考虑用构建工具提供全局的 node-sass 二进制。第二个高频问题是在 npm 7 下安装老项目会出现 ERESOLVE 错误。npm 7 引入了严格依赖树解析规则而老项目的依赖树非常扁平化很容易冲突。解决办法有两个一是把 lockfile 删掉用 npm install 重新解析二是在 install 命令后加--legacy-peer-deps。我在这个项目里用的是前者因为删除 lockfile 后重新解析更彻底。第三个问题是二进制下载失败。有些包在安装后会通过 postinstall 脚本下载二进制文件比如 node-sass 和像素字体处理库。这类问题的排查思路是先确认网络能不能访问下载地址再确认下载地址是否被镜像源正确覆盖。如果不行可以手动把二进制文件下载到本地通过环境变量指给安装脚本。4.2 编译与启动阶段的集中问题编译启动阶段我遇到的第一个问题是 Babel 配置缺失。老项目用了 ES6、ES7 的一些语法但 webpack.config.js 里如果没有配置 babel-loader 的 include/exclude 规则或者缺少对应 preset编译时就会报SyntaxError: Unexpected token。解决方法是确认 package.json 里有babel-preset-env或babel-preset-es2015并在 webpack 配置里给 babel-loader 指定 presets。第二个问题是 webpack 配置文件本身写得太老。Webpack 2 如果用了ModuleConcatenationPlugin或UglifyJsPlugin在 Node 14 下运行不一定有问题但在新版 Node 下用老版本 UglifyJS会有正则表达式解析失败的风险。如果遇到压缩阶段报错最直接的办法是不压缩直接开发模式运行。也就是说项目只要能在开发服务里跑起来暂时不要碰生产构建。第三个常见情况是 watch 模式不生效。项目 scripts 里如果配置了webpack --watch在部分环境中会出现文件变化但不重新编译的问题。这个我遇到过一次最后发现是项目把源文件放在src目录而 config 里 watchOptions 的 ignored 配置把src给忽略了。去掉这个配置后热更新正常。4.3 浏览器运行阶段的问题与应急手段浏览器里跑老代码最容易出现的是 requestAnimationFrame 相关的问题。当年很多项目或者浏览器兼容层喜欢调用window.webkitRequestAnimationFrame现代浏览器已经移除或只保留标准接口。如果控制台报错这个函数不存在最简单是在游戏入口文件的顶部手动做一个 polyfillwindow.requestAnimationFrame window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { return setTimeout(callback, 1000 / 60); };这行代码不要求你懂多少底层知识就是为了让老代码的调用路径能走通。放在入口文件的第一个执行代码块里越早越好。另一个浏览器端问题是对 pointer 事件的支持。老项目用 mouse 事件监听现代浏览器虽然还兼容但如果你是想在触摸屏上运行会发现点击没反应。这个属于可以接受的范畴不建议做大规模改造毕竟核心目的是让它跑起来而不是兼容所有输入设备。还有一个视觉层面问题Canvas 被 CSS 拉伸后边缘出现黑线。这个在像素风游戏里尤其常见通常是因为 Canvas 的物理尺寸和 CSS 尺寸不成整数倍。解决方式是让 Canvas 的 CSS 尺寸等于物理尺寸除以整数倍或者直接用Math.floor规则化坐标。4.4 问题速查表下面这张表是我这次实操过程中遇到的问题汇总以及对应的处理顺序。排查时建议从上往下看先解决环境问题再处理编译问题最后看运行时报错。现象常见原因推荐解法npm install 卡住不动网络问题或镜像源不稳定换镜像源或检查网络代理node-sass 报 Unsupported runtimeNode 版本与 node-sass 不兼容换 Node 14 或直接换成 sassnode-sass 本地编译失败缺少 Python、构建工具不折腾直接替换成 sassnpm 安装报 ERESOLVEnpm 版本过高用 --legacy-peer-deps 或换低版本 npmwebpack-dev-server 启动报 EntryPlugin 错误版本不匹配锁定 webpack-dev-server 2.xbabel 编译报 Unexpected token缺少 preset 或配置安装 babel-preset-env 并在 config 里引用浏览器控制台找不到 requestAnimationFrame老代码兼容问题入口文件顶部加 polyfill画面模糊Canvas 未适配高 DPI加 image-rendering: pixelated页面白屏但无报错可能入口文件路径错误看 webpack output.publicPath 配置热更新/监听不生效watchOptions 配错去掉 ignored 里对源码目录的忽略端口被占用8080 被其他服务占用改 webpack-dev-server 的 port 参数地图生成结果不一致项目使用 Math.random若需要稳定复现可替换为随机数种子实现5. 跑起来之后源码阅读与二次开发建议5.1 建议先读哪几个核心模块项目成功运行后如果只是看一眼画面就关掉那就白白折腾了这么久。OpenSC2K 这类项目真正的价值在源码。我建议按这个顺序去读代码入口文件、地图生成器、渲染循环、数据管理。入口文件会告诉你整个应用的初始化流程包括 Canvas 获取、事件绑定、UI 布局。地图生成器是整个项目的灵魂它负责用随机种子生成地形高度、水域分布、可用地块。读这部分时重点看它如何处理边界条件比如河流穿过地图边缘时的裁剪逻辑。渲染循环则展示了老式的 Canvas 游戏如何组织绘制批次是逐地块绘制还是按屏幕可视区域动态裁剪。数据管理部分可以对比今天的 Redux 或者 Zustand理解当年怎么用全局对象和事件回调来同步状态。读代码时不要只盯着实现要问三个问题这个函数是为什么业务场景设计的如果数据量变大瓶颈在哪个环节如果用现代框架重新写哪个模块改动收益最大带着问题去读收获会翻倍。5.2 小改动练手改地图、改资源、改标题如果你觉得光读不过瘾可以试试改代码。首次练手我推荐做三件小事风险低、效果明显。第一件是改地图生成参数。在地图相关模块里通常有一个随机数种子或者地图宽度高度变量把默认值调大比如从 64 改到 128然后看渲染速度有没有明显变化。这一改动能让立刻感受到 Canvas 渲染性能的边界。第二件是改 UI 标题或字体。老项目用的资源文件可能是一堆图片或 JSON 配置找到表示标题文字的地方改成自己名字刷新页面就能看到效果。这一步能帮你确认资源加载路径和 UI 绘制链路。第三件是调整道路自动连接逻辑。模拟城市游戏里道路放置后会自动延伸如果相邻地块已经被占用它会选择跳过或终止。把这段逻辑加一条日志输出你就能在控制台看到每一步的判定结果。这种带反馈的修改比一开始就重写整块功能要稳妥得多。5.3 把老工具链换成新工具链的成本账很多朋友读完源码后会冒出同样一个念头能不能把老项目迁移到 Vite 或者新版 webpack理论上完全能但这是一笔成本账取决于你想保留多少原有行为逻辑。如果只是想用新工具链跑起来最简单的方案是保留所有 src 代码不动只替换构建层配置。你需要把 entry 指向原来的入口文件把 sass-loader 换成新版把 babel-loader 换成 swc-loader 或 esbuild-loader然后把 webpack 配置里的相关插件按新版语法重写。这中间最麻烦的是老代码依赖某些运行时的全局变量或 polyfill迁移构建层并不会自动补上这些东西。比较现实的一种迁移路径是先跑通开发模式不要急着做生产构建。因为老项目的资源路径、publicPath、代码分割方式可能与新工具差异很大一上来就追求完美会导致大量精力花在构建配置上背离了“让老游戏重见天日”的初衷。对大多数场景完成开发模式迁移就足够了。从我个人经验来看这个项目用 Vite 迁移最快的路径是把 scss 导入改成一个独立的 css 入口把入口文件里的历史包袱做最小化处理然后开发服务器只要不报错就算阶段性成功。等到所有页面和玩法都能正常操作再回头考虑生产构建、资源压缩这些事。6. 最后的几点体会把 7 年前的开源项目跑起来的整个过程其实和做项目考古没有太大区别。你不能用今天的开发习惯去要求它也不能因为一个报错就放弃。每一步修改都要留底每一条报错都要记录这种土办法反而是最有效率的方式。我在实操中最大的感受是很多坑是可以通过“换一个等价工具”来绕开的比如 node-sass 换 sass、老 devServer 版本锁定、加一行 CSS 属性解决糊屏。不是所有问题都要抓到最底层原理才能解。但反过来说如果你愿意深入一点会发现这些老代码里藏着一批非常实在的算法实现比如地图噪声生成、四叉树空间管理、Canvas 分层渲染。这些知识放到今天依然不过时。最后分享一个具体的小技巧启动完项目后先不着急点击游戏里的任何按钮打开浏览器控制台手动调用一下项目暴露出来的全局对象查看地图数据、当前视口坐标、已放置建筑列表。这比看代码更直观。尤其是在修改完地图参数后通过控制台数据对比能快速判断改动是否生效。对我而言这就是“重见天日”最有趣的部分老游戏不只是用来玩它还是一扇打开旧时代前端思维的窗口。
返回列表