📜 ⬆️ ⬇️

Beginning the translation of the “legacy” of the Angular JS project to the Angular 1.5 Components / ES6 and TypeScript

In the article, I wanted to share a version of the beginning of the gradual migration of the Angular JS project “legacy” to all the good things that Angular 1.5 gave us and a bunch of ES6 / TypeScript.

So, it’s given: a standard project, the development of which began even on the bearded Angular 1.2 (a person far from the world of the frontend), presented in a more or less standard form - modules for routs, services, directives and incredibly fat controllers, the functionality of which slowly stand out in separate directives. Hell of a flow of features to implement, the complete absence of models, access to objects and their modifications - as God would per capita.

Also, the project already has a more or less well-established and prescribed process for assembling / minifying and deploying all this stuff with the help of gulp, CI, and so on.
')
The task is not to go into the support of the project in the form in which it is, to start writing good, supported code, learn something new.

Introductory


Just arrived Angular 1.5, who presented the "components" and after a certain amount of read manuals on various related topics (including migration 1.3 -> 1.4, 1.4 -> 1.5, 1.x -> 2) such points were adopted as a program for the foreseeable future :

  1. Old functionality for the time being just do not touch
  2. We immediately write the functional in the form of a component (we store styles, patterns and tests in the same place where the code of a specific component)
  3. We are not at all embarrassing to use features from ES6 / ES7
  4. We write on Typescript
  5. We rework the old functionality in a new way as it receives tasks large enough to refactor.

Now you need to decide on the strapping.

Browsers at this stage of development do not support ES6 imports, which means that in order to use them (and I wanted more “neutive”), you need to build the project under the browser by one of the “collectors”. After some research, the choice fell on the webpack - its ideology is perfectly combined with the ideology of the components and allows you to connect the necessary templates and styles directly from the component code.

After a couple of months, it was updated from a stable 1.x to 2 (beta). Version 2 has some very important innovations - first of all, this is native support for ES6 Imports (including the gradual loading of parts of the code as the client needs in this code).

For the webpack, we will need several “loaders” - these are some kind of middleware, giving the webpack how to add this or that file to the assembly. I have this set relatively modest:


Even with the webpack almost bundled webpack-dev-server is supplied, which allows you to greatly accelerate the recompilation with changes - this is a local server that distributes the usual statics from this directory, and the code collected by the webpack keeps, reassembles and distributes directly from memory.

We also need the TypeScript compiler itself. After the same couple of months, it was updated with beta 2.0 to beta 2.0, mainly due to the fact that 2.0 allows you to set the base url for all imports, finally get rid of the prevalence of relative paths within the files and replace a bit depressing:

 import IConversation from "../../../interfaces/IConversation"; import Conversation from "../../../models/Conversation"; import Interaction from "../../../models/Interaction"; import NotificationService from "../../../helpers/NotificationService"; 

On quite enterprise:

 import IConversation from "interfaces/IConversation"; import Conversation from "models/Conversation"; import Interaction from "models/Interaction"; import NotificationService from "helpers/NotificationService"; 

We will also probably need a transpiler (this is like a compiler, only to compile ES6 to ES5) in order for our most modern code to work normally for the most ordinary users of the service. The most popular now is Babel JS . Of course, you can use the TypeScript compiler directly as a transspiler, but it does worse than babel (async / await, for example, typescript does not transpaylit, as far as I know), so I decided to compile TypeScript in ES6 and then using Babel and preset es2015 -webpack (this is a special preset for webpack 2, it doesn’t convert ES6 Imports to CommonJS, as the webpack can now build ES6 Imports by itself).

We will also need a TypeScript Definition Manager (formerly tsd. Many articles recommend tsd, but tsd is already deprecated and as such asks to use the typings project instead).

So let's get started.

Install and configure the environment


First of all, install all of the above:

 npm install --save-dev webpack@2.1.0-beta.20 typescript@2 less-loader raw-loader style-loader typescript typings webpack webpack-dev-server babel-runtime babel-preset-es2015-webpack babel-polyfill babel-plugin-angularjs-annotate babel-loader babel-core awesome-typescript-loader 

Probably typescript, webpack and typings will have to be installed globally in order to work comfortably.

We will also need to install all the necessary definitions for our comfortable work with typescript:

 typings install angular --source=dt --global --save 

And maybe

 typings install jquery --source=dt --global --save 

And all that is still used there.

The result of these commands will be the typings.json file created in the current directory, which will later restore all your typings by calling the command

 typings install 

Those. This is a similar analog of the lock-file or package.json for the definition manager. This file must be added to the repository. There will also be a typings folder with the actual downloaded definitions for use in typescript. (you can add it to .gitignore and make typing call a part of the project build)

Next, let's start writing for all this configs.

Configurations


./declarations.d.ts
It is used for the same purpose for which definitions from typings are used, but contains those interfaces that were not found in the typings manager's repositories. I have there for example

 declare function require(name: string): any; // used by webpack declare let antlr4: any; declare let rangy: any; 

