Omstrukturering af TypeScript-interne moduler til eksterne moduler


Jeg har en hjemmeside, der bruger en stor TypeScript-kodebase. Alle klasser er i deres egne filer og indpakket i et internt modul som følger:

fil BaseClass.ts

module my.module {
  export class BaseClass {
  }
}

fil ChildClass.ts

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

Alle filer er inkluderet globalt med script-tags, i den passende rækkefølge (ved brug af ASP.NET Bundling).

Jeg vil gerne skifte til en mere moderne opsætning og bruge webpack. Jeg vil gerne have, at min modulesyntaks bruger hvad end den nye ECMASCRIPT-modulstandard er. Men der er meget kode, der bruger de eksisterende "modulnavneområder", så jeg vil gerne have en opdateringsvej, der understøtter denne type kode -

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

Så jeg tror, jeg har brug for noget som dette -

import * as my.module from ???;

Eller bruge navneområder?

Men hvis det ikke er bedste praksis, vil jeg gerne holde mig til bedste praksis. De interne moduler er i øjeblikket meget nyttige til at organisere de forskellige applikationslag og -tjenester...

Hvordan ville jeg gennemføre dette, da "modulet" er fordelt på mange filer? Egentlig er alt, hvad jeg prøver at opnå, at have et navnerum og slippe af med globale scripts.

Svar #1

Ansvarsfraskrivelse (dette er ikke ment som en omfattende guide, men snarere som et konceptuelt udgangspunkt. Jeg håber at demonstrere gennemførligheden af migration, men i sidste ende involverer det en ret stor indsats)

Jeg har gjort dette i et stort virksomhedprojekt. Det var ikke sjovt, men det virkede.

Nogle tips:

  1. Behold kun det globale navneområdeobjekt(er), så længe du har brug for dem.

  2. Start ved bladene af din kilde og konverter filer, der ikke har afhængige, til eksterne moduler.

  3. Selvom disse filer selv vil være afhængige af de globale navneområdesobjekter, du har brugt, vil dette ikke være et problem, hvis du arbejder omhyggeligt udefra og ind.

Forestil dig, at du har et globalt navnerum som utils, og det er spredt over 3 filer som følger

// 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);
      }
    };
  }
}

Forestil dig nu, at du har endnu en fil med globalt scope og navneområde, men denne gang kan vi nemt bryde den ud til et korrekt modul, fordi den ikke afhænger af andre medlemmer af sit egne navneområde. For eksempel vil jeg bruge en service, der forespørger vejrinfo på tilfældige steder rundt om i verden ved hjælp af tingene fra 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}`);
    }
  }
}

Nej, vi vil gøre vores services.weatherSercice globale, navnerums-konstante til et ordentligt eksternt modul, og det vil være ret nemt i dette tilfælde

// 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}`);
  } 
}

Almindelige faldgruber og løsninger:

Et problem kan opstå, hvis vi har brug for at referere til funktionaliteten af denne nyligt modulificerede kode fra et af vores eksisterende globale navneområder

Da vi nu bruger moduler for mindst en del af vores kode, har vi en modulindlæser eller bundler i spil (Hvis du skriver for NodeJS, f.eks. en Express-applikation, kan du se bort fra dette, da platformen integrerer en indlæser, men du kan også bruge en brugerdefineret indlæser). Den modulindlæser eller bundler kunne være SystemJS, RequireJS, Webpack, Browserify eller noget mere esoterisk.

Den største og mest almindelige fejl er at have noget som dette

// app.ts

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

Og da det ikke længere virker, skriver vi i stedet denne ødelagte kode

// app.ts

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

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

Koden ovenfor er ødelagt, fordi vi, simpelthen ved at tilføje en import... from '...'-erklæring (det samme gælder for import ...=require(...)), har gjort app til et modul ved et uheld, før vi var klar.

Så vi har brug for en løsning. Midlertidigt, gå tilbage til services-mappen og tilføj et nyt Modul, her kaldet 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;

Ændr derefter app.ts til

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

Bemærk, dette bør ikke gøres, medmindre du har brug for det. Prøv at organisere din konvertering til moduler for at minimere dette.

Bemærkninger:

For at kunne udføre denne gradvise migration korrekt er det vigtigt præcist at forstå, hvad der definerer, hvad der er, og hvad der ikke er en modul.

Dette bestemmes af sprogparse på kildniveau for hver fil.

Når en ECMAScript-fil bliver analyseret, er der to mulige mål-symboler, Script og Module.

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

5.1.4 Den syntaktiske grammatik

Den syntaktiske grammatik for ECMAScript er givet i afsnit 13 til 16. Denne grammatik har ECMAScript-tokens defineret af den leksikalske grammatik som sine terminalsymboler (5.1.2). Den definerer et sæt produktioner, der starter fra to alternative målsymboler Script og Module, som beskriver, hvordan sekvenser af tokens danner syntaktisk korrekte, uafhængige komponenter af ECMAScript-programmer.

Når en strøm af kodepunkter skal parses som et ECMAScript-skript eller -modul, konverteres den først til en strøm af inputelementer ved gentagen anvendelse af den leksikalske grammatik; denne strøm af inputelementer parses derefter ved en enkelt anvendelse af den syntaktiske grammatik. Inputstrømmen er syntaktisk forkert, hvis tokens i strømmen af inputelementer ikke kan parses som en enkelt forekomst af målnonterminalen (Script eller Module), uden at der er tokens tilbage.

Hvis man skal sige det lidt forhastet, er et Script globalt. Kode skrevet med TypeScript's interne moduler falder altid i denne kategori.

En kildefil er et Modul hvis og kun hvis den indeholder en eller flere topniveau import eller export erklæringer*. TypeScript plejede at referere til sådanne kilder som eksterne moduler, men de kaldes nu simpelthen moduler for at matche terminologien i ECMAScript-specifikationen.

Her er nogle kildeeksempler på scripts og moduler. Bemærk, at hvordan de adskilles, er subtilt, men klart defineret.

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());

Relaterede artikler