• 欢迎访问新概念博客,研究javascript,css3,html5,nodejs,Ext js等技术研究,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入新概念博客

浏览器原生支持ES6 export和import模块

程序人生 新概念 来源:zhangxinxu 2年前 (2022-09-22) 3241次浏览 0个评论 扫描二维码

JS 中直接import其他模块是个很棒的能力,ES6 规范中就提供了这样的特性。然后,长久以来,都只有在 Node.js 中才能无阻使用,浏览器都没有原生支持。

Node.js 对于我而言,就像是个在另外一个城市结交的好朋友,简单了解,能和睦相处即可,因此,Node.js 支持import功能,就好像朋友升职赚了大钱一样,替他开心,不过也就只是替他开心,自己其实还是淡然的。但是,web浏览器就不一样了,这个可是我打算厮守一生的伴侣,因此,web浏览器原生支持import功能,那就好像自己的老婆升职赚了大钱一样,那比自己赚了大钱还开心,心中一百个“万岁”。

ES6 在浏览器中的 import 功能分为静态 import动态 import

其中静态 import 出现更早,浏览器兼容性更好,支持浏览器包括:Safari 10.1+,Chrome 61+,Firefox 60+,Edge 16+。

浏览器原生支持ES6 export和import模块

动态 import 支持晚一些,兼容性要差一些,目前 Chrome浏览器和 Safari 浏览器支持,不过相信很快其他浏览器也会跟进。

浏览器原生支持ES6 export和import模块

更新于 2022-01-09
目前的兼容性:
浏览器原生支持ES6 export和import模块

——-更新 end——–

本文会对这两种模块导入都做介绍,因此,本文内容篇幅较长,且有一定深度,需要预留较多时间阅读。

二、静态 import

我们先从最简单的案例说起,例如,我想想,demo 比较方便演示的效果,啊,那就实现改变<p>元素的文字颜色。

主页面相关 script 代码如下:

<script type="module">
  // 导入 firstBlood 模块
  import { pColor } from './firstBlood.mjs';
  // 设置颜色为红色
  pColor('red');
</script>

然后 firstBlood.mjs 文件中代码为:

// export 一个改变<p>元素颜色的方法
export function pColor (color) {
  const p = document.querySelector('p');
  p.style.color = color;
}

您可以狠狠地点击这里:浏览器原生 import 实现文字变红 demo

可以看到<p>文字变红了:

浏览器原生支持ES6 export和import模块


有了案例,下面基础知识就更好消化与理解了。

  • 对于需要引入模块的<script>元素,我们需要添加type="module",这个时候,浏览器会把这段内联 script 或者外链 script 认为是 ECMAScript 模块。
  • 模块 JS 文件,业界或者官方约定俗成命名为.mjs文件格式,一来可以和普通 JavaScript 文件(.js后缀)进行区分,一看就知道是模块文件;二来 Node.js 中 ES6 的模块化特性只支持.mjs后缀的脚本,可以和 Node.js 保持一致。当然,我们直接使用.js作为模块 JS 文件的后缀也是可以的。在浏览器侧进行 import 模块引入,其对模块 JS 文件的 mime type 要求非常严格,务必和 JS 文件一致。这就导致,如果我们使用.mjs文件格式,则需要在服务器配置 mime type 类型,否则会报错:

    Failed to load module script: The server responded with a non-JavaScript MIME type of “”. Strict MIME type checking is enforced for module scripts per HTML spec.

    浏览器原生支持ES6 export和import模块

    Nginx 对于不识别后缀默认会给一个application/octet-stream的 MIME type,方便下载等处理,但是,不好意思,在模块化引入这里,这个 MIME type 无效,需要足够精准才行,为application/javascript,然后根据自己测试,IIS 服务器中application/x-javascript也是可以的。

    无论是 Apache 服务器还是 Nginx,都可以修改 mime.types 文件使.mjs的 MIME type 和.js文件一样。


除了export普通的function,我们还可以export const或者其他任何变量或者声明。也支持default命令。再看下面一个例子,<p>文字变红,以及垂直翻转,演示constdefault使用。

假设模块脚本文件名是 doubleKill.mjs,其代码如下:

// doubleKill.mjs
// const 和 default 功能演示
export default () => {
  const p = document.querySelector('p');
  p.style.transform = 'scaleY(-1)';
};
export const pColor = (color) => {
  const p = document.querySelector('p');
  p.style.color = color;
}

import 部分逻辑代码为:

