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

初始化项目

使用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345