上个月接手了一个NFT数字藏品项目,代码写得惨不忍睹。智能合约里各种硬编码,后端服务一个函数几百行,前端页面把所有逻辑都写在一个组件里,没有注释,没有文档,改一个地方要翻半天代码,还经常改出bug。

这个项目是之前一个外包团队做的,功能基本能用,但代码质量极差,完全无法维护。业务方想加新功能,评估了一下,在现有代码上加新功能还不如重写。但重写风险太大,时间也不允许,最后决定先重构,把代码质量提上来,再在重构后的代码上加新功能。

花了两周时间对整个项目进行了重构,代码质量有了质的提升,加新功能的效率也提高了很多。本文分享这次重构的过程和经验,包括智能合约、后端服务、前端页面三个部分,聊聊如何从烂代码演进到优雅代码。

先说明一下技术栈:智能合约用Solidity,后端用Node.js + Express,数据库用MongoDB,前端用React。NFT标准用的是ERC-721,部署在以太坊侧链上。

一、重构前的代码有多烂

先说说重构前的代码有多烂,让大家有个直观的感受。

智能合约部分:

  • 所有逻辑都写在一个合约里,铸造、转账、白名单、抽奖、空投,全混在一起,一个文件一千多行
  • 各种硬编码,价格写死在代码里,白名单地址写死在代码里,连NFT的元数据URL都写死了
  • 没有权限控制,谁都能调用铸造函数,上线后差点被人薅羊毛
  • 没有事件日志,链上操作无法追踪,出了问题查不到原因
  • 变量命名随意,a、b、c、data、info,看名字不知道是什么意思
  • 没有注释,复杂的逻辑没有任何说明,看代码全靠猜

后端服务部分:

  • 一个路由处理函数几百行,从参数校验到业务逻辑到数据库操作到返回结果,全写在一个函数里
  • 重复代码满天飞,同样的数据库查询逻辑在不同的地方写了好几遍
  • 没有错误处理,异步操作不catch错误,出了问题服务直接崩溃
  • 数据库操作直接写在路由里,没有数据访问层,换数据库要改所有地方
  • 配置信息硬编码,数据库地址、密钥、API地址都写死在代码里,不同环境要改代码
  • 没有日志,出了问题不知道发生了什么,只能靠console.log调试

前端页面部分:

  • 所有逻辑都写在一个组件里,一个组件两千多行,状态、事件、渲染全混在一起
  • 没有组件拆分,导航栏、列表、详情、弹窗全写在一个文件里
  • 状态管理混乱,useState到处都是,同一个状态在不同的地方重复定义
  • 没有接口层,API调用直接写在组件里,改接口要改所有组件
  • 样式内联,每个元素都写style,没有统一的样式管理
  • 复制粘贴代码,同样的UI逻辑在不同的页面复制了好几份

看完这样的代码,我当时的心情是崩溃的。但项目已经上线了,有用户在使用,不能推倒重来,只能在现有基础上重构。

二、重构的原则和策略

开始重构之前,先确定了重构的原则和策略,避免重构过程中出问题。

1. 功能不变,只改代码结构。 重构的目标是提高代码质量,不是改变功能。重构过程中,对外的功能和接口保持不变,用户感知不到变化。这样可以降低风险,也方便验证重构是否正确。

2. 小步快跑,逐步重构。 不要想着一次性把所有代码都重构完,那样风险太大,而且容易出问题。把重构分成小步骤,一个模块一个模块地重构,每重构完一个模块就测试验证,确保没问题再继续下一个。

3. 每一步都可回滚。 重构过程中,每一步都要提交代码,确保出了问题可以回滚到上一个正常版本。用Git管理,每重构完一个功能点就commit一次,commit message写清楚改了什么。

4. 先加测试,再重构。 重构之前,先给核心功能加测试,确保重构之后功能和之前一致。没有测试的重构是盲目的,改完之后不知道有没有改坏。

5. 重构和新功能分开。 重构期间不加新功能,新功能等重构完之后再加。一边重构一边加新功能,容易混乱,也容易出问题。

确定了这些原则之后,就开始了重构工作。下面分智能合约、后端、前端三个部分,详细说说重构的过程。

三、智能合约重构

智能合约是NFT项目的核心,也是风险最高的部分,因为合约部署到链上之后就不能改了(除非用代理合约)。所以智能合约的重构要特别小心。

好消息是,这个项目的合约还没有部署到主网,只在测试网上跑,所以可以重新部署。如果已经部署到主网了,就不能大改了,只能用代理合约或者部署新合约迁移数据。

