JavaScript进阶之(十) 模块化

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

一、模块化的背景

Javascript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 Javascript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。

因此,近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。Node.js 已经提供这个能力很长时间了,还有很多的 Javascript 库和框架 已经开始了模块的使用(例如, CommonJS 和基于 AMD 的其他模块系统 如 RequireJS, 以及最新的 Webpack 和 Babel)。

好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 — 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。

二、例子讲解

为了演示模块的使用,我们创建了一个 simple set of examples ,你可以在Github上找到。这个例子演示了一个简单的模块的集合用来在web页面上创建了一个 标签,在canvas上绘制 (并记录有关的信息) 不同形状。

这的确有点简单,但是保持足够简单能够清晰地演示模块。

2.1 基本的文件的结构

其中一个例子 (basic-modules) 文件结构如下
在这里插入图片描述
modules 目录下的两个模块的描述如下:

  • canvas.js — 包含与设置画布相关的功能:
    create() —在指定ID的包装器<div>内创建指定width 和height 的画布,该ID本身附加在指定的父元素内。 返回包含画布的2D上下文和包装器ID的对象。
    createReportList() — 创建一个附加在指定包装器元素内的无序列表,该列表可用于将报告数据输出到。 返回列表的ID。
  • square.js — 包含:
    name — 包含字符串’square’的常量。
    draw() — 在指定画布上绘制一个正方形,具有指定的大小,位置和颜色。 返回包含正方形大小,位置和颜色的对象。
    reportArea() — 在给定长度的情况下,将正方形区域写入特定报告列表。
    reportPerimeter() — 在给定长度的情况下,将正方形的周长写入特定的报告列表。
  • mian.js (2.3 小结)
    import 导入canvas.js和square.js声明的函数,并使用他们。
  • index.html (2.4 小结)
    应用模块到我们的HTML中

2.2 导出模块的功能

为了获得模块的功能要做的第一件事是把它们导出来。使用 export 语句来完成。

最简单的方法是把它(指上面的export语句)放到你想要导出的项前面,比如:

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
  };
}

你能够导出函数,var,let,const, 和等会会看到的类。export要放在最外层;比如你不能够在函数内使用export。
一个更方便的方法导出所有你想要导出的模块的方法是在模块文件的末尾使用一个export 语句, 语句是用花括号括起来的用逗号分割的列表。比如:

export { name, draw, reportArea, reportPerimeter };

2.2.1 导出export语法

在创建JavaScript模块时,export 语句用于从模块中导出实时绑定的函数、对象或原始值,以便其他程序可以通过 import 语句使用它们。被导出的绑定值依然可以在本地进行修改。在使用import进行导入时,这些绑定值只能被导入模块所读取,但在export导出模块中对这些绑定值进行修改,所修改的值也会实时地更新。

无论您是否声明,导出的模块都处于严格模式。 export语句不能用在嵌入式脚本中。

2.2.1.1 语法