<script type="module">
  // 导入 doubleKill 模块
  import * as module from './doubleKill.mjs';
  // 执行默认方法
  module.default();
  // 设置颜色为红色
  module.pColor('red');
</script>

就可以实现<p>元素文字变红同时垂直翻转的效果,如下截图:

浏览器原生支持ES6 export和import模块

您可以狠狠地点击这里:静态 import 模块 const 和 default 使用 demo

三、nomodule 与向下兼容

模块脚本我们可以使用type="module"进行设定,对于并不支持exportimport的浏览器,我们可以使用 nomodule 进行向下兼容。

<script type="module" src="module.mjs"></script>
<script nomodule src="fallback.js"></script>

对于支持 ES6 模块导入的浏览器,自然也支持原生的nomodule属性,此时fallback.js是忽略的;但是,对于不支持的老浏览器,无视nomodule,此时fallback.js就会执行,于是浏览器全兼顾。

理论就如上面分析得这么完美,然后实际上,还是存在问题的。

主要问题在低端浏览器.mjs资源会冗余加载,例如这个测试 demo在 IE11 下的网络请求:

浏览器原生支持ES6 export和import模块

不过这并不是什么大问题,多一点请求和流量,功能这块可以不影响的。

四、静态 import 更多细节

1. 目前 import 不支持裸露的说明符

目前 import 不支持裸露的说明符,用白话讲就是 import 的地址前面不能是光秃秃的。例如下面这些就不支持:

// 目前不支持,以后可能支持
import {foo} from 'bar.mjs';
import {foo} from 'utils/bar.mjs';

下面这些则支持,可以是根路径的/,同级路径./亦或者是父级../,甚至完整的非相对地址也是可以的。

// 支持
import {foo} from 'https://www.zhangxinxu.com/utils/bar.mjs';
import {foo} from '/utils/bar.mjs';
import {foo} from './bar.mjs';
import {foo} from '../bar.mjs';

2. 默认 Defer 行为

传统<script>属性支持一个名为defer的属性值,可以让 JS 资源异步加载,同时保持顺序。例如:

<!-- 同步 -->
<script src="1.js"></script>

<!-- 异步但顺序保证 -->
<script defer src="2.js"></script>
<script defer src="3.js"></script>

加载顺序一定是1.js2.js3.js。我们只要看2.js3.js,由于设置了defer,这两个 JS 异步加载,因此,就算1.js放在最下面,也多半1.js先加载完。而多个<script>同时设置defer会从前往后依次加载执行。因此,一定是先加载完2.js然后是3.js

回到本文的 ES6 module 导入,对于type="module"<script>元素,天然外挂defer特性,也就是天然异步,所有 module 脚本按顺序,因此,下面这段脚本执行顺序就好理解了:

<!-- 此 script 稍后执行 -->
<script type="module" src="1.mjs"></script>

<!-- 硬加载嘛 -->
<script src="2.js"></script>

<!-- 比第一个要晚一点 -->
<script defer src="3.js"></script>

最终的加载执行顺序是:2.js1.mjs3.js2.js同步,解析这里就加载。1.mjs虽然没有设置defer,但默认defer,因此和3.js其实是一样的,都是异步defer加载。由于1.mjs对于的<script>3.js前面,因此,先1.mjs3.js

相信不难理解。

3. 内联 script 同样 defer 特性

如下代码:

<script type="module">
  console.log("Inline module 执行");
</script>

<script src="1.js"></script>

<script defer>
  console.log("Inline script 执行");
</script>

<script defer src="2.js"></script>

最后的执行顺序是:1.jsInline scriptInline module2.js

在线 demo控制台输出可以证明上面的结论。

浏览器原生支持ES6 export和import模块

原因在于,传统的内联<script>是没有defer这种概念的,从不异步,大家可以直接忽略,认为什么也没设置即可;而type="module"<script>天然defer。因此,先1.jsInline script;然后按照defer规则,从前往后依次是Inline module2.js

4. 支持 async

无论是内联的 module <script>还是外链的<script>,都支持async这个异步标识属性。这个有别于传统的<script>,也就是传统<script>仅外链 JS 才支持async,内联 JS 直接忽略async

asyncdefer都可以让 JavaScript 异步加载,区别在于defer保证执行顺序,而async谁先加载好谁先执行。这个特性表现在type="module"<script>元素这里同样适用。

例如下面例子:

<!-- firstBlood 模块一加载完就会执行 -->
<script async type="module">
  import { pColor } from './firstBlood.mjs';
  pColor('red');
