You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
回到我们刚刚说的模块的定义,模块就是一个实现特定功能的文件 or 代码块(这是我自己给定义的)。专业定义是,在程序设计中,为完成某一功能所需的一段程序或子程序;或指能由编译程序、装配程序等处理的独立程序单位;或指大型软件系统的一部分。而函数的一个功能就是实现特定逻辑的一组语句打包。并且 JavaScript 的作用域就是基于函数的。所以最原始之处,函数必然是作为模块化的第一步。
letmodule1={lettag : 1,letname:'module1',fun1(){console.log('this is fun1')},fun2(){console.log('this is fun2')}}
我们在使用的时候呢,就直接
module1.fun2();
优点
一定程度上优化了命名冲突,降低了全局变量污染的风险
有一定的模块封装和隔离,并且还可以进一步语义化一些
缺点
并没有实质上改变命名冲突的问题
外部可以随意修改内部成员变量,还是容易产生意外风险
IIFE
IIFE 就是立即执行函数,我们可以通过匿名闭包的形式来实现模块化
基本语法
letglobal='Hello, I am a global variable :)';(function(){// 在函数的作用域中下面的变量是私有的constmyGrades=[93,95,88,0,55,91];letaverage=function(){lettotal=myGrades.reduce(function(accumulator,item){returnaccumulator+item},0);return'Your average grade is '+total/myGrades.length+'.';}letfailing=function(){letfailingGrades=myGrades.filter(function(item){returnitem<70;});return'You failed '+failingGrades.length+' times.';}console.log(failing());console.log(global);}());// 控制台显示:'You failed 2 times.'// 控制台显示:'Hello, I am a global variable :)'
require.config({//默认情况下从这个文件开始拉去取资源baseUrl:'scripts/app',//如果你的依赖模块以pb头,会从scripts/pb加载模块。paths:{pb:'../pb'},// load backbone as a shim,所谓就是将没有采用requirejs方式定义//模块的东西转变为requirejs模块shim:{'backbone':{deps:['underscore'],exports:'Backbone'}}});
//article.js文件// 定义有依赖的模块define(['user'],function(user){letname='THE LAST TIME'functionconsoleMsg(){console.log(`${name} by ${user.getAuthor()}`);}// 暴露模块return{ consoleMsg }})
examples/|--sea-modules存放seajs、jquery等文件,这也是模块的部署目录|--static存放各个项目的js、css文件||--hello||--lucky|`-- todo `--app存放html等文件|--hello.html|--lucky.html
`--todo.html
前言
【THE LAST TIME】一直是我想写的一个系列,旨在厚积薄发,重温前端。
也是对自己的查缺补漏和技术分享。
欢迎大家多多评论指点吐槽。
随着互联网的发展,前端开发也变的越来越复杂,从一开始的表单验证到现在动不动上千上万行代码的项目开发,团队协作就是我们不可避免的工作方式,为了更好地管理功能逻辑,模块化的概念也就渐渐产生了。
好的书籍📚会分章节,好的代码得分模块。
JavaScript 在早期的设计中就没有模块、包甚至类的概念,虽然
ES6
中有了class
关键字,那也只是个语法糖。随意随着项目复杂度的增加,开发者必然需要模拟类的功能,来隔离、封装、组织复杂的 JavaScript 代码,而这种封装和隔离,也被被我们称之为模块化。模块就是一个实现特定功能的文件 or 代码块。随着前端工程体系建设的愈发成熟,或许模块化的概念已经在前端圈子里已经耳熟能详了。
但是对于很多开发者而言,ES6 中的
export
、import
,nodejs
中的require
、exports.xx
、module.exports
到底有什么区别?为什么又有CommonJS
,又有AMD
,CMD
,UMD
?区别是什么?甚至我们在编写 ts 文件的时候,还需要在配置文件里面说明什么模块方式,在项目中使用的时候,我们又是否真正知道,你用的到底是基于哪一种规范的模块化?本文对你写代码没有一点帮助,但是如果你还对上述的问题存有疑惑或者想了解JavaScript 模块化的前世古今,那么我们开始吧~
模块化的价值
所谓的模块化,粗俗的讲,就是把一大坨代码,一铲一铲分成一个个小小坨。当然,这种分割也必须是合理的,以便于你增减或者修改功能,并且不会影响整体系统的稳定性。
个人认为模块化具有以下几个好处:
npm
上找package
的时候,是在干啥?原始模块化
对于某一工程作业或者行为进行定性的信息规定。主要是因为无法精准定量而形成的标准,所以,被称为规范。在模块化还没有规范确定的时候,我们都称之为原始模块化。
函数封装
回到我们刚刚说的模块的定义,模块就是一个实现特定功能的文件 or 代码块(这是我自己给定义的)。专业定义是,在程序设计中,为完成某一功能所需的一段程序或子程序;或指能由编译程序、装配程序等处理的独立程序单位;或指大型软件系统的一部分。而函数的一个功能就是实现特定逻辑的一组语句打包。并且 JavaScript 的作用域就是基于函数的。所以最原始之处,函数必然是作为模块化的第一步。
基本语法
优点
缺点
对象封装
基本语法
我们在使用的时候呢,就直接
优点
缺点
IIFE
IIFE
就是立即执行函数,我们可以通过匿名闭包的形式来实现模块化基本语法
这种方法的好处在于,你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量
类似如上的
IIFE
,还有非常多的演进写法比如引入依赖:
还有一种所谓的揭示模块模式
Revealing module pattern
这和我们之前的实现方法非常相近,除了它会确保,在所有的变量和方法暴露之前都会保持私有.
优点
public
和private
的概念缺点
CommonJS
上述的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。
虽然每种方法都比较有效,但也都有各自的短板。
随着大前端时代的到来,常见的 JavaScript 模块规范也就有了:
CommonJS
、AMD
、CMD
、UMD
、ES6
原生。基本介绍
CommonJS
是 JavaScript 的一个模块化规范,主要用于服务端Nodejs 中,当然,通过转换打包,也可以运行在浏览器端。毕竟服务端加载的模块都是存放于本地磁盘中,所以加载起来比较快,不需要考虑异步方式。根据规范,每一个文件既是一个模块,其内部定义的变量是属于这个模块的,不会污染全局变量。
CommonJS
的核心思想是通过require
方法来同步加载所依赖的模块,然后通过exports
或者module.exprots
来导出对外暴露的接口。模块定义
CommonJS
的规范说明,一个单独的文件就是一个模块,也就是一个单独的作用域。并且模块只有一个出口,module.exports
/exports.xxx
加载模块
加载模块使用
require
方法,该方法读取文件并且执行,返回文件中module.exports
对象在浏览器中使用 CommonJS
由于浏览器不支持
CommonJS
规范,因为其根本没有module
、exports
、require
等变量,如果要使用,则必须转换格式。Browserify是目前最常用的CommonJS格式转换的工具,我们可以通过安装browserify
来对其进行转换.但是我们仍然需要注意,由于 CommonJS 的规范是阻塞式加载,并且模块文件存放在服务器端,可能会出现假死的等待状态。然后使用如下命令
然后在 HTML 中引入使用即可。
有一说一,在浏览器中使用 CommonJS 的规范去加载模块,真的不是很方便。如果一定要使用,我们可以使用browserify编译打包,也可以使用require1k,直接在浏览器上运行即可。
特点
补充知识点
其实在 nodejs 中模块的实现并非完全按照 CommonJS 的规范来的,而是进行了取舍。
Node 中,一个文件是一个模块->module
源码定义如下:
CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。
上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略不介绍了。
以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
再去深究具体的实现细节。。那就。。。下一篇分享吧~
AMD
Asynchronous Module Definition:异步模块定义。
也就是解决我们上面说的 CommonJS 在浏览器端致命的问题:假死。
介绍
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是异步加载模块,允许指定回调函数。
由于其并非原生 js 所支持的那种写法。所以使用 AMD 规范开发的时候就需要大名鼎鼎的函数库
require.js
的支持了。require.js
require.js
主要解决两个问题:定义模块
id
,一个可选参数,说白了就是给模块取个名字,但是却是模块的唯一标识。如果没有提供则取脚本的文件名dependence
,以来的模块数组callback
,工厂方法,模块初始化的一些操作。如果是函数,应该只被执行一次。如果是对象,则为模块的输出值使用模块
moduleName
,以来的模块数组callback
,即为依赖模块加载成功之后执行的回调函数(前端异步的通用解决方案),data-main
data-main
指定入口文件,比如这里指定scripts
下的app.js
文件,那么只有直接或者间接与app.js
有依赖关系的模块才会被插入到html中。require.config
通过这个函数可以对
requirejs
进行灵活的配置,其参数为一个配置对象,配置项及含义如下:baseUrl
——用于加载模块的根路径。paths
——用于映射不存在根路径下面的模块路径。shims
——配置在脚本/模块外面并没有使用RequireJS的函数依赖并且初始化函数。假设underscore并没有使用RequireJS
定义,但是你还是想通过RequireJS来使用它,那么你就需要在配置中把它定义为一个shimdeps
——加载依赖关系数组Demo 演示
如果我们需要引入第三方库,则需要在 main.js 文件中引入
特点
CMD
基本介绍
CMD是阿里的玉伯提出来的(大神的成长故事可在公众号回复【大佬】),js 的函数为
sea.js
,它和 AMD 其实非常的相似,文件即为模块,但是其最主要的区别是实现了按需加载。推崇依赖就近的原则,模块延迟执行,而 AMD 所依赖模块式提前执行(requireJS 2.0
后也改为了延迟执行)SeaJs
准确的说
CMD
是SeaJS
在推广过程中对模块定义的规范化产物。也可以说
SeaJS
是一个遵循CMD
规范的JavaScript
模块加载框架,可以实现 JavaScript 的 CMD 模块化开发方式。SeaJS
只是实现 JavaScript的模块化和按需加载,并未扩展 JavaScript 语言本身。SeaJS
的主要目的是让开发人员更加专注于代码本身,从繁重的 JavaScript 文件以及对象依赖处理中解放出来。Seajs
追求简单、自然的代码书写和组织方式,具有如下核心特性:Sea.js
遵循CMD
规范,可以像 Node.js 一般书写模块代码。Sea.js 还提供常用插件,非常有助于开发调试和性能优化,并具有丰富的可扩展接口。
Demo 演示
我们从 hello.html 入手,来瞧瞧使用 Sea.js 如何组织代码。
在 hello.html 页尾,通过 script 引入 sea.js 后,有一段配置代码
sea.js 在下载完成后,会自动加载入口模块。页面中的代码就这么简单。
这个小游戏有两个模块 spinning.js 和 main.js,遵循统一的写法:
上面就是 Sea.js 推荐的 CMD 模块书写格式。如果你有使用过 Node.js,一切都很自然。
特点
UMD
UMD 其实我个人还是觉得非常。。。。不喜欢的。
ifElse
就universal
了。。。。基本介绍
UMD
是AMD
和CommonJS
的综合产物。如上所说,AMD
的用武之地是浏览器,非阻塞式加载。CommonJS 主要用于服务端 Nodejs 中使用。所以人们就想到了一个通用的模式UMD
(universal module definition)。来解决跨平台的问题。没错!就是
ifElse
的写法。核心思想就是:先判断是否支持Node.js的模块(
exports
)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(
define
是否存在),存在则使用AMD
方式加载模块。常规用法
ES6
如果你一直读到现在,那么恭喜你,我们开始介绍我们最新的模块化了!
通过上面的介绍我们知道,要么模块化依赖环境,要么需要引入额外的类库。说到底就是社区找到的一种妥协方案然后得到了大家的认可。但是归根结底不是官方呀。终于,ECMAScript 官宣了模块化的支持,真正的规范。
基本介绍
在ES6中,我们可以使用
import
关键字引入模块,通过export
关键字导出模块,功能较之于前几个方案更为强大,也是我们所推崇的,但是由于ES6目前无法在所有浏览器中执行,所以,我们还需通过babel将不被支持的import
编译为当前受到广泛支持的require
。ES6 的模块化汲取了
CommonJS
和AMD
的优点,拥有简洁的语法和异步的支持。并且写法也和 CommonJS 非常的相似。关于 ES6 模块的基本用法相比大家都比较熟悉了。这里我们主要和 CommonJS 对比学习。
与 CommonJS 的差异
两大差异:
值拷贝&值引用
在 main.js 当中的实例是和原本模块完全不相干的。这也就解释了为什么调用了 counter.increment() 之后仍然返回1。因为我们引入的 counter 变量和模块里的是两个不同的实例。
所以调用 counter.increment() 方法只会改变模块中的 counter .想要修改引入的 counter 只有手动一下啦:
而通过 import 语句,可以引入实时只读的模块:
加载 & 编译
因为
CommonJS
加载的是一个对象(module.exports
),对象只有在有脚本运行的时候才能生成。而 ES6 模块不是一个对象,只是一个静态的定义。在代码解析阶段就会生成。ES6 模块是编译时输出接口,因此有如下2个特点:
循环加载的差异
“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
循环加载如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。
在 CommonJS 中,脚本代码在
require
的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。输出结果为:
从上面我们可以看出:
b.js
之中,a.js
没有执行完毕,只执行了第一行。main.js
执行到第二行时,不会再次执行b.js
,而是输出缓存的b.js
的执行结果,即它的第四行ES6 处理“循环加载”与 CommonJS 有本质的不同**。ES6 模块是动态引用**,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
运行结果如下:
上面代码中,执行a.mjs以后会报错,foo变量未定义.
具体的执行结果如下:
解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。
最后执行结果为:
特点
参考文献
学习交流
The text was updated successfully, but these errors were encountered: