The opening

Component libraries save us the development effort of having to build everything from scratch, piecing together the pieces to get the final page we want. In daily development, if there are no specific business requirements, using component libraries for development is undoubtedly more convenient and efficient, and the quality is relatively higher.

At present, there are many open source component libraries. Whether react or Vue, there are many excellent component libraries, such as ElementUI and iView, which I often use. Of course, there are other component libraries that are essentially designed to save the process of reconstructing the basic component wheel. Also some companies may have special demand for your company’s products, are reluctant to use open source component library style, or need some internal business project, but open source projects can meet the components of the need to settle, build a set of component library has become a project that you need as a business drivers.

This article will describe the two stages of “preparation” and “practice” to complete a component library step by step. The general contents are as follows:

  1. Preparation: Before we talk about building component libraries, we need to cover some basic knowledge to prepare for the practical phase.
  2. Practice: With some basic concepts in mind, let’s go straight to building a basic set of component libraries with a practice case. Get a feel for the design of the component library from the process.

Hopefully, by sharing this article and including a simple hands-on example, you will be able to take a small step from the role of a component library user to the role of a component library creator, and have something in mind when using component libraries on a daily basis. That will be my goal.

Our case address is: arronkler.github. IO/lim-ui /

The corresponding repo is github.com/arronKler/l…

Preparation: What should you know before building a component library?

The purpose of this chapter is to clarify some of the concepts that are used in building component libraries, which are rarely discussed in business concepts. I’ll break it down into engineering and component aspects, and deliver some of the tricks and pitfalls that I know to help prepare us for the actual process.

Projects: What are the additional considerations for a component library project?

There are definitely some things that we don’t need for a business project, but usually do for a class library project. This section explains some of the things we need to think about when doing a component library.

Component test

Many developers are busy with business projects, and then in general business projects, they don’t write test scripts. But in the process of doing a component library project, it is best to have the corresponding component test script. There are at least two advantages:

  1. Automate testing the functionality of the components you write
  2. Change the code without worrying about affecting the previous users. (The test script will tell you if there are any unexpected effects.)

For library projects, I think the second benefit is very important, it can ensure your constantly promote the project to upgrade the iteration process, to ensure that there will be no influence of the libraries are already using your created the people, after all, if you upgrade a big problems for his project, that is not allowed to protect others jobs can be lost. (Just like antD’s Christmas snowflake event before)

Since we are writing vue’s component library, the recommended test suite is the Vue-test-utils suite, vue-test-utils.vuejs.org/zh/. It provides a variety of test functions and methods can be very good to meet our testing needs. You can refer to its documentation for the specific installation.

The main thing we want to talk about here is what does component testing really measure?

So we’ve got a pretty intuitive picture here, and you should know the answer to this question by looking at this picture

This is from the video www.youtube.com/watch?v=OIp… “Is also a great talk recommended by Vue-test-util, you can check it out if you want to know more about it.

So going back to component testing, it actually requires us to test the features of a component from more than just a creator’s perspective. From the user’s point of view, consider the component as a “black box”. What we can give it is the user’s interaction behavior, props data, etc. This “black box” will also feedback certain events and render views that can be captured and observed by the user. By checking these locations, we can tell if a component is behaving as we want it to, and ensure that it is behaving in a consistent manner.

Another off-topic that I want to mention is the spirit of contract. If I use your component as a user of the component, we have signed a contract that all the behaviors of the component should be consistent with your description, and there will be no third unexpected possibility. After all, for enterprise projects, we don’t like Surprise. The Easter Egg event of ANTD is also a reminder to all of you. We can play with technology in this way and it is quite creative. However, this kind of public class library, especially those used by enterprises, is also quite popular. Even if you use a component library within your own enterprise, unless it is recognized by business people, do not do this dangerous testing.

Good component tests can also help us identify the surprises that we create either intentionally or unintentionally. Intentional, if not unintended, surprises can be fatal, so it is necessary to write good component tests.

Document generation

Generally speaking, we do a library project will have corresponding documentation, some projects a readme.md document will be sufficient, others may need several Markdown documents. For projects such as component libraries, we can use documentation tools to assist in generating documentation directly. Here recommended vuepress, can help us quickly complete the construction of component library documentation. (vuepress.vuejs.org/zh/guide/)

Vuepress is a document generation tool. The default style of vuepress is almost the same as the vUE official document, because it was originally created to provide documentation support for VUE and related subprojects. The best part is that you can use the Vue component directly in the Markdown file. This means that each component written in our component library can be put directly in the document to show how the component actually works. Our case site was written using Vuepress, generated a static site, and deployed directly to Github using Gh-Pages.

What’s even better about Vuepress is that you can customize its Webpack configuration and theme, which means you can make your own document site more feature-supported during development, and you can change the site style to your own set of themes. This does not require us to start from scratch and is useful if we want to quickly complete the component library documentation.

But this is just what we want to do an auxiliary thing, so the specific use of us in the practice stage to explain, here is not to repeat.

Custom themes

