QQ个性网:专注于分享免费的QQ个性内容

关于我们| 网站公告| 广告服务| 联系我们| 网站地图

搜索
编程 JavaScript Java C++ Python SQL C Io ML COBOL Racket APL OCaml ABC Sed Bash Visual Basic Modula-2 Logo Delphi IDL Groovy Julia REXX Chapel X10 Forth Eiffel C# Go Rust PHP Swift Kotlin R Dart Perl Ruby TypeScript MATLAB Shell Lua Scala Objective-C F# Haskell Elixir Lisp Prolog Ada Fortran Erlang Scheme Smalltalk ABAP D ActionScript Tcl AWK IDL J PostScript IDL PL/SQL PowerShell

在浏览器中使用原生 JavaScript 模块

日期:2025/04/06 18:59来源:未知 人气:54

导读:JS 模块 目前已得到所有主流浏览器的支持,本文将讲述什么是 JS 模块,如何使用 JS 模块,以及 Chrome 团队未来计划如何优化 JS 模块。什么是 JavaScript 模块JS modules 实际上是一系列功能的集合。之前你可能听过说 Common JS ,AMD 等模块标准,不同标准的模块功能都是类似的,都允许你 import 或者 export 一些东西。Ja......

JS 模块 目前已得到所有主流浏览器的支持,本文将讲述什么是 JS 模块,如何使用 JS 模块,以及 Chrome 团队未来计划如何优化 JS 模块。

什么是 JavaScript 模块

JS modules 实际上是一系列功能的集合。之前你可能听过说 Common JSAMD 等模块标准,不同标准的模块功能都是类似的,都允许你 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 标签引入脚本有一些区别,如下:

  • JS模块默认使用严格模式
  • 模块中不支持使用 html 格式的注释,即<!-- TODO: Rename x to y. -->
  • 模块会创建自己的顶级词义上下文,这意味着,当在模块中使用var foo = 42; 语句时,并不会创建一个全局变量foo, 因此也不能通过window.foo在浏览器中访问该变量。
  • importexport 关键字只在模块中有效。

由于存在上述不同,通过传统方式引入的脚本 和 以模块方式引入的脚本,就会有相同的代码,也会产生不同的行为,因而 JS 执行环节需要知道那些脚本是模块。

在 浏览器中使用模块

在 浏览器中,通过设置 <script> 元素的type 属性为 module 可以声明其实一个模块。

支持type="module"的浏览器会忽略带有nomudule属性的的<script>元素,这样就提供了降级处理的空间。其意义不仅如此,支持type="module"的环境意味着其也支持箭头函数,async-await等新语法功能,这样引入的脚本无须再做转义处理了。

浏览器会区别对待 JS 模块 和传统方式引入的脚本

如果模块引入了多次,浏览器只会执行一次相同模块中的代码,而对传统的方式引入的脚本引入了多少次,浏览器就会执行多少次。

此外,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%`的网页目前在使用` 浏览器按照上述方法在 `