重构步骤一:拆分合约,单一职责。

原来的合约把所有逻辑都写在一个文件里,一千多行,什么都干。重构的第一步是拆分合约,按照单一职责原则,把不同的功能拆到不同的合约里。

拆分成了几个合约:

  • NFTCore:核心的ERC-721逻辑,铸造、转账、余额查询等
  • NFTMint:铸造相关的逻辑,公开铸造、白名单铸造、空投等
  • NFTWhitelist:白名单管理,添加白名单、移除白名单、查询是否在白名单
  • NFTAirdrop:空投相关的逻辑,批量空投、条件空投等
  • NFTMetadata:元数据管理,设置基础URL、设置单个NFT的元数据

每个合约只负责一个功能,代码量控制在几百行以内,清晰明了。合约之间通过继承或者接口调用的方式协作。

拆分之后,每个合约的职责清晰,改某个功能只需要改对应的合约,不会影响其他功能。代码可读性也大大提高了。

重构步骤二:消除硬编码,用变量和配置。

原来的合约里各种硬编码,价格、白名单地址、元数据URL都写死了。重构之后,把这些都改成可配置的变量,通过管理员函数来设置。

比如:

  • 铸造价格:改成mintPrice变量,通过setMintPrice函数设置
  • 白名单:改成mapping(address => bool),通过addWhitelist和removeWhitelist函数管理
  • 元数据URL:改成baseURI变量,通过setBaseURI函数设置
  • 最大供应量:改成maxSupply变量,构造函数里设置
  • 每个地址限购数量:改成mintLimitPerAddress变量,可配置

这样改之后,调整参数不需要改代码重新部署,只需要调用管理员函数就行。运营人员也可以自己调整价格、管理白名单,不需要开发人员介入。

重构步骤三:加权限控制。

原来的合约没有权限控制,谁都能调用管理员函数,这是严重的安全隐患。重构之后,加了权限控制。

用了OpenZeppelin的Ownable合约,只有合约所有者才能调用管理员函数,比如设置价格、管理白名单、设置元数据URL等。普通用户只能调用铸造、转账、查询等普通函数。

对于一些更敏感的操作,比如提现、暂停合约,用了Pausable合约,可以在紧急情况下暂停合约,防止攻击。

还加了一个多签管理员的设计,关键操作需要多个管理员签名才能执行,防止单点故障和私钥泄露导致的损失。

重构步骤四:加事件日志。

原来的合约几乎没有事件,链上操作无法追踪。重构之后,给所有重要的操作都加了事件。

比如:

  • Minted:铸造事件,记录铸造者、NFT ID、铸造价格、时间
  • WhitelistAdded:白名单添加事件,记录添加的地址、操作者、时间
  • PriceChanged:价格变更事件,记录旧价格、新价格、操作者、时间
  • BaseURIChanged:元数据URL变更事件
  • AirdropSent:空投事件,记录接收者、NFT ID、数量

加了事件之后,链上的所有操作都可以通过事件查询到,出了问题可以追溯,也方便后端服务监听事件来更新数据库。

重构步骤五:规范命名和注释。

原来的合约变量命名随意,没有注释。重构之后,统一了命名规范,变量和函数用驼峰命名,名字要能表达含义,不要用a、b、c这种无意义的名字。

给所有的公共函数和复杂的内部逻辑都加了注释,说明函数的作用、参数的含义、返回值的意义、可能的异常情况。复杂的逻辑加行内注释,说明为什么这么写。

还加了NatSpec格式的文档注释,用///开头,可以自动生成文档,方便其他人理解合约的接口。

重构步骤六:安全检查和测试。

智能合约的安全很重要,重构完之后做了全面的安全检查。

  • 用Slither做了静态分析,检查常见的安全漏洞,比如重入、整数溢出、未检查的返回值等
  • 人工review了所有关键逻辑,尤其是涉及资金转移的部分
  • 写了全面的单元测试,覆盖所有的函数和边界条件
  • 在测试网上做了完整的功能测试,模拟各种场景
  • 找了第三方做了安全审计,确保没有严重的安全漏洞

经过这些步骤,智能合约的代码质量和安全性都有了很大的提升。

四、后端服务重构

后端服务是连接前端和区块链的中间层,负责业务逻辑、数据库操作、和链上交互。原来的后端代码也是一团糟,重构工作量很大。

重构步骤一:分层架构,职责分离。

