前端模块化演化阶段
模块化就是将一个复杂的应用程序,按照规范拆分成几个相互独立的文件;方便高效管理我们的代码及依赖。这在发展初期并不容易。
当开发的项目越来越大,代码复杂性也不断增加时,我们必须要思考如何去管理代码,很容易想到,模块化代码能帮我们.
模块化主要特点是:可复用性、可组合性、独立性、中心化
总的来说模块化帮我们解决了下面几个问题。
- 解决了命名冲突:因为每个模块是独立的,所以变量或函数名重名不会发生冲突
- 提高可维护性:因为每个文件的职责单一,有利于代码维护
- 性能优化:异步加载模块对页面性能会非常好
- 模块的版本管理:通过别名等配置,配合构建工具,可以实现模块的版本管理
- 跨环境共享模块
# 模块化演变
# 第一阶段 文件划分方式
每个功能单独一个文件存放;我们约定每一个文件即为一个单独模块;使用时将文件直接引入即可,每个script标签即对应一个模块。然后直接使用其暴露的成员即可。
前端发展初期,这样是够用的。随着前端发展及业务承载变得越来越复杂。也逐渐暴露出几个缺点
- 没有私有空间,会污染全局作用域,外部可修改内部成员及属性
- 命名冲突问题
- 无法管理模块依赖关系
# 第二阶段 命名空间方式
每个模块为一个对象,所有的成员均为此对象的属性,然后再暴露出此对象供引用使用。
这种方式解决了命名冲突的问题,但还是没有是有空间,外部依然可修改模块中的成员及属性;模块中的依赖关系也没有解决
# 第三阶段 立即执行函数 IIFE
利用立即执行函数提供的私有作用域,对于需要暴露的成员,我们可以通过模块内挂载到全局对象上。
此种方式解决了私有成员变量的概念,外部通过闭包方式访问模块变量,不能再修改模块内的属性。如此便确保了私有属性的安全。
另外,自执行函数声明可以传递参数。如此便解决的模块直接依赖关系的管理。
注意,以上实现方式均为原始js为基础,通过约定的方式来实现模块化的。他们依然存在一些问题。比如以下问题
- 不同开发者实现时因为个人习惯或者风格会有些许差别,这时候,就需要一套规范来统一。
- 模块引入均通过script标签手动引入,如果有依赖的模块,或者忘记引入,代码均不能正常运行,这就需要开发者处理此类问题
- 依赖管理会随着项目迭代与复杂化变得越来越难以维护。如不再使用的模块能否及时删除,模块的更新等等
这就催生了新的需求,能否通过标准代码管理模块,以实现自动的引入,删除,加载模块等通用功能。
即我们需要模块化规范和模块加载器。
# 第四阶段 模块化规范出现
# CommonJs规范
nodejs中使用的规范,他有如下几点约定。
- 一个文件就是一个模块
- 每个模块都有单独作用域
- 通过module.exports导出成员
- 通过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代码了。
这样执行与原本标签执行有什么区别呢?
- 自动采用严格模式
- 每个模块均运行在单独的私有作用域中
- 通过CORS方式请求外部JS模块
- 延迟执行脚本,相当于加了defer属性
# 导入导出
简单说就是两个关键词export
和import
// ./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函数需要几点说明
- import 不能导入变量路径,即import from 只能是字符串路径,而不能是变量定义的路径。
- 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等其他模块化语法有以下几点区别:
语法不同:ES6使用import和export语句来导入和导出模块,而CommonJS使用require和module.exports语句,AMD使用define和require语句。
加载方式不同:ES6使用静态加载,即在编译时就能确定模块的依赖关系,而CommonJS和AMD使用动态加载,即在运行时才能确定模块的依赖关系。
环境不同:ES6模块化语法主要用于浏览器和Node.js环境中,而CommonJS和AMD主要用于Node.js和浏览器环境中。
作用域不同:ES6模块化语法中的模块是单例模式,只会被加载一次,而CommonJS和AMD中的模块是每次加载都会创建一个新的实例。
功能不同: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差异很大,目前只是并存过渡状态,是实验特性。