The ability to customize themes is certainly beneficial for an open source library, so that users can use the functionality of the component library and use their own design styles in interface design. The functionality of most component libraries is pretty well designed, so in general, small and medium-sized companies that want to implement their own set of component-style things use the functionality and interaction logic provided by open source libraries like Element, iView, or React-based Antd. Then you can customize the theme on it and basically meet your needs (unless your designer is very thoughtful…). .

The general way to use the custom theme function is as follows

  1. Through the theme generation tool. (Creator needs to make a separate tool)
  2. Import the key theme files and override the theme variables. (This usually requires the CSS preprocessor used by the creator.)

In the first way, the makers of component libraries make a tool out of the set of things that generate component styles, then provide it to users to adjust according to their own needs, and finally generate a set of specific style files for use.

In the second way, what you are doing as a user is essentially overwriting some of the theme variables in the component library, because the specific component style file is not written with fixed style values, but uses defined variables, so your custom theme is in effect. But this introduces a small problem where you have to adapt the style preprocessor used by the creator of the component library. For example, if you use iView, your project must be able to parse Less files, and if you use ElementUI, your project must be able to parse SCSS.

In fact, the first way is mainly to adjust the theme variables. So when we want to build a set of component libraries, it is not difficult to see, a core point is to need to separate the theme variable file and style file, the rest of the simple.

Webpack packaging

There are two points about building a class library project:

  1. Exposure to entry
  2. Externalize dependencies

Let’s start with the first point, exposing interfaces. In a business project, our entire project is packaged into one or more bundles via WebPack or other packaging tools, which are loaded into the browser and run directly. But a library project is often not run alone, but by exposing an “entry point” that I call from within the business project. In the WebPack configuration file, we can control an “entry variable” that we expose and the object code that we build by defining the Library and libraryTarget in the output.

This can be a detailed reference webpack official document: webpack.js.org/configurati…

module.exports = {
  // other config
	output: {
    library: "MyLibName".libraryTarget: "umd".umdNamedDefine: true}}Copy the code

When we do a vUE component library project, our components are dependent on Vue. When we introduce vue somewhere in the component library project, the runtime of vue will be packaged together into the final component library bundle. The problem with this is that our vUE component library is used by the VUE project, so there is no need to add a runtime to the component library because it will increase the bundle size. Use Webpack’s externals to “externalize” vue dependencies.

module.exports = {
	// other config
	externals: {
    vue: {
      root: 'Vue'.commonjs: 'vue'.commonjs2: 'vue'.amd: 'vue'}}}Copy the code

According to the need to load

The on-demand loading function of the component library is very practical, so that we can avoid packaging all the used and unused content into the business code during the process of using the component library, resulting in the final bundle file which will greatly affect the user experience.

In business projects, our on-demand loading is to generate a chunk of the place that needs to be loaded on demand, and then when the browser runs our packaging code, it finds that we need this chunk of resources, and then initiates a request to obtain the corresponding required code.

In the component library, we need to change the way we introduce it. For example, when we first introduce a component library, we introduce the component library and styles directly. As follows

import LimeUI from 'lime-ui' // Import the component library
import 'lime-ui/styles/index.css' // Import the style file for the entire component library

Vue.use(LimeUI)
Copy the code

So, instead of manually loading on demand

import { Button } from 'lime-ui' // Introduce the Button component
import 'lime-ui/styles/button.css' // Introduce the button style

Vue.component('l-button', Button) // Register the component
Copy the code

It’s true that this approach is introduced on demand, but the uncomfortable part is that we have to manually introduce components and styles every time we introduce them. Generally speaking, there are at least ten components used in a project, which is more troublesome. How do component libraries solve this problem?

Automate patterns that introduce component libraries and component styles through Babel plug-ins, Such as Babel-plugin-import used by AntD, AntD-Mobile, and Material – UI, and Babel-plugin-Component used by ElementUI. Once the Babel plug-in is configured in the business project, it can internally do just that (in this case, babel-plugin-Component)

// The original code
import { Button } from 'components'
 

// Convert the code
var button = require('components/lib/button')
require('components/lib/button/style.css')
Copy the code

OK, so since the code can do this, the only thing we need to do is to put the package code of our component library under the corresponding file directory structure when we build the component library. Users can choose to load the components manually, or they can use the Babel plug-in to optimize this step.

Babel – plugin – component documentation: www.npmjs.com/package/bab…

Babel – pluigin – the import documents: www.npmjs.com/package/bab…

Components: What more do you need to know about component libraries than routine component design?

There are some differences between the techniques used to make components in a component library and those used in a project. This section is to show you what else you need to know about component design in a component library.

Component communication: What else is there besides data communication between superiors and superiors?

The common way to communicate with a component is to use props and $emit to transfer data between the parent component and the child component. The parent component uses props to send data to the child component, the child component uses $emit to send data to the parent component, or at most through eventBus or Vuex to achieve data communication between any component. These methods are useful in the normal business development process, but in the component library development process is a little inadequate, the main problem is: how to handle the data communication between the components across the level?

In everyday projects, we can use vuex to “outsource” component data directly to achieve cross-level access, but VUEX is always an external dependency, and the design of the component library is definitely not to allow such a strong dependency to exist. Let’s take a look at two data communication methods that we will use in a component library project.

