參考ElementUI的文檔實現方案,實現本身組件庫的說明文檔

實現使用markdown編寫的我的組件庫說明文檔

前一篇文章實現了按需加載封裝我的的組件庫功能,有了組件庫,固然還要有配套說明文檔,這樣使者用起來才更方便。打包完成的dist目錄是最終可放到服務器中,直接訪問到文檔的喲。
項目github地址:https://github.com/yuanalina/installAsRequiredcss

在項目中配置打包examples

上篇文章中,執行打包命令會將項目打包至lib下,打包完成的目錄結構是適合直接發佈爲npm包,使用時使用import等引入的。其中並無html文件入口,因此要有說明文檔,直接在瀏覽器中可訪問,還須要從新配置打包。html

打包examples相關目錄結構及webpack配置

1、package.json增長打包命令"build_example": "node build/build.js examples"vue

clipboard.png

2、在src同級增長examples目錄,存放文檔相關文件node

examples目錄中:一、assets目錄存放靜態資源依賴,二、components存放vue組件,三、docs目錄存放.md文件,說明文檔,四、main.js會做爲打包的入口,在這裏引入項目的組件、第三方依賴:element-ui、路由配置等,五、route.js路由配置,六、index.html做爲打包的html模版,七、App.vuewebpack

clipboard.png

3、webpack相關配置git

在build目錄中增長webpack.prod.examples.conf.js文件,配置打包example。這個文件是vue-cli生成項目中的webpack.prod.conf.js稍做修改,改動部分:github

一、增長output出口配置,因爲以前在config中將這個值設置成了../lib,這裏把值設置爲../dist,將examples打包後輸出到distweb

clipboard.png

二、設置打包入口爲examples下的main.jsvue-router

clipboard.png

三、設置html模版爲./examples/index.htmlvue-cli

clipboard.png

另外在build/build.js中,須要判斷example參數,更改一下output出口路徑,如圖:

clipboard.png

技術實現

編寫說明文檔,最直觀的仍是使用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中的代碼。改動部分以下:
1、首先增長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();
};

2、webpack.base.conf.js的改動
一、增長引入strip-tags和markdown-it

const striptags = require('./strip-tags')
const md = require('markdown-it')()

二、增長工具函數

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
}

三、增長.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中的文檔編寫部分
1、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')

2、設置路由配置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

3、App.vue、以及相關佈局組件
一、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>

二、header.vue

<template lang="html">
  <div class="header-model">
    <h1 class="info">
      通用組件庫
    </h1>
  </div>
</template>

<script>
export default {
  data () {
    return {
    }
  }
}
</script>

三、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>

4、重要組件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>

5、docs中的.md文檔文件
.md文件編寫時有幾個須要注意的地方:
具備交互功能的說明文檔,須要有<script></script>標籤,在標籤元素中定義須要導出的vue實例。
在:::demo ::: 代碼塊中定義的模版<template></template>會做爲導出的vue實例的模版,可是在代碼塊中的<script></script>中的內容僅做爲展現。

clipboard.png
.md文件粘貼進來會展現有誤,這裏只進行了截圖,有須要的夥伴能夠進入github查看
6、樣式調整
樣式相關的調整代碼這裏就不單獨列出來講明瞭,須要的夥伴能夠進入github查看

開發中的調試

設置webpack.dev.conf.js文件的入口爲./examples/main.js,這樣便可以邊開發組件邊調試,同時也能夠調試到說明文檔。

entry: {
    app: './examples/main.js'
  },

本文結束啦~但願對你有所幫助。。學無止境,與諸君共勉~~

相關文章
相關標籤/搜索