本文概要

  • Webpack事件流机制

  • Webpack流程概览

  • Webpack流程图示

  • Webpack流程详解

  • Webpack执行流程源码分析

Webpack事件流机制

Webpack是基于事件流的插件集合,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,Tapable是一个类似Node.js的EventEmitter的库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。Webpack中最核心的负责编译的Compiler和负责创建的捆绑包的Compilation都是Tapable实例。
Tapable库暴露很多Hook类,为插件提供挂载的钩子。

const {SyncHook,  // 同步钩子SyncBailHook, // 同步熔断钩子SyncWaterfallHook, // 同步流水钩子SyncLoopHook, // 同步循环钩子AsyncParallelHook, // 异步并发钩子AsyncParallelBailHook, // 异步并发熔断钩子AsyncSeriesHook, // 异步串行钩子AsyncSeriesBailHook, // 异步串行熔断钩子AsyncSeriesWaterfallHook // 异步串行流水钩子} = require("tapable");
const hook = new SyncHook(["arg1", "arg2", "arg3"]);

 

Tapable提供了同步和异步绑定钩子的方法,并且他们都有绑定事件以及执行事件对应的方法。

Async*
绑定: tapAsync/tapPromise/tap   执行: callAsync/promise
Sync*
绑定: tap                       执行: call

Tapable使用实际例子:

const { SyncHook } = require("tapable");
let queue = new SyncHook(['name']); //所有的构造函数都接收一个可选的参数,这个参数是一个字符串的数组。// 订阅
queue.tap('1', function (name, name2) {// tap 的第一个参数是用来标识订阅的函数的console.log(name, name2, 1);return'1'
});
queue.tap('2', function (name) {console.log(name, 2);
});
queue.tap('3', function (name) {console.log(name, 3);
});// 发布
queue.call('webpack', 'webpack-cli');// 发布的时候触发订阅的函数 同时传入参数// 执行结果:
/*
webpack undefined 1 // 传入的参数需要和new实例的时候保持一致,否则获取不到多传的参数
webpack 2
webpack 3
*/

Webpack中Tapable的应用

if (Array.isArray(options)) {compiler = new MultiCompiler(Array.from(options).map(options => webpack(options)));} elseif (typeof options === "object") {// 1 做初始的操作options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);compiler.options = options;// 2  必须插件有apply接受compiler 参数new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作if (options.plugins && Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);} else {plugin.apply(compiler);}}}compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();compiler.options = new WebpackOptionsApply().process(options, compiler);}

Webpack流程概览

Webpack首先会把配置参数和命令行的参数及默认参数合并,并初始化需要使用的插件和配置插件等执行环境所需要的参数;初始化完成后会调用Compiler的run来真正启动webpack编译构建过程,webpack的构建流程包括compile、make、build、seal、emit阶段,执行完这些阶段就完成了构建过程。

  • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。

  • 开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory,不然后面碰到了就不知道如何处理了。

  • 编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。

  • 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。

  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

Webpack打包流程图示

Webpack流程详解

分为三个阶段: 初始化阶段,编译阶段,输出文件(chunk)。

初始化阶段:

  • 初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。

  • 初始化默认参数配置: new WebpackOptionsDefaulter().process(options)

  • 实例化Compiler对象:用上一步得到的参数初始化Compiler实例,Compiler负责文件监听和启动编译。Compiler实例中包含了完整的Webpack配置,全局只有一个Compiler实例。

  • 加载插件: 依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时给插件传入compiler实例的引用,以方便插件通过compiler调用Webpack提供的API。

  • 处理入口: 读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备。

new EntryOptionPlugin().apply(compiler)  new SingleEntryPlugin(context, item, name)  compiler.hooks.make.tapAsync

编译阶段

  • run阶段:启动一次新的编译。this.hooks.run.callAsync。

  • compile: 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象。

  • compilation: 当Webpack以开发模式运行时,每当检测到文件变化,一次新的Compilation将被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation对象也提供了很多事件回调供插件做扩展。

  • make:一个新的 Compilation 创建完毕主开始编译  完毕主开始编译this.hooks.make.callAsync。

  • addEntry: 即将从 Entry 开始读取文件。

  • _addModuleChain: 根据依赖查找对应的工厂函数,并调用工厂函数的create来生成一个空的MultModule对象,并且把MultModule对象存入compilation的modules中后执行MultModule.build。

  • buildModules: 使用对应的Loader去转换一个模块。开始编译模块,this.buildModule(module)  buildModule(module, optional, origin,dependencies, thisCallback)。

  • build: 开始真正编译模块。

  • doBuild: 开始真正编译入口模块。

  • normal-module-loader: 在用Loader对一个模块转换完后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便Webpack后面对代码的分析。

  • program: 从配置的入口模块开始,分析其AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。

输出阶段

  • seal: 封装 compilation.seal seal(callback)。

  • addChunk: 生成资源 addChunk(name)。

  • createChunkAssets: 创建资源 this.createChunkAssets()。

  • getRenderManifest: 获得要渲染的描述文件 getRenderManifest(options)。

  • render: 渲染源码 source = fileManifest.render()。

  • afterCompile: 编译结束   this.hooks.afterCompile。

  • shouldEmit: 所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。this.hooks.shouldEmit。

  • emit: 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。

this.emitAssets(compilation)  this.hooks.emit.callAsync const emitFiles = err this.outputFileSystem.writeFile
  • done: 全部完成     this.hooks.done.callAsync。

Webpack执行流程源码分析

注: 因webpack源码量较大,笔者在此依据上文执行流程关键节点进行源码分析,建议读者依据webpack源码对照本文进行阅读。

Step1: 初始化入口分析 entery-option

webpack内部会有一个默认的配置,在 webpack.js 入口处理函数中,初始化了所有的默认配置, 在WebpackOptionsDefaulter()中,配置了很多关于 resolve 和 resolveLoader 的配置。主要有三种 对模块的默认配置,输出output,解析optimization,加载resolve模块以及resolveLoader。我们首先看一下webpack.js 入口:

// ~/webpack/lib/webpack.jsif (Array.isArray(options)) {compiler = new MultiCompiler(Array.from(options).map(options => webpack(options)));} elseif (typeof options === "object") {// 做初始的操作options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);compiler.options = options;// 可以看出插件必须有apply以及接受compiler 参数new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);// 插件接收compiler对象上的hooks,议案事件触发,插件也会执行操作if (options.plugins && Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);} else {plugin.apply(compiler);}}}compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();compiler.options = new WebpackOptionsApply().process(options, compiler);}

process方法将我们写的 webpack 的配置 和默认的配置合并。并且 process 过程里会注入关于 normal/context/loader 的默认配置的获取函数。

// ~/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

我们接下来看下EntryOptionPlugin的实现:

// ~/webpack/lib/EntryOptionPlugin.js
const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");const itemToPlugin = (context, item, name) => {if (Array.isArray(item)) {returnnew MultiEntryPlugin(context, item, name);}returnnew SingleEntryPlugin(context, item, name);
};module.exports = class EntryOptionPlugin {apply(compiler) {compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {// string 类型则为 new SingleEntryPlugin// array 类型则为 new MultiEntryPluginif (typeof entry === "string" || Array.isArray(entry)) {itemToPlugin(context, entry, "main").apply(compiler);} elseif (typeof entry === "object") {// 对于 object 类型,遍历其中每一项for (const name ofObject.keys(entry)) {itemToPlugin(context, entry[name], name).apply(compiler);}} elseif (typeof entry === "function") {// function 类型则为 DynamicEntryPluginnew DynamicEntryPlugin(context, entry).apply(compiler);}returntrue;});}
};

在 EntryOptionsPlugin中注册了 entryOption 的事件处理函数,根据 entry 值的不同类型(string/array/object中每一项/functioin)实例化和执行不同的 EntryPlugin:string 对应 SingleEntryPlugin;array 对应 MultiEntryPlugin;function 对应 DynamicEntryPlugin。而对于 object 类型来说遍历其中的每一个 key,将每一个 key 当做一个入口,并根据类型 string/array 的不同选择 SingleEntryPlugin 或 MultiEntryPlugin。在最后执行 compiler.run(callback)。

Step2: run

直接看源码,在源码中打注释:

// ~/webpack/lib/Compiler.jsrun(callback) {if (this.running) return callback(new ConcurrentCompilationError());const finalCallback = (err, stats) => {this.running = false;if (err) {this.hooks.failed.call(err);}if (callback !== undefined) return callback(err, stats);};// 计算编译进行时间的初始时间点const startTime = Date.now();this.running = true;// 当编译完成后需要执行的函数,处理编译完成后的事情const onCompiled = (err, compilation) => {if (err) return finalCallback(err);if (this.hooks.shouldEmit.call(compilation) === false) {const stats = new Stats(compilation);stats.startTime = startTime;stats.endTime = Date.now();this.hooks.done.callAsync(stats, err => {if (err) return finalCallback(err);return finalCallback(null, stats);});return;}// 调用emitAsset方法,emitAsset主要负责写入文件输出文件this.emitAssets(compilation, err => {if (err) return finalCallback(err);if (compilation.hooks.needAdditionalPass.call()) {compilation.needAdditionalPass = true;const stats = new Stats(compilation);stats.startTime = startTime;stats.endTime = Date.now();//继续异步调用时间流this.hooks.done.callAsync(stats, err => {if (err) return finalCallback(err);//  这次多了一个时间流,调用额外编译,告知编译终于编完了this.hooks.additionalPass.callAsync(err => {if (err) return finalCallback(err);this.compile(onCompiled);});});return;}this.emitRecords(err => {if (err) return finalCallback(err);const stats = new Stats(compilation);stats.startTime = startTime;stats.endTime = Date.now();this.hooks.done.callAsync(stats, err => {if (err) return finalCallback(err);return finalCallback(null, stats);});});//最终总结,无论走那个分支都是 new Stats(compilation) 返回stats的回调函数,按照目前的流程走的是最后一个分支,调用 this.emitRecords});};// this.hooks.beforeRun this.hooks.run 主要是构建一个架子,面向切面编程this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);this.readRecords(err => {if (err) return finalCallback(err);// 执行compile 方法 /把onCompiled函数传入,调用compilethis.compile(onCompiled);});});});}

Step3: 执行compile 方法

// ~/webpack/lib/Compiler.jscompile(callback) {const params = this.newCompilationParams();this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);this.hooks.compile.call(params);// 新建一个 compilation// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样const compilation = this.newCompilation(params);// 执行make函数, 从 entry开始递归分析依赖,对每个依赖模块进行build,对complie.hooks.make进行执行this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);compilation.finish(err => {if (err) return callback(err);compilation.seal(err => {if (err) return callback(err);this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);return callback(null, compilation);});});});});});}

Step4: make实现

在complie中进行 this.hooks.make.callAsync,在compiler.hooks.make.tapAsync进行监听,执行监听 监听SingleEntryPlugin中的插件,webpack入口条目,参数是单入口字符串,单入口数组,多入口对象还是动态函数,无论是什么都会调用compilation.addEntry方法,这个方法会执行_addModuleChain,将入口文件加入需要编译的体积中。然后少量中的文件被一个一个处理,文件中的import约会了其他的文件又会通过addModuleDependencies加入到编译层次中。最终当这个编译堆栈中的内容完成被处理完时,就完成了文件到模块的转化。

// ~/webpack/lib/SingleEntryPlugin.jsapply(compiler) {compiler.hooks.compilation.tap("SingleEntryPlugin",(compilation, { normalModuleFactory }) => {compilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory);});// 对compiler.hooks进行tapAsync 进行异步绑定,在compiler进行监听compiler.hooks.make.tapAsync("SingleEntryPlugin",(compilation, callback) => {const { entry, name, context } = this;const dep = SingleEntryPlugin.createDependency(entry, name);compilation.addEntry(context, dep, name, callback);});

Step5: addEntry内部实现

// ~/webpack/lib/Compilation.js
addEntry(context, entry, name, callback) {// context => 默认为process.cwd()// entry => dep => SingleEntryDependency// name => 单入口默认为main// callback => 后面的流程const slot = {name: name,module: null};// 初始为[]this.preparedChunks.push(slot);this._addModuleChain(context, entry, (module) => { /**/ }, (err, module) => { /**/ });}

Step6: _addModuleChain

主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。通过 ModuleFactory.create *ModuleFactory.create方法创建模块,(有NormalModule , MultiModule , ContextModule , DelegatedModule 等)

// ~/webpack/lib/Compilation.js
class Compilation extends Tapable {// ..._addModuleChain(context, dependency, onModule, callback) {// profile => options.profile// 不传则start为undefinedconst start = this.profile && Date.now();// bail => options.bailconst errorAndCallback = this.bail ? (err) => {callback(err);} : (err) => {err.dependencies = [dependency];this.errors.push(err);callback();};if (typeof dependency !== "object" || dependency === null || !dependency.constructor) {thrownewError("Parameter 'dependency' must be a Dependency");}// dependencyFactories包含了所有的依赖集合const moduleFactory = this.dependencyFactories.get(dependency.constructor);if (!moduleFactory) {thrownewError(`No dependency factory available for this dependency type: ${dependency.constructor.name}`);}this.semaphore.acquire(() => {moduleFactory.create({contextInfo: {issuer: "",compiler: this.compiler.name},context: context,dependencies: [dependency]},(err, module) => {if (err) {this.semaphore.release();return errorAndCallback(new EntryModuleNotFoundError(err));}let afterFactory;if (currentProfile) {afterFactory = Date.now();currentProfile.factory = afterFactory - start;}// 将所有的依赖放入modulesconst addModuleResult = this.addModule(module);module = addModuleResult.module;onModule(module);dependency.module = module;module.addReason(null, dependency);const afterBuild = () => {if (addModuleResult.dependencies) {this.processModuleDependencies(module, err => {if (err) return callback(err);callback(null, module);});} else {return callback(null, module);}};if (addModuleResult.issuer) {if (currentProfile) {module.profile = currentProfile;}}if (addModuleResult.build) {// 执行buildModulethis.buildModule(module, false, null, null, err => {if (err) {this.semaphore.release();return errorAndCallback(err);}if (currentProfile) {const afterBuilding = Date.now();currentProfile.building = afterBuilding - afterFactory;}this.semaphore.release();afterBuild();});} else {this.semaphore.release();this.waitForBuildingFinished(module, afterBuild);}});});}
}

_addModuleChain 和 addModuleDependencies 函数中都会调用 this.semaphore.acquire 这个函数的具体实现在 lib/util/Semaphore.js 文件中。看一下具体的实现:

// lib/util/Semaphore.js
class Semaphore {constructor(available) {// available 为最大的并发数量this.available = available;this.waiters = [];this._continue = this._continue.bind(this);}acquire(callback) {if (this.available > 0) {this.available--;callback();} else {this.waiters.push(callback);}}release() {this.available++;if (this.waiters.length > 0) {process.nextTick(this._continue);}}_continue() {if (this.available > 0) {if (this.waiters.length > 0) {this.available--;const callback = this.waiters.pop();callback();}}}
}

对外暴露的只有两个个方法:

  • acquire: 申请处理资源,如果有闲置资源(即并发数量)则立即执行处理,并且闲置的资源减1;否则存入等待队列中。

  • release: 释放资源。在 acquire 中会调用 callback 方法,在这里需要使用 release 释放资源,将闲置资源加1。同时会检查是否还有待处理内容,如果有则继续处理

这个 Semaphore 类借鉴了在多线程环境中,对使用资源进行控制的 Semaphore(信号量)的概念。其中并发个数通过 available 来定义,那么默认值是多少呢?在 Compilation.js 中可以找到 this.semaphore = new Semaphore(options.parallelism || 100); 复制代码 默认的并发数是 100,注意这里说的并发只是代码设计中的并发,不要和js的单线程特性搞混了。

Step7: buildModule

对modules进行build包括 调用loader处理源文件,使用acorn生成AST并且遍历AST,遇到require创建依赖depende,并且加入数组.

// ~/webpack/lib/Compilation.jsbuildModule(module, optional, origin, dependencies, thisCallback) {let callbackList = this._buildingModules.get(module);if (callbackList) {callbackList.push(thisCallback);return;}this._buildingModules.set(module, (callbackList = [thisCallback]));const callback = err => {this._buildingModules.delete(module);for (const cb of callbackList) {cb(err);}};this.hooks.buildModule.call(module);// 开始build,主要执行的是NormalModuleFactory生成的NormalModule中的build方法,中的build方法,打开NormalModulemodule.build(this.options,this,this.resolverFactory.get("normal", module.resolveOptions),this.inputFileSystem,error => {const errors = module.errors;for (let indexError = 0; indexError < errors.length; indexError++) {const err = errors[indexError];err.origin = origin;err.dependencies = dependencies;if (optional) {this.warnings.push(err);} else {this.errors.push(err);}}const warnings = module.warnings;for (let indexWarning = 0;indexWarning < warnings.length;indexWarning++) {const war = warnings[indexWarning];war.origin = origin;war.dependencies = dependencies;this.warnings.push(war);}const originalMap = module.dependencies.reduce((map, v, i) => {map.set(v, i);return map;}, newMap());module.dependencies.sort((a, b) => {const cmp = compareLocations(a.loc, b.loc);if (cmp) return cmp;return originalMap.get(a) - originalMap.get(b);});if (error) {this.hooks.failedModule.call(module, error);return callback(error);}this.hooks.succeedModule.call(module);return callback();});}

Step8: build方法

Compilation.buildModule 中调用的 module.build 方法实际是NormalModule.build。

//~/webpack/lib/NormalModule.jstry {// 调用parse方法,创建依赖Dependency并放入依赖数组,这里会将 source 转为 AST,分析出所有的依赖著作权归原作者所有。const result = this.parser.parse(this._ast || this._source.source(), {current: this,module: this,compilation: compilation,options: options},(err, result) => {if (err) {handleParseError(err);} else {handleParseResult(result);}});if (result !== undefined) {// parse is synchandleParseResult(result);}}build(options, compilation, resolver, fs, callback) {this.buildTimestamp = Date.now();this.built = true;this._source = null;this._sourceSize = null;this._ast = null;this._buildHash = "";this.error = null;this.errors.length = 0;this.warnings.length = 0;this.buildMeta = {};this.buildInfo = {cacheable: false,fileDependencies: newSet(),contextDependencies: newSet(),assets: undefined,assetsInfo: undefined};returnthis.doBuild(options, compilation, resolver, fs, err => {this._cachedSources.clear();// if we have an error mark module as failed and exitif (err) {this.markModuleAsErrored(err);this._initBuildHash(compilation);return callback();}// check if this module should !not! be parsed.// if so, exit here;const noParseRule = options.module && options.module.noParse;if (this.shouldPreventParsing(noParseRule, this.request)) {this._initBuildHash(compilation);return callback();}const handleParseError = e => {const source = this._source.source();const loaders = this.loaders.map(item =>contextify(options.context, item.loader));const error = new ModuleParseError(this, source, e, loaders);this.markModuleAsErrored(error);this._initBuildHash(compilation);return callback();};const handleParseResult = result => {this._lastSuccessfulBuildMeta = this.buildMeta;this._initBuildHash(compilation);return callback();};try {// / 调用parse方法,创建依赖Dependency并放入依赖数组,这时传入 parse 方法中的就是 loader 处理之后,// 返回的 extraInfo.webpackAST,类型是 AST 对象。这么做的好处是什么呢?如果 loader 处理过程中已经执行过将文件转化为 AST 了,// 那么这个 AST 对象保存到 extraInfo.webpackAST 中,在这一步就可以直接复用,以避免重复生成 AST,提升性能。const result = this.parser.parse(this._ast || this._source.source(), {current: this,module: this,compilation: compilation,options: options},(err, result) => {if (err) {handleParseError(err);} else {handleParseResult(result);}});if (result !== undefined) {// parse is synchandleParseResult(result);}} catch (e) {handleParseError(e);}});}

Step9: doBuild

//~/webpack/lib/NormalModule.jsdoBuild(options, compilation, resolver, fs, callback) {const loaderContext = this.createLoaderContext(resolver,options,compilation,fs);// 获取loader相关的信息并转换成webpack需要的js文件runLoaders({resource: this.resource,loaders: this.loaders,context: loaderContext,readResource: fs.readFile.bind(fs)},(err, result) => {if (result) {this.buildInfo.cacheable = result.cacheable;this.buildInfo.fileDependencies = newSet(result.fileDependencies);this.buildInfo.contextDependencies = newSet(result.contextDependencies);}if (err) {if (!(err instanceofError)) {err = new NonErrorEmittedError(err);}const currentLoader = this.getCurrentLoader(loaderContext);const error = new ModuleBuildError(this, err, {from: currentLoader &&compilation.runtimeTemplate.requestShortener.shorten(currentLoader.loader)});return callback(error);}const resourceBuffer = result.resourceBuffer;const source = result.result[0];const sourceMap = result.result.length >= 1 ? result.result[1] : null;const extraInfo = result.result.length >= 2 ? result.result[2] : null;// runLoader 结果是一个数组:[source, sourceMap, extraInfo], extraInfo.webpackAST 如果存在,则会被保存到 module._ast 中。// 也就是说,loader 除了返回处理完了 source 之后,还可以返回一个 AST 对象。在 doBuild 的回调中会优先使用 module._astif (!Buffer.isBuffer(source) && typeof source !== "string") {const currentLoader = this.getCurrentLoader(loaderContext, 0);const err = newError(`Final loader (${currentLoader? compilation.runtimeTemplate.requestShortener.shorten(currentLoader.loader): "unknown"}) didn't return a Buffer or String`);const error = new ModuleBuildError(this, err);return callback(error);}this._source = this.createSource(this.binary ? asBuffer(source) : asString(source),resourceBuffer,sourceMap);// ExtraInfo.webpackAST 如果存在,则会被保存到 module._ast 中。this._sourceSize = null;this._ast =typeof extraInfo === "object" &&extraInfo !== null &&extraInfo.webpackAST !== undefined ?extraInfo.webpackAST :null;// 回build中执行ast(抽象语法树,编译过程中常见结构,vue、babel原理都有)return callback();});}

整个 parse 的过程关于依赖的部分,我们总结一下:

  • 将 source 转为 AST(如果 source 是字符串类型)

  • 遍历 AST,遇到 import 语句就增加相关依赖,代码中出现 A(import 导入的变量) 的地方也增加相关的依赖。

所有的依赖都被保存在 module.dependencies 中,一共有下面4个

HarmonyCompatibilityDependency
HarmonyInitDependency
ConstDependency
HarmonyImportSideEffectDependency
HarmonyImportSpecifierDependency

到此 build 阶段就结束了,回到 module.build 的回调函数。对于所有的依赖再次经过 create->build->add->processDep。如此递归下去,最终我们所有的文件就都转化为了 module,并且会得到一个 module 和 dependencies 的关系结构,这个结构会交给后续的 chunck 和 生成打包文件代码使用。module 生成的过程结束之后,最终会回到 Compiler.js 中的 compile 方法的 make 事件回调中,module 生成的过程就结束了。

Step10: Seal函数

seal函数之后的流程,就主要和chunk函数生成有关了。回调的 seal 方法中,将运用这些 module 以及 module 的 dependencies 信息整合出最终的 chunck。

  • module,就是不同的资源文件,包含了你的代码中提供的例如:js/css/图片 等文件,在编译环节,webpack 会根据不同 module 之间的依赖关系去组合生成 chunk。webpack 打包构建时会根据你的具体业务代码和 webpack 相关配置来决定输出的最终文件,具体的文件的名和文件数量也与此相关。而这些文件就被称为 chunk。例如在你的业务当中使用了异步分包的 API。

  • chunk由 module 组成,一个 chunk 可以包含多个 module,它是 webpack 编译打包后输出的最终文件。例如:

import('./foo.js').then(bar => bar())

在最终输出的文件当中,foo.js会被单独输出一个 chunk 文件。这些生成的 chunk 文件当中即是由相关的 module 模块所构成的。下面是seal函数的入口:

~/webpack/lib/Compiler.jscompile(callback) {const params = this.newCompilationParams();this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);this.hooks.compile.call(params);// 新建一个 compilation// compilation 里面也定义了 this.hooks , 原理和 Compiler 一样const compilation = this.newCompilation(params);// 执行make函数,四:从 entry开始递归分析依赖,对每个依赖模块进行build,对complie.hooks.make进行执行this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);compilation.finish(err => {if (err) return callback(err);compilation.seal(err => {if (err) return callback(err);this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);return callback(null, compilation);});});});});});}

下面我们看下seal函数具体实现:

// ~/lib/Compilation.jsseal(callback) {this.hooks.seal.call();while (this.hooks.optimizeDependenciesBasic.call(this.modules) ||this.hooks.optimizeDependencies.call(this.modules) ||this.hooks.optimizeDependenciesAdvanced.call(this.modules)) {/* empty */}this.hooks.afterOptimizeDependencies.call(this.modules);this.hooks.beforeChunks.call();// 根据 addEntry 方法中收集到入口文件组成的 _preparedEntrypoints 数组for (const preparedEntrypoint ofthis._preparedEntrypoints) {constmodule = preparedEntrypoint.module;const name = preparedEntrypoint.name;const chunk = this.addChunk(name); // 入口 chunk 且为 runtimeChunkconst entrypoint = new Entrypoint(name); // 每一个 entryPoint 就是一个 chunkGroupentrypoint.setRuntimeChunk(chunk); // 设置 runtime chunkentrypoint.addOrigin(null, name, preparedEntrypoint.request);this.namedChunkGroups.set(name, entrypoint); // 设置 chunkGroups 的内容this.entrypoints.set(name, entrypoint);this.chunkGroups.push(entrypoint);// 建立起 chunkGroup 和 chunk 之间的关系GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);// 建立起 chunk 和 module 之间的关系GraphHelpers.connectChunkAndModule(chunk, module);chunk.entryModule = module;chunk.name = name;this.assignDepth(module);}buildChunkGraph(this,/** @type {Entrypoint[]} */ (this.chunkGroups.slice()));// 对 module 进行排序this.sortModules(this.modules);// 创建完 chunk 之后的 hookthis.hooks.afterChunks.call(this.chunks);this.hooks.optimize.call();while (this.hooks.optimizeModulesBasic.call(this.modules) ||this.hooks.optimizeModules.call(this.modules) ||this.hooks.optimizeModulesAdvanced.call(this.modules)) {/* empty */}// 优化 module 之后的 hookthis.hooks.afterOptimizeModules.call(this.modules);while (this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||// 主要涉及到 webpack config 当中的有关 optimization 配置的相关内容this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)) {/* empty */}// 优化 chunk 之后的 hookthis.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups);
.....}

在这个过程当中首先遍历 webpack config 当中配置的入口 module,每个入口 module 都会通过addChunk方法去创建一个 chunk,而这个新建的 chunk 为一个空的 chunk,即不包含任何与之相关联的 module。之后实例化一个 entryPoint,而这个 entryPoint 为一个 chunkGroup,每个 chunkGroup 可以包含多的 chunk,同时内部会有个比较特殊的 runtimeChunk(当 webpack 最终编译完成后包含的 webpack runtime 代码最终会注入到 runtimeChunk 当中)。到此仅仅是分别创建了 chunk 以及 chunkGroup,接下来便调用GraphHelpers模块提供的connectChunkGroupAndChunk及connectChunkAndModule方法来建立起 chunkGroup 和 chunk 之间的联系,以及 chunk 和 入口 module 之间(这里还未涉及到依赖 module)的联系。

Step11: createChunkAssets

// ~/lib/Compilation.jscreateChunkAssets() {const outputOptions = this.outputOptions;const cachedSourceMap = newMap();/** @type {Map<string, {hash: string, source: Source, chunk: Chunk}>} */const alreadyWrittenFiles = newMap();for (let i = 0; i < this.chunks.length; i++) {const chunk = this.chunks[i];chunk.files = [];let source;let file;let filenameTemplate;try {// 1 chunk.entry判断是mainTemplate(入口文件打包)还是chunkTemplate(异步加载js打包模板)const template = chunk.hasRuntime()? this.mainTemplate: this.chunkTemplate;// 2 生成manifest 对象const manifest = template.getRenderManifest({chunk,hash: this.hash,fullHash: this.fullHash,outputOptions,moduleTemplates: this.moduleTemplates,dependencyTemplates: this.dependencyTemplates}); // [{ render(), filenameTemplate, pathOptions, identifier, hash }]for (const fileManifest of manifest) {const cacheName = fileManifest.identifier;const usedHash = fileManifest.hash;filenameTemplate = fileManifest.filenameTemplate;const pathAndInfo = this.getPathWithInfo(filenameTemplate,fileManifest.pathOptions);file = pathAndInfo.path;const assetInfo = pathAndInfo.info;// check if the same filename was already written by another chunkconst alreadyWritten = alreadyWrittenFiles.get(file);if (alreadyWritten !== undefined) {if (alreadyWritten.hash === usedHash) {if (this.cache) {this.cache[cacheName] = {hash: usedHash,source: alreadyWritten.source};}chunk.files.push(file);this.hooks.chunkAsset.call(chunk, file);continue;} else {thrownewError(`Conflict: Multiple chunks emit assets to the same filename ${file}` +` (chunks ${alreadyWritten.chunk.id} and ${chunk.id})`);}}if (this.cache &&this.cache[cacheName] &&this.cache[cacheName].hash === usedHash) {// 3 chunk 打包封装的入口source = this.cache[cacheName].source;} else {source = fileManifest.render();// Ensure that source is a cached source to avoid additional cost because of repeated accessif (!(source instanceof CachedSource)) {// 4 存放cacheEntry源码const cacheEntry = cachedSourceMap.get(source);if (cacheEntry) {source = cacheEntry;} else {const cachedSource = new CachedSource(source);cachedSourceMap.set(source, cachedSource);source = cachedSource;}}if (this.cache) {this.cache[cacheName] = {hash: usedHash,source};}}this.emitAsset(file, source, assetInfo);chunk.files.push(file);this.hooks.chunkAsset.call(chunk, file);alreadyWrittenFiles.set(file, {hash: usedHash,source,chunk});}} catch (err) {this.errors.push(new ChunkRenderError(chunk, file || filenameTemplate, err));}}}

下面我们用流程图表示:

从上图可以看出不同的chunk处理模版不一样,根据chunk的entry判断是选择mainTemplate(入口文件打包模版)还是chunkTemplate(异步加载js打包模版);选择模版后根据模版的template.getRenderManifest生成manifest对象,该对象中的render方法就是chunk打包封装的入口;mainTemplate和chunkTemplate的唯一区别就是mainTemplate多了wepback执行的bootsrap代码。当调用render时会调用template.renderChunkModules方法,该方法会创建一个ConcatSource容器用来存放chunk的源码,该方法接下来会对当前chunk的module遍历并执行moduleTemplate.render获得每一个module的源码;在moduleTemplate.render中获取源码后会触发插件去封装成wepack需要的代码格式;当所有的module都生成完后放入ConcatSource中返回;并以该chunk的输出文件名称为key存放在Compilation的assets中。

Step12: this.emitAssets

webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项异步将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。

// ~/webpack/lib/Compiler.jsthis.emitAssets(compilation, err => {if (err) return finalCallback(err);if (compilation.hooks.needAdditionalPass.call()) {compilation.needAdditionalPass = true;const stats = new Stats(compilation);stats.startTime = startTime;stats.endTime = Date.now();//继续异步调用时间流this.hooks.done.callAsync(stats, err => {if (err) return finalCallback(err);//  这次多了一个时间流,调用额外编译,告知编译终于编完了this.hooks.additionalPass.callAsync(err => {if (err) return finalCallback(err);this.compile(onCompiled);});});return;}

Step13: compiler.hooks.emit.callAsync()

最后我们到达了compiler.emitAssets方法体中。在compiler.emitAssets中会先调用this.hooks.emit生命周期,之后根据webpack config文件的output配置的path属性,将文件输出到指定的文件夹。至此,你就可以在./debug/dist中查看到调试代码打包后的文件了。

// ~/webpack/lib/Compiler.js
this.hooks.emit.callAsync(compilation, () => {outputPath = compilation.getPath(this.outputPath, {})mkdirp(this.outputFileSystem, outputPath, emitFiles)})
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. ANR 定位和修正( Application Not Responding)

    ANR 定位和修正( Application Not Responding)1、ANR排错一般有三种情况KeyDispatchTimeout(5 seconds) –主要类型按键或触摸事件在特定时间内无响应BroadcastTimeout(10 secends) –BroadcastReceiver在特定时间内无法处理完成ServiceTimeout(20 secends) –小概率事件 Ser…...

    2024/4/15 3:41:54
  2. CSND发布文章时提示:标题含有非法字符,请修改

    CSND发布文章时提示:标题含有非法字符 问题描述 在CSDN中编辑好文章,发布时提示“标题中含有非法字符”,看下图: 这个是我的标题这个是下方的提示解决方法 当标题中的存在英文引号时会出现提示,经过测试,英文的双引号和单引号都会出现这种提示。存在英文逗号和英文空格不会…...

    2024/4/19 12:05:52
  3. Spring Cloud Config 生产测试环境配置实践

    Spring Cloud Config 生产测试环境配置实践前言Spring Cloud Config开发准备搭建目标Git仓库Spring Cloud Config Serverpom.xmlConfigServer.javaapplication.yml注意要点Spring Cloud Config Clientpom.xmlbootstrap.yml注意要点远程配置资源定位规则配置隔离规则 前言 本篇主…...

    2024/5/6 9:45:43
  4. 04、数据操作(DML)

    04、数据操作(DML) 增删改查有一个术语:CRUD操作 Create(增) Retrieve(检索) Update(修改) Delete(删除) 1、插入:insert insert 语句插入数据 语法格式: ​ insert into 表名 (字段名1,字段名2,字段名3,……) values (值1,值2,值3,……); 要求:字段的数…...

    2024/5/6 9:23:44
  5. python爬虫——爬取豆瓣top250电影信息

    python爬虫——爬取豆瓣top250电影信息 获取豆瓣top250电影信息,包括电影海报链接、详情链接、中/外文名、评分、评价人数、一句话评价等 环境系统 :macOS Catalina -V 10.15.4 IDE :Vscode -V 1.46.0 语言 :Python -V 3.7.7 库 :urllib、bs4、re、ssl、xlwt 非自带包都是…...

    2024/5/3 1:48:45
  6. 集成FireBase小米手机闪退问题

    记录一下,小米手机集成FireBase统计闪退Firebase 版本16.0.4 升级到16.0.7 搞定...

    2024/4/15 3:41:48
  7. 【问题记录】Unity莫名其妙的几百个报错,原来是许可证过期了

    报错:Assertion failed on expression: m_ErrorCode == MDB_MAP_RESIZED || !HasAbortingErrors()Asset database transaction committed twice!Assertion failed on expression: errors == MDB_SUCCESS || errors == MDB_NOTFOUND原因:Unity许可证过期解决:重新激活许可证即…...

    2024/5/6 7:00:47
  8. C++的set和map的使用 - (leetcode两个数组的交集)

    set和map都是一种关联式容器,它们通常都是语言标准库内置的,其有以下操作: 插入 s.insert()查找 if(s.find(nums2[i]) != s.end()) //找到的情况删除 s.erase(10) //删除10号元素修改 change (map专有) set有以下特性所有元素只有key没有value 不允许出现重复值 所…...

    2024/4/15 3:41:46
  9. BN层理解

    bn层计算的均值和方差是channel的输入数据是nchw,求得的均值和方差均是长度为c的向量mini-batch指的是一个batch的所有样本对应通道组合成一个minibatch,1个nchw的数据有c个mini-batch一个mini-batch在一起进行求均值和方差HW的归一化,求出NC个均值与方差,然后N个均值与方差…...

    2024/5/6 9:15:23
  10. 十九、 彻底掌握金融量化交易库Talib

    @Author : By Runsen @Date : 2020/6/16 作者介绍:Runsen目前大三下学期,专业化学工程与工艺,大学沉迷日语,Python, Java和一系列数据分析软件。导致翘课严重,专业排名中下。.在大学60%的时间,都在CSDN。 在之前都是去年的笔记,在今天我写了一份金融数据和量化交易文…...

    2024/4/25 23:13:55
  11. 05、约束(Constraint)

    05、约束(constraint) 1、什么是约束? ​ 在创建表的时候,可以给表的字段添加相应的约束,添加约束的目的是为了保证表中数据的合法性、有效性、完整性。 ​ 常见的约束:表达式 作用not null 非空约束,不能为NULLunique 唯一约束,不能重复,但可以为NULLprimary key 主键…...

    2024/5/5 9:55:28
  12. xmind格式测试用例导入禅道

    官方使用说明:https://github.com/zhuifengshen/xmind2testcase 使用前需要准备的条件。电脑安装git; 电脑安装Python3; 电脑安装pip; 从https://github.com/zhuifengshen/xmind2testcase git仓库clone到本地; 安装xmind2testcase:pip3 install xmind2testcase。(版本升级…...

    2024/5/6 2:59:47
  13. 聊聊 iframe 的优缺点以及使用场景

    一,使用 iframe 的优缺点1,优点实现了页面的“模块化”。比如一个网站的多个页面有统一的导航栏,这样就可以写成一个页面,用 iframe 来嵌套,增加代码的重用性。2,缺点滥用 iframe 会增加页面的性能开销二,为什么的 iframe 会影响页面性能?1,iframe 阻塞页面加载,影响…...

    2024/4/15 3:36:46
  14. 2020.6.15刷题 求1到N的全排列组合,最佳观光组合1趟遍历,把数字翻译成字符串dp,转变数组后最接近目标值的数组和(排序二分查找),等式方程的可满足性构建并查集,

    1.求1到N的全排列组合#include "stdio.h" #define N 3 int x[N]; int count = 0;void dump() {int i = 0;for (i = 0; i < N; i++) {printf("%d", x[i]);}printf("\n"); }void swap(int a, int b) {int t = x[a];x[a] = x[b];x[b] = t; }void…...

    2024/4/18 13:37:59
  15. 前端模拟数据---mockjs

    源于https://gitee.com/vsdeveloper/heima_76使用步骤:基于vue下使用mockjs一:安装依赖安装mockjs: npm install mockjs安装axios :npm install axios二、建立有关文件夹mock(index.js books.js exend.js) components (mock.vue)新建mock目录与main.js同级 ->index.j…...

    2024/5/6 5:17:38
  16. Twain之旅

    基础理论:https://blog.csdn.net/mysouling/article/details/8624882...

    2024/4/15 3:41:42
  17. TensorFlow报错:

    TensorFlow报错:AttributeError: module ‘tensorflow_core._api.v2.train’ has no attribute Optimizer‘ 原因: TensorFlow版本更新到2.0,Optimizer方法调用方式改变 。 将tf.train.Optimizer,更改为tf.optimizers.Optimizer即可。...

    2024/4/15 3:41:41
  18. java中的mysql优化

    Mysql优化 a.设计合理Schema(表结构)通常来说把可为NULL的列改为NOT NULL不会对性能提升有多少帮助,只是如果计划在列上创建索引,就应该将该列设置为NOT NULL。 对整数类型指定宽度,比如INT(11),没有任何卵用。INT使用32位(4个字节)存储空间,那么它的表示范围已经确定…...

    2024/4/15 3:41:40
  19. 服务器硬件及RAID配置实战

    文章目录一、磁盘阵列介绍RAID 0RAID 1RAID 5RAID 6RAID 1+0二、阵列卡介绍与缓存阵列卡介绍阵列卡缓存 一、磁盘阵列介绍 磁盘阵列(Redundant Arrays of Independent Disks,RAID),有“独立磁盘构成的具有冗余能力的阵列”之意。 磁盘阵列是由很多块独立的磁盘,组合成一个…...

    2024/4/15 3:41:39
  20. Web网站渗透

    1.无论bai什么站,无论什么语言,我要渗透,第du一件事就是扫目录,最好一下扫出zhi个上传点,直接上传shell,诸dao位不要笑,有时候你花很久搞一个站,最后发现有个现成的上传点,而且很容易猜到,不过这种情况发生在asp居多! 2.asp(aspx)+MSSQL先考虑注入,一般的注入都有…...

    2024/5/6 6:36:08

最新文章

  1. 百度文库最新AI旋转验证码

    上个月发现百度文库最新出了一个验证码&#xff0c;是AI生成的。内容每次可能都不一样&#xff0c;所以给识别造成 了很大困难。传统的比对放松完全失效。 一、介绍 这个是最近才出的最新验证码&#xff0c;内容主要以工厂、建筑、山峰、机器人、汽车、盆栽植物等为主。如下图…...

    2024/5/6 10:37:21
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/5/6 9:38:23
  3. 基于springboot实现影城管理系统项目【项目源码+论文说明】

    基于springboot实现影城管理系统演示 摘要 随着现在网络的快速发展&#xff0c;网上管理系统也逐渐快速发展起来&#xff0c;网上管理模式很快融入到了许多生活之中&#xff0c;随之就产生了“小徐影城管理系统”&#xff0c;这样就让小徐影城管理系统更加方便简单。 对于本小…...

    2024/5/5 8:52:30
  4. 大数据学习十三天(hadhoop基础2)

    一: MapReduce概述(了解) MapReduce是hadoop三大组件之一,是分布式计算组件 Map阶段 : 将数据拆分到不同的服务器后执行Maptask任务,得到一个中间结果 Reduce阶段 : 将Maptask执行的结果进行汇总,按照Reducetask的计算 规则获得一个唯一的结果 我们在MapReduce计算框架的使用过…...

    2024/5/2 21:17:01
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/5/4 23:54:56
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/5/4 23:54:56
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/5/4 23:54:56
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/5/6 9:21:00
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/5/4 23:54:56
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/5/4 23:55:05
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/5/4 23:54:56
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/5/4 23:55:16
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/5/4 23:54:56
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/5/6 1:40:42
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/5/4 23:54:56
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/5/4 23:55:17
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/5/4 23:55:06
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/5/4 23:54:56
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/5/4 23:55:06
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/5/5 8:13:33
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/5/4 23:55:16
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/5/4 23:54:58
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/5/4 23:55:01
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/5/4 23:54:56
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57