前端模块化演化阶段

模块化就是将一个复杂的应用程序,按照规范拆分成几个相互独立的文件;方便高效管理我们的代码及依赖。这在发展初期并不容易。

当开发的项目越来越大,代码复杂性也不断增加时,我们必须要思考如何去管理代码,很容易想到,模块化代码能帮我们.

模块化主要特点是:可复用性、可组合性、独立性、中心化

总的来说模块化帮我们解决了下面几个问题。

  1. 解决了命名冲突:因为每个模块是独立的,所以变量或函数名重名不会发生冲突
  2. 提高可维护性:因为每个文件的职责单一,有利于代码维护
  3. 性能优化:异步加载模块对页面性能会非常好
  4. 模块的版本管理:通过别名等配置,配合构建工具,可以实现模块的版本管理
  5. 跨环境共享模块

# 模块化演变

# 第一阶段 文件划分方式

每个功能单独一个文件存放;我们约定每一个文件即为一个单独模块;使用时将文件直接引入即可,每个script标签即对应一个模块。然后直接使用其暴露的成员即可。

前端发展初期,这样是够用的。随着前端发展及业务承载变得越来越复杂。也逐渐暴露出几个缺点

  1. 没有私有空间,会污染全局作用域,外部可修改内部成员及属性
  2. 命名冲突问题
  3. 无法管理模块依赖关系

# 第二阶段 命名空间方式

每个模块为一个对象,所有的成员均为此对象的属性,然后再暴露出此对象供引用使用。

这种方式解决了命名冲突的问题,但还是没有是有空间,外部依然可修改模块中的成员及属性;模块中的依赖关系也没有解决

# 第三阶段 立即执行函数 IIFE

利用立即执行函数提供的私有作用域,对于需要暴露的成员,我们可以通过模块内挂载到全局对象上。

此种方式解决了私有成员变量的概念,外部通过闭包方式访问模块变量,不能再修改模块内的属性。如此便确保了私有属性的安全。

另外,自执行函数声明可以传递参数。如此便解决的模块直接依赖关系的管理。

注意,以上实现方式均为原始js为基础,通过约定的方式来实现模块化的。他们依然存在一些问题。比如以下问题

  1. 不同开发者实现时因为个人习惯或者风格会有些许差别,这时候,就需要一套规范来统一。
  2. 模块引入均通过script标签手动引入,如果有依赖的模块,或者忘记引入,代码均不能正常运行,这就需要开发者处理此类问题
  3. 依赖管理会随着项目迭代与复杂化变得越来越难以维护。如不再使用的模块能否及时删除,模块的更新等等

这就催生了新的需求,能否通过标准代码管理模块,以实现自动的引入,删除,加载模块等通用功能。

即我们需要模块化规范和模块加载器。

# 第四阶段 模块化规范出现

# CommonJs规范

nodejs中使用的规范,他有如下几点约定。

  1. 一个文件就是一个模块
  2. 每个模块都有单独作用域
  3. 通过module.exports导出成员
  4. 通过require函数载入模块

注意,如果想在浏览器中使用Commonjs会有问题。由于node执行机制是在启动时加载模块,Commonjs是以同步方式加载模块的,这在浏览器中会导致大量同步加载阻塞进程。这样无疑效率是很低下的。

所以浏览器也有另外一套规范,AMD(Asynchronous Module Definition)。

# AMD规范

即异步加载模块规范,其中著名的Require.js这个库实现了此规范,同时Require.js也是非常优秀的加载器

define('module1', ['jquery", './module2'], function($, module2){
return {
    start: function () {
        $("body").animate({margin: '200px'})
        module2()
    }
}
})

amd 规范约定了每个模块通过define函数来定义模块,其中第一个参数为模块名称,第二个参数为依赖模块,第三个参数是一个函数,其中函数参数与第二个参数一一对应。此函数返回一个对象,供其他模块调用。

amd规范还提供了require函数来用于载入模块,当require需要加载模块是,其内部会自动创建script标签去发送请求并执行相应代码。

// 载入模块
require([' . /module1'], function (module1) {
    modulel.start()
})

目前绝大多数第三方库均支持AMD规范,其生态还是比较好的。

但相对来说,使用起来比较复杂,模块依赖及导出时要是用define require等函数。而且,当我们项目中模块划分比较细致,就会导致请求次数过多。

# 目前最佳实践

NodeJs中使用等CommonJs规范是其内置模块,目前成熟使用,没什么问题。

浏览器中等ESModule是ES6(2015)提出的规范。推出初期还存在兼容问题,随着webpack rollup等打包工具的使用,此规范也普及开来。近几年,浏览器各大厂商已开始原生支持此规范,我们可以放心使用。社区也已经有很多新玩具在推出。

所以目前流行的方案是浏览器中使用ES Module,NodeJS中使用CommonJs

# ES Module

# 基本特性

由于浏览器已经原生支持,我们可以直接在script标签中增加 type ='module'的属性,就可以执行其中的js代码了。

这样执行与原本标签执行有什么区别呢?

  1. 自动采用严格模式
  2. 每个模块均运行在单独的私有作用域中
  3. 通过CORS方式请求外部JS模块
  4. 延迟执行脚本,相当于加了defer属性

# 导入导出

简单说就是两个关键词exportimport

// ./module.js

const foo = 'es modules' 
const fun = ()=>{
  console.log(1)
}
export { foo,fun }

// ./app.js 
import { foo ,fun} from './module.is' 
console.log(foo)
fun()

可以看出来,使用非常方便。其中import函数需要几点说明

  1. import 不能导入变量路径,即import from 只能是字符串路径,而不能是变量定义的路径。
  2. import只能位于顶层,即如果想条件导入也是不行的
// 以下都是不行的
var path = './module.js'
import {name} form path
console.log(name)

if(true){
    import {name} form './module.js'
    console.log(name)
}

如果需要动态导入,则需要另一个函数

import('./module.js').then((module)=>{
   // dosomething(module)
}).catch(()=>{
    // dosomething()
})

这个函数可以在任意位置调用,返回值为promise;

# 总结

ES6的模块化语法和CommonJS、AMD等其他模块化语法有以下几点区别:

  1. 语法不同:ES6使用import和export语句来导入和导出模块,而CommonJS使用require和module.exports语句,AMD使用define和require语句。

  2. 加载方式不同:ES6使用静态加载,即在编译时就能确定模块的依赖关系,而CommonJS和AMD使用动态加载,即在运行时才能确定模块的依赖关系。

  3. 环境不同:ES6模块化语法主要用于浏览器和Node.js环境中,而CommonJS和AMD主要用于Node.js和浏览器环境中。

  4. 作用域不同:ES6模块化语法中的模块是单例模式,只会被加载一次,而CommonJS和AMD中的模块是每次加载都会创建一个新的实例。

  5. 功能不同:ES6模块化语法支持默认导出、命名导出和命名导入等多种导出和导入方式,而CommonJS和AMD只支持单一的导出和导入方式。

# 一些技巧

// 直接导出导入的模块
export {name,title} from './moduleB.js'
export {btn,val} from './moduleA.js'

常用于组织零散的模块,集中导出,方便其他地方引用。

# ES Module in Nodejs

随着ES Module的流行,NodeJs也在最近版本中增加了对esm对支持。当然由于esm与commonjs差异很大,目前只是并存过渡状态,是实验特性。

最近更新
01
echarts扇形模拟镜头焦距与可视化角度示意图
03-10
02
vite插件钩子
03-02
03
vite的依赖预构建
02-13
更多文章>