和你們分享一下建立 Vue 組件庫的過程, 方便你們參考, 項目的地址在這裏:css
項目的結構以下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
複製代碼
格式化用到了 eslint
和 prettier
yarn add eslint prettier eslint-config-prettier eslint-plugin-jest eslint-plugin-vue husky pretty-quick -D
添加相應的配置到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 爲你的組件提供良好的生產環境下的展現, 你能夠經過它寫組件的各類用例, 結合它提供的各類插件, 你的組件使用文檔能夠實現實時交互, 組件點擊監聽, 查看源代碼, 寫 markdown 文檔, 不一樣視窗下展現組件等功能.
有了 storybook 的幫助, 咱們就不必安裝各類 npm 包, 節省了咱們搭建組件用例的時間, 而且能夠更直觀地給使用者展現組件的各類用法.
下面是 storybook 的安裝配置
yarn add @storybook/vue vue-loader vue-template-compiler @babel/core babel-loader babel-preset-vue -D
複製代碼
在.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 配置到根目錄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