常見loader源碼簡析,以及動手實現一個md2html-loader

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

md2html-loader源碼地址html

loader簡介

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

loader 的配置的兩種形式

方案1:node

// 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(右到左地被調用)webpack

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

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

使用 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,咱們能夠經過returnthis.callback返回輸出的內容github

module.exports = function(content, map, meta) {
  //一些同步操做
  outputContent=someSyncOperation(content)
  return outputContent;
}
複製代碼

若是返回結果只有一個,也能夠直接使用 return 返回結果。可是,若是有些狀況下還須要返回其餘內容,如sourceMap或是AST語法樹,這個時候能夠藉助webpack提供的api this.callbackweb

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,這樣作有助於加速編譯時間。】。npm

異步loader

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

// 讓 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經過正則切割的方法轉成抽象語樹

md-ast

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字符串

md-ast-string

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處理代碼,這樣就比較清晰明瞭了

style-loader
返回一段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輸出

babel-loader-console
上圖咱們能夠看到是輸出 transpile(source, options)的code和map 再來看下 transpile方法作了啥
babel-loader-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-loader-result
當引入一個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 處理

vue-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將執行templateLoaderstyle將執行stylePostLoader
  3. templateLoader中,會經過vue-template-compiler將template轉換爲render函數,在此過程當中, 會將傳入的scopeId追加到每一個標籤的上,最後做爲vnode的配置屬性傳遞給createElemenet方法, 在render函數調用並渲染頁面時,會將scopeId屬性做爲原始屬性渲染到頁面上
  4. stylePostLoader中,經過PostCSS解析style標籤內容

參考文獻

  1. webpack官網loader api
  2. 手把手教你寫webpack yaml-loader
  3. 言川-webpack 源碼解析系列
  4. 從vue-loader源碼分析CSS Scoped的實現
相關文章
相關標籤/搜索