Of course, with any - I was too lazy, in theory there is a need to completely describe the interface and then you will have the correct autocomplete for these objects in your IDE and, most importantly, validation of using the methods / properties of these objects at the compilation stage. This is for me todo, so to speak.

Require is mandatory here, otherwise your webpack code will not be collected.

./tsconfig.json
This is the typescript compiler configuration.

 { "compilerOptions": { "target": "ES6", "sourceMap": true, // for debug "experimentalDecorators": true, // decorators support, see ts reference "baseUrl": "./path/to/your/app" // url that will be 'root' for imports }, "files": [ "declarations.d.ts", // declarations file from previous point "typings/index.d.ts" // declarations, downloaded by definition manager ] } 

The files array declared here allows us not to write terrifying in every file

 /// <reference path="..." /> 

Well, as you can see in this config there is no outfile and any significant project files. Just because we give these questions to the webpack.

./webpack.config.js
Actually webpack configuration.

 'use strict'; var path = require('path'); var webpack = require('webpack'); var TsConfigPathsPlugin = require('awesome-typescript-loader').TsConfigPathsPlugin; // plugin to work with typescript base path. Skip it if you don't need this. var babelSettings = { plugins: [['angularjs-annotate', {'explicitOnly' : true}]], //explicitOnly here to disallow auto-annotating of each function. Skip it if you need automatioc anotation presets: ['es2015-webpack'] }; module.exports = { module: { loaders: [ { test: /\.tsx?$/, loader: 'babel-loader?' + JSON.stringify(babelSettings) + '!awesome-typescript-loader', }, {test: /\.html$/, loader: 'raw'}, {test: /\.less$/, loader: 'style!css?sourceMap!less?sourceMap'} ] }, entry: { components: './path/to/your/app/components/components.ts' // entry1: './path/to/your/app/components/entry1/entry1.component.ts' // entry1: './path/to/your/app/components/entry2/entry2.component.ts' // models: './path/to/your/app/models/models.bundle.ts' // ...whatever you want }, resolve: { extensions: ['.ts', '.js', '.html', '.css', '.less'], alias: { // lessWebApp: path.join(__dirname, '/path/to/your/app/less') - whatever you want to be used in your code }, plugins: [ new TsConfigPathsPlugin() ] }, devtool: 'source-map', output: { path: path.join(__dirname, 'path/to/your/build/js/bundles'), publicPath: '/js/bundles', filename: '[name].bundle.js' }, plugins: [ // new webpack.optimize.CommonsChunkPlugin({ name: 'common', filename: 'common.bundle.js' }) - use this to move out common chunks to one separate chunk ], devServer: { contentBase: path.join(__dirname, 'path/to/your/build/'), publicPath: '/js/bundles/' } }; 

So…

Let's go directly to


Here it is worth describing a few more points. In our standard directory structure of the application, among all these directives / controllers / modules we have created a new component (as well as models, helpers, etc ...), in which the components themselves will live. The base file in this directory is the components.ts file, it imports our components from subdirectories. We use this file as an entry-point for the webpack. It looks something like this:

./path/to/your/app/components/components.ts
 // component-based modules with their own routes import Module1 from "components/module1/module1"; import Module2 from "components/module2/module2"; // ts helpers and services import AnnotateHelper from "services/AutoMarkupService"; import ParserHelper from "helpers/ParserHelper"; // .... // not organized in modules components import AgentAvatarComponent from "components/agent/agent_avatar/agentAvatar.component"; import StaticInfoComponent from "components/shared/static_info/staticInfo.component"; // .... let componentModule = angular.module('api.components', [ Module1.name, Module2.name // .... ]); componentModule .component(AgentAvatarComponent.name, AgentAvatarComponent) .component(StaticInfoComponent.name, StaticInfoComponent) // .... .factory('ParserService', () => ParserHelper) // for static helpers // .... .factory('autoMarkupService', AutoMarkupService.getInstance); // for helpers that handles something inside export default componentModule; 

It is necessary to add that there may be (and should be) more than one entry point, otherwise, as the project grows, everything will start awfully long to compile. Above in the webpack config you can see how several entry-points are set. Well, yes, they should not intersect over imports. If there is something common (for example, models), then this common thing also needs to be allocated to a separate bundle and not to forget about optimization , common chunks and the ability to organize lazy loading of scripts.

It is quite clear that the same ParserHelper from this example - just a class with static methods - can be imported into a ts-file directly, without using the Angulyarovsky DI (which is often nice), but here it is registered as a factory to ensure backward compatibility with the legacy part of the application. . Those. This is one of the services that have already been rewritten to ts. But in AutoMarkupService we already want to store some state, or maybe we just need a standard Angulyarovsky DI there. And therefore for its registration in an angulyar we use a simple pattern with getInstance:

./path/to/your/app/services/AutoMarkupService.ts
 import IHttpService = angular.IHttpService; import IPromise = angular.IPromise; import Model from "models/Model"; export default class AutoMarkupService { private static instance: AutoMarkupService = null; public static getInstance($http, legacyUrlConfig) { /*@ngInject*/ if (!AutoMarkupService.instance) { AutoMarkupService.instance = new AutoMarkupService($http, legacyUrlConfig); } return AutoMarkupService.instance; } constructor(private $http: IHttpService, private legacyUrlConfig: {modelUrl: string}) { // do something } public doSomething(): IPromise<Model> { return this.$http.get(this.legacyUrlConfig.modelUrl); } } 

In an amicable way, all this needs to be redone to some kind of base class or immediately to a decorator.

Now for the components themselves:

First of all, we need a very small decorator, which will greatly facilitate our work:

./path/to/your/app/helpers/decorators.ts
 // .... export const Component = function(options: ng.IComponentOptions): Function { return (controller: Function) => { return angular.extend(options, {controller}); }; }; // .... 

And now carefully look at what can be done with all that we have already built

./path/to/your/app/components/shared/static_info/staticInfo.component.ts
 import {Component} from "helpers/decorators"; require('./staticInfo.style.less'); @Component({ bindings: { message: "@" }, template: require('./staicInfo.template.html'), controllerAs: 'vm' }) export default class StaticInfoComponent { public message: string; /** * here you can put any angular DI and it will work */ constructor() { // this.message -> undefined } /** * function that will be called right after constructor(), * but in constructor() you will not have any bindings applied and here - will be */ $onInit() { // this.message -> already binded and working. } } 

Notice that here through require we connect the template and style. This require is for the webpack, after the build, instead of require, in this place there will be the actual css and html in text form. Well, or (depending on the webpack settings) they will be somewhere in other files, but by the time this function is called, they will definitely be loaded.

The same important point about $ onInit is that while you are in es2015 it is in fact not needed. In es2015, there are no classes yet and all this has been transferred to the object and by the time the constructor is called, all the binding has already been transferred. But you only need to change the preset to es2016 or even throw out Babel (for example, for ease of debugging), as everything stops working. $ onInit is generally an Angular standard callback.

How to collect all this and make it work


After all the preparatory stages, it remains only in the root directory (where we have all of our package.json, tsconfig.json, webpack.config.js, etc.) run

 webpack 

According to the results of the work, a .js file will be collected in the directory specified by the webpack config file, which should be included on your .html page along with all the others (or add special automation to your build, which will be collected and minimized on the same page as all others).

Team

 webpack -w 

It will launch the webpack in watcher mode and rebuild everything with every change to ts or related html and less.

Team

 webpack-dev-server -w 

It will launch webpack-dev-server, which will give the usual statics (in our case, this is the “legacy” part of the application) from the addresses specified in the config, and the part for which the webpack is now responsible is kept in memory and recompiled very quickly.

Some more hints


  1. Starting a webpack build can be easily added to your main build process (for example, in some gulp build). With us, it looks something like this:

    ./gulp/tasks/scripts.js
     // .... // use webpack.config.js to build modules gulp.task('webpack', "executes build of ts/es6 part of application", function (cb) { if (shared.state.isSkipWebpack) { console.log('Skipping webpack task during watch. Please use internal webpack watch'); return cb(); } let config = require('../../webpack.config'); webpack(config, function (err, stats) { if (err) { console.log('webpack', err); } console.log('[webpack]', stats.toString({ chunks: false, errorDetails: true })); cb(); }); }); // .... 


  2. If it seems to you that the webpack is slow, it's time to optimize the build. CommonChunks plugin, splitting a project into many different logical bundles, tuning cache settings, using dev-server in the end.

  3. Also, if your frontend is non-detachable from the backend and it seems that for this reason using webpack-dev-server is impossible, just know that one of the standard webpack-dev-server components is node-http-proxy and correspondingly in a couple of movements. in the direction of changing the config, you can configure a proxy, which will redirect your requests to it ... for example, to your staging server. Or somewhere else.

  4. Babel is a very powerful tool that is almost completely beyond the scope of this article. It includes a huge number of plug-ins that you can use on your project.

  5. To facilitate the work of people who are not involved in the development of frontend, or to simplify containerization, or for something else, you can create simple helpers to run all the necessary applications. This will not put them globally.

    ./runners/typings
      #!/bin/sh "node/node" "node_modules/typings/dist/bin.js" "$@" 

    ./runners/webpack
      #!/bin/sh "node/node" "node_modules/webpack/bin/webpack.js" "$@" 


    etc. (yes, by the way, our node is also put locally in our project directory, next to node_modules)

    And run the packages installed in node_modules, not globally. This is very useful if you, for example, build a project with some kind of build system, you have some npm there and in order not to put the rest globally, in this build system you can call the necessary commands in this way:

     ./runners/typings install 

  6. Write tests and documentation.

  7. Smoking will kill.

the end


Here you go. In general, the final (to date) construction looks something like this, these are probably the results of the month for some very inconsistent reading of various articles devoted to this topic, most of which are somewhat outdated (for example, how to get beaten from the reference path and relative import paths in they were not written) and dozens of different experiments (this is not the first or second iteration, all mostly in their free time - on which you have to cut features - and personal time, of course). I hope someone this excursion will be useful. I am also waiting for critics and suggestions for improving everything that I’ve pondered here (after all, in fact, I’m not really a frontend developer and probably missed a lot). Thank.

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


All Articles