如何手把手打造本身的Vue组件库

和你们分享一下建立 Vue 组件库的过程, 方便你们参考, 项目的地址在这里:css

github.com/gaoljie/vue…html

项目的结构以下vue

build (webpack配置)
lib (打包文件位置)
- button (按需加载组件位置)
- - index.js
- theme (组件样式)
- - base.css (公用样式)
- - button.css(组件样式)
- index.js (全局引用入口)
src (项目文件)
- assets (公共资源)
- components (vue组件)
- button
- - button.vue (组件逻辑)
- - button.scss (组件样式)
- - button.test.js (组件测试)
- - button.story.js (组件用例)
- - index.js (组件入口)
- directives (vue directive 命令)
- locale (国际化)
- mixins (vue mixins)
- styles (样式)
- - variables (样式变量)
- - - index.scss (变量入口)
- - - color.scss (颜色变量)
- - vendors (公共样式, 样式重置)
- - index.scss (样式入口)
- utils (公用方法)
- index.js (项目入口)
复制代码

初始化项目

建立项目node

mkdir vue-uikit
cd vue-uikit
mkdir src
复制代码

初始化 gitwebpack

git init
复制代码

在项目根目录建立 .gitignore文件git

node_modules/
复制代码

初始化 npmgithub

yarn init
复制代码

安装 vueweb

yarn add vue -D
复制代码

之因此把vue安装在devDependencies, 是由于咱们的组件库是依赖于使用者的安装的vue包的, 咱们打包本身组件并不须要把vue一块儿打包进去.vue-cli

安装 webpacknpm

yarn add webpack webpack-cli webpack-merge -D
复制代码

格式化

格式化用到了 eslintprettier

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

  • husky (pre commit 的工具)
  • pretty-quick (用 prettier 格式化 git changed 文件)
  • eslint-plugin-jest, eslint-plugin-vue, eslint-plugin-prettier (eslint 相关插件, 用于和 jest 单元测试, vue 文件和 prettier 兼容)

添加相应的配置到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"
    ]
  },
复制代码

添加 .eslintrc 到根目录

{
  "env": {
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:jest/recommended",
    "plugin:vue/recommended",
    "prettier"
  ]
}
复制代码

每次 commit 代码以前会用husky格式化代码, 确保代码风格统一.

初始化样式结构

按照最上面的项目结构在 src/styles/variables 建立 color.scss 文件

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

复制代码

在同级目录下建立 index.scss引用 color.scss

@import "color";
复制代码

以后若是建立其余类型的样式变量也是一样的步骤

src/styles/vendors建立normalize.scss文件, 用来规范各个浏览器之间的样式差别, 源码能够在 github 上面看.

以后在src/styles下建立index.scss做为样式入口

@import "vendors/normalize";
复制代码

安装相应 npm 包

yarn add sass-loader node-sass style-loader css-loader -D
复制代码

建立组件

src/components/button 建立 button.vue 文件

<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>
复制代码

同级目录下建立index.js用做组件入口

import MlButton from "./button.vue";
export default MlButton;
复制代码

安装 storybook

storybook 为你的组件提供良好的生产环境下的展现, 你能够经过它写组件的各类用例, 结合它提供的各类插件, 你的组件使用文档能够实现实时交互, 组件点击监听, 查看源代码, 写 markdown 文档, 不一样视窗下展现组件等功能.

有了 storybook 的帮助, 咱们就不必安装各类 npm 包, 节省了咱们搭建组件用例的时间, 而且能够更直观地给使用者展现组件的各类用法.

下面是 storybook 的安装配置

yarn add @storybook/vue vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue -D
复制代码
  • @storybook/vue (storybook vue 核心包)
  • vue-loader (webpack loader, 用于 webpack 解析 vue 单文件组件)
  • vue-template-compiler (vue loader 要用到的 vue 编译器, 须要和 vue 包版本保持一致)
  • @babel/core (babel 核心包)
  • babel-loader (webpack loader, 让 webpack 使用 babel 解析 js 文件)
  • babel-preset-vue (babel 用于解析 vue jsx 的插件)

