前端模块化

2021-12-10 10:54:31 浏览数 (1)

js本身的问题: 不具有模块化的语法规则,在语言层面没有命名空间。 JavaScript 编程过程中很多时候,我们都在修改变量,在一个复杂的项目开发过程中,如何管理函数和变量作用域,显得尤为重要。

代码语言:javascript复制
  function m1(){
    //...
  }
  function m2(){
    //...
  }
  1. 通用模块将所有函数方法暴露给全局作用域,造成命名冲突。
  2. 多个script标签在解析过程中,按照从上到下的顺序解析,如果有依赖规则,必须按照执行顺序,被依赖者先执行,依赖者后执行。

模块化的作用:

  1. 避免命名冲突
  2. 依赖管理
  3. 提供可维护和可复用的代码
  4. 对象写法:函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。module1.m1();但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。module1._count = 5;
代码语言:javascript复制
  var module1 = new Object({
    _count : 0,
    m1 : function (){
      //...
    },
    m2 : function (){
      //...
    }
  });
  1. 命名空间:顶级的应用命名空间被挂载到唯一一个全局对象上的对象(在浏览器中是 window,在 Node.js 应用中是 global)
代码语言:javascript复制
var MYNAMESPACE = MYNAMESPACE || {};
​
MYNAMESPACE.person = function(name) {
    this.name = name;
};
​
MYNAMESPACE.person.prototype.getName = function() {
    return this.name;
};
​
// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName();
​
// 嵌套的命名空间
var myMasterNS = myMasterNS || {};
myMasterNS.mySubNS = myMasterNS.mySubNS || {};
myMasterNS.mySubNS.someFunction = function(){
    //插入逻辑 
};
  1. 闭包:匿名自执行函数,外部代码无法直接改变内部计数器的值。module1._count = 5
代码语言:javascript复制
  var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();
  1. 继承模块:
代码语言:javascript复制
  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);
​
// 模块可能为空
  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});

解决方案

模块化 CJS、AMD、CMD、UMD、ESM

统一模块规范

commonjs:

代码语言:javascript复制
var MySalute = "Hello";
module.exports = MySalute;
​
// world.js
var MySalute = require("./salute");
var Result = MySalute   " world!";
console.log(Result);
  1. node环境 CommonJS规范

所有代码都运行在模块作用域,不会污染全局作用域。 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。 模块加载的顺序,按照其在代码中出现的顺序。

代码语言:javascript复制
var math = require('math');
​
var math = require('math');
math.add(2,3); // 5
  1. 浏览器环境 Uncaught ReferenceError: require is not defined 浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的变量: module exports require global 浏览器加载 CommonJS 模块的原理与实现 browserify

第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说, 如果加载时间很长,整个应用就会停在那里等。这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

AMD:

以浏览器第一的原则发展,异步加载模块。主要有两个Javascript库实现了AMD规范:require.js和curl.js。

  1. require.js js 文件越来越大,需要同时加载多个js文件。
代码语言:javascript复制
<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>

问题:

  1. 加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长
  2. 其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。

require.js的诞生,就是为了解决这两个问题: (1)实现js文件的异步加载,避免网页失去响应; (2)管理模块之间的依赖性,便于代码的编写和维护。

代码语言:javascript复制
require([module], callback);

require(['math'], function (math) {
   math.add(2, 3);
});

require.js的加载:

代码语言:javascript复制
<script src="js/require.js" defer async="true" ></script> 
// IE不支持这个属性,只支持defer,所以把defer也写上。
// 加载requirejs文件,也可能造成网页失去响应。
// 解决办法有两个,一个是把它放在网页底部加载,
// 另一个是写成上面这样

// 加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。
<script src="js/require.js" data-main="js/main"></script>

CMD:lazyload

依赖就近

代码语言:javascript复制
define(function(require, exports, module) {
   var clock = require('clock');
   clock.start();
});

AMD和CMD最大的区别是对依赖模块的执行时机处理不同,二者皆为异步加载模块。

AMD依赖前置,js可以方便知道依赖模块是谁,立即加载;而CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是很多人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

代码语言:javascript复制
// CMD
define(function(require, exports, module) {   
    var a = require('./a')  
    a.doSomething()   // 此处略去 100 行   
    var b = require('./b') // 依赖可以就近书写   
    b.doSomething()   // 
    ... 
})
// AMD 
define(['./a', './b'], function(a, b) {  // 依赖必须一开始就写好    
    a.doSomething()    // 此处略去 100 行    
    b.doSomething()    
    ...
})

UMD:

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

代码语言:javascript复制
(function (window, factory) {
    if (typeof exports === 'object') {
     
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
     
        define(factory);
    } else {
     
        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});

ES6 Module

代码语言:javascript复制
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

// defer与async的区别是:
// defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;
// async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
// 一句话,defer是“渲染完再执行”,async是“下载完就执行”。
// 另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,
// 而多个async脚本是不能保证加载顺序的。

浏览器:

type属性设为module,浏览器知道这是一个 ES6 模块。 浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

代码语言:javascript复制
<script type="module" src="./foo.js"></script>

如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

代码语言:javascript复制
<script type="module" src="./foo.js" async></script>

一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

代码语言:javascript复制
<script type="module">
  import utils from "./utils.js";

  // other code
</script>

node:

Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"

如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。 如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs

CommonJS和ES6的区别:

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

CommonJs

代码语言:javascript复制
// lib.js
var counter = 3;
function incCounter() {
  counter  ;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};
代码语言:javascript复制
// main.js
var mod = require('./lib');

console.log(mod.counter);  // 3
mod.incCounter();
console.log(mod.counter); // 3
代码语言:javascript复制
// lib.js
var counter = 3;
function incCounter() {
  counter  ;
}
module.exports = {
  get counter() {
    return counter
  },
  incCounter: incCounter,
};

ES6

代码语言:javascript复制
// lib.js
export let counter = 3;
export function incCounter() {
  counter  ;
}

// main.js
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

umd规则实现方法

0 人点赞