前言:应上级要求,搭建一个公司内部的vue组件库,由于临近我预计的离职时间,所以只将流程梳理实践了一遍。假设组件库名称为ui-library。
一、使用vue-cli3创建ui-library项目
vue create ui-library
创建项目时选择自定义模板,我的配置如下
记得vue版本需要选择2.x。
ui-library目录如下
二、在根目录添加vue.config.js添加开发和打包配置
vue.config.js代码如下
const path = require('path')
const join = path.join; // 拼接路径
const fs = require('fs');
function resolve(dir) { //获取绝对路径
return path.resolve(__dirname, dir)
}
// 获取文件夹下所有index.js的绝对路径
function getEntries(path) {
let files = fs.readdirSync(resolve(path)); // 获取文件夹下所有文件名称数组
const entries = files.reduce((ret, item) => {
const itemPath = join(path, item) // 获取每个文件路径
const isDir = fs.statSync(itemPath).isDirectory(); // 判断是否为文件夹
if (isDir) { // 文件夹
ret[item] = resolve(join(itemPath, 'index.js')) // 获取index.js的绝对路径
} else { // 不是文件夹
const [name] = item.split('.') // key值
ret[name] = resolve(`${itemPath}`) // 获取path文件夹跟目录下的index.js绝对路径
}
return ret
}, {})
return entries
}
const devConfig = { // 开发配置
lintOnSave: false,
pages: {
index: {
entry: 'examples/main.js', // 入口文件
template: 'public/index.html',
filename: 'index.html'
}
},
configureWebpack: {
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: { // 别名
"@": resolve('packages'),
"assets": resolve('examples/assets'),
"views": resolve("examples/views")
}
}
},
chainWebpack: config => {
// 将新增的packages文件夹加入babel编译
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
})
}
}
const buildConfig = { // 打包配置
outputDir: 'lib', // 输出文件夹名
productionSourceMap: false, // 禁止打包生成源码映射
// 在css.extract.filename上配置样式打包路径和文件名称
css: {
sourceMap: true,
extract: {
filename: 'style/[name].css' // 在lib文件夹中建立style文件夹中,生成对应的css文件。
}
},
configureWebpack: {
entry: {
...getEntries('packages') // 入口文件
},
output: { // 出口文件
filename: '[name]/index.js', // 文件名
libraryTarget: 'commonjs2',
}
},
chainWebpack: config => {
// 在生产环境下也要将新增的packages文件夹加入babel转码编译
config.module
.rule('js')
.include
.add('/packages')
.end()
.use('babel')
.loader('babel-loader')
.tap(options => {
return options
})
// 删除Vue CLI3原先打包编译的一些无用功能
config.optimization.delete('splitChunks') // 删除splitChunks,因为每个组件是独立打包,不需要抽离每个组件的公共js出来
config.plugins.delete('copy') // 删除copy,不要复制public文件夹内容到lib文件夹中。
config.plugins.delete('html') // 删除html,只打包组件,不生成html页面。
config.plugins.delete('preload')
config.plugins.delete('prefetch') // 删除preload以及prefetch,因为不生成html页面,所以这两个也没用。
config.plugins.delete('hmr') // 删除hmr,删除热更新。
config.entryPoints.delete('app') // 删除自动加上的入口App。
// 配置字体的loader
config.module
.rule('fonts')
.use('url-loader')
.tap(option => {
option.fallback.options.name = 'static/fonts/[name].[hash:8].[ext]'
return option
})
}
}
module.exports = process.env.NODE_ENV === 'development' ? devConfig : buildConfig // 判断环境变量使用相应的配置
三、组件库开发
将ui-library根目录下的src文件夹名称修改为examples,用于在本地测试开发的组件(你也可以用来编写组件库使用文档页面,我并没有考虑到这块,所以将examples打包成项目的配置需要自己写,大致思路是把默认的打包配置的入口文件改为examples)
在ui-library跟目录下新建packages文件夹,用于存放组件库核心代码
此时目录如下图
我们编写两种组件,一种为html调用的,另一种为vue全局api调用的
(1)html调用
我们创建一个button组件:
在packages文件夹下创建th-button文件夹
在th-button文件夹下创建src文件夹,src文件夹为组件核心代码
在src文件夹下创建index.vue,内容如下
<!-- 按钮组件 -->
<template>
<button class="th-button">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'ThButton', // name必须,在给组件命名时会用到
data() {
return {
};
}
}
</script>
<style lang='scss' scoped>
@import "./css/index.scss";
</style>
在th-button文件夹下创建index.js,内容如下
// 导入组件,组件必须声明 name
import ThButton from './src/index.vue'
// 为组件提供 install 安装方法,供按需引入
ThButton.install = function(Vue) {
Vue.component(ThButton.name, ThButton)
}
// 导出组件
export default ThButton
简单的button组件就此完成
(2)vue全局api调用
目标:创建一个消息提示组件
在packages文件夹下新建toast文件夹
在toast文件夹下新建src文件夹
在src文件夹下新建index.vue,用来编写主要改组件,内容如下
<!-- 消息提示组件 -->
<template>
<!-- 添加过渡动画 -->
<transition name="toast-from-top" appear @after-leave="handleAfterLeave">
<div class="my-toast" v-show="show">
<div class="my-toast-content">
{{ message }}
</div>
</div>
</transition>
</template>
<script>
export default {
name: "MyToast",
data() {
return {
time: 2000, // toast 展示时长
timer: null, // 存储延时器id
show: false, // toast 是否展示
message: "", // 消息内容
onClose: null // 关闭后的回调
};
},
watch: {
show(val) { // 监听显示,设置延时器自动消失
if (val) {
this.timer = setTimeout(() => {
this.show = false;
this.timer = null;
// 消失后执行onClose
if (typeof this.onClose === 'function') {
this.onClose(this);
}
}, this.time);
}
},
},
methods: {
// 销毁组件
handleAfterLeave() {
this.$destroy() // 销毁组件
this.$el.remove() // 移除页面dom
},
},
};
</script>
<style lang="scss">
.my-toast {
position: fixed;
top: 25%;
width: 100%;
text-align: center;
}
.my-toast-content {
display: inline-block;
text-align: center;
max-width: 80%;
box-sizing: border-box;
padding: 10px;
background-color: hsla(0, 0%, 7%, 0.7);
color: #fff;
border-radius: 3px;
}
.toast-from-top-enter-active,
.toast-from-top-leave-active{
transition: all 0.5s;
}
.toast-from-top-enter,
.toast-from-top-leave-active {
opacity: 0;
transform: translateY(-10px);
}
</style>
在src文件夹下新建main.js,内容如下
import Vue from 'vue'
// 引入组件
import Index from './index.vue'
// 创建toast构造器
let ToastConstructor = Vue.extend(Index)
let instance;
// 定义toast函数
const Toast = function(options) {
options = options || {} // 添加options默认值
if (typeof options === 'string') { // options为字符串,放入展示内容中
options = {
message: options
}
}
instance = new ToastConstructor({ // 创建toast实例,替换data中与options键值相同的项
data: options
})
instance.$mount() // 挂载空dom,生成$el对象
document.body.appendChild(instance.$el) // 将dom放至body下
instance.show = true // 显示组件
}
export default Toast
在toast文件夹下新建index.js,导出toast组件,内容如下
import Toast from './src/main.js'
export default Toast
在packages文件夹下新建index.js,用于全局引入配置,内容如下
// 导入组件
import ThButton from './th-button/index.js'
import Toast from './toast/index.js'
// 需要全局注册的组件放在此
const components = [
ThButton
]
const install = function(Vue) {
if (install.installed) return
// 全局注册组件
components.map(component => Vue.component(component.name, component))
// 声明vue全局函数
Vue.prototype.$toast = Toast
}
if (typeof window !== undefined && window.Vue) {
install(window.Vue)
}
export default {
install,
ThButton,
Toast
}
此时packages文件夹目录如下图
四、本地测试
整个过程和使用组件库流程一致(在本地最好全局引入,局部引入需要一个一个引入),以下是我自己简单的测试内容
在examples下的main.js中引入和注册组件库,main.js内容如下
import Vue from 'vue'
import App from './App.vue'
// 引入组件库
import ThUI from '../packages/index.js'
// 注册组件库
Vue.use(ThUI)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
在App.vue中使用,内容如下
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
<th-button>我是猪</th-button>
<button @click="test">消息提示</button>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
data() {
return {
}
},
methods: {
test() {
// this.$toast('hhhhhhhhhh')
this.$toast({
message: 'wwwwwwwwwwww',
onClose() {
alert('ooooooooooooo')
}
})
}
}
}
</script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
五、发布准备
(1)项目准备
组件库打包,执行
npm run build
打包后生成lib文件夹,结构如下
配置package.json,其中name:组件库的名称,version:版本号(每次上传需要进行更新),设置private:false,description:项目描述,main:组件库入口文件,keyword:搜索关键字。具体内容如下
{
"name": "thsm-ui",
"version": "0.1.4",
"private": false,
"description": "基于vue的桃花水母组件库",
"main": "lib/index/index.js",
"keyword": "thsm-ui th-ui",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^2.6.11"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"vue-template-compiler": "^2.6.11"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
在ui-library下新建.npmignore文件,此文件用于npm发布时配置忽略的文件,内容如下
#忽略目录
/examples
/packages
/public
#忽略文件
vue.config.js
babel.config.js
最后的项目目录如下
(2)npm内网的搭建
使用xshell或其他方式连接到内网服务器,由于服务器是Ubuntu的,安装nodejs方式仅以参考。
首先确认是否装有nodejs(nodejs版本需要在14以上)和npm
node -v
v14.16.0
npm -v
6.14.8
如果没有安装nodejs,Ubuntu的以下作为参考,其他请自行查找
添加下载源,14.x为nodejs大版本号,可按自己需求修改
curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
下载nodejs,执行后需要输入密码
sudo apt-get install -y nodejs
如果没有npm,下载npm
sudo apt-get install npm
安装verdaccio
sudo npm i verdaccio -g
运行verdaccio,查看配置文件位置,然后ctrl+c停止verdaccio运行
verdaccio
verdaccio执行后内容如下,图中红框部分为verdaccio配置文件地址
修改verdaccio配置文件,添加listen地址
进入编辑配置文件
vim /home/yg/.config/verdaccio/config.yaml
备注:按i键进入插入模式,完成后按Esc取消插入,按shift+左边加好键+冒号后输入wq,按enter保存并退出编辑
添加listen,之后可以使用服务器地址:4873访问
listen: 0.0.0.0:4873
下载pm2挂起verdaccio服务,挂起后可以通过服务器地址:4873访问
sudo npm i pm2 -g
挂起verdaccio服务,pm2其他命令请自行搜索
pm2 start verdaccio
六、发布组件库
在ui-library下使用命令行(这里假设npm内网地址为192.168.50.14:4873)
设置npm下载镜像
npm set registry http://192.168.50.14:4873
添加用户,需要输入用户名、密码和邮箱,该操作完成会自动处于登录状态
npm adduser
注:添加用户后,以后使用npm login登录
发布组件库
npm publish
成功后访问192.168.50.14:4873如下图
七、使用组件库
背景:项目使用vue-cli3搭建
下载插件前需要确认当前npm的下载源(不要使用cnpm,两者并不相同)
确认npm下载源
npm get registry
http://192.168.50.14:4873/
如果不是npm内网地址,设置下载源
npm set registry http://192.168.50.14:4873/
下载组件库,我们的组件库名称是packages.json中name值thsm-ui
npm i thsm-ui -S
备注:如果你需要一次性下载所有依赖,可以直接执行npm i,npm会现在下载源上寻找,然后到npm外网寻找
(1)整体引入
在/src/main.js中加入下面代码
import ThUI from 'thsm-ui'
Vue.use(ThUI)
(2)局部引入
安装babel插件,为什么需要安装babel
npm install babel-plugin-import -D
在根目录babel.config.js添加配置,内容如下
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
plugins: [
// 其他组件库的babel配置
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
],
// 自己开发的组件库的babel配置
[
"import",
{
"libraryName": "thsm-ui",
"style": (name) =>{
const cssName = name.split('/')[2];
return `thsm-ui/lib/style/${cssName}.css`
}
}
]
]};
在/src/main.js中添加以下内容
import { ThButton, Toast } from 'thsm-ui'
Vue.use(ThButton)
Vue.use(Toast)
调用方式
// th-button
<th-button>test<th-button>
// toast
this.$toast('测试')
八、参考
Vue CLI3搭建组件库并实现按需引入实战操作
使用verdaccio 搭建私有npm 服务器