In my playing around with React and Redux/Relay I wanted a way to serve them up from one host. Along with the Rails GraphQL/API backend.
There were plenty of examples of getting a single React app up and running with rewrite rules or .htaccess
. But I wanted things a little differently. Mostly I didn’t want to have to have the trailing /
.
I had initially set this up with Apache 2.4 and it was working great, until I wanted to deploy it with ActionCable integration I spent way too much time trying to get websockets to work with Apache so I ended up switching to nginx. So long Apache, ~20 years was a good run.
Here’s the Apache config I was using.
<VirtualHost :443>
ServerName example.com
RailsEnv production
RewriteEngine On
DocumentRoot /path/to/your_app/public
<Directory /path/to/your_app/public>
Require all granted
</Directory>
<LocationMatch "/redux.*">
# match without a trailing /
RewriteCond %{REQUEST_URI} ^/redux$
RewriteRule ^ %{DOCUMENT_ROOT}/redux/index.html [L]
# handle any files local to the parent directory
# this allows deep links into the app to work
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
RewriteRule ^ %{DOCUMENT_ROOT}/redux/index.html [L]
</LocationMatch>
<LocationMatch "/relay.*">
RewriteCond %{REQUEST_URI} ^/relay$
RewriteRule ^ %{DOCUMENT_ROOT}/relay/index.html [L]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
RewriteRule ^ %{DOCUMENT_ROOT}/relay/index.html [L]
</LocationMatch>
</VirtualHost>
And here’s the same config for nginx
server {
listen 443 ssl;
server_name example.com;
passenger_enabled on;
passenger_app_env production;
root /path/to/your_app/public;
# match without a trailing /
location ~ ^/redux$ {
try_files $uri/index.html /index.html;
}
# handle any files local to the parent directory
# this allows deep links into the app to work
location /redux {
try_files $uri $uri/ $uri/index.html /redux/index.html;
}
location ~ ^/relay$ {
try_files $uri/index.html /index.html;
}
location /relay {
try_files $uri $uri/ $uri/index.html /redux/index.html;
}
}
By the way, that nginx config is the actual config that is serving up the Rails app for API and GraphQL, React Apps and the ActionCable endpoint.
I’ve been playing around with React lately.
I built a Rails API backed app as a learning exercise, the app is a Todo List, but not the usual TodoMVC. Think multiple users with multiple lists of items. I felt that would be enough exposure into the React way of doing things.
Well, actually since I like to torture myself when learning new things, I started out doing this in Relay, including building the GraphQL server using graphql-ruby.
Next stop was doing it in Vanilla React, which felt horribly painful after all the magick that Relay brings to the table.
After that came doing it again with Redux, I figured it made sense to expose myself to more than one way of doing things in React.
Once I got it all running halfway decently I wanted to try and get RSpec integration tests up and running. After much searching and finding random tidbits that worked here and there, once I got a working setup I figured I’d document it. This setup will save a screenshot on failure and open it in the appropriate app (I’ve only tested this bit on macOS).
You will need to install chromedriver, you can get it here or install it via homebrew.
Here are the relevant files for a working (as of 2018-02-26) RSpec/React integration test setup.
Gemfile
...
group :test do
gem 'capybara', '~> 2.4.4'
gem 'capybara-screenshot', '~> 1.0.11'
gem 'database_cleaner'
gem 'rspec-rails'
gem 'selenium-webdriver'
end
Procfile-react-test
web: cd clients/react && yarn start
api: RAILS_ENV=test bundle exec rails s -p 3001
spec/rails_helper.rb (add this bit after require 'rspec/rails'
)
Dir['spec/support/**/*.rb'].each do |file|
require Rails.root.join(file).to_s
end
spec/support/capybara.rb
require 'capybara/rspec'
require 'capybara/rails'
require 'capybara-screenshot/rspec'
Capybara.app_host = "http://localhost:3000"
Capybara.default_wait_time = 5
Capybara::Screenshot.webkit_options = {
width: 1024,
height: 768
}
Capybara::Screenshot.autosave_on_failure = false
Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
config.after do |example|
if example.metadata[:type] == :feature and example.metadata[:js] and example.exception.present?
Capybara::Screenshot.screenshot_and_open_image
end
end
end
spec/support/capybara_drivers/selenium.rb (I have multiple files as I was testing different drivers, that’s also the reason for catching LoadError)
begin
require 'selenium/webdriver'
require 'capybara/rspec'
Capybara.register_driver :selenium do |app|
options = Selenium::WebDriver::Chrome::Options.new(
args: %w[headless disable-gpu no-sandbox]
)
Capybara::Selenium::Driver.new(app, browser: :chrome, options: options)
end
Capybara.javascript_driver = :selenium
rescue LoadError
end
spec/features/signups_spec.rb
require 'rails_helper'
RSpec.feature "Signups", type: :feature, js: true do
scenario "Visitor signs up for a new account" do
visit("/signup")
fill_in "name", with: "Bob"
fill_in "email", with: "bob@example.com"
fill_in "password", with: "password"
click_button "Create Account"
expect(page).to have_text("Logout Bob")
end
end
To run the tests you need to start the server and client.
foreman start -p 3000 -f Procfile-react-test
And run your feature specs
% bundle exec rspec spec/features
.
Finished in 4.98 seconds (files took 9.46 seconds to load)
1 example, 0 failures
And there you have it, those are the changes you need to get this working.
What’s new:
So I finally got around to updating JABE yeah I know its been forever (about a year).
Well it now plays nice with Rails 3.1+ and you can now mount it to any path you want instead of just the root.
Here’s yet another take on how to handle generated files when you are deploying to “Heroku”:http://www.heroku.com/ while keeping your git repo free of the artifacts.
We deploy using a rake task rake deploy
that was generating Jammit files, committing them to the repo and pushing them up. This works fine, but adds “junk” commits to your repo and even the occasional merge conflict from the generated files.
Since Heroku doesn’t care what you push to it as long as it is in the master branch on their end, why not generate the assets in a throwaway branch and push that up?
Here are the basic commands you would run in the shell. Wrap them up in a rake task that fits your project and off you go.
# send it up to github
git push origin master
# get rid of the branch if it exists (it shouldn't)
git branch -D deploy
# generate Jammit, Sass, etc. files
rake cache_assets
# push it up to heroku, need force since it's a different branch every time
git push heroku deploy:master --force
git checkout master
# get rid of the old branch
git branch -D deploy
As long as everybody uses the rake task to deploy you shouldn’t have any problems with this technique.