溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

webpack構建的詳細流程探底

發(fā)布時間:2020-09-16 12:57:11 來源:腳本之家 閱讀:258 作者:Curious 欄目:web開發(fā)

作為模塊加載和打包神器,只需配置幾個文件,加載各種 loader 就可以享受無痛流程化開發(fā)。但對于 webpack 這樣一個復雜度較高的插件集合,它的整體流程及思想對我們來說還是很透明的。

本文旨在搞清楚從命令行下敲下 webpack 命令,或者配置 npm script 后執(zhí)行 package.json 中的命令,到工程目錄下出現(xiàn)打包的后的 bundle 文件的過程中,webpack都替我們做了哪些工作。

測試用webpack版本為 webpack@3.4.1

webpack.config.js中定義好相關配置,包括 entry、output、module、plugins等,命令行執(zhí)行 webpack 命令,webpack 便會根據(jù)配置文件中的配置進行打包處理文件,并生成最后打包后的文件。

第一步:執(zhí)行 webpack 命令時,發(fā)生了什么?(bin/webpack.js)

命令行執(zhí)行 webpack 時,如果全局命令行中未找到webpack命令的話,執(zhí)行本地的node-modules/bin/webpack.js 文件。

在bin/webpack.js中使用 yargs庫 解析了命令行的參數(shù),處理了 webpack 的配置對象 options,調(diào)用 processOptions() 函數(shù)。

// 處理編譯相關,核心函數(shù)
function processOptions(options) {
 // promise風格的處理,暫時還沒遇到這種情況的配置
 if(typeof options.then === "function") {...} 
 // 處理傳入的options為數(shù)組的情況
 var firstOptions = [].concat(options)[0];
 var statsPresetToOptions = require("../lib/Stats.js").presetToOptions;
 // 設置輸出的options
 var outputOptions = options.stats;
 if(typeof outputOptions === "boolean" || typeof outputOptions === "string") {
 outputOptions = statsPresetToOptions(outputOptions);
 } else if(!outputOptions) {
 outputOptions = {};
 }
 // 處理各種現(xiàn)實相關的參數(shù)
 ifArg("display", function(preset) {
 outputOptions = statsPresetToOptions(preset);
 });
 ...
 // 引入lib下的webpack.js,入口文件
 var webpack = require("../lib/webpack.js");
 // 設置最大錯誤追蹤堆棧
 Error.stackTraceLimit = 30;
 var lastHash = null;
 var compiler;
 try {
 // 編譯,這里是關鍵,需要進入lib/webpack.js文件查看
 compiler = webpack(options);
 } catch(e) {
 // 錯誤處理
 var WebpackOptionsValidationError = require("../lib/WebpackOptionsValidationError");
 if(e instanceof WebpackOptionsValidationError) {
 if(argv.color)
 console.error("\u001b[1m\u001b[31m" + e.message + "\u001b[39m\u001b[22m");
 else
 console.error(e.message);
 process.exit(1); // eslint-disable-line no-process-exit
 }
 throw e;
 }
 // 顯示相關參數(shù)處理
 if(argv.progress) {
 var ProgressPlugin = require("../lib/ProgressPlugin");
 compiler.apply(new ProgressPlugin({
 profile: argv.profile
 }));
 }
 // 編譯完后的回調(diào)函數(shù)
 function compilerCallback(err, stats) {}
 // watch模式下的處理
 if(firstOptions.watch || options.watch) {
 var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
 if(watchOptions.stdin) {
 process.stdin.on("end", function() {
 process.exit(0); // eslint-disable-line
 });
 process.stdin.resume();
 }
 compiler.watch(watchOptions, compilerCallback);
 console.log("\nWebpack is watching the files…\n");
 } else
 // 調(diào)用run()函數(shù),正式進入編譯過程
 compiler.run(compilerCallback);
}

第二步: 調(diào)用 webpack,返回 compiler 對象的過程(lib/webpack.js)

如下圖所示,lib/webpack.js 中的關鍵函數(shù)為 webpack,其中定義了編譯相關的一些操作。

"use strict";
const Compiler = require("./Compiler");
const MultiCompiler = require("./MultiCompiler");
const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin");
const WebpackOptionsApply = require("./WebpackOptionsApply");
const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter");
const validateSchema = require("./validateSchema");
const WebpackOptionsValidationError = require("./WebpackOptionsValidationError");
const webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json");
// 核心方法,調(diào)用該方法,返回Compiler的實例對象compiler
function webpack(options, callback) {...}
exports = module.exports = webpack;
// 設置webpack對象的常用屬性
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;
webpack.WebpackOptionsApply = WebpackOptionsApply;
webpack.Compiler = Compiler;
webpack.MultiCompiler = MultiCompiler;
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
webpack.validate = validateSchema.bind(this, webpackOptionsSchema);
webpack.validateSchema = validateSchema;
webpack.WebpackOptionsValidationError = WebpackOptionsValidationError;
// 對外暴露一些插件
function exportPlugins(obj, mappings) {...}
exportPlugins(exports, {...});
exportPlugins(exports.optimize = {}, {...});

