《JavaScript 模块化:解锁前端开发的新境界》

一、JavaScript 模块化发展史

JavaScript的发展史主要分为四个阶段

第一阶段

js诞生阶段

这个阶段js能做的事情很少,它仅仅能实现页面中的一些小的效果,那个时候,一个页面能用到的js代码也就几百行。

这个时候js这门语言本身的缺陷被大家忽略,因为程序的规模太小了

这个时候没有专业的前端工程师。

第一阶段有两个大的事件

1、 1996年 NetScape将javascript语言提交给欧洲的一个标准制定组织ECMA(欧洲计算机制造协会)

2、 1998年 NetScape在与微软浏览器IE的竞争中失利,宣布破产,退出历史舞台

第二阶段

ajax的出现,逐渐改变了js在浏览器中扮演的角色。现在,它不仅能实现简单小的效果,还能和服务器进行交互,js代码量急速增加。

这个时候一些公司开始招募专业的前端开发工程师。

这个时候的前端开发者承担的任务还是比较简单。

根本的原因,是因为前端还有几个大的问题没有解决。

1、浏览器解释执行js的速度太慢了

2、 用户端的电脑配置不足

3、更多的代码带来了全局变量的污染、依赖关系混乱等问题

这个阶段的前端处于非常尴尬的位置,在传统的开发模式(不分前后端)和前后分离之间痛苦。

第二阶段的大事件

1、 IE浏览器制霸市场后,几乎不更新

2、 ES4.0流产,导致js语言近10年间毫无变化

3、 2008年ES5发布,仅仅解决了一些JS API不足的糟糕局面

第三阶段

2008年谷歌推出v8引擎。将JS的执行速度推上了新的台阶,甚至可以和后端语言媲美。同时个人电脑的配置也开始飞跃。这个时候制约前端发展的两个大的问题得到了解决。

目前只剩最后一个问题还在负隅顽抗。即全局变量和依赖混乱的问题。解决了它,js就能胜任大项目的开发了。

这个时候全世界的前端开发者在社区中进行了激烈的讨论。

直到,2008年一个Ryan Dahl的小伙子出现。他的一个业务需求使他焦头烂额。他需要手写一个高性能服务器,这个服务器在市场上没有满足要求的。

经过他的分析,这个高性能服务器,要减少线程,而减少线程,就无可避免的要使用异步处理方案。

这个时候,v8引擎引起了他的注意。js语言天生就是一门单线程语言,并且是基于异步的。同时有v8的支持,它的执行速度完全可以支撑起一个服务器。而且v8是chrome发布的,后续一定会不断优化

于是他基于开源的v8引擎,对源码做了一些修改,就完成了这项任务。

第二年 Ryan推出了该项目,命名为node.js

至此js正式登上舞台,不再是浏览器的玩具了。它是一门真正的语言了。它依附于运行环境,不再依赖浏览器。

js在浏览器中执行过程

graph TD js程序 -->|交给浏览器|浏览器 浏览器 -->|交给|v8引擎 浏览器 -->|交给|webAPI v8引擎 -->|语法解析| 操作系统 webAPI-->|语法解析|操作系统 操作系统 -->计算机硬件

js在node.js中执行过程

graph TD js程序 -->|交给浏览器|浏览器 浏览器 -->|交给|v8引擎 浏览器 -->|交给|nodeAPI v8引擎 -->|语法解析| 操作系统 nodeAPI-->|语法解析|操作系统 操作系统 -->计算机硬件

node.js的诞生便把最后一个问题放在台前,即全局变量污染和依赖混乱的问题

须知node.js是服务端,如果不解决这个问题,分模块开发就无法实现。而模块化开发是后端程序必不可少的

于是在社区激烈讨论之后,最终形成了一个模块化方案,就是大名鼎鼎的CommonJS,该方案,彻底解决了全局变量污染和依赖混乱的问题。

该方案一出立马被node.js支持。于是node.js成为了第一个为js语言实现模块化的平台。为前端的飞速发展奠定了基础。

第三阶段的大事件

1、 2008,v8发布

