日期:2025/04/06 18:59来源:未知 人气:54
JS 模块 目前已得到所有主流浏览器的支持,本文将讲述什么是 JS 模块,如何使用 JS 模块,以及 Chrome 团队未来计划如何优化 JS 模块。
JS modules 实际上是一系列功能的集合。之前你可能听过说 Common JS
,AMD
等模块标准,不同标准的模块功能都是类似的,都允许你 import
或者 export
一些东西。
JavaScript 模块目前有标准的语法,在模块中,你可以通过 export
关键字,导出一切东西(变量,函数,其它声明等等)
// lib.mjsexport const repeat = (string) => ${string} ${string}
;export function shout(string) { return ${string.toUpperCase()}!
;}
而想要导入该模块,只需要在其它文件中使用import
关键字引入即可
// main.mjsimport {repeat, shout} from './lib.mjs';repeat('hello');// → 'hello hello'shout('Modules in action');// → 'MODULES IN ACTION!'
模块中还可以导出默认值
// lib.mjsexport default function(string) { return ${string.toUpperCase()}!
;}
具有默认值的模块可以以任意名字导入到其它模块中
// main.mjsimport shout from './lib.mjs';// ^^^^^
模块和传统的script
标签引入脚本有一些区别,如下:
html
格式的注释,即<!-- TODO: Rename x to y. -->
var foo = 42;
语句时,并不会创建一个全局变量foo
, 因此也不能通过window.foo
在浏览器中访问该变量。import
和 export
关键字只在模块中有效。由于存在上述不同,通过传统方式引入的脚本 和 以模块方式引入的脚本,就会有相同的代码,也会产生不同的行为,因而 JS 执行环节需要知道那些脚本是模块。
在 浏览器中,通过设置 <script>
元素的type
属性为 module
可以声明其实一个模块。
支持type="module"
的浏览器会忽略带有nomudule
属性的的<script>
元素,这样就提供了降级处理的空间。其意义不仅如此,支持type="module"
的环境意味着其也支持箭头函数,async-await
等新语法功能,这样引入的脚本无须再做转义处理了。
如果模块引入了多次,浏览器只会执行一次相同模块中的代码,而对传统的方式引入的脚本引入了多少次,浏览器就会执行多少次。
此外,JS 模块对于的脚本存在跨域限制,传统的脚本引入则不存在。 对于`async`属性,浏览器对二者也会区别对待,`async`属性被用来告知浏览器下载脚本但不要阻塞 HTML 渲染,并且希望一旦下载完成,就立即执行,不用考虑顺序,不用考虑HTML渲染是否完成,`async` 属性在传统的行内` 不像静态`import()`, 动态`import()`可以还在常规的脚本中使用,更多细节可以参考Dynamic import() > 注:这和 webpack 提供的动态加载有所不同,webpack 有其独特的做法进行代码分割以满足按需加载。 #### `import.meta` `import.meta`是模块相关的另一个特性,此特性包含关于当前模块的`metadata`,准确的`metadata` 并未定义为 ECMAScript 标准的一部分。`import.meta`的值其实依赖于宿主环境,在浏览器和 NodeJS 中可能就会得到不同的值。 以下是一个`import.meta`的使用示例,默认情况下,图片是基于当前 HTML 的 URL 的相对地址,`import.meta.url`使得基于当前URL引入图片成为可能 function loadThumbnail(relativePath) {const url = new URL(relativePath, import.meta.url);const image = new Image();image.src = url;return image;} const thumbnail = loadThumbnail('../img/thumbnail.png');container.append(thumbnail); ## 性能优化建议 ### 还是需要打包的 使用模块,使得不使用诸如 `webpack` , `Roolup` 或者 `Parcel` 之类的构建工具成为可能。在以下情况下直接使用原生的 JS module 是可行的: * 在本地开发环境中 * 小型项目(所依赖模块不超过100个,依赖树浅,比如依赖层级不超过5层) 参考Chrome 加载瓶颈一文,当加载模块数量为300个时,打包过的 app 的加载性能比未打包的好得多。 产生这种现象的原因在于,静态的`import/export` 会执行静态分析,用以帮助打包工具去除未使用的`exports`以优化代码,可见静态的`import` 和 `export` 不仅仅是起到语法作用,它们还起到工具的作用。 > 我们推荐在部署代码到生产环境之前继续使用构建工具,构建工具也会通过优化来减少你的代码,并由此带来运行性能的提升。 谷歌开发者工具中的Code Coverage功能可以帮你识别,那些是不必要的代码,我们推荐使用代码分割延迟加载非首屏需要的代码。 ### 对使用打包文件和使用未打包的模块的权衡 在 web 上,很多事情都需要权衡,加载未打包的组件可能会降低初次加载的效率(cold cache),但是比起没有代码分割的打包,可以明显提高二次访问(warm cache)时的性能。比如说大小为 200kb 的代码,如果后期又改变了一个细粒度的模块,二次访问时,未打包的代码的性能会比打包的好得多。 这是矛盾所在,如果你不知道 二次访问的体验 和 首次加载的性能那个更重要,可以AB测试一下,用数据来看那种效果更好。 浏览器工程师们正在努力改进模块的性能。希望在不久的将来,未打包的模块可以在更多的场景中使用。 ### 使用细粒度的模块 我们应该养成使用细粒度模块的习惯。在开发过程中,通常来说,一个文件只有少数几个`export`比包含大量`export`的要好。 比如说在`./utils.mjs`模块中,`export`了三个方法,`drop`,`pluck`,`zip`: export function drop() { /* … */ }export function pluck() { /* … */ }export function zip() { /* … */ } 如果你的函数只需要`pluck`方法,你会以下面的方法引入: import { pluck } from './util.mjs'; 这种情况下,如果没有不通过构建过程,浏览器依旧会下载并解析整个`./utils.mjs`文件,这样明显有些浪费。 如果`pluck()`和`zip()`,`drop()`没有什么共用的代码,更好的实现是将其移动到自己独立的细粒度模块中: export function pluck() { /* … */ } 这样再导入 `pluck` 时就无需解析没有用到的模块了。 这样做不仅保持了你的源码的简洁干净,同时还能减轻了构建工具的压力,如果你的源代码中某个模块从未被`import`过,浏览器就永远不会下载它,而那些用到了的模块则会被浏览器缓存。 使用细粒度的模块,也使得在将来原生的打包方案到来时,你现有的代码能更好的进行适配。 ### 预加载模块 你可以通过使用``来进一步的优化你的模块,这样做之后,浏览器能预加载甚至预解析预编译模块及其依赖。 这在处理依赖复杂的app时效果尤为明显,如果不使用`rel="modulepreload"`,浏览器需要执行多个 HTTP 请求来获得完成的依赖,如果你使用上述方法指明了依赖,浏览器则不需要渐进的来查找相关依赖。 ### 使用 HTTP/2 如果可能,尽量使用HTTP/2 ,这对性能的提升也是显而易见的,`multiplexing support` 允许多请求和多响应可以同时进行,如果模块数量很大,这一点尤为有用。 Chrome 团队还调查过 HTTP/2 的另一个特性,server push 能不能也成为开发高模块化 app 的解决方案,但是不幸的是,HTTP/2 push is tougher than I thought - JakeArchibald.com,web 服务器和浏览器的实现目前还没有针对高模块化的 web 应用程序用例进行优化, 因此很难实现推送用户没有缓存的内容,而如果要对比整个cache,对用户来说存在隐私风险。 不过,不管怎么样,用 HTTP/2 还是很有好处的,不过 HTTP/2 server push 还不是一个有效的方案. ## web 上目前JS 模块的使用情况 JS 模块在逐步被 web 采用,据usage counters统计,大概有`0.08%`的网页目前在使用` 浏览器按照上述方法在 `