接下來看在webpack函數(shù)中主要定義了哪些操作

// 核心方法,調(diào)用該方法,返回Compiler的實例對象compiler
function webpack(options, callback) {
 // 驗證是否符合格式
 const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
 if(webpackOptionsValidationErrors.length) {
 throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
 }
 let compiler;
 // 傳入的options為數(shù)組的情況,調(diào)用MultiCompiler進行處理,目前還沒遇到過這種情況的配置
 if(Array.isArray(options)) {
 compiler = new MultiCompiler(options.map(options => webpack(options)));
 } else if(typeof options === "object") {
 // 配置options的默認參數(shù)
 new WebpackOptionsDefaulter().process(options);
 // 初始化一個Compiler的實例
 compiler = new Compiler();
 // 設置context的默認值為進程的當前目錄,絕對路徑
 compiler.context = options.context;
 // 定義compiler的options屬性
 compiler.options = options;
 // Node環(huán)境插件,其中設置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定義了before-run的鉤子函數(shù)
 new NodeEnvironmentPlugin().apply(compiler);
 // 應用每個插件
 if(options.plugins && Array.isArray(options.plugins)) {
 compiler.apply.apply(compiler, options.plugins);
 }
 // 調(diào)用environment插件
 compiler.applyPlugins("environment");
 // 調(diào)用after-environment插件
 compiler.applyPlugins("after-environment");
 // 處理compiler對象,調(diào)用一些必備插件
 compiler.options = new WebpackOptionsApply().process(options, compiler);
 } else {
 throw new Error("Invalid argument: options");
 }
 if(callback) {
 if(typeof callback !== "function") throw new Error("Invalid argument: callback");
 if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
 const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {});
 return compiler.watch(watchOptions, callback);
 }
 compiler.run(callback);
 }
 return compiler;
}

webpack函數(shù)中主要做了以下兩個操作,

  • 實例化 Compiler 類。該類繼承自 Tapable 類,Tapable 是一個基于發(fā)布訂閱的插件架構。webpack 便是基于Tapable的發(fā)布訂閱模式實現(xiàn)的整個流程。Tapable 中通過 plugins 注冊插件名,以及對應的回調(diào)函數(shù),通過 apply,applyPlugins,applyPluginsWater,applyPluginsAsync等函數(shù)以不同的方式調(diào)用注冊在某一插件下的回調(diào)。
  • 通過WebpackOptionsApply 處理webpack compiler對象,通過 compiler.apply的方式調(diào)用了一些必備插件,在這些插件中,注冊了一些 plugins,在后面的編譯過程中,通過調(diào)用一些插件的方式,去處理一些流程。

第三步:調(diào)用compiler的run的過程(Compiler.js)

run()調(diào)用

run函數(shù)中主要觸發(fā)了before-run事件,在before-run事件的回調(diào)函數(shù)中觸發(fā)了run事件,run事件中調(diào)用了readRecord函數(shù)讀取文件,并調(diào)用compile()函數(shù)進行編譯。

compile()調(diào)用

compile函數(shù)中定義了編譯的相關流程,主要有以下流程:

  • 創(chuàng)建編譯參數(shù)
  • 觸發(fā) before-compile 事件,
  • 觸發(fā) compile 事件,開始編譯
  • 創(chuàng)建 compilation對象,負責整個編譯過程中具體細節(jié)的對象
  • 觸發(fā) make 事件,開始創(chuàng)建模塊和分析其依賴
  • 根據(jù)入口配置的類型,決定是調(diào)用哪個plugin中的 make 事件的回調(diào)。如單入口的 entry,調(diào)用的是SingleEntryPlugin.js下 make 事件注冊的回調(diào)函數(shù),其他多入口同理。
  • 調(diào)用 compilation 對象的 addEntry 函數(shù),創(chuàng)建模塊以及依賴。
  • make 事件的回調(diào)函數(shù)中,通過seal 封裝構建的結果
  • run 方法中定義的 onCompiled回調(diào)函數(shù)被調(diào)用,完成emit過程,將結果寫入至目標文件

compile函數(shù)的定義