2、 IE市场逐步被firefox和chrome蚕食,无力回天

3、 2009年,node.js发布,并且附带common.js模块化规范

第四阶段

commonjs的出现打开了前端开发者的思路

既然node.js可以作为后端使用模块化,作为js语言的老东家浏览器为什么不可以呢。

于是就有人想办法把commonjs运用到浏览器中

很遗憾由于有诸多的问题,没办法把commonjs规范直接移植到浏览器中

厉害的开发者们又想到为什么不自己实现一套出来呢?又不是非要使用commonjs标准

很快 AMD规范出炉,它解决的问题和commonjs规范解决的问题是一样的。它可以适应浏览器环境

紧接着CMD规范出炉,它对AMD进行了优化改进

终于!官方坐不住了。

2015年,es6发布。提出了官方的模块化解决方案,即我们熟知的 es6模块化

从此js这个语言就有了本身的特质了,也有了和其他语言较量的资本了。各种框架也相继诞生,如react、vue、angular等等。

二、commonjs

在nodejs中,由于只有一个入口文件,而开发一个应用肯定会涉及到多个文件的配合,因此nodejs对模块化的需求比浏览器高。

graph TD 执行入口,index.js--> a.js-->b.js 执行入口,index.js-->c.js a.js-->d.js

由于nodejs刚刚发布的时候,前端没有统一。因此它选择了社区的commonjs规范

要理解commonjs规范,首先认识两个重要的概念 模块的导出和模块的导入

模块的导出

什么是模块?

模块其实就是一个js文件。它实现了一些功能,并隐藏内部的实现,同时提供一些接口,供其他模块使用。

模块有两个核心,隐藏和暴露

隐藏自己实现

暴露接口,外部调用

任何一个正常的模块化标准,都应该默认隐藏模块中的所有实现,而通过一些语法或者api调用来暴露接口。

暴露接口的过程就是模块导出

模块的导入

当需要使用一个模块时,使用的是该模块暴露部分(导出的部分),隐藏的部分永远无法使用。

通过某种语法或者api去使用一个模块时,这个过程叫做模块的导入

commonjs规范

commonjs使用exports导出模块,require导入模块

具体规范如下:

1、如果一个js文件中存在exports或require,该js文件是一个模块

arduino 复制代码
//a.js文件中
const test = 123
//a.js文件 不是一个模块文件

2、模块内的所有代码均为隐藏代码,包括全局变量、全局函数。这些全局内用均不应该对全局变量造成污染

3、 如果一个模块需要暴露一些API提供给外部使用,需要通过exports导出,exports是一个空对象,可以理解为该对象添加任何需要导出的内容

ini 复制代码
//a.js文件中
const a = "a"
const test = "test";
exports.test = test;
//编译之后为

//exports:{test:"test"}
//a没有暴露,因此外面没办法访问

4、 如果一个模块需要导入其他模块,通过require 实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容。

bash 复制代码
//index.js文件中
const test = require("./a");
console.log(test); 

//{test:"test"}

node对commonjs实现原理

为了实现commonjs规范,nodejs对模块化做出了以下处理

1、为了保证高效的执行,仅加载必要的模块,nodejs只有执行到require的函数时才会加载并执行模块

bash 复制代码
//index.js文件中
//const test = require("./a"); 注释之后,这个模块就不会执行,只有用到了才会执行
console.log(test); 

2、为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置到一个函数中执行(实际上不是函数,这里简单理解就是函数),以保证不污染全局变量

ini 复制代码
例如以a.js文件为例
(function(){
    //放入模块的代码
    const a = "a"
    const test = "test";
    exports.test = test;
})()

3、 为了保证顺利导出模块内容,nodejs作了以下处理

  • 在模块开始前执行,初始化一个值module.exports = {}

  • modules.exports 即模块的导出值

  • 为了方便开发者便捷导出,nodejs在初始化完module.exports后,又声明了一个变量,exports=module.exports

java 复制代码
//伪代码

例如以a.js文件为例
(function(module){
    //放入模块的代码
    module.exports={}
    var exports = modules.exports
    const a = "a"
    const test = "test";
    exports.test = test;
    return module.exports
})()