The built-in dojo.provide/inject

Provide/Inject is a solution that provides vue with cross-level access to parent component data from child components. This pair of things is similar to the Context in React, both of which are designed to deal with data passing across levels of components.

When used, the data to be injected is declared at the inject of the child component, and then the data required by the child component is provided at a place containing corresponding data in the parent component. No matter how many components they span, the child components can get the corresponding data. (See the pseudocode example below)

// CompA --> CompB --> CompC -->... --> ChildComp

// CompA.vue
export default {
  provide: {
    theme: 'dark'}}// CompB.vue
// CompC.vue
// ... 

// ChildComp.vue
export default {
  inject: ['theme'],
	mounted() {
    console.log(this.theme) // Print the result: dark}}Copy the code

However, provide/inject means that the child component obtains its state from the parent component across levels, but it cannot perfectly solve the following problems:

  1. Child components pass data across levels to parent components
  2. Parent components pass data across levels to child components

Dispatch and broadcast: Customize the dispatch and broadcast functions

Dispatch and broadcast can be used for cross-level communication between parent and child components. In vuE1.x, there are dispatch and broadcast functions, but they are cancelled in vue2.x. Refer to v1.x, which is linked below.

Dispatch file (v1.x) : v1.vuejs.org/api/#vm-dis…

Broadcast document (V1.x) : v1.vuejs.org/api/#vm-bro…

According to the documents, we know

  • Dispatch sends an event that is first fired on its component instance and then bubbles up the parent chain until it fires one of the parent listeners declared for the event, and then stops unless that listener returns true. Of course, the listener can also get all the parameters passed in the event dispatch through the callback function. This is a lot like the event bubbling mechanism we have in the DOM and should make sense.

  • Broadcast broadcasts events to all of its sub-component instances, layer by layer, and because of the component tree, the process of going down will encounter “bifurcation”, which can be regarded as multiple paths. Events bubble radially down each subpath, stopping when the listener fires on each path, and continuing down if the listener returns true.

Just to summarize. The event is bubbling up to dispatch, and the event is being broadcasted down to the listener that is handling the event. If the listener does not return true, the event is stopped

It is important to note that both dispatch and broadcast events are cross-hierarchical and can carry parameters, which means that data can be communicated across the hierarchy.

Since dispatch and broadcast are cancelled in vvue2.x, we can write one ourselves here and mixin it into the components that need to be used for cross-level component communication.

The content of the method is actually quite simple, so I’ll just list the code here

// Reference from iView implementation
function broadcast(componentName, eventName, params) {
  this.$children.forEach(child= > {
    const name = child.$options.name;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else{ broadcast.apply(child, [componentName, eventName].concat([params])); }}); }export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;

      while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if(parent) { name = parent.$options.name; }}if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params); }}};Copy the code

In fact, there are some differences between this implementation and vue1.x:

  1. Dispatch has no event bubble. Find the one and execute it
  2. A name parameter is set to trigger events only for components with a specific name

Once you understand the code here, you should be able to figure out how to find any component, either up or down, by iterating through and iterating until the target component appears and then calling it. Distributing and broadcasting is simply finding and using vUE’s built-in event mechanism to publish an event, and then listening for and handling the event in a specific component.

Render functions: This frees the power of javascript

First, let’s review how a component goes from writing code to being transformed into an interface. When we write vue single-file components, there are usually three parts: Template, script, and style. When we package vue components, the Vue-Loader first compiles the template part of the component into the code required by the Render option of the vue instance. At runtime, the vue runtime will render it with $mount and then mount it to the DOM node you provided.

In the whole process, we only pay the most attention to the template part, of course, but template is just a grammar sugar provided by Vue, which makes it as easy to write code as HTML, and reduces the learning cost of friends who just start vue. React does not provide template syntactic sugar, but instead uses JSX to reduce the complexity of writing components. (Vue was able to thrive under the react and Angular frameworks, and the simple template syntax helped, because it seemed simpler.)

From what we’ve reviewed above, we’ve actually noticed that the templates we’re writing are, in the end, javascript. When the template is compiled and given to the render function, vue performs the operations in Render to render our component.

So template is fine, but if you want to use the full power of javascript, you can use the render function.

Render function &JSX (official document) : cn.vuejs.org/v2/guide/re…

When writing business components, we can use templates, but when it comes to complex situations, we can’t handle the complexity of writing components –> introducing –> registering –> using components. For example, in the following two cases:

  1. Dynamically render components through code
  2. Render the component to another location

In the first case, components are rendered dynamically through code, such as running a frequently used activity H5 page. Each activity is different and is either redone or modified each time. However, this kind of modification of the page structure is very big, each time will be destructive, and it is no different from redoing. That way, the front end has to get up and write code for every activity, regardless of the content. But in fact, only need to do an active editor in the management background, the content of the editor is directly converted into the code of render function, and then delivered to a page through the configuration, the bearing page to get data to render function to perform rendering. This allows you to dynamically render component content according to the way the backend configuration is managed, and each active page and operation can be generated by the editor itself.

