手寫本身的webpack插件『plugin』

上篇文章實現了一個自定義的loader,那確定就有了自定義plugin的實現。javascript

前言

前端不少時候會用到markdown格式轉html標籤的需求,至少我本身有遇到過,就是我第一個博客後臺項目,是用的md格式寫的,寫完存儲在數據庫中,在前臺展現的時候會拉取到md字符串,而後經過md2html這樣的插件轉換成html甚至高亮梅美化事後展現在頁面上,效果仍是不錯的,那麼本身來實現一個這樣的插件有多困難呢,其實否則。css

項目初始化

建立一個工做文件夾,取名就叫md-to-html-plugin,初始化npm倉庫html

mkdir md-to-html-plugin
npm init -y

引入基礎的webpack依賴,這裏我仍然使用4.X版本前端

"devDependencies": {
  "webpack": "^4.30.0",
  "webpack-cli": "^3.3.0",
  "webpack-dev-server": "^3.7.2"
}

安裝依賴java

npm i或者yarn installnode

修改script腳本webpack

"scripts": {
  "dev": "webpack"
}

根目錄下新建webpack.config.js文件,進行簡單配置,並建立對應的測試文件,如:test.mdsrc/app.jsweb

const {resolve} = require('path');
const MdToHtmlPlugin = require('./plugins/md-to-html-plugin');

module.exports = {
  mode: 'development',
  entry: resolve(__dirname, 'src/app.js'),
  output: {
    path: resolve(__dirname, 'dist'),
    filename: 'app.js'
  },
  plugins: [
    new MdToHtmlPlugin({
      // 要解析的文件
      template: resolve(__dirname, 'test.md'),
      // 解析後的文件名
      filename: 'test.html'
    })
  ]
}

test.md正則表達式

# 這是H1標題

- 這是ul列表第1項
- 這是ul列表第2項
- 這是ul列表第3項
- 這是ul列表第4項
- 這是ul列表第5項
- 這是ul列表第6項


## 這是H2標題

1. 這是ol列表第1項
2. 這是ol列表第2項
3. 這是ol列表第3項
4. 這是ol列表第4項
5. 這是ol列表第5項
6. 這是ol列表第6項

根目錄下建立plugins,存放咱們要開發的插件shell

最終的目錄結構以下:

image-20210209135644335

建立MdToHtmlPlugin

class MdToHtmlPlugin {
  constructor({ template, filename }) {

    if (!template) {
      throw new Error('template can not be empty!')
    }

    this.template = template;
    this.filename = filename ? filename : 'md.html';
  }

  /**
   * 編譯過程當中在apply方法中執行邏輯, 裏面會有不少相關的鉤子集合
   */
  apply(compiler) {
    
  }

}

module.exports = MdToHtmlPlugin;

初始化的時候接受webpack.config.js中傳入的options,對應一個要解析的md文件,一個解析後的文件路徑

預解析

編譯過程在apply中執行,咱們在這個方法裏先粗略的把咱們邏輯框架寫出來,大概思路以下:

1. markdown文件
2. template模板 html文件
3. markdown -> html
4. html標籤替換掉template.html的佔位符`<!-- inner -->`
5. webpack打包

解釋下就是:

  1. 把咱們要解析的md文件內容讀取出來
  2. 把插件的模板文件讀取出來
  3. 讀取後的md文件內容確定是字符串,若是後期要逐個解析成html的話,確定是轉成數組而後遍歷解析比較方便,那就先把讀取到的md文件轉成數組,即字符串轉數組
  4. 數組解析成html標籤
  5. 把模板中的佔位區替換成解析後的html內容
  6. 把解析完成的文件動態添加到資源中輸出到打包路徑下
/**
 * 編譯過程當中在apply方法中執行邏輯, 裏面會有不少相關的鉤子集合
 * hooks: emit
 * // 生成資源到 output 目錄以前觸發,這是一個異步串行 AsyncSeriesHook 鉤子
 * // 參數是 compilation
 * @param compiler, 編譯器實例, Compiler暴露了和webpack整個生命週期相關的鉤子
 */
apply(compiler) {
  // 但願在生成的資源輸出到output指定目錄以前執行某個功能
  // 經過tap來掛載一個函數到鉤子實例上, 第一個參數傳插件名字, 第二個參數接收一個回調函數,參數是compilation,compilation暴露了與模塊和依賴有關的粒度更小的事件鉤子
  compiler.hooks.emit.tap('md-to-html-plugin', (compilation) => {
    const _assets = compilation.assets;
    // 讀取資源, webpack配置裏面咱們傳的template(要解析的md文件)
    const _mdContent = readFileSync(this.template, 'utf8');
    // 讀取插件的模板文件html
    const _templateHTML = readFileSync(resolve(__dirname, 'template.html'), 'utf8');
    // 處理預解析的md文件, 將字符串轉爲數組, 而後逐個轉換解析
    const _mdContentArr = _mdContent.split('\n');
    // 數組解析成html標籤
    const _htmlStr = compileHTML(_mdContentArr);

    const _finalHTML = _templateHTML.replace(INNER_MARK, _htmlStr);
    // 增長資源(解析後的html文件)
    _assets[this.filename] = {
      // source函數return的資源將會放在_assets下的this.filename(解析後的文件名)裏面
      source() {
        return _finalHTML;
      },
      // 資源的長度
      size() {
        return _finalHTML.length;
      }
    }
  })
}

查看_assets

image-20210209105003080

讀取md資源

image-20210209110407954

加載插件html模板

image-20210209111304437

解析md文件成數組格式,方便後續對md文件內容逐行解析

image-20210209111839348

添加資源

image-20210209114942679

compileHTML這個核心的方法還沒寫,可是大概的框架已經出來了,這裏就是要重點掌握一下tapable這個事件流

什麼是webpack事件流?

webpack本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable。

Webpack 的 Tapable 事件流機制保證了插件的有序性,將各個插件串聯起來, Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條webapck機制中,去改變webapck的運做,使得整個系統擴展性良好。

Tapable也是一個小型的 library,是Webpack的一個核心工具。相似於node中的events庫,核心原理就是一個訂閱發佈模式。做用是提供相似的插件接口。

webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的實例

Tapable類暴露了tap、tapAsync和tapPromise方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯

compileHTML方法分析

拿到md的內容數組格式後,咱們能夠將其遍歷組裝析成樹形結構化的數據而後解析成咱們想要的html結構,分析完md數據特色,能夠大概轉化成以下的樹形結構:

/**
 * {
 *   h1: {
 *     type: 'single',
 *     tags: [
 *       '<h1>這是h1標題</h1>'
 *     ]
 *   },
 *   ul: {
 *     type: 'wrap',
 *     tags: [
 *       '<li>這是ul列表第1項</li>'
 *       '<li>這是ul列表第2項</li>'
 *       '<li>這是ul列表第3項</li>'
 *       '<li>這是ul列表第4項</li>'
 *       '<li>這是ul列表第5項</li>'
 *       '<li>這是ul列表第6項</li>'
 *     ]
 *   }
 * }
 */

plugins目錄下建立compiler.js文件,裏面暫時只有一個compileHTML方法

function compileHTML(_mdArr) {
  console.log('_mdArr', _mdArr)
}

module.exports = {
  compileHTML
}

編譯樹

匹配H標籤

