万岁,浏览器原生支持ES6 export和import模块啦!

一、前言

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+。

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

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

二、静态import

我们先从最简单的案例说起,例如,我想想,demo比较方便演示的效果,啊,那就实现改变

元素的文字颜色。

主页面相关script代码如下:

// 导入firstBlood模块

import { pColor } from './firstBlood.mjs';// 设置颜色为红色pColor('red');

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

// export一个改变

元素颜色的方法export function pColor (color) {  const p = document.querySelector('p');  p.style.color = color;}

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

可以看到

文字变红了:

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

对于需要引入模块的元素,我们需要添加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.

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命令。再看下面一个例子,

文字变红,以及垂直翻转,演示const和default使用。

假设模块脚本文件名是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部分逻辑代码为:

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

就可以实现

元素文字变红同时垂直翻转的效果,如下截图:

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

三、nomodule与向下兼容

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

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

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

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

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

四、静态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行为

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

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

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

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

相信不难理解。

3. 内联script同样defer特性

如下代码:

  console.log("Inline module执行");

  console.log("Inline script执行");

最后的执行顺序是:1.js,Inline script,Inline module,2.js。

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

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

4. 支持async

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

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

例如下面例子:

  import { pColor } from './firstBlood.mjs';  pColor('red');

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

5. 模块只会执行一次

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

  import "./1.mjs";

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

6. 总是CORS跨域

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

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

window.addEventListener('DOMContentLoaded', function () {

    console.log(window.$);

});

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

如何使支持跨域呢?

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

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

window.addEventListener('DOMContentLoaded', function () {

    console.log(window.$);

});

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

7. 无凭证

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

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

crossOrigin可以有下面两个值:

关键字释义

anonymous元素的跨域资源请求不需要凭证标志设置。

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

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

回到本节案例。

传统JS加载,都是默认带凭证的(对应注释①)。

module模块加载默认不带凭证(注释②)。

如果我们设置crossOrigin为匿名anonymous,又会带凭证(注释③)。

如果import模块跨域,则设置crossOrigin为anonymous不带凭证(注释④)。

如果import模块跨域,且明确设置crossOrigin为使用凭证use-credentials,则带凭证(注释⑤)。

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

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

8. 天然严格模式

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

四、动态import

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

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

语法为:

import(moduleSpecifier);

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

案例

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

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

最后效果和静态import一样:

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

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

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

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

五、交互中的动态import

不像静态import只能用在

首先,页面HTML代码如下:

    美女1

    美女2

    美女3

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

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

  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`的方法,内容是写入一段HTMLmodule.loadPageInto(main);    });  }

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容