background

A lot of front-end projects, despite all the new technology, will eventually be compiled into ES5, which will run on older browsers, but the reality is that browsers are now mature enough not to compile to ES5. An engineer from Google uild-es2015-code-in-production-today shared an article that presents a practical solution. Unfortunately, most projects still follow the old-style development model, which makes the project more bloated. It also runs slower. It’s time for a change, and today I want to talk about how the front-end project can integrate the relevant technical solutions to achieve the dynamic loading of es5/ ES6 code and finally run ES6 in the new version of the browser.

Technical solution

The technical solution of this paper is basically an extension of the article mentioned above. In brief, the differences before and after transformation are as follows:

Before modification:

  1. Js modules compile into the ES5 version bundle;
  2. The browser loads js and runs.

After transforming:

  1. Js modules compile into es5 and ES6 bundles;
  2. Determine whether the browser supports ES6 and decide to dynamically load the corresponding version of the bundle.

Uild-es2015-code-in-production-today When you read the reference article uild-es2015-code-in-production-Today you’ll find that he does something like this:

<! -- Browsers with ES module support load this file. --> <script type="module" src="main.mjs"></script> <! -- Older browsers load this file (and module-supporting --> <! -- browsers know *not* to load this file). --> <script nomodule src="main.es5.js"></script>Copy the code

There will be a problem, that is, some browsers will download all js, then execute one support, although the author also said not much problem, and gives a large section of the reasons, but I just don’t want to download so much useless js, so I realize, it is not directly introduce entrance bundle, but to determine which version needs to be loaded, Then create the label import dynamically. In fact, the overall implementation is not difficult, just need to change the configuration of webpack.

Implementation approach

Dual version output

Here we use webPack itself to support multiple versions of output compilation at the same time, such as the example given on the official website:

module.exports = [{
  output: {
    filename: './dist-amd.js',
    libraryTarget: 'amd'
  },
  name: 'amd',
  entry: './app.js',
  mode: 'production',
}, {
  output: {
    filename: './dist-commonjs.js',
    libraryTarget: 'commonjs'
  },
  name: 'commonjs',
  entry: './app.js',
  mode: 'production',
}];
Copy the code

The difference between the two versions is reflected in the configuration of babel-Loader. After the modification, the configuration is as follows (the irrelevant configuration is omitted) :

=== before ===
const configs = {
    output: {
        ...
        filename: '[name]-[fullhash:10]' +'.js',
        chunkFilename: '[name]-[fullhash:10]' +'.js',
    },
};
module.exports = configs;

=== after ===
function createConfigs(ecmaVersion = 'es2015') {
    return {
        output: {
            ...
            filename: ecmaVersion +'_[name]-[fullhash:10]' +'.js',
            chunkFilename: ecmaVersion +'_[name]-[fullhash:10]' +'.js',
        },
        module: {
            rules: [{
                loader: 'babel-loader',
		options: {
                    presets: [[
			"@babel/preset-env", {
                            useBuiltIns: "entry",
                            targets: ecmaVersion === 'es6' ? { chrome: "71" } : { chrome: "58", ie: "11" }
                    }]
                }
            }]
        }
    }; 
}
module.exports = [createConfigs(), createConfigs('es5')];
Copy the code

During the transformation process, there were also some problems, such as the clean-webpack-plugin, which deleted the static resources generated for the first time. Therefore, the project did not use this plug-in, and implemented a simple script to delete the compiled target folder before compiling.

"scripts": {
    "clean": "node ./scripts/cleanDist.js",
    "build": "npm run clean && ...."
}
Copy the code

The cleanDist file is simple and meets our needs:

// scripts/cleanDist.js const fs = require('fs'); const path = require('path'); function cleanDist(dir = path.join(__dirname, '.. ', 'dist')) { fs.rmSync(dir, { force: true, recursive: true, }); } cleanDist();Copy the code

After the above modification, the successfully compiled file should look like the following, and the corresponding version is known by the prefix:

Initialize script injection

The generated HTML file contains all import bundles by default, but according to our requirements, no bundles are imported by default (CSS is not affected), so we need to modify the HtmlWebpackPlugin:

New HtmlWebpackPlugin({inject: DEBUG, // local development, or direct injection, production mode is not automatically injected...... templateParameters: (compilation, assets, assetTags, Assettags. headTags = assettags.headtag. filter((tag) => {tag.tagName! == 'script'; }); assetTags.bodyTags = assetTags.bodyTags.filter((tag) => { tag.tagName ! == 'script'; }); return { compilation, webpackConfig: compilation.options, htmlWebpackPlugin: { tags: assetTags, files: assets, options }, }; }}),Copy the code

The HTML template page is also modified:

<! DOCTYPE html> <html class="borderbox"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1"> <meta name="renderer" content="webkit"> <%= htmlWebpackPlugin.tags.headTags %> </head> <body> <div id="app"></div> <%= htmlWebpackPlugin.tags.bodyTags %> </body> </html>Copy the code

Is that it? Or is it not? We want the effect to look like this:

<script type="text/javascript">
(function() {
    // 所有版本的bundle列表
    var es2015 = ["/resources/js/es2015_197-697d619283-1.0.0.js","/resources/js/es2015_main-697d619283-1.0.0.js"];
    var es5 = ["/resources/js/es5_197-6cc8e71f60-1.0.0.js","/resources/js/es5_main-6cc8e71f60-1.0.0.js"];
    
    if (support es2015) {
        load(es2015);
    } else {
        load(es5);
    }
}());
</script>
Copy the code

The problem here is that we need to get the js list compiled twice, and the two compilations of WebPack are independent, so we need to cache the two arrays, wait for the compilation to complete, and then rewrite the HTML page with a script to implement the above structure.

In the configuration of HtmlWebpackPlugin filter out all script tags, in fact, you know the compiled JS list, save to the local,

function appendTagToCache(ecmaVersion, injectTags) { fs.appendFileSync('./cache.js', `exports.${ecmaVersion} = ${JSON.stringify(injectTags)}; \n\r`); }... templateParameters: AppendTagToCache (ecmaVersion, assetTags, options) => {appendTagToCache(ecmaVersion, assets.js); return ... ; }Copy the code

We have all the JS lists and their versions, leaving only one final step: rewriting the HTML.

// package.json

"scripts": {
    "injectScripts": "node ./scripts/injectScripts.js",
}
Copy the code
// scripts/injectScripts.js const fs = require('fs'); const path = require('path'); const tagCache = require('./cache.js'); // Cache all js list file const htmlPath = path.join(__dirname, '.. /', 'dist', 'index.html'); function getVars() { let vars = ``; for (const [key, value] of Object.entries(tagCache)) { vars += `var ${key} = ${JSON.stringify(value)}; \n`; } return vars; } const scriptStr = ` <script type="text/javascript"> (function() { function createScript(src) { var s = document.createElement('script'); s.type = 'text/javascript'; s.src = src; document.body.appendChild(s); } function injectTags(srcArray) { for (var i = 0; i < srcArray.length; i++) { createScript(srcArray[i]); } } ${getVars()} var check = document.createElement('script'); if ('noModule' in check === false) { // support es6 injectTags(es6); } else { injectTags(es5); }} ()); </script> </body> `; const templateContent = fs.readFileSync(htmlPath, { encoding: 'utf8' }); fs.writeFileSync(htmlPath, templateContent.replace('</body>', scriptStr));Copy the code

Finally, remember to run NPM Run injectScripts after compiling, and you’re done with your goal of dynamically loading different versions.