Home  >  Article  >  Web Front-end  >  Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

亚连
亚连Original
2018-05-21 09:23:512579browse

The following is the large-scale JavaScript application architecture design pattern I compiled for you. Interested students can take a look.

The following are the main chapters of this article:

1. What is a "JavaScript large program"?

2. Consider the current program architecture

3. Think long-term

4. Brainstorm

5. Suggested architecture

5.1 Design pattern

5.1.1 Module theory

5.1.1.1 Overview

5.1.1.2 Module pattern

5.1.1.3 Object self-facet

    5.1.1.4 CommonJS module

  5.1.2 Facade pattern

    5.1.3 Mediator pattern

  5.2 Apply to your architecture

  5.2 .1 Facade - core abstraction

5.2.2 Mediator - program core

5.2.3 Work closely together

6. Extension of publishing/subscribing to Sub: automatic registration Event

7. Q & A

8. Acknowledgments

What is a "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 is considered large. Some people also say that JavaScript code must exceed 1MB in size. In fact, neither of the two is correct, because it cannot be measured by the amount of installed code. Many trivial JS codes 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 integrated into It is a program that uses the efforts of many outstanding developers to process heavyweight data and display it to the browser.

Reviewing the current program architecture

I cannot emphasize how important this issue is. Many experienced developers often say: "Existing ideas and design patterns worked very well on my last medium-sized project, so it should be no problem to use it again in a slightly larger program, right?", This is true for certain programs, but don't forget, since It is a large program, and usually there should be large concerns that need to be broken down and paid attention to. I will briefly explain that 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 the following (note, it is a JS architecture, not the ASP.NET MVC we often call):
custom widgets
models
views
controllers
templates
libraries/toolkits
an application core.

You may also encapsulate the program into multiple modules separately, or use other design patterns, which is good, but if 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 immediately? Come out and reuse it? Are there any separate modules that do not depend on other code? Is it self-contained? If I go to the code base you are using and randomly select some module code, and then put it on a new page, will it be available immediately? You may say that it’s enough to understand the principles. I suggest you think about it for a long time. If your company has developed many important programs before, suddenly one day someone says that the chat module in this project is good. Let’s take it out and put it in another project. Come on, can you just take it and use it without modifying the code?

2. How many modules in the system need to depend on other modules? Are the various modules of the system tightly coupled? Before I address this question as a concern, let me first explain that it does not mean 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 between different functional modules. In theory, all different functional modules should not have too many dependencies.

3. If a certain part of your program fails, can other parts still work? If you build a program similar to Gmail, you can find that many modules in Gmail are dynamically loaded. For example, the chat module is not loaded when the page is initialized, and even if an error occurs after loading, the page Other parts can also be used normally.

4. Can each of your modules be easily tested? Each of your modules may be used on a large site with millions of users, or even used by multiple sites, so your module needs to be able to withstand testing, that is, whether it is within the architecture It should be easy to test whether it is external to the architecture, including that most assertions can pass in different environments.

Long-term thinking

When constructing a large program, the most important thing is to be forward-looking. You can’t just consider the situation one month or a year from now, but also consider the future. What are the possibilities for change in the long term? Developers often bind the DOM manipulation code to the program too tightly, even though sometimes they have encapsulated separate logic into different modules. Think about it, why is it not good in the long run?

A colleague of mine once said that a precise architecture may not be suitable for future scenarios. This is sometimes true, but when you need to do it, the money you have to pay is quite a lot. . For example, you may need to choose and replace between Dojo, jQuery, Zepto, and YUI due to certain performance, security, and design reasons. At this time, there will be a problem. Most modules have dependencies, which requires money and time. , we need people, right?

It’s okay for some small sites, but large sites really need to provide a more flexible mechanism without worrying about various problems between various modules. This saves money and time.

To summarize, are you sure now that you can replace some class libraries without rewriting the entire program? If not, then I guess what we are going to talk about below will be more suitable for you.

Many experienced JavaScript developers have given some key notes:

Justin Meyer, author of JavaScriptMVC, said:

The biggest problem when building large programs is The secret is to never build large programs, but to break the program into small modules so that each small module can be tested, sized, and then integrated 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 is changeable, so you need to abstract.

