前言
任何事物的产生都有他的必然性,就像是冥冥之中注定了一样,在JavaScript
刀耕火种的时代,前端是被定义为切图的一项工作,页面逻辑
与交互
全部由服务端工程师完成,前端开发几乎不受服务端开发重视
,那时候也没有很好的制定前端的规范,原因归结于并没有想到现如今前端的快速发展与规模,现如今前端开发已经在web
新时代占据了半壁江山
,在前端演变的过程中,社区规范也在不断的完善,那在前端的发展过程中,前端模块化究竟经历了什么样的变革
呢,这将是我们这一章将要探索的问题。
模块化的概念
站在现在的角度来解释模块化
,我相信同学们都能够说出一二:模块是一个具有某种功能,能够解决某种问题的独立区块,模块化则是由多个模块组合,相互协调解决某类问题的一种方式
模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。–来自百度百科,
早期的模块化
上面说到,早期前端只是个"切图工程师",模块化
的概念产生,还是源自于前后端交互的媒人 – AJAX
,因为AJAX
得以让前后端分离,经过数年的发展,业务系统逐渐庞大,前端逻辑变得愈加复杂,那时候模块化表现在一个文件就是一个模块,通过script
标签引入多个文件就能使用,一般一个脚本文件几千行代码,其中就会导致一些问题的产生:
- 超多全局变量产生问题。
- 变量、函数名冲突问题。
- 无法管理模块与模块之前的依赖关系。
- 其他问题。
出现了上述问题怎么办,接着社区规范继续约定,每一个文件只允许暴露出一个全局对象,文件里的所有属性都必须暴露在对象里面,以达到解决重复的问题。
紧接着社区又约定了一种IIFE
的方式,也就是在第二种的形势下,在外层包裹了一层自执行函数。
这一种方式所带来的好处是什么呢?
- 因为是在自执行函数里面执行的,也就是利用
闭包
的特性,形成了私有
的属性成员,能够很好的避免全局变量
的污染。 - 自执行函数可以传参,那也就很好的描述了模块与模块之间的
依赖
关系,使得模块管理变得可行。
上述三种方式为最早的社区约定的模块化方式,确实能够解决一些问题,但是同学们有没有发现,这三种方式都是通过script
标签进行引入到html
页面的,也就是同步
加载的,如果在很复杂的具有多个请求的场景下使用的话,那么这些ajax
将会是同步发起的,会带来很大的性能问题
,为了解决这个加载方式
的问题,社区又提出了一个新的规范,那就是AMD (Asynchronous Module Definition)
。
模块化的青铜器时代
AMD的设计灵感来自于以往开发者的约定俗成的规范的统一:
- 灵感一:用
script
引入一个文件到页面中,其余模块能不能够用代码控制按需加载。 - 灵感二:前端行业蒸蒸日上,能不能设计一个符合行业的统一的规范。
- 灵感三:设计的基础库应该能够实现模块的自动加载。
- 灵感四:可以借鉴于
Common.js
实现的模块化规范。
既然这里提到了Common.js
规范,那么就顺带的说一下:
Common.js
规范Common.js
规范,是Node.js
中所遵循的模块化规范,该规范规定,每一个文件就是一个模块,每个模块有自己单独的作用域,通过module.exports
导出成员,通过require
导入另外的模块。
因为Common.js
加载模块的方式为同步加载,所以它并不适用于在浏览器环境,所以综上所述AMD
规范就产生了,也产生了一个符合AMD
规范的库require.js
。
扩展
为什么node.js
使用同步
加载不受影响?而浏览器中就不能使用同步
加载?
因为在
node.js
中模块分为内置模块
、自定义模块
、第三方模块
三种,node.js
的加载方式为启动式加载(启动式加载:系统根据配置自行去加载依赖的行为),根据机制,node.js
在运行时只是去使用
模块,所以同步加载并不会给node.js
造成性能
问题。但是在浏览器端如果也是采用同步加载的策略
,那么将会产生很多的同步请求
,这样应用程序的执行效率将会变得很低很低
。
AMD基础的实践
AMD
的基础实践需要用define()
来实施,require.js
则通过require
函数进行模块加载。
// 如何使用AMD定义一个模块?
define(['module_1.js', 'module_2.js'], function(module_1, module_2){return { morning: function(){ sayHello();// module_1.js中定义的函数 callHello();// module_2.js中定义的函数 }}
})
// require.js
require(['module_1.js'], function(module_1){sayHello();// module_1.js中定义的函数
})
上述代码表明,AMD
不仅可以定义模块,还可以使用模块。require.js
只是模块加载使用。这种加载方式的原理也就是当页面需要的时候,创建了一个script
区引入模块到页面中。但是这一种方式和IIFE
的方式并没有什么很大的差别。
现代模块化规范
经过几年的发展,前端模块化规范逐渐趋于完善,最后也形成了统一:
Node.js
遵循Common.js
规范。- 浏览器遵循
ESM
规范。
ESM
是ECMAScript2015(es6)
才被制定出来的一个统一规范,因为前端必须跟浏览器打交道,在推出之初并不是所有浏览器都支持的,于是乎webpack
等一些比较强大的工具的产生,就解决了兼容性
的问题。
ESM的特性
- 自动采用严格模式(在严格模式中
this
不会默认指向window
对象)。 - 每个
ESM
都有单独的私有作用域。 ESM
是通过CORS
去请求外部js
模块的,被访问的地址需要支持CORS
。ESM
的script
标签会延迟执行脚本,不会阻塞浏览器渲染,相当于加了defer
属性。ESM
的模块导入导出。
// 直接使用export关键词导出想暴露给其他模块的成员
export const word = "hello"
export const fn = () => {}
// 先声明,再集中导出
const word = "hello"
const fn = () => {}
export { word, fn }
export default fn // 将成员导出为默认成员
// 导出时重命名,以及将某个成员作为默认导出成员
export { word as wordRename, fn as default }
import { word } from './code1.js' // 导入命名成员
import fn from './code1.js' // 导入默认成员
// 重命名
import { word as wordNewName } from './code1.js' // 导入时重命名
import { default as fn } from './code1.js' // 给默认成员重命名
// 同时导入命名成员和默认成员的两种方式
import fn, { word } from './code1.js'
import { word, default as fn} from './code1.js'
import * from '/code1.js' // 导入子模块的所有成员
import '/code1.js' // 仅执行子模块,不需要使用子模块导出的成员
import('./code1.js').then(function(module) {}) // 动态加载模块
扩展
关于script
标签加载模块,会阻碍浏览器UI
线程执行,我们一般可以给script
标签加上defer
/ async
,Mdn
上面也有关于模块延迟加载的介绍:
- 单独设置
defer
,浏览器解析到模块的时候会与UI
线程并行处理,解析完毕之后在UI
线程空闲的时候继续执行。不会阻碍UI
线程渲染。 - 单独设置
async
,浏览器解析到模块的时候会与UI
线程并行处理,解析完毕之后会立即加入到UI
线程执行,会阻碍UI
线程渲染。 - 如果加上了
type='module'
字段,则会和defer
达到一样的效果。
扩展
Common.js
规范与ESM
的区别总结,参考ECMAScript 6 入门
CommonJS
模块输出的是一个值的拷贝,ESM
模块输出的是值的引用。* 因为CommonJ
输出的值是一个静态值,而且这个值会被缓存
到,所以每一次获取到的都是缓存值,改变原来的值,引用值不会改变。ESM
输出的是一个只读引用,模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。CommonJS
模块是运行时加载,ESM
模块是编译时输出接口。* 因为CommonJS
加载的是一个对象(即module.exports
属性),该对象只有在脚本运行完才会生成。而ESM
模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS
模块的require()
是同步加载模块,ESM
模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。
模块化打包工具
因为最新的模块化规范依然存在着浏览器兼容
的问题,并且前端模块化划分出的文件过多
,诸多文件全部是来自于网络请求
,这无疑是加重了浏览器的工作负担
,进而影响到了开发者的工作效率
。再者模块化并不是JavaScript
文件仅仅独有的,在前端项目日益复杂的过程中,Html
、Css
也会面临着模块化的趋势。面对这些问题,我们应该会有一些想法:
- 想法一:
高版本
的语法代码能不能通过某种办法编译成低版本
的代码,以达到兼容
的效果。 - 想法二:诸多文件编译后能不能只生成
一个
或多个
入口文件,以此来降低
网络请求,降低部分浏览器压力。 - 想法三:对于
不同类型
的文件,能不能通过某种方式处理成模块,以便于当做模块使用。
如果能够满足上述的三个想法,这样的工具也就满足了社区的规范的统一
,所有的模块的加载都可以通过代码进行控制
。所以我们要介绍的主角webpack
就诞生了。
扩展
你觉得webpack
它的定位是什么呢?
总结
这一章我们探索了前端模块化的演变历史,也学习了Common.js
规范与ESM
规范的落地实践,初步揭开了webpack
打包工具的神秘面纱,下一章我们将去学习webpack
到底解决了目前前端开发中遇到的什么问题,webpack
又是怎么样去解决的
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享