JavaScript模块化开发的演进历程

JavaScript模块化开发的演进历程JavaScript 模块化开发的演进历程

写在前面的话

  • js模块化历程记录了js模块化思想的诞生与变迁
  • 历史不是过去,历史正在上演,一切终究都会成为历史
  • 拥抱变化,面向未来

延伸阅读 – JavaScript诞生(这也解释了JS为何一开始没有模块化)

  • JavaScript因为互联网而生,紧随着浏览器的出现而问世
  • 1990年底,欧洲核能研究组织(CERN)科学家Tim,发明了万维网(World Wide Web),最早的网页只能在操作系统的终端里浏览,非常不方便。
  • 1992年底,美国国家超级电脑应用中心(NCSA)开发了第一个独立的浏览器,叫做Mosaic,从此网页可以在图形界面的窗口浏览。
  • 1994年10月,NCSA的一个主要程序员Marc,成立了Mosaic通信公司,不久后改名为Netscape,开发面向普通用户的新一代的浏览器Netscape Navigator
  • 1994年12月,Navigator发布了1.0版,市场份额一举超过90%。
  • Netscape公司发现,Navigator浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。
  • 当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写xx栏”。
  • 管理层对这种浏览器脚本语言的设想是:功能不要太强,语法要简单,容易学习和部署。那一年,正逢Java语言开始推向市场,Netscape公司决定,脚本语言的语法要接近Java,并且可以支持Java程序。
  • 1995年,Netscape公司雇佣了程序员Brendan Eich开发这种网页脚本语言。
  • Brendan Eich只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。
  • 为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型等等,但是可以利用现有功能找出解决办法

什么是模块化

  • 模块是系统中职责单一可替换的部分,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块
  • 模块化是一种规范,一种约束,这种约束会大大提升开发效率。将每个js文件看作是一个模块,每个模块通过固定的方式引入,向外暴露指定的内容的方式
  • 按照js模块化的设想,一个个模块按照其依赖关系组合,最终插入到主程序中。

为什么需要模块化

  • js作为脚本语言,一开始只是用来在网页上进行表单校验、点击事件、实现简单的动画效果等,1999年的时候,绝大部分工程师做JS开发的时候就直接将变量定义在全局,做的好一些的或许会做一些文件目录规划,将资源归类整理,这种方式被称为直接定义依赖
  • 前端工程师在页面上写写js就能搞定需求,代码简单的堆在一起,只要能从上往下依次执行,实现基本的交互效果就可以了
  • 后来项目越来越复杂,功能越来越多,业务逻辑越来越多,代码量也越来越大,前端开发得到重视
  • JavaScript却没有为组织代码提供任何明显帮助,甚至没有类的概念,更不用说模块(module)了
  • 即使有规范的目录结构,也不能避免由此而产生的大量全局变量,这就导致了一不小心就会有变量冲突的问题,就好比下面这个例子中的onShow。
  • js逐渐拆分,项目中引入的js越来越多,前端代码变成了这样:
    <script src="a.js"> 
           script> <script src="b.js"> 
            script> <script src="util/wxbridge.js"> 
             script> 
    //A同学a.js function onShow(){ 
         //xxxx} //B同学b.js function onShow(){ 
         //xxxx} //重名概率太高 
  • 容易冲突容易覆盖

这样带来的问题

  • 全局变量污染:各个文件的变量都是挂载到window对象上,污染全局变量。
  • 变量重名:不同文件中的变量如果重名,后面的会覆盖前面的,造成程序运行错误。
  • 文件依赖顺序:多个文件之间存在依赖关系,需要保证一定加载顺序问题严重。

模块化解决方案

  • 命令空间模式
  • 闭包模块化模式(IIFE结合闭包)
  • 面向对象开发
  • YUI
  • CommonJS
  • AMD
  • CMD
  • ES6模块化

命名空间模式

  • 无法避免的全局变量污染
  • 2002年左右,有人提出了命名空间模式的思路,用于解决全局变量被污染的问题
  • 不过这种方式,毫无隐私可言,本质上就是全局对象,谁都可以来访问并且操作,一点都不安全
    //A同学a.js var A={ 
          onShow(){ 
         }onXXX(){ 
         }//... } //B同学b.js var B={ 
          onShow(){ 
         } } //还是会重名,但比以前好一点 //比如 其他班A同学,由于合作,引入a.js、b.js  var A={ 
          onShow(){ 
         } } 
  • 模块太臃肿,不敢私自拆模块,又重名
  • 拆模块带来的问题,私有属性乱,代码不灵活
  • 多个文件之间存在依赖关系,需要保证加载顺序
    //a.js var a=''//重名,全局变量不安全 var A={ 
          a:''//不重名,用起来比较乱 A1{ 
         } A2{ 
          B:{ 
          a:'' } } } console.log(A.A2.B.a); 

