Assume we are implementing a module which has its own localization files (.properties)
We need to add a new file loader in webpack.config.js
Then we will get the same file name in output folder (/dist), and so far we can include that in index.html because mozL10n needs to parse the document.head to know about the localization files.
Once we import a new module which is having localization files, we need to add all the localization files into index.html. It seems redundant. We should make webpack be able to generate total properties just like ExtractTextPlugin.
I am designing a transition component which only cares about open/close method from its parent and update the class + data-transition-state on its own element. Let's call it XWindow, and its controller is XWindowManager
How to avoid re-rendering Inbox when Thread is opened?
Usally, whenever the top most component is updated by setState, every sub component will be updated, too. React won't perform the real DOM operation if it founds the newly Virtual DOM is the same as the existing Virtual DOM. However, I just don't want any sub component to even compare the Virtual DOM nor to compare the next state and the current state. It's really unneccessary.
How about to tell the children please don't update during the transition?
This just solves part of the problem. Whenever a XWindow is closed and then opened again, the subcomponent of the XWindow will be updated.
Also, it makes the implementation of the child component become dirty: needs to know the parent's transition state to do the update instead of only its own state.
Use CSSTransitionGroup in React.addon
React has a CSSTransitionGroup component to help us to do the transition; however, it does not solve this problem as well. Every children in CSSTransitionGroup will be asked to update once a new children is added.
Final simple answer: Create a Gap Component Between Transition Component and Content!
The solution I come up with is pretty simple and make thing really clear:
to implement a gap component which will always not update.
Then we just need to insert the gap component between XWindow and the content:
Now the XWindow and the content is totally separated. No matter how we transition the XWindow, the content won't be asked to update anymore. It could live with its own state.
Each module has an initing phase, and the phase might be asynchoronous because you want to do something asynchronously. For example, a module may need to read some configuration from db to set up the environment; we will say this module is not ready yet if it's still in the init phase.
Service is playing the role of message queuing between modules
We call this 'Durable subscriber' in the messaging pattern.
That is, event dispatching will be lost if the subscriber is not ready when the dispatcher loads before the subscriber. To avoid this, we need a third module to queue the request of the dispatcher before the subscriber is able to register to the requests.
How to know the application is fully loaded if we decide to delay something?
A rough idea here is let every module to notify the centralized controller when it starts.
But this does not make sense because we need to maintain a list of all the modules in the centralized controller.
A promise chain of start operations
A module may have some sub modules to load and start in the init phase. If we connect all the start function using Promise, we could have a simple start function call from the application root and simply knows when all the sub modules are started.
Tricky: Define critical modules and side modules but keep the start promise
For reason of performance we don't want everything to be loaded together in startup. That is to say, we need to figure out what's the critical module and what's the side modules.
The loadWhenIdle function call will not being resolved once the consumer of 'schedule' Service tell it now is time to do your operation. So now we need a new module to schedule the non critical operations. It should know the timing that all critical modules are loaded and started, and then execute the queued requests.
Whenever a new feature in system is done, find somewhere in the header to insert the new scripts.
If it needs an entry point, put it somewhere in bootstrap.js
Leave it if the ordering of the module loaded does not cause error.
If there is an error? Re-arrange the scripts until the error disappears.
The result is the scripts being added grows unbelievably long and the device boot time is slower and slower.
To improve this
Find out the critical launch path - what is necessary to boot into homescreen or FTU or lockscreen?
Leave every module else as optional and find out a good timing to load them after entering homescreen.
Build dependency tree to make sure dynamic load won't cause race.
Critical launch path
The first app to be launched on a phone is either 'First Time Usage' or 'Homescreen'. To decide which to launch, we need to read several asynchronous values:
homescreen.manifestURL from settings db
ftu.enabled from async storage
In the past most of the value fetching is done in FtuLauncher module; however, it's running at the same time as other non-critical scripts are busy running.
To refine this, we removed most scripts from the header, and only let one module named for launcher to get these values at the startup, to make sure all of the boot preference are done before the real bootup procedure.
Dependency Tree Architecture
After launcher decides what to do, it would launch the system core.
The task of system core is simple: it will launch every subsystem which is responsible to certain hardware API. Every subsystem has a core, too, to load all its dependency modules to make sure we control the hardware well.
The hardware subsystem is expected to be independent running from all other concurrent subsystems. Each subsystem has a core as well, which plays as a "smart" loader and decides the priority to load modules, also sometimes cooperate the child modules if necessary.
Each subsystem is expected to be independent from other subsystems.
However, sometimes we still need to query or request other modules across subsystems;
'Service' is created to serve as the mediator between all modules who needs other's help.
Hierarchy Manager is designed to resolve these problems:
Some UI events are "top most only"
When topmost UI are changed, we need to inform other UI for that.
Some UI events are "radiolucent"
Before we could lazily load every modules, the biggest problem is several UI components are listening to some UI events by the ordering of DOM event registration.
The problem is this kind of event priorization implementation needs to start the module event registration in certain ordering. This makes lazy load difficult because the ordering of loading scripts are not guranteed.
The proper fix is to have a mediator to deal with all UI events priority according to a predefined hierarchy map. With that, an UI module don't need to know the existence of other UI modules, but instead it would be noticed once there is a higher priority one activated/deactivated.
One key point is, without knowing each other, an UI module could be safely started/stopped to join the hierarchy competition, which also means it's safe to remove unwanted UI module or add new UI module somedays, or for certain device type.
During the design of the new architecure, I found that we almost have the same pattern of the behavior of a module:
rendering if it's an UI module
To group all these patterns together, BaseModule is designed to support them by configuration on the constructor. Each part could be swapped out if necessary. For example, event publish/subscribe is ready to be switched to pubsub library or any other event registration library if necessary.
SettingsCore is implemented to replace all navigator.mozSettings libraries in the current system to provide a more generic interface to observe the settings. Moreover, an settings should be only observed by a certain settings manager module, and it should notify others when it observes the settings is changed instead of letting every module to observe the same settings at the same time. It's similar to put a cache between settings db and the observer to make sure the getter is getting the value without doing the asynchorous operation each time it needs the value.
Event is an important part to decouple modules; currently we are using DOM events to dispatch the event and mostly on the window object. With a centralized event interface it's possible to switch the DOM event into any other 3rd party event library. This is especially important if we are migrating the UI-less modules into ServiceWorker because we cannot access DOM there.
The ideal world is everything could be started automatically and don't need to care others; however, in real world we need to make sure something is ready before we really init a module.
The paring start/stop's work is to
Register/un-register pre-defined events in start/stop
Observe/de-observe pre-defined settings db keys in start/stop
load+start/stop predefined child modules in start/stop
(optional) Render DOM elements if necessary
Future work: supprting multi-device
To achieve the goal: 'one system to serve multiple devices', there are two solutions
Build time solution: define build flags and inject/generate modules according to the config.
Run time solution:
However, it might be insufficient to only use 1. for the second display case
To support the remote display which might has different dimension, we still need to have the ability to load a different UI set at run time.
Ideally, the goal is to split UI-less modules and UI modules; each core in the subsystem is expected to load the UI-less part then the UI part per device type or the request from the remote display; it's still vague at this point, but IMO we have two ways to deal with multiple devices:
Centralized render method per configs
If everyone is using BaseUI/BaseModule, it's possible to have the render method and the view method being managed by a ViewManager will will load view for phone or view for tablet accordingly.
Making each module less dependent from others if possible.
Only parent is responsible to load/start/stop child in the timing being designed.
The launching timing of non-blocking modules should not affect each other. A.k.a., the ordering does not matter and everything will work if any new module is started/stopped. Requests before an module which provides the service would be queued until it is started.
Generic interface for UI-less module(BaseModule) and UI module(BaseUI).
The bigest change in new window management system is to deprecate the usage of module pattern. In my opinion, the main reason to use module pattern is to enable private variables. But this is a strong requirement, and the drawback it brings to us is much more than the value of private variables and functions.
The main problem is the private variables/method inside module pattern are not that easy to test. Yes, you could still work hard to figure out all the code path to trigger the inner function, but that is in-efficient and a simple function in object literal saves your time.
A mixin is another object/class which is "dumped" into current class. The object could be standalone, swapped in the future, or be dumped into another class.
The way how we dump is simple: iterate the mixin object and append it to the target prototype.
If you look at the Mixin Pattern sample closer, you will find browser_mixin.js is also a sample of proxy pattern. What is a proxy? It creates one more layer to access the mozBrowserAPI: setVisible, getScreenshot, reload... on the mozBrowser iframe. The mediator of AppWindow should not call these API directly but encouraged to call the proxy. The proxy will protect the API at certain degree.
Exactly, there's no strict factory pattern.
Whenever gecko/platform asks us to open a new window, we will get an event. AppWindowFactory will instantiate a proper AppWindow instance from the event detail,
or bypass the event to the proper AppWindow's childWindowFactory.
I am not using a general state machine but having a specific transition state machine to deal with the open/close request on the AppWindow object.
A regular transition could be defined as 4 states: closed, opening, opened, closing.
What we care:
State change needs to tell others.
We are using animationend event to switch from opening state to opened state; and from closing state to closed state.
But sometimes, we will lose the animationend event. We cannot wait infinitely in opening state and in closing state. We need to create timeout event to force the state change.
We are bypassing the opening and closing states if the open/close animation is requested as 'immediate'.
AppWindowManager is the mediator of all AppWindow instances.
All AppWindow don’t need to know each other when they are trying to do UI operation.
If the operation affects the other classes, they should publish a request. Mediator will check for them.
A real use case is, when an instance requests to open, we need to animate current displayed instance. But according to the rule "module should not know each other's state", we(AppWindow) are not supposed to do close operation to other instance and we don't even know who is currently opened instance.
AppWindowManager(AppWindowMediator) should deal with the open request and make sure the current opened instance is closing.
The observer pattern is highly used to prevent tight coupling. However, we are having a more complex event machenism right now. Let's step by step look into it.
We are using DOM events to realize the publish/subscribe pattern right now.
The subscriber don't need to know the DOM element to attach the event listener because the event will be bubbling to the window.
The publisher is encourged to have an event prefix to represent itself. Usually it's the class name.
Also note the event detail including the whole object so we could access any information we want in the subscriber, even calling the method.
2 phase observer
AppWindow is not only a simple module but also plays as a mediator of all sub modules inside it.
The event processing is indeed including two phases:
Private event phase - module inside the AppWindow will notice the private event.
Public event phase - module outside the AppWindow will notice the public event after all private events are resolved.
Let's go through the sample.
The _opened subcriber is handled before appopened subsriber.
This is to ensure the UI consistency:
When we want to tell others something happens, we need to settle everything corresponding to the event before going public.
When something happens but it's not public enough, use broadcast() to tell the modules which are managed by us. It would be dispatched on this.element.
The underline before private event is a convension. Internally we don't need a class-like prefix.
To understand the two phase observer, let's treat AppWindow as the local government, and the mediator of AppWindow is the central government.
The central government will know something happens in local government in the long run, but the local government is responsible for manipulating the events at first. In the local government there are many different departments which have different responsibilities. Some of them might be interested in the same thing but has different behavior when they know the event happens.
Mediator as Observer
For some honored reason, AppWindow is playing the role of Mediator as its opened window now.
That is to say, if an app is using window.open(), the newly created AppWindow instance is managed by the caller instance.
But note we are using DOM events to pass the events, if we don't do something, the observer of the outer instance will be invoked if the inner instance triggers some event.