彻底明白前端模块化

打印 上一主题 下一主题

主题 837|帖子 837|积分 2511

模块化是一种处置惩罚复杂系统分解成为更好的可管理模块的方式,模块化开辟最终的目标是将步调划分成一个个小的结构


  • 这个结构中编写属于自己的逻辑代码,有自己的作用域,界说变量名词时不会影响到其他的结构
  • 可以将自己希望袒露的变量、函数、对象等导出给其结构使用
  • 可以通过某种方式,导入别的结构中的变量、函数、对象等
引入

历史



  • 在网页开辟的早期,Brendan Eich开辟JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的,只需要将JavaScript代码写到<script>标签即可,没有必要放到多个文件中来编写
  • 随着前端和JavaScript的快速发展,JavaScript代码变得越来越复杂了

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

  • 所以,模块化已经是JavaScript一个非常迫切的需求:

    • 但是JavaScript本身,直到ES6(2015)才推出了自己的模块化方案
    • 在此之前,为了让JavaScript支持模块化,社区出了很多不同的模块化规范:AMD、CMD、CommonJS等

问题

早期没有模块化带来了很多的问题:比如定名辩说的问题,我们是使用 立刻函数调用表达式(IIFE) 来办理的,但也会有其他问题:


  • 必须记得每一个模块中返回对象的定名,才能在其他模块使用过程中正确的使用
  • 代码写起来杂乱不堪,每个文件中的代码都需要包裹在一个匿名函数中来编写
  • 在没有合适的规范情况下,每个人都大概会任意定名、甚至出现模块名称雷同的情况
需要制定一定的规范来约束每个人都按照这个规范去编写模块化的代码,JavaScript社区为相识决上面的问题,涌现出一系列好用的规范,接下来我们逐一学习
CommonJS

CommonJS 是一种模块系统规范,告急用于在服务器端情况(如 Node.js)中管理模块。它提供了模块的界说、加载、导出机制,答应开辟者在不同模块之间共享代码。在 Node.js 中,CommonJS 是默认的模块系统,虽然现在 Node.js 也支持 ECMAScript 模块,但 CommonJS 仍旧广泛使用


  • 最初提出来是在欣赏器以外的地方使用,并且当时被定名为ServerJS,后来为了表现它 的广泛性,修改为CommonJS,平时也会简称为CJS
  • Node是CommonJS在服务器端一个具有代表性的实现,Node中对CommonJS举行了支持和实现
  • Browserify库是CommonJS在欣赏器中的一种实现
  • webpack打包工具具备对CommonJS的支持和转换
  • 在Node中每一个js文件都是一个单独的模块
  • 这个模块中包罗CommonJS规范的核心变量:exports、module.exports、require,可以使用这些变量来方便的举行模块化开辟

    • exports和module.exports可以负责对模块中的内容举行导出
    • require函数可以帮助我们导入其他模块(自界说模块、系统模块、第三方库模块)中的内容

exports导出

exports是一个对象,我们可以在这个对象中添加很多个属性,添加的属性会导出
  1. // a.js
  2. function add(num1, num2) {
  3.   return num1 + num2;
  4. }
  5. const message = "hello world";
  6. console.log(exports); // {}
  7. exports.add = add;
  8. exports.message = message;
  9. // main.js
  10. // const { add, message } = require("./a"); // 可以拿到文件中导出的exports对象,相当于引用赋值
  11. // console.log(add(10, 30)); // 40
  12. // console.log(message); // hello world
  13. const a = require("./a");
  14. console.log(a.add(10, 30)); // 40
  15. console.log(a.message); // hello world
复制代码
上面代码原理:

exports是一个对象,在内存中就会有个对象地址比如是0x100,那么exports就指向这个引用地址
当执行const a = require("./a")时require就会找到a模块导出的exports,把exports的引用地址赋值给a,a和exports指向了同一个对象
也就意味着你在main.js两秒后修改了message的值,两秒后在a.js中获取时会酿成你新改的值
module.exports导出

  1. // b.js
  2. function add(num1, num2) {
  3.   return num1 + num2;
  4. }
  5. const message = "hello world";
  6. // 方式一
  7. // module.exports.add = add;
  8. // module.exports.message = message;
  9. // console.log(module.exports === exports); // true
  10. // 方式二:开发中常用,module.exports赋值新对象更灵活方便
  11. module.exports = {
  12.   add,
  13.   message,
  14. };
  15. // main.js
  16. const b = require("./b");
  17. console.log(b.add(10, 20)); // 30
  18. console.log(b.message); // hello world