存在两种 exports 导出方式:

  • 命名导出(每个模块包含任意数量)
  • 默认导出(每个模块包含一个
// 导出单个特性
export let name1, name2,, nameN; // also var, const
export let name1 =, name2 =,, nameN; // also var, const
export function FunctionName(){...}
export class ClassName {...}

// 导出列表
export { name1, name2,, nameN };

// 重命名导出
export { variable1 as name1, variable2 as name2,, nameN };

// 解构导出并重命名
export const { name1, name2: bar } = o;

// 默认导出
export default expression;
export default function () {} // also class, function*
export default function name1() {} // also class, function*
export { name1 as default,};

// 导出模块合集
export * from; // does not set the default export
export * as name1 from; // Draft ECMAScript® 2O21
export { name1, name2,, nameN } from;
export { import1 as name1, import2 as name2,, nameN } from;
export { default } from;
2.2.1.2 描述

有两种不同的导出方式,命名导出和默认导出。你能够在每一个模块中定义多个命名导出,但是只允许有一个默认导出。每种方式对应于上述的一种语法:

  • (1)命名导出:
// 导出事先定义的特性
export { myFunction,myVariable }; 

// 导出单个特性(可以导出var,let,
//const,function,class)
export let myVariable = Math.sqrt(2);
export function myFunction() { ... };
  • (2)默认导出:
// 导出事先定义的特性作为默认值
export { myFunction as default };

// 导出单个特性作为默认值
export default function () { ... } 
export default class { .. }

// 每个导出都覆盖前一个导出

在导出多个值时,命名导出非常有用。在导入期间,必须使用相应对象的相同名称。

但是,可以使用任何名称导入默认导出,例如:

// 文件 test.js
let k; export default k = 12; 
// 另一个文件
import m from './test'; // 由于 k 是默认导出,所以可以自由使用 import m 替代 import k
console.log(m);        // 输出为 12 

你也可以重命名命名导出以避免命名冲突:

export { myFunction as function1,
         myVariable as variable };
  • (3)重导出 / 聚合

为了使模块导入变得可用,在一个父模块中“导入/导出”这些不同模块也是可行的。也就是说,你可以创建单个模块,集中多个模块的多个导出。

这个可以使用“export from”语法实现:

export { default as function1,
         function2 } from 'bar.js';

与之形成对比的是联合使用导入和导出:

import { default as function1,
         function2 } from 'bar.js';
export { function1, function2 };

但这里的 function1 和 function2 在当前模块中变得不可用。

注意:尽管与import等效,但以下语法在语法上无效:

import DefaultExport from 'bar.js'; // 有效的
export DefaultExport from 'bar.js'; // 无效的

这里正确的做法是重命名这个导出:

export { default as DefaultExport } from 'bar.js';
2.2.1.3 例子

(1)使用命名导出
在模块 my-module.js 中,可能包含以下代码:

// module "my-module.js"
function cube(x) {
  return x * x * x;
}

const foo = Math.PI + Math.SQRT2;

var graph = {
    options: {
        color:'white',
        thickness:'2px'
    },
    draw: function() {
        console.log('From graph draw function');
    }
}

export { cube, foo, graph };

然后,在你的 HTML 页面的顶级模块中:

import { cube, foo, graph } from 'my-module.js';

graph.options = {
    color:'blue',
    thickness:'3px'
};
 
graph.draw();
console.log(cube(3)); // 27
console.log(foo);    // 4.555806215962888

着重注意以下几点:

  • 在你的 HTML 中需要包含 type=“module” 的 <script> 元素这样的脚本,以便它被识别为模块并正确处理
  • 不能通过 file:// URL 运行 JS 模块 — 这将导致 CORS 错误。你需要通过 HTTP 服务器运行

(2)使用默认导出
如果我们要导出一个或得到模块中的返回值,就可以使用默认导出:

// module "my-module.js"

export default function cube(x) {
  return x * x * x;
}

然后,在另一个脚本中,可以直接导入默认导出:

import cube from './my-module.js';
console.log(cube(3)); // 27​​​​​

(3)模块重定向
举个例子,假如我们有如下层次结构:

  • childModule1.js: 导出 myFunction 和 myVariable
  • childModule2.js: 导出 myClass
  • parentModule.js: 作为聚合器(不做其他事情)
  • 顶层模块:调用 parentModule.js 的导出项

你的代码看起来应该像这样:

// childModule1.js 中
let myFunction = ...; // assign something useful to myFunction
let myVariable = ...; // assign something useful to myVariable
export {myFunction, myVariable};
// childModule2.js 中
let myClass = ...; // assign something useful to myClass
export myClass;
// parentModule.js 中
// 仅仅聚合 childModule1 和 childModule2 中的导出
// 以重新导出他们
export { myFunction, myVariable } from 'childModule1.js';
export { myClass } from 'childModule2.js';
// 顶层模块中
// 我们可以从单个模块调用所有导出,因为 parentModule 事先
// 已经将他们“收集”/“打包”到一起
import { myFunction, myVariable, myClass } from 'parentModule.js'

2.2.2 例子中的详细代码

canvas.js 中代码及导出模块

function create(id, parent, width, height) {
  let divWrapper = document.createElement('div');
  let canvasElem = document.createElement('canvas');
  parent.appendChild(divWrapper);
  divWrapper.appendChild(canvasElem);

  divWrapper.id = id;
  canvasElem.width = width;
  canvasElem.height = height;

  let ctx = canvasElem.getContext('2d');

  return {
    ctx: ctx,
    id: id
  };
}

function createReportList(wrapperId) {
  let list = document.createElement('ul');
  list.id = wrapperId + '-reporter';

  let canvasWrapper = document.getElementById(wrapperId);
  canvasWrapper.appendChild(list);

  return list.id;
}

export { create, createReportList };

square.js 中代码及导出模块

const name = 'square';

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
  };
}

