Skip to content

modulelize模块化

Node.js是在2009年诞生的,目前最新的版本是分别是 LTS 16.15.1以及Current 18.4.0:

  • LTS版本:(Long-term support, 长期支持)相对稳定一些,推荐线上环境使用该版本;

  • Current版本:最新的Node版本,包含很多新特性;

  • 安装:

    1. .msi安装包,Mac选择,.pkg安装包,Linux会在后续部署中讲解;
    2. .msi安装包 安装过程中会配置环境变量(让我们可以在命令行使用),并且默认选项会安装npm(Node Package Manager)工具;

Node.js是一个基于V8 JavaScript 引擎的JavaScript运行时环境。

  • Node.js基于V8引擎来执行JavaScript的代码,但是不仅仅只有V8引擎:
  • 在Node.js中也会有一些额外的功能,比如文件系统读/写、网络IO、加密、压缩解压文件等操作;
  1. JavaScript代码执行的时候会经过V8引擎 解析执行,再通过Node.js的Bindings,将任务放到Libuv的事件循环中

    就是放在事件队列当中等待执行。

    • libuv(Unicorn Velociraptor—独角伶盗龙)是使用C语言编写的库;

    • libuv 提供了事件循环、文件系统读写、网络IO、线程池等等内容;

  2. libuv 处理完之后执行回调,将执行结果或者状态,返回到js当中

  • 浏览器:blink+css/html, js+v8 运行在浏览器当中

    如果在blink+css/html, js+v8 执行的时候 还存在对计算机系统的一些操作,在这中 间浏览器会有一些中间层来进行处理

  • node:v8+js 和 libuv(包含了一些除了v8执行js引擎之外的其他的库),用来提供一些额外的功能

    node 也是一样,当 v8 在执行js代码遇见需要对数据以外计算机,文件之类的操作也会交由 node 中间层 libuv 进行处理

    • libuv 也是node的核心,使用c语言来编写的, 是c语言的库,v8引擎是使用c++写的, 所以在node中 不仅仅存在 js,也存在其他的语言

      libuv 在 github 上是可以找到的

    • node 不存在是由 js 编写的这种说法

  • 总结:windows 和 nodejs 都是可以说是js 运行时环境,但是都不仅仅包括运行js的操作,浏览器的架构会注重客户端操作,要渲染页面,计算节点属性。node 也会有其他的核心模块,库可以及逆行网络请求,文件读取数据处理,包管理器

  • 可以通过 node.js来编写应用:js语言

  • 之后的js代码 交由 v8 来处理翻译,明确要做的操作

  • 之后会由NODE.JS bindings (应该是node底层的一个接口API) 绑定到 libuv,libuv 找到处理该类型操作的模块之后绑定对应操作系统上的某一个操作,去执行操作系统当中的操作。例:读取文件,读取数据库(这个在node.js中是异步的),libuv就会在操作系统当中执行这个操作

    执行时是异步的会加入到事件队列当中,等待操作完成之后在继续被执行

  • 执行完之后,就会加入到事件循环队列当中等待被执行。

  • 作完成之后在通过 NODE.JS 内部api 将这个任务在返回到v8当中执行一般使用callback的方式 ,返回结果或者状态

  • 起始执行操作之前还有一个 BLOCKING OPERATION 阻塞的操作

    如果出现阻塞的操作(一般不会有)的话会交由一个单独的线程 worker Threads(多线程-线程池) 来操作的

je7b1e.png

Node.js的快速发展也让企业对Node.js技术越来越重视,在前端招聘中通常会对Node.js有一定的要求,特别对于高级前端开发工程师,Node.js更是必不可少的技能:

  • 应用一:目前前端开发的库都是以node包的形式进行管理;
  • 应用二:npm、yarn、pnpm工具成为前端开发使用最多的工具;
  • 应用三:越来越多的公司使用Node.js作为web服务器开发、中间件、代理服务器;
  • 应用四:大量项目需要借助Node.js完成前后端渲染的同构应用;
  • 应用五:资深前端工程师需要为项目编写脚本工具(前端工程师编写脚本通常会使用JavaScript,而不是Python或者shell);
  • 应用六:很多企业在使用Electron来开发桌面应用程序;

Node的版本工具, 可以帮助我们切换,更新,管理不同的node版本

  • 常见的node管理工具 n,nvm

都很好用,推荐使用 ”n“, 不过两个工具都不支持windows

  • nvm:Node Version Manager;
  • n(tj开发的):Interactively Manage Your Node.js Versions(交互式管理你的Node.js版本)
