最详细的前端模块化解决方案梳理

前言

美好的一天从 npm run dev 开始,对于如今的前端而言从百家争鸣到逐渐统一的辅助开发工具对前端效率的提高有着不可替代的做用,这一切都必须依赖前端的模块化。在前端还处在刀耕火种的年代想实现模块化只能经过闭包也就是 IIFE ,而现在 ES6 Modules 多是前端最经常使用的模块解决方案,那么本篇从 IIFE 来开始概括前端模块化的前世此生。哦,对了,没说为何要琢磨模块化了,模块化便于拆封代码,避免变量冲突,没有模块化想开发一个大工程,呃,不太行。html

IIFE(闭包)

咱们该怎么理解模块这个概念,在刀耕火种的年代很天然的能够把一个js文件理解成一个模块,看下面的demo:前端

<!DOCTYPE html>
<html>
<head>
  <title></title>
</head>
<body>
  <script src="./main.js"></script>
  <script src="./app.js"></script>
  <script src="./api.js"></script>
</body>
</html>
复制代码

main.js、app.js、api.js 把他们仨理解成是三个模块,可是这三个js文件里面的代码应该如何构建呢?由于js自己机制的缘由只能将代码暴露到全局做用域下,若是不去避免全局变量污染那么这样的模块是毫无心义的也是后患无群的。在刀耕火种的年代天然而然想到了闭包,咱们尝试用下面的方法去构建这三个js文件达到隔离做用域的目的。node

(function(){
  var name = 'main.js'
  ...
})()
复制代码

咱们用匿名函数而不是声明一个function再调用是由于匿名函数,执行完后很快就会被释放,这种机制不存在函数名污染全局做用域的状况。这种匿名函数的方式就是IIFE,虽然它能够有效解决命名冲突的问题,可是对于依赖管理,仍是一筹莫展。因为浏览器解析器是从上至下执行脚本,所以为了保证脚本间的依赖关系,就必须手动维护好script标签的引入顺序,涉及到模块间的通讯能够用参数的方式传入。webpack

CommonJS(node.js)

CommonJS 是一位叫 Kevin Dangoor 的国际友人提出的关于js模块的一种规范,注意了是规范不是实现方案,而 node.js 就是基于 CommonJS 规范实现的模块化。es6

咱们先看一下CommonJS规范都规定了那些内容:web

  1. 一个单独的文件就是一个模块。
  2. 每个模块都是一个单独的做用域。

node.js 对 CommonJS 规范作了实现,那么 node.js 到底作了什么?npm

  1. node.js 的实现中,给每一个文件赋予了一个 module 对象,这个对象包括了描述当前模块的全部信息,它的 exports 属性(即module.exports)是对外的接口。加载某个模块,实际上是加载该模块的 module.exports 属性。对 module 对象拥有的属性简单列一下:
  • module.id 模块的识别符,一般是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其余模块。
  • module.paths 表示模块的搜索路径,路径的多少取决于目录的深度。
  • module.exports 表示模块对外输出的值。
  1. 同时为每一个文件提供了一个 require 方法用于加载模块。
  2. 为了方便 node.js 为每一个模块提供一个 exports 变量,指向 module.exports。这等同在每一个模块头部声明 var exports = module.exports 。

module.exports 和 exports 在使用上有什么区别?json

上面说过 exports 的实现方法等同于 var exports = module.exports,也就是说想要经过 exports 关节字向外输出值只能给 exports 添加属性好比下面这种写法。gulp

exports.myName = 'tom'
exports.getMyName = function(){
  return 'tom'
}
复制代码

要注意的是,不能直接将 exports 变量指向一个值,由于这样等于切断了exports 与 module.exports 的联系。好比下面这种写法是无效的,由于 exports 再也不指向 module.exports 了。api

exports = function(){
  return 'tom'
}
// or
exports = 'tom'
复制代码

require() 方法的加载机制:

require 命令用于加载模块文件。require 命令的基本功能是,读入并执行一个 js 文件,而后返回该模块的 exports 对象,若是没有发现指定模块,会报错。require 方法默认读取 js 文件,因此能够省略 js 后缀名,js文件名前面须要加上路径,能够是相对路径也能够是绝对路径。若是省略路径,node.js 会认为你要加载一个内部模块,或者已经安装在本地的 node_modules 目录中的模块。若是加载的是一个目录,node.js会首先寻找该目录中的 package.json 文件,加载该文件 main 字段配置的模块,不然就寻找该目录下的 index.js 文件。在加载 node_modules 目录中的模块时按照下面的路径顺序去查找。

/usr/local/lib/node/x.js
/home/user/projects/node_modules/x.js
/home/user/node_modules/x.js
/home/node_modules/x.js
/node_modules/x.js

require() 方法的缓存机制:

在 node.js 中经过 require 屡次引入同一模块,模块内的代码并不会重复执行屡次,这依赖于 node.js 的缓存机制,第一次加载某个模块时,node.js 会缓存该模块,一个模块被加载一次以后,就会在缓存中维持一个副本,若是遇到重复加载的模块会直接提取缓存中的副本,也就是说在任什么时候候每一个模块都只在缓存中有一个实例,模块加载的顺序,按照其在代码中出现的顺序。

关于运行时:

在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块须要提早编译打包处理(gulp, webpack)。

require() 加载模块是同步仍是异步?

