摘要:相關(guān)的內(nèi)容為這樣對(duì)于一個(gè)處理的第二階段也就結(jié)束了,通過去攔截不同類型的,并返回新的,跳過后面的的執(zhí)行,同時(shí)在內(nèi)部會(huì)剔除掉,這樣在進(jìn)入到下一個(gè)處理階段的時(shí)候,不在使用的范圍之內(nèi),因此下一階段便不會(huì)經(jīng)由來處理。
文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~
Webpack 系列文章:
Webpack Loader 高手進(jìn)階(一)
Webpack Loader 高手進(jìn)階(二)
Webpack Loader 高手進(jìn)階(三)
前2篇文章主要通過源碼分析了 loader 的配置,匹配和加載,執(zhí)行等內(nèi)容,這篇文章會(huì)通過具體的實(shí)例來學(xué)習(xí)下如何去實(shí)現(xiàn)一個(gè) loader。
這里我們來看下 vue-loader(v15) 內(nèi)部的相關(guān)內(nèi)容,這里會(huì)講解下有關(guān) vue-loader 的大致處理流程,不會(huì)深入特別細(xì)節(jié)的地方。
git clone [email protected]:vuejs/vue-loader.git
我們使用 vue-loader 官方倉(cāng)庫(kù)當(dāng)中的 example 目錄的內(nèi)容作為整篇文章的示例。
首先我們都知道 vue-loader 配合 webpack 給我們開發(fā) Vue 應(yīng)用提供了非常大的便利性,允許我們?cè)?SFC(single file component) 中去寫我們的 template/script/style,同時(shí) v15 版本的 vue-loader 還允許開發(fā)在 SFC 當(dāng)中寫 custom block。最終一個(gè) Vue SFC 通過 vue-loader 的處理,會(huì)將 template/script/style/custom block 拆解為獨(dú)立的 block,每個(gè) block 還可以再交給對(duì)應(yīng)的 loader 去做進(jìn)一步的處理,例如你的 template 是使用 pug 來書寫的,那么首先使用 vue-loader 獲取一個(gè) SFC 內(nèi)部 pug 模板的內(nèi)容,然后再交給 pug 相關(guān)的 loader 處理,可以說 vue-loader 對(duì)于 Vue SFC 來說是一個(gè)入口處理器。
在實(shí)際運(yùn)用過程中,我們先來看下有關(guān) Vue 的 webpack 配置:
const VueloaderPlugin = require("vue-loader/lib/plugin") module.exports = { ... module: { rules: [ ... { test: /.vue$/, loader: "vue-loader" } ] } plugins: [ new VueloaderPlugin() ] ... }
一個(gè)就是 module.rules 有關(guān)的配置,如果處理的 module 路徑是以.vue形式結(jié)尾的,那么會(huì)交給 vue-loader 來處理,同時(shí)在 v15 版本必須要使用 vue-loader 內(nèi)部提供的一個(gè) plugin,它的職責(zé)是將你定義過的其它規(guī)則復(fù)制并應(yīng)用到 .vue 文件里相應(yīng)語(yǔ)言的塊。例如,如果你有一條匹配 /.js$/ 的規(guī)則,那么它會(huì)應(yīng)用到 .vue 文件里的 塊,說到這里我們就一起先來看看這個(gè) plugin 里面到底做了哪些工作。
VueLoaderPlugin我們都清楚 webpack plugin 的裝載過程是在整個(gè) webpack 編譯周期中初始階段,我們先來看下 VueLoaderPlugin 內(nèi)部源碼的實(shí)現(xiàn):
// vue-loader/lib/plugin.js class VueLoaderPlugin { apply() { ... // use webpack"s RuleSet utility to normalize user rules const rawRules = compiler.options.module.rules const { rules } = new RuleSet(rawRules) // find the rule that applies to vue files // 判斷是否有給`.vue`或`.vue.html`進(jìn)行 module.rule 的配置 let vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue`)) if (vueRuleIndex < 0) { vueRuleIndex = rawRules.findIndex(createMatcher(`foo.vue.html`)) } const vueRule = rules[vueRuleIndex] ... // 判斷對(duì)于`.vue`或`.vue.html`配置的 module.rule 是否有 vue-loader // get the normlized "use" for vue files const vueUse = vueRule.use // get vue-loader options const vueLoaderUseIndex = vueUse.findIndex(u => { return /^vue-loader|(/||@)vue-loader/.test(u.loader) }) ... // 創(chuàng)建 pitcher loader 的配置 const pitcher = { loader: require.resolve("./loaders/pitcher"), resourceQuery: query => { const parsed = qs.parse(query.slice(1)) return parsed.vue != null }, options: { cacheDirectory: vueLoaderUse.options.cacheDirectory, cacheIdentifier: vueLoaderUse.options.cacheIdentifier } } // 拓展開發(fā)者的 module.rule 配置,加入 vue-loader 內(nèi)部提供的 pitcher loader // replace original rules compiler.options.module.rules = [ pitcher, ...clonedRules, ...rules ] } }
這個(gè) plugin 主要完成了以下三部分的工作:
判斷是否有給.vue或.vue.html進(jìn)行 module.rule 的配置;
判斷對(duì)于.vue或.vue.html配置的 module.rule 是否有 vue-loader;
拓展開發(fā)者的 module.rule 配置,加入 vue-loader 內(nèi)部提供的 pitcher loader
我們看到有關(guān) pitcher loader 的 rule 匹配條件是通過resourceQuery方法來進(jìn)行判斷的,即判斷 module path 上的 query 參數(shù)是否存在 vue,例如:
// 這種類型的 module path 就會(huì)匹配上 "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
如果存在的話,那么就需要將這個(gè) loader 加入到構(gòu)建這個(gè) module 的 loaders 數(shù)組當(dāng)中。以上就是 VueLoaderPlugin 所做的工作,其中涉及到拓展后的 module rule 里面加入的 pitcher loader 具體做的工作后文會(huì)分析。
Step 1接下來我們看下 vue-loader 的內(nèi)部實(shí)現(xiàn)。首先來看下入口文件的相關(guān)內(nèi)容:
// vue-loader/lib/index.js ... const { parse } = require("@vue/component-compiler-utils") function loadTemplateCompiler () { try { return require("vue-template-compiler") } catch (e) { throw new Error( `[vue-loader] vue-template-compiler must be installed as a peer dependency, ` + `or a compatible compiler implementation must be passed via options.` ) } } module.exports = function(source) { const loaderContext = this // 獲取 loaderContext 對(duì)象 // 從 loaderContext 獲取相關(guān)參數(shù) const { target, // webpack 構(gòu)建目標(biāo),默認(rèn)為 web request, // module request 路徑(由 path 和 query 組成) minimize, // 構(gòu)建模式 sourceMap, // 是否開啟 sourceMap rootContext, // 項(xiàng)目的根路徑 resourcePath, // module 的 path 路徑 resourceQuery // module 的 query 參數(shù) } = loaderContext // 接下來就是一系列對(duì)于參數(shù)和路徑的處理 const rawQuery = resourceQuery.slice(1) const inheritQuery = `&${rawQuery}` const incomingQuery = qs.parse(rawQuery) const options = loaderUtils.getOptions(loaderContext) || {} ... // 開始解析 sfc,根據(jù)不同的 block 來拆解對(duì)應(yīng)的內(nèi)容 const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(), filename, sourceRoot, needMap: sourceMap }) // 如果 query 參數(shù)上帶了 block 的 type 類型,那么會(huì)直接返回對(duì)應(yīng) block 的內(nèi)容 // 例如: foo.vue?vue&type=template,那么會(huì)直接返回 template 的文本內(nèi)容 if (incomingQuery.type) { return selectBlock( descriptor, loaderContext, incomingQuery, !!options.appendExtension ) } ... // template let templateImport = `var render, staticRenderFns` let templateRequest if (descriptor.template) { const src = descriptor.template.src || resourcePath const idQuery = `&id=${id}` const scopedQuery = hasScoped ? `&scoped=true` : `` const attrsQuery = attrsToQuery(descriptor.template.attrs) const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}` const request = templateRequest = stringifyRequest(src + query) templateImport = `import { render, staticRenderFns } from ${request}` } // script let scriptImport = `var script = {}` if (descriptor.script) { const src = descriptor.script.src || resourcePath const attrsQuery = attrsToQuery(descriptor.script.attrs, "js") const query = `?vue&type=script${attrsQuery}${inheritQuery}` const request = stringifyRequest(src + query) scriptImport = ( `import script from ${request} ` + `export * from ${request}` // support named exports ) } // styles let stylesCode = `` if (descriptor.styles.length) { stylesCode = genStylesCode( loaderContext, descriptor.styles, id, resourcePath, stringifyRequest, needsHotReload, isServer || isShadow // needs explicit injection? ) } let code = ` ${templateImport} ${scriptImport} ${stylesCode} /* normalize component */ import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)} var component = normalizer( script, render, staticRenderFns, ${hasFunctional ? `true` : `false`}, ${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`}, ${hasScoped ? JSON.stringify(id) : `null`}, ${isServer ? JSON.stringify(hash(request)) : `null`} ${isShadow ? `,true` : ``} ) `.trim() + ` ` if (descriptor.customBlocks && descriptor.customBlocks.length) { code += genCustomBlocksCode( descriptor.customBlocks, resourcePath, resourceQuery, stringifyRequest ) } ... // Expose filename. This is used by the devtools and Vue runtime warnings. code += ` component.options.__file = ${ isProduction // For security reasons, only expose the file"s basename in production. ? JSON.stringify(filename) // Expose the file"s full path in development, so that it can be opened // from the devtools. : JSON.stringify(rawShortFilePath.replace(//g, "/")) }` code += ` export default component.exports` return code }
以上就是 vue-loader 的入口文件(index.js)主要做的工作:對(duì)于 request 上不帶 type 類型的 Vue SFC 進(jìn)行 parse,獲取每個(gè) block 的相關(guān)內(nèi)容,將不同類型的 block 組件的 Vue SFC 轉(zhuǎn)化成 js module 字符串,具體的內(nèi)容如下:
import { render, staticRenderFns } from "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&" import script from "./source.vue?vue&type=script&lang=js&" export * from "./source.vue?vue&type=script&lang=js&" import style0 from "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&" /* normalize component */ import normalizer from "!../lib/runtime/componentNormalizer.js" var component = normalizer( script, render, staticRenderFns, false, null, "27e4e96e", null ) /* custom blocks */ import block0 from "./source.vue?vue&type=custom&index=0&blockType=foo" if (typeof block0 === "function") block0(component) // 省略了有關(guān) hotReload 的代碼 component.options.__file = "example/source.vue" export default component.exports
從生成的 js module 字符串來看:將由 source.vue 提供 render函數(shù)/staticRenderFns,js script,style樣式,并交由 normalizer 進(jìn)行統(tǒng)一的格式化,最終導(dǎo)出 component.exports。
Step 2這樣 vue-loader 處理的第一個(gè)階段結(jié)束了,vue-loader 在這一階段將 Vue SFC 轉(zhuǎn)化為 js module 后,接下來進(jìn)入到第二階段,將新生成的 js module 加入到 webpack 的編譯環(huán)節(jié),即對(duì)這個(gè) js module 進(jìn)行 AST 的解析以及相關(guān)依賴的收集過程,這里我用每個(gè) request 去標(biāo)記每個(gè)被收集的 module(這里只說明和 Vue SFC 相關(guān)的模塊內(nèi)容):
[ "./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&", "./source.vue?vue&type=script&lang=js&", "./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&", "./source.vue?vue&type=custom&index=0&blockType=foo" ]
我們看到通過 vue-loader 處理到得到的 module path 上的 query 參數(shù)都帶有 vue 字段。這里便涉及到了我們?cè)谖恼麻_篇提到的 VueLoaderPlugin 加入的 pitcher loader。如果遇到了 query 參數(shù)上帶有 vue 字段的 module path,那么就會(huì)把 pitcher loader 加入到處理這個(gè) module 的 loaders 數(shù)組當(dāng)中。因此這個(gè) module 最終也會(huì)經(jīng)過 pitcher loader 的處理。此外在 loader 的配置順序上,pitcher loader 為第一個(gè),因此在處理 Vue SFC 模塊的時(shí)候,最先也是交由 pitcher loader 來處理。
事實(shí)上對(duì)一個(gè) Vue SFC 處理的第二階段就是剛才提到的,Vue SFC 會(huì)經(jīng)由 pitcher loader 來做進(jìn)一步的處理。那么我們就來看下 vue-loader 內(nèi)部提供的 pitcher loader 主要是做了哪些工作呢:
剔除 eslint loader;
剔除 pitcher loader 自身;
根據(jù)不同 type query 參數(shù)進(jìn)行攔截處理,返回對(duì)應(yīng)的內(nèi)容,跳過后面的 loader 執(zhí)行的階段,進(jìn)入到 module parse 階段
// vue-loader/lib/loaders/pitcher.js module.export = code => code module.pitch = function () { ... const query = qs.parse(this.resourceQuery.slice(1)) let loaders = this.loaders // 剔除 eslint loader // if this is a language block request, eslint-loader may get matched // multiple times if (query.type) { // if this is an inline block, since the whole file itself is being linted, // remove eslint-loader to avoid duplicate linting. if (/.vue$/.test(this.resourcePath)) { loaders = loaders.filter(l => !isESLintLoader(l)) } else { // This is a src import. Just make sure there"s not more than 1 instance // of eslint present. loaders = dedupeESLintLoader(loaders) } } // 剔除 pitcher loader 自身 // remove self loaders = loaders.filter(isPitcher) if (query.type === "style") { const cssLoaderIndex = loaders.findIndex(isCSSLoader) if (cssLoaderIndex > -1) { const afterLoaders = loaders.slice(0, cssLoaderIndex + 1) const beforeLoaders = loaders.slice(cssLoaderIndex + 1) const request = genRequest([ ...afterLoaders, stylePostLoaderPath, ...beforeLoaders ]) return `import mod from ${request}; export default mod; export * from ${request}` } } if (query.type === "template") { const path = require("path") const cacheLoader = cacheDirectory && cacheIdentifier ? [`cache-loader?${JSON.stringify({ // For some reason, webpack fails to generate consistent hash if we // use absolute paths here, even though the path is only used in a // comment. For now we have to ensure cacheDirectory is a relative path. cacheDirectory: path.isAbsolute(cacheDirectory) ? path.relative(process.cwd(), cacheDirectory) : cacheDirectory, cacheIdentifier: hash(cacheIdentifier) + "-vue-loader-template" })}`] : [] const request = genRequest([ ...cacheLoader, templateLoaderPath + `??vue-loader-options`, ...loaders ]) // the template compiler uses esm exports return `export * from ${request}` } // if a custom block has no other matching loader other than vue-loader itself, // we should ignore it if (query.type === `custom` && loaders.length === 1 && loaders[0].path === selfPath) { return `` } // When the user defines a rule that has only resourceQuery but no test, // both that rule and the cloned rule will match, resulting in duplicated // loaders. Therefore it is necessary to perform a dedupe here. const request = genRequest(loaders) return `import mod from ${request}; export default mod; export * from ${request}` }
對(duì)于 style block 的處理,首先判斷是否有 css-loader,如果有的話就重新生成一個(gè)新的 request,這個(gè) request 包含了 vue-loader 內(nèi)部提供的 stylePostLoader,并返回一個(gè) js module,根據(jù) pitch 函數(shù)的規(guī)則,pitcher loader 后面的 loader 都會(huì)被跳過,這個(gè)時(shí)候開始編譯這個(gè)返回的 js module。相關(guān)的內(nèi)容為:
import mod from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&" export default mod export * from "-!../node_modules/vue-style-loader/index.js!../node_modules/css-loader/index.js!../lib/loaders/stylePostLoader.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=style&index=0&id=27e4e96e&scoped=true&lang=css&"
對(duì)于 template block 的處理流程類似,生成一個(gè)新的 request,這個(gè) request 包含了 vue-loader 內(nèi)部提供的 templateLoader,并返回一個(gè) js module,并跳過后面的 loader,然后開始編譯返回的 js module。相關(guān)的內(nèi)容為:
export * from "-!../lib/loaders/templateLoader.js??vue-loader-options!../node_modules/pug-plain-loader/index.js!../lib/index.js??vue-loader-options!./source.vue?vue&type=template&id=27e4e96e&scoped=true&lang=pug&"
這樣對(duì)于一個(gè) Vue SFC 處理的第二階段也就結(jié)束了,通過 pitcher loader 去攔截不同類型的 block,并返回新的 js module,跳過后面的 loader 的執(zhí)行,同時(shí)在內(nèi)部會(huì)剔除掉 pitcher loader,這樣在進(jìn)入到下一個(gè)處理階段的時(shí)候,pitcher loader 不在使用的 loader 范圍之內(nèi),因此下一階段 Vue SFC 便不會(huì)經(jīng)由 pitcher loader 來處理。
Step 3接下來進(jìn)入到第三個(gè)階段,編譯返回的新的 js module,完成 AST 的解析和依賴收集工作,并開始處理不同類型的 block 的編譯轉(zhuǎn)換工作。就拿 Vue SFC 當(dāng)中的 style / template block 來舉例,
style block 會(huì)經(jīng)過以下的流程處理:
source.vue?vue&type=style -> vue-loader(抽離 style block) -> stylePostLoader(處理作用域 scoped css) -> css-loader(處理相關(guān)資源引入路徑) -> vue-style-loader(動(dòng)態(tài)創(chuàng)建 style 標(biāo)簽插入 css)
template block 會(huì)經(jīng)過以下的流程處理:
source.vue?vue&type=template -> vue-loader(抽離 template block ) -> pug-plain-loader(將 pug 模塊轉(zhuǎn)化為 html 字符串) -> templateLoader(編譯 html 模板字符串,生成 render/staticRenderFns 函數(shù)并暴露出去)
我們看到經(jīng)過 vue-loader 處理時(shí),會(huì)根據(jù)不同 module path 的類型(query 參數(shù)上的 type 字段)來抽離 SFC 當(dāng)中不同類型的 block。這也是 vue-loader 內(nèi)部定義的相關(guān)規(guī)則:
// vue-loader/lib/index.js const qs = require("querystring") const selectBlock = require("./select") ... module.exports = function (source) { ... const rawQuery = resourceQuery.slice(1) const inheritQuery = `&${rawQuery}` const incomingQuery = qs.parse(rawQuery) ... const descriptor = parse({ source, compiler: options.compiler || loadTemplateCompiler(), filename, sourceRoot, needMap: sourceMap }) // if the query has a type field, this is a language block request // e.g. foo.vue?type=template&id=xxxxx // and we will return early if (incomingQuery.type) { return selectBlock( descriptor, loaderContext, incomingQuery, !!options.appendExtension ) } ... }
當(dāng) module path 上的 query 參數(shù)帶有 type 字段,那么會(huì)直接調(diào)用 selectBlock 方法去獲取 type 對(duì)應(yīng)類型的 block 內(nèi)容,跳過 vue-loader 后面的處理流程(這也是與 vue-loader 第一次處理這個(gè) module時(shí)流程不一樣的地方),并進(jìn)入到下一個(gè) loader 的處理流程中,selectBlock 方法內(nèi)部主要就是根據(jù)不同的 type 類型(template/script/style/custom),來獲取 descriptor 上對(duì)應(yīng)類型的 content 內(nèi)容并傳入到下一個(gè) loader 處理:
module.exports = function selectBlock ( descriptor, loaderContext, query, appendExtension ) { // template if (query.type === `template`) { if (appendExtension) { loaderContext.resourcePath += "." + (descriptor.template.lang || "html") } loaderContext.callback( null, descriptor.template.content, descriptor.template.map ) return } // script if (query.type === `script`) { if (appendExtension) { loaderContext.resourcePath += "." + (descriptor.script.lang || "js") } loaderContext.callback( null, descriptor.script.content, descriptor.script.map ) return } // styles if (query.type === `style` && query.index != null) { const style = descriptor.styles[query.index] if (appendExtension) { loaderContext.resourcePath += "." + (style.lang || "css") } loaderContext.callback( null, style.content, style.map ) return } // custom if (query.type === "custom" && query.index != null) { const block = descriptor.customBlocks[query.index] loaderContext.callback( null, block.content, block.map ) return } }總結(jié)
通過 vue-loader 的源碼我們看到一個(gè) Vue SFC 在整個(gè)編譯構(gòu)建環(huán)節(jié)是怎么樣一步一步處理的,這也是得益于 webpack 給開發(fā)這提供了這樣一種 loader 的機(jī)制,使得開發(fā)者通過這樣一種方式去對(duì)項(xiàng)目源碼做對(duì)應(yīng)的轉(zhuǎn)換工作以滿足相關(guān)的開發(fā)需求。結(jié)合之前的2篇有關(guān) webpack loader 源碼的分析,大家應(yīng)該對(duì) loader 有了更加深入的理解,也希望大家活學(xué)活用,利用 loader 機(jī)制去完成更多貼合實(shí)際需求的開發(fā)工作。
文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/102787.html
摘要:在一個(gè)構(gòu)建過程中,首先根據(jù)的依賴類型例如調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù)來創(chuàng)建對(duì)應(yīng)的模塊。 文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~ Webpack 系列文章: Webpack Loader 高手進(jìn)階(一)Webpack Loader 高手進(jìn)階(二)Webpack Loader 高手進(jìn)階(三) Webpack loader 詳解 loader 的配置 Webpack...
webpack的loaders是一大特色,也是很重要的一部分。這遍博客我將分類講解一些常用的laodershowImg(https://segmentfault.com/img/remote/1460000005742040); 一、loaders之 預(yù)處理 css-loader 處理css中路徑引用等問題 style-loader 動(dòng)態(tài)把樣式寫入css sass-loader scss編譯器 ...
摘要:如果函數(shù)沒有返回值的話,那么進(jìn)入到下一個(gè)的函數(shù)的執(zhí)行階段。這也是異步化的一種方式如果執(zhí)行后有返回值,執(zhí)行開始下一個(gè)執(zhí)行以上就是對(duì)于在構(gòu)建過程中執(zhí)行流程的源碼分析。 文章首發(fā)于個(gè)人github blog: Biu-blog,歡迎大家關(guān)注~ Webpack 系列文章: Webpack Loader 高手進(jìn)階(一)Webpack Loader 高手進(jìn)階(二)Webpack Loader 高手...
摘要:基本環(huán)境搭建就不展開講了一插件篇自動(dòng)補(bǔ)全前綴官方是這樣說的,也就是說它是一個(gè)自動(dòng)檢測(cè)兼容性給各個(gè)瀏覽器加個(gè)內(nèi)核前綴的插件。 上一篇博客講解了webpack環(huán)境的基本,這一篇講解一些更深入的內(nèi)容和開發(fā)技巧?;经h(huán)境搭建就不展開講了showImg(http://static.xiaomo.info/images/webpack.png); 一、插件篇 1. 自動(dòng)補(bǔ)全css3前綴 autop...
摘要:,我想大家應(yīng)該都知道或者聽過,是前端一個(gè)工具可以讓各個(gè)模塊進(jìn)行加載預(yù)處理再進(jìn)行打包。 webpack,我想大家應(yīng)該都知道或者聽過,Webpack是前端一個(gè)工具,可以讓各個(gè)模塊進(jìn)行加載,預(yù)處理,再進(jìn)行打包?,F(xiàn)代的前端開發(fā)很多環(huán)境都依賴webpack構(gòu)建,比如vue官方就推薦使用webpack.廢話不多說,我們趕緊開始吧. 第一步、安裝webpack 新建文件夾webpack->再在web...
閱讀 3246·2021-09-07 10:10
閱讀 3591·2019-08-30 15:44
閱讀 2593·2019-08-30 15:44
閱讀 3024·2019-08-29 15:11
閱讀 2234·2019-08-28 18:26
閱讀 2754·2019-08-26 12:21
閱讀 1121·2019-08-23 16:12
閱讀 3038·2019-08-23 14:57