模块化

68 阅读9分钟

模块化发展史

js最开始只被用于实现页面中的一些小效果,ajax的出现,逐渐改变了js在浏览器中扮演的角色,不仅可以实现小的效果,还可以与服务器之间进行交互,以更好的体验来改变数据,js代码的数量开始增加,前端程序逐渐变得复杂

但是前端开发有几个问题没有解决,这些问题严重制约着前端程序的规模进一步扩大:

  1. 浏览器执行js的速度很慢
  2. 用户端的电脑配置不足
  3. 更多的代码带来了全局变量污染,依赖关系混乱等问题

08年,谷歌的v8引擎发布,将js的执行速度推上了一个新的台阶,个人电脑的配置开始飞跃,至此,制约前端程序发展的前两个问题得到了解决

09年,web服务项目nodejs被推出,js正式入主后端,此时人们认识到,js是一门真正的语言,它依附于运行环境(运行时)(宿主程序)而执行

image.png

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导入模块 具体规范如下:

  1. 如果一个文件中存在exports或require,则该js文件是一个模块
  2. 模块内的所有代码均为隐藏代码,包括全局变量,全局函数,这些全局的内容均不应该对全局变量造成污染
  3. 如果一个模块需要暴露一些api提供给外部使用,需要通过exports导出,exports是一个空的对象,可以为该对象添加任何需要导出的内容
  4. 如果一个模块需要导入其他模块,通过require实现,require是一个函数,传入模块的路径即可返回该模块导出的整个内容

nodejs对commonjs的实现

为了实现commonjs规范,nodejs对模块做了以下处理

1、为了保证高效的执行,仅加载必要的模块,nodejs只有执行到require函数时才会加载并执行模块

2、为了隐藏模块中的代码,nodejs执行模块时,会将模块中的所有代码放置在一个函数中执行,以保证不污染全局变量

(function(){
    //模块中的代码
})()

3、为了保证顺利的导出模块的内容,nodejs做了以下处理

  1. 在模块化开始执行前,初始化一个module.exports={}
  2. module.exports即模块的导出值
  3. 为了便捷导出,exports=module.exports
(function(){
    module.exports={}
    let exports = module.exports
    
    //模块中的代码
    return module.exports
})()
  1. 为了避免反复加载一个模块。nodejs默认开启模块缓存,如果加载的模块已经被加载过了,则会自动使用之前的导出结果

基于上面nidejs对commonjs实现的第三条,我们的导出写法可以有:

exports.name='xxx'

module.exports.name='xxx'

//直接修改module.exports的指向,会导致module.exports和exports的指向不同
//在此句之后再exports.name='xxx',导出的内容中不会有name
module.exports={}

浏览器端模块化的难题

commonjs的工作原理

当使用require()导入一个模块时,node会做以下两件事情(不考虑模块缓存):

  1. 通过模块路径找到本机文件,并读取文件内容
  2. 将文件中的代码放入到一个函数环境中执行,并将执行后的module.exports的值作为require()的返回结果

可以认为,commonJS是同步的,必须要等到加载完文件并执行完代码后才能继续往后执行

当浏览器遇到commonJs

当想要把commonjs放到浏览器端时,遇到了一些挑战

  1. 浏览器加载js文件,需要远程从服务器读取,而网络传输效率远低于node环境中读取本地文件的效率,由于commonjs是同步的,这会极大的降低运行性能
  2. 如果需要读取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模块化

特点

  1. 使用依赖预声明的方式导入模块
    • 依赖延迟声明(commonJs):
      • 优点:在某些时候可以提高效率(条件语句判断是否加载依赖)
      • 缺点:无法在最开始就确定模块的依赖关系
    • 依赖预声明(AMD)
      • 优点:在一开始就可以确定模块的依赖关系
      • 缺点:某些时候效率较低
  2. 灵活的导入导出方式
  3. 规范的路径表示法:所有的路径必须以./或者../开头

基本导入导出

浏览器引入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'