整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          webpack的幾個常見loader源碼淺析,動手實

          webpack的幾個常見loader源碼淺析,動手實現一個md2html-loader

          本文會帶你簡單的認識一下webpack的loader,動手實現一個利用md轉成抽象語法樹,再轉成html字符串的loader。順便簡單的了解一下幾個style-loader,vue-loader,babel-loader的源碼以及工作流程。

          loader簡介

          webpack允許我們使用loader來處理文件,loader是一個導出為function的node模塊??梢詫⑵ヅ涞降奈募M行一次轉換,同時loader可以鏈式傳遞。loader文件處理器是一個CommonJs風格的函數,該函數接收一個 String/Buffer 類型的入參,并返回一個 String/Buffer 類型的返回值。

          loader 的配置的兩種形式

          方案1:

          // webpack.config.js
          module.exports = {
            ...
            module: {
              rules: [{
                test: /.vue$/,
                loader: 'vue-loader'
              }, {
                test: /.scss$/,
                // 先經過 sass-loader,然后將結果傳入 css-loader,最后再進入 style-loader。
                use: [
                  'style-loader',//從JS字符串創建樣式節點
                  'css-loader',// 把  CSS 翻譯成 CommonJS
                  {
                    loader: 'sass-loader',
                    options: {
                      data: '$color: red;'// 把 Sass 編譯成 CSS
                    }
                  }
                ]
              }]
            }
            ...
          }

          方法2(右到左地被調用)

          // module
          import Styles from 'style-loader!css-loader?modules!./styles.css';

          當鏈式調用多個 loader 的時候,請記住它們會以相反的順序執行。取決于數組寫法格式,從右向左或者從下向上執行。像流水線一樣,挨個處理每個loader,前一個loader的結果會傳遞給下一個loader,最后的 Loader 將處理后的結果以 String 或 Buffer 的形式返回給 compiler。

          使用 loader-utils 能夠編譯 loader 的配置,還可以通過 schema-utils 進行驗證

          import { getOptions } from 'loader-utils'; 
          import { validateOptions } from 'schema-utils';  
          const schema = {
            // ...
          }
          export default function(content) {
            // 獲取 options
            const options = getOptions(this);
            // 檢驗loader的options是否合法
            validateOptions(schema, options, 'Demo Loader');
          
            // 在這里寫轉換 loader 的邏輯
            // ...
             return content;   
          };
          • content: 表示源文件字符串或者buffer
          • map: 表示sourcemap對象
          • meta: 表示元數據,輔助對象

          同步loader

          同步 loader,我們可以通過return和this.callback返回輸出的內容

          module.exports = function(content, map, meta) {
            //一些同步操作
            outputContent=someSyncOperation(content)
            return outputContent;
          }

          如果返回結果只有一個,也可以直接使用 return 返回結果。但是,如果有些情況下還需要返回其他內容,如sourceMap或是AST語法樹,這個時候可以借助webpack提供的api this.callback

          module.exports = function(content, map, meta) {
            this.callback(
              err: Error | null,
              content: string | Buffer,
              sourceMap?: SourceMap,
              meta?: any
            );
            return;
          }

          第一個參數必須是 Error 或者 null 第二個參數是一個 string 或者 Buffer??蛇x的:第三個參數必須是一個可以被這個模塊解析的 source map??蛇x的:第四個選項,會被 webpack 忽略,可以是任何東西【可以將抽象語法樹(abstract syntax tree - AST)(例如 ESTree)作為第四個參數(meta),如果你想在多個 loader 之間共享通用的 AST,這樣做有助于加速編譯時間?!?。

          異步loader

          異步loader,使用 this.async 來獲取 callback 函數。

          // 讓 Loader 緩存
          module.exports = function(source) {
              var callback = this.async();
              // 做異步的事
              doSomeAsyncOperation(content, function(err, result) {
                  if(err) return callback(err);
                  callback(null, result);
              });
          };

          詳情請參考官網API

          開發一個簡單的md-loader

          const marked = require("marked");
          
          const loaderUtils = require("loader-utils");
          module.exports = function (content) {
             this.cacheable && this.cacheable();
             const options = loaderUtils.getOptions(this);
             try {
                 marked.setOptions(options);
                 return marked(content)
             } catch (err) {
                 this.emitError(err);
                 return null
             }
              
          };

          上述的例子是通過現成的插件把markdown文件里的content轉成html字符串,但是如果沒有這個插件,改怎么做呢?這個情況下,我們可以考慮另外一種解法,借助 AST 語法樹,來協助我們更加便捷地操作轉換。

          利用 AST 作源碼轉換

          markdown-ast是將markdown文件里的content轉成數組形式的抽象語法樹節點,操作 AST 語法樹遠比操作字符串要簡單、方便得多:

          //通過正則的方法把字符串處理成直觀的AST語法樹
          const md = require('markdown-ast');
          module.exports = function(content) {
              this.cacheable && this.cacheable();
              const options = loaderUtils.getOptions(this);
              try {
                console.log(md(content))
                const parser = new MdParser(content);
                return parser.data
              } catch (err) {
                console.log(err)
                return null
              }
          };
          const md = require('markdown-ast');
          const hljs = require('highlight.js');//代碼高亮插件
          // 利用 AST 作源碼轉換
          class MdParser {
           constructor(content) {
              this.data = md(content);
              console.log(this.data)
            this.parse()
           }
           parse() {
            this.data = this.traverse(this.data);
           }
           traverse(ast) {
              console.log("md轉抽象語法樹操作",ast)
               let body = '';
              ast.map(item => {
                switch (item.type) {
                  case "bold":
                  case "break":
                  case "codeBlock":
                    const highlightedCode = hljs.highlight(item.syntax, item.code).value
                    body += highlightedCode
                    break;
                  case "codeSpan":
                  case "image":
                  case "italic":
                  case "link":
                  case "list":
                    item.type = (item.bullet === '-') ? 'ul' : 'ol'
                    if (item.type !== '-') {
                      item.startatt = (` start=${item.indent.length}`)
                    } else {
                      item.startatt = ''
                    }
                    body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
                    break;
                  case "quote":
                    let quoteString = this.traverse(item.block)
                    body += '<blockquote>\n' + quoteString + '</blockquote>\n';
                    break;
                  case "strike":
                  case "text":
                  case "title":
                    body += `<h${item.rank}>${item.text}</h${item.rank}>`
                    break;
                  default:
                    throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
                }
              })
              return body
           }
          }

          md 轉成抽象語樹

          ast抽象語法數轉成html字符串


          md2html-loader源碼地址(https://github.com/6fedcom/fe-blog/blob/master/webpack-loader/loaders/md-loader.js)

          loader的一些開發技巧

          1. 盡量保證一個loader去做一件事情,然后可以用不同的loader組合不同的場景需求
          2. 開發的時候不應該在 loader 中保留狀態。loader必須是一個無任何副作用的純函數,loader支持異步,因此是可以在 loader 中有 I/O 操作的。
          3. 模塊化:保證 loader 是模塊化的。loader 生成模塊需要遵循和普通模塊一樣的設計原則。
          4. 合理的使用緩存 合理的緩存能夠降低重復編譯帶來的成本。loader 執行時默認是開啟緩存的,這樣一來, webpack 在編譯過程中執行到判斷是否需要重編譯 loader 實例的時候,會直接跳過 rebuild 環節,節省不必要重建帶來的開銷。但是當且僅當有你的 loader 有其他不穩定的外部依賴(如 I/O 接口依賴)時,可以關閉緩存:
          this.cacheable&&this.cacheable(false);
          1. loader-runner 是一個非常實用的工具,用來開發、調試loader,它允許你不依靠 webpack 單獨運行 loadernpm install loader-runner --save-dev
          // 創建 run-loader.js
          const fs = require("fs");
          const path = require("path");
          const { runLoaders } = require("loader-runner");
          
          runLoaders(
            {
              resource: "./readme.md",
              loaders: [path.resolve(__dirname, "./loaders/md-loader")],
              readResource: fs.readFile.bind(fs),
            },
            (err, result) => 
              (err ? console.error(err) : console.log(result))
          );

          執行 node run-loader

          認識更多的loader

          style-loader源碼簡析

          作用:把樣式插入到DOM中,方法是在head中插入一個style標簽,并把樣式寫入到這個標簽的 innerHTML 里 看下源碼。

          先去掉option處理代碼,這樣就比較清晰明了了

          返回一段js代碼,通過require來獲取css內容,再通過addStyle的方法把css插入到dom里 自己實現一個簡陋的style-loader.js

          module.exports.pitch = function (request) {
            const {stringifyRequest}=loaderUtils
            var result = [
              //1. 獲取css內容。2.// 調用addStyle把CSS內容插入到DOM中(locals為true,默認導出css)
              'var content=require(' + stringifyRequest(this, '!!' + request) + ')’, 
              'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’, 
              'if(content.locals) module.exports = content.locals’ 
            ]
            return result.join(';')
          }

          需要說明的是,正常我們都會用default的方法,這里用到pitch方法。pitch 方法有一個官方的解釋在這里 pitching loader。簡單的解釋一下就是,默認的loader都是從右向左執行,用 pitching loader 是從左到右執行的。

          {
            test: /\.css$/,
            use: [
              { loader: "style-loader" },
              { loader: "css-loader" }
            ]
          }

          為什么要先執行style-loader呢,因為我們要把css-loader拿到的內容最終輸出成CSS樣式中可以用的代碼而不是字符串。

          addstyle.js

          module.exports = function (content) {
            let style = document.createElement("style")
            style.innerHTML = content
            document.head.appendChild(style)
          }

          babel-loader源碼簡析

          首先看下跳過loader的配置處理,看下babel-loader輸出

          上圖我們可以看到是輸出transpile(source, options)的code和map 再來看下transpile方法做了啥

          babel-loader是通過babel.transform來實現對代碼的編譯的, 這么看來,所以我們只需要幾行代碼就可以實現一個簡單的babel-loader

          const babel = require("babel-core")
          module.exports = function (source) {
            const babelOptions = {
              presets: ['env']
            }
            return babel.transform(source, babelOptions).code
          }

          vue-loader源碼簡析

          vue單文件組件(簡稱sfc)

          <template>
            <div class="text">
              {{a}}
            </div>
          </template>
          <script>
          export default {
            data () {
              return {
                a: "vue demo"
              };
            }
          };
          </script>
          <style lang="scss" scope>
          .text {
            color: red;
          }
          </style>

          webpack配置

          const VueloaderPlugin = require('vue-loader/lib/plugin')
          module.exports = {
            ...
            module: {
              rules: [
                ...
                {
                  test: /\.vue$/,
                  loader: 'vue-loader'
                }
              ]
            }
          
            plugins: [
              new VueloaderPlugin()
            ]
            ...
          }

          VueLoaderPlugin作用:將在webpack.config定義過的其它規則復制并應用到 .vue 文件里相應語言的塊中。plugin-webpack4.js

           const vueLoaderUse = vueUse[vueLoaderUseIndex]
              vueLoaderUse.ident = 'vue-loader-options'
              vueLoaderUse.options = vueLoaderUse.options || {}
              // cloneRule會修改原始rule的resource和resourceQuery配置,
              // 攜帶特殊query的文件路徑將被應用對應rule
              const clonedRules = rules
                .filter(r => r !== vueRule)
                .map(cloneRule)
          
              // global pitcher (responsible for injecting template compiler loader & CSS
              // post 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
                }
              }
          
              // 更新webpack的rules配置,這樣vue單文件中的各個標簽可以應用clonedRules相關的配置
              compiler.options.module.rules = [
                pitcher,
                ...clonedRules,
                ...rules
              ]

          獲取webpack.config.js的rules項,然后復制rules,為攜帶了?vue&lang=xx...query參數的文件依賴配置xx后綴文件同樣的loader 為Vue文件配置一個公共的loader:pitcher 將[pitchLoder, ...clonedRules, ...rules]作為webapck新的rules。

          再看一下vue-loader結果的輸出

          當引入一個vue文件后,vue-loader是將vue單文件組件進行parse,獲取每個 block 的相關內容,將不同類型的 block 組件的 Vue SFC 轉化成 js module 字符串。

          // vue-loader使用`@vue/component-compiler-utils`將SFC源碼解析成SFC描述符,,根據不同 module path 的類型(query 參數上的 type 字段)來抽離 SFC 當中不同類型的 block。
          const { parse } = require('@vue/component-compiler-utils')
          // 將單個*.vue文件內容解析成一個descriptor對象,也稱為SFC(Single-File Components)對象
          // descriptor包含template、script、style等標簽的屬性和內容,方便為每種標簽做對應處理
          const descriptor = parse({
            source,
            compiler: options.compiler || loadTemplateCompiler(loaderContext),
            filename,
            sourceRoot,
            needMap: sourceMap
          })
          
          // 為單文件組件生成唯一哈希id
          const id = hash(
            isProduction
            ? (shortFilePath + '\n' + source)
            : shortFilePath
          )
          // 如果某個style標簽包含scoped屬性,則需要進行CSS Scoped處理
          const hasScoped = descriptor.styles.some(s => s.scoped)

          然后下一步將新生成的 js module 加入到 webpack 的編譯環節,即對這個 js module 進行 AST 的解析以及相關依賴的收集過程。

          來看下源碼是怎么操作不同type類型(template/script/style)的,selectBlock 方法內部主要就是根據不同的 type 類型,來獲取 descriptor 上對應類型的 content 內容并傳入到下一個 loader 處理

          這三段代碼可以把不同type解析成一個import的字符串

          import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
          import script from "./App.vue?vue&type=script&lang=js&"
          export * from "./App.vue?vue&type=script&lang=js&"
          import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"

          總結一下vue-loader的工作流程

          1. 注冊VueLoaderPlugin 在插件中,會復制當前項目webpack配置中的rules項,當資源路徑包含query.lang時通過resourceQuery匹配相同的rules并執行對應loader時 插入一個公共的loader,并在pitch階段根據query.type插入對應的自定義loader
          2. 加載*.vue時會調用vue-loader .vue文件被解析成一個descriptor對象,包含template、script、styles等屬性對應各個標簽, 對于每個標簽,會根據標簽屬性拼接src?vue&query引用代碼,其中src為單頁面組件路徑,query為一些特性的參數,比較重要的有lang、type和scoped 如果包含lang屬性,會匹配與該后綴相同的rules并應用對應的loaders 根據type執行對應的自定義loader,template將執行templateLoader、style將執行stylePostLoader
          3. 在templateLoader中,會通過vue-template-compiler將template轉換為render函數,在此過程中, 會將傳入的scopeId追加到每個標簽的segments上,最后作為vnode的配置屬性傳遞給createElemenet方法, 在render函數調用并渲染頁面時,會將scopeId屬性作為原始屬性渲染到頁面上
          4. 在stylePostLoader中,通過PostCSS解析style標簽內容

          參考文獻

          1. webpack官網loader api(https://www.webpackjs.com/api/loaders/)
          2. 手把手教你寫webpack yaml-loader(https://mp.weixin.qq.com/s/gTAq5K5pziPT4tmiGqw5_w)
          3. 言川-webpack 源碼解析系列(https://github.com/lihongxun945/diving-into-webpack)
          4. 從vue-loader源碼分析CSS Scoped的實現(https://juejin.im/post/5d8627355188253f3a70c22c)

          文會帶你簡單的認識一下webpack的loader,動手實現一個利用md轉成抽象語法樹,再轉成html字符串的loader。順便簡單的了解一下幾個style-loader,vue-loader,babel-loader的源碼以及工作流程。

          loader簡介

          webpack允許我們使用loader來處理文件,loader是一個導出為function的node模塊。可以將匹配到的文件進行一次轉換,同時loader可以鏈式傳遞。 loader文件處理器是一個CommonJs風格的函數,該函數接收一個 String/Buffer 類型的入參,并返回一個 String/Buffer 類型的返回值。

          loader 的配置的兩種形式

          方案1:

          // webpack.config.js
          module.exports = {
            ...
            module: {
              rules: [{
                test: /.vue$/,
                loader: 'vue-loader'
              }, {
                test: /.scss$/,
                // 先經過 sass-loader,然后將結果傳入 css-loader,最后再進入 style-loader。
                use: [
                  'style-loader',//從JS字符串創建樣式節點
                  'css-loader',// 把  CSS 翻譯成 CommonJS
                  {
                    loader: 'sass-loader',
                    options: {
                      data: '$color: red;'// 把 Sass 編譯成 CSS
                    }
                  }
                ]
              }]
            }
            ...
          }

          方法2(右到左地被調用)

          // module
          import Styles from 'style-loader!css-loader?modules!./styles.css';

          當鏈式調用多個 loader 的時候,請記住它們會以相反的順序執行。取決于數組寫法格式,從右向左或者從下向上執行。像流水線一樣,挨個處理每個loader,前一個loader的結果會傳遞給下一個loader,最后的 Loader 將處理后的結果以 String 或 Buffer 的形式返回給 compiler。

          使用 loader-utils 能夠編譯 loader 的配置,還可以通過 schema-utils 進行驗證

          import { getOptions } from 'loader-utils'; 
          import { validateOptions } from 'schema-utils';  
          const schema = {
            // ...
          }
          export default function(content) {
            // 獲取 options
            const options = getOptions(this);
            // 檢驗loader的options是否合法
            validateOptions(schema, options, 'Demo Loader');
          
            // 在這里寫轉換 loader 的邏輯
            // ...
             return content;   
          };
          • content: 表示源文件字符串或者buffer
          • map: 表示sourcemap對象
          • meta: 表示元數據,輔助對象

          同步loader

          同步 loader,我們可以通過return和this.callback返回輸出的內容

          module.exports = function(content, map, meta) {
            //一些同步操作
            outputContent=someSyncOperation(content)
            return outputContent;
          }

          如果返回結果只有一個,也可以直接使用 return 返回結果。但是,如果有些情況下還需要返回其他內容,如sourceMap或是AST語法樹,這個時候可以借助webpack提供的api this.callback

          module.exports = function(content, map, meta) {
            this.callback(
              err: Error | null,
              content: string | Buffer,
              sourceMap?: SourceMap,
              meta?: any
            );
            return;
          }

          第一個參數必須是 Error 或者 null 第二個參數是一個 string 或者 Buffer。 可選的:第三個參數必須是一個可以被這個模塊解析的 source map。 可選的:第四個選項,會被 webpack 忽略,可以是任何東西【可以將抽象語法樹(abstract syntax tree - AST)(例如 ESTree)作為第四個參數(meta),如果你想在多個 loader 之間共享通用的 AST,這樣做有助于加速編譯時間?!?。

          異步loader

          異步loader,使用 this.async 來獲取 callback 函數。

          // 讓 Loader 緩存
          module.exports = function(source) {
              var callback = this.async();
              // 做異步的事
              doSomeAsyncOperation(content, function(err, result) {
                  if(err) return callback(err);
                  callback(null, result);
              });
          };

          詳情請參考官網API

          開發一個簡單的md-loader

          const marked = require("marked");
          
          const loaderUtils = require("loader-utils");
          module.exports = function (content) {
             this.cacheable && this.cacheable();
             const options = loaderUtils.getOptions(this);
             try {
                 marked.setOptions(options);
                 return marked(content)
             } catch (err) {
                 this.emitError(err);
                 return null
             }
              
          };

          上述的例子是通過現成的插件把markdown文件里的content轉成html字符串,但是如果沒有這個插件,該怎么做呢?這個情況下,我們可以考慮另外一種解法,借助 AST 語法樹,來協助我們更加便捷地操作轉換。

          利用 AST 作源碼轉換

          markdown-ast是將markdown文件里的content轉成數組形式的抽象語法樹節點,操作 AST 語法樹遠比操作字符串要簡單、方便得多:

          const md = require('markdown-ast');//通過正則的方法把字符串處理成直觀的AST語法樹
          module.exports = function(content) {
              this.cacheable && this.cacheable();
              const options = loaderUtils.getOptions(this);
              try {
                console.log(md(content))
                const parser = new MdParser(content);
                return parser.data
              } catch (err) {
                console.log(err)
                return null
              }
          };

          md通過正則切割的方法轉成抽象語樹

          const md = require('markdown-ast');//md通過正則匹配的方法把buffer轉抽象語法樹
          const hljs = require('highlight.js');//代碼高亮插件
          // 利用 AST 作源碼轉換
          class MdParser {
           constructor(content) {
              this.data = md(content);
              console.log(this.data)
            this.parse()
           }
           parse() {
            this.data = this.traverse(this.data);
           }
           traverse(ast) {
              console.log("md轉抽象語法樹操作",ast)
               let body = '';
              ast.map(item => {
                switch (item.type) {
                  case "bold":
                  case "break":
                  case "codeBlock":
                    const highlightedCode = hljs.highlight(item.syntax, item.code).value
                    body += highlightedCode
                    break;
                  case "codeSpan":
                  case "image":
                  case "italic":
                  case "link":
                  case "list":
                    item.type = (item.bullet === '-') ? 'ul' : 'ol'
                    if (item.type !== '-') {
                      item.startatt = (` start=${item.indent.length}`)
                    } else {
                      item.startatt = ''
                    }
                    body += '<' + item.type + item.startatt + '>\n' + this.traverse(item.block) + '</' + item.type + '>\n'
                    break;
                  case "quote":
                    let quoteString = this.traverse(item.block)
                    body += '<blockquote>\n' + quoteString + '</blockquote>\n';
                    break;
                  case "strike":
                  case "text":
                  case "title":
                    body += `<h${item.rank}>${item.text}</h${item.rank}>`
                    break;
                  default:
                    throw Error("error", `No corresponding treatment when item.type equal${item.type}`);
                }
              })
              return body
           }
          }

          完整的代碼參考這里

          ast抽象語法數轉成html字符串

          loader的一些開發技巧

          1. 盡量保證一個loader去做一件事情,然后可以用不同的loader組合不同的場景需求
          2. 開發的時候不應該在 loader 中保留狀態。loader必須是一個無任何副作用的純函數,loader支持異步,因此是可以在 loader 中有 I/O 操作的。
          3. 模塊化:保證 loader 是模塊化的。loader 生成模塊需要遵循和普通模塊一樣的設計原則。
          4. 合理的使用緩存 合理的緩存能夠降低重復編譯帶來的成本。loader 執行時默認是開啟緩存的,這樣一來, webpack 在編譯過程中執行到判斷是否需要重編譯 loader 實例的時候,會直接跳過 rebuild 環節,節省不必要重建帶來的開銷。 但是當且僅當有你的 loader 有其他不穩定的外部依賴(如 I/O 接口依賴)時,可以關閉緩存:
          this.cacheable&&this.cacheable(false);
          1. loader-runner 是一個非常實用的工具,用來開發、調試loader,它允許你不依靠 webpack 單獨運行 loader npm install loader-runner --save-dev
          // 創建 run-loader.js
          const fs = require("fs");
          const path = require("path");
          const { runLoaders } = require("loader-runner");
          
          runLoaders(
            {
              resource: "./readme.md",
              loaders: [path.resolve(__dirname, "./loaders/md-loader")],
              readResource: fs.readFile.bind(fs),
            },
            (err, result) => 
              (err ? console.error(err) : console.log(result))
          );

          執行 node run-loader

          認識更多的loader

          style-loader源碼簡析

          作用:把樣式插入到DOM中,方法是在head中插入一個style標簽,并把樣式寫入到這個標簽的 innerHTML 里 看下源碼。

          先去掉option處理代碼,這樣就比較清晰明了了

          返回一段js代碼,通過require來獲取css內容,再通過addStyle的方法把css插入到dom里 自己實現一個簡陋的style-loader.js

          module.exports.pitch = function (request) {
            const {stringifyRequest}=loaderUtils
            var result = [
              //1. 獲取css內容。2.// 調用addStyle把CSS內容插入到DOM中(locals為true,默認導出css)
              'var content=require(' + stringifyRequest(this, '!!' + request) + ')’, 
              'require(' + stringifyRequest(this, '!' + path.join(__dirname, "addstyle.js")) + ')(content)’, 
              'if(content.locals) module.exports = content.locals’ 
            ]
            return result.join(';')
          }

          需要說明的是,正常我們都會用default的方法,這里用到pitch方法。pitch 方法有一個官方的解釋在這里 pitching loader。簡單的解釋一下就是,默認的loader都是從右向左執行,用 pitching loader 是從左到右執行的。

          {
            test: /\.css$/,
            use: [
              { loader: "style-loader" },
              { loader: "css-loader" }
            ]
          }

          為什么要先執行style-loader呢,因為我們要把css-loader拿到的內容最終輸出成CSS樣式中可以用的代碼而不是字符串。

          addstyle.js

          module.exports = function (content) {
            let style = document.createElement("style")
            style.innerHTML = content
            document.head.appendChild(style)
          }
          babel-loader源碼簡析

          首先看下跳過loader的配置處理,看下babel-loader輸出

          上圖我們可以看到是輸出transpile(source, options)的code和map 再來看下transpile方法做了啥

          babel-loader是通過babel.transform來實現對代碼的編譯的, 這么看來,所以我們只需要幾行代碼就可以實現一個簡單的babel-loader

          const babel = require("babel-core")
          module.exports = function (source) {
            const babelOptions = {
              presets: ['env']
            }
            return babel.transform(source, babelOptions).code
          }
          vue-loader源碼簡析

          vue單文件組件(簡稱sfc)

          <template>
            <div class="text">
              {{a}}
            </div>
          </template>
          <script>
          export default {
            data () {
              return {
                a: "vue demo"
              };
            }
          };
          </script>
          <style lang="scss" scope>
          .text {
            color: red;
          }
          </style>
          

          webpack配置

          const VueloaderPlugin = require('vue-loader/lib/plugin')
          module.exports = {
            ...
            module: {
              rules: [
                ...
                {
                  test: /\.vue$/,
                  loader: 'vue-loader'
                }
              ]
            }
          
            plugins: [
              new VueloaderPlugin()
            ]
            ...
          }

          VueLoaderPlugin 作用:將在webpack.config定義過的其它規則復制并應用到 .vue 文件里相應語言的塊中。

          plugin-webpack4.js

           const vueLoaderUse = vueUse[vueLoaderUseIndex]
              vueLoaderUse.ident = 'vue-loader-options'
              vueLoaderUse.options = vueLoaderUse.options || {}
              // cloneRule會修改原始rule的resource和resourceQuery配置,
              // 攜帶特殊query的文件路徑將被應用對應rule
              const clonedRules = rules
                .filter(r => r !== vueRule)
                .map(cloneRule)
          
              // global pitcher (responsible for injecting template compiler loader & CSS
              // post 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
                }
              }
          
              // 更新webpack的rules配置,這樣vue單文件中的各個標簽可以應用clonedRules相關的配置
              compiler.options.module.rules = [
                pitcher,
                ...clonedRules,
                ...rules
              ]

          獲取webpack.config.js的rules項,然后復制rules,為攜帶了?vue&lang=xx...query參數的文件依賴配置xx后綴文件同樣的loader 為Vue文件配置一個公共的loader:pitcher 將[pitchLoder, ...clonedRules, ...rules]作為webapck新的rules。

          再看一下vue-loader結果的輸出

          當引入一個vue文件后,vue-loader是將vue單文件組件進行parse,獲取每個 block 的相關內容,將不同類型的 block 組件的 Vue SFC 轉化成 js module 字符串。

          // vue-loader使用`@vue/component-compiler-utils`將SFC源碼解析成SFC描述符,,根據不同 module path 的類型(query 參數上的 type 字段)來抽離 SFC 當中不同類型的 block。
          const { parse } = require('@vue/component-compiler-utils')
          // 將單個*.vue文件內容解析成一個descriptor對象,也稱為SFC(Single-File Components)對象
          // descriptor包含template、script、style等標簽的屬性和內容,方便為每種標簽做對應處理
          const descriptor = parse({
            source,
            compiler: options.compiler || loadTemplateCompiler(loaderContext),
            filename,
            sourceRoot,
            needMap: sourceMap
          })
          
          // 為單文件組件生成唯一哈希id
          const id = hash(
            isProduction
            ? (shortFilePath + '\n' + source)
            : shortFilePath
          )
          // 如果某個style標簽包含scoped屬性,則需要進行CSS Scoped處理
          const hasScoped = descriptor.styles.some(s => s.scoped)

          然后下一步將新生成的 js module 加入到 webpack 的編譯環節,即對這個 js module 進行 AST 的解析以及相關依賴的收集過程。

          來看下源碼是怎么操作不同type類型(template/script/style)的,selectBlock 方法內部主要就是根據不同的 type 類型,來獲取 descriptor 上對應類型的 content 內容并傳入到下一個 loader 處理

          這三段代碼可以把不同type解析成一個import的字符串

          import { render, staticRenderFns } from "./App.vue?vue&type=template&id=7ba5bd90&"
          import script from "./App.vue?vue&type=script&lang=js&"
          export * from "./App.vue?vue&type=script&lang=js&"
          import style0 from "./App.vue?vue&type=style&index=0&lang=scss&scope=true&"

          總結一下vue-loader的工作流程

          1. 注冊VueLoaderPlugin 在插件中,會復制當前項目webpack配置中的rules項,當資源路徑包含query.lang時通過resourceQuery匹配相同的rules并執行對應loader時 插入一個公共的loader,并在pitch階段根據query.type插入對應的自定義loader
          2. 加載*.vue時會調用vue-loader,.vue文件被解析成一個descriptor對象,包含template、script、styles等屬性對應各個標簽, 對于每個標簽,會根據標簽屬性拼接src?vue&query引用代碼,其中src為單頁面組件路徑,query為一些特性的參數,比較重要的有lang、type和scoped 如果包含lang屬性,會匹配與該后綴相同的rules并應用對應的loaders 根據type執行對應的自定義loader,template將執行templateLoader、style將執行stylePostLoader
          3. 在templateLoader中,會通過vue-template-compiler將template轉換為render函數,在此過程中, 會將傳入的scopeId追加到每個標簽的上,最后作為vnode的配置屬性傳遞給createElemenet方法, 在render函數調用并渲染頁面時,會將scopeId屬性作為原始屬性渲染到頁面上
          4. 在stylePostLoader中,通過PostCSS解析style標簽內容


          文中涉及的demo源碼

          點擊github倉庫

          參考文獻

          1. webpack官網loader api
          2. 手把手教你寫webpack yaml-loader
          3. 言川-webpack 源碼解析系列
          4. 從vue-loader源碼分析CSS Scoped的實現

          面簡單了解了webpack4的安裝、入口、輸出、以及插件,似乎webpack是通過這個入口文件以及各種插件的配置將各種依賴關系(例如前面的html和js之間的關系)整理后輸出,成為一個經過優化了的項目,其實這就是webpack的本質,只不過我們的例子非常簡單沒有體現出webpack的強大而已。

          一、整理項目

          前面我們的項目建立的相對隨意,現在我們把項目的目錄規整以下,讓他盡量的符合規范一些。項目結構如下:

          從圖中我們看出src目錄我們將資源分了類,有js文件夾、less文件夾、imgs文件夾,分別對應各自類型的文件。這時輸出也應該是這樣的結構,所以webpack.config.js也需要做相應的調整,主要是路徑的調整,代碼如下:

          const { CleanWebpackPlugin }=require('clean-webpack-plugin');
          const HtmlWebpackPlugin=require('html-webpack-plugin');
          module.exports={
           mode:"development",
           entry:{
           "common":"./src/js/common.js",
           "index":"./src/js/index.js",
           "login":"./src/js/login.js"
           },
           output:{
           filename:"js/[name].js",
           },
           plugins:[
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
           template:'./src/index.html',
           filename:'index.html',
           chunks:["common","index"],
           hash:true
           }),
           new HtmlWebpackPlugin({
           template:'./src/login.html',
           filename:'login.html',
           chunks:["common","login"],
           hash:true
           })
           ]
          }
          

          入口對應的是各自目錄下的js文件,輸出的話filename看起來像路徑,其實這種寫法也是可以的,webpack會自動在默認的dist目錄下創建js文件夾以及js文件。

          其實output項中有一個path配置項,這個配置項是定義所有輸出文件的根目錄,不能定義子目錄。

          再來看mode:"development"這項設置,這個是告訴webpack現在的開發模式是什么,他的值有兩個一個是development,一個是production,如果使用production那么webpack在處理時會啟用自己的一些插件對項目進行優化,例如壓縮js文件,意思就是如果設置成production的話打包后的js就已經時經過壓縮的文件了,不用在特意安裝針對js的壓縮插件了。

          我們前面一直在運行ngx webpack命令時,命令行工具中老是有一段黃色的警告其實就是因為沒有設置它而產生的。

          二、添加樣式使用loader

          loader 用于對模塊的源代碼進行轉換。loader 可以使你在 import 或"加載"模塊時預處理文件。loader 可以將文件從不同的語言(如 typescript)轉換為 javascript,或將內聯圖像轉換為 data URL。loader 甚至允許你直接在 JavaScript 模塊中 import css文件!

          下面我們要給項目中的頁面添加樣式,這個樣式是使用less寫的,我們不光要使less文件打包成css文件,還要在js模塊中引入css,不在像以前一樣直接在html頁面中link樣式了,因為webpack就是處理這些資源依賴關系的。less文件內容如下:

          @base-color:red;
          body{
           color:@base-color;
          }
          

          處理上面的這些問題,我們需要一系列的loader,首先需要將less轉換成css 需要less,加載less文件又需要less-loader。

          我們在index.js中使用的是 import "../less/index.less"; 來引入的css,所以需要css-loader。index.js內容如下:

          import "../less/index.less";
          console.log('index');
          

          將樣式寫入頁面又需要style-loader。

          所以我們需要安裝less、less-loader、css-loader、style-loader。安裝命令如下:

          cnpm install less less-loader css-loader style-loader --save-dev
          

          然后修改webpack.config.js,內容如下:

          const { CleanWebpackPlugin }=require('clean-webpack-plugin');
          const HtmlWebpackPlugin=require('html-webpack-plugin');
          module.exports={
           mode:"development",
           entry:{
           "common":"./src/js/common.js",
           "index":"./src/js/index.js",
           "login":"./src/js/login.js"
           },
           output:{
           filename:"js/[name].js",
           },
           plugins:[
           new CleanWebpackPlugin(),
           new HtmlWebpackPlugin({
           template:'./src/index.html',
           filename:'index.html',
           chunks:["common","index"],
           hash:true
           }),
           new HtmlWebpackPlugin({
           template:'./src/login.html',
           filename:'login.html',
           chunks:["common","login"],
           hash:true
           })
           ],
           module:{
           rules:[
           {
           test:/\.less$/,
           use:[
           {loader:"style-loader"},
           {loader:"css-loader"},
           {loader:"less-loader"}
           ]
           }
           ]
           }
          }
          

          我們將這些處理的內容寫到module的配置中,下面定義了一個規則,意思就是遇到以.less為結尾的文件名時使用下面的loader來處理。

          然后命令行中運行如下命令:

          npx webpack
          

          我們會發現樣式確實添加到了頁面中,但是這個樣式是以style的方式添加到index.html文件中的,如下圖:

          從上面的過程中我們可以理解,各種loader 就是讓 webpack 能夠去處理那些非 JavaScript 文件(webpack 自身只理解 JavaScript),loader 可以將所有類型的文件轉換為 webpack 能夠處理的有效模塊,然后在利用 webpack 的打包能力,對它們進行處理。

          下一節中我們將了解怎么將怎么將樣式提取出來成為一個樣式文件,并且以link的方式添加到index.html文件中,以及如何處理頁面中的圖片以及樣式中的圖片地址等圖片資源。


          主站蜘蛛池模板: 亚洲韩国精品无码一区二区三区| 99久久精品费精品国产一区二区| 欧美av色香蕉一区二区蜜桃小说| 波多野结衣免费一区视频| 精品国产高清自在线一区二区三区| 亚洲午夜精品一区二区公牛电影院| 久久中文字幕无码一区二区 | 亚洲色偷精品一区二区三区| 中文字幕色AV一区二区三区| 国产伦理一区二区| 国精无码欧精品亚洲一区| 精品国产日产一区二区三区 | 国产av福利一区二区三巨| 99精品一区二区三区| 亚洲国产一区二区三区在线观看| 久久久国产一区二区三区| 国产AV一区二区精品凹凸 | 伊人久久一区二区三区无码 | 亚洲福利一区二区精品秒拍| 亚洲AV无码一区二区三区系列| 丝袜美腿一区二区三区| 爆乳熟妇一区二区三区霸乳| 久久无码AV一区二区三区| 日本精品一区二区三本中文 | 中文字幕在线观看一区| 中文字幕一区二区三区乱码| 内射一区二区精品视频在线观看| 免费视频一区二区| 久久一本一区二区三区| 日韩精品一区二区三区不卡| 无码少妇一区二区性色AV| 国产在线一区视频| 久久4k岛国高清一区二区| 国内精品一区二区三区东京| 精品国产一区二区三区久久| 动漫精品专区一区二区三区不卡| 国产激情无码一区二区| 无码人妻精品一区二区三区99性| 在线观看免费视频一区| 精品无码一区二区三区电影| 国产精品亚洲一区二区三区在线观看|