深圳Web前端培训学习:js中的模块化--【千锋】

深圳 Web 前端培训学习: js 中的模块化 --【千锋】

创新互联公司2013年成立,是专业互联网技术服务公司,拥有项目成都网站建设、网站制作网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元扬中做网站,已为上家服务,为扬中各地企业和个人服务,联系电话:13518219792

0. 前言   

我们知道最常见的模块化方案有CommonJS 、 AMD 、 CMD 、 ES6 , AMD 规范一般用于浏览器,异步的,因为模块加载是异步的, js 解释是同步的,所以有时候导致依赖还没加载完毕,同步的代码运行结束; CommonJS 规范一般用于服务端,同步的,因为在服务器端所有文件都存储在本地的硬盘上,传输速率快而且稳定。

1.script 标签引入

最开始的时候,多个script 标签引入 js 文件。但是,这种弊端也很明显,很多个 js 文件合并起来,也是相当于一个 script ,造成变量污染。项目大了,不想变量污染也是很难或者不容易做到,开发和维护成本高。 而且对于标签的顺序,也是需要考虑一阵,还有加载的时候同步,更加是一种灾难,幸好后来有了渲染完执行的 defer 和下载完执行的 async ,进入新的时代了。

接着,就有各种各样的动态创建script 标签的方法,最终发展到了上面的几种方案。

2.AMD 与 CMD

2.1AMD

异步模块定义,提供定义模块及异步加载该模块依赖的机制。AMD 遵循依赖前置,代码在一旦运行到需要依赖的地方,就马上知道依赖是什么。而无需遍历整个函数体找到它的依赖,因此性能有所提升。但是开发者必须先前知道依赖具体有什么,并且显式指明依赖,使得开发工作量变大。而且,不能保证模块加载的时候的顺序。 典型代表 requirejs 。 require.js 在声明依赖的模块时会立刻加载并执行模块内的代码。 require 函数让你能够随时去依赖一个模块,即取得模块的引用,从而即使模块没有作为参数定义,也能够被使用。他的风格是依赖注入,比如:

/api.js

define('myMoudle',['foo','bar'],function(foo,bar){

    // 引入了 foo 和 bar ,利用 foo 、 bar 来做一些事情

    return {

        baz:function(){return 'api'}

               }

});

require(['api'],function(api) {

    console.log(api.baz())

})

复制代码

然后你可以在中间随时引用模块,但是模块第一次初始化的时间比较长。这就像开始的时候很拼搏很辛苦,到最后是美滋滋。

2.2CMD

通用模块定义,提供模块定义及按需执行模块。遵循依赖就近,代码在运行时,最开始的时候是不知道依赖的,需要遍历所有的require 关键字,找出后面的依赖。一个常见的做法是将 function toString 后,用正则匹配出 require 关键字后面的依赖。 CMD 里,每个 API 都简单纯粹。可以让浏览器的模块代码像 node 一样,因为同步所以引入的顺序是能控制的。 对于典型代表 seajs ,一般是这样子:

define(function(require,exports,module){

    //... 很多代码略过

     var a = require('./a');

    // 要用到 a ,于是引入了 a

    // 做一些和模块 a 有关的事情

});

复制代码

对于b.js 依赖 a.js

//a.js

define(function(require, exports) {

    exports.a = function(){// 也可以把他暴露出去

    // 很多代码    

    };

});

//b.js

define(function(require,exports){

      // 前面干了很多事情,突然想要引用 a 了

        var fun = require('./a');

    console.log(fun.a()); // 就可以调用到及执行 a 函数了。

   })

// 或者可以 use

seajs.use(['a.js'], function(a){

    // 做一些事情

});

复制代码

AMD 和 CMD 对比: AMD 推崇依赖前置、提前执行, CMD 推崇依赖就近、延迟执行。

AMD 需要先列出清单,后面使用的时候随便使用(依赖前置),异步,特别适合浏览器环境下使用(底层其实就是动态创建 script 标签)。而且 API 默认是一个当多个用。

CMD 不需要知道依赖是什么,到了改需要的时候才引入,而且是同步的,就像临时抱佛脚一样。

对于客户端的浏览器,一说到下载、加载,肯定就是和异步脱不了关系了,注定浏览器一般用AMD 更好了。但是, CMD 的 api 都是有区分的,局部的 require 和全局的 require 不一样。

3.CommonJS 与 ES6

3.1 ES6

ES6 模块的 script 标签有点不同,需要加上 type='module'

复制代码

对于这种标签都是异步加载,而且是相当于带上defer 属性的 script 标签,不会阻塞页面,渲染完执行。但是你也可以手动加上 defer 或者 async ,实现期望的效果。 ES6 模块的文件后缀是 mjs ,通过 import 引入和 export 导出。我们一般是这样子:

//a.mjs

import b from 'b.js'

//b.mjs

export default b

复制代码

ES6 毕竟是 ES6 ,模块内自带严格模式,而且只在自身作用域内运行。在 ES6 模块内引入其他模块就要用 import 引入,暴露也要用 export 暴露。另外,一个模块只会被执行一次。 import 是 ES6 新语法,可静态分析,提前编译。他最终会被 js 引擎编译,也就是可以实现编译后就引入了模块,所以 ES6 模块加载是静态化的,可以在编译的时候确定模块的依赖关系以及输入输出的变量。 ES6 可以做到编译前分析,而 CMD 和 AMD 都只能在运行时确定具体依赖是什么。

3.2CommonJS

一般服务端的文件都在本地的硬盘上面。对于客户,他们用的浏览器是要从这里下载文件的,在服务端一般读取文件非常快,所以同步是不会有太大的问题。require 的时候,马上将 require 的文件代码运行

代表就是nodejs 了。用得最多的,大概就是:

//app.js

var route = require('./route.js')// 读取控制路由的 js 文件

//route.js

var route  = {......}

module.exports = route

复制代码

require 第一次加载脚本就会马上执行脚本,生成一个对象

区别: CommonJS 运行时加载,输出的是值的拷贝,是一个对象(都是由 module.export 暴露出去的),可以直接拿去用了,不用再回头找。所以,当 module.export 的源文件里面一些原始类型值发生变化, require 这边不会随着这个变化而变化的,因为被缓存了。但是有一种常规的操作,写一个返回那个值的函数。就像 angular 里面 $watch 数组里面的每一个对象,旧值是直接写死,新值是写一个返回新值的函数,这样子就不会写死。 module.export 输出一个取值的函数,调用的时候就可以拿到变化的值。

ES6 是编译时输出接口,输出的是值的引用,对外的接口只是一种静态的概念,在静态解释后已经形成。当脚本运行时,根据这个引用去原本的模块内取值。所以不存在缓存的情况, import 的文件变了,谁发出 import 的也是拿到这个变的值。模块里面的变量绑定着他所在的模块。另外,通过 import 引入的这个变量是只读的,试图进行对他赋值将会报错。

4. 循环依赖

就是a 依赖 b , b 依赖 a ,对于不同的规范也有不同的结果。

4.1CommonJS

对于node ,每一个模块的 exports={done:false} 表示一个模块有没有加载完毕,经过一系列的加载最后全部都会变为 true 。 同步,从上到下,只输出已经执行的那部分代码 首先,我们写两个 js 用 node 跑一下:

//a.js

console.log('a.js')

var b = require('./b.js')

console.log(1)

//b.js

console.log('b.js')

var a = require('./a.js')

console.log(2)

// 根据他的特点, require 一个文件的时候,马上运行内部的代码,所以相当于

console.log('a.js')

console.log('b.js')

console.log(2)

console.log(1)

// 输出是 a.js 、 b.js 、 2 、 1

复制代码

加上export 的时候:

//a.js

module.exports = {val:1}

var b = require('./b.js')

console.log(b.val)

module.exports = {val:2}

b.val = 3

console.log(b)

//b.js

module.exports = {val:1}

var a = require('./a.js')

console.log(a.val)

module.exports = {val:2}

a.val = 3

