幾個月前看到了這篇文章 https://philipwalton.com/articles/deploying-es2015-code-in-production-today/,給了我很大的啓發,原本是想使用 vue 來當實驗對象的,可是在 vue-cli3 的測試版中就有了這個內容,因此此次使用 react 來實驗, 如今 cra 中還未採用該方法;css
借用 vue-cli3 中文檔的幾句話來講明下他的做用:html
<script type="module">
在被支持的瀏覽器中加載 (他的語法是 es6 以上的,能夠直接運行)<script nomodule>
加載,並會被支持 ES modules 的瀏覽器忽略。下面列出:vue
去除 package.json 中的 babel 參數react
複製 /config/webpack.config.prod.js 一份在當前目錄, 命名爲 webpack.config.prod.es5.jswebpack
添加引用:git
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') const htmlWebpackAddModulePlugin = require('html-webpack-add-module-plugin') const fs = require('fs')
說明:es6
UglifyJsPlugin 是由於 webpack.optimize.UglifyJsPlugin 沒法壓縮 es6 以上的代碼因此須要該插件
htmlWebpackAddModulePlugin 是能夠將 生成的 script 轉換爲 module 或者 nomodule 的插件
fs 是能夠對於文件進行一系列操做,這裏只是用來判斷文件是否存在github
修改代碼:
修改 oneOf 中的 test: /\.(js|jsx|mjs)$/
該 loader 將其 options 改成web
options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15', ] }, }], "react", ], plugins: ["transform-class-properties", "syntax-dynamic-import"], compact: true }
能夠將 include: paths.appSrc
去除(注意,若是這樣作,可能會引發某些錯誤)vue-cli
在 plugins 中添加插件:
new htmlWebpackAddModulePlugin({ module: 'all', }), new UglifyJsPlugin(),
註釋 webpack.optimize.UglifyJsPlugin 插件:
// new webpack.optimize.UglifyJsPlugin({ // compress: { // warnings: false, // // Disabled because of an issue with Uglify breaking seemingly valid code: // // https://github.com/facebookincubator/create-react-app/issues/2376 // // Pending further investigation: // // https://github.com/mishoo/UglifyJS2/issues/2011 // comparisons: false, // }, // mangle: { // safari10: true, // }, // output: { // comments: false, // // Turned on because emoji and regex is not minified properly using default // // https://github.com/facebookincubator/create-react-app/issues/2488 // ascii_only: true, // }, // sourceMap: shouldUseSourceMap, // }),
修改 HtmlWebpackPlugin 插件爲:
new HtmlWebpackPlugin({ inject: true, template: fs.existsSync(`${paths.appBuild}/index.html`) ? `${paths.appBuild}/index.html` : paths.appHtml, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }),
webpack.config.prod.js的修改到此爲止
添加包引用:
const htmlWebpackAddModulePlugin = require('html-webpack-add-module-plugin')
修改入口名:
entry: { 'main.es5': [require.resolve('./polyfills'),"babel-polyfill", paths.appIndexJs] },
與以前同樣的修改 oneOf 中的 babel loader 的 options:
options: { presets: [ ['env', { modules: false, useBuiltIns: true, targets: { browsers: [ "> 1%", 'last 2 version', 'firefox ESR' ] }, }], "react" ], plugins: ["transform-class-properties", "syntax-dynamic-import"], compact: true, },
添加插件:
new htmlWebpackAddModulePlugin({ nomodule: 'all', removeCSS: 'main' }),
webpack.config.prod.es5.js的修改到此爲止
添加 es5 config 文件的引用:
const es5config = require('../config/webpack.config.prod.es5');
在 build 函數以前添加函數:
function compiler(config, previousFileSizes, prevResult) { return new Promise((resolve, reject) => { config.run((err, stats) => { if (err) { return reject(err); } const messages = formatWebpackMessages(stats.toJson({}, true)); if (messages.errors.length) { // Only keep the first error. Others are often indicative // of the same problem, but confuse the reader with noise. if (messages.errors.length > 1) { messages.errors.length = 1; } return reject(new Error(messages.errors.join('\n\n'))); } if ( process.env.CI && (typeof process.env.CI !== 'string' || process.env.CI.toLowerCase() !== 'false') && messages.warnings.length ) { console.log( chalk.yellow( '\nTreating warnings as errors because process.env.CI = true.\n' + 'Most CI servers set it automatically.\n' ) ); return reject(new Error(messages.warnings.join('\n\n'))); } // console.log(stats) let result = { stats, previousFileSizes, warnings: messages.warnings, } if (prevResult) { result.prevResult = prevResult } return resolve(result); }); }); }
修改剛剛的 build 函數爲:
async function build(previousFileSizes) { console.log('Creating an optimized production build...'); let modernConfig = webpack(config); let es5Config = webpack(es5config) let result = await compiler(es5Config, previousFileSizes); // remove main.es5.css let arr = Object.keys(result.stats.compilation.assets) const path = arr.find(v => v.indexOf('css') > -1 && v.indexOf('main') > -1) await fs.remove(result.previousFileSizes.root + '/' + path) result = await compiler(modernConfig, previousFileSizes, result); return result }
在 /public/index.html 中的
後面添加:<script> (function() { var check = document.createElement('script'); if (!('noModule' in check) && 'onbeforeload' in check) { var support = false; document.addEventListener('beforeload', function(e) { if (e.target === check) { support = true; } else if (!e.target.hasAttribute('nomodule') || !support) { return; } e.preventDefault(); }, true); check.type = 'module'; check.src = '.'; document.head.appendChild(check); check.remove(); } }()); </script>
解決 safari 的重複加載問題
基礎的修改到此爲止了,接下來運行指令 : npm run build
便可
雖然如今有一個規範,模塊的JS必須添加mjs後綴,可是若是這樣作,你不能在本地構建後運行HTML文件,你必須在服務器上運行它,不然你報錯:
Failed to load module script: The server responded with a non-JavaScript MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <link rel="manifest" href="./manifest.json"> <link rel="shortcut icon" href="./favicon.ico"> <title>React App</title> <script>!function () { var t = document.createElement("script"); if (!("noModule" in t) && "onbeforeload" in t) { var n = !1; document.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 = ".", document.head.appendChild(t), t.remove() } }()</script> <link href="./static/css/main.c17080f1.css" rel="stylesheet"> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <script src="./static/js/main.es5.bfc0d013.js" nomodule></script> <script src="./static/js/main.eee0168c.js" type="module"></script> </body> </html>
兩個 main 版本只相差 async/await 和 polyfill 的轉譯:
main.js :123k
main.es5.js :220k
兩個 chunk 相差一個 async/await 的轉譯:
es6:
0.chunk.js : 362b = 0.29k
es5:
0.chunk.js : 2k
這裏借用開頭文章的運行速度表格(他是沒有加上 babel-polyfill 的):
Version | Parse/eval time (individual runs) | Parse/eval time (avg) |
---|---|---|
ES2015+ (main.mjs) | 184ms, 164ms, 166ms | 172ms |
ES5 (main.es5.js) | 389ms, 351ms, 360ms | 367ms |
算是一種生硬的實現方案, webpack 4的異步組件還未測試
缺點是 webpack 重複生成,會減慢 build 的時間
vue-cli3 已經有了這種方式,期待下 react-script 的官方指令
解決 css 的問題,可是 es5 的代碼大小不會打印出來