javascript模块化

名词:

文件划分  命名空间  IIFE(Immediately Invokable Function Expressions)私有作用域  AMD  UMD  CommonJS  ES Modules  CMD

模块化规范:

  CommonJS

定义和引用

CommonJS 规定每个文件就是一个模块,有独立的作用域,这个模块中包括CommonJS规范的核心变量:exports、module.exports、require;每个模块内部,都有一个 module 对象,代表当前模块。通过它来导出 API,它有以下属性:

  • id 模块的识别符,通常是带有绝对路径的模块文件名;
  • filename 模块的文件名,带有绝对路径;
  • loaded 返回一个布尔值,表示模块是否已经完成加载;
  • parent 返回一个对象,表示调用该模块的模块;
  • children 返回一个数组,表示该模块要用到的其他模块;
  • exports 表示模块对外输出的值;
  • module.exports是模块最终对外输出的值,最初它和exports指向同一个对象,exports是较晚提出的简单替代写法。

引用模块则需要通过 require 函数,它的基本功能是,读入并执行一个 JavaScript 文件,然后返回该模块的 exports 对象。

特性

CommonJS ,它采用的是值拷贝和动态声明。值拷贝和值引用相反,一旦输出一个值,模块内部的变化就影响不到这个值了,可以简单地理解为变量浅拷贝。

动态声明,就是消除了静态声明的限制,可以“自由”地在表达式语句中引用模块。

require文件查找规则

导入格式如下:require(X)

情况一:X是一个Node核心模块,比如path、http

直接返回核心模块,并且停止查找

情况二:X是以./ 或…/ 或/(根目录)开头的

第一步:将X当做一个文件在对应的目录下查找;

1.如果有后缀名,按照后缀名的格式查找对应的文件
2.如果没有后缀名,会按照如下顺序:

  • 直接查找文件X
  • 查找X.js文件
  • 查找X.json文件
  • 查找X.node文件

第二步:没有找到对应的文件,将X作为一个目录查找目录下面的index文件

    • 查找X/index.js文件
    • 查找X/index.json文件
    • 查找X/index.node文件

如果没有找到,那么报错:not found

模块的加载过程

一:模块在被第一次引入时,模块中的js代码会被运行一次
二:模块被多次引入时,会缓存,最终只加载(运行)一次

只会加载运行一次是因为每个模块对象module都有一个属性:loaded,loaded为false表示还没有加载,为true表示已经加载;

三:如果有循环引入,那么加载顺序是什么?

如图:


这个其实是一种数据结构:图结构;
图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search);
Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

CommonJS规范缺点

    • 它的模块加载器由 Node.js 提供,依赖了 Node.js 本身的功能实现。如果 CommonJS 模块直接放到浏览器中无法执行。
    • CommonJS 约定以同步的方式进行模块加载,这种加载机制放到浏览器端,会带来明显的性能问题。它会产生大量同步的模块请求,浏览器要等待响应返回后才能继续解析模块。即,模块请求会造成浏览器 JS 解析过程的阻塞,导致页面加载速度缓慢。
    • CommonJS 的这种加载机制放在服务端是没问题的,一来模块都在本地,不需要进行网络 IO,二来只有服务启动时才会加载模块,而服务通常启动后会一直运行,所以对服务的性能并没有太大的影响。
    • 在早期为了可以在浏览器中使用模块化,通常会采用AMD或CMD,但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ESModule代码的转换,所以AMD和CMD已经使用非常少了。

    

  AMD(Asynchromous Module Definition - 异步模块定义

  AMD全称为Asynchronous Module Definition,即异步模块定义规范,是开源社区早期为浏览器提供的,它采用同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。

AMD 实现的比较常用的库是 require.js 和 curl.js

AMD 规范只定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:define(id?, dependencies?, factory);

  • id 为模块的名称,该参数是可选的。如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;如果提供了该参数,模块名必须是“顶级”的和绝对的(不允许相对名字)。
  • dependencies 是个数组,它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入(定义中模块的)工厂函数中。
  • factory 为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。

举例:

//foo.js
define(function() {
  const name = "pjy";
  const age = 19;
  function sum(num1,num2) {
    return num1 + num2
  }

  return {
    name,
    age,
    sum
  }
})

//main.js
require.config({
  baseUrl: './src',
  paths: {
    foo: "./foo",
    bar: "./bar"
  }
})

require(["foo","bar"],function(foo) {
  console.log("main:",foo);
})

 

使用如下:

// main.js
define(["./print"], function (printModule) {
  printModule.print("main");
});

// print.js
define(function () {
  return {
    print: function (msg) {
      console.log("print " + msg);
    },
  };
});

在 AMD 规范当中,我们可以通过 define 去定义或加载一个模块,比如上面的main模块和print模块,如果模块需要导出一些成员需要通过在定义模块的函数中 return 出去(参考print模块),如果当前模块依赖了一些其它的模块则可以通过 define 的第一个参数来声明依赖(参考main模块),这样模块的代码执行之前浏览器会先加载依赖模块。可以使用 require 关键字来加载一个模块,如:

// module-a.js
require(["./print.js"], function (printModule) {
  printModule.print("module-a");
});

不过 require 与 define 的区别在于前者只能加载模块,不能定义一个模块。

由于没有得到浏览器的原生支持,AMD 规范仍然需要由第三方的 loader 来实现。不过 AMD 规范使用起来稍显复杂,代码阅读和书写都比较困难。因此,关于新的模块化规范的探索,业界从仍未停止脚步。

CMD(Common Module Definition - 公共模块定义)

CMD 是 SeaJS 在推广过程中对模块定义的规范,具有CommonJS 特性和 AMD 特性

定义模块 define(factory)

加载模块 require(id)

UMD(AMD和CommonJS的糅合)

它让 CommonJS 和 AMD 模块跨端运行

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

在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

ES Modules(又称ESM)

是由 ECMAScript 官方提出的模块化规范,它已经得到了现代浏览器的内置支持。

在现代浏览器中,如果在 HTML 中加入含有type="module"属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析,这也是 Vite 在开发阶段实现 no-bundle 的原因。

由于模块加载的任务交给了浏览器,即使不打包也可以顺利运行模块代码。

除了浏览器端,一直以 CommonJS 作为模块标准的 Node.js 也从12.20版本开始正式支持原生 ES Module。

模块化发展历程:

ES6模块化理解:

在ES6中每一个模块即是一个文件,在文件中定义的变量,函数,对象在外部是无法获取的。如果你希望外部可以读取模块当中的内容,就必须使用export来对其进行暴露(输出)

b.js
import { aa }  from "./a"
a.js
export const aa = '123'

输出文件(test.js)

export let myName="itlong";

引入文件(index.js)

import {myName} from './test.js';
console.log(myName); //itlong

 

输出/导出/暴露

一、默认暴露

注意:一个模块只能有一个默认导出,对于默认导出,导入的名称可以和导出的名称不一致;

/************导出  test.js  *************/
export default function(){
    return "默认导出一个方法"
}
/************引入***********/
import myFn from "./test.js";//注意这里默认导出不需要用{}。
console.log(myFn());//默认导出一个方法

也可以将所有需要导出的变量放入一个对象中,然后通过默认暴露进行导出;

/************导出 test.js ****************/
export default {
    myFn(){
        return "默认导出一个方法"
    },
    myName:"itlong"
}
/*************引入 test.js *****************/
import myObj from "./test.js";
console.log(myObj.myFn(),myObj.myName);//默认导出一个方法 itlong

 

二、统一暴露 

如果想要导出多个变量,可以将这些变量包装成对象进行模块化输出:

/***************导出 test.js*************/
let myName="itlong";
let myAge=18;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
    myName,
    myAge,
    myfn
}
/***************引入 test.js *************/
import {myfn,myAge,myName} from "./test.js"; console.log(myfn());//我是itlong!今年18岁了 console.log(myAge);//18 console.log(myName);//itlong

如果不想暴露模块当中的变量名字,可以通过as来进行操作:

/***************导出 test.js*************/
let myName="itlong";
let myAge=18;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
    myName as name,
    myAge as age,
    myfn as fn
}
/*****************接收的代码调整为***********/ import {fn,age,Name} from "./test.js"; console.log(fn());//我是itlong!今年18岁了 console.log(age);//18 console.log(name);//itlong

   

三、分别暴露

export let school = 'gc';
export function teach() {
    	console.log("m1--我们可以教给你很多东西!");
};

  

重命名export和import:

理解:

如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6为提供了重命名的方法,当你在导入名称时可以这样做:

/*******************test1.js****************/
export let myName="我来自test1.js";
/*******************test2.js***************/
export let myName="我来自test2.js";
/*************index.js*****************/
import {myName as name1} from "./test1.js";
import {myName as name2} from "./test2.js";
console.log(name1);//我来自test1.js
console.log(name2);//我来自test1.js

 

 导入

通用的导入方式

直接导入整个模块:

/***************导出 test.js **************/

let myName="itlong";
let myAge=18;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
myName as name,
myAge as age,
myfn as fn
}

/************* 导入 test.js **************/
import * as info from "./test.js"; //通过*来批量接收,用as来指定接收的名字 console.log(info.fn());//我是itlong!今年18岁了 console.log(info.age);//18 console.log(info.name);//itlong

 

当在一个文件中使用import导入多个模块时,不论import语句与其它代码的顺序如何,最终都会最先使用import导入并运行。

导出语法深入理解

用法注意

分别暴露 export

统一暴露 export {}

 

导出的到底是啥?

导出语法总结

导入语法深入理解

用法注意

具名导入

默认导入

整体导入

导入的到底是啥

导入语法总结

导入导出复合写法

 

 

Node.js

是JavaScript的服务器运行环境,对ES6的支持度更高除了那些默认打开的功能,还有一些语法功能已经实现了,但默认没打开

Babel

可以将ES6代码转为ES5代码,从而在老版本的浏览器执行,Babel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件

posted on 2022-07-27 22:35  前端码牛  阅读(398)  评论(0编辑  收藏  举报

导航