从零搭建Vue3.0组件之环境搭建

一.组件库初始化

1.monorepo项目初始化

$ yarn global add lerna
$ lerna init
1
2

lerna.json

{
    "packages": [
        "packages/*"
    ],
    "version": "0.0.0",
    "npmClient": "yarn", // 使用yarn管理
    "useWorkspaces": true // 使用workspace,需要配置package.json
}
1
2
3
4
5
6
7
8

package.json

{
    "name": "root",
    "private": true,
    "workspaces": [
        "packages/*"
    ],
    "devDependencies": {
        "lerna": "^3.22.1"
    }
}
1
2
3
4
5
6
7
8
9
10

2.初始化组件

$ lerna create button
$ lerna create icon
1
2
├─button
│  │  package.json
│  │  README.md
│  ├─src
|  ├─  button.vue
│  ├─index.ts # 组件入口
│  └─__tests__ # 测试相关
└─icon
    │  package.json
    │  README.md
    ├─src
    ├─  icon.vue
    ├─index.ts # 组件入口
    └─__tests__ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3.tsconfig生成

yarn add typescript 
npx tsc --init
1
2
{
  "compilerOptions": {
    "target": "ESNext", // 打包的目标语法
    "module": "ESNext", // 模块转化后的格式
    "esModuleInterop": true, // 支持模块转化
    "skipLibCheck": true, // 跳过类库检测
    "forceConsistentCasingInFileNames": true, // 强制区分大小写
    "moduleResolution": "node", // 模块解析方式
    "jsx": "preserve", // 不转化jsx
    "declaration": true, // 生成声明文件
    "sourceMap": true // 生成映射文件
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

解析esModuleInterop属性

import fs from 'fs'; // 编译前
let fs = require('fs');
fs.default // 编译后   fs无default属性,所引引用时会出问题
1
2
3

二.组件初始化

$ yarn add vue@next -W
1

1.编写组件入口及出口

<template>
  <button> 按钮 </button>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "ZButton",
});
</script>
1
2
3
4
5
6
7
8
9
10
<template>
    <div> icon </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
    name:'ZIcon'
})
</script>
1
2
3
4
5
6
7
8
9
10

入口声明对应的install方法

import { App } from 'vue'
import Button from './src/button.vue'

Button.install = (app: App): void => {
    app.component(Button.name, Button);
}
export default Button;
1
2
3
4
5
6
7

默认无法解析.vue文件后缀的文件,增加typings

typings/vue-shim.d.ts

declare module '*.vue' {
    import {App,defineComponent} from 'vue';
    const component: ReturnType<typeof defineComponent> & {
        install(app:App):void
    };;
    export default component
}
1
2
3
4
5
6
7

2.整合所有组件

z-ui/index.ts

