Rails 2 个小时把 Rails 5 升级 Rails 7

a112121788 · 2021年11月19日 · 最后由 1519868095 回复于 2021年11月30日 · 996 次阅读

Rails 版本升级难度与项目的复杂度有关。如果你使用的是 Rails 5.0 或者 5.1 ,建议先升级到 5.2 ,然后再升级到 7.0。如果你使用的 Ruby 版本 < 2.7,建议先升级到 2.7.4,个人感觉 Ruby 3.x 在 Rails 项目上的的表现目前还不如 2.7.4。

1 项目简介

https://tablecloud.cn/ 一个把 Excel 表格文件导入到数据库,在线进行可视化分析的工具服务。

升级之前的技术栈信息如下:

$ rails about
About your application's environment
Rails version             5.2.6
Ruby version              2.7.4-p191 (x86_64-linux)
RubyGems version          3.1.6
Rack version              2.2.3
JavaScript Runtime        Node.js (V8)
Middleware                Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper, Warden::Manager
Application root          /home/peng/data-studio/table-cloud
Environment               development
Database adapter          sqlite3
Database schema version   20210923015456

2 升级到 Rails 7 的一些好处

  1. 更丰富的功能特性
  2. 无需 package.json,也能使用现代前端构建工具
  3. ......

我升级 Rails 7 的原因,主要是不想再使用下面的方式

//= require activestorage

管理前端资源了,刚好 Rails 7 给出了一个比较满意的解决方法(忍不住想吐槽 Rails 6,算了还是忍一下吧)。

3 升级 rails gem 版本

修改 rails 的版本,把 rails 版本改为 7.0.0.alpha2。Rails 7 还没有发布正式版,可以使用 alpha2 版本。

在升级 rails 版本之前,建议先升级其他 gem,

bundle update

然后就是升级 rails 。

gem 'rails', '~> 7.0.0.alpha2'

删除 Gemfile.lock。然后执行 bundle install。

bundle install

由于是个人项目,使用的 gem 不多,就 devise 遇到了点问题,解决方式就是用最新版本的 devise。按照以下方式修改:

gem 'devise', git: 'git@github.com:heartcombo/devise', branch: 'main'

4 命令升级

建议升级 bin 目录下的命令。如果不想一个个升级。可以使用如下命令创建一个 rails 7 模板项目

gem install rails:7.0.0.alpha2
rails new demo_rails7

把 demo_rails7/bin 目录下的所有命令复制到你当先的项目的 bin 目录中,强制覆盖。

升级 bin/bundle

修改为如下内容

#!/usr/bin/env ruby
# frozen_string_literal: true

#
# This file was generated by Bundler.
#
# The application 'bundle' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require "rubygems"

