【记一忘三二】模块基础

177 阅读1分钟

起因

起初没有模块化概念之前,编写多个js文件时,必须按照顺序引入所需要的js文件

<script src="jqery.js"></script>
<script src="./a.js"></script>
<script src="./b.js"></script>

这样的缺点是

  • 全局污染,每个js文件都会向全局作用域外暴露自己的变量和函数。比如:jqery.js会在全局环境暴露$jqery函数,a.js也在全局中暴露同样名称的函数,覆盖了jqery.js全局函数,那么b.js就无法在访问到jqery.js暴露的函数
  • 加载顺序,因为每个模块的执行存在关联性,并且执行和加载js模块都会阻塞js执行线程,所以js的加载顺序必须固定,比如:a.js需要访问jqery.js的方法,那么jqery.js一定要在a.js前面加载运行

闭包

js中,不止全局作用域,还有函数作用域,通过闭包的方式减少向外暴露的变量和方法数量

// a.js
(function (win) {
  win.aModule = {
    data: [1, 2, 3],
  };
})(window);
​
// b.js
(function (win, aModule) {
  win.bModule = {
    data: aModule.data.concat(5, 6),
  };
})(window, window.aModule);

全局污染得到缓解,但是加载顺序的问题还是无法解决

commonJS

node提供的一套模块化方案,这里需要注意require加载是同步的,也就是在运行到require代码时,才会去加载其他包,并且执行也是同步的,但是这里需要注意每个模块式会被加载一次,以后加载运行都是直接使用缓存

// a.js
module.exports = {
  data: [1, 2, 3],
};
​
// b.js
const { data } = require("./a.js");
​
module.exports = {
  data: data.concat([5, 6]),
};
​
// index.js
const aModule = require("./a.js");
const bModule = require("./b.js");
​
console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]

解决了全局污染和加载顺序的问题,那commonJS可以使用在浏览器端吗?

显然时不能的,在使用require记载模块时会阻塞整个程序,在node端完全不是问题,这是因为运行在后端,但是在浏览器端肯定时不行的,如果浏览器加载很多模块,那白屏或者浏览器卡死的时间就会非常的长,而且网速还是不可预知的

AMD

AMD规范其实是仿照commonJS来的,上面说过之所以在浏览器不能直接使用commonJS规范,是因为commonJS是同步加载同步运行的,但是在浏览器端实现同步加载时不现实的

那么AMD案件出现了,他提出了异步加载并,已经是先加载好每个模块并且执行回去模块值之后,再去运行业务代码

// a.js 
define(function () {
  return {
    data: [1, 2, 3],
  };
});
​
// b.js
define(["a"], function (aModule) {
  return {
    data: aModule.data.concat([5, 6]),
  };
});
​
​
// index.js
require.config({
  paths: {
    a: "./a",
    b: "./b",
  },
});
​
require(["a", "b"], function (aModule, bModule) {
  console.log(aModule.data); // [1, 2, 3]
  console.log(bModule.data); // [1, 2, 3, 5, 6]
});
​
​
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
    <script src="index.js"></script>
</body>
</html>

实现一个AMD其实也很简单,就是先根据配置加载模块并且运行(require.config.paths就是预先加载模块的配置),在去执行业务代码的回调函数

CMD

上面在讲AMD提到一个现象,就是AMD在加载模块时,还会先运行先运行模块里面的代码,得到模块值,也就是提前执行、依赖前置

CMD觉得可以提前加载模块,但是不应该提前运行模块代码,而应该在使用到这个模块的时候才运行模块代码,也就是延迟执行、依赖就近

但是这里需要注意,无论是AMD还是CMD,都需要提前加载模块,区别在于什么时候执行模块

AMD是通过执行查找加载模块,而在CMD因为不会预先执行,就会通过正则匹配的方式加载模块

// a.js
define(function () {
  return {
    data: [1, 2, 3],
  };
});
​
// b.js
define(function (require) {
  const aModule = require("./a.js");
  return {
    data: aModule.data.concat([5, 6]),
  };
});
​
// index.js
define(function (require) {
  const aModule = require("./a.js");
  const bModule = require("./b.js");
  console.log(aModule.data); // [1, 2, 3]
  console.log(bModule.data); // [1, 2, 3, 5, 6]
});
​
<!-- index.html --><!DOCTYPE html>
<html lang="en"><head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head><body>
    <script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
    <script type="text/javascript">
        seajs.use('./index.js');
    </script>
</body></html>

自定义模块

引入路径

导入自定义模块必须使用相对路径,以./../开头

