现代脚本加载

为每个浏览器传递正确的代码并非易事。

在本文中,我们将考虑如何解决此问题的几种选择。



通过现代浏览器传递现代代码可以极大地提高性能。 您的JavaScript软件包将能够包含更紧凑或优化的现代语法,并支持较旧的浏览器。

在用于开发人员的工具中,声明性加载现代或遗留代码的模块/无模块模式占主导地位,这为浏览器提供了源,并允许您确定要使用的源:

<script type="module" src="/modern.js"></script> <script nomodule src="/legacy.js"></script> 

不幸的是,并非一切都如此简单。 上面显示的HTML方法触发Edge和Safari中的脚本重新加载

该怎么办?


根据浏览器的不同,我们需要提供已编译脚本的选项之一,但是一些旧的浏览器并不支持为此所需的所有语法。

首先,有Safari Fix 。 Safari 10.1支持JS模块,而不是脚本中的nomodule属性,因此它可以执行现代代码和旧代码。 但是,Safari 10和11支持的非标准beforeload事件可用于nomodule

方法一:动态下载


您可以通过实现一个小的脚本加载器来解决这些问题。 类似于LoadCSS的工作方式。 与其希望在nomodule实现ES模块和nomodule属性, nomodule尝试执行模块脚本作为“用石蕊试纸进行测试”,然后根据结果选择下载现代代码或旧代码。

 <!-- use a module script to detect modern browsers: --> <script type="module"> self.modern = true </script> <!-- now use that flag to load modern VS legacy code: --> <script> addEventListener('load', function() { var s = document.createElement('script') if ('noModule' in s) { // notice the casing s.type = 'module' s.src = '/modern.js' } else { s.src = '/legacy.js' } document.head.appendChild(s) }) </script> 

但是,使用这种方法,您必须等待“ litmus”模块脚本完成,然后才能实现正确的脚本。 发生这种情况是因为<sript type="module">始终异步工作。 但是有更好的方法!

您可以通过检查浏览器是否nomodule来实现独立选项。 这意味着我们将不赞成Safari 10.1这样的浏览器,即使它们支持模块。 但这可能是 最好的 。 以下是相关代码:

 var s = document.createElement('script') if ('noModule' in s) { // notice the casing s.type = 'module' s.src = '/modern.js' } else s.src = '/legacy.js' } document.head.appendChild(s) 

可以很快将其转换为加载现代代码或旧代码的函数,并提供异步代码的加载:

 <script> $loadjs("/modern.js","/legacy.js") function $loadjs(src,fallback,s) { s = document.createElement('script') if ('noModule' in s) s.type = 'module', s.src = src else s.async = true, s.src = fallback document.head.appendChild(s) } </script> 

这里有什么妥协?

预载

由于该解决方案是完全动态的,因此浏览器将无法检测到我们的JavaScript资源,除非它启动我们编写的用于插入现代脚本或旧脚本的引导代码。 通常,浏览器会扫描HTML以查找可以预先下载的资源。 此问题已解决,但并非理想情况:您可以使用<link rl=modulpreload>在现代浏览器中预加载现代版本的软件包。

不幸的是,到目前为止, 只有Chrome浏览器支持 modulepreload

 <link rel="modulepreload" href="/modern.js"> <script type="module">self.modern=1</script> <!-- etc --> 

如果该技术适合您,则可以减小嵌入这些脚本的HTML文档的大小。 如果您的有效载荷很小,例如启动屏幕或客户端应用程序下载代码,则放下预加载扫描器不太可能影响性能。 而且,如果您在服务器上绘制了许多重要的HTML以发送给浏览器,则预加载扫描程序将对您有用,并且所描述的方法将不是您的最佳选择。

以下是此解决方案在使用中的外观:

 <link rel="modulepreload" href="/modern.js"> <script type="module">self.modern=1</script> <script> $loadjs("/modern.js","/legacy.js") function $loadjs(e,d,c){c=document.createElement("script"),self.modern?(c.src=e,c.type="module"):c.src=d,document.head.appendChild(c)} </script> 

还应注意,支持JS模块的浏览器列表与支持<link rl=preload>的浏览器几乎相同。 对于某些站点,使用<link rl=preload as=script crossorigin>而不是modulepreload可能是合适的。 性能可能会modulepreload ,因为经典脚本预加载并不意味着随着时间的推移会进行统一解析,就像modulepreload

方法二:跟踪用户代理


我没有合适的代码示例,因为跟踪用户代理是一项艰巨的任务。 但是您可以在Smashing Magazine中阅读出色的文章。

实际上,所有浏览器的HTML都以相同的<scrit src=bundle.js> 。 当请求bundle.js时,服务器将解析发出请求的浏览器的用户代理字符串,并根据所识别的浏览器来选择要返回的JavaScript(现代或旧版)。

该方法是通用的,但会带来严重的后果:

  • 由于需要智能服务器,因此该方法在静态部署条件(静态站点生成器,Netlify等)下将不起作用。
  • 现在,这些JavaScript URL的缓存取决于用户代理,该代理非常易变。
  • UA的定义很困难,可能导致错误的分类。
  • 用户代理行很容易被欺骗,并且每天都会出现新的UA。

解决这些限制的一种方法是将模块/无模块模式与用户代理区别相结合,以避免将包的多个版本发送到同一地址。 这种方法降低了页面的可缓存性,但是提供了有效的预加载:HTML生成服务器知道何时使用modulepreload以及何时preload

 function renderPage(request, response) { let html = `<html><head>...`; const agent = request.headers.userAgent; const isModern = userAgent.isModern(agent); if (isModern) { html += ` <link rel="modulepreload" href="modern.mjs"> <script type="module" src="modern.mjs"></script> `; } else { html += ` <link rel="preload" as="script" href="legacy.js"> <script src="legacy.js"></script> `; } response.end(html); } 

对于已经在服务器上响应每个请求而生成HTML的站点,这可以是向现代脚本下载的有效过渡。

方法三:好的旧浏览器


在较旧版本的Chrome,Firefox和Safari中,可以看到模块/无模块模式的负面影响-它们的数量很小,因为浏览器会自动更新。 对于Edge 16-18,情况有所不同,但仍有希望:Edge的新版本将使用基于Chromium的呈现引擎,而不会出现此类问题。

对于某些应用程序,这将是一个理想的折衷方案:在90%的浏览器中下载现代版本的代码,并将旧代码提供给旧版本。 旧版浏览器中的负载会增加。

顺便说一句,对于此类重新启动是有问题的用户代理,它们都不占据移动市场的很大份额。 因此,所有这些额外字节的来源都不太可能是移动设备或处理器较弱的设备。

如果要创建一个主要由移动或新浏览器访问的网站,则对于大多数此类用户而言,最简单的模块/无模块模式是合适的。 如果要使用较旧的iOS设备,只需确保添加了Safari 10.1修复程序即可。

    iOS-. <!-- polyfill `nomodule` in Safari 10.1: --> <script type="module"> !function(e,t,n){!("noModule"in(t=e.createElement("script")))&&"onbeforeload"in t&&(n=!1,e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove())}(document) </script> <!-- 90+% of browsers: --> <script src="modern.js" type="module'></script> <!-- IE, Edge <16, Safari <10.1, old desktop: --> <script src="legacy.js" nomodule async defer></script> 

方法四:应用套餐条款


一个好的解决方案是使用nomodule有条件地下载带有现代浏览器不需要的代码的程序包,例如polyfills。 使用这种方法,在最坏的情况下,将填充甚至执行polyfill(在Safari 10.1中),但是这种效果仅限于“重新polyfilling”。 考虑到当今在所有浏览器中下载并执行polyfill的方法已盛行,这可能是值得改进的。

 <!-- newer browsers will not load this bundle: --> <script nomodule src="polyfills.js"></script> <!-- all browsers load this one: --> <script src="/bundle.js"></script> 

您可以将Angular CLI配置为将这种方法与polyfill配合使用,如Minko Gachev 所示 。 了解了这种方法后,我意识到您可以在preact-cli中启用自动填充灌装-此PR演示了实现此技术的容易程度。

而且,如果您使用的是WebPack,则有一个方便html-webpack-plugin ,可以轻松地向带有polyfills的软件包中添加nomodule。

那么该选择什么呢?


答案取决于您的情况。 如果创建的是客户端应用程序,并且HTML包含的内容少于<sript> ,则可能需要第一种方法

如果要创建在服务器上呈现的站点,并且可以负担缓存的费用,则第二种方法可能适合

如果使用通用渲染,则预加载扫描提供的性能提升可能非常重要。 因此,请注意第三种第四种方法。 选择适合您的体系结构。

我个人选择的重点是在移动设备上进行解析的持续时间,而不是桌面版本的下载成本。 移动用户将解析和数据传输成本视为实际支出(电池消耗和数据传输费),而台式机用户则没有这种限制。 我还针对90%的用户进行了优化-我项目的主要受众是使用现代和/或移动浏览器。

读什么


想更多地了解这个话题? 您可以从这里开始:

Source: https://habr.com/ru/post/zh-CN473994/


All Articles