闭包模块化模式(IIFE结合闭包)

  • 2003年左右就有人提出利用IIFE结合闭包特性,以此解决私有变量的问题,这种模式被称为闭包模块化模式
  • Imdiately Invoked Function Expression,立即执行的函数表达式
  • 像如下的代码所示,就是一个匿名立即执行函数:
    (function(window, undefined){ 
          // 代码...  })(window); 
  • js文件都是使用IIFE包裹的,各个js文件分别在不同的作用域中,相互隔离,最后通过闭包的方式暴露变量
    // a.js var a = (function(cNum){ 
          var aStr = 'a'; var aNum = cNum + 1; return { 
          aStr: aStr, aNum: aNum }; })(cNum); // c.js var cNum = (function(){ 
          var cNum = 0; return cNum; })(); //index.js ;(function(a, cNum){ 
          console.log(a.aNum, cNum); })(a, cNum) 
    //引入 <script src="./c.js"> 
           script> <script src="./a.js"> 
            script> <script src="./index.js"> 
             script> 
  • 这种方法解决了
  • 私有属性乱,代码不灵活
  • 问题:命名会重复,文件依赖顺序仍然需要在入口处严格保证加载顺序

面向对象开发 和 YUI

  • 面向对象开发
    // b.js var b = (function(a){ 
          var bStr = a.aStr + ' bb'; return { 
          bStr: bStr }; })(a); 
  • YUI(雅虎)

CommonJs – 具有里程碑式意义的模块化工具

  • 2009年Nodejs发布,其中Commonjs是作为Node中模块化规范以及原生模块面世的
  • 原生Module对象,每个文件都是一个Module实例(模块)
  • 文件内通过require对象引入指定模块
  • 所有文件加载均是同步完成
  • 通过module.exports或者exports来暴露接口或者数据

语法(关键字)

module export require

// a.js module.exports = { 
    aStr: 'a' }; 
// b.js var a = require('./a'); exports.bStr = a.aStr + ' bb'; 
// index.js var a = require('./a'); var b = require('./b'); console.log(a.aNum, b.bStr); 

源码理解

//module.js function Module(id, parent) { 
    this.id = id; // 文件验重的表示,字符串形式的绝对路径 this.exports = { 
   }; this.parent = parent; if (parent && parent.children) { 
    parent.children.push(this); } this.filename = null; this.loaded = false; this.children = []; } module.exports = Module; var module = new Module(filename, parent); //在commonjs规范中每个模块都是一个Module实例 //require方法调用__load方法加载模块文件 //require的返回值是module.exports || {} //exports = module.exports 
//gulp 'use strict'; var util = require('util'); //... function Gulp() { 
    //... } Gulp.prototype.task = Gulp.prototype.add; Gulp.prototype.run = function() { 
    var tasks = arguments.length ? arguments : ['default']; this.start.apply(this, tasks); }; //... var inst = new Gulp(); module.exports = inst; 
var gulp = require('gulp') 

优点

  • 强大的查找模块功能,开发十分方便
  • 标准化的输入输出,非常统一
  • 每个文件引入自己的依赖,最终形成文件依赖树
  • 解决了重名问题
  • 解决了私有变量问题
  • 解决了加载顺序问题
  • module export require

局限性

  • Commonjs在服务端可以实现模块同步加载,服务器上通过require加载资源是直接读取文件的,因此中间所需的时间可以忽略不计
  • 浏览器这种需要依赖HTTP获取资源的,时间消耗非常大,资源的获取所需的时间不确定,这就导致必须使用异步机制,代表主要有2个:AMD和CMD

AMD(Asynchronous Module Definition)

  • AMD与Commonjs一样都是js模块化规范,是一套抽象的约束,2009年诞生
  • RequireJs,是AMD规范的具体实现(RequireJs诞生之后,推广过程中产生的AMD规范)
  • 语法
    //引入 require([module], callback); //定义 define(id?, dependencies?, factory); 

示例

//引入require.js <script src="/require.js" data-main="/main" async="async" defer> 
     script> 
//main.js requirejs.config({ 
    //... paths: { 
    a: '/a.js', c: '/c.js', index: '/index.js' } }); require(['index'], function(index){ 
    index(); }); 
//a.js define('a', ['c'], function(c){ 
    return { 
    aStr: 'aStr', aNum: c.cNum + 1 } }); 
//c.js define('c', function(){ 
    return { 
    cNum: 0 } }); 
//index/js define('index', ['a', 'c'], function(a, c){ 
    return function(){ 
    console.log(a.aNum, c.cNum); } }); 

CMD(Common Module Definition)

  • 同样是受到Commonjs的启发,国内(阿里)诞生了CMD规范。该规范借鉴了Commonjs的规范与AMD规范,在两者基础上做了改进。
  • SeaJs是CMD规范的实现,跟RequireJs类似,CMD也是SeaJs推广过程中产生的规范

