经验分享 - 如何从 0 到 1 开发一个 ElementUI 官网
本文参考了 Element UI
的 md-loader
源码,为 Element UI
团队表示深深的敬意。
以下示例是层层递进的,从简单到复杂的顺序进行叙述。另外只有部分代码片段,如果要运行:
- 使用
vue-cli3
初始化一个标准工程vue create demo-md
- 新建或者修改按照下面对应的示例文件即可
一、工程需要有解析 Markdown 文件的能力
demo-md-1
演示一个 Vue 工程如何解析一个基本的 Markdown 文件
Demo1.vue
<template> <div class="demo"> <p class="description"> 直接引入 MarkdownIt 插件,调用其函数 md.render 渲染字符串。 </p> <div class="content" v-html="mdResult"></div> </div> </template> <script> import MarkdownIt from 'markdown-it' export default { data () { return { mdResult: '' } }, mounted () { const md = new MarkdownIt() this.mdResult = md.render(` # demo1 ## 二级标题 ### 三级标题 1. 有序列表项 1 1. 有序列表项 2 * 无序列表项 1 * 无序列表项 2 * 无序列表项 3 `) } } </script>
demo-md-2
演示一个 Vue 工程如何加载外部 .md
文件解析并显示到页面上
Demo2.vue
<template> <div class="demo"> <p class="description"> 将外部 MD 文件通过 require 形式引入,必须增加 loader 来处理 </p> <div class="content" v-html="mdResult"></div> </div> </template> <script> export default { data () { return { mdResult: '' } }, mounted () { // OR // import demo2 from './demo2.md' // this.mdResult = demo2 this.mdResult = require('./demo2.md').default } } </script>
demo2.md
# demo2 ## 二级标题 ### 三级标题 1. 有序列表项 1 1. 有序列表项 2 * 无序列表项 1 * 无序列表项 2 * 无序列表项 3
build/md-loader/index.js
const MarkdownIt = require('markdown-it') const md = new MarkdownIt() module.exports = function (source) { return md.render(source) }
vue.config.js
注意: 一定要结合 raw-loader
使用,详细查看文档。
const path = require('path') // vue.config.js module.exports = { // options... configureWebpack: { module: { rules: [ { test: /\.md$/, use: [ // https://webpack.docschina.org/loaders/raw-loader/ 'raw-loader', { loader: path.resolve(__dirname, './build/md-loader/index.js') } ] } ] } } }
demo-md-3
演示如何高亮显示代码块
Demo3.vue
<template> <div class="demo"> <p class="description"> MD 文件中有代码块,需要高亮显示,引入插件 highlight.js </p> <div class="content" v-html="mdResult"></div> </div> </template> <script> export default { data () { return { mdResult: '' } }, mounted () { this.mdResult = require('./demo3.md').default } } </script>
demo3.md
# demo3 这是一个基本的示例说明,使用到了 `highlight.js` 插件 ## 菜单配置说明 ```js // 注释模块 export default [{ id: 1, name: '张三' }]; ```
main.js
import 'highlight.js/styles/tomorrow.css'
build/md-loader/index.js
const hljs = require('highlight.js') // https://highlightjs.org/ const md = require('markdown-it')({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>' } catch (__) {} } return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; } }) module.exports = function (source) { return md.render(source) }
vue.config.js
注意: 一定要结合 raw-loader
使用,详细查看文档。
const path = require('path') // vue.config.js module.exports = { // options... configureWebpack: { module: { rules: [ { test: /\.md$/, use: [ // https://webpack.docschina.org/loaders/raw-loader/ 'raw-loader', { loader: path.resolve(__dirname, './build/md-loader/index.js') } ] } ] } } }
二、工程需要有能独立访问 Markdown 文件页面的能力
demo-md-4
要能让 vue-router
有能力直接跳转显示一个 Markdown 文件,必须将整个 Markdown 文件转换为一个 Vue 独立组件
router/index.js
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/', redirect: 'demo4' }, { path: '/demo4', name: 'demo4', component: () => import('./demo4.md') }, { path: '*', redirect: '/' } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router
demo4.md
# demo4 ## npm 安装 推荐使用 npm 的方式安装,它能更好地和 webpack 打包工具配合使用。 ## 示例演示 ### Button ```html <el-button>默认按钮</el-button> <el-button type="primary">主要按钮</el-button> ```
build/md-loader/index.js
const hljs = require('highlight.js') // https://highlightjs.org/ const md = require('markdown-it')({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>' } catch (__) {} } return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>'; } }) module.exports = function (source) { return ` <template> <div class="content">${md.render(source)}</div> </template> <script> export default { name: 'demo' } </script> ` }
vue.config.js
注意: 不再需要 raw-loader
插件了,因为 .md
文件已经被解析为 vue 组件了,可以通过路由显示,或者作为 components
组件使用
const path = require('path') // vue.config.js module.exports = { // options... configureWebpack: { module: { rules: [ { test: /\.md$/, use: [ // https://github.com/vuejs/vue-loader 'vue-loader', // https://webpack.docschina.org/loaders/raw-loader/ // 'raw-loader', { loader: path.resolve(__dirname, './build/md-loader/index.js') } ] } ] } } }
.eslintignore
由于 .md
文件变成了 Vue 组件,所以 Eslint 默认会对其进行代码检查,但是 MD 里面的写法肯定不如标准的 Vue 规范,所以直接忽略掉。
node_modules *.md
三、工程需要有直接预览显示 Markdown 文件中代码块的能力
demo-md-5
要直接预览显示 Markdown 文件中的代码块,只需要 2 步:
- 将 Markdown 文件通过
vue-loader
转换为一个 Vue Component - 将 Markdown 文件中的代码块,也转换为一个 Vue Component,作为整个页面的子组件
关键点:
- 如何写
markdown-it
插件,标记 MD 文件中要运行的代码块?- 参考作者已有的示例自己摸索: https://github.com/markdown-it/markdown-it-emoji
- 作者打死都不坑写一篇关于插件如何开发的文档(看下这个 ISSUE 能笑死): How to write plugins? markdown-it/markdown-it#10
- 我们这里没有自己写了,使用了一个已有的插件: markdown-it-container
- 如何在插件中解析代码块,变成一个
vue Component
,然后组织内容变成一个整体 vue 组件?- 参考本示例中
build/md-loader
的写法 - 参考了 Vue 源码,其核心代码: https://github.com/vuejs/vue-loader/blob/423b8341ab368c2117931e909e2da9af74503635/lib/loaders/templateLoader.js
- 参考本示例中
demo5.md
注意: 下面示例代码块的写法。
# demo5 ## 示例 1 ::: demo 示例 1 的描述信息 ```html <el-button>默认按钮</el-button> ``` ::: ## 示例 2 ::: demo 示例 2 的描述信息 ```html <template> <el-select v-model="value" placeholder="请选择"> <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select> </template> <script> export default { data() { return { options: [{ value: '选项 1', label: '黄金糕' }, { value: '选项 2', label: '双皮奶' }, { value: '选项 3', label: '蚵仔煎' }, { value: '选项 4', label: '龙须面' }, { value: '选项 5', label: '北京烤鸭' }], value: '' } } } </script> ``` :::
router.js
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/', redirect: 'demo5' }, { path: '/demo5', name: 'demo5', component: () => import('./demo5.md') }, { path: '*', redirect: '/' } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router
main.js
import Vue from 'vue' import App from './App.vue' import router from './router' import 'highlight.js/styles/tomorrow.css' import 'element-ui/lib/theme-chalk/index.css' import ElementUI from 'element-ui' import DemoBlock from './DemoBlock.vue' Vue.config.productionTip = false Vue.use(ElementUI) Vue.component('demo-block', DemoBlock) new Vue({ router, render: h => h(App) }).$mount('#app')
DemoBlock.vue
这个组件在 md-loader
中有所使用,所以先初始化。
<template>
<div>
<section class="description" v-if="$slots.default">
<slot></slot>
</section>
<section class="source">
<slot name="source"></slot>
</section>
</div>
</template>
<script>
export default {
name: 'demo-block'
}
</script>
build/md-loader/index.js
const hljs = require('highlight.js') // https://highlightjs.org/ const mdContainer = require('markdown-it-container') const parser = require('./parser') const md = require('markdown-it')({ highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) { try { return '<pre class="hljs"><code>' + hljs.highlight(lang, str, true).value + '</code></pre>' } catch (__) {} } return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>' } }).use(mdContainer, 'demo', { // https://github.com/markdown-it/markdown-it-container#example 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>` : ''} <!--demo: ${content}:demo--> ` } return '</demo-block>' } }) module.exports = function (source) { return parser(md, source) }
build/md-loader/parser.js
以下内容大部分出自
Element UI
源码
const { stripScript, stripStyle, stripTemplate, genInlineComponentText } = require('./util') module.exports = function (md, source) { const content = md.render(source) const startTag = '<!--demo:' const startTagLen = startTag.length const endTag = ':demo-->' const endTagLen = endTag.length let componenetsString = '' let id = 0 // demo 的 id let output = [] // 输出的内容 let start = 0 // 字符串开始位置 let styles = [] 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) const style = stripStyle(commentContent) styles.push(style) let demoComponentContent = genInlineComponentText(html, script) const demoComponentName = `demo${id}` output.push(`<template slot="source"><${demoComponentName} /></template>`) componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},` // 重新计算下一次的位置 id++ start = commentEnd + endTagLen commentStart = content.indexOf(startTag, start) commentEnd = content.indexOf(endTag, commentStart + startTagLen) } let pageScript = '' if (componenetsString) { pageScript = `<script> export default { name: 'demo', components: { ${componenetsString} } } </script>` } else if (content.indexOf('<script>') === 0) { start = content.indexOf('</script>') + '</script>'.length pageScript = content.slice(0, start) } output.push(content.slice(start)) return ` <template> <div class="content"> ${output.join('')} </div> </template> ${pageScript} ` }
build/md-loader/util.js
以下内容大部分出自
Element UI
源码
const { compileTemplate } = require('@vue/component-compiler-utils') const compiler = require('vue-template-compiler') function stripScript (content) { const result = content.match(/<(script)>([\s\S]+)<\/\1>/) return result && result[2] ? result[2].trim() : '' } function stripStyle (content) { const result = content.match(/<(style)\s*>([\s\S]+)<\/\1>/) return result && result[2] ? result[2].trim() : '' } // 编写例子时不一定有 template。所以采取的方案是剔除其他的内容 function stripTemplate (content) { content = content.trim() if (!content) { return content } return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim() } function pad (source) { return source .split(/\r?\n/) .map(line => ` ${line}`) .join('\n') } function genInlineComponentText (template, script) { // https://github.com/vuejs/vue-loader/blob/423b8341ab368c2117931e909e2da9af74503635/lib/loaders/templateLoader.js const finalOptions = { source: `<div>${template}</div>`, filename: 'inline-component', compiler } const compiled = compileTemplate(finalOptions) // tips if (compiled.tips && compiled.tips.length) { compiled.tips.forEach(tip => { console.warn(tip) }) } // errors if (compiled.errors && compiled.errors.length) { console.error( `\n Error compiling template:\n${pad(compiled.source)}\n ${compiled.errors.map(e => ` - ${e}`).join('\n')} ` ) } let demoComponentContent = ` ${compiled.code} ` script = script.trim() if (script) { script = script.replace(/export\s+default/, 'const democomponentExport =') } else { script = 'const democomponentExport = {}' } demoComponentContent = `(function() { ${demoComponentContent} ${script} return { render, staticRenderFns, ...democomponentExport } })()` return demoComponentContent } module.exports = { stripScript, stripStyle, stripTemplate, genInlineComponentText }
以上代码块中, democomponentExport
这个对象比较重要,如果没有的话,嵌在 .md
文件中的代码片段,如果有 <script>
脚本片段无法解析的。
vue.config.js
const path = require('path') // vue.config.js module.exports = { // options... configureWebpack: { module: { rules: [ { test: /\.md$/, use: [ // https://github.com/vuejs/vue-loader 'vue-loader', // https://webpack.docschina.org/loaders/raw-loader/ // 'raw-loader', { loader: path.resolve(__dirname, './build/md-loader/index.js') } ] } ] } } }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论