乐趣区

关于javascript:手写自己的webpack插件『plugin』

上篇文章实现了一个自定义的 loader,那必定就有了自定义plugin 的实现。

前言

前端很多时候会用到 markdown 格局转 html 标签的需要,至多我本人有遇到过,就是我第一个博客后盾我的项目,是用的 md 格局写的,写完存储在数据库中,在前台展现的时候会拉取到 md 字符串,而后通过 md2html 这样的插件转换成 html 甚至高亮梅丑化过后展现在页面上,成果还是不错的,那么本人来实现一个这样的插件有多艰难呢,其实不然。

我的项目初始化

创立一个工作文件夹,取名就叫 md-to-html-plugin,初始化npm 仓库

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"
}

装置依赖

npm i或者yarn install

批改 script 脚本

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

根目录下新建 webpack.config.js 文件,进行简略配置,并创立对应的测试文件,如:test.mdsrc/app.js

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,寄存咱们要开发的插件

最终的目录构造如下:

创立 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

读取 md 资源

加载插件 html 模板

解析 md 文件成数组格局,不便后续对 md 文件内容逐行解析

增加资源

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 看看是否正确生成预期的树结构:

匹配无序列表

// 匹配无序列表
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>

浏览器关上预览成果:

写在最初的话

其实理论利用过程中还能够针对特定的标签做 css 丑化,例如微信公众号的编辑器,能够看到每个标签都会有响应的款式润饰过,原理不变,js 秀到底层就是操作字符串。

退出移动版