头图

1 概述

1.1 前端为什么需要模块化

  • 解决命名冲突
  • 提供代码复用性和可维护性
  • 灵活架构,焦点分离,方便模块间组合、分解
  • 多人协作互不干扰

1.2 js模块化发展历程

1.2.1 script标签

随着前端复杂度提高,为什么能够提高项目代码的复用性和可维护性等,就将一个功能封装成一个文件,把一个js文件当成一个模块,这样js文件也就多了起来。js引入方式大概是下面这样:

<script src="jquery.js"></script>
<script src="jquery_scroller.js"></script>
<script src="main.js"></script>
<script src="other1.js"></script>
<script src="other2.js"></script>
<script src="other3.js"></script>

简单的将js文件放在一起,但是彼此的引用顺序不能出错。比如必须先引入jquery,才能使用jquery_scroller,不然就会报错

优点:

相对于所有逻辑都在同一个js文件,模块化思想是进步的。

缺点:

污染全局作用域。因为每一个都是暴露在全局的,会导致全局变量命名冲突,可通过命名空间解决
依赖关系不明显,不利于维护。 比如main.js需要使用jquery,但是,从上面的文件中,我们是看不出来的,如果jquery忘记了,那么就会报错。

1.2.2 CommonJS规范

CommonJS规范是一个对js模块化的规范,最初用在服务端node上。webpack是支持CommonJS规范的。

根据规范,每一个文件都是模块,其内部定义的变量属于这个模块,不会对外暴露,即不会污染全局变量。中心思想是通过require方法同步加载所依赖的模块,然后通过exports或者module.exports来导出需要暴露的接口。如下所示:

// a.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

//b.js
var a = require('./a.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6

这里的a.js就是CommonJS规范的模块了。module代表这个模块,exports属性就是对外暴露的接口,可以对外导出可访问的变量和方法,比如x和addX

exports是对module.exports的引用,所以我们可以认为模块顶部有exports = module.exports这样一个定义,所以我们不能直接给exports赋值

这样我们在b.js中就能获取到a.js暴露的变量和方法

优点:

解决了依赖和全局变量污染的问题

缺点:

因为CommonJS是同步加载的,服务器端没有问题,但是在浏览器端需要将文件从服务器端请求过来,只有加载完成才能执行后面的操作,会阻塞渲染,所以不适用在浏览器端

1.2.3 AMD规范

AMD规范是非同步加载模块,允许指定回调函数。AMD规范的实现就是require.js
AMD标准中定义了下面两个api

   - require([module], callback)
   - define(id, [depends], callback)

即通过define定义一个模块,然后使用require来加载一个模块,并且require还支持CommonJS的导出方式

定义alert模块:

define(function () {
    var alertName = function (str) {
      alert("I am " + str);
    }
    var alertAge = function (num) {
      alert("I am " + num + " years old");
    }
    return {
      alertName: alertName,
      alertAge: alertAge
    };
  });

引入模块:

require(['alert'], function (alert) {
  alert.alertName('zhansan');
  alert.alertAge(18);
});
优点:

支持浏览器环境异步加载模块,可以并行加载多个模块

缺点:

提高了开发成本,并且不能按需加载,必须提前加载所有的依赖

1.2.4 CMD规范

CMD和AMD类似,即一个js文件就是一个模块,但是CMD是按需加载,不是在模块开始加载所有的依赖,如下所示:

define(function(require, exports, module) {
  var $ = require('jquery');
  var Spinning = require('./spinning');
  exports.doSomething = ...
  module.exports = ...
})

优点:

实现了浏览器端的模块话加载
按需加载,依赖就近

缺点:

依赖SPM打包,模块的加载逻辑偏重。

这时我们就可以看出AMD和CMD的区别了,前者是对于依赖的模块提前执行,而后者是延迟执行。 前者推崇依赖前置,而后者推崇依赖就近,即只在需要用到某个模块的时候再require。 如下:

// AMD
define(['./a', './b'], function(a, b) {  // 依赖必须一开始就引入  
   a.doSomething()      
   b.doSomething()    
   //...
});

// CMD
define(function(require, exports, module) {
   var a = require('./a')   
   a.doSomething()   
   var b = require('./b') 
   // 依赖可以就近引入
   b.doSomething()
   // ... 
});

1.2.5 ES6模块化

ES6模块化方案是真正的规范,上面的几种方案只是前段社区自己实现的。在ES6中,通过import关键字引入模块,export关键字导出模块。ES6模块化和CommonJs的一个区别就是前者导入导出都是引用,而后者导入的是值的拷贝。

/**
 * 首字母大写
 * @param str
 */
export const capitalize = (str: string) => {
  str = str || '';
  if (str.length > 0) {
    const first = str.substr(0, 1).toUpperCase();
    const spare = str.substr(1, str.length);
    return first + spare;
  }
};

2.打包工具

2.1 Webpack

webpack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

优点:

一切皆模块,可以模块化任何资源
支持各种模块化语法
开发便捷
扩展性强,插件机制完善

缺点:

配置复杂,文档滞后
通过babel编译之后的js代码打包后体积过大

2.2 Parcel

超快的打包速度,多线程在多核上并发编译,不用任何配置。

优点:

能做到无配置完成以上项目构建要求;
内置了常见场景的构建方案及其依赖,无需再安装各种依赖;
能以 HTML 为入口,自动检测和打包依赖资源;
默认支持模块热替换,真正的开箱即用;

缺点:

不支持 SourceMap
不支持剔除无效代码 ( TreeShaking )
一些依赖会 让Parcel 出错:当你的项目依赖了一些 Npm 上的模块时,有些 Npm 模块会让 Parcel 运行错误;
不灵活的配置,无法自定义

如图:
模块化

参考如下:
https://zhuanlan.zhihu.com/p/...
https://blog.csdn.net/github_...
https://segmentfault.com/a/11...


予怀
13 声望0 粉丝

生命不息,bug不止