function random(min, max) {
   let num = Math.floor(Math.random() * (max - min)) + min;
   return num;
}

function reportArea(length, listId) {
  let listItem = document.createElement('li');
  listItem.textContent = `${name} area is ${length * length}px squared.`

  let list = document.getElementById(listId);
  list.appendChild(listItem);
}

function reportPerimeter(length, listId) {
  let listItem = document.createElement('li');
  listItem.textContent = `${name} perimeter is ${length * 4}px.`

  let list = document.getElementById(listId);
  list.appendChild(listItem);
}

function randomSquare(ctx) {
  let color1 = random(0, 255);
  let color2 = random(0, 255);
  let color3 = random(0, 255);
  let color = `rgb(${color1},${color2},${color3})`
  ctx.fillStyle = color;

  let x = random(0, 480);
  let y = random(0, 320);
  let length = random(10, 100);
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}

export { name, draw, reportArea, reportPerimeter };
export default randomSquare;

2.3 导入功能到你的脚本

你想在模块外面使用一些功能,那你就需要导入他们才能使用。最简单的就像下面这样的

import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/basic-modules/modules/square.js';

使用 import 语句,然后你被花括号包围的用逗号分隔的你想导入的功能列表,然后是关键字from,然后是模块文件的路径。模块文件的路径是相对于站点根目录的相对路径,对于我们的basic-modules 应该是 /js-examples/modules/basic-modules。

当然,我们写的路径有一点不同—我们使用点语法意味 “当前路径”,跟随着包含我们想要找的文件的路径。这比每次都要写下整个相对路径要好得多,因为它更短,使得URL 可移植 —如果在站点层中你把它移动到不同的路径下面仍然能够工作。

那么看看例子吧:

/js/examples/modules/basic-modules/modules/square.js

变成了

./modules/square.js

2.3.1 导入import 语法

静态的import 语句用于导入由另一个模块导出的绑定。无论是否声明了 strict mode ,导入的模块都运行在严格模式下。在浏览器中,import 语句只能在声明了 type=“module” 的 script 的标签中使用。

此外,还有一个类似函数的动态 import(),它不需要依赖 type=“module” 的script标签。

在 script 标签中使用 nomodule 属性,可以确保向后兼容。

在您希望按照一定的条件或者按需加载模块的时候,动态import() 是非常有用的。而静态型的 import 是初始化加载依赖项的最优选择,使用静态 import 更容易从代码静态分析工具和 tree shaking 中受益。

2.3.2.1 语法
import defaultExport from "module-name";
import * as name from "module-name";
import { export } from "module-name";
import { export as alias } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");//这是一个处于第三阶段的提案。
  • defaultExport

导入模块的默认导出接口的引用名。

  • module-name

要导入的模块。通常是包含目标模块的.js文件的相对或绝对路径名,可以不包括.js扩展名。某些特定的打包工具可能允许或需要使用扩展或依赖文件,它会检查比对你的运行环境。只允许单引号和双引号的字符串。

  • name

导入模块对象整体的别名,在引用导入模块时,它将作为一个命名空间来使用。

  • export, exportN

被导入模块的导出接口的名称。

  • alias, aliasN

将引用指定的导入的名称。

2.3.2.2 描述

name参数是“导入模块对象”的名称,它将用一种名称空间来引用导入模块的接口。export参数指定单个的命名导出,而import * as name语法导入所有导出接口,即导入模块整体。以下示例阐明该语法。

(1)导入整个模块的内容
这将myModule插入当前作用域,其中包含来自位于/modules/my-module.js文件中导出的所有接口。

import * as myModule from '/modules/my-module.js';

在这里,访问导出接口意味着使用模块名称(在本例为“myModule”)作为命名空间。例如,如果上面导入的模块包含一个接口doAllTheAmazingThings(),你可以这样调用:

myModule.doAllTheAmazingThings();

(2)导入单个接口
给定一个名为myExport的对象或值,它已经从模块my-module导出(因为整个模块被导出)或显式地导出(使用export语句),将myExport插入当前作用域。

import {myExport} from '/modules/my-module.js';

(3)导入多个接口
这将foo和bar插入当前作用域。

import {foo, bar} from '/modules/my-module.js';

