摘要:但也是最復雜的一個。中當一旦某個返回值結果不為便結束執行列表中的插件中上一個插件執行結果當作下一個插件的入參調用并行執行插件流程篇本文關于的流程講解是基于的。
webpack是現代前端開發中最火的模塊打包工具,只需要通過簡單的配置,便可以完成模塊的加載和打包。那它是怎么做到通過對一些插件的配置,便可以輕松實現對代碼的構建呢?
webpack的配置const path = require("path"); module.exports = { entry: "./app/entry", // string | object | array // Webpack打包的入口 output: { // 定義webpack如何輸出的選項 path: path.resolve(__dirname, "dist"), // string // 所有輸出文件的目標路徑 filename: "[chunkhash].js", // string // 「入口(entry chunk)」文件命名模版 publicPath: "/assets/", // string // 構建文件的輸出目錄 /* 其它高級配置 */ }, module: { // 模塊相關配置 rules: [ // 配置模塊loaders,解析規則 { test: /.jsx?$/, // RegExp | string include: [ // 和test一樣,必須匹配選項 path.resolve(__dirname, "app") ], exclude: [ // 必不匹配選項(優先級高于test和include) path.resolve(__dirname, "app/demo-files") ], loader: "babel-loader", // 模塊上下文解析 options: { // loader的可選項 presets: ["es2015"] }, }, }, resolve: { // 解析模塊的可選項 modules: [ // 模塊的查找目錄 "node_modules", path.resolve(__dirname, "app") ], extensions: [".js", ".json", ".jsx", ".css"], // 用到的文件的擴展 alias: { // 模塊別名列表 "module": "new-module" }, }, devtool: "source-map", // enum // 為瀏覽器開發者工具添加元數據增強調試 plugins: [ // 附加插件列表 // ... ], }
從上面我們可以看到,webpack配置中需要理解幾個核心的概念Entry 、Output、Loaders 、Plugins、 Chunk
Entry:指定webpack開始構建的入口模塊,從該模塊開始構建并計算出直接或間接依賴的模塊或者庫
Output:告訴webpack如何命名輸出的文件以及輸出的目錄
Loaders:由于webpack只能處理javascript,所以我們需要對一些非js文件處理成webpack能夠處理的模塊,比如sass文件
Plugins:Loaders將各類型的文件處理成webpack能夠處理的模塊,plugins有著很強的能力。插件的范圍包括,從打包優化和壓縮,一直到重新定義環境中的變量。但也是最復雜的一個。比如對js文件進行壓縮優化的UglifyJsPlugin插件
Chunk:coding split的產物,我們可以對一些代碼打包成一個多帶帶的chunk,比如某些公共模塊,去重,更好的利用緩存。或者按需加載某些功能模塊,優化加載時間。在webpack3及以前我們都利用CommonsChunkPlugin將一些公共代碼分割成一個chunk,實現多帶帶加載。在webpack4 中CommonsChunkPlugin被廢棄,使用SplitChunksPlugin
webpack詳解讀到這里,或許你對webpack有一個大概的了解,那webpack 是怎么運行的呢?我們都知道,webpack是高度復雜抽象的插件集合,理解webpack的運行機制,對于我們日常定位構建錯誤以及寫一些插件處理構建任務有很大的幫助。
不得不說的tapablewebpack本質上是一種事件流的機制,它的工作流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable,webpack中最核心的負責編譯的Compiler和負責創建bundles的Compilation都是Tapable的實例。在Tapable1.0之前,也就是webpack3及其以前使用的Tapable,提供了包括
plugin(name:string, handler:function)注冊插件到Tapable對象中
apply(…pluginInstances: (AnyPlugin|function)[])調用插件的定義,將事件監聽器注冊到Tapable實例注冊表中
applyPlugins*(name:string, …)多種策略細致地控制事件的觸發,包括applyPluginsAsync、applyPluginsParallel等方法實現對事件觸發的控制,實現
(1)多個事件連續順序執行
(2)并行執行
(3)異步執行
(4)一個接一個地執行插件,前面的輸出是后一個插件的輸入的瀑布流執行順序
(5)在允許時停止執行插件,即某個插件返回了一個undefined的值,即退出執行
我們可以看到,Tapable就像nodejs中EventEmitter,提供對事件的注冊on和觸發emit,理解它很重要,看個栗子:比如我們來寫一個插件
function CustomPlugin() {} CustomPlugin.prototype.apply = function(compiler) { compiler.plugin("emit", pluginFunction); }
在webpack的生命周期中會適時的執行
this.apply*("emit",options)
當然上面提到的Tapable都是1.0版本之前的,如果想深入學習,可以查看Tapable 和 事件流
那1.0的Tapable又是什么樣的呢?1.0版本發生了巨大的改變,不再是此前的通過plugin注冊事件,通過applyPlugins*觸發事件調用,那1.0的Tapable是什么呢?
暴露出很多的鉤子,可以使用它們為插件創建鉤子函數
const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable");
我們來看看 怎么使用。
class Order { constructor() { this.hooks = { //hooks goods: new SyncHook(["goodsId", "number"]), consumer: new AsyncParallelHook(["userId", "orderId"]) } } queryGoods(goodsId, number) { this.hooks.goods.call(goodsId, number); } consumerInfoPromise(userId, orderId) { this.hooks.consumer.promise(userId, orderId).then(() => { //TODO }) } consumerInfoAsync(userId, orderId) { this.hooks.consumer.callAsync(userId, orderId, (err, data) => { //TODO }) } }
對于所有的hook的構造函數均接受一個可選的string類型的數組
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
// 調用tap方法注冊一個consument order.hooks.goods.tap("QueryPlugin", (goodsId, number) => { return fetchGoods(goodsId, number); }) // 再添加一個 order.hooks.goods.tap("LoggerPlugin", (goodsId, number) => { logger(goodsId, number); }) // 調用 order.queryGoods("10000000", 1)
對于一個 SyncHook,我們通過tap來添加消費者,通過call來觸發鉤子的順序執行。
對于一個非sync*類型的鉤子,即async*類型的鉤子,我們還可以通過其它方式注冊消費者和調用
// 注冊一個sync 鉤子 order.hooks.consumer.tap("LoggerPlugin", (userId, orderId) => { logger(userId, orderId); }) order.hooks.consumer.tapAsync("LoginCheckPlugin", (userId, orderId, callback) => { LoginCheck(userId, callback); }) order.hooks.consumer.tapPromise("PayPlugin", (userId, orderId) => { return Promise.resolve(); }) // 調用 // 返回Promise order.consumerInfoPromise("user007", "1024"); //回調函數 order.consumerInfoAsync("user007", "1024")
通過上面的栗子,你可能已經大致了解了Tapable的用法,它的用法
插件注冊數量
插件注冊的類型(sync, async, promise)
調用的方式(sync, async, promise)
實例鉤子的時候參數數量
是否使用了interception
Tapable詳解
對于Sync*類型的鉤子來說。
注冊在該鉤子下面的插件的執行順序都是順序執行。
只能使用tap注冊,不能使用tapPromise和tapAsync注冊
// 所有的鉤子都繼承于Hook class Sync* extends Hook { tapAsync() { // Sync*類型的鉤子不支持tapAsync throw new Error("tapAsync is not supported on a Sync*"); } tapPromise() {// Sync*類型的鉤子不支持tapPromise throw new Error("tapPromise is not supported on a Sync*"); } compile(options) { // 編譯代碼來按照一定的策略執行Plugin factory.setup(this, options); return factory.create(options); } }
對于Async*類型鉤子
支持tap、tapPromise、tapAsync注冊
class AsyncParallelHook extends Hook { constructor(args) { super(args); this.call = this._call = undefined; } compile(options) { factory.setup(this, options); return factory.create(options); } }
class Hook { constructor(args) { if(!Array.isArray(args)) args = []; this._args = args; // 實例鉤子的時候的string類型的數組 this.taps = []; // 消費者 this.interceptors = []; // interceptors this.call = this._call = // 以sync類型方式來調用鉤子 this._createCompileDelegate("call", "sync"); this.promise = this._promise = // 以promise方式 this._createCompileDelegate("promise", "promise"); this.callAsync = this._callAsync = // 以async類型方式來調用 this._createCompileDelegate("callAsync", "async"); this._x = undefined; // } _createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type }); } _createCompileDelegate(name, type) { const lazyCompileHook = (...args) => { this[name] = this._createCall(type); return this[name](...args); }; return lazyCompileHook; } // 調用tap 類型注冊 tap(options, fn) { // ... options = Object.assign({ type: "sync", fn: fn }, options); // ... this._insert(options); // 添加到 this.taps中 } // 注冊 async類型的鉤子 tapAsync(options, fn) { // ... options = Object.assign({ type: "async", fn: fn }, options); // ... this._insert(options); // 添加到 this.taps中 } 注冊 promise類型鉤子 tapPromise(options, fn) { // ... options = Object.assign({ type: "promise", fn: fn }, options); // ... this._insert(options); // 添加到 this.taps中 } }
每次都是調用tap、tapSync、tapPromise注冊不同類型的插件鉤子,通過調用call、callAsync 、promise方式調用。其實調用的時候為了按照一定的執行策略執行,調用compile方法快速編譯出一個方法來執行這些插件。
const factory = new Sync*CodeFactory(); class Sync* extends Hook { // ... compile(options) { // 編譯代碼來按照一定的策略執行Plugin factory.setup(this, options); return factory.create(options); } } class Sync*CodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } }
compile中調用HookCodeFactory#create方法編譯生成執行代碼。
class HookCodeFactory { constructor(config) { this.config = config; this.options = undefined; } create(options) { this.init(options); switch(this.options.type) { case "sync": // 編譯生成sync, 結果直接返回 return new Function(this.args(), ""use strict"; " + this.header() + this.content({ // ... onResult: result => `return ${result}; `, // ... })); case "async": // async類型, 異步執行,最后將調用插件執行結果來調用callback, return new Function(this.args({ after: "_callback" }), ""use strict"; " + this.header() + this.content({ // ... onResult: result => `_callback(null, ${result}); `, onDone: () => "_callback(); " })); case "promise": // 返回promise類型,將結果放在resolve中 // ... code += "return new Promise((_resolve, _reject) => { "; code += "var _sync = true; "; code += this.header(); code += this.content({ // ... onResult: result => `_resolve(${result}); `, onDone: () => "_resolve(); " }); // ... return new Function(this.args(), code); } } // callTap 就是執行一些插件,并將結果返回 callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { let code = ""; let hasTapCached = false; // ... code += `var _fn${tapIndex} = ${this.getTapFn(tapIndex)}; `; const tap = this.options.taps[tapIndex]; switch(tap.type) { case "sync": // ... if(onResult) { code += `var _result${tapIndex} = _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })}); `; } else { code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })}); `; } if(onResult) { // 結果透傳 code += onResult(`_result${tapIndex}`); } if(onDone) { // 通知插件執行完畢,可以執行下一個插件 code += onDone(); } break; case "async": //異步執行,插件運行完后再將結果通過執行callback透傳 let cbCode = ""; if(onResult) cbCode += `(_err${tapIndex}, _result${tapIndex}) => { `; else cbCode += `_err${tapIndex} => { `; cbCode += `if(_err${tapIndex}) { `; cbCode += onError(`_err${tapIndex}`); cbCode += "} else { "; if(onResult) { cbCode += onResult(`_result${tapIndex}`); } cbCode += "} "; cbCode += "}"; code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined, after: cbCode //cbCode將結果透傳 })}); `; break; case "promise": // _fn${tapIndex} 就是第tapIndex 個插件,它必須是個Promise類型的插件 code += `var _hasResult${tapIndex} = false; `; code += `_fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })}).then(_result${tapIndex} => { `; code += `_hasResult${tapIndex} = true; `; if(onResult) { code += onResult(`_result${tapIndex}`); } // ... break; } return code; } // 按照插件的注冊順序,按照順序遞歸調用執行插件 callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) { // ... const firstAsync = this.options.taps.findIndex(t => t.type !== "sync"); const next = i => { // ... const done = () => next(i + 1); // ... return this.callTap(i, { // ... onResult: onResult && ((result) => { return onResult(i, result, done, doneBreak); }), // ... }); }; return next(0); } callTapsLooping({ onError, onDone, rethrowIfPossible }) { const syncOnly = this.options.taps.every(t => t.type === "sync"); let code = ""; if(!syncOnly) { code += "var _looper = () => { "; code += "var _loopAsync = false; "; } code += "var _loop; "; code += "do { "; code += "_loop = false; "; // ... code += this.callTapsSeries({ // ... onResult: (i, result, next, doneBreak) => { // 一旦某個插件返回不為undefined, 即一只調用某個插件執行,如果為undefined,開始調用下一個 let code = ""; code += `if(${result} !== undefined) { `; code += "_loop = true; "; if(!syncOnly) code += "if(_loopAsync) _looper(); "; code += doneBreak(true); code += `} else { `; code += next(); code += `} `; return code; }, // ... }) code += "} while(_loop); "; // ... return code; } // 并行調用插件執行 callTapsParallel({ onError, onResult, onDone, rethrowIfPossible, onTap = (i, run) => run() }) { // ... // 遍歷注冊都所有插件,并調用 for(let i = 0; i < this.options.taps.length; i++) { // ... code += "if(_counter <= 0) break; "; code += onTap(i, () => this.callTap(i, { // ... onResult: onResult && ((result) => { let code = ""; code += "if(_counter > 0) { "; code += onResult(i, result, done, doneBreak); code += "} "; return code; }), // ... }), done, doneBreak); } // ... return code; } }
在HookCodeFactory#create中調用到content方法,此方法將按照此鉤子的執行策略,調用不同的方法來執行編譯 生成最終的代碼。
SyncHook中調用callTapsSeries編譯生成最終執行插件的函數,callTapsSeries做的就是將插件列表中插件按照注冊順序遍歷執行。
class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); } }
SyncBailHook中當一旦某個返回值結果不為undefined便結束執行列表中的插件
class SyncBailHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ // ... onResult: (i, result, next) => `if(${result} !== undefined) { ${onResult(result)}; } else { ${next()}} `, // ... }); } }
SyncWaterfallHook中上一個插件執行結果當作下一個插件的入參
class SyncWaterfallHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ // ... onResult: (i, result, next) => { let code = ""; code += `if(${result} !== undefined) { `; code += `${this._args[0]} = ${result}; `; code += `} `; code += next(); return code; }, onDone: () => onResult(this._args[0]), }); } }
AsyncParallelHook調用callTapsParallel并行執行插件
class AsyncParallelHookCodeFactory extends HookCodeFactory { content({ onError, onDone }) { return this.callTapsParallel({ onError: (i, err, done, doneBreak) => onError(err) + doneBreak(true), onDone }); } }webpack流程篇
本文關于webpack 的流程講解是基于webpack4的。
webpack 入口文件從webpack項目的package.json文件中我們找到了入口執行函數,在函數中引入webpack,那么入口將會是lib/webpack.js,而如果在shell中執行,那么將會走到./bin/webpack.js,我們就以lib/webpack.js為入口開始吧!
{ "name": "webpack", "version": "4.1.1", ... "main": "lib/webpack.js", "web": "lib/webpack.web.js", "bin": "./bin/webpack.js", ... }webpack入口
const webpack = (options, callback) => { // ... // 驗證options正確性 // 預處理options options = new WebpackOptionsDefaulter().process(options); // webpack4的默認配置 compiler = new Compiler(options.context); // 實例Compiler // ... // 若options.watch === true && callback 則開啟watch線程 compiler.watch(watchOptions, callback); compiler.run(callback); return compiler; };
webpack 的入口文件其實就實例了Compiler并調用了run方法開啟了編譯,webpack的編譯都按照下面的鉤子調用順序執行。
before-run 清除緩存
run 注冊緩存數據鉤子
before-compile
compile 開始編譯
make 從入口分析依賴以及間接依賴模塊,創建模塊對象
build-module 模塊構建
seal 構建結果封裝, 不可再更改
after-compile 完成構建,緩存數據
emit 輸出到dist目錄
編譯&構建流程webpack中負責構建和編譯都是Compilation
class Compilation extends Tapable { constructor(compiler) { super(); this.hooks = { // hooks }; // ... this.compiler = compiler; // ... // template this.mainTemplate = new MainTemplate(this.outputOptions); this.chunkTemplate = new ChunkTemplate(this.outputOptions); this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate( this.outputOptions ); this.runtimeTemplate = new RuntimeTemplate( this.outputOptions, this.requestShortener ); this.moduleTemplates = { javascript: new ModuleTemplate(this.runtimeTemplate), webassembly: new ModuleTemplate(this.runtimeTemplate) }; // 構建生成的資源 this.chunks = []; this.chunkGroups = []; this.modules = []; this.additionalChunkAssets = []; this.assets = {}; this.children = []; // ... } // buildModule(module, optional, origin, dependencies, thisCallback) { // ... // 調用module.build方法進行編譯代碼,build中 其實是利用acorn編譯生成AST this.hooks.buildModule.call(module); module.build(/**param*/); } // 將模塊添加到列表中,并編譯模塊 _addModuleChain(context, dependency, onModule, callback) { // ... // moduleFactory.create創建模塊,這里會先利用loader處理文件,然后生成模塊對象 moduleFactory.create( { contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { const addModuleResult = this.addModule(module); module = addModuleResult.module; onModule(module); dependency.module = module; // ... // 調用buildModule編譯模塊 this.buildModule(module, false, null, null, err => {}); } }); } // 添加入口模塊,開始編譯&構建 addEntry(context, entry, name, callback) { // ... this._addModuleChain( // 調用_addModuleChain添加模塊 context, entry, module => { this.entries.push(module); }, // ... ); } seal(callback) { this.hooks.seal.call(); // ... const chunk = this.addChunk(name); const entrypoint = new Entrypoint(name); entrypoint.setRuntimeChunk(chunk); entrypoint.addOrigin(null, name, preparedEntrypoint.request); this.namedChunkGroups.set(name, entrypoint); this.entrypoints.set(name, entrypoint); this.chunkGroups.push(entrypoint); GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); GraphHelpers.connectChunkAndModule(chunk, module); chunk.entryModule = module; chunk.name = name; // ... this.hooks.beforeHash.call(); this.createHash(); this.hooks.afterHash.call(); this.hooks.beforeModuleAssets.call(); this.createModuleAssets(); if (this.hooks.shouldGenerateChunkAssets.call() !== false) { this.hooks.beforeChunkAssets.call(); this.createChunkAssets(); } // ... } createHash() { // ... } // 生成 assets 資源并 保存到 Compilation.assets 中 給webpack寫插件的時候會用到 createModuleAssets() { for (let i = 0; i < this.modules.length; i++) { const module = this.modules[i]; if (module.buildInfo.assets) { for (const assetName of Object.keys(module.buildInfo.assets)) { const fileName = this.getPath(assetName); this.assets[fileName] = module.buildInfo.assets[assetName]; this.hooks.moduleAsset.call(module, fileName); } } } } createChunkAssets() { // ... } }
在webpack make鉤子中, tapAsync注冊了一個DllEntryPlugin, 就是將入口模塊通過調用compilation.addEntry方法將所有的入口模塊添加到編譯構建隊列中,開啟編譯流程。
compiler.hooks.make.tapAsync("DllEntryPlugin", (compilation, callback) => { compilation.addEntry( this.context, new DllEntryDependency( this.entries.map((e, idx) => { const dep = new SingleEntryDependency(e); dep.loc = `${this.name}:${idx}`; return dep; }), this.name ), // ... ); });
隨后在addEntry 中調用_addModuleChain開始編譯。在_addModuleChain首先會生成模塊,最后構建。
class NormalModuleFactory extends Tapable { // ... create(data, callback) { // ... this.hooks.beforeResolve.callAsync( { contextInfo, resolveOptions, context, request, dependencies }, (err, result) => { if (err) return callback(err); // Ignored if (!result) return callback(); // factory 鉤子會觸發 resolver 鉤子執行,而resolver鉤子中會利用acorn 處理js生成AST,再利用acorn處理前,會使用loader加載文件 const factory = this.hooks.factory.call(null); factory(result, (err, module) => { if (err) return callback(err); if (module && this.cachePredicate(module)) { for (const d of dependencies) { d.__NormalModuleFactoryCache = module; } } callback(null, module); }); } ); } }
在編譯完成后,調用compilation.seal方法封閉,生成資源,這些資源保存在compilation.assets, compilation.chunk, 在給webpack寫插件的時候會用到
class Compiler extends Tapable { constructor(context) { super(); this.hooks = { beforeRun: new AsyncSeriesHook(["compilation"]), run: new AsyncSeriesHook(["compilation"]), emit: new AsyncSeriesHook(["compilation"]), afterEmit: new AsyncSeriesHook(["compilation"]), compilation: new SyncHook(["compilation", "params"]), beforeCompile: new AsyncSeriesHook(["params"]), compile: new SyncHook(["params"]), make: new AsyncParallelHook(["compilation"]), afterCompile: new AsyncSeriesHook(["compilation"]), // other hooks }; // ... } run(callback) { const startTime = Date.now(); const onCompiled = (err, compilation) => { // ... this.emitAssets(compilation, err => { if (err) return callback(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 callback(err); this.hooks.additionalPass.callAsync(err => { if (err) return callback(err); this.compile(onCompiled); }); }); return; } // ... }); }; this.hooks.beforeRun.callAsync(this, err => { if (err) return callback(err); this.hooks.run.callAsync(this, err => { if (err) return callback(err); this.readRecords(err => { if (err) return callback(err); this.compile(onCompiled); }); }); }); } // 輸出文件到構建目錄 emitAssets(compilation, callback) { // ... this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); }); } newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params; } compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); const compilation = this.newCompilation(params); this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); compilation.finish(); // make 鉤子執行后,調用seal生成資源 compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); // emit, 生成最終文件 return callback(null, compilation); }); }); }); }); } }最后輸出
在seal執行后,便會調用emit鉤子,根據webpack config文件的output配置的path屬性,將文件輸出到指定的path.
最后騰訊IVWEB團隊的工程化解決方案feflow已經開源:Github主頁:https://github.com/feflow/feflow
如果對您的團隊或者項目有幫助,請給個Star支持一下哈~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93357.html
摘要:接著上文,重新在文件夾下面新建一個項目文件夾,然后用初始化項目的配置文件,然后安裝,然后創建基本的項目文件夾結構,好了,我們的又一個基本項目結構就搭建好了第一開始通過文件配置我們的項目首先在項目文件夾下面,新建一個文件,這個文件可 接著上文,重新在webpack文件夾下面新建一個項目文件夾demo2,然后用npm init --yes初始化項目的package.json配置文件,然后安...
摘要:本文繼續接著上文,繼續寫下的其他配置用法一把兩個文件打包成一個,怎么配置在上文中的中,用數組配置文件代碼,就是當前文件所在的絕對路徑輸出路徑,要用絕對路徑打包之后輸出的文件名然后在目錄下面新建一個文件,代碼如下之前的文件的代碼告訴你怎么學習 本文繼續接著上文,繼續寫下webpack.config.js的其他配置用法. 一、把兩個文件打包成一個,entry怎么配置? 在上文中的webpa...
摘要:安裝要開始使用在項目中進行開發前我們首先需要在全局環境中進行安裝。使用它可以將的語法轉換為的語法,以便在現在有的環境執行。,是一段正則,表示進行匹配的資源類型。為指定應該被忽略的文件,我們在這兒指定了。 1.認識Webpack 構建應用前我們先來了解一下Webpack, Webpack是一個模塊打包工具,能夠把各種文件(例如:ReactJS、Babel、Coffeescript、Les...
摘要:配置一些開發環境的提示工具例如當項目中報錯可以準確的定位到哪個文件報錯對比項構建速度重新構建速度代碼提示定位原始源代碼生成后的代碼可根據場景使用這兩個值調試代碼項目中配置引入文件的快捷路徑中導出的對象內配置快捷方式名對應的路 devtool: 配置一些開發環境的提示工具 例如: devtool: cheap-module-eval-source-map 當項目中報錯可以準確的定位到哪個...
摘要:由于這種差異我們將對預處理器文件的配置封裝為函數,由參數生成對應配置,將文件放入文件內,將配置放在文件內。 前言 源代碼 熟悉 webpack 與 webpack4 配置。 webpack4 相對于 3 的最主要的區別是所謂的零配置,但是為了滿足我們的項目需求還是要自己進行配置,不過我們可以使用一些 webpack 的預設值。同時 webpack 也拆成了兩部分,webpack 和 w...
閱讀 487·2019-08-30 15:44
閱讀 902·2019-08-30 10:55
閱讀 2735·2019-08-29 15:16
閱讀 934·2019-08-29 13:17
閱讀 2807·2019-08-26 13:27
閱讀 576·2019-08-26 11:53
閱讀 2125·2019-08-23 18:31
閱讀 1892·2019-08-23 18:23