[js基础篇]模块化小结

模块化Module

文章目录

模块化简介

什么是模块化

  • 模块是什么

一个模块是实现一个特定功能的代码集合,其内部数据/实现是私有的,只向外部暴露一些接口(方法)与外部其它模块通信。也就是说模块是一个具有独立作用域,对外暴露特定功能接口的代码集合。

  • 模块的组成

一个模块由数据和操作数据的行为这两部分组成。数据即内部的属性,操作数据的行为即内部的函数。

  • 模块化是什么

模块化就是指将一个大的程序文件,拆分成许多小的文件,然后将小文件组合起来。这些小文件就是模块。

为什么需要模块化

非模块化的问题

当我们加载多个js文件时,假设每个js实现一个功能模块,功能模块之间存在依赖(module2.js依赖module1.js),那么就需要注意引入顺序问题

<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript" src="module3.js"></script>
<script type="text/javascript" src="module4.js"></script>

显然,这样会存在以下几个问题:

  • js文件引入顺序问题可能出错
  • 难以维护
  • 依赖模糊
  • 请求过多

因此我们需要通过模块化来解决。

模块化的优点

1.避免命名冲突:减少命名空间污染。模块化中的每一个模块都是一个独立作用域,任何形式的命名都不会和其他模块有冲突;

2.更好地分离:避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处;

3.高可维护性,提高代码复用性

4.按需加载:提高使用性能和下载速度,按需求加载需要的模块。

模块化的发展历程

原始写法

使用函数作为模块,几个函数放在一起作为一个模块。

function m1(){
  //...
}
function m2(){
  //...
}

缺点:容易造成全局变量的污染,并且模块间没有联系。

对象写法

使用对象作为模块,对象内的属性和方法作为模块的成员。

let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模块内部的数据
myModule.foo() // foo() other data

作用:减少了全局变量,解决命名冲突问题

缺点:会暴露所有模块成员,外部代码可以修改内部属性的值。

立即执行函数(IIFE模式)

为保证数据是私有的,使用立即执行函数,利用闭包来建立模块私有作用域,同时不会对全局作用域造成污染。

var module1 = (function(){
	var count = 0;
	var fun1 = function(){
		//...
	}
	var fun2 = function(){
		//...
	}
	//将想要暴露的内容放置到一个对象中,通过return返回到全局作用域。
	return{
		fun1:fun1,
		fun2:fun2
	}
})()

作用:对于未暴露的数据count,外部无法访问。

缺点:模块间的没有关系,无法解决模块间的依赖

IIFE的增强(引入依赖)

是现代模块实现的基石

//module1.js
(function (window, $) {
  //数据
  let data = 'atguigu.com'
  //操作数据的函数
  function foo() { //用于暴露有函数
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {//用于暴露有函数
    console.log(`bar() ${data}`)
    otherFun() //内部调用
  }
  function otherFun() { //内部私有的函数
    console.log('otherFun()')
  }
  //暴露行为
  window.myModule = {foo, bar}
})(window, jQuery)

//test.html
//引入jquery
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
  myModule.foo()
</script>

模块化规范

CommonJS 双端规范

概述

  • Node 应用由模块组成,采用 CommonJS 模块规范。
  • 每个文件就是一个模块,每个模块都是一个单独的作用域。
  • 每个模块内部,module变量代表当前模块

特点

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

基本语法

暴露模块

1.基本语法

module.exports = valueexports.xxx = value

注意:在CommonJs模块规范中,module.exports 和 exports.xxx 不能混用,如果混用,以module.exports为主

示例:

// 1.module.exports = value
//module1.js
module.exports = {
  msg: 'module1',
  foo() {
    console.log(this.msg)
  }
}
//module2.js
module.exports = function() {
  console.log('module2')
}
// 2.exports.xxx = value
//module3.js
exports.foo = function() {
  console.log('foo() module3')
}
exports.arr = [1, 2, 3, 3, 2]

2.暴露的本质/CommonJS暴露的模块到底是什么?

CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。

本质是暴露module.exports所指的对象

module.exports = {
    a: 1
}
// module.exports的基本实现
var module = {
  exports: {} // exports 就是个空对象
}

// 内置对象关系:module.exports = exports = {}
// 也是exports 和 module.exports 用法相似的原因

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sC7SBEGr-1620658485638)(C:\Users\Daii\AppData\Roaming\Typora\typora-user-images\image-20210510112751259.png)]

3.因此不能对 exports 直接赋值,不会有任何效果,如

// add.js
exports = function(a, b) {
  return a + b;
};
// main.js
var add = require("./add.js");

exports 指向了 function,但module.exports 的内容并没有改变,所以这个模块的导出为空对象。

引入模块

require(xxx),如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径

示例:

let uniq = require('uniq')   // 第三方库
let module1 = require('./modules/module1') // 自定义模块
let module2 = require('./modules/module2')

模块的加载机制

加载时机

  • 服务器端Node,模块的加载是运行时动态同步加载的;

  • 在浏览器端Browserify,运行前对模块进行编译打包处理,运行的是打包生成的js,运行时不需要再从远程引入依赖模块。

加载机制

导出的数据是值拷贝(浅拷贝),也就是说,一旦导出一个值,改变导出的值也不会影响模块内部的变化,即导入的值不会改变

举例:

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
};

导出内部变量counter和改写这个变量的内部方法incCounter

// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3

结果显示,counter导出后,lib.js模块内部的变化无法影响导出的counter。

因为counter是一个原始类型的值,会被缓存。如果写成函数,实际是内存地址拷贝,可以得到内部变动后的值

AMD 浏览器端

概述

  • AMD规范采用异步方式加载模块(CommonJs是同步方式),模块的加载不影响它后面语句的运行。

  • 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

  • 如果是浏览器环境,要从服务器端加载模块,这时就必须采用异步模式,因此浏览器端一般采用AMD规范。

AMD规范基本语法

requireJS

用**require.js实现AMD规范的模块化**,RequireJS是一个工具库,主要用于客户端的模块管理,它的模块管理遵守AMD规范,基本思想

require.config()指定引用路径等,用define()定义模块,用require()加载模块。

requireJS特点
  • 实现js文件的异步加载,避免网页失去响应;
  • 管理模块之间的依赖性,便于代码的编写和维护。
requireJS核心原理

通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。

暴露模块

有依赖的模块:define([依赖模块名], function(){return 模块对象})

//定义没有依赖的模块
define(function(){
   return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
   return 模块
})

引入模块

require([module], callback) 要求两个参数:(CommonJs只要求一个参数)

  • [module],是一个数组,里面的成员就是要加载的模块;

  • callback,则是加载成功之后的回调函数。

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

CMD 浏览器端

概述

  • CMD规范专门用于浏览器端,异步加载模块,模块使用时才会加载执行
  • CMD规范整合了CommonJS和AMD规范的特点。
  • 在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

CMD规范基本语法

SeaJS 加载原理也是动态创建异步 Script 标签。

基本语法与AMD类似

暴露模块

// 没有依赖的模块
define(function(require, module, exports){
   let value = 'xxx';
   //通过require引入依赖模块
   //通过module.exports/exports来暴露模块
   exports.xxx = value
   module.exports = value
})
// 有依赖的模块
define(function(require, exports, module){
   //引入依赖模块(同步)
   var module2 = require('./module2')
   //引入依赖模块(异步)
   require.async('./module3', function (m3) {
 	  ......
   })
   //暴露模块
   exports.xxx = value
})

引入模块

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

ES6模块化

概述

  • 模块化的规范:CommonJS和AMD两种。前者用于服务器,后者用于浏览器。

  • ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。

  • ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

ES6模块化语法

模块功能主要由两个命令构成:exportimport

  • export命令用于规定模块的对外接口,
  • import命令用于输入其他模块提供的功能。

暴露模块

export有三种暴露方式

//分别暴露数据 m1.js
export let firstname = 'Michael'
export let lastName = 'Jackson'

// 统一暴露 m2.js
let firstname = 'Michael'
let lastName = 'Jackson'
let year = 1999
export { firstname, lastName, year }

// 默认暴露 m3.js
export default {
    name: '刘德华',
    sex: '男'
}

引入模块

		// 1.解构赋值形式
        import { firstName, lastName} from "./js/m1.js"
        console.log(firstName, lastName);
        // 有重名的变量需要使用as关键字进行重命名,否则会报错
        import { firstName as first, lastName as last, year as y } from "./js/m2.js"
		// 2.仅针对默认暴露的引入写法
		import m3 from "./js/m3.js"
        console.log(m3);
	    // 3.整体加载 用星号(*)指定一个对象,所有输出值都加载在这个对象上面,全部加载
        import * as m1 from "./js/m1.js"

加载规则

浏览器异步加载

浏览器异步加载的两种语法:

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

<script>标签打开**deferasync属性**,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

deferasync的区别

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

ES6模块加载规则

浏览器使用<script>标签加载 ES6 模块,但是要加入**type="module"属性**。

<script type="module" src="./foo.js"></script>

浏览器对于带有type="module"<script>等同于带defer属性的<script>

都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,即“渲染完再执行”,

模块化规范的区别

CommonJS 模块与 ES6 模块的区别

1.CommonJS 模块导出的是一个值的拷贝(浅拷贝),ES6 模块导出的是值的引用

对于前者来说,导出的值改变,不会影响导入的值,所以如果想更新值,必须重新导入一次。

而后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。

2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。

ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

3.CommonJS 模块的require()同步加载模块,ES6 模块的import命令是异步加载

原因:ES6 模块的import命令有一个独立的模块依赖的解析阶段,又主要用于浏览器,需要下载文件,如果采用同步导入会对渲染有很大影响。

AMD模块与CMD模块的区别

1.在模块定义时对依赖的处理不同。

AMD推崇提前加载依赖(依赖前置),在定义模块的时候就要声明其依赖的模块。

而CMD推崇就近依赖,只有在需要用的地方才进行依赖加载。

2.对依赖模块的执行时机处理不同。

AMD 在依赖模块加载完毕后就立即执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。

而 CMD 在依赖模块加载完毕后,执行回调函数,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序和我们书写的顺序保持一致。

总结

四种模块加载方案:

  • CommonJS ,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
  • AMD 方案,采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
  • CMD 方案,与AMD 方案类似,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
  • ES6 模块化,使用 import 和 export 的形式来导入导出模块。

对于服务端的模块而言,由于其模块都是存储在本地的,模块加载方便,所以通常是采用同步读取文件的方式进行模块加载。

而对于浏览器而言,其模块一般是存储在远程网络上的,模块的下载是一个十分耗时的过程,所以通常是采用动态异步脚本加载的方式加载模块文件。

无论是客户端还是服务端的 JavaScript 模块化实现,都会对模块进行缓存,以此减少二次加载的开销。

参考资料

https://juejin.cn/post/6844903817272623117

https://juejin.cn/post/6844903744518389768

https://es6.ruanyifeng.com/#docs/module-loader

上一篇:前端模块化发展简史


下一篇:node系列扯犊子之二聊聊模块系统