//注意:虽然源码不是这么实现的,但是这么理解时最简单的

通过上述代码,发现其实模块的导出,不仅可以使用exports.的方式导出,还能使用module.exports={}的方式导出

ini 复制代码
const a = 123;
module.exports = {
  a,
};
//{a:123}

注意:当exports和module.exports={}同时出现的情况

ini 复制代码
const test = "test";
exports.test = test;
const a = 123;

module.exports = {
  a,
};
module.exports.b = 123;
//{a:123,b:123}

会发现exports中的数据没有了;带入上面的源码分析

ini 复制代码
(function(module){
    //放入模块的代码
    module.exports={}
    var exports = modules.exports
    const test = "test";
    exports.test = test;
    const a = 123;
    module.exports = {
      a,
    };
    module.exports.b = 123;
    return module.exports
})()

//{a:123,b:123}
ini 复制代码
(function(module){
    //放入模块的代码
    module.exports={}
    var exports = modules.exports
    const test = "test";
    exports.test = test;
    module.exports.b = 123;
    const a = 123;
    module.exports = {
      a,
    };
    return module.exports
})()

//{a:123}

理解起来简单多了,因为 module.exports={a}创建了一个新的对象,最后return的是这个新对象,当然exports中的就没了。

来个好玩的

ini 复制代码
module.exports = "123";

上面导出会的到什么?

会直接的到字符串123;带入源码分析就懂了。

4、为了避免反复加载同一个模块,nodejs默认开启了模块缓存策略。如果模块加载过了,则自动使用之前的导出结果

注意涉及到的路径,原则上使用相对路径,必须使用./或者../开头

浏览器端模块化的难题

commonjs的工作原理

当使用require(模块路径)导入一个模块时,node会做以下两件事情(暂时不考虑缓存)

1、通过模块路径找到本机文件,并读取文件内容

2、将文件中的代码放入到一个函数环境中执行,并将执行后module.exports的值作为require函数的返回结果

以上这两个步骤,使得commonjs在node端可以良好的被支持

可以简单理解commonjs是同步的,必须要等到加载完文件并执行代码后才能向后执行

当浏览器遇到commonjs,就行不通了

1、浏览器要加载js文件,需要远程从服务器读取,而网络传输的效率低于node环境中读取本地文件的效率,由于commonjs是同步的,极大的降低了性能

2、如果需要读取js文件内容并把它放到一个环境中执行,需要浏览器厂商的支持,可是浏览器厂商不愿意支持。原因是因为commonjs是社区标准,非官方的

新的规范

基于以上两点,浏览器无法支持模块化

可这并不代表模块化不能在浏览器中实现。只要解决掉上面两个问题就行了。

解决办法其实非常简单

1、远程加载js浪费时间,做成异步即可。加载完后调用一个回调就可以了

2、模块中的代码需要放置函数中执行,编写模块时,直接放函数中就行了。

基于以上解决思路,诞生了AMD和CMD规范。

三、AMD

全称Asynchronous Module Definition,即异步模块加载机制

require.js实现了AMD规范

在ADM中,导入和导出模块代码,都必须放在define函数中

javascript 复制代码
define([依赖模块列表],function(模块名称列表){
    return 导出内容
})

了解即可,现在已经不更新了。

还是举个例子吧

xml 复制代码
//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>
    <script data-main="./index.js" src="./require.js"></script> 
</head>
<body>
    
</body>
</html>

//index.js
console.log("文件入口");

//require.js

const script = document.getElementsByTagName("script");
for (const item of script) {
  if (item.dataset?.main) {
    const appendScript = document.createElement("script");
    appendScript.src = item.dataset.main;
    item.appendChild(appendScript);
  }
}

打开控制台会打印出 "文件入口"

入口文件好了,继续实现a.js导出,

amd导出使用define函数

scss 复制代码
define(123)
//或者传入一个函数

define(()=>{
    return123
})

导入也是使用define函数

css 复制代码
define(["a"],(a:对应a模块的内容)=>{
    模块内部的代码
})

