最近打算重寫本身組件庫的官網,在寫展現組件部分的時候遇到了一個問題,全部組件的功能展現都在一個.vue文件裏面寫的話,會很麻煩。若是隻用簡單的md就能夠轉成須要的頁面而且有代碼高亮、demo展現框和頁面樣式,那該多好。vue
module: {
rules: [
//.....
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
},
]
},
複製代碼
目錄 | 大致功能 |
---|---|
index.js | 入口文件 |
config.js | markdown-it的配置文件 |
containers.js | render添加自定義輸出配置 |
fence | 修改fence渲染策略 |
util | 一些處理解析md數據的函數 |
先看看demowebpack
//demo.md
## Table 表格
用於展現多條結構相似的數據,可對數據進行排序、篩選、對比或其餘自定義操做。
### 基礎表格
基礎的表格展現用法。
:::demo 當`el-table`元素中注入`data`對象數組後,在`el-table-column`中用`prop`屬性來對應對象中的鍵名便可填入數據,用`label`屬性來定義表格的列名。可使用`width`屬性來定義列寬。
```html
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>
<script>
export default {
data() {
return {
tableData: [{
date: '2016-05-02',
name: '王小虎',
address: '上海市普陀區金沙江路 1518 弄'
}
}
}
}
</script>
:::
(```)
複製代碼
把.md文件解析出來包成一個.vue。git
代碼展現和實例展現都須要在一個卡片裏面,卡片能夠展開關閉github
一套code兩個用途,一個是做爲示例展現,一個是做爲示例代碼(代碼高亮),也就是隻寫一套代碼就夠了 web
好了,下面就是一步步搞懂element的md-loader是如何作到這些的數組
首先須要安裝這兩個依賴bash
yarn add markdown-it-chain markdown-it-anchor -D
複製代碼
config.js對markdown-it
作配置markdown
//config.js
const Config = require('markdown-it-chain');
const anchorPlugin = require('markdown-it-anchor');//給頁眉添加錨點
const config = new Config();
config
.options.html(true).end()
.plugin('anchor').use(anchorPlugin, [
{
level: 2,
slugify: slugify,
permalink: true,
permalinkBefore: true
}
]).end()
const md = config.toMd();
module.exports = md;
複製代碼
用markdown-it-anchor
給頁眉添加錨點ide
markdown-it-chain
的鏈式配置參考文檔
如今在index.js
裏面引入config.js
const md = require('./config');
module.exports = function(source) {
const content = md.render(source) //獲得來自.md的解析出來的數據,請記住這個content
//....
}
複製代碼
既然是包裝成.vue,那麼最終輸出的確定是和日常見到的如出一轍吧
//大概是這樣
<template>
</template>
<script>
export default {
}
</script>
複製代碼
因而修改index.js
module.exports = function(source) {
const content = md.render(source) //獲得來自.md的解析出來的數據
//....
let script = `
<script>
export default {
name: 'component-doc',
}
</script>`
//script標籤
//輸出這個template和script合併的字符串
return `
<template>
<section class="content element-doc"> //template
</section>
</template>`
${pageScript};
}
複製代碼
如今須要考慮一個問題,一個md裏面有不少相似的demo,最終輸出的確定是一個vue對象,那麼這些demo就必須包裝成一個個組件。
很明顯這裏不能用模板建立組件,因而須要用到渲染函數的形式。
須要用到的插件
名字 | 大致功能 |
---|---|
vue-template-compiler | template轉爲render函數(做爲配置項) |
component-compiler-utils | 編譯Vue單文件組件的工具 |
(須要知道的)vue-loader 會藉助 component-compiler-utils 的工具編譯Vue單文件組件
如今在util.js
裏面引入他們
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
複製代碼
在源碼裏面這個功能在util.js
裏面的genInlineComponentText
函數完成。(這個函數挺複雜和冗長的,只能拆分說明)
function genInlineComponentText(template, script) {}
複製代碼
首先這個函數接受兩個參數 template
和script
,他們由各自對應的處理函數處理的, 數據來源即是一開始就解析出來的content
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
} //只輸出帶有script標籤的
function stripTemplate(content) {
content = content.trim();
if (!content) return content;
return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}//過濾清空掉script和style,輸出天然是template
複製代碼
再來請點開上面table裏面工具的github文檔說明,對應裏面的options配置,反正照着文檔來就行
const options = {
source: `<div>${template}</div>`,
filename: 'inline-component',
compiler // 這個compiler便是vue-template-compiler
}
複製代碼
利用上面引入的compileTemplate
編譯
const compiled = compileTemplate(options)
複製代碼
若是有則拋出編譯過程當中的報錯和警告
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach(tip => {
console.warn(tip);
});
}
// errors
if (compiled.errors && compiled.errors.length) {
console.error(
//.....
);
}
複製代碼
最後拿到編譯後的code
let demoComponentContent = `${compiled.code}`
複製代碼
如今處理script
script = script.trim() //去掉兩邊空格
if (script) {
script = script.replace(/export\s+default/, 'const democomponentExport =')
} else {
script = 'const democomponentExport = {}';
}
複製代碼
這部分是把字符串裏面的export default
替換爲const democomponentExport =
便於後面的解構賦值
最後return出去
demoComponentContent = `(function() {
${demoComponentContent} // 跑了一遍
${script}
return {
render, //下面有說
staticRenderFns, //這裏不須要關注這個
...democomponentExport //解構出上面的對象的屬性
}
})()`
return demoComponentContent;
複製代碼
上面的render
實際上在函數裏面的${demoComponentContent}
跑了一遍的時候就已經賦值了 能夠看看這裏demo編譯出來compiled.code
的結果
var render = function() {
var _vm = this
var _h = _vm.$createElement
var _c = _vm._self._c || _h //能夠把下面return的_c當作是$createElement
return _c(
"div", // 一個 HTML 標籤名
[//.....] // 子節點數組 ,實際上也是由由$createElement構建而成
)
}
var staticRenderFns = []
render._withStripped = true
複製代碼
那麼如今就很明瞭了,從新看回以前的index.js
let script = ` <script> export default {
name: 'component-doc',
components: {
'demo-components':(function() {
var render = function() {
//.....
return {
render,
staticRenderFns,
...democomponentExport
}
})()
}
}
</script>
`
複製代碼
關於render._withStripped = true
若是未定義,則不會被get
所攔截,這意味着在訪問不存在的值後不會拋錯。 這裏使用了@component-compiler-utils
,因此是自動加上的,能夠不用關心。
回顧一下上面作了什麼
markdown-it
解析.md裏面的數據template
和script
,經過插件編譯成render Functioon
,利用他建立組件。如今的問題是一個content
裏面包含着全部的demo的數據,如何分辨並對每一個demo作以上的操做,並且components
裏面的組件名字是不能重名的,如何解決這一點。
首先須要下載依賴
yarn add markdown-it-container -D
複製代碼
直接看源碼
文檔示例
element源碼
const mdContainer = require('markdown-it-container');
module.exports = md => {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s*(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : '';
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
return `<demo-block>
${description ? `<div>${md.render(description)}</div>` : ''}
<!--element-demo: ${content}:element-demo-->
`;
}
return '</demo-block>';
}
})
};
複製代碼
須要瞭解的,下面的是一個塊級容器,用(:::符號包裹),這個插件能夠自定義這個塊級所渲染的東西
::: demo 我是description(1)
*demo*(2)
:::
複製代碼
tokens
是一個數組,裏面是塊級容器裏面全部md代碼的code,按照必定規則分割,例如tokens[idx].type === 'fence'
意味着被```包裹的東西
塊級容器默認返回的是:::
符號包裹的內容,也就是說即便是寫在同一行的我是description
默認是不會在content
裏面的,這裏render
所返回的就是插在content
返回值裏面的,至因而在包裹內容的前仍是後,取決於頁初仍是頁尾,也就是tokens[idx].nesting
是否等於1,這一點在文檔的Examples能夠知道。
因而如今這段代碼的功能就很明顯了,頁初添加<demo-block>
,頁尾添加</ demo-block>
,組成一個 <demo-block />
組件, <demo-block/>
是一個全局註冊的組件,是示例展現用的,後面會提到。
在這個demo-block裏面就是三樣東西
${description} //(1) description即爲demo的開頭說明,請返回demo.md查看
<!--element-demo: ${content}:element-demo--> //(2) 展現組件 前面和後面的即是標記,在index.js會根據這個標記找到這段內容
${content} // (3) 展現的代碼 這個東西會在fence.js文件裏面作渲染覆蓋,修改它的標籤內容
複製代碼
每一個demo都由這三樣東西構成,請記住這個結構。
const startTag = '<!--element-demo:';
const startTagLen = startTag.length;
const endTag = ':element-demo-->';
const endTagLen = endTag.length;
let componenetsString = '';
let id = 0; // demo 的 id
let output = []; // 輸出的內容
let start = 0; // 字符串開始位置
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
while (commentStart !== -1 && commentEnd !== -1) {
output.push(content.slice(start, commentStart));
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
const html = stripTemplate(commentContent);
const script = stripScript(commentContent);
let demoComponentContent = genInlineComponentText(html, script);
const demoComponentName = `element-demo${id}`;
output.push(`<template slot="source"><${demoComponentName} /></template>`);
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;
//這裏start會是前一個標記結尾的地方,常規說來就是前一個demo的代碼展現的開頭
id++;
start = commentEnd + endTagLen;
commentStart = content.indexOf(startTag, start);
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
output.push(content.slice(start)) //相當重要的一步,後面會講
複製代碼
源碼這一段有點多,首先須要搞懂各個變量的做用
demoComponentName
與id
,命名每一個demo組件名字用的,id會在while每一次循環+1,這樣子組件從第一個到最後一個都不會重名。
output
是一個數組,最終會拼接放入輸出vue文件的template
裏面,也就是這一頁的HTML咯。 從上面代碼能夠看到,對output
進行操做的只有三個地方,while
循環開頭會把description
推動去,而後是推入展現組件
字符串,
output.push(`<template slot="source"><${demoComponentName} /></template>`)
複製代碼
這裏面的demoComponentName
會在最終輸出的類vue對象字符串裏面進行局部註冊,也就是最上面提到的。
而後就是循環結束後會再push一次,這一步相當重要,下面是講解
let content = `
Description1 //description
Component1 // 展現組件
componentCode1//展現代碼
Description2
Component2
componentCode2
Description3
Component3
componentCode3
`
複製代碼
content
就是上面這樣的結構,那麼output
實際上就是經歷瞭如下過程
1·第一次循環
output.push(Description1)
output.push(Component1)
2.第二次循環
output.push(componentCode1)
output.push(Description2)
output.push(Component2)
3.第三次循環
output.push(componentCode2)
output.push(Description3)
output.push(Component3)
4.循環結束
也就是說循環結束後,componentCode3
是沒有推入output
的,而componentCode3
包含 </ demo-block>
,這樣子在最後拼接的時候,HTML結構是有問題的。
demoComponentContent
前面有講過是返回的render Function
,componenetsString
的結構相似下面的代碼`componentName1:(renderFn1)(),componentName2:(renderFn2)()`
複製代碼
最終在script
的代碼就是
script = `<script>
export default {
name: 'component-doc',
components: {
component1:(function() {*render1* })(),
component2:(function() {*render2* })(),
component3:(function() {*render3* })(),
}
}
</script>`;
複製代碼
而後index.js
裏就能夠返回這個啦,
return `
<template>
<section class="content element-doc">
${output.join('')}
</section>
</template>
${script}
`;
複製代碼
最後輸出的大體代碼就是
<h3>我是demo1</h3>
<template slot="source"><demo1/></template>
<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(content)}</code></pre></template>
//第三個其實就是展現代碼,在fence.js裏面對其作了修改,以此對應具名插槽。
複製代碼
fence.js
的操做大體就和上面我寫的註釋是同樣的,主要代碼以下
if (token.info === 'html' && isInDemoContainer) {
return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
}
複製代碼
經過覆蓋修改默認輸出,添加<template slot="highlight">
便於在demo-block
分發內容。
<template slot="source"><${demoComponentName} /></template> //展現組件
複製代碼
打開Element的源碼,在example=>components裏面能夠找到demo-block.vue
, 裏面有這樣的代碼
<div class="source">
<slot name="source"></slot>
</div>
複製代碼
在 template 上使用特殊的 slot 特性,能夠將內容從父級傳給具名插槽 . 這裏面description
是默認的插槽,展現代碼也是具名可是由於須要代碼高亮就複雜了一點。
至此,Element的md-loader
的大部分代碼都及功能都看完了,感謝Element團隊貢獻出的源碼,讓我獲益良多。
但願能對你理解源碼有幫助,若是有什麼不對的地方,歡迎批評指正。