(4)导入带有别名的接口
你可以在导入时重命名接口。例如,将shortName插入当前作用域。

import {reallyReallyLongModuleExportName as shortName}
  from '/modules/my-module.js';

(5)导入时重命名多个接口

import {
  reallyReallyLongModuleMemberName as shortName, 
  anotherLongModuleName as short
} from '/modules/my-module.js';

(6)仅为副作用而导入一个模块
整个模块仅为副作用(中性词,无贬义含义)而导入,而不导入模块中的任何内容(接口)。 这将运行模块中的全局代码, 但实际上不导入任何值

import '/modules/my-module.js';

(7)导入默认值
引入模块可能有一个default export(无论它是对象,函数,类等)可用。然后可以使用import语句来导入这样的默认接口。

最简单的用法是直接导入默认值:

import myDefault from '/modules/my-module.js';

也可以同时将default语法与上述用法(命名空间导入或命名导入)一起使用。在这种情况下,default导入必须首先声明。 例如:

import myDefault, * as myModule from '/modules/my-module.js';
// myModule used as a namespace

或者

import myDefault, {foo, bar} from '/modules/my-module.js';
// specific, named imports

当使用动态导入一个默认的导出时,情况稍微一些不同。你需要解构和重命名“default”关键字以返回一个对象。
When importing a default export with dynamic imports, it works a bit differently. You need to destructure and rename the “default” key from the returned object.

(async () => {
  if (somethingIsTrue) {
    const { default: myDefault, foo, bar } = await import('/modules/my-module.js');
  }
})();

(8)动态import
标准用法的import导入的模块是静态的,会使所有被导入的模块,在加载时就被编译(无法做到按需编译,降低首页加载速度)。有些场景中,你可能希望根据条件导入模块或者按需导入模块,这时你可以使用动态导入代替静态导入。下面的是你可能会需要动态导入的场景:

  • 当静态导入的模块很明显的降低了代码的加载速度且被使用的可能性很低,或者并不需要马上使用它。
  • 当静态导入的模块很明显的占用了大量系统内存且被使用的可能性很低。
  • 当被导入的模块,在加载时并不存在,需要异步获取
  • 当导入模块的说明符,需要动态构建。(静态导入只能使用静态说明符)
  • 当被导入的模块有副作用(这里说的副作用,可以理解为模块中会直接运行的代码),这些副作用只有在触发了某些条件才被需要时。(原则上来说,模块不能有副作用,但是很多时候,你无法控制你所依赖的模块的内容)

请不要滥用动态导入(只有在必要情况下采用)。静态框架能更好的初始化依赖,而且更有利于静态分析工具和tree shaking发挥作用

关键字import可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个 promise。

import('/modules/my-module.js')
  .then((module) => {
    // Do something with the module.
  });

这种使用方式也支持 await 关键字。

let module = await import('/modules/my-module.js');
2.3.2.3 例子

(1)标准导入
下面的代码将会演示如何从辅助模块导入以协助处理AJAX JSON请求。

模块: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 '/modules/file.js';

getUsefulContents('http://www.example.com',
    data => { doSomethingUseful(data); });

(2)动态导入
此示例展示了如何基于用户操作去加载功能模块到页面上,在例子中通过点击按钮,然后会调用模块内的函数。当然这不是能实现这个功能的唯一方式,import()函数也可以支持await。

const main = document.querySelector("main");
for (const link of document.querySelectorAll("nav > a")) {
  link.addEventListener("click", e => {
    e.preventDefault();

    import('/modules/my-module.js')
      .then(module => {
        module.loadPageInto(main);
      })
      .catch(err => {
        main.textContent = err.message;
      });
  });
}

2.3.1 mian.js中代码

import { create, createReportList } from './modules/canvas.js';
import { name, draw, reportArea, reportPerimeter } from './modules/square.js';
import randomSquare from './modules/square.js';

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);

// Use the default
let square2 = randomSquare(myCanvas.ctx);

2.4 应用模块到你的HTML

现在我们只需要将main.js模块应用到我们的HTML页面。 这与我们将常规脚本应用于页面的方式非常相似,但有一些显着的差异。

首先,你需要把 type=“module” 放到 <script> 标签中, 来声明这个脚本是一个模块:

<script type="module" src="main.js"></script>

你导入模块功能的脚本基本是作为顶级模块。 如果省略它,Firefox就会给出错误“SyntaxError: import declarations may only appear at top level of a module。

