Click here to Skip to main content
15,867,686 members
Articles / Programming Languages / Typescript

Modularizing TypeScript + RequireJS + AngularJS + OWIN (WebAPI + FileServer)

Rate me:
Please Sign up or sign in to vote.
4.56/5 (6 votes)
6 Jan 2015CPOL20 min read 31.8K   13   2
Intro This post shows a way to structure the web application scripts using TypeScript so that a modular approach may be achieved. The associated sample solution is available on GitHub at: https://github.com/omacarena/novaburst.ModularTypeScript and is best to download it to follow along the way.

Intro

This post shows a way to structure the web application scripts using TypeScript so that a modular approach may be achieved.

The associated sample solution is available on GitHub at: https://github.com/omacarena/novaburst.ModularTypeScript and is best to download it to follow along the way.

The purpose is to allow developers to define the application business domains, to have for each such domain a module and each module to have multiple scripts like models, interfaces, services and many others which are also required by Angular applications like bootstrapping, services, controllers, directives, filters.

The following languages will be used for this example:

  • TypeScript
  • JavaScript
  • C#

The following tools/frameworks will be used for this example:

  • Visual Studio 2013 Update 4
  • TypeScript Tools 1.3 (can be downloaded from the bottom of the following page – http://www.typescriptlang.org/ – inside the Tools section)
  • OWIN (Open Web Interface for .NET) with WebAPI and FileServer middleware
  • RequireJS (available via NuGet)
  • AngularJS (available via NuGet)
  • Squishit (available via NuGet) (https://github.com/jetheredge/SquishIt)
  • Web API 2.0 (available via NuGet)

So this sample will be using Visual Studio as the IDE, OWIN for hosting the web application and REST services inside a console application or windows service, RequireJS for loading JS modules, Squishit to minify and bundle modules scripts for Release, Web API for providing REST services and AngularJS for implementing the SPA and connecting to the REST services to fetch data from the server and thus simulate a real application.

NOTE: Though this sample is developed using OWIN with FileServer middleware to serve static files (HTML, css, js, images, fonts, …) this solution is mostly about JavaScript and less related to how you want to serve static files (whether through OWIN, ASP.NET or any other framework). OWIN with FileServer middleware is just the simple version.

Running the Sample Application

The sample application can be run by selecting the project NovaBurst.ModularTypeScript.AppX.Front.Host as the startup project. Also specify a port in App.config that is not reserved. The default port is 12345.

<frontHost url="http://+:12345"; rootDirectory="..\..\..\">

After starting the application this should be available at http://localhost:12345.

Considerations

The reason of structuring is to split an application into its components driven by respective business cases and be able to share these components. Using TypeScript and RequireJS sounds to be a good option and this post show some details that some may run across while trying to obtain a modular and clean development. Why TypeScript? Because it includes classes and modules proposed by Ecmascript 6 as well as other constructs and type safety which improves the overall code quality and development speed. And also there is a large set of typings which adds type safety for many popular libraries out there. Why RequireJS? Because it is a flexible and extensible library for loading modules which enforces a modular development style. TypeScript has a language feature called external module which, if compiled using the AMD option, may produce a RequireJS module with its associated dependencies. This sounds as solved but when trying to achieve a domain-driven development and defining what a module is made of then some problems arise when using TypeScript external modules.

There are some points to consider while finding a way to structure the client-side of a web application while working with TypeScript:

  • there is an 1:1:1 relationship between TypeScript external modules and RequireJS modules and physical files. Therefore the smallest element in an external module is actually the module itself corresponding to one file which cannot be divided into smaller parts. This means that if one domain should be associated to one module then this module (file) will be very large and the development cost will grow over time.
  • it should be ideal to use TS internal modules and try to export these internal modules as external modules which are usable in module loaders like RequireJS. But TypeScript does not allow us to do this and therefore we need to create our own loader that acts like a proxy to use RequireJS.
  • TypeScript compiler is slow. So if multiple files will be compiled into a single file to serve as a module then this process will take some time and the development effort will grow.
  • TypeScript external modules require() use physical paths which need to be fixed at deployment time through RequireJS path configuration.
  • cannot compile multiple external modules files into a single JavaScript file

This post tries to work around these limitations.

Solution Structure

To get it started, the image below show the solution structure and explain each of its components following a complete description of each one.

Supose that the application to be developed is called AppX and the company name is called NovaBurst.

Also the application needs a Front-End in which a Customer can see his data and a list of Products.

Therefore the application can be structured as follows:

SolutionStructure

.Core = core library that may be shared by current application AppX and any other application. Therefore is not aware of AppX business.

.Core.UI = core client side library that may be shared by current application AppX and any other application. Therefore is not aware of AppX business.

.AppX.Core.UI = core client side library usable only in the context of application AppX. Therefore knows of AppX business and is usable in AppX modules.

.AppX.Core.WebApi = core library for building REST services in the context of application AppX. Therefore knows of AppX business and is usable in builing AppX REST services.

.AppX.Sales.UI = client side library for AppX specific domain called Sales. Knows about Customer and Product.

.AppX.Sales.WebApi = REST service for AppX Sales domain. Knows about Customer and Product.

.AppX.Front.UI = client side library for AppX Front-End implementation. Knows about Sales.

.AppX.Front.Host = AppX  Front-End hosting. OWIN hosting for Front-End (all HTML and assets from Core.UI, Sales.UI, Front.UI) and REST services (Sales.WebApi). Knows about Sales.

NOTE: Though in this sample .AppX.Front.Host is used to host, in addition to the Front-End HTML and assets, also the Sales.WebApi REST service, in real-world you may adopt a different strategy for deploying REST services for various domains (maybe in their own host or other solutions indicated by your SOA architect and your specific business cases).

So the application has multiple layers which may be shared between multiple applications or between multiple components of the same application.

Going from top to bottom the layers have more and more business knowledge and become more specialized having knowledge of the upper layers.

Therefore is straight forward to say that:

  • Core may be used server-side in AppX.CoreAppX.Sales and AppX.Front.
  • AppX.Core may be used in AppX.Sales and AppX.Front.
  • AppX.Sales may be used in App.Front.
  • Core.UI may be used client-side on AppX.Core.UIAppX.Sales.UI and AppX.Front.UI.
  • AppX.Core.UI may be used in AppX.Sales.UI and AppX.Front.UI
  • AppX.Sales.UI may be used in AppX.Front.UI

Beyond server-side dependencies which may be solved by referencing the required projects, there are dependencies which needs to be solved on client-side.

Module loaders like RequireJS are made especially for keeping modules disconnected and describing each module dependencies.

As pointed in Considerations section we would be tempted to use TypeScript external modules but there is 1:1 relationship between a module and a physical file.

Because the requirement is to define business domains means that need to be defined also the concerns that the domain has in the form of entities, interfaces, services and also Angular components like controllers, directives, filters, services/factories/providers, boostrapper.

And also there are also views, images, css, fonts which need to be part of a specific domain.

So to wrap it up, the following structure should satisfy the requirements for .UI projects:

  • Content = css, images (also those from NuGet)
  • fonts = fonts, web fonts (also those from NuGet)
  • modules = domain modules
  • scripts = 3rd party scripts (mostly included through NuGet)
  • Views = module Views and templates (HTML)

*.UI modules Folder

This folder contents may be of any form but it needs to specify the structure of current module.

Because there is a 1:1 relationship between one external module and one file it should be simpler to express the module as a collection for TypeScript internal module files and in addition to express dependencies (which scripts are required by a module).

Because this sample will follow an Angular development style, there will be files and folders related to Angular components. So is feasible to consider that a domain has associated a module and that a module has associated an Angular module. Then let the file called module.angular.ts be the declaration and configuration of the Angular module and in case the module is the startup module the module.angular.bootstrap.ts should be the one that bootstraps the entire Angular application.

For example in case of a complex module there may be a folder structure like the one bellow:

  • models folder = entities
  • interfaces folder = service interfaces
  • services folder = Angular services
  • controllers folder = Angular controllers
  • directives folder = Angular directives
  • filters folder = Angular filters
  • module.angular.ts = Angular module initialization file which creates the Angular module used by any file from this module
  • module.angular.bootstrap.ts = Angular application bootstrapper. In case the module is also the starting point of the application.
  • ref.ts = is the file that references any file in current module and any other external reference
  • module.json = defines the contents of the current module and also the dependencies
  • main.js = JavaScript file used, in case this module is a startup module, to configure RequireJS and to load initial scripts.

This structure was written as plain as possible but can be organized in any way to fit the actual business.

The only files that need to stay in place are those that describe the module itself – ref.ts and module.json.

The rest are variable and therefore may appear or disappear.

Bellow is a description of each of the core files:

ref.ts file

Is used only to provide intellisense across modules.

This file does not add any value to JavaScript output.

Modules can be spanned across different Visual Studio projects and therefore this file ensures that all types defined in one project are visible also in another project if referenced.

For example the AppX.Sales.UI has the following contents for ref.ts file:

/// <reference path="../../../NovaBurst.ModularTypeScript.Core.UI/modules/Core/ref.ts" />
/// <reference path="../../../NovaBurst.ModularTypeScript.AppX.Core.UI/modules/AppX.Core/ref.ts" />

/// <reference path="../../../NovaBurst.ModularTypeScript.Core.UI/scripts/typings/angularjs/angular.d.ts" />

/// <reference path="module.angular.ts" />
/// <reference path="models/Customer.ts" />
/// <reference path="models/Product.ts" />
/// <reference path="interfaces/ICustomerService.ts" />
/// <reference path="interfaces/IProductService.ts" />
/// <reference path="services/CustomerService.ts" />
/// <reference path="services/ProductService.ts" />
/// <reference path="controllers/SalesControllerBase.ts" />
/// <reference path="controllers/CustomerController.ts" />
/// <reference path="controllers/ProductController.ts" />

So this module will have intellisense from the Core.UI/modules/Core module and AppX.Core.UI/modules/AppX.Core module:

/// <reference path="../../../NovaBurst.ModularTypeScript.Core.UI/modules/Core/ref.ts" />
/// <reference path="../../../NovaBurst.ModularTypeScript.AppX.Core.UI/modules/AppX.Core/ref.ts" />

Is clear that for enabling intellisense is sufficient to reference only the ref.ts files from other modules.

Also to enable intellisense for Angular, the TypeScript definition (typing) file is also referenced:

/// <reference path="../../../NovaBurst.ModularTypeScript.Core.UI/scripts/typings/angularjs/angular.d.ts" />

Note: Normally all typings will be available in the same Visual Studio project but not cross projects. In current sample only the Core.UI project contains external scripts (Angular, Bootstrap, jQuery, RequireJS, …) and all the other projects reference them from this project. The scripts were added for a single project to prevent errors comming from the fact that the same definition file could come from multiple sources and thus defining the same thing multiple times.

The rest from the ref.ts file are the files included in the actual module.

As stated, the entire file contains only comments and therefore is not adding any value to the JavaScript output of the module.

The file was created only to fix intellisense in other projects by only referencing the ref.ts file instead of referencing all the modules files.

module.json file

This file purpose is to describe the module contents and dependencies.

For example AppX.Sales.UI/modules/AppX.Sales has the following contents:

{
 "dependencies": [ "modules/AppX.Core/module.json", "scripts/angular" ],

 "scripts": [
 "module.angular",
 "models/Customer",
 "models/Product",
 "interfaces/ICustomerService",
 "interfaces/IProductService",
 "services/CustomerService",
 "services/ProductService",
 "controllers/SalesControllerBase",
 "controllers/CustomerController",
 "controllers/ProductController"
 ]
}

The dependencies property show which scripts/modules are required by current module.

So current module will require “modules/AppX.Core/module.json” file and “scripts/angular“.

These are the names used by RequireJS to determine which script/module to load.

Requiring the file modules/AppX.Core/module.json is a way to specify that current module requires the AppX.Core module by pointing to its module.json file.

In fact what will happen is that the JSON file will be used to obtain the scripts that will be passed to RequireJS in addition to any dependencies recursively.

Lastly the scripts/angular will point to Angular script meaning that current module requires AngularJS.

Client-Side Application Startup

The picture will become more clear after understanding the steps followed to start the application:

  • User requests the NovaBurst.ModularTypeScript.AppX.Front.UI/Views/Index.html which is the main HTML for current SPA (Single Page Application).
  • the requested page will load RequireJS and a file named main.ts used to configure the startup.
  <script src="scripts/require.js"></script>
 <script src="modules/AppX.Front/main.js"></script>
  • the main.ts file determine whether to load in debug mode by checking a query string param named debug using a Regex:
window['isDebugMode'] = new RegExp('(\\?|(\\?.*&))debug(|&|=)').exec(document.URL) ? true : false;
  • next whether the app is in debug mode RequireJS will be configured accordingly. This means that in Debug mode will be loading the un-minified version of scripts and in Release will use the minified versions. This is achieved simply by using the RequireJS path configuration. Note that there is also a RequireJS shim configuration and this was added to fix the dependencies between the 3rd party scripts. The shim configuration is used for non-module scripts, this means for scripts which does not use define() to set themselves as modules. For more information on RequireJS and configuration read the short documentation http://requirejs.org/docs/api.html#config-shim.
  • next the following script will load the initial module which is the AppX.Front.
  require(['modules/Core/Module/moduleLoader!AppX.Front'],
 // success
 function () { },
 // error
 function () { });

For short what is happening here is that the AppX.Front module is been loaded by RequireJS but with the helper of a loader plugin which in this case is called modules/Core/Module/moduleLoader (the name before the exclamation mark). Additional information will be found in the next section named RequireJS Module Loader Plugin. The loader plugin will load the requested module and any other dependencies included in the module.json file and its dependencies and so on recursively until the entire chain is loaded. So the AppX.Front requires AppX.Sales which require AppX.Core which require Core. In RequireJS module loading is done async.

  • because of the dependencies, AppX.Front is loaded only after the following modules are loaded in order: Core, AppX.Core and AppX.Sales.
  • next the AppX.Front module gets loaded and the following scripts get executed in order
    • module.angular = defines the NovaBurst.ModularTypeScript.AppX.Front module and configures the Angular application by configuring routing (ngRoute)
    • controllers/FrontController = will be used as main controller for the main page after Angular bootstrap (see bellow).
    • module.angular.bootstrap = start Angular application (bootstrap). Note that in Index.html no module was specified for ngApp because when the index.html is loaded into the browser the AppX.Front module is not yet loaded. Therefore the bootstrap stage is deferred at the right moment inside this script.
  • at this point all modules are loaded and the Angular application is running with all bindings

RequireJS Module Loader Plugin

RequireJS loader plugin helps to customize module loading in RequireJS.

Loader plugins are documented at http://requirejs.org/docs/api.html#plugins.

Why such plugin was required? Because of the complication induced by the fact that TypeScript internal modules cannot be expressed as TypeScript external modules. So to overcome this, every module was placed under modules folder and for every module module.json was added to specify its dependencies and contents.

Because of this custom approach of expressing a module also a module loader needed to be created to load every module file one after another but not before loading its dependencies. So the module loading is async by nature because each script and module.json file need to be fetched from the server.

There are two problems that the loader plugin solves in this case:

  • such a custom module may be injected in RequireJS modules by using the format <loader_plugin>!<module> (question mark between loader plugin name and module name to load)
  • can tell exactly when module loading finishes by calling a loader plugin callback method

The module loader source is located at NovaBurst.ModularTypeScript.Core.UI/modules/Core/Module/moduleLoader.ts.

Writing a module loader in RequireJS is very simple and is a simple load() function returned within an object from a define() function:

<script src="https://gist.github.com/0948fd86044606d92399.js"></script>

So the moduleLoader is in fact a TypeScript external module which renders the define() function that returns an object containing the load function (specified in export list).

Therefore by using instead of require([‘AppX.Front’], …) the loader plugin as require([‘modules/Core/Module/moduleLoader!AppX.Front’], …) the AppX.Front is loaded correctly. Also this syntax is available in define() to inject a module in another module definition.

So after driving away from TypeScript external modules and thus RequireJS by going TS internal module way, the loader plugin brings the actual solution close to RequireJS.

In RequireJS documentation, non-modular approach is referred as the old approach, the modern approach is defining modules and dependencies with define() and obtaining modules with require(). So some might say that current approach is a mixture between old and new.

Angular JS TypeScript Design Guidelines

Writing Angular components can also benefit from TypeScript.

An example should be the NovaBurst.ModularTypeScript.AppX.Sales.UI project organized as follows:

AppX.Sales.UI

So this module has models, controllers, interfaces and services.

What can be achieved with TypeScript is a much clearer coding style:

  • In Customer.ts the the Customer is modeled as a class having a reference to a Person defined in AppX.Core module.
  • CustomerService and ProductService:
    • are classes
    • implement the service interface ICustomerService/IProductService
    • constructors parameters correspond to the actual dependencies injected by Angular
    • services are registered simply by pointing to the class constructor:
angular.module(angularModuleName).service('ProductService', ['EntityService', ProductService]);
  • CustomerController and ProductController
    • extend the SalesControllerBase class which provide the base functionality for all Sales controllers
    • it uses protected access modifier (available from TypeScript 1.3) for its dependencies to allow visibility only in the inheritance chain
    • constructors parameters correspond to the actual dependencies injected by Angular
    • controllers are registered simply by pointing to the class constructor:
angular.module(angularModuleName).controller('ProductController', ['$scope', '$location', 'ProductService', ProductController]);
  • there is a clear separation of concerns. For example handling Customer entity is done using separate service and controller than handling Product entity. Also the Views folder has separate views Customer.html and Products.html for each concern. Also services like ProductService and CustomerService have only the role to offer operations for Product/Customer. On the other hand controllers like ProductController and CustomerController use those services to provide the user experience. Also a scenario may get as complex as possible while keeping everything granular.
  • another code reusability may be achieved from another concern: the interaction with a REST services. This concern is built in the Core module in the form of EntityService which is an Angular service. Therefore this service may be injected in Angular components like controllers, services, directives, filters. In this sample the service has the ability to issue GET requests to get, filter and page data. The structure from Core.UI project looks as follows:

Core.UI.REST

So regarding listing an entity the request can be expressed as a ListOptions which data to retrieve like only the count of entities (fetchCount) or a sorted list of entities using PagingOptions and OrderOptions. All these are passed to IEntityService operations which perform the actual request.

Server-Side

In this sample the only concerns the server-side have are serving static files (scripts, HTML, css, images, fonts, …) and exposing REST services for manipulating Customer and Product entities.

The project which hosts REST services and serving static files is: NovaBurst.ModularTypeScript.AppX.Front.Host which can start as a Console Application or Windows Service. In any case it starts the OWIN host which adds the FileServer middleware for hosting static files and the WebAPI middleware for hosting REST services.

Also note that in most cases the models and services found on the server-side can be found also on client-side. This is normal because the services from client-side act like a proxy for the services from the server-side.

The startup steps are as follows:

  • start NovaBurst.ModularTypeScript.AppX.Front.Host application whether by installing as a Windows Service or starting as a Console Application
  • next the Program.cs :: Main() method will be called and decide which approach to follow (Windows Service or Console Application):

<script src="https://gist.github.com/335c5f0a0d76c276cd85.js"></script>

  • In each case the OWIN host will be started on the configured URL using the Startup class:
WebApp.Start<Startup>(config.Url)
  • Startup class will do the following:
    • bundle and minify TS internal modules which files are found from each of the module module.json file that contains the module contents and its dependencies
    • configure File Server
    • configure Web API

<script src="https://gist.github.com/564f4a896b9d118775b2.js"></script>

  • there are some points to note about File Server middleware configuration:
    • because the application is split among multiple modules, this means that the File Server middleware needs to serve files from multiple folders. In IIS this should be done using virtual folders but in self hosted OWIN environment this may be done only using PshysicalFileSystem to provide files to the File Server. The problem with the PhysicalFileSystem is that it can provide files only within a folder. To overcome this limitation I developed a new file system called AggregateFileSystem which uses a collection of IFileSystem to provide the files thus achieving serving files from multiple locations. The file system is located in NovaBurst.ModularTypeScript.Core\Web\Owin\FileSystems folder.
    • before using the File Server middleware, default files are set using the Default Files middleware (app.UseDefaultFiles(…)). This is used to specify the default documents and in this case the “views/index.html” which is the main HTML file that will load require.js and other modules and have Angular applied on it.
    • next the File Server middleware is used.
    • Note that both the Default Files and File Server middlewares are configured with the same AggregateFileSystem so that they can serve files from the same location.
  • also there are some points to note about Web API middleware configuration:
    • httpConfig.MapHttpAttributeRoutes() is used to enable routing attributes –  RouteAttribute and RoutePrefixAttribute for Web API controllers
    • httpConfig.Formatters.Remove(httpConfig.Formatters.XmlFormatter) – is used to remove the XML formatter so that if we make a request directly from the browser to receive always JSON instead of XML. The XML response is received because each browser issues a list of accepted files via the Accept header (text/xml and application/xml). In WebAPI the Accept header is used for content negotiation to determine which formatter to use for rendering the response.
    • the following enable camel casing when serializing to JSON the DTO property names – jsonSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    • the following has two roles – NovaBurst.ModularTypeScript.AppX.Sales.WebApi.WebApiConfig.Configure(httpConfig);
      • one role is to further configure the Web API if is anything to configure in addition to what was already configured
      • another role is to load the referenced assembly into the application domain. This is required because Web API uses reflection to find ApiControllers. The AppX.Sales.WebApi project is referenced in the host project but when using reflection will not be automatically loaded into AppDomain and the controllers will not be found thus obtaining a 404 Not Found Error. Using at least one line from the referenced assembly automatically loads the assembly into AppDomain and reflection finds all its types.
    • app.UseWebApi(httpConfig) – is using the Web API middleware to add Web API functionality to the application after all these configurations have been done
    • httpConfig.EnsureInitialized() - is used to throw any error as part of Web API initialization. Therefore this method ensures that any initialization error should be thrown at this point instead of being deferred to a later time and blow up unexpectedly.

After these steps are completed the application is ready to run.

Other Considerations

Other things to consider when having this mixture of technologies:

  • bundling multiple RequireJS modules or TypeScript external modules require to use the RequireJS optimizer so that the module name is included into define() statement before bundling. This is because each module name is determined from file name but one file may hold multiple modules by including the name in the define(). See: http://requirejs.org/docs/optimization.html. This will require running Node.js with Grunt or Gulp.
  • there are many bundling tools for ASP.NET with IIS hosting but very few documented that support OWIN and that actually work (eg: Squishit). There are always Node.js counterparts. Also not all tools generate source maps and thus being unable to debug in the browser by using un-minified scripts when the minified script was loaded.
  • unit testing (eg: Protractor for Angular) and other tasks related to client-side development (eg: ng-annotate for Angular, r.js for RequireJS) may be done mostly with Node.js.
  • if the application client-side is not implemented using ASP.NET and is just a collection of static files then a good option as an IDE for client-side is also WebStorm from JetBrains. Check its many features and if you own a Pluralsight account there is a nice and through presentation on http://www.pluralsight.com/courses/webstorm-fundamentals. This IDE might be much comprehensive at this time in respect to web client-side development than Visual Studio. It has Node.js, TypeScript, CSS, LESS, SASS, HTML, Angular integration, can benefit from TypeScript typings, has refactoring, intellisense and integration with many libraries.


License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 3 Pin
Dumi7-Jul-15 3:58
Dumi7-Jul-15 3:58 
QuestionToo complex for teaching Pin
frazGJF13-Jan-15 18:00
frazGJF13-Jan-15 18:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.