jQuery Fundamentals author Rebecca Murphey:
The closer the connection between each module, the less reusable and the more difficult it is to change.

These important points above are the core elements of building an architecture, and we need to keep them in mind at all times.

Brainstorming

Let's brainstorm, we need a loosely coupled architecture, there are no dependencies between modules, each module communicates with the program, and then in the middle The layer takes over and processes the feedback corresponding messages.

For example, if we have a JavaScript to build an online bakery program, a module sends a message that may be "42 buns need to be delivered." We use different layers to process messages sent by modules, as follows:

The module does not directly access the program core
The module does not directly call or affect other modules

This will This prevents us from causing errors in all modules due to an error in one module.

Another issue is security. The real situation is that most people do not think internal security is a problem. We say in our hearts that I built the program myself, and I know which ones are public and which ones are private. Security is no problem, but is there any way to define which module has permission to access the core of the program? For example, if there is a chat module, 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 the relationship between them is very fragile and can easily lead to XSS attacks. Each module shouldn't be able to do everything, but this is the problem with JavaScript code in most current architectures. Provide a middle layer to control which module can access the authorized part. In other words, the module can only do the most authorized part.

Proposed architecture

The focus of our article is here. This time our proposed architecture uses design patterns that we are all familiar with: module, facade and mediator.

Different from the traditional model, in order to decouple each module, we only let the modules publish some events. The mediator mode can be responsible for subscribing message messages from these modules, and then controlling the response of the notification, and the facade mode Users limit 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 pattern
1.1.3 Object self-face
                                                                                                                                                         closely united How it works


Module theory


Everyone may have used modular code to a greater or lesser extent. A module is part of a complete and robust program architecture. Each Modules are created for separate purposes. Back to Gmail, let's take an example. The chat module seems to be a separate part. In fact, it is composed of many separate sub-modules. For example, the emoticon module inside is actually a separate part. The submodule is also used in the window for sending emails.

The other is that modules can be dynamically loaded, deleted and replaced. In JavaScript, we have several ways to implement modules. The most well-known ones are module mode and object literals. If you are already familiar with these, please ignore this section and jump directly to the CommonJS part.

Module pattern

The module pattern is a popular design pattern. It can encapsulate private variables, methods, and states through curly 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 mode is similar to the self-executing function expression. The only difference is that the module returns an object, while the self-executing function expression returns a function. As we all know, JavaScript does not have access modifiers like other languages. It cannot declare private and public modifiers for each field or method. So how do we implement this pattern? That is to return an object, which contains some public methods. These methods have the ability to call internal objects.

Look at the following code. This code is a self-executing code. The declaration includes a global object basketModule. The basket array is private, so your entire program cannot access this private array. At the same time, we returned an object, which contains three methods (such as addItem, getItemCount, getTotal). These three methods can access the private basket array.

