[2023秋第9节课]模块化

134 阅读12分钟

一. 了解了解模块化

1.什么是模块化?

模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了:

  • ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过JavaScript进行前端页面的渲染。

  • SPA(单页应用)的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript来实现。

  • 包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤。 所以,模块化已经是JavaScript一个非常迫切的需求。所以ES6(2015)才推出了自己的模块化方案。

目前流行的js模块化规范有CommonJS、AMD、CMD以及ES6的模块系统。

167b650e8d1fcc23~tplv-t2oaga2asx-jj-mark_3024_0_0_0_q75.png

2.模块化的好处

(1)模块间解耦,复用,避免命名冲突

对业务进行模块化拆分后,为了使各业务模块间解耦,因此各个都是独立的模块,它们之间是没有依赖关系。 每个模块负责的功能不同,业务逻辑不同,模块间业务解耦。模块功能比较单一,可在多个项目中使用。

(2)可单独编译某个模块,提升开发效率

每个模块实际上也是一个完整的项目,可以进行单独编译,调试

(3)可以多团队并行开发,测试

原因:每个团队负责不同的模块,提升开发,测试效率。

3.模块化业务分层

基础组件层: 底层使用的库和封装的一些工具库(libs),比如okhttp,rxjava,rxandroid,glide等

业务组件层: 与业务相关,封装第三方sdk,比如封装后的支付,即时通行等 业务模块层:按照业务划分模块,比如说IM模块,资讯模块等

4.模块化发展史

截屏2024-03-02 17.36.59.png

二. LEVEL1: 最原始的模块化思想——原生js 模块思想写法

这里只要把不同的函数(以及记录状态的变量)放在一起,就算是一个模块

var number=0
function add(){
    return number++
}
function reduce(){
    return number--
}

在这里需要用到add()reduce()函数时只需要直接调用就行

但如此写法的缺点是:如果有人同样定义了名为add()reduce()的函数,但执行的逻辑不同,就会导致运行出问题

于是以下向大家介绍“进阶版”的对象写法

let module = new Object({
    number: 0, 
    add: function () { 
        return this.number++; 
    },
    reduce: function () {
        return this.number--;
    }
});

console.log(module.add())

将原先的代码包裹在一个对象当中,需要用到当中的函数时直接通过module.调用函数()即可

对象写法也有缺点: 这种写法会暴露所有定义的变量,导致其可以从外部改写

比如:

moudle.number=1145
console.log(moudle.add())    //输出结果为1146

三. LEVEL2: 通用JS模块化写法——CommonJS和AMD

1. CommonJS

CommonJS有四个重要的环境变量为模块化提供支持:

  1. module:代表当前模块的对象。它包含了与当前模块相关的属性和方法,可以通过 module.exports 将模块的功能暴露给其他模块使用。
  2. exports:是 module.exports 的一个引用。它是一个空对象,用于向外部暴露模块的功能。可以通过给 exports 对象添加属性和方法来导出模块的内容。
  3. require:用于引入其他模块。它是一个函数,接受模块标识符作为参数,并返回被引入模块导出的内容。通过 require 可以实现模块之间的依赖关系。
  4. global
// 定义模块math.js 
var basicNum = 0; 
function add(a, b) {
    return a + b; 
} 

module.exports = {
    //在这里写上需要向外暴露的函数、变量 
    //“:”后面的可省略
    add: add, 
    basicNum: basicNum 
} 

// 引用自定义的模块时,参数包含路径,可省略.js 
var math = require('./math'); 
math.add(2, 5);

CommonJS的优缺点:

commonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。

但是在浏览器端,限于网络原因,更合理的方案是使用异步加载。这样会降低浏览器请求时间,提升浏览器的性能。

于是乎接下来给大家介绍AMD~

2. AMD

AMD意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

(1)定义模块:

define(['dependency1', 'dependency2'], function(dep1, dep2) {
    // 模块的功能代码 
    return {
        // 导出的功能
    };
});

define 函数中,第一个参数是一个数组,包含了当前模块所依赖的其他模块的标识符。第二个参数是一个回调函数,它接受依赖模块的实例作为参数,并定义了当前模块的功能代码。可以在回调函数中返回一个对象,将需要导出的功能暴露出来。

(2)引入模块:

require(['module1', 'module2'], function(mod1, mod2) {
  // 使用模块的功能
});

require 函数中,第一个参数是一个数组,包含了需要引入的模块的标识符。第二个参数是一个回调函数,它接受引入的模块实例作为参数,并可以在回调函数中使用这些模块的功能。

四. LEVEL3: 基于AMD的Require.js(不常用)

引言

当我们没有接触到模块化开发的时候,通常通过<script src="hello.js"></script>的方式导入JS部分的代码。最开始所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。但后来代码越来越多,一个文件不够了,必须分成多个文件,依次加载。那么就会出现以下这种情况:

<script src="hello1.js"></script>
<script src="hello2.js"></script>
<script src="hello3.js"></script>
<script src="hello4.js"></script>
<script src="hello5.js"></script>
...

这样的写法有很大的缺点。

首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;

其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

Require.JS

首先我们要去官网下载最新版本的RequireJS文件,然后将它放到<script></script>标签里面加载即可。

<script src="js/require.js" defer async="true" ></script>

async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。

加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:

 <script src="js/require.js" data-main="js/main"></script>

data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。

Require.JS的使用方法:

(1)定义和加载模块:使用Require.js,你可以将JavaScript代码拆分为多个模块,并按需加载这些模块。在你的JavaScript文件中,使用 define 函数来定义模块,使用 require 函数来加载模块。例如:

