开发者社区> 前端修罗场> 正文

【模块化】再谈模块化

简介: 【模块化】再谈模块化
+关注继续查看

引言

一次又一次的事实证明,小的、组织好的代码远比庞大的代码更容易理解和维护。

因此,优化程序 的结构和组织方式,就是把它们分成小的、耦合度低的片段。我们把这样的片段,称为 模块。

  • 模块

模块是比对象和函数更大的代码单元。使用模块可以将程序进行归类。为什么需要模块?在有些时候,js中可能都出是全局变量(如果你在主线程代码中定义变量,该变量会被自动识别为全局变量),并且可被其他部分的代码访问。当,程序开始扩展,引入第三方代码后,命名冲突的可能性就会大大提高。

在ES6之前,javascript并没有提供内置的模块特性,通常是开发者利用js的特性,如对象、闭包、立即执行函数等,开发出模块化技术。

当我们要开发模块化技术时,请牢记模块化系统至少具备下列2点功能:

  • 定义模块接口:供外部代码调用该模块
  • 隐藏模块的内部实现细节:模块的调用者/使用者无需关心模块内部的实现细节。

了解了模块,下面我们就来谈谈几种模块化方案。

ES6之前的模块化方案

(1)对象+闭包+立即执行函数方案

基于模块化的2个特点,在该方案中:

  • 立即执行函数:隐藏内部实现细节
  • 对象+闭包:形成接口,对外暴露模块功能,同时保持闭包活跃。

示例:

const MouseCounterModule = function() {//MouseCounter 全局模块变量,返回立即执行函数的结果
    let numClick = 0 ;//模块私有变量
    const handleClick = () => { //模块私有方法
        alert(++numClick);
    };
    return {//接口:返回一个对象
        countClicks: ()=> {//通过闭包,可以访问模块私有变量和方法
            document.addEventListener("click",handleClick);
        }
    };
}();

我们把立即执行函数+对象+闭包来创建模块的方式,称为模块模式

一旦有能力定义模块,就能将不同的模块拆分为多个文件。或者在已有模块上不修改原有代码就可以定义更多功能。

扩展模块

  • 模块模块时,不能修改原有模块的代码,原有模块代码需要保持不变。

示例:我们对MouseCounterModule模块进行扩展

const MouseCounterModule = function() {//MouseCounter 全局模块变量,返回立即执行函数的结果
    let numClick = 0 ;//模块私有变量
    const handleClick = () => { //模块私有方法
        alert(++numClick);
    };
    return {//接口:返回一个对象
        countClicks: ()=> {//通过闭包,可以访问模块私有变量和方法
            document.addEventListener("click",handleClick);
        }
    };
}();
//扩展模块: 调用立即执行函数,并传入需要扩展的模块作为参数:
(function(module) {
    let numScrolls = 0;
    const handleScroll = () => {
        alert(++numScrolls);
    }
    module.countScrolls = () => {//扩展模块接口
        document.addEventListener("wheel",handleScroll);
    }
})(MouseCounterModule); //将模块传入,作为参数

使用:

MouseCounterModule.countClicks(); //初始化接口
MouseCounterModule.countScrolls(); //调用模块新增扩展的方法

通过模块模式的方式,建立模块技术,有一点缺点,即模块扩展无法共享模块的私有变量,因为扩展的函数和原有模块里的模块私有函数是处在不同的环境中定义,不可以访问对方的内部变量。但这并不是非常糟糕!

糟糕的是,当我们创建模块化应用时,模块本身常常会依赖其他模块的功能(如jquery),模块模式无法实现这样的依赖关系。作为开发者,在这种情况下,不得不考虑正确的依赖顺序,这样模块才能具有执行时所需的完整依赖。

(2)AMD与CMD

开发者为了解决前面提到的问题,AMD和CMD出现了。

AMD

AMD源于Dojo toolkit,可以很容易指定模块及依赖关系。目前流行的实现有RequireJS

示例:

define('MouseCounterModule',['jQuery'],$=> { 
//定义模块ID : MouseCounterModule, 依赖列表:['jQuery'] 
    let numClicks = 0;
    const handleClick = () => {
        alert(++numClicks);
    };
    return {//模块公共接口
        countClicks: () => {
            $(document).on("click",handleClick);
        }
    };
});

上面看到,在ADM中,使用define函数指定模块及其依赖,模块工厂函数会创建对应的模块。由此归纳define接收参数:

  • 新创建模块的ID。使用该ID,可以在系统的其他部分引用该模块。
  • 当前模块依赖的模块ID列表。
  • 初始化模块的工厂函数,该工厂函数接收依赖的模块列表作为参数。

上面的例子中,模块MouseCounterModule依赖于JQuery,因此AMD首先请求JQuery模块,如果需要从服务端请求,那么请求上需要时间。同时,这个过程是异步的,可以避免阻塞。当所有依赖的模块下载并解析完成后,调用模块的工厂函数,并传入所依赖的模块(如JQuery)。

