Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
sunmaobin opened this issue Jan 8, 2020 · 0 comments
Open

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

sunmaobin opened this issue Jan 8, 2020 · 0 comments
Assignees
Labels
Projects
Milestone

Comments

@sunmaobin
Copy link
Owner

@sunmaobin sunmaobin commented Jan 8, 2020

本文参考了 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#L46
    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')
                        }
                    ]
                }
            ]
        }
    }
}

(全文完)

@sunmaobin sunmaobin added this to the 2020年 milestone Jan 8, 2020
@sunmaobin sunmaobin self-assigned this Jan 8, 2020
@sunmaobin sunmaobin changed the title 如何从 0 到 1 开发一个 ElementUI 官网 经验分享-如何从 0 到 1 开发一个 ElementUI 官网 Jan 8, 2020
@sunmaobin sunmaobin added Vue and removed JS/NodeJS/ES6+ labels Jan 9, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
前端
Awaiting triage
1 participant
You can’t perform that action at this time.