1.Compilation
// 经过之前的步骤我们得到了创建出来的modules 执行回调会到compilation.seal封装代码
function seal() {
this.hooks.seal.call();
// 触发各种hook 为我们构建提供很多的钩子
this.hooks.beforeChunks.call();
// _preparedEntrypoints 在addEntry的时候添加的 为每个入口生成一个chunk
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module; // 入口模块
const name = preparedEntrypoint.name; // main
// new Chunk(name)
const chunk = this.addChunk(name); // 新建chunk将module添加到chunk中
// extend ChunkGroup 主要用来优化 chunk graph
const entrypoint = new Entrypoint(name); // 生成入口点
entrypoint.setRuntimeChunk(chunk); // 运行时chunk
entrypoint.addOrigin(null, name, preparedEntrypoint.request); // 增加来源
this.namedChunkGroups.set(name, entrypoint); // key为chunk的名称 值为chunkGroup
this.entrypoints.set(name, entrypoint); // key为chunk的名称 值为chunkGroup
this.chunkGroups.push(entrypoint); // 添加一个新的chunkGroup
// 建立chunkGroup和chunk的关系 chunk.addGroup(chunkGroup);
// this._groups.add(chunkGroup)
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
// 建立chunk和module的关系 chunk.addModule(module);
// this._modules.add(module);
GraphHelpers.connectChunkAndModule(chunk, module);
chunk.entryModule = module; // 代码块的入口模块
chunk.name = name; // 代码块的名称
// 依赖的深度??
// this.assignDepth(module);
}
// [{Entrypoint: chunks: [ chunk: [{_modules, _groups}] ]}]
// 构建chunkGraph 用来生成优化chunk依赖图
buildChunkGraph(this, this.chunkGroups.slice());
}
2. buildChunkGraph
// seal的流程很多 先分析 buildChunkGraph 主要分为三步
const buildChunkGraph = (compilation, inputChunkGroups) => {
// 1. 建立chunkGroup chunk module之间的关系 使用 blockInfoMap 保存信息
visitModules();
// 2. 建立不同chunkGroup之间的父子关系 优化chunkGroup
connectChunkGroups();
// 3. 清理无用的chunk 清理相关的联系
cleanupUnconnectedGroups();
};
2.1 demo
// 为了方便看里面的逻辑 建立一个demo
// index.js
import common from "./common.js";
import("./async.js").then((result) => console.log(result));
// async.js
import title from "./title.js";
import("./common.js").then((result) => console.log(result));
export const lazy = "lazy";
// common.js
import title from "./title.js";
// title.js
export const title = "title";
2.2 visitModules
// 1. 建立关联
const visitModules = (
compilation,
inputChunkGroups,
chunkGroupInfoMap,
blockConnections,
blocksWithNestedBlocks,
allCreatedChunkGroups
) => {
// 1.使用blockInfo用来存储模块关系 同步存入modules 异步blocks module graph
// 经过处理 blockInfoMap 中共有6条记录 特别注意ImportDependenciesBlock_async的两个
// const obj = {
// NormalModule_index: { modules: [common], blocks: [async] },
// ImportDependenciesBlock_async: { modules: [async], blocks: [] },
// NormalModule_common: { modules: [title], blocks: [] },
// NormalModule_async: { blocks: [common], modules: [title] },
// ImportDependenciesBlock_common: { modules: [common] },
// NormalModule_title: { modules: [] },
// };
const blockInfoMap = extraceBlockInfoMap(compilation);
// 2.处理模块之见的关系 chunk graph
let nextFreeModuleIndex = 0; // 下一个模块的空闲索引 每个模块是有两个index的默认是0
let nextFreeModuleIndex2 = 0; // 通过下表去添加值
const ADD_AND_ENTER_MODULE = 0; // 增加进入模块
const ENTER_MODULE = 1; // 进入模块
const PROCESS_BLOCK = 2; // 处理代码块
const LEAVE_MODULE = 3; // 离开模块
const reduceChunkGroupToQueueItem = (queue, chunkGroup) => {
for (const chunk of chunkGroup.chunks) {
queue.push({
action: ENTER_MODULE,
block: module,
module: chunk.entryModule,
chunk,
chunkGroup,
});
}
// 设置 chunkGroupInfoMap 映射 chunkGroup 和相关的信息对象
chunkGroupInfoMap.set(chunkGroup, { chunkGroup, children: undefined });
return queue;
};
// 将 entryPoint(chunkGroup) 变成一个queue {block, chunk, chunkGroup, module}
let queue = inputChunkGroups
.reduce(reduceChunkGroupToQueueItem, [])
.reverse();
const iteratorBlock = (b) => {
// 1. 为block创建一个chunk import动态引入的会单独生成一个chunk(这个时候chunk中还没有依赖的module)
// const chunkGroup = new ChunkGroup(groupOptions);
// if (module) chunkGroup.addOrigin(module, loc, request); this.origins.push({})
// const chunk = this.addChunk(name);
// this.chunkGroups.push(chunkGroup);
c = compilation.addChunkInGroup(b.chunkName, module);
blockChunkGroups.set(b, c); // c是一个chunkGroup
allCreatedChunkGroups.add(c); // 已经创建的chunksGroup
blockConnections.set(b, []); // chunk Group的依赖
// 2. 建立 module 所属的 chunkGroup 和 block 和 block 属于的chunkGroup的依赖关系
// 主要用于优化 chunk graph
blockConnections.get(b).push({
originChunkGroupInfo: chunkGroupInfo, // chunkGroupInfoMap对应的信息
chunkGroup: c,
});
// 3. 创建/跟新 chunk Group 的信息
queueConnect.set(chunkGroup, connectList);
connectList.add(c);
// 4. 代码块 用作外层的遍历
queueDelayed.push({
action: PROCESS_BLOCK,
block: b,
module: module,
chunk: c.chunks[0],
chunkGroup: c,
});
};
/**
* 第一次外层while是入口 entry: {block, chunk, module, chunkGroup}
* 第一次内层循环
* 进入ENTER_MODULE 处理 完成后(没有break)进入 PROCESS_BLOCK
* PROCESS_BLOCK中会处理 modules 和 blocks
* index中的
* modules 是 common queue.push()
* blocks 是 async iteratorBlock是 新建chunk queueDelayed.push() 外层循环
*
* 第二次内层循环
* 处理 ./common.js 调用 module.addChunk(chunk);
* 在处理 common 的 modules(./title.js) 和 blocks(无)
*
* 第三次内层循环
* 处理 title.js 没有 modules 和 blocks
*
* 处理queueConnect 在处理blocks的时候会处理
*
* 在处理 queueDelayed 开始外层的循环
* // 这里使用了reverse的
* queue = queueDelayed.reverse()
*
* 开始第二次外层循环
* 开始一次内循环
* queueDelayed的参数 {
* block: b, // index中blocks的block async.js
module: module, // index的module index.js
* }
从bockInfoMap中找到 async 对应的 blockInfo
block为 './async.js' modules为空
*
* 。。。 通过while有一直处理下去 建立 graph图
*
* 我们得到三个 chunkGroup entry async common
*/
// 外层循环 会对queueDelayed的数据集进行处理 第一次的queue是entry开始
while (queue.length) {
// 每一轮的内层都对应同一个chunkGroup 对chunkGroup中所有的module进行处理
while (queue.length) {
const queueItem = queue.pop();
module = queueItem.module;
block = queueItem.block;
chunk = queueItem.chunk;
chunkGroup = queueItem.chunkGroup;
chunkGroupInfo = chunkGroupInfoMap.get(chunkGroup);
// 判断不同的同作
switch (queueItem.action) {
case ADD_AND_ENTER_MODULE: {
if (chunk.addModule(module)) {
module.addChunk(chunk);
}
break;
}
// 入口出的就是进入模块
case ENTER_MODULE: {
chunkGroup.setModuleIndex(
module,
chunkGroupCounters.get(chunkGroup).index++
);
// 添加一个离开的动作
queue.push({ action: LEAVE_MODULE });
}
// 上面没有break 执行完ENTER_MODULE就会执行PROCESS_BLOCK处理代码快
case PROCESS_BLOCK: {
// 获取这个module的同步依赖modules和异步依赖blocks
// 入口出的block就是chunk.entryModule
const blockInfo = blockInfoMap.get(block);
// 同步的依赖
for (const refModule of blockInfo.modules) {
if (chunk.containsModule(refModule)) {
continue; // 有就跳过 否则添加 继续内层的遍历
} else {
// 将依赖的module添加到这个chunkGroup中了
queueBuffer.push({
action: ADD_AND_ENTER_MODULE,
block: refModule,
module: refModule,
chunk,
chunkGroup,
});
}
}
// 处理blocks
for (const block of blockInfo.blocks) iteratorBlock(block);
break;
}
case LEAVE_MODULE: {
// 将这个module添加都了chunkGroup中
chunkGroup.setModuleIndex(module);
break;
}
}
}
// block的处理会set值
while (queueConnect.size > 0) {
for (const [chunkGroup, targets] of queueConnect) {
const info = chunkGroupInfoMap.get(chunkGroup);
// 1. 添加i个新的模块数据集 添加chunkGroup中chunk的模块
const resultingAvailableModules = new Set(minAvailableModules);
resultingAvailableModules.add(m);
info.children = targets;
// 2. 更新chunkGroup的信息
chunkGroupInfoMap.set(target, chunkGroupInfo);
if (outdatedChunkGroupInfo.size > 0) {
// 合并模块
for (const info of outdatedChunkGroupInfo) {
}
}
}
}
// 内层遍历完成表示一个chunkGroup完了
// 处理queueDelayed 就是 用来处理block 异步的是在说有的同步处理了再处理的
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
};
2.3 extraceBlockInfoMap
// 经过之前的流程 我们可以得到4个模块
// 1. 处理index.js对应的模块 因为是pop会先处理block push的
// {NormalModule => Object} // 1.index.js
// {ImportDependenciesBlock => Object} // 2.async
// 2.处理common模块
// {NormalModule => Object} // 3.common
// 3.处理async模块
// {NormalModule => Object} // 4.async模块 里面 import 了 common
// {ImportDependenciesBlock => Object} // 5.async 模块中的 common
// 4.处理title的module
// {NormalModule => Object} // 6.title模块
const extraceBlockInfoMap = () => {
const blockInfoMap = new Map();
const iteratorDependency = (d) => {
// module
blockInfoModules.add(refModule);
};
const iteratorBlockPrepare = (b) => {
blockInfoBlocks.push(b);
// index和async中有block的依赖会再次遍历
blockQueue.push(b); // 将block加入到blockQueue中 下一次遍历
};
// 遍历执行所有的模块 开始我们是有四个模块的
// {"~/src/index.js" => NormalModule} 里面有 import('./async')
// {"~/src/common.js" => NormalModule}
// {"~/src/async.js" => NormalModule} 里面有 import('./common')
// {"~/src/title.js" => NormalModule}
for (const module of compilation.modules) {
blockQueue = [module]; // blockQueue
currentModule = module; // 当前模块
// block的需要加入到队列中再次处理
while (blockQueue.length > 0) {
block = blockQueue.pop(); // 这里用的是pop 所有会先处理block加进来的
// 分别处理 variables dependencies blocks
if (block.variables) {
// 变量
iteratorDependency();
}
if (block.dependencies) {
// 依赖
iteratorDependency();
}
if (block.blocks) {
// import的动态模块依赖
iteratorBlockPrepare();
}
const blockInfo = {
modules: blockInfoModules,
blocks: blockInfoBlocks,
};
// compilation.modules module
blockInfoMap.set(block, blockInfo);
}
}
return blockInfoMap;
};
2.4 connectChunkGroups
// 经过上面的处理 我们得到三个chunkGroup 连接chunkGroup 建立父子关系
const connectChunkGroups = (
blocksWithNestedBlocks,
blockConnections,
chunkGroupInfoMap
) => {
// 检查代码块中是否已经有了所有的模块
const areModulesAvailable = () => {};
// 遍历所有的connections chunk group的依赖
// 在处理blocks的时候 async和common的时候 会被加到里面 blockConnections.set()
// [ImportDependenciesBlock => Array(1), ImportDependenciesBlock => Array(1)]
const map = {
"./sync.js": {
// const chunkGroup = new ChunkGroup(groupOptions);
chunkGroup: compilation.addChunkInGroup(b.chunkName, module),
},
};
// block是blocks中的block
// blockConnections.set('./async', [{originChunkGroupInfo, chunkGroup}])
for (const [block, connections] of blockConnections) {
// 1. 检查连接是否有必要
// 2. Foreach edge
for (let i = 0; i < connections.length; i++) {
// chunkGroup是new 出来的c originChunkGroupInfo是map对象中值
const { chunkGroup, originChunkGroupInfo } = connections[i];
// 之前是通过 chunkGroup.setModuleIndex(module); 往里面添加了module
// 3. Connect block with chunk 把block添加到chunkGroup中
GraphHelpers.connectDependenciesBlockAndChunkGroup(block, chunkGroup);
// 4. Connect chunk with parent建立父子关系 blockInfoMap中的属性
GraphHelpers.connectChunkGroupParentAndChild(
originChunkGroupInfo.chunkGroup,
chunkGroup
);
}
}
};
const connectChunkGroupParentAndChild = (parent, child) => {
// chunkGroup
if (parent.addChild(child)) {
// this._parents.add(parentChunk);
child.addParent(parent);
}
};
// async.js: {chunkGroup: chunkGroup}
const connectDependenciesBlockAndChunkGroup = (depBlock, chunkGroup) => {
if (chunkGroup.addBlock(depBlock)) {
depBlock.chunkGroup = chunkGroup;
}
};
2.5 cleanupUnconnectedGroups
const cleanupUnconnectedGroups = (compilation, allCreatedChunkGroups) => {
// async和common建立的chunkGroup
for (const chunkGroup of allCreatedChunkGroups) {
if (chunkGroup.getNumberOfParents() === 0) {
// 如果有父亲 就将chunkGroup中所有的chunk删除
for (const chunk of chunkGroup.chunks) {
const idx = compilation.chunks.indexOf(chunk);
if (idx >= 0) compilation.chunks.splice(idx, 1);
chunk.remove("unconnected");
}
chunkGroup.remove("unconnected");
}
}
};
3. optimizeChunk
// 生成chunk之后我们对进行一些优化的处理
this.hooks.optimize.call();
// module
this.hooks.optimizeModulesBasic.call(this.modules)
this.hooks.optimizeModules.call(this.modules)
this.hooks.optimizeModulesAdvanced.call(this.modules)
// chunks
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups)
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => {
this.applyModuleIds(); // 设置模块id
this.applyChunkIds(); // 设置 chunk.id
this.createHash(); // hash
// 生成资源
this.createModuleAssets();
// 生成chunk资源
this.hooks.beforeChunkAssets.call();
this.createChunkAssets();
})
4. SplitChunksPlugin
// 这个特性非常重要 单独说明下 当我们触发optimizeChunksAdvanced的时候
// 面试被问过 splitChunks的原理是什么 不知道
// 也是在webpackOptionsApply中处理的
if (options.optimization.splitChunks) {
// 配置的splitChunks
const SplitChunksPlugin = require("./optimize/SplitChunksPlugin");
new SplitChunksPlugin(options.optimization.splitChunks).apply(compiler);
}
// 配置的runtimeChunk
if (options.optimization.runtimeChunk) {
const RuntimeChunkPlugin = require("./optimize/RuntimeChunkPlugin");
new RuntimeChunkPlugin(options.optimization.runtimeChunk).apply(compiler);
}
class SplitChunksPlugin {
constructor(options) {
// 格式化
this.options = SplitChunksPlugin.normalizeOptions(options);
}
apply(compiler) {
compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
// 监听这个钩子 在生成了chunkGroup之后 优化的时候触发的代码分割
compilation.hooks.optimizeChunksAdvanced.tap(
"SplitChunksPlugin",
(chunks) => {}
)
})
}
}
4.1 config
// 以element-admin的配置为例子 加一些配置的说明
config.optimization.splitChunks({
// initial async all function(return chunks) 要提取的模块
chunks: 'all',
// minSize: 10000, // 最小的体积 10k的才会分出来
// maxSize: 0, // 代表能分则分 分不了就算了 保证尽量的小
// minChunks: 1, // 最小的被引用次数
// maxAsyncRequests: 5, // 按需加载的代码块最多允许的并行请求数 在webpack5里默认值变为6
// maxInitialRequests: 3, // 入口代码块最多允许的并行请求数,在webpack5里默认值变为4
// automaticNameDelimiter: "~", // 代码块命名分割符
// name: true, // 每个缓存组打包得到的代码块的名称
cacheGroups: {
libs: {
name: 'chunk-libs', // 打包的名字
test: /[\\/]node_modules[\\/]/, // 正则
priority: 10, // 优先级
chunks: 'initial'
},
elementUI: {
name: 'chunk-elementUI',
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/
},
// 之前一直打包不出来是因为没有设置 minSize
commons: {
name: 'chunk-commons',
test: resolve('src/components'),
minSize: 0, // 最小提取字节数
minChunks: 2, // 最小的被引用次数
priority: 5,
reuseExistingChunk: true // 重用模块
}
}
})
4.2 demo
// 为了演示效果 我们安装element-ui和jquery
// index.js
import $ from "jquery";
import ElementUI from "element-ui";
import("./async.js").then((result) => console.log(result));
import("./async1.js").then((result) => console.log(result));
import("./async2.js").then((result) => console.log(result));
// async async1 async2 设置了minChunks表示至少被几个chunk引用
import title from "./title.js";
4.3
// splitChunk就是将每个模块按照规则分配到不同的缓存组中
// 每个缓存组对应最终分割出来的新代码块
// chunksInfoMap
// addModuleToChunksInfoMap
// newChunk = compilation.addChunk(chunkName);
function apply(compiler) {
compiler.hooks.thisCompilation.tap("SplitChunksPlugin", (compilation) => {
// 监听这个钩子 在生成了chunkGroup之后 优化的时候触发的代码分割
compilation.hooks.optimizeChunksAdvanced.tap(
"SplitChunksPlugin",
(chunks) => {
// 生成一个index 给每个选中的chunk一个index create strings from chunks
indexMap.set(chunk, index++);
// key和chunkSet的关系
const chunkSetsInGraph = new Map();
// 遍历modules为每个module的chunk生成一个key
// 经过上面的处理 我们得到四个chunk n个modules(element-ui和jquery中有很多个module)
// {1: {}, 2: {}, 3: {}, 4: {}, '2, 3, 4': {set(3)}} title.js是在三个chunks中的
for (const module of compilation.modules) {
// chunksIterable return this._chunks
const chunksKey = getKey(module.chunksIterable);
if (!chunkSetsInGraph.has(chunksKey)) {
chunkSetsInGraph.set(chunksKey, new Set(module.chunksIterable));
}
}
// 按照count对这些chunk进行分组
const chunkSetsByCount = new Map();
// {1: [4], 3: [3]} 数量为1的是四个chunk 3个的是 set(3)
for (const chunksSet of chunkSetsInGraph.values()) {
// 每个module对应一个chunksSet 一个chunkSet中有多个module就是有多个count
const count = chunksSet.size;
let array = chunkSetsByCount.get(count);
if (array === undefined) {
array = [];
chunkSetsByCount.set(count, array);
}
array.push(chunksSet);
}
// 创建一个可能组合的列表 Map<string, Set<Chunk>[]>
const combinationsCache = new Map();
// 根据key得到module中对应的chunks集合 set
const chunksSet = chunkSetsInGraph.get(key);
const getCombinations = (key) => {
const chunksSet = chunkSetsInGraph.get(key);
var array = [chunksSet];
if (chunksSet.size > 1) {
for (const [count, setArray] of chunkSetsByCount) {
// "equal" is not needed because they would have been merge in the first step
if (count < chunksSet.size) {
for (const set of setArray) {
if (isSubset(chunksSet, set)) {
array.push(set);
}
}
}
}
}
return array;
};
// 处理缓存
// const selectedChunksCacheByChunksSet = new WeakMap();
// // 通过将过滤器功能应用于列表来获取列表和键 出于性能原因,它被缓存
// const getSelectedChunks = (chunks, chunkFilter) => {};
// Map a list of chunks to a list of modules 代码分割的信息
const chunksInfoMap = new Map();
const addModuleToChunksInfoMap = (
cacheGroup, // the current cache group 当前的cache组
cacheGroupIndex, // the index of the cache group of ordering
selectedChunks, // chunks selected for this module
selectedChunksKey, // a key of selectedChunks
module // the current module
) => {
// minChunks属性
if (selectedChunks.length < cacheGroup.minChunks) return;
// name
const name = cacheGroup.getName();
// key
const key =
cacheGroup.key +
(name ? ` name:${name}` : ` chunks:${selectedChunksKey}`);
let info = chunksInfoMap.get(key);
// 添加新的缓存组信息
chunksInfoMap.set(
key,
(info = {
modules: new SortableSet(undefined, sortByIdentifier),
cacheGroup,
cacheGroupIndex,
name,
size: 0,
chunks: new Set(),
reuseableChunks: new Set(),
chunksKeys: new Set(),
})
);
info.modules.add(module);
info.size += module.size(); // 判断我们设置的miniSize
info.chunksKeys.add(selectedChunksKey);
for (const chunk of selectedChunks) {
info.chunks.add(chunk); // 最后打包的是chunk
}
};
// 遍历模块 分组
for (const module of compilation.modules) {
// 一个module可能符合多个条件 我们会根据 优先级来判断
let cacheGroups = this.options.getCacheGroups(module, context);
const chunksKey = getKey(module.chunksIterable);
for (const cacheGroupSource of cacheGroups) {
const minSize = cacheGroupSource.minSize;
const cacheGroup = {
// 一系列的属性 配置
key: cacheGroupSource.key,
// 默认优先级是0
priority: cacheGroupSource.priority || 0,
minSize,
minChunks: cacheGroupSource.minChunks,
};
}
// 根据webpack的配置 选出符合添加的chunk
for (const chunkCombination of combs) {
addModuleToChunksInfoMap();
}
}
// 处理size < minSize
for (const pair of chunksInfoMap) {
chunksInfoMap.delete(pair[0]);
}
// maxSize
const maxSizeQueueMap = new Map();
while (chunksInfoMap.size > 0) {
let bestEntryKey; // 最匹配的cacheGroup分组信息
let bestEntry;
for (const pair of chunksInfoMap) {
bestEntry = pair[1];
bestEntryKey = pair[0];
}
const item = bestEntry;
chunksInfoMap.delete(bestEntryKey);
let chunkName = item.name;
// 新的chunk
let newChunk;
// 重用模块 如果没有name 看是否能复用
if (item.cacheGroup.reuseExistingChunk) {
}
// 创建新的代码块
newChunk = compilation.addChunk(chunkName);
for (const chunk of usedChunks) {
// Add graph connections for splitted chunk 建立关系
chunk.split(newChunk);
}
if (chunkName) {
const entrypoint = compilation.entrypoints.get(chunkName);
}
// 删除提取出来的模块
for (const [key, info] of chunksInfoMap) {
}
// Make sure that maxSize is fulfilled
for (const chunk of compilation.chunks.slice()) {
}
}
}
);
});
}
5. hash
// 各种hash值 content hash chunk hash full hash module hash
function createHash() {
const hashFunction = outputOptions.hashFunction;
const hash = createHash(hashFunction);
// new MainTemplate(this.outputOptions);
this.mainTemplate.updateHash(hash);
this.chunkTemplate.updateHash(hash);
for (const key of Object.keys(this.moduleTemplates).sort()) {
this.moduleTemplates[key].updateHash(hash);
}
// module hash
for (let i = 0; i < this.modules.length; i++) {
const module = modules[i];
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash);
}
const chunks = this.chunks.slice();
chunks.sort((a, b) => {});
// chunk hash
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const chunkHash = createHash(hashFunction);
// hasRuntime
this.hooks.contentHash.call(chunk);
}
this.fullHash = /** @type {string} */ (hash.digest(hashDigest));
this.hash = this.fullHash.substr(0, hashDigestLength);
}
发表评论
还没有评论,快来抢沙发吧!