Terminal window
dnf module list nodejs
dnf module install nodejs:<stream>
#安装node18
dnf module install nodejs:18/common

由于两个工具都不支持,widows版本的,针对nvm,在GitHub上有提供对应的window版本

  • 找到对应得github,下载安装,最好是默认路径
  • 会有提示发现本地是否安装过node,貌似没有安装到c盘,需要选择下, 出现问题得时候在排查
  • nvm -v/version 查看版本
  • nvm install latest 安装最新的node版本
  • nvm install lts 安装最新得稳定长期支持的版本
  • nvm uninstall 版本 卸载指定版本
  • nvm list/ls 展示目前安装的所有版本
  • nvm use 切换版本

n 是Node的一个模块,作者是TJ Holowaychuk(鼎鼎大名的Express框架作者)

支持mac/linux

前面添加的sudo是权限问题;

  • 安装n:直接使用npm安装即可

    Terminal window
    #安装工具n
    npm install -g n
    #查看安装版本
    n --version
    #安装最新
    n latest
    #安装稳定
    n stable
    #删除某个版本
    n rm 0.10.1
  • 安装node

    Terminal window
    #安装lts版本
    n lts
    #安装latest 最新版本
  • 查看所有版本

    Terminal window
    #查看所有版本,之后可以使用光标选择要使用的node版本
    n
  • 其他命令,百度查一下

当我们先安装node之后,在安装n的时候,使用n切换发现失效了,问题在于node安装的目录和n的安装目录不一致造成了,需要手动修改下环境配置

  1. 编辑环境配置文件:
Terminal window
vim ~/.bash_profile
  1. 将下面代码插入到文件末尾:
Terminal window
export NODE_HOME=/usr/local
export PATH=$NODE_HOME/bin:$PATH
export NODE_PATH=$NODE_HOME/lib/node_modules:$PATH
  1. 执行source使修改生效:
Terminal window
source ~/.bash_profile

再使用 n 切换,便能成功。

  • 目前我们知道有两种方式可以执行:

    1. 将代码交给浏览器执行;
    2. 将代码载入到node环境中执行;
  • 如果我们希望把代码交给浏览器执行:

    1. 要通过让浏览器加载、解析html代码,所以我们需要创建一个html文件;
    2. 在html中通过script标签,引入js文件;
    3. 当浏览器遇到script标签时,就会根据src加载、执行JavaScript代码;
  • 如果我们希望把js文件交给node执行:

    1. 首先电脑上需要安装Node.js环境,安装过程中会自动配置环境变量;
    2. 可以通过终端命令node js文件的方式来载入和执行对应的js文件

REPL是Read-Eval-Print Loop的简称,翻译为“读取-求值-输出”循环; REPL是一个简单的、交互式的编程环境;

例:浏览器控制台 console 的输入输出。就是一个repl

  • Node也有对应的REPL环境,我们可以在系统终端里演练简单的代码。
  • 在C/C++程序中的main函数中,可以获取到两个参数:

    1. argc:argument counter 的缩写,传递参数的个数;
    2. argv:argument vector(向量、矢量)的缩写,传入的具体参数。
      • vector翻译过来是矢量的意思,在程序中表示的是一种数据结构。
      • 在C++、Java中都有这种数据结构,是一种数组结构;
      • 在JavaScript中也是一个数组,里面存储一些参数信息;
  • 在node中可以获取到 argv

    返回的的是一个字符串类型的数组,保存的是在控制台输入的命令

    1. process.argv[0]: 一般情况下都是用 node执行 所以第一个一般都是 node命令的

    2. process.argv[1]: 第二个一般情况下,就是需要执行js的文件

    3. 后面就是对应的命令

const util = require('util');
const child_process = require('child_process');
/* 调用util.promisify方法,返回一个promise,*/
const exec = util.promisify(child_process.exec);
// const appPath = join(__dirname, 'app');
const run = async function () {
// 参数1:shell,参数2:传递的参数 如const { stdout, stderr } = await exec('rm -rf build')
// await exec(`npm run serve`, { cwd: appPath });
await exec(`npm run serve`);
}
run();
  • 这些全局对象实际上是模块中的变量,只是每个模块都有看来像是全局变量
  • 注意:在命令行交互中是不可以使用的;
1.1.__dirname 获取当前文件所在的路径,绝对路径
Section titled “1.1.__dirname 获取当前文件所在的路径,绝对路径”

注意:不包括后面的文件名只是路径, 也不包括最后面的\

1.2.__filename 获取当前文件所在的路径和文件名称
Section titled “1.2.__filename 获取当前文件所在的路径和文件名称”

注意:包括文件名称,也包括扩展名

