组件预览和在线运行
# 前言
前面我们已经配置了组件库的基本配置,但是可以看下 ElementUI
的组件展示的官网效果:
我们可以从图中发现有如下功能:
- 显示和隐藏代码
- 能预览组件的效果
- 展示组件的源代码
- 支持codepen在线运行
同时,我们希望增加一键复制
的功能,下面我们来一一看下这个功能是如何实现的。
# 显示和隐藏代码效果
<template>
<div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
:class="{ 'is-fixed': fixedControl }"
:style="{width:width+'px'}"
@click="isExpanded = !isExpanded">
<transition name="arrow-slide">
<i :class="[iconClass, { 'hovering': hovering }]"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
<transition name="text-slide">
<span
class="control-button"
v-show="hovering || isExpanded"
@click.stop="goCodepen"
>{{ langConfig['button-text'] }}</span>
</transition>
</div>
</div>
</template>
<script>
export default {
name: 'DemoBlock',
data () {
return {
isExpanded: false, // 是否展开代码
fixedControl: false, // 是否固定操作区域(复制、展开代码、codepen在线运行)
scrollParent: null, // 滚动的父容器
}
},
methods: {
// this.$refs.meta拿到的是源代码的展示区域
// bottom > document.documentElement.clientHeight => 底部的操作区域滚动到页面底部
// top + 44 <= document.documentElement.clientHeight => 底部的操作区域滚动到页面底部 44px处
// 满足上述两个条件,则fixed操作区域,否则不做处理,主要是用户体验的优化。
scrollHandler () {
if (!this.$refs.meta) {
return
}
const { top, bottom, left } = this.$refs.meta.getBoundingClientRect()
this.fixedControl = bottom > document.documentElement.clientHeight &&
top + 44 <= document.documentElement.clientHeight
this.$refs.control.style.left = this.fixedControl ? `${left}px` : '0'
},
// 移除滚动事件
removeScrollHandler () {
this.scrollParent && this.scrollParent.removeEventListener('scroll', this.scrollHandler)
},
},
computed: {
// 中间三角形的操作的文字
langConfig () {
return {
'hide-text': '隐藏代码',
'show-text': '显示代码',
'button-text': '在线运行',
'tooltip-text': '前往 codepen.io 运行此示例'
}
},
// 三角形的方向
iconClass () {
return this.isExpanded ? 'ml-icon-caret-top' : 'ml-icon-caret-bottom'
},
// 动态渲染操作的提示文字
controlText () {
return this.isExpanded ? this.langConfig['hide-text'] : this.langConfig['show-text']
},
// 获取代码区域的容器
codeArea () {
return this.$el.getElementsByClassName('meta')[0]
},
// 代码块高度 + 20
codeAreaHeight () {
if (this.$el.getElementsByClassName('description').length > 0) {
return this.$el.getElementsByClassName('description')[0].clientHeight + 20
}
}
},
watch: {
// 如果isExpanded为true,表示此时展开了代码
// 重置代码区域的高度 this.codeAreaHeight
isExpanded (val) {
this.codeArea.style.height = val ? `${this.codeAreaHeight + 1}px` : '0'
if (!val) {
this.fixedControl = false
this.$refs.control.style.left = '0'
this.removeScrollHandler()
return
}
// 监听代码区域的滚动事件
setTimeout(() => {
window.addEventListener('scroll', this.scrollHandler)
this.scrollHandler()
}, 200)
}
},
// 移除滚动事件
beforeDestroy () {
this.removeScrollHandler()
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
代码中注释已经注释得很明白了。主要思路就是:
利用
isExpanded
属性,如果isExpanded
为true,则表示展开代码,将代码区域的高度设置为真实的高度。否则,将代码块的高度设置为0。同时,三角形的向上和向下的方向也是由isExpanded
属性控制。页面滚动时做了用户体验的优化,会在满足前提条件的基础上,固定操作区域。
# 预览组件功能
这个功能其实很简单,因为 vuepress 默认就是支持展示 vue组件的,我们只需要使用 插槽
,将渲染的组件放置的合适的位置就行了。
<template>
<div>
<div class="source">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
:class="{ 'is-fixed': fixedControl }"
:style="{width:width+'px'}"
@click="isExpanded = !isExpanded">
<transition name="arrow-slide">
<i :class="[iconClass, { 'hovering': hovering }]"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
<transition name="text-slide">
<span
class="control-button"
v-show="hovering || isExpanded"
@click.stop="goCodepen"
>{{ langConfig['button-text'] }}</span>
</transition>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
如上,增加高亮的代码。前面我们提到,组件定义在.vuepress/components
下,组件的文档说明放置在 .vuepress/components
下,现在我们回到组件的文档说明,我们已经能够预览组件效果了,打开.vuepress/component/Button.md
:
<demo-block>
<button-demo-base slot="source"></button-demo-base>
</demo-block>
2
3
4
5
6
7
这样组件就渲染出来了。
# 展示组件的源代码
vuepress 也提供了 >>>
。用于告诉 vuepress,不需要编译 >>>
后面的组件,而是直接展示组件的源代码。
相当于这样使用,再次以 Button.md
为例:
<demo-block>
<button-demo-base slot="source"></button-demo-base>
<<< @/src/docs/.vuepress/components/button/demo-base.vue
</demo-block>
2
3
4
5
6
7
8
其实也就是展示在 ref="meta"
的区域:
<template>
<div>
<div class="source">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
:class="{ 'is-fixed': fixedControl }"
:style="{width:width+'px'}"
@click="isExpanded = !isExpanded">
<transition name="arrow-slide">
<i :class="[iconClass, { 'hovering': hovering }]"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
<transition name="text-slide">
<span
class="control-button"
v-show="hovering || isExpanded"
@click.stop="goCodepen"
>{{ langConfig['button-text'] }}</span>
</transition>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 一键复制功能
<template>
<div>
<div class="source">
<slot name="source"></slot>
</div>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
</div>
<div
class="demo-block-control"
ref="control"
:class="{ 'is-fixed': fixedControl }"
:style="{width:width+'px'}"
@click="isExpanded = !isExpanded">
<transition name="text-slide">
<span
class="copy-code-button"
v-show="isExpanded || isExpanded"
@click.stop="copyCode"
>复制代码</span>
</transition>
<transition name="arrow-slide">
<i :class="[iconClass, { 'hovering': hovering }]"></i>
</transition>
<transition name="text-slide">
<span v-show="hovering">{{ controlText }}</span>
</transition>
<transition name="text-slide">
<span
class="control-button"
v-show="hovering || isExpanded"
@click.stop="goCodepen"
>{{ langConfig['button-text'] }}</span>
</transition>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
增加如图高亮的代码。然后可以看到点击复制代码时,触发了 copyCode
函数,主要逻辑如下:
<script>
// 复制code
copyCode () {
if (this.code) {
this.copyToClipboard(this.code)
}
},
copyToClipboard (textToCopy) {
if (!textToCopy || textToCopy === '-') {
return
}
const textArea = document.createElement('textarea')
document.body.appendChild(textArea)
textArea.value = textToCopy
textArea.select()
document.execCommand('Copy')
textArea.remove()
this.$message.success('代码已复制到剪切板')
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
可以看到,其实就是触发了 textarea
的复制事件,将 code
复制到剪贴板,但是 code
如何获取呢?我们知道组件的源代码是渲染到 ref="meta"
区域内,所以我们的思路就是利用 this.$slots.default
, 该属性是 Vue 提供用来访问默认插槽分发的内容,从中我们可以拿到组件的源代码。如下:
mounted () {
// stripTemplate(code) 获取 .vue 文件中的template
// stripScript(code) 获取 .vue文件中的script
// stripStyle(code) 获取 .vue文件中的style
this.$nextTick(() => {
this.width = this.$refs.block.clientWidth
// this.$slots.default 拿到默认插槽中渲染内容的VNode,
// VNode 中保存了插槽中渲染内容的字符串
// 思路是:拿到渲染字符串后,使用stripTemplate、stripScript、stripScript三个函数进行正则表达式匹配
const highlight2 = this.$slots.default
if (highlight2 && highlight2[0]) {
let code = ''
let cur = highlight2[2] || {}
if (cur.elm && cur.elm.children[0]) {
code = cur.elm.children[0].innerText
this.code = code
}
if (code) {
this.codepen.html = stripTemplate(code)
this.codepen.script = stripScript(code)
this.codepen.style = stripStyle(code)
}
}
})
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 接入codepen
codepen (opens new window) 是一款前端代码在线的编辑器,你可以将代码保存到codepen上,codepen会提供一个在线链接,点击打开就能预览代码结果。提交代码给codepen有很多方式,我们这里采用codepen提供的接口进行组件代码的提交。
import { stripScript, stripStyle, stripTemplate } from '@/utils'
export default {
methods: {
goCodepen () {
// 获取组件的script、html、style
// 引入vue和mssui的CDN
const { script, html, style } = this.codepen
const resourcesTpl = `
<script src="https://unpkg.com/vue/dist/vue.js"><\/script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/mssui@0.1.13/lib/theme-chalk/index.css">
<script src="https://unpkg.com/mssui@0.1.13/lib/index.js"></script>
<div id="app">
${html}
</div>
`
// 组件挂载
let jsTpl = (script || '').replace(/export default/, 'var Main =').trim()
let cssTpl=`${(style||'')}`
jsTpl = jsTpl
? jsTpl + '\nvar Ctor = Vue.extend(Main)\nnew Ctor().$mount(\'#app\')'
: 'new Vue().$mount(\'#app\')'
// 定义好https://blog.codepen.io/documentation/prefill/接口需要的数据结构
// 使用form表单发起POST请求 向codepen提交代码
const data = {
js: jsTpl,
css: cssTpl,
html: resourcesTpl
}
// see: https://blog.codepen.io/documentation/prefill/
const form = document.createElement('form')
form.method = 'POST'
form.action = 'https://codepen.io/pen/define/'
form.target = '_blank'
form.style.display = 'none'
const input = document.createElement('input')
input.setAttribute('name', 'data')
input.setAttribute('type', 'hidden')
input.setAttribute('value', JSON.stringify(data))
form.appendChild(input)
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
这样我们就实现了 demo-block
所有的功能,最后实现的效果如下:
最后你就能发挥你的想象力和创造力,书写组件的官方文档了。