.storybook/config.js建立 storybook config 文件

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

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

configure(loadStories, module);
复制代码

这个配置文件会自动加载src目录下的*.story.js文件

由于 vue 组件用到了scss, 须要在.storybook目录下建立 storybook 的 webpack 配置 webpack.config.js

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;
};
复制代码

src/components/button 建立 storybook 用例 button.story.js

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>'
}));
复制代码

package.json添加 npm script, 便于启动以及打包 storybook 服务

"scripts": {
 "dev": "start-storybook",
 "build:storybook": "build-storybook -c .storybook -o dist",
}
复制代码

启动 storybook

yarn dev
复制代码

有了 storybook, 咱们没必要再辛苦的写组件的用例, 也不用去搭建 webpack 的配置了~

项目打包

storybook 只是帮咱们用来展现组件, 咱们还须要打包组件, 让其余的项目使用.

首先在src目录下建立entry.js用来引入组件

export { default as MlButton } from "./components/button";
复制代码

以后建立其余组件也须要添加到这个文件里面

src目录下建立index.js做为项目入口

// 引入样式
import "./styles/index.scss";
// 引入组件
import * as components from "./entry";

//建立 install 方法, 方法里面将全部组件注册到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;
复制代码

webpack 配置

build文件夹下建立webpack.base.js文件, 填入公用的一些配置

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"
    }
  }
};
复制代码

将 vue 添加到 externals 里面, 这样就不会把vue一块儿打包进去了.

同个目录下面建立webpack.config.js做为打包入口

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"]
      }
    ]
  }
});
复制代码

package.json添加打包命令

"scripts": {
    "build": "webpack --mode production --config build/webpack.config.js"
 }
复制代码

运行yarn build进行打包

验证打包文件

打包以后能够看到主项目多了一个lib目录, 里面有一个vue-uikit文件, 咱们要先验证下这个文件是否有正常打包, 首先要改package.json的入口

"main": "lib/vue-uikit.js"
复制代码

这样在其余项目引入组件库的时候, 才能够知道入口在哪: lib/vue-uikit.js

在正式打包以前, 能够运行yarn link, 会有如下提示

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.
复制代码

你能够在本身的项目, 或者用 vue-cli 建立一个新的项目, 里面运行 yarn link "vue-uikit", 你的项目 node_modules 会临时添加你的组件库, 这时候就能够正常引入组建库了

<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>
复制代码

按需加载

有时候页面只须要用到几个组件, 并不想引入整个组件库, 因此组件库最好可以实现按需加载的功能, 参考element-ui 的例子, 它用了一个 babel 组件: babel-plugin-component.

在你的项目引入这个 plugin 以后, 相似这样的代码

import { Button } from "components";
复制代码

会被解析成

var button = require("components/lib/button");
require("components/lib/button/style.css");
复制代码

可是以前咱们打包的时候只打包了一个 lib/vue-uikit.js 文件, 组件库不进行相应的配置是没办法直接使用这个插件的, 在上面的代码咱们能够看到它按需引用了 lib/button文件夹里的index.js文件, 还引用了相应的样式文件, 因此咱们组件库打包也要按照组件分类打包到对应的文件夹, 还须要把样式提取出来.

咱们先把组件分类打包, 首先在build/webpack.component.js建立相应的 webpack 配置

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"]
      }
    ]
  }
});
复制代码

而后在package.json建立相应的脚本

"build:com": "webpack --mode production --config build/webpack.component.js"
复制代码

以后运行 yarn build:com, 打包好以后, 能够看到当前的目录结构是这样的

lib
 - ml-button
   - index.js
 - vue-uikit.js
复制代码

在你的主项目改一下引入的代码, 看是否能够正常使用

<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>
复制代码

如今还有几个问题, webpack.component.js里面的entry是这样的

entry: {
    "ml-button": path.resolve(__dirname, "../src/components/button/index.js")
  }
复制代码

之后每次添加一个新组件的话还须要手动添加一个entry, 最好是可以按照文件目录自动生成, 还有按照babel-plugin-compoent的要求,咱们是须要把样式文件提取出来的.

