前端模块化

364 阅读5分钟

什么是模块

将一个复杂的程序依据一定的规则(规范)封装成几个块(文件),并进行组合在一起 内部数据与实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信,模块化主要用于抽离公共代码,隔离作用域,避免变量冲突等

早期实现

  • script标签分离代码
  • 全局变量连接不同文件
  • 依赖关系为被依赖文件需要先加载

基于这样的形式,暴露出的问题有:

  • 全局变量易受污染
  • 不同script文件间依赖顺序要维护,依赖关系不明确
  • script请求过多且同步阻塞加载 根据解决方法的不同,我将其分为四部分,依次为「语法hack」、「模仿node」、「预编译」以及「ES6 Module」

语法 hack

这一时期解决的是「全局变量污染」的问题,有两个方向:

  • 定义一个对象(命名空间),包裹全局变量,减少全局变量数量
  • 通过函数闭包形成安全空间,公有变量变成私有变量

原始写法

// 容易命名冲突
function foo() {}

function bar() {}

NameSpace

// 减少公共变量,但module是对象,不安全
var module = {
  foo: function() {},
  bar: function() {}
}

IIFE 匿名闭包(立即执行函数)

// 利用闭包特性,形成私有作用域。_private是私有变量,更加安全

var module = (function () {
  var _private = 'safe' 
  function foo () {
    console.log(_private)
  }
  return {
    foo: foo
  }
}());

公共变量传入,jquery用这种方法

// 与上一方法类似
(function ($) {
  $.foo = function () {
    console.log('jquery')
  }
}(jQuery));

「语法hack」阶段,大概是1999-2009,在语言层面上,利用「变量对象」和「函数闭包」减少了全局变量。但是:没有完全消除全局变量;而且依赖问题没有涉及

模仿node

在这一阶段,js模块化在nodejs端取得不错的实践,故开发者想将nodejs端模块化的规范迁移到前端,规范命名为:CommonJs

  • 每个文件是一个模块,有自己作用域,
  • 模块内变量、函数、类对外不可见,但模块内内置module对象,module.exports对外暴露接口,exports对象指向module.exports,故也可通过exports暴露接口
  • require用于加载模块

// math.js
exports.add = function(a, b){
    return a + b;
}

// main.js
var math = require('math')
console.log(math.add(1, 2));

CommonJs解决了两个问题:

  • 去掉了全局变量冲突(引入每个模块都能用变量重命名)
  • 模块间依赖关系清晰

将CommonJs引入到前端,出现了问题:服务端的文件存储在本地,能同步加载。而在浏览器,模块通过script标签异步加载进来,这样模块还没加载完就运行。所以前端的模块化都需要异步加载,具体根据实现方式又可分两种,AMD和CMD。

AMD: AMD是一种异步加载规范,它的特点是预执行,如下图:在define定义了回调函数的依赖有aModule和bModule,在回调函数开始执行时,依赖模块已经加载并执行。即依赖前置,提前执行


// require.js
require(['math'], function(math) {
  math.add(1, 2)
})

// math.js
define(['aModule', 'bModule'], function(a, b) {
  // a模块已经执行完成
  // b模块虽然没有使用,但也已经执行
  console.log(a.hello());
});

CMD:CMD也是一种异步加载规范,它的特点是懒执行,如下图:在define里面,如果没有require('./a'),a模块就不会先执行。即依赖就近,延迟执行。

define(function(require, exports) {
    var a = require('./a'); // require才执行a模块
    a.doSomething();

    exports.foo = 'bar';
    exports.doSomething = function() {};
});

webpack

相对于require.js的语法糖,browserify从不同思路解决模块化问题。与其每个文件都变成一个script文件加载,不如所有都打包成一个文件。那么就不存在异步加载的问题,也不会script标签过多。但是浏览器还是不能识别module、exports、require。没关系,预编译时也同时处理。处理的原理涉及抽象语法树,使用时只需按CommonJs的写代码,并用browserify编译就能在浏览器中运行

// main.js是我们写的符合CommonJs语法的代码
// compiled.js是编译后浏览器能识别的代码
browserify main.js > compiled.js

webpack将预编译发挥到极致,不仅js文件可以编译,css,图片等也可以编译。webpack基本成为现代项目必备的一项。从模块化历史来看,webpack是预编译的集大成者。当然现在配合webpack的模块方案不是commonJs,而是ES6 Module。

结论:

预编译另辟蹊径,同时解决了公共变量,script标签相互依赖,script文件过多等问题。但是对于编译工具的依赖和使用,已越来越影响到我们开发。现在就有所谓的webpack配置工程师,每一个项目配置webpack也是很头疼一件事。如果我们能直接使用模块化编程那该多好

第四部曲--ES6 Module

ES6 Module彻底解决了javascript没有模块化的历史。Modules使用不同的方式加载js文件(与js原先的脚本加载方式相对)。它有特别的语义:(以下几点来源《深入理解ES6》)

  • 自动为严格模式,并且无法跳出
  • 模块内部顶级作用域变量只会在模块内部,不会变成全局变量
  • 模块顶级作用域的this为undefined
  • 模块不允许在代码中使用HTML风格的注释
  • 对于需要让模块外部代码访问的内容,必须导出它们(export)
  • 允许模块从其他模块导入绑定(import)

ES6 Module是未来,现在能通过babel或typescript体验,有部分浏览器已经支持ES6 Module,只等待兼容性跟上。

总结

模块化是伴随着代码量增多出现的。本质上是要解决作用域问题,使得前端能更好的组织代码。早期通过NameSpace,IIFE等语法hack方案减少公共变量;中期借鉴nodejs端模块化方案,形成前端的commonJs,但本质是js语法糖,使用起来繁琐;后期有了预编译,很好解决了问题,但是处理预编译各种配置,也消耗着精力;ES6是官方统一的js模块方案,是现在,也是未来