likes
comments
collection
share

🎉 JavaScript模块化:让代码“各就各位”,告别“一锅粥”🍲

作者站长头像
站长
· 阅读数 28

前言

模块化是编程中的一个重要概念,它虽然比较简单但是比较重要,是通向前端工程化的一个重要门槛,现在的程序已经不像以前那样只满足于做一些页面上的效果,而是要向做大型应用的方向发展,而且现在市面上的一些工具webpack、vue、react也都是建立在模块化的基础上的。那下面就开始带你了解模块化的“前世今生”。

模块化发展史

第一阶段

在JavaScript语言刚诞生之时,它仅仅用来实现页面中的一些效果,程序较为简单代码量也不多,所以这门语言自身所存在的一些缺陷并没有被暴露出来,也不会造成什么太大的问题。

第二阶段

随着ajax的出现JS不仅可以用来实现页面上的一些效果,还可以用来和服务器之间进行交互,以前都是通过a标签超链接的方式打开一个新的页面来完成与服务器之间的交互,浏览器只能通过本身与服务器之间进行交互,而无法通过JS语言本身与服务器之间进行交互,有了ajax之后就可以实现页面的局部刷新,伴随着前端程序越来越复杂,代码量也逐渐增多,一些潜在的问题逐渐被暴露出来,浏览器解释执行JS的速度太慢(IE),更多的代码带来了全局变量污染,依赖关系混乱等问题。比如代码量的增多我们可以分为多个js文件进行管理(每个文件相当于是一个模块),但是当js文件逐渐增多,全局变量就变得不可维护,那么可能会想到用立即执行函数把变量约束在函数作用域中,但是有时候也会遇到向外暴露一些对象或者函数的场景,这时候暴露出去的变量一多又变得不可维护了。

var module1 = (function(){
    return {
    
    }
})();

这些问题的根本原因就在于JS没有模块化功能,不同的功能分到不同的文件并且文件之间形成良好、清晰的依赖关系,并且每个模块之间不会造成全局变量的污染。

第三阶段

谷歌V8引擎的发布将JS的执行速度推向了一个新的台阶,同时基于V8引擎的nodejs也诞生了,因为服务器端本身就支持模块化,nodejs又是运行在服务器端的程序,所以nodejs也必须支持模块化,于是就诞生了模块化方案(模块化标准)CommonJS,CommonJS彻底解决了全局变量污染和依赖关系混乱问题。

第四阶段

此时在服务器端可以使用模块化的JS,但在浏览器中依然没有模块化的概念,能不能把CommonJS运用到浏览器中呢?这里面其实存在很多困难。随后又诞生了AMD、CMD规范,最终官方提出了模块化的解决方案-ES6模块化。

CommonJS

CommonJS实现了nodejs中的模块化,主要有两个重要的概念:模块的导出模块的导入 ,使用exports/module.exports导出模块,使用require导入模块。

模块的定义

模块就是一个js文件,里面是一些功能代码(变量,对象、函数等),模块外无法直接进行访问,我们可以选择性的向外进行暴露,向外暴露的过程就是模块的导出, 那么使用模块暴露的部分就是模块的导入

CommonJS规范

  • 如果一个js文件中存在exportsrequire,该文件就是一个模块;
  • 模块内的所有代码均为隐藏代码默认对外不可见,包括在模块中声明的全局变量、全局函数等避免造成全局污染;
  • 模块内部向外暴露需要使用exports导出,exports是一个空的对象可以在该对象上添加任何需要导出的内容;
  • 模块内部需要导入其他模块使用require函数导入,传入模块的路径(必须是相对路径形式)即可返回该模块内导出的整个内容;
// 1.js
var count = 0;
function getNumber() {
    count++;
    return count;
}
exports.getNumber = getNumber;

// exports.getNumber = function (){
//     count++;
//     return count;
// }

/**
 * 相当于在exports对象上添加属性
 * exports:{
 *  getNumber:getNumber
 * }
 */
 
 // 2.js
 var module = require('1.js');
 console.log(module); // {getNumber:fn}

