The global scope (aka namespace in TypeScript) is no longer cool. It can take a long time to list the advantages of the modules (ES6 modules, in particular), but for me personally, the ability to use SystemJS for dynamically loading source files and Rollup to build a bundle became decisive for me.
However, the first thing you had to face when introducing ES6 modules is an insane amount of import expressions, with an insane amount of points inside:
import { FieldGroup } from "../../../Common/Components/FieldGroup/FieldGroup";
The ES6 specification doesn’t really say anything about it, waving the phrase that the paths to the modules are "loader specific". Well, that is, if you use SystemJS, then the format of the paths is determined by SystemJS, if Webpack, then Webpack. Work on the bootloader specification is underway, but, as the main page of the watwg repository says :
This spec is currently undergoing global redesigns (see # 147 and # 149) and is not ready for implementations.
The agreement between the loaders so far is only that the path starting with "./" means that you need to search in the same directory where the current module is located. The double dots "../", respectively, allow you to go up a level and look in the parent directory. At the same time, even in the simplest project it is very easy to get paths that contain 3-4 double points "../../../", which is terrible in every sense.
Since there is no specification, now everyone solves the problem who can. Usually, a root folder is configured for this purpose and all paths are relative to it. For example, the babel community invented a plugin for itself, and the webpack supports the setting resolve.root.
import { BasicEvent } from '~/Common/Utils/Events/BasicEvent'
However, even if you set up a root folder, it still does not save you from the huge header of import-expressions at the beginning of each file. The rules of good form speak about breaking the code into as small modules as possible, which is the source of the problem (they will now say that it is better to decompose the code, but the real world is always not the same as we would like).
What is especially sad is that every time you import a module, you create a hard link to the location of this module in the file system. Therefore, 1) you, at a minimum, need to remember exactly where each module is located 2) if you want to do refactoring (for example, rename a file), then you will have a lot of pain.
Well, the last pain you will face when using TypeScript in VisualStudio is that syntax highlighting does not work there, as well as JSX lintting for imported characters. For example:
import { FieldGroup } from "../../Components/FieldGroup/FieldGroup"; import { BasicEvent } from "../../Common/Utils/Events/BasicEvent' ... var event = new BasicEvent(); // BasicEvent VisualStudio ... render() { // JSX FieldGroup VisualStudio ( ), // intellicese , .. FieldGroup return <FieldGroup name="blabla" />; }
In Microsoft, it seems, they are not in a hurry to solve the problem ( issue 1 , issue 2 ).
The solution to the problem is to abandon the idea of ​​individual modules, randomly interconnected, and start using, hmm, something like “module packages”. I'm not sure if such a decision has already been published somewhere in this context (UPD: gogolor suggested that Angular is called a barrel at the docks), but the idea itself is not new. For example, in C # we also have separate files with code, but at the same time, these files are collected in "assemblies" (dll), which already explicitly declare references to other assemblies.
Imagine that we have the following project structure (screenshot from the real project of some admin panel):
In order for the file AssignmentTemplatSettings.tsx to reach BasicEvent.ts, you would have to write something like:
import { BasicEvent } from '../../Common/Utils/Events/BasicEvent';
This is terrible for all the reasons I described above. However, if you look at the project structure, it is easy to see that all modules are naturally distributed into folders. The more complex the project, the more extensive the folder structure becomes. This desire for ordering lives in every developer, and most likely there is something similar in your project.
The good news is that ES6 modules allow you to convert this folder structure into a "package" structure, very much like dll in the desktop world. You can make each folder a package (for example, Common / Utils / Events will be nested packages), you can limit it to larger units (only Common / Utils). For each package of modules, it will be clearly indicated which packages it depends on and what it “puts out”. All these dependencies will be collected at one point, so that the modules of the package will not know anything about the location of the modules of other packages. In this case, the number of points ("../../") in relative paths will be no more than the nesting of folders inside one package, and the number of import-expressions will be reduced down to one.
In order to convert a folder into a package, it is enough to add two files to it - imports and exports. In the first file, we import and re-export everything that is necessary for the modules of this package. In the second file is placed the export of all that the package makes available for import into other packages.
Let's try to make a package from the Events folder. Let him expose two classes - BasicEvent and SimpleEvent. Then, the @ EventsExports.ts file will look like this:
export * from "./BasicEvent"; export * from "./SimpleEvent";
The dog "@" in the file name ensures that he is not lost among the other package files and will always be at the very top. We don’t need anything from other packages here, so we don’t make the imports file here yet. Next, convert the parent folder Utils and Common into packages. For example, @ UtilsExports.ts will contain:
import * as Events from "./Events/@EventsExports"; import * as ModalWindow from "./ModalWindow/@ModalWindowExports"; import * as Other from "./Other/@OtherExports"; import * as RequestManager from "./RequestManager/@RequestManagerExports"; import * as ServiceUtils from "./ServiceUtils/@ServiceUtilsExports"; export { Events, ModalWindow, RequestManager, ServiceUtils };
The CachingLoader and other modules that were directly in the Utils folder are not listed here. This is a limitation of this approach; packages that export other packages cannot contain their modules. Therefore, I had to move all these files to the child package Other. The contents of the imports file will be reviewed later.
Similarly, we do @ CommonExports.ts:
import * as Components from "./Components/@ComponentsExports"; import * as Extensibility from "./Extensibility/@ExtensibilityExports"; import * as Models from "./Models/@ModelsExports"; import * as Services from "./Services/@ServicesExports"; import * as Utils from "./Utils/@UtilsExports"; export { Components, Extensibility, Models, Services, Utils };
We now turn to the Tabs package. Obviously, he will need a lot of classes from the Common package. Accordingly, its @ TabsImports.ts file will look like this:
import * as Common from "../Common/@CommonExports"; export { Common };
Now in the package's AssignmentTemplatesSettings.tsx module, it suffices to write the following:
import { Common } from "../@TabsImports"; // - using var Events = Common.Utils.Events; // BasicEvent Common/Utils/Events/BasicEvent.ts var basicEvent = new Events.BasicEvent();
As you can see, instead of specifying the full path to the BasicEvent file, we simply indicate in which package it is located. What is especially nice is that when writing Events.BasicEvent syntax highlighting and linting JSX in VisualStudio work great!
If the Tabs package only needs the Events package, then you can rewrite TabsImports.ts as follows:
import * as Common from "../Common/@CommonExports"; var Events = Common.Utils.Events; export { Events };
Either way:
import * as Events from "../Common/Utils/@EventsExports"; export { Events };
In the latter case, we again become attached to the path, however this binding goes at the packet level, so pain refactoring will be much less than when the binding is in each module. By reducing the number of imported code, we limited the number of files that the loader must prepare before executing the code of our module (for example, this is true if you break the bundle into several parts that are loaded lazily).
Communication modules inside the package is not such a terrible problem, because they are all close by. However, for several reasons, it may be necessary to use the same mechanism for importing modules of the current package. You can't use the exports file because by definition, it should not include the entire contents of the package. However, you can use it to create the third internals service file:
export * from "./@EventsExports"; // - export * from "./SomeInternalEventImpl"; // export * from "./SomeAnotherInternalEventImpl
Accordingly, after that we can use this file everywhere inside the package:
import * as Events from "./@EventsInternals"; let eventImpl = new Events.SomeInternalEventImpl();
Cyclic dependencies are resolved by specification, so there should be no problems with the import of internals. At the very least, SystemJS correctly handles such situations.
Among the shortcomings, additional imports / exports files have appeared that need to be constantly updated. In principle, the compilation of such files can be automated by a not very complicated gulp-task; the main thing is to come up with a convention on how to distinguish between exported and internal modules of a package. Well, and as a drawback - when accessing imported characters, you must add the name of the package (Events.BasicEvent instead of BasicEvnet). But, I think you can accept this, given that we get in return.
UPD: justboris noticed that exports can be conveniently called index.ts, since Many collectors and IDE consider it as the “default” file in the directory.
UPD: dzigoro noted that WebStorm supports the automatic addition of import declarations, as well as their updating during refactoring.
Source: https://habr.com/ru/post/326602/
All Articles