compile(callback) {
 // 創(chuàng)建編譯參數(shù),包括模塊工廠和編譯依賴參數(shù)數(shù)組
 const params = this.newCompilationParams();
 // 觸發(fā)before-compile 事件,開始整個編譯過程
 this.applyPluginsAsync("before-compile", params, err => {
 if(err) return callback(err);
 // 觸發(fā)compile事件
 this.applyPlugins("compile", params);
 // 構建compilation對象,compilation對象負責具體的編譯細節(jié)
 const compilation = this.newCompilation(params);
 // 觸發(fā)make事件,對應的監(jiān)聽make事件的回調(diào)函數(shù)在不同的EntryPlugin中注冊,比如singleEntryPlugin
 this.applyPluginsParallel("make", compilation, err => {
 if(err) return callback(err);
 compilation.finish();
 compilation.seal(err => {
 if(err) return callback(err);
 this.applyPluginsAsync("after-compile", compilation, err => {
 if(err) return callback(err);
 return callback(null, compilation);
 });
 });
 });
 });
}

【問題】make 事件觸發(fā)后,有哪些插件中注冊了make事件并得到了運行的機會呢?

以單入口entry配置為例,在EntryOptionPlugin插件中定義了,不同配置的入口應該調(diào)用何種插件進行解析。不同配置的入口插件中注冊了對應的 make 事件回調(diào)函數(shù),在make事件觸發(fā)后被調(diào)用。

如下所示:

一個插件的apply方法是一個插件的核心方法,當說一個插件被調(diào)用時主要是其apply方法被調(diào)用。

EntryOptionPlugin 插件在webpackOptionsApply中被調(diào)用,其內(nèi)部定義了使用何種插件來解析入口文件。

const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");
module.exports = class EntryOptionPlugin {
 apply(compiler) {
 compiler.plugin("entry-option", (context, entry) => {
 function itemToPlugin(item, name) {
 if(Array.isArray(item)) {
 return new MultiEntryPlugin(context, item, name);
 } else {
 return new SingleEntryPlugin(context, item, name);
 }
 }
 // 判斷entry字段的類型去調(diào)用不同的入口插件去處理
 if(typeof entry === "string" || Array.isArray(entry)) {
 compiler.apply(itemToPlugin(entry, "main"));
 } else if(typeof entry === "object") {
 Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(entry[name], name)));
 } else if(typeof entry === "function") {
 compiler.apply(new DynamicEntryPlugin(context, entry));
 }
 return true;
 });
 }
};

entry-option 事件被觸發(fā)時,EntryOptionPlugin 插件做了這幾個事情:

判斷入口的類型,通過 entry 字段來判斷,對應了 entry 字段為 string object function的三種情況

每種不同的類型調(diào)用不同的插件去處理入口的配置。大致處理邏輯如下:

  • 數(shù)組類型的entry調(diào)用multiEntryPlugin插件去處理,對應了多入口的場景
  • function的entry調(diào)用了DynamicEntryPlugin插件去處理,對應了異步chunk的場景
  • string類型的entry或者object類型的entry,調(diào)用SingleEntryPlugin去處理,對應了單入口的場景

【問題】entry-option 事件是在什么時機被觸發(fā)的呢?

如下代碼所示,是在WebpackOptionsApply.js中,先調(diào)用處理入口的EntryOptionPlugin插件,然后觸發(fā) entry-option 事件,去調(diào)用不同類型的入口處理插件。

注意:調(diào)用插件的過程也就是一個注冊事件以及回調(diào)函數(shù)的過程。

WebpackOptionApply.js

// 調(diào)用處理入口entry的插件
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option", options.context, options.entry);

前面說到,make事件觸發(fā)時,對應的回調(diào)邏輯都在不同配置入口的插件中注冊的。下面以SingleEntryPlugin為例,說明從 make 事件被觸發(fā),到編譯結束的整個過程。

SingleEntryPlugin.js

class SingleEntryPlugin {
 constructor(context, entry, name) {
 this.context = context;
 this.entry = entry;
 this.name = name;
 }
 apply(compiler) {
 // compilation 事件在初始化Compilation對象的時候被觸發(fā)
 compiler.plugin("compilation", (compilation, params) => {
 const normalModuleFactory = params.normalModuleFactory;
 compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
 });
 // make 事件在執(zhí)行compile的時候被觸發(fā)
 compiler.plugin("make", (compilation, callback) => {
 const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
 // 編譯的關鍵,調(diào)用Compilation中的addEntry,添加入口,進入編譯過程。
 compilation.addEntry(this.context, dep, this.name, callback);
 });
 }
 static createDependency(entry, name) {
 const dep = new SingleEntryDependency(entry);
 dep.loc = name;
 return dep;
 }
}
module.exports = SingleEntryPlugin;