import Button from "@z-ui/button";
import Icon from "@z-ui/icon";
import { App } from "vue";
const components = [ // 引入所有组件
    Button,
    Icon
];
const install = (app: App): void => {
    components.forEach(component => {
        app.component(component.name, component);
    })
}
export default {
    install // 导出install方法
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

三.搭建文档

1.搭建文档环境

安装webpack构建工具

yarn add webpack webpack-cli webpack-dev-server vue-loader@next @vue/compiler-sfc -D
yarn add babel-loader @babel/core @babel/preset-env @babel/preset-typescript babel-plugin-module-resolver url-loader file-loader html-webpack-plugin css-loader sass-loader style-loader sass -D
1
2

babel.config.js

module.exports = {
    presets: [
        '@babel/preset-env',
        "@babel/preset-typescript" // 解析ts语法,在采用preset-env
    ],
    overrides: [{
        test: /\.vue$/,
        plugins: [ // ?
            '@babel/transform-typescript',
        ],
    }],
    env: {
        utils: {
            plugins: [ // ?
                [
                    'babel-plugin-module-resolver', // 为了能正确找到z-ui模块
                    { root: 'z-ui' }
                ]
            ]
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

使用webpack进行文档构建工作

const { VueLoaderPlugin } = require("vue-loader");
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
    mode: 'development',
    devtool:'source-map',
    entry: path.resolve(__dirname, 'main.ts'), // 打包入口
    output: {
        path: path.resolve(__dirname, '../website-dist'), // 出口
        filename: 'bundle.js'
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.vue', '.json'] // 解析文件顺序
    },
    module: {
        rules: [{ // 识别vue
                test: /\.vue$/,
                use: 'vue-loader',
            },
            { // 识别tsx
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                loader: 'babel-loader'
            },
            { // 识别图标...
                test: /\.(svg|otf|ttf|woff|eot|gif|png)$/,
                loader: 'url-loader',
            },
            { // 识别样式
                test: /\.(scss|css)$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }
        ],
    },
    plugins: [
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({ // html插件
            template: path.resolve(__dirname, 'template.html')
        })
    ]
}
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
"scripts": {
    "website-dev": "webpack serve --config ./website/webpack.config.js"
}
1
2
3

配置运行命令:后续可以采用 "website-dev" 运行文档来预览组件效果

import {createApp} from 'vue';
import ZUI from 'z-ui';
import App from './App.vue'
createApp(App).use(ZUI).mount('#app'); // 入口文件中使用组件即可
1
2
3
4

四.组件库打包

1.打包Umd格式组件库

使用webpack打包成umd格式

const path = require('path');
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
    mode: 'production',
    entry: path.resolve(__dirname, '../packages/z-ui/index.ts'),
    output: {
        path: path.resolve(__dirname, '../lib'),
        filename: 'index.js',
        libraryTarget: 'umd',
        library: 'z-ui',
    },
    externals: { // 排除vue打包
        vue: {
            root: 'Vue',
            commonjs: 'vue',
            commonjs2: 'vue',
        },
    },
    module: {
        rules: [{
                test: /\.vue$/,
                use: 'vue-loader',
            },
            {
                test: /\.(ts|js)x?$/,
                exclude: /node_modules/,
                loader: 'babel-loader',
            },
        ]
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
    },
    plugins: [
        new VueLoaderPlugin(),
    ]
}
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

通过组件库入口进行打包

"build": "webpack --config ./build/webpack.config.js"
1

2.打包esModule格式组件库

使用rollup进行打包,安装所需依赖

yarn add rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve rollup-plugin-vue -D
1

全量打包

import typescript from 'rollup-plugin-typescript2';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import path from 'path';
import { getPackagesSync } from '@lerna/project';
import vue from 'rollup-plugin-vue'

const inputs = getPackagesSync().map(pck => pck.name).filter(name => name.includes('@z-ui'));
export default {
    input: path.resolve(__dirname, `../packages/z-ui/index.ts`),
    output: {
        format: 'es',
        file: `lib/index.esm.js`,
    },
    plugins: [
        nodeResolve(),
        vue({
            target: 'browser'
        }),
        typescript({
           exclude: [
               'node_modules',
               'website'
           ]
        })
    ],
    external(id) { // 排除vue本身
        return /^vue/.test(id)
    },
}

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
"build:esm-bundle": "rollup -c ./build/rollup.config.bundle.js",
1

按组件打包

import typescript from 'rollup-plugin-typescript2';
import { nodeResolve } from '@rollup/plugin-node-resolve';
import path from 'path';
import { getPackagesSync } from '@lerna/project';
import vue  from 'rollup-plugin-vue'

const inputs = getPackagesSync().map(pck => pck.name).filter(name => name.includes('@z-ui'));
export default inputs.map(name => {
    const pckName = name.split('@z-ui')[1]
    return {
        input: path.resolve(__dirname, `../packages/${pckName}/index.ts`),
        output: {
            format: 'es',
            file: `lib/${pckName}/index.js`,
        },
        plugins: [
            nodeResolve(),
            vue({
                target: 'browser'
            }),
            typescript({
                tsconfigOverride: {
                    compilerOptions: { // 打包单个组件的时候不生成ts声明文件
                        declaration: false,
                    },
                    exclude: [
                        'node_modules'
                    ],
                }
            })
        ],
        external(id) { // 对vue本身 和 自己写的包 都排除掉不打包
            return /^vue/.test(id) ||  /^@z-ui/.test(id)
        },
    }
})
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

为了能单独使用,增加声明

type IWithInstall<T> = T & { install(app: App): void; } // 携带install方法
const _Button:IWithInstall<typeof Button> = Button;
export default _Button;
1
2
3

五.组件样式处理

1.使用gulp打包scss文件

安装gulp, 打包样式

yarn add gulp gulp-autoprefixer gulp-cssmin gulp-dart-sass gulp-rename -D
1
│ button.scss
│ icon.scss
├─common
│    var.scss # 提供scss变量
├─fonts # 字体
└─mixins
     config.scss # 提供名字
     mixins.scss # 提供mixin方法
  index.scss  # 整合所有scss
1
2
3
4
5
6
7
8
9

config.scss

$namespace:'z'; // 修饰命名空间
$state-prefix: 'is-';// 修饰状态
$modifier-separator:'--'; // 修饰类型的
$element-separator: '__'; // 划分空间分隔符
1
2
3
4

var.scss

@import "../mixins/config.scss";
$--color-primary: #409EFF;
$--color-white: #FFFFFF;
$--color-black: #000000;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
1
2
3
4
5
6
7
8

mixin.scss

@import "../common/var.scss";

// .z-button{}
@mixin b($block) {
    $B: $namespace+'-'+$block;
    .#{$B}{
        @content;
    }
}
// .z-button.is-xxx
@mixin when($state) {
    @at-root {
        &.#{$state-prefix + $state} {
            @content;
        }
    }
}
// &--primary => .z-button--primary
@mixin m($modifier) {
    @at-root {
        #{&+$modifier-separator+$modifier} {
            @content;
        }
    }
}
// &__header  => .z-button__header
@mixin e($element) {
    @at-root {
        #{&+$element-separator+$element} {
            @content;
        }
    }
}
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

