美文网首页
用vue-cli3开发一个模仿饿了吗的ui库

用vue-cli3开发一个模仿饿了吗的ui库

作者: 闲余幽梦 | 来源:发表于2020-06-29 23:04 被阅读0次

    初始化项目

    使用vue-cli3初始化项目,初始化目录如下:


    image.png

    将src修改为packages,用于放置组件源文件。新建example目录用于放组件案例,获得最新目录:


    最新目录

    配置vue.config.js

    修改目录后需要修改打包文件中对应的文件名,在vue-cli3中需要新增vue.config.js来扩展打包配置,参考文档: https://cli.vuejs.org/zh/config/
    配置如下:

    module.exports = {
      outputDir: 'dist',    // 输出目录(打包后的文件夹)
      publicPath: './',
      pages: {
        index: {
          entry: 'example/main.js',     // 入口文件(开发和生产中案例的入口文件) 
          template: 'public/index.html',
          filename: 'index.html'
        }
      }
    }
    

    按照以上配置执行npm run build后会直接生成dist目录,且将example中的案例打包,不是组件文件的打包

    配置package.json

    修改package.json文件:
    script中新增lib命令"lib": "vue-cli-service build --target lib --name base-main --dest lib packages/index.js"

             --target:  app | lib | wc | wc-async (默认值:app)
               --name:  打包后的组件名称
               --dest:  指定输出目录 (默认值:dist)
    packages/index.js:  入口js文件
    

    参考文档:https://cli.vuejs.org/zh/guide/cli-service.html#vue-cli-service-build
    运行npm run lib,生成lib文件夹和组件文件base-main.umd.min.js

    lib目录

    上传组件文件到npm

    1. 配置 package.json 文件中发布到 npm 的字段
      name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
      version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
      description: 描述。
      main: 入口文件,该字段需指向我们最终编译后的组件包文件(上面的lib文件夹中的lib/base-main.umd.min.js)。
      keyword:关键字,以空格分离希望用户最终搜索的词。
      author:作者
      private:是否私有,需要修改为 false 才能发布到 npm
      license: 开源协议(可以填自己的github地址)
    2. 添加 .npmignore 文件,设置忽略发布文件
      发布到 npm 中,只有编译后的 lib 目录、package.json、README.md才是需要被发布的。所以我们需要设置忽略目录和文件。和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况。
    3. 登录npm
      npm login

    如果配置了淘宝镜像,先设置回npm镜像:
    npm config set registry http://registry.npmjs.org/

    1. 发布到npm
      npm publish

    2. 更新npm版本包
      使用npm version <update_type>,对npm版本进行更新,版本号的三位分别是大号·中号·小号·预发布号
      update_type可以为以下值:

      1. prerelease:有预发布号的,版本号+1;无预发布号的,小号+1且预发布号初始为0
        运行:npm version prerelease
        package.json 中的版本号1.0.0变为 1.0.1-0
        再运行
        package.json 中的版本号1.0.1-0变为 1.0.1-1
    
    1. prepatch:小号+1;预发布号初始为0
       运行:npm version prepatch
       1. package.json 中的版本号1.0.0变为 1.0.1-0
       2. package.json 中的版本号1.0.1-1变为 1.0.2-0
    
    1. preminor:中号+1;小号和预发布号初始为0
        运行:npm version preminor
        1. package.json 中的版本号1.0.2-0变为 1.1.0-0
        2. package.json 中的版本号1.0.1-1变为 1.1.0-0
    
    1. premajor:大号+1;中号,小号和预发布号初始为0
        运行:npm version premajor
        1. package.json 中的版本号1.1.0-0变为 2.0.0-0
    
    1. patch:有预发布号的去掉预发布号,其他不变;无预发布号的小号+1
        运行:npm version patch
        1. package.json 中的版本号1.1.0-0变为 1.1.0
        2. package.json 中的版本号1.1.0变为 1.1.1
    
    1. minor:有预发布号的,小号为0时去掉预发布号,其他不变,小号不为0时中号+1且其他置为0去掉预发布号;无预发布号的中号+1,小号置为0
        运行:npm version minor
        1. package.json 中的版本号1.1.0变为 1.2.0
        2. package.json 中的版本号1.1.0-0变为 1.1.0
        3. package.json 中的版本号1.1.1-0变为 1.2.0
    
    1. major: 无预发布号的,大号+1其他置为0;有预发布号的,中号和小号为0时去除预发布号,其他不变。如果中号和小号中有一个不为0的话,大号+1,其他重置为0,去除预发布号
    运行:npm version major
        1. package.json 中的版本号1.1.0变为 2.0.0
        2. package.json 中的版本号1.0.0-0变为 1.0.0
        3. package.json 中的版本号1.1.1-0变为 2.0.0
    

    UI文档的编写

    这一块参考的element-ui做法,需要新增外部包:

    highlight.js    // 用于代码的高亮
    transliteration  // 用于中文拼音转换
    markdown-it
    markdown-it-anchor
    markdown-it-container
    vue-markdown-loader
    

    新增demo-block.vue文件

    用于展示案例效果和代码,参考element-ui的demo-block.vue文件做了修改,去除了其他语言,如下:

    <template>
      <div
        class="demo-block"
        :class="[blockClass, { 'hover': hovering }]"
        @mouseenter="hovering = true"
        @mouseleave="hovering = false">
        <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="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="scss">
    .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": "显示代码"
            }
          };
        },
    
        props: {
          jsfiddle: Object,
          default() {
            return {};
          }
        },
    
        methods: {
          scrollHandler() {
            const { top, bottom, left, width } = 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';
            this.$refs.control.style.width = this.fixedControl ? `${ width }px` : 'auto';
          },
    
          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('#ex-r-area');
              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>
    

    配置vue.config.js

    修改vue.config.js文件,新增chainWebpack属性,用于文档文件的转换。如下:

    chainWebpack: config => {
        // 设置文件夹别名
        config.resolve.alias
          .set('@', resolve('example'))
          .set('~', resolve('packages'))
        config.module
          .rule('js')
          .include
            .add(__dirname + 'packages')
            .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) => {
              // 对于markdown中的table,
              MarkdownIt.renderer.rules.table_open = function() {
                return '<table class="doctable">';
              };
              // 对于代码块去除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: [
              // 标题锚点
              [
                require("markdown-it-anchor"),
                {
                  level: 2, // 添加超链接锚点的最小标题级别, 如: #标题 不会添加锚点
                  slugify: slugify, // 自定义slugify, 我们使用的是将中文转为汉语拼音,最终生成为标题id属性
                  permalink: true, // 开启标题锚点功能
                  permalinkBefore: true // 在标题前创建锚点
                }
              ],
              // :::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"]
            ]
          });
      }
    

    设置文档文件

    在example中新增doc文件夹用于存放md文件,在md文件中使用markdown语法书写文档。其中以:::demo开始对组件进行使用的讲解,在demo后面可以填写组件的中相关的属性和方法的使用,在```html 代码块 ```的代码块中填写组件案例的使用代码,和函数方法。案例:

    :::demo 使用`type`、`plain`、`round`和`circle`属性来定义 Button 的样式。
    
     ```html
    <div class="mb-20">
      <el-button>默认按钮</el-button>
      <el-button type="primary">主要按钮</el-button>
      <el-button type="success">成功按钮</el-button>
      <el-button type="info">信息按钮</el-button>
      <el-button type="warning">警告按钮</el-button>
      <el-button type="danger">危险按钮</el-button>
    </div>
    <script lang="babel">
    export default{
    }
    </script>
    ``` // html的结尾符
    :::
    

    修改example中的路由文件,设置对应案例的对应的路由,如下:

    new Router({
      routes: [
        {
          path: '/ElButton',
          name: 'ElButton',
          text: 'button按钮',
          component: () => import(`@/doc/ElButton.md`)
        }
      ]
    })
    

    预览地址:https://erpang123.github.io/C-UI/CUI/index.html
    参考的相关文章:https://blog.csdn.net/qq_31126175/article/details/100527322
    https://segmentfault.com/a/1190000021140844

    相关文章

      网友评论

          本文标题:用vue-cli3开发一个模仿饿了吗的ui库

          本文链接:https://www.haomeiwen.com/subject/cicbfktx.html