关于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秀到底层就是操作字符串。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理