The PDF version of PPT download address: http://www.slideshare.net/jibyjohnc/jqquerysummit-largescale-javascript-application-architecture
Note: During the collation process, I found that the author’s thoughts were repeated, so he deleted some of them. If your English is good, please read the English PPT directly.
The following are the main chapters of this article:
1. What is "JavaScript Large Program"?
2. Consider the current program architecture
3. Long-term consideration
4. Brainstorm
5. Suggested architecture
5.1 Design Pattern
5.1.1 Module Theory
5.1.1.1 Overview
5.1.1.2 Module mode
5.1.1.3 Object self-face size
5.1.1.4 CommonJS module
5.1.2 Facade mode
5.1.3 Mediator mode
5.2 Apply to your architecture
5.2.1 Facade - Core Abstraction
5.2.2 Mediator - Program Core
5.2.3 Closely working
6. Publish Pub/Sub Extension: Automatic Registration Events
7. Q & A
8. Acknowledgements
What is "JavaScript Large Program"?
Before we start, let’s define what a large JavaScript site is. Many experienced JS development experts have also been challenged. Some people say that more than 100,000 lines of JavaScript code are considered large, and some people say that JavaScript code must be more than 1MB in size. In fact, neither of them is correct, because the amount of code cannot be measured as the number of installed codes. Many trivial JS code can easily exceed 100,000 lines.
My definition of "big" is as follows. Although it may not be correct, it should be relatively close:
I personally think that large JavaScript programs should be very important and incorporate many outstanding developers' efforts to process heavyweight data and display it to the browser.
Review the current program architecture
I can't emphasize how important this issue is. Many experienced developers often say, "The existing creative and design patterns run very well on my previous mid-sized project, so it should be fine to use them again in a slightly larger program, right?", It's true on certain programs, but don't forget that since it's a large program, there should usually be big Concerns that need to be broken down and paid attention. I briefly explain how it takes time to review the program architecture that has been running for a long time. In most cases, the current JavaScript program architecture should look like this (note that it is a JS architecture, not what everyone often calls ASP.NET MVC):
custom widgets
models
views
Controllers
templates
libraries/toolkits
an application core.
You may also encapsulate the program into multiple modules alone, or use other design patterns, which is great, but if these structures completely represent your architecture, there may be some potential problems. Let's take a look at a few important points:
1. How many things in your architecture can be taken out and reused immediately?
Are there some separate modules that do not depend on other code? Is it self-contained? If I go to the code base you are using and then select some module module codes and put them on a new page, can it be used immediately? You may say that the principle is OK. I suggest you plan for a long time. If your company has developed many important programs before, suddenly one day someone said that the chat module in this project is good, let’s take it out and put it in another project. Can you just use it without modifying the code?
2. How many modules do the system need to rely on other modules?
Are all modules of the system tightly coupled? Before I take this question as concern, I will explain first. It is not that all modules must not have any dependencies. For example, a fine-grained function may be extended from the base function. My problem is different from this situation. I am talking about the dependencies before different functional modules. In theory, all different functional modules should not have too many dependencies.
3. If something goes wrong with one part of your program, will the other parts still work?
If you build a program similar to Gmail, you can find that many modules in Gmail are loaded dynamically, such as the chat chat module, which is not loaded when initializing the page, and even if an error occurs after loading, other parts of the page can be used normally.
4. Can you test your modules in a very simple way?
Each of your modules may be used on large sites with millions of users, or even multiple sites use it, so your modules need to stand the test, that is, whether inside or outside the architecture, they should be able to be tested very simply, including most of the assertions that can be passed in different environments.
Long-term consideration
When structuring large programs, the most important thing is to be forward-looking. You cannot only consider the situation one month or one year later. You should consider the possibility of changes in a longer period of time? Developers often bind DOM operations code and program too tightly, although sometimes they have encapsulated separate logic into different modules. Think about why it is not very good in the long run.
A colleague of mine once said that an accurate architecture may not be suitable for future scenarios, and sometimes it is correct, but when you need to do it, you will pay a lot of money. For example, you may need to choose to replace Dojo, jQuery, Zepto, and YUI for certain performance, security, and design reasons. At this time, there is a problem. Most modules have dependencies, which require money, time, and people, right?
It's okay for some small sites, but large sites do need to provide a more flexible mechanism without worrying about various problems between various modules. This saves money and time.
To sum up, can you now be sure that you can replace some class libraries without rewriting the entire program? If not, then what we are going to talk about below is more suitable for you.
Many experienced JavaScript developers have given some key notes:
Justin Meyer, author of JavaScriptMVC, said:
The biggest secret to building large programs is that you never build large programs, but break the programs into small modules to make each small module testable, sizeable, and then integrate them into the program.
High-performance JavaScript websites Author Nicholas, Zakas:
"The key is to acknowledge from the start that you have no idea how this will grow. When you accept that you don't know everything, you begin to design the system defensively. You identify the key areas that may change, which often is very easy when you put a little bit of time into it. For instance, you should expect that any part of the app that communicates with another system will likely change, so you need to abstract that away." -
A lot of text problems are too troublesome. To sum up, everything can be changed, so it must be abstract.
jQuery Fundamentals Author Rebecca Murphey:
The closer the connection between each module is, the less reusable it is, and the greater the difficulty of changing it.
The above important views are the core elements of building the architecture and we need to remember them all the time.
Brainstorm
Let’s brainstorm. We need a loosely coupled architecture, with no dependencies between modules, each module and program communicate, and then the intermediate layer takes over and processes the corresponding messages.
For example, if we have a JavaScript building an online bakery program, a module sends a message that might be "There are 42 rounds that need to be delivered". We use different layer layers to process messages sent by the module, and do the following:
Modules do not directly access the program core
Modules do not call directly or affect other modules
This will prevent us from making errors in all modules due to errors in a certain module.
Another problem is security. The real situation is that most people do not think internal security is a problem. We say in our hearts that the programs are built by myself. I know which ones are public and private. There is no problem with security, but do you have a way to define which module to access the program core? For example, there is a chat chat module that I don't want it to call the admin module, or I don't want it to call a module with DB write permissions, because there is a fragility between them and it is easy to cause XSS attacks. Each module should not be able to do everything, but JavaScript code in most architectures currently has this problem. Provide an intermediate layer to control which module can access that authorized part, that is, the module can only achieve the most of the part we authorized.
Suggested architecture
The focus of our article is that this time the architecture we propose uses design patterns that we are all well-known: module, facade, and mediator.
Unlike traditional models, in order to decouple each module, we only let the module publish some event events. The mediator mode can be responsible for subscribing to message messages from these modules, and then controlling the notification response. Facade mode users restrict the permissions of each module.
The following are the parts we need to pay attention to:
1 Design Pattern
1.1 Module Theory
1.1.1 Overview
1.1.2 Module mode
1.1.3 Object self-face size
1.1.4 CommonJS module
1.2 Facade mode
1.3 Mediator mode
2 Apply to your architecture
2.1 Facade - Core Abstraction
2.2 Mediator - Program Core
2.3 Work closely together
Modal Theory
Everyone may have used modular code more or less. The module is part of a complete and robust program architecture. Each module is created for a separate purpose. Going back to Gmail, let’s take an example. The chat chat module seems to be a separate part, but in fact it has many separate submodules. For example, the expression module inside is actually a separate submodule, which is also used on the window for sending emails.
Another is that modules can be loaded, deleted and replaced dynamically.
In JavaScript, we have several ways to implement modules. Everyone is familiar with module patterns and object literals. If you are already familiar with these, please ignore this section and jump directly to the CommonJS part.
Module mode
The module pattern is a relatively popular design pattern. It can encapsulate private variables, methods, and states through braces. By wrapping these contents, generally global objects cannot be accessed directly. In this design pattern, only one API is returned, and all other contents are encapsulated as private.
In addition, this pattern is similar to self-executed function expressions. The only difference is that module returns an object, while self-executed function expression returns a function.
As we all know, JavaScript does not want other languages to have access modifiers, and cannot declare private and public modifiers for each field or method. So how do we implement this pattern? That is to return an object, including some public methods, which have the ability to call internal objects.
Take a look at the code below. This code is a self-executing code. The declaration includes a global object batteryModule. The basket array is a private, so your entire program cannot access this private array. At the same time, we return an object, which contains 3 methods (such as addItem, getItemCount, getTotal). These 3 methods can access the private basket array.
var basketModule = (function() {var basket = []; //privatereturn { //exposed to public addItem: function(values) { basket.push(values); }, getItemCount: function() { return basket.length; }, getTotal: function() { var q = this.getItemCount(),p=0; while(q--){ p+= basket[q].price; } return p; } }}());Also note that the object we return is assigned directly to the basketModule, so we can use it like the following:
//basketModule is an object with properties which can also be methodsbasketModule.addItem({item:'bread',price:0.5});basketModule.addItem({item:'butter',price:0.3}); console.log(basketModule.getItemCount()); console.log(basketModule.getTotal()); //however, the following will not work:console.log(basketModule.basket);// (undefined as not inside the returned object)console.log(basket); //(only exists within the scope of the closure)So how do it in various popular class libraries (such as Dojo, jQuery)?
Dojo
Dojo attempts to use dojo.declare to provide class-style declaration methods. We can use it to implement the Module pattern. For example, if you want to declare a basket object under the store namespace, you can do this:
//traditional wayvar store = window.store || {};store.basket = store.basket || {}; //using dojo.setObjectdojo.setObject("store.basket.object", (function() { var basket = []; function privateMethod() { console.log(basket); } return { publicMethod: function(){ privateMethod(); } };}()));It is very powerful when combined with dojo.provide.
YUI
The following code is the original implementation of YUI:
YAHOO.store.basket = function () { //"private" variables: var myPrivateVar = "I can be accessed only within YAHOO.store.basket ."; //"private" method: var myPrivateMethod = function () { YAHOO.log("I can be accessed only from within YAHOO.store.basket"); } return { myPublicProperty: "I'm a public property.", myPublicMethod: function () { YAHOO.log("I'm a public method."); //Within basket, I can access "private" vars and methods: YAHOO.log(myPrivateVar); YAHOO.log(myPrivateMethod()); //The native scope of myPublicMethod is store so we can //access public members using "this": YAHOO.log(this.myPublicProperty); } };} ();jQuery
There are many implementations of the Module pattern in jQuery. Let's take a look at a different example. A library function declares a new library. Then when creating the library, the init method is automatically executed in document.ready.
function library(module) { $(function() { if (module.init) { module.init(); } }); return module;} var myLibrary = library(function() { return { init: function() { /*implementation*/ } };}());Object self-face size
The object self-face measurement is declared in braces, and the new keyword is not required when using it. If you don’t care much about the public/private of the attribute fields in a module, you can use this method, but please note that this method is different from JSON. Object self-face size: var item={name: "tom", value:123} JSON:var item={"name":"tom", "value":123}.
var myModule = { myProperty: 'someValue', //object literals can contain properties and methods. //here, another object is defined for configuration //purposes: myConfig: { useCaching: true, language: 'en' }, //a very basic method myMethod: function () { console.log('I can haz functionality?'); }, //output a value based on current configuration myMethod2: function () { console.log('Caching is:' + (this.myConfig.useCaching) ? 'enabled' : 'disabled'); }, //override the current configuration myMethod3: function (newConfig) { if (typeof newConfig == 'object') { this.myConfig = newConfig; console.log(this.myConfig.language); } }}; myModule.myMethod(); //I can haz functionalitymyModule.myMethod2(); //outputs enabledmyModule.myMethod3({ language: 'fr', useCaching: false }); //frCommonJS
I won't talk about the introduction of CommonJS here. Many articles have introduced it before. What we want to mention here is that there are two important parameters exports and require in the CommonJS standard. exports represent the module to be loaded, and require means that these loaded modules need to rely on other modules and also need to be loaded.
/*Example of achieving compatibility with AMD and standard CommonJS by putting boilerplate around the standard CommonJS module format:*/ (function(define){ define(function(require,exports){ // module contents var dep1 = require("dep1"); exports.someExportedFunction = function(){...}; //... });})(typeof define=="function"?define:function(factory){factory(require,exports)});There are many CommonJS standard module loading implementations. What I prefer is RequireJS. Can it load modules and related dependency modules very well? Let’s take a simple example. For example, if you need to convert the image into ASCII code, we first load the encoder module, and then obtain its encodeToASCII method. Theoretically, the code should be as follows:
var encodeToASCII = require("encoder").encodeToASCII; exports.encodeSomeSource = function(){ //After other operations, then call encodeToASCII}However, the above code does not work, because the encodeToASCII function is not used to attach to the window object, so it cannot be used. This is what the improvement of the code needs to do:
define(function(require, exports, module) { var encodeToASCII = require("encoder").encodeToASCII; exports.encodeSomeSource = function(){ //process then call encodeToASCII }});CommonJS has great potential, but since the uncle is not very familiar with it, I will not introduce it much.
Facade mode
The Facade model occupies an important role in the architecture of this model. Many JavaScript class libraries or frameworks are reflected in this model. The biggest function is to include the API of the High level to hide specific implementations. This means that we only expose the interfaces, and we can make the decisions of the internal implementations by ourselves, which also means that the internal implementation code can be easily modified and updated. For example, today you use jQuery to implement it, and tomorrow you want to change YUI, which is very convenient.
In the following example, we can see that we provide many private methods, and then expose a simple API to allow the outside world to execute and call internal methods:
var module = (function () { var _private = { i: 5, get: function () { console.log('current value:' + this.i); }, set: function (val) { this.i = val; }, run: function () { console.log('running'); }, jump: function () { console.log('jumping'); } }; return { facade: function (args) { _private.set(args.val); _private.get(); if (args.run) { _private.run(); } } }} ());module.facade({run:true, val:10});//outputs current value: 10, runningThe difference between Facade and what we are talking about below is that facade only provides existing functions, while mediators can add new functions.
Mediator mode
Before talking about the modiator, let’s first give an example. The airport flight control system, which is the legendary tower, has absolute power. It can control the takeoff and landing time and place of any aircraft. The aircraft and the aircraft are not allowed to communicate before, which means that the tower is the core of the airport, and the mediator is equivalent to this tower.
Mediator is used to have multiple modules in a program and you don’t want each module to have dependencies, then the mediator mode can achieve the purpose of centralized control. In actual scenarios, mediator encapsulates many modules that they don't want to do, allowing them to be connected through mediators, and also loosely coupled them, so that they must communicate through mediators.
So what are the advantages of the mediator mode? That is decoupling. If you have a good understanding of the observer pattern before, it will be relatively simple to understand the mediator diagram below. The following figure is a high level mediator pattern diagram:
Think about it, each module is a publisher, and the mediator is both a publisher and a subscriber.
Module 1 broadcasts an actual thing to Mediator, saying something needs to be done
After the Mediator captures the message, immediately start the Module 2 that needs to be used to process the message. After the Module 2 processing is completed, return the information to the Mediator.
At the same time, Mediator also starts Module 3, and automatically logs to Module 3 when receiving the return message of Module 2.
It can be seen that there is no communication between the modules. In addition, Mediator can also implement the function of monitoring the status of each module. For example, if there is an error in Module 3, Mediator can temporarily only want other modules, then restart Module 3, and then continue to execute.
Looking back, we can see that the advantage of Mediator is that the loosely coupled module is controlled by the same Mediator. The module only needs to broadcast and listen to events, and there is no need for direct connection between modules. In addition, multiple modules can be used for processing information at a time, which also facilitates us to add new modules to the existing control logic in the future.
It is certain that since all modules cannot communicate directly, there may be a slight decline in performance in all relatively, but I think it is worth it.
Let's use a simple demo based on the above explanation:
var mediator = (function(){ var subscribe = function(channel, fn){ if (!mediator.channels[channel]) mediator.channels[channel] = []; mediator.channels[channel].push({ context: this, callback: fn }); return this; }, publish = function(channel){ if (!mediator.channels[channel]) return false; var args = Array.prototype.slice.call(arguments, 1); for (var i = 0, l = mediator.channels[channel].length; i < l; i++) { var subscription = mediator.channels[channel][i]; subscription.callback.apply(subscription.context, args); } return this; }; return { channels: {}, publish: publish, subscribe: subscribe, installTo: function(obj){ obj.subscribe = subscribe; obj.publish = publish; } }; }());Then there are 2 modules called:
//Pub/sub on a centralized mediator mediator.name = "tim";mediator.subscribe('nameChange', function(arg){ console.log(this.name); this.name = arg; console.log(this.name);}); mediator.publish('nameChange', 'david'); //tim, david //Pub/sub via third party mediator var obj = { name: 'sam' };mediator.installTo(obj);obj.subscribe('nameChange', function(arg){ console.log(this.name); this.name = arg; console.log(this.name);}); obj.publish('nameChange', 'john'); //sam, johnApplication Facade: The abstraction of the core of the application
A facade works as an abstract of the application core, and is responsible for communication between the mediator and the module. Each module can only communicate with the program core through this facade. The responsibility as an abstract is to ensure that these modules can be provided with a consistent interface at any time, which is similar to the role of sendbox controller. All module components communicate with mediators through it, so facade needs to be reliable and trustworthy. At the same time, as a function to provide an interface to the module, facade needs to play another role, that is, security control, that is, to determine which part of the program can be accessed by a module. Module components can only call their own methods and cannot access any unauthorized content. For example, a module might broadcast dataValidationCompletedWriteToDB, where security checks need to ensure that the module has write permissions to the database.
In short, mediator can only perform information processing after facade authorization detection.
Application Mediator: The core of the application
Mediator works as a core role for the application, let's briefly talk about his responsibilities. The most core job is to manage the lifecycle of the module. When this core captures any information, it needs to judge how the program handles it - that is, to decide which module to start or stop. When a module starts, it should be able to execute automatically, without the application core to decide whether it should be executed (for example, whether it should be executed when DOM is ready), so the module itself needs to determine.
You may have a question, under what circumstances will a module stop? When the program detects that a module fails or makes an error, the program needs to make a decision to prevent the method in the module from continuing to execute so that the component can be restarted, with the main purpose of improving the user experience.
In addition, the core should be able to dynamically add or delete modules without affecting any other functions. A common example is that a module is unavailable at the beginning of page loading, but after the user operates, it needs to dynamically load the module and then execute it. Just like the chat function in Gmail, it should be easy to understand from the purpose of performance optimization.
Exception error handling is also handled by the application core. In addition, when each module broadcasts information, it also broadcasts any errors to the core so that the program core can stop/restart these modules according to the situation. This is also a very important part of the loosely coupled architecture. We do not need to manually change any modules. We can do this by using publish/subscribe through the mediator.
Assemble
Each module contains various functions in the program. When they have information to be processed, they issue information notifying the program (this is their main responsibility). The following QA section mentioned that modules can rely on some DOM tool operation methods, but they should not depend on other modules of the system. A module should not pay attention to the following content:
1. Which object or module subscribes to the information published by this module
2. Are these objects client or server-side objects
3. How many objects have subscribed to your information
The core of Facade abstraction application avoids direct communication between modules. It subscribes to information from each module and is also responsible for authorization detection, ensuring that each module has its own separate authorization.
Mediator (application core) uses mediator mode to play the role of publish/subscribe manager, responsible for module management and start/stop module execution, and can dynamically load and restart modules with errors.
The result of this architecture is that there is no dependency between modules, because of loosely coupled applications, they can be easily tested and maintained, each module can be easily reused in other projects, or can be dynamically added and deleted without affecting the program.
Publish Pub/Sub Extension: Automatic Event Registration
Regarding automatic registration of events, certain naming specifications need to be followed. For example, if a module publishes an event named messageUpdate, then all modules with the messageUpdate method will be automatically executed. There are benefits and advantages and disadvantages. For specific implementation methods, you can see another post from me: the magic upgraded version of jQuery custom binding.
QA
1. Is it possible not to use facade or similar sandbox mode?
Although the outline of the architecture proposes that facade can implement authorization checking functions, it is actually entirely possible that mediators can do it. What the light architecture needs to do is almost the same, that is, decoupling and ensuring that each module directly communicates with the core of the application is fine.
2. You have improved that the module cannot be directly dependent. Does it mean that it cannot rely on any third-party library (such as jQuery).
This is actually a two-sided issue. As we mentioned above, a module may have some submodules, or basic modules, such as basic DOM operation tool classes, etc. At this level, we can use third-party library, but please make sure that we can easily replace them.
3. I like this architecture and want to start using this architecture. Are there any code samples that can be used to refer to?
I plan to make a code sample for your reference, but before that, you can refer to Andrew Burgees' post Writing Modular JavaScript.
4. Is it feasible if the module needs to communicate directly with the application core?
Technically, there is no reason why modules cannot communicate directly with the application core now, but for most application experiences, it is still not allowed. Since you have chosen this architecture, you must abide by the rules defined by the architecture.
Acknowledgements
Thanks to Nicholas Zakas for the original post, to sum up the ideas together, to Andree Hansson for technical review, to Rebecca Murphey, Justin Meyer, John Hann, Peter Michaux, Paul Irish and Alex Sexton, all of them provided a lot of information related to this Session.