从零搭建Vue3.0组件之环境搭建
一.组件库初始化
1.monorepo项目初始化
$ yarn global add lerna
$ lerna init
1
2
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
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
3
4
5
6
7
8
9
10
2.初始化组件
$ lerna create button
$ lerna create icon
1
2
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
2
3
4
5
6
7
8
9
10
11
12
13
14
3.tsconfig生成
yarn add typescript
npx tsc --init
1
2
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
2
3
4
5
6
7
8
9
config.scss
$namespace:'z'; // 修饰命名空间
$state-prefix: 'is-';// 修饰状态
$modifier-separator:'--'; // 修饰类型的
$element-separator: '__'; // 划分空间分隔符
1
2
3
4
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
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
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
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
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
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
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
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
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
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
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
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
2
3
4