# 使用前提

由于摇树是通过 ES6 Import 和 Export 实现找到已使用和未使用的代码,所以摇树的使用前提:源码必须遵循 ES6 的模块规则(import & export),如果是基于 cjs 规范则目前无法使用。

# 实例分析

# 关闭 optimization

webpack 在生产模式下才会开启摇树,所以需要把 mode 设置为 production。

由上一节的摇树机制我们得知,我们需要把 webpack 的代码压缩器关闭才能看到 webpack 对代码使用情况的标注,所以需要关闭 webpack 的 optimization。

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    mode: 'production',
    optimization: {
        minimize: false,
        concatenateModules: false
    },
    devtool: false
}

util.js

export function usedFunction() {
    return 'usedFunction'
}

export function unusedFunction() {
    return 'unusedFunction'
}

index.js

import {
    usedFunction,
    unusedFunction
} from './util'

let result1 = usedFunction()
// let result2 = unusedFunction()

console.log(result1)

打包结果 bundle.js 主要部分(果然看到了 webpack 对代码使用情况额标注)

/************************************************************************/
/******/
([
    /* 0 */
    /***/
    (function(module, __webpack_exports__, __webpack_require__) {

        "use strict";
        /* harmony export (binding) */
        __webpack_require__.d(__webpack_exports__, "a", function() {
            return usedFunction;
        });
        /* unused harmony export unusedFunction */
        function usedFunction() {
            return 'usedFunction'
        }

        function unusedFunction() {
            return 'unusedFunction'
        }

        /***/
    }),
    /* 1 */
    /***/
    (function(module, __webpack_exports__, __webpack_require__) {

        "use strict";
        __webpack_require__.r(__webpack_exports__);
        /* harmony import */
        var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(0);

        let result1 = Object(_util__WEBPACK_IMPORTED_MODULE_0__[ /* usedFunction */ "a"])()
        // let result2 = unusedFunction()

        console.log(result1)

        /***/
    })
    /******/
]);

显然:webpack 负责对代码进行标记,把 import & export 标记为 3 类:

  • 被使用过的 export 标记为 /* harmony export ([type]) */ ,其中 [type] 和 webpack 内部有关,可能是 binding、immutable 等等;
  • 没被使用过的 export 标记为 /* unused harmony export [FuncName] */ ,其中 [FuncName] 是 export 的方法名称;
  • 所有 import 标记为 / harmony import /

# 开启 optimization

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    mode: 'production',
    optimization: {
        minimize: true,
        concatenateModules: true
    },
    devtool: false
}

打包结果

! function(e) {
    var t = {};

    function n(r) {
        if (t[r]) return t[r].exports;
        var o = t[r] = {
            i: r,
            l: !1,
            exports: {}
        };
        return e[r].call(o.exports, o, o.exports, n), o.l = !0, o.exports
    }
    n.m = e, n.c = t, n.d = function(e, t, r) {
        n.o(e, t) || Object.defineProperty(e, t, {
            enumerable: !0,
            get: r
        })
    }, n.r = function(e) {
        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, {
            value: "Module"
        }), Object.defineProperty(e, "__esModule", {
            value: !0
        })
    }, n.t = function(e, t) {
        if (1 & t && (e = n(e)), 8 & t) return e;
        if (4 & t && "object" == typeof e && e && e.__esModule) return e;
        var r = Object.create(null);
        if (n.r(r), Object.defineProperty(r, "default", {
                enumerable: !0,
                value: e
            }), 2 & t && "string" != typeof e)
            for (var o in e) n.d(r, o, function(t) {
                return e[t]
            }.bind(null, o));
        return r
    }, n.n = function(e) {
        var t = e && e.__esModule ? function() {
            return e.default
        } : function() {
            return e
        };
        return n.d(t, "a", t), t
    }, n.o = function(e, t) {
        return Object.prototype.hasOwnProperty.call(e, t)
    }, n.p = "", n(n.s = 0)
}([function(e, t, n) {
    "use strict";
    n.r(t);
    console.log("usedFunction")
}]);

显然,会在代码标注的基础上进行代码精简,把没用的都删除。

# 实例分析总结

webpack 摇树分两步走:

  1. 标注代码使用情况
  2. 对未使用的代码进行删除

# 源码分析

# 代码静态分析,标注代码使用情况