</script>

<!-- doubleKill 模块一加载完就会执行 -->
<script async type="module" src="./doubleKill.mjs"></script>

无论是firstBlood.mjs还是doubleKill.mjs都是异步加载,然后执行顺序不固定,有可能先firstBlood.mjs,也有可能先doubleKill.mjs,这样看哪个模块脚本先加载完毕。

5. 模块只会执行一次

传统的<script>如果引入的 JS 文件地址是一样的,则 JS 会执行多次。但是,对于type="module"<script>元素,即使模块地址一模一样,也只会执行一次。例如:

<!-- 1.mjs 只会执行一次 -->
<script type="module" src="1.mjs"></script>
<script type="module" src="1.mjs"></script>
<script type="module">
  import "./1.mjs";
</script>

<!-- 下面传统 JS 引入会执行 2 次 -->
<script src="2.js"></script>
<script src="2.js"></script>

我们看下在线 demo控制台输出的结果,2.js执行了 2 次,而1.mjs模块虽然 3 次引入,但只执行了一次。截图如下:

浏览器原生支持ES6 export和import模块

6. 总是 CORS 跨域

传统 JS 文件的加载,我们直接跨域也可以解析,例如,我们会使用一些大网站的 CDN 服务,例如,加载个百度提供的 jQuery 地址:

<script src="//apps.bdimg.com/libs/jquery/1.9.0/jquery.min.js"></script>

可以正常解析。但是,如果是 module 模式下import脚本资源,则不会执行,例如:

<script type="module" src="//apps.bdimg.com/.../jquery.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
    console.log(window.$);
});
</script>

我们使用 Chrome 浏览器跑一下在线 demo,结果浏览器报 CORS policy 跨域相关错误,自然window.$undefined

浏览器原生支持ES6 export和import模块

如何使支持跨域呢?

需要模块资源服务端配置Access-Control-Allow-Origin,可以指定具体域名,或者直接使用*通配符,Access-Control-Allow-Origin:*

本站 cdn.zhangxinxu.com 域名有配置Access-Control-Allow-Origin,所以,下面代码打印出来的值就不是undefined

<script type="module" src="//cdn.zhangxinxu.com/study/js/jquery-1.4.2.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', function () {
    console.log(window.$);
});
</script>

访问在线 demo,打开控制台,可以看到输出如下内容:

浏览器原生支持ES6 export和import模块

7. 无凭证

如果请求来自同一个源(域名一样),大多数基于 CORS 的 API 将发送凭证(如 cookie 等),但fetch()和模块脚本是例外 – 除非您要求,否则它们不会发送凭证。

我们通过下面例子理解上面这句话的含义:

<!-- ① 获取资源会带上凭证(如 cookie 等)-->
<script src="1.js"></script>

<!-- ② 获取资源不带凭证 -->
<script type="module" src="1.mjs"></script>

<!-- ③ 获取资源带凭证 -->
<script type="module" crossorigin src="1.mjs?"></script>

<!-- ④ 获取资源不带凭证 -->
<script type="module" crossorigin src="//cdn.zhangxinxu.com/.../1.mjs"></script>

<!-- ⑤ 获取资源带凭证 -->
<script type="module" crossorigin="use-credentials" src="//cdn.zhangxinxu.com/.../1.mjs?"></script>

这里出现了一个 HTML 属性crossorigin,该属性在“解决 canvas 图片跨域问题”这篇文章有介绍,可以明确<script>以及<img>等可外链元素在获取资源时候,是否带上凭证。

crossOrigin可以有下面两个值:

关键字 释义
anonymous 元素的跨域资源请求不需要凭证标志设置。
use-credentials 元素的跨域资源请求需要凭证标志设置,意味着该请求需要提供凭证。

其中,只要crossOrigin的属性值不是use-credentials,全部都会解析为anonymous

回到本节案例。

  1. 传统 JS 加载,都是默认带凭证的(对应注释①)。
  2. module 模块加载默认不带凭证(注释②)。
  3. 如果我们设置crossOrigin为匿名anonymous,又会带凭证(注释③)。
  4. 如果 import 模块跨域,则设置crossOriginanonymous不带凭证(注释④)。
  5. 如果 import 模块跨域,且明确设置crossOrigin为使用凭证use-credentials,则带凭证(注释⑤)。

注意,如果跨域,需要同时服务器侧返回Access-Control-Allow-Credentials:true头信息。

