likes
comments
collection
share

面试准备-JS模块化

作者站长头像
站长
· 阅读数 12

关于模块化,我们今天主要来关注浏览器模块化(ES Module)跟Node.js的模块化,当然最早还有AMD跟CMD两种模块化方式,不过那个不是我们今天关注的重点,作者后续会写考古系列,到时候欢迎大家来观看监督,今天这一期可能会相对于之前的复习章节短一些。

ES Module

首先ES Module有专门对应的模块化语法,关于上的学习可以阅读阮一峰大大编写的ES6 入门教程-# Module 的语法进行学习,那么在了解完语法之后,我们需要了解一下当前语法,在浏览器端的一个兼容情况,我们下面引入一下MDN的兼容表

面试准备-JS模块化

面试准备-JS模块化

根据兼容表,如果所属项目需要兼容IE浏览器,那么是不适合直接使用ES Module标准的。

那么我们如何在浏览器端使用使用ES Module呢,首先我们先随便部署一个本地的静态资源服务

为什么需要静态资源服务,因为MDN模块介绍章节中有介绍到对应的内容,感兴趣可以点击查看

mkdir ./mysever
cd ./mysever
mkdir ./static
npm init
...这里我就略过一大堆的配置了
npm install koa koa-static

然后在./mysever中编写一个脚本app.js可以像是下面这样

const Koa = require('koa')
const path = require('path')
const koaStatic = require('koa-static')
const app = new Koa()
app.use(koaStatic(
  path.join(__dirname, './static')
))
app.listen(4000, function () {
  console.log('本地服务启动成功')
})

然后执行脚本node app.js,你就得到了一个本地的静态资源服务了,你可以在static文件夹中放置你所需要的任何时候静态资源文件,接下来我们在static文件夹中编写一个*.html文件,内容如下

<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>模块化</title>
    </head>
    <body>
      <script src="./main.js" type="module"></script>
    </body>
</html>

然后我们分别编写两个JS文件:main.js,moduleA.js

// main.js
import { a } from './moduleA.js'
console.log(a)
// moduleA.js
export const a = 1

然后我们根据本地的IP+端口+文件路径,比如我这里就是http://**.**.**.**:4000/index.html来查看我们的资源情况。

打开本地网页后我们打开控制台,可以看到数据已经按照预期打印出来了

面试准备-JS模块化

那么接下来我们要尝试几种不同的场景:

  • 多层加载
  • 相同模块的多次加载
  • 模块循环引入

多层加载

我们定义一个新的模块叫moduleB.js,其中编写内容

// moduleB.js
export default {
    desc: 'i am moduleB'
}

然后修改moduleA.js以及main.js

// moduleA.js
import bObj from './moduleB'
const a = 1
export { bObj, a }
// main.js
import { a, bObj } from './moduleA.js'
console.log(a)
console.log(bObj)

浏览器能正常的打印abObj的结果:

面试准备-JS模块化

此时我们来看一下chrome devtools network(网络)

面试准备-JS模块化

我们会发现模块文件的加载呈现瀑布状,那么也就证明存在一个问题,那就是如果我们在开发设计的过程中做了嵌套过深的模块设计,需要进行模块合并,或者模块加载过程的重新设计,比如基于我们上述场景就可以在main.js中进行moduleA.jsmoduleB.js的引入,而不是通过moduleA.js,如下图。

面试准备-JS模块化

相同模块的多次加载

接着我们尝试单一模块的多次引入,基于上次优化后的结果,在moduleA.js引入moduleB.js的同时,main.js也引入moduleB.js,那么此时我们来看看网络情况,此时的网络如下

面试准备-JS模块化

我们会发现当前的网络情况跟上一张加载图的情况基本一致,那么实际上我们可以假设,我们基于每个模块的引入,是存在记录以及判断的,它也许是一个这样的结构(假设结构)

enum ModuleRecordsStatus {
    unlinked = 'unlinked', // 未连接
    linking  = 'linking',  // 链接中
    linked   = 'linked'    // 已连接
    ...
}
interface ModuleRecords {
   Status: ModuleRecordsStatus
}
interface ModuleMap {
    [x: string]: ModuleRecords
}
// 下方记录信息
const globalModuleMap: ModuleMap = {
    ...
}

针对每个模块的加载我们假设浏览器会把模块进行记录,当多个模块需要同一个模块时,会对其进行状态判断以及评估执行

