The background is that you want to load the corresponding module under certain conditions. Outside the condition range, the module may not exist at all.

The general intention is to load module A in environment A (the path of module B does not exist) :

If (special environment A) {require('moduleA'); } else { require('module') }Copy the code

Runtime is naturally supported, but compile time is a problem. For WebPack, modules are compiled ahead of time whether they are loaded synchronously or asynchronously.

Next, take require as an example (similar to import(), described at the end) to implement some code that does not compile when webPack is built.

1. Based on static conditions

Take an extreme example, for example:

if (true) {
    console.log('ok');
} else {
    require('@#$%^&');
}

// 'ok'
Copy the code

This code should run directly in the browser without any problems.

Take a look at the webPack build results:

In production mode:

if (true) {
    console.log('ok');
} else {
}
Copy the code

In development mode:

eval("if (true) {\n console.log('ok'); \n} else {}\n\n//# sourceURL=webpack:///./src/apps/adhome/main.js?" );Copy the code

As you can see, statements with an if condition of false are not generated in the build result and are simply killed (for reasons explained later). The build and run times are fine.

2. Based on variable expressions

Of course, it is unlikely to be written as a fixed Boolean in practice, so let’s try using a variable based expression instead:

const flag = false;

if (flag) {
    require('@#$%^&*');
}
Copy the code

It still works fine in the browser. Take a look at the webPack build result:

Production mode error:

[Build failed]. ModuleNotFoundError: Module not found: Error: Can't resolve '@#$%^&*' 
Copy the code

Change the WebPack configuration Bail: true and force the build to see the result of the Devlopment mode:

eval("var flag = false; \n\nif (flag) {\n __webpack_require__(! (function webpackMissingModule() { var e = new Error(\"Cannot find module '@#$%^&*'\"); e.code = 'MODULE_NOT_FOUND'; throw e; } ())); \n}\n\n//# sourceURL=webpack:///./src/apps/adhome/main.js?" );Copy the code

Webpack based on static analysis is not aware of the value of the “variable” and still does a full build of the condition. __webpack_require__ A module not found, generating an error.

Modify to a valid path and see what the production mode output looks like.

// test.js
export const name = 'test';

// index.js
const flag = false;
if (flag) {
    const { name } = require('./test');
    console.log(name);
}
Copy the code

Construction results:

var flag = false; if (flag) { var _require = __webpack_require__(12), name = _require.name; console.log(name); } / /... /* 12 */ /***/ (function(module, exports, __webpack_require__) { // ... Object.defineProperty(exports, "__esModule", { value: true}); exports.name = void 0; var name = 'test'; exports.name = name; / * * * /})Copy the code

So for a variable based expression condition, require will not be executed at runtime, but the module will still be compiled ahead of time.

Because this is unknown to Webpack. Bundler does not exclude “Dead Branch” because it cannot verify that the “if condition statement “is an accurate value.

Since it cannot be ruled out, it will compile normally. To review the webpack load logic:

(function(modules) { 

})
([
/* 0 */ (function () {}),/* 1 */ (function () {})
])
Copy the code

Modules compile modules ahead of time, either synchronously based on require or asynchronously based on import().

Back to the point, if we could get a Boolean value at build time, we would be able to do what we did at the beginning of this article.

We define build-time variables through DefinePlugin

new DefinePlugin({
    'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), // production
}),
Copy the code

Load condition changed to

if (process.env.NODE_ENV === '$$$') {
    require('@#$%^&');
}
Copy the code

The build is successful and the result is:

if (false) {}
Copy the code

So, I understand implementing some code at compile time without compiling and defining build time variable + module dynamic loading via DefinePlugin (require/import()).

Webpack, however, has a few “tricks” for handling conditional expressions for if statements. Non-standard build-time variable definitions can even cause problems.

3. AST based expression processing

If we change the condition to

if (process.env.NODE_ENV === 2) {
    require('@#$%^&');
}
Copy the code

It is also ok in theory because the expression is false whether process.env.node_env is ‘production’ or ‘development’. However, an error was reported:

[Build failed]. ModuleNotFoundError: Module not found: Error: Can't resolve '@#$%^&'
Copy the code

Let’s look at what the output is (if the build is interrupted and you can’t see the output, you can replace it with an existing address first) :

if ("production" === 2) {
    __webpack_require__(1);
}
Copy the code

The expression is not translated to a Boolean, and __webpack_require__(1) appears in the conditional statement. This means that if a module does not exist, an error will be reported during compilation.

Let’s look at two simple examples:

// code if (2 === '3') {console.log('inner log'); } if (2 === 3) { console.log('inner log'); } / / compile results if (= = = 2 '3') {the console. The log (' inner log '); } if (false) {}Copy the code

It turned out differently.

Since Webpack has the ability to replace expressions with Boolean results, it is suspected that some work was done while ast parse.

Look for the answer in WebPack Parser.

You can see that for our operator ===, the resulting Boolean value is updated only if the left and right types are the same.

For 2 === ‘3’, bool is null.

For 2 === 3, bool has a value and is false.

So we have to add one more qualification to our previous conclusion. The ** expression operators must be of the same type to be converted to Boolean directly in the build result. If (2 === ‘3’) {if(2 === ‘3’);

In particular, build-time variables whose type definition is not canonical or whose value is undefined will not be converted to a Boolean value. Note again under reference: webpack.docschina.org/plugins/def…

Therefore, it is better to declare the build-time variable as a Boolean directly:

// webpack.config.js
new webpack.DefinePlugin({
  'IS_PROD': process.env.NODE_ENV === 'production'
});
Copy the code

4. What did you do with Dead Branch?

Ok, so what does Webpack do to replace “dead Branch” in IfStatement with “{}”?

// source code if (false) {(async () => {await import('@#¥%... & * '); }) (); } // How to compile?? if (false) {}Copy the code

Create a breakpoint in Compiler and discover that content has already been processed to “if (false) {}”.

'/******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {}; \n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[moduleId].exports; \n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = InstalledModules/moduleId = {\ n / * * * * * * /... property) { return Object.prototype.hasOwnProperty.call(object, property); }; \n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = "/"; \n/******/\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(__webpack_require__.s = 0); \n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\nif (false) {}\n\n\n/***/ })\n/******/ ]); 'Copy the code

If you look at source, the logic is that if there is _cachedSource, it reads directly from the cache. Otherwise execute and write to the cache.

Enter the _replaceString. When we find that the parameter STR is still written by us, the output resultStr is changed. The interesting thing is that this.replacements has already calculated the parts to be replaced.

Continue tracing the stack changes to this.replacements. Found from substitution related expression and range from this.dependencies.

ConstDependency seems to be what we want, so let’s look at where ConstDependency is instantiated.

This is the exciting moment. Find ConstPlugin, found handler Ricky in parser. The hooks a few contain statementIf, expressionConditionalOperator, expressionLogicalOperator method. So let’s focus on statementIf, other dead Branch processing is also involved, interested students can go to have a look.

parser.hooks.statementIf.tap("ConstPlugin", statement => { // ... const param = parser.evaluateExpression(statement.test); // BasicEvaluatedExpression returns false const bool = param.asbool (); if (typeof bool === "boolean") { // ... const branchToRemove = bool ? statement.alternate : statement.consequent; if (branchToRemove) { // Before removing the dead branch, the hoisted declarations // must be collected. let declarations; if (parser.scope.isStrict) { // If the code runs in strict mode, variable declarations // using `var` must be hoisted. declarations = getHoistedDeclarations(branchToRemove, false); } else { // Otherwise, collect all hoisted declaration. declarations = getHoistedDeclarations(branchToRemove, true); } let replacement; If (declarations. Length > 0) {// Retain declaration replacement = '{var ${declarations. Join (", ")}; } `; } else {// which is the final result we get from replacement = "{}"; } const dep = new ConstDependency( replacement, branchToRemove.range ); dep.loc = branchToRemove.loc; parser.state.current.addDependency(dep); } return bool; }});Copy the code

The bool value is the previously mentioned BasicEvaluatedExpression this.bool.

That is, Dead Branch removes the core based on the expression return value result + string replacement implemented after ast parse. This is different from tree Shaking static analysis based on ES Module comments + TerserPlugin**** erasure.

5. Say import() again

Import () differs from require in that one is loaded asynchronously and the other synchronously.

Async /await: async/await

(async () => {
    if (false) {
        await import('@#$%^&');
    }
})();
Copy the code

Another error.

That must be all right

if (false) {
    import('@#$%^&').then(data => console.log(data));
}
Copy the code

So let’s write async/await again

(async () => {
    if (false) {
        // await import('@#$%^&');
        import('@#$%^&').then(data => console.log(data));
    }
})();
Copy the code

If (false) {}

_asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee() {
    return _regeneratorRuntime.wrap(function _callee$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    if (false) {}
                    case 1:
                    case "end":
                        return _context.stop();
            }
        }
    }, _callee);
}))();
Copy the code

Let’s look at the output from a different valid path:

_asyncToGenerator( /*#__PURE__*/ _regeneratorRuntime.mark(function _callee() {
    return _regeneratorRuntime.wrap(function _callee$(_context) {
        while (1) {
            switch (_context.prev = _context.next) {
                case 0:
                    if (true) {
                        _context.next = 3;
                        break;
                    }
                    _context.next = 3;
                    return __webpack_require__.e( /* import() */ 2).then(__webpack_require__.bind(null, 100));
                case 3:
                case "end":
                    return _context.stop();
            }
        }
    }, _callee);
}))();
Copy the code

Although __webpack_require__.e does not run from a runtime perspective, it is compiled ahead of time by Webpack.

Looks like Babel Preset treats the code like this.

Try unmasking @babel/preset-env and it’s normal.

Put a limit on Babel. Just tweak it so that Babel compiles inside conditional statements.

If (false) {(async () => {await import('@#¥%... & * '); }) (); }Copy the code

Conclusion 6.

A section of code that implements Webpack at compile time does not compile and can be loaded by defining build-time variables + dynamic modules based on the “Dead Branch” of the if statement.

The so-called dead Branch removal principle is based on AST analysis and string replacement. In a narrow sense, it is different from module-based Tree shaking.

In Babel’s treatment of IIAFE (Immediately Invoked Async Function Expression) to generator coroutine, the internal IF statement Expression is changed during compilation. May affect “Dead Branch” analysis during Webpack compilation.