console.log(a)

//1. 在 a.js 暴露出去一个对象 module.exports = {val:1}

//2.require 了 b ,来到 b ,运行 b 脚本

//3.b 的第一行,把 {val:1} 暴露出去,引入刚刚 a 暴露的 {val:1} ,打印 a.val 的结果肯定是 1

//4. 重新暴露一次,是 {val:2} ,然后做了一件多余的事情,改 a.val 为 3 (反正是拷贝过的了怎么改都不会影响 a.js ),毫无疑问打印出 { val: 3 }

//5. 回到 a ,继续第三行,打印 b.val ,因为 b 暴露的值是 2 ,打印 2

//6. 继续再做一件无意义的事情,打印 { val: 3 }

复制代码

解决办法:代码合理拆分

4.2ES6 模块

ES6 模块是输出值的引用,是动态引用,等到要用的时候才用,因此可以完美实现相互依赖,在相互依赖的 a.mjs 和 b.mjs ,执行 a 的时候,当发现 import 马上进入 b 并执行 b 的代码。当在 b 发现了 a 的时候,已经知道从 a 输入了接口来到 b 的,不会回到 a 。但是在使用的过程中需要注意,变量的顺序。

如果是单纯的暴露一个基本数据类型,当然会报错not defined 。 因为函数声明会变量提升,所以我们可以改成函数声明(不能用函数表达式)

//a.mjs

import b from './b'

console.log(b())

function a(){return 'a'}

export default a

//b.mjs

import a from './a'

console.log(a())

function b(){return 'b'}

export default b

复制代码

4.3 require

我们一般使用的时候,都是依赖注入,如果是有循环依赖,那么可以直接利用require 解决

define('a',['b'],function(b){

    //dosomething

});

define('b',['a'],function(a){

    //dosomething

});

// 为了解决循环依赖,在循环依赖发生的时候,引入 require :

define('a',['b','require'],function(b,require){

    //dosomething

    require('b')

});

复制代码

4.4 sea

循环依赖,一般就是这样

//a.js

define(function(require, exports, module){

    var b = require('./b.js');

    //......

});

//b.js

define(function(require, exports, module){

    var a = require('./a.js');

    //......

});

复制代码

而实际上,并没有问题,因为sea 自己解决了这个问题: 一个模块有几种状态:

'FETCHING': 模块正在下载中 'FETCHED': 模块已下载 'SAVED': 模块信息已保存 'READY': 模块的依赖项都已下载,等待编译 'COMPILING': 模块正在编译中 'COMPILED': 模块已编译

步骤:

1. 模块 a 下载并且下载完成 FETCHED

2. 编译 a 模块(执行回调函数)

3. 遇到了依赖 b , b 和自身没有循环依赖, a 变成 SAVED

4. 模块 b 下载并且下载完成 FETCHED

5.b 遇到了依赖 a , a 是 SAVED ,和自身有循环依赖, b 变成 READY ,编译完成后变成 COMPILED

6. 继续回到 a ,执行剩下的代码,如果有其他依赖继续重复上面步骤,如果所有的依赖都是 READY , a 变成 READY

7. 继续编译,当 a 回调函数部分所有的代码运行完毕, a 变成 COMPILED

对于所有的模块相互依赖的通用的办法,将相互依赖的部分抽取出来,放在一个中间件,利用发布订阅模式解决

5.webpack 是如何处理模块化的

假设我们定义两个js : app.js 是主入口文件, a.js 、 b.js 是 app 依赖文件,用的是 COMMONJS 规范 webpack 首先会从入口模块 app.js 开始,根据引入方法 require 把所有的模块都读取,然后写在一个列表上:

var modules = {

  './b.js': generated_b,

  './a.js': generated_a,

  './app.js': generated_app

}

复制代码

'generated_'+name 是一个 IIFE ,每个模块的源代码都在里面,不会暴露内部的变量。比如对于没有依赖其他模块的 a.js 一般是这样,没有变化:

function generated_a(module, exports, webpack_require) {

   // ...a 的全部代码

}

复制代码

对于app.js 则不一样了:

function generated_app(module, exports, webpack_require) {

  var a_imported_module = __webpack_require__('./a.js');

  var b_imported_module = __webpack_require__('./b.js');

  a_imported_module['inc']();

  b_imported_module['inc']();

}

复制代码

webpack_require 就是 require 、 exports 、 import 这些的具体实现,够动态地载入模块 a 、 b ,并且将结果返回给 app

对于webpack_require ,大概是这样的流程

var installedModules = {};// 保存已经加载完成的模块

function webpack_require(moduleId) {

  if (installedModules[moduleId]) {// 如果已经加载完成直接返回

    return installedModules[moduleId].exports;

  }

  var module = installedModules[moduleId] = {// 如果是第一次加载,则记录在表上

            i: moduleId,

            l: false,// 没有下载完成

            exports: {}

  };

// 在模块清单上面读取对应的路径所对应的文件,将模块函数的调用对象绑定为 module.exports ,并返回

  modules[moduleId].call(module.exports, module, module.exports,__webpack_require__);

  module.l = true;// 下载完成

  return module.exports;

}

复制代码

对于webpack 打包后的文件,是一个庞大的 IIFE ,他的内容大概是这样子:

(function(modules) {

    var installedModules = {};

    function __webpack_require__(moduleId) { /*...*/}

    __webpack_require__.m = modules;// 所有的文件依赖列表

    __webpack_require__.c = installedModules;// 已经下载完成的列表

    __webpack_require__.d = function(exports, name, getter) {// 定义模块对象的 getter 函数

        if(!__webpack_require__.o(exports, name)) {

            Object.defineProperty(exports, name, {

                configurable: false,

                enumerable: true,

                get: getter

            });

        }

    };

    __webpack_require__.n = function(module) {// 当和 ES6 模块混用的时候的处理

        var getter = module && module.__esModule ?// 如果是 ES6 模块用 module.default

            function getDefault() { return module['default']; } :

            function getModuleExports() { return module; };// 是 COMMONJS 则继续用 module

        __webpack_require__.d(getter, 'a', getter);

        return getter;

    };

    __webpack_require__.o = function(object, property) { // 判断是否有某种属性(如 exports )

return Object.prototype.hasOwnProperty.call(object, property);

};

    __webpack_require__.p = "";// 默认路径为当前

    return __webpack_require__(__webpack_require__.s = 0);// 读取第一个模块

})

/************************************************************************/

//IIFE 第二个括号部分

([

(function(module, exports, __webpack_require__) {

var a = __webpack_require__(1);

var b = __webpack_require__(2);

// 模块 app 代码

}),

(function(module, exports, __webpack_require__) {

// 模块 a 代码

module.exports = ...

}),

(function(module, exports, __webpack_require__) {

// 模块 b 代码

module.exports = ...

})

]);

复制代码

如果是ES6 模块,处理的方法也不一样。还是假设我们定义两个 js : app.js 是主入口文件, a.js 、 b.js 是 app 依赖文件。

(function(modules) {

// 前面这段是一样的

})

([

(function(module, __webpack_exports__, __webpack_require__) {// 入口模块

    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

    var __WEBPACK_IMPORTED_MODULE_0__m__ = __webpack_require__(1);

    var __WEBPACK_IMPORTED_MODULE_1__m__ = __webpack_require__(2);

    Object(__WEBPACK_IMPORTED_MODULE_0__m__["a"])();// 用 object 包裹着,使得其他模块 export 的内容即使是基本数据类型,也要让他变成一个引用类型

    Object(__WEBPACK_IMPORTED_MODULE_1__m__["b"])();

}),

(function(module, __webpack_exports__, __webpack_require__) {

__webpack_exports__["a"] = a;// 也就是 export xxx

//....

}),

(function(module, __webpack_exports__, __webpack_require__) {

__webpack_exports__["b"] = b;

//....

})

]);


文章名称:深圳Web前端培训学习:js中的模块化--【千锋】
本文链接:http://scyanting.com/article/iecjpi.html