2.预览环境中使用SCSS

import {createApp} from 'vue';
import ZUI from 'z-ui';
import App from './App.vue'

import "theme-chalk/src/index.scss"
createApp(App).use(ZUI).mount('#app');
1
2
3
4
5
6

最终使用打包后的css引入即可,这里为了方便调试,不需要每次进行重新打包

六.Icon组件编写

这里我们使用iconfont实现字体图标 z-ui

项目名称 z-ui
项目描述 组件库图标
FontClass/Symbol 前缀 z-icon-
Font Family z-ui-icons

theme-chalk/icon.scss

@import "common/var.scss";
@font-face {
    font-family: "z-ui-icons"; // 不考虑兼容性
    src:url('./fonts/iconfont.woff') format('woff'),
    url('./fonts/iconfont.ttf') format('truetype');
}
[class^="#{$namespace}-icon-"] {
    font-family: "z-ui-icons" !important;
    font-size: 14px;
    display: inline-block;
    font-style: normal;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
@keyframes rotating {
  0% {
    transform: rotateZ(0deg);
  }
  100% {
    transform: rotateZ(360deg);
  }
}
.#{$namespace}-icon-loading {
    animation: rotating 1.5s linear infinite;
}
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
<template>
  <i :class="`z-icon-${name}`"></i>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "ZIcon",
  props: {
    name: {
      type: String,
      default: "",
    }
  },
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

实现对应组件编写。

七.Button组件编写

1.button组件结构

typings/vue-shim.ts 定义组件大小状态

declare type ComponentSize = 'large' | 'medium' | 'small' | 'mini'
1
<template>
  <button
    :class="classs"
    @click="handleClick"
    :disabled="disabled"
  >
    <i v-if="loading" class="z-icon-loading"></i>
    <i v-if="icon && !loading" :class="icon"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from "vue";