var basketModule = (function() {
var basket = []; //private
return { //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 directly assigned to basketModule, so we can use it as follows:

//basketModule is an object with properties which can also be methods
basketModule.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)

That is in various popular libraries (such as Dojo, jQuery) How to do it?

Dojo

Dojo tries to use dojo.declare to provide a class-style declaration method. We can use it to implement the Module mode, for example if If you want to declare the basket object under the store namespace, you can do this:

//traditional way
var store = window.store || {};
store.basket = store.basket || {};
 
//using dojo.setObject
dojo.setObject("store.basket.object", (function() {
  var basket = [];
  function privateMethod() {
    console.log(basket);
  }
  return {
    publicMethod: function(){
      privateMethod();
    }
   };
}()));
Use it in conjunction with dojo.provide, which is very powerful.

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 look at a different example. A library function declares a new library, and then when creating the library, in document.ready Automatically execute the init method.

function library(module) {
  $(function() {
    if (module.init) {
      module.init();
    }
  });
  return module;
}
 
var myLibrary = library(function() {
  return {
    init: function() {
      /*implementation*/
      }
  };
}());
Object self-face value

Object self-face value is declared using braces, and there is no need to use the new keyword when using it. If If you don't care much about the public/private nature 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: 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 functionality
myModule.myMethod2(); //outputs enabled
myModule.myMethod3({ language: 'fr', useCaching: false }); //fr
CommonJS

I won’t say much about the introduction of CommonJS here. Many articles have introduced it before, so we want to mention it here. What's more, there are two important parameters in the CommonJS standard, exports and require. Exports represents the modules to be loaded, and require represents that these loaded modules need to depend 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. The one I prefer is RequireJS. Can it load modules and related dependent modules very well? Let’s take a simple example, such as converting pictures 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(){
  //其它操作以后,然后调用encodeToASCII
}

但是上述代码并没用工作,因为encodeToASCII函数并没用附加到window对象上,所以不能使用,改进以后的代码需要这样才行:

define(function(require, exports, module) {
  var encodeToASCII = require("encoder").encodeToASCII;
    exports.encodeSomeSource = function(){
    //process then call encodeToASCII
  }
});

CommonJS 潜力很大,但是由于大叔不太熟,所以就不过多地介绍了。

Facade模式

Facade模式在本文架构里占有重要角色,关于这个模式很多JavaScript类库或者框架里都有体现,其中最大的作用,就是包括High level的API,以此来隐藏具体的实现,这就是说,我们只暴露接口,内部的实现我们可以自己做主,也意味着内部实现的代码可以很容易的修改和更新,比如今天你是用jQuery来实现的,明天又想换YUI了,这就非常方便了。

下面这个例子了,可以看到我们提供了很多私有的方法,然后通过暴露一个简单的 API来让外界执行调用内部的方法:

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, running

Facade和下面我们所说的mediator的区别是,facade只提供现有存在的功能,而mediator可以增加新功能。

 Mediator模式

讲modiator之前,我们先来举个例子,机场飞行控制系统,也就是传说中的塔台,具有绝对的权利,他可以控制任何一架飞机的起飞和降落时间以及地方,而飞机和飞机之前不允许通信,也就是说塔台是机场的核心,mediator就相当于这个塔台。

mediator就是用在程序里有多个模块,而你又不想让各个模块有依赖的话,那通过mediator模式可以达到集中控制的目的。实际场景中也是,mediator封装了很多不想干的模块,让他们通过mediator联系在一起,同时也松耦合他们,使得他们之间必须通过mediator才能通信。

那mediator模式的优点是什么?那就是解耦,如果你之前对观察者模式比较了解的话,那理解下面的mediator图就相对简单多了,下图是一个high level的mediator模式图:

Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

想想一下,各模块是发布者,mediator既是发布者又是订阅者。

    Module 1向Mediator广播一个实际,说需要做某事
    Mediator捕获消息以后,立即启动处理该消息需要使用的Module 2,Module 2处理结束以后返回信息给Mediator
    与此同时,Mediator也启动了Module 3,当接受Module 2 返回消息的时候自动记录日志到Module 3里

可以看到,各模块之间并没有通信,另外Mediator也可以实现监控各模块状态的功能,例如如果Module 3出错了,Mediator可以暂时只想其它模块,然后重启Module 3,然后继续执行。

回顾一下,可以看到,Mediator的优点是:松耦合的模块由同一的Mediator来控制,模块只需要广播和监听事件就可以了,而模块之间不需要直接联系,另外,一次信息的处理可以使用多个模块,也方便我们以后统一的添加新的模块到现有的控制逻辑里。

确定是:由于所有的模块直接都不能直接通信,所有相对来说,性能方面可能会有少许下降,但是我认为这是值得的。

我们根据上面的讲解来一个简单的Demo:

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;
 }
 };
 
}());

然后有2个模块分别调用:

//Pub/sub on a centralized mediator
 
mediator.name = "tim";
mediator.subscribe(&#39;nameChange&#39;, function(arg){
 console.log(this.name);
 this.name = arg;
 console.log(this.name);
});
 