Compilation中負責具體編譯的細節(jié),包括如何創(chuàng)建模塊以及模塊的依賴,根據(jù)模板生成js等。如:addEntry,buildModule, processModuleDependencies等。

Compilation.js

addEntry(context, entry, name, callback) {
 const slot = {
 name: name,
 module: null
 };
 this.preparedChunks.push(slot);
 // 添加該chunk上的module依賴
 this._addModuleChain(context, entry, (module) => {
 entry.module = module;
 this.entries.push(module);
 module.issuer = null;
 }, (err, module) => {
 if(err) {
 return callback(err);
 }
 if(module) {
 slot.module = module;
 } else {
 const idx = this.preparedChunks.indexOf(slot);
 this.preparedChunks.splice(idx, 1);
 }
 return callback(null, module);
 });
}
_addModuleChain(context, dependency, onModule, callback) {
 const start = this.profile && Date.now();
 ...
 // 根據(jù)模塊的類型獲取對應的模塊工廠并創(chuàng)建模塊
 const moduleFactory = this.dependencyFactories.get(dependency.constructor);
 ...
 // 創(chuàng)建模塊,將創(chuàng)建好的模塊module作為參數(shù)傳遞給回調(diào)函數(shù)
 moduleFactory.create({
 contextInfo: {
 issuer: "",
 compiler: this.compiler.name
 },
 context: context,
 dependencies: [dependency]
 }, (err, module) => {
 if(err) {
 return errorAndCallback(new EntryModuleNotFoundError(err));
 }
 let afterFactory;
 if(this.profile) {
 if(!module.profile) {
 module.profile = {};
 }
 afterFactory = Date.now();
 module.profile.factory = afterFactory - start;
 }
 const result = this.addModule(module);
 if(!result) {
 module = this.getModule(module);
 onModule(module);
 if(this.profile) {
 const afterBuilding = Date.now();
 module.profile.building = afterBuilding - afterFactory;
 }
 return callback(null, module);
 }
 if(result instanceof Module) {
 if(this.profile) {
 result.profile = module.profile;
 }
 module = result;
 onModule(module);
 moduleReady.call(this);
 return;
 }
 onModule(module);
 // 構建模塊,包括調(diào)用loader處理文件,使用acorn生成AST,遍歷AST收集依賴
 this.buildModule(module, false, null, null, (err) => {
 if(err) {
 return errorAndCallback(err);
 }
 if(this.profile) {
 const afterBuilding = Date.now();
 module.profile.building = afterBuilding - afterFactory;
 }
  // 開始處理收集好的依賴
 moduleReady.call(this);
 });
 function moduleReady() {
 this.processModuleDependencies(module, err => {
 if(err) {
 return callback(err);
 }
 return callback(null, module);
 });
 }
 });
}

_addModuleChain 主要做了以下幾件事情:

  • 調(diào)用對應的模塊工廠類去創(chuàng)建module
  • buildModule,開始構建模塊,收集依賴。構建過程中最耗時的一步,主要完成了調(diào)用loader處理模塊以及模塊之間的依賴,使用acorn生成AST的過程,遍歷AST循環(huán)收集并構建依賴模塊的過程。此處可以深入了解webpack使用loader處理模塊的原理。

第四步:模塊build完成后,使用seal進行module和chunk的一些處理,包括合并、拆分等。

Compilation的 seal 函數(shù)在 make 事件的回調(diào)函數(shù)中進行了調(diào)用。

