Reorganizarea modulelor interne TypeScript în module externe


Am un site web care folosește o bază mare de cod TypeScript. Toate clasele sunt în propriile fișiere și încapsulate într-un modul intern astfel:

fișier BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

fișier ChildClass.ts

module my.module {
  export ChildClass extends my.module.BaseClass  {
  }
}

Toate fișierele sunt incluse global cu taguri script, în ordinea corespunzătoare (folosind ASP.NET Bundling).

Aș dori să trec la o configurație mai modernă și să folosesc webpack. Aș dori ca sintaxa modulului meu să folosească standardul nou ECMASCRIPT pentru module. Dar există mult cod care folosește „spații de nume module” existente, așa că mi-aș dori un traseu de actualizare care să suporte acest tip de cod -

let x = new my.module.ChildClass();

Deci cred că am nevoie de ceva de genul acesta -

import * as my.module from ???;

Sau să folosești spații de nume?

Totuși, dacă aceasta nu este considerată o practică recomandată, aș dori să rămân la practicile recomandate. Modulele interne sunt în prezent foarte utile pentru organizarea diferitelor straturi și servicii ale aplicației...

Cum aș realiza acest lucru, având în vedere că „modulul” se întinde pe mai multe fișiere? De fapt, tot ce încerc să realizez este să am un spațiu de nume și să scap de scripturile globale.

Răspuns #1

Avertisment (acesta nu este destinat a fi un ghid complet, ci mai degrabă un punct de plecare conceptual. Sper să demonstrez fezabilitatea migrației, dar, în cele din urmă, implică o cantitate considerabilă de muncă grea)

Am făcut asta într-un proiect mare de întreprindere. Nu a fost amuzant, dar a funcționat.

Câteva sfaturi:

  1. Păstrează obiectul(ele) din spațiul de nume global doar atâta timp cât ai nevoie de ele.

  2. Începeți cu frunzele sursei dvs., convertind fișierele care nu au dependente în module externe.

  3. Deși aceste fișiere se vor baza ele însele pe obiectele din spațiul de nume global pe care le-ați folosit, acest lucru nu va fi o problemă dacă lucrați cu atenție de la exterior spre interior.

Să presupunem că aveți un namespace global precum utils și acesta este distribuit pe 3 fișiere după cum urmează

// utils/geo.ts
namespace utils {
  export function randomLatLng(): LatLng { return implementation(); };
}

// utils/uuid.ts
namespace utils {
  export function uuid(): string { return implementation(); };
}

// utils/http.ts

/// <reference path="./uuid.ts" />
namespace utils {
  export function createHttpClient (autoCacheBust = false) {
    const appendToUrl = autoCacheBust ? `?cacheBust=${uuid()}` : '';
    return {
      get<T>(url, options): Promise<T> {
        return implementation.get(url + appendToUrl, {...options}).then(({data}) => <T>data);
      }
    };
  }
}

Acum imaginează-ți că ai un alt fișier cu namespace la scară globală, doar că de data aceasta, îl putem separa cu ușurință într-un modul propriu pentru că nu depinde de niciun alt membru al propriei sale namespace. De exemplu, voi folosi un serviciu care interoghează informații meteo în locații aleatorii din întreaga lume folosind lucrurile din utils.

// services/weather-service.ts

/// <reference path="../utils/http.ts" />
/// <reference path="../utils/geo.ts" />
namespace services {
  export const weatherService = {
    const http = utils.http.createHttpClient(true);
    getRandom(): Promise<WeatherData> {
      const latLng = utils.geo.randomLatLng();
      return http
        .get<WeatherData>(`${weatherUrl}/api/v1?lat=${latLng.lat}&lng=${latLng.lng}`);
    }
  }
}

Nu, vom transforma constanta globală și cu spațiu de nume services.weatherSercice într-un modul extern propriu și va fi destul de ușor în acest caz

// services/weather-service.ts

import "../utils/http"; // es2015 side-effecting import to load the global
import "../utils/geo";  // es2015 side-effecting import to load the global
// namespaces loaded above are now available globally and merged into a single utils object

const http = utils.http.createHttpClient(true);

export default { 
    getRandom(): Promise<WeatherData> {
      const latLng = utils.geo.randomLatLng();
      return http
        .get<WeatherData>(`${weatherUrl}/api/v1?lat=${latLng.lat}&lng=${latLng.lng}`);
  } 
}

Capcane comune și soluții:

Apare o problemă dacă trebuie să facem referire la funcționalitatea acestui cod recent modularizat din unul dintre namespace-urile noastre globale existente