process 提供了 Node 进程中相关的信息

  • 比如Node的运行环境、参数信息等;
  • 在项目中,会将一些环境变量读取到 process 的 env 中;

console:更多的方法查看 mdn 的文档

  • console.log 最常用的输入内容的方式:console.log
  • console.clear 清空控制台:console.clear
  • console.trace 打印函数的调用栈:console.trace
4.1.setTimeout(callback, delay[, …args])
Section titled “4.1.setTimeout(callback, delay[, …args])”

callback在delay毫秒后执行一次;

4.2.setInterval(callback, delay[, …args])
Section titled “4.2.setInterval(callback, delay[, …args])”

callback每delay毫秒重复执行一次;

callbackI / O事件后的回调的“立即”执行;

  • 这里先不展开讨论它和setTimeout(callback, 0)之间的区别;
  • 因为它涉及到事件循环的阶段问题,我会在后续详细讲解事件循环相关的知识;

添加到下一次tick队列中;

global是一个全局对象,事实上前端我们提到的process、console、setTimeout等都有被放到global中:

  • 我们之前讲过:在新的标准中还有一个globalThis,也是指向全局对象的;es13 新增的标准
  • 类似于浏览器中的window

在浏览器中,全局变量都是在window上的,比如有document、setInterval、setTimeout、alert、console等等

  • 但是在浏览器中执行的JavaScript代码,如果我们在顶级范围内通过var定义的一个属性,默认会被添加到window对象上
  • 但是在node中,我们通过var定义一个变量,它只是在当前模块中有一个变量,不会放到全局对象中

注意:使用webpack在node中默认是不能使用 ES Module 的。

Section titled “注意:使用webpack在node中默认是不能使用 ES Module 的。”
  • 就是webpack 打包 js 的时候, node 是不能处理 ES Module

    如果要在 webpack 使用不同的模块化的话需要在 package.json 中配置 type: "module",注意修改之后不能使用 commonjs 模块化规范了

  • 使用 webpack 打包的话,webpack 会默认帮忙处理。

  • 模块化开发最终的目的是将程序划分成多个小的结构

  • 在这个结构中编写属于自己的逻辑代码,有自己的作用域定义变量命名时不会影响到其他的结构;

  • 这个结构可以将自己 希望暴露变量、函数、对象导出给其结构使用;

  • 也可以通过某种方式,导入另外结构中的变量、函数、对象等;

  1. 作用域的原因,var声明的变量冲突以及外部变量的污染,随着javaScript 代码越来越多的情况下,这种问题越来越明显
  2. 命名的冲突
  • 使用立即函数调用表达式(IIFE)

    • IIFE (Immediately Invoked Function Expression)

    使用立即执行表达式,返回你需要变量

  • 第一,在没有合适的规范 ** 我必须记得每一个模块中返回对象的命名**,才能在其他模块使用过程中正确的使用,甚至出现模块名称相同的情况;

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

在网页开发的早期,Brendan Eich开发JavaScript仅仅作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的:

  • 这个时候我们只需要讲 JavaScript 代码写到 <script>标签中即可;

    • 并没有必要放到多个文件中来编写;甚至流行:通常来说 JavaScript 程序的长度只有一行。
  • 但是随着前端和 JavaScript 的快速发展,JavaScript 代码变得越来越复杂了:

    • ajax的出现,前后端开发分离,意味着后端返回数据后,我们需要通过JavaScript进行前端页面的渲染;
    • SPA 的出现,前端页面变得更加复杂:包括前端路由、状态管理等等一系列复杂的需求需要通过JavaScript 来实现;
    • 包括Node的实现,JavaScript编写复杂的后端程序,没有模块化是致命的硬伤;
  • 所以,模块化已经是JavaScript一个非常迫切的需求:

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

CommonJS是一个规范

  • CommonJS 规范中加载模块是同步的
  • CommonJS 最初提出来是在浏览器以外的地方使用,并且当时被命名为ServerJS,后来为了体现它的广泛性,修改为 CommonJS,平时我们也会简称为CJS。

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

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

    • 在Node中每一个js文件都是一个单独的模块;
    • 这个模块中包括CommonJS规范的核心变量exportsmodule.exportsrequire
  • Browserify是CommonJS在浏览器中的一种实现;

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

  • exportsmodule.exports 可以负责对模块中的内容进行导出;
  • require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

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

  • 使用的是引用赋值的方式来进行数据和表达式的传递

    (已测试)意味着,当修改导出变量的值之后 exports 里对应的值也会改变

    //output.js 中导出
    const obj = {
    name: "zhangsan",
    age: 18,
    score: 78
    }
    //在exports 对象中添加属性并赋值
    exports.obj = obj
    //导入到 index.js
    const obj = {
    name: "zhangsan",
    age: 18,
    score: 78
    }
    //在exports 对象中添加属性并赋值
    exports.obj = obj

