Skip to content

Module Specifiers Versus Relative Import References

Video Lecture

Module Specifiers Versus Relative Import References

Description

So far I have been demonstrating how to import ES6 modules into the client scripts by using relative URLS to reference the specific modules, and also adding paths to the ./src/client/tsconfig.json to indicate to VSCode and the TypeScript compiler, which type definition file (.d.ts) it should use for which import. I call this method using Relative Import References*.

Many javascript projects on the web, including Threejs, may show examples where you import a module using a Module Specifier instead. For some people, this approach may be easier to understand, but it comes with its own unique set of problems to solve.

eg, using Module Specifiers

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

Versus using Relative Import References

import * as THREE from '/build/three.module.js';
import { OrbitControls } from '/jsm/controls/OrbitControls';

This course, by default uses Relative Import References, but I will show you how to use Module Specifiers so that you have a choice.

To import modules client side using the Module Specifiers method, there are a few different steps involved with setting up your project structure. Without these steps you may see an error in the browser console similar to

Uncaught TypeError: Failed to resolve module specifier "three". 
Relative references must start with either "/", "./", or "../".

When writing your import statements in the VSCode IDE using Module Specifiers, you normally won't see an error underlining your import statements. This is true if your TypeScript compilerOptions are set to use moduleResolution: "node", which is actually the default setting. The VSCode IDE and TypeScript compiler will scan the node_modules folder using various rules and attempt to auto link a type definition file it finds to the Module Specifier that you wrote in the import statement.

You only become aware of the error when you finally try to run your code in the browser. For many developers, new and old, this doesn't make much sense, and can be a major source of confusion with many people giving up or trying to resolve there problems by asking questions on internet forums.

Module Specifiers rely on a Module Resolution traversing strategy. This doesn't work by default in browsers since the browser doesn't have direct access to the file system on the server in order to traverse all the folders and try all the methods of finding references that are involved during Module Resolution. If this was to run from the browser, it would trigger many 404 errors in the client as it tries out all the possible rules for finding references. You can read more about Module Resolution strategies at TypeScript Module Resolution

A common approach to solve this problem in the browser is to introduce a step that bundles all the required imports into one javascript file to be used on the client side. The client loads this javascript bundle and sets up all the module namespaces, for example three, into the browsers memory ready for use.

The tool that is used is commonly called a bundler. There are many bundlers that we can choose from to add to our projects, eg Webpack, Parcel, Rollup, Browserify and many more. The most common bundler at the time of writing this documentation is Webpack. So I will demonstrate setting up Webpack.

When bundling all the code and imports at compile time, a Module Resolution strategy will not be necessary for the browser to support in order to get Module Specifiers to work. All the code required by the client should already be sorted in memory ready for referencing for when the page has downloaded.

When using Relative Import References, it is also not necessary for the browser to understand any Module Resolution strategy, since you are already explicitly telling the browser where it can locate its resource by using a specific URL in the import statement. No guess work is required by the browser. URLs are one of the fundamental building blocks of the internet. If the path portion of the URL cannot be found by the Domain or IP of the server that you are requesting it from, it will return a 404 error indicating that it could not find anything at that resource location that you requested.

When using URLs in your import statements, you can also reference an import from an external web server by using the full domain name/ip and path, eg, this example below targets a specific Threejs release from a public CDN

import * as THREE from 'https://unpkg.com/three@0.120.1/build/three.module.js';
import { OrbitControls } from 'https://unpkg.com/three@0.120.1/jsm/controls/OrbitControls';

In this course, I have been using paths relative to the local web server which is the NodeJS Express server the we've setup in earlier lessons. eg,

import * as THREE from '/build/three.module.js';
import { OrbitControls } from '/jsm/controls/OrbitControls';

In the NodeJs Express server, it serves html and javascript files from the ./dist/client/ folder and also serves the Threejs libraries via static routes.

When using Webpack, you also have the option of using it's own development server instead of the NodeJS and Express server that I have demonstrated so far. The development server also provides Hot Module Replacement (HMR) functionality. Many developers will use the Webpack development server with HMR during development so it is good to be aware of the option.

I have created a version of the course boilerplate that uses the Webpack bundler, development server and HMR.

If you would like to try using the Webpack option throughout the course, you can download this alternative boilerplate and compare the differences to the existing boilerplate that uses Relative Import References served by the NodeJS Express server. I will point out the major differences in the project structure and code in the video.

Download the Alternative Threejs TypeScript Webpack Boilerplate

Git clone the boilerplate into a new folder so that you don't overwrite what you have so far.

git clone https://github.com/Sean-Bradley/Three.js-TypeScript-Boilerplate.git Threejs-TS-Webpack

This will place the repository into a new folder called Threejs-TS-Webpack. You can use any folder name you prefer. Just modify the folder name in the above command to be something other than Threejs-TS-Webpack.

