ARTICLE DETAIL

资讯详情

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

构建可维护的 Ruby 项目:多文件组织、require 加载机制与 Bundler 依赖管理实战指南

构建可维护的 Ruby 项目:多文件组织、require 加载机制与 Bundler 依赖管理实战指南 构建可维护的 Ruby 项目多文件组织、require 加载机制与 Bundler 依赖管理实战指南【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum本篇文章围绕 Ruby 项目的工程化组织展开系统讲解如何将一个项目拆分为多个文件并通过require/require_relative正确加载、如何用命名空间module避免代码冲突、如何理解 gem 与依赖声明以及如何使用 Bundler 与Gemfile/Gemfile.lock锁定项目依赖版本。读完本文你将掌握一套从目录结构、加载机制到依赖管理的完整 Ruby 项目搭建方案并能配合.ruby-version与 Ruby LSP 让 VSCode 下的开发体验接近专业水准。混乱、惯例与便利为什么要拆分文件回想一下你在 Foundations 阶段接触的项目HTML、CSS、JS 各自存放在独立文件中浏览器里之所以看起来浑然一体是因为 HTML 通过link和script把它们串联了起来。这个思路同样适用于 Ruby。把项目组织进多个文件有实实在在的好处最核心的是让代码更加模块化modular随着业务复杂度上升将不同职责的代码隔离到不同文件会大幅降低理解与调整的难度。那句老话——每样东西都有它的位置每样东西都在它的位置上A place for everything and everything in its place——同样适用于软件项目。对于 Ruby 项目业界约定俗成的规则主要有两条一个类一个文件每当你新建一个类就应该为它单独创建一个文件。所有 Ruby 源文件放入lib目录这是 Ruby 社区最普遍的项目布局惯例例如project_name ├── lib │ └── lovely_file_of_yours.rb └── main.rb这种入口文件 lib 目录的结构在后续课程中会反复出现比如 RSpec 测试课程 就展示了lib/todo_list.rb存放业务代码、spec/存放测试代码的划分方式。多文件加载require_relative 与 require代码拆分后紧接着的问题就是如何让一个文件中的代码能被另一个文件使用考虑如下文件结构├── lib │ ├── sort │ │ ├── bogo_sort.rb │ │ ├── bubble_sort.rb │ │ └── merge_sort.rb │ └── sort.rb └── main.rbRuby 提供了两种主要加载方式require_relative与require。两者都会执行目标文件从而让你使用其中定义的内容如果对同一个文件重复加载第二次及以后不会再执行调用会返回false。require_relative以被加载文件自身为参照假设你的终端当前位于项目根目录即存放main.rb的目录main.rb与lib/sort.rb内容如下# main.rb require_relative lib/sort# lib/sort.rb require_relative sort/bubble_sort require_relative sort/bogo_sort require_relative sort/merge_sort官方文档对require_relative的定义是require_relative(string) → true or falseRuby 尝试加载名为string的库路径相对于包含该 require 语句的文件所在的目录。如果文件不存在则抛出LoadError。文件被加载时返回true若此前已加载过则返回false。关键在于相对于包含该 require 语句的文件所在的目录relative to the directory containing the requiring file。也就是说无论你从哪个目录执行代码require_relative都以写下这行代码的文件为起点解析路径。因此main.rb会进入lib查找sort.rb扩展名可以省略而sort.rb会进入sort目录查找那三个排序文件。require以运行时的当前目录为参照并搜索 $LOAD_PATHrequire的解析规则更复杂官方文档的关键描述是如果特性是绝对路径如以/开头将直接按绝对路径加载。如果特性是显式相对路径如以./或../开头将相对于当前目录加载。否则特性将在$LOAD_PATH列出的库目录中搜索。绝对路径无需赘言重点在于相对路径的差异require以你运行代码时所在的目录为参照点。仍以上述项目为例终端位于项目根目录# main.rb require lib/sort# lib/sort.rb require sort/bubble_sort require sort/bogo_sort require sort/merge_sort直接运行会报错——因为lib/sort不是显式相对路径Ruby 不会把它当作相对路径处理而去$LOAD_PATH里也找不到于是抛出LoadError。改成显式相对路径后# main.rb require ./lib/sort# lib/sort.rb require ./sort/bubble_sort require ./sort/bogo_sort require ./sort/merge_sort此时main.rb中的require ./lib/sort可以正常工作但sort.rb里的require ./sort/bubble_sort会再次报错——因为它不是从sort.rb的角度解析而是仍然从main.rb所在的当前目录去找./sort/bubble_sort自然找不到。再看$LOAD_PATH的部分# main.rb require csv require_relative lib/sortrequire csv会在 Ruby 的全局变量$LOAD_PATH中查找csv.rb该变量默认包含 Ruby 标准库路径。require会自动尝试.rb等扩展名而无需显式声明如果在$LOAD_PATH中没有找到它还会继续在已安装的 gem中查找这正是后面要讲的 RubyGems 的功劳。由此可以得出社区通行的约定require_relative用于加载自己的代码require用于加载外部的东西——比如项目依赖的 gem。实战示例多文件项目如何串联借助这种加载机制你无需把整个应用的代码塞进一个文件。假设文件结构如下├── lib │ ├── flight.rb │ ├── hotel.rb │ └── airport.rb └── main.rb各文件内容# lib/airport.rb class Airport def introduce puts Im at the airport! end end# lib/flight.rb class Flight def introduce puts Im on the flight! end end# lib/hotel.rb class Hotel def introduce puts Im at the hotel! end end# main.rb require_relative lib/airport require_relative lib/flight require_relative lib/hotel Airport.new.introduce # Im at the airport! Flight.new.introduce # Im on the flight! Hotel.new.introduce # Im at the hotel!这样就不必在airport.rb里同时定义Flight和Hotel类。习惯做法是在最顶层的文件如这里的main.rb集中 require 所有文件让其他人只要拿到main.rb就能获得完整代码。需要注意两点加载时的边界行为局部变量不会被加载如果airport.rb里定义了局部变量coolest_airports在main.rb中访问它会直接报错常量会被加载模块、类等常量定义在 require 后可以在其他文件中正常访问。命名空间为什么要把代码包进 module一个必须牢记的事实是所有被 require 进来的代码都处于同一个命名空间。如果不同文件里定义了同名的方法、模块或类后加载的定义会覆盖先加载的定义。例如你和朋友都写了同名方法为简化假设文件都在同一目录# not_so_green.rb def food_opinion(food) #{food} is awesome! end# scheals.rb def food_opinion(food) #{food} is awful! end# main.rb require_relative not_so_green require_relative scheals puts food_opinion(Cereal) # Cereal is awful!因为food_opinion被定义了两次最后一次定义胜出。为了避免代码被意外覆盖Ruby 开发者会把代码包裹在模块module中利用模块获得命名空间的隔离效果# not_so_green.rb module NotSoGreen def self.food_opinion(food) #{food} is awesome! end end# scheals.rb module Scheals def self.food_opinion(food) #{food} is awful! end end# main.rb require_relative not_so_green require_relative scheals puts NotSoGreen.food_opinion(Cereal) # Cereal is awesome! puts Scheals.food_opinion(Marmite) # Marmite is awful! puts food_opinion(Cereal) # Errors out—theres no longer a free floating food_opinion method to use.使用模块限定名NotSoGreen.food_opinion/Scheals.food_opinion后两个同名方法互不干扰而顶层不再存在游离的food_opinion方法。在后续编写多类项目如 Tic Tac Toe 项目、Mastermind 项目时这一约定能有效避免类与方法名冲突。Gem 与依赖使用别人的代码学会组织自己的文件后就该学习如何使用他人的文件了。Gem就是别人写好的、打包好的 Ruby 工具库——本质上就是一些代码。其中一部分 gem 属于 Ruby 标准库但绝大多数需要单独安装。如果你在项目中使用某个 gem它就成了你的依赖dependency——你的代码依赖它才能正常运行。有些依赖只在特定场景下使用例如只有开发环境或测试环境才需要的 gem 集合。很多 gem 又依赖其他 gem而且它们所依赖的版本可能各不相同。你当然可以手动完成安装、更新、解决版本冲突的全过程但 Ruby 社区早已有了趁手的工具。RubyGemsgem 的安装通道RubyGems自 Ruby 1.9 起就是 Ruby 标准库的一部分用来把 gem 安装到你的电脑上。还记得前面提到require会在已安装的 gem 中查找文件吗那正是 RubyGems 的工作。更有趣的是RubyGems 本身也是一个 gem动手试一试。在一个名为colorful的目录中创建main.rbrequire colorize puts Red goes faster!.colorize(:red) puts Im blue da ba dee da ba di!.colorize(:blue) puts It aint easy bein green....colorize(:green)运行ruby main.rb期待看到颜色……结果却是一个LoadError。没错——你需要先安装这个 gemgem install colorize执行后你的系统就能访问Colorizegem 了。但这只是你自己的系统别人想运行你的代码也得各自gem install。一个两个倒还好可如果项目里有几十个 gem 呢如何保证大家下载到的版本完全一致这就要请出Bundler了——它本身也是 RubyGems 旗下的一个 gem只是独立发布。Bundler声明依赖并锁定版本Bundler 允许你声明项目需要哪些 gem精确到版本。其他人拿到这份声明——一个名为Gemfile的简单文件——就可以通过一条bundle install快速安装全部 gem。由于gem install是全局的你还需要一种方式让运行时只使用Gemfile中声明的那些特定版本。方法是使用bundle exec前缀后面跟上你想执行的命令——最常见的就是bundle exec ruby foo.rb。让我们确保任何人想运行我们的脚本都能如愿bundle init # 在当前工作目录创建一个默认 Gemfile bundle add colorize # 把 colorize gem 加入 Gemfile 并执行 bundle install这两条命令生成了两个文件Gemfile和Gemfile.lock。来看它们的内容# Gemfile # frozen_string_literal: true source https://rubygems.org # gem rails gem colorize, ~ 1.1# Gemfile.lock GEM remote: https://rubygems.org/ specs: colorize (1.1.0) PLATFORMS ruby x86_64-linux # This might be different for you if youre using a different CPU and OS. DEPENDENCIES colorize (~ 1.1) BUNDLED WITH 2.5.4可以看到Gemfile包含了从哪里获取 gemsource行以及需要哪些 gemgem声明行。Gemfile.lock则记录了上一次成功运行应用的环境快照Bundler 会依据它安装相同版本的 gem即使Gemfile本身允许安装更新的版本。版本约束读懂 ~ 1.1Gemfile中的~ 1.1是一种版本约束更准确地说叫悲观约束pessimistic constraint它建立在**语义化版本semantic versioning**的基础上。语义化版本号由三部分组成第一位是major主版本第二位是minor次版本第三位如果存在是patch补丁号。主版本升级可能破坏旧版本的兼容性——比如改动方法名次版本可以新增、修改功能但不能破坏兼容性补丁号则用于不影响兼容性的 bug 修复。因此只要 gem 维护者遵守语义化版本规范你就可以信赖悲观约束不会让项目拿到一个可能破坏应用的版本。gem colorize, ~ 1.1等价于gem colorize, 1.1, 2.0。Gemfile.lock的作用可以概括为记录能够运行你的应用的上一个环境。即使Gemfile允许安装更新的版本Bundler 也会用Gemfile.lock安装相同的版本从而保证在我机器上能跑在别人机器上同样能跑。.ruby-version声明目标 Ruby 版本除了依赖运行代码的人还需要知道项目的目标 Ruby 版本。一条命令就能做到rbenv local 3.2.2它会创建一个.ruby-version文件内容即声明的版本号此处为3.2.2。你可以先用rbenv versions查看本机通过 rbenv 安装的 Ruby 版本列表挑一个实际存在的版本试运行该命令。许多工具都会读取.ruby-version来判断项目使用的 Ruby 版本例如 rbenv 将不再使用全局globalRuby 版本Ruby LSP的 VSCode 扩展也会随之调整行为。VSCode 中的 Ruby LSP让编辑器感知你的项目在课程更早的阶段你可能被要求在选择Dont show again选项后忽略Ruby LSP关于找不到 lock 文件的提示也可能见过与RuboCop相关的报错。这些报错的出现是有原因的Ruby LSP 需要项目的Gemfile/Gemfile.lock才能确定要加载哪些工具与版本而 RuboCop 作为规范检查工具也应当作为项目依赖出现在Gemfile中。在下一课linting_and_rubocop中会详细讲解 RuboCop 的安装与配置。届时你将看到这样的完整工作流先用 Bundler 在项目里声明并安装 RuboCopbundle exec rubocop保证运行的是项目本地版本再通过 Ruby LSP 与 VSCode 集成在编辑过程中持续获得即时反馈、自动修复quickfix与悬停提示。把上面这些要素——Gemfile、Gemfile.lock、.ruby-version、RuboCop 依赖——逐一就位后你的项目就完成了专业级的初始化Ruby LSP 的所有能力含 RuboCop 集成都会自动生效。值得一提的是这种Gemfile声明 bundle install安装 Gemfile.lock锁定的流程在整个课程中会反复出现RSpec 测试课程 里就是先创建Gemfile写入gem rspec, 3.10再运行bundle install生成Gemfile.lock从而保证测试环境在所有机器上一致。知识自检为什么要把代码拆分到多个文件参见上文混乱、惯例与便利一节——核心是模块化便于维护与理解。如何让不同文件中的代码互相可用通过require_relative相对被加载文件与require相对当前目录或$LOAD_PATH/ 已装 gem实现。为什么要用模块包裹代码所有 require 进来的代码共享同一命名空间模块可以隔离同名方法、类避免后者覆盖前者。什么是 gem别人写好的、打包的 Ruby 工具库是代码复用的基本单元。如何安装 gem运行gem install gem_name如gem install colorize。Bundler 的用途是什么声明项目所需的 gem 及其版本Gemfile并通过bundle install一键安装。为什么要用bundle execgem 安装是全局的bundle exec能确保运行时只使用Gemfile中声明的版本。Gemfile与Gemfile.lock各有什么用Gemfile声明依赖来源与约束Gemfile.lock锁定上一成功环境的精确版本保证跨机器一致性。至此你已经掌握了从单文件脚本迈向可维护工程的关键三步用require_relative组织自己的文件、用 module 隔离命名空间、用 Bundler 管理外部依赖。以此为起点后续学习 RuboCoplinting_and_rubocop与 RSpec 测试rspec_part_one_basics时你会发现整个工具链都建立在本文这套项目组织与依赖管理的基础之上。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表