和你们分享一下建立 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