seal(callback) {
 const self = this;
 // 觸發(fā)seal事件,提供其他插件中seal的執(zhí)行時機
 self.applyPlugins0("seal");
 self.nextFreeModuleIndex = 0;
 self.nextFreeModuleIndex2 = 0;
 self.preparedChunks.forEach(preparedChunk => {
 const module = preparedChunk.module;
 // 將module保存在chunk的origins中,origins保存了module的信息
 const chunk = self.addChunk(preparedChunk.name, module);
 // 創(chuàng)建一個entrypoint
 const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
 // 將chunk創(chuàng)建的chunk保存在entrypoint中,并將該entrypoint的實例保存在chunk的entrypoints中
 entrypoint.unshiftChunk(chunk);
 // 將module保存在chunk的_modules數(shù)組中
 chunk.addModule(module);
 // module實例上記錄chunk的信息
 module.addChunk(chunk);
 // 定義該chunk的entryModule屬性
 chunk.entryModule = module;
 self.assignIndex(module);
 self.assignDepth(module);
 self.processDependenciesBlockForChunk(module, chunk);
 });
 self.sortModules(self.modules);
 self.applyPlugins0("optimize");
 while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||
 self.applyPluginsBailResult1("optimize-modules", self.modules) ||
 self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { /* empty */ }
 self.applyPlugins1("after-optimize-modules", self.modules);
 while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) ||
 self.applyPluginsBailResult1("optimize-chunks", self.chunks) ||
 self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)) { /* empty */ }
 self.applyPlugins1("after-optimize-chunks", self.chunks);
 self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) {
 if(err) {
 return callback(err);
 }
 self.applyPlugins2("after-optimize-tree", self.chunks, self.modules);
 while(self.applyPluginsBailResult("optimize-chunk-modules-basic", self.chunks, self.modules) ||
 self.applyPluginsBailResult("optimize-chunk-modules", self.chunks, self.modules) ||
 self.applyPluginsBailResult("optimize-chunk-modules-advanced", self.chunks, self.modules)) { /* empty */ }
 self.applyPlugins2("after-optimize-chunk-modules", self.chunks, self.modules);
 const shouldRecord = self.applyPluginsBailResult("should-record") !== false;
 self.applyPlugins2("revive-modules", self.modules, self.records);
 self.applyPlugins1("optimize-module-order", self.modules);
 self.applyPlugins1("advanced-optimize-module-order", self.modules);
 self.applyPlugins1("before-module-ids", self.modules);
 self.applyPlugins1("module-ids", self.modules);
 self.applyModuleIds();
 self.applyPlugins1("optimize-module-ids", self.modules);
 self.applyPlugins1("after-optimize-module-ids", self.modules);
 self.sortItemsWithModuleIds();
 self.applyPlugins2("revive-chunks", self.chunks, self.records);
 self.applyPlugins1("optimize-chunk-order", self.chunks);
 self.applyPlugins1("before-chunk-ids", self.chunks);
 self.applyChunkIds();
 self.applyPlugins1("optimize-chunk-ids", self.chunks);
 self.applyPlugins1("after-optimize-chunk-ids", self.chunks);
 self.sortItemsWithChunkIds();
 if(shouldRecord)
 self.applyPlugins2("record-modules", self.modules, self.records);
 if(shouldRecord)
 self.applyPlugins2("record-chunks", self.chunks, self.records);
 self.applyPlugins0("before-hash");
 // 創(chuàng)建hash
 self.createHash();
 self.applyPlugins0("after-hash");
 if(shouldRecord)
 self.applyPlugins1("record-hash", self.records);
 self.applyPlugins0("before-module-assets");
 self.createModuleAssets();
 if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) {
 self.applyPlugins0("before-chunk-assets");
 // 使用template創(chuàng)建最后的js代碼
 self.createChunkAssets();
 }
 self.applyPlugins1("additional-chunk-assets", self.chunks);
 self.summarizeDependencies();
 if(shouldRecord)
 self.applyPlugins2("record", self, self.records);
 self.applyPluginsAsync("additional-assets", err => {
 if(err) {
 return callback(err);
 }
 self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => {
 if(err) {
 return callback(err);
 }
 self.applyPlugins1("after-optimize-chunk-assets", self.chunks);
 self.applyPluginsAsync("optimize-assets", self.assets, err => {
 if(err) {
 return callback(err);
 }
 self.applyPlugins1("after-optimize-assets", self.assets);
 if(self.applyPluginsBailResult("need-additional-seal")) {
 self.unseal();
 return self.seal(callback);
 }
 return self.applyPluginsAsync("after-seal", callback);
 });
 });
 });
 });
}

在 seal 中可以發(fā)現(xiàn),調(diào)用了很多不同的插件,主要就是操作chunk和module的一些插件,生成最后的源代碼。其中 createHash 用來生成hash,createChunkAssets 用來生成chunk的源碼,createModuleAssets 用來生成Module的源碼。在 createChunkAssets 中判斷了是否是入口chunk,入口的chunk用mainTemplate生成,否則用chunkTemplate生成。

第五步:通過 emitAssets 將生成的代碼輸入到output的指定位置

在compiler中的 run 方法中定義了compile的回調(diào)函數(shù) onCompiled, 在編譯結束后,會調(diào)用該回調(diào)函數(shù)。在該回調(diào)函數(shù)中調(diào)用了 emitAsset,觸發(fā)了 emit 事件,將文件寫入到文件系統(tǒng)中的指定位置。

總結

webpack的源碼通過采用Tapable控制其事件流,并通過plugin機制,在webpack構建過程中將一些事件鉤子暴露給plugin,使得開發(fā)者可以通過編寫相應的插件來自定義打包。

好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。

參考文章:

細說 webpack 之流程篇

webpack 源碼解析

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI