We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
用过 vue-cli 3.0 的人可能会知道,vue-cli 提供了一个 modern 模式,可以在一个工程同时构建打包 ES5 和 ES6 两份代码:
vue-cli 3.0
Vue CLI 会产生两个应用的版本:一个现代版的包,面向支持 ES modules 的现代浏览器,另一个旧版的包,面向不支持的旧浏览器。 最酷的是这里没有特殊的部署要求。其生成的 HTML 文件会自动使用 Phillip Walton 精彩的博文(译文)中讨论到的技术: 现代版的包会通过 <script type="module"> 在被支持的浏览器中加载;它们还会使用 <link rel="modulepreload"> 进行预加载。 旧版的包会通过 <script nomodule> 加载,并会被支持 ES modules 的浏览器忽略。 一个针对 Safari 10 中 <script nomodule> 的修复会被自动注入。 对于一个 Hello World 应用来说,现代版的包已经小了 16%。在生产环境下,现代版的包通常都会表现出显著的解析速度和运算速度,从而改善应用的加载性能。
Vue CLI 会产生两个应用的版本:一个现代版的包,面向支持 ES modules 的现代浏览器,另一个旧版的包,面向不支持的旧浏览器。
最酷的是这里没有特殊的部署要求。其生成的 HTML 文件会自动使用 Phillip Walton 精彩的博文(译文)中讨论到的技术:
现代版的包会通过 <script type="module"> 在被支持的浏览器中加载;它们还会使用 <link rel="modulepreload"> 进行预加载。
<script type="module">
<link rel="modulepreload">
旧版的包会通过 <script nomodule> 加载,并会被支持 ES modules 的浏览器忽略。
<script nomodule>
一个针对 Safari 10 中 <script nomodule> 的修复会被自动注入。
对于一个 Hello World 应用来说,现代版的包已经小了 16%。在生产环境下,现代版的包通常都会表现出显著的解析速度和运算速度,从而改善应用的加载性能。
简单来说,通过这种技术能够
这么棒的技术已经在 vue-cli 上集成了,可惜在 create-react-app 上有过一阵讨论,却依旧没什么进展。
所以我个人根据 create-react-app 2.x 的 react-scripts 做了些自己的改造,在 react 上实现了这个现代模式的构建。具体可以访问 github 仓库 : react-scripts-modern
接下来我们来看看基本实现思路。
上面说过,我们需要用 webpack 构建编译出 ES5 和 ES6 两份代码。
首先是编译出 ES5 的代码,大家应该很熟悉 babel 7 那一套了,这里不再多说,这里直接给出 babel 配置代码。还对 babel 7
babel 7
babel
// .babelrc.js module.exports = { "plugins": [ [ "@babel/plugin-transform-runtime", { "corejs": false, // 默认值,可以不写 "helpers": true, // 默认,可以不写 "regenerator": false, // 通过 preset-env 已经使用了全局的 regeneratorRuntime, 不再需要 transform-runtime 提供的 不污染全局的 regeneratorRuntime "useESModules": true, // 使用 es modules helpers, 减少 commonJS 语法代码 } ] ], presets: [ [ "@babel/preset-env", { "modules": false, // 模块使用 es modules ,不使用 commonJS 规范 "useBuiltIns": 'usage', // 默认 false, 可选 entry , usage } ] ] }
对babel 7不是很了解的同学,可以看下官方文档或者我之前写的文章: Show me the code,babel 7 最佳实践!
要编译 ES6 是不是说就可以直接不用 babel 了呢?
然鹅并不是,因为还有一些 ES7/ES8 特性是 浏览器尚未正式支持但我们确实需要的,例如:异步加载,JSX 语法等等,我们一般需要找到对应的 babel plugin 来实现,所以还是需要 babel。
babel plugin
module.exports = { // ... 其他可能需要的 plugin presets: [ [ "@babel/preset-env", { "modules": false, // 模块使用 es modules ,不使用 commonJS 规范 "targets": { "esmodules": true, // 忽略 browserslist 配置,不转换 ES6 语法也不 polyfill ES6 的 API }, } ] ] }
正常的构建过程,我们一般使用 HtmlWebpackPlugin 来将 webpack 构建出来的 JS/CSS 资源嵌入到 我们的 HTML 模板中。
HtmlWebpackPlugin
那么,若要将我们的 HTML 的 script 标签加上 module 和 nomodule 属性,我们就需要额外写一个 webpack 插件,在 HtmlWebpackPlugin 的钩子中,做一些我们的处理。
script
module
nomodule
// 在 htmlWebpackPlugin 拿到资源的钩子函数中, // 给 script 标签加上 type=module 或者 nomodule 属性 this.htmlWebpackPlugin .getHooks(compilation) .alterAssetTags .tapAsync( id, (data, cb) => { data.assetTags.scripts.forEach(tag => { // 遍历下资源,把 script 中的 ES2015+ 和 legacy 的处理开 if (tag.tagName === 'script') { // 给 legacy 的资源加上 nomodule 属性,反之加上 type="module" 的属性 if (/-legacy\./.test(tag.attributes.src)) { delete tag.attributes.type tag.attributes.nomodule = true } else { tag.attributes.type = 'module' } } }) } )
完全的插件代码可直接访问 html-webpack-esmodules-plugin.js
上面说到,我们其实是分别进行了 两次 webpack 编译打包构建:
(async ()=>{ await buildByConfig(es6WebpackConfig) await buildByConfig(es5WebpackConfig) })()
那么其实存在一个问题,打包一次,生成一份 js 代码,一个 index.html
那如果 webpack 打包了两次,构建出了两份 JS 代码, 那岂不是也会出现 两份 index.html ?
index.html
当然事实上虽然不会构建出两份 index.html,但是这个问题明显会导致第二次构建出来 index.html 覆盖掉 第一次构建出来的 index.html。**导致只有 ES5 或者 ES6 的资源被 外链进 index.html **,不符合我们的预期。
解决方案其实很简单,第一次构建是用 public/index.html 为模板,构建出来 build/index.html;那么,第二次构建,就明显不能用 public/index.html 为模板,而是用第一次构建生成的 build/index.html 为模板,进行第二次构建,那么即便生成的 build/index.html 模板覆盖掉了原有的 build/index.html,但仍然是包含 es5 和 es6 的代码。
public/index.html
build/index.html
<!-- ES6 的代码,只会被 现代浏览器下载执行 --> <script type="module" src="/js/main.min.js" ></script> <!-- ES5 的代码,只会被老版浏览器下载执行 --> <script nomodule src="/js/main-legacy.min.js" ></script>
safari 10.3 不支持 nomodule, 需要进行简单的 polyfill。
在 safari 浏览器或者 IOS webview 的场景下, 如果同时使用了 module/nomodule 和 常规的 script 外链。例如:
<script src="https://www.google.com/some-script.js" ></script> <script type="module" src="/js/main.min.js" ></script> <script nomodule src="/js/main-legacy.min.js" ></script>
由于<script src="https://www.google.com/some-script.js" ></script> 的存在, **safari 会同时下载 main.min.js(ES6的代码), main-legacy.min.js(ES5的代码),但只会执行其中一份代码(所以不会影响代码逻辑),但是下载了 ES5 + ES6 的代码,有一份代码却没有用到,终究是造成了负面影响。
<script src="https://www.google.com/some-script.js" ></script>
main.min.js
main-legacy.min.js
解决方案是将所有带有 type=module/nomodule 的 script 标签放到没有 type=module/nomodule 的 script 标签之前:
type=module/nomodule
<script type="module" src="/js/main.min.js" ></script> <script nomodule src="/js/main-legacy.min.js" ></script> <script src="https://www.google.com/some-script.js" ></script>
在更低的浏览器版本(例如 Chrome 43),会出现和 2 类似的问题,同样会下载两份代码,却执行其中一份,同时2 的解决方案对此无效。这种情况,很大程度上只能用动态插入 script 标签取代 type=module/nomodule 来解决。
<!-- 将代码内联在 HTML --> <head> <script> (function(){ var insertScript = function(option, elem) { if(!option) return false; var s = document.createElement("script"); elem = elem || document.head for(var name in option) { s.defer = true s.setAttribute(name, option[name]) } elem.appendChild(s) } var script = document.createElement("script"); var supportEsModule = 'noModule' in script; var modernList = [{src: '/main.min.js'}]; var legacyList = [{src: '/main-legacy.min.js'}]; var scriptList = supportEsModule ? modernList : legacyList scriptList.forEach(function(item){ insertScript(item) }) })() </script> </head>
但这种方法明显延缓 资源的下载时机(大概十几毫秒),虽然 现代浏览器通过 preload 还是能够实现提前下载资源避免这种方案的缺陷,但是 不支持nomodule 也不支持 preload 的老版浏览器依然会有这种缺陷。
preload
所以除非你的用户里老版浏览器占据了相当一部分份额,否则个人不建议这种做法。
以上全部实现,我都已经封装到了 react-scripts-modern
习惯用 create-react-app 生成项目的同学 可以快速通过 create-react-app 工程名 --scripts-version react-scripts-modern (加上 --typescript 属性 生成 react + ts 工程) 命令生成支持 modern build 的工程,开箱即用
create-react-app
create-react-app 工程名 --scripts-version react-scripts-modern
The text was updated successfully, but these errors were encountered:
非常感谢,参考你的代码成功实现了现代模式构建两套代码。 总结下基于create-react-app V5.0的经验,eject之后需要改的地方有:
Sorry, something went wrong.
No branches or pull requests
前言
用过
vue-cli 3.0
的人可能会知道,vue-cli 提供了一个 modern 模式,可以在一个工程同时构建打包 ES5 和 ES6 两份代码:简单来说,通过这种技术能够
这么棒的技术已经在 vue-cli 上集成了,可惜在 create-react-app 上有过一阵讨论,却依旧没什么进展。
所以我个人根据 create-react-app 2.x 的 react-scripts 做了些自己的改造,在 react 上实现了这个现代模式的构建。具体可以访问 github 仓库 : react-scripts-modern
接下来我们来看看基本实现思路。
基本实现
编译
上面说过,我们需要用 webpack 构建编译出 ES5 和 ES6 两份代码。
编译 ES5
首先是编译出 ES5 的代码,大家应该很熟悉
babel 7
那一套了,这里不再多说,这里直接给出babel
配置代码。还对babel 7
对
babel 7
不是很了解的同学,可以看下官方文档或者我之前写的文章: Show me the code,babel 7 最佳实践!编译 ES6
要编译 ES6 是不是说就可以直接不用
babel
了呢?然鹅并不是,因为还有一些 ES7/ES8 特性是 浏览器尚未正式支持但我们确实需要的,例如:异步加载,JSX 语法等等,我们一般需要找到对应的
babel plugin
来实现,所以还是需要babel
。嵌入代码到 HTML
正常的构建过程,我们一般使用
HtmlWebpackPlugin
来将 webpack 构建出来的 JS/CSS 资源嵌入到 我们的 HTML 模板中。那么,若要将我们的 HTML 的
script
标签加上module
和nomodule
属性,我们就需要额外写一个 webpack 插件,在HtmlWebpackPlugin
的钩子中,做一些我们的处理。完全的插件代码可直接访问 html-webpack-esmodules-plugin.js
编译流程
上面说到,我们其实是分别进行了 两次 webpack 编译打包构建:
那么其实存在一个问题,打包一次,生成一份 js 代码,一个 index.html
那如果 webpack 打包了两次,构建出了两份 JS 代码, 那岂不是也会出现 两份
index.html
?当然事实上虽然不会构建出两份 index.html,但是这个问题明显会导致第二次构建出来 index.html 覆盖掉 第一次构建出来的 index.html。**导致只有 ES5 或者 ES6 的资源被 外链进 index.html **,不符合我们的预期。
解决方案其实很简单,第一次构建是用
public/index.html
为模板,构建出来build/index.html
;那么,第二次构建,就明显不能用public/index.html
为模板,而是用第一次构建生成的build/index.html
为模板,进行第二次构建,那么即便生成的build/index.html
模板覆盖掉了原有的build/index.html
,但仍然是包含 es5 和 es6 的代码。编译结果
浏览器的存在的坑
safari 10.3 不支持 nomodule, 需要进行简单的 polyfill。
在 safari 浏览器或者 IOS webview 的场景下, 如果同时使用了 module/nomodule 和 常规的 script 外链。例如:
由于
<script src="https://www.google.com/some-script.js" ></script>
的存在, **safari 会同时下载main.min.js
(ES6的代码),main-legacy.min.js
(ES5的代码),但只会执行其中一份代码(所以不会影响代码逻辑),但是下载了 ES5 + ES6 的代码,有一份代码却没有用到,终究是造成了负面影响。解决方案是将所有带有
type=module/nomodule
的script
标签放到没有type=module/nomodule
的script
标签之前:在更低的浏览器版本(例如 Chrome 43),会出现和 2 类似的问题,同样会下载两份代码,却执行其中一份,同时2 的解决方案对此无效。这种情况,很大程度上只能用动态插入 script 标签取代
type=module/nomodule
来解决。但这种方法明显延缓 资源的下载时机(大概十几毫秒),虽然 现代浏览器通过
preload
还是能够实现提前下载资源避免这种方案的缺陷,但是 不支持nomodule
也不支持preload
的老版浏览器依然会有这种缺陷。所以除非你的用户里老版浏览器占据了相当一部分份额,否则个人不建议这种做法。
react-scripts-modern
以上全部实现,我都已经封装到了 react-scripts-modern
习惯用
create-react-app
生成项目的同学 可以快速通过create-react-app 工程名 --scripts-version react-scripts-modern
(加上 --typescript 属性 生成 react + ts 工程) 命令生成支持 modern build 的工程,开箱即用参考文章
The text was updated successfully, but these errors were encountered: