New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Server rendering #20

Merged
merged 1 commit into from Apr 20, 2016

Conversation

Projects
None yet
2 participants
@jgaskins
Member

jgaskins commented Sep 2, 2015

Rendering your content on the server brings a few benefits:

  • A user can see your content without having to wait for a JS app to parse, load, and initialize.
  • If you're rendering on the server, you already have your data loaded and you can pass it along to the client (via gon, for example) to "hydrate" the app without having to make one or more AJAX requests back to the server after the JS app loads to get it.

Since this is a Ruby framework and you can run Ruby on the server pretty easily (I've heard there were a few Ruby backend frameworks for this), I thought server rendering would be pretty easy, but given that this diff is only around 300 lines as of this PR submission (and most of them are the list of HTML tags), this is a whole lot easier than I imagined.

A couple things I wanted to discuss before this gets merged are:

  • Testing
    • As mentioned in #11, I don't have the Opal command-line testing stuff figured out yet, but since this moves some code to where the server can execute it, I can probably bring some of the tests in from the app Clearwater was extracted from to be run on the command line with the usual rspec command.
    • Unfortunately, not everything is the same on the client and server, so we do need to test the Opal-specific stuff.
    • Even with some of the shared code, it might be a good idea to run tests for them both in "server-side" Ruby and in Opal. This way, we can see places where an assumption that Opal implements things identically to MRI is wrong.
  • Routes
    • Routing works just fine server-side, but sharing routes between the Opal version and the server-rendered version of the app is a copy/paste job, which is gross. I want to figure out a way to do that better.
    • To initialize the server-side Clearwater router, you have to pass a hash to the initializer:
      router = Clearwater::Router.new(location: { pathname: request.path })
      I would like this to be a cleaner experience somehow.
  • Extract shared code
    • In the classes that need different implementations, there is still some shared code. For example, since Component needs to generate HTML on the server instead of virtual-DOM nodes, it has a different implementation. However, the HTML_TAGS list is duplicated. I'm sure there are other places where this happens. That's just the thing that stood out the most because it's a whole lotta LoC.

I'll update this issue as I think of more discussion points.

@krainboltgreene

This comment has been minimized.

Show comment
Hide comment
@krainboltgreene

krainboltgreene Oct 3, 2015

Member

Should we merge master into this?

Member

krainboltgreene commented Oct 3, 2015

Should we merge master into this?

@jgaskins

This comment has been minimized.

Show comment
Hide comment
@jgaskins

jgaskins Oct 3, 2015

Member

Might not be a bad idea. I was talking with someone at work about the server rendering today and realized it's probably out of date.

Member

jgaskins commented Oct 3, 2015

Might not be a bad idea. I was talking with someone at work about the server rendering today and realized it's probably out of date.

@jgaskins

This comment has been minimized.

Show comment
Hide comment
@jgaskins

jgaskins Dec 30, 2015

Member

Just following up here. I've rebased this branch on the current master branch and fixed the things that broke in the interim. Since master now has specs and this branch also had a few specs for things on the server side, I've split client-side specs into the spec-opal directory and server-side ones now occupy spec. They all pass.

I'm still trying to come up with a way to make the router something better than a copy/paste job from the client-side code to the server side. That seems to be the last thing that's absolutely necessary to making this workable. What does everyone think of this?

location = if RUBY_ENGINE == 'opal'
  Bowser.window.location
else
  # This doesn't exist yet, but it'd just be an object with methods to match `Bowser.location`.
  Clearwater::Router.location(request.path)
end

# Store in a constant so it's available outside this file
Router = Clearwater::Router.new(location: location) do
  # ...
end
Member

jgaskins commented Dec 30, 2015

Just following up here. I've rebased this branch on the current master branch and fixed the things that broke in the interim. Since master now has specs and this branch also had a few specs for things on the server side, I've split client-side specs into the spec-opal directory and server-side ones now occupy spec. They all pass.

I'm still trying to come up with a way to make the router something better than a copy/paste job from the client-side code to the server side. That seems to be the last thing that's absolutely necessary to making this workable. What does everyone think of this?

location = if RUBY_ENGINE == 'opal'
  Bowser.window.location
else
  # This doesn't exist yet, but it'd just be an object with methods to match `Bowser.location`.
  Clearwater::Router.location(request.path)
end

# Store in a constant so it's available outside this file
Router = Clearwater::Router.new(location: location) do
  # ...
end
@jgaskins

This comment has been minimized.

Show comment
Hide comment
@jgaskins

jgaskins Dec 30, 2015

Member

The idea is to find a way that the router code can be executed on both the server and the client. So an app would probably do something like require 'router' to load the routes. This would give us a Router object we can use to initialize our Clearwater app with on both sides.

The only thing that differs between the two would be how we figure out the current location. For both Rails and Roda (the two frameworks I've tested this branch with), this is done with request.path but we need to provide an object that quacks like a Bowser::Location.

Actually, now that I think about it, it probably just needs to respond to path, so in the case of Rails and Roda, we might be able to use the request object. However, we should still supply something in case the host framework doesn't have something like that.

Member

jgaskins commented Dec 30, 2015

The idea is to find a way that the router code can be executed on both the server and the client. So an app would probably do something like require 'router' to load the routes. This would give us a Router object we can use to initialize our Clearwater app with on both sides.

The only thing that differs between the two would be how we figure out the current location. For both Rails and Roda (the two frameworks I've tested this branch with), this is done with request.path but we need to provide an object that quacks like a Bowser::Location.

Actually, now that I think about it, it probably just needs to respond to path, so in the case of Rails and Roda, we might be able to use the request object. However, we should still supply something in case the host framework doesn't have something like that.

@jgaskins

This comment has been minimized.

Show comment
Hide comment
@jgaskins

jgaskins Dec 30, 2015

Member

I just realized when I tried the above example that it doesn't work because request isn't available outside the controller. This makes sense, of course. So instead, we'll need to encourage using a method or something to generate a router. Here's what I came up with in the app I just tried it on:

require 'components/subcomponent'

module Routing
  module_function

  def create_router location=Bowser.window.location
    Clearwater::Router.new(location: location) do
      route 'foo' => Subcomponent.new('Foo')
      route 'bar' => Subcomponent.new('Bar')
    end
  end
end

I put this file in app/shared, then in my Clearwater app and my Rails controller:

# app/assets/javascripts/application.rb
require 'routing'
app = Clearwater::Application.new(
  component: Layout.new,
  router: Routing.create_router,
  element: Bowser.document['#app']
)
app.call

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @pre_render = Clearwater::Application.new(
      component: Layout.new,
      router: Routing.create_router
    ).render
  end
end
Member

jgaskins commented Dec 30, 2015

I just realized when I tried the above example that it doesn't work because request isn't available outside the controller. This makes sense, of course. So instead, we'll need to encourage using a method or something to generate a router. Here's what I came up with in the app I just tried it on:

require 'components/subcomponent'

module Routing
  module_function

  def create_router location=Bowser.window.location
    Clearwater::Router.new(location: location) do
      route 'foo' => Subcomponent.new('Foo')
      route 'bar' => Subcomponent.new('Bar')
    end
  end
end

I put this file in app/shared, then in my Clearwater app and my Rails controller:

# app/assets/javascripts/application.rb
require 'routing'
app = Clearwater::Application.new(
  component: Layout.new,
  router: Routing.create_router,
  element: Bowser.document['#app']
)
app.call

# app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
    @pre_render = Clearwater::Application.new(
      component: Layout.new,
      router: Routing.create_router
    ).render
  end
end

@jgaskins jgaskins referenced this pull request Jan 10, 2016

Closed

Roadmap to 1.0 #33

7 of 7 tasks complete

@jgaskins jgaskins merged commit 32c4a44 into master Apr 20, 2016

2 checks passed

continuous-integration/travis-ci/pr The Travis CI build passed
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details

@jgaskins jgaskins deleted the server-render branch Apr 20, 2016

@jgaskins jgaskins changed the title from [DON'T MERGE YET] Server rendering to Server rendering Aug 13, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment