javascript复习之旅 13.2 模块化(下)

start

为什么要模块化

  • 名称污染
  • 依赖管理

主流的模块化

  1. CommonJS
  2. ES Module

当然还有:AMD CMD (这里暂时就不详细说明)

1. CommonJS

1.1 基础知识

CommonJS 的提出,弥补 Javascript 对于模块化,没有统一标准的缺陷。nodejs 借鉴了 CommonJS 的 Module ,实现了良好的模块化管理。

目前 CommonJS 广泛应用于以下几个场景:

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

    我们很多的依赖包,都是基于CommonJS模块化规范去做的。

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

  • webpack 打包工具对 CommonJS 的支持和转换;也就是前端应用也可以在编译之前,尽情使用 CommonJS 进行开发。

1.2 显著特点

在使用 CommonJS 规范下,有几个显著的特点。

  • commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;

  • 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;

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

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

1.3 基础使用

// main.js
var a = require('./a')
console.log('打印', a) // 打印 { value: '你好', say: [Function: say] }


// a.js
var value = '你好'
function say() {
  console.log('你好呀')
}

exports.value = value
exports.say = say

1.4 CommonJS 模块整体是如何运行的。

// 实际上是在外层包装了一层,传入了 exports,require,module,__filename,__dirname 这些变量。
(function(exports,require,module,__filename,__dirname){
   const sayName = require('./hello.js')
    module.exports = function say(){
        return {
            name:sayName()
        }
    }
})

1.5 require

1.5.1 require 加载的文件类别

① 为 nodejs 底层的核心模块。(例如 fs path)

② 为我们编写的文件模块,比如上述 sayName (绝对或相对路径的文件)

③ 为我们通过 npm 下载的第三方自定义模块,比如 crypto-js。 (第三方依赖库)

