问题
I have a website that uses a large typescript code base. All clases as in their own files, and wrapped with an internal module like so:
file BaseClass.ts
module my.module {
export class BaseClass {
}
}
file ChildClass.ts
module my.module {
export ChildClass extends my.module.BaseClass {
}
}
All file are included globally with script tags, in the appropriate order (using ASP.NET Bundling).
I would like to move to a more modern setup and use webpack. I would like my module syntax to use whatever the new ECMASCRIPT module standard is. But there is much code using the existing "module namespaces" so I would like an update path that will support this type of code -
let x = new my.module.ChildClass();
So I think I need to have something like this -
import * as my.module from ???;
Or use namespaces?
However, if that is not best practices, I would like to stick with best practices. The internal modules are currently very helpful for organizing the different application layers and services...
How would I accomplish this since the "module" is across many files? Really, all I am trying to accomplish is to have a namespace, and get away from global scripts.
回答1:
Disclaimer (this is not meant as a comprehensive guide but rather as a conceptual starting point. I hope to demonstrate the feasibility of migration, but ultimately it involves a fair amount of hard work)
I've done this in a large enterprise project. It was not fun, but it worked.
Some tips:
Only keep the global namespace object(s) around for as long as you need them.
Start at the leaves of your source, converting files which have no dependents into external modules.
Although these files will themselves rely on the global namespace object(s) you have been using, this will not be a problem if you work carefully from the outside in.
Say you have a global namespace like utils
and it is spread across 3 files as follows
// 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()}` : '';
get<T>(url, options): Promise<T> {
return implementation.get(url, {...options}).then(({data}) => data);
}
}
}
Now imagine you have another globally scoped namespaced file only, this time, we can easily break it out into a proper module because it does not depend on any other members of its own namespace. For example I will use a service that queries for weather info at random locations around the globe using the stuff from 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}`);
}
}
}
No we are going to turn our services.weatherSercice
global, namespaced constant into a proper external module and it will be fairly easy in this case
// 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}`);
}
}
Common Pitfalls and Workarounds:
A snag can occur if we need to reference the functionality of this newly modulified code from one of our existing global namespaces
Since we are now using modules for at least some part of our code, we have a module loader or bundler in play (If you writing for NodeJS, i.e an express application you can disregard this as the platform integrates a loader, but you can also use a custom loader). That module loader or bundler could be SystemJS, RequireJS, Webpack, Browserify, or something more esoteric.
The biggest, and most common mistake is to have something like this
// app.ts
/// <reference path="./services/weather-service.ts" />
namespace app {
export async function main() {
const dataForWeatherWidget = await services.weatherService.getRandom();
}
}
And, as that no longer works, we write this broken code instead
// app.ts
import weatherService from './services/weather-service';
namespace app {
export async function main() {
const dataForWeatherWidget = await weatherService.getRandom();
}
}
The above code is broken because, simply by adding an import... from '...'
statement (the same applies to import ... = require(...)
) we have turned app
into a module accidentally, before we were ready.
So, we need a workaround. Temporarily, return to the services
directory and add a new Module, here called 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;
Then, change app.ts
to
/// <reference path="./services/weather-service.shim.ts" />
namespace app {
export async function main() {
const dataForWeatherWidget = await services.weatherService.getRandom();
}
}
Note, this should not be done unless you need to. Try to organize you conversion to modules so as to minimize this.
Remarks:
In order to correctly perform this gradual migration it is important to understand precisely what defines what is and what is not a module.
This is determined by language parsers at the source level for each file.
When an ECMAScript file is parsed, there are two possible goal symbols, Script and Module.
https://tc39.github.io/ecma262/#sec-syntactic-grammar
5.1.4The Syntactic Grammar The syntactic grammar for ECMAScript is given in clauses 11, 12, 13, 14, and 15. This grammar has ECMAScript tokens defined by the lexical grammar as its terminal symbols (5.1.2). It defines a set of productions, starting from two alternative goal symbols Script and Module, that describe how sequences of tokens form syntactically correct independent components of ECMAScript programs. When a stream of code points is to be parsed as an ECMAScript Script or Module, it is first converted to a stream of input elements by repeated application of the lexical grammar; this stream of input elements is then parsed by a single application of the syntactic grammar. The input stream is syntactically in error if the tokens in the stream of input elements cannot be parsed as a single instance of the goal nonterminal (Script or Module), with no tokens left over.
Hand-wavingly, a Script is a global. Code written using TypeScript's internal modules, always falls into this category.
A source file is a Module when and only when it contains one or more top level import
or export
statements*. TypeScript used to refer to such sources as external modules but they are now known simply as modules in order to match the ECMAScript specification's terminology.
Here are some source examples of scripts and modules and how they are differentiated is subtle yet well-defined.
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 --> Module
// 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 --> Module
// 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());
来源:https://stackoverflow.com/questions/44029788/restructuring-typescript-internal-modules-to-external-modules