复制代码
上面代码原理:

module.exports和exports有什么关系呢?


  • exports 是 module.exports 的简写,起初它们都指向同一个对象的引用地址
  • module.exports = exports = main.js中引入的变量
我们开辟中常用的是module.exports,而且他俩还相等,那有exports另有什么意义那?


  • CommonJS中是没有module.exports的概念的,但Node要实现commonjs尺度所以有exports,并且让exports = module.exports
  • 但为了实现模块的导出,Node中使用的是Module的类,每⼀个模块都是Module的⼀个实例也就是module
  • 所以在Node中真正⽤于导出的实在不是exports,使用require导入时查找的本质也不是exports,而是module.exports
  • 也就是说module.exports可以通过赋值为一个新对象导出,但exports不行,由于改变了exports的引用没有效,node中找的还是module.exports
require导入

我们已经知道,require是⼀个函数,可以帮助我们引⼊⼀个⽂件(模块)中导出的对象
require的查找规则是怎么样的呢?导⼊格式如下:require(X)


  • X是⼀个Node核⼼内置模块,⽐如path、http:直接返回核⼼模块,并且停⽌查找
    1. console.log("path:", require("path"));
    2. console.log("http:", require("http"));
    复制代码

  • X是以 ./ 或 ../ 或 /(根⽬录)开头的

    • 第⼀步:将X当做⼀个⽂件在对应的⽬录下查找

      • 直接查找⽂件X
      • 查找X.js⽂件
      • 查找X.json⽂件
      • 查找X.node⽂件

    • 第⼆步:没有找到对应的⽂件,将X作为⼀个⽬录:查找⽬录下⾯的index⽂件

      • 查找X/index.js⽂件
      • 查找X/index.json⽂件
      • 查找X/index.node⽂件

    • 如果没有找到,那么报错:not found


  • 直接是⼀个X(没有路径),并且X不是⼀个核⼼模块

    • 我们可以看到它是会报错的

    • 引入的hello我们可以在目录下建个node_modules内里再建个hello文件夹并包含index.js入口文件,这时require就可以找到了,这也是npm install 依赖 下载依赖的原理,那么axios我们就可以用npn install 下载

    • 那么它的查找规律就是会先在当前目录的node_modules文件夹(必须有入口文件)中探求
    • 没有找到的话,会再到上一级目录的node_modules文件夹中探求,直到找到根目录还没有就会报错

加载过程



  • 模块在被第一次引入时,模块中的js代码会被运行一次,这个我们在上面的练习中就能发现
  • 模块被多次引入时,会缓存,最终只加载(运行)一次

    • 这是由于每个模块对象module都有一个属性loaded记载是否被加载过,默以为false

  • 如果有循环引入,那么加载序次是什么?

    • 这个实在是一种数据结构:图结构
    • 图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)和广度优先搜索(BFS, breadth first search)
    • Node采用的是深度优先算法(在一层内里走到底):main -> aaa -> ccc -> ddd -> eee ->bbb


缺点



  • CommonJS加载模块是同步的

    • 意味着只有比及对应的模块加载完毕,当前模块中的内容才能被运⾏
    • 在服务器不会有什么问题,由于服务器加载的js⽂件都是当地⽂件,加载速率⾮常快

  • 如果将它⽤于欣赏器呢?

    • 欣赏器加载js⽂件需要先从服务器将⽂件下载下来,之后再加载运⾏
    • 那么采⽤同步的就意味着后续的js代码都⽆法正常运⾏,纵然是⼀些简单的DOM操作
    • 在欣赏器中,我们通常不使⽤CommonJS规范,在webpack中使⽤CommonJS是别的⼀回事

  • 在早期为了可以在欣赏器中使⽤模块化,通常会采⽤AMD或CMD

    • ⽬前⼀⽅⾯现代的欣赏器已经支持ES Modules
    • 另⼀⽅⾯借助于webpack等⼯具可以实现对CommonJS或者ES Module代码的转换
    • AMD和CMD已经使⽤⾮常少了

