JavaScript: Developing a Custom Framework for Single-Page-Apps
SPAC is a custom JavaScript framework for client-side, single-page web applications. It stands for "Stateful Pages, Actions and Components". Its design goal is to provide robust and simple entities that help you to structure apps. Pages and components provide the HTML, JavaScript functions and UI interactions. Actions govern external API calls. You define these entities in plain JavaScript, load up the central controller, and your app is ready to be served. Read the development journey of SPAC in my series: https://admantium.com/category/spac-framework/.
This article introduces the SPAC framework. Before we dive into the design of the framework itself, we will briefly touch upon how JavaScript is loaded in your browser - this understanding is the foundation how to structure your code. Read along and get some ideas and inspirations how to make PlainJS projects more maintainable.
Essentials: JavaScript in your Browser
In your browser, each tab opens a new browser session. And for each session, a new thread with a JavaScript interpreter is started. This interpreter is invoked by the browser during HTML processing whenever it is instructed to execute JavaScript.
As a developer, you have different options to load JavaScript - and they all behave a bit different.
- Load JavaScript file with the
<script src="">
tag.- The browser stops loading any other resource. It will execute all code in the context of the
global
object. Variable declaration will happen in this global space.
- The browser stops loading any other resource. It will execute all code in the context of the
- Define inline JavaScript with `tag
- The browser stops loading any other resource. The code can access all variables defined in the global scope. It is not possible to either load additional modules, or to declare modules that can be imported with statements in other
<script>
tags. It will execute all code in the context of theglobal
object. Variable declaration will happen in this global space.
- The browser stops loading any other resource. The code can access all variables defined in the global scope. It is not possible to either load additional modules, or to declare modules that can be imported with statements in other
- Register inline event listener on input elements, like
<button onclick=parseData>
- The browser will define an event listener for the DOM object by the given function name. In JavaScript, function definitions in the
global
namespace will be hoisted up, which means you can use a function name before its declaration. However, the browser also happily allows aundefined
function to be used in this context - this can result in hard to figure out bugs.
- The browser will define an event listener for the DOM object by the given function name. In JavaScript, function definitions in the
- Load JavaScript modules with the
<script src="" type="module">
tag- The browser stops loading any other resource. It will execute all code in the context of the
global
object, but allow the definition and loading of modules.
- The browser stops loading any other resource. It will execute all code in the context of the
Depending which methods you use, different challenges need to be considered:
- Page load interrupt: Some methods will stop the browser from loading any additional resources before the JavaScript is parsed completely. If you load either very complex code or a lot of code, this might interrupt the page load speed
- Execution context pileup: When you constantly load new scripts from newly rendered pages, the total amount of JavaScript inside the browser thread continues to pile up and can slow down the page performance
- Namespace pollution: Inside the browser, the
global
object will bewindow
. Any JavaScript that is executed can change the definition of thewindow
object. It can happen that you accidentally overwrite function definitions when scripts on different pages use the same function names, because they will be re-defined the global object.
With this knowledge, we can now design the essential requirements of our custom framework.
Architecture of the Custom Framework
The custom frameworks needs to consider the above-mentioned challenges as well as adhering to the principle separation of concerns. Its architecture is influenced by the model-view-controller pattern and uses concepts similar as in React.
In a nutshell, the requirements are:
- Use JavaScript modules to keep the namespace clear
- Separate the application into the controller, action, and pages & components
- Encapsulate HTML and JavaScript in the relevant components
- Dynamically load and execute only required JavaScript
Let’s consider the central building blocks of the framework one-by-one.
JavaScript Modules
First of all, all entities of the framework are defined as modules. Using modules enables the application to expose only required functions for each entity, which can be considered as an interface. This interface helps to standardize and to make the entities compatible with each other.
Controller
The controller
is the central entity of the framework and the only JavaScript that will be loaded to the application. It provides the complete functionality to control which pages are rendered and loads the required JavaScript. Furthermore, it is responsible to keep the applications state and to communicate with any external API. Finally, it also serves as a gateway by importing and exposing shared JavaScript functions that are exposed by other entities.
Actions
When your application needs to connect to an external API, you will be using actions. Actions are JavaScript promises that execute API interactions and deliver the results. The action caller, a component or page, then defines how to process the results, like updating the state or refreshing the HTML.
Pages and Components
Composing the presentation and UI functions is the task of pages
and components
. The controller requests to load a page by calling it with a root DOM element and passing the state. Then, the page creates its own DOM elements, attaches them to the root DOM, and also executes additional JavaScript. Afterwards, it loads all the components that are present on the page.
Components work similar to pages: They also receive a root DOM and the state. They build their own DOM and attach JavaScript to it. The difference is that they provide additional helper functions that are specific to this component, complex UI functions or even functions that operate on the state.
State
The state is the globally available and persistent data of the application. Everything from user input to application operational data is kept inside the state. Between page refresh, data is persisted inside the user’s browser storage. Logically, each active page holds the state, and passes its’ state to the components. The page can call methods in the controller to persist the state in other stores, such as databases like MongoDB.
Conclusion
The custom JavaScript framework is a generic approach to structure client-side applications that need to provide complex UI interactions. It is persistent in its abstractions and consistent in dividing the concerns of a web application. Read more about this in the next article.
Previous: ApiBlaze: Development Phases