在平时的工作开发中,若遇到可重复使用的内容,或者代码块过长,经常会把代码封装成组件,减少重复代码,提高开发效率。写的较多的还是业务组件,主要和业务绑定比较深入,最近,尝试写了一下ui组件,模仿element.ui的样式和使用写了一个aiyaya-ui,并且发布在了npm上。
npm网站
总结一下这次ui组件开发过程中遇到的点和经验:

  1. Dialog组件中sync的妙用
    我们需要对一个父组件传进来的visible进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以变更父组件,且在父组件和子组件两侧都没有明显的变更来源。只能通过$emit,主动调起父组件中的事件。
    vue官方推荐以update:myPropName的模式触发事件来解决该问题
    ui子组件代码中

    1
    2
    3
    4
    5
    6
    <div class="ya-dialog__wrapper" 
    style="z-index: 2002"
    v-show="visible"
    @click.self="handleClick">
    <!-- 其他代码 -->
    </div>
    1
    2
    3
    4
    handleClick() {
    // 将false赋予给visible
    this.$emit('update:visible', false);
    }

    然后父组件可以监听那个事件并根据需要更新一个本地的数据 property
    父组件:

    1
    2
    3
    4
    5
     <ya-dialog
    width="60%"
    :visible="show"
    @update:visible="show= $event">
    <ya-dialog>

    为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符,所以.sync其实可以被当作是上述写法的一种语法糖,主要用于能比较便捷地在子组件中实现对props父组件传入变量的值更新。

    1
    2
    3
    4
    <ya-dialog 
    width="60%"
    :visible.sync="visible">
    </ya-dialog>
  2. Dialog组件中的动画效果
    通过vue动画实现对话框的淡入淡出效果

    Vue 提供了 transition 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡。

    • 条件渲染 (使用 v-if)
    • 条件展示 (使用 v-show)
    • 动态组件
    • 组件根节
    1
    2
    3
    <transition name="dialog-transition">
    <!-- content -->
    </transition>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    /* 进入/离开过渡生效时的状态 */
    /* transition中的name + 动画的时刻*/
    .dialog-transition-enter-active, .dialog-transition-leave-active {
    transition: opacity .5s;
    }
    /* 进入/离开过度开始 */
    .dialog-transition-enter, .dialog-transition-leave-to /* .fade-leave-active below version 2.1.8 */ {
    opacity: 0;
    }
  3. 插槽
    插槽是在开发ui组件的过程中必不可少的部分,扩展了组件的用途。

  4. 在组件中实现v-model双向绑定
    在我们使用像input一类的自定义ui组件的时候,需要给input双向绑定一个变量,来实现数据在view层,model层的相互交互。v-model其实是value属性和input事件的语法糖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     <!-- v-model双向绑定 -->
    <input
    class="ya-input__inner"
    :class="{'is-disabled': disabled}"
    :placeholder="placeholder"
    :type="showPassword? (isShowPassword? 'text' : 'password'): type"
    :disabled="disabled"
    :name="name"
    :value="value"
    @input="changeVal"
    />
    1
    2
    3
    4
     // 子组件修改父组件
    changeVal(e) {
    this.$emit('input', e.target.value)
    },

    由此,实现了在子组件上的v-model双向绑定。
    另外,在checkboxradio组件中要实现v-model, 也是类似的原理,checkboxradio组件是input组件的变体

    1
    2
    3
    4
    5
    6
    <input 
    type="checkbox"
    class="ya-checkbox__original"
    :value="label"
    :name="name"
    v-model="checkStatus">
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     checkStatus: {
    get() {
    // label值
    // console.log(this.CheckboxGroup.value)
    return this.value
    },
    set(value) {
    // 将checkStatus值传给父组件
    this.$emit('input', value)
    }
    }

    还有一种写法,后面看vue官网看到的,使用model选项

    允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

model选项

1
2
3
4
model: {
prop: 'label',
event: 'change'
},
1
2
3
4
5
6
<input 
type="checkbox"
class="ya-checkbox__original"
:name="name"
v-bind:label="label"
v-on:change="$emit('change', $event.target.label)">

在父组件中引用子组件:

1
2
3
4
<ya-checkbox  
v-model="checked"
label="测试">
</ya-checkbox>

等同于

1
2
3
4
<ya-checkbox
:label="foo"
@change="val => { foo = val }">
</ya-checkbox>
  1. injectprovide传值
    在平时的业务开发过程中,很少用到inject/provide传值,这种传值方式可以用在子孙组件之间,比较便捷。
    在写-group组件的时候,遇到一个场景,使用radio组件的时候,需要给每个组件都绑定v-model,可以使用radio-group包裹radio,将v-model直接放在radio-group组件之上,这个时候,在使用radio组件的时候就需要判断我的最外层有没有被-group包裹
    group
    1
    2
    3
    4
    5
    6
     provide() {
    return {
    // 将当前组件赋予给CheckboxGroup
    CheckboxGroup: this,
    }
    }
    checkbox
    1
    2
    3
    4
    5
    inject: {
    CheckboxGroup: {
    default: ''
    }
    },

最后,组件开发完成,需要打包成一个组件库,并且发布到npm上,这样子下次别的项目需要用就只需要install然后引入使用了。
修改项目的文件夹,如下图
目录结构
新增vue.config.js配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')
module.exports = {
pages: {
index: {
entry: 'examples/main.js', // 入口文件
template: 'public/index.html', // 模板
filename: 'index.html'
}
},
// 扩展 webpack 配置,使 packages 加入编译
chainWebpack: config => {
config.module
.rule('js')
.include.add(path.resolve(__dirname, 'packages')).end()
.use('babel')
.loader('babel-loader')
.tap(options => {
// 修改它的选项...
return options
})
}
}

packages添加入口文件index.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 导入组件
import Button from './button.vue';
import CheckboxGroup from './checkbox-group.vue';
import Checkbox from './checkbox.vue';
import Dialog from './dialog.vue';
import FormItem from './form-item.vue';
import Form from './form.vue';
import Input from './input.vue';
import RadioGroup from './radio-group.vue';
import Radio from './radio.vue';
import Switch from './switch.vue';
import './fonts/font.scss'

const components = [
Button,
CheckboxGroup,
Checkbox,
Dialog,
FormItem,
Form,
Input,
RadioGroup,
Radio,
Switch
];
// 定义install方法
// Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
const install = (Vue) => {
components.forEach(item => {
// 组件挂载
Vue.component(item.name, item)
})
}
// 判断是否是直接引入文件,如果是,就不用调用 Vue.use()
if (typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}

// 导出的对象必须具有 install,才能被 Vue.use() 方法安装
export default { install }

一切准备就绪,发布到npm,首先要有个npm的账号,然后修改package.json中的脚本命令

1
2
3
4
5
6
7
"name": "aiyaya-ui",
"version": "0.1.0", // 版本号 上传一次改一下
"private": false, // 这个一定要改成false
"main": "dist/aiyaya-ui.umd.min.js",
"author": {
"name": "aiyaya"
},

增加 .npmignore文件,撇除不需要上传的文件

1
2
3
4
5
6
7
8
9
# 忽略目录
examples/
packages/
public/

# 忽略指定文件
vue.config.js
babel.config.js
*.map

最后运行发布命令,就ok啦

1
npm login npm publish