Skip to content
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

Feature/node #380

Merged
merged 1 commit into from
May 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,4 @@ script:
notifications:
slack:
secure: LfcUk4AJ4vAxWwRIyw4tFh8QNbYefMwfG/oLfsN3CdRMWMOtCOHR1GGsRhAOlfVVJ/FvHqVqWj5gK7z7CaO5Uvl7rD3/zJ8QzExKx/iH9yWj55iIPuKLzwFNnBwRpFW/cqyU2lFPPRxGD50BUn3c+qybkuSqtKZ6qtTowwqlxLa5iyM3N95aZp7MEIKCP7cPcnHfLbJyP8wBpotp/rtw62eXM2HIRJJwgjcp+n+My7VFR9DnBXNFf6R91aZHM4U4cHHDbu15HFtH8honVrzK1JQdyqMNHga+j04dFuaS7z9Q369/hsELMOBp/227+Pz7ZRfWZFK4UASguOvyeX7RmGTRpTuWLm1XJeUzfsPZVROecaSVQBve+U7F12yKqilt97QlvRXn2EGyBILqvxtFNNR4S9kgAf72/6EFgiM1TKq7i9zy6lVOnagU2+7amq7UeopX1uoFsUfNKMR7YbgV1WjF0IK95UP0b0/7ZOJlPYgi5zzkQi129qAFWSMmxGk+ZpsttHh/tjJtvAh0A3mHq/zb5w4ub/MbSyZqeDUNgGj72QArOWUFSAStQT1ybsVLeDoKPgOvVq7OV1D64rpcHjBXcqOCit8tDZ+TqkFhcYJo2cITSaqE4zJXn+4F5s7So5O8CyfKYQq+kFJCooYGmfgTUckJpGl7eIvKmL4TN9Q=