通过搜索 webpack 源码,包含 harmony export 的部分,发现对 used export 和 unused export 的标注具体实现:

# lib/dependencies/HarmoneyExportInitFragment.js

class HarmonyExportInitFragment extends InitFragment {
    /**
     * @param {string} exportsArgument the exports identifier
     * @param {Map<string, string>} exportMap mapping from used name to exposed variable name
     * @param {Set<string>} unusedExports list of unused export names
     */
    constructor(
        exportsArgument,
        exportMap = EMPTY_MAP,
        unusedExports = EMPTY_SET
    ) {
        super(undefined, InitFragment.STAGE_HARMONY_EXPORTS, 1, "harmony-exports");
        this.exportsArgument = exportsArgument;
        this.exportMap = exportMap;
        this.unusedExports = unusedExports;
    }

    merge(other) {
        let exportMap;
        if (this.exportMap.size === 0) {
            exportMap = other.exportMap;
        } else if (other.exportMap.size === 0) {
            exportMap = this.exportMap;
        } else {
            exportMap = new Map(other.exportMap);
            for (const [key, value] of this.exportMap) {
                if (!exportMap.has(key)) exportMap.set(key, value);
            }
        }
        let unusedExports;
        if (this.unusedExports.size === 0) {
            unusedExports = other.unusedExports;
        } else if (other.unusedExports.size === 0) {
            unusedExports = this.unusedExports;
        } else {
            unusedExports = new Set(other.unusedExports);
            for (const value of this.unusedExports) {
                unusedExports.add(value);
            }
        }
        return new HarmonyExportInitFragment(
            this.exportsArgument,
            exportMap,
            unusedExports
        );
    }

    /**
     * @param {GenerateContext} generateContext context for generate
     * @returns {string|Source} the source code that will be included as initialization code
     */
    getContent({
        runtimeTemplate,
        runtimeRequirements
    }) {
        runtimeRequirements.add(RuntimeGlobals.exports);
        runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);

        const unusedPart =
            this.unusedExports.size > 1 ?
            `/* unused harmony exports ${joinIterableWithComma(
                        this.unusedExports
                  )} */\n` :
            this.unusedExports.size > 0 ?
            `/* unused harmony export ${
                        this.unusedExports.values().next().value
                  } */\n` :
            "";
        const definitions = [];
        for (const [key, value] of this.exportMap) {
            definitions.push(
                `\n/* harmony export */   ${JSON.stringify(
                    key
                )}: ${runtimeTemplate.returningFunction(value)}`
            );
        }
        const definePart =
            this.exportMap.size > 0 ?
            `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
                        this.exportsArgument
                  }, {${definitions.join(",")}\n/* harmony export */ });\n` :
            "";
        return `${definePart}${unusedPart}` ;
    }
}

# harmoney export

getContent 处理 exportMap,对原来的 export 进行 replace

        const definePart =
            this.exportMap.size > 0 ?
            `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${
                        this.exportsArgument
                  }, {${definitions.join(",")}\n/* harmony export */ });\n` :
            "";
        return `${definePart}${unusedPart}` ;
        }

# unused harmoney exports

getContent 处理 unExportMap,对原来的 export 进行 replace

        const unusedPart =
            this.unusedExports.size > 1 ?
            `/* unused harmony exports ${joinIterableWithComma(
                        this.unusedExports
                  )} */\n` :
            this.unusedExports.size > 0 ?
            `/* unused harmony export ${
                        this.unusedExports.values().next().value
                  } */\n` :
            "";

# lib/dependencies/HarmonyExportSpecifierDependency.js

声明 used 和 unused,调用 harmoneyExportInitFragment 进行 replace 掉源码里的 export。

HarmonyExportSpecifierDependency.Template = class HarmonyExportSpecifierDependencyTemplate extends NullDependency.Template {
    /**
     * @param {Dependency} dependency the dependency for which the template should be applied
     * @param {ReplaceSource} source the current replace source which can be modified
     * @param {DependencyTemplateContext} templateContext the context object
     * @returns {void}
     */
    apply(
        dependency,
        source,
        { module, moduleGraph, initFragments, runtimeRequirements, runtime }
    ) {
        const dep = /** @type {HarmonyExportSpecifierDependency} */ (dependency);
        const used = moduleGraph
            .getExportsInfo(module)
            .getUsedName(dep.name, runtime);
        if (!used) {
            const set = new Set();
            set.add(dep.name || "namespace");
            initFragments.push(
                new HarmonyExportInitFragment(module.exportsArgument, undefined, set)
            );
            return;
        }

        const map = new Map();
        map.set(used, `/* binding */ ${dep.id}`);
        initFragments.push(
            new HarmonyExportInitFragment(module.exportsArgument, map, undefined)
        );
    }
};

