Last time we traced the steps that Nitrogen takes to initialize the Context and reached wf_core:run_catched function which builds the Response.
run_catched() -> % Get the handlers from querystring, if they exist... deserialize_context(), % Initialize all handlers... call_init_on_handlers(), % Deserialize the event if available... wf_event:update_context_with_event(), % TODO - Check for access % Call the module... case wf_context:type() of first_request -> run_first_request(), finish_dynamic_request(); postback_request -> run_postback_request(), finish_dynamic_request(); static_file -> finish_static_request() end.
Firstly, this function updates the Context with the page State which comes from the browser in the http Request in serialized form.
deserialize_context() -> RequestBridge = wf_context:request_bridge(), Params = RequestBridge:post_params(), % Save the old handles... OldHandlers = wf_context:handlers(), % Deserialize page_context and handler_list if available... SerializedPageContext = proplists:get_value("pageContext", Params), [Page, Handlers] = case SerializedPageContext of undefined -> [wf_context:page_context(), wf_context:handlers()]; Other -> wf_pickle:depickle(Other) end, % Config is not serialized, so copy config from old handler list to new % handler list. Handlers1 = copy_handler_config(OldHandlers, Handlers), % Create a new context... wf_context:page_context(Page), wf_context:handlers(Handlers1), % Return the new context... ok.
deserialize_context() uses RequestBridge:post_params() which knows how to read and parse the http request body.
RequestBridge uses cowboy_request_bridge.erl which implements interface specified by parametrized module (we discussed it here).
The APIs of cowboy_request_bridge.erl are mostly wrappers around functions in cowboy_http_req.erl module which allow to get http headers, body, url etc, from the http request.
Due to the fact that Cowboy tries to optimize performance and memory consumption it allows to read the request body only once, which means that cowboy_request_bridge.erl has to cache it when it reads it the first time and caches the copy in process dictionary.
The problem though is that cowboy_request_bridge.erl uses now() to generate unique cache key:
new_key() -> {cowboy_bridge,now()}.
now() is the VM blocking call so using it for key generation on a busy website might be a performance hit but this bit could be changed, it is not too big of a problem.
here is the example of parsed request body :
[{<<"pageContext">>, <<"OvC5X4NQAAADpXichZJNTsMwEIWngUhAC63Ej8qSFVuEQD0HJ7AcexI7OHawHam9AHtujCORNn_IO-s9v2_mWRaJAoBEpBxWNS2QMKM97v0nXHms6rfd--vuhUMqNcd9GQ7MVOjbzLLNrAXVXKHtYhxuwimXB fkzODxwzGmjPBkbl01g5lIjL-dRS2V61287zkCNQra1NQydIxYL6bw9nLKPuvWCPuNFudeMMoGnwP2x5lCPg74a7E8-gkZ6FLR2oaY0uvf0Tla1QjIx4ks5T_1cu5F-ArVfYiGSIAnqSK6oExzOvW3wnxEbyVF76XsVt92UqRX deGWN6i1216GGcry4Nc2w-EHTSjIy0qOgjUPW2Pl6U6uHK0txlgGkHxlc_HxnsHh6_gWHuEr0">>}, {<<"wfid_temp246765.wfid_temp394681.wfid_temp394715.wfid_west.wfid_temp824755. wfid_west.wfid_temp824793.wfid_control_panel.wfid_temp824826.wfid_btn_disable">>, <<"Disable All tabs">>}, {<<"wfid_temp246765.wfid_temp394681.wfid_temp394715.wfid_west.wfid_temp824755. wfid_west.wfid_temp824793.wfid_control_panel.wfid_temp860654.wfid_btn_disable1">>, <<"Disable Some tabs">>}, {<<"eventContext">>, <<"0IIymINQAAAAiXicy2DKYEth4E0tS80riU_OzytJrShJYeBJzUnNBYm UJCYVZzClMAiAGPGpFYm5BTmpQNF0kCALSDCFgQNE5eQnpqQwcJbmpaSmZealpmQzCOiVp2W mxJek5hZYWFqYGhlmM3BBhUCGMicxMLAGJjFwTGxNYuATiQQARMgrcQ">>}]
notice that it contains the keys for “pageContext” and “eventContext” with pickled values and also nested hierarchy of control IDs.
and here is how the this pageContext looks like after it was deserialized with wf_pickle:depickle() function:
{page_context,"temp475270",index,[],comet} ; [{handler_context,config_handler,default_config_handler,undefined,[]}, {handler_context,log_handler,default_log_handler,undefined,[]}, {handler_context,process_registry_handler,nprocreg_registry_handler,undefined,[]}, {handler_context,cache_handler,default_cache_handler,undefined,[]}, {handler_context,query_handler,default_query_handler,undefined,[]}, {handler_context,session_handler,simple_session_handler,undefined,[]}, {handler_context,state_handler,default_state_handler,undefined,[{has_flash,true}]}, {handler_context,identity_handler,default_identity_handler,undefined,[]}, {handler_context,role_handler,default_role_handler,undefined,[]}, {handler_context,route_handler,dynamic_route_handler,undefined,[]}, {handler_context,security_handler,default_security_handler,undefined,[]}]
here we have un-pickled page_context and a list of Handlers.
Nitrogen then adds page_context to the Context with:
wf_context:page_context(Page)
and then applies the state of the handlers to the Context with:
call_init_on_handlers()
and also deserialises Event information found in the request body with the following code:
update_context_with_event() -> SerializedEvent = wf:q(eventContext), Event = wf_pickle:depickle(SerializedEvent), % Update the Context... PageModule = wf_context:page_module(), IsPostback = is_record(Event, event_context), case {PageModule, IsPostback} of {static_file, _} -> update_context_for_static_file(); {_, false} -> update_context_for_first_request(); {_, true} -> update_context_for_postback_request(Event) end.
This code determines the type of request and saves this type into Context:
- static_file (if the request was of a file with any extension)
- first_request – dynamic request
- postback_request
and finally, depending on the type of request we run the functions to generate dynamic html, process postback or return a static file:
% Call the module... case wf_context:type() of first_request -> run_first_request(), finish_dynamic_request(); postback_request -> run_postback_request(), finish_dynamic_request(); static_file -> finish_static_request() end.
if, for example, we did the first request to “\” which will be mapped to “index” – the type of request will be first_request and when we reach run_first_request() function it will finally call main() function of the erlang module which implements the page (e.g. index.erl)
run_first_request() -> % Some values... Module = wf_context:event_module(), {module, Module} = code:ensure_loaded(Module), Data = Module:main(), wf_context:data(Data).
and this is where we finally reach the step which is described in Nitrogen introductory manual, here we call Module:main() which calls the page template:
-module(index). -include_lib("nitrogen_elements/include/nitrogen_elements.hrl"). -compile(export_all). main(); #template { file=filename:join([web_common:templates(), "onecolumn.html"]) }.
template “onecolumn.html” has placeholders like [[[page:title()]]] and [[[page:body()]]]
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Nitrogen - [[[page:title()]]]</title> </head> <body> [[[page:body()]]] <script> [[[script]]] </script> </div> </body> </html>
and Nitrogen will call corresponding functions from index.erl module and substitute the placeholders with html generated by these calls.
Final thoughts:
Well, this was quite tedious process and it is probably pain to read. But, hopefully, this will answer some of questions about internal working of Nitrogen. (writing this surely helped me to understand some of the Nitrogen design decisions).
I honestly think that Nitrogen can’t be used effectively until you have some rough ideas about what is going on in its guts.
Nitrogen is complex but elegant framework and what we have discussed so far is just the part of the story – we only briefly touched on Erlang side of things.
There is also the second part of the puzzle which is happening on the browser side and implemented by nitrogen.js module. This is javascript library which is responsible for postbacks generation (via ajax), communicating with jQuery, events handling, page_context serialization, objects lookups and many other things. Maybe we will discuss all this in one of the future posts.