使用 script 的 module 屬性實現 es6 以上的兼容

幾個月前看到了這篇文章 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

  • "babel-core": "^6.26.0"
  • "babel-plugin-syntax-dynamic-import": "^6.18.0"
  • "babel-plugin-transform-class-properties": "^6.24.1"
  • "babel-polyfill": "^6.26.0"
  • "babel-preset-env": "^1.7.0"
  • "babel-preset-react": "^6.24.1"
  • "html-webpack-add-module-plugin": "^1.0.3"
  • "uglifyjs-webpack-plugin": "^1.2.7"

去除 package.json 中的 babel 參數react

複製 /config/webpack.config.prod.js 一份在當前目錄, 命名爲 webpack.config.prod.es5.jswebpack

在 prod.js 中:

添加引用: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的修改到此爲止


在 webpack.config.prod.es5.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的修改到此爲止


開始修改 /scripts/build.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 便可

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.

Build 結果

<!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 的代碼大小不會打印出來

這是修改實例: https://github.com/Grewer/react-add-module#chinese

相關文章
相關標籤/搜索