// 匹配md每行開頭的標符
const reg_mark = /^(.+?)\s/;
// 匹配#字符
const reg_sharp = /^\#/;
function createTree(mdArr) {
  let _htmlPool = {};
  let _lastMark = '';

  mdArr.forEach((mdFragment) => {
    const matched = mdFragment.match(reg_mark);
    /**
     * [ '# ', '#', index: 0, input: '# 這是H1標題', groups: undefined ]
     * 第一項是匹配到的內容,第二項是子表達式,就是正則表達式裏的內容(.+?)
     */
    if (matched) {
      const mark = matched[1];
      const input = matched['input'];

      if (reg_sharp.test(mark)) {
        const tag = `h${mark.length}`;
        const tagContent = input.replace(reg_mark, '');

        if (_lastMark === mark) {
          _htmlPool[tag].tags = [..._htmlPool[tag].tags, `<${tag}>${tagContent}</${tag}>`]
        } else {
          _lastMark = mark;
          _htmlPool[tag] = {
            type: 'single',
            tags: [`<${tag}>${tagContent}</${tag}>`]
          }
        }

      }
    }
  })
  console.log('_htmlPool', _htmlPool)
}

function compileHTML(_mdArr) {
  const _htmlPool = createTree(_mdArr);
}

module.exports = {
  compileHTML
}

打印_htmlPool看看是否正確生成預期的樹結構:

image-20210209153647886

匹配無序列表

// 匹配無序列表
const reg_crossbar = /^\-/;

// 匹配無序列表
if (reg_crossbar.test(mark)) {
  const _key = `ul-${Date.now()}`;
  const tag = 'li';
  const tagContent = input.replace(reg_mark, '');
  // 注意, 這個key必須不能重複
  if (reg_crossbar.test(_lastMark)) {
    _htmlPool[_key].tags = [..._htmlPool[_key].tags, `<${tag}>${tagContent}</${tag}>`];
  } else {
    _lastMark = mark;
    _htmlPool[_key] = {
      type: 'wrap',
      tags: [`<${tag}>${tagContent}</${tag}>`]
    }
  }
}

匹配有序列表

// 匹配有序列表(數字)
const reg_number = /^\d/;

// 匹配有序列表
if (reg_number.test(mark)) {
  const tag = 'li';
  const tagContent = input.replace(reg_mark, '');
  if (reg_number.test(_lastMark)) {
    _htmlPool[`ol-${_key}`].tags = [..._htmlPool[`ol-${_key}`].tags, `<${tag}>${tagContent}</${tag}>`];
  } else {
    _key = randomNum();
    _lastMark = mark;
    _htmlPool[`ol-${_key}`] = {
      type: 'wrap',
      tags: [`<${tag}>${tagContent}</${tag}>`]
    }
  }
}

html字符串拼接

function compileHTML(_mdArr) {
  const _htmlPool = createTree(_mdArr);
  let _htmlStr = '';
  let item;
  for (const k in _htmlPool) {
    item = _htmlPool[k];
    if (item.type === 'single') {
      item.tags.forEach(tag => {
        _htmlStr += tag;
      })
    } else if (item.type === 'wrap') {
      let _list = `<${k.split('-')[0]}>`;
      item.tags.forEach(tag => {
        _list += tag;
      })
      _list += `</${k.split('-')[0]}>`;
      _htmlStr += _list;
    }
  }
  return _htmlStr;
}

預覽

npm run dev後發現dist目錄下生成了app.jstest.html,打開test.html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>這是H1標題</h1><ul><li>這是ul列表第1項</li><li>這是ul列表第2項</li><li>這是ul列表第3項</li><li>這是ul列表第4項</li><li>這是ul列表第5項</li><li>這是ul列表第6項</li></ul><h2>這是H2標題</h2><ol><li>這是ol列表第1項</li><li>這是ol列表第2項</li><li>這是ol列表第3項</li><li>這是ol列表第4項</li><li>這是ol列表第5項</li><li>這是ol列表第6項</li></ol>
</body>
</html>

瀏覽器打開預覽效果:

image-20210209163123095

寫在最後的話

其實實際應用過程當中還能夠針對特定的標籤作css美化,例如微信公衆號的編輯器,能夠看到每一個標籤都會有響應的樣式修飾過,原理不變,js秀到底層就是操做字符串。

相關文章
相關標籤/搜索