export default defineComponent({
  name: "ZButton",
  props: {
    type: {
      type: String as PropType<
        "primary" | "success" | "warning" | "danger" | "info" | "default"
      >,
      default: "primary",
      validator: (val: string) => {
        return [
          "default",
          "primary",
          "success",
          "warning",
          "danger",
          "info",
        ].includes(val);
      },
    },
    size: {
      type: String as PropType<ComponentSize>,
    },
    icon: {
      type: String,
      default: "",
    },
    loading: Boolean,
    disabled: Boolean,
    round: Boolean,
  },

  emits: ["click"],
  setup(props, ctx) {
    const classs = computed(() => [
      "z-button",
      "z-button--" + props.type,
      props.size ? "z-button--" + props.size : "",
      {
        "is-disabled": props.disabled, // 状态全部以 is-开头
        "is-loading": props.loading,
        "is-round": props.round,
      },
    ]);
    const handleClick = (e) => {
      ctx.emit("click", e);
    };
    return {
      classs,
      handleClick,
    };
  },
});
</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
54
55
56
57
58
59
60
61
62
63
64
65
66

2.button样式处理


@include b(button) {
    // BEM规范
    display: inline-block;
    cursor: pointer;
    outline: none;
    border: #fafafa;
    border-radius: 5px;
    user-select: none;
    min-height: 40px;
    line-height: 1;
    vertical-align: middle;
    & [class*="#{$namespace}-icon-"] { // 处理icon 和文字间距
        &+span {
            margin-left: 5px;
        }
    }
    @include when(disabled) { // 针对不同类型处理
        &,
        &:hover,
        &:focus {
            cursor: not-allowed
        }
    }
    @include when(round) {
        border-radius: 20px;
        padding: 12px 23px;
    }
    @include when(loading) {
        pointer-events: none;
    }
    @include m(primary) { //渲染不同类型的button
        @include button-variant($--color-white, $--color-primary, $--color-primary)
    }
    @include m(success) {
        @include button-variant($--color-white, $--color-success, $--color-success)
    }
    @include m(warning) {
        @include button-variant($--color-white, $--color-warning, $--color-warning)
    }
    @include m(danger) {
        @include button-variant($--color-white, $--color-danger, $--color-danger)
    }
    @include m(info) {
        @include button-variant($--color-white, $--color-info, $--color-info)
    }
}
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

提供scss的辅助方法,方便后续使用

@mixin button-variant($color, $background-color, $border-color) {
    color: $color;
    background: $background-color;
    border-color: $border-color;
}
1
2
3
4
5

3.Button组件试用

<template>
  <div>
    <z-button :loading="buttonLoading" @click="buttonClick">珠峰架构</z-button>
    <z-icon name="loading"></z-icon>
  </div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
function useButton(){
    const buttonLoading = ref(true);
    onMounted(()=>{
        setTimeout(() => {
            buttonLoading.value = false;
        }, 2000);
    });
    const buttonClick = () =>{
        alert('点击按钮')
    }
    return {
        buttonLoading,
        buttonClick
    }
}
export default defineComponent({
  setup() {
      return {
          ...useButton()
      }
  },
});
</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

Button-group组件

packages/button-group/index.ts

import { App } from 'vue'
import ButtonGroup from '../button/src/button-group.vue'

ButtonGroup.install = (app: App): void => {
  app.component(ButtonGroup.name, ButtonGroup)
}

type IWithInstall<T> = T & { install(app: App): void; }
const _ButtonGroup:IWithInstall<typeof ButtonGroup> = ButtonGroup
export default _ButtonGroup

1
2
3
4
5
6
7
8
9
10
11
@include b(button-group) {
    // BEM规范
    &>.#{$namespace}-button {
        &:first-child {
            border-top-right-radius: 0;
            border-bottom-right-radius: 0;
        }
        &:last-child {
            border-top-left-radius: 0;
            border-bottom-left-radius: 0;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<z-button-group>
    <z-button type="primary" icon="z-icon-arrow-left-bold">上一页</z-button>
    <z-button type="primary" >下一页<i class="z-icon-arrow-right-bold"></i></z-button>
</z-button-group>
1
2
3
4