likes
comments
collection
share

JS 模块化- 01 模块化前传 (举例:高质量男和拼团名媛相亲)

作者站长头像
站长
· 阅读数 15

JS 模块化 01 - 模块化前传

前端技术的发展不断融入了很多后端的思想,逐步形成前端的 ”四个现代化“:工程化、模块化、规范化、流程化。这个主题介绍 模块化 ,主要内容包括模块化前传(早期模块化的实现)、模块化的四个规范(Common JS、AMD、CMD、ESM)。本文就聊聊早期的模块化。

1. 原始时代 - 无模块

十多年前,工程师们一般都不好意思说自己会 JS 语言,太 Low逼 —— 大神们随便翻翻书,几下就可以把什么元素显示隐藏、表单校验等需求的 JS 脚本写出来了。反正在这群家伙眼里,写 JS、CSS 的人不是程序员,而是美工(仅仅是在过去的那个年代二逼们才会这么说)。代码中都从到尾都是逻辑代码,一堆变量函数和流程控制语句( if/else/for/switch.... ),JS 脚本是从上到下顺序执行,反正功能给你堆出来了,至于会带来什么后果,关我屁事,有接盘侠去弄。。。

二逼的人直接就在 HTML 文件中写 JS 代码;自我感觉良好一些的大神就独立一个 JS 文件,然后在 HTML 页面上通过 标签引入。

例如人类高质量男士阿三和拼团名媛阿花相亲,无模块时代就会这么写:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模块化 - 无模块化的原始时代</title>
</head>
<body>
<h1>模块化 - 无模块化的原始时代</h1>
</body>
<script>
    /** 人类高质量男士-姓名 */
    let manName = '阿三'

    /** 人类高质量男士-自我介绍 */
    function manIntro() {
        console.log('我是人类高质量男士,我叫', manName)
    }

    /** 拼团名媛-姓名 */
    let womanName = '阿花'

    /** 拼团名媛-自我介绍 */
    function womanIntro() {
        console.log('我是拼团名媛,我叫', womanName);
    }

    /** 约会 */
    function dating () {
        console.log('在无模块化的原始社会')
        console.log('两人开始约会....')

        manIntro()
        womanIntro()
    }

    // 调用约会函数,让两人真正开始约会
    dating()
</script>
</html>

在浏览器中直接运行,浏览器控制台显示结果:

JS 模块化- 01 模块化前传 (举例:高质量男和拼团名媛相亲)

在上面的代码中,如果 dating 函数定义在 manIntro 函数或 womanIntro 前,就会执行失败。所以 JS 的执行顺序是非常重要的。

2. 石器时代 - 全局函数

后端大神写后端代码,大多围绕着类和对象展开,每个类都可以写在一个独立的文件中,还能提取一些公共的工具类之类的,于是他们就想这个思路能否用于 JS 呢? 于是就诞生了全局函数方式。

将不同功能的函数和变量拆分到不同的 JS 文件中,在入口 HTML 文件中按照顺序,通过 script 标签依次引入这些 JS 文件。

于是案例就可以抽取为三个 JS 文件:人类高质量男士 - man.js,拼团名媛 - woman.js,约会 - dating.js。 这三个 JS 文件就是石器时代模块化的雏形了。代码实现如下:

人类高质量男士 man.js

/**
 * 人类高质量男士
 */

let manName = '阿三'

function manIntro() {
    console.log('我是人类高质量男士,我叫', manName)
}

拼团名媛 woman.js

/**
 * 拼团名媛
 */

let womanName = '阿花'

function womanIntro() {
    console.log('我是拼团名媛,我叫', womanName);
}

约会 dating.js

function dating() {
    console.log('石器时代 - 全局函数')
    console.log('两人开始约会....')

    manIntro()
    womanIntro()
}

入口HTML文件 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模块化 - 全局函数 Demo</title>
    <script src="./modules/man.js"></script>
    <script src="./modules/woman.js"></script>
    <script src="./modules/dating.js"></script>
    <script>
        dating()
    </script>
</head>
<body>
<h1>模块化 - 全局函数 Demo</h1>
</body>
</html>

在浏览器中直接运行 index.html 文件,浏览器控制台显示:

JS 模块化- 01 模块化前传 (举例:高质量男和拼团名媛相亲)

在这一时代,虽然解决了单一 JS 文件臃肿的问题,并且将不同的功能封装为不同的全局函数。但根本问题没解决:

  1. script 标签引入的顺序不能乱来,必须手动严格按照使用顺序进行加载;
  2. Global 被污染,会引起命名冲突。三个文件中的变量和函数都是全局变量和函数,script标签中,后面引入的 JS 文件中定义的变量和函数会覆盖前面 JS 文件中定义的变量和函数。

3. 青铜器时代 - 简单对象包装

上一时代大量全局变量、全局函数,污染环境。所以前辈们想到一个办法来减小全局污染 —— 定义简单的对象,使用对象来包装变量和函数。优雅(装逼)的说法就是 命名空间 Namespace 模式。代码实现如下:

人类高质量男士 man.js

