項目中遇到了須要單獨加載某個 markdown 文件顯示在頁面中,相似於操做指引的感受,因而找到了 vue-markdown-loader 這個工具,以爲很好用,因而我打算開個專題看一下里面都作了些什麼,有助於對 webpack loader 的理解。javascript
這裏須要先了解 webpack loader 的原理,後面的代碼須要配合 webpack 的官網對照着來看。中文版在這裏。css
首先咱們須要知道 loader 是什麼,那麼 loader 其實就是一個 JavaScript module,導出的是一個函數。loader runner 會調用這個函數,將前一個 loader 執行的結果或者源文件做爲參數傳遞進來。函數中的 this 上下文由 webpack 填充,loader runner 有一些實用的方法能夠容許 loader 改變其觸發方式爲異步,或者獲取 query 參數。html
在這裏摘錄一下 loader 的特性:vue
options
對象進行配置。package.json
常見的 main
屬性,還能夠將普通的 npm 模塊導出爲 loader,作法是在 package.json
裏定義一個 loader
字段。瞭解原理以後,咱們還要再看一篇文檔,如何編寫一個loader。java
若是你想更方便的調試代碼,須要配置一下調試環境,這裏我用的 VSCode 自帶的調試工具。node
在 debug 模式下點擊配置按鈕,就會生成一個 .vscode/launch.json
的文件,裏面的配置改爲以下便可:webpack
{
"type": "node",
"request": "launch",
"name": "啓動程序",
"program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js",
"cwd": "${workspaceFolder}/example"
}
複製代碼
其中 program 設置成 webpack 自己的文件執行的位置,cwd 設置成執行 webpack 所在的根目錄。點擊啓動程序按鈕就能夠斷點調試了,想看啥看啥。git
下面就能夠開始看源碼了。github
首先看下目錄結構:web
.
├── README.md
├── example
│ ├── index.html
│ ├── src
│ │ ├── app.vue
│ │ ├── custom.css
│ │ ├── entry.js
│ │ └── markdown.md
│ └── webpack.config.js
├── index.js
├── lib
│ ├── core.js
│ └── markdown-compiler.js
├── package-lock.json
└── package.json
複製代碼
結構很清晰,東西也不算太多,涉及到的源碼就是 index.js
和 lib
下的兩個 js 文件,example
裏面的是示例。
那咱們就先從 index.js
開始看起吧。
就一句話:
module.exports = require('./lib/core');
複製代碼
這個文件是你引入這個包的入口,這裏直接去找的 ./lib/core
,因而咱們繼續去 ./lib/core
看看。
在聲明階段:
var path = require('path');
var loaderUtils = require('loader-utils');
var markdownCompilerPath = path.resolve(__dirname, 'markdown-compiler.js');
複製代碼
這裏引用了 loader-utils 和同級目錄下的 markdown-compiler.js
在這裏咱們逐行解析 core.js
裏面的代碼,遇到什麼就去查什麼(帶序號的註釋是我本身加的,對應下面的解釋)。
module.exports = function(source) {
// (1)是否可緩存
this.cacheable();
// (2)獲取 options
var options = loaderUtils.getOptions(this) || {};
// (3)爲 Compilation 對象添加 __vueMarkdownOptions__ 屬性
Object.defineProperty(this._compilation, '__vueMarkdownOptions__', {
value: options,
enumerable: false,
configurable: true
})
// (4) 獲取資源文件的路徑
var filePath = this.resourcePath;
// (5) 生成 result
var result =
'module.exports = require(' +
loaderUtils.stringifyRequest(
this,
'!!vue-loader!' +
markdownCompilerPath +
'?raw!' +
filePath +
(this.resourceQuery || '')
) +
');';
console.log(result)
return result;
};
複製代碼
首先能夠看到這個文件最後是導出了一個 result,參數傳進來的是 source,也就是以前 loader 產生過的結果或者是源文件,有個上下文 this。
(1)是否可緩存
this.cacheable();
對應的是是否可緩存。
默認狀況下,loader 的處理結果會被標記爲可緩存。調用這個方法而後傳入
false
,能夠關閉 loader 的緩存。一個可緩存的 loader 在輸入和相關依賴沒有變化時,必須返回相同的結果。這意味着 loader 除了
this.addDependency
裏指定的之外,不該該有其它任何外部依賴。
(2)獲取 options
參考 loader-utils 的文檔
是檢索調用 loader 的 options 的推薦方式。
本文中爲{}
(3)爲 Compilation 對象添加__vueMarkdownOptions__
屬性
(4)獲取資源文件的路徑
獲取到的路徑是
"vue-markdown-loader/example/src/markdown.md"
複製代碼
(5)生成 result
"module.exports = require("!!vue-loader!../../lib/markdown-compiler.js?raw!./markdown.md");"
複製代碼
參數 source
這裏傳進來的 source 就是原始文件:
"# Hello`<span>{{sss}}</span>`> This is test.- How are you?- Fine, Thank you, and you?- I'm fine, too. Thank you.- 🌚```javascriptimport Vue from 'vue'Vue.config.debug = true```<div class="test"> {{ model }} test</div><compo>{{ model }}</compo><div class="abc" @click="show = false"> 啊哈哈哈</div>> All script or style tags in html mark will be extracted.Script will be excuted, and style will be added to document head.> Notice if there is a string instance which contains special word "</script>", it will fetch a SyntaxError.> Due to the complexity to solve it, just don't do that.```html<style scoped> .test { background-color: green; }</style><style scoped> .abc { background-color: yellow; }</style><script> let a=1<2; let b="<-forget it-/script>"; console.log("***This script tag is successfully extracted and excuted.***") module.exports = { components: { compo: { render(h) { return h('div', { style: { background: 'red' } }, this.$slots.default); } } }, data () { return { model: 'abc' } } }</script>jjjjjjjjjjjjjjjjjjjjjj<template> <div></div></template>```<div></div>sadfsfs你們哦哦好啊誰都發生地方上的馮紹峯s> sahhhh<compo>{{ model }}</compo>```html<compo>{{model }}{{model }}{{model }}{{model }}{{ model }}</compo>```<style src="./custom.css"></style>## 引入 style 文件<div class="custom"> 原諒色</div>"
複製代碼
經過 require 裏的參數,咱們知道,這個 vue-markdown-loader loader 首先會加載 .md
文件,而後經過 markdown-compiler.js?raw
來處理該文件,再經過 vue-loader
處理。
因此咱們須要繼續去 markdown-compiler.js
裏一探究竟。
首先咱們看一下引入聲明部分:
var loaderUtils = require('loader-utils');
var hljs = require('highlight.js');
var cheerio = require('cheerio');
var markdown = require('markdown-it');
var Token = require('markdown-it/lib/token');
複製代碼
再看下面的代碼以前,有必要了解一下上面引入的東西
將 markdown 轉換成 html 的本尊。
有兩種使用方式:render函數和傳遞 options
// node.js, "classic" way:
var MarkdownIt = require('markdown-it'),
md = new MarkdownIt();
var result = md.render('# markdown-it rulezz!');
// node.js, the same, but with sugar:
var md = require('markdown-it')();
var result = md.render('# markdown-it rulezz!');
複製代碼
// full options list (defaults)
var md = require('markdown-it')({
html: false, // Enable HTML tags in source
xhtmlOut: false, // Use '/' to close single tags (<br />).
// This is only for full CommonMark compatibility.
breaks: false, // Convert '\n' in paragraphs into <br>
langPrefix: 'language-', // CSS language prefix for fenced blocks. Can be
// useful for external highlighters.
linkify: false, // Autoconvert URL-like text to links
// Enable some language-neutral replacement + quotes beautification
typographer: false,
// Double + single quotes replacement pairs, when typographer enabled,
// and smartquotes on. Could be either a String or an Array.
//
// For example, you can use '«»„「' for Russian, '„「‚‘' for German,
// and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp).
quotes: '「」‘’',
// Highlighter function. Should return escaped HTML,
// or '' if the source string is not changed and should be escaped externaly.
// If result starts with <pre... internal wrapper is skipped.
highlight: function (/*str, lang*/) { return ''; }
});
複製代碼
語法高亮工具
Fast, flexible & lean implementation of core jQuery designed specifically for the server.
接下來,咱們先看幾個聲明的函數:
/** * `<pre></pre>` => `<pre v-pre></pre>` * `<code></code>` => `<code v-pre></code>` * @param {string} str * @return {string} */
var addVuePreviewAttr = function(str) {
return str.replace(/(<pre|<code)/g, '$1 v-pre');
};
複製代碼
這個函數就是查找 html 標籤中全部 <pre
或者 <code
,替換成 <pre v-pre
和 <code v-pre
/** * renderHighlight * @param {string} str * @param {string} lang */
var renderHighlight = function(str, lang) {
if (!(lang && hljs.getLanguage(lang))) {
return '';
}
return hljs.highlight(lang, str, true).value;
};
複製代碼
返回經過 highlight.js 高亮後的數據
/** * html => vue file template * @param {[type]} html [description] * @return {[type]} [description] */
var renderVueTemplate = function(html, wrapper) {
// 用 cheerio 提取參數傳進來的要進行處理的 html
var $ = cheerio.load(html, {
decodeEntities: false, // 是否解碼文檔實體,默認爲 false
lowerCaseAttributeNames: false, // 是否將全部屬性名設置成小寫,會對速度有影響,默認爲 false
lowerCaseTags: false // 是否將全部標籤轉換成小寫
});
// 將原 html 中的 style 和第一個 script 標籤緩存起來
var output = {
style: $.html('style'),
// get only the first script child. Causes issues if multiple script files in page.
script: $.html($('script').first())
};
var result;
$('style').remove();
$('script').remove();
// 生成最後的結果
result =
`<template><${wrapper}>` +
$.html() +
`</${wrapper}></template>\n` +
output.style +
'\n' +
output.script;
return result;
};
複製代碼
這是整個 loader 的核心功能,就是把 html 包裹一層 vue 的語法變成一個 vue 的組件,而後再讓後面的 vue-loader 接收。這裏用到了 cheerio 來作一些簡單的 DOM 操做。
說完了函數聲明,就繼續來看整個處理過程(帶序號的註釋是我本身加的,對應下面的解釋):
module.exports = function(source) {
// (1) 是否可緩存
this.cacheable && this.cacheable();
var parser, preprocess;
// (2)獲取參數,此時把外面傳進來的解析成 Object {raw: true}(來自core.js)
var params = loaderUtils.getOptions(this) || {};
// (3)獲取 __vueMarkdownOptions__(來自core.js)
var vueMarkdownOptions = this._compilation.__vueMarkdownOptions__;
// (4)繼承 vueMarkdownOptions 的原型,賦值給opts
var opts = vueMarkdownOptions ? Object.create(vueMarkdownOptions.__proto__) : {}; // inherit prototype
var preventExtract = false;
// (5)合併全部來源的參數、屬性,彙總給opts
opts = Object.assign(opts, params, vueMarkdownOptions); // assign attributes
// (6)判斷 options 中 preventExtract 是都爲 true
if (opts.preventExtract) {
delete opts.preventExtract;
preventExtract = true;
}
// (7)判斷 options 中 render 的類型
if (typeof opts.render === 'function') {
// (8)若是是 function,parser 就是 opts
parser = opts;
} else {
// (9)若是不是 function,爲 opts 添加一些屬性,以及後面的一系列操做。opts 最後是做爲 option 傳入 markdown-it 中的。
opts = Object.assign(
{
preset: 'default',
html: true,
highlight: renderHighlight,
wrapper: 'section'
},
opts
);
// (10)初始化 插件 plugins
var plugins = opts.use;
// (11)初始化 預處理 preprocess
preprocess = opts.preprocess;
delete opts.use;
delete opts.preprocess;
// (12)在這裏初始化 markdown-it
parser = markdown(opts.preset, opts);
// (13)添加 ruler:從 html token 中提取 script 和 style
//add ruler:extract script and style tags from html token content
!preventExtract &&
parser.core.ruler.push('extract_script_or_style', function replace( state ) {
let tag_reg = new RegExp('<(script|style)(?:[^<]|<)+</\\1>', 'g');
let newTokens = [];
state.tokens
.filter(token => token.type == 'fence' && token.info == 'html')
.forEach(token => {
let tokens = (token.content.match(tag_reg) || []).map(content => {
let t = new Token('html_block', '', 0);
t.content = content;
return t;
});
if (tokens.length > 0) {
newTokens.push.apply(newTokens, tokens);
}
});
state.tokens.push.apply(state.tokens, newTokens);
});
// (14)若是有插件,就應用一下
if (plugins) {
plugins.forEach(function(plugin) {
if (Array.isArray(plugin)) {
parser.use.apply(parser, plugin);
} else {
parser.use(plugin);
}
});
}
}
// (15)覆蓋默認的 parser rules,在 'code' 和 'pre' 標籤上添加 v-pre 屬性
/** * override default parser rules by adding v-pre attribute on 'code' and 'pre' tags * @param {Array<string>} rules rules to override */
function overrideParserRules(rules) {
if (parser && parser.renderer && parser.renderer.rules) {
var parserRules = parser.renderer.rules;
rules.forEach(function(rule) {
if (parserRules && parserRules[rule]) {
var defaultRule = parserRules[rule];
parserRules[rule] = function() {
return addVuePreviewAttr(defaultRule.apply(this, arguments));
};
}
});
}
}
// (16)覆蓋這三種默認規則
overrideParserRules(['code_inline', 'code_block', 'fence']);
// (17)若是有預處理,執行一下
if (preprocess) {
source = preprocess.call(this, parser, source);
}
// (18)將 source 中全部的 @ 替換成 '__at__'
source = source.replace(/@/g, '__at__');
var content = parser.render(source).replace(/__at__/g, '@');
var result = renderVueTemplate(content, opts.wrapper);
if (opts.raw) {
return result;
} else {
return 'module.exports = ' + JSON.stringify(result);
}
};
複製代碼
(6)preventExtract
preventExtract 是 vue-markdown-loader 提供的一個選項
Since
v2.0.0
, this loader will automatically extract script and style tags from html token content (#26). If you do not need, you can set this option
(13)添加 ruler:從 html token 中提取 script 和 style
走到這裏,就須要對 token 有一個認識才行,因此建議先看一下這篇文章。我摘錄一部分:
當你建立了一個
md = require('markdown-it')()
對象以後,就能夠用它來渲染 MD 文檔了,例如:md.render("# I'm H1 ")
。這個渲染過程分爲主要的兩步:
- 將 MD 文檔 Parsing 爲 Tokens。
- 渲染這個 Tokens。
Parsing 的過程是,首先建立一個 Core Parser,這個 Core Parser 包含一系列的缺省 Rules。這些 Rules 將順序執行,每一個 Rule 都在前面的 Tokens 的基礎上,要麼修改原來的 Token,要麼添加新的 Token。這個 Rules 的鏈條被稱爲 Core Chain。
在全部 Tokens 都得到以後,就能夠渲染了。渲染就是把特定 Token 轉變爲特定的 HTML 的過程。
Markdown-It 容許你爲特定的 Token Type 掛載本身的渲染函數,這個函數稱爲 Renderer Rule。Markdown-It 已經定義了幾個 缺省的 Renderer Rules
(14)若是有插件,就應用一下
在當前的解析實例中應用指定的插件。
最終輸出的結果以下:
"<template><section><h1>Hello</h1><p><code v-pre=""><span>{{sss}}</span></code></p><blockquote><p>This is test.</p></blockquote><ul><li>How are you?</li><li>Fine, Thank you, and you?</li><li>I'm fine, too. Thank you.</li><li>🌚</li></ul><pre v-pre=""><code v-pre="" class="language-javascript"><span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>Vue.config.debug = <span class="hljs-literal">true</span></code></pre><div class="test"> {{ model }} test</div><p><compo>{{ model }}</compo></p><div class="abc" @click="show = false"> 啊哈哈哈</div><blockquote><p>All script or style tags in html mark will be extracted.Script will be excuted, and style will be added to document head.Notice if there is a string instance which contains special word "</script>", it will fetch a SyntaxError.Due to the complexity to solve it, just don't do that.</p></blockquote><pre v-pre=""><code v-pre="" class="language-html"><span class="hljs-tag"><<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>></span><span class="css"> <span class="hljs-selector-class">.test</span> { <span class="hljs-attribute">background-color</span>: green; }</span><span class="hljs-tag"></<span class="hljs-name">style</span>></span><span class="hljs-tag"><<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>></span><span class="css"> <span class="hljs-selector-class">.abc</span> { <span class="hljs-attribute">background-color</span>: yellow; }</span><span class="hljs-tag"></<span class="hljs-name">style</span>></span><span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript"> <span class="hljs-keyword">let</span> a=<span class="hljs-number">1</span><<span class="hljs-number">2</span>; <span class="hljs-keyword">let</span> b=<span class="hljs-string">"<-forget it-/script>"</span>; <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"***This script tag is successfully extracted and excuted.***"</span>) <span class="hljs-built_in">module</span>.exports = { <span class="hljs-attr">components</span>: { <span class="hljs-attr">compo</span>: { render(h) { <span class="hljs-keyword">return</span> h(<span class="hljs-string">'div'</span>, { <span class="hljs-attr">style</span>: { <span class="hljs-attr">background</span>: <span class="hljs-string">'red'</span> } }, <span class="hljs-keyword">this</span>.$slots.default); } } }, data () { <span class="hljs-keyword">return</span> { <span class="hljs-attr">model</span>: <span class="hljs-string">'abc'</span> } } }</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>jjjjjjjjjjjjjjjjjjjjjj<span class="hljs-tag"><<span class="hljs-name">template</span>></span> <span class="hljs-tag"><<span class="hljs-name">div</span>></span><span class="hljs-tag"></<span class="hljs-name">div</span>></span><span class="hljs-tag"></<span class="hljs-name">template</span>></span></code></pre><div></div><p>sadfsfs</p><p>你們哦哦好啊誰都發生地方上的馮紹峯s</p><blockquote><p>sahhhh</p></blockquote><p><compo>{{ model }}</compo></p><pre v-pre=""><code v-pre="" class="language-html"><span class="hljs-tag"><<span class="hljs-name">compo</span>></span>{{model }}{{model }}{{model }}{{model }}{{ model }}<span class="hljs-tag"></<span class="hljs-name">compo</span>></span></code></pre><h2>引入 style 文件</h2><div class="custom"> 原諒色</div></section></template><style src="./custom.css"></style><style scoped> .test { background-color: green; }</style><style scoped> .abc { background-color: yellow; }</style><script> let a=1<2; let b="<-forget it-/script>"; console.log("***This script tag is successfully extracted and excuted.***") module.exports = { components: { compo: { render(h) { return h('div', { style: { background: 'red' } }, this.$slots.default); } } }, data () { return { model: 'abc' } } }</script>"
複製代碼
若是中間哪步不太明白,也能夠自行斷點調試。總的思路仍是很清晰的,就是把 .md
文件經過 markdown-it
轉成 html
,中間經過選項設置高亮,而後再包裹上 vue 組件的語法形式便可,後續再應用 vue-loader
作後面的處理。