「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。
很多小伙伴使用ESModules都是在Webpack的环境下使用的,如Vue、React项目中使用,以前是因为浏览器不支持,如今浏览器已经原生支持ESModules,我们是时候表演真正的技术了。
历史上,JavaScript一直没有自己模块体系(Module),无法实现模块化,即无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来的方式,其他语言如java、python等都具备这项功能,唯独Javascript没有,因为在早期设计javascript的时候并没想到web前端开发会发展到如今的规模,需要处理的业务会这么复杂。
为什么需要模块化开发
那么我们现在为什么需要模块化开发呢?不用会有什么问题?
如果是一个小型的应用,没有太复杂的业务逻辑,代码一眼能望到头,一个人就可以快速搞定,这种应用我们确实不需要模块化开发。但现在的前端考虑的东西变得越来越多,几乎都是具有复杂业务的多人协作项目,这种项目让我们不得不考虑以下几个问题:
- 如何组织代码?
- 如何避免冲突?
- 如何给团队成员分配工作?
- 所写的代码如何实现复用?
- 后期如何更好地维护与迭代?
很显然,以前那一套把所有代码写在一起的做法已经无法适用现有的项目开发,需要解决以上问题,就必须采用模块化开发。
前端模块化开发的发展历程
前面说了,javascript一开始时是没有自己的模块化体系的,NodeJS中虽然实现了commonJS规范的模块化,但却不适用于前端 ,一直以来,在前端要想做模块化开发,一般依赖第三方框架来实现,
- require.js
- sea.js
它们都是比较成功的第三方模块化解决方案,require.js采用的是AMD规范,sea.js采用的是CMD规范,由于两者功能高度重合,后来sea.js不再维护,从此淡出人们的视野,于是require.js一家独大,一度成为web前端必用的模块化方案。
直到2015年,随着ES6的正式发布,该版本ECMAScript推出了最激动人心人功能:ESModules模块化开发,且迅速成为前端模块化解决方案,从此原生javascript终于有了属于自己的模块化,成为最亮的仔,完全可以取代require.js与sea.js等第三方框架,现实情况也是如此,自从浏览器支持了ESModules,require.js停止了更新,乃至后来服务端的NodeJS也开始支持ESModules规范。
ESModules的使用
接下来我们一起来感受一下ES Modules的强大与其所拥有的魅力,我会从以下几个方面来讲解ES Modules:
ES Modules的用法- 浏览器支持情况
- 实际项目开发中的应用
ES Modules把一个文件当作一个模块,每个模块有自己的独立作用域,那如何把每个模块联系起来呢?核心点就是模块的导入(import)与导出(export)。
1、export导出模块
如要在其它模块中使用这里的数据,则必须导出,先来一波代码:
/**
* @模块A --moduleA.js
* 每个模块拥有自己独立的作用域
*/
// 这里的变量、方法只会在当前模块生效,不会影响全局作用域
let goodsName = 'huawei mate30 pro'
let qty = 5;
function getData(){
return {
goodsName,
qty
}
}
export default getData
在模块A中定义了一些变量与方法,然后将其导出。注意,export后只能跟以下关键字或符号:
- function
- class
- var
- let
- const
- default
- {}
通俗的理解,export的作用就是给当前模块对象添加属性,方便后期导入到其他模块中,export的用法如下:
// @如要在其它模块中使用这里的数据,则必须导出
// 1. 给模块对象添加default属性,值为getData函数
export default getData
// 2. 给模块对象添加myClass属性,值为一个类
export class myClass {}
// 3. 给模块对象添加a,b,c属性
export var a = 'xxx'
export let b = 10
export const c = 20
// 4. 给模块对象添加多个已声明属性(注意不是导出一个对象)
export {
goodsName,
qty,
// a:100 //不支持这种写法
}
其中 export default方法最常用,至于为什么,先卖个关子,接着我们来说说模块的导入,搞懂导入的方式,你自然就明白了。
2、import导入模块
import命令用于导入其他模块暴露的数据,格式:import <name> from <url>
如在模块B中导入模块A中的数据,代码如下:
/**
* @模块B --moduleB.js
* 要在当前模块导入其他模块数据,使用import
*/
// 1. 导入模块对象中的default属性
import getData from './moduleA.js'
import getGoods from './moduleA.js'
console.log(getData,getGoods);
// 2. 导入模块对象中的其他属性
import {myClass,a,b,c,goodsName,qty} from './moduleA.js'
到这大家应该已经看出来export default与其他的区别了,其实就是导入比较简单,且名字随意修改,不受导出模块的限制(代码中的getData和getGoods得到的都是模块A中的getData方法)。 那其它方式导出的属性就不能改名字了么? 其实ES Module早就帮我们考虑好了,一个神奇的关键字 as,用法如下:
// 导入模块属性时同时修改变量名
import {myClass as MyGoods,qty as goodsQty} from './moduleA.js'
console.log(MyGoods,goodsQty)
注意,当前模块声明的变量为MyGoods而不是MyClass,以上代码的意思时导入moduleA.js中的myClass属性并赋值给MyGoods变量,goodsQty同理,这样可以有效避免当前模块变量命名冲突的情况。
也可以一次性导入一个模块中所有属性,使用*符号,但必须使用as关键字,以下代码的作用就是导入moduleA中所有属性并赋值给all对象
import * as all from './moduleA.js'
注意事项
使用ESModules模块时还需要注意一些细节,
- 必须在服务器环境下使用(不能从浏览器直接打开html文件)
- url地址必须是相对路径或绝对路径,相对路径一定要以
./或../开头, - 只能导入js文件模块,且导入时必须填写
.js扩展名 - 只支持静态导入和导出
- 只可以在模块的最外层作用域使用import和export
避免项目中踩坑,请看以下代码
// 支持
import base from 'http://laoxie.com/js/base.js';
import base from '/js/base.js';
import base from './base.js';
import base from '../base.js';
// 不支持
import base from 'base.js';
import base from 'js/base.js';
import base from '../base';
let url = '../base.js'
import base from url;
if(a>0){
import base from url;
}
另外ES Module还具有的一些特点:
- ES Modules 导入的模块会被预解析,以便在代码运行前导入
// a.js
console.log("hello a.js")
import {msg} from './b.js'
console.log(msg);
// b.js
console.log('hello b.js')
export const msg = 'I am b'
// 运行a.js模块,会输出以下结果
/*
hello b.js
hello a.js
I am b
*/
- ESModules对模块的加载、解析和执行都是异步的
- 每一个模块内声明的变量都是局部变量, 不会污染全局作用域;
浏览器支持情况
ES Modules的浏览器支持情况怎么样,毕竟我们是要在浏览器环境中使用的,推荐一个网站(caniuse.com/)给大家,快速查看一些新特性的浏览器支持情况,ES Module的支持情况如下:
从上图中我们发现,PC端浏览器Chrome61(2017-9)、Firefox60(2018-5)开始支持ES Modules,IE直接不支持;移动端Android5、iOS11开始支持,以目前设备和浏览器版本来说,我们可以放心大胆地使用ESModules。
肯定有小伙伴会问,那如果要兼容低版本浏览器怎么办呢?有听过大名鼎鼎的babel么?接下来我们讲讲ES Module在实际开发中的使用以及我们是怎么兼容低版本浏览器的。
实际开发中的应用
1、在现代浏览器中直接使用
如果要在浏览器中直接使用,首先你的浏览器必须要支持ES Modules特性,另外script标签的写法有别于传统的写法,平时我们的写法如下:
<script type="text/javascript">
let goodsName = 'huawei mate30 pro'
let qty = 5;
</script>
这种传统写法,由于在全局作用域下声明变量,会让goodsName和qty成为全局变量,但如果type属性改成module 浏览器就会将代码视为 ES Modules 处理,代码如下:
<script type="module">
// 添加module属性,这里的声明的变量只在当前模块生效,不会成为全局变量
let goodsName = 'huawei mate30 pro'
let qty = 5
</script>
我们还可以引入其他模块到当前模块调用,代码如下:
<script type="module">
// 引入moduleA.js的模块的成员属性
// 利用as避免与当前模块的变量名冲突
import getData,{goodsName as name} from './moduleA.js'
let goodsName = 'huawei mate30 pro'
let qty = 5
</script>
当然,添加type="module"的script标签也支持使用src属性直接引入其他模块,代码如下:
<script type="module" src="./moduleB.js"></script>
2、在构建工具中的使用
要想让低版本的浏览器也能运行模块化的代码,需要在项目中使用babel这样的工具来进行编译,一般会配合webpack或gulp这样的构建工具,实现的原理其实很简单,就是把ES Modules的代码编译成浏览器支持的ES5代码,由于babel和webpack不在本文的讨论内容范围,大家可以自行查找相关资料,或持续关注我的其他文章,下面演示的是moduleA.js经过Babel编译后的代码:
从代码中大家可以看到,已经把ES Modules的代码编译成ES5代码,看不到import和export代码了,低版本浏览器自然就可以运行了。
后话
目前几乎所有的主流浏览器都已经支持了ESModule,做为开发者我们更应该往前看,不要死抓着那几个低版本浏览器不放,做到与时俱进,该放手时就放手,连NodeJS都已经支持ESModule了(详情请看《抛弃commonJS,在NodeJS中使用ESModules》)ESModules一统江湖的时间还远么