你只能在模块内部使用 import 和export 语句 ;不是普通脚本文件。

index.html中的代码

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>Basic JavaScript module example</title>
    <style>
      canvas {
        border: 1px solid black;
      }
    </style>
    <script type="module" src="main.js"></script> //代码源 main.js 2.3.1 小结
  </head>
  <body>

  </body>
</html>

三、导入导出的提高

3.1 模块(module)与标准脚本的不同

  • 你需要注意本地测试 — 如果你通过本地加载Html 文件 (比如一个 file:// 路径的文件), 你将会遇到 CORS
    错误,因为Javascript 模块安全性需要。你需要通过一个服务器来测试。
  • 另请注意,您可能会从模块内部定义的脚本部分获得不同的行为,而不是标准脚本。 这是因为模块自动使用严格模式。 加载一个模块脚本时不需要使用defer 属性 (see <script> attributes) 模块会自动延迟加载。
  • 最后一个但不是不重要,你需要明白模块功能导入到单独的脚本文件的范围 —他们无法在全局获得。因此,你只能在导入这些功能的脚本文件中使用他们,你也无法通过Javascript console中获取到他们,比如,在DevTools 中你仍然能够获取到语法错误,但是你可能无法像你想的那样使用一些debug 技术

3.2 默认导出和命名导出对比

还有一种导出类型叫做 default export —这样可以很容易地使模块提供默认功能,并且还可以帮助JavaScript模块与现有的CommonJS和AMD模块系统进行互操作。

看个例子来解释它如何工作。在我们的基本模块square.js中,您可以找到一个名为randomSquare()的函数,它创建一个具有随机颜色,大小和位置的正方形。我们想作为默认导出,所以在文件的底部我们这样写 :

export default randomSquare;

注意,不要大括号。

我们可以把 export default 放到函数前面,定义它为一个匿名函数,像这样:

export default function(ctx) {
  ...
}

在我们的main.js 文件中,我们使用以下行导入默认函数:

import randomSquare from './modules/square.js';

同样,没有大括号,因为每个模块只允许有一个默认导出, 我们知道 randomSquare 就是需要的那个。上面的那一行相当于下面的缩写:

import {default as randomSquare} from './modules/square.js';

3.3 重命名导出与导入

在你的 import 和 export 语句的大括号中,可以使用 as 关键字跟一个新的名字,来改变你在顶级模块中将要使用的功能的标识名字。因此,例如,以下两者都会做同样的工作,尽管方式略有不同:

// 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';

让我们看一个真实的例子。在我们的重命名目录中,您将看到与上一个示例中相同的模块系统,除了我们添加了circle.js和triangle.js模块以绘制和报告圆和三角形。

在每个模块中,我们都有export 相同名称的功能,因此每个模块底部都有相同的导出语句:

export { name, draw, reportArea, reportPerimeter };

将它们导入main.js时,如果我们尝试使用

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';

浏览器会抛出一个错误,例如“SyntaxError: redeclaration of import name”(Firefox)。
相反,我们需要重命名导入,使它们是唯一的:

mport { 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';

请注意,您可以在模块文件中解决问题,例如

// 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 '/js-examples/modules/renaming/modules/square.js';

它也会起作用。 你使用什么样的风格取决于你,但是单独保留模块代码并在导入中进行更改可能更有意义。 当您从没有任何控制权的第三方模块导入时,这尤其有意义。

3.4 创建模块对象

上面的方法工作的挺好,但是有一点点混乱、亢长。一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式:

import * as Module from '/modules/module.js';

这将获取module.js中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间。 例如:

Module.function1()
Module.function2()
etc.

再次,让我们看一个真实的例子。如果您转到我们的module-objects目录,您将再次看到相同的示例,但利用上述的新语法进行重写。在模块中,导出都是以下简单形式:

export { name, draw, reportArea, reportPerimeter };

另一方面,导入看起来像这样:

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';

在每种情况下,您现在可以访问指定对象名称下面的模块导入

let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);

因此,您现在可以像以前一样编写代码(只要您在需要时包含对象名称),并且导入更加整洁。

3.5 模块与类(class)

正如我们之前提到的那样,您还可以导出和导入类; 这是避免代码冲突的另一种选择,如果您已经以面向对象的方式编写了模块代码,那么它尤其有用。

您可以在我们的classes 目录中看到使用ES类重写的形状绘制模块的示例。 例如,square.js 文件现在包含单个类中的所有功能:

class Square {
  constructor(ctx, listId, length, x, y, color) {
    ...
  }

  draw() {
    ...
  }

  ...
}

然后我们导出:

export { Square };

在main.js中,我们像这样导入它:

import { Square } from './modules/square.js';

然后使用该类绘制我们的方块:

let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();

3.6 合并模块

有时你会想要将模块聚合在一起。 您可能有多个级别的依赖项,您希望简化事物,将多个子模块组合到一个父模块中。 这可以使用父模块中以下表单的导出语法:

export * from 'x.js'
export { name } from 'x.js'

有关示例,请参阅我们的module-aggregation。 在这个例子中(基于我们之前的类示例),我们有一个名为shapes.js的额外模块,它将circle.js,square.js和riangle.js中的所有功能聚合在一起。 我们还将子模块移动到名为shapes的modules目录中的子目录中。 所以模块结构现在是这样的:

modules/
  canvas.js
  shapes.js
  shapes/
    circle.js
    square.js
    triangle.js

在每个子模块中,输出具有相同的形式,例如,

export { Square };

接下来是聚合部分。 在shapes.js里面,我们包括以下几行:

export { Square } from '/js-examples/modules/module-aggregation/modules/shapes/square.js';
export { Triangle } from '/js-examples/modules/module-aggregation/modules/shapes/triangle.js';
export { Circle } from '/js-examples/modules/module-aggregation/modules/shapes/circle.js';

它们从各个子模块中获取导出,并有效地从shapes.js模块中获取它们。

所以现在在main.js 文件中,我们可以通过替换来访问所有三个模块类

import { Square } from './modules/square.js';
import { Circle } from './modules/circle.js';
import { Triangle } from './modules/triangle.js';

使用以下单行:

import { Square, Circle, Triangle } from './modules/shapes.js';

3.7 动态加载模块

浏览器中可用的JavaScript模块功能的最新部分是动态模块加载。 这允许您仅在需要时动态加载模块,而不必预先加载所有模块。 这有一些明显的性能优势; 让我们继续阅读,看看它是如何工作的。

这个新功能允许您将import()作为函数调用,将其作为参数传递给模块的路径。 它返回一个 promise,它用一个模块对象来实现(参见Creating a module object),让你可以访问该对象的导出,例如

import('/modules/myModule.js')
  .then((module) => {
    // Do something with the module.
  });

我们来看一个例子。 在dynamic-module-imports 目录中,我们有另一个基于类示例的示例。 但是这次我们在示例加载时没有在画布上绘制任何东西。 相反,我们包括三个按钮 - “圆形”,“方形”和“三角形” - 按下时,动态加载所需的模块,然后使用它来绘制相关的形状。

在这个例子中,我们只对index.html 和main.js文件进行了更改 - 模块导出保持与以前相同。

在main.js中,我们使用document.querySelector()调用获取了对每个按钮的引用,例如:

let squareBtn = document.querySelector('.square');

然后,我们为每个按钮附加一个事件监听器,以便在按下时,相关模块被动态加载并用于绘制形状:

squareBtn.addEventListener('click', () => {
  import('/js-examples/modules/dynamic-module-imports/modules/square.js').then((Module) => {
    let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
    square1.draw();
    square1.reportArea();
    square1.reportPerimeter();
  })
});

请注意,由于promise履行会返回一个模块对象,因此该类成为对象的子特征,因此我们现在需要使用 Module访问构造函数。 在它之前,例如 Module.Square( … )。

3.8 故障排除

如果为了你的模块有问题,这里有一些提示有可能帮助到你。如果你发现更多的内容欢迎添加进来!

  • 在前面已经提到了,在这里再重申一次: .js 后缀的文件需要以 MIME-type 为 javascript/esm
    来加载(或者其他的JavaScript 兼容的 MIME-type ,比如 application/javascript),
    否则,你会一个严格的 MIME 类型检查错误,像是这样的 “The server responded with a
    non-JavaScript MIME type”.

  • 如果你尝试用本地文件加载HTML 文件 (i.e. with a file:// URL), 由于JavaScript
    模块的安全性要求,你会遇到CORS 错误。你需要通过服务器来做你的测试。GitHub pages is ideal as it also
    serves .js files with the correct MIME type.

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值