- PVSM.RU - https://www.pvsm.ru -
В статье я хотел поделиться уже работающим в продакшене вариантом начала постепенной миграции «legacy» Angular JS проекта на все хорошее, что дал нам [1] Angular 1.5 и связку ES6/TypeScript.
Итак дано: стандартный проект, разработка которого началась еще на бородатом Angular 1.2 (человеком, далеким от мира фронтенда), представленный в более или менее стандартном виде — отдельно по директориям сгруппированы модули с роутами, сервисы, директивы и невероятно жирные контроллеры, функционал из которых потихоньку выделяестся в отдельные директивы. Адский поток фич к реализации, полное отсутствие моделей, доступ к объектам и их модификации — как бог на душу положит.
Также в проекте уже присутствует более или менее налаженный и прописанный процесс сборки/минификации и деплоя всего этого добра при помощи gulp, CI и прочее.
Задача — не уйти в себя на поддержке проекта в таком виде, в каком он есть, начать писать хороший, поддерживаемый код, научиться чему-то новому.
Как раз подоспел Angular 1.5, представивший «компоненты» и после некоторого количества прочитанных мануалов по различным смежным темам (включая миграцию 1.3 -> 1.4, 1.4 -> 1.5, 1.x -> 2) в качестве программы на обозримое будущее были приняты такие пункты:
Теперь нужно определиться с обвязкой.
Браузеры на данном этапе развития не поддерживает ES6 imports, а значит чтобы использовать их (а мне хотелось больше «нейтива»), нужно собирать проект под браузер одним из «сборщиков». После некоторых изысканий выбор пал на webpack [2] — его идеология отлично сочетается с идеологией компонент и позволяет прямо из кода компонента подключать необходимые шаблоны и стили.
Спустя пару месяцев он был обновлен со стабильной 1.x до 2 (beta). Версия 2 имеет несколько очень важных нововведений [3] — в первую очередь это нативная поддержка ES6 Imports (в том числе и постепенная подгрузка частей кода по мере появления нужды клиента в этом коде).
Для вебпака нам нужно будет несколько «лоадеров» — это эдакие middleware, дающие вебпаку понять каким именно образом добавлять в сборку тот или иной файл. У меня этот набор относительно скромный:
.factory('serviceId', ['depService', function(depService) {
// ...
}])
начать писать более или менее человечное:
.factory('serviceId', function(depService) {
/*@ngInject*/
// ...
})
Еще с вебпаком почти в комплекте поставляется webpack-dev-server, который позволяет очень сильно ускорить перекомпиляцию при изменениях — это локальный сервер, который раздает обычную статику из данной директории, а код, собираемый webpack-ом держит, пересобирает и раздает прямо из памяти.
Так же нам понадобится собственно компилятор TypeScript [5]. Он спустя те же пару месяцев вместе с webpack был обновлен до беты 2.0, в основном из-за того, что 2.0 позволяет задавать базовый url для всех импортов, избавиться наконец от засилия относительных путей внутри файлов и заменить немного удручающее:
import IConversation from "../../../interfaces/IConversation";
import Conversation from "../../../models/Conversation";
import Interaction from "../../../models/Interaction";
import NotificationService from "../../../helpers/NotificationService";
На вполне энтерпрайзное:
import IConversation from "interfaces/IConversation";
import Conversation from "models/Conversation";
import Interaction from "models/Interaction";
import NotificationService from "helpers/NotificationService";
Еще нам вполне вероятно понадобится транспайлер (это как компилятор, только для компиляции ES6 в ES5) для того чтобы наш самый современный код нормально работал у самых обыкновенных пользователей сервиса. Самый популярный сейчас — это Babel JS [6]. Конечно можно в роли траспайлера использовать непосредственно компилятор TypeScript, но он некоторые вещи делает хуже babel'a (async/await, например, typescript не транспайлит, насколько мне известно), поэтому я решил компилировать TypeScript в ES6 и потом с помощью Babel и пресета es2015-webpack (это специальный пресет для webpack 2, он не превращает ES6 Imports в CommonJS, т.к. webpack теперь умеет собирать ES6 Imports сам по себе).
Еще нам понадобится TypeScript Definition Manager [7] (бывший tsd. Многие статьи рекомендуют ставить tsd, но tsd уже deprecated и сам по себе просит использовать вместо него проект typings).
Итак, давайте уже приступим.
В первую очередь установим все вышеописанное:
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
Вероятно typescript, webpack и typings придется установить еще и глобально для того, чтобы удобно работать.
Также нам нужно будет установить все нужные для нашей комфортной работы с typescript definition'ы:
typings install angular --source=dt --global --save
А возможно и
typings install jquery --source=dt --global --save
И все такое, что там у вас еще используется.
Результатом выполнения этих команд будет созданный в текущей директории файл typings.json, который впоследствии восстановит все ваши typings'ы по вызову команды
typings install
Т.е. это эдакий аналог lock-файла или package.json для definition manager'а. Этот файл надо добавить в репозиторий. Также появится папка typings с собственно скачанными definition'ами для использования в typescript. (ее можно добавить в .gitignore и сделать вызов typings install частью сборки проекта)
Далее давайте уже начнем писать для всего этого конфиги.
./declarations.d.ts
Используется для того же самого для чего используются definition'ы из typings, но содержит те интерфейсы, которых не нашлось в репозиториях typings manager'а. У меня там например
declare function require(name: string): any; // used by webpack
declare let antlr4: any;
declare let rangy: any;
Конечно с any — это я поленился, по идее там надо полностью описать интерфейс и тогда у вас появится корректный автокомплит по этим объектам в вашей IDE и, что самое главное, проверки корректности использования методов/свойств этих объектов на этапе компиляции. Это для меня todo, так сказать.
require здесь обязателен, иначе ваш код для вебпака просто не будет собираться.
./tsconfig.json
Это конфигурация компилятора typescript
{
"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
]
}
Объявленный здесь массив files позволяет нам не писать в каждом файле ужасающий
/// <reference path="..." />
Ну и как видно в этом конфиге отсутствует outfile и любые значащие файлы проекта. Просто потому что мы отдаем эти вопросы на откуп webpack.
./webpack.config.js
Собственно конфигурация webpack.
'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/'
}
};
Итак…
Тут стоит описать еще несколько моментов. В нашей стандартной структуре директорий приложения, среди всех этих directives/controllers/modules мы создали новую — components (а также models, helpers, etc...), в которой собственно и будут жить компоненты. Базовым файлом в этой директории является файл components.ts, в него импортятся наши компоненты из поддиректорий. Этот файл мы и используем в качестве entry-point для webpack. Выглядит он как-то так:
./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;
Надо добавить что точек входа может быть (и должно быть) больше одной, иначе у вас просто с ростом проекта все начнет ужасно долго компилироваться. Выше в конфиге вебпака можно увидеть как устанавливается несколько entry-points. Ну и да, они не должны пересекаться по импортам. Если есть что-то общее (например модели) — то это общее так же нужно выделять в отдельный бандл и не забывать про оптимизацию [8], common chunks и возможность организовать ленивую загрузку скриптов.
Вполне понятно что тот же ParserHelper из этого примера — просто класс со статическими методами — может быть импортирован в ts-файл напрямую, без использования ангуляровского DI (что зачастую приятно), но здесь он регистрируется как фабрика для обеспечения обратной совместимости с legacy-частью приложения. Т.е. это один из уже переписанных на ts сервисов. А вот в AutoMarkupService мы уже хотим хранить какое-то состояние, или может быть нам просто нужен там стандартный ангуляровский DI. И потому для его регистрации в ангуляре используем нехитрый паттерн с 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);
}
}
По-хорошему это все надо переделать на какой-то базовый класс или сразу на декоратор.
Теперь что касается самих компонентов:
В первую очередь нам понадобится совсем небольшой декоратор, который сильно облегчит нам работу:
./path/to/your/app/helpers/decorators.ts
// ....
export const Component = function(options: ng.IComponentOptions): Function {
return (controller: Function) => {
return angular.extend(options, {controller});
};
};
// ....
А теперь внимательно смотрим на то, что можно сделать со всем тем, что мы уже понастраивали
./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.
}
}
Заметьте что тут через require мы подключаем шаблон и стиль. Этот require — для webpack, после сборки вместо require в этом месте будут собственно итоговый css и html в текстовом виде. Ну или (в зависимости от настроек webpack) они будут где-то в других файлах, но к моменту вызова этой функции — уже точно будут загружены.
Так же важный момент насчет $onInit — пока вы транспайлите в es2015 он фактически не нужен. В es2015 еще нет классов и все это транспайлится в объект и к моменту вызова constructor все биндинги уже переданы. Но стоит только поменять пресет на es2016 или вовсе выкинуть Babel (для простоты отладки, например), как у вас все перестанет работать. $onInit — это в общем стандартный ангуляровский callback.
После всех подготовительных этапов осталось только в корневой директории (там, где у нас лежат все наши package.json, tsconfig.json, webpack.config.js и прочее) запустить
webpack
В директорию, указанную конфиге webpack по результатам работы соберется .js файл, который нужно наравне со всеми прочими включить в вашу .html-страницу (или добавить к вашей сборке специальную автоматику, которая будет этот файл собирать и минимизировать наравне со всеми прочими).
Команда
webpack -w
Запустит webpack в режиме watcher'а и будет пересобирать все при каждом изменении в ts или связанных с ними html и less.
Команда
webpack-dev-server -w
Запустит webpack-dev-server, который будет отдавать обычную статику (в нашем случае это «legacy» часть приложения) с указанных в конфиге адресов, а часть, за которую теперь отвечает вебпак, держать в памяти и очень быстро перекомпилировать.
./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();
});
});
// ....
./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. (да, кстати, нода у нас нашим сборщиком также ставится локально в директорию проекта, рядом с node_modules)
И запускать пакеты, установленные в node_modules, а не глобально. Это очень полезно если вы, например, собираете проект какой-то системой сборки, у вас там есть какой-то npm и вот чтобы не ставить глобально остальное, можно в этой системе сборки вызывать нужные команды таким вот образом:
./runners/typings install
Ну вот. В общем итоговая (на сегодняшний день) конструкция выглядит как-то вот так, это результаты где-то наверное месяца весьма непоследовательно чтения различных посвященных этой теме статей, большая часть из которых несколько устарела (например как избиваться от reference path и относительных путей импортов в них не было написано) и десятков различных экспериментов (это не первая и не вторая итерация, все по большей части в свободное от работы — на которой надо пилить фичи — и личной жизни время, конечно же). Надеюсь кому-нибудь этот экскурс будет полезен. Также жду критики и предложений по улучшению всего, что я тут понагородил (ведь на самом деле я не совсем frontend developer и наверняка многое упустил из виду). Спасибо.
Автор: Gugic
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/170295
Ссылки в тексте:
[1] дал нам: https://habrahabr.ru/post/277087
[2] webpack: https://webpack.github.io/
[3] нововведений: https://gist.github.com/sokra/27b24881210b56bbaff7
[4] minification-proof dependency injection: https://docs.angularjs.org/guide/di
[5] TypeScript: https://www.typescriptlang.org/
[6] Babel JS: https://babeljs.io/
[7] TypeScript Definition Manager: https://github.com/typings/typings
[8] оптимизацию: https://github.com/webpack/docs/wiki/optimization
[9] node-http-proxy: https://github.com/nodejitsu/node-http-proxy
[10] Источник: https://habrahabr.ru/post/304806/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.