Node 中我们经常导出东西的时候,又是通过 module.exports 导出的:

module.exports = {
//通过这样的方式导出,更加的便捷 不需要一个一个的赋值,导入的时候会直接获取到exports
}
  • 注意:这种情况会改变module.exports 的内存指向。

    这样的话 module.exports 就不会和 exports 对象指向同一个地址,exports对象就不会被导出

    • 同样 exports = {} 这样的方法也会改变指向的内存地址,exports对象不会被导出

3、(重要)exprots 和 module.exports的区别

Section titled “3、(重要)exprots 和 module.exports的区别”

在Node中通常情况下是使用module.exports导出的

  1. CommonJS 中是没有 module.exports 的概念的;

    • 但是为了实现模块的导出Node中使用的是Module的类,每一个模块都是 Module的一个实例也就是module;
  2. (重点) 所以在Node中真正用于导出的其实根本不是exports而是 module.exports;

    只是为了实现CommonJS的规范而将 module.exports 引用赋值给了 exports,因此两个对象都指向同一个内存地址

    • 这样做的原因就是,方便于只了解 CommonJS 而不了解 Node 的人更加方便的入手
//使用了这个写法之后 exports对象就不能正常导出了
module.exports = {
//需要导出的数据
}

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

  • 导入格式:require(X)

X是一个Node核心模块,比如path、http

require() 的查找规则一 省略后缀名
Section titled “require() 的查找规则一 省略后缀名”

X如果是以 ./..//(根目录)开头的

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

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

    • 1> 查找X/index.js文件
    • 2> 查找X/index.json文件
    • 3> 查找X/index.node文件
  • 还是没有找到,那么报错:not found

require() 的查找规则二 省略路径和后缀名
Section titled “require() 的查找规则二 省略路径和后缀名”
  • require 首先会 查找node内置的核心模块,比如path、http

    直接返回核心模块并且停止查找

  • 如果直接是一个X没有路径也没有后缀名,并且也不是node的核心模块的话

    1. 会从当前目录 一层一层的向上查找看有没有 node_modules的文件夹,
    2. 当有 node_modulds 文件夹的话就会根据上面 提到的查找规则在 node_modules 里面查找 对应的文件夹,或者文件
    3. 没有的话会继续向上找知道找到根路径,还没有的话就会报错:not found
  • 如果没有在 node_modules 中找到文件夹的情况下,但是没有index.js/.json/.node 的话就会找到 package.json 文件

    • 会直接导出 package.json 中main 属性指定的文件
    • (重点)如果存在 reports字段优先获取 reports 字段指定的文件 . 是该字段的语法糖
  • 找到文件的话直接导出

    提示是文件不是文件夹。

  • 结论一:模块在被第一次引入时,模块中的js代码会被运行一次

  • 结论二:模块被多次引入时,会缓存最终只加载(运行)一次

    • 这是在node当中每一个使用commonjs 的文件都会被抽象成一个module对象,每个模块对象module都有一个属性:loaded
    • 为false表示还没有加载,为true表示已经加载;
    • 已经加载执行过的代码就不会在执行了。
  • 结论三:如果有循环引入,node的加载顺序

    node 中 js 文件的引入是一种数据结构,图结构

    • 图结构

      特点:例:树结构的子节点有引用了别的节点

    • 如果在node中存在循环引入js文件的话,会使用深度优先算法(图结构的算法),是从一层当中走到底的

      • 图结构在遍历的过程中,有深度优先搜索(DFS, depth first search)广度优先搜索(BFS, breadth first search);
      • Node采用的是 深度优先算法
    • 文件引入的顺序node-1,node-2

      1. node-1-> node-3->node-4->node-5 发现没有引用的了就会停掉继续加载下一个文件
      2. node-2 -> 由于 node-3,node-5已经被加载过了 就不会在加载了

      jeoHUK.png