AMD规范(根本不消)

AMD告急是⽤于欣赏器的⼀种模块化规范


  • AMD是Asynchronous Module Definition`(异步模块界说)的缩写,⽤的是异步加载模块
  • 事实上 AMD的规范还要早于CommonJS,但是CommonJS⽬前依然在被使⽤,⽽AMD使⽤的较少了
  • 规范只是界说代码的应该怎样去编写,需有了详细的实现才能被应⽤,AMD实现的⽐较常⽤的库是require.js和curl.js
require.js使⽤



  • 下载require.js:下载地址:https://github.com/requirejs/requirejs 找到此中的require.js⽂件
  • 界说HTML的script标签引⼊require.js和界说⼊⼝⽂件:

    • <script src="./lib/require.js" data-main="./index.js"></script>
    • data-main属性的作⽤是在加载完src的⽂件后会加载执⾏该⽂件

CMD规范(根本不消)

CMD规范也是⽤于欣赏器的⼀种模块化规范:


  • CMD 是Common Module Definition(通⽤模块界说)的缩写
  • ⽤的也是异步加载模块,但是它将CommonJS的长处吸收了过来,但是⽬前CMD使⽤也⾮常少了
  • CMD也有⾃⼰⽐较优秀的实现⽅案:SeaJS
SeaJS的使⽤



  • 下载SeaJS:下载地址:https://github.com/seajs/seajs 找到dist⽂件夹下的sea.js
  • 引⼊sea.js和使⽤主⼊⼝⽂件:seajs是指定主⼊⼝⽂件的
ES Module

JavaScript没有模块化⼀直是它的痛点,所以才会产⽣我们前⾯学习的社区规范:CommonJS、AMD、CMD等,所以在 ECMA 推出⾃⼰的模块化系统时,⼤家也是很兴奋


  • ES Module模块采用 export负责将模块内的内容导出import负责从其他模块导入内容来实现模块化
  • ES Module模块答应编译器在编译时举行静态分析,也加入了动态引用的方式
  • 使用ES Module将主动采用严格模式:use strict
简单使用



  • 在欣赏器中,ES Modules 通过 <script type="module"> 标签引入,来声明这个脚本是一个模块
    1. <!DOCTYPE html>
    2. <html lang="en">
    3.   <head>
    4.     <meta charset="UTF-8" />
    5.     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    6.     <title>Document</title>
    7.   </head>
    8.   <body>
    9.     <script src="./main.js" type="module"></script>
    10.   </body>
    11. </html>
    复制代码

    但欣赏器打开当地文件会报错,这个在MDN上面有给出解释:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules#其他模块与尺度脚本的不同
    需要留意当地测试:如果你通过当地加载 HTML 文件(比如一个 file:// 路径的文件),你将会遇到 CORS 错误,由于 JavaScript 模块安全性需要。你需要通过一个服务器来测试
    这里使用的VSCode插件:Live Server,可以执行并打印main.js代码
  • 在 Node.js 中使用 ES Modules(ESM)可以通过以下几种方式实现:

    • 如果你的模块文件使用 .mjs 扩展名,Node.js 会将其辨以为 ES Module
    1. // example.mjs
    2. export const greeting = "Hello, World!";
    3. export function sayHello() {
    4.   console.log(greeting);
    5. }
    6. // main.mjs
    7. import { sayHello } from './example.mjs';
    8. sayHello(); // 输出: Hello, World!
    复制代码
      

    • 如果你希望在整个项目中使用 ES Modules,而不仅仅是单个文件,可以在 package.json 文件中添加 "type": "module",所有 .js 文件都将被视为 ES Modules
    1. // package.json
    2. {
    3.   "name": "my-project",
    4.   "version": "1.0.0",
    5.   "type": "module"
    6. }
    7. // example.js
    8. export const greeting = "Hello, World!";
    9. export function sayHello() {
    10.   console.log(greeting);
    11. }
    12. // main.js
    13. import { sayHello } from './example.js';
    14. sayHello(); // 输出: Hello, World!
    复制代码

export关键字

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


  • 方式一:想导出谁就在语句声明的前面直接加上export关键字
  • 方式二:想导出谁则将需要导出的标识符,放到export后面的 {} 中

    • 留意:这里的 {} 内里不是ES6的对象字面量的增强写法,{} 也不是表示一个对象, export { message: message } 是错误的写法;

  • 方式三:在方式二导出时给标识符起一个别名
  1. // 方式一
  2. export const message1 = "hello world1";
  3. export function add1(num1, num2) {
  4.   return num1 + num2;
  5. }
  6. export class Person1 {
  7.   constructor(name) {
  8.     this.name = name;
  9.   }
  10. }
  11. // 方式二
  12. const message2 = "hello world2";
  13. function add2(num1, num2) {
  14.   return num1 + num2;
  15. }
  16. class Person2 {
  17.   constructor(name) {
  18.     this.name = name;
  19.   }
  20. }
  21. export { message2, add2, Person2 };
  22. // 方式三
  23. const message3 = "hello world3";
  24. function add3(num1, num2) {
  25.   return num1 + num2;
  26. }
  27. class Person3 {
  28.   constructor(name) {
  29.     this.name = name;
  30.   }
  31. }
  32. export { message3, add3 as add0, Person3 as Person0 };
复制代码
import关键字

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


  • 方式一:import { 标识符列表 } from '模块'

    • 留意:这里的 {} 也不是一个对象,内里只是存放导入的标识符列表内容

  • 方式二:通过as关键字在导入时给标识符起别名
  • 方式三:通过 * as 自己名字 将模块功能放到一个模块功能对象上
  1. // 结合export中的代码学习
  2. import {
  3.   message1, // 方式一
  4.   message2,
  5.   message3,
  6.   add0 as add3, // 方式二
  7.   add1,
  8.   add2,
  9.   Person0 as Person3,
  10.   Person1,
  11.   Person2,
  12. } from "./a.js";
  13. import * as a from "./a.js"; // 方式三
  14. console.log(
  15.   message1,
  16.   message2,
  17.   message3,
  18.   add1,
  19.   add2,
  20.   add3,
  21.   Person1,
  22.   Person2,
  23.   Person3,
  24.   a.message1,
  25.   a.message2,
  26.   a.message3,
  27.   a.add1,
  28.   a.add2,
  29.   a.add0,
  30.   a.Person1,
  31.   a.Person2,
  32.   a.Person0
  33. );
复制代码
export和import结合

在开辟和封装一个功能库时,通常希望将袒露的所有接口放到一个文件中,这样方便指定统一的接口规范也方便阅读,这个时候就可以使用export和import结合使用
  1. /* util/index 通常是不编写逻辑的,在这里统一导入并导出 */
  2. // 方式一
  3. import {
  4.   message1,
  5.   message2,
  6.   message3,
  7.   add0 as add3,
  8.   add1,
  9.   add2,
  10.   Person0 as Person3,
  11.   Person1,
  12.   Person2,
  13. } from "./a.js";
  14. import { getData } from "./b.js";
  15. export {
  16.   message1,
  17.   message2,
  18.   message3,
  19.   add3,
  20.   add1,
  21.   add2,
  22.   Person3,
  23.   Person1,
  24.   Person2,
  25.   getData,
  26. };
  27. // 方式二:结合
  28. export {
  29.   message1,
  30.   message2,
  31.   message3,
  32.   add0 as add3,
  33.   add1,
  34.   add2,
  35.   Person0 as Person3,
  36.   Person1,
  37.   Person2,
  38. } from "./a.js";
  39. export { getData } from "./b.js";
  40. // 方式三:建议当有相应的文档时再这样写
  41. export * from "./a.js";
  42. export * from "./b.js";
复制代码
default⽤法

前面学习的导出都是有名字的导出(named exports):在导出export时指定了名字,在导入import时需要知道详细的名字,文件只有一个想要导出的内容并且文件名已经概括时就可以使用默认导出(default export)


  • 默认导出:在一个模块中,只能有一个默认导出

    • 默认导出export时可以不指定名字
    • 在导入时不需要使用 {},并且可以自己来指定名字
    • 也方便我们和现有的CommonJS等规范相互操作
    1. /* validMobile.js */
    2. // 方式一
    3. // function validMobile(str) {
    4. //   const reg = /^1[3-9]\d{9}$/;
    5. //   return reg.test(str);
    6. // }
    7. // export default validMobile;
    8. // 方式二
    9. export default (str) => {
    10.   const reg = /^1[3-9]\d{9}$/;
    11.   return reg.test(str);
    12. };
    13. /* main.js */
    14. import validMobile from "./validMobile.js";
    15. console.log(validMobile("12345678910")); // false
    复制代码

import函数

import 是 静态的,这意味着在编译或打包阶段,模块依赖关系就已经确定了。JavaScript 引擎需要在脚本开始执行之前分析所有的模块和依赖项,以便优化打包、代码分割、死代码消除等操作。如果需要根据不同的条件,动态来选择加载模块,这个时候我们需要使⽤ import() 函数


  • import()是异步的,返回的是个promise
  • 结合 export default 使用时,要用.default取值
  1. /* a.js */
  2. export function add(num1, num2) {
  3.   return num1 + num2;
  4. }
  5. /* validMobile.js */
  6. export default (str) => {
  7.   const reg = /^1[3-9]\d{9}$/;
  8.   return reg.test(str);
  9. };
  10. /* main.js */
  11. import validMobile from "./validMobile.js";
  12. console.log(validMobile("12345678910")); // false
  13. if (validMobile("13626068779")) {
  14.   // 结合 export
  15.   import("./a.js").then((a) => {
  16.     console.log(a.add(10, 20)); // 30
  17.   });
  18. } else {
  19.   // 结合 export default
  20.   import("./validMobile.js").then((v) => {
  21.     console.log(v.default("13626068779")); // true
  22.   });
  23. }
复制代码
import meta

import.meta是⼀个给JavaScript模块袒露特定上下⽂的元数据属性的对象,它包含了这个模块的信息,⽐如说这个模块的URL



  • url:模块的完整 URL,包罗查询参数和/或哈希(位于?或之后#)。在欣赏器中,这是获取脚本的 URL(对于外部脚本)或包含文档的 URL(对于内联脚本)。在 Node.js 中,这是文件路径(包罗file://协议)
  • resolve:使用当前模块的 URL 作为基础,将模块阐明符解析为 URL
ES Module的解析

ES Module是怎样被欣赏器解析并且让模块之间可以相互引⽤的呢?


  • 可以看下这篇文章:https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/
  • ES Module的解析过程可以划分为三个阶段

    • 阶段⼀:构建(Construction),根据地址查找js⽂件,并且下载,将其解析成模块记载(Module Record)

      • 当欣赏器(或 JavaScript 引擎)遇到 <script type="module"> 或 import 语句时,它会辨认这是一个 ESM 模块
      • 解析器会辨认所有的 import 语句,并在构建依赖图的同时请求这些模块文件的下载
      • 依赖图构建的过程中,欣赏器会为每个模块分配一个唯一的模块记载,这个模块记载保存了模块的状态和依赖关系
      • 模块情况记载可参考学习这篇文章:https://blog.csdn.net/qq_45730399/article/details/141196562?spm=1001.2014.3001.5501


       

    • 阶段⼆:实例化(Instantiation),对模块记载进⾏实例化,并且分配内存空间,解析模块的导⼊和导出语句,把模块指向对应的内存地址

      • 在所有依赖项(模块)都下载完成后,欣赏器会执行模块链接。在此阶段模块的依赖关系会被处置惩罚,导入的模块和导出的符号都会被绑定起来
      • 在这个过程中,欣赏器不会执行模块的代码,只是查抄模块之间的依赖关系,并将导入和导出的值关联起来

    • 阶段三:运⾏(Evaluation),运⾏代码,盘算值,并且将值填充到内存地址中

      • 关联完成后才会开始逐个执行模块的代码,模块执行后,模块的状态会更新,表明它已经执行完毕,之后如果再次请求该模块,执行的效果会直接从缓存中获取




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

涛声依旧在

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表