前端模块化的前世今生

199 阅读7分钟

文章摘要:

  • 一、概念
  • 二、作用
  • 三、历史
    • 1.全局函数
    • 2.命名空间
    • 3.巧用闭包
    • 4.现代模块化机制
  • 四、现代模块化方案
    • 1.要解决的问题
      • 规避全局污染,变量冲突
      • 高内聚
      • 模块依赖已经循环引用等边界问题
    • 2.常用模块化方案
      • commonJS
        • 典型实践:nodeJS
      • ESModule
        • 典型实践:ES6
    • 3.commonJS 和 ESModule 差别
    • 4.commonJS 和 ESModule 加载机制
    • 5.script标签接轨模块化

一、概念

本质上模块就是一种对外要提供通信接口,对代码进行切分/组合的管理方式。

二、作用

  • 把复杂功能进行拆分(关注点分离)
  • 大型软件开发的思想基础,更好的内聚功能、方便复用
  • 方便多人协同、专注过程开发即可

三、历史

历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能。

发展阶段

  • 起初开发,只是一堆函数的堆砌,组织代码靠经验;
  • 后来挂载到对象身上,起到分离和内聚的作用。这种方式学名:命名空间;
  • 后来利用闭包使得污染的问题得到解决,内聚的更加纯粹;
  • 但是上边的都解决不了模块间依赖的问题,于是有了现代模块化机制。

四、现代模块化方案

js这门语言没支持模块化之前,社区制定了一些模块加载方案: CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。现如今 ES6 在语言层面上,实现了模块功能,而且书写简单,完全可替代前两种规范,成为浏览器和服务器通用的模块化解决方案,进而也就取代了UMD。

1.要解决的问题

  • 规避全局污染,变量冲突
  • 高内聚
  • 如何引入其他模块,如何暴露出api给其他模块使用
  • 模块间依赖以及可能出现的循环引用等边界问题

2.常用模块化方案

JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS,CommonJS 的目标是为浏览器之外的 JavaScript 指定一个生态系统。

CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()module.exports,ES6 模块使用importexport

它们采用不同的加载方案。从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。

2.1 commonJS

是一种思想、经典践行者:nodeJS

文件是一个模块,私有。内置两个变量 module 和 require (exports = module.exports)

一个引入一个导出,就构成了通信的基本结构

使用

m1.js文件:

导出某个方法

exports.fn1 = function(){}

导出一个类、对象


module.exports = Class1
module.exports = {fn1:function(){}}

导入模块

const aModule = require('./m1.js')

注意

  1. exportsmodule.exports的关系 exports 是 module.exports 的引用! module.exports指向堆的哪一块区域,则 exports 也跟着指向哪里!
console.log(module.exports === exports);//true

true其实就说明它们就是一个东西,其实exports = module.exports,因为他们是引用类型的一个变量名,所以当exports再指向一个新的引用类型的时候,那么他们就不再相等!

exports = [0, 1];
console.log(exports === module.exports);//false

所以就容易理解:

1.exports不进行赋值操作。(赋值后,也就斩断了和module.exports的链接,指向发生改变。)
2.exports常用来挂载一个属性。(方法或是值),exports.name = "utils"
3.赋值的行为一般都是给module.exports去完成,然后再 exports = module.exports

  1. 当nodejs执行模块中的代码时,它会将模块中的代码,用一个函数进行包裹:
function (exports, require, module, __filename, __dirname) {
    // 所以文件中可以直接使用,不报错!
}

2.2 ESModule

原生js也开始对模块化进行官方支撑。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块间的依赖关系,以及输入和输出的变量。

export 命令用于对外提供功能出口。 import 命令用于对内引入其他模块功能的入口。

使用

m1.js文件

导出某个方法

export function fn1(){}
//or
export const fn1 =()=>{}
//or
const fn1=()=>{}
export {fn1}//export fn1 是错误的

对应的导入写法

import {fn1} from './m1.js'//from指定模块文件的位置

关键字 import 导入模块(文件)有2种形式:

import ‘模块名称’ from ‘路径’; 
import ‘路径’;//此方式一般用来引入样式文件或预处理文件,因为他们不需要用变量来接收。

模块名称:等同于定义一个变量,用来存即将导入的模块。

路径:可以是绝对路径,也可以是相对路径,甚至是模块名称,文件后缀也可以省略。当你使用该命令时,系统会自动从配置文件中指定的路径中去寻找并加载正确的文件。

import Vue from "vue";//实际解析是 "../node_modules/vue/dist/vue.js";

JS文件可以向外输出变量、函数和对象。导出的含义是向外暴露,暴露多个需要{}包裹,暴露一个不用。 指定模块默认的输出

ps:一个模块仅允许导出一个default对象

export default function fnx() {}
//or
export default{fnx:()=>{}}

则对应的导入则省去 {};同时随便取个名字

import anyName from './m1.js'
//等价写法
import {default as anyName } from './m1.js'

两种导出都存在一个文件中时,对应的导入写法:

import anyName, {fn1} from './m1';

与 export 相比,export default 有以下几点不同:

  1. 在同一个模块中,export default 只允许向外暴露一次成员;
  2. 这种方式可以使用任意的名称接收,不像 export 那样有严格的要求;
  3. export 和 export default 可以在同一模块中同时存在。

注意

  1. 由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

// 报错
import { 'f' + 'oo' } from 'm1';


// 报错
if (x === 1) {
  import { foo } from 'm1';
} else {
  import { foo } from 'm2';
}

2.区分import()import

import()import主要区别为前者是动态加载,后者静态执行。
import()返回一个Promise对象
import()主要解决:按需加载、条件加载、动态的模块路径

import('./m1.js')
.then(({fn1, fn2}) => {
  // ...
});

3. commonJS 和 ESModule 差别

三大差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

    • 二者运行机制不一样导致的!
    • CommonJS 模块输出的是值的拷贝(给缓存起来了),也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
  • CommonJS 模块的require()是同步(阻塞式)加载模块,ES6 模块的import命令是异步(非阻塞式)加载,有一个独立的模块依赖的解析阶段。

  • CommonJS 模块的顶层this指向当前模块,ES6 模块之中,顶层的this指向undefined

第二个差异是因为 CommonJS 加载的是一个实打实的对象实体,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,所以导入的只是一种引入关系而已,在代码静态解析阶段就会生成!

4.commonJS 和 ESModule 加载机制

commonJS

CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

ES6 模块

ES6 模块是动态引用,如果使用import从一个模块加载变量(即import fn1 from './m1.js'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

5.script标签接轨模块化

意义

普通的script标签都是全局的,没有隔离的效果。支持ES6模块化后,意味着:这些script标签都是彼此独立的js模块。

实现

把script标签的type属性设为module,浏览器就知道这是一个 ES6 模块。

<script type="module" src="./m1.js"></script>

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性

ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

<script type="module">
  import fn1 from "./m1.js";

  //todo something
</script>

参考文章: Module 的语法 Module 的加载实现