上篇文章實現了一個自定義的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 install
node
修改script腳本webpack
"scripts": { "dev": "webpack" }
根目錄下新建webpack.config.js
文件,進行簡單配置,並建立對應的測試文件,如:test.md
、src/app.js
web
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
最終的目錄結構以下:
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打包
解釋下就是:
/** * 編譯過程當中在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本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是Tapable。
Webpack 的 Tapable 事件流機制保證了插件的有序性,將各個插件串聯起來, Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條webapck機制中,去改變webapck的運做,使得整個系統擴展性良好。
Tapable也是一個小型的 library,是Webpack的一個核心工具。相似於node中的events庫,核心原理就是一個訂閱發佈模式。做用是提供相似的插件接口。
webpack中最核心的負責編譯的Compiler和負責建立bundles的Compilation都是Tapable的實例
Tapable類暴露了tap、tapAsync和tapPromise方法,能夠根據鉤子的同步/異步方式來選擇一個函數注入邏輯
拿到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 }
// 匹配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}>`] } } }
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.js
和test.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秀到底層就是操做字符串。