The second case is when you want to render the component to a different location. Our daily writing of business components is basically writing a component and using it as needed. If you simply write a component in a Template, the contents of your component will be rendered as children of the current component, and the generated DOM structure will be below the current DOM structure. We can create a new vue instance and manually mount it to any DOM location after dynamically rendering it.

import CompA from './CompA.vue'

let Instance = new Vue({
  render(h) {
    return h(CompA)
  }
})

let component = Instance.$mount() // Perform rendering
document.body.appendChild(component.$el) // Mount to the body element

Copy the code

The element in which we use this.$message is rendered dynamically and then manually mounted to the specified location.

Practice: Do it once and you’ll get it

Here’s our Github address so you can check it out as you go along. Github.com/arronKler/l…

Create an engineered project

The first step is to build an engineered structure

Here is no nonsense, directly posted directory structure and explanation

|- assets/   # Store additional resource files, images, etc
|- build/  # WebPack configuration
|- docs/  # Store the document
	|- .vuepress  # vuepress configure directory
	|- component The document related to the component is put here
	|- README.md # Static Home page
|- lib/  Put the generated file here
	|- styles/ # Packaged style file
|- src/ # Write code here
	|- mixins/ # a mixin file
	|- packages/ Each component is a subdirectory
	|- styles/ # Style files
		|- common/ # Common style content
		|- mixins/ # Reusable mixins
	|- utils  # Tool directory
	|- index.js  Package entry, export of component
|- test/  # Test folder
	|- specs/  # Store all test cases
|- .npmignore
|- .gitignore
|- .babelrc
|- README.md
|- package.json
Copy the code

The most important directory here is our SRC directory, which holds our individual components and a set of style libraries, as well as some other ancillary things. When we write a document, we write under the docs directory. The outermost layer of the project directory is the usual configuration content, such as.npmignore and.gitignore, which are common files, so I won’t go into the details. If you are in doubt, you can refer to the source code on Github.

Here we need to use the library files also set up first

Create an emitter under SRC /mixins and write the following, which is our dispatch and broadcast methods, which will be used later in the component design

function broadcast(componentName, eventName, params) {
  this.$children.forEach(child= > {
    const name = child.$options.name;

    if (name === componentName) {
      child.$emit.apply(child, [eventName].concat(params));
    } else{ broadcast.apply(child, [componentName, eventName].concat([params])); }}); }export default {
  methods: {
    dispatch(componentName, eventName, params) {
      let parent = this.$parent || this.$root;
      let name = parent.$options.name;

      while(parent && (! name || name ! == componentName)) { parent = parent.$parent;if(parent) { name = parent.$options.name; }}if (parent) {
        parent.$emit.apply(parent, [eventName].concat(params));
      }
    },
    broadcast(componentName, eventName, params) {
      broadcast.call(this, componentName, eventName, params); }}};Copy the code

Then create an assist.js file under SRC /utils and write down the auxiliary functions

export function oneOf(value, validList) {
  for (let i = 0; i < validList.length; i++) {
    if (value === validList[i]) {
      return true; }}return false;
}
Copy the code

Both of these places will be used later, and if you need additional auxiliary content, you can create it in the same directory as these two files.

The second step is to improve the packaging process

If we want to package a component library project, we must first configure our WebPack, otherwise we can’t write the source code to run. So let’s go to the build directory and create three files in the build directory

  • Webpack. Base. Js. Store some basic rules configuration

  • Webpack. Prod. Js. Packaging configuration of the entire component library

  • Gen – style. Js. Package styles separately

The following is the specific configuration content

/* webpack.base.js */
const path = require('path');
const webpack = require('webpack');
const pkg = require('.. /package.json');
const VueLoaderPlugin = require('vue-loader/lib/plugin')

function resolve(dir) {
  return path.join(__dirname, '.. ', dir);
}