CommonJS 中加载模块是同步的,这个是一个比较耗时的操作

  • 由于 require 是同步引入的,这个就意味着需要等待上一个文件完全执行加载完才能继续加载下面的代码

    • 一般在服务器里是没有什么问题的,因为 **不需要下载js文件 **直 接加载本地的js文件时很快的
    • 上面NODE架构了解,一旦node当中出现阻塞的操作,会交由 word-threads 线程池里的线程来进行执行
  • 如果在浏览器中使用的话,需要设计成异步的

    因为浏览器在执行到 require 的时候需要下载对应的js文件,之后在进行加载,这样就意味着,后面的所有的 js 代码都无法加载,即使是一些简单的 dom 操作也是不能运行的。

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

    require 是同步的情况下 CommonJS 是不适用浏览器的

    • 所以在早期的时候 为了可以在浏览器中使用模块化通常会采用 AMD 或 CMD

      但是目前一方面现代的浏览器已经支持 ES Modules另一方面借助于 webpack 等工具可以实现对 CommonJS 或者 ES Module 代码的转换;

    • 因此:AMD和CMD已经使用非常少了

  • 在 webpack 当中,是支持 CommonJS 规范

    • 当然在 webpack 中使用 CommonJS 是没有问题的,因为 **webpack最终会将我们的代码进行解析转成浏览器可以直接执行的代码;**进行打包,打包出来的代码都是可执行的代码

AMD主要是应用于浏览器的一种模块化规范

  • AMD是 Asynchronous Module Definition异步模块定义)的缩写

  • 它采用的是异步加载模块

  • 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了

  • 规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用

    • AMD实现的比较常用的库是 require.jscurl.js
  • 定义 HTMLscript 标签引入 require.js 和定义入口文件,使用 data-main 属性的作用是在加载完src的文件后会加载执行该文件。
//意思是执行完require.js的主文件之后,在加载index.js文件
<script src="./lib/require.js" data-main="./index.js"></script>
  1. 导出

    //方式一
    define(["当前组件需要的依赖"], function() {
    const name = "why"
    const age = 18
    function sum(num1, num2) {
    return num1 + num2
    }
    //导出定义的变量
    return {
    name,
    age,
    sum
    }
    })
    //方式二
    define(function(require,exports,module){
    var $=require('jquery');
    console.info($)
    })
  2. 导入

    //第一个参数应该是 require.config 中配置的路径属性
    define(["foo"], function(foo) {
    console.log("--------")
    // require(["foo"], function(foo) {
    // console.log("bar:", foo)
    // })
    console.log("bar:", foo)
    })
  3. 使用配置项

    • 要点: 无论是根级别deps 还是 shim 级别deps ,都只不过是在加载插件前执行下js而已
    require.config({
    //配置基础路径
    baseUrl: '',
    //RequireJS获取资源时附加在URL后面的额外的query参数。作为浏览器或服务器未正确配置时的“cache bust”手段很有用。使用cache bust配置的一个示例:
    urlArgs: "bust=" + (new Date()).getTime(),
    //配置模块文件路径
    paths: {
    //定义模块属性,和js文件,应该不能加后缀,没细研究
    foo: "./src/foo",
    bar: "./src/bar"
    },
    shim: {
    deps: [],//数组,元素类型是字符串,作用是,当前非标准的amd模块的依赖
    export: "", //作用,当前非amd规范模块的”全局变量“、或者函数
    init: () => {} //函数类型,初始化模块函数
    },
    callback: () => {},//当deps中的自动加载模块加载完毕时,触发的回调函数。
    waitSeconds: 0,//通过该参数可以设置requireJS在加载脚本时的超时时间,它的默认值是7,便会放弃等待该脚本文件,从而报出timeout超时的错误信息,考虑到国内网络环境不稳定的因素,所以这里我建议设置为0。当然一般不需要去改动它,除非到了你需要的时候。
    deps:[]//用于声明require.js在加载完成时便会”自动加载“{就是不用require导入}的模块,值是一个数组,数组元素便是模块名。
    //map - 对于给定的模块,应用程序通过共享其ID来为不同的条件使用相同的代码,从而针对不同的目标使用不同版本的相同模块。
    map: {
    //‘*’应该表示的是多有模块
    '*': {
    css: 'require-css/css'
    }
    },
    })
    //index.js文件时在 require.js 文件执行完之后执行,可以使用require函数
    //第一个参数只能是数组类型,定义的模块
    require(["foo", "bar"], function(foo) {
    console.log("main:", foo)
    })
  4. 注意事项:

    为什么要加时间戳: URL后面添加随机数通常用于防止客户端(浏览器)缓存页面。 浏览器缓存是基于url进行缓存的,如果页面允许缓存,则在一定时间内(缓存时效时间前)再次访问相同的URL,浏览器就不会再次发送请求到服务器端,而是直接从缓存中获取指定资源。

