前端模块化

自从 JavaScript 诞生以来,人们一直将它作为一种网页脚本语言,一般只用作表单校验跟页面特效,由于被仓促的创造出来,所以它自身的各种陷阱和缺点也被编程人员所诟病。随着 Web2.0 的流行,各种前端库和框架被开发出来,随后随着更多的用户需求在前端被实现,JavaScript 也从表单校验跃迁到应用开发的级别上。在这个过程中,它大致经历了工具类库、组件库、前端框架、前端应用的变迁。

随着业务发展,为了能够更好的组织业务逻辑,JavaScript 有了模块化的需求。

一般的,我们在 JavaScript 中通过 script 标签引入代码的方式来实现模块化,但这样往往缺乏组织能力跟约束能力,对于开发者而言需要人为的对代码进行约束,避免变量、函数冲突等问题。


模块化的理解

什么是模块化

  • 将代码按照功能进行封装,遵循单一职责。
  • 模块内部数据私有, 通过向外暴露的变量、方法与外部通信。

模块化的好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化方案

  • 全局 function 模式,根据功能将模块封装成不同的全局函数。
    弊端:污染全局命名空间,容易造成命名冲突,模块之间的关系模糊
    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    funtion defaultValue(){
    return {
    a: 2
    }
    }

    function sum(a, b){
    return a + b
    }

    const value = defaultValue();
    const a = sum(value.a, 2)
  • nameSpace 模式,对对象进行封装。减少全局变量,解决命名冲突。
    弊端:数据不安全,外部可以直接对模块内的数据进行修改
    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    window.__commonFuc = {
    a: 1,
    defaultValue(){
    xxx
    },
    sum(a,b){
    return a + b
    }
    }

    const module = window.__commonFuc
    const data = module.defaultValue()

    console.log(module.x) // 1
    module.x = 2 // 模块内的数据被修改
  • IIFE 立即执行函数,数据是私有的, 外部只能通过暴露的方法操作。通过闭包实现
    弊端:无法处理模块相互依赖的场景
    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    (function(window){
    var x = 1;

    function api(){
    xxx
    }

    function setX(v){
    x = v
    }

    function getX(){
    return x
    }

    window.__Module = {
    x,
    setX,
    getX,
    api,
    }
    })(window)

    const m = window.__Module

    // 修改函数作用域内变量的值
    m.setX(10)
    console.log(m.getX()) // 10

    // 这里改的是对象属性的值,不是修改的模块内部的data
    m.x = 2
    console.log(m.getX()) // 10

模块化规范

CommonJs

概述

CommonJs 规范主要用于 Node.js,每一个文件就是一个模块,有自己的作用域,每个文件中的变量、函数、类都是私有的。

特点

  • 导出的是值的拷贝
  • 每个文件就是一个模块,独立作用域,不污染全局作用域
  • 根据导入的顺序加载
  • 支持缓存,模块可以多次加载,但只会在首次加载时运行一次

使用

  • 导出:使用 module.export 导出,也可以简写为 exports。CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports属性。

    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // module.export 导出
    function add(a, b) {
    return a + b;
    }
    module.export = {
    add: add,
    };

    // exports 导出
    export.add = function(a, b){
    return a + b;
    }

  • 导入:使用 require 同步加载模块

    COPY
    1
    2
    3
    // 导入
    var math = require("./math");
    math.add(2, 5);
  • 模块重命名

    COPY
    1
    2
    // 使用新的求和方法 newAdd
    let { add: newAdd } = require("./math");

加载机制

CommonJS 模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。这部分跟 ES6 模块化有所不同。在服务器端运行时同步加载,在浏览器端提前编译打包处理方式

COPY
1
2
3
4
5
6
7
8
9
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
COPY
1
2
3
4
5
6
7
// main.js
var counter = require("./lib").counter;
var incCounter = require("./lib").incCounter;

console.log(counter); // 3
incCounter();
console.log(counter); // 3

在执行 incCounter 前后,lib.js 中的 counter 始终为 3,可以得知 CommonJS 输出的是值的拷贝。


ES6 模块化

概述

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export 和 import。export 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

特点

  • 浏览器环境默认支持
  • 输出的是值的引用
  • 动态引用,不会缓存值
  • 静态化,编译时就能确定模块间的依赖关系,以及输入输出的变量,可以 tree shaking

使用

  • 导出:使用 export 导出
COPY
1
2
3
4
5
6
// math.js
var total = 10;
var add = function (a, b) {
return a + b;
};
export { add, total };
  • 导入:使用 import 导入
COPY
1
import { add, total } from "./math";
  • 模块重命名