原来的后端把所有逻辑都写在路由处理函数里,没有分层。重构之后,采用了经典的分层架构:

  • 路由层(Routes):定义API路由,只做参数接收和返回,不包含业务逻辑
  • 控制器层(Controllers):处理请求,调用服务层,组织返回数据
  • 服务层(Services):核心业务逻辑,一个服务类负责一个业务领域
  • 数据访问层(Repositories):数据库操作,封装MongoDB的查询,服务层不直接操作数据库
  • 中间件(Middlewares):鉴权、错误处理、日志、参数校验等横切关注点

每一层只和相邻的层交互,依赖关系清晰。改数据库只需要改数据访问层,改业务逻辑只需要改服务层,改接口只需要改控制器和路由。

分层之后,代码的可维护性大大提高。加新功能的时候,只需要在对应的层加代码,不会影响其他部分。

重构步骤二:消除重复代码,抽象公共逻辑。

原来的代码里重复逻辑很多,同样的数据库查询、同样的参数校验、同样的错误处理,在不同的地方写了好几遍。重构之后,把公共逻辑抽象出来。

  • 数据访问层封装了通用的CRUD操作,每个Repository继承基础Repository,不用重复写增删改查
  • 参数校验用了express-validator,统一的校验中间件,不用在每个路由里手写校验
  • 错误处理用了统一的错误处理中间件,所有的错误都集中处理,返回统一的错误格式
  • 响应格式统一,成功和失败都用统一的JSON格式,前端处理方便
  • 工具函数抽象,比如日期处理、字符串处理、加密解密等,都放到utils目录里

消除重复代码之后,代码量减少了将近一半,而且逻辑更清晰,改一个地方所有地方都生效。

重构步骤三:加错误处理和日志。

原来的代码几乎没有错误处理,异步操作不catch,出了问题服务直接崩溃。重构之后,加了完善的错误处理。

  • 所有的异步操作都用try/catch包裹,捕获异常并处理
  • 自定义了错误类型,比如NotFoundError、ValidationError、UnauthorizedError等,方便区分不同的错误
  • 统一的错误处理中间件,根据错误类型返回对应的HTTP状态码和错误信息
  • 关键操作加了事务,出错的时候回滚,保证数据一致性
  • 进程级别的错误捕获,uncaughtException和unhandledRejection都做了处理,防止服务崩溃

日志方面,用了winston做日志管理,替代了原来的console.log。

  • 日志分级:debug、info、warn、error,不同级别输出到不同的地方
  • 关键操作都加了日志,记录操作人、操作内容、操作时间、结果
  • 错误日志记录完整的错误堆栈,方便排查问题
  • 日志按天滚动存储,保留最近30天的日志,自动清理旧日志
  • 生产环境的日志输出到文件,开发环境同时输出到控制台

加了错误处理和日志之后,服务的稳定性大大提高,出了问题也能快速定位原因。

重构步骤四:配置管理,环境隔离。

原来的配置信息都硬编码在代码里,不同环境要改代码,很容易出错。重构之后,用了dotenv做配置管理。

  • 所有的配置信息都放在.env文件里,包括数据库地址、端口、密钥、API地址、链上节点地址等
  • 不同环境用不同的.env文件,.env.development、.env.production,部署的时候自动加载对应的配置
  • 代码里通过process.env读取配置,不直接写死
  • 敏感信息(私钥、密钥)不提交到代码仓库,.env文件加入.gitignore
  • 加了配置校验,启动的时候检查必要的配置是否存在,不存在就报错,避免启动后出问题

配置管理做好之后,部署和切换环境都很方便,不需要改代码,只需要改配置文件。

重构步骤五:和链上交互的封装。

后端需要和智能合约交互,原来的代码里到处都是web3.js的调用,重复且混乱。重构之后,把和链上交互的逻辑封装成了一个独立的模块。

  • 封装了区块链服务类(BlockchainService),提供统一的接口,比如mintNFT、transferNFT、getBalance、getTokenURI等
  • 合约的ABI和地址统一管理,通过配置加载
  • 封装了交易发送和确认的逻辑,处理nonce、gasPrice、交易确认等细节
  • 加了交易重试机制,网络波动的时候自动重试
  • 封装了事件监听,监听链上事件并更新数据库

封装之后,业务代码不需要关心web3.js的细节,只需要调用BlockchainService的方法就行。换链或者换合约的时候,只需要改这一个模块。

重构步骤六:加测试。