m = Module.new do
  module_function

  def invoked_as_script?
    File.expand_path($0) == File.expand_path(__FILE__)
  end

  def env_var_version
    ENV["BUNDLER_VERSION"]
  end

  def cli_arg_version
    return unless invoked_as_script? # don't want to hijack other binstubs
    return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update`
    bundler_version = nil
    update_index = nil
    ARGV.each_with_index do |a, i|
      if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
        bundler_version = a
      end
      next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
      bundler_version = $1
      update_index = i
    end
    bundler_version
  end

  def gemfile
    gemfile = ENV["BUNDLE_GEMFILE"]
    return gemfile if gemfile && !gemfile.empty?

    File.expand_path("../../Gemfile", __FILE__)
  end

  def lockfile
    lockfile =
      case File.basename(gemfile)
      when "gems.rb" then gemfile.sub(/\.rb$/, gemfile)
      else "#{gemfile}.lock"
      end
    File.expand_path(lockfile)
  end

  def lockfile_version
    return unless File.file?(lockfile)
    lockfile_contents = File.read(lockfile)
    return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
    Regexp.last_match(1)
  end

  def bundler_version
    @bundler_version ||=
      env_var_version || cli_arg_version ||
        lockfile_version
  end

  def bundler_requirement
    return "#{Gem::Requirement.default}.a" unless bundler_version

    bundler_gem_version = Gem::Version.new(bundler_version)

    requirement = bundler_gem_version.approximate_recommendation

    return requirement unless Gem::Version.new(Gem::VERSION) < Gem::Version.new("2.7.0")

    requirement += ".a" if bundler_gem_version.prerelease?

    requirement
  end

  def load_bundler!
    ENV["BUNDLE_GEMFILE"] ||= gemfile

    activate_bundler
  end

  def activate_bundler
    gem_error = activation_error_handling do
      gem "bundler", bundler_requirement
    end
    return if gem_error.nil?
    require_error = activation_error_handling do
      require "bundler/version"
    end
    return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
    warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
    exit 42
  end

  def activation_error_handling
    yield
    nil
  rescue StandardError, LoadError => e
    e
  end
end

m.load_bundler!

if m.invoked_as_script?
  load Gem.bin_path("bundler", "bundle")
end

修改 bin/rails

#!/usr/bin/env ruby
APP_PATH = File.expand_path("../config/application", __dir__)
require_relative "../config/boot"
require "rails/commands"

修改 bin/rake

#!/usr/bin/env ruby
require_relative "../config/boot"
require "rake"
Rake.application.run

修改 bin/setup

#!/usr/bin/env ruby
require "fileutils"

# path to your application root.
APP_ROOT = File.expand_path("..", __dir__)

def system!(*args)
  system(*args) || abort("\n== Command #{args} failed ==")
end

FileUtils.chdir APP_ROOT do
  # This script is a way to set up or update your development environment automatically.
  # This script is idempotent, so that you can run it at any time and get an expectable outcome.
  # Add necessary setup steps to this file.

  puts "== Installing dependencies =="
  system! "gem install bundler --conservative"
  system("bundle check") || system!("bundle install")

  # puts "\n== Copying sample files =="
  # unless File.exist?("config/database.yml")
  #   FileUtils.cp "config/database.yml.sample", "config/database.yml"
  # end

  puts "\n== Preparing database =="
  system! "bin/rails db:prepare"

  puts "\n== Removing old logs and tempfiles =="
  system! "bin/rails log:clear tmp:clear"

  puts "\n== Restarting application server =="
  system! "bin/rails restart"
end

添加命令 bin/importmap

#!/usr/bin/env ruby

require_relative "../config/application"
require "importmap/commands"

删除命令

  1. bin/spring
  2. bin/update
  3. bin/yarn

删除 gem

删除不需要的 gem,如:

gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'

5 调整 js 文件位置

调整 js 文件位置并不是必须的,但是我还是比较喜欢把 js 文件放在 app/javascript 下。

修改 /app/assets/config/manifest.js 中的路径,最终修改为

//= link_tree ../images
//= link_directory ../stylesheets .css
//= link_tree ../../javascript .js
//= link_tree ../../../vendor/javascript .js

把 /app/assets/javascript/ 文件夹移动到 /app/javascript/

6 添加 importmap-rails 到 Gemfile

gem "importmap-rails", ">= 0.3.4"

7 修改 /app/views/layouts/application.html.erb

<%#= javascript_include_tag 'application' %>
<%= javascript_importmap_tags %>

8 修改 /app/assets/javascript/application.js

调整 application.js js 文件的引入形式

传统

//= require jquery3
//= require rails-ujs
//= require activestorage
//= require_tree .

改进

jQuery 的引用改为

<script src = "https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>

rails-ujs 不再推荐使用了,先移除,并换成 mrujs。

9 添加 importmap.rb

在 /config 中 添加 importmap.rb

pin "application", preload: true

使用 /bin/importmap 添加 npm 包

./bin/importmap pin mrujs@0.6.0

importmap.rb 文件变更为

pin "application", preload: true

pin "mrujs", to: "https://ga.jspm.io/npm:mrujs@0.6.0/dist/mrujs.module.js"
pin "morphdom", to: "https://ga.jspm.io/npm:morphdom@2.6.1/dist/morphdom.js"

./bin/importmap pin 命令会自动更新 importmap.rb 文件。相比 npm,我个人更喜欢 importmap 的 npm 包导入方式。这种设计,估计也就只有 Ruby/Rails 社区才能出现。

10 生产环境静态资源编译

先修改 /config/application.rb

config.load_defaults 7.0

编译资源

RAILS_MASTER_KEY=xxx RAILS_ENV=production bundle exec rake assets:precompile

11 小问题

在正式版上测试,没有重大问题,还有一些小问题:

  1. Chartkick 用法调整
  2. Cable 用法调整

Chartkick 相对容易,Cable 比较麻烦,暂时移除相关的业务,等相关的 js 都调整为现代 JS 写法后再修复 Cable 相关的业务。

$ rails about
About your application's environment
Rails version             7.0.0.alpha2
Ruby version              ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux]
RubyGems version          3.1.6
Rack version              2.2.3
JavaScript Runtime        Node.js (V8)
Middleware                ActionDispatch::HostAuthorization, Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::ActionableExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ContentSecurityPolicy::Middleware, ActionDispatch::PermissionsPolicy::Middleware, Rack::Head, Rack::ConditionalGet, Rack::ETag, Rack::TempfileReaper, Warden::Manager
Application root          /home/peng/data-studio/table-cloud
Environment               development
Database adapter          sqlite3
Database schema version   20210923015456

12 Ruby 版本是否需要升级

有,但不是现在,等 Ruby 3.1.0 正式版和 Rails 7.0.0 正式版发布以后在升级也不迟。如果现在升级也没问题,但也没特别的好处。

我使用如下 ruby 3.1.0dev 版本

ruby 3.1.0dev (2021-11-02T18:52:28Z master 4b248e7994) [x86_64-linux]

做了下不严格的性能对比,在我的项目中,ruby 3.1.0dev 的表现略低于 ruby 2.7.4,在我开启 yjit 的情况下也是如此。 yjit 的表现和项目规模有关系。

如果想尝试 ruby 3.1.0 ,需要安装如下三个 gem

gem 'net-smtp', '~> 0.3.0'
gem 'net-pop', '~> 0.1.1'
gem 'net-imap'

ruby 最新版把 smtp、pop、imap 移除了标准库。需要手动安装。

13 建议

Rails 新手不建议升级你公司的 Rails 项目,公司项目一般比较复杂,坑比较多,可以先用 Rails 7 多练练手,自己对比下使用差别,再慢慢升级。

升级过程总体来说,无聊且顺利。

这两天刚好也在折腾,准备把一个 rails5 升级到 rails6.1 我使用的是直接新建一个同名项目然后把一些文件拷贝过去
过程中主要的问题是前端的改变,升级后有 webpacker 管理
但原先项目是前后端一起的,没找到文档要怎么改才能兼容
参考一下 lz 的帖子试试直接升到 7 好了~
ruby 我用的 3.0,看来还是改回用 2.74

不错的,大哥,先看下

公司项目别折腾。

我这段时间也在折腾,将 几个 Rails 4、5 时候的项目都升级到 Rails 最新的版本了,由于写了测试,升级起来还是挺方便安全的,就是对比,然后替换文件。 整体来说,值得升级。

官方提供了正经的教程啊。也有正经的命令bin/rails app:update

网址是 https://guides.rubyonrails.org/upgrading_ruby_on_rails.html

小版本不要跳过,一个个升级就行了,还蛮好玩的。

ken 回复

有时候官方教程没什么用,比如跳过大版本

7.0.0.alpha2 的一堆之前累积的deprecations还没移除呢,sprockets 也是,autoloading 也是,所以严格来说,这版本都不算 7.0,应该是 6.2,兼容性当然好。。

后面出的 beta 才是真正的 Rails 7。

设置了 MALLOC_ARENA_MAX=2 ,使用 3.1.0-preview1 的时候我的 Rails 项目有一个常驻进程会持续内存泄漏,降级到 3.0.3 后这个现象就消失了。

另外之前用 3.1.0-preview1 的时候我需要禁用 newrelic_rpm

当时我需要添加 4 个 gem :

gem 'digest', '~> 3.1.0.pre2', require: false
gem 'net-imap', '~> 0.2.2', require: false
gem 'net-pop', '~> 0.1.1', require: false
gem 'net-smtp', '~> 0.3.0', require: false
1519868095 回复

能再打个折吗

1519868095 回复

你这广告刷屏了吧

需要 登录 后方可回复, 如果你还没有账号请 注册新账号