📜 ⬆️ ⬇️

Building scalable applications in TypeScript. Part 1 - Asynchronous Module Loading

The idea of ​​this article was born after a hard day at 30 degrees in the office and serious thoughts and holivars on the topic: “How should a modern web application be built?”

And then the thought occurred to me to state my process of working on the Habré task. And I myself will understand to the smallest detail, and will contribute to the knowledge of the community.

What will be discussed in this article? I will write a (not) large TypeScript application that will implement a modular architecture, asynchronous module loading, an abstract event model, and update the state of modules upon the occurrence of certain events. This article will act as a diary and journal of my actions and reflections. My personal goal is to create some working prototype, the experience of which I later could use as part of a real project. The code will be written as accurately as possible and close to the requirements of the actual development. Explanations will be given as if it will then be read by the juniors working under my leadership, who have never written such systems before.
')
The article will be broken into pieces, which I will lay out to the public as ready. The first part is devoted to the general formulation of the problem, the modules and their asynchronous loading.

So, giving myself and the community these promises, turning on AC / DC and collecting my thoughts, I will proceed.

Part 2: Building Scalable Applications in TypeScript. Part 2 - Events or why you should reinvent your own bike

Part 2.5: Building Scalable Applications on TypeScript - Part 2.5. Bug fixes and delegates

Used software and other notes

In this article, I will use Visual Studio Express 2012 for Web with all the latest updates as a working tool. The reason is the only IDE with adequate TypeScript support for today.

About the most TypeScript. A three-month experience with TS 0.8.3 shows that TS is a really working tool for creating really large web applications with tens of thousands of lines of code. Static code analysis actually reduces the number of errors by an order of magnitude. We also have almost full-fledged classic OOP with us, real language-level modularity, which allows to integrate transparently with Require.js and Node.js, IntelliSense in Visual Studio, and indeed, the ideology clearly gives away C #, which is extremely close to me. Transparent transformation TS to JS allows you to easily debug the code even without the help of sourcemaps, although they are present. For writing this article I will use the latest version - 0.9 with generics and other goodies.

Require.js will be used to implement asynchronous loading. Node.js will be used to simulate the server side.

As a “standard library”, jQuery 1.10 (we need IE8 compatibility) and Underscore.js will be connected to the project. Backbone.js will be the basis for the interface for us. Header d.ts files are used from the standard delivery of TS (jQuery) and from the project DefinitelyTyped - github.com/borisyankov/DefinitelyTyped (Require.js, Underscore, Backbone, Node.js).

To play AC / DC, WinAmp is used.

A bit of TypeScript

The program on TS consists of a set of * .ts and * .d.ts files. They can be represented as some analogues of * .cpp and * .h files from C ++. Those. the first ones contain the real code, the second ones describe the interfaces that the implementation file of the same name provides. For development purely on TS header d.ts files are not needed. Also, d.ts is not compiled into anything and is not used in runtime. But d.ts files are indispensable for describing existing JS code. Since TS is completely converted to JS, it can fully use any JS code, but the compiler needs to be aware of the types, variables and functions that are used in JS. The .ts files just serve the purpose of this description. I will not stop in more detail, everything is in the TS specification.

In general, when compiling example.ts, the following files can be created:


Project structure

Create a TypeScript project in Visual Studio:

image

I will publish all source codes on CodePlex: tsasyncmodulesexampleapp.codeplex.com .

Add the default code, create the 2nd project - Server, connect all the necessary source files of libraries and get the following structure:

image

Practice shows that d.ts files for shared libraries are best brought to the same level as projects, since then it will be terribly useful when writing tests, but more on that another time, and also if several projects use the same libraries.

Here you should talk a little bit about how to build a TypeScript project in and without Visual Studio. In the first case, the studio does everything for the developer, passing the files to the compiler one by one if they are not referenced. Those. all ts files will always be compiled. If we build the project from the command line, it is necessary that the build starts with a file that has links to all other files. Usually I create in the project root the file Build.d.ts, containing ////>, i.e. links to all project files that need to be compiled and passed to the compiler via the console, since This way allows you to control the settings of the TS compiler much more flexibly than the current plugin in the studio, which we will definitely need in the future.

Node.exe and the full TS distribution kit have been added in order not to be tied to the studio and other installed software when getting acquainted with the project. Cmd files for easy launch and integration with the studio I will write later.

Description of the training task

As an educational example, I will write a simple client for a personal messaging system on a site consisting of several independent interface components, some intermediate layer for uploading data to the server and a fake server to simulate hectic activity. Communication with the server will occur through restful services, the quality writing of which is not the main task. Enough for them to work. The system will have 2 screens - a short list of the last 3 messages and a full client screen. There will also be some menu that allows you to switch between them. Authorization, etc. In this article I will not consider.

Screens are completely autonomous modules that do not know about each other, but know about the data access layer. All screens and data access objects are wrapped in separate modules, loaded asynchronously and communicate with each other by publishing and subscribing to events through some event manager, i.e. according to the publisher / subscriber pattern.
Configuring modular loading

Everything is very simple. We add the following code to the App.ts file of the Client project, which we received by default:

export class App { public static Main(): void { alert('Main'); } } 


Create a file RequireJSConfig.ts in the project root:

 /// <reference path="../Lib/require.d.ts" /> /// <reference path="App.ts" /> require(["App"], function(App: any) { App.App.Main(); }); 


