这篇文章发布于 2018年08月2日,星期四,01:09,归类于 JS API。 阅读 74886 次, 今日 12 次 37 条评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=7876
本文可全文转载,但需得到原作者书面许可,同时保留原作者和出处,摘要引流则随意。
一、前言
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浏览器支持,不过相信很快其他浏览器也会跟进。
更新于2022-01-09
目前的兼容性:
——-更新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>
文字变红了:
有了案例,下面基础知识就更好消化与理解了。
- 对于需要引入模块的
<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.
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>
文字变红,以及垂直翻转,演示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部分逻辑代码为:
<script type="module"> // 导入doubleKill模块 import * as module from './doubleKill.mjs'; // 执行默认方法 module.default(); // 设置颜色为红色 module.pColor('red'); </script>
就可以实现<p>
元素文字变红同时垂直翻转的效果,如下截图:
您可以狠狠地点击这里:静态import模块const和default使用demo
三、nomodule与向下兼容
模块脚本我们可以使用type="module"
进行设定,对于并不支持export
和import
的浏览器,我们可以使用nomodule进行向下兼容。
<script type="module" src="module.mjs"></script> <script nomodule src="fallback.js"></script>
对于支持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行为
传统<script>
属性支持一个名为defer
的属性值,可以让JS资源异步加载,同时保持顺序。例如:
<!-- 同步 --> <script src="1.js"></script> <!-- 异步但顺序保证 --> <script defer src="2.js"></script> <script defer src="3.js"></script>
加载顺序一定是1.js
, 2.js
, 3.js
。我们只要看2.js
和3.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.js
, 1.mjs
, 3.js
。2.js
同步,解析这里就加载。1.mjs
虽然没有设置defer
,但默认defer
,因此和3.js
其实是一样的,都是异步defer
加载。由于1.mjs
对于的<script>
在3.js
前面,因此,先1.mjs
后3.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.js
,Inline script
,Inline module
,2.js
。
从在线demo控制台输出可以证明上面的结论。
原因在于,传统的内联<script>
是没有defer
这种概念的,从不异步,大家可以直接忽略,认为什么也没设置即可;而type="module"
的<script>
天然defer
。因此,先1.js
,Inline script
;然后按照defer
规则,从前往后依次是Inline module
,2.js
。
4. 支持async
无论是内联的module <script>
还是外链的<script>
,都支持async
这个异步标识属性。这个有别于传统的<script>
,也就是传统<script>
仅外链JS才支持async
,内联JS直接忽略async
。
async
和defer
都可以让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次引入,但只执行了一次。截图如下:
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
:
如何使支持跨域呢?
需要模块资源服务端配置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,打开控制台,可以看到输出如下内容:
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
。
回到本节案例。
- 传统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')
,代码如下:
<script type="module"> // 导入doubleKill模块 import('./doubleKill.mjs').then((module) => { // 执行默认方法 module.default(); // 设置颜色为红色 module.pColor('red'); }); </script>
最后效果和静态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>
元素中的美女图片就会发生变化,例如默认是这个:
点击“美女2”选项卡按钮,此时浏览器会动态加载mm2.mjs
这个模块,然后执行这个模块中暴露的loadPageInfo
方法,从而改变呈现内容。
您可以狠狠地点击这里:选项卡模块动态import demo
六、结语
这篇文章写了一个月,从7月30号写到8月2号,是不是跨了一个月?
最近看自己很多年前写的技术文章,不太正经,插科打诨的东西比较多,甚至有时候会花一般篇幅讲一个不知所云的故事。后来,有人说啰嗦,于是自己文风尝试简洁,持续了差不多2年,最近发现这样不行,完全就成了干巴巴的技术科普,很无聊,很没劲,没有辨识度,缺少有趣的灵魂,时间流逝,很容易湮没在茫茫多的技术洪流中,所以呢,决定,还是回到过去,本站就是个个人网站,所谓个人网站,不就是用来展示自己的特质的嘛,精神无限自由的自留地,不必因为某些言论而局促自己。
哎呀呀呀,这世上很多事情都是这样,实践了一圈下来,发现,还是最初的决策是最准确的。
参考文章
感谢阅读,行文仓促,如果文中有表述不准的地方,欢迎指正。
本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=7876
(本篇完)
- Promise.all、race和any方法都是什么意思? (0.520)
- Service Worker实现浏览器端页面渲染或CSS,JS编译 (0.455)
- Web Components中引入外部CSS的3种方法 (0.306)
- polyfill、ponyfill、prollyfill傻傻分不清楚 (0.195)
- 使用jsPDF导出PDF文件实践分享 (0.195)
- 纯JS实现图像的人脸识别功能 (0.195)
- CSS的样式合并与模块化 (0.156)
- 页面重构“鑫三无准则” 之“无宽度”准则 (0.156)
- 高富帅seajs使用示例及spm合并压缩工具露脸 (0.156)
- 基于HTML5 drag/drop模块拖动插入排序删除完整实例 (0.156)
- 页面可用性之浏览器默认字体与CSS中文字体 (RANDOM - 0.018)
引用楼上评论
“建议多开车,车速慢一点没关系”
写的不错
经验证: 同域时即使没有 crossorigin 也会带上凭证cookie(chrome:96.0.4664.45)
我有一个问题, 就是默认defer行为和内联也同样的性质的地方,
在默认defer的例子里面, 是先执行type=”module”的, 我想的是他写在defer的前面,
但是在内联那个地方, 你举得例子, 虽然type=”module”写在了前面, defer写在后面, 但是执行结果是先执行的defer, 然后是type=”module”
我就感觉有点矛盾了, 有点想不通
type=”module”的默认defer(包括内联的type=”module”的)
但是,传统的内联是没有defer这种概念的
建议多开车,车速慢一点没关系
我觉得鑫哥以前讲故事一样的幽默风格比较喜欢。
es/ts 有没有办法间接 export+await 呀?
const config_path = ‘./’+ (APP_ENV==’production’? ‘production.ts’: ‘development.ts’)
export default config = await import(config_path);
大神大神,import()能跨域么,服务器没设置Access-Control-Allow-Origin的情况下
你可以试一试,这个细节我也不确定。
不可以跨域的,import必须起服务器,而且要设置Access-Control-Allow-Origin
如果import()能直接跨域岂不是跟import静态用法自相矛盾吗?
import和import()只是两种不同的表现形式,满足不同的需求场景。
基础设计应该是一样的。
建议作者(大神)把”循环依赖“也顺道说下,感谢。
import 裸js现在可以用啦,不过要加上importmap(
学习了
哥,你的特色就是无处不在的美女图片和各种美女图片。。
google前端工程师之前写过script module的支持情况。看到这篇文章介绍了基于peg.js可以自己实现这种module级别的http code loader, 传送门:http://www.fed123.com/dsl-ast-peg/
这个评论的链接是广告
# markdown title testing
点个赞,汇总的比较详细。
虽然开发中,文中提到的好多坑已经踩过了。
http://2ality.com/2017/01/import-operator.html#async-functions-and-import
可以看下,这篇文章也不错!
# css trick ???
https://github.com/xgqfrms/FEIQA/issues/31#issuecomment-418226565
> -1, 费解
transform: scale(-1);
transform: scaleX(-1);
transform: scaleY(-1);
# good
> 语义化,好理解
transform: rotate(180deg);
transform: rotateX(180deg);
transform: rotateY(180deg);
今天刚发现import返回的是promise,百度了一下结果很少有讲到这个点的,看到你这篇文章大致明白了,感谢大神。
我个人觉得,风格幽默是好事,比如偶尔抖个包袱什么的,但是大量穿插一些无关的内容,读起来确实会分心,感觉你早期的文章就有点这个情况,不过这篇看得挺舒服的,当然像你说的,不必因为别人的评论而局促自己。
大佬,什么时候给个webpack跟vue的课程呢
vue 一直用的就是这
太久没有来逛大神的博客了,受益匪浅
赞
caniuse 支持 embed 加载,求老大别再截图啦
以前用过一段时间,严重拖慢页面加载速度,我也是没得办法呀。
赞
这个好,Angular中用过…
赞坑!!!!!!
这个必须马上立即迅速的阅读并收藏一下。
食之无味,弃之可惜
node.js并不是支持import,是经过babel转译
在控制台
直接运行
await import(‘./doubleKill.mjs’);
会抛异常,
直接运行
var im = async src=>import(src); await im(‘./doubleKill.mjs’);
也抛异常,
分两次运行
var im = async src=>import(src);
和
await im(‘./doubleKill.mjs’);
却不会呢
await不能直接使用,必须在async函数中使用
踩个沙发
学到了很多,谢谢旭叔
倒不觉得啰嗦,篇幅也不算长,适合我这种有点阅读障碍的人看,看完也get到了知识点。棒