# 使用前提
由于摇树是通过 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 摇树分两步走:
- 标注代码使用情况
- 对未使用的代码进行删除
# 源码分析
# 代码静态分析,标注代码使用情况
通过搜索 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);
}
# 总结分析
- webpack 在编译阶段将发现的 modules 放入 ModuleGraph
- HarmoneyExportSpecifierDependency 和 HarmoneyImportSpecifierDependency 识别 import 和 export 的 module
- HarmoneyExportSpecifierDependency 识别 used export 和 unused export
- used 和 unused
- 把 used export 的 export 替换为
/ *harmony export ([type])* /
- 把 unused export 的 export 替换为
/ *unused harmony export [FuncName]* /
- 把 used export 的 export 替换为
# 总结
- 使用 ES6 模块语法编写代码,这样摇树才能生效
- 工具类函数尽量单独输出,不要集中成一个对象或类,避免打包对象和类为使用的部分