原来的后端没有任何测试,全靠手动测试,改完代码不知道有没有改坏。重构之后,加了测试。

  • 单元测试:用Jest写单元测试,覆盖服务层和工具函数的核心逻辑
  • 接口测试:用supertest写接口测试,覆盖所有API的正常和异常场景
  • 测试用例覆盖边界条件,比如参数为空、参数非法、权限不足等
  • 测试数据库用独立的测试库,不和开发库混用,每个测试用例执行完清理数据
  • CI集成,提交代码自动跑测试,测试不通过不能合并

加了测试之后,改代码有了保障,不用担心改完之后出问题。重构过程中,测试帮我发现了好几个隐藏的bug。

五、前端页面重构

前端页面原来也是一团糟,一个组件两千多行,什么都混在一起。重构前端的工作量也很大。

重构步骤一:组件拆分,单一职责。

原来的前端把所有逻辑都写在一个组件里。重构的第一步是拆分组件,按照单一职责原则,把大组件拆成小组件。

拆分成了几类组件:

  • 布局组件:Layout、Header、Footer、Sidebar,负责页面布局
  • 业务组件:NFTList、NFTCard、NFTDetail、MintModal、WhitelistForm,每个组件负责一个业务功能
  • 通用组件:Button、Input、Modal、Loading、Toast,可复用的UI组件
  • 页面组件:HomePage、DetailPage、MyCollectionPage、AdminPage,每个页面对应一个组件

每个组件只负责一个功能,代码量控制在几百行以内。组件之间通过props传递数据,通过回调函数通信,依赖关系清晰。

拆分之后,组件的复用性大大提高,同样的UI逻辑不用重复写。改某个功能只需要改对应的组件,不会影响其他组件。

重构步骤二:状态管理,统一管理。

原来的前端状态管理混乱,useState到处都是,同一个状态在不同的地方重复定义。重构之后,用了Redux Toolkit做全局状态管理。

  • 全局状态(用户信息、NFT列表、链上数据)放在Redux里,统一管理
  • 组件内部的局部状态(弹窗显示、输入框内容)用useState,不放到全局
  • 用Redux Toolkit的createSlice写reducer,代码简洁,不用写大量的action类型
  • 异步操作用createAsyncThunk,处理loading、success、error三种状态
  • 用useSelector和useDispatch在组件里读写状态,不用一层层传props

状态管理统一之后,数据流向清晰,不会出现同一个状态在不同地方不一样的情况。改全局数据只需要改一个地方,所有用到的组件都会自动更新。

重构步骤三:接口层封装。

原来的前端API调用直接写在组件里,改接口要改所有组件。重构之后,把API调用封装成了独立的接口层。

  • 用axios封装了HTTP客户端,统一设置baseURL、超时、请求头
  • 请求拦截器:自动添加token、处理请求参数
  • 响应拦截器:统一处理响应数据、错误处理、token过期跳转登录
  • 按业务领域封装API模块,比如nftApi、userApi、orderApi,每个模块里是相关的接口调用
  • 组件里只调用API模块的方法,不直接用axios,不关心接口的URL和参数细节

封装之后,改接口只需要改API模块,不用改组件。接口的baseURL、鉴权等逻辑统一管理,不用在每个地方重复写。

重构步骤四:样式管理,统一规范。

原来的前端样式都是内联的,每个元素都写style,没有统一的样式管理。重构之后,用了CSS Modules做样式管理。

  • 每个组件对应一个.module.css文件,样式局部作用域,不会冲突
  • 抽离了全局样式变量,颜色、字体、间距等都用变量,统一管理
  • 通用的样式(按钮、卡片、表单)抽成公共类,复用
  • 响应式布局,适配不同屏幕尺寸
  • 去掉了所有的内联样式,统一用CSS

样式管理规范之后,页面的视觉风格统一了,改样式也方便,不用在每个组件里找style。

重构步骤五:和链上交互的封装。

前端需要和钱包、链上合约交互。原来的代码里到处都是ethers.js的调用。重构之后,把和链上交互的逻辑封装成了独立的服务。

  • 封装了WalletService,处理钱包连接、账号切换、网络切换
  • 封装了ContractService,封装合约调用,比如mint、balanceOf、ownerOf等
  • 封装了事件监听,监听链上事件更新前端状态
  • 统一处理交易的发送、确认、错误提示
  • 组件里只调用服务的方法,不直接用ethers.js

封装之后,组件代码更简洁,不用关心链上交互的细节。换钱包或者换合约的时候,只需要改服务层。

