Javascript学习笔记-模块化

Javascript模块化.png

1. 简介

前端发展初期或者Javascript发展初期,我们所要解决的问题只是一个页面上各种内容的排版,表单验证之类的简单交互逻辑处理,这个时候单个js文件就完全能满足我们的开发要求。
随着Web的发展,前端已经从单纯HTML展示发展到前端应用,同时伴随着语言的发展,产生了大量的第三方帮助开发的库(例如:JQuery),我们所需要的Javascript脚本逐渐变大,变多,HTML页面中的<script>逐渐从单个变成了多个,但是在这种从单个到多个的转变中,却遇到了一些问题:

  1. Javascript文件之间的依赖:因为<script>脚本的机制,虽然每一个<script>共享了全局变量,但是却没有办法将后一个<script>脚本中声明的变量提升到前一个中,如果前一个<script>中依赖了该变量,则需要调整脚本顺序,但是单纯的从HTML引入脚本的顺序我们又不能明确知道脚本间的依赖关系
  2. 变量的泄漏:因为<script>之间是共享全局变量的,但是每个Javascript脚本在编写初期都是基于单个脚本来编写,不可避免的会出现多个脚本之间使用了同名的全局变量,那么后引入必定会覆盖之前的变量值,从而导致一些难以发现的问题

随着Javascript文件数量的增加,问题发生的频率就越来越大,工程越来越难以维护,于是机智的工程师们为了解决的这两个问题,提出了模块化解决方案(我始终觉得前端在工程化的过程中是逐步将后端的编码思想引进并不断完善,包括模块化其实就是类似于参考了例如:Java的包或者类引入)

2. 模块化原理

模块化的核心有两个方面:

  1. 模块的声明和模块的导入,也就是模块化的语法,如何定义一个模块
  2. 模块的加载器,也就是如何将识别声明和导入的模块,将他们有条件的加载

因为模块的声明和导入是依赖于模块加载器的写法,因此说到底,模块化最主要的也就是如何根据规范编写一个模块加载器。编写模块加载器要解决的就是提到的两个痛点,先看如何解决第二个问题,为了避免泄漏,就是创建一个有限的作用域范围,最佳解决方案就是使用闭包来产生相关的作用域,使用IIFE返回相关函数实在是好的不能再好了

// IIFE函数闭包
(function(){
    var a = 1;
    return {
      f() {
        // 一些逻辑处理
        console.log(a);
      }
    }
})()

接下来就是解决依赖关联,也就是我想明确知道自己使用了那些模块,于是如果能将依赖的模块作为参数传递,其实就可以从一定程度上明确我们的依赖关系,于是

// 定义一个IIFE函数
var $ = (function(){
    var a = 1;
    return {
      f() {
        // 一些逻辑处理
        console.log(a);
      }
    }
})()
// 创建一个名字和对象的映射关系
var modules = {'$': $}
// 建立依赖关系
function f(depsname, callback) {
  let deps  = [];
  for (let name of depsname) {
    deps.push(modules[name]);
  }
  callback(deps)
}
// 使用依赖关联
f(['$'], ([$]) => { $.f()});

类似上面的处理,我们就可以很明确的知道了依赖了一个名叫$的模块,而这个模块我们在modules对象里面进行的映射关联,指向了具体的内容
这里的示例只是对原理的简单性阐述,并不是一种最佳实践

3. 模块化方案

3.1 CommonJS

CommonJS是最先提出的一种模块化解决方案,与其说它是解决方案,不如说它是一个规范,它约定了Javascript模块化的各种内容,基本上可以是模块化思想的先驱

3.1.1 语法

NodeJS中的模块化处理方法使用了CommonJS的核心思想,是CommonJS规范的实践者

// 模块的声明(导出)
module.exports = {foo}
exports.foo = foo
// 模块的引用(导入)
var foo = require('foo');

有两个地方需要说明:

  1. NodeJSexports === module.exports,因为默认在每个NodeJS模块都会在最上层添加var exports = module.exports的声明
  2. require引用的是对应模块文件的路径,有三种方式:相对路径,绝对路径,文件名,NodeJS的模块引擎会根据require中路径的写法,去获取对应的模块
3.1.2 特点

CommonJS的特点是模块加载是同步的,在模块使用前,需要经过模块加载器将模块语法转换为对应的Javascript代码,之后才能正常运行,这样带来的问题就是他不能直接在浏览器端进行使用,只能在服务端进行转换后使用

3.2 AMD

AMD(Asynchronies Module Definition),异步的模块定义,一大波人研究CommonJS,从CommonJS分离出来的,自行发展的一个新分支

3.2.1 语法

AMD的基于CommonJS规范,衍生出了自己的一套体系,比较出名的模块化脚本加载器就是Require.js了(之前阿里的Seajs也很出名的,当然发起SAP(单页面应用)趋势的Angular.js也是基于Require.js来进行模块加载的)以Require.js为例,说明下基本语法

// 入口配置
<script data-main="app" src="require.js">
// 模块的声明
define(module_id, deps, function(deps){});
define(function(require, export, module));
// 模块的引用
requirejs(deps, function(deps))
3.2.2 特点

AMD一诞生就是针对浏览器端模块化提出的解决方案,他不需要提前对模块进行转换操作(不过需要配置一个模块的入口文件,注意入口配置中的data-main属性),直接就可以在浏览器端实现模块的加载,同时模块化过程是异步的,可以做到CommonJS做不到的模块延迟加载

3.3 ES6模块

随着模块语法的发展,ES6基于CommonJSAMD,定义了一套模块化的方案,虽然现在浏览器对ES6模块化方案支持不多(Chrome似乎已经开始支持<script type="module">的提案了),但是我们仍然可以通过TypeScriptBabel提供的模块Loader,类似CommonJS模块化的处理手段,利用webpack等工具优先进行模块化转换

3.3.1 语法

ES6模块的根据不同情况,写法会有一定出入,但是主要就是区分了普通的情况和有default声明的情况

// ---模块导出---
// 1. 对象,函数,值导出
export { foo }
export function f() {}
export let a = 1
// 2. default导出
export default {}
export { x as default}
// 3. 导出其他模块的非default内容
export * from 'module_name'
export { foo } from 'module_name'
// 4. 导出其他模块的default内容
import A from 'module_name'
export default A

// ---模块导入---
// 1. 导入Default
import A from 'module_name'
// 2. 导出非Default
import { name1 } from 'module_name'
import { name1 as alias } from 'module_name'
// 3. 导入所有非Default内容并创建命名空间
import * as namespace from 'module_name' 
// 4. 同时导入Default和非Default内容
import A, {name1} from 'module_name'
import A, * as namespace from 'module_name'
// 5. 单纯进行模块加载
import 'module_name'
3.2.2 特点

ES6模块化方案有几个需要特别注意的特点:

  1. 通常理解为一个模块一个文件,而一个文件就是一个模块,每个模块中只能最多有一个defaultexport
  2. 每个模块只会加载一次
  3. 每个模块的变量声明默认只在当前模块内有效
  4. 模块的引入是单例的,即使多次引入,也只会共享一个实例
  5. import引入的模块是readonly

4. 总结

总的来说,随着前端项目的逐渐项目化,使用模块化避免了可能带来的隐藏问题,使得整个项目更便于维护和管理,我们也可以很放心的开发各种扩展库,而不用担心和外部变量冲突的问题,很大程度的促进了前端社区的繁荣

5. 参考

《You Don‘t know Javascript - ES6 & Beyond》
MDN-export
MDN-import
Writing Modular JavaScript With AMD, CommonJS & ES Harmony
CommonJS规范

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容