模块化开发

个人主页:爱吃炫迈
系列专栏:前端工程化
‍座右铭:道阻且长,行则将至

文章目录

  • 模块化的历史
  • 没有模块化带来的问题
  • CommonJS
    • CommonJS和Node关系
    • 模块化案例
    • exports导出
    • module.exports导出
    • require导入
    • CommonJS规范缺点
  • 模块加载过程
  • ES Module
    • export关键字
    • import关键字
    • export和import结合使用
    • default默认导出
    • ES Module的解析流程


# 什么是模块化

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

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

模块化的历史

  1. 在网页开发的早期,JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或者动画实现等,那个时候代码还是很少的,我们只需要将JavaScript代码写到

没有模块化带来的问题

  1. 命名冲突问题

举个栗子:

//A.js
var name = "jack"
console.log(name)
//B.js
var name = "belle"
console.log(name)
//index.html
<body>
    <script src="./A.js"></script>
    <script src="./B.js"></script>
</body>

解决方案:立即函数调用表达式

因为是函数,函数有自己的作用域。两个name没有关系,就不会产生命名冲突的问题

//A.js
const moduleA = (function() {
  var name = "jack"
  console.log(name)  
  
  return {
      name,
  }
}())
//B.js
const moduleB = (function() {
  var name = "belle"
  console.log(name)
  
  return {
      name,
  }
}())

这又带来了新的问题:

  • 我必须记得每一个模块中返回对象的命名,才能在其他模块使用过程中正确的使用

  • 代码写起来混乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写

  • 在没有合适的规范情况下,每个人、每个公司都可能会任意命名,甚至出现模块名称相同的情况

  1. 文件只能按照

CommonJS

CommonJS是一个规范,最初提出来是在浏览器以外的地方使用

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

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

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

CommonJS和Node关系

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

  • 在Node中每一个JS文件都是一个单独的模块,这个模块中包括CommonJS规范的核心变量:exportsmodule.exportsrequire,我们可以使用这些变量来方便的进行模块化开发。
  • exportsmodule.exports可以负责对模块中的内容进行导出
  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容

模块化案例

模块化开发_第1张图片

exports导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出

exports.name = name
exports.age = age
exports.sayHello = sayHello

main.js文件中可以导入:

const bar = require('./bar')

解释:

这行代码使得main中的bar变量等于exports对象,也就是require通过各种查找方式,最终找到了exports这个对象,并且将这个exports对象赋值给了bar变量,即bar变量就是exports对象了。

内存图:

模块化开发_第2张图片

module.exports导出

写法:

module.exports.name = name
module.exports.age = age
module.exports.sayHello = sayHello

在Node中我们经常用module.exports导出东西,那为什么exports对象也可以导出呢?

CommonJS中是没有module.exports概念的,但是为了实现模块的导出,Node中使用的是Module的类,每一个模块都是Module的一个实例,也就是module对象,所以在Node中真正用于导出的根本不是exports,而是module.exports,因为module才是导出的真正实现者。那么为什么exports也可以导出呢,这是因为module对象的exports属性exports对象的一个引用,也就是说,module.exports = exports = main中的bar

模块化开发_第3张图片

开发中常用的写法:

module.exports = {
    name,
    age,
    sayHello
}

模块化开发_第4张图片

此时module对象指向一个新的对象,此时如果再使用exports导出是没有用的。

require导入

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

require查找规则:

导入格式如下:require(X)

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

    • 直接返回核心模块,并且停止查找
  2. 情况二:X是以./ 或 …/ 或 /(根目录)开头的

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

      • 如果有后缀名,按照后缀名的格式查找对应的文件
      • 如果没有后缀名,会按照如下顺序:
        • 直接查找文件X
        • 查找X.js文件
        • 查找X.json文件
        • 查找X.node文件
    • 第二步:没有找到对应的文件,将X作为一个目录

      • 查找目录下面的index文件
        • 查找X/index.js文件
        • 查找X/index.json文件
        • 查找X/index.node文件
    • 如果没有找到,那么报错:not found

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

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