1.5.2 require 的查找逻辑

  1. 缓存的模块(require过一次就会被缓存起来)

  2. 核心模块,绝对或相对路径文件模块(直接读取)

  3. 第三方模块

    • 优先当前目录下的 node_modules
    • 查找上一级文件夹直到根目录
    • 找到 node_modules 查找同一名称的文件包。
    • 查找对应包的 (package.json的main属性指向的文件 > index.js > index.json > index.node

1.5.3 require 模块引入与处理(代码执行的顺序,同步执行)

  1. 主文件,从上往下执行。
  2. require()的文件未被缓存 ,就执行对应的文件;
  3. 如果有缓存,就不进入这个文件,继续向后执行。

1.5.4 测试

main.js

console.log('main1')
var a = require('./a')
console.log('main2')
var b = require('./b')
a.say()
console.log('main2')

a.js

const b = require('./b')

console.log('我是 a 文件', b)

exports.say = function () {
  const getMes = require('./b')
  const message = getMes()
  console.log(message)
}

b.js

const say = require('./a')
const object = {
  name: '你好呀',
}
console.log('我是 b 文件')
console.log('打印 a 模块', say)

setTimeout(() => {
  console.log('异步打印 a 模块', say)
}, 0)

module.exports = function () {
  return object
}

node ./main.js

看下输出结果

main1
我是 b 文件
打印 a 模块 {}
我是 a 文件  [Function]
main2
{
  name: '你好呀',
}
main2
异步打印 a 模块 { say: [Function] }

why?

执行顺序可参考1.5.3的内容,CommonJS同步执行,其实也蛮好理解的。

1.6 exports

先啰嗦几句:

  • 单词请记好,加s
  • 千万不要因为单词混淆了导致出错!!!

先打印看看

console.log(module.exports) // {}
console.log(exports) // {}
console.log(module.exports === exports) // true

这两个变量 全等于,那为什么会有两种写法?

// main.js
var a = require('./a')
var b = require('./b')
console.log('a', a) // a {}
console.log('b', b) // {name:"新的b对象"}

// a.js
exports={name:"新的a对象"}

// b.js
module.exports={name:"新的b对象"}

why? 其实这个地方很好理解,可以这样理解:

我们实际导出的是 `module.exports`

然后我们在当前模块又声明一个变量 `exports = module.exports ` 赋值了值引用。

所以两者 全等于

但是当我们 exports={name:"新的a对象"}, 改变了它的值引用,所以上述示例中,读取的是空对象。

其次,既然导出的是 `module.exports`, 那么可以不仅仅局限于导出对象,还可以导出其他类型的变量,例如数组 函数 字符串等等

1.7 个人总结

我个人理解CommonJS是如何解决开头提到的痛点:

  1. 每个模块的变量都是独立的(因为有包装函数),输出数据包裹在对象中,来解决命名冲突的问题。
  2. 其次除了暴露的数据,内部变量私有,外部无法直接修改。
  3. 代码同步执行,配合上缓存,来解决依赖之间错综复杂的关系。

1.8 其他情况的试验

输出的数据和模块数据之间的关系?

// main.js
var a = require('./a')
console.log('a', a) // a { value: 1, obj: { a: 1 }, add: [Function: add] }

a.add()
console.log('a', a) // a { value: 1, obj: { a: 2 }, add: [Function: add] }
/* 我理解这里的导出的赋值逻辑类似于`=`; 原始数据类型赋复制的值 对象数据类型赋引用地址 */


// a.js
var value = 1
var obj = {
  a: 1,
}

function add() {
  value++
  obj.a++
}

module.exports = {
  value,
  obj,
  add,
}

2. ES Module

2.1 基础知识

Nodejs 借鉴了 Commonjs 实现了模块化 ,从 ES6 开始, JavaScript 才真正意义上有自己的模块化规范,

ES Module 的产生有很多优势,比如:

  • 借助 Es Module 的静态导入导出的优势,实现了 tree shaking
  • Es Module 还可以 import() 懒加载方式实现代码分割。

Es Module 中用 export 用来导出模块,import 用来导入模块。

2.2 基本使用


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>Documenttitle>
head>

<body>
  
  <script src="./main.js" type="module">script>
body>

html>
// main.js
import { increment, count } from './a.js'

increment()
console.log(count) // 2

// a.js
export let count = 1

export function increment() {
  count++
}

2.3 ES Module 如何运行

2.3.1 注意事项

  • ES Module 的引入和导出是静态的;

  • import 会自动提升到代码的顶层 ;

  • import , export 不能放在块级作用域或条件语句中;

  • import 的导入名不能为字符串或在判断语句;

2.3.2 如何执行

// main.js
console.log('main.js开始执行')
import say from './a'
import say1 from './b'
console.log('main.js执行完毕')

// a.js
import b from './b'
console.log('a模块加载')
export default  function say (){
    console.log('hello , world')
}

// b.js
console.log('b模块加载')
export default function sayhello(){
    console.log('hello,world')
}
解释:

1. 入口main.js
2. main.js 的 import 语句提升到顶部
3. (main文件的import say from './a')深度优先遍历,先进入到 `./a`
4. (a文件的import b from './b')深度优先遍历,先进入到 `./b`
5. 运行b.js 打印 `b模块加载`
6. 导出一个sayhello函数,返回
7. 继续执行a.js  打印 `a模块加载` 
8. 导出say函数返回
9. 继续执行main.js, (main文件的import say1 from './b') 已经引入过了,直接跳过
9. 继续执行main.js,打印 `main.js开始执行`&&`main.js执行完毕` 

输出:

b模块加载
a模块加载
main.js开始执行
main.js执行完毕
  1. 使用 import 被导入的模块运行在严格模式下。

  2. 使用 import 被导入的变量是只读的,可以理解默认为 const 装饰,无法被赋值

  3. 使用 import 被导入的变量是与原变量绑定/引用的,可以理解为 import 导入的变量无论是否为基本类型都是引用传递。

  4. 支持动态引入,import()import() 返回一个 Promise 对象, 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。

  5. ES6 模块会在程序开始前先根据模块关系查找到所有模块,生成一个无环关系图,并将所有模块实例都创建好,这种方式天然地避免了循环引用的问题,当然也有模块加载缓存,重复 import 同一个模块,只会执行一次代码。

CommonJS 总结

Commonjs 的特性如下:

  • CommonJS 模块由 JS 运行时实现。
  • CommonJS 是单个值导出,本质上导出的就是 exports 属性。
  • CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
  • CommonJS 模块同步加载并执行模块文件。

ES Module 总结

Es module 的特性如下:

  • ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
  • ES6 Module 的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果。
  • ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
  • ES6 模块提前加载并执行模块文件,
  • ES6 Module 导入模块在严格模式下。
  • ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。

优质的模块化相关的博客

  • https://juejin.cn/post/6994224541312483336

  • https://juejin.cn/post/6844904080955932680

end

  • 看了很多优质的博客,才对模块化有一个初步的认识。
  • 使用一个东西,肯定是希望能够借助它,解决我们遇到的痛点。模块化的出现,其实就是为了解决命名冲突和依赖管理。
  • 学到这里,暂时就了解了两个模块化规范。

最后提一个问题,打包工具例如 webpack 如何应对这么多种模块化规范的呢?

你可能感兴趣的:(javascript复习之旅,javascript,前端)