CommonJS的实现

CommonJS只是一套规范,在了解了CommonJS的规范后,再来看下在nodejs中是如何实现这套规范的。

  1. nodejs只有在执行到require函数时才会加载该模块;
  2. nodejs在执行模块中的代码时,会将模块中的代码放到一个类似立即执行函数中执行避免污染全局变量;
  3. nodejs为了实现模块的导出,会在模块开始执行前初始化一个module.exports={}的对象,完成此对象的初始化后,会声明一个exports=module.exports的变量,最终将module.exports导出
(function () {
    module.exports = {};
    var exports = module.exports;
    // 模块中的代码
    return module.exports;
})()

也就是说exportsmodule.exports指向的是同一个对象的引用,因此可以有多种方法进行导出。

var count = 0;
function getNumber() {
    count++;
    return count;
}
// 相当于是在同一个对象上添加属性
module.exports.count = count;
exports.fn=getNumber; // {count:0,fn:Function}

// 当为module.exports重新赋值为一个对象时,module.exports和exports之间将不再是同一个对象的引用
console.log(module.exports === exports); // true
module.exports={
    count
}
console.log(module.exports === exports); // false
exports.fn=getNumber; // {count:0} 

推荐使用module.exports方式进行导出,实际上它的出现就是为了方便我们进行导出,使用module.exports方式导出比较灵活,可以根据需要导出字符串函数等,但使用exports的方式只能以对象的形式导出(默认导出的是module.export而不是exports

此外 nodejs默认开启了模块缓存,如果加载的模块已经加载过了则会自动使用之前的导出结果,避免反复加载同一个模块。

var module1=require("./1.js");
var module2=require("./1.js"); // 会复用之前的导出结果

require函数的原理

下面先写一段伪代码

    function require(modulePath) {
        // 1、根据传递的模块路径得到完整的绝对路径
        var moduleId = getModuleId(modulePath)
        // 2、判断是否有缓存
        if (cache[moduleId]) {
            return cache[moduleId]
        }
        // 3、真正运行模块代码的辅助函数
        function _require(exports, require, module, __filename, __dirname) {
            // 执行我们写得所有代码
        }
        // 4、准备并运行辅助函数
        var module = {
            exports: {}
        }
        var exports = module.exports;
        // 当前文件的绝对路径
        var __filename = moduleId;
        // 当前文件所在的文件夹的绝对路径
        var __dirname = getDirname(__filename)
        _require.call(exports, exports, require, module, __filename, __dirname)
        // 5、将结果缓存起来 缓存module.exports
        cache[moduleId] = module.exports
        // 6、返回module.exports
        return module.exports
    }

require函数中的第一步先把我们传递的相对路径解析为一个绝对路径,然后会判断是否有缓存也就是该模块是否 被重复引入,将我们所写的代码放到一个函数中与外界进行隔离,在运行该函数之前会对module.exports对象和exports变量进行声明并作为参数传入函数,这也就是为什么可以在require函数中直接使用它们的原因。然后调用该函数时将this指向为exports变量并把声明好的一些变量作为参数传入,所以在模块中就可以使用这些传递的参数,可以看出this、exports、module.exports这三个指向的都是同一个对象。

在模块中打印arguments验证一下发现形参就是我们上面传递的那些变量。

🎉 JavaScript模块化:让代码“各就各位”,告别“一锅粥”🍲

CommonJS与浏览器

了解了CommonJS的工作原理后我们可以知道CommonJS在node可以被很好的支持,那么能不能放到浏览器端实现模块化呢?答案是否定的,原因在于CommonJS是同步的, 在使用require导入一个模块文件时node会先通过模块路径找到对应本机中的文件,所以我们写得模块中的代码必须要等到加载完文件后才会向后继续执行。这个问题放在node中是没有太大问题的,因为node可以直接读取本机的文件速度比较快,但是浏览器端是需要对服务器发送请求的,因为开发完的项目会部署到远程服务器中,所以浏览器需要远程读取服务器中的资源,而网络传输的效率远远低于node环境中读取本地文件的效率,所以会极大影响性能效率。

ESModule

ESModule实现了浏览器端的模块化,主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于引入其他模块提供的功能。

ESModule规范

  1. 模块分为基本导入导出和默认导入导出,类似于CommonJS中的exports.xxxmodule.exports={}
  2. 在编译时就可以确认模块之间的依赖关系

基本导入导出

基本导出

ESModule中使用export关键字来进行基本的导出,类似与CommonJS中的exports可以导出多个,但后面必须是一个声明表达式或者是一个具名符号,也就是说必须有个名字不然不知道导出的是什么,导入的时候自然也不知道要导入什么。

export var a = 1;
export function test(){};

export 1; // 语法错误
var a = 1;
export a; // 语法错误

上面是声明表达式的写法,具名符号就是当要导出的变量比较多时可以进行集体导出。

var a = 1;
var b = 2;
export {a,b}; // 注意这里并不是导出一个对象而是语法上的需要

基本导入

基本导入的格式为imoprt {} from '模块路径',并且导入模块时这行代码必须放在所有代码的最前面,因为ESModule使用的是依赖预加载,如果不放在最前面的话也能正常运行,浏览器在编译代码时会自动帮我们把导入代码放到最上面,所以还是养成好的习惯。

console.log(a);
import {a} from './test.js'; // 将被浏览器自动提升

在导入的时候还有一些小细节:

  • 导入时可以通过as对导入的名称进行重命名;
import {a as a1} from './test.js;

  • 导入后的名称为常量,相当于是使用const进行了声明不可重新修改;
  • 当需要导入的变量比较多时可以使用*导入所有名称形成一个对象,然后使用as重新对其重命名;
import * as obj from './test.js

  • 如果只想单纯运行一个模块而不使用其导出的东西可以使用下面这种方式进行导入;
import './init.js';

默认导出导入

默认导出

默认导出类似于CommonJS中的module.exports,模块中仅允许有一个默认导出,由于只导出一个因此无需具名,具体的语法为export default 要导出的数据。可以和基本导出结合使用。

export var a = 1;
export default 2;

默认导入

默认导入使用import 变量名 from '模块路径'语法,由于是默认导出所以不可以使用别名。如果同时有基本导出和默认导出的值可以使用imoprt a,{b} from '模块路径'的方式。

// a.js
export var a = 1;
export default 2;

// b.js
import * as data from 'a.js';
console.log(data); // {a:1,default:2}

若使用的是导出所有名称并形成了一个对象(*)时,默认导出的值会被放到default属性当中。

CommonJS VS ESModule

  • CommonJS是在运行时加载,ESModule是在编译时加载,也就是说CommonJS是动态语法,ESModule是静态语法;
  • CommonJS使用require函数同步加载模块,ESModule使用import异步加载模块,有一个依赖预加载的阶段;
  • CommonJS中有缓存功能,ESModule则是动态加载;
  • CommonJS 模块输出的是一个值的拷贝所以是可变的,ESModule模块输出的是值的引用所以是只读的;
// =========CommonJS=========
// a.js
var name = "xxx";
module.exports=name;

// b.js
var b = require('./a.js');
setTimeout(()=>{
    b = "sss"
    console.log(b); // sss
},1000);

// =========ESModule=========
// a.js
export let name = 'sss';

// b.js
import {name} from './2.js';
name = "sss"; // TypeError: Assignment to constant variable.

由此可见CommonJS 模块输出的值是可变的,ESModule模块输出的是值是只读的。

写在最后

这篇文章从模块化的发展史开始,中间简要介绍了CommonJS和ESModule两种模块化,它们分别用于服务端和浏览器端,最后又基于以上的阐述对它们两者之间的区别进行了总结。

转载自:https://juejin.cn/post/7384274075231207462
评论
请登录