Some time ago, I was trying to implement a component library from 0 to 1. I summarized some common functions, such as component encapsulation, on-demand loading, automatic document generation and other Github addresses

Encapsulate popover components

Vue components encapsulate common apis

The Vue component API consists of three parts: props, Slot, and Event

  1. Props represents the parameters that the component receives. It is best written as an object. This allows you to set the type, default value, or custom value of the validation attribute for each attribute
  2. Slot can dynamically insert some content or components into a component, which is an important way to implement higher-order components. When multiple slots are required, named slots can be used
  3. Event is an important way for children to send messages to their parents,($emit)

Props one-way data flow

A warning error occurs if you change props directly, because Vue passes data in one direction: updates to the parent prop flow down to the child, but not the other way around, to prevent accidental changes to the parent component’s state from the child.

Intercomponent communication

  1. The parent-child component relationship can be summarized as prop passing down and events passing up
  2. Data transfer between ancestor and descendant components (across multiple generations) can be implemented using provide and Inject

Encapsulate a popover component

Blue Lake address: lanhuapp.com/web/#/item/…

Look at the design draft to analyze the prop that needs to be passed in:

  1. Controls show and hide
  2. The title
  3. content
  4. Bottom button copy (Cancel and confirm)
Props :{// Control Display Visible :{type: Boolean, default: false}, // Title :{type: String, default: "}, // Content description desc: {type: String, default: ""}, // cancelText: {type: String, default: 'later'}, // Confirm the text okText: {type: String, default: 'later'}, String, default: 'I know'},},Copy the code

Modal components

<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:{// Control display hide visible: {type: Boolean, default: false}, // Title: {type: String, default: "}, // Content description desc: {type: String, default: }, // cancelText: {type: String, default: 'later'}, // confirm copy okText: {type: String, default: 'I know'}}, the data () {return {}}, the methods: {the close () {enclosing $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.6 rem; padding: .5rem; box-sizing: border-box; Min - height: 2.6 rem; 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>Copy the code

use

< mod@close ="close" :visible="visible" title=" Sure to pull blacklist?" Desc =" After blocking you will no longer receive messages from the other party "></modal> data(){return {visible: false } }, methods:{ close(value){ this.visible = value }, open(){ this.visible = true } }Copy the code

Use the.sync modifier

The above implementation of the control component to show hiding: pass a Boolean value of true to the component when opened, and send a close event with false to the parent component when closed, allowing the parent component to modify the original prop data to complete the state update.

But this is too cumbersome, so you can use the. Sync modifier instead.

The child components

close(){
    this.$emit('close', false)
    this.$emit('update:visible', false)
  },
Copy the code

The parent component

<modal :visible.sync="visible" title=" Sure to pull blacklist?" Desc =" After blocking you will no longer receive messages from the other party "></modal>Copy the code

So this is the same thing as

<modal :visible="visible" @update:visible="val => visible= val" title=" Sure to pull blacklist?" Desc =" After blocking you will no longer receive messages from the other party "></modal>Copy the code

Visible (@update:visible) is an event that displays an update, and (visible) is an event that needs to change the corresponding props.

v-model

We often use v-model bidirectional binding in input elements

<input v-model="something"> = <input :value="something" @input="something = $event.target.value">Copy the code

$event. Target. Value = $event. $event. Since I am not using the Input field at this time, I should pass the value of the first argument [0]

Use v-Models in components

<modal V-model ="visible" title=" Sure to pull blacklist?" Desc =" After blocking you will no longer receive messages from the other side "></modal> equivalent to <modal :value="visible" @input="visible = arguments[0]" title=" Sure to blacklist?" Desc =" After blocking you will no longer receive messages from the other party "></modal>Copy the code

The child component receives a prop with the name Value and sends an input event

<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:{// control display hide value value: {type: Boolean, default: false}, // Title: {type: String, default: "}, // Content description desc: {type: String, default: }, // cancelText: {type: String, default: 'later'}, // confirm copy okText: {type: String, default: 'I know'}}, the data () {return {}}, the methods: {the close () {enclosing $emit (" close ", false) enclosing $emit (" input ", false)}, confirm(){ } } } </script>Copy the code

By default, a component’s V-Model uses the value attribute and input event

Sometimes the value value is occupied, or the form’s $emit(‘input’) event conflicts with the custom V-Model. To avoid this conflict, you can customize the component V-Model

In the child component

<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:{// Control display visible: {type: Boolean, default: false}, // title: {type: String, default: }, // Content Description desc: {type: String, default: "}, // cancelText: {type: String, default: "}, // cancelText: {type: String, default: OkText: {type: String, default: 'I know'}}, the methods: {the close () {enclosing $emit (" close ", false) enclosing $emit (' toggle 'false)}, confirm () {}}} < / script >Copy the code

By changing the model option, the props was changed from value to visible, and the input-triggered event was changed to toggle, which solved the conflict problem.

Sync and V-model: Data needs to be bidirectionally bound between parent and child components.

Use slots to dynamically replace content

Now the popover content can only be modified based on text passed in to props. If you want to pass in a component or an image, you need to use a slot.

Using a slot rewrite

<template> <div class="modal_wrapper" v-show="visible"> <div class="modal"> <div class="modal_body"> <div class="title"> <! - anonymous slot - > < slot > {{title}} < / slot > < / div > < div class = "desc" > <! - named slot - > < slot name = "desc" > {{desc}} < / slot > < / div > < / div > < div class = "modal_footer" > <! Name 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>Copy the code

Parent component use

<modal V-model ="visible" title=" Sure to pull blacklist?" <template slot="desc"> Unlimited number of matches per day </template> <template slot="footer"> <div class=" BTN ">Copy the code

Anonymous slot: Name The default value is default. Named slot: has the name attribute

Everything in the

Vue-cli3 scaffold-based components are loaded and packaged on demand

Component library packaging

There are two ways to introduce a general reference component library, one is to introduce all, one is to load on demand

We usually use the second option, where the component can be loaded in this way

import { modal }  from 'my-ui'
Vue.use(modal)
Copy the code

Load on demand can be implemented in two ways:

  1. Each component is packaged separately and used with babel-plugin-import installed (currently used in Ant Design Vue and Element-UI)
  2. Tree-shaking in a WebPack production environment (removing unreferenced code from JavaScript context), but with ES2015 module syntax (i.e., import and export), Webpack does not currently support packages in ES Modules output format, but rollup does support packages packaged as ES Module modules

CommonJS is a module defined through module.exports. Require introduces modules, but the specification is not supported in the front-end browser

Asynchronous Module Definition (AMD): Asynchronous Module Definition. The RequireJS library file must be imported from a third party. It is used to load modules asynchronously in a browser environment and can load multiple modules in parallel.

CMD(Common Module Definition) : Common Module Definition. It addresses the same issues as the AMD specification, but in terms of how modules are defined and when modules are loaded, CMD also requires the introduction of an additional third-party library: SeaJS

UMD: Compatible with CommonJS and AMD, can run in the browser and Node or Webpack environment, and also supports the Window global variable specification

ES6 Module: Import and export. The import command is used to input functions provided by other modules. The export command is used to standardize the external interface of the module. The browser has compatibility problems, and Babel needs to be compiled into ES5 to use it

Because we are based on vuE-CLI3 development, so we use the first way to achieve on-demand loading

  1. Create a project using VUE-cli3
vue create my-ui
Copy the code
  1. Create packages folder to hold component library source code.
  2. Create component files. Each component contains at least two files, an index.js file to export the component for use as a plug-in, and a. Vue file to write the component.

Index.js under the Modal component

import modal from './modal.vue'
modal.install = (Vue)=>{
  Vue.component(modal.name, modal)
}
export default modal
Copy the code

File directory

Create index.js in the Packages file to export all component libraries

packages/index.js

Import './index.less' const components = [] const CTX = require.context('./',true,/\.js$/) Ctx.keys ().foreach (path =>{if (path.startswith ('./index')) return const componentConfig = ctx.keys().foreach (path =>{if (path.startswith (' CTX (path) / / / / import components compatibility import export and the require of the module. The export two specifications const comp = componentConfig. Default | | componentConfig components.push(comp) }) const install = (Vue)=>{ components.forEach(comp =>{ Vue.use(comp) }) } // If (typeof Window! == 'undefined' && window.Vue) { install(window.Vue) } export default { install }Copy the code

require.context

Three parameters are passed in: a directory to search, a tag to indicate whether subdirectories are also searched, and a regular expression to match files.

The require.context function returns an (require) function that takes the path of the module to be loaded, and has three attributes: resolve, keys, and ID. Here we’re going to use keys

Keys (Function) – Returns an array of the names of successfully matched modules

const ctx = require.context('./', true, /\.js$/)
console.log(ctx.keys())
// ['./index.js', './modal/index.js', './toast/index.js']
Copy the code

Webpack will parse require.context() in the code in the build

Vue.use

When you load a plug-in using vue.use (), you must provide an install method, which passes in the Vue instance and registers the component globally with Vue.component(name, Component)

Because of the need to support on-demand loading, all components must implement the install method, registering components globally

packages/modal/index.js

import modal from './modal.vue'
modal.install = (Vue)=>{
  Vue.component(modal.name, modal)
}
export default modal
Copy the code
  1. Create the vue.config.js configuration file and modify the multi-entry configuration during packaging
const path = require('path') const fs = require('fs') function resolve(name){ return path.resolve(__dirname, Const files = fs.readDirsync (resolve('./packages')) files.foreach (name =>{ Name = name.split('.')[0] entry[name] = resolve('./packages/'+ name)}) console.log(entry) /* Multi-entry package configuration {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 =>{// @ default to SRC directory, Resolve ('examples'). Set ('@', path.resolve('examples')). Set ('~', package config.resoly.alias.set ('@', path.resolve('examples')). path.resolve('packages')) config.module.rule('js') .include.add(/packages/).end() .include.add(/examples/).end() Use (' Babel). Loader (' Babel - loader). Tap (options = > {/ / modify it 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 =>{// @ default to SRC directory, Resolve ('examples'). Set ('@', path.resolve('examples')). Set ('~', package config.resoly.alias.set ('@', path.resolve('examples')). path.resolve('packages')) config.module.rule('js') .include.add(/packages/).end() .include.add(/examples/).end() Use (' Babel). Loader (' Babel - loader). Tap (options = > {/ / modify it options... return options }) }, } module.exports = process.env.NODE_ENV === 'production'? prod: devCopy the code

If you run NPM run build in the production environment, the prod configuration is exported and the generated folder directory is packaged

The babel-plugin-import plug-in needs to be installed at the time of use to allow components to be imported on demand. (Element-UI and Ant Design Vue currently use this approach as well.)

npm install babel-plugin-import --save-dev
Copy the code

Modify the babel.config.js configuration

module.exports = { "presets": ["@vue/app"], "plugins": [ [ "import", { "libraryName": "My - UI", / / the name of the component library "camel2DashComponentName" : false, / / do you need hump short-term "camel2UnderlineComponentName" : false / / do you need hump underscore "style" : (name) = > {/ / automatic introduced CSS const cssName = name. The split ('/') [2]; return ` my - UI/lib/style / ${cssName}. The CSS `}}],]}Copy the code

What does this plug-in do?

import { modal, toast } from 'my-ui'; Equivalent to 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"Copy the code

The plugin will convert you to the my-UI /lib/ XXX script, so that only the JS and CSS files used by the component will be introduced and loaded on demand.

Automatic document generation

Vue components generally need to be exposed: Interface information, such as props, Event, and slot, also needs to be previewed for UI components to facilitate quick selection of appropriate components. If Markdown is used, API documents can be written, but component previews cannot be provided, and manual document writing costs a lot

Automate document generation using vue-cli-plugin-styleguidist and provide component previews

Installation:

npm install vue-cli-plugin-styleguidist --save-dev
Copy the code

Then configure the following two commands in package.json for development preview and deployment packaging, respectively

{
  "scripts": {
    "styleguide": "vue-styleguidist server",
    "styleguide:build": "vue-styleguidist build"
  }
}
Copy the code

In the project root directory, create styleguide.config.js

// styleguide.config module.exports = {title: 'my-ui', // http://styleguide.config module.exports = {title: 'my-ui', // http://styleguide.config module.exports = {title: 'my-ui', // http://styleguide.config module.exports = {title: 'my-ui', // http://styleguide.config module.exports = {title: 'my-ui', // ExampleMode: 'expand', // Whether to expand sample code styleguideDir: 'styleguide', // Package directory codeSplit: True, // Whether the package should be sharded};Copy the code

Write good component comments

<template> <div class="modal_wrapper" v-show="visible"> <div class="modal"> <div class="modal_body"> <div class="title"> <! - @ slot anonymous slots title - > < slot > {{title}} < / slot > < / div > < div class = "desc" > <! - @ slot named slot content description - > < slot name = "desc" : the user = "title" > {{desc}} < / slot > < / div > < / div > < div class = "modal_footer" > <! -- @slot name slot bottom button --> <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: {/** * Control display hide * @model */ visible: {type: Boolean, default: false}, /** * title: {type: String, default: ""}, /** Content description */ desc: {type: String, default: ""}, /** Content description */ desc: {type: String, default: ""}, /** cancelText: {type: String, default: ""}, /** cancelText: {type: String, default: ""}, /** cancelText: {type: String, default: ""}, /** okText: {type: String, default: "I know"}}, data() {return {on: false}; $emit("toggle", false);}, methods: {close() {/** *}}, methods: {close() {/** **}}, methods: {close() {/** **}}, methods: {close() {/** **}}, methods: {close() {/** **}}, methods: {close() {/** ** }, confirm() {} } }; </script> <style lang="less" scoped> .modal_wrapper { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; Background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 999; .modal { background-color: #fff; Border - the radius: 0.16 rem; . Modal_body {width: 5.6 rem; Padding: 0.5 rem; box-sizing: border-box; Min - height: 2.6 rem; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center; The title {margin - bottom: 0.3 rem; The font - size: 0.32 rem; color: #333333; font-weight: bold; The line - height: 0.42 rem; }. Desc {font-size: 0.28rem; color: #999999; font-weight: bold; The line - height: 0.42 rem; } } .modal_footer { border-top: 1px solid #e5e5e5; .btn-list { display: flex; The font - size: 0.32 rem; 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>Copy the code

rendering

UI component preview, create a readme. md file in the component directory marked with the beginning of vue. This plug-in will compile this code into vUE components and provide interaction. You can use this plug-in to document components while developing them

The readme. Md document

Vue <template> <div id="app"> <div @click="open"> Open popup </div> <modal V-model ="visible" title=" Sure to pull blacklist?" <template #desc="{user}"> Unlimited number of matches per day 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> ```vueCopy the code

rendering

Published to the NPM

Publishing to NPM is an easy step, first registering an NPM account and then modifying the packes.js configuration

{" name ":" my - UI ", / / package name "version" : "0.1.0 from", / / each release is to modify the version number "private" : {"scripts": "vue-cli-service serve", "build": {"scripts": "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", // access entry "files": ["lib"] // Need to publish files to NPM, which is a package folder}Copy the code

Then there’s login publishing

npm login

npm publish
Copy the code