To share with you the process of creating Vue component library, for your reference, the project address is here:

Github.com/gaoljie/vue…

The structure of the project is as follows

Build (Webpack configuration) lib (Pack file location) - button (load component location on demand) - index. js-theme (component style) - - base.css (common style) - - button.css(component style) - index.js (global reference entry) SRC (project file) - Assets (public resources) - Components (vue components) - button - button.vue (component logic) - button.scss (Component style) -- button.test.js (component test) -- button.story.js (component use case) -- index.js (component entry) -- directives (vue directive command) -- locale (Internationalization) - mixins (vue mixins) - styles (styles) - variables (index. SCSS (variable entry) - color. SCSS (color variable) - - Vendors (Public styles, style resets) - index.scss (style entry) - utils (public methods) - index.js (Project entry)Copy the code

Initialize the project

Create a project

mkdir vue-uikit
cd vue-uikit
mkdir src
Copy the code

Initialize the git

git init
Copy the code

Create a.gitignore file in the project root directory

node_modules/
Copy the code

Initialize the NPM

yarn init
Copy the code

Install the vue

yarn add vue -D
Copy the code

We install vUE in devDependencies because our component library depends on the vUE package installed by the user, and we do not need to package our own components with vUE.

Install webpack

yarn add webpack webpack-cli webpack-merge -D
Copy the code

formatting

Formatting uses esLint and prettier

yarn add eslint prettier eslint-config-prettier eslint-plugin-jest eslint-plugin-vue husky pretty-quick -D

  • Husky (Pre-Commit tool)
  • Pretty -quick (formatting git changed with prettier)
  • Eslint-plugin-jest, eslint-plugin-vue, eslint-plugin-prettier esLint-plugin-jest, eslint-plugin-vue, eslint-plugin-prettier

Add the appropriate configuration to package.json

"husky": {
    "hooks": {
      "pre-commit": "pretty-quick --staged && eslint --ext .js,.vue src"
    }
  },
  "eslintConfig": {
    "env": {
      "node": true
    },
    "extends": [
      "eslint:recommended",
      "plugin:jest/recommended",
      "plugin:vue/recommended",
      "plugin:prettier/recommended"
    ]
  },
Copy the code

Add.eslintrc to the root directory

{
  "env": {
    "node": true
  },
  "extends": [
    "eslint:recommended"."plugin:jest/recommended"."plugin:vue/recommended"."prettier"]}Copy the code

Husky is used to format the code before each commit to ensure consistency.

Initialize the style structure

Create a color. SCSS file in SRC /styles/variables as shown in the top project structure

$color_primary: #ff5722;
$color_disabled: #d1d1d1;

Copy the code

Create index.scss reference color.scss in the sibling directory

@import "color";
Copy the code

Do the same for creating other types of style variables later

SCSS file is created on SRC /styles/vendors to standardize style differences between browsers, and the source code can be viewed on Github.

Then create index.scss under SRC /styles as the style entry

@import "vendors/normalize";
Copy the code

Install the appropriate NPM package

yarn add sass-loader node-sass style-loader css-loader -D
Copy the code

Create components

Create button.vue file in SRC /components/button

<template>
  <button class="ml-button" :class="btnClass">
    <slot></slot>
  </button>
</template>

<script>
const prefix = "ml-button";
export default {
  name: "MlButton",
  props: {
    type: {
      type: String,
      default: "primary"
    },
    disabled: {
      type: Boolean,
      default: false
    },
    round: {
      type: Boolean,
      default: false
    }
  },
  computed: {
    btnClass() {
      return [
        `${prefix}--${this.type}`,
        this.disabled ? `${prefix}--disabled` : "",
        this.round ? `${prefix}--round` : ""
      ];
    }
  }
};
</script>

<style lang="scss">
@import "../../styles/variables/index";
.ml-button {
  color: white;
  font-size: 14px;
  padding: 12px 20px;
  border-radius: 5px;
  &:focus {
    cursor: pointer;
  }
  &--primary {
    background-color: $color-primary;
  }
  &--disabled {
    background-color: $color_disabled;
    color: rgba(255, 255, 255, 0.8);
  }
  &--round {
    border-radius: 20px;
  }
}
</style>
Copy the code

Create index.js in the sibling directory to use as a component entry

import MlButton from "./button.vue";
export default MlButton;
Copy the code

Install the storybook

Storybook provides a good presentation of your components in the production environment. You can write various use cases for your components through storybook. Combined with various plug-ins, your components can interact with documents in real time, listen to component clicks, view source code, write Markdown documents, display components in different Windows and so on.

With the help of Storybook, we do not need to install various NPM packages, saving us time to build component use cases, and can more intuitively show users the various uses of components.

Here is the storybook installation configuration

yarn add @storybook/vue vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue -D
Copy the code
  • @storybook/vue (Storybook Vue Core package)
  • Vue-loader (Webpack loader, used by Webpack to parse vUE single file components)
  • Vue-template-compiler (Vue compiler used by the Vue loader, which must be consistent with the vUE package version)
  • @babel/core
  • Babel-loader (webpack loader, let webpack use Babel to parse JS files)
  • Babel-preset – Vue (Plugin used by Babel to parse Vue JSX)

Create the storybook config file in.storybook/config.js

import { configure } from "@storybook/vue";

function loadStories() {
  const req = require.context(".. /src".true, /\.story\.js$/);
  req.keys().forEach(filename= > req(filename));
}

configure(loadStories, module);
Copy the code

This configuration file automatically loads the *.story.js file in the SRC directory

Because the Vue component uses SCSS, the storybook webpack configuration webpack.config.js needs to be created in the.storybook directory

const path = require("path");

module.exports = async ({ config, mode }) => {
  config.module.rules.push({
    test: /\.scss$/.use: ["vue-style-loader"."css-loader"."sass-loader"].include: path.resolve(__dirname, ".. /")});// Return the altered config
  return config;
};
Copy the code

Create the storybook use case button.story.js in SRC/Components /button

import { storiesOf } from "@storybook/vue";
import MlButton from "./button.vue";
storiesOf("Button".module).add("Primary", () = > ({components: { MlButton },
  template: '<ml-button type="primary">Button</ml-button>'
}));
Copy the code

Add NPM Script to package.json to make it easy to start and package the Storybook service

"scripts": {
 "dev": "start-storybook",
 "build:storybook": "build-storybook -c .storybook -o dist",
}
Copy the code

Start the storybook

yarn dev
Copy the code

With Storybook, we don’t have to write component use cases and build webPack configurations anymore

Project package

The storybook is just for us to show the components, and we need to package the components for other projects to use.

Entry.js is first created in the SRC directory to import the component

export { default as MlButton } from "./components/button";
Copy the code

Then create other components that need to be added to this file

Create index.js in the SRC directory as the project entry

// Introduce styles
import "./styles/index.scss";
// Import components
import * as components from "./entry";

// Create an install method that registers all components with vue
const install = function(Vue) {
  Object.keys(components).forEach(key= > {
    Vue.component(key, components[key]);
  });
};

// auto install
if (typeof window! = ="undefined" && window.Vue) {
  install(window.Vue);
}

const plugin = {
  install
};

export * from "./entry";

export default plugin;
Copy the code

Webpack configuration

Create a webpack.base.js file in the build folder and fill in the common configuration

const VueLoaderPlugin = require("vue-loader/lib/plugin");

module.exports = {
  module: {
    rules: [{test: /\.js$/.exclude: /(node_modules)/.use: {
          loader: "babel-loader"}}, {test: /\.vue$/.use: {
          loader: "vue-loader"}}},plugins: [new VueLoaderPlugin()],
  externals: {
    vue: {
      root: "Vue".commonjs: "vue".commonjs2: "vue".amd: "vue"}}};Copy the code

Add vUE to externals so that vUE is not packaged together.

Create webpack.config.js under the same directory as the package entry

const path = require("path");
const merge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base.js");

module.exports = merge(webpackBaseConfig, {
  entry: {
    main: path.resolve(__dirname, ".. /src/index")},output: {
    path: path.resolve(__dirname, ".. /lib"),
    filename: "vue-uikit.js".library: "vue-uikit".libraryTarget: "umd"
  },
  module: {
    rules: [{test: /\.scss$/.use: ["vue-style-loader"."css-loader"."sass-loader"]}]}});Copy the code

Add a package command to package.json

"scripts": {
    "build": "webpack --mode production --config build/webpack.config.js"
 }
Copy the code

Run yarn Build to package the package

Verify the package file

After packaging, you can see that the main project has a lib directory, which contains a vue-UIkit file. We need to verify whether this file is properly packaged, first of all, we need to change the package.json entry

  "main": "lib/vue-uikit.js"
Copy the code

So that when other projects introduce component libraries, they know where the entry is: lib/vue-uikit.js

Before formal packaging, you can run Yarn Link. The following message is displayed

success Registered "vue-uikit".
info You can now run `yarn link "vue-uikit"` in the projects where you want to use this package and it will be used instead.
Copy the code

You can create a new project in your own project, or with vue-CLI, and run YARN Link “vue-UIKit” in it. Your project node_modules will temporarily add your component library, which can then be introduced as normal

<template>
  <div id="app">
    <ml-button type="primary">button</ml-button>
  </div>
</template>

<script>
import UIKit from "vue-uikit";
import Vue from "vue";
Vue.use(UIKit);

export default {
  name: "app"
};
</script>
Copy the code

According to the need to load

Sometimes a page only needs a few components and does not want to introduce the entire library, so it is better for the library to implement on-demand functionality. See the example of element-UI, which uses a Babel component: babel-plugin-Component.

After introducing the plugin to your project, code like this

import { Button } from "components";
Copy the code

It’s going to be resolved into

var button = require("components/lib/button");
require("components/lib/button/style.css");
Copy the code

This plugin can be used only in lib/vue-uikit.js. The plugin can be used only in lib/button. Also reference the corresponding style file, so our component library packaging should also be packaged according to the component classification into the corresponding folder, also need to extract the style.

We first classify component packaging, first in the build/webpack.com ponent. Js create the corresponding webpack configuration

const path = require("path");
const merge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base.js");

module.exports = merge(webpackBaseConfig, {
  entry: {
    "ml-button": path.resolve(__dirname, ".. /src/components/button/index.js")},output: {
    path: path.resolve(__dirname, ".. /lib"),
    filename: "[name]/index.js".libraryTarget: "umd"
  },
  module: {
    rules: [{test: /\.scss$/.use: ["vue-style-loader"."css-loader"."sass-loader"]}]}});Copy the code

Then create the corresponding script in package.json

"build:com": "webpack --mode production --config build/webpack.component.js"
Copy the code

Then run YARN Build :com and, once packaged, you can see that the current directory structure looks like this

lib
 - ml-button
   - index.js
 - vue-uikit.js
Copy the code

Modify the introduced code in your main project to see if it works

<template> <div id="app"> <ml-button type="positive">button</ml-button> </div> </template> <script> import MlButton from  "vue-uikit/lib/ml-button"; export default { name: "app", components: { MlButton } }; </script>Copy the code

Now there are a few more questions. The entry in webpack.component.js looks like this

 entry: {
    "ml-button": path.resolve(__dirname, ".. /src/components/button/index.js")}Copy the code

In the future, each time a new component is added, we need to manually add an entry, preferably automatically generated according to the file directory, and according to the requirements of babel-plugin-compoent, we need to extract the style file.

To solve the entry problem, install the glob package to obtain the file that matches the corresponding rule yarn add glob -d

const glob = require("glob");

console.log(glob.sync("./src/components/**/index.js"));

// [ './src/components/button/index.js' ]
Copy the code

By processing the returned array, you can generate the corresponding entry

const entry = Object.assign(
  {},
  glob
    .sync("./src/components/**/index.js")
    .map(item= > {
      return item.split("/") [3];
    })
    .reduce((acc, cur) = > {
      acc[`ml-${cur}`] = path.resolve(
        __dirname,
        `.. /src/components/${cur}/index.js`
      );
      return{... acc }; }, {}));Copy the code

For extracting style files, install the mini-CSs-extract-Plugin package and configure it:

yarn add mini-css-extract-plugin -D
Copy the code

The final webpack.component.js looks like this

const path = require("path");
const merge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const glob = require("glob");

const entry = Object.assign(
  {},
  glob
    .sync("./src/components/**/index.js")
    .map(item= > {
      return item.split("/") [3];
    })
    .reduce((acc, cur) = > {
      acc[`ml-${cur}`] = path.resolve(
        __dirname,
        `.. /src/components/${cur}/index.js`
      );
      return{... acc }; }, {}));module.exports = merge(webpackBaseConfig, {
  entry: entry,
  output: {
    path: path.resolve(__dirname, ".. /lib"),
    filename: "[name]/index.js".libraryTarget: "umd"
  },
  module: {
    rules: [{test: /\.scss$/.use: [MiniCssExtractPlugin.loader, "css-loader"."sass-loader"]]}},plugins: [
    new MiniCssExtractPlugin({
      filename: "theme/[name].css"]}}));Copy the code

The generated style files are placed separately in the lib/theme folder.

The import MlButton from ‘vue-UIkit /lib/ml-button’ does not contain the style and needs to be reintroduced:

<template> <div id="app"> <ml-button type="positive">button</ml-button> </div> </template> <script> import MlButton from  "vue-uikit/lib/ml-button"; import "vue-uikit/lib/theme/ml-button.css"; export default { name: "app", components: { MlButton } }; </script>Copy the code

We certainly don’t want to add styles every time we add a new component when loading on demand:

import MlButton from "vue-uikit/lib/ml-button";
import "vue-uikit/lib/theme/ml-button.css";
import MlMessage from "vue-uikit/lib/ml-message";
import "vue-uikit/lib/theme/ml-message.css";
Copy the code

This is where babel-plugin-compnent comes in.

After the main project (not the component library) installs babel-plugin-compnent, add the following code to the Babel configuration file

plugins: [
  [
    "component",
    {
      libraryName: "vue-uikit".// This plugin is used when introducing vue-UIKit
      styleLibraryName: "theme" // The style file is found in the theme}]].Copy the code

Rerun the main project and you will find an error

* vue-uikit/lib/theme/base.css in./node_modules/cache-loader/dist/cjs.js?? ref--12-0! ./node_modules/babel-loader/lib! ./node_modules/cache-loader/dist/cjs.js?? ref--0-0! ./node_modules/vue-loader/lib?? vue-loader-options! ./src/App.vue? vue&type=script&lang=js&
Copy the code

The theme/base.css file cannot be found, babel-plugin-compnent will introduce common styles for all components by default, so we need to change the Webpack configuration again

const entry = Object.assign(
  {},
  {
    base: path.resolve(__dirname, ".. /src/styles/index.scss")
  },
  glob
    .sync("./src/components/**/index.js")
    .map(item= > {
      return item.split("/") [3];
    })
    .reduce((acc, cur) = > {
      acc[`ml-${cur}`] = path.resolve(
        __dirname,
        `.. /src/components/${cur}/index.js`
      );
      return{... acc }; }, {}));Copy the code

After packing, we found a lib/base folder. This is because webpack generates a js file by default when packing index. SCSS. We can delete it manually or use webpack component

yarn add rimraf -D

Rewrite the NPM script

 "scripts": {
    "build": "rimraf lib && yarn build:web && yarn build:com && rimraf lib/base",
    "build:web": "webpack --mode production --config build/webpack.config.js",
    "build:com": "webpack --mode production --config build/webpack.component.js"
  },
Copy the code

Run YARN Build, at which point the main project should be running properly

Style optimization

In addition to extracting the style file, we can also make some optimizations, such as compression and adding prefix

Install postCSS and related plug-ins

yarn add postcss autoprefixer cssnano -D

Autoprefix is used to add prefixes and cssnano is used to compress style files

After the installation is complete, add the corresponding configuration postcss.config.js to the root directory

module.exports = {
  plugins: {
    autoprefixer: {},
    cssnano: {}
  }
};
Copy the code

Add postCSS-loader to Webpack.

In addition, it is best to extract all styles into a single file, so that when importing a component library globally, you can easily import style files in the HTML header independently, avoiding FOUC problems. The final WebPack configuration looks like this

webpack.component.js

const path = require("path");
const merge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const glob = require("glob");

const entry = Object.assign(
  {},
  {
    base: path.resolve(__dirname, ".. /src/styles/index.scss")
  },
  glob
    .sync("./src/components/**/index.js")
    .map(item= > {
      return item.split("/") [3];
    })
    .reduce((acc, cur) = > {
      acc[`ml-${cur}`] = path.resolve(
        __dirname,
        `.. /src/components/${cur}/index.js`
      );
      return{... acc }; }, {}));module.exports = merge(webpackBaseConfig, {
  entry: entry,
  output: {
    path: path.resolve(__dirname, ".. /lib"),
    filename: "[name]/index.js".libraryTarget: "umd"
  },
  module: {
    rules: [{test: /\.scss$/.use: [
          MiniCssExtractPlugin.loader,
          "css-loader"."postcss-loader"./ / add postcss - loader
          "sass-loader"]]}},plugins: [
    new MiniCssExtractPlugin({
      filename: "theme/[name].css"]}}));Copy the code

webpack.config.js

const path = require("path");
const merge = require("webpack-merge");
const webpackBaseConfig = require("./webpack.base.js");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = merge(webpackBaseConfig, {
  entry: {
    main: path.resolve(__dirname, ".. /src/index")},output: {
    path: path.resolve(__dirname, ".. /lib"),
    filename: "vue-uikit.js".library: "vue-uikit".libraryTarget: "umd"
  },
  module: {
    rules: [{test: /\.scss$/.use: [
          MiniCssExtractPlugin.loader,
          "css-loader"."postcss-loader"."sass-loader"]]}},plugins: [
    // Extract all style files to index.css
    new MiniCssExtractPlugin({
      filename: "index.css"]}}));Copy the code

Repackage yarn Build

Because we’re pulling styles out, global imports require additional styles

import Vue from 'vue' import UIkit from 'vue-uikit' import
'vue-uikit/lib/index.css' Vue.use(UIkit)
Copy the code

Unit testing

For unit tests we’ll use @vue/test-utils

Yarn add Jest @vue/test-utils jest-serializer-vue vue-jest babel-jest @babel/preset-env babel-core@^7.0.0-bridge.0 -d

  • Jest-serializer -vue (Snapshot test)
  • Babel-core @^7.0.0-bridge.0

Add the jest configuration to the root directory jest.config.js

module.exports = {
  moduleFileExtensions: ["js", "json", "vue"],
  transform: {
    "^.+\\.js$": "<rootDir>/node_modules/babel-jest",
    ".*\\.(vue)$": "vue-jest"
  },
  snapshotSerializers: ["jest-serializer-vue"]
};

Copy the code

Add the Babel configuration to the root directory. Babelrc

{
  "presets": [
    "@babel/preset-env"]."env": {
    "test": {
      "presets": [["@babel/preset-env",
          {
            "targets": {
              "node": "current"}}]]}}}Copy the code

Add test command to NPM script: “test”: “jest”

Add test cases to SRC/components/button/button test. Js

import { shallowMount } from "@vue/test-utils";
import MlButton from "./button.vue";

describe("Button", () => {
  test("is a Vue instance", () = > {const wrapper = shallowMount(MlButton);
    expect(wrapper.isVueInstance()).toBeTruthy();
  });

  test("positive color", () = > {const wrapper = shallowMount(MlButton, {
      propsData: {
        type: "positive"}}); expect(wrapper.classes("ml-button--positive")).toBeTruthy();
  });
});
Copy the code

Run the yarn test

The next best thing is to put the unit tests into PreCommit and check them every time the code commits

 "scripts": {
    "test": "jest",
    "lint": "pretty-quick --staged && eslint --ext .js,.vue src",
    "dev": "start-storybook",
    "build:storybook": "build-storybook -c .storybook -o dist",
    "build": "rimraf lib && yarn build:web && yarn build:com && rimraf lib/base",
    "build:web": "webpack --mode production --config build/webpack.config.js",
    "build:com": "webpack --mode production --config build/webpack.component.js"
  },
  "husky": {
    "hooks": {
      "pre-commit": "yarn test && yarn lint"
    }
  },
Copy the code

release

First go to www.npmjs.com/ to register an account, after registration

Log in from the component library

npm login

And then it’s ready to publish

npm publish