module.exports = {
  module: {
    rules: [{test: /\.vue$/.loader: 'vue-loader'.options: {
          loaders: {
            css: [
              'vue-style-loader',
              {
                loader: 'css-loader'.options: {
                  sourceMap: true,}},],less: [
              'vue-style-loader',
              {
                loader: 'css-loader'.options: {
                  sourceMap: true,}}, {loader: 'less-loader'.options: {
                  sourceMap: true,},},],},postLoaders: {
            html: 'babel-loader? sourceMap'
          },
          sourceMap: true,}}, {test: /\.js$/.loader: 'babel-loader'.options: {
          sourceMap: true,},exclude: /node_modules/}, {test: /\.css$/.loaders: [{loader: 'style-loader'.options: {
              sourceMap: true,}}, {loader: 'css-loader'.options: {
              sourceMap: true,},}]}, {test: /\.less$/.loaders: [{loader: 'style-loader'.options: {
              sourceMap: true,}}, {loader: 'css-loader'.options: {
              sourceMap: true,}}, {loader: 'less-loader'.options: {
              sourceMap: true,},},]}, {test: /\.scss$/.loaders: [{loader: 'style-loader'.options: {
              sourceMap: true,}}, {loader: 'css-loader'.options: {
              sourceMap: true,}}, {loader: 'sass-loader'.options: {
              sourceMap: true,},},]}, {test: /\.(gif|jpg|png|woff|svg|eot|ttf)\?? . * $/.loader: 'url-loader? limit=8192'}},resolve: {
    extensions: ['.js'.'.vue'].alias: {
      'vue': 'vue/dist/vue.esm.js'.The '@': resolve('src')}},plugins: [
    new webpack.optimize.ModuleConcatenationPlugin(),
    new webpack.DefinePlugin({
      'process.env.VERSION': ` '${pkg.version}'`
    }),
    new VueLoaderPlugin()
  ]
};
Copy the code
/* webpack.prod.js */
const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');

process.env.NODE_ENV = 'production';

module.exports = merge(webpackBaseConfig, {
  devtool: 'source-map'.mode: "production".entry: {
    main: path.resolve(__dirname, '.. /src/index.js')  // Use index.js under SRC as the entry point
  },
  output: {
    path: path.resolve(__dirname, '.. /lib'),
    publicPath: '/lib/'.filename: 'lime-ui.min.js'.// Change to your own library name
    library: 'lime-ui'.// Class library export
    libraryTarget: 'umd'.umdNamedDefine: true
  },
  externals: { // Externalize the dependency on vue
    vue: {
      root: 'Vue'.commonjs: 'vue'.commonjs2: 'vue'.amd: 'vue'}},plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"']}}));Copy the code
/* gen-style.js */
const gulp = require('gulp');
const cleanCSS = require('gulp-clean-css');
const sass = require('gulp-sass');
const rename = require('gulp-rename');
const autoprefixer = require('gulp-autoprefixer');
const components = require('./components.json')

function buildCss(cb) {
  gulp.src('.. /src/styles/index.scss')
    .pipe(sass())
    .pipe(autoprefixer())
    .pipe(cleanCSS())
    .pipe(rename('lime-ui.css'))
    .pipe(gulp.dest('.. /lib/styles'));
  cb()
}

exports.default = gulp.series(buildCss)
Copy the code

The webpack.base. Js configuration is mainly for loader and plug-in configuration. The specific entry and exit are configured in webpack.prod.js. Here webpack.prod.js incorporates configuration items from Webpack.base.js. With regards to output.libary and externals, you should be familiar with the previous “prepare” phase.

The gen-style.js file uses gulp alone to package the style files. We use SCSS syntax here. If you want to use less or another preprocessor, you can modify the files and dependencies.

This is certainly not the end of the configuration. First we need to install the various loaders and plugins used in this configuration. For consistency and consistency, you can copy the following configuration contents directly into package.json and install via NPM install. It is important to note that once the installation is complete, all the dependencies for the following content are installed.

"Dependencies" : {" async - the validator ":" ^ 3.0.4 ", "core - js" : "2.6.9", "webpack" : "^ 4.39.2", "webpack - cli" : "^ 3.3.7"}, "devDependencies" : {" @ Babel/core ":" ^ 7.5.5 ", "@ Babel/plugin - transform - runtime" : "^ 7.5.5 @", "Babel/preset - env" : "^ 7.5.5", "@ vue/test - utils" : "^ 1.0.0 - beta. 29", "Babel - loader" : "^ 8.0.6", "chai" : "^ 4.2.0", "cross - env" : "^ 5.2.0", "CSS - loader" : "2.1.1", "file - loader" : "^ 4.2.0", "gh - pages" : "^ 2.1.1", "gulp" : "^ 4.0.2," "gulp - autoprefixer" : "^ 7.0.0", "gulp - clean - CSS" : "^ 4.2.0", "gulp - rename" : "^ 1.4.0", "gulp - sass" : "^ 4.0.2", "karma", "^ 4.2.0", "karma - chai" : "^ 0.1.0 from", "karma - chrome - the launcher" : "^ 3.1.0", "karma - coverage" : "^ 2.0.1," "karma - mocha" : "^ 1.3.0", "karma - sinon - chai", "^ 2.0.2", "karma - sourcemap - loader" : "^ 0.3.7 karma - the spec -", "reporter" : "^ 0.0.32", "karma - webpack" : "^ 4.0.2", "less" : "^ 3.10.2", "less - loader" : "^ 5.0.0" and "mocha", "^ 6.2.0", "node - sass" : "^ 4.12.0", "rimraf" : "^ 3.0.0", "sass - loader" : "^ 7.3.1," "sinon" : "^ 7.4.1 sinon -", "chai", "^ 3.3.0", "style - loader" : "^ 1.0.0", "url - loader" : "^ 2.1.0", "vue - loader" : "^ 15.7.1 vue - style -", "loader" : "^ 4.1.2", "vuepress" : "^ 1.0.3"},Copy the code

In addition, since we are using Babel, we need to set up a.babelrc file in the root of the project, as follows:

{
  "presets": [["@babel/preset-env",
      {
        "loose": false."modules": "commonjs"."spec": true."useBuiltIns": "usage"."corejs": "2.6.9."}]],"plugins": [
    "@babel/plugin-transform-runtime"]},Copy the code

Also don’t forget to write scripts in the package.json file to simplify the process of typing commands manually

{
	"scripts": {
    "build:style": "gulp --gulpfile build/gen-style.js"."build:prod": "webpack --config build/webpack.prod.js",}}Copy the code

The third step is to create a documentation tool

If vuepress was not installed in the previous step, you can install it by NPM install vuepress –save-dev.

Then add a script to package.json for a quick start

{
  "scripts": {
    // ...
    "docs:dev": "vuepress dev docs",
    "docs:build": "vuepress build docs"
  }
}
Copy the code

At this point you can write something in your docs/ readme.md file and run NPM run docs:dev to see the local document. Use NPM run docs:build when you need to pack.

If our project is going to be put on Github, we can also put our document on Github and use github pages to make the local document run online. (Github Pages hosts our static pages and resources)

You can run NPM install gh-pages –save-dev to install gh-pages, a tool that will help you deploy github Pages documentation with one click. It works by migrating the resources in the corresponding folder to the gh-pages branch of our current project. After this branch is pushed to Github, github will serve the contents of this branch. To better use it, we can add scripts to package.json

{
  "scripts": {
    // ...
  	"deploy": "gh-pages -d docs/.vuepress/dist",
    "deploy:build": "npm run docs:build && npm run deploy",
  }
}
Copy the code

You can then use NPM Run Deploy to deploy your vuepress-generated static site directly, but be sure to run the documentation builder before deploying. So we also added an NPM run deploy:build command, which solves the build and deployment of the document directly together. Isn’t that easy?

But in order for us to use our own components directly, we need to configure VuePress a little bit. Create a new enhanceapp.js file in the docs/.vuepress directory and write the following to inject the entry and style of our component library

import LimeUI from '.. /.. /src/index.js'
import ".. /.. /src/styles/index.scss"

export default ({
  Vue,
  options,
  router
}) => {
  Vue.use(LimeUI)
}
Copy the code

At this point, the components we write later can be used directly in the document.

Step four, style building

It should be noted that the style preprocessor syntax we use here is SCSS. The section “Improving the packaging process” shows the code packaged in Gulp, but it’s worth explaining how we integrate the styling content.

First of all, in order to facilitate on-demand loading later, the style of each component is a separate SCSS file. When writing the style, in order to avoid too much hierarchical nesting, the style is written in a BEM-style way.

We need to generate a basic style file by executing the following command in the SRC /styles directory

cd src/styles
mkdir common
mkdir mixins
touch common/var.scss  # style variable file
touch common/mixins.scss
touch index.scss  # Introduce all styles
Copy the code

Then populate the corresponding var.scss and mixins.scss files with some basic content

/* common/var.scss */

$--color-primary: #ff6b00! default;$--color-white: #FFFFFF! default;$--color-info: #409EFF! default;$--color-success: #67C23A! default;$--color-warning: #E6A23C! default;$--color-danger: #F56C6C! default;Copy the code
/* mixins/mixins.scss */
$namespace: 'lime';  /* Component library style prefix */

/* BEM -------------------------- */
@mixin b($block) {
  $B: $namespace+The '-'+$block! global; . # {$B} {@content; }}Copy the code

In the mixins file we declare a mixin to help us build the style file better.

Component Building Cases

With the above content set up, we can start to do a specific component to try

Simple Button component

This is what it looks like when it’s done

OK, let’s create the basic button component file

cd src/packages
mkdir button && cd button
touch index.js
touch button.vue
Copy the code

Write the contents of button.vue

<template> <button class="lime-button" :class="{[`lime-button-${type}`]: true}" type="button"> <slot></slot> </button> </template> <script> import { oneOf } from '.. /.. /utils/assist'; export default { name: 'Button', props: { type: { validator (value) { return oneOf(value, ['default', 'primary', 'info', 'success', 'warning', 'error']); }, type: String, default: 'default' } } } </script>Copy the code

Here we need to export the component in index.js

import Button from './button.vue'
export default Button
Copy the code

This is done with a single component, and you can do several more components later, but one thing is that these components need a unified package entry, which is already configured in webPack, and that is the SRC /index.js file. In this file, we need to import the Button component we just wrote, along with the other components you wrote yourself, and export them to WebPack. See the code below

import Button from './packages/button'

const components = {
  lButton: Button,
}

const install = function (Vue, options = {}) {

  Object.keys(components).forEach(key= > {
    Vue.component(key, components[key]);
  });
}

export default install
Copy the code

As you can see, in index.js we finally export a function called install. This function is actually a way of writing the Vue plug-in, so that we can use the Vue. Use method to automatically install our entire component library when we introduce it in the real project. Install takes two arguments, one is Vue, which we use to register each component. Options allows you to pass in initialization parameters when registering a component, such as the default button size, theme, and so on.

Then we can create a new button.scss file in the SRC /styles directory and write our button styles

/* button.scss */
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";

@include b(button) {
  min-width: 60px;
  height: 36px;
  font-size: 14px;
  color: # 333;
  background-color: #fff;
  border-width: 1px;
  border-radius: 4px;
  outline: none;
  border: 1px solid transparent;
  padding: 0 10px;

  &:active,
  &:focus {
    outline: none;
  }

  &-default {
    color: # 333;
    border-color: # 555;

    &:active,
    &:focus,
    &:hover {
      background-color: rgba($--color-primary.0.3);
    }
  }
  &-primary {
    color: #fff;
    background-color: $--color-primary;

    &:active,
    &:focus,
    &:hover {
      background-color: mix($--color-primary.#ccc);
    }
  }

  &-info {
    color: #fff;
    background-color: $--color-info;

    &:active,
    &:focus,
    &:hover {
      background-color: mix($--color-info.#ccc);
    }
  }
   &-success {
    color: #fff;
    background-color: $--color-success;

    &:active,
    &:focus,
    &:hover {
      background-color: mix($--color-success.#ccc); }}}Copy the code

Finally, we need to import the button style in SRC /styles/index.scss

@import "button";
Copy the code

For a simple experiment, you can write two button components directly under docs/ readme.md

<template>
	<l-button type="primary">Click me</l-button>
</template>
Copy the code

If you want to get the same effect as I did on Arronkler.github. IO /lime-ui/, see github.com/arronKler/l… Configuration in the docs directory of the project. For a more personalized configuration, check out vuePress’s official documentation.

Notice Prompt component

This component will use our dynamic rendering related stuff. The final use of the specific way is like this

this.$notice({
  title: 'tip'.content: this.content || 'content'.duration: 3
})
Copy the code

The effect is something like this

OK, let’s first write a basic source code for this component

Create a notice folder under SRC /packages and create a notice.vue file

<template>
  <div class="lime-notice">
    <div class="lime-notice__main" v-for="item in notices" :key="item.id">
      <div class="lime-notice__title">{{item.title}}</div>
      <div class="lime-notice__content">{{item.content}}</div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      notices: []
    }
  },
  methods: {
    add(notice) {
      let id = +new Date()
      notice.id = id
      this.notices.push(notice)

      const duration = notice.duration
      setTimeout(() => {
        this.remove(id)
      }, duration * 1000)
    },
    remove(id) {
      for(let i = 0; i < this.notices.length; i++) {
        if (this.notices[i].id === id) {
          this.notices.splice(i, 1)
          break;
        }
      }
    }
  }
}
</script>

Copy the code

The code is simple. We declare a container, and then display and hide it by controlling the data. Then we create a notice.js file in the same directory to render dynamically

import Vue from 'vue'
import Notice from './notice.vue'

Notice.newInstance = (properties) = > {
  let props = properties || {}
  const Instance = new Vue({
    render(h) {
      return h(Notice, {
        props
      })
    }
  })

  const component = Instance.$mount()
  document.body.appendChild(component.$el)

  const notice = component.$children[0]

  return {
    add(_notice) {
      notice.add(_notice)
    }, 
    remove(id) {

    }
  }
}

let noticeInstance


export default (_notice) => {
  noticeInstance = noticeInstance || Notice.newInstance()
  noticeInstance.add(_notice)
}
Copy the code

Here we render dynamically so that our component can hang directly under the body, instead of belonging to the root mount point.

Then create a notice.scss file in SRC /styles and write our style file

/* notice.scss */
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";

@include b(notice) {
  position: fixed;
  right: 20px;
  top: 60px;
  z-index: 1000;

  &__main {
    min-width: 100px;
    padding: 10px 20px;
    box-shadow: 0 0 4px #aaa;
    margin-bottom: 10px;
    border-radius: 4px;
  }

  &__title {
    font-size: 16px;
  }
  &__content {
    font-size: 14px;
    color: # 777; }}Copy the code

Last but not least, notice needs to be processed in the SRC /index.js entry file. The complete code looks like this.

import Button from './packages/button'
import Notice from './packages/notice/notice.js'

const components = {
  lButton: Button
}

const install = function (Vue, options = {}) {

  Object.keys(components).forEach(key= > {
    Vue.component(key, components[key]);
  });

  Vue.prototype.$notice = Notice;
}

export default install
Copy the code

We can see that we have hung our $notice method on the Vue prototype, which when called triggers us to dynamically render the component in the notice.js file. At this point we are ready to test it in docs/ readme.md.

<script>
export default() {
  mounted() {
    this.$notice({
        title: 'tip'.content: this.content,
        duration: 3
    })
  }
}
<script>
Copy the code

Package styles and components separately

In order to support on-demand loading, in addition to packaging the entire component library, we need to package the styles and components individually into a single file. There are two things we need to do here

  1. Package individual CSS files
  2. Package individual component content

For the first point, we need to make a change to the build/gen-style.js file and add the buildSeperateCss task. The complete code is as follows

// Other previous code...

function buildSeperateCss(cb) {
  Object.keys(components).forEach(compName= > {
    gulp.src(`.. /src/styles/${compName}.scss`)
      .pipe(sass())
      .pipe(autoprefixer())
      .pipe(cleanCSS())
      .pipe(rename(`${compName}.css`))
      .pipe(gulp.dest('.. /lib/styles'));
  })

  cb()
}

exports.default = gulp.series(buildCss, buildSeperateCss) / / plus buildSeperateCss
Copy the code

For the second point, we can use a new webpack configuration to deal with, create a new build/webpack.com ponent. Js file, write

const path = require('path');
const webpack = require('webpack');
const merge = require('webpack-merge');
const webpackBaseConfig = require('./webpack.base.js');
const components = require('./components.json')
process.env.NODE_ENV = 'production';

const basePath = path.resolve(__dirname, '.. / ')
let entries = {}
Object.keys(components).forEach(key= > {
  entries[key] = path.join(basePath, 'src', components[key])
})

module.exports = merge(webpackBaseConfig, {
  devtool: 'source-map'.mode: "production".entry: entries,
  output: {
    path: path.resolve(__dirname, '.. /lib'),
    publicPath: '/lib/'.filename: '[name].js'.chunkFilename: '[id].js'.// library: 'lime-ui',
    libraryTarget: 'umd'.umdNamedDefine: true
  },
  externals: {
    vue: {
      root: 'Vue'.commonjs: 'vue'.commonjs2: 'vue'.amd: 'vue'}},plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"']}}));Copy the code

Here we refer to a file called Component. json in the Build folder, which I use to identify our components and component paths. In fact, you can get this information automatically by using the script to traverse the SRC/Packages directory directly. The code for build/ Component. json is shown below

{
  "button": "packages/button/index.js"."notice": "packages/notice/notice.js"
}
Copy the code

With all the individual packaging processes configured, we can add the scripts command to the package.json file

{
	"scripts": {
    // ...
		"build:components": "webpack --config build/webpack.component.js",
    "dist": "npm run build:style && npm run build:prod && npm run build:components",
	}
}
Copy the code

OK, now just run the NPM run dist command and it will automatically build the full style content and individual style content for each component, and then package a complete component package and individual packages for each component.

The important thing to note here is that the fields in your package.json file need to be adjusted

{" name ":" lime - UI ", "version" : "1.0.0", "main" : "lib/lime - UI. Min. Js", / /... }Copy the code

The name field indicates the package name when someone else uses your package, and the main field is important because it indicates which entry file is used when someone directly imports your package. In this case, since our webPack package is lib/ lim-ui.min.js, we set it this way.

With that in place, you can package your library by running NPM Run dist, and then publish your library by NPM Publish (NPM Login is required before publishing).

Use your own component library

Direct use of

We can use vue-CLI or other tools to generate a separate demo project to introduce our component library. If your package is not published, you can create a link in your component library project directory using the NPM link or yarn link command (yarn is recommended).

Then use NPM link package_name or Yarn link package_name in your demo directory, where package_name is the package name of your component library, and put it in the entry file of your demo project

import Vue from vue
import LimeUI from 'lime-ui'
import 'lime-ui/lib/styles/lime-ui.css'
// Other code...

Vue.use(LimeUI)
Copy the code

With this set up, the components we created are ready to use in the project

According to the need to load

We’ve talked about global loading in one way, but how about on-demand loading? We’ve talked about that a little bit before

Install the babel-plugin-Component package via NPM and write it in the.babelrc file of your demo project

{
    "plugins": [
        ["component", {
            "libraryName": "lime-ui",
            "libDir": "lib",
            "styleLibrary": {
                "name": "styles",
                "base": false, // no base.css file
                "path": "[module].css"
            }
        }]
    ]
}
Copy the code

The configuration here is to conform to a directory structure of our lime-UI. With this configuration we can load on demand. You can load a Button like this

import Vue from 'vue'
import { Button } from 'lime-ui'

Vue.component('a-button', Button)
Copy the code

As you can see, we didn’t load any styles at this location, because babel-plugin-Component already does that for us, but since we only set the install method in the entry point of the component library to register the component, when we import as needed, You need to manually register yourself.

Theme customization

With that done, it’s easy to customize the theme by creating a globally. SCSS file in the same directory as the entry file of the DEMO project, and then writing code like this.

$--color-primary: red;
@import "~lime-ui/src/styles/index.scss";
Copy the code

Then change the way you import the component library in the entry file

import Vue from vue
import LimeUI from 'lime-ui'
import './global.scss'
// Other code...

Vue.use(LimeUI)
Copy the code

Instead of importing the style for the component library in the entry file, we imported our own custom globally.scss file.

This overwrites the value of the var. SCSS variable in the component library project, and then the rest of the component base styles use their own style content, so that the theme can be customized.

conclusion

Through the introduction of some features of component library and an actual operation case, this paper expounds some basic things to build a set of component library. Hope to through such a share, let us not only to use component library, but can know the birth process of component library and learn about some internal properties of component library, help us in our daily use in the process of “relationship”, when there is a problem or component library needs may not meet think when there is a new starting point, that is enough.

Refer to the reference

  1. Vue$dispatchand$broadcastA:Juejin. Cn/post / 684490…
  2. Component Tests with Vue.js-Matt O’Connell: www.youtube.com/watch?v=OIp…
  3. Nugget Booklet: Vue. Js components
  4. ElementUI: github.com/ElemeFE/ele…
  5. IView: github.com/iview/iview