define中的第二个参数是一个异步的,要等到第一个数组中的模块加载之后才会执行

四、CMD

全称 Common Module Definition ,公共模块定义规范

sea.js实现了CMD规范

CMD中,导入导出模块的代码也都必须放在define函数中

javascript 复制代码
define(function(require,exports,module){
    模块内部代码
})

与AMD区别知识define只接收一个函数参数

因为AMD的CMD现在都不再使用了。有兴趣可以看下require.js和sea.js的实现原理

五、ES6

简介

ECMA组织参考了众多社区模块化标准,终于在2015年,随着ES6发布了官方的模块化标准,后成为ES6模块化

ES6模块化具有以下特点

1、使用依赖 预声明 的方式导入模块

  • 依赖延迟声明

    • 优点:某些时候可以提高效率,比如导入模块不确定场景
    • 缺点:无法在一开始确定模块关系
  • 依赖预声明

    • 优点:一开始可以确定模块关系
    • 缺点:某些时候效率低

2、灵活的多种导入导出模式

3、规范的路径表示法,所有路径必须以./或者../开头

基本导入导出

模块引入

注意 这一部分非模块化标准,目前浏览器使用以下方式引入一个ES6模块文件

xml 复制代码
<script src="入口文件" type="module"></script>

模块的基本导入导出

ES6中的模块导入导出分为两种

基本导入导出

1、基本导出 类似exports.xxx=xxx

基本导出可以有多个,每个必须有名称

基本语法

arduino 复制代码
export 声明表达式

或者

arduino 复制代码
export {具名符号} //注意这个{}不是对象,是一种特殊的语法

由于基本导出必须具有名称,所以要求导出内容必须跟声明表达试活具名符号

2、基本导入

对于基本导出,如果要进行导入,使用下面的代码

javascript 复制代码
import {导入的符号列表} from "模块路径"

注意细节:

  • 导入时,可以通过关键字as对导入的符号进行重命名
  • 导入时使用的符号是常量,不可修改
  • 可以使用*号导入所有的基本导出,形成一个对象

补充 import 模块路径 这种语句,仅会运行模块,不使用内部任何导出,一般使用在项目初始化

默认导入导出

1、默认导出

每个模块,除了允许有多个基本导出外,还允许有一个默认导出

默认导出类似于CommonJs的module.exports,由于只有一个,因此无需具名

具体语法

arduino 复制代码
export default 默认导出数据

或者

javascript 复制代码
export {默认导出的数据 as default}

由于每个模块仅允许有一个默认导出,因此每个模块不能出现多个默认导出语句

2、默认导入

需要想要导入一个模块的默认导出,需要使用下面语法

javascript 复制代码
import 接收变量名 from "模块路径"

类似commonjs的

javascript 复制代码
var接收变量名= require("模块路径")

由于默认导入时变量名是自定义的,因此没有别名一说

如果希望同时导入某个模块的默认导出和基本导出,可以使用下面语法

javascript 复制代码
import 接收默认导出的变量,{接收基本导出的变量} from "模块路径"
相关推荐
若谦2 分钟前
大文件断点续传
前端
iOS大前端海猫2 分钟前
深入解析 Vue.js 中的选择器:从 id 到类,再到标签选择器
前端·vue.js
SurgeJS7 分钟前
我造了一个轮子:Norm Axios(约定式请求)
前端·vue.js
USER_A00124 分钟前
【VUE3】练习项目——大事件后台管理
前端·vue.js·axios·pinia·elementplus·husky·vuerouter4
fruge27 分钟前
Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
前端·css·sass
鸿蒙场景化示例代码技术工程师43 分钟前
基于AssetStoreKit实现免密登录鸿蒙示例代码
前端
在掘金44 分钟前
【kk-utils】Excel工具——excel-js
前端·excel
Danny_FD1 小时前
Canvas的应用与实践
前端·javascript
_请输入用户名1 小时前
husky 切换 simlple-git-hook 失效解决方法
前端
前端九哥1 小时前
🚀Vue 3 hooks 每次使用都是新建一个实例?一文彻底搞懂!🎉
前端·vue.js