模块化发展史
js最开始只被用于实现页面中的一些小效果,ajax的出现,逐渐改变了js在浏览器中扮演的角色,不仅可以实现小的效果,还可以与服务器之间进行交互,以更好的体验来改变数据,js代码的数量开始增加,前端程序逐渐变得复杂
但是前端开发有几个问题没有解决,这些问题严重制约着前端程序的规模进一步扩大:
- 浏览器执行js的速度很慢
- 用户端的电脑配置不足
- 更多的代码带来了全局变量污染,依赖关系混乱等问题
08年,谷歌的v8引擎发布,将js的执行速度推上了一个新的台阶,个人电脑的配置开始飞跃,至此,制约前端程序发展的前两个问题得到了解决
09年,web服务项目nodejs被推出,js正式入主后端,此时人们认识到,js是一门真正的语言,它依附于运行环境(运行时)(宿主程序)而执行
nodejs的诞生,把js的最后一个问题放在了台前,即全局变量污染和依赖混乱问题;nodejs是服务器端的,如果不解决这个问题。分模块开发就无法实现,而模块化开发是所有后端程序必不可少的内容
经过社区的激烈讨论,最终形成了一个模块化方案,即CommonJS,该方案,彻底解决了全局变量污染和依赖混乱问题
该方案一出,立即被nodejs支持,nodejs就成为了第一个为js语言实现模块化的平台,为前端接下来的迅猛发展奠定了实践基础
因为CommonJs运用到浏览器端困难,于是很快推出了AMD,解决的问题和CommonJs一样,但是更好的适应浏览器环境,相继的,CMD规范推出,对AMD规范进行了改进
2015年,ES6发布,它提出了官方的模块化解决方案---ES6模块化
从此,模块化成为了JS本身特有的性质,成为了可以编写大型应用的正式语言
既然JS能编写大型应用,那么自然需要像其他语言那样有解决复杂问题的开发框架:
- angular、react、vue等前端开发框架出现
- express、koa等后端开发框架出现
要开发大型应用,自然少不了实用的第三方库的支持
- npm包管理器的出现,使用第三方库变得极其方便
- webpack等构建工具的出现,专门用于打包和部署
js在其他终端环境运行:
- electron发布,可以使用js开发桌面应用程序
- RN和Vuex发布,可以使用js编写移动端应用程序
- 各种小程序的出现,可以使用js编写依附于其他应用的小程序
CommonJS
背景
在nodejs中,由于有且仅有一个入口文件,开发一个应用肯定涉及到多个文件配合,因此,nodejs对模块化的需求比浏览器端大得多
模块的导出
模块:就是一个js文件,实现了一部分的功能,并隐藏了自己的内部实现,同时提供了一些接口供其他模块使用
模块的核心:隐藏和暴露,隐藏自己的内部实现,暴露希望外部使用的接口
暴露接口的过程即模块的导出
模块的导入
当通过某种语法或api去使用一个模块时,这个过程就是模块的导入
CommonJs规范
commonjs使用exports导出模块,require导入模块 具体规范如下:
- 如果一个文件中存在exports或require,则该js文件是一个模块
- 模块内的所有代码均为隐藏代码,包括全局变量,全局函数,这些全局的内容均不应该对全局变量造成污染
- 如果一个模块需要暴露一些api提供给外部使用,需要通过exports导出,exports是一个空的对象,可以为该对象添加任何需要导出的内容
- 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容
nodejs对commonjs的实现
为了实现commonjs规范,nodejs对模块做了以下处理
1、为了保证高效的执行,仅加载必要的模块,nodejs只有执行到require函数时才会加载并执行模块
2、为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置在一个函数中执行,以保证不污染全局变量
(function(){
//模块中的代码
})()
3、为了保证顺利的导出模块的内容,nodejs做了以下处理
- 在模块化开始执行前,初始化一个module.exports={}
- module.exports即模块的导出值
- 为了便捷导出,exports=module.exports
(function(){
module.exports={}
let exports = module.exports
//模块中的代码
return module.exports
})()
- 为了避免反复加载一个模块。nodejs默认开启模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果
基于上面nidejs对commonjs实现的第三条,我们的导出写法可以有:
exports.name='xxx'
module.exports.name='xxx'
//直接修改module.exports的指向,会导致module.exports和exports的指向不同
//在此句之后再exports.name='xxx',导出的内容中不会有name
module.exports={}
浏览器端模块化的难题
commonjs的工作原理
当使用require()导入一个模块时,node会做以下两件事情(不考虑模块缓存):
- 通过模块路径找到本机文件,并读取文件内容
- 将文件中的代码放入到一个函数环境中执行,并将执行后的module.exports的值作为require()的返回结果
可以认为,commonJS是同步的,必须要等到加载完文件并执行完代码后才能继续往后执行
当浏览器遇到commonJs
当想要把commonjs放到浏览器端时,遇到了一些挑战
- 浏览器加载js文件,需要远程从服务器读取,而网络传输效率远低于node环境中读取本地文件的效率,由于commonjs是同步的,这会极大的降低运行性能
- 如果需要读取js文件内容并把它放在一个函数环境中执行,需要浏览器厂商的支持,但是commonjs非官方标准,浏览器厂商不愿意提供支持
基于以上两点,浏览器无法支持commonJs
要想在浏览器端实现模块化,只要解决上面两个问题就行了
解决方案:
1、远程加载做成异步,加载完成后调用一个回调
2、编写模块时,直接放函数中
以上解决方案就是AMD、CMD的原理
AMD
AMD:Asynchronous Module Definition,异步模块加载机制
require.js实现了AMD规范
指定入口文件
<script data-main='入口文件路径' src='./require.js'></script>
原理:读取script的data-main的值,添加一个src为该值的script加入html文件
在AMD中,导入和导出模块的代码,都必须放置在define函数中
用法:
define(要导入的模块名称组成的数组,function(define函数第一个参数数组的各模块导出内容){
//此回调函数会等第一个参数数组中所有模块都加载完成才会执行,是异步的
//此函数的返回值作为本模块的导出内容
}
)
define(['A','B'],function(A,B){
return xxx
}
)
避免重复加载,第一次加载的模块会被缓存,再次加载则会自动使用之前的导出结果
CMD
CMD:Common Module Definition,公共模块规范
sea.js实现了CMD规范
指定入口文件
<script src='./sea.js'></script>
<script> seajs.use(入口文件路径)</script>
在CMD中,导入和导出模块的代码,都必须放置在define函数中
用法:
define(function(require,exports,module){
//导入导出和commonjs保持一致
//require()导入模块,exports和module.exports导出模块
//同步导入
const a = require()
//异步导入
require.async('a',function(a){})
//本模块的导出内容赋值给module.exports
module.exports={
}
})
避免重复加载,第一次加载的模块会被缓存,再次加载则会自动使用之前的导出结果
在CMD提出以后,require.js借鉴了sea.js,也实现了CMD规范,上述写法在AMD中也可用
ES6模块化
特点
- 使用依赖预声明的方式导入模块
- 依赖延迟声明(commonJs):
- 优点:在某些时候可以提高效率(条件语句判断是否加载依赖)
- 缺点:无法在最开始就确定模块的依赖关系
- 依赖预声明(AMD)
- 优点:在一开始就可以确定模块的依赖关系
- 缺点:某些时候效率较低
- 依赖延迟声明(commonJs):
- 灵活的导入导出方式
- 规范的路径表示法:所有的路径必须以./或者../开头
基本导入导出
浏览器引入es6模块文件
<script src='' type="module"></script>
基本导出
export 声明表达式 或者 export {具名符号}
export let a=123
export let fun = ()=>{}
export function fun1(){}
let b=1
export {b}
基本导入
import {要导入的具名符号} from '路径'
import {a,fun,b} from './xxx.js'
// 修改导入名称(别名)----使用as
import {a as myA,fun} from './xxx.js'
console.log(myA)
默认导入导出
每个模块,允许多个基本导出和一个默认导出,默认导出只有一个,无需具名
默认导出
export default 默认导出的数据或者export {默认导出的数据具名符号 as default}
export default function(){}
export default {
a:1,
b:2
}
let a = 123
export {a as default}
默认导入
import 接收变量名 from '路径',接收变量名自行定义,所以无别名一说
import anyName from './xxx.js'
其他细节
// 导入的数据是一个常量,进行修改会报错
import {a} from './xxx.js'
a='hello' //报错
//使用*号会将所有基本导出和默认导出聚合到一个对象中,默认导出会作为属性default存在
import * as all from './xxx.js'
//使用无绑定的导入执行初始化代码,只是想执行js文件
import './XXX.js'
//使用绑定再导出,来重新导出来自另一个模块的内容
//聚合多个模块的数据,统一在本模块导出
export {a,b},myData from './a.js'
//把b模块的所有内容全部导出,注意名称冲突
export * from './b.js'
//把a的默认值作为本模块默认值导出
export {a,b,default} from './a.js'
//把a的默认值作为本模块的基本导出
export {a,b,default as t} from './a.js'