实现使用markdown编写的个人组件库说明文档
前一篇文章实现了按需加载封装个人的组件库功能,有了组件库,当然还要有配套说明文档,这样使者用起来才更方便。打包完成的dist目录是最终可放到服务器中,直接访问到文档的哟。
项目github地址:https://github.com/yuanalina/installAsRequired
在项目中配置打包examples
上篇文章中,执行打包命令会将项目打包至lib下,打包完成的目录结构是适合直接发布为npm包,使用时使用import等引入的。其中并没有html文件入口,所以要有说明文档,直接在浏览器中可访问,还需要重新配置打包。
打包examples相关目录结构及webpack配置
一、package.json增加打包命令"build_example": "node build/build.js examples"
二、在src同级增加examples目录,存放文档相关文件
examples目录中:1、assets目录存放静态资源依赖,2、components存放vue组件,3、docs目录存放.md文件,说明文档,4、main.js会作为打包的入口,在这里引入项目的组件、第三方依赖:element-ui、路由配置等,5、route.js路由配置,6、index.html作为打包的html模版,7、App.vue
三、webpack相关配置
在build目录中增加webpack.prod.examples.conf.js文件,配置打包example。这个文件是vue-cli生成项目中的webpack.prod.conf.js稍作修改,改动部分:
1、增加output出口配置,由于之前在config中将这个值设置成了../lib,这里把值设置为../dist,将examples打包后输出到dist
2、设置打包入口为examples下的main.js
3、设置html模版为./examples/index.html
另外在build/build.js中,需要判断example参数,更改一下output出口路径,如图:
技术实现
编写说明文档,最直观的还是使用markdown编写,看了elementUI的实现方案,决定按elementUI的技术方案去实现。特别说明:本文中有部分实现是copy了elementUI的代码实现的。后面会特别指出
相关依赖安装
npm i highlight -D //安装语法高亮
npm i markdown-it markdown-it-anchor markdown-it-container -D // 安装markdown相关依赖
npm i vue-markdown-loader -D //安装vue-markdown-loader,解析.md文件为.vue文件
webpack相关配置
安装了vue-markdown-loader解析.md文件,在webpack.base.conf.js文件中,需要进行相关的loader配置,这里的配置相关,都是copy的element-ui中的代码。改动部分如下:
一、首先增加strip-tags文件到/build目录中,strip-tags内容如下:
/*!
* strip-tags <https://github.com/jonschlinkert/strip-tags>
*
* Copyright (c) 2015 Jon Schlinkert, contributors.
* Licensed under the MIT license.
*/
'use strict';
var cheerio = require('cheerio');
exports.strip = function(str, tags) {
var $ = cheerio.load(str, {decodeEntities: false});
if (!tags || tags.length === 0) {
return str;
}
tags = !Array.isArray(tags) ? [tags] : tags;
var len = tags.length;
while (len--) {
$(tags[len]).remove();
}
return $.html();
};
exports.fetch = function(str, tag) {
var $ = cheerio.load(str, {decodeEntities: false});
if (!tag) return str;
return $(tag).html();
};
二、webpack.base.conf.js的改动
1、增加引入strip-tags和markdown-it
const striptags = require('./strip-tags')
const md = require('markdown-it')()
2、增加工具函数
const wrap = function(render) {
return function() {
return render.apply(this, arguments)
.replace('<code v-pre class="', '<code class="hljs ')
.replace('<code>', '<code class="hljs">')
}
}
function convert(str) {
str = str.replace(/(&#x)(\w{4});/gi, function($0) {
return String.fromCharCode(parseInt(encodeURIComponent($0).replace(/(%26%23x)(\w{4})(%3B)/g, '$2'), 16))
})
return str
}
3、增加.md相关loader配置,将.md文件解析为.vue文件,同时,解析处理::: demo :::代码块等,解析处理::: demo :::代码块为demo-block vue组件,并传入对应参数.
{
test: /\.md$/,
loader: 'vue-markdown-loader',
options: {
use: [
[require('markdown-it-container'), 'demo', {
validate: function(params) {
return params.trim().match(/^demo\s*(.*)$/)
},
render: function(tokens, idx) {
var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
if (tokens[idx].nesting === 1) {
var description = (m && m.length > 1) ? m[1] : ''
var content = tokens[idx + 1].content
var html = convert(striptags.strip(content, ['script', 'style'])).replace(/(<[^>]*)=""(?=.*>)/g, '$1')
var script = striptags.fetch(content, 'script')
var style = striptags.fetch(content, 'style')
var jsfiddle = { html: html, script: script, style: style }
var descriptionHTML = description
? md.render(description)
: ''
jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle))
return `<demo-block class="demo-box" :jsfiddle="${jsfiddle}">
<div class="source" slot="source">${html}</div>
${descriptionHTML}
<div class="highlight" slot="highlight">`
}
return '</div></demo-block>\n'
}
}],
[require('markdown-it-container'), 'tip'],
[require('markdown-it-container'), 'warning']
],
preprocess: function(MarkdownIt, source) {
MarkdownIt.renderer.rules.table_open = function() {
return '<table class="table">';
};
MarkdownIt.renderer.rules.fence = wrap(MarkdownIt.renderer.rules.fence)
return source;
}
}
}
文档编写部分
配置相关的就告一段落了,接下来进入examples中的文档编写部分
一、main.js入口文件编写
在入口文件中,引入相关依赖,项目样式入口、路由、组件以及element-ui
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
// 引入组件
import JY from '../src'
Vue.use(JY)
// 引入element-ui
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI)
// 引入demo-block
import DemoBlock from './components/demoBlock'
Vue.component('demo-block', DemoBlock)
// 引入项目样式入口
import './assets/less/index.less'
// 引入路由
import routes from './route'
Vue.use(VueRouter)
const router = new VueRouter({
routes
})
/* eslint-disable no-new */
new Vue({
render(createElement) {
return createElement(App)
},
router
}).$mount('#app')
二、设置路由配置route.js
路由配置时,将路由路径对应的组件设置为引入的.md文件
import Install from './docs/install.md'
import QuikeStart from './docs/quikeStart.md'
import Input from './docs/input.md'
const routes = [
{
path: '/',
component: Install,
name: 'default'
},
{
path: '/guide/install',
name: 'Install',
component: Install
},
{
path: '/guide/quikeStart',
name: 'quikeStart',
component: QuikeStart
},
{
path: '/input',
name: 'input',
component: Input
}
]
export default routes
三、App.vue、以及相关布局组件
1、App.vue
<template lang="html">
<div style="height:100%">
<el-container style="height:100%">
<el-header height="40">
<header-model></header-model>
</el-header>
<el-container>
<el-aside width="200px">
<menu-model></menu-model>
</el-aside>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<style>
/* 引入代码高亮样式 */
@import 'highlight.js/styles/color-brewer.css';
</style>
<script>
import HeaderModel from './components/header'
import MenuModel from './components/menu'
export default {
components: {
HeaderModel,
MenuModel
},
data() {
return {
}
},
methods: {
}
}
</script>
2、header.vue
<template lang="html">
<div class="header-model">
<h1 class="info">
通用组件库
</h1>
</div>
</template>
<script>
export default {
data () {
return {
}
}
}
</script>
3、menu.vue
<template lang="html">
<div class="menu-model">
<el-menu
default-active="1"
:unique-opened="true"
:default-openeds="['1', '2', '3']"
:default-active="defaultActive"
:router="true"
>
<el-submenu index="1">
<template slot="title">
<span>开发指南</span>
</template>
<el-menu-item-group>
<el-menu-item index="/guide/install">安装</el-menu-item>
<el-menu-item index="/guide/quikeStart">快速上手</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-submenu index="2">
<template slot="title">
<span>通用模块</span>
</template>
<el-menu-item-group>
<el-menu-item index="/input">Input</el-menu-item>
</el-menu-item-group>
</el-submenu>
</el-menu>
</div>
</template>
<script>
export default {
data () {
return {
defaultActive: '/guide/install'
}
},
created () {
const path = this.$route.fullPath
this.defaultActive = path == '/' ? '/guide/install' : path
},
methods: {
}
}
</script>
<style lang="css">
</style>
四、重要组件demoBlock.vue
demoBlock组件是解析.md中的::: demo ::: 代码块需要用到的组件,这里的demoBlock.vue文件是copy的element-ui的代码后稍作修改
<template>
<div
class="demo-block"
:class="[blockClass, { 'hover': hovering }]"
@mouseenter="hovering = true"
@mouseleave="hovering = false">
<slot name="source"></slot>
<div class="meta" ref="meta">
<div class="description" v-if="$slots.default">
<slot></slot>
</div>
<slot name="highlight"></slot>
</div>
<div
class="demo-block-control"
ref="control"
@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>
</div>
</div>
</template>
<style lang="less">
.demo-block {
border: solid 1px #ebebeb;
border-radius: 3px;
transition: .2s;
&.hover {
box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5);
}
code {
font-family: Menlo, Monaco, Consolas, Courier, monospace;
}
.demo-button {
float: right;
}
.source {
padding: 24px;
}
.meta {
background-color: #fafafa;
border-top: solid 1px #eaeefb;
overflow: hidden;
height: 0;
transition: height .2s;
}
.description {
padding: 20px;
box-sizing: border-box;
border: solid 1px #ebebeb;
border-radius: 3px;
font-size: 14px;
line-height: 22px;
color: #666;
word-break: break-word;
margin: 10px;
background-color: #fff;
p {
margin: 0;
line-height: 26px;
}
code {
color: #5e6d82;
background-color: #e6effb;
margin: 0 4px;
display: inline-block;
padding: 1px 5px;
font-size: 12px;
border-radius: 3px;
height: 18px;
line-height: 18px;
}
}
.highlight {
pre {
margin: 0;
}
code.hljs {
margin: 0;
border: none;
max-height: none;
border-radius: 0;
&::before {
content: none;
}
}
}
.demo-block-control {
border-top: solid 1px #eaeefb;
height: 44px;
box-sizing: border-box;
background-color: #fff;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
text-align: center;
margin-top: -1px;
color: #d3dce6;
cursor: pointer;
position: relative;
&.is-fixed {
position: fixed;
bottom: 0;
width: 868px;
}
i {
font-size: 16px;
line-height: 44px;
transition: .3s;
&.hovering {
transform: translateX(-40px);
}
}
> span {
position: absolute;
transform: translateX(-30px);
font-size: 14px;
line-height: 44px;
transition: .3s;
display: inline-block;
}
&:hover {
color: #409EFF;
background-color: #f9fafc;
}
& .text-slide-enter,
& .text-slide-leave-active {
opacity: 0;
transform: translateX(10px);
}
.control-button {
line-height: 26px;
position: absolute;
top: 0;
right: 0;
font-size: 14px;
padding-left: 5px;
padding-right: 25px;
}
}
}
</style>
<script type="text/babel">
export default {
data() {
return {
hovering: false,
isExpanded: false,
fixedControl: false,
scrollParent: null,
langConfig: {
"hide-text": "隐藏代码",
"show-text": "显示代码",
"button-text": "在线运行",
"tooltip-text": "前往 jsfiddle.net 运行此示例"
}
};
},
props: {
jsfiddle: Object,
default() {
return {};
}
},
methods: {
scrollHandler() {
const { top, bottom, left } = this.$refs.meta.getBoundingClientRect();
this.fixedControl = bottom > document.documentElement.clientHeight &&
top + 44 <= document.documentElement.clientHeight;
},
removeScrollHandler() {
this.scrollParent && this.scrollParent.removeEventListener('scroll', this.scrollHandler);
}
},
computed: {
lang() {
return this.$route.path.split('/')[1];
},
blockClass() {
return `demo-${ this.lang } demo-${ this.$router.currentRoute.path.split('/').pop() }`;
},
iconClass() {
return this.isExpanded ? 'el-icon-caret-top' : 'el-icon-caret-bottom';
},
controlText() {
return this.isExpanded ? this.langConfig['hide-text'] : this.langConfig['show-text'];
},
codeArea() {
return this.$el.getElementsByClassName('meta')[0];
},
codeAreaHeight() {
if (this.$el.getElementsByClassName('description').length > 0) {
return this.$el.getElementsByClassName('description')[0].clientHeight +
this.$el.getElementsByClassName('highlight')[0].clientHeight + 20;
}
return this.$el.getElementsByClassName('highlight')[0].clientHeight;
}
},
watch: {
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(() => {
this.scrollParent = document.querySelector('.page-component__scroll > .el-scrollbar__wrap');
this.scrollParent && this.scrollParent.addEventListener('scroll', this.scrollHandler);
this.scrollHandler();
}, 200);
}
},
mounted() {
this.$nextTick(() => {
let highlight = this.$el.getElementsByClassName('highlight')[0];
if (this.$el.getElementsByClassName('description').length === 0) {
highlight.style.width = '100%';
highlight.borderRight = 'none';
}
});
},
beforeDestroy() {
this.removeScrollHandler();
}
};
</script>
五、docs中的.md文档文件
.md文件编写时有几个需要注意的地方:
具有交互功能的说明文档,需要有<script></script>标签,在标签元素中定义需要导出的vue实例。
在:::demo ::: 代码块中定义的模版<template></template>会作为导出的vue实例的模版,但是在代码块中的<script></script>中的内容仅作为展示。
.md文件粘贴进来会展示有误,这里只进行了截图,有需要的伙伴可以进入github查看
六、样式调整
样式相关的调整代码这里就不单独列出来说明了,需要的伙伴可以进入github查看
开发中的调试
设置webpack.dev.conf.js文件的入口为./examples/main.js,这样即可以边开发组件边调试,同时也可以调试到说明文档。
entry: {
app: './examples/main.js'
},
本文结束啦~希望对你有所帮助。。学无止境,与诸君共勉~~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。