1、问题: 有的插件依赖是提示未定义
Section titled “1、问题: 有的插件依赖项总是提示未定义,”
  1. 插件不是 adm 规范的,没有 define() 函数,所以无法通过 adm 规范 获取不到依赖项
  2. 依赖 adm 规范的,所以并没有将依赖放到 window 对象中,所以非 adm 规范的插件,也无法正常获取
  3. 解决:1、修改源码,2、自己定义 define() 函数 将需要的依赖赋值给 windows 对象
//1、修改源码将你需要的依赖手动添加进来
define(["toastr"], function (toastr) {
/**
* 设置 toastr
*/
function initToastr() {
if (typeof toastr != "undefined") {
toastr.options.progressBar = true;
toastr.options.closeButton = true;
toastr.options.timeOut = 2000;
toastr.options.positionClass = "toast-top-center";
}
}
})
//2、添加 deps
require.config({
urlArgs: "bust=" + (new Date()).getTime(),
baseUrl: "/static/",
paths: {
'toastr': "components/toastr/toastr.min",
},
shim: {
'jpressutils':{
deps:["toastr", "init/toastrInit"], //依赖模块
}
},
})
//init/toastrInit
define(["toastr"], function (toastr) {
toastr.options = {
"positionClass": "toast-top-center",//显示的动画时间
"showDuration": "1000", //显示的动画时间
"hideDuration": "1000", //消失的动画时间
"timeOut": "800", //展现时间
"extendedTimeOut": "1000",
"showEasing": "swing", //显示时的动画缓冲方式
"hideEasing": "linear", //消失时的动画缓冲方式
"showMethod": "fadeIn", //显示时的动画方式
"hideMethod": "fadeOut" //消失时的动画方式
}
//这样非adm规范的js插件也能用了
window.toastr = toastr

CMD规范也是应用于浏览器的一种模块化规范:

  • CMD 是Common Module Definition(通用模块定义)的缩写;
  • 它也采用的也是异步加载模块,但是它将CommonJS的优点吸收了过来;
  • 但是目前CMD使用也非常少了;
  • CMD也有自己比较优秀的实现方案:
    • SeaJS
  • 第一步:下载SeaJS

  • 第二步:引入sea.js和使用主入口文件

    主入口文件的指的是seajs

    //定义
    define(function(require, exports, module) {
    const name = "why"
    const age = 18
    function sum(num1, num2) {
    return num1 + num2
    }
    //导出变量
    module.exports = {
    name,
    age,
    sum
    }
    });
    //导入使用参数fn中的 require 方法进行导入
    define(function(require, exports, module) {
    const foo = require("./foo")
    console.log("main:", foo)
    })
    • 使用,在html文件中导入sea.js的主文件
    <script src="./lib/sea.js"></script>
    <script>
    //加载执行main.js
    seajs.use("./src/main.js")
    </script>

JavaScript没有模块化一直是它的痛点,所以社区才会产生相应的规范:CommonJS、AMD、CMD等,一直到到ES6 的时候才新增了

自己的模块化系统ES Module,

  • 注意: 当使用ES6 ES Module模块化的时候会自动使用严格模式 “use strict”
  • 在浏览器当中使用ES Module 需要在 <script> 标签中声明引入 js 文件类型,告诉浏览器将这个文件当成一个模块进行解析

    不声明类型的话就不会当成一个模块来进行解析,之后在使用模块化的语法在控制台会报错

    <script src="./modules/foo.js" type="module"></script>
  • import … from 导入操作不能路径拼接,因为 预编译阶段没有执行代码 就没有办法确定 path 所以不能进行拼接

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

  • ES Module 使用的是 export 关键字不加 s
  • 模块化默认时严格模式,所以在默认调用的时候 this 指向的是 undefined

语句声明的前面直接加上export关键字就是在声明变量和函数的时候就直接导出

  • 缺点:声明变量时导出的话不能配置别名
export const name = "zhangsan";
export const age = "45";
export const addr = "北京天桥"
export function fn() {
return "变量声明导出"
}

将所有需要导出的标识符,放到export后面的 {}中

  • 注意:这里的 {} 里面不是ES6的对象字面量的增强写法{} 并不是表示一个对象而是模块化导出特殊的语法{} 里面的变量,表达式导出

    所以: export {name: name},是错误的写法;注意:不能在 {} 语法里面使用对象的写法*

const name = "zhangsan";
const score = "78"
function fn() {
return "天津"
}
export {
name, score, fn
}

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

  • import {} from “这里的路径不能像 CommonJS 的查找规则一样,路径和后缀名都不能省略,也不会默认找index.js文件

    • 需要使用和 CommonJS 一样的查找规则,后期需要在 webpack 里面配置查找规则
  • **注意:**在 import 导入的变量要和导出的变量标识符 保持一致

import {标识符列表} from ‘js模块路径’;

  • 这里的 {} 语法同样不是对象 不能看作是结构赋值而是es6 导入的特殊语法,
import { fn ,name, score } from "./foo.js";
console.log(fn(), name, score)

通过 *模块功能放到一个模块功能对象(a module object)上,需要配合起别名的方式,获取指定变量

import * as z from "./foo.js";
console.log(z.name, z.fn(), z.addr)

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

  • ES Module 的 default 对应这 CommonJSmodule.exports 可以将变量赋值给一个对象,进行导出

    默认导入会在 module 对象当中添加一个defult属性将导出的数据 赋值default。

  • default export 导出变量时可以省略 引用类型 的 变量名,可以直接导出一个对象,匿名函数

    • 基本导出,在导入得时候要和导出得变量保持一致
  • import 导入时不在需要使用 {} 的语法,使用的话会报错,并且可以自己来指定接受变量名字;

    • 使用 import 表达式语法导入会直接赋值给自定义的变量当中
    • 使用 import 动态引入的时候,则需要手动获取default属性
  • 注意:不要直接在import语句上面进行结构,会被当作导入语法。

//可以导出一个对象,或者函数。
export default {
name: "zhangsan",
addr: "天桥",
job: "著名相声演员"
}
//导入,不能使用{}语法,同样需要结构的化不要在import语句里面使用,会被当成的导入语法
import zhangsan from "./bar.js"
console.log(zhangsan)

在ES Module在被JS引擎解析时,就必须知道它的依赖关系才能下载对应的 js 文件进行构建实例化,所以导入的声明必须要处于顶层语法, 因此就不能在js运行的时候动态的导入 js 模块

  • 导入对于CommonJS require() 也会有对应的 import() 函数

  • 导入声明只能在模块的顶层使用,当逻辑成立时才需要某个module,这个时候需要import 函数

  • 当我们的使用场景需要动态引用模块时,ES Module 提供了 import() 函数可以实现动态加载

  • 注意:import() 动态引用函数返回值是个 promise

    • 这个时候 then 回调函数的参数就是,module了,相当于commonjs 中的 exports 对象
const btnEl = document.querySelector(".btn")
btnEl.addEventListener("click", function () {
//返回promise,注意执行代码的顺序这里是异步微任务
const obj = import("./foo.js")
//默认导出的要使用default
obj.then(data => console.log(data.default))
//基本导出返回普通的 module 对象,相当于 * as 别名时的对象
obj.then(data => console.log(data))
console.log("结束")
})

由于导出的变量标识符和导入的变量标识符需要保持一致,这样有可能会造成变量冲突

  • 使用 as 关键字起别名的方式可以有效的避免变量冲突

export别名

const product = "car";
const model = "X1 150";
function run() {
console.log("run")
}
export {
product as fporduct,
model as porductModel,
run as frun
}

import别名

import { fporduct as product, porductModel as model, frun as run} from "./foo.js";
console.log(product, model, run())

在开发和封装一个功能库时,通常我们希望将暴露的所有接口放到一个文件中;

  • 这样方便指定统一的接口规范,也方便阅读;

  • 可以使用 exportimport 结合使用;

  • 注意:默认导出要结合使用的化要在 {} 里添加 default标识符,或者使用 *** as别名的方式才可以**

    * 代表了整个模块,默认导出是将导出数据赋值给了 module对象中的defult属性当中

//注意:默认导出要结合使用的化要在 {} 里添加 default标识符
export { default } from "./foo.js"
//或者使用起别名这种方式
export * as a from "./foo.js"
//基本导入的简便写法
export * from "./bar.js"
//也可以使用这种方式,阅读性更好一点
export {supplier, send} from "./baz.js"

import.meta是一个给JavaScript模块暴露特定上下文的元数据属性的对象。

  • 它包含了这个模块的信息,比如说这个模块的URL;

    目前就只包含了这个模块的url。了解一下

  • ES11(ES2020)中新增的特性;

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Model Viewer</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.114.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.114.0/examples/jsm/"
}
}
</script>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('myCanvas') });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const loader = new GLTFLoader();
loader.load('/asset/3d/product.glb', function (gltf) {
console.log(gltf.scene);
scene.add(gltf.scene);
renderer.render(scene, camera);
}, undefined, function (error) {
console.error(error);
});
camera.position.z = 5;
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
  1. 一方面它使用了 importexport 关键字;这里 注意和 exports 书写的区分

  2. 另一方面它采用编译期的静态分析(import…from),并且也加入了**动态引用(import函数)**的方式;

    由于要进行静态分析会在编译的时候提前下载js(重要),只能声明在最顶层作用域。在浏览器解析js的时候可以提前下载

  3. CommonJS 只会在执行到 require 的时候会进行导入,由于 exportsrequire模块变量不需要声明在顶层作用域,会自动到上层作用域找相关变量

  4. 一个是用在服务器的模块化,一个是可以用在浏览器当中的

  5. ES Module 不能使用 file//协议,因为js是运行在浏览器上的,如果可以随便的获取计算机上的数据是很危险的

    • 注意EM Module 是在浏览使用的模块化规范,因为是运行在浏览器当中的不能像 CommonJS 一样直接在运行的计算机当中使用file:// 协议直接获取到js代码,而是需要在主机当中要将js下载到浏览器当中,浏览器获取到js文件才会进行执行

    所以在ES Module当中不能使用 file://协议来获取查找js文件(有可能导致安装浏览器的主机访问网页的时候造成安全问题),只支持http, https, data …等协议

    这里也意味这如果使用本地打开使用模块化的js文件会遇到 CORS 错误,因为Javascript 模块安全性需要

    • 因此,使用模块化的情况不能使用本地路径打开html文件,需要使用服务器打开,可以使用的VSCode插件:Live Server

    • 所以 ES Module 是作用在浏览器当中