首先解决entry的问题, 咱们能够安装glob包, 来获取匹配相应规则的文件 yarn add glob -D

const glob = require("glob");

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

// [ './src/components/button/index.js' ]
复制代码

经过对返回的数组进行处理, 就能够生成相应的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 };
    }, {})
);
复制代码

关于样式文件的提取, 能够安装 mini-css-extract-plugin包并进行配置:

yarn add mini-css-extract-plugin -D
复制代码

最后的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(
  {},
  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"
    })
  ]
});
复制代码

生成的样式文件会单独放在lib/theme文件夹.

打包完以后会发现主项目(引用组件库的项目)的样式不见了, 由于样式已经提取出来, import MlButton from 'vue-uikit/lib/ml-button' 是不会包含样式的, 须要从新引入:

<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>
复制代码

咱们确定不但愿按需加载的时候每次新加一个组件就须要添加一次样式:

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";
复制代码

这时候就须要babel-plugin-compnent的帮助了.

主项目(不是组件库)安装完babel-plugin-compnent以后, 在 babel 的配置文件添加以下代码

plugins: [
  [
    "component",
    {
      libraryName: "vue-uikit", //引入vue-uikit的时候会用此插件
      styleLibraryName: "theme" //样式文件在theme里面找
    }
  ]
];
复制代码

从新运行主项目, 会发现有报错

* 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&
复制代码

提示没法找到theme/base.css文件, babel-plugin-compnent会默认引入全部组件须要的公共样式, 全部咱们要从新改一下 webpack 配置

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 };
    }, {})
);
复制代码

打包以后发现多了一个lib/base文件夹, 这是由于 webpack 打包index.scss 的时候回默认生成一个 js 文件, 咱们能够手动删掉或者用 webpack 组件, 这里我安装了一个rimraf

yarn add rimraf -D

从新改一下 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"
  },
复制代码

运行 yarn build, 这时候主项目应该能够正常运行了

样式优化

除了把样式文件抽取出来, 咱们还能够作一些优化, 好比压缩和加 prefix

安装postcss和相关插件

yarn add postcss autoprefixer cssnano -D

autoprefix 用来添加前缀, cssnano用来压缩样式文件

安装完成以后在根目录添加相应的配置 postcss.config.js

module.exports = {
  plugins: {
    autoprefixer: {},
    cssnano: {}
  }
};
复制代码

添加postcss-loader到 webpack 中.

另外, 最好把全部的样式都抽取成一个文件, 这样在全局引入组件库的时候,方便独立在 html 头部引入样式文件, 避免 FOUC 问题. 最后的 webpack 配置是这样的

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", //添加postcss-loader
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "theme/[name].css"
    })
  ]
});
复制代码

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: [
    //把全部样式文件提取到index.css
    new MiniCssExtractPlugin({
      filename: "index.css"
    })
  ]
});
复制代码

从新打包 yarn build

由于咱们把样式提出起来, 全局引入须要另外引入样式

import Vue from 'vue' import UIkit from 'vue-uikit' import
'vue-uikit/lib/index.css' Vue.use(UIkit)
复制代码

单元测试

单元测试咱们会用到@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 (快照测试)
  • babel-core@^7.0.0-bridge.0 (jest 用的不是 babel 7, 须要另外下载这个包兼容)

添加 jest 配置到根目录jest.config.js

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

复制代码

添加 babel 配置到根目录 .babelrc

{
  "presets": [
    "@babel/preset-env"
  ],
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "node": "current"
            }
          }
        ]
      ]
    }
  }
}
复制代码

添加测试命令到 npm script: "test": "jest"

添加测试用例到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();
  });
});
复制代码

运行yarn test

接下来最好把单元测试放到 precommit, 在每次代码提交以前检查一次

"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"
    }
  },
复制代码

发布

先到 www.npmjs.com/ 注册帐号, 注册完以后

在组件库登陆

npm login

以后就能够发布了

npm publish

相关文章
相关标签/搜索