COPY
1
import * as newAdd from "./math";

加载机制

模块加载方式取决于所处的环境,Node.js 环境中同步加载,浏览器端异步加载

原理

ES Module 从加载入口模块到所有模块实例的执行主要经历了三步:构建、实例化和运行。
在构建时,根据 import 分析模块间的依赖关系,这点不同于 CommonJS,在浏览器中运行时加载会主线程长期被占用,ES Module 在构建过程不会实例化和执行任何的js代码,也就是所谓的 静态解析 过程。

但是但是,在日常开发中我们发现以CommonJs模式引入的插件也可以跑在浏览器中,这是为什呢?

概括的来说,webpack会根据webpack.config.js入口文件识别模块依赖,统一对模块进行分析,通过转换、编译,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的 webpack_require(es5代码),所以打包后的文件可以跑在浏览器上。即 webpack会对各种模块进行语法分析,并做转换编译

来看看webpack对此做了些什么:

  • 简单的对webpack进行配置:

    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // webpack.config.js
    const path = require('path');

    module.exports = {
    mode: 'development',
    entry: './src/main.js',
    output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, './dist'),
    }
    };

  • 创建一个EsModule格式的模块并导入

    COPY
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    // src/add
    export default function(a, b) {
    let { name } = { name: 'hello world,'};
    return name + a + b;
    }

    // src/main.js
    import add from './add'
    console.log('求和', add(1, 2))

  • 打包完成后的bundle.js文件输出如下:

COPY
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

// modules是存放所有模块的数组,数组中每个元素存储{ 模块路径: 模块导出代码函数 }
(function(modules) {
// 模块缓存作用,已加载的模块可以不用再重新读取,提升性能
var installedModules = {};

// 关键函数,加载模块代码
// 形式有点像Node的CommonJS模块,但这里是可跑在浏览器上的es5代码
function __webpack_require__(moduleId) {
// 缓存中存在则直接导出
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 否则新建
var module = installedModules[moduleId] = {
i: moduleId,
l: false, // 标记是否已经加载
exports: {} // 初始模块为空
};

// 把要加载的模块内容,挂载到module.exports上
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.l = true; // 标记为已加载

// 返回加载的模块,调用方直接调用即可
return module.exports;
}

// __webpack_require__对象下的r函数
// 在module.exports上定义__esModule为true,表明是一个模块对象
__webpack_require__.r = function(exports) {
Object.defineProperty(exports, '__esModule', { value: true });
};

// 启动入口模块main.js
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
// add模块
"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {
// 在module.exports上定义__esModule为true
__webpack_require__.r(__webpack_exports__);
// 直接把add模块内容,赋给module.exports.default对象上
__webpack_exports__["default"] = (function(a, b) {
let { name } = { name: 'hello world,'}
return name + a + b
});
}),

// 入口模块
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
__webpack_require__.r(__webpack_exports__)
// 拿到add模块的定义
// _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports,有点类似require
var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");
// add模块内容: _add__WEBPACK_IMPORTED_MODULE_0__["default"]
console.log(_add__WEBPACK_IMPORTED_MODULE_0__["default"], Object(_add__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
})
});

至此,打包后的代码直接跑在浏览器中,是因为webpack通过__webpack_require__ 函数模拟了模块的加载,并使用module.exports 导出。

import()

import()函数在 ES2020 增加的可以异步动态加载模块的机制,import()函数与所加载的模块没有静态连接关系,所以不能进行静态解析,也不能进行tree shaking,这点也是与import语句不相同之一。
import()的返回值是promise对象,可以使用 .then.catch方法进行接收数据处理,import()加载模块成功以后,这个模块会作为一个对象,当作 then 方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
允许模块路径动态生成
import() 可以放在任何地方,因为它是运行时执行的,什么时候执行到它,就什么时候进行指定模块的加载,所以它可以在条件语句和函数中进行动态的加载。

COPY
1
2
3
4
5
6
7
if(true){
return import('./xxx/aaa').then(msg=>{
//加载内容 不会报错
}).catch(err=>{
//error codo
})
}

简单使用:

  • 定义一个需要按需加载的模块 a.js

    COPY
    1
    2
    3
    export default function a() {
    console.log('我是模块 a');
    }
  • 引入模块 a

    COPY
    1
    2
    3
    import('./a').then(({ default: a }) => {
    console.log(a);
    });
作者: 果汁
文章链接: https://guozhigq.github.io/post/2b2f44ba.html
版权声明: All posts on this blog are licensed under the CC BY-NC-SA 4.0 license unless otherwise stated. Please cite 果汁来一杯 !