手把手做一个基于vue-cli的组件库(下篇)

基于vue-cli4的ui组件库,上篇:如何做一个初步的组件。下篇:编写说明文档及页面优化。接上篇,开工。
GitHub源码地址:https://github.com/sq-github/sq-ui
GitHub预览地址:https://sq-github.github.io/sq-ui/dist
五、添加markdown说明文本
1、删除app.vue原有的app.vue内容,及其他一些项目创建时引用的不需要的组件。修改后app.vue如下:
<template>
  <div id="app">
    <router-view />
  </div>
</template>

2、因为文档是用markdown写的,需要项目能识别markdown组件。

npm i vue-markdown-loader -D
3、修改vue.config.js 需要能识别md文件。
const path = require('path')
module.exports = {
  // 修改 pages 入口
  pages: {
    index: {
      entry: 'examples/main.js', // 入口
      template: 'public/index.html', // 模板
      filename: 'index.html' // 输出文件
    }
  },
  parallel: false,
  // 扩展 webpack 配置
  chainWebpack: config => {
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
      .set('@', path.resolve('examples'))
      .set('~', path.resolve('packages'))

    // 把 packages 和 examples 加入编译,因为新增的文件默认是不被 webpack 处理的
    config.module
      .rule('js')
      .include.add(/packages/)
      .end()
      .include.add(/examples/)
      .end()
      .use('babel')
      .loader('babel-loader')
      .tap(options => {
        // 修改它的选项...
        return options
      })
    config.module
      .rule('md')
      .test(/\.md/)
      .use('vue-loader')
      .loader('vue-loader')
      .end()
      .use('vue-markdown-loader')
      .loader('vue-markdown-loader/lib/markdown-compiler')
      .options({
        raw: true
      })
  }
}

4、创建文档的目录及文件。

在 examples 目录下创建 docs 文件夹,在docs文件夹下创建 test1.md,文件内容如下

## tip

:::tip
这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。这是一个 tip。
:::

## warning

:::warning
这是一个 warning,**这是一个 warning,**。
这是一个 warning。
这是一个 warning,这是一个 warning,这是一个 warning,这是一个 warning。
:::

## demo

:::demo 这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。这是一个 demo。

```html
<sq-button></sq-button>
```

:::

将 test1.md 添加进路由进行测试

在router/index.js中添加

 {
    path: '/test1',
    name: 'test1',
    component: () => import(/* webpackChunkName: "about" */ '../docs/test1.md')
  }

重新运行测试:如果没有报错,页面能正确显示 test1.md 内的文本,这一步就算成功了。