Deoarece folosim acum module pentru cel puțin o parte a codului nostru, avem în funcțiune un încărcător sau un pachet de module (dacă scrii pentru NodeJS, adică o aplicație Express, poți ignora acest lucru deoarece platforma integrează un încărcător, dar poți folosi și un încărcător personalizat). Acest încărcător sau pachet de module ar putea fi SystemJS, RequireJS, Webpack, Browserify sau ceva mai exotic.

Cea mai mare și mai comună greșeală este să ai ceva de genul acesta

// app.ts

/// <reference path="./services/weather-service.ts" />
namespace app {
  export async function main() {
    const dataForWeatherWidget = await services.weatherService.getRandom();
  }
}

Și, cum asta nu mai funcționează, scriem în schimb acest cod stricat

// app.ts

import weatherService from './services/weather-service';

namespace app {
  export async function main() {
    const dataForWeatherWidget = await weatherService.getRandom();
  }
}

Codul de mai sus este stricat pentru că, simplu prin adăugarea unei instrucțiuni import... from ''...'' (același lucru se aplică și pentru import ...=require(...)) am transformat app într-un modul accidental, înainte să fim pregătiți.

Deci, avem nevoie de o soluție alternativă. Temporar, reveniți în directorul services și adăugați un nou Modul, aici numit weather-service.shim.ts

// services/weather-service.shim.ts

import weatherService from './weather-service.ts';

declare global {
  interface Window {
    services: {
      weatherService: typeof weatherService;
    };
  }
}
window.services.weatherService = weatherService;

Apoi, modifică app.ts astfel

/// <reference path="./services/weather-service.shim.ts" />
namespace app {
  export async function main() {
    const dataForWeatherWidget = await services.weatherService.getRandom();
  }
}

Notă, acest lucru nu ar trebui făcut decât dacă este necesar. Încercați să vă organizați conversia în module pentru a minimiza acest lucru.

Observații:

Pentru a efectua corect această migrare treptată, este important să înțelegem precis ce definește ce este și ce nu este un modul.

Acest lucru este determinat de analizatoarele de limbaj la nivel de sursă pentru fiecare fișier.

Atunci când un fișier ECMAScript este analizat sintactic, există două posibile simboluri de scop, Script și Module.

https://tc39.es/ecma262/multipage/notational-conventions.html#sec-syntactic-grammar

5.1.4 Gramatica Sintactică

Gramatica syntactică pentru ECMAScript este prezentată în clauzele 13 până la 16. Această gramatică are tokenuri ECMAScript definite de gramatica lexicală ca simboluri terminale (5.1.2). Ea definește un set de producții, începând de la două simboluri alternative de scop Script și Module, care descriu cum secvențele de tokenuri formează componente independent corecte sintactic ale programelor ECMAScript.

Când un flux de puncte de cod urmează să fie analizat ca un Script sau Modul ECMAScript, acesta este mai întâi convertit într-un flux de elemente de intrare prin aplicarea repetată a gramaticii lexicale; acest flux de elemente de intrare este apoi analizat printr-o singură aplicare a gramaticii sintactice. Fluxul de intrare este sintactic eronat dacă token-urile din fluxul de elemente de intrare nu pot fi analizate ca o singură instanță a nonterminalului țintă (Script sau Modul), fără token-uri rămase neutilizate.

În termeni generali, un Script este global. Codul scris folosind modulele interne ale TypeScript intră întotdeauna în această categorie.

Un fișier sursă este un Modul dacă și numai dacă conține una sau mai multe declarații de nivel superior import sau export*. TypeScript obișnuia să se refere la astfel de surse ca module externe, dar acum ele sunt cunoscute pur și simplu ca module pentru a corespunde terminologiei specificației ECMAScript.

Iată câteva exemple sursă de scripturi și module. Rețineți că modul în care sunt diferențiate este subtil, dar bine definit.

square.ts --> Script

// This is a Script
// `square` is attached to the global object.

function square(n: number) {
  return n ** 2;
}

now.ts --> Script

// This is also a Script
// `now` is attached to the global object.
// `moment` is not imported but rather assumed to be available, attached to the global.

var now = moment();

square.ts --> Modul

// This is a Module. It has an `export` that exports a named function, square.
// The global is not polluted and `square` must be imported for use in other modules.

export function square(n: number) {
  return n ** 2;
}

bootstrap.ts --> Modul

// This is also a Module it has a top level `import` of moment. It exports nothing.
import moment from 'moment';

console.info('App started running at: ' + moment()); 

bootstrap.ts --> Script

// This is a Script (global) it has no top level `import` or `export`.
// moment refers to a global variable

console.info('App started running at: ' + moment());