如何手把手打造本身的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

相關文章
相關標籤/搜索