- The Asynchronous Module Definition (AMD) format is used in browsers and uses a
define function to define modules.
- The CommonJS (CJS) format is used in Node.js and uses
require and
module.exports to define dependencies and modules. The npm ecosystem is built upon this format.
- The ES Module (ESM) format. As of ES6 (ES2015), JavaScript supports a native module format. It uses an
export keyword to export a module’s public API and an
import keyword to import it.
- The System.register format was designed to support ES6 modules within ES5.
- The Universal Module Definition (UMD) format can be used both in the browser and in Node.js. It’s useful when a module needs to be imported by a number of different module loaders.
A background on modules
JavaScript programs started off pretty small — most of its usage in the early days was to do isolated scripting tasks, providing a bit of interactivity to your web pages where needed, so large scripts were generally not needed. Fast forward a few years and we now have complete applications being run in browsers with a lot of JavaScript, as well as JavaScript being used in other contexts (Node.js, for example).
It has therefore made sense in recent years to start thinking about providing mechanisms for splitting JavaScript programs up into separate modules that can be imported when needed. Node.js has had this ability for a long time, and there are a number of JavaScript libraries and frameworks that enable module usage (for example, other CommonJS and AMD-based module systems like RequireJS, and more recently Webpack and Babel).
The good news is that modern browsers have started to support module functionality natively, and this is what this article is all about. This can only be a good thing — browsers can optimize loading of modules, making it more efficient than having to use a library and do all of that extra client-side processing and extra round trips.
Introducing an example
To demonstrate usage of modules, we’ve created a simple set of examples that you can find on GitHub. These examples demonstrate a simple set of modules that create a <canvas>
element on a webpage, and then draw (and report information about) different shapes on the canvas.
These are fairly trivial, but have been kept deliberately simple to demonstrate modules clearly.
Note: If you want to download the examples and run them locally, you’ll need to run them through a local web server.
Basic example structure
In our first example (see basic-modules) we have a file structure as follows:
index.html
main.js
modules/
canvas.js
square.js
Note: All of the examples in this guide have basically the same structure; the above should start getting pretty familiar.
The modules directory’s two modules are described below:
canvas.js — contains functions related to setting up the canvas:
create() — creates a canvas with a specified width and height inside a wrapper <div> with a specified ID, which is itself appended inside a specified parent element. Returns an object containing the canvas's 2D context and the wrapper's ID.
- createReportList() — creates an unordered list appended inside a specified wrapper element, which can be used to output report data into. Returns the list’s ID.
- square.js — contains:
- name — a constant containing the string ‘square’.
- draw() — draws a square on a specified canvas, with a specified size, position, and color. Returns an object containing the square’s size, position, and color.
- reportArea() — writes a square’s area to a specific report list, given its length.
- reportPerimeter() — writes a square’s perimeter to a specific report list, given its length.
Aside — .mjs versus .js
Throughout this article, we’ve used .js extensions for our module files, but in other resources you may see the .mjs extension used instead. V8’s documentation recommends this, for example. The reasons given are:
- It is good for clarity, i.e. it makes it clear which files are modules, and which are regular JavaScript.
- It ensures that your module files are parsed as a module by runtimes such as Node.js, and build tools such as Babel.
However, we decided to keep to using .js, at least for the moment. To get modules to work correctly in a browser, you need to make sure that your server is serving them with a Content-Type header that contains a JavaScript MIME type such as text/javascript. If you don’t, you’ll get a strict MIME type checking error along the lines of “The server responded with a non-JavaScript MIME type” and the browser won’t run your JavaScript. Most servers already set the correct type for .js files, but not yet for .mjs files. Servers that already serve .mjs files correctly include GitHub Pages and http-server for Node.js.
This is OK if you are using such an environment already, or if you aren’t but you know what you are doing and have access (i.e. you can configure your server to set the correct Content-Type for .mjs files). It could however cause confusion if you don’t control the server you are serving files from, or are publishing files for public use, as we are here.
For learning and portability purposes, we decided to keep to .js.
If you really value the clarity of using .mjs for modules versus using .js for “normal” JavaScript files, but don’t want to run into the problem described above, you could always use .mjs during development and convert them to .js during your build step.
It is also worth noting that:
- Some tools may never support .mjs, such as TypeScript.
- The <script type=”module”> attribute is used to denote when a module is being pointed to, as you’ll see below.
Exporting module features
The first thing you do to get access to module features is export them. This is done using the export statement.
The easiest way to use it is to place it in front of any items you want exported out of the module, for example:
export const name = 'square'; export function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; }
You can export functions, var, let, const, and — as we’ll see later — classes. They need to be top-level items; you can’t use export inside a function, for example.
A more convenient way of exporting all the items you want to export is to use a single export statement at the end of your module file, followed by a comma-separated list of the features you want to export wrapped in curly braces. For example:
export { name, draw, reportArea, reportPerimeter };
Importing features into your script
Once you’ve exported some features out of your module, you need to import them into your script to be able to use them. The simplest way to do this is as follows:
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
You use the import statement, followed by a comma-separated list of the features you want to import wrapped in curly braces, followed by the keyword from, followed by the path to the module file — a path relative to the site root, which for our basic-modules example would be /js-examples/modules/basic-modules.
However, we’ve written the path a bit differently — we are using the dot (.) syntax to mean “the current location”, followed by the path beyond that to the file we are trying to find. This is much better than writing out the entire relative path each time, as it is shorter, and it makes the URL portable — the example will still work if you move it to a different location in the site hierarchy.
So for example:
/js-examples/modules/basic-modules/modules/square.js
becomes
./modules/square.js
You can see such lines in action in main.js.
Note: In some module systems, you can omit the file extension and the dot (e.g. ‘/modules/square’). This doesn’t work in native JavaScript modules.
Once you’ve imported the features into your script, you can use them just like they were defined inside the same file. The following is found in main.js, below the import lines:
let myCanvas = create('myCanvas', document.body, 480, 320); let reportList = createReportList(myCanvas.id); let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue'); reportArea(square1.length, reportList); reportPerimeter(square1.length, reportList);
Note: Although imported features are available in the file, they are read only views of the feature that was exported. You cannot change the variable that was imported, but you can still modify properties similar to const. Additionally, these features are imported as live bindings, meaning that they can change in value even if you cannot modify the binding unlike const.
Applying the module to your HTML
Now we just need to apply the main.js module to our HTML page. This is very similar to how we apply a regular script to a page, with a few notable differences.
First of all, you need to include type=”module” in the <script> element, to declare this script as a module. To import the main.js script, we use this:
<script type="module" src="main.js"></script
You can also embed the module’s script directly into the HTML file by placing the JavaScript code within the body of the <script> element:
<script type="module"> /* JavaScript module code here */ </script>
The script into which you import the module features basically acts as the top-level module. If you omit it, Firefox for example gives you an error of “SyntaxError: import declarations may only appear at top level of a module”.
You can only use import and export statements inside modules, not regular scripts.
Other differences between modules and standard scripts
- You need to pay attention to local testing — if you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server.
- Also, note that you might get different behavior from sections of script defined inside modules as opposed to in standard scripts. This is because modules use strict mode automatically.
- There is no need to use the defer attribute (see <script> attributes) when loading a module script; modules are deferred automatically.
- Modules are only executed once, even if they have been referenced in multiple <script> tags.
- Last but not least, let’s make this clear — module features are imported into the scope of a single script — they aren’t available in the global scope. Therefore, you will only be able to access imported features in the script they are imported into, and you won’t be able to access them from the JavaScript console, for example. You’ll still get syntax errors shown in the DevTools, but you’ll not be able to use some of the debugging techniques you might have expected to use.
Default exports versus named exports
The functionality we’ve exported so far has been comprised of named exports — each item (be it a function, const, etc.) has been referred to by its name upon export, and that name has been used to refer to it on import as well.
There is also a type of export called the default export — this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems (as explained nicely in ES6 In Depth: Modules by Jason Orendorff; search for “Default exports”).
Let’s look at an example as we explain how it works. In our basic-modules square.js you can find a function called randomSquare() that creates a square with a random color, size, and position. We want to export this as our default, so at the bottom of the file we write this:
export default randomSquare;
Note the lack of curly braces.
We could instead prepend export default onto the function and define it as an anonymous function, like this:
export default function(ctx) { ... }
Over in our main.js file, we import the default function using this line:
import randomSquare from './modules/square.js';
Again, note the lack of curly braces. This is because there is only one default export allowed per module, and we know that randomSquare is it. The above line is basically shorthand for:
import {default as randomSquare} from './modules/square.js';
Note: The as syntax for renaming exported items is explained below in the Renaming imports and exports section.
Avoiding naming conflicts
So far, our canvas shape drawing modules seem to be working OK. But what happens if we try to add a module that deals with drawing another shape, like a circle or triangle? These shapes would probably have associated functions like draw(), reportArea(), etc. too; if we tried to import different functions of the same name into the same top-level module file, we’d end up with conflicts and errors.
Fortunately there are a number of ways to get around this. We’ll look at these in the following sections.
Renaming imports and exports
Inside your import and export statement’s curly braces, you can use the keyword as along with a new feature name, to change the identifying name you will use for a feature inside the top-level module.
So for example, both of the following would do the same job, albeit in a slightly different way:
// inside module.js export { function1 as newFunctionName, function2 as anotherNewFunctionName }; // inside main.js import { newFunctionName, anotherNewFunctionName } from './modules/module.js';
// inside module.js export { function1, function2 }; // inside main.js import { function1 as newFunctionName, function2 as anotherNewFunctionName } from './modules/module.js';
Let’s look at a real example. In our renaming directory you’ll see the same module system as in the previous example, except that we’ve added circle.js and triangle.js modules to draw and report on circles and triangles.
Inside each of these modules, we’ve got features with the same names being exported, and therefore each has the same export statement at the bottom:
export { name, draw, reportArea, reportPerimeter };
When importing these into main.js, if we tried to use
import { name, draw, reportArea, reportPerimeter } from './modules/square.js'; import { name, draw, reportArea, reportPerimeter } from './modules/circle.js'; import { name, draw, reportArea, reportPerimeter } from './modules/triangle.js';
The browser would throw an error such as “SyntaxError: redeclaration of import name” (Firefox).
Instead we need to rename the imports so that they are unique:
import { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter } from './modules/square.js'; import { name as circleName, draw as drawCircle, reportArea as reportCircleArea, reportPerimeter as reportCirclePerimeter } from './modules/circle.js'; import { name as triangleName, draw as drawTriangle, reportArea as reportTriangleArea, reportPerimeter as reportTrianglePerimeter } from './modules/triangle.js';
Note that you could solve the problem in the module files instead, e.g.
// in square.js export { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter };
// in main.js import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from './modules/square.js';
And it would work just the same. What style you use is up to you, however it arguably makes more sense to leave your module code alone, and make the changes in the imports. This especially makes sense when you are importing from third party modules that you don’t have any control over.
Creating a module object
The above method works OK, but it’s a little messy and longwinded. An even better solution is to import each module’s features inside a module object. The following syntax form does that:
import * as Module from './modules/module.js';
This grabs all the exports available inside module.js, and makes them available as members of an object Module, effectively giving it its own namespace. So for example:
Module.function1() Module.function2() etc.
Again, let’s look at a real example. If you go to our module-objects directory, you’ll see the same example again, but rewritten to take advantage of this new syntax. In the modules, the exports are all in the following simple form:
export { name, draw, reportArea, reportPerimeter };
The imports on the other hand look like this:
import * as Canvas from './modules/canvas.js'; import * as Square from './modules/square.js'; import * as Circle from './modules/circle.js'; import * as Triangle from './modules/triangle.js';
In each case, you can now access the module’s imports underneath the specified object name, for example:
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue'); Square.reportArea(square1.length, reportList); Square.reportPerimeter(square1.length, reportList);
So you can now write the code just the same as before (as long as you include the object names where needed), and the imports are much neater.
Modules and classes
As we hinted at earlier, you can also export and import classes; this is another option for avoiding conflicts in your code, and is especially useful if you’ve already got your module code written in an object-oriented style.
You can see an example of our shape drawing module rewritten with ES classes in our classes directory. As an example, the square.js file now contains all its functionality in a single class:
class Square { constructor(ctx, listId, length, x, y, color) { ... } draw() { ... } ... }
which we then export:
export { Square };
Over in main.js, we import it like this:
import { Square } from './modules/square.js';
And then use the class to draw our square:
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter();
Aggregating modules
There will be times where you’ll want to aggregate modules together. You might have multiple levels of dependencies, where you want to simplify things, combining several submodules into one parent module. This is possible using export syntax of the following forms in the parent module:
export * from 'x.js' export { name } from 'x.js'
For an example, see our module-aggregation directory. In this example (based on our earlier classes example) we’ve got an extra module called shapes.js, which aggregates all the functionality from circle.js, square.js, and triangle.js together. We’ve also moved our submodules inside a subdirectory inside the modules directory called shapes. So the module structure in this example is:
modules/ canvas.js shapes.js shapes/ circle.js square.js triangle.js
In each of the submodules, the export is of the same form, e.g.
export { Square };
Next up comes the aggregation part. Inside shapes.js, we include the following lines:
export { Square } from './shapes/square.js'; export { Triangle } from './shapes/triangle.js'; export { Circle } from './shapes/circle.js';
These grab the exports from the individual submodules and effectively make them available from the shapes.js module.
Note: The exports referenced in shapes.js basically get redirected through the file and don’t really exist there, so you won’t be able to write any useful related code inside the same file.
So now in the main.js file, we can get access to all three module classes by replacing
import { Square } from './modules/square.js'; import { Circle } from './modules/circle.js'; import { Triangle } from './modules/triangle.js';
with the following single line:
import { Square, Circle, Triangle } from './modules/shapes.js';
Dynamic module loading
The newest part of the JavaScript modules functionality to be available in browsers is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages; let’s read on and see how it works.
This new functionality allows you to call import() as a function, passing it the path to the module as a parameter. It returns a Promise, which fulfills with a module object (see Creating a module object) giving you access to that object’s exports, e.g.
import('./modules/myModule.js') .then((module) => { // Do something with the module. });
Let’s look at an example. In the dynamic-module-imports directory we’ve got another example based on our classes example. This time however we are not drawing anything on the canvas when the example loads. Instead, we include three buttons — “Circle”, “Square”, and “Triangle” — that, when pressed, dynamically load the required module and then use it to draw the associated shape.
In this example we’ve only made changes to our index.html and main.js files — the module exports remain the same as before.
Over in main.js we’ve grabbed a reference to each button using a Document.querySelector() call, for example:
let squareBtn = document.querySelector('.square');
We then attach an event listener to each button so that when pressed, the relevant module is dynamically loaded and used to draw the shape:
squareBtn.addEventListener('click', () => { import('./modules/square.js').then((Module) => { let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter(); }) });
Note that, because the promise fulfillment returns a module object, the class is then made a subfeature of the object, hence we now need to access the constructor with Module. prepended to it, e.g. Module.Square( … ).
Troubleshooting
Here are a few tips that may help you if you are having trouble getting your modules to work. Feel free to add to the list if you discover more!
- We mentioned this before, but to reiterate: .js files need to be loaded with a MIME-type of text/javascript (or another JavaScript-compatible MIME-type, but text/javascript is recommended), otherwise you’ll get a strict MIME type checking error like “The server responded with a non-JavaScript MIME type”.
- If you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server. GitHub pages is ideal as it also serves .js files with the correct MIME type.
- Because .mjs is a non-standard file extension, some operating systems might not recognise it, or try to replace it with something else. For example, we found that macOS was silently adding on .js to the end of .mjs files and then automatically hiding the file extension. So all of our files were actually coming out as x.mjs.js. Once we turned off automatically hiding file extensions, and trained it to accept .mjs, it was OK.
为什么有模块概念
理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,Javascript不是一种模块化编程语言,在es6以前,它是不支持”类”(class),所以也就没有”模块”(module)了。
require时代
Javascript社区做了很多努力,在现有的运行环境中,实现”模块”的效果。
原始写法
模块就是实现特定功能的一组方法。
只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。
function m1(){ //... } function m2(){ //... }
上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。
这种做法的缺点很明显:”污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } });
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性
module1.m1();
这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module._count = 1;
立即执行函数写法
使用”立即执行函数”(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的
var module = (function() { var _count = 0; var m1 = function() { alert(_count) } var m2 = function() { alert(_count + 1) } return { m1: m1, m2: m2 } })()
使用上面的写法,外部代码无法读取内部的_count变量。
console.info(module._count); //undefined
module就是Javascript模块的基本写法。
主流模块规范
在es6以前,还没有提出一套官方的规范,从社区和框架推广程度而言,目前通行的javascript模块规范有两种:CommonJS 和 AMD
CommonJS规范
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
这标志”Javascript模块化编程”正式诞生。前端的复杂程度有限,没有模块也是可以的,但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node编程中最重要的思想之一就是模块,而正是这个思想,让JavaScript的大规模工程成为可能。模块化编程在js界流行,也是基于此,随后在浏览器端,requirejs和seajs之类的工具包也出现了,可以说在对应规范下,require统治了ES6之前的所有模块化编程,即使现在,在ES6 module被完全实现之前,还是这样。
在CommonJS中,暴露模块使用module.exports和exports,很多人不明白暴露对象为什么会有两个,后面会介绍区别
在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
然后,就可以调用模块提供的方法:
var math = require('math'); math.add(2,3); // 5
正是由于CommonJS 使用的require方式的推动,才有了后面的AMD、CMD 也采用的require方式来引用模块的风格
AMD规范
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题
var math = require('math'); math.add(2, 3);
第二行math.add(2, 3),在第一行require(‘math’)之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
模块必须采用特定的define()函数来定义。
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
- factory: 工厂方法,返回一个模块函数
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
// math.js define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; });
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['Lib'], function(Lib){ function foo(){ Lib.doSomething(); } return { foo : foo }; });
当require()函数加载上面这个模块的时候,就会先加载Lib.js文件。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) { math.add(2, 3); });
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
CMD规范
CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:
define(function(require, exports, module) { var clock = require('clock'); clock.start(); });
CMD与AMD一样,也是采用特定的define()函数来定义,用require方式来引用模块
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
- factory: 工厂方法,返回一个模块函数
define('hello', ['jquery'], function(require, exports, module) { // 模块代码 });
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
define(function(require, exports, module) { // 模块代码 });
注意:带 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。
CMD与AMD区别
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
现阶段的标准
ES6标准发布后,module成为标准,标准使用是以export指令导出接口,以import引入模块,但是在我们一贯的node模块中,我们依然采用的是CommonJS规范,使用require引入模块,使用module.exports导出接口。
export导出模块
export语法声明用于导出函数、对象、指定文件(或模块)的原始值。
注意:在node中使用的是exports,不要混淆了
export有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。
export { name1, name2, …, nameN }; export { variable1 as name1, variable2 as name2, …, nameN }; export let name1, name2, …, nameN; // also var export let name1 = …, name2 = …, …, nameN; // also var, const export default expression; export default function (…) { … } // also class, function* export default function name1(…) { … } // also class, function* export { name1 as default, … }; export * from …; export { name1, name2, …, nameN } from …; export { import1 as name1, import2 as name2, …, nameN } from …;
- name1… nameN-导出的“标识符”。导出后,可以通过这个“标识符”在另一个模块中使用import引用
- default-设置模块的默认导出。设置后import不通过“标识符”而直接引用默认导入
- -继承模块并导出继承模块所有的方法和属性
- as-重命名导出“标识符”
- from-从已经存在的模块、脚本文件…导出
命名式导出
模块可以通过export前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。
export { myFunction }; // 导出一个已定义的函数 export const foo = Math.sqrt(2); // 导出一个常量
我们可以使用*和from关键字来实现的模块的继承:
export * from 'article';
模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name, domain}; // 相当于导出 {name:name,domain:domain}
模块导出时,我们可以使用as关键字对导出成员进行重命名:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name as siteName, domain};
注意,下面的语法有严重错误的情况:
// 错误演示 export 1; // 绝对不可以 var a = 100; export a;
export在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在import的时候有一个变量与之对应
export a虽然看上去成立,但是a的值是一个数字,根本无法完成解构,因此必须写成export {a}的形式。即使a被赋值为一个function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个export导出所有的接口,例如:
export {fun as default,a,b,c};
默认导出
默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在import引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import导入时也会很容易引用。
export default function() {}; // 可以导出一个函数 export default class(){}; // 也可以出一个类
命名式导出与默认导出
默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default名称的命名导出。
下面两种导出方式是等价的:
const D = 123; export default D; export { D as default };
export使用示例
使用名称导出一个模块时:
// "my-module.js" 模块 export function cube(x) { return x * x * x; } const foo = Math.PI + Math.SQRT2; export { foo };
在另一个模块(脚本文件)中,我们可以像下面这样引用:
import { cube, foo } from 'my-module'; console.log(cube(3)); // 27 console.log(foo); // 4.555806215962888
使用默认导出一个模块时:
// "my-module.js"模块 export default function (x) { return x * x * x; }
在另一个模块(脚本文件)中,我们可以像下面这样引用,相对名称导出来说使用更为简单:
// 引用 "my-module.js"模块 import cube from 'my-module'; console.log(cube(3)); // 27
import引入模块
import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
import的语法跟require不同,而且import必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。
import defaultMember from "module-name"; import * as name from "module-name"; import { member } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name";
- name-从将要导入模块中收到的导出值的名称
- member, memberN-从导出模块,导入指定名称的多个成员
- defaultMember-从导出模块,导入默认导出成员
- alias, aliasN-别名,对指定导入成员进行的重命名
- module-name-要导入的模块。是一个文件名
- as-重命名导入成员名称(“标识符”)
- from-从已经存在的模块、脚本文件等导入
命名式导入
我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:
注意,花括号里面的变量与export后面的变量一一对应
import {myMember} from "my-module"; import {foo, bar} from "my-module";
通过*符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块(’my-module.js’)所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中:
import * as myModule from "my-module";
导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
导入多个成员时,同样可以使用别名:
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
导入一个模块,但不进行任何绑定:
import "my-module";
默认导入
在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import指令导出这些默认值。
直接导入默认值:
import myDefault from "my-module";
也可以在命名空间导入和名称导入中,同时使用默认导入:
import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用 或 import myDefault, {foo, bar} from "my-module"; // 指定成员导入
import使用示例
// --file.js-- function getJSON(url, callback) { let xhr = new XMLHttpRequest(); xhr.onload = function () { callback(this.responseText) }; xhr.open("GET", url, true); xhr.send(); } export function getUsefulContents(url, callback) { getJSON(url, data => callback(JSON.parse(data))); } // --main.js-- import { getUsefulContents } from "file"; getUsefulContents("http://itbilu.com", data => { doSomethingUseful(data); });
default关键字
// d.js export default function() {} // 等效于: function a() {}; export {a as default};
在import的时候,可以这样用:
import a from './d'; // 等效于,或者说就是下面这种写法的简写,是同一个意思 import {default as a} from './d';
这个语法糖的好处就是import的时候,可以省去花括号{}。
简单的说,如果import的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的as语法。
所以,下面这种写法你也应该理解了吧:
import $,{each,map} from 'jquery';
import后面第一个$是{defalut as $}的替代写法。
as关键字
as简单的说就是取一个别名,export中可以用,import中其实可以用:
// a.js var a = function() {}; export {a as fun}; // b.js import {fun as a} from './a'; a();
上面这段代码,export的时候,对外提供的接口是fun,它是a.js内部a这个函数的别名,但是在模块外面,认不到a,只能认到fun。
import中的as就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之所以是这样,是因为有的时候不同的两个模块可能通过相同的接口,比如有一个c.js也通过了fun这个接口:
// c.js export function fun() {};
如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。
CommonJS中module.exports 与 exports的区别
Module.exports
The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.
译文:module.exports对象是由模块系统创建的。 有时这是难以接受的;许多人希望他们的模块成为某个类的实例。 为了实现这个,需要将期望导出的对象赋值给module.exports。 注意,将期望的对象赋值给exports会简单地重新绑定到本地exports变量上,这可能不是你想要的。
Module.exports
The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:
译文:exports变量是在模块的文件级别作用域内有效的,它在模块被执行前被赋于 module.exports 的值。它有一个快捷方式,以便 module.exports.f = … 可以被更简洁地写成exports.f = …。 注意,就像任何变量,如果一个新的值被赋值给exports,它就不再绑定到module.exports(其实是exports.属性会自动挂载到没有命名冲突的module.exports.属性)
从Api文档上面的可以看出,从require导入方式去理解,关键有两个变量(全局变量module.exports,局部变量exports)、一个返回值(module.exports)
function require(...) { var module = { exports: {} }; ((module, exports) => { // 你的被引入代码 Start // var exports = module.exports = {}; (默认都有的) function some_func() {}; exports = some_func; // 此时,exports不再挂载到module.exports, // export将导出{}默认对象 module.exports = some_func; // 此时,这个模块将导出some_func对象,覆盖exports上的some_func // 你的被引入代码 End })(module, module.exports); // 不管是exports还是module.exports,最后返回的还是module.exports return module.exports; }
demo.js:
console.log(exports); // {} console.log(module.exports); // {} console.log(exports === module.exports); // true console.log(exports == module.exports); // true console.log(module); /** Module { id: '.', exports: {}, parent: null, filename: '/Users/larben/Desktop/demo.js', loaded: false, children: [], paths: [ '/Users/larben/Desktop/node_modules', '/Users/larben/node_modules', '/Users/node_modules', '/node_modules' ] } */
注意
- 每个js文件一创建,都有一个var exports = module.exports = {},使exports和module.exports都指向一个空对象。
- module.exports和exports所指向的内存地址相同
- The Asynchronous Module Definition (AMD) format is used in browsers and uses a
define function to define modules.
- The CommonJS (CJS) format is used in Node.js and uses
require and
module.exports to define dependencies and modules. The npm ecosystem is built upon this format.
- The ES Module (ESM) format. As of ES6 (ES2015), JavaScript supports a native module format. It uses an
export keyword to export a module’s public API and an
import keyword to import it.
- The System.register format was designed to support ES6 modules within ES5.
- The Universal Module Definition (UMD) format can be used both in the browser and in Node.js. It’s useful when a module needs to be imported by a number of different module loaders.
A background on modules
JavaScript programs started off pretty small — most of its usage in the early days was to do isolated scripting tasks, providing a bit of interactivity to your web pages where needed, so large scripts were generally not needed. Fast forward a few years and we now have complete applications being run in browsers with a lot of JavaScript, as well as JavaScript being used in other contexts (Node.js, for example).
It has therefore made sense in recent years to start thinking about providing mechanisms for splitting JavaScript programs up into separate modules that can be imported when needed. Node.js has had this ability for a long time, and there are a number of JavaScript libraries and frameworks that enable module usage (for example, other CommonJS and AMD-based module systems like RequireJS, and more recently Webpack and Babel).
The good news is that modern browsers have started to support module functionality natively, and this is what this article is all about. This can only be a good thing — browsers can optimize loading of modules, making it more efficient than having to use a library and do all of that extra client-side processing and extra round trips.
Introducing an example
To demonstrate usage of modules, we’ve created a simple set of examples that you can find on GitHub. These examples demonstrate a simple set of modules that create a <canvas>
element on a webpage, and then draw (and report information about) different shapes on the canvas.
These are fairly trivial, but have been kept deliberately simple to demonstrate modules clearly.
Note: If you want to download the examples and run them locally, you’ll need to run them through a local web server.
Basic example structure
In our first example (see basic-modules) we have a file structure as follows:
index.html
main.js
modules/
canvas.js
square.js
Note: All of the examples in this guide have basically the same structure; the above should start getting pretty familiar.
The modules directory’s two modules are described below:
canvas.js — contains functions related to setting up the canvas:
create() — creates a canvas with a specified width and height inside a wrapper <div> with a specified ID, which is itself appended inside a specified parent element. Returns an object containing the canvas's 2D context and the wrapper's ID.
- createReportList() — creates an unordered list appended inside a specified wrapper element, which can be used to output report data into. Returns the list’s ID.
- square.js — contains:
- name — a constant containing the string ‘square’.
- draw() — draws a square on a specified canvas, with a specified size, position, and color. Returns an object containing the square’s size, position, and color.
- reportArea() — writes a square’s area to a specific report list, given its length.
- reportPerimeter() — writes a square’s perimeter to a specific report list, given its length.
Aside — .mjs versus .js
Throughout this article, we’ve used .js extensions for our module files, but in other resources you may see the .mjs extension used instead. V8’s documentation recommends this, for example. The reasons given are:
- It is good for clarity, i.e. it makes it clear which files are modules, and which are regular JavaScript.
- It ensures that your module files are parsed as a module by runtimes such as Node.js, and build tools such as Babel.
However, we decided to keep to using .js, at least for the moment. To get modules to work correctly in a browser, you need to make sure that your server is serving them with a Content-Type header that contains a JavaScript MIME type such as text/javascript. If you don’t, you’ll get a strict MIME type checking error along the lines of “The server responded with a non-JavaScript MIME type” and the browser won’t run your JavaScript. Most servers already set the correct type for .js files, but not yet for .mjs files. Servers that already serve .mjs files correctly include GitHub Pages and http-server for Node.js.
This is OK if you are using such an environment already, or if you aren’t but you know what you are doing and have access (i.e. you can configure your server to set the correct Content-Type for .mjs files). It could however cause confusion if you don’t control the server you are serving files from, or are publishing files for public use, as we are here.
For learning and portability purposes, we decided to keep to .js.
If you really value the clarity of using .mjs for modules versus using .js for “normal” JavaScript files, but don’t want to run into the problem described above, you could always use .mjs during development and convert them to .js during your build step.
It is also worth noting that:
- Some tools may never support .mjs, such as TypeScript.
- The <script type=”module”> attribute is used to denote when a module is being pointed to, as you’ll see below.
Exporting module features
The first thing you do to get access to module features is export them. This is done using the export statement.
The easiest way to use it is to place it in front of any items you want exported out of the module, for example:
export const name = 'square'; export function draw(ctx, length, x, y, color) { ctx.fillStyle = color; ctx.fillRect(x, y, length, length); return { length: length, x: x, y: y, color: color }; }
You can export functions, var, let, const, and — as we’ll see later — classes. They need to be top-level items; you can’t use export inside a function, for example.
A more convenient way of exporting all the items you want to export is to use a single export statement at the end of your module file, followed by a comma-separated list of the features you want to export wrapped in curly braces. For example:
export { name, draw, reportArea, reportPerimeter };
Importing features into your script
Once you’ve exported some features out of your module, you need to import them into your script to be able to use them. The simplest way to do this is as follows:
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
You use the import statement, followed by a comma-separated list of the features you want to import wrapped in curly braces, followed by the keyword from, followed by the path to the module file — a path relative to the site root, which for our basic-modules example would be /js-examples/modules/basic-modules.
However, we’ve written the path a bit differently — we are using the dot (.) syntax to mean “the current location”, followed by the path beyond that to the file we are trying to find. This is much better than writing out the entire relative path each time, as it is shorter, and it makes the URL portable — the example will still work if you move it to a different location in the site hierarchy.
So for example:
/js-examples/modules/basic-modules/modules/square.js
becomes
./modules/square.js
You can see such lines in action in main.js.
Note: In some module systems, you can omit the file extension and the dot (e.g. ‘/modules/square’). This doesn’t work in native JavaScript modules.
Once you’ve imported the features into your script, you can use them just like they were defined inside the same file. The following is found in main.js, below the import lines:
let myCanvas = create('myCanvas', document.body, 480, 320); let reportList = createReportList(myCanvas.id); let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue'); reportArea(square1.length, reportList); reportPerimeter(square1.length, reportList);
Note: Although imported features are available in the file, they are read only views of the feature that was exported. You cannot change the variable that was imported, but you can still modify properties similar to const. Additionally, these features are imported as live bindings, meaning that they can change in value even if you cannot modify the binding unlike const.
Applying the module to your HTML
Now we just need to apply the main.js module to our HTML page. This is very similar to how we apply a regular script to a page, with a few notable differences.
First of all, you need to include type=”module” in the <script> element, to declare this script as a module. To import the main.js script, we use this:
<script type="module" src="main.js"></script
You can also embed the module’s script directly into the HTML file by placing the JavaScript code within the body of the <script> element:
<script type="module"> /* JavaScript module code here */ </script>
The script into which you import the module features basically acts as the top-level module. If you omit it, Firefox for example gives you an error of “SyntaxError: import declarations may only appear at top level of a module”.
You can only use import and export statements inside modules, not regular scripts.
Other differences between modules and standard scripts
- You need to pay attention to local testing — if you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server.
- Also, note that you might get different behavior from sections of script defined inside modules as opposed to in standard scripts. This is because modules use strict mode automatically.
- There is no need to use the defer attribute (see <script> attributes) when loading a module script; modules are deferred automatically.
- Modules are only executed once, even if they have been referenced in multiple <script> tags.
- Last but not least, let’s make this clear — module features are imported into the scope of a single script — they aren’t available in the global scope. Therefore, you will only be able to access imported features in the script they are imported into, and you won’t be able to access them from the JavaScript console, for example. You’ll still get syntax errors shown in the DevTools, but you’ll not be able to use some of the debugging techniques you might have expected to use.
Default exports versus named exports
The functionality we’ve exported so far has been comprised of named exports — each item (be it a function, const, etc.) has been referred to by its name upon export, and that name has been used to refer to it on import as well.
There is also a type of export called the default export — this is designed to make it easy to have a default function provided by a module, and also helps JavaScript modules to interoperate with existing CommonJS and AMD module systems (as explained nicely in ES6 In Depth: Modules by Jason Orendorff; search for “Default exports”).
Let’s look at an example as we explain how it works. In our basic-modules square.js you can find a function called randomSquare() that creates a square with a random color, size, and position. We want to export this as our default, so at the bottom of the file we write this:
export default randomSquare;
Note the lack of curly braces.
We could instead prepend export default onto the function and define it as an anonymous function, like this:
export default function(ctx) { ... }
Over in our main.js file, we import the default function using this line:
import randomSquare from './modules/square.js';
Again, note the lack of curly braces. This is because there is only one default export allowed per module, and we know that randomSquare is it. The above line is basically shorthand for:
import {default as randomSquare} from './modules/square.js';
Note: The as syntax for renaming exported items is explained below in the Renaming imports and exports section.
Avoiding naming conflicts
So far, our canvas shape drawing modules seem to be working OK. But what happens if we try to add a module that deals with drawing another shape, like a circle or triangle? These shapes would probably have associated functions like draw(), reportArea(), etc. too; if we tried to import different functions of the same name into the same top-level module file, we’d end up with conflicts and errors.
Fortunately there are a number of ways to get around this. We’ll look at these in the following sections.
Renaming imports and exports
Inside your import and export statement’s curly braces, you can use the keyword as along with a new feature name, to change the identifying name you will use for a feature inside the top-level module.
So for example, both of the following would do the same job, albeit in a slightly different way:
// inside module.js export { function1 as newFunctionName, function2 as anotherNewFunctionName }; // inside main.js import { newFunctionName, anotherNewFunctionName } from './modules/module.js';
// inside module.js export { function1, function2 }; // inside main.js import { function1 as newFunctionName, function2 as anotherNewFunctionName } from './modules/module.js';
Let’s look at a real example. In our renaming directory you’ll see the same module system as in the previous example, except that we’ve added circle.js and triangle.js modules to draw and report on circles and triangles.
Inside each of these modules, we’ve got features with the same names being exported, and therefore each has the same export statement at the bottom:
export { name, draw, reportArea, reportPerimeter };
When importing these into main.js, if we tried to use
import { name, draw, reportArea, reportPerimeter } from './modules/square.js'; import { name, draw, reportArea, reportPerimeter } from './modules/circle.js'; import { name, draw, reportArea, reportPerimeter } from './modules/triangle.js';
The browser would throw an error such as “SyntaxError: redeclaration of import name” (Firefox).
Instead we need to rename the imports so that they are unique:
import { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter } from './modules/square.js'; import { name as circleName, draw as drawCircle, reportArea as reportCircleArea, reportPerimeter as reportCirclePerimeter } from './modules/circle.js'; import { name as triangleName, draw as drawTriangle, reportArea as reportTriangleArea, reportPerimeter as reportTrianglePerimeter } from './modules/triangle.js';
Note that you could solve the problem in the module files instead, e.g.
// in square.js export { name as squareName, draw as drawSquare, reportArea as reportSquareArea, reportPerimeter as reportSquarePerimeter };
// in main.js import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from './modules/square.js';
And it would work just the same. What style you use is up to you, however it arguably makes more sense to leave your module code alone, and make the changes in the imports. This especially makes sense when you are importing from third party modules that you don’t have any control over.
Creating a module object
The above method works OK, but it’s a little messy and longwinded. An even better solution is to import each module’s features inside a module object. The following syntax form does that:
import * as Module from './modules/module.js';
This grabs all the exports available inside module.js, and makes them available as members of an object Module, effectively giving it its own namespace. So for example:
Module.function1() Module.function2() etc.
Again, let’s look at a real example. If you go to our module-objects directory, you’ll see the same example again, but rewritten to take advantage of this new syntax. In the modules, the exports are all in the following simple form:
export { name, draw, reportArea, reportPerimeter };
The imports on the other hand look like this:
import * as Canvas from './modules/canvas.js'; import * as Square from './modules/square.js'; import * as Circle from './modules/circle.js'; import * as Triangle from './modules/triangle.js';
In each case, you can now access the module’s imports underneath the specified object name, for example:
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue'); Square.reportArea(square1.length, reportList); Square.reportPerimeter(square1.length, reportList);
So you can now write the code just the same as before (as long as you include the object names where needed), and the imports are much neater.
Modules and classes
As we hinted at earlier, you can also export and import classes; this is another option for avoiding conflicts in your code, and is especially useful if you’ve already got your module code written in an object-oriented style.
You can see an example of our shape drawing module rewritten with ES classes in our classes directory. As an example, the square.js file now contains all its functionality in a single class:
class Square { constructor(ctx, listId, length, x, y, color) { ... } draw() { ... } ... }
which we then export:
export { Square };
Over in main.js, we import it like this:
import { Square } from './modules/square.js';
And then use the class to draw our square:
let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter();
Aggregating modules
There will be times where you’ll want to aggregate modules together. You might have multiple levels of dependencies, where you want to simplify things, combining several submodules into one parent module. This is possible using export syntax of the following forms in the parent module:
export * from 'x.js' export { name } from 'x.js'
For an example, see our module-aggregation directory. In this example (based on our earlier classes example) we’ve got an extra module called shapes.js, which aggregates all the functionality from circle.js, square.js, and triangle.js together. We’ve also moved our submodules inside a subdirectory inside the modules directory called shapes. So the module structure in this example is:
modules/ canvas.js shapes.js shapes/ circle.js square.js triangle.js
In each of the submodules, the export is of the same form, e.g.
export { Square };
Next up comes the aggregation part. Inside shapes.js, we include the following lines:
export { Square } from './shapes/square.js'; export { Triangle } from './shapes/triangle.js'; export { Circle } from './shapes/circle.js';
These grab the exports from the individual submodules and effectively make them available from the shapes.js module.
Note: The exports referenced in shapes.js basically get redirected through the file and don’t really exist there, so you won’t be able to write any useful related code inside the same file.
So now in the main.js file, we can get access to all three module classes by replacing
import { Square } from './modules/square.js'; import { Circle } from './modules/circle.js'; import { Triangle } from './modules/triangle.js';
with the following single line:
import { Square, Circle, Triangle } from './modules/shapes.js';
Dynamic module loading
The newest part of the JavaScript modules functionality to be available in browsers is dynamic module loading. This allows you to dynamically load modules only when they are needed, rather than having to load everything up front. This has some obvious performance advantages; let’s read on and see how it works.
This new functionality allows you to call import() as a function, passing it the path to the module as a parameter. It returns a Promise, which fulfills with a module object (see Creating a module object) giving you access to that object’s exports, e.g.
import('./modules/myModule.js') .then((module) => { // Do something with the module. });
Let’s look at an example. In the dynamic-module-imports directory we’ve got another example based on our classes example. This time however we are not drawing anything on the canvas when the example loads. Instead, we include three buttons — “Circle”, “Square”, and “Triangle” — that, when pressed, dynamically load the required module and then use it to draw the associated shape.
In this example we’ve only made changes to our index.html and main.js files — the module exports remain the same as before.
Over in main.js we’ve grabbed a reference to each button using a Document.querySelector() call, for example:
let squareBtn = document.querySelector('.square');
We then attach an event listener to each button so that when pressed, the relevant module is dynamically loaded and used to draw the shape:
squareBtn.addEventListener('click', () => { import('./modules/square.js').then((Module) => { let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue'); square1.draw(); square1.reportArea(); square1.reportPerimeter(); }) });
Note that, because the promise fulfillment returns a module object, the class is then made a subfeature of the object, hence we now need to access the constructor with Module. prepended to it, e.g. Module.Square( … ).
Troubleshooting
Here are a few tips that may help you if you are having trouble getting your modules to work. Feel free to add to the list if you discover more!
- We mentioned this before, but to reiterate: .js files need to be loaded with a MIME-type of text/javascript (or another JavaScript-compatible MIME-type, but text/javascript is recommended), otherwise you’ll get a strict MIME type checking error like “The server responded with a non-JavaScript MIME type”.
- If you try to load the HTML file locally (i.e. with a file:// URL), you’ll run into CORS errors due to JavaScript module security requirements. You need to do your testing through a server. GitHub pages is ideal as it also serves .js files with the correct MIME type.
- Because .mjs is a non-standard file extension, some operating systems might not recognise it, or try to replace it with something else. For example, we found that macOS was silently adding on .js to the end of .mjs files and then automatically hiding the file extension. So all of our files were actually coming out as x.mjs.js. Once we turned off automatically hiding file extensions, and trained it to accept .mjs, it was OK.
为什么有模块概念
理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,Javascript不是一种模块化编程语言,在es6以前,它是不支持”类”(class),所以也就没有”模块”(module)了。
require时代
Javascript社区做了很多努力,在现有的运行环境中,实现”模块”的效果。
原始写法
模块就是实现特定功能的一组方法。
只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。
function m1(){ //... } function m2(){ //... }
上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就行了。
这种做法的缺点很明显:”污染”了全局变量,无法保证不与其他模块发生变量名冲突,而且模块成员之间看不出直接关系。
对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面
var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } });
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性
module1.m1();
这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module._count = 1;
立即执行函数写法
使用”立即执行函数”(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的
var module = (function() { var _count = 0; var m1 = function() { alert(_count) } var m2 = function() { alert(_count + 1) } return { m1: m1, m2: m2 } })()
使用上面的写法,外部代码无法读取内部的_count变量。
console.info(module._count); //undefined
module就是Javascript模块的基本写法。
主流模块规范
在es6以前,还没有提出一套官方的规范,从社区和框架推广程度而言,目前通行的javascript模块规范有两种:CommonJS 和 AMD
CommonJS规范
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
这标志”Javascript模块化编程”正式诞生。前端的复杂程度有限,没有模块也是可以的,但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node编程中最重要的思想之一就是模块,而正是这个思想,让JavaScript的大规模工程成为可能。模块化编程在js界流行,也是基于此,随后在浏览器端,requirejs和seajs之类的工具包也出现了,可以说在对应规范下,require统治了ES6之前的所有模块化编程,即使现在,在ES6 module被完全实现之前,还是这样。
在CommonJS中,暴露模块使用module.exports和exports,很多人不明白暴露对象为什么会有两个,后面会介绍区别
在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math = require('math');
然后,就可以调用模块提供的方法:
var math = require('math'); math.add(2,3); // 5
正是由于CommonJS 使用的require方式的推动,才有了后面的AMD、CMD 也采用的require方式来引用模块的风格
AMD规范
有了服务器端模块以后,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS规范不适用于浏览器环境。还是上一节的代码,如果在浏览器中运行,会有一个很大的问题
var math = require('math'); math.add(2, 3);
第二行math.add(2, 3),在第一行require(‘math’)之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于”假死”状态。
因此,浏览器端的模块,不能采用”同步加载”(synchronous),只能采用”异步加载”(asynchronous)。这就是AMD规范诞生的背景。
AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
模块必须采用特定的define()函数来定义。
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
- factory: 工厂方法,返回一个模块函数
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
// math.js define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; });
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['Lib'], function(Lib){ function foo(){ Lib.doSomething(); } return { foo : foo }; });
当require()函数加载上面这个模块的时候,就会先加载Lib.js文件。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
require(['math'], function (math) { math.add(2, 3); });
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
CMD规范
CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:
define(function(require, exports, module) { var clock = require('clock'); clock.start(); });
CMD与AMD一样,也是采用特定的define()函数来定义,用require方式来引用模块
define(id?, dependencies?, factory)
- id:字符串,模块名称(可选)
- dependencies: 是我们要载入的依赖模块(可选),使用相对路径。,注意是数组格式
- factory: 工厂方法,返回一个模块函数
define('hello', ['jquery'], function(require, exports, module) { // 模块代码 });
如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
define(function(require, exports, module) { // 模块代码 });
注意:带 id 和 dependencies 参数的 define 用法不属于 CMD 规范,而属于 Modules/Transport 规范。
CMD与AMD区别
AMD和CMD最大的区别是对依赖模块的执行时机处理不同,而不是加载的时机或者方式不同,二者皆为异步加载模块。
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;
而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。
现阶段的标准
ES6标准发布后,module成为标准,标准使用是以export指令导出接口,以import引入模块,但是在我们一贯的node模块中,我们依然采用的是CommonJS规范,使用require引入模块,使用module.exports导出接口。
export导出模块
export语法声明用于导出函数、对象、指定文件(或模块)的原始值。
注意:在node中使用的是exports,不要混淆了
export有两种模块导出方式:命名式导出(名称导出)和默认导出(定义式导出),命名式导出每个模块可以多个,而默认导出每个模块仅一个。
export { name1, name2, …, nameN }; export { variable1 as name1, variable2 as name2, …, nameN }; export let name1, name2, …, nameN; // also var export let name1 = …, name2 = …, …, nameN; // also var, const export default expression; export default function (…) { … } // also class, function* export default function name1(…) { … } // also class, function* export { name1 as default, … }; export * from …; export { name1, name2, …, nameN } from …; export { import1 as name1, import2 as name2, …, nameN } from …;
- name1… nameN-导出的“标识符”。导出后,可以通过这个“标识符”在另一个模块中使用import引用
- default-设置模块的默认导出。设置后import不通过“标识符”而直接引用默认导入
- -继承模块并导出继承模块所有的方法和属性
- as-重命名导出“标识符”
- from-从已经存在的模块、脚本文件…导出
命名式导出
模块可以通过export前缀关键词声明导出对象,导出对象可以是多个。这些导出对象用名称进行区分,称之为命名式导出。
export { myFunction }; // 导出一个已定义的函数 export const foo = Math.sqrt(2); // 导出一个常量
我们可以使用*和from关键字来实现的模块的继承:
export * from 'article';
模块导出时,可以指定模块的导出成员。导出成员可以认为是类中的公有对象,而非导出成员可以认为是类中的私有对象:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name, domain}; // 相当于导出 {name:name,domain:domain}
模块导出时,我们可以使用as关键字对导出成员进行重命名:
var name = 'IT笔录'; var domain = 'http://itbilu.com'; export {name as siteName, domain};
注意,下面的语法有严重错误的情况:
// 错误演示 export 1; // 绝对不可以 var a = 100; export a;
export在导出接口的时候,必须与模块内部的变量具有一一对应的关系。直接导出1没有任何意义,也不可能在import的时候有一个变量与之对应
export a虽然看上去成立,但是a的值是一个数字,根本无法完成解构,因此必须写成export {a}的形式。即使a被赋值为一个function,也是不允许的。而且,大部分风格都建议,模块中最好在末尾用一个export导出所有的接口,例如:
export {fun as default,a,b,c};
默认导出
默认导出也被称做定义式导出。命名式导出可以导出多个值,但在在import引用时,也要使用相同的名称来引用相应的值。而默认导出每个导出只有一个单一值,这个输出可以是一个函数、类或其它类型的值,这样在模块import导入时也会很容易引用。
export default function() {}; // 可以导出一个函数 export default class(){}; // 也可以出一个类
命名式导出与默认导出
默认导出可以理解为另一种形式的命名导出,默认导出可以认为是使用了default名称的命名导出。
下面两种导出方式是等价的:
const D = 123; export default D; export { D as default };
export使用示例
使用名称导出一个模块时:
// "my-module.js" 模块 export function cube(x) { return x * x * x; } const foo = Math.PI + Math.SQRT2; export { foo };
在另一个模块(脚本文件)中,我们可以像下面这样引用:
import { cube, foo } from 'my-module'; console.log(cube(3)); // 27 console.log(foo); // 4.555806215962888
使用默认导出一个模块时:
// "my-module.js"模块 export default function (x) { return x * x * x; }
在另一个模块(脚本文件)中,我们可以像下面这样引用,相对名称导出来说使用更为简单:
// 引用 "my-module.js"模块 import cube from 'my-module'; console.log(cube(3)); // 27
import引入模块
import语法声明用于从已导出的模块、脚本中导入函数、对象、指定文件(或模块)的原始值。
import模块导入与export模块导出功能相对应,也存在两种模块导入方式:命名式导入(名称导入)和默认导入(定义式导入)。
import的语法跟require不同,而且import必须放在文件的最开始,且前面不允许有其他逻辑代码,这和其他所有编程语言风格一致。
import defaultMember from "module-name"; import * as name from "module-name"; import { member } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name";
- name-从将要导入模块中收到的导出值的名称
- member, memberN-从导出模块,导入指定名称的多个成员
- defaultMember-从导出模块,导入默认导出成员
- alias, aliasN-别名,对指定导入成员进行的重命名
- module-name-要导入的模块。是一个文件名
- as-重命名导入成员名称(“标识符”)
- from-从已经存在的模块、脚本文件等导入
命名式导入
我们可以通过指定名称,就是将这些成员插入到当作用域中。导出时,可以导入单个成员或多个成员:
注意,花括号里面的变量与export后面的变量一一对应
import {myMember} from "my-module"; import {foo, bar} from "my-module";
通过*符号,我们可以导入模块中的全部属性和方法。当导入模块全部导出内容时,就是将导出模块(’my-module.js’)所有的导出绑定内容,插入到当前模块(’myModule’)的作用域中:
import * as myModule from "my-module";
导入模块对象时,也可以使用as对导入成员重命名,以方便在当前模块内使用:
import {reallyReallyLongModuleMemberName as shortName} from "my-module";
导入多个成员时,同样可以使用别名:
import {reallyReallyLongModuleMemberName as shortName, anotherLongModuleName as short} from "my-module";
导入一个模块,但不进行任何绑定:
import "my-module";
默认导入
在模块导出时,可能会存在默认导出。同样的,在导入时可以使用import指令导出这些默认值。
直接导入默认值:
import myDefault from "my-module";
也可以在命名空间导入和名称导入中,同时使用默认导入:
import myDefault, * as myModule from "my-module"; // myModule 做为命名空间使用 或 import myDefault, {foo, bar} from "my-module"; // 指定成员导入
import使用示例
// --file.js-- function getJSON(url, callback) { let xhr = new XMLHttpRequest(); xhr.onload = function () { callback(this.responseText) }; xhr.open("GET", url, true); xhr.send(); } export function getUsefulContents(url, callback) { getJSON(url, data => callback(JSON.parse(data))); } // --main.js-- import { getUsefulContents } from "file"; getUsefulContents("http://itbilu.com", data => { doSomethingUseful(data); });
default关键字
// d.js export default function() {} // 等效于: function a() {}; export {a as default};
在import的时候,可以这样用:
import a from './d'; // 等效于,或者说就是下面这种写法的简写,是同一个意思 import {default as a} from './d';
这个语法糖的好处就是import的时候,可以省去花括号{}。
简单的说,如果import的时候,你发现某个变量没有花括号括起来(没有*号),那么你在脑海中应该把它还原成有花括号的as语法。
所以,下面这种写法你也应该理解了吧:
import $,{each,map} from 'jquery';
import后面第一个$是{defalut as $}的替代写法。
as关键字
as简单的说就是取一个别名,export中可以用,import中其实可以用:
// a.js var a = function() {}; export {a as fun}; // b.js import {fun as a} from './a'; a();
上面这段代码,export的时候,对外提供的接口是fun,它是a.js内部a这个函数的别名,但是在模块外面,认不到a,只能认到fun。
import中的as就很简单,就是你在使用模块里面的方法的时候,给这个方法取一个别名,好在当前的文件里面使用。之所以是这样,是因为有的时候不同的两个模块可能通过相同的接口,比如有一个c.js也通过了fun这个接口:
// c.js export function fun() {};
如果在b.js中同时使用a和c这两个模块,就必须想办法解决接口重名的问题,as就解决了。
CommonJS中module.exports 与 exports的区别
Module.exports
The module.exports object is created by the Module system. Sometimes this is not acceptable; many want their module to be an instance of some class. To do this, assign the desired export object to module.exports. Note that assigning the desired object to exports will simply rebind the local exports variable, which is probably not what you want to do.
译文:module.exports对象是由模块系统创建的。 有时这是难以接受的;许多人希望他们的模块成为某个类的实例。 为了实现这个,需要将期望导出的对象赋值给module.exports。 注意,将期望的对象赋值给exports会简单地重新绑定到本地exports变量上,这可能不是你想要的。
Module.exports
The exports variable is available within a module’s file-level scope, and is assigned the value of module.exports before the module is evaluated. It allows a shortcut, so that module.exports.f = … can be written more succinctly as exports.f = …. However, be aware that like any variable, if a new value is assigned to exports, it is no longer bound to module.exports:
译文:exports变量是在模块的文件级别作用域内有效的,它在模块被执行前被赋于 module.exports 的值。它有一个快捷方式,以便 module.exports.f = … 可以被更简洁地写成exports.f = …。 注意,就像任何变量,如果一个新的值被赋值给exports,它就不再绑定到module.exports(其实是exports.属性会自动挂载到没有命名冲突的module.exports.属性)
从Api文档上面的可以看出,从require导入方式去理解,关键有两个变量(全局变量module.exports,局部变量exports)、一个返回值(module.exports)
function require(...) { var module = { exports: {} }; ((module, exports) => { // 你的被引入代码 Start // var exports = module.exports = {}; (默认都有的) function some_func() {}; exports = some_func; // 此时,exports不再挂载到module.exports, // export将导出{}默认对象 module.exports = some_func; // 此时,这个模块将导出some_func对象,覆盖exports上的some_func // 你的被引入代码 End })(module, module.exports); // 不管是exports还是module.exports,最后返回的还是module.exports return module.exports; }
demo.js:
console.log(exports); // {} console.log(module.exports); // {} console.log(exports === module.exports); // true console.log(exports == module.exports); // true console.log(module); /** Module { id: '.', exports: {}, parent: null, filename: '/Users/larben/Desktop/demo.js', loaded: false, children: [], paths: [ '/Users/larben/Desktop/node_modules', '/Users/larben/node_modules', '/Users/node_modules', '/node_modules' ] } */
注意
- 每个js文件一创建,都有一个var exports = module.exports = {},使exports和module.exports都指向一个空对象。
- module.exports和exports所指向的内存地址相同