14.2.2. Modern JavaScript
Since the beginning, the client-side part of NextGIS Web was based on the Dojo 1.x framework. It was simple enough and great in terms of modularity in those times. But times had changed and now we have modern JavaScript which Dojo 1.x doesn’t support. NextGIS Web is now in transition from old-style Dojo-based code to modern JavaScript with ES modules, Webpack, and lots of other stuff.
The bad news is that the migration is hard and will take lots of resources, but the good news is interoperability between old-style and modern JavaScript code. Thus you can use modern code from old-style code and vice versa.
The key points:
Each NextGIS Web component may own one (or more) corresponding Node package, which belongs to the
@nextgisweb
scope (true for NextGIS Web extension packages likenextgisweb_qgis
). These packages are located undernodepkg
subdirectories of corresponding components.These packages are joined into one big Yarn workspace root, and Node packages become Yarn workspaces. It looks like working with monorepository on multiple libraries but in multiple source code repositories.
There is a modular Webpack config on top of that. Some Node packages can have their modules, but most of them use the
main
Webpack module.Modules that are part of the
main
Webpack module should use modern ESM module syntax. Webpack compiles them into ES5 compatible modules using the Babel compiler.Each Node package can provide one or more entrypoint for the “main” Webpack module. These entrypoints are compiled to AMD modules and can be loaded by Dojo AMD loader on the client-side.
On the NextGIS Web side, the jsrealm
component manages this enviroment and
provides some tools to work with it.
14.2.2.1. Setup and directory layout
Yarn workspaces feature depends on directory layout. In the examples below, we
assume that NextGIS Web is installed into home directory of ngw
user with
the following layout (some files are not shown):
🗁 ~ngw
├── 🗀 env
├── 🗀 data
├── 🗀 config
└── 🗁 package
├── 🗀 nextgisweb
└── 🗁 nextgisweb_foo
├── 🗁 nextgisweb_foo
│ └── 🗀 bar
└── 🗎 setup.py
14.2.2.2. Working with packages
Let’s say we have the following directory structure for component bar
which
already has Dojo-based JavaScript code in bar/amd/ngw-bar
directory:
🗁 ~ngw/package/nextgisweb_foo/nextgisweb_foo/bar
├── 🗁 amd
│ └── 🗁 ngw-bar
│ ├── 🗎 module-one.js
│ └── 🗎 module-two.js
└── 🗎 __init__.py
To add bar
package you should create bar/nodepkg
directory and
bar/nodepkg/package.json
file with the following content:
{
"name": "@nextgisweb/bar",
"version": "0.0.0",
"type": "module"
}
NextGIS Web component identifiers use snake_case
, but kebab-case
is
preferred in the JavaScript ecosystem and we shouldn’t break this convention. So
if a component identifier consists of two or more words, it should be converted
in Node package name. Thus file_upload
NextGIS Web component becomes
@nextgisweb/file-upload
in Node package.
Note
Version 0.0.0
may seem strange, but any Node package is required to have a
version. Versions are managed on NextGIS Web package level and we don’t plan
to publish this package on NPM.
After that it will look like this:
🗁 ~ngw/package/nextgisweb_foo/nextgisweb_foo/bar
├── 🗀 amd
├── 🗁 nodepkg
│ └── 🗎 package.json
└── 🗎 __init__.py
Let’s include this package into Yarn workspace root configuration:
$ cd ~ngw
$ nextgisweb jsrealm install
And now you can add some external dependency for this package, for example:
$ cd ~ngw
$ yarn workspace "@nextgisweb/bar" add faker
Then you can see that dependency was added to package.json
and now
bar/nodepkg/package.json
looks like this:
{
"name": "@nextgisweb/bar",
"version": "0.0.0",
"type": "module",
"dependencies": {
"faker": "^5.5.3"
}
}
Now let’s add an entrypoint and use the faker
library from it. Create file
bar/nodepkg/entrypoint.js
with the following content:
/** @entrypoint */
import faker from "faker";
export function greet() {
console.log(`Hello, ${faker.name.findName()}!`);
}
export function lorem() {
console.log(faker.lorem.paragraph());
}
The most important thing in the example is the /** @entrypoint */
, which
tells the main
Webpack module to create a separate library from this file.
Now build Webpack bundles and start development webserver:
$ cd ~ngw
$ yarn run build
$ nextgisweb server
Then go to http://localhost:8080/
open console in web developer tools and
execute the following expression:
require(["@nextgisweb/bar/entrypoint"], function (entrypoint) {
entrypoint.greet();
entrypoint.lorem();
})
And you will see something like this:
Hello, Stephen Hagenes!
Nobis porro officiis natus id ex hic blanditiis
commodi tenetur. Sint sed et voluptatibus ratione non quo natus. Odio dolorem
ipsum sapiente dolores autem modi. Deleniti eos possimus minima vitae dolore.
If you look at network requests, you will see how the browser loads entrypoint and their chunks:
GET .../main/@nextgisweb/bar/entrypoint.js [HTTP/1.1 200 OK 2ms]
GET .../main/chunk/vendors-...-eb4fd9.js [HTTP/1.1 200 OK 3ms]
GET .../main/chunk/vendors-...-faker_index_js.js [HTTP/1.1 200 OK 27ms]
14.2.2.3. Webpack modules
The main
Webpack module which collects entrypoints from NextGIS Web
component packages provides the following features:
Compilation of modules to browser-compatible format using the Babel and CoreJS libraries.
Automatic chunk generation and loading with Dojo AMD loader.
Support of CSS imports like
import "./resource.css"
.
The external
module delivers prebuilt libraries which are primarily used by
old-style JavaScript code. Dojo (dojo
, dijit
, dojox
), libraries are
delivered by this module. Before NextGIS Web was integrated with Webpack and
Node, these libraries were included in NextGIS Web source tree.
The stylesheet
module delivers compiled Less stylesheets and some fonts
which are installed from NPM registry. Previously fonts were also included in
the source tree.
It’s possible to provide additional Webpack modules. They can be declared under
nextgisweb.webpackConfigs
in package.json
. Here is the example from
@nextgisweb/stylesheet
Node package:
{
"nextgisweb": {
"webpackConfigs": {
"stylesheet": "webpack.stylesheet.cjs"
}
}
}
14.2.2.4. Writing modules
All modules should be written as ES modules. ES versions of libraries should be
preferred in dependencies. For example, use lodash-es
instead of lodash
:
// Right way
import { set } from "lodash-es";
set(...);
// Wrong way
import lodash from "lodash";
lodash.set(...);
14.2.2.4.1. Dynamic imports
In general dynamic imports work correctly and on-demand chunks are created. Then this code will produce an additional on-demand chunk:
async function doSomething() {
const dynamic = await import("./some-module");
}
But it works until demanded module becomes an entrypoint. After that, it starts
being a required chunk, not on-demand. This issue can be solved with the
auxiliary module @nextgisweb/jsrealm/entrypoint
, which uses AMD require
machinery and also supports expressions in module names:
import entrypoint from "@nextgisweb/jsrealm/entrypoint";
async function doSomething() {
// NOTE: This method doesn't support relative entrypoint names!
const dynamic = await entrypoint("@nextgisweb/bar/some-module");
}
14.2.2.5. Interoperability
It’s possible to import old-style libraries from modern ones:
import { default as Dialog } from "dijit/Dialog";
import { add, remove } from "dojo/dom-class";
And from old-style import entrypoints based on modern ones. As in the example above:
define(["@nextgisweb/bar/entrypoint"], function (entrypoint) {
entrypoint.greet();
});