ES Module是如何被浏览器解析并且让模块之间可以相互引用的

  • 模块化的规范中每一个模块都会有自己的一个作用域

    个人觉得就是模块记录,针对每一个模块都有一个对应的模块环境记录,也会分配内存空间

  • ES Module解析通常会有三个阶段

    构建 -> 实例化环境记录 -> 运行js赋值

在构建根据script标签获取到地址查找js文件,并且下载,将其解析成 模块记录(Module Record);

  • 也就是将每一个js文件都会抽象成一个模块记录 这样的一个对象
  • 解析最顶层的语法,进行词法分析,查看是否有导入的操作
  • 发现 import 有导入 js 文件的操作,时候会在服务器将这个js下载下来,之后也会解析成对应的 Module Record
  • 下载好之后内部还会有生成一个映射关系(就是表格缓存),
  • 针对每一个模块都有一个对应的模块记录会分配内存空间
  • 如果有重复的引用关系,不会重新生成会直接引用创建好的模块环境记录,没有的话在解析对应的模块环境记录

jnv0Wn.md.png

jnvdij.png

阶段二:实例化(Instantiation),

Section titled “阶段二:实例化(Instantiation),”

模块记录进行实例化,

  • 就是确定模块化js文件之间的一个关系

将分配内存空间,根据解析模块的导入与导出语法来确定其他模块之间产生的关系,之后把引用模块的内存地址关联起来

  • 解析最顶层的语法,查找是否存在import导入的语法,会实例化对应的 Module Environment record(模块环境记录) 来存放导入的数据,变量表达式