1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Contributors: please follow the recommendations outlined at [keepachangelog.com]
## [5.2.0] - 2016-04-08
##### Added
- Support for React 15.0 to react_on_rails. See [#379](https://github.com/shakacode/react_on_rails/pull/379) by [brucek](https://github.com/brucek).
- Support for Node.js server side rendering. See [#380](https://github.com/shakacode/react_on_rails/pull/380) by [alleycat](https://github.com/alleycat-at-git) and [doc](https://github.com/shakacode/react_on_rails/blob/master/docs/additional-reading/node-server-rendering.md)

##### Removed
- Generator removals to simplify installer. See [#363](https://github.com/shakacode/react_on_rails/pull/363) by [jbhatab](https://github.com/jbhatab).
Expand Down
17 changes: 17 additions & 0 deletions docs/additional-reading/node-server-rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Node Server Rendering

### Warning: this is an experimental feature

The default server rendering exploits ExecJS to render react components.
Node server rendering allows you to use separate NodeJS process as a renderer. The process loads server-bundle.js and
then executes javascript to render the component inside its environment. The communication between rails and node occurs
via socket (`client/node/node.sock`)

### Getting started

To use node process just set `server_render_method = "NodeJS"` in `config/initializers/react_on_rails.rb`. To change back
to ExecJS set `server_render_method = "ExecJS"`

### Configuration

To change the name of server bundle adjust npm start script in `client/node/package.json`
6 changes: 5 additions & 1 deletion lib/generators/react_on_rails/base_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,13 @@ def install_server_rendering_files_if_enabled
return unless options.server_rendering?
base_path = "base/server_rendering/"
%w(client/webpack.server.rails.config.js
client/app/bundles/HelloWorld/startup/serverRegistration.jsx).each do |file|
client/app/bundles/HelloWorld/startup/serverRegistration.jsx
client/node/package.json
client/node/server.js).each do |file|
copy_file(base_path + file, file)
end

copy_file("base/base/lib/tasks/load_test.rake", "lib/tasks/load_test.rake")
end

def template_assets_rake_file
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
web: rails s
client: sh -c 'rm app/assets/webpack/* || true && cd client && npm run build:dev:client'
<%- if options.server_rendering? %>server: sh -c 'cd client && npm run build:dev:server'<%- end %>
<%- if options.server_rendering? %>
server: sh -c 'cd client && npm run build:dev:server'
node: sh -c 'cd client/node && npm start'
<%- end %>
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ ReactOnRails.configure do |config|
# For server rendering. This can be set to false so that server side messages are discarded.
# Default is true. Be cautious about turning this off.
config.replay_console = true
# Default is true. Logs server rendering messags to Rails.logger.info
# Default is true. Logs server rendering messages to Rails.logger.info
config.logging_on_server = true

# The following options can be overriden by passing to the helper method:
Expand All @@ -40,4 +40,7 @@ ReactOnRails.configure do |config|
config.trace = Rails.env.development?
# Default is false, enable if your content security policy doesn't include `style-src: 'unsafe-inline'`
config.skip_display_none = false

# The server render method - either ExecJS or NodeJS
config.server_render_method = "ExecJS"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace :load_test do
desc "Load test with apache benchmark"
task :run, [:url, :count] do |_, args|
url = args[:url] || "http://localhost:3000/hello_world"
count = args[:count] || 500
system("ab -c 10 -n #{count} #{url}")
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "react_on_rails_node",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./server.js -s server-bundle.js"
},
"dependencies": {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
var net = require('net');
var fs = require('fs');

var bundlePath = '../../app/assets/webpack/';
var bundleFileName = 'server-bundle.js';

var currentArg;

function Handler() {
this.queue = [];
this.initialized = false;
}

Handler.prototype.handle = function (connection) {
var callback = function () {
connection.setEncoding('utf8');
connection.on('data', (data)=> {
console.log('Processing request: ' + data);
var result = eval(data);
connection.write(result);
});
};

if (this.initialized) {
callback();
} else {
this.queue.push(callback);
}
};

Handler.prototype.initialize = function () {
console.log('Processing ' + this.queue.length + ' pending requests');
var callback;
while (callback = this.queue.pop()) {
callback();
}

this.initialized = true;
};

var handler = new Handler();

process.argv.forEach((val) => {
if (val[0] == '-') {
currentArg = val.slice(1);
return;
}

if (currentArg == 's') {
bundleFileName = val;
}
});

try {
fs.mkdirSync(bundlePath);
} catch (e) {
if (e.code != 'EEXIST') throw e;
}

fs.watchFile(bundlePath + bundleFileName, (curr) => {
if (curr && curr.blocks && curr.blocks > 0) {
if (handler.initialized) {
console.log('Reloading server bundle must be implemented by restarting the node process!');
return;
}

require(bundlePath + bundleFileName);
console.log('Loaded server bundle: ' + bundlePath + bundleFileName);
handler.initialize();
}
});

var unixServer = net.createServer(function (connection) {
handler.handle(connection);
});

unixServer.listen('node.sock');

process.on('SIGINT', () => {
unixServer.close();
process.exit();
});
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
RSpec.configure do |config|
# Ensure that if we are running js tests, we are using latest webpack assets
# This will use the defaults of :js and :server_rendering meta tags
ReactOnRails::TestHelper.launch_node if ReactOnRails.configuration.server_render_method == "NodeJS"
ReactOnRails::TestHelper.configure_rspec_to_compile_assets(config)

# Remove this line if you"re not using ActiveRecord or ActiveRecord fixtures
Expand Down
1 change: 1 addition & 0 deletions lib/react_on_rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
require "react_on_rails/test_helper/webpack_assets_status_checker"
require "react_on_rails/test_helper/webpack_process_checker"
require "react_on_rails/test_helper/ensure_assets_compiled"
require "react_on_rails/test_helper/node_process_launcher"
9 changes: 6 additions & 3 deletions lib/react_on_rails/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ def self.configuration
server_renderer_timeout: 20,
skip_display_none: false,
webpack_generated_files: [],
rendering_extension: nil
rendering_extension: nil,
server_render_method: ""
)
end

Expand All @@ -66,15 +67,15 @@ class Configuration
:logging_on_server, :server_renderer_pool_size,
:server_renderer_timeout, :raise_on_prerender_error,
:skip_display_none, :generated_assets_dirs, :generated_assets_dir,
:webpack_generated_files, :rendering_extension
:webpack_generated_files, :rendering_extension, :server_render_method

def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,
trace: nil, development_mode: nil,
logging_on_server: nil, server_renderer_pool_size: nil,
server_renderer_timeout: nil, raise_on_prerender_error: nil,
skip_display_none: nil, generated_assets_dirs: nil,
generated_assets_dir: nil, webpack_generated_files: nil,
rendering_extension: nil)
rendering_extension: nil, server_render_method: nil)
self.server_bundle_js_file = server_bundle_js_file
self.generated_assets_dirs = generated_assets_dirs
self.generated_assets_dir = generated_assets_dir
Expand All @@ -97,6 +98,8 @@ def initialize(server_bundle_js_file: nil, prerender: nil, replay_console: nil,

self.webpack_generated_files = webpack_generated_files
self.rendering_extension = rendering_extension

self.server_render_method = server_render_method
end
end
end
160 changes: 9 additions & 151 deletions lib/react_on_rails/server_rendering_pool.rb
Original file line number Diff line number Diff line change
@@ -1,165 +1,23 @@
require "connection_pool"
require_relative "server_rendering_pool/exec"
require_relative "server_rendering_pool/node"

# Based on the react-rails gem.
# None of these methods should be called directly.
# See app/helpers/react_on_rails_helper.rb
module ReactOnRails
class ServerRenderingPool
def self.reset_pool
options = { size: ReactOnRails.configuration.server_renderer_pool_size,
timeout: ReactOnRails.configuration.server_renderer_timeout }
@js_context_pool = ConnectionPool.new(options) { create_js_context }
end

def self.reset_pool_if_server_bundle_was_modified
return unless ReactOnRails.configuration.development_mode
file_mtime = File.mtime(ReactOnRails::Utils.default_server_bundle_js_file_path)
@server_bundle_timestamp ||= file_mtime
return if @server_bundle_timestamp == file_mtime
ReactOnRails::ServerRenderingPool.reset_pool
@server_bundle_timestamp = file_mtime
end

# js_code: JavaScript expression that returns a string.
# Returns a Hash:
# html: string of HTML for direct insertion on the page by evaluating js_code
# consoleReplayScript: script for replaying console
# hasErrors: true if server rendering errors
# Note, js_code does not have to be based on React.
# js_code MUST RETURN json stringify Object
# Calling code will probably call 'html_safe' on return value before rendering to the view.
def self.server_render_js_with_console_logging(js_code)
if trace_react_on_rails?
@file_index ||= 1
trace_messsage(js_code, "tmp/server-generated-#{@file_index % 10}.js")
@file_index += 1
end
json_string = eval_js(js_code)
result = JSON.parse(json_string)

if ReactOnRails.configuration.logging_on_server
console_script = result["consoleReplayScript"]
console_script_lines = console_script.split("\n")
console_script_lines = console_script_lines[2..-2]
re = /console\.log\.apply\(console, \["\[SERVER\] (?<msg>.*)"\]\);/
if console_script_lines
console_script_lines.each do |line|
match = re.match(line)
Rails.logger.info { "[react_on_rails] #{match[:msg]}" } if match
end
end
end
result
end

module ServerRenderingPool
class << self
private

def trace_messsage(js_code, file_name = "tmp/server-generated.js", force = false)
return unless trace_react_on_rails? || force
# Set to anything to print generated code.
puts "Z" * 80
puts "react_renderer.rb: 92"
puts "wrote file #{file_name}"
File.write(file_name, js_code)
puts "Z" * 80
end

def trace_react_on_rails?
ENV["TRACE_REACT_ON_RAILS"].present?
end

def eval_js(js_code)
@js_context_pool.with do |js_context|
result = js_context.eval(js_code)
js_context.eval("console.history = []")
result
end
end

def create_js_context
server_js_file = ReactOnRails::Utils.default_server_bundle_js_file_path
if server_js_file.present? && File.file?(server_js_file)
bundle_js_code = File.read(server_js_file)
base_js_code = <<-JS
#{console_polyfill}
#{execjs_timer_polyfills}
#{bundle_js_code};
JS
file_name = "tmp/base_js_code.js"
begin
trace_messsage(base_js_code, file_name)
ExecJS.compile(base_js_code)
rescue => e
msg = "ERROR when compiling base_js_code! "\
"See file #{file_name} to "\
"correlate line numbers of error. Error is\n\n#{e.message}"\
"\n\n#{e.backtrace.join("\n")}"
puts msg
Rails.logger.error(msg)
trace_messsage(base_js_code, file_name, true)
raise e
end
else
if server_js_file.present?
msg = "You specified server rendering JS file: #{server_js_file}, but it cannot be "\
"read. You may set the server_bundle_js_file in your configuration to be \"\" to "\
"avoid this warning"
Rails.logger.warn msg
puts msg
end
ExecJS.compile("")
end
end

def execjs_timer_polyfills
<<-JS
function getStackTrace () {
var stack;
try {
throw new Error('');
}
catch (error) {
stack = error.stack || '';
}
stack = stack.split('\\n').map(function (line) { return line.trim(); });
return stack.splice(stack[0] == 'Error' ? 2 : 1);
}

function setInterval() {
#{undefined_for_exec_js_logging('setInterval')}
}

function setTimeout() {
#{undefined_for_exec_js_logging('setTimeout')}
}
JS
end

def undefined_for_exec_js_logging(function_name)
if trace_react_on_rails?
"console.error('#{function_name} is not defined for execJS. See "\
"https://github.com/sstephenson/execjs#faq. Note babel-polyfill may call this.');\n"\
" console.error(getStackTrace().join('\\n'));"
def pool
if ReactOnRails.configuration.server_render_method == "NodeJS"
ServerRenderingPool::Node
else
""
ServerRenderingPool::Exec
end
end

# Reimplement console methods for replaying on the client
def console_polyfill
<<-JS
var console = { history: [] };
['error', 'log', 'info', 'warn'].forEach(function (level) {
console[level] = function () {
var argArray = Array.prototype.slice.call(arguments);
if (argArray.length > 0) {
argArray[0] = '[SERVER] ' + argArray[0];
}
console.history.push({level: level, arguments: argArray});
};
});
JS
def method_missing(sym, *args, &block)
pool.send sym, *args, &block
end
end
end
Expand Down
Loading