经验分享 - 如何从 0 到 1 开发一个 ElementUI 官网

发布于 2023-11-26 05:55:03 字数 18348 浏览 32 评论 0

本文参考了 Element UImd-loader 源码,为 Element UI 团队表示深深的敬意。

以下示例是层层递进的,从简单到复杂的顺序进行叙述。另外只有部分代码片段,如果要运行:

  1. 使用 vue-cli3 初始化一个标准工程 vue create demo-md
  2. 新建或者修改按照下面对应的示例文件即可

一、工程需要有解析 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 步:

  1. 将 Markdown 文件通过 vue-loader 转换为一个 Vue Component
  2. 将 Markdown 文件中的代码块,也转换为一个 Vue Component,作为整个页面的子组件

关键点:

  1. 如何写 markdown-it 插件,标记 MD 文件中要运行的代码块?
  2. 如何在插件中解析代码块,变成一个 vue Component ,然后组织内容变成一个整体 vue 组件?

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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

飘然心甜

暂无简介

文章
评论
493 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文