处理完当前模块 import 的依赖文件之后,之后继续解析查看是否存在 export 标识符导出数据

  • 解析最顶层的语法,查找是否存在export导出的语法,也会对导出的数据也进行一个实例化,实例化一个 Module Environment record(模块环境记录)来存放导入的数据,变量表达式
  • 模块环境记录当中的只会存放导出的一些变量,表达式,
  • 构建和实例化阶段这些变量默认都是没有值的也不会有任何代码的执行只是做一个记录,
  • 只有赋值阶段才会真正 执行js代码 的时候才会进行赋值
5.模块记录 和 模块环境记录关系
Section titled “5.模块记录 和 模块环境记录关系”
  1. 每一个 js 模块都会关联一个模块记录Module Record

  2. 在解析 js 模块中的 import 和 export实例化出对应的Module Environment record(模块环境记录)

  3. 模块记录Module Record 包括了 import 和 export 的 Module Environment record(模块环境记录)

    ecma文档貌似import 和 export是同一个

  4. 环境记录是要在执行代码创建执行上下文的时候被创建

  5. 导出的环境记录可以修改,但是导入的环境记录不能修改,所以根据情况而定 (是否有export或者import)会有几个和环境记录

阶段三:运行-求值阶段(Evaluation)

Section titled “阶段三:运行-求值阶段(Evaluation)”

实例化完成之后开始运行代码,计算值,并且将值填充到内存地址中;

  • 在导出前阶段变量,对象…是可以修改的

  • 但是在这个导入的模块当中的变量 js 引擎会做限制,表达式是不允许修改的,可以拿到但是不能修改

  • 这个阶段会将其他模块记录导入的变量赋值给导入模块的import对应的 Module Environment record(模块环境记录) 当中

jnvwJs.png