博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

前言:应上级要求,搭建一个公司内部的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 服务器