前端的模块

283 阅读7分钟

在没有模块化解决方案之前,前端工作者通常使用立即执行函数,命名空间等方式来解决命名冲突等问题,同时开发者想要引用别人的代码要么只能把别人的代码全盘拷到自己的代码里面,要么借助Ajax等工具去引入一些JS文件,这样一来会有两个问题:

  • 容易造成全局变量的污染
  • 如果有依赖关系,那么依赖关系的管理将会是一场灾难
  1. COMMONJS 随着node的诞生,JS可以写服务端的代码,自然就需要模块化开发,commonjs就诞生了,commonjs的显著特征
  • commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module;

  • 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require;

  • exports 和 module.exports 可以负责对模块中的内容进行导出;

  • require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容;

  1. 使用示例:
const sayName = require('./hello.js') 
module.exports = function say(){ 
    return {
        name:sayName(), 
        author:'xxxx'
        } 
}

  1. 原理 在编译的过程中,实际 Commonjs 对 js 的代码块进行了首尾包装,在每个模块文件上存在 moduleexportsrequire三个变量的原因就是commonjs将每个模块包装的同事将这三个参数通过形参的形式传进去,三个变量的意思
  • module 记录当前模块信息。
  • require 引入模块的方法。
  • exports 当前模块导出的属性 这个包装函数就是
    return '(function (exports, require, module, __filename, __dirname) {' + 
        script +       //script就是我们的代码
     '\n})'
}

不过这样是字串,在通过runInThisContext函数(可以理解成eval)进行执行

runInThisContext(modulefunction)(module.exports, require, module, __filename, __dirname)

3.加载流程

const fs =      require('fs')      // 核心模块
const sayName = require('./hello.js')  // 文件模块
const crypto =  require('crypto-js')   // 第三方自定义模块

核心模块的优先级仅次于缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载核心模块,加载过程中速度最快。 文件模块都会根据路径找到对应的文件,首次进行编译然后并缓存,这样二次遇到这个文件就直接读取缓存了 第三方自定义模块会在当前目录下找node_modules文件,找不到就回去根目录下找node_modules,知道找到项目的根目录下的node_modules

  1. require的引入和处理 CommonJS 模块同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖,采用深度优先遍历(depth-first traversal),执行顺序是父 -> 子 -> 父;

首先我们要明白两个感念,那就是 moduleModule

module :在 Node 中每一个 js 文件都是一个 module ,module 上保存了 exports 等信息之外,还有一个 loaded 表示该模块是否被加载。

  • false 表示还没有加载;
  • true 表示已经加载

Module :以 nodejs 为例,整个系统运行之后,会用 Module 缓存每一个模块加载的信息。

从上面我们总结出一次 require 大致流程是这样的;

  • require 会接收一个参数——文件标识符,然后分析定位文件,分析过程我上述已经讲到了,接下来会从 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容。
  • 如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。借此完成模块加载流程。
  • 模块导出就是 return 这个变量的其实跟 a = b 赋值一样, 基本类型导出的是值, 引用类型导出的是引用地址。
  • exportsmodule.exports 持有相同引用,因为最后导出的是 module.exports, 所以对 exports 进行赋值会导致 exports 操作的不再是 module.exports 的引用。实际这个是 js 本身的特性决定的。通过上述讲解都知道 exports , module 和 require 作为形参的方式传入到 js 模块中。我们直接 exports = {} 修改 exports ,等于重新赋值了形参,那么会重新赋值一份,但是不会在引用原来的形参。但是我们export.xxx = xxx这样写是可以,换句话说就是如果使用exports导出单个值之后,就不能在导出一个对象值,这只会修改exports的对象改变,然而修改无效,最终导出还是name,和sex,因为最终的导出是由module.exports决定的。
  1. 动态加载

require 可以在任意的上下文,动态加载模块。

2.AMD

commonjs默认只能在node环境下使用,不能在浏览器中直接使用。

根本原因是缺少moduleexportsrequireglobal四个模块。只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。

另外,由于CommonJS同步加载模块,这对于服务器端不是一个问题,因为所有的模块都放在本地硬盘。

但是,对于浏览器而言,它需要从服务器加载模块,涉及到网速,代理等原因,一旦等待时间过长,浏览器处于”假死”状态。所以在浏览器端,不适合于CommonJS规范

采用异步方式加载模块,模块的加载不影响它后面语句的运行。

RequireJS是一个非常小巧的JavaScript模块载入框架,是AMD规范最好的实现者之一。

模块功能主要的几个命令:definerequirereturndefine.amd

define是全局函数,用来定义模块,define(id?, dependencies?, factory)

require命令用于输入其他模块提供的功能

return命令用于规范模块的对外接口

define.amd属性是一个对象,此属性的存在来表明函数遵循AMD规范。

3.CMD

CMD(Common Module Definition - 通用模块定义)规范主要是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。

AMD类似,不同点在于:

  • AMD 推崇依赖前置、提前执行
  • CMD推崇依赖就近、延迟执行

/** sea.js **/ 
// 定义模块 math.js 
define(function(require, exports, module) { 
    var $ = require('jquery.js'); 
    var add = function(a,b){ 
        return a+b; 
    } 
    exports.add = add;
    }
); 
// 加载模块 
seajs.use(['math.js'], function(math){ 
    var sum = math.add(1+2);
    }
);

  1. ESModules

Nodejs 借鉴了 Commonjs 实现了模块化 ,从 ES6 开始, JavaScript 才真正意义上有自己的模块化规范,

Es Module 的产生有很多优势,比如:

  • 借助 Es Module 的静态导入导出的优势,实现了 tree shaking
  • Es Module 还可以 import() 懒加载方式实现代码分割。

Es Module 中用 export 用来导出模块,import 用来导入模块

  1. 静态语法 ES6 module 的引入和导出是静态的,import 会自动提升到代码的顶层 ,import , export 不能放在块级作用域或条件语句中

2.导入和导出

  • 默认导出 export default
  • 属性导出 export xxx
  • 导入方式
import theSay , { name, author as  bookAuthor } from './a.js'
import theSay, * as mes from './a'
  • 重署名导入
import {  name as bookName , say,  author as bookAuthor  } from 'module'
  • 动态导入
const promise = import('module')

import() 返回一个 Promise 对象, 返回的 Promise 的 then 成功回调中,可以获取模块的加载成功信息。

image.png 可以做为动态加载使用放在执行上下文或者条件语句中。

项目常用:

  • import() 可以实现懒加载,举个例子;
// vue 中的路由懒加载
[ 
    {    
        path: 'home',
        name: '首页', 
        component: ()=> import('./home') ,
    },
]
//react中使用
const LazyComponent = React.lazy(()=>import('./text'))
  1. ES6 模块提前加载并执行模块文件,ES6 模块在预处理阶段分析模块依赖,在执行阶段执行模块,CommonJS 模块同步加载并执行模块文件

  2. 不能修改import导入的属性

  3. tree sharking webpack中是在打包的时候tree sharking,esm是静态导入,所以在编译期间进行Tree Shaking,减少js体积

    ES Modules 之所以能 Tree-shaking 主要为以下四个原因(摘自尤雨溪在知乎的回答):

    5.1. import 只能作为模块顶层的语句出现,不能出现在 function 里面或是 if 里面。

    5.2. import 的模块名只能是字符串常量。

    5.3. 不管 import 的语句出现的位置在哪里,在模块初始化的时候所有的 import 都必须已经导入完成。

    5.4. import bindingimmutable 的,类似 const。比如说你不能 import { a } from ‘./a’ 然后给 a 赋值。

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

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。