# lib/dependencies/HarmonyExportSpecifierDependency.js

传入 moduleGraph 获取所有 export 的 name 值

    /**
     * Returns the exported names
     * @param {ModuleGraph} moduleGraph module graph
     * @returns {ExportsSpec | undefined} export names
     */
    getExports(moduleGraph) {
        return {
            exports: [this.name],
            terminalBinding: true,
            dependencies: undefined
        };
    }

# moduleGraph (建立 ES6 模块规范的图结构)

lib/ModuleGraph.js (该处代码量过多,不作展示)

class ModuleGraph {
    constructor() {
        /** @type {Map<Dependency, ModuleGraphDependency>} */
        this._dependencyMap = new Map();
        /** @type {Map<Module, ModuleGraphModule>} */
        this._moduleMap = new Map();
        /** @type {Map<Module, Set<ModuleGraphConnection>>} */
        this._originMap = new Map();
        /** @type {Map<any, Object>} */
        this._metaMap = new Map();

        // Caching
        this._cacheModuleGraphModuleKey1 = undefined;
        this._cacheModuleGraphModuleValue1 = undefined;
        this._cacheModuleGraphModuleKey2 = undefined;
        this._cacheModuleGraphModuleValue2 = undefined;
        this._cacheModuleGraphDependencyKey = undefined;
        this._cacheModuleGraphDependencyValue = undefined;
    }
// ...

在不同的处理阶段调用对应的 ModuleGraph 里面的 function 做代码静态分析,构建 moduleGraph 为 export 和 import 标注等等操作做准备。

# Compilation

lib/Compilation.js (部分代码) 在 编译阶段 中将分析所得 的 module 入栈到 ModuleGraph。

    /**
     * @param {Chunk} chunk target chunk
     * @param {RuntimeModule} module runtime module
     * @returns {void}
     */
    addRuntimeModule(chunk, module) {
        // Deprecated ModuleGraph association
        ModuleGraph.setModuleGraphForModule(module, this.moduleGraph);

        // add it to the list
        this.modules.add(module);
        this._modules.set(module.identifier(), module);

        // connect to the chunk graph
        this.chunkGraph.connectChunkAndModule(chunk, module);
        this.chunkGraph.connectChunkAndRuntimeModule(chunk, module);

        // attach runtime module
        module.attach(this, chunk);

        // Setup internals
        const exportsInfo = this.moduleGraph.getExportsInfo(module);
        exportsInfo.setHasProvideInfo();
        if (typeof chunk.runtime === "string") {
            exportsInfo.setUsedForSideEffectsOnly(chunk.runtime);
        } else if (chunk.runtime === undefined) {
            exportsInfo.setUsedForSideEffectsOnly(undefined);
        } else {
            for (const runtime of chunk.runtime) {
                exportsInfo.setUsedForSideEffectsOnly(runtime);
            }
        }
        this.chunkGraph.addModuleRuntimeRequirements(
            module,
            chunk.runtime,
            new Set([RuntimeGlobals.requireScope])
        );

        // runtime modules don't need ids
        this.chunkGraph.setModuleId(module, "");

        // Call hook
        this.hooks.runtimeModule.call(module, chunk);
    }

# 总结分析

  1. webpack 在编译阶段将发现的 modules 放入 ModuleGraph
  2. HarmoneyExportSpecifierDependency 和 HarmoneyImportSpecifierDependency 识别 import 和 export 的 module
  3. HarmoneyExportSpecifierDependency 识别 used export 和 unused export
  4. used 和 unused
    1. 把 used export 的 export 替换为 / *harmony export ([type])* /
    2. 把 unused export 的 export 替换为 / *unused harmony export [FuncName]* /

# 总结

  1. 使用 ES6 模块语法编写代码,这样摇树才能生效
  2. 工具类函数尽量单独输出,不要集中成一个对象或类,避免打包对象和类为使用的部分