Javascript模块化

模块化开发

  • 将程序划分成一个个小的结构
  • 这个结构中编写属于自己的逻辑代码,有自己的作用域 不会影响其他的结构
  • 这个结构可以将自己希望暴露的变量 函数 对象 等导出给其结构使用
  • 也可以导入另外结构中的变量 函数 对象

按照结构划分开发程序的过程 就是模块化开发的过程


Js被称之为是披着C语言外衣的Lisp(用于人工智能)

早期JS存在的缺陷:

  • 比如var 定义的变量作用域问题
  • JS的面向对象并不能像常规面向对象语言一样使用class
  • Js没有模块化

在ES6之前 为了让js支持模块化 涌现出了很多不同的模块化规范AMD CMD CommonJS

没有模块化带来问题

命名冲突

通过立即执行函数可以解决问题,但是存在缺点

  • 第一,我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用
  • 第二,代码写起来很混乱,每个文件中的代码都需要包裹在一个匿名函数中来编写
  • 第三,在没有合适的规范情况下,每个人、每个公司都可能会任意命名、甚至出现模块名称相同的情况

CommonJS 和Node

CommonJS是一个规范 最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了

体现它的广泛性,修改为CommonJS,平时我们也会简称为CJS

  • Node是CommonJS在服务器端一个具有代表性的实现;

  • Browserify是CommonJS在浏览器中的一种实现;

  • webpack打包工具具备对CommonJS的支持和转换;

Node中对CommonJS进行了支持和实现

  • 在Node中每一个JS文件都是一个单独的模块

  • 这个模块中包括CommonJs规范的核心变量: exports module.exports require

  • exports和module.exports可以负责对模块中的内容进行导出

  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

exports导出

exports是一个对象 。 可以在这个对象中添加很多个属性

1
2
3
4
//bar
exports.name = name
exports.age =age
exports.sayhello =sayhello

在另一个文件中导入

1
2
//main
const {name,age,sayhello} = require('./bar')

意味着main中的bar变量等于exports对象;

  • 也就是require通过各种查找方式,最终找到了exports这个对象;

  • 并且将这个exports对象赋值给了bar变量;

  • bar变量就是exports对象了

  • bar对象是exports对象的浅拷贝(引用赋值)

  • 浅拷贝的本质就是一种引用的赋值而已

module.exports

  • CommonJS中是没有module.exports的概念的;
    • 但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module;
    • 所以在Node中真正用于导出的其实根本不是exports,而是module.exports;
    • 因为module才是导出的真正实现者;
  • exports也可以导出原因
    • module对象的exports属性是exports对象的一个引用
    • 源码中先给module.exports赋值 然后给了exports

Require细节

require是一个函数,可以帮助我们引入一个文件(模块)中导入的对象

require查找规则: require(X)

  • 情况一: :X是一个核心模块,比如path、http
    • 直接返回核心模块 并停止查找
  • 情况二 X以 ./ ../ 或 /(根目录)开头的
    • 第一步
      • 有后缀名
      • 没有后缀名
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
    • 第二步 没有找到对应的文件 将X作为一个目录
      • 查找目录下面的index文件
        • 查找X/index.js文件
        • 查找X/index.json文件
        • 查找X/index.node文件
    • 如果没有找到 报错
  • 情况三:直接是一个X(没有路径) 并且X不是一个核心模块
    • 先查看是否是一个核心模块
    • 会一层一层的在node_modules目录下找
      • 没有找到就报错

模块加载过程

  • 模块被第一次引入时,模块中的js代码会被运行一次

    • CommonJS模块加载js文件的过程是运行时加载的,并且是同步的
    • 运行时加载意味着是js引擎在执行js代码的过程中加载 模块
    • 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行
  • 模块被多次引入时,会缓存,最终只加载(运行)一次

    • 每个模块对象module都有一个属性:loaded。
    • 为false表示还没有加载,为true表示已经加载;
  • CommonJS通过module.exports导出的是一个对象:

    • 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量;
    • 但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改;
  • 循环引入的加载顺序

    • 深度优先算法
    • main -> aaa -> ccc -> ddd -> eee ->bbb

CommonJS 规范的缺点

  • CommonJS加载模块是同步的
    • 同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行
    • 这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快
  • CommonJS一般不应用于浏览器
    • 浏览器加载js文件需要先从服务器将文件下载下来,之后在加载运行
    • 那么采用同步的就意味着后续的js代码都无法正常运行,即使是一些简单的DOM操作
    • webpack中使用CommonJs是因为webpack会将代码转成浏览器可以直接执行的代码

AMD

