关于ES module我想说。。

1,013 阅读4分钟

前言

我们在项目中经常会使用import导入所需的依赖包,但是很少会去关注ES module的规则,上一篇讲了vite也是利用module特性使用浏览器解析import在服务器端按需编译返回来完成即时编译,省略打包步骤,看到这里你是不是也很想了解一下它,这篇就来讲一下ECMAScript中的module特性。

加载规则

我们想要实现模块化首先要知道浏览器加载ES6模块需要在script标签上加type=module属性。

<script type="module" src="./foo.js"></script>

浏览器对于带有type=modulescript标签,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了defer属性。async属性也可以打开,执行机制是只要该模块加载完成,就执行该模块。

ES6模块运行时加载

ES module还有个更强大的功能,因为import()接受一个参数,即加载的模块所在位置并返回一个Promise对象,所以有了这个函数我们就可以做更多的操作,比如按需加载、动态加载、条件加载:

// 正常加载
import main from './main'

// 条件加载
let pathName = 'main'
let flag = true
if(flag){  
    import(`./${pathName}.js`)  
    .then(module => {    
        console.log(module)  
     })  
    .catch(err => { 
        console.log(err)  
     });
}

异步加载js文件

因为浏览器渲染机制,渲染引擎加载到js脚本文件就会停下来等待执行完成后继续渲染,这样就会造成如果你的js文件执行过慢或者出现异常的时候,会造成页面卡死给用户带来非常不好的体验,所以浏览器允许异步加载js脚本,只需要在script标签上加defer或者async属性即代表开启异步加载。

<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>

这个时候又来问题了,那既然这两个属性都可以异步加载js脚本,它们又有什么区别呢?别急,让我们带着问号继续往下看。

defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

ES6模块与CommonJS模块的差异

既然说了ES module那自然也少不了CommonJS规范,我们知道nodejs基本是支持所有es6语法规则的,但唯独跟ES module不是那么友好,因为它有自己使用的CommonJS规范,这也一直是让前端纠结的一件事。我们先来看看CommonJS是怎么引入模块的:

// 导出
module.exports = {
    name: function() {
        console.log('jscodev')
    }
}

// 引入
var name = require('./main')

接下来我们在看一个例子:

// main.mjs
import { a, add } from './a.mjs'

console.log(a) // 1

add()

console.log(a) // 2



// index.js
let { a, add } = require ('./a.js')

console.log(a) // 1

add()

console.log(a) // 1

这个例子说明了CommonJS是执行时引入,因为CommonJS加载的对象只有在脚本执行完之后生成这样其实也就是在整个对象中拿出三个方法,因为只有运行时才能得到这个对象,导致完全没办法在编译时做静态优化。

下面看一下CommonJS输出模块的拷贝(属于浅拷贝):

// b.js
let count = 1
let add = () => {
  count++
}
setTimeout(() => {
  console.log('b1', count)
}, 1000)
module.exports = {
  count,
  add
}

// a.js
let a = require('./b.js')
console.log('a1', a.count)
a.add()
console.log('a2', a.count)
setTimeout(() => {
    a.count = 3
    console.log('a3', a.count)
}, 2000)

// 输出
a1 1
a2 1
b1 2  // 1秒后
a3 3  // 2秒后

总结一下就是:

  • 语法差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

  • CommonJS 是同步加载,ES6 模块是异步加载

nodejs里使用es6模块

nodejs想要处理es6模块必须把文件后缀改成mjs,或者在package.json里面配置type:module:

// package.json

{ 
   "type": "module"
}

需要注意的是一旦你配置了这个字段,那么该配置文件目录中的所有js文件将被解释使用es6模块。如果这时候还想用commonjs则把文件后缀改成cjs

还有一种做法是在package.json文件的exports字段,指明两种格式模块各自的加载入口。

// package.json

{
    "exports": {
      "require": "main.js",
      "import": "index.js"    
    }
}

结语

这篇文章对于模块化没有完全铺开讲,其实除了这两个还有其他的模块化比如/AMD/CMD等,不过一般我们了解ES6和CommonJS就足够应付大多任务了,对模块化感兴趣的小伙伴还可以再深入了解一下。