基于vue3环境搭建

# 简介

# 说明

基于vue3语法,非lerna方式实现;

# 比较

跟v2版本主要的不同点:

  • vue3 js中操作的语法;
  • 组件样式做了封装处理;
  • 测试用例的方式引入不同;
  • 文档库该用vitepress;
  • 文档中的引入组件示范格式不同;

# 组件的划分

elementUi为基准划分为

  • Basic:Icon图标ButtonLayout布局container布局容器...
  • Form: InputRadiocheckboxDatePickerUpload...
  • Data:TableTreePagination...
  • Notice:AlertLoadingMessage...
  • Navigation: TabsDropdownNavMenu...
  • Others:Popover,DialoginifiniteScrollCarousel...

# 初始化项目

vue create rat-ui-vue
1
? Check the features needed for your project:
 (*) Choose Vue version
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 ( ) Router
 ( ) Vuex
 (*) CSS Pre-processors
 ( ) Linter / Formatter
 (*) Unit Testing
 ( ) E2E Testing
1
2
3
4
5
6
7
8
9
10
11
  2.x
> 3.x (Preview)
1
2
> Sass/SCSS (with dart-sass)  
  Sass/SCSS (with node-sass)
  Less
  Stylus
1
2
3
4

为什么选择dart-sass (opens new window)?

? Pick a unit testing solution:
> Mocha + Chai # ui测试需要使用karma
  Jest
1
2
3

# 目录结构配置

│  .browserslistrc # 兼容版本
│  .gitignore
│  babel.config.js # babel的配置文件
│  package-lock.json
│  package.json
│  README.md   
|  examples   # 组件使用案例
├─public
│      favicon.ico
│      index.html 
├─src
│  │  App.vue 
│  │  main.js
│  │  
│  ├─packages # 需要打包的组件
│  │      button
|  |      button-group
│  │      icon
│  │      index.js # 所有组件的入口
│  │       
│  └─styles # 公共样式
│         common
|         mixins
└─tests # 单元测试
    └─unit
          button.spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 编写插件入口