重构步骤六:性能优化。

重构的同时,也做了一些性能优化。

  • 用React.memo优化组件渲染,避免不必要的重渲染
  • 用useMemo和useCallback缓存计算结果和函数,避免重复计算
  • 图片懒加载,NFT列表滚动到可视区域再加载图片
  • 分页加载,NFT列表不用一次性加载所有数据,滚动加载更多
  • 打包优化,用React.lazy做代码分割,首屏加载更快

性能优化之后,页面加载速度和流畅度都有了提升。

六、重构的效果

花了两周时间,整个项目重构完成。重构之后的效果很明显:

1. 代码质量提升。 代码结构清晰,分层合理,命名规范,注释完善。新人接手项目,看一两天就能理解代码结构,开始开发。原来的代码新人要看一周才能看懂。

2. 开发效率提高。 加新功能的时候,只需要在对应的层加代码,不用到处改。重复代码少了,通用逻辑可以复用。原来加一个功能要三天,现在一天就能完成。

3. Bug减少。 加了测试之后,改代码有了保障,很多bug在开发阶段就发现了。错误处理和日志完善之后,线上出了问题也能快速定位修复。线上bug数量减少了70%。

4. 性能提升。 后端服务的响应速度更快了,数据库查询优化了,没有了重复查询。前端页面加载更快了,渲染更流畅了。用户体验有了明显提升。

5. 可维护性提高。 代码结构清晰,职责分离,改一个地方不会影响其他地方。配置管理完善,不同环境切换方便。文档和注释齐全,维护成本大大降低。

重构完成之后,业务方再加新功能,评估时间从原来的两周缩短到了三天,而且质量有保障。业务方很满意,我们开发也轻松了很多。

七、重构的经验和教训

这次重构也积累了一些经验和教训,分享给大家。

1. 重构之前一定要有测试。 没有测试的重构是盲目的,改完之后不知道有没有改坏。如果原来的代码没有测试,先给核心功能加测试,再开始重构。测试是重构的安全网。

2. 小步快跑,不要贪大求全。 不要想着一次性把所有代码都重构完,那样风险太大。把重构分成小步骤,一个模块一个模块地来,每一步都验证没问题再继续。重构的过程中,随时可以停下来,不会影响正常使用。

3. 重构期间不要加新功能。 一边重构一边加新功能,容易混乱,也容易出问题。先把重构做完,代码质量提上来,再加新功能。新功能在高质量的代码基础上加,效率更高,质量也更好。

4. 和团队成员充分沟通。 重构会改变代码结构,影响所有开发人员。重构之前要和团队成员充分沟通,说明重构的目标、计划、影响,让大家都理解和支持。重构过程中,及时同步进展,遇到问题一起讨论。

5. 不要为了重构而重构。 重构的目的是提高代码质量,方便后续开发,不是为了炫技或者追求完美。如果代码已经够用,改的频率也不高,就没必要重构。重构是有成本的,要评估投入产出比。

6. 重构不是一次性能完成的。 代码质量的提升是一个持续的过程,不是一次重构就能彻底解决的。重构完成之后,还要在日常开发中保持代码质量,持续优化,防止代码再次腐化。可以在每次加新功能的时候,顺便优化一下相关的旧代码。

八、写在最后

这次NFT数字藏品项目的重构,是我做过的比较大的一次重构。从智能合约到后端到前端,整个项目的代码质量都有了质的提升。过程很辛苦,两周时间几乎天天加班,但结果很值得。

很多团队都有这样的问题:项目初期为了赶进度,代码写得很随意,能跑就行。等到项目上线了,要加新功能了,才发现代码烂得没法维护,改一个地方要翻半天,还经常改出bug。这时候就需要重构。

重构不是推倒重来,而是在现有代码的基础上,逐步优化结构,提高质量。好的重构,用户感知不到变化,但开发人员的体验会有天壤之别。

代码质量是技术团队的生命线。短期看,写好代码会慢一点;但长期看,好的代码能大大提高开发效率,减少bug,降低维护成本。不要为了赶进度而牺牲代码质量,因为出来混迟早要还的,技术债欠得越多,还的时候越痛苦。

希望这篇文章能给正在做重构或者准备做重构的朋友一些参考。也希望大家都能写出优雅的代码,少被烂代码折磨。

最后用一句话结束本文:优秀的代码是写给人看的,只是顺便能在机器上运行。从烂代码到优雅代码,是每个程序员的修行。