前段时间自己在尝试从0到1实现一个组件库,把一些常见功能总结下,如组件封装、按需加载、文档自动生成等
封装弹窗组件
Vue组件封装常用api
Vue组件的API主要包含三部分:props、slot、event
- props 表示组件接收的参数,最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值,此外还可以通过type、validator等方式对输入进行验证
- slot 可以给组件动态插入一些内容或组件,是实现高阶组件的重要途径;当需要多个插槽时,可以使用具名slot
- event 是子组件向父组件传递消息的重要途径,($emit)
props单向数据流
如果直接改变 props 时会发生一个警告报错,因为Vue传递数据时是单向数据流的:父级 prop 的更新会向下流动到子组件中,但是反过来则不行,这是为了防止从子组件意外变更父级组件的状态。
组件间通信
- 父子组件的关系可以总结为 prop 向下传递,事件event向上传递
- 祖先组件和后代组件(跨多代)的数据传递,可以使用provide和inject来实现
封装一个弹窗组件
蓝湖地址:lanhuapp.com/web/#/item/…
看设计稿分析需要传进来的prop:
- 控制显示与隐藏
- 标题
- 内容
- 底部按钮文案(取消跟确定)
props:{
    // 控制显示隐藏
    visible: {
      type: Boolean,
      default: false
    },
    // 标题
    title: {
      type: String,
      default: ''
    },
    // 内容描述
    desc: {
      type: String,
      default: ''
    },
    // 取消文案
    cancelText: {
      type: String,
      default: '以后再说'
    },
    // 确定文案
    okText: {
      type: String,
      default: '我知道了'
    },
},
复制代码modal组件
<template>
  <div  class="modal_wrapper" v-show="visible">
    <div class="modal">
      <div class="modal_body">
        <div class="title"> 
            {{title}}
        </div>
        <div class="desc">
            {{desc}}
        </div>
      </div>
      <div class="modal_footer">
        <div class="btn-list">
          <div class="cancel-btn" @click="close">{{cancelText}}</div>
          <div class="confirm-btn" @click="confirm">{{okText}}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: "Modal",
  props:{
    // 控制显示隐藏
    visible: {
      type: Boolean,
      default: false
    },
    // 标题
    title: {
      type: String,
      default: ''
    },
    // 内容描述
    desc: {
      type: String,
      default: ''
    },
    // 取消文案
    cancelText: {
      type: String,
      default: '以后再说'
    },
    // 确定文案
    okText: {
      type: String,
      default: '我知道了'
    },
  },
  data() {
    return {}
  },
  methods: {
    close() {
      this.$emit("toggle", false);
    },
    confirm() {
      this.$emit("confirm"); 
    }
  }
};
</script>
<style lang='less' scoped>
.modal_wrapper{
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  background-color: rgba(0,0,0,0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 999;
  .modal {
    background-color: #fff;
     border-radius: .16rem;
     
    .modal_body {
      width: 5.6rem;
      padding: .5rem;
      box-sizing: border-box;
      min-height: 2.6rem;
      text-align: center;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      .title {
        margin-bottom: .3rem;
        font-size: .32rem;
        color: #333333;
        font-weight: bold;
        line-height: .42rem;
      }
      .desc {
        font-size: .28rem;
        color: #999999;
        font-weight: bold;
        line-height: .42rem;
      }
    }
    .modal_footer{
     border-top: 1px solid #E5E5E5;
      .btn-list {
       
        display: flex;
        font-size: .32rem;
        
        align-items: center;
        .cancel-btn {
          flex: 1;
          color: #999999;
          height: 1rem;
          line-height: 1rem;
        }
        .confirm-btn {
          flex: 1;
          color: #FE5D72;
          border-left: 1px solid #E5E5E5;
          height: 1rem;
          line-height: 1rem;
        }
      }
    }
  }
}
</style>
复制代码使用
<modal @close="close" :visible="visible" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息"></modal>
data(){
  return {
    visible: false
  }
},
methods:{
  close(value){
    this.visible = value
  },
  open(){
    this.visible = true
  }
}
复制代码使用.sync 修饰符
上面控制组件显示隐藏的实现方式:打开时向组件传入一个为true布尔值,关闭时向父组件发送一个close事件并带上false参数,让父组件中修改原始的prop数据,完成状态的更新。
但是这样做过于繁琐,这时候可以使用.sync修饰符进行简写。
子组件中
close(){
    this.$emit('close', false)
    this.$emit('update:visible', false)
  },
复制代码父组件
<modal :visible.sync="visible" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息"></modal>
复制代码上面的写法等同于
<modal :visible="visible" @update:visible="val => visible = val" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息"></modal>
复制代码@update:visible 中是update是显示更新的事件,跟在后面的:visible则是需要改变对应的props值。
v-model
在input元素中我们常常用v-model进行双向绑定
<input v-model="something">
等同于
<input :value="something" @input="something = $event.target.value">
复制代码既然在元素上能进行双向绑定,那在组件中进行双向绑定又如何实现,原理其实都是一样的,只是应用在自定义的组件上时,拿的并不是$event.target.value,因为我此时不作用在 Input 输入框上,拿的值应该传过来的第一个参数arguments[0]
在组件中使用v-model
<modal  v-model="visible" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息"></modal>
等同于
<modal  :value="visible" @input="visible = arguments[0]" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息"></modal>
复制代码那么在子组件中就要接收名字为value的prop和发送input事件
<template>
  <div  class="modal_wrapper" v-show="value">
    <div class="modal">
      <div class="modal_body">
        <div class="title"> 
            {{title}}
        </div>
        <div class="desc">
            {{desc}}
        </div>
      </div>
      <div class="modal_footer">
        <div class="btn-list">
          <div class="cancel-btn" @click="close">{{cancelText}}</div>
          <div class="confirm-btn" @click="confirm">{{okText}}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script >
  export default {
    name: 'Modal',
    props:{
      // 控制显示隐藏value
      value: {
        type: Boolean,
        default: false
      },
      // 标题
      title: {
        type: String,
        default: ''
      },
      // 内容描述
      desc: {
        type: String,
        default: ''
      },
      // 取消文案
      cancelText: {
        type: String,
        default: '以后再说'
      },
      // 确定文案
      okText: {
        type: String,
        default: '我知道了'
      },
    },
    data () {
      return {
      }
    },
    methods:{
      close(){
        this.$emit('close', false)
        this.$emit('input', false)
      },
      confirm(){
      }
    }
  }
</script>
复制代码默认情况下,一个组件的 v-model 会使用 value 属性和 input 事件
往往有些时候,value 值被占用了,或者表单的和自定义v-model的$emit(‘input’)事件发生冲突,为了避免这种冲突,可以定制组件 v-model
在子组件中
<template>
  <div  class="modal_wrapper" v-show="visible">
    <div class="modal">
      <div class="modal_body">
        <div class="title"> 
            {{title}}
        </div>
        <div class="desc">
            {{desc}}
        </div>
      </div>
      <div class="modal_footer">
        <div class="btn-list">
          <div class="cancel-btn" @click="close">{{cancelText}}</div>
          <div class="confirm-btn" @click="confirm">{{okText}}</div>
        </div>
      </div>
    </div>
  </div>
</template>
<script >
  export default {
    name: 'Modal',
    model:{
      prop: 'visible',
      event: 'toggle'
    },
    props:{
      // 控制显示隐藏
      visible: {
        type: Boolean,
        default: false
      },
      // 标题
      title: {
        type: String,
        default: ''
      },
      // 内容描述
      desc: {
        type: String,
        default: ''
      },
      // 取消文案
      cancelText: {
        type: String,
        default: '以后再说'
      },
      // 确定文案
      okText: {
        type: String,
        default: '我知道了'
      },
    },
    methods:{
      close(){
        this.$emit('close', false)
        this.$emit('toggle', false)
      },
      confirm(){
      }
    }
  }
</script>
复制代码通过 model 选项的改变,把 props 从原本的value换成了visible,input触发的事件换成了toggle,解决了冲突的问题。
.sync和v-model使用场景:父子组件间需要双向绑定某个数据的时候。
使用插槽动态替换内容
现在弹窗内容只能单单根据props传进来的文字进行修改,如果想传入一个组件或者一张图片呢, 那就要用到插槽了。
使用插槽改写
<template>
  <div  class="modal_wrapper" v-show="visible">
    <div class="modal">
      <div class="modal_body">
          <div class="title"> 
            <!-- 匿名插槽 -->
            <slot>
              {{title}}
            </slot>
          </div>
        <div class="desc">
           <!-- 具名插槽 -->
          <slot name="desc">{{desc}}</slot>
        </div>
      </div>
      <div class="modal_footer">
        <!-- 具名插槽 -->
        <slot name="footer">
          <div class="btn-list">
            <div class="cancel-btn" @click="close">{{cancelText}}</div>
            <div class="confirm-btn" @click="confirm">{{okText}}</div>
          </div>
        </slot>
      </div>
    </div>
  </div>
</template>
复制代码父组件使用
<modal v-model="visible" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息">
    好的
    <template slot="desc">
      开通会员,每日不限匹配次数
    </template>
    <template slot="footer">
      <div class="btn">知道了</div>
    </template>
</modal>
复制代码匿名插槽: name 属性默认是default
具名插槽: 带有name 属性
<template> 元素中的所有内容都将会被传入相应名字的插槽。没有插槽名字的 <template> 中的内容都会被视为默认插槽的内容。
基于vue-cli3 脚手架实现组件按需加载打包
组件库打包
一般引用组件库有两种方式引入,一种是全部引入,一种是按需加载
我们一般采用第二种,就是可以这样加载组件
import { modal }  from 'my-ui'
Vue.use(modal)
复制代码按需加载的实现方式有两种:
- 各个组件分开打包,使用时要安装babel-plugin-import 利用它实现组件按需引入(目前ant design vue 跟element-ui 都是采用这种方式)
- 利用webpack 生产环境下的Tree-shaking(移除 JavaScript 上下文中的未引用代码), 但需要满足使用 ES2015 模块语法(即 import 和 export),webpack 目前还并没有支持 ES modules 输出格式的包,但是rollup支持打包成es module模块的包
CommonJS是通过module.exports定义模块, require引入模块的,但在前端浏览器中并不支持该规范,webpack以及Node是采用CommonJS的规范来写的
AMD(Asynchronous Module Definition):异步模块定义,使用时需要引入第三方的库文件:RequireJS,是运行在浏览器环境中异步加载模块,可以并行加载多个模块。
CMD(Common Module Definition):通用模块定义。它解决的问题和AMD规范是一样的,只不过在模块定义方式和模块加载时机上不同,CMD也需要额外的引入第三方的库文件:SeaJS
UMD:兼容CommonJS 和 AMD,能运行在浏览器和node或者webpack环境中,同时还支持window的全局变量规范
ES6 module:import和export。import命令用于输入其他模块提供的功能。export命令用于规范模块的对外接口, 浏览器还有兼容问题,需要babel编译成es5 才能使用
我们由于是基于vue-cli3 进行开发的,所以我们就采用第一种方式实现按需加载
- 使用vue-cli3 创建一个项目
vue create my-ui
复制代码- 创建packages文件夹用来存放组件库源码。
- 创建组件文件,一个组件至少包括两个文件,一个index.js用来导出组件作为插件使用,一个.vue文件,用来编写组件。
modal组件下的index.js
import modal from './modal.vue'
modal.install = (Vue)=>{
  Vue.component(modal.name, modal)
}
export default modal
复制代码文件目录

在packages文件新建index.js用来导出所有的组件库
packages/index.js
import './index.less'
const components = []
// 遍历当前文件夹中所有.js文件 自动导入组件
const ctx = require.context('./',true,/\.js$/)
ctx.keys().forEach(path =>{
  if (path.startsWith('./index')) return  // 如果是最外面的index.js 就退出
  const componentConfig = ctx(path) //导入组件
  // 兼容 import export 和 require module.export 两种规范
  const comp = componentConfig.default || componentConfig
  components.push(comp)
})
const install = (Vue)=>{
  components.forEach(comp =>{
    Vue.use(comp)
  })
}
// 判断是否是直接引入vue文件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}
export default {
  install
}
复制代码require.context
传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
require.context函数执行后返回的是一个(require)函数,参数是想要加载模块的路径,并且这个函数有3个属性:resolve, keys, id。这里主要用到keys
keys(Function) -返回匹配成功模块的名字组成的数组
const ctx = require.context('./', true, /\.js$/)
console.log(ctx.keys())
// ['./index.js', './modal/index.js', './toast/index.js']
复制代码webpack 会在构建中解析代码中的 require.context()
Vue.use
使用Vue.use()方法加载插件时必须提供一个install方法,在install方法中会传入Vue实例,通过Vue.component(name, component)全局注册组件
由于需要支持按需加载,所有每个组件都必须实现install方法,全局注册组件
packages/modal/index.js
import modal from './modal.vue'
modal.install = (Vue)=>{
  Vue.component(modal.name, modal)
}
export default modal
复制代码- 新建vue.config.js配置文件,修改打包时多入口配置
const path =  require('path')
const fs = require('fs')
function resolve(name){
  return path.resolve(__dirname, name)
}
const entry={}
//获取packages文件夹下所有文件名
const files = fs.readdirSync(resolve('./packages'))
files.forEach(name =>{
  name = name.split('.')[0]
  entry[name] = resolve('./packages/'+ name)
})
console.log(entry)
/*
多入口打包配置
{ 
  index: 'E:\\demo\\my-ui\\packages\\index', 
  modal: 'E:\\demo\\my-ui\\packages\\modal', 
  toast: 'E:\\demo\\my-ui\\packages\\toast' 
}
*/
const prod = {
  css: {
    sourceMap: true,
    extract: {
        filename: 'style/[name].css'
    }
  },
  configureWebpack: {
      entry: {
          ...entry,
      },
      output: {
          filename: '[name]/index.js',
          libraryTarget: 'umd',
      }
  },
  chainWebpack: config =>{
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
    .set('@', path.resolve('examples'))
    .set('~', path.resolve('packages'))
    config.module.rule('js')
    .include.add(/packages/).end()
    .include.add(/examples/).end()
    .use('babel')
    .loader('babel-loader')
    .tap(options => {
      // 修改它的选项...
      return options
    })
    config.optimization.delete('splitChunks')
    config.plugins.delete('copy')
    config.plugins.delete('html')
    config.plugins.delete('preload')
    config.plugins.delete('prefetch')
    config.plugins.delete('hmr')
    config.entryPoints.delete('app')
  },
  outputDir: 'lib',
  productionSourceMap: false,
}
const dev = {
  pages: {
    index: {
      entry: 'examples/main.js',
      template: 'public/index.html',
      filename: 'index.html',
    },
},
  chainWebpack: config =>{
    // @ 默认指向 src 目录,这里要改成 examples
    // 另外也可以新增一个 ~ 指向 packages
    config.resolve.alias
    .set('@', path.resolve('examples'))
    .set('~', path.resolve('packages'))
    config.module.rule('js')
    .include.add(/packages/).end()
    .include.add(/examples/).end()
    .use('babel')
    .loader('babel-loader')
    .tap(options => {
      // 修改它的选项...
      return options
    })
  },
  
} 
module.exports = process.env.NODE_ENV === 'production'? prod: dev
复制代码运行 npm run build 生产环境下会导出prod的配置 进行打包
打包后生成的文件夹目录

在使用时候需要安装babel-plugin-import插件,利用它实现组件按需引入。(element-ui和ant design vue 目前也是采用这种方法)
npm install babel-plugin-import --save-dev
复制代码修改babel.config.js配置
module.exports = {
    "presets": ["@vue/app"],
    "plugins": [
        [
            "import",
            {
                "libraryName": "my-ui",//组件库名称
                "camel2DashComponentName": false,//是否需要驼峰转短线
                "camel2UnderlineComponentName": false//是否需要驼峰转下划线
                "style": (name) =>{ // 自动引入css
                    const cssName = name.split('/')[2];
                    return `my-ui/lib/style/${cssName}.css` 
                }
            }
        ],
    ]
}
复制代码这个插件做了什么?
import { modal, toast } from 'my-ui';
相当于
import modal from  "my-ui/lib/modal/index.js";
import "my-ui/lib/style/modal.css" 
import toast from  "my-ui/lib/toast/index.js";
import "my-ui/lib/style/toast.css"
复制代码插件会帮你转换成 my-ui/lib/xxx 的写法,这样的话就只会引入使用到组件的js和css文件,做到按需加载。
文档自动生成
对于 Vue 组件,一般来说需要对外暴露: props、event、slot 等接口信息
对于 UI 组件,还需要提供预览,方便快速选择合适的组件
如果使用 Markdown 撰写,虽然能写 API 文档,但是无法提供组件预览,并且手动写文档的成本也很大
使用 vue-cli-plugin-styleguidist 进行自动化文档的生成,并提供组件预览
安装:
npm install vue-cli-plugin-styleguidist --save-dev
复制代码然后在 package.json 配置下面两行命令,分别用于开发预览和部署打包
{
  "scripts": {
    "styleguide": "vue-styleguidist server",
    "styleguide:build": "vue-styleguidist build"
  }
}
复制代码在项目根目录下,创建 styleguide.config.js
// styleguide.config
module.exports = {
  title: 'my-ui',          // 文档的标题
  components: 'packages/**/*.vue', // 组件的目录
  usageMode: 'expand',                   // 是否展开用法
  exampleMode: 'expand',                 // 是否展开示例代码
  styleguideDir: 'styleguide',           // 打包的目录
  codeSplit: true,                       // 打包时是否进行分片
};
复制代码编写好的组件注释
<template>
  <div class="modal_wrapper" v-show="visible">
    <div class="modal">
      <div class="modal_body">
        <div class="title">
          <!--  @slot 匿名插槽 标题 -->
          <slot>
            {{ title }}
          </slot>
        </div>
        <div class="desc">
          <!--  @slot 具名插槽 内容描述-->
          <slot name="desc" :user="title">{{ desc }}</slot>
        </div>
      </div>
      <div class="modal_footer">
        <!--  @slot 具名插槽 底部按钮-->
        <slot name="footer">
          <div class="btn-list">
            <div class="cancel-btn" @click="close">{{ cancelText }}</div>
            <div class="confirm-btn" @click="confirm">{{ okText }}</div>
          </div>
        </slot>
      </div>
    </div>
  </div>
</template>
<script>
import "../index.less";
export default {
  name: "Modal",
  model: {
    prop: "visible",
    event: "toggle"
  },
  props: {
    /**
     * 控制显示隐藏
     * @model
     */
    visible: {
      type: Boolean,
      default: false
    },
    /**
     * 标题
     *
     */
    title: {
      type: String,
      default: ""
    },
    /** 内容描述*/
    desc: {
      type: String,
      default: ""
    },
    /** 取消文案*/
    cancelText: {
      type: String,
      default: "以后再说"
    },
    /** 确定文案*/
    okText: {
      type: String,
      default: "我知道了"
    }
  },
  data() {
    return {
      on: false
    };
  },
  methods: {
    close() {
      /**
       * 按钮点击成功emit事件
       * @event toggle
       * @type {boolean}
       */
      this.$emit("toggle", false);
    },
    confirm() {}
  }
};
</script>
<style lang="less" scoped>
.modal_wrapper {
  width: 100vw;
  height: 100vh;
  position: fixed;
  top: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 999;
  .modal {
    background-color: #fff;
    border-radius: 0.16rem;
    .modal_body {
      width: 5.6rem;
      padding: 0.5rem;
      box-sizing: border-box;
      min-height: 2.6rem;
      text-align: center;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      .title {
        margin-bottom: 0.3rem;
        font-size: 0.32rem;
        color: #333333;
        font-weight: bold;
        line-height: 0.42rem;
      }
      .desc {
        font-size: 0.28rem;
        color: #999999;
        font-weight: bold;
        line-height: 0.42rem;
      }
    }
    .modal_footer {
      border-top: 1px solid #e5e5e5;
      .btn-list {
        display: flex;
        font-size: 0.32rem;
        align-items: center;
        .cancel-btn {
          flex: 1;
          color: #999999;
          height: 1rem;
          line-height: 1rem;
        }
        .confirm-btn {
          flex: 1;
          color: #fe5d72;
          border-left: 1px solid #e5e5e5;
          height: 1rem;
          line-height: 1rem;
        }
      }
    }
  }
}
</style>
复制代码效果图

UI组件预览,在组件目录下新建 Readme.md文件 用“`vue开头做的标识,这个插件会把这一段代码编译成vue组件,并且能够提供交互。组件开发时候可以用这个插件一边调式组件一般写文档
readme.md 文档
 ```vue
<template>
  <div id="app">
    <div @click="open">打开弹窗</div>
    <modal  v-model="visible" title="确定拉到黑名单?" desc="拉黑后你将不再受到对方消息">
      你还不是会员
      <template #desc="{ user }">
        开通会员,每日不限匹配次数a {{user}}
      </template>
    </modal>
  </div>
</template>
<script>
import '../index.less'
export default {  
  name: 'App',
  data(){
    return {
      visible: false
    }
  },
  methods:{
    close(){
      this.visible = false
    },
    open(){
      console.log('a')
      this.visible = true
    }
  }
}
</script>
<style lang="less" scoped>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  /deep/
  .btn {
    line-height: 1rem;
    height: 1rem;
    color: #FE5D72;
    font-size: .32rem;
  }
}
</style>
 ```vue
复制代码效果图

发布到npm
发布到npm这一步比较简单,首先得注册npm账号, 然后修改packages.js配置
{
  "name": "my-ui", // 包名
  "version": "0.1.0", // 每次发布都要修改版本号
  "private": false, // 这里传到npm 必须设置false,表示是公开的包,除非给钱
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "styleguide": "vue-styleguidist server",
    "styleguide:build": "vue-styleguidist build"
  },
  "main": "lib/index/index.js", // 访问入口
  "files": [ "lib" ] //需要发布到npm的文件, 一般是打包后的文件夹
}
复制代码然后就是登录发布了
npm login
npm publish
复制代码








![[Python人工智能] 十一.Tensorflow如何保存神经网络参数 丨【百变AI秀】-一一网](https://www.proyy.com/wp-content/uploads/2024/10/load.gif )












![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)