模块化开发_第5张图片

  • 如果上面的路径中都没有找到,那么报错:not found

CommonJS规范缺点

  1. CommonJS加载模块是同步的

同步的意味着只有等到对应的模块加载完毕,当前模块中的内容才能被运行,这个在服务器不会有什么问题,因为服务器加载的js文件都是本地文件,加载速度非常快。

  1. 如果将它应用于浏览器

浏览器加载js文件需要先从服务器将文件下载下来,之后再加载运行,那么采用同步就意味着后序的js代码都无法正常运行,即使是一些简单的DOM操作。

  1. 所以在浏览器中,我们通常不使用CommonJS规范

当然在webpack中使用CommonJS是另外一回事,因为它会将我们的代码转成浏览器可以直接执行的代码。

  1. 在早期为了可以在浏览器中使用模块化,通常会采用AMD或者CMD

但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助webpack等工具可以实现对CommonJS或者ES Modules代码的转换,所以AMD和CMD已经使用非常少了。

模块加载过程

  1. 模块在被第一次引入时,模块中的js代码会被运行一次
  2. 模块被多次引入时,会缓存,最终只加载(运行)一次,之后,缓存起来的导出对象被重复利用。

因为每个模块对象module都有一个loaded属性,为false表示还没有加载,为true表示已经加载

  1. 如果有循环引入,那么加载顺序是深度优先算法

模块化开发_第6张图片

Node采用的是深度优先算法:main -> aaa -> ccc -> ddd -> eee ->bbb

ES Module

ES Module和CommonJS的不同:

一方面它使用了import和export关键字

另一方面它采用编译期的静态分析,并且也加入了动态引用的方式

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

export负责将模块内的内容导出;

import负责从其他模块导入内容;

export关键字

export关键字将一个模块中的变量、函数、类等导出

  1. 方式一:将所有需要导出的标识符,放到export后面的{}中
export {
    name,
    age,
    sayHello
}

  1. 方式二:导出时通过as关键字给标识符起一个别名
export {
    name as myname,
    age,
    sayHello
}
  1. 方式三:在语句声明的前面直接加上export关键字
export const name = "kobe"
export const age = 18
export function sayHello() {
    console.log("sayHello")
}

import关键字

import关键字负责从另外一个模块中导入内容

  1. 方式一:import{标识符列表} from ‘模块’
import {name, age, sayHello} from "./bar.js"
  1. 方式二:通过as关键字给标识符起别名
import {name as myname, age, sayHello} from "./bar.js"
  1. 方式三:通过*将模块功能放到一个模块功能对象上(导入时给整个模块起别名)
import * as bar from "./bar.js"

export和import结合使用

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

import {sayHello, sayNo} from './bar.js'
import {sayHi} from './foo.js'

export {
   sayHello,
   sayNo,
   sayHi
}

优化:

export {sayHello, sayNo} from './bar.js'
export {sayHi} from './foo.js'

default默认导出

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

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

  • 它也方便我们和现有的CommonJS等规范相互操作;

  • 在一个模块中,只能有一个默认导出(default export)

  1. 方式一:默认导出
export default sayHello
  1. 方式二:定义标识符直接作为默认导出
export default function() {
    return ["hello"]
}

导入

import abc from './bar.js'

ES Module的解析流程

模块化开发_第7张图片

  1. 阶段一:构建,根据地址查找js文件,并且下载,将其解析成模块记录

模块化开发_第8张图片

  1. 阶段二:实例化,对模块记录进行实例化,并且分配内存空间,解析模块的导入和导出语句,把模块指向对应的内存地址

模块化开发_第9张图片

  1. 运行,运行代码,计算值,并且将值填充到内存地址中

你可能感兴趣的:(前端工程化,前端,javascript)