二、模块化开发

61 阅读8分钟

JavaScript模块化开发

CommonJS和Node

我们需要知道CommonJS是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了 体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS。

  • Node是CommonJS在服务器端一个具有代表性的实现;

  • Browserify是CommonJS在浏览器中的一种实现;

  • webpack打包工具具备对CommonJS的支持和转换;

所以, Node中对CommonJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

  • 在Node中每一个js文件都是一个单独的模块;

  • 这个模块中包括CommonJS规范的核心变量: exports、 module.exports、 require;

  • 我们可以使用这些变量来方便的进行模块化开发;

前面我们提到过模块化的核心是导出和导入, Node中对其进行了实现:

  • exports和module.exports可以负责对模块中的内容进行导出;

  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;3

exports导出

注意: exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出:

exports.name = name;
exports.age = age;
exports.sayHello = sayHello;

另外一个文件中可以导入:

const { name, age, sayHello } = require("./bar");

它们实际上是一个浅层拷贝

为了进一步论证, bar和exports是同一个对象:

  • 所以, bar对象是exports对象的浅拷贝(引用赋值);

  • 浅拷贝的本质就是一种引用的赋值而已;

image.png

module.exports又是什么

我们追根溯源,通过维基百科中对CommonJS规范的解析:

  • CommonJS中是没有module.exports的概念的;

  • 但是为了实现模块的导出, Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是 module;

  • 所以在Node中真正用于导出的其实根本不是exports,而是module.exports;

  • 因为module才是导出的真正实现者;

node源码里做了module.exports=exports处理

require细节

require(x)几个常见的查找规则

一、 X是一个核心模块,比如path、http

  • 直接返回核心模块,停止查找

二、 X是以./或../或/(根目录)开头的

  • 第一步:将X当做一个文件在对应的目录下查找;

    • 1.如果有后缀名,按照后缀名的格式查找对应的文件

    • 2.如果没有后缀名,会按照如下顺序:

      • 1> 直接查找文件X

      • 2> 查找X.js文件

      • 3> 查找X.json文件

      • 4> 查找X.node文件

  • 第二步:没有找到对应的文件,将X作为一个目录

    • 查找目录下面的index文件

      • 1> 查找X/index.js文件

      • 2> 查找X/index.json文件

      • 3> 查找X/index.node文件

    • 如果没有找到,那么报错:not found

  • 情况三:直接是一个X(没有路径),并且X不是一个核心模块

    • 会从当前目录逐级往上级查找 node_modules目录

模块的加载过程

  • 结论一:模块在被第一次引入时,模块中的js代码会被运行一次

  • 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次

    • 为什么只会加载运行一次呢?

    • 这是因为每个模块对象module都有一个属性:loaded。

    • 为false表示还没有加载,为true表示已经加载;

  • 结论三:如果有循环引入,那么加载顺序是什么?

image.png

- 如果出现上图模块的引用关系,那么加载顺序是什么呢?

- 这个其实是一种数据结构:图结构;

- 图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);

- Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

CommonJS规范缺点

  • CommonJS加载模块是同步的:

    • 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行;

    • 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快;

  • 如果将它应用于浏览器呢?

    • 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行;

    • 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作;

  • 所以在浏览器中,我们通常不使用CommonJS规范:

    • 当然在webpack中使用CommonJS是另外一回事;

    • 因为它会将我们的代码转成浏览器可以直接执行的代码;

  • 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD:

    • 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者 ES Module代码的转换;

    • AMD和CMD已经使用非常少了

AMD规范

  • AMD主要是应用于浏览器的一种模块化规范:

    • AMD是Asynchronous Module Definition(异步模块定义)的缩写;

    • 它采用的是异步加载模块;

    • 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;

  • 我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:

    • AMD实现的比较常用的库是require.js和curl.js;

require.js

  • 第一步:下载require.js

  • 第二步:定义HTML的script标签引入require.js和定义入口文件:

    • data-main属性的作用是在加载完src的文件后会加载执行该文件

image.png

  • bar.js
define(function () {
  const name = "Jay";
  const age = 25;
  const sayHello = function (name) {
    console.log("你好" + name);
  };

  return {
    name,
    age,
    sayHello,
  };
});
  • foo.js
define(["bar"], function (bar) {
  console.log(bar.name);
  console.log(bar.age);
  bar.sayHello("Jay");
});
  • index.js
(function () {
  require.config({
    baseUrl: "",
    paths: {
      bar: "./modules/bar",
      foo: "./modules/foo",
    },
  });

  require(["foo"], function (foo) {});
})();
  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./lib/require.js" data-main="./index.js"></script>
  </body>
</html>

最终输出:

image.png

CMD规范

  • CMD规范也是应用于浏览器的一种模块化规范:

    • CMD 是Common Module Definition(通用模块定义)的缩写;

    • 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;

    • 但是目前CMD使用也非常少了;

  • CMD也有自己比较优秀的实现方案:

    • SeaJS

SeaJS的使用

  • 第一步:下载SeaJS

  • 第二步:引入sea.js和使用主入口文件

    • seajs是指定主入口文件的

image.png

  • foo.js
define(function (require, exports, module) {
  module.exports = {
    name: "Jay",
    age: 28,
    sayHello: function name(params) {
      console.log("你好" + params);
    },
  };
});

  • index.js
define(function (require, exports, module) {
  const foo = require("./modules/foo");

  console.log(foo.name);
  console.log(foo.age);
  foo.sayHello("Jay");
});

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="./lib/sea.js"></script>
    <script>
      seajs.use("./index.js");
    </script>
  </body>
</html>

最终输出:

image.png

认识 ES Module

JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等, 所以在ES推出自己的模块化系统时,大家也是兴奋异常。

  • ES Module和CommonJS的模块化有一些不同之处:

    • 一方面它使用了import和export关键字;

    • 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;

  • ES Module模块采用export和import关键字来实现模块化:

    • export负责将模块内的内容导出;

    • import负责从其他模块导入内容;

  • 了解:采用ES Module将自动采用严格模式:use strict

export关键字

export关键字将一个模块中的变量、函数、类等导出

  • 方式一:在语句声明的前面直接加上export关键字

  • 方式二:将所有需要导出的标识符,放到export后面的 {}中

    • 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;

    • 所以: export {name: name},是错误的写法;

  • 方式三:导出时给标识符起一个别名

import关键字

导入内容的方式也有多种:

  • 方式一:import {标识符列表} from '模块';

    • 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
  • 方式二:导入时给标识符起别名

  • 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上

default用法

  • 还有一种导出叫做默认导出(default export)

    • 默认导出export时可以不需要指定名字;

    • 在导入时不需要使用 {},并且可以自己来指定名字;

    • 它也方便我们和现有的CommonJS等规范相互操作;

在一个模块中,只能有一个默认导出(default export)

import函数

  • 通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:

        if(true){
            import { name } from ("./index.js");
        }
    
    • 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;

    • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;

  • 但是某些情况下,我们确确实实希望动态的来加载某一个模块:

    • 如果根据不同的条件,动态来选择加载模块的路径;

    • 这个时候我们需要使用 import() 函数来动态加载;

    import("./modules/foo.js).then(aaa=>{
        aaa.foo();
    })