AMD CMD区别

  • AMD,依赖前置,提前执行
  • CMD,按需加载,延迟执行,用到时才运行
    // AMD define( ['./a', './b'], // <- 前置声明,也就是在主体运行前就已经加载并运行了模块a和模块b function (a, b) { 
          a.doSomething(); b.doSomething(); } ) 
    // CMD define(function (require) { 
          var a = require('./a'); // <- 运行到此处才开始加载并运行模块a a.doSomething(); var b = require('./b'); // <- 运行到此处才开始加载并运行模块b b.doSomething(); }) 

ES6中的模块化

  • 2015年6月,ES6发布,JavaScript在语言标准的层面上,实现了模块功能,使得在编译时就能确定模块的依赖关系,以及其输入和输出的变量,不像 CommonJS、AMD之类的需要在运行时才能确定
  • 关键字import,export,default,as,from
  • CommonJS和ES6有两点主要的区别
    • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
    • CommonJS 模块是运行时加载(动态加载),ES6 模块是编译时输出(静态解析)

一个经典的例子

// counter.js exports.count = 0 setTimeout(function () { 
    console.log('500ms后自增的count的值为', ++exports.count, '') }, 500) // commonjs.js const { 
   count} = require('./counter') setTimeout(function () { 
    console.log('commonjs:1000ms后读取count的值是', count) }, 1000) //es6.js import { 
   count} from './counter' setTimeout(function () { 
    console.log('es6:1000ms后读取count的值是', count) }, 1000) 

分别运行 commonjs.js 和 es6.js:

➜ test node commonjs.js 500ms后自增的count的值为 1 commonjs:1000ms后读取count的值是 0 ➜ test babel-node es6.js 500ms后自增的count的值为 1 es6:1000ms后读取count的值是 1 

示例解析

  • 这个例子解释了CommonJS 模块输出的是值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,原始值变了,import加载的值也会跟着变

Tree-Shaking

  • Tree-Shaking,代表的大意就是删除没用到的代码。这样的功能对于构建大型应用时是非常好的,因为日常开发经常需要引用各种库,但大多时候仅仅使用了这些库的某些部分,并非需要全部,此时Tree-Shaking如果能帮助我们删除掉没有使用的代码,将会大大缩减打包后的代码量,减少js包的大小,从而减少用户等待的时间。

tree-shaking的实现

  • 著名的代码压缩优化工具uglify,uglify完成了javascript的DCE(dead code elimination)
  • 其实关于tree shaking的实现原理,找到你整个代码里真正使用的代码,打包进去,那么没用的代码自然就剔除了。
  • ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础
  • tree shaking首先会分析文件项目里具体哪些代码被引入了,哪些没有引入,然后将真正引入的代码打包进去

支持tree-shaking的构建工具

  • Rollup
  • Webpack2
  • closure compiler

ES6支持度

  • 目前浏览器和Node.js的支持程度并不理想
  • Chrome:51 版起便可以支持 97% 的 ES6 新特性
  • Firefox:53 版起便可以支持 97% 的 ES6 新特性
  • Safari:10 版起便可以支持 99% 的 ES6 新特性
  • IE:IE7~11 基本不支持 ES6(IE内心独白:这种事别扯上我好不好)
  • Node.js:6.5 版起便可以支持 97% 的 ES6 新特性。(6.0 支持 92%)
  • 大部分项目已通过 babel 或 typescript 提前体验

写在后面的话

  • 2015 年提出的标准,依然没有得到完全实现
  • JS模块化与落实都非常缓慢,与 javascript 越来越流行的趋势逐渐脱节

参考资料 模块化

  • JavaScript模块化发展
    https://segmentfault.com/a/02578#articleHeader7

  • JavaScript模块化开发的演进历程
    https://segmentfault.com/a/81338

  • 详解JavaScript模块化开发
    https://segmentfault.com/a/33959

  • 精读 js 模块化发展
    https://zhuanlan.zhihu.com/p/

  • JavaScript 模块化历程
    http://web.jobbole.com/83761/

  • 闲聊——浅谈前端js模块化演变
    http://www.cnblogs.com/qingkong/p/5092003.html

  • Javascript模块化编程(一):模块的写法
    http://www.ruanyifeng.com/blog/2012/10/javascript_module.html

  • JavaScript语言的历史
    http://javascript.ruanyifeng.com/introduction/history.html

  • require() 源码解读
    http://www.ruanyifeng.com/blog/2015/05/require.html

  • Node中的Module源码分析
    https://segmentfault.com/a/39548

  • CommonJS AMD CMD对比
    https://segmentfault.com/a/99566

参考资料 tree-shaking

  • 浅谈性能优化之Tree Shaking
    https://www.jianshu.com/p/5028ebad5bb8

  • tree-shaking简单分析
    https://www.jianshu.com/p/ff0230

  • Tree-Shaking性能优化实践 – 原理篇
    https://juejin.im/post/5a4dc8e7279a9

  • Tree-Shaking性能优化实践 – 实践篇
  • https://www.colabug.com/2139553.html

Thanks!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/211694.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 下午9:49
下一篇 2026年3月18日 下午9:50


相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号