import Icon from './icon';
import Button from './button';
import ButtonGroup from './button-group';
const plugins = [
    Icon,
    Button,
];
const install = (app: any) => {
    plugins.forEach(plugin => app.use(plugin));
}
//全局导出;
export default {
    install
}
//单个导出
export {
    Icon,
    Button,
    ButtonGroup,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'
import App from './App.vue'
import Rat from './packages/index';

createApp(App).use(Rat).mount('#app')
1
2
3
4
5

可以通过插件的方式去引入我们的组件库

# Icon组件

使用iconfont添加图标 (opens new window)

# 代码

<template>
  <svg class="r-icon" aria-hidden="true">
    <use :xlink:href="`#icon-${icon}`" />
  </svg>
</template>
<script>
import "./font";
export default {
  props: {
    icon: String,
  },
  name: "RIcon",
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Icon from './icon.vue'
import '../../style/icon.scss'
// Button组件是可以单独使用
// import {Button} from 'r-ui';
// app.use(Button)

Icon.install = (app) => { // Vue3.0 app  createApp().use().mount()
    app.component(Icon.name,Icon)
}
export default Icon
1
2
3
4
5
6
7
8
9
10

# 样式抽取

$namespace:'r-';

@mixin blockquote($block) {
    $blockName: $namespace + $block !global;// r-xxx
    .#{$blockName} {
        @content;
    }
}
1
2
3
4
5
6
7
8
@import "./common/var.scss";
@import "./mixins/mixins.scss";
@include blockquote(icon) {
    width: 24px;
    height: 24px;
    vertical-align: middle;
}
1
2
3
4
5
6
7

# 效果

image-20210825181402078

# Button组件

# 实现功能规划

  • [x] 按钮的基本用法
  • [x] 按钮加载中状态
  • [x] 图标按钮
  • [x] 按钮组的实现

# 准备备用样式

├─common
│   |-- var.scss  # 基本样式
└─mixins
│   |-- mixins.scss # 混合的方法
│   button.scss
|   button-group.scss
|   icon.scss
1
2
3
4
5
6
7
// 样式变量
$primary: #409EFF;
$success: #67C23A;
$warning: #E6A23C;
$danger: #F56C6C;
$info: #909399;

$primary-active: #3a8ee6;
$success-active: #5daf34;
$warning-active: #cf9236;
$danger-active: #dd6161;
$info-active: #82848a;
$font-size:12px;
$border-radius:4px;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@import './common/_var.scss'; // 全局的样式
@import './mixins/mixins.scss'; // 方法

$color-list:(primary:$primary,
    success:$success,
    info:$info,
    warning:$warning,
    danger:$danger);
$color-active-list:(primary:$primary-active,
    success:$success-active,
    info:$info-active,
    warning:$warning-active,
    danger:$danger-active);

@include blockquote(button) {
    @include status($color-list);
    display: inline-flex;
    font-size: $font-size;
    border-radius: $border-radius;
    padding: 0px 20px;
    border: none;
    outline: none;
    min-width: 80px;
    box-shadow: 2px 2px #ccc;
    color: #fff;
    align-items: center;
    justify-content: center;
    height: 40px;
    line-height: 40px;
    vertical-align: middle;

    @keyframes rotate {
        from {
            transform: rotate(0deg);
        }
        to {
            transform: rotate(360deg);
        }
    }

    .loading {
        animation: rotate 1s linear infinite;
    }

    &:disabled {
        cursor: not-allowed;
    }

    .icon {
        fill: #fff;
        vertical-align: middle;
    }

    &:active:not(:disabled) {
        @include status($color-active-list);
    }
    &.r-button-left {
        .icon {
            order:1
        }
        span{
            order:2
        }
    }
    &.r-button-right {
        .icon {
            order:2
        }
        span{
            order:1
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# 按钮的实现

带图标的按钮可增强辨识度(有文字)或节省空间(无文字)。

要设置为 loading 状态,只要设置loading属性为true即可。

<template>
  <button :class="classs" :disabled="loading">
    <r-icon :icon="icon" v-if="icon && !loading" class="icon"></r-icon>
    <r-icon icon="loading" v-if="loading"  class="icon loading"></r-icon>
    <span v-if="$slots.default">
      <slot></slot>
    </span>
  </button>
</template>
<script>
import { computed } from "vue";
export default {
  props: {
    type: {
      type: String,
      default: "primary",
      validator(type) {
        if (
          type &&
          !["warning", "success", "danger", "info", "primary"].includes(type)
        ) {
          console.log(
            "组件的type类型必须为:" +
              ["warning", "success", "danger", "info", "primary"].join("、")
          );
        }
        return true;
      },
    },
    icon: String,
    loading:{
      type:Boolean,
      default:false
    },
    position:{
       type:String,
       default:'left'
    }
  },
  name: "RButton",
  setup(props, context) {
    // 计算出所有样式
    const classs = computed(() => [
      `r-button`, 
      `r-button-${props.type}`,
      `r-button-${props.position}`
    ]);
    return {
      classs,
    };
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

# 入口导出

/*
 * @Author: samy
 * @email: yessz#foxmail.com
 * @time: 2021-08-20 18:39:41
 * @modAuthor: samy
 * @modTime: 2021-08-20 18:45:11
 * @desc: Button组件
 * Copyright © 2015~2021 BDP FE
 */
import Button from './button.vue'
import '../../style/button.scss'

Button.install = (app) => {
    app.component(Button.name, Button)
}
export default Button

// Button组件是可以单独使用
// import {Button} from 'r-ui';
// app.use(Button)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# ButtonGroup组件

# 逻辑

以按钮组的方式出现,常用于多项类似操作。主要还是拿到实例节点下的孩子判断是否是Buton验证;

<template>
  <div class="r-button-group">
    <slot></slot>
  </div>
</template>
<script>
import { onMounted, getCurrentInstance } from "vue";
export default {
  name: "RButtonGroup",
  setup(props) {
    onMounted(() => {
      let context = getCurrentInstance();
      let ele = context.ctx.$el;
      let children = ele.children;
      for (let i = 0; i < children.length; i++) {
        console.assert(children[i].tagName === "BUTTON", "必须子节点是button");
      }
    });
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 样式

处理左右两边及中间样式;

@import "./common/var.scss"; // 公共样式
@import "./mixins/mixins.scss";
@include blockquote(button-group) {
    display: inline-flex;
    vertical-align: middle;
    button {
        border-radius: 0;
        position: relative;
        box-shadow: none;
        &:not(first-child) {
            margin-left: -1px;
        }
        &:first-child {
            border-top-left-radius: $border-radius;
            border-bottom-left-radius: $border-radius;
        }
        &:last-child {
            border-top-right-radius: $border-radius;
            border-bottom-right-radius: $border-radius;
        }
    }
    button:hover {
        z-index: 1;
    }
    button:focus {
        z-index: 2;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 组件测试

需要测试ui渲染后的结果。需要在浏览器中测试,所有需要使用Karma

# Karma配置

# 安装karma

npm install --save-dev  karma karma-chrome-launcher karma-mocha karma-sourcemap-loader karma-spec-reporter karma-webpack mocha karma-chai
1

# 配置karma文件

karma.conf.js

var webpackConfig = require('@vue/cli-service/webpack.config')

module.exports = function(config) {
  config.set({
    frameworks: ['mocha'],
    files: ['tests/**/*.spec.js'],
    preprocessors: {
      '**/*.spec.js': ['webpack', 'sourcemap']
    },
    autoWatch: true,
    webpack: webpackConfig,
    reporters: ['spec'],
    browsers: ['ChromeHeadless']
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "scripts": {
    "test": "karma start"
  }
}
1
2
3
4
5

# 单元测试

以button为测试示范使用;

import { expect } from 'chai'
import Button from '@/packages/button';
import Icon from '@/packages/icon';
// @ts-ignore
import { createApp } from 'vue/dist/vue.esm-bundler.js';

describe('HelloWorld.vue', () => {
  it('测试插槽显示是否正常', () => {
    const container = document.createElement('div');
    const app = createApp({
      template: `<RButton>hello</RButton>`,
      components: {
        "RButton": Button,
      }
    }, {
      icon: 'edit',
    }).mount(container);
    let html = app.$el.innerHTML
    expect(html).to.match(/hello/)
  });

  it('测试icon是否能够正常显示', () => {
    const container = document.createElement('div');
    const app = createApp({
      ...Button,
    }, {
      icon: 'edit',
    }).use(Icon).mount(container);
    let useEle = app.$el.querySelector('use');
    let href = useEle.getAttribute('xlink:href');
    expect(href).to.eq('#icon-edit');
  });

  it('测试传入loading时 按钮为禁用态', () => {
    const container = document.createElement('div');
    const app = createApp({
      template: `<RButton></RButton>`,
      components: {
        "RButton": Button,
      }
    }, {
      loading: true,
    }).use(Icon).mount(container);
    let useEle = app.$el.querySelector('use');
    let href = useEle.getAttribute('xlink:href');
    let disabeld = app.$el.getAttribute('disabled')
    expect(href).to.eq('#icon-loading');
    expect(disabeld).not.to.eq(null);
  });
  // todo....
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 打包组件【核心】

# 配置打包命令【要点】

构建示范

scripts": {
    "lib": "vue-cli-service build --target lib --name plugin_demo --dest lib src/installComponents.js", //专属打包
 }
 //npm run lib
1
2
3
4
  • --target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。

  • --dest : 输出目录,默认 dist。这里我们改成 lib

  • [entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 全局注册组件的installComponents.js

构建配置

"build": "vue-cli-service build --target lib --name R ./src/packages/index.ts --no-clean && vue-cli-service build  --all --no-clean",
1
const args = process.argv.slice(2);
if (process.env.NODE_ENV == 'production' && args.includes('--all')) {
    const fs = require('fs');
    const path = require('path');
    const getEntries = (dir) => {
        let absPath = path.resolve(dir);
        let files = fs.readdirSync(absPath);
        let entries = {}
        files.forEach(item => {
            let p = path.resolve(absPath, item);
            if (fs.statSync(p).isDirectory()) {
                p = path.resolve(p, 'index.ts')
                entries[item.split('.')[0]] = p
            }
        });
        return entries;
    }
    module.exports = {
        outputDir: 'dist', // 打包出口
        configureWebpack: {
            entry: { // 配置多入口
                ...getEntries('./src/packages')
            },
            output: {
                filename: `lib/[name]/index.js`,
                libraryTarget: 'umd',
                libraryExport: 'default',
                library: ['rat', '[name]']
            },
            externals:{
                vue: {
                    root: 'Vue',
                    commonjs: 'vue',
                    commonjs2: 'vue',
                    amd: 'vue'
                  }
            },
        },
        css: {
            sourceMap: true,
            extract: {
                filename: 'css/[name]/style.css'
            }
        },
        chainWebpack: config => {
            config.optimization.delete('splitChunks')
            config.plugins.delete('copy')
            config.plugins.delete('preload')
            config.plugins.delete('prefetch')
            config.plugins.delete('html')
            config.plugins.delete('hmr')
            config.entryPoints.delete('app')
        },
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 配置运行入口

package.json

"main": "./dist/rat.umd.min.js"
1

# link到全局下【要点】

npm link
1

# 搭建库文档

# 基本配置

# 安装

npm install vitepress -D
1

# 配置scripts

{
    "docs:dev": "vitepress dev docs",
    "docs:build": "vitepress build docs"
}
1
2
3
4

# 初始化docs

增加入口页面index.md

# 配置导航

增加config.js

module.exports = {
    title: 'r-ui', // 设置网站标题
    description: 'ui 库', //描述
    dest: './build', // 设置输出目录
    themeConfig: { //主题配置
        nav: [
            { text: '主页', link: '/' },
            { text: '联系我', link: '/contact/' },
            { text: '我的博客', link: 'https://' },
        ],
        // 为以下路由添加侧边栏
        sidebar: [
            {
                text: 'Button 按钮', // 必要的
                link: '/button/', // 可选的, 标题的跳转链接,应为绝对路径且必须存在
                collapsable: false, // 可选的, 默认值是 true,
                sidebarDepth: 1, // 可选的, 默认值是 1
            },
            {
                text: 'Icon 图标', // 必要的
                link: '/icon/', // 可选的, 标题的跳转链接,应为绝对路径且必须存在
                collapsable: false, // 可选的, 默认值是 true,
                sidebarDepth: 1, // 可选的, 默认值是 1
            },
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 可能碰到的问题

# 提交及发布

# 发布到npm

配置.npmignore配置文件

npm addUser
npm publish
1
2

# 推送到git

添加npm图标 https://badge.fury.io/for/js

git remote add origin 
git push origin master
1
2
上次更新: 2022/04/15, 05:41:28
×