// 定义模块 
define(['dependency1', 'dependency2'], function(dep1, dep2) {
// 模块的功能代码
return {
    // 导出的功能
};
});
// 加载模块
require(['module1', 'module2'], function(mod1, mod2) { 
// 使用模块的功能
});

define 函数中,第一个参数是一个数组,包含了当前模块所依赖的其他模块的路径。第二个参数是一个回调函数,它接受依赖模块的实例作为参数,并定义了当前模块的功能代码。可以在回调函数中返回一个对象,将需要导出的功能暴露出来。

require 函数中,第一个参数是一个数组,包含了需要加载的模块的路径。第二个参数是一个回调函数,它接受加载的模块实例作为参数,并可以在回调函数中使用这些模块的功能。

使用这种方法的好处:

require()异步加载moduleAmoduleBmoduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

下面,我们看一个实际的例子

假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:

require(['jquery', 'underscore', 'backbone'], function (hello1, hello2, hello3){

    // some code here...
    
    console.log(hello1); 
    console.log(hello2); 
    console.log(hello3); 
});

上面示例中,主模块的依赖模块是['jquery', 'underscore', 'backbone']。默认情况下,require.js假定这三个模块与main.js在 同一个目录文件名分别为 jquery.js,underscore.js和backbone.js,然后自动加载。

指定路径:require.config()

上面的代码给出了三个模块的文件名,路径默认与main.js在同一个目录(js子目录) 。如果这些模块在其他目录,比如在js/myJS目录下,则需要用require.config()来特别指定其路径:

require.config({ 
paths: {     
    "jquery": "myJS/jquery.min",   
    "underscore": "myJS/underscore.min",   
    "backbone": "myJS/backbone.min"   
}  
});

同样我们也可以通过在require.config()中添加baseUrl:“”来省略在每一个路径都要添加的相同路径

五. CMD与sea.js(感觉也不常用)

CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

CMD规范的库就是 sea.js, 大家可以去官网下载

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要时申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

六. LEVEL4: ES6模块化规范(简单~)

ES6(ECMAScript 2015)引入了官方支持的JavaScript模块化系统,它提供了一种更现代化和更强大的模块化方式。ES6模块化使用 importexport 关键字来导入和导出模块的功能

exports关键字

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

我们希望将其他中内容全部导出,它可以有如下的方式:

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

    export const name = "zh"
    export const age = 22
  • 方式二:将所有需要导出的标识符,放到export后面的 {} 中。注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的。所以: export {name: name},是错误的写法。

    const name = "zh"
    const age = 22
    function foo() {
      console.log("foo function")
    }

    export {
      name,
      age,
      foo
    }
  • 方式三:导出时给标识符起一个别名。(基本没用,一般在导入文件中起别名)。然后在导入文件中就只能使用别名来获取。

    export {
      name as fName,
      age as fAge,
      foo as fFoo
    }

import关键字

import关键字负责从另外一个模块中导入内容。

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

  • 方式一:import {标识符列表} from '模块'。注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容。

    import { name, age } from "./foo.js"
  • 方式二:导入时给标识符起别名。

    import { name as fName, age as fAge } from './foo.js'
  • 方式三:通过 * 将模块功能放到一个模块功能对象(a module object)上。然后通过起别名来使用。

    import * as foo from './foo.js'

export和import结合使用

表示导入导出。


    import { add, sub } from './math.js'
    import {otherProperty} from './other.js'

    export {
      add,
      sub,
      otherProperty
    }

等价于


    // 导入的所有文件会统一被导出
    export { add, sub } from './math.js'
    export {otherProperty} from './other.js'

等价于


    export * from './math.js'
    export * from './other.js'

为什么要这样做呢?

在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中。 这样方便指定统一的接口规范,也方便阅读。这个时候,我们就可以使用export和import结合使用。

注意:export ... from ...语法,不能直接导出默认对象。例如:


   // a.js
    const a = 1;
    export default a
  
   // b.js
    export A from "a.js" // 报错
    export {default as A} from "a.js" // 在导出时使用别名
    export {default} from "a.js" // 当外界导入后使用别名也可以。

default用法

前面我们学习的导出功能都是有名字的导出(named exports):

  • 在导出export时指定了名字。
  • 在导入import时需要知道具体的名字。

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


    // foo.js
    const name = "zh"
    cconst age = 22
    export {
      name,
      // 或者这样的默认导出
      // age as default
    }

    export default age

    // 导入语句: 导入的默认的导出
    import foo, {name} from './foo.js'

    console.log(foo, name) // 22 zh
  • 默认导出export时可以不需要指定名字。
  • 在导入时不需要使用 {},并且可以自己来指定名字。
  • 它也方便我们和现有的CommonJS等规范相互操作。 注意:在一个模块中,只能有一个默认导出(default export)。

import函数

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


    if(true) {
        import foo from './foo.js'
    }

为什么会出现这个情况呢?

  • 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系。
  • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况。

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

  • 如果根据不同的条件,动态来选择加载模块的路径。 这个时候我们需要使用 import() 函数来动态加载。import函数返回的结果是一个Promise。
    import("./foo.js").then(res => {
      console.log("res:", res.name)
    })

es11新增了一个属性。meta属性本身也是一个对象: { url: "当前模块所在的路径" }

    console.log(import.meta)

参考文献(更推荐🤤):

# 理解模块化和总结各种模块化规范使用

# 前端模块化:CommonJS,AMD,CMD,ES6

# 模块化(模块化的基本概念,Node.js 中的模块化,npm与包,模块的加载机制)

# 你不容错过的JavaScript高级语法(模块化)