AMD(Asynchronous Module Definition(异步模块定义))主要是应用于浏览器的一种模块化规范

  • 它采用的是异步加载模块
  • AMD实现的比较常用的库是require.js和curl.js;

使用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script src="./lib/requirejs/require.js"data-main="./index.js"  ></script>  //index.html中

(function(){
require.config({
baseUrl:'',
paths:{
"bar":"./modules/bar",
"boo":"./modules/boo"
}
})

require(['boo'],function(boo){})
})() //index.js中
----------------------------------------------

define(
function(){
const name ="hyp";
const age = 18;
const sayhello=function(name){
console.log("hello" + name);
}
return{
name,
age,
sayhello
}
}
) //bar.js

---------------------------------------------
define([
'bar',
], function(bar) {
console.log(bar.name);
console.log(bar.age);
bar.sayhello('ko')
}); //boo中

CMD

CMD(Common Module Definition(通用模块定义))也是应用于浏览器的一种模块化规范

CMD实现方案 SeaJS

使用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script src="./lib/sea.js"></script>
<script>
seajs.use('./index.js');
</script> //index.html
-------------------------------

define(function(require, exports, module) {
const foo = require('./modules/foo');

console.log(foo.name);
console.log(foo.age);
foo.sayHello("王小波");
})
//index.js
--------------------------------

define(function(require, exports, module) {
const name = "李银河";
const age = 20;
const sayHello = function(name) {
console.log("你好" + name);
}

module.exports = {
name: name,
age: age,
sayHello: sayHello
}
});
//foo.js

ES Module

ES Module和CommonJS的模块化有一些不同之处:

  • 一方面它使用了import和export关键字;
  • 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式;

import export 关键字 实现导入导出

  • export 负责将模块内的内容导出
  • import 负责从其他模块导入内容
  • ES Module 自动采用 严格模式 : use strict

常见导入导出方式

导出

1
2
3
4
5
export const name = "why";
export const age = 18;
export const sayHello = function(name) {
console.log("你好" + name);
}
1
2
3
4
5
6
7
const name = "why";
const age = 18;
const sayHello = function(name) {
console.log("你好" + name);
}
{}中统一导出 {}大括号, 但是不是一个对象 {放置要导出的变量的引用列表}
export {name,age,sayHello}
1
2
3
4
5
6
7
   const name = "why";
const age = 18;
const sayHello = function(name) {
console.log("你好" + name);
}
导出时, 可以给变量起别名
export {name as fName, age as fAge , sayHello as fSayHello}

导入

1
2
方式一: import {} from '路径';
import { name, age, sayHello } from './modules/foo.js';
1
2
3
4
方式二: 导出变量之后可以起别名
import { name as wName, age as wAge, sayHello as wSayHello } from './modules/foo.js'
如果导出时 有别名 那么
import { fName as wName, fAge as wAge, fSayHello as wSayHello } from './modules/foo.js';
1
2
3
方式三: * as foo    
import * as foo from './modules/foo.js'
把属性放到了foo这个对象中

Export和import结合使用

1
export {sum as barSum} from './bar.js'

作用

  • 在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中
  • 这样方便指定统一的接口规范,也方便阅读
  • 这个时候,我们就可以使用export和import结合使用

default 用法

  • 默认导出export时可以不需要指定名字

  • 在导入时不需要使用 {},并且可以自己来指定名字

  • 默认的只能有一个

import函数

通过import加载一个模块,是不可以在其放到逻辑代码中的,

  • ES Module解析时已经确定了依赖关系

  • 这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;

  • 使用require时候可以是因为require是一个函数

  • 解决方法

    • 如果在webpack的环境下 直接使用require()

    • 纯ES Module 环境下面 :import() 异步加载

      • import()函数 本质上返回值是一个promise函数
    1
    2
    3
    4
    5
    6
    import('./modules/foo.js').then(res => {
    console.log("在then中的打印");
    console.log(res.name);
    console.log(res.age);
    }).catch(err => {
    })
    • 脚手架 会单独打包 用到的时候加载

ES Module加载过程

ES Module加载js文件是解析阶段加载的 并且是异步的 不会阻塞主线程的执行

  • 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用
  • 比如from后面的路径需要动态获取。
  • 比如不能将import放到if等语句的代码块中
  • 有时候也称ES Module是静态解析的,而不是动态或者运行时解析的

异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行

  • 也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;
  • 如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行

node对ESmodule的支持

  • 文件后缀名应为 .mjs
  • 导入路径要加后缀

CommonJS和ES Module交互

  • 通常情况下,CommonJS不能加载ES Module

  • 多数情况下,ES Module可以加载CommonJS