Javascript模块化编程
这篇文章是关于怎样按照没有规范和有规范(CommonJS、AMD、CMD)对Js进行模块化的编程,还有包括在Angular1.x中的模块机制。
无规范的模块化编程
不依据特定的规范对javascript进行模块化
函数模块化
直接是将不同的功能放在不同的函数中,不同的函数就是不同的模块。
这种方法适用于少量的程序代码,缺点是污染了全局变量,无法保证不与其他模块发生变量名冲突,模块成员之间看不出直接关系。
对象写法—-命名空间模式
为了能看出模块成员间的直接联系,可以把模块写成一个对象,所有模块成员都放在这个对象中。命名空间有助于减少程序中所需要的全局变量的数量,为程序创建一个全局对象,将所有功能啊添加到这个全局对象中。
这种方法可以避免代码中的命名冲突,并还可以避免在同一个页面中自己的代码与第三方代码之间的命名冲突。
缺点是:
- 需要属兔更多的字符,每个变量和函数前都要附加前缀,增加了代码量。
- 仅有一个全局实例意味着任何部分的代码都可以修改全局实例,不安全。
- 长嵌套名字使得属性解析查询时间增长。
命名空间这种模式在创建全局对象时总假设自己的创建的这个全局对象是第一个是很危险的,这导致可能会覆盖之前已经存在的对象内容。
命名空间模式优化一 ——- 通用的命名空间函数
若是想创建MYAPP.modules.module1 这样的命名空间就需要检查多次,若是更长则需要更多次的检查,所以最好是有一个通用的命名空间函数
。
|
|
这样减少了命名空间过长造成的字符增加的麻烦并且不会产生命名空间被重写的危险。
命名空间模式优化二 ——- 私有属性与方法
javascript并没有特殊的语法来表示私有、保护、或公有属性和方法。
|
|
上面这种方法是可以访问到私有属性的。
|
|
对于这种私有性失效的情况,一种方法是使用最低授权原则(POLA),规定应该永远不要给予超过需要的特权。另一种方法是对象克隆。将数组或者对象的副本进行返回。
上面那种方法是使用构造函数创建私有性
,还可以使用匿名即使函数创建私有性
。
|
|
这其实也就是简单的模块模式了。
对象写法—-模块模式
模块模式其实就是将命名空间模式+即使函数一起使用。即上面的最后的情况即为模块模式。
在模块模式中,也可将参数传递到包装了模块的即时函数中。这些参数通常是对全局变量、甚至是全局对象的引用。导入全局变量有助于加速即时函数中的全局符号解析的速度,因为这些导入的变量成为了该函数的局部变量。
|
|
在使用了无规范的模块模式写完后的js文件,最后按照下面的方式进行加载
|
|
这又会产生很多缺点: 文件只能按照<script>
的书写顺序进行加载,开发人员必须主观的解决模式和代码库的依赖关系等等。
针对这些缺点,产生了多种模块的规范,包括CommonJS 、AMD、CMD 。
CommonJS 规范
CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。CommonJS 规范是为了解决 JavaScript 的作用域问题而定义的模块形式,可以使每个模块在它自身的命名空间中执行。该规范的主要内容是,模块必须
通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。Node.js的模块化是基于CommonJS。主要的使用方法是:
|
|
CommonJs这种规范方式并不适用于浏览器端,因为是同步的模块加载方式。同步意味着阻塞加载,浏览器资源师异步加载的。这对于服务器不是问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间是硬盘的读取时间。但是,对于浏览器来说各个模块都放在服务器端,等待时间取决于网速,可能会很慢。 浏览器不能够进行同步加载,所以产生了AMD即“异步加载”。
对于CommonJS也有浏览器实现为 Browserify,但是编译打包后的文件体积可能很大。
AMD 规范 与CMD 规范
AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。对于AMD规范的实现是require.js。对于CMD规范的实现是sea.js。sea.js只会在真正需要加载的时候才会执行该模块,并且是按照在代码中出现的顺序出现。 require.js 会先尽早的执行模块,并且顺序也不一定。
ES6 模块化
ES6模块的思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和AMD模块,都只能在运行时确定这些。
|
|
Webpack
期望的模块系统是可以兼容多种模块风格,尽量可以利用已有的代码,不仅仅是Javascript模块化,还有css、图片、字体等资源也需要模块化。前端模块要在客户端中执行,他们需要增量加载到浏览器中。模块的加载和传输,我们首先能想到两种极端的方式,一种是每个模块文件都单独请求,
另一种是把所有模块打包成一个文件然后只请求一次。显而易见,每个模块都发起单独的请求造成了请求次数过多,导致应用启动速度慢;一次请求加载所有模块导致流量浪费、初始化过程慢。这两种方式都不是好的解决方案,它们过于简单粗暴。
分块传输,按需进行懒加载
,在实际用到某些模块的时候再增量更新,才是较为合理的模块加载方案。要实现模块的按需加载,就需要一个对整个代码库中的模块进行静态分析、编译打包的过程。
除此之外,在前端中除了js模块文件还会有样式、图片、字体、HTML模板等众多资源,最好是将这些资源都当成模块。并且能够在编译的时候可以对整个代码进行惊天分析,分析出各个模块的类型和他们的依赖关系,然后将不同类型的模块提交给适配的加载器进行处理。
并且,为了能利用已经存在的各种框架、库和已经写好的文件,还需要一个模块加载的兼容策略,来避免重写所有的模块。
为了上面的需求就有了Webpack
。
Webpack 本身只能处理原生的 JavaScript 模块,但是 loader 转换器可以将各种类型的资源转换成 JavaScript 模块。这样,任何资源都可以成为 Webpack 可以处理的模块。 Webpack 能处理模块形式是CommonJs、AMD 或者CMD形式的第三方库。
Angular1.X 模块机制
module是angular中重要的模块组织方式,它提供了将一组内聚的业务组件(controller、service、filter、directive…)封装在一起的能力。这样做可以将代码按照业务领域问题分module的封装,然后利用module的依赖注入其关联的模块内容。同时module也是我们angular代码的入口,首先需要声明module,然后才能定义angular中的其他组件元素,如controller、service、filter、directive、config代码块、run代码块等。
对于angular.module方法,常用的方式有有种,分别为angular.module(‘myModule’, [可选依赖模块])和angular.module(‘myModule’)。请注意它是完全不同的方式,一个是声明创建module,而另外一个则是获取已经声明了的module。在应用程序中,对module的声明应该有且只有一次。对于获取module,则可以有多次。推荐将angular组件独立分离在不同的文件中,module文件中声明module,其他组件则引入module。
在Angular1.X 中module和在AMD中的module是不同的,在Angular中module相当于一个namespace或者package,表示的是一些内置功能组件的集合。或者module的意义是用于标识在一个页面中可能包含的一个或者多个Angular应用。
|
|
这样可以在同一个页面中创建同一module的不同实例。两个应用互不干涉,在各自的容器中运行。
参考文献
- 阮一峰的网络日志 :Javascript模块化编程
- Webpack 中文指南
- Stoyan Stefanov《JavaScript 模式》
- Angular中的模块机制