5、接下来安装其他的markdown插件
mpm i markdown-it markdown-it-container -S
6、再次修改vue.config.js文件
const path = require('path')
const md = require('markdown-it')() // 引入markdown-it
module.exports = {
  // 修改 pages 入口
  pages: {
    index: {
      entry: 'examples/main.js', // 入口
      template: 'public/index.html', // 模板
      filename: 'index.html' // 输出文件
    }
  },
  parallel: false,
  // 扩展 webpack 配置
  chainWebpack: config => {
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
      .set('@', path.resolve('examples'))
      .set('~', path.resolve('packages'))

    // 把 packages 和 examples 加入编译,因为新增的文件默认是不被 webpack 处理的
    config.module
      .rule('js')
      .include.add(/packages/)
      .end()
      .include.add(/examples/)
      .end()
      .use('babel')
      .loader('babel-loader')
      .tap(options => {
        // 修改它的选项...
        return options
      })
    config.module
      .rule('md')
      .test(/\.md/)
      .use('vue-loader')
      .loader('vue-loader')
      .end()
      .use('vue-markdown-loader')
      .loader('vue-markdown-loader/lib/markdown-compiler')
      .options({
        raw: true,
        preventExtract: true, // 这个加载器将自动从html令牌内容中提取脚本和样式标签
        // 定义处理规则
        preprocess: (MarkdownIt, source) => {
          // 对于代码块去除v - pre, 添加高亮样式;
          const defaultRender = md.renderer.rules.fence
          MarkdownIt.renderer.rules.fence = (
            tokens,
            idx,
            options,
            env,
            self
          ) => {
            const token = tokens[idx]
            // 判断该 fence 是否在 :::demo 内
            const prevToken = tokens[idx - 1]
            const isInDemoContainer =
              prevToken &&
              prevToken.nesting === 1 &&
              prevToken.info.trim().match(/^demo\s*(.*)$/)
            if (token.info === 'html' && isInDemoContainer) {
              return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(
                token.content
              )}</code></pre></template>`
            }
            return defaultRender(tokens, idx, options, env, self)
          }
          return source
        },
        use: [
          // :::demo ****
          //
          // :::
          // 匹配:::后面的内容 nesting == 1,说明:::demo 后面有内容
          // m为数组,m[1]表示 ****
          [
            require('markdown-it-container'),
            'demo',
            {
              validate: function(params) {
                return params.trim().match(/^demo\s*(.*)$/)
              },

              render: function(tokens, idx) {
                const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/)
                if (tokens[idx].nesting === 1) {
                  //
                  const description = m && m.length > 1 ? m[1] : '' // 获取正则捕获组中的描述内容,即::: demo xxx中的xxx
                  const content =
                    tokens[idx + 1].type === 'fence'
                      ? tokens[idx + 1].content
                      : ''

                  return `<demo-block>
                  <div slot="source">${content}</div>
                  ${description ? `<div>${md.render(description)}</div>` : ''}
                  `
                }
                return '</demo-block>'
              }
            }
          ],
          [require('markdown-it-container'), 'tip'],
          [require('markdown-it-container'), 'warning']
        ]
      })
  }
}
7、重新运行 会有一个报错 说没有<demo-block>组件,接下来只需要添加这个组件即可。在 examples/components 下添加 DemoBlock.vue,内容如下:
<template>
  <div class="demo-block" :class="blockClass">
    <!-- 源码运行 -->
    <div class="source">
      <slot name="source"></slot>
    </div>
    <!-- 源码 -->
    <div class="meta" ref="meta">
      <!-- 描述 -->
      <div class="description" v-if="$slots.default">
        <slot></slot>
      </div>
      <!-- 源码 -->
      <div class="highlight">
        <slot name="highlight"></slot>
      </div>
    </div>
    <!-- 源码 显示或者隐藏 -->
    <div
      class="demo-block-control"
      ref="control"
      :class="{ 'is-fixed': fixedControl }"
      @click="isExpanded = !isExpanded"
    >
      <transition name="text-slide">
        <span>{{ controlText }}</span>
      </transition>
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      isExpanded: false,
      fixedControl: false,
      scrollParent: null
    }
  },

  computed: {
    lang() {
      return this.$route.path.split('/')[1]
    },

    blockClass() {
      return `demo-${this.lang} demo-${this.$router.currentRoute.path
        .split('/')
        .pop()}`
    },
    controlText() {
      return this.isExpanded ? '隐藏代码' : '显示代码'
    },
    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'
      console.log(this.$el.getElementsByClassName('description').length)
      console.log(this.$el.getElementsByClassName('highlight'))
      console.log(this.codeAreaHeight)
      if (!val) {
        this.fixedControl = false
        this.$refs.control.style.left = '0'
      }
    }
  }
}
</script>
<style lang="scss">
.demo-block {
  width: 60%;
  padding: 8px 16px;
  margin: auto;
  margin-top: 10px;
  border-left: solid 5px#fc297f;
  background-color: #f8d1db;
  border-radius: 3px;
  transition: 0.2s;
  &.hover {
    box-shadow: 0 0 8px 0 rgba(232, 237, 250, 0.6),
      0 2px 4px 0 rgba(232, 237, 250, 0.5);
  }
  .meta {
    margin-top: 10px;
    background-color: #fafafa;
    border-radius: 8px;
    overflow: hidden;
    height: 0;
    transition: height 0.2s;
  }

  .description {
    box-sizing: border-box;
    border-radius: 3px;
    font-size: 14px;
    line-height: 22px;
    color: #666;
    word-break: break-word;
    margin: 10px;
    background-color: #fff;
    p {
      width: 100%;
    }
  }
  .demo-block-control {
    border-top: solid 1px #eaeefb;
    height: 44px;
    box-sizing: border-box;
    background-color: #fff;
    border-radius: 8px;
    text-align: center;
    margin-top: 10px;
    color: #d3dce6;
    cursor: pointer;
    position: relative;

    &.is-fixed {
      position: fixed;
      bottom: 0;
      width: 868px;
    }

    i {
      font-size: 16px;
      line-height: 44px;
      transition: 0.3s;
      &.hovering {
        transform: translateX(-40px);
      }
    }

    > span {
      position: absolute;
      transform: translateX(-30px);
      font-size: 14px;
      line-height: 44px;
      transition: 0.3s;
      display: inline-block;
    }

    &:hover {
      color: #fc297f;
      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>
然后在main.js中引用组件
import DemoBlock from './components/DemoBlock.vue'
Vue.component('DemoBlock', DemoBlock)
8、现在应该能看到demo组件的效果了,后面接下来需要添加tip和warning的样式
添加一个公共scss文件,在assets下新建common.scss文件
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  background-color: #17171d;
  font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
    'Microsoft YaHei', SimSun, sans-serif;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;

  &.is-component {
    overflow: hidden;
  }
}

#app {
  height: 100%;
  &.is-component {
    overflow-y: hidden;
    .main-cnt {
      padding: 0;
      margin-top: 0;
      height: 100%;
      min-height: auto;
    }
    .headerWrapper {
      position: fixed;
      width: 100%;
      left: 0;
      top: 0;
      z-index: 1500;
      .container {
        padding: 0;
      }
    }
  }
}

a {
  color: #409eff;
  text-decoration: none;
}

code {
  padding: 0 4px;
  border: 1px solid #eaeefb;
  border-radius: 4px;
}

button,
input,
select,
textarea {
  font-family: inherit;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
}

.hljs {
  line-height: 20px;
  font-family: Menlo, Monaco, Consolas, Courier, monospace;
  font-size: 12px;
  padding: 10px 24px 18px 24px;
  border: solid 1px #eaeefb;
  border-radius: 4px;
  -webkit-font-smoothing: auto;
}

.main-cnt {
  margin-top: -80px;
  padding: 80px 0 340px;
  box-sizing: border-box;
  min-height: 100%;
}

#app {
  h2 {
    font-size: 28px;
    color: #fc297f;
    margin: 0;
  }
  h3 {
    font-size: 22px;
  }
  h2,
  h3,
  h4,
  h5 {
    width: 60%;
    margin: auto;
    margin-top: 10px;
    font-weight: normal;
    color: #fc297f;
    a {
      display: none;
    }
    &:hover a {
      opacity: 0.4;
    }
  }

  p {
    width: 60%;
    margin: auto;
    padding: 10px;
    font-size: 16px;
    color: #d3aec2;
    line-height: 30px;
  }

  .tip {
    width: 60%;
    margin: auto;
    margin-top: 10px;
    padding: 8px 16px;
    background-color: #ecf8ff;
    border-radius: 4px;
    border-left: #1b9ae4 5px solid;
    p {
      width: 100%;
    }
    code {
      background-color: rgb(255, 255, 255);
      color: #445368;
    }
  }

  .warning {
    width: 60%;
    margin: auto;
    margin-top: 10px;
    padding: 8px 16px;
    background-color: #fff6f7;
    border-radius: 4px;
    border-left: rgb(252, 122, 2) 5px solid;
    p {
      width: 100%;
    }
    code {
      background-color: rgba(255, 255, 255, 0.7);
      color: #445368;
    }
  }
}
@media (max-width: 1140px) {
  .container,
  .page-container {
    width: 100%;
  }
}

@media (max-width: 768px) {
  .container,
  .page-container {
    padding: 0 20px;
  }

  #app.is-component .headerWrapper .container {
    padding: 0 12px;
  }
}
在main.js引入
import './assets/common.scss' // 公共样式
9、现在效果应该都出来了,可以给代码添加高亮,使其更漂亮。
npm i highlight.js -S
再在main.js中添加如下配置,然后代码就能语法高亮了,perfact!
import hljs from 'highlight.js'
import 'highlight.js/styles/monokai-sublime.css'

router.afterEach(() => {
  Vue.nextTick(() => {
    const blocks = document.querySelectorAll('pre code:not(.hljs)')
    Array.prototype.forEach.call(blocks, hljs.highlightBlock)
  })
})
10、最后有个小问题,如果有eslint检查的话,在md文件中添加vue模板文件时会报错,比如下面这种:
:::demo 这里贴出的是源码,刷新可重播。

```html
<template>
  <div>
    <div>测试</div>
  </div>
</template>

<script></script>
<style></style>
```

:::
解决方法是:在跟目录添加一个.eslintignore文件,目录和内容如下:
*.sh
node_modules
lib
coverage
*.md
*.scss
*.woff
*.ttf
aui-web
build
六、现在说明文件格式已经弄好了,类似下面这种效果,最后一步就是将路由和左侧的导航菜单弄好。

 

1、添加路由配置文件routerCon.json
[
  {
    "name": "test",
    "groups": [
      {
        "groupName": "测试组件",
        "list": [
          {
            "path": "/test1",
            "title": "测试1"
          },
          {
            "path": "/test2",
            "title": "测试2"
          }
        ]
      }
    ]
  }
]
2、修改路由的index.js文件,倒数第二行的路由重定向 redirect ,可以自己定义。
// export default router
import Vue from 'vue'
import Router from 'vue-router'

import navConfig from './routerCon'

Vue.use(Router)
const docsRoutefun = navConfig => {
  const route = []
  navConfig.forEach(item => {
    if (item.groups) {
      item.groups.forEach(group => {
        group.list.forEach(nav => {
          route.push({
            path: nav.path,
            name: nav.name,
            component: r =>
              require.ensure([], () => r(require(`@/docs${nav.path}.md`)))
          })
        })
      })
    } else {
      route.push({
        path: item.path,
        name: item.name,
        component: r =>
          require.ensure([], () => r(require(`@/docs${item.path}.md`)))
      })
    }
  })
  return route
}
const docsRoute = docsRoutefun(navConfig)
export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [{ path: '/', redirect: '/test1' }, ...docsRoute]
})
3、添加左侧菜单组件menuCom.vue

 

 

<template>
  <div id="app">
    <div class="main">
      <!-- sidebar -->
      <div class="sidebar">
        <menuCom :data="navsData"></menuCom>
      </div>
      <div class="view page-container">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>
<script>
import menuCom from './components/menuCom'
import navsData from './router/routerCon.json'
export default {
  components: {
    menuCom
  },
  data() {
    return {
      navsData
    }
  }
}
</script>
<style lang="scss">
html,
body {
  margin: 0;
  height: 100%;
  background-color: #17171d;
}
.header {
  top: 0;
  height: 60px;
  background-color: #121217;
}
.footer {
  position: absolute;
  height: 60px;
  background-color: antiquewhite;
  width: 100%;
}
.footer {
  bottom: 0;
}
.main {
  background-color: #17171d;
  position: absolute;
  bottom: 0;
  top: 60px;
  width: 100%;
  overflow: hidden;
}
.sidebar,
.view {
  overflow: auto;
}
.sidebar {
  float: left;
  height: 100%;
  width: 200px;
  padding: 10px 0 10px 0;
  border-right: #000000 3px solid;
}
.view {
  padding: 0 0 80px 0;
  float: left;
  height: calc(100% - 50px);
  width: calc(100% - 203px);
  overflow: auto;
}
</style>
最后:赶紧npm run lib 加 npm publish,引用看看效果吧,别忘了修改发布版本哟。
总算码完了,期间看了一些博文和源码,有些文章不太完整,踩了一些坑。现在自己从头总结,感觉算是尽力在这篇中将详细的步骤和源码贴出来了,主要是想分享交流,互相避坑,如有不足,希望大家交流指正。
如果需要完成开头图片那种效果,页头布局、logo以及其他的组件都放在github的源码里面了。如果觉得还有趣,不妨star一下,十分感谢。
参考项目链接:
https://github.com/xiaolannuoyi/yuan-ui
https://segmentfault.com/a/1190000018310478
https://blog.csdn.net/qq_31126175/article/details/100527322?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522158788190919725247652639%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.57644%2522%257D&request_id=158788190919725247652639&biz_id=0&utm_source=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v25-7
posted @ 2020-05-03 16:23  sq-blog  阅读(857)  评论(0编辑  收藏  举报