const name = "李白"
const age = 18
const running = function () {
    console.log("我是" + this.name + ",我跑的很快");
}
​
module.exports = {
    name,
    age,
    running
}
const my = require("./my.js")
​
console.log(my.name);
my.running()

省略后缀

在导入后缀为 jsjson文件时,可以省略省略后缀

const my = require("./my")
const config = require("./config")
​
console.log(my.name);
my.running()
​
console.log(config.basePath);
​

但是在引入在同名的 jsjson文件时,省略后缀会优先引入 js 文件

在导入 C++ 书写的 node后缀文件也可以省略后缀

导入文件的执行方式

JS文件

正常执行,需要 module.exports 语句导出数据

// my.js
const name = "李白"
const age = 18
const running = function () {
    console.log("我是" + this.name + ",我跑的很快");
}
​
module.exports = {
    name,
    age,
    running
}
const my = require("./my.js")
​
console.log(my);  // { name: '李白', age: 18, running: [Function: running] }
console.log(my.name); // 李白
JSON文件

不需要 module.exports 语句导出数据,并且会自动转为对象

{
    "basePath":"./module"
}
const config = require("./config.json")
​
console.log(config) // { basePath: './module' }
console.log(config.basePath);  // ./module
其他后缀或无后缀

当做 JS 文件进行执行,比如 .babel.eslintrc

// .babel
module.exports = {
    presets: [
        ["@babel/preset-env"]
    ]
}
const babel = require("./.babel")
​
console.log(babel) // { presets: [ [ '@babel/preset-env' ] ] }

导入文件夹模块执行流程

image.png esModuelcommonJS 规范导入模块搜索顺序完全一致

esModuelcommonJS模块导出字段是不一样

引入模块时入口文件查找顺序

如果文件夹没有package.json配置或者package.json没有相对应的入口配置,就会使用当前文件夹index.js或者index.ts

main:一般常用的入口,也是保底入口,如果没有其他入口配置就会根据这个入口的配置,在浏览器和node中都可以使用

modlueEsModule规范的入口

browser:浏览器环境的入口

  • 浏览器环境使用import browser > modlue > main > 默认
  • node端使用requiremain > 默认

exports

node在16版本之后提出新的的入口配置,会直接覆盖前面的入口配置

{
  "main": "index.main.ts",
  "module": "index.module.ts",
  "browser": "index.browser.ts",
  "exports":{
    ".":{
      "require": "./index.module.ts"
    }
  },
}

比如说,上面的配置,在浏览器使用import导包,exports选项没有import入口配置,虽然有mainmodulebrowser选项,但是都不会采用,会直接使用默认入口

"exports":{
    ".":{
        "require": "./index.module.ts"
    },
    "./style":{
        "import":"./style.import.css",
        "require":"./style.require.css"
    }
},

exports还可以配置其他目录引入的情况,但需要主要,其他目录引入的规则,必须包在node_modules才有效

简要源码

const path = require("node:path")
const fs = require("node:fs")


const cacheModule = new Map()
function myRequire(filePath) {

    // 1. 拼接文件模块绝对路径
    const absolutePath = path.resolve(__dirname, filePath)

    // 2. 检查缓存是否存在模块结果
    if(cacheModule.has(absolutePath)){
        return cacheModule.get(absolutePath)
    }

    // 3. 读取文件内容
    const code = fs.readFileSync(absolutePath).toString()

    // 4. 包装函数
    const wrapper = Function('global', 'require', 'module', '__filename', '__dirname', `
        const exports = module.exports;
        ${code};
        return module.exports;
    `)

    // 5. 执行包装函数
    const module = {
        exports: {}
    }
    const global = {}
    const result = wrapper(global, myRequire, module, absolutePath, path.dirname(absolutePath))

    // 6. 缓存模块结果
    cacheModule.set(absolutePath, result)

    // 7. 返回模块结果
    return result
}


const my = myRequire("./my.js")

esModel

上面介绍的都是民间开发自己研究出来的,并不具有系统性、规范性

所以在Es6时,Es规范提出了esModel规范

esModelcommonJS一样使用同步加载依赖包,和commonJS不同的是返回值一个引用地址

// a.js
const data = [1, 2, 3];

export default {
  data,
};

//b.js
import aModule from "./a.js";

const data = aModule.data.concat([4, 5]);

export default {
  data,
};

// index.js
import aModule from "./a.js";
import bModule from "./b.js";

console.log(aModule.data); // [1, 2, 3]
console.log(bModule.data); // [1, 2, 3, 5, 6]
<!-- index.html -->

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

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

</html>