Now CD into the new folder

cd Threejs-TS-Webpack

Checkout the statsguiwebpack branch to get the code specific to this branch.

git checkout statsguiwebpack

Install dependencies

npm install

And run it, visit http://127.0.0.1:8080 and it should appear in the browser identical to the existing boilerplate we have manually created so far. The browser should now show the green wire frame cube with the Stats panel and Dat GUI panel allowing you to rotate it.

Note

The default webpack dev server uses port 8080 and not port 3000 which I have been using when starting the NodeJS Express server.

The major difference when developing using the Module Specifiers construct with Webpack, its development server and HMR, is that,

  • when using the Webpack development server, the bundle.js file is not saved to disk, but served directly from memory.
  • You do not need to copy JavaScript libraries to web accessible folders, or create the equivalent static routes in the ./src/server/sever.ts since the imported libraries would now be included into the compiled bundle.js.
  • The bundle.js being served via the Webpack development server contains a large amount of extra code relating to HMR and dev server functionality. It is also not compressed or minified and is a significantly larger file than just downloading the libraries manually using the Relative Import Reference method.
  • You do not run the compiled bundle.js through the Webpack development server on a production server. Instead you run the npm run build command and it will produce an optimised bundle.js that is minified, and contains only the code required to run statically in the browser. There should also be no HMR related code included in the output. This production version of bundle.js is usually smaller and faster to download then importing reverences individually such as when using the Relative Import Reference method.
  • The Webpack development server is not a production quality server, so you will still need to resolve how to serve your project from a live public facing web server. Later in the course I demonstrate Deploying to Production by setting up the NodeJS Express server behind a Nginx proxy with a domain name and SSL certificate.

Note

This course is written to use Relative Import References by default. I have added code to the next several lessons that you can comment out in the lesson code samples, that will let you choose whether you want to import using Module Specifiers or Relative Import References. I have not added these comments to every code example throughout the course. After seeing this a few times, the differences will become obvious for you to recognise and then you make the required changes yourself depending on which import construct you prefer.

Code

For reference, I have added the code to this page, but I advise that it will be easier to download the Git repository instead.

In summary, this branch of the boilerplate contains 4 extra libraries involved in using Webpack with TypeScript and is less reliant on the NodeJS Express server during the development phase.

npm install --save-dev webpack webpack-cli webpack-dev-server ts-loader

./src/client/tsconfig.json

It has a modified client tsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "ES6",
        //"outDir": "../../dist/client",
        "baseUrl": ".",
        "paths": {
            "three/examples/jsm/libs/dat.gui.module": ["../../node_modules/@types/dat.gui"]
        },
        "moduleResolution": "node",
        "allowJs": true,
        "strict": true
    },
    "include": [
        "**/*.ts"
    ]
}

./src/client/client.ts

It has modified import statements in the client.ts that uses Module Specifiers instead of Relative Import References.

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import Stats from 'three/examples/jsm/libs/stats.module'
import { GUI } from 'three/examples/jsm/libs/dat.gui.module'

... etc, the remaining code is exactly the same

./src/client/webpack.dev.js

Added a file called webpack.dev.js

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './src/client/client.ts',
    devtool: 'eval-source-map',
    devServer: {
        contentBase: './dist/client',
        hot: true,
    },
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/,
        }, ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, '../../dist/client')
    },
    performance: {
        hints: false
    }
};

./src/client/webpack.prod.js

Added a file called webpack.prod.js

const path = require('path');

module.exports = {
    mode: 'production',
    entry: './src/client/client.ts',
    module: {
        rules: [{
            test: /\.tsx?$/,
            use: 'ts-loader',
            exclude: /node_modules/,
        }, ],
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, '../../dist/client'),
    },
    performance: {
        hints: false
    }
};

./package.json

The package.json has modified scripts and dependencies sections specific for using webpack with the dev server option and HMR.

{
  "name": "three.js-typescript-boilerplate",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "build": "webpack --config ./src/client/webpack.prod.js",
    "dev": "webpack serve --config ./src/client/webpack.dev.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Sean Bradley",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^13.13.15",
    "ts-loader": "^8.0.14",
    "webpack": "^5.12.1",
    "webpack-cli": "^4.3.1",
    "webpack-dev-server": "^3.11.1"
  },
  "dependencies": {
    "@types/dat.gui": "^0.7.5",
    "@types/express": "^4.17.7",
    "three": "^0.124.0",
    "typescript": "3.8.3"
  }
}

./dist/client/index.html

index.html now references bundle.js instead of client.js

<!DOCTYPE html>
<html>

<head>
    <title>Three.js TypeScript Tutorials by Sean Bradley</title>
    <style>
        body {
            overflow: hidden;
            margin: 0px;
        }
    </style>
</head>

<body>
    <script type="module" src="bundle.js"></script>
</body>

</html>

TypeScript Module Resolution