Node.js系列三 - 彻底掌握前端模块化(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 彻底掌握前端模块化

三. AMD和CMD规范


3.1. CommonJS规范缺点


CommonJS加载模块是同步的:

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

如果将它应用于浏览器呢?

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

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

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

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

  • 但是目前一方面现代的浏览器已经支持ES Modules,另一方面借助于webpack等工具可以实现对CommonJS或者ES Module代码的转换;
  • AMD和CMD已经使用非常少了,所以这里我们进行简单的演练;


3.2. AMD规范


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

  • AMD是Asynchronous Module Definition(异步模块定义)的缩写;
  • 它采用的是异步加载模块;
  • 事实上AMD的规范还要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的较少了;

我们提到过,规范只是定义代码的应该如何去编写,只有有了具体的实现才能被应用:

  • AMD实现的比较常用的库是require.js和curl.js;

这里我们以require.js为例讲解:

第一步:下载require.js

第二步:定义HTML的script标签引入require.js和定义入口文件:

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

第三步:编写如下目录和代码

├── index.html
├── index.js
├── lib
│   └── require.js
└── modules
    ├── bar.js
    └── foo.js

index.js

(function() {
  require.config({
    baseUrl: '',
    paths: {
      foo: './modules/foo',
      bar: './modules/bar'
    }
  })
  // 开始加载执行foo模块的代码
  require(['foo'], function(foo) {
  })
})();

modules/bar.js

  • 如果一个模块不依赖其他,那么直接使用define(function)即可
define(function() {
  const name = "coderwhy";
  const age = 18;
  const sayHello = function(name) {
    console.log("Hello " + name);
  }
  return {
    name,
    age, 
    sayHello
  }
})

modules/foo.js

define(['bar'], function(bar) {
  console.log(bar.name);
  console.log(bar.age);
  bar.sayHello('kobe');
})


3.3. CMD规范


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

  • CMD 是Common Module Definition(通用模块定义)的缩写;
  • 它也采用了异步加载模块,但是它将CommonJS的优点吸收了过来;
  • 但是目前CMD使用也非常少了;

CMD也有自己比较优秀的实现方案:

  • SeaJS

我们一起看一下SeaJS如何使用:

第一步:下载SeaJS

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

  • seajs是指定主入口文件的
<script src="./lib/sea.js"></script>
<script>
  seajs.use('./index.js');
</script>

第三步:编写如下目录和代码

├── index.html
├── index.js
├── lib
│   └── sea.js
└── modules
    ├── bar.js
    └── foo.js

index.js

define(function(require, exports, module) {
  const foo = require('./modules/foo');
})

bar.js

define(function(require, exports, module) {
  const name = 'lilei';
  const age = 20;
  const sayHello = function(name) {
    console.log("你好 " + name);
  }
  module.exports = {
    name,
    age,
    sayHello
  }
})

foo.js

define(function(require, exports, module) {
  const bar = require('./bar');
  console.log(bar.name);
  console.log(bar.age);
  bar.sayHello("韩梅梅");
})


四. ES Module


4.1. 认识ES Module


JavaScript没有模块化一直是它的痛点,所以才会产生我们前面学习的社区规范:CommonJS、AMD、CMD等,所以在ES推出自己的模块化系统时,大家也是兴奋异常。

ES Module和CommonJS的模块化有一些不同之处:

  • 一方面它使用了import和export关键字;
  • 另一方面它采用编译期静态类型检测,并且动态引用的方式;

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

  • export负责将模块内的内容导出;
  • import负责从其他模块导入内容;

了解:采用ES Module将自动采用严格模式:use strict


4.2. ES Module的使用


4.2.1. 代码结构组件


这里我在浏览器中演示ES6的模块化开发:

代码结构如下:

├── index.html
├── main.js
└── modules
    └── foo.js

index.html中引入两个js文件作为模块:

<script src="./modules/foo.js" type="module"></script>
<script src="main.js" type="module"></script>

如果直接在浏览器中运行代码,会报如下错误:

image.png                                                      模块化运行

这个在MDN上面有给出解释:

我这里使用的VSCode,VSCode中有一个插件:Live Server

  • 通过插件运行,可以将我们的代码运行在一个本地服务中;

image.png


4.2.2. export关键字


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

foo.js文件中默认代码如下:

const name = 'coderwhy';
const age = 18;
let message = "my name is why";
function sayHello(name) {
  console.log("Hello " + name);
}

我们希望将其他中内容全部导出,它可以有如下的方式:

方式一:在语句声明的前面直接加上export关键字

export const name = 'coderwhy';
export const age = 18;
export let message = "my name is why";
export function sayHello(name) {
  console.log("Hello " + name);
}

方式二:将所有需要导出的标识符,放到export后面的 {}

  • 注意:这里的 {}里面不是ES6的对象字面量的增强写法,{}也不是表示一个对象的;
  • 所以:export {name: name},是错误的写法;
const name = 'coderwhy';
const age = 18;
let message = "my name is why";
function sayHello(name) {
  console.log("Hello " + name);
}
export {
  name,
  age,
  message,
  sayHello
}

方式三:导出时给标识符起一个别名

export {
  name as fName,
  age as fAge,
  message as fMessage,
  sayHello as fSayHello
}


4.2.3. import关键字


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

导入内容的方式也有多种:

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

  • 注意:这里的{}也不是一个对象,里面只是存放导入的标识符列表内容;
import { name, age, message, sayHello } from './modules/foo.js';
console.log(name)
console.log(message);
console.log(age);
sayHello("Kobe");

方式二:导入时给标识符起别名

import { name as wName, age as wAge, message as wMessage, sayHello as wSayHello } from './modules/foo.js';

方式三:将模块功能放到一个模块功能对象(a module object)上

import * as foo from './modules/foo.js';
console.log(foo.name);
console.log(foo.message);
console.log(foo.age);
foo.sayHello("Kobe");


4.2.4. export和import结合


如果从一个模块中导入的内容,我们希望再直接导出出去,这个时候可以直接使用export来导出。

bar.js中导出一个sum函数:

export const sum = function(num1, num2) {
  return num1 + num2;
}

foo.js中导入,但是只是做一个中转:

export { sum } from './bar.js';

main.js直接从foo中导入:

import { sum } from './modules/foo.js';
console.log(sum(20, 30));

甚至在foo.js中导出时,我们可以变化它的名字

export { sum as barSum } from './bar.js';

为什么要这样做呢?

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


4.2.4. default用法


前面我们学习的导出功能都是有名字的导出(named exports):

  • 在导出export时指定了名字;
  • 在导入import时需要知道具体的名字;

还有一种导出叫做默认导出(default export)

  • 默认导出export时可以不需要指定名字;
  • 在导入时不需要使用 {},并且可以自己来指定名字;
  • 它也方便我们和现有的CommonJS等规范相互操作;

导出格式如下:

export default function sub(num1, num2) {
  return num1 - num2;
}

导入格式如下:

import sub from './modules/foo.js';
console.log(sub(20, 30));

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


4.2.5. import()


通过import加载一个模块,是不可以在其放到逻辑代码中的,比如:

if (true) {
  import sub from './modules/foo.js';
}

为什么会出现这个情况呢?

  • 这是因为ES Module在被JS引擎解析时,就必须知道它的依赖关系;
  • 由于这个时候js代码没有任何的运行,所以无法在进行类似于if判断中根据代码的执行情况;
  • 甚至下面的这种写法也是错误的:因为我们必须到运行时能确定path的值;
const path = './modules/foo.js';
import sub from path;

但是某些情况下,我们确确实实希望动态的来加载某一个模块:

  • 如果根据不懂的条件,动态来选择加载模块的路径;
  • 这个时候我们需要使用 import() 函数来动态加载;

aaa.js模块:

export function aaa() {
  console.log("aaa被打印");
}

bbb.js模块:

export function bbb() {
  console.log("bbb被执行");
}

main.js模块:

let flag = true;
if (flag) {
  import('./modules/aaa.js').then(aaa => {
    aaa.aaa();
  })
} else {
  import('./modules/bbb.js').then(bbb => {
    bbb.bbb();
  })
}


4.3. ES Module的原理


4.3.1. ES Module和CommonJS的区别


CommonJS模块加载js文件的过程是运行时加载的,并且是同步的:

  • 运行时加载意味着是js引擎在执行js代码的过程中加载 模块;
  • 同步的就意味着一个文件没有加载结束之前,后面的代码都不会执行;
console.log("main代码执行");
const flag = true;
if (flag) {
  // 同步加载foo文件,并且执行一次内部的代码
  const foo = require('./foo');
  console.log("if语句继续执行");
}

CommonJS通过module.exports导出的是一个对象:

  • 导出的是一个对象意味着可以将这个对象的引用在其他模块中赋值给其他变量;
  • 但是最终他们指向的都是同一个对象,那么一个变量修改了对象的属性,所有的地方都会被修改;

ES Module加载js文件的过程是编译(解析)时加载的,并且是异步的:

  • 编译时(解析)时加载,意味着import不能和运行时相关的内容放在一起使用:
  • 比如from后面的路径需要动态获取;
  • 比如不能将import放到if等语句的代码块中;
  • 所以我们有时候也称ES Module是静态解析的,而不是动态或者运行时解析的;
  • 异步的意味着:JS引擎在遇到import时会去获取这个js文件,但是这个获取的过程是异步的,并不会阻塞主线程继续执行;
  • 也就是说设置了 type=module 的代码,相当于在script标签上也加上了 async 属性;
  • 如果我们后面有普通的script标签以及对应的代码,那么ES Module对应的js文件和代码不会阻塞它们的执行;
<script src="main.js" type="module"></script>
<!-- 这个js文件的代码不会被阻塞执行 -->
<script src="index.js"></script>

ES Module通过export导出的是变量本身的引用:

  • export在导出一个变量时,js引擎会解析这个语法,并且创建模块环境记录(module environment record);
  • 模块环境记录会和变量进行 绑定(binding),并且这个绑定是实时的;
  • 而在导入的地方,我们是可以实时的获取到绑定的最新值的;

image.png                                         export和import绑定的过程

所以我们下面的代码是成立的:

bar.js文件中修改

let name = 'coderwhy';
setTimeout(() => {
  name = "湖人总冠军";
}, 1000);
setTimeout(() => {
  console.log(name);
}, 2000);
export {
  name
}

main.js文件中获取

import { name } from './modules/bar.js';
console.log(name);
// bar中修改, main中验证
setTimeout(() => {
  console.log(name);
}, 2000);

但是,下面的代码是不成立的:main.js中修改

import { name } from './modules/bar.js';
console.log(name);
// main中修改, bar中验证
setTimeout(() => {
  name = 'kobe';
}, 1000);

image.png                                          导入的变量不可以被修改

思考:如果bar.js中导出的是一个对象,那么main.js中是否可以修改对象中的属性呢?

  • 答案是可以的,因为他们指向同一块内存空间;(自己编写代码验证,这里不再给出)


4.3.2. Node中支持 ES Module


在Current版本中

在最新的Current版本(v14.13.1)中,支持es module我们需要进行如下操作:

  • 方式一:在package.json中配置 type: module(后续再学习,我们现在还没有讲到package.json文件的作用)
  • 方式二:文件以 .mjs 结尾,表示使用的是ES Module;

这里我们暂时选择以 .mjs 结尾的方式来演练:

bar.mjs

const name = 'coderwhy';
export {
  name
}

main.mjs

import { name } from './modules/bar.mjs';
console.log(name);

在LTS版本中

在最新的LST版本(v12.19.0)中,我们也是可以正常运行的,但是会报一个警告:

image.png                                                    lts版本的警告


4.3.3. ES Module和CommonJS的交互


CommonJS加载ES Module

结论:通常情况下,CommonJS不能加载ES Module

  • 因为CommonJS是同步加载的,但是ES Module必须经过静态分析等,无法在这个时候执行JavaScript代码;
  • 但是这个并非绝对的,某些平台在实现的时候可以对代码进行针对性的解析,也可能会支持;
  • Node当中是不支持的;

ES Module加载CommonJS

结论:多数情况下,ES Module可以加载CommonJS

  • ES Module在加载CommonJS时,会将其module.exports导出的内容作为default导出方式来使用;
  • 这个依然需要看具体的实现,比如webpack中是支持的、Node最新的Current版本也是支持的;
  • 但是在最新的LTS版本中就不支持;

foo.js

const address = 'foo的address';
module.exports = {
  address
}

main.js

import foo from './modules/foo.js';
console.log(foo.address);
相关文章
|
1月前
|
JavaScript 前端开发 程序员
前端原生Js批量修改页面元素属性的2个方法
原生 Js 的 getElementsByClassName 和 querySelectorAll 都能获取批量的页面元素,但是它们之间有些细微的差别,稍不注意,就很容易弄错!
|
27天前
|
JavaScript 前端开发 Java
springboot解决js前端跨域问题,javascript跨域问题解决
本文介绍了如何在Spring Boot项目中编写Filter过滤器以处理跨域问题,并通过一个示例展示了使用JavaScript进行跨域请求的方法。首先,在Spring Boot应用中添加一个实现了`Filter`接口的类,设置响应头允许所有来源的跨域请求。接着,通过一个简单的HTML页面和jQuery发送AJAX请求到指定URL,验证跨域请求是否成功。文中还提供了请求成功的响应数据样例及请求效果截图。
springboot解决js前端跨域问题,javascript跨域问题解决
|
1月前
|
缓存 JavaScript 前端开发
JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用
本文深入讲解了 JavaScript 与 DOM 交互的基础及进阶技巧,涵盖 DOM 获取、修改、创建、删除元素的方法,事件处理,性能优化及与其他前端技术的结合,助你构建动态交互的网页应用。
42 5
|
28天前
|
缓存 前端开发 JavaScript
JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式
本文深入解析了JavaScript前端路由的实现原理及其在单页应用中的重要性,涵盖前端路由概念、基本原理、常见实现方式(Hash路由和History路由)、优点及挑战,并通过实际案例分析,帮助开发者更好地理解和应用这一关键技术,提升用户体验。
68 1
|
1月前
|
JSON 前端开发 JavaScript
聊聊 Go 语言中的 JSON 序列化与 js 前端交互类型失真问题
在Web开发中,后端与前端的数据交换常使用JSON格式,但JavaScript的数字类型仅能安全处理-2^53到2^53间的整数,超出此范围会导致精度丢失。本文通过Go语言的`encoding/json`包,介绍如何通过将大整数以字符串形式序列化和反序列化,有效解决这一问题,确保前后端数据交换的准确性。
36 4
|
1月前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
117 1
|
1月前
|
移动开发 前端开发 JavaScript
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
于辰在大学期间带领团队参考网易游戏官网的部分游戏页面,开发了一系列前端实训作品。项目包括首页、2021校园招聘页面和明日之后游戏页面,涉及多种特效实现,如动态图片切换和人物聚合效果。作品源码已上传至CSDN,视频效果可在CSDN预览。
42 0
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
|
1月前
|
JavaScript 前端开发 开发者
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第27天】在前端开发领域,Vue.js和Angular是两个备受瞩目的框架。本文对比了两者的优劣,Vue.js以轻量级和易上手著称,适合快速开发小型到中型项目;Angular则由Google支持,功能全面,适合大型企业级应用。选择时需考虑项目需求、团队熟悉度和长期维护等因素。
48 1
|
1月前
|
JavaScript 前端开发 API
前端框架对比:Vue.js与Angular的优劣分析与选择建议
【10月更文挑战第26天】前端技术的飞速发展让开发者在构建用户界面时有了更多选择。本文对比了Vue.js和Angular两大框架,介绍了它们的特点和优劣,并给出了在实际项目中如何选择的建议。Vue.js轻量级、易上手,适合小型项目;Angular结构化、功能强大,适合大型项目。
38 1
|
1月前
|
前端开发 JavaScript UED
"前端小技巧大揭秘:JS如何将后台时间戳秒变亲切小时前、分钟前,让用户秒懂,提升互动体验!"
【10月更文挑战第23天】在Web开发中,将后台返回的时间戳转换为“小时前”、“分钟前”、“刚刚”等友好的时间描述是常见需求。本文介绍如何用JavaScript实现这一功能,通过计算当前时间和时间戳的差值,返回相应的描述,提升用户体验。
34 1