模块的工厂函数,是与前面提到的模块模式类似的创建模块的过程。

AMD的优点:

  • 自动处理依赖,无需考虑依赖顺序
  • 异步加载模块,避免阻塞
  • 在同一个文件中可以定义多个模块。

CMD

AMD与CMD最大一个区别在于AMD面向浏览器,CMD面向通用Javascript环境。

因此,CMD目前拥有更多的用户。

CMD基于文件模块,每个文件中只能定义一个模块。CMD提供module变量,其具有exports属性,通过exports可以很容易扩展额外属性。module.exports是模块的公共接口。

前面提到,CMD拥有广泛的用户,主要因为客户端与服务端原因。因为CMD基于文件,在服务端只需要读取文件系统,加载速度更快。而在客户端必须从远程服务器下载文件,且是同步下载文件,故会更慢下载,容易造成阻塞。因此,在Nodejs中,默认使用CMD方式引入模块/包。

示例:

//MouseCounterModule.js
const $ = require("jQuery"); //同步方式,引入JQuery模块
let numClicks = 0;
const handleClick = () => {
    alert(++numClicks)
};
module.exports = { //使用module.exports定义模块公共接口
    countClicks: () => {
            $(document).on('click',handleClick);
    }
}

//a.js 引用MouseCounterModule.js
const MouseCounterModule = require('MouseCounter.js');
MouseCounterModule.countClicks();

由此我们知道,CMD规定:一个文件只允许定义一个模块。同时,不需要使用立即执行函数包装变量。而是使用module.exports

在模块中定义的变量都是安全地包含在当前模块中,不会泄露到全局作用域。

例如:上面的例子中,模块变量 $,numClicks,handleClick虽然是在模块代码顶部定义,但仍然在模块作用域中。如果是在标准Javascript文件中,这样的写法将产生全局作用域!

同时,只有通过module.exports对象暴露的对象或函数才可以在模块外部访问。

CMD优点

  • 语法简单。只需要定义module.exports属性。剩下的模块代码与标准的Javascript无大差异。同时,只需要使用require函数引用模块。
  • CMD是NodeJS默认的模块格式。

CMD缺点

  • 不能显式支持浏览器。因为浏览器不支持module变量、exports属性,需要使用浏览器支持的打包工具(如Browserify)来实现。

小结

上面提到的AMD与CMD,两者是属于相互竞争的方案。这就不可避免的产生问题:如当我们偏向一方使用,如果与其他项目(使用另一方方案)产生冲突,就需要解决障碍。脑壳疼!那么ES6模块化方案出现了!

ES6 模块化方案

ES6 的模块化方案结合了CMD和AMD的优点,例如:

  • 模块语法简单,基于文件,即每个文件是一个模块
  • 异步加载模块

ES6 目前还有一些浏览器不支持,可以使用其他工具进行编译,如:

主要思想

ES6模块化方案,必须显式地使用标识符导出模块,才能从外部访问模块。其它标识符,甚至在最顶级作用域中定义的标识符,只能在模块内使用。为了实现这样的功能,ES6提供两个关键字:

  • export :从模块外部指定标识符
  • import :导入模块标识符

示例:

//a.js
const name = "imaginecode"; //在模块a.js中定义一个顶级变量name
export function sayHello() { //通过模块公共API访问模块内部变量
    return 'Hello' + name;
}
//b.js
import A from 'a.js' ;  //导入模块
A.sayHello();

注意:

  • 导入已经命令的导出内容,必须使用花括号
  • 导入默认的导出不需要使用花括号
  • 重命名:只能在export/import 表达式中进行重命名。重命名后只能使用别名。当需要在当前上下文提供更合适的命名,或者避免命名冲突,别名可以发挥作用。

参考


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
23548 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
22282 0
阿里云ECS云服务器初始化设置教程方法
阿里云ECS云服务器初始化是指将云服务器系统恢复到最初状态的过程,阿里云的服务器初始化是通过更换系统盘来实现的,是免费的,阿里云百科网分享服务器初始化教程: 服务器初始化教程方法 本文的服务器初始化是指将ECS云服务器系统恢复到最初状态,服务器中的数据也会被清空,所以初始化之前一定要先备份好。
16606 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
22274 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
19251 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
11992 0
+关注
前端修罗场
前端修罗场,CSDN 博客专家 华为云享专家,微信公众号同名。为你提供优质原创内容。三门课程作者:《ElementUI 详解与实战》| 《ThreeJS 在网页中创建动画》|《PWA 渐进式Web应用开发》。蓝桥云课2021年度人气作者Top2。前端进阶之路 | 中大厂、外企、国企内推 | 面试培训
223
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
JS零基础入门教程(上册)
立即下载
性能优化方法论
立即下载
手把手学习日志服务SLS,云启实验室实战指南
立即下载