闲聊前端模块化

131 阅读5分钟

前言

import * as foo VS import foo

之前在整理项目代码规范时突然发现下面两种引入 react 的写法:

import * as React from 'react';
// VS
import React from 'react;

实际上这两种写法都正常生效,但是好奇顺便就去了解了一下两者的区别,具体应该使用哪一种本文暂不讨论。

import * as Types from "./store/types";
import EEnv from "./store/types";
import foo from "./store/types";

console.log(Types);
// {
//     "ELevels": {
//         "1": "FIRSTLEVEL",
//         "2": "SECONDLEVEL",
//         "3": "THIRDLEVEL",
//         "FIRSTLEVEL": 1,
//         "SECONDLEVEL": 2,
//         "THIRDLEVEL": 3
//     },
//     "ETagType": {
//         "COLOR": "颜色"
//     },
//     "EEnv": {
//         "0": "DEV",
//         "1": "PROD",
//         "DEV": 0,
//         "PROD": 1
//     },
//     "default": {
//         "COLOR": "颜色"
//     }
// }
console.log(EEnv);
// {
//     "COLOR": "颜色"
// }
console.log(foo);
// {
//     "COLOR": "颜色"
// }
// 注意:如果这里我们不 export default ETagType, 那么 foo 就为 unfined 了

结论:

  • import * as foo 会导入导出文件中所有的 export export default
  • import foo 则只会导入 foo 这个变量,其他的不会导入进来

接下来就去了解下模块化的相关知识(其实是复习 ^_^)

前端模块化

什么是模块化

我们可以拆开来看,模块就是组成一个整体的一个部分,这些部分组装起来又成为这个整体;而模块化就是我们在项目中可以将其分为各个模块,以功能或者页面等来划分,这样我们就可以针对每个具体的模块去做一些事情而不影响其他模块。

模块化带来了什么

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

前端模块化发展

原生方式

  • 函数:按照功能封装为一个个函数
  • 对象:将具有某些共性的东西作为一个对象的属性
  • IIFE:将它们放入立即调用函数中(立即调用函数就可以形成一个封闭的空间)
  • window:其实可以归结为上面第二种,可以将一些全局变量存于 window

但是呢,我们通过上面这些方式隔离成一个个模块后,然后通过 script 标签引入:

<script src="./a.js"></script>
<script src="./b.js"></script>
<script src="./c.js"></script>
// ......

一开始学习 JS 的时候其实经常这样做(熟悉的感觉),不过学到后面我们其实知道它有一系列问题:

  • 每个模块都是一个 script 标签导入,如果是大型项目中那就有非常多,也就会有过多的请求
  • 如果模块间仅是简单的依赖关系倒还行,一旦复杂后就无法快速知道模块间的依赖关系,开发体验极度不友好
  • script 标签默认是通过同步的方式进行加载,除非有 async 和 defer 这些标签,这样会导致首屏时加长白屏时间
  • 如果是 asyncdefer 模式的时候,相互之间的加载顺序将无法确定

正是因为上述一系列的痛点,所以在后面的一段时间,各个模块规范也相继出炉!

AMD & CMD

两者都是异步加载机制且历史久远和已经被淘汰,加上笔者基本上也没用过,本文也只是稍稍带过。

使用:define 定义和 require 导入

代表工具库:

  • AMD: RequireJS
  • CMD: Sea.js

在浏览器执行时,RequireJS 会对每一个模块创建一个Script标签,同时加上 async 参数,加载完所有依赖的模块之后再通过 load 事件回调去执行我们最终输出的 JS 逻辑 (类似 Promise.all)。

虽然 AMD & CMD 的出现解决了原生 JS 组织模块的一些问题,但是这种方案的显著痛点就是必须得等到用户浏览器下载了对应的 require.js 或者 sea.js 文件之后,才能进行模块依赖关系的分析,等于说整个过程放在了线上去执行,这必然会延长前端页面的加载时间,影响用户体验,同时在加载过程中突然生出了众多 script 标签, http 请求也影响页面性能。所以它们自然而然被时代抛弃了。

Commonjs

Node.js 的使用规范,在 Node.js 模块系统中,每个文件都被视为独立的模块(对象),模块的本地变量将是私有的,因为在执行模块代码之前,Node.js 将使用 IIFE 的方式对其进行封装。

function (exports, require, module, __filename, __dirname) {}

特点:

  • 运行时输出的是一个值的拷贝
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
  • 模块加载的顺序,按照其在代码中出现的顺序。

使用:module.exports or exports 导出 & require (去拿 module.exports 的值) 导入

// a.js
exports.x = 2;
exports.y = 3;
moudle.exports = 1;

// b.js
import a from "./a.js";
// 此时 a 为 1, 因为 module.exports 已改变

ES6

ES6 模块的设计思想是尽量的静态化(不执行代码,从字面量上对代码进行分析),其模块依赖关系是确定的,在编译时就引入模块代码,和运行时的状态无关。

特点:

  • 编译时输出的是值的引用且不会像 commonjs 缓存值
  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量

使用:export or export dafault 导出 & import 导入

// a.js
export const a = 1;
const x = 2;
const y = 3;
export { x, y };
export default x;

// b.js
import a from "./a.js";
import x from "./a.js";
import obj from "./a.js";

正是因为 ES6 在编译时就导入了想要导入的值,使 tree-shaking 也得以实现,并且在编辑器也能直接点击查看他们的依赖关系,大大提升了维护性。