/**
 * 人类高质量男士
 */
let man = {
    name: '阿三',

    intro() {
        console.log('我是人类高质量男士,我叫', this.name)
    }
}

拼团名媛 woman.js

/**
 * 拼团名媛
 */
let woman = {
    name: '阿花',

    intro() {
        console.log('我是拼团名媛,我叫', this.name);
    }
}

约会 dating.js

let dating = {
    dating() {
        console.log('青铜器时代 - 简单对象包装')
        console.log('两人开始约会....')

        man.intro()
        woman.intro()
    }
}

入口HTML文件 - index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模块化 - Namespace Demo</title>
    <script src="./modules/man.js"></script>
    <script src="./modules/woman.js"></script>
    <script src="./modules/dating.js"></script>
    <script>
        dating.dating()
    </script>
</head>
<body>
<h1>模块化 - Namespace Demo</h1>
</body>
</html>

简单对象封装的方式,一定程度上减少了全局变量,但也仅仅是减少而已,因为上面的 man, woman, dating 都是全局变量。此外,数据并不是私有的,例如,在 dating.js 的 dating 方法中,可以通过 man.name = '阿四' 改变 man 对象的 name 属性:

let dating = {
    dating() {
        console.log('模块化青铜器')
        console.log('两人开始约会....')

        man.name = '阿四'
        man.intro()
        woman.intro()
    }
}

4. 铁器时代 - 匿名自执行函数

上面简单对象包装的方式,外部可以概念简单对象的属性。为了解决这一问题,前辈们发明了“匿名自执行函数”。这有个高端的单词描述这一模式:IIFE - Immediately-Invoked Function Expression(立即调用函数表达式)。前面写的中文 “匿名自执行函数” 可以记不得,但一定要记住 IIFE 这个词,不然装逼不够优雅。但优雅哥还是觉得“匿名自执行函数”一词能更好的理解。

  • “函数”:就是指定义一个函数;
  • “匿名”:就是定义的这个函数没有名字;
  • “自执行” - 通常调用函数的方式是函数名() ,而这个函数没有名字,那就没法通过函数名调用,如果不调用,那还定义函数搞个毛线啊?所以就只能定义这个函数后立即调用它 —— 让其立即执行。

定义格式如下:

    (function(/* 形式参数 */) {
        // 方法体
    })(/* 实际参数 */)

来个简单的例子,定义两个数相加的匿名自执行函数:

    (function(num1, num2) {
        console.log(num1 + num2)
    })(10, 20)

浏览器中执行运行后,浏览器控制台输出: 30

那这样做的好处是啥呢?保证了数据的私有性! IIFE 中方法体类定义的变量,外部是无法修改的。

/**
 * 人类高质量男士
 */
(function () {
    let name = '阿三'

    function intro() {
        console.log('我是人类高质量男士,我叫', name)
    }

    window.man = {
        intro
    }
})()

在方法体中,定义了name变量和intro函数,但只把 intro 函数挂载在 window 对象上。

在外部,可以使用 man.intro()进行调用,但无法使用 man.name 属性修改方法体内部的name属性。

上面改造了 人类高质量男士 man.js,接下来改造剩余的部分。

拼团名媛 woman.js

/**
 * 拼团名媛
 */
(function () {
    let name = '阿花'

    function intro() {
        console.log('我是拼团名媛,我叫', name);
    }
  
    window.woman = {
        intro
    }
})()

重头戏 dating.js 来了,dating 如何依赖 man 和 woman 呢?仍然通过实际参数传递给形式参数。

dating.js

(function (m, w) {
    function dating() {
        console.log('铁器时代 - IIFE')
        console.log('两人开始约会....')

        m.intro()
        w.intro()
    }

    window.dating = {
        dating
    }
})(man, woman)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模块化 - IIFE Demo</title>
    <script src="./modules/man.js"></script>
    <script src="./modules/woman.js"></script>
    <script src="./modules/dating.js"></script>
    <script>
        dating.dating()
    </script>
</head>
<body>
<h1>模块化 - IIFE Demo</h1>
</body>
</html>

这个模式有效解决了私有变量的问题,而且支持模块的依赖(dating.js 使用到了 man.js 和 woman.js ),成为现代化各种五花八门的模块化实现的重要基石! 但 script 标签中 JS 的顺序依然得手动有效控制才行。这也是各种模块化实现所解决的问题。

5. 工业时代 - 百家争鸣

模块化需要解决哪些问题呢?针对上面发展的描述,可分析出有以下几个点:

- 不对模块外的代码造成污染;
- 如何标识一个模块?(有标识、其他模块才能使用,而不是一味依赖script导入顺序)
- 如何暴露模块的方法?
- 如果调用其他模块?

围绕这四个问题,现代模块化一步步来临:

  • Common JS
  • AMD / RequireJS
  • CMD / SeaJS
  • ES Module

后面的文章会依次介绍。 感谢你阅读本文,如果本文给了你一点点帮助或者启发,还请三连支持一下,点赞、关注、收藏,作者会持续与大家分享更多干货