那么这里是否可以利用这个点,针对有较多模块依赖的文件进行提前加载呢?,那么此时我们声明模块moduleC.js,并且在A跟B中都引入了当前模块,我们先来看看效果

面试准备-JS模块化

跟我们预期一致,呈现串行加载,那么我们把模块C放到*.html文件中,新增一个script标签。然后再次来看看网络路径

面试准备-JS模块化

OK那么我们发现提前进行进行预加载时可行的,那么我们在后续的项目过程中就可以使用这个方式来对项目的文件加载路径进行优化(基于ES Module开发的项目)

模块循环引入

首先我们来修改我们的代码,html文件保持引入main.js删除针对模块C的引入, 分别对各个文件进行如下代码编写

// main.js
import * as moduleA from './moduleA.js'
console.log(moduleA)

// moduleA.js
import moduleB from './moduleB.js'
import moduleD from './moduleD.js'
const desc = 'i am moduleA'
console.log('some body call me A')
export { moduleB, moduleD, desc }

// moduleB.js
import moduleC from './moduleC.js'
console.log('some body call me B')
export default {
  desc: 'i am moduleB',
  moduleC
}
// moduleC.js
import * as moduleA from './moduleA.js'
console.log('some body call me C')
export default {
  desc: 'i am moduleC',
  moduleA
}

新增模块

// moduleD.js
console.log('some body call me D')
export default {
  desc: 'i am moduleD'
}

我们做完如上准备,那么我们刷新一下浏览器,查看一下我们的控制台

面试准备-JS模块化

同时看一下网络

面试准备-JS模块化 OK 那么我们基于以上内容来进行假设推理,首先整体过程分为两个阶段,分别是 加载阶段执行阶段

  • 加载过程

个人理解这个过程主要做的事情就是根据入口模块,进行解析,每个并针对每个模块的所依赖的文件进行加载,也就是说是一个类似于 广度优先(BFS) 的过程,每检查到一个模块需要拉取就会及时修改状态,防止重复拉取以及循环拉取。这点我们可以根据网络的情况推断出来

  • 执行过程

当模块整体的依赖树构建完成以后,开始进入评估执行过程,那么根据console.log(...)的执行情况。我们会发现,实际的执行过程中更像是一个 深度优先(DFS) 的算法,他会先跟据调用路径从A->B->C->A(重复)然后从最后一位开始进行调用,但是由于A出现过了,所以需要剔除A那么就会从C模块开始进行执行。然后到C模块一直到A模块。然后再由创建调用路径A->DD模块一直到A模块,清空依赖后,就开始执行A模块本身的代码。 那么为什么最后我们可以在获取到一个循环引用的对象呢。 我个人是这么假设的,实际上在执行过程前(是不是在加载过程中或者加载过程前这个我不太清楚)。有一个阶段已经对所有的模块进行了声明并且针对模块的import以及export语句进行了解析,生成了一个引用对象。然后到了代码的实际执行阶段的时候很对引用对象进行赋值。那么最终我们可以得到一个循环引用的对象

虽然我们假设知道执行顺序以及过程,但是希望大家在实际的使用过程中更加的规范,针对模块间的操作尽可能的不要依赖于执行顺序,这样不仅容易产生问题,而且不易理解

Node.js的模块化

首先node.js的是同时支持CommonJS 以及 ES Module标准的,不过我们在日常的使用过程中,大部分都是使用的CommonJS的内容进行学习。

那么首先我们需要了解一下node.js是如何区分使用模块加载系统,我们看一下官网

面试准备-JS模块化

也就是说通常情况下,我们在node中进行模块引入,都是使用的CommonJS的。

模块语法

// 引入模块
const moduleA = require('./moduleA')

// do some thing

// 导出模块,以下方式都是可以的(只接受同步导出)
exports.a = 1
module.exports.a = 2
module.exports = {
    a: 1
}
// 但是千万不能是下面这种,打灭
exports = {
    a: 1
}

为什么不能使用最后一种方式进行导出呢?因为实际上每个JS文件执行期前都是被模块封装器所封装的,如下所示:

(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});

然后我们的exports实际上是针对module.exports的引用,如果我们重新给exports赋值,那么引用指向就发生变更,最终导致导出失败

循环引用

我们尝试进行了循环引用,得到了类似浏览器的模块化循环引用的执行结果,大家可以返回上面去看看

最后我们今天的模块化讲解就大致的结束了,欢迎大家留言来说明自己还想要看什么样的内容,我也会在后续进行补充或者开辟新的文章跟大家一起学习准备

完美!

下一章:面试准备-JS worker