然后,上面的凭证规则以后有可能会调整,欢迎大家及时反馈。

8. 天然严格模式

import 的 JS 模块代码天然严格模式,如果里面有不太友好的代码会报错,例如:

浏览器原生支持ES6 export和import模块

四、动态 import

静态 import 在首次加载时候会把全部模块资源都下载下来,但是,我们实际开发时候,有时候需要动态 import(dynamic import),例如点击某个选项卡,才去加载某些新的模块,这个动态 import 特性浏览器也是支持的。

具体是使用一个长得像函数的import(),注意,只是长得像函数,import()实际上就是个单纯的语法,类似于super()。这就意味着import()不会从Function.prototype获得继承,因此您无法callapply它,并且const importAlias = import之类的东西不起作用,甚至import()都不是对象!

语法为:

import(moduleSpecifier);

moduleSpecifier为模块说明符,其实就是模块地址,规则和静态import一样,不能是裸露的地址。

案例

静态import()那个红色翻转案例我们改造成动态import,也就是把import xxxx from 'xxxx'改成import('xxxx'),代码如下:

<script type="module">
  // 导入 doubleKill 模块
  import('./doubleKill.mjs').then((module) => {
    // 执行默认方法
    module.default();
    // 设置颜色为红色
    module.pColor('red');
  });
</script>

最后效果和静态 import 一样:

浏览器原生支持ES6 export和import模块

您可以狠狠地点击这里:ES6 动态 import 模块基本使用 demo


由于import()返回一个promise,所以,我们可以使用 async/await 来代替then这种回调形式。

<script type="module">
(async () => {
  // 导入 doubleKill 模块
  const module = await import('./doubleKill.mjs');
  // 执行默认方法
  module.default();
  // 设置颜色为红色
  module.pColor('red');
})();
</script>

您可以狠狠地点击这里:async/await 下的动态 import 演示 demo

五、交互中的动态 import

不像静态import只能用在<script type="module>"一样,动态import()也可以用在普通的 script,我们来看一个更接近真实开发的案例——选项卡内容动态加载。

首先,页面 HTML 代码如下:

<nav>
    <a href="javascript:" class="active" data-module="mm1">美女 1</a>
    <a href="javascript:" data-module="mm2">美女 2</a>
    <a href="javascript:" data-module="mm3">美女 3</a>
</nav>
<main><img src="mm1.jpg"></main>

需求如下,点击不同的美女选项卡的时候,去加载对应的模块,模块有个方法可以改变<main>元素内容。

则,我们的的交互 JS 和动态import()JS 如下:

<script>
  const main = document.querySelector('main');
  const links = document.querySelectorAll('nav > a');
  for (const link of links) {
    link.addEventListener('click', async (event) => {
      const module = await import(`./${link.dataset.module}.mjs`);
      // 模块暴露名为`loadPageInto`的方法,内容是写入一段 HTML
      module.loadPageInto(main);
    });
  }
</script>

结果,当我们点击其他选项卡的时候,<main>元素中的美女图片就会发生变化,例如默认是这个:

浏览器原生支持ES6 export和import模块

点击“美女 2”选项卡按钮,此时浏览器会动态加载mm2.mjs这个模块,然后执行这个模块中暴露的loadPageInfo方法,从而改变呈现内容。

浏览器原生支持ES6 export和import模块

您可以狠狠地点击这里:选项卡模块动态 import demo

六、结语

这篇文章写了一个月,从 7 月 30 号写到 8 月 2 号,是不是跨了一个月?浏览器原生支持ES6 export和import模块

最近看自己很多年前写的技术文章,不太正经,插科打诨的东西比较多,甚至有时候会花一般篇幅讲一个不知所云的故事。后来,有人说啰嗦,于是自己文风尝试简洁,持续了差不多 2 年,最近发现这样不行,完全就成了干巴巴的技术科普,很无聊,很没劲,没有辨识度,缺少有趣的灵魂,时间流逝,很容易湮没在茫茫多的技术洪流中,所以呢,决定,还是回到过去,本站就是个个人网站,所谓个人网站,不就是用来展示自己的特质的嘛,精神无限自由的自留地,不必因为某些言论而局促自己。

哎呀呀呀,这世上很多事情都是这样,实践了一圈下来,发现,还是最初的决策是最准确的。

参考文章

感谢阅读,行文仓促,如果文中有表述不准的地方,欢迎指正。

 


新概念博客 , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:浏览器原生支持ES6 export和import模块
喜欢 (58)
[新概念]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址