首先 CommonJS 规范加载模块是同步的,因为 node.js 主要用于服务器,模块文件通常都已经存在于本地硬盘,何况有上文提到的缓存机制模块常常被复用,因此加载起来比较快,IO开销能够忽略,不用考虑非同步加载的方式,因此 CommonJS 规范比较适用。也就是说,只有加载完成,才能执行后面的操做。可是,若是是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,所以浏览器端通常采用AMD规范。

如何实现做用域隔离?

在浏览器端经过观察打包以后的代码能够发现实现做用域隔离的本质仍是经过函数做用域也就是闭包,这部分不展开说了,后面计划出一篇分析 webpack 打包生成代码的博客...

AMD(RequireJS)

AMD 也是关于模块化的规范,RequireJS 是基于 AMD 规范实现的模块化解决方案。上文说过 CommonJS 规范中模块的加载是同步的,那么试想在浏览器环境中模块是存在于服务器的,在加载模块的过程当中会致使阻塞,使得咱们后面的步骤没法进行下去致使很长的时间内浏览器是被阻塞的,还可能会执行一个未定义的方法而致使出错。相对于服务端的模块化,浏览器环境下模块化的标准必须知足一个新的需求就是“异步的模块管理”,在这样的背景下,RequireJS 出现了。

RequireJS 提供了两个方法:

  • require() 用于引入其余模块
  • define() 定义新的模块

RequireJS 的内部逻辑是经过函数 define() 将须要的依赖模块加载进来,在回调里拿到被依赖也就是被加载的模块而后返回一个新的值(模块),咱们全部的关于新模块的业务代码都在这个函数内部操做。

咱们看下面的例子:

定义模块

// 定义不依赖其余模块的模块
define(function() {
  return {
    ...
  }
})

// 定义依赖其余模块的模块
define([ './module1', './module2' ], function(m1, m2) {
  // m一、m2 就是 './module1''./module2' 加载到的值
  return {
    ...
  }
})
复制代码

加载模块

require(['./module1', './module2'], function(m1, m2) {
  // m一、m2 一样是 './module1''./module2' 加载到的值
  m1.fun()
  m2.fun()
})
复制代码

CMD(CommonJS)

CommonJS是规范,AMD是规范,CMD固然也是规范,基于CMD的实现方案是 Sea.js。经过对 RequireJS 的分析咱们发现一个问题,就是在用 RequireJS 声明一个模块时要提早指定全部的依赖,这些依赖项会被当作形参传到 define() 方法中,这加大了开发的难度,由于依赖的模块会优先所有加载,那么在阅读代码的时候要先把依赖模块都阅读一次(由于这些依赖模块是初始化的时候执行的)。偷懒是开发者提升效率的初衷,那么若是能够在业务代码中使用到依赖模块的时候再去加载该模块这样就没必要提早阅读所有依赖模块代码了。这样 Sea.js 就出现了,在使用上能够理解 Sea.js 是 CommonJS 和 AMD 的结合。

define(function(require, exports, module) {
  let myHeader, myBody, myFooter
  if (status1) {
    var header = require('./header')
    myHeader = header.fun()
  }
  if (status2) {
    var body = require('./body')
    myBody = body.fun()
  }
  if (status3) {
    var footer = require('./footer')
    myFooter = footer.fun()
  }
  require.async('./module', function (m) { })

  module.exports = {
    header: myHeader,
    body: myBody,
    footer: myFooter
  }
})
复制代码

要注意的是虽然 require() 方法夹杂在了业务代码中可是仍是会提早加载,加载完某个依赖模块后并不执行,只是下载而已,在全部依赖模块加载完成后进入主逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是彻底一致的。若是使用 require.async() 方法,能够实现模块的懒加载。

虽然 RequireJS 也支持 SeaJS 的写法,可是依赖的模块任然是预先加载的。

UMD(AMD + CommonJS)

CommonJS 适用于服务端,AMD、CMD 适用于web端,那么须要同时运行在这两端的模块就能够采用 UMD 的方法,使用该模块化方案,能够很好地兼容AMD、CommonJS语法。UMD 先判断是否支持Node.js的模块(exports)是否存在,存在则使用 node.js 模块模式。再判断是否支持 AMD(define是否存在),存在则使用AMD方式加载模块。因为这种通用模块解决方案的适用性强,不少JS框架和类库都会打包成这种形式的代码。

ES6 Module

在 ES6 中没必要使用特殊的手段,ES6在语法层面就支持了模块化,然而受限于浏览器的实现程度,若是想要在浏览器中运行,仍是须要经过 Babel 等转译工具进行编译。ES6提供了 importexport 命令,分别对应模块的导入和导出功能。es6模块相关的语法有好多,不展开说了。

特色:

  1. 语法是静态的,import 会自动提高到代码的顶层。
  2. 使用 import 导入的变量是只读的,没法被赋值,并且是引用传递。(若是你的模块在运行过程当中修改了导出的变量值,就会反映到使用模块的代码中去。因此,不推荐在模块中修改导出值,导出的变量应该是静态的。)
  3. ES6使用的是基于文件的模块,也就是一个文件一个模块。
  4. ES6模块API是静态的,一旦导入模块后,没法再在程序运行过程当中增添方法。

Webpack中的模块化方案

webpack 兴起以后,咱们可使用 CommonJS、AMD、CMD、UMD、ES6 任意的方法来开发最终 webpack 会打包成你须要的样子。

相关文章
相关标签/搜索