超过90%的浏览器能够运行现代JavaScript,但遗留JavaScript的流行仍然是当今网络上性能问题的最大原因之一。今天的网络受限于传统的JavaScript,没有任何单一的优化可以提高使用ES2017语法编写、发布和交付网页或软件包的性能。现代JavaScript现代JavaScript的特点不是使用特定版本的ECMAScript规范编写代码,而是使用所有现代浏览器都支持的语法。Chrome、Edge、Firefox和Safari等现代网络浏览器占浏览器市场的90%以上,其他依赖相同底层渲染引擎的浏览器又占5%。这意味着世界上95%的网络流量来自支持过去10年中使用最广泛的JavaScript语言特性的浏览器,包括:类(ES2015)箭头函数(ES2015)生成器(ES2015)块作用域(ES2015)解构(ES2015)rest和unwrapparameters(ES2015)objectshorthand(ES2015)async/await(ES2017)较新版本的语言规范中的特性在现代浏览器中的支持通常不太一致。例如,许多ES2020和ES2021功能仅在70%的浏览器市场中受支持——仍然是大多数,但还不够安全,无法直接依赖它们。这意味着虽然“现代”JavaScript是一个移动的目标,但ES2017具有最广泛的浏览器兼容性,同时包括最常用的现代语法功能。也就是说,ES2017是目前最接近现代语法的。传统JavaScript传统JavaScript是明确避免上述所有语言功能的代码。大多数开发人员使用现代语法编写源代码,但将所有内容编译为传统语法以增加浏览器支持。编译为遗留语法确实会增加浏览器支持,但效果通常比我们想象的要小。在许多情况下,支持率从95%左右增加到98%,但代价是巨大的:传统JavaScript通常比等效的现代代码大20%左右,而且速度更慢。工具缺陷和错误配置通常会进一步扩大这种差距。安装的库占典型生产JavaScript代码的90%。由于polyfill和helper的重复,库代码会产生更高的传统JavaScript开销,而交付现代代码则避免了这个问题。npm上的现代JavaScriptNode.js标准化了一个“exports”字段来定义包的入口点:{“exports”:“./index.js”}“exports”字段引用的模块意味着Node版本在至少12.8,支持ES2019。这意味着任何使用“exports”字段引用的模块都可以用现代JavaScript编写。包消费者必须假定具有“导出”字段的模块包含现代代码,并在必要时转换它们。Modernonly只有当你想用现代代码发布一个包并让消费者在将其用作依赖项时处理转换时才使用“exports”字段。{"name":"foo","exports":"./modern.js"}不推荐这种方法。在一个完美的世界中,每个开发人员都已经配置了一个构建系统来将所有依赖项(node_modules)转换为所需的语法。然而,目前情况并非如此,发布仅具有现代语法的包将使其无法在通过旧浏览器访问的应用程序中使用。带有遗留回退的现代代码使用带有“main”的“exports”字段来发布使用现代代码的包,但也包括用于旧浏览器的ES5+CommonJS回退。{"name":"foo","exports":"./modern.js","main":"./legacy.cjs"}除了定义回退CommonJS入口点之外,还具有遗留回退和ESM捆绑器优化的现代代码,您还可以使用“模块”字段指向类似的传统后备包,但该包使用JavaScript模块语法(导入和导出)。{"name":"foo","exports":"./modern.js","main":"./legacy.cjs","module":"./module.js"}许多捆绑器(例如webpack和Rollup)依靠这个字段来利用模块特性并实现tree-shaking优化。这仍然是一个遗留包,除了导入/导出语法外不包含任何现代代码,因此此方法用于传输具有遗留后备但仍针对捆绑进行优化的现代代码。应用程序中的现代JavaScript第三方依赖项构成了Web应用程序中绝大多数典型的生产JavaScript代码。虽然npm依赖项历来以ES5语法提供,但这不再是一个安全的假设,并且依赖项更新可能会破坏应用程序的浏览器支持。随着越来越多的npm包转向现代JavaScript,确保您的构建工具能够处理它们非常重要。您所依赖的某些npm包很可能已经使用了现代语言功能。在不破坏应用程序在旧版浏览器中的体验的情况下,使用来自npm的现代代码有很多选择,但一般的想法是让构建系统将依赖项转换为与源代码相同的目标语法。webpack从webpack5开始,现在可以配置webpack在为包和模块生成代码时使用的语法。这不会转换您的代码或依赖项,只会转换webpack生成的“胶水”代码。要指定浏览器支持目标,请在项目中添加browserslist配置,或者直接将其添加到webpack配置中:module.exports={target:['web','es2017'],};您还可以添加webpack配置以生成优化的捆绑包,这些捆绑包在针对现代ES模块环境时省略了不必要的包装函数。这也将webpack配置为使用加载代码拆分包。module.exports={target:['web','es2017'],output:{module:true,},experiments:{outputModule:true,},};有许多webpack插件可以编译和转译现代JavaScript,同时仍然支持旧浏览器,例如OptimizePlugin和BabelEsmPlugin。OptimizePluginOptimizePlugin是一个webpack插件,它将最终的捆绑代码从现代JavaScript转换为传统JavaScript,而不是单个源文件。这是一个独立的设置,允许webpack配置假设一切都是现代JavaScript,没有用于多个输出或语法的特殊分支。因为OptimizePlugin对包而不是单个模块进行操作,所以它平等对待应用程序代码和依赖项。这使得使用来自npm的现代JavaScript依赖项变得安全,因为它们的代码将被捆绑并转换为正确的语法。它也可以比涉及两个编译步骤的传统解决方案更快,同时仍然为现代和传统浏览器生成单独的包。这两组包被设计为使用模块/无模块模式加载。//webpack.config.jsconstOptimizePlugin=require('optimize-plugin');module.exports={//...插件:[newOptimizePlugin()],};OptimizePlugin可以比自定义webpack配置更快,更高效,后者通常将现代代码和遗留代码分开捆绑。它还处理运行Babel并使用Terser在单独的最小包中输出现代和传统的优化设置。最后,生成的遗留包所需的polyfill被提取到专用脚本中,这样它们就不会被复制或不必要地加载到较新的浏览器中。BabelEsmPluginBabelEsmPlugin是一个webpack插件,它与@babel/preset-env一起使用来生成现有包的现代版本,以将较少转译的代码转译到现代浏览器。它是Next.js和PreactCLI最常用的模块/无模块现成解决方案。//webpack.config.jsconstBabelEsmPlugin=require('babel-esm-plugin');module.exports={//...module:{rules:[//你现有的babel-loader配置:{test:/\.js$/,排除:/node_modules/,使用:{loader:'babel-loader',options:{presets:['@babel/preset-env'],},},},],},插件:[新BabelEsmPlugin()],};BabelEsmPlugin支持多种webpack配置,因为它运行两个本质上独立的应用程序版本。对于大型应用程序,编译两次可能会花费一些额外的时间,但这种技术可以让BabelEsmPlugin无缝集成到现有的webpack配置中,使其成为最方便的选择之一。配置babel-loader来转换node_modules如果你使用babel-loader而没有前两个插件之一,你需要执行一个重要的步骤才能使用现代JavaScriptnpm模块。定义两个单独的babel-loader配置可以自动将node_modules中的现代语言特性编译为ES2017,同时仍然使用Babel插件和项目配置中定义的预设转译您自己的第一方代码。这不会为模块/无模块设置生成现代和遗留包,但可以在不破坏遗留浏览器体验的情况下安装和使用包含现代JavaScript的npm包。webpack-plugin-modern-npm使用此技术编译在package.json中具有“exports”字段的npm依赖项,因为它们可能包含现代语法://webpack.config.jsconstModernNpmPlugin=require('webpack-plugin-modern-npm');module.exports={plugins:[//自动转译在node_modules中发现的现代内容newModernNpmPlugin(),],};或者,您可以在解析模块“导出”字段时检查package.json中是否存在,在webpack配置中手动实现此技术。为简洁起见省略缓存,自定义实现可能如下所示://webpack.config.jsmodule.exports={module:{rules:[//Transpileforyourownfirst-partycode:{test:/\.js$/i,loader:'babel-loader',exclude:/node_modules/,},//转译现代依赖:{test:/\.js$/i,include(file){letdir=file.match(/^.*[/\\]node_modules[/\\](@.*?[/\\])?.*?[/\\]/);尝试{returndir&&!!require(dir[0]+'package.json').exports;}catch(e){}},使用:{loader:'babel-loader',options:{babelrc:false,configFile:false,presets:['@babel/preset-env'],},},},],},};使用此方法时,需要确保minifier支持现代语法。Terser和uglify-es都有指定{ecma:2017}的选项,以便在缩小和格式化期间保留并在某些情况下生成ES2017语法。RollupRollup内部支持生成多组包作为单个构建的一部分,并默认生成现代代码。因此,Rollup可以配置为通过您可能已经在使用的官方插件生成现代和遗留包。@rollup/plugin-babel如果使用Rollup,getBabelOutputPlugin()方法(由Rollup的官方Babel插件提供)转换生成包中的代码,而不是单个源模块。Rollup内部支持生成多组包作为单个构建的一部分,每个包都有自己的插件。您可以通过不同的Babel输出插件配置传递单个包,从而产生不同的现代和传统包://rollup.config.jsimport{getBabelOutputPlugin}from'@rollup/plugin-babel';exportdefaultjs',output:[//modernbundles:{format:'es',plugins:[getBabelOutputPlugin({presets:[['@babel/preset-env',{targets:{esmodules:true},bugfixes:true,loose:true,},],],}),],},//legacy(ES5)bundles:{格式:'amd',entryFileNames:'[name].legacy.js',chunkFileNames:'[name]-[hash].legacy.js',插件:[getBabelOutputPlugin({presets:['@babel/preset-env'],}),],},],};其他构建工具Rollup和webpack是高度可配置的,这通常意味着每个项目都必须更新其配置以在依赖项中启用现代JavaScript语法。还有更高级的构建工具更倾向于约定和默认配置,例如Parcel、Snowpack、Vite和WMR。大多数这些工具假定npm依赖项可能包含现代语法,并在为生产编译时将它们转换为适当的语法级别。除了用于webpack和Rollup的专用插件之外,您还可以使用devolution将带有传统回退的现代JavaScript包添加到任何项目。Devolution是一个独立的工具,可以转换编译系统的输出以生成遗留的JavaScript变体,从而允许捆绑和转换以采用现代输出目标。