js代码冗长之后,全局变量混乱,为何应该使用模块化

341 阅读7分钟

一、代码增长后的每次优化

一开始我做个轮播图,我新建一个carousel.html,里面的结构如下:

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="../css/common.css">
    <style>

**写样式**

    </style>
</head>
<body>
</body>
<script>

**写js代码**
    
</script>
</html>

css样式行数增多后,可以放到一个外部css文件中,根据css样式层叠权重来渲染,样式这里暂且不谈。

主要说js代码。

1、流水式使用全局变量

一开始我呢,按顺序流水式实现代码,所有变量放到外面,如下:


<script>
    let init_x = 0;//定位播放位置
    const img_x = 220;

    let autoPlay = true;
    let direction = 1;//记录播放方向
    let timeout;
    let isStartPlay = false;

    //手动结合自动播放
    function slideReal(direction) {
        if ((init_x + direction) > 0 || (init_x + direction) < -(8 - 3)) {
            return false;
        }
        init_x = init_x + direction;
        ulElem.style.translate = init_x * img_x + "px";
        return true;
    }
</script>

如果只是一个单页面,而且这个单页面里只有一个轮播图,还好办。

但是后来又加入了一个日历表的功能。

3、使用闭包,只暴露少量特定需要的全局变量

为了防止轮播图和日历表的变量和方法相互污染影响,我特意把轮播图封装进一个闭包和class类里面:

const Carousel = (() => {
    let init_x = 0;//定位播放位置
    const img_x = 220;
    ...

    //手动结合自动播放
    function slideReal(direction) {}

    class Carousel {
        constructor(srcList, imgNodeList) {}

        initSrc() {}

        startPlay() {}

        paused() {}

        //恢复轮播
        resume(direct) { }

        //点击按钮
        slide(direct) {}
    }

    return Carousel;
})();

然后我只需实例化轮播图class就可以进行相关的操作:

    const carousel = new Carousel(srcList, imgNodeList);
    carousel.initSrc();
    carousel.startPlay();

这样的好处是,轮播图的私有变量和方法全写在class Carousel类里面,不会影响到其他js代码比如后来加入的日历表功能的js变量。

同理,我的日历表的js变量和函数也是这样封装起来的,最后暴露出来的仅有两个class,内部的变量互不干扰。

3、把内部封装的代码各自放到一个独立的js文件中,简化主文件的结构

我发现class内部的东西太多没必要放到carousel.html中,因为每个class是相对独立的,于是我分别放到carousel.js和my-date.js两个js文件中,通过js引用的方式获取到CarouselMyDate这两个全局变量,然后实例化它们就可以使用了,在html文件中看起来结构还挺简单清晰的。

二、对于上面优化的进一步思考

我发现隐藏内部实现,暴露外部公共接口,是一个很好的代码优化实践。

但这里还有几个问题:

  1. 每一个需要隐藏的实例,都需要使用一个(function(){})()立即执行函数包裹
  2. 立即执行函数返回的结果必须马上赋值给一个全局变量(必须马上命名并给到外部使用)

基于以上两个问题,延伸出,比如,

假如有很多js需要使用到同一个立即执行函数暴露的接口,那么在多次引入script过程中会不断执行这个立即执行函数。

立即执行函数的写法不够优雅,所有变量、方法都被包裹在内部。

立即执行函数返回的变量仍然是一个全局变量,仍然可能会跟被引用的主js文件内的产生冲突。

仍然是一个主js文件引用多个js文件的思路,该思路有一个问题就是,其他引用的js全局变量需要根据主js的逻辑来组装,也就是需要在主js中统筹所有依赖js,这样虽然做到了js的工具化,

但是无法做到单独js的模块化,比如我在carousel.js立即执行函数内部需要用到另一个立即执行函数暴露的全局变量,这时要依靠主js同时引用这两个js,通过这样的组装之后才能起作用,那么在还没组装之前,单独js本身就是不完整的,这样还增加了主js(html)组装的复杂度(必须清楚各个js文件内部细节),显然是不合理的。

三、第一种模块化解决方案:CommonJS

