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/1190000015302578#articleHeader7
  • JavaScript模块化开发的演进历程
    https://segmentfault.com/a/1190000011081338
  • 详解JavaScript模块化开发
    https://segmentfault.com/a/1190000000733959
  • 精读 js 模块化发展
    https://zhuanlan.zhihu.com/p/26118022
  • 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/1190000015139548
  • CommonJS AMD CMD对比
    https://segmentfault.com/a/1190000009899566

参考资料 tree-shaking

  • 浅谈性能优化之Tree Shaking
    https://www.jianshu.com/p/5028ebad5bb8
  • tree-shaking简单分析
    https://www.jianshu.com/p/f525570f0230
  • Tree-Shaking性能优化实践 - 原理篇
    https://juejin.im/post/5a4dc842518825698e7279a9
  • Tree-Shaking性能优化实践 – 实践篇
  • https://www.colabug.com/2139553.html

Thanks!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值