Default.htm:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>TSAsyncModulesExampleApp</title> <link rel="stylesheet" href="app.css" type="text/css" /> <script src="js/jquery-1.10.1.min.js"></script> <script src="js/underscore-min.js"></script> <script src="js/backbone.js"></script> <script src="js/require.js" data-main="RequireJSConfig"></script> </head> <body> </body> </html> 


Run the application:

image

Congratulations, we got an application with asynchronously loaded modules.

Let us dwell on what we have done in more detail.

Firstly, it is worthwhile to dwell on how files are compiled in TS. If the file contains an export directive, it means that it will definitely be compiled into a CommonJS or AMD module, depending on the compiler settings. By default, compilation in VS comes in AMD format, which is more than satisfactory in terms of the use of Require.js. TS completely eliminates the need to write the "creepy" manual code of the AMD module wrapper and takes care of the correct installation of dependencies. This is the behavior we see for App.ts:

 define(["require", "exports"], function(require, exports) { var App = (function () { function App() { } App.Main = function () { alert('Main'); }; return App; })(); exports.App = App; }); //@ sourceMappingURL=App.js.map 


RequireJSConfig.ts does not contain export directives and compiles into the usual "flat" JS:

 /// <reference path="../Lib/require.d.ts" /> /// <reference path="App.ts" /> require(["App"], function (App) { App.App.Main(); }); //@ sourceMappingURL=RequireJSConfig.js.map 


Comments remain in the code solely for debugging purposes, since I have a Debug build mode for the application. In Release configuration, everything will be cleared of comments.

But back to our bar ... modules. What happened:

  1. Booted default.htm
  2. Css and statically given js files were loaded, which have no sense, in my subjective opinion, to load asynchronously, since they are needed by almost all modules that we will create.
  3. Among js in clause 2 require.js was loaded
  4. Require.js read the data-main = "RequireJSConfig" attribute value and loaded the corresponding JS file, which is treated as the starting one.
  5. In RequireJSConfig, we use the require method in the global context for the first and last time. Further, all module calls must come from other modules.
  6. In the require function, we say that after loading the App module (the first parameter), you need to call a callback, where to transfer the loaded module as a variable of the same name. Here we make a deal with a conscience and do not type it in TS, because At this point, there is a conflict between the ideology of partitioning into modules in TS and the specific implementations of Require.js, as the manager of asynchronous script loading. More details below.
  7. Require.js loads the App module. You can read about module naming conventions in detail in the documentation for Require.js. In short, we specify the path from the root, which we can specify different from the root of the site, which we use by default, omitting the file extension. Next, RequireJS loads each dependent file as a script tag using head.appendChild (). The loading of the required module occurs last, i.e. after dependencies, which means that we can always be sure that all dependencies are always loaded. Require.js and TS work on this issue in a fully coordinated manner. The syntax and compilation process of TS is specifically adapted for this scenario.
  8. The callback passed to the require method calls the static Main method, the App class, of the App module.
  9. Called alert ('Main');


The callback parameter of the require method is not typed due to the fact that this type is not known in this context to the TS compiler. To type it, you need to use a TS construct of the following form:

 import App = require('App'); 


This design can only be used in the context of a module for loading other modules and leads to the formation of corresponding dependencies in the AMD / CommonJS module wrapper, which we don’t have here, because we have the usual flat code, without wrappers, i.e. we get a contradiction. In other words, TS is fully consistent with Require.js in the area of ​​generating code for modules, but does not support the script loading the first module. Therefore, in this situation, the only thing left for us to do is to roll back to using the good old approach in the style of vanilla JS.

Loading additional modules

So, we loaded the first module, called the Main method, the application started working. According to our training TZ, we should be able to load an arbitrary number of modules. But before you start downloading, you need to understand what a module in TS is:



In fact, the module is all at once. The use of modules is also ambiguous. The only thing that can be said for sure is that the modules break the application into autonomous, reusable components, which fully meets our goals.

Let's create a new Framework / Events.ts file, which we will use as a starting point in the future to implement our abstract event model:

 export interface IEventPublisher { On(); Off(); Trigger(); } export class EventPublisherBase implements IEventPublisher { On() { } Off() { } Trigger() { } constructor() { alert('EventPublisher'); } } 


While all methods are just stubs. The principle of work is important for us.

Change App.ts:

 import Events = require('Framework/Events'); export class App { public static Main(): void { var e = new Events.EventPublisherBase(); } } 


We have a loading directive for the module:

 import Events = require('Framework/Events'); 


The path is indicated from the root. As it was written above, the module is assigned to a variable. Next, we create a new instance of the EventPublisherBase class, from the Events namespace, in which we have declared an alert in the constructor:

At the same time, the Events variable is strongly typed. The compiler strictly keeps track of the types of loadable modules. It should be noted that the compiler does not need a reference directive to control types in modules, but it is necessary for it to compile correctly. Those. the compiler will not be able to track dependencies without it. Now VS does the work for the compiler, substituting file names for it explicitly. This can be solved by creating the above-mentioned Build.d.ts file by running a compilation against it:

 /// <reference path="Framework/Events.ts" /> /// <reference path="App.ts" /> /// <reference path="RequireJSConfig.ts" /> 


An example cmd file (Build-Client.cmd) that can be used for compilation is attached at the root of the solution.

Actually, in terms of asynchronous loading, that's all. Everything is very simple and just works.

I hope my grafomansky opus will be interesting to the community. In the next part of the article I plan to consider the issues of building an abstract event model and the reasons for its use in asynchronous web applications.

Thank you all for reading to the end!

Source: https://habr.com/ru/post/184942/


All Articles