由社区发起,约定俗成的规范,2009年开始被nodejs实现使用。

文件后缀:index.cjs ,cjs也就是CommonJS

原理跟我上面把相对独立的内容抽调成一个外部立即执行函数的思路相似。

如下代码:

    (function (module) {
        let exports = module.exports;

        let abc = 1;
        let def = function () {
        }
        module.exports = {
            abc: abc,
            def: def
        }

        return module.exports
    })();

对于一个立即执行函数,把内部想要暴露的内容放到对象 module.exports里,

CommonJS如果把一个js文件识别为一个module模块,那么(function (module) {内部代码})();这样的包裹层就不用写了,可以直接在该js文件里直接写:

    let abc = 1;
    let def = function () {
    }
    module.exports = {
        abc: abc,
        def: def
    }

CommonJS会默认把该js内部代码包裹执行,并导出(暴露)module.exports这个对象实例给其他导入它的地方。

在其他js中就可以导入使用了:

    let myName =  require("./exports.js")

可以把require理解为上面的立即执行函数,那么就可知,require函数的返回值就是module.exports

相当于

    let myName = module.exports
    
    //也就是
   
    let myName = {
        abc: abc,
        def: def
    }

了解CommonJS模块化后,发现它很好的解决了以上碰到的问题:

  1. 全局变量冲突问题:在导入时赋值时才给变量取名
  2. 闭包,或者立即执行函数问题,在module的js文件里,可以直接写变量,不会暴露也不会跟其他模块冲突
  3. 依赖问题:一个js可以只require导入需要的模块,而无需通过主html来操心

CommonJS注意问题

  • require("./exports.js")的文件路径必须以./或者../开头,不能省略
  • let exports = module.exports,exports指向module.exports,不能给exports重新赋值。
  • 默认情况下,如果一个module多次被require,里面的代码只会执行一次,然后被缓存起来。
  • CommonJS不是es6的规范

四、模块化的进一步发展成为浏览器端es规范

CommonJS规范应用于浏览器产生的问题

  1. CommonJS是同步加载的,在浏览器端需要异步先加载,否则同步过程中需要等待卡死
  2. CommonJS每个模块使用一个独立的执行环境运行,这个规范难以被所有浏览器厂商实现

在CommonJS规范之后更进一步的AMD规范

AMD规范被requirejs.org这个js库实现:

AMD规范使用异步加载js模块的方式:

定义模块:plus.js文件

    function callback() {
        let exports = {
            add(a, b) {
                return a + b;
            }
        };
        return exports;
    }
    define(callback)

define是AMD规范实现中的方法函数

可以看到,定义模块的代码是放在一个callback函数中,然后暴露导出exports。

然后在主js文件中导入使用:

define(["plus", ...others], function (plus, ...others) {
    let add = plus.add(1, 2);
    console.log(add);
})

define的第一个参数是依赖的js文件数组,第二个是等待依赖异步加载完成后执行的回调函数,回调函数的参数就是各个导出的模块内容。

在AMD规范之后更加简洁的CMD规范

seajs实现了CMD规范,这个规范的用法更接近CommonJS:

seajs.use('js/main') //入口js

依赖和定义模块:

    define(function(require, exports, module){
        document.onclick = function(){
            var a = require('js/a');
            a.test();
            
            require.async('js/b', function(b){
                b.test();
            })
            
            module.exports = {}
        }    
    });

上面代码中的require, exports, module用法跟CommonJS一致。

注意:AMD和CMD规范都只是社区规范,被民间实践使用,并未是ES官方标准

五、第二种模块化解决方案:ES module

2015年在es6规范发布时就包括了ESM模块化规范标准。

由于es 模块化太常用了,所以这里就不讲了。

说几个module的点:

  1. 依赖预加载:import 关键字的声明必须放到整个js的头部(浏览器默认提升到头部),所有import都会先请求网络加载好之后,再运行下面的代码。
  2. 可以同时export 多个变量和一个default,使用import defaultName,{a as a1,b} from "./a.js"来导入。
  3. 可以export * from "./a.js",把其他模块整合到一个公共模块一起导出,当然,要解决重名变量。