一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范

文章目录

  • 一、什么是模块化?
  • 二、CommonJS与Node
    • 2.1 exports 的 导入与导出
    • 2.2 module.exports 又是什么东西?
    • 2.3 require的细节
    • 2.4 模块的加载过程
  • 三、ES Module
    • 3.1 尝试使用ES Modules
    • 3.2 常见的导入与导出方式 export 与 import
    • 3.3 export default
    • 3.4 import 函数
    • 3.5 异步的 import
    • 3.6 ES Module 的加载过程

一、什么是模块化?

  • 事实上模块化开发最终的目的是将程序划分成一个个小的结构
  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,不会影响到其他的结构;
  • 这个结构可以将自己希望暴露的变量、函数、对象等导出给其结构使用
  • 也可以通过某种方式,导入另外结构中的变量、函数、对象

上面说提到的结构,就是模块;按照这种结构划分开发程序的过程,就是模块化开发的过程

无论你多么喜欢 Javascript,以及它现在发展的有多好,我们都需要承认在B/ endan Eichi用了10天写出 javascript的时候,它都有很多的缺陷:

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

在网页开发的早期, Brendan Eich开发 javascriptt仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:

  • 这个时候我们只需要讲 Javascript代码写到< script>标签中即可;
  • 并没有必要放到多个文件中来编写;甚至流行:通常来说 Javascript程序的长度只有一行。

但是随着前端和 javascriptl的快速发展, Javascript代码变得越来越复杂了

  • ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过 javascripti进行前端页面的渲染;
  • SPA的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过 Javascript来实现
  • 包括Node的实现, Javascript编写复杂的后端程序,没有模块化是致命的硬仿;

所以,模块化已经是 Javascript一个非常迫切的需求
但是 Javascript本身,直到ES6(2015)オ推出了自己的模块化方案
在此之前,为了让 Javascript支持模块化,涌现出了很多不同的模块化规范:AMD、CMD、 Commonjs等;

二、CommonJS与Node

我们需要知道 CommonJs是一个规范,最初提出来是在浏览器以外的地方使用,并且当时被命名为 ServerJs ,后来为了体现它的广泛性,修改为 CommonJs ,平时我们也会筒称为 CJS.

  • Node是 CommonJs 在服务器端一个具有代表性的实现;
  • Browserify是 CommonJS在浏览器中的一种实现
  • webpack打包工具具备 Commonjs的支持和转换;

所以,Node中对 CommonsJS进行了支持和实现,让我们在开发node的过程中可以方便的进行模块化开发:

  • 在Node中每一个js文件都是一个单独的模块
  • 这个模块中包括 CommonJs规范的核心变量 exports、 module. exports、 require;

我们可以使用这些变量来方便的进行模块化开发;
exports和 module. exports 可以负责对模块中的内容进行导出
require函数可以帮助我们 导入其他模块(自定义模块、系统模块、第三方库模块) 中的内容

2.1 exports 的 导入与导出

bar.js

// Node中每一个Js文件就是一个模块
const name = 'PengSir'
const age = 18
let message = 'My name is peng'

function sayHello(name) {
     
  console.log('hello' + name);
}

// exports 默认是空对象
// console.log(exports); // {}


// 1.导出
exports.name = name
exports.age = age
exports.sayHello = sayHello
exports.message = message

main.js

// 导入:require 是一个函数
// const bar = require('./bar')
const {
     name,age,sayHello,message} = require('./bar')

console.log(name);// PengSir

图解:导入导出原理
一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第1张图片
在每个Node应用中都有一个 exports 对象,在其他文件导入某个文件时,其实就是拿到该对象的内存地址。

为了验证这一想法,写个demo来确定一下。

bar.js

// 就是一个模块
let name = 'PengSir'

setTimeout(() => {
     
  exports.name = 'test'
}, 1000);
console.log(name)
exports.name = name

main.js

const bar = require('./bar.js')

console.log(bar.name)
setTimeout(() =>{
     
  console.log(bar.name)
})

输出:

PengSir
test

可以发现结果确实如我们所料。
所以,bar对象是 exports 对象的浅拷贝(引用赋值)

2.2 module.exports 又是什么东西?

但是在Node中我们经常导出东西的时候,又是通过 module. exports 导出的

  • module. exports和 exports1有什么关系或者区别呢?

我们追根溯源,通过维基百科中对 Commonjs规范的解析:

  • CommonJS中是没有 module. exports的概念的
  • 但是为了实现模块的导出,Node中使用的是 Module的类,每一个模块都是 Module的一个实例,也就是new module( 一个JS文件就是一个Module实例 )
  • 所以在Node中真正用于导出的其实根本不是 exports,而是 module. exports;
  • 因为 module才是导出的真正实现者

一个文件把它当成一个对象的时候,Node底层就会 new module

Node的底层 实际上做了这么一步操作 module.exports = exports

所以咱们上述的bar = exports = module.exports

验证如下:

bar.js

const name = 'PengSir'
setTimeout(() => {
     
  module.exports.name = 'hahaha'
  console.log(exports.name);
}, 1000);
exports.name = name

main.js

const bar = require('./bar')
console.log(bar.name);

setTimeout(() => {
     
  console.log(bar.name);
}, 2000);

output:

PengSir
hahaha
hahaha

由此可见,我们上述的论述完全正确,即bar = exports = module.exports

图解:
一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第2张图片

一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第3张图片

2.3 require的细节

require的加载过程是同步的,意味着必须等到引入的文件(模块)加载完成之后,才会继续执行其他代码,也就是会造成阻塞(因为引入一个文件则该文件内部的所有代码都会被执行一次)。

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

那么,require的查找规则是怎样的呢?
中文文档:require

这里总结一下 require常见的查找规则:
require(X)

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

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

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

  • 第一步:将X当做一个文件在对应的目录下查找;
    1. 如果有后缀名,按照后缀名的格式查找对应的文件
    2. 如果没有后缀名,按照如下顺序
      1. 直接查找文件X
      2. 查找X.js
      3. 查找X.json
      4. 查找X.node
  • 第二步:没有找到对应的文件,将X作为一个目录
    • 查找目录下面的index文件
      1. 查找X/index.js
      2. 查找X/index.json
      3. 查找X/index.node

情况三:直接是一个X(没有路径),并且X不是一个核心模块

例如我在如下目录的main.js中编写了require('test’)

/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/0 5_javascript-module/02_commonjs/main.js中编写require('why’)

则它的查找规则如下,会逐级查找上一层目录下的node_modules
一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第4张图片

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

2.4 模块的加载过程

  • 结论一:模块在第一次被引入的时候,模块中的js代码会被运行一次
  • 结论二:模块被多次引入时,会缓存,最终只加载(运行)一次
    • 为什么只会加载运行一次呢?
    • 这是因为每个模块对象 module都有一个属性: loaded
    • 为 false表示还没有加载,为true表示已经加载
  • 结论三:如果有循环引入,那么加载顺序是什么?

顺序为:图结构的深度优先算法

一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第5张图片

三、ES Module

小知识:ES Module是ES6推出的。 即es 2015

Javascript.没有模块化一直是 它的痛点 ,所以オ会在社区产生许多的规范: Commonjs、AMD、CMD等,所以在ES推出自己的模块化系统时,大家也是兴奋异常

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

  • 一方面它使用了 Import和 export 关键字 ,不是模块也不是函数。
  • 另一方面它采用编译期的静态分析,并且也加入了动态引用的方式

ES Module模块采用 export 和 import 关键字来实现模块化:

  • export负责将模块内的内容导出
  • import负责从其他模块导入内容

ES Module将自动采用严格模式:use strict

3.1 尝试使用ES Modules

注意在浏览器使用ES Modules时,要在script标签上加上 type="module",且要在服务器上运行,不支持本地运行的file协议(触发CORS),

index.html


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <script src="./index.js" type="module">script>
body>
html>

index.js

console.log('hello EsModules');

结果:

hello EsModules

3.2 常见的导入与导出方式 export 与 import

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./index.js" type="module"></script>
</body>
</html>

index.js 导入

console.log('hello EsModules');

// 常见的导入方式


// 方式一: import {} from '路径'
// 注意此处的{}不是对象,导入时后边必须要加.js,脚手架里和webpack会自动加
// import { name, age, sayhello } from './modules/foo.js'



// 方式二:导出的变量可以起别名
// import { name as Fname, age as Fage, sayhello as FsayHello } from './modules/foo.js'

// 2.1 导出时已经起了别名的,接收要使用别名接受,可以给别名再起别名
// import {Fname as FooName,Fage as FooAge,FsayHello as FooSayHello} from './modules/foo.js'



// 方式三:import * as foo from '路径'
import * as foo from './modules/foo.js'

console.log(foo.name);
console.log(foo.age);
foo.sayHello('彭先生')

foo.js 导出

const name = 'pengsir'
const age = 18
const sayHello = function (name) {
     
  console.log('姓名' + name);
}


// 1.导出方式

// 方式一:

// export const name = 'pengsir'
// export const age = 18
// export const sayHello = function (name) {
     
//   console.log('姓名' + name);
// }

// 方式二: 常用!!!!!!
// {} 这里不是类 就和 if(){} 的大括号一样
// {放置要导出变量的引用列表}

export {
     
  name,
  age,
  sayHello
}


// 方式三:{} 导出时,可以给变量起别名
// export {
     
//   name as Fname,
//   age as Fage,
//   sayHello as FsayHello
// }

输出结果:

hello EsModules
pengsir
18
姓名彭先生

3.3 export default

前面我们学习的导出功能都是有名字的导出( named exports)

  • 在导出(exporte) 时指定了名字;
  • 在导入(import) 时需要知道具体的名字

某些情况下不是很方便,所以还有另外一种导出方式,叫做 default export

  • 默认导出 export时可以不需要指定名字;
  • 在导入时十分方便,并且可以自己来指定名字,类如我们导入Vue、Vuex、VueRoter时内部就是使用的默认导出;

bar.js 导出 :

// 方式四:默认导出
export default function format() {
     
  console.log('对某一个东西,进行格式化!');
}

index.js 导入:

// 方式四: 演示 export default如何导入
import utils from './modules/foo.js'

utils() // 实际是调用 format

结果:

对某一个东西,进行格式化!

注意: 一个文件只能有一个默认导出:export default

3.4 import 函数

通过 Import加载的模块,是不可以在其放到逻辑代码中的,比如:

let flag = true
if (flag) {
     
  // 错误用法,语法错误,不能在逻辑在逻辑代码中使用 import 关键字
  import format from './modules/foo.js'
}

为什么会出现这个情况呢?
parse => AST => 字节码 =>

  • 这是因为 ES Module在被JS引擎(parse)解析时,就必须知道它的依赖关系
  • 由于这个时JS代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况

解决办法:

import() 函数 或者 require()

// 方式五:import() 函数
// 注意:上边使用import时是作为关键字使用,现在是作为函数使用,
// 该函数为异步函数,返回值为promise
let flag = true
if (flag) {
     
  import('./modules/foo.js').then(res => {
     
    console.log('then里边的回调');
    console.log(res); 
  }, err => {
     
    console.log(err);
  })
}

3.5 异步的 import

使用 type="module"时,加载该模块时异步加载的,就相当于给script加了一个async属性。

示例:
index.html


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Documenttitle>
head>
<body>
  <script src="./index.js" type="module">script>
  <script src="./normal.js">script>
body>
html>

index.js

console.log('hello EsModules');

normal.js

console.log('我是普通的js文件');

结果:

我是普通的js文件
hello EsModules

由此可见我们的 ES Module 是异步的。

3.6 ES Module 的加载过程

  • 结论一:ES Module 导出的数据是实时变化的
    • 如果在bar.js中导出一个变量A,在index.js中导入该变量,如果1秒之后bar.js中的变量A值被修改,index.js中的导入的该变量也会修改。
    • 但是我们在index.js中却不可以修改导入的该变量,除非该变量是一个对象类型(保存的是 该对象的内存地址),因为ES Module在底层实现的时候,每次 导出的变量发生变化,都会在模块环境记录中创建一个最新的该变量,类似:const name = name发生变化后const name = name; const name = name,所以能拿到最新的值,且因为底层是使用const 定义的,所以导入后的变量内存地址不能发生变化,但是对象类型的值却可以。

一文彻底搞懂前端模块化:CommonJS规范 与 ES Module规范_第6张图片

你可能感兴趣的:(Node,JS学习笔记,模块化,exports,import,export,module.exorts)