mediator.publish(&#39;nameChange&#39;, &#39;david&#39;); //tim, david
 
 
//Pub/sub via third party mediator
 
var obj = { name: &#39;sam&#39; };
mediator.installTo(obj);
obj.subscribe(&#39;nameChange&#39;, function(arg){
 console.log(this.name);
 this.name = arg;
 console.log(this.name);
});
 
obj.publish(&#39;nameChange&#39;, &#39;john&#39;); //sam, john

应用Facade: 应用程序核心的抽象

一个facade是作为应用程序核心的一个抽象来工作的,在mediator和模块之间负责通信,各个模块只能通过这个facade来和程序核心进行通信。作为抽象的职责是确保任何时候都能为这些模块提供一个始终如一的接口(consistent interface),和sendbox controller的角色比较类似。所有的模块组件通过它和mediator通信,所以facade需要是可靠的,可信赖的,同时作为为模块提供接口的功能,facade还需要扮演另外一个角色,那就是安全控制,也就是决定程序的哪个部分可以被一个模块访问,模块组件只能调用他们自己的方法,并且不能访问任何未授权的内容。例如,一个模块可能广播dataValidationCompletedWriteToDB,这里的安全检查需要确保该模块拥有数据库的写权限。

总之,mediator只有在facade授权检测以后才能进行信息处理。

应用Mediator:应用程序的核心

Mediator works as the core role of the application. Let’s briefly talk about his responsibilities. The core job is to manage the life cycle of the module. When this core captures any information coming in, it needs to determine how the program should handle it - that is, decide which module or modules to start or stop. When a module starts, it should be able to execute automatically without the application core deciding whether it should be executed (for example, whether it should be executed when the DOM is ready), so the module itself needs to make a decision.

You may still have questions, that is, under what circumstances will a module stop. When the program detects that a module has failed or an error has occurred, the program needs to make a decision to prevent the method in the module from continuing to be executed so that the component can be restarted. The main purpose is to improve 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 not available at the beginning of page loading, but after user operation, the module needs to be dynamically loaded and executed, just like the chat function in Gmail. From the perspective of performance optimization, it should be very good. Understand it.

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 an 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 process, they release information to notify the program (this is their Main responsibilities), as mentioned in the QA section below, modules can rely on some DOM tool operation methods, but they should not depend on other modules of the system. A module should not focus on the following:

1. Which object Or the module subscribes to the information published by this module
2. Are these objects client-side objects or server-side objects
3. How many objects subscribe to your information

Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

Facade abstracts the core of the application and avoids direct communication between modules. It subscribes to information from each module and is also responsible for authorization detection to ensure that each module has its own separate authorization.

Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

Mediator (application core) uses the mediator mode to play the role of a publish/subscribe manager. It is responsible for module management and starting/stopping module execution. It can dynamically load and restart with errors. module.

Large-Scale JavaScript Application Architecture Design Patterns (Advanced)

The result of this architecture is that there are no dependencies between modules. Because of loosely coupled applications, they can be easily tested and maintained, and each module can be easily It can be reused in other projects and can be added and deleted dynamically without affecting the program.

Extension of publishing Pub/subscribing Sub: Automatic Event Registration

Regarding automatic registration events, certain naming conventions need to be followed, such as if a module When an event named messageUpdate is published, all modules with the messageUpdate method will be automatically executed. There are advantages and disadvantages. For the specific implementation method, you can read my other post: 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 the facade can implement the authorization check function, in fact it is entirely possible for the mediator to do it. What the lightweight architecture has to do is actually almost the same, that is, decoupling to ensure that each module is directly There is no problem in communicating with the application core.

2. You have improved that the module cannot have dependencies directly. Does this mean that it cannot rely on any third-party libraries (such as jQuery).

This is actually a two-sided problem. As we mentioned above, a module may have some sub-modules, or basic modules, such as basic DOM operation tool classes, etc. At this level, we can use third-party libraries, but please make sure that we can easily replace them.

3. I like this architecture and want to start using it. Are there any code samples I can refer to?

I’m going to get a code sample for everyone’s reference, but before that, you can refer to Andrew Burgees’ post Writing Modular JavaScript.

4. If the module needs to communicate directly with the application core, is it feasible?

Technically speaking, there is no reason why the module cannot communicate directly with the application core now, but for most application experiences, it is still not necessary. Since you have chosen this architecture, you must abide by the rules defined by the architecture.

The above is what I compiled for everyone. I hope it will be helpful to everyone in the future.

Related articles:

Detailed answers to events in JS (graphic tutorial)

Convert JSON object to string (Detailed answers to the code are attached)

JS event bubbling and event capture (graphic tutorial, simple violence)

The above is the detailed content of Large-Scale JavaScript Application Architecture Design Patterns (Advanced). For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn