Vue UI 组件库开发 - 从开发到发布到 npm 并支持按需加载

发布于 2023-10-22 12:00:38 字数 22153 浏览 40 评论 0

在参考过一些资料和开源的 UI 组件库后,写下了这篇文章,希望能给大家一些帮助。

UI 组件库前后摸索了差不多近一个星期才勉强学会,其实组件库开发不算特别复杂(复杂组件当我没说 #滑稽保命),期间主要是卡在了目录组织和打包的问题上,目录组织的问题主要是涉及到按需加载的问题,如果一把梭全部引入反而没那么复杂。然后打包问题也是因为按需加载的需要,需要配置不同的 webpack 配置文件,这里摸索了特别久,最后再看了 vant、nutui 这些开源的组件库后,还算是入门了吧,这里其实我主要参考了 nutui 的组织结构,webpack 的配置基本也是这么来的。文章虽然含金量不多,希望能帮助到需要自己学习组件库开发的同学。

好了,废话不多说,直接开始!

搭建项目

本次开发没有基于 Vue CLI 来搭建开发环境,而是基于我上次 webpack4 自己搭建的环境来构建的环境,虽然官方 CLI 也可以,而且也有自己的库模式来发布组件库。但是为了能够更多的有自己自定义的选项,还是没选择官方脚手架。如果你想要使用官方 CLI 来开发,那么本文章虽然不能手把手教你,但应该还是可以起到抛砖引玉的作用。

接下来我们需要做这几步,先把目录结构定下来:

1.png

examples 文件夹下用来展示你的组件基本用法。其实相当于把 src 的文件目录改为了 examples

packages 文件夹下用来书写你的组件。

修改了结构后还涉及到一些细微的修改,比如文件夹改为你项目的名字(我这儿叫 cookie-ui),package.json 里面的 name 根据自己的需要来修改。

postcss.config.js 里面关于 px 转 rem 和 px 转 vw|vh 那个也要去掉,这儿我们使用 px 单位来开发。

同时我们需要修改我们的入口,因为默认搭建出来的入口是 src。在 bulid 目录下修改 webpack.config.js 中以下代码:

module.exports = {
  /* 省略代码 */
  entry: {
    main: path.resolve(__dirname, "../examples/main.js")
  },
  resolve: {
    /* 省略代码 */
    '@': path.resolve(__dirname, '../examples')
    /* 省略代码 */
  }
  /* 省略代码 */
}

这样一来,再把 examples 改造一下,便于我们写演示代码。examples 目录如下图:

2.png

这个文件夹目录的结构就跟我们平时做单页面开发一样,这里我就不再赘述各个文件夹的功能,其实这个结构也不是非得这样来布置,可以根据自己的情况来规划目录即可。

编写组件库

重点主要在 packages 目录下组件库的编写,目前我的目录结构如下所示:

3.png

  • components 存放你的组件。
  • style 存放你的相关的样式,组件单独的样式我放在了组件目录里面管理了,比如图片你面的 button.scss。
  • index.js 组件库的注册和导出都在这里。

style 文件夹下的样式大家根据自己需要来规划就好,不一定按我这个方式来

1、普通组件

接下来我以一个 button 组件来举例,先按图建好相关文件,相关代码我会加一些注释,如果你不太清楚 Vue 插件开发,Sass 等这些知识点建议先学习一下相关知识,这里不再展开。

4.png

  • button.scss
@import '../../style/common/variable.scss';
.cookie-button {
  position: relative;
  display: inline-block;
  width: 90px;
  height: 40px;
  line-height: 40px;
  border-radius: 4px;
  outline: 0;
  border: 0;
  appearance: none;
  color: #fff;
  font-size: 16px;
  &.cookie-button--primary {
    background-color: $button-primary;
    border: 1px solid $button-primary;
  }
  &.cookie-button--danger {
    background-color: $button-danger;
    border: 1px solid $button-danger;
  }
  &.cookie-button--warning {
    background-color: $button-warning;
    border: 1px solid $button-warning;
  }
  &.cookie-button--info {
    background-color: $button-info;
    border: 1px solid $button-info;
  }
}
/* 加这个代码是为了让按钮点击看起有个反馈效果 */
.cookie-button::before {
  position: absolute;
  content: "";
  left: 50%;
  top: 50%;
  width: 100%;
  height: 100%;
  background-color: #000;
  opacity: 0;
  transform: translate(-50%, -50%);
  border: inherit;
  border-color: #000;
  border-radius: inherit;
}
.cookie-button:active::before {
  opacity: 0.1;
}
  • Button.vue
<template>
  <button :class="classSet" @click="handleClick">
    // 这里使用了插槽知识
    <slot></slot>  
  </button>  
</template>


<script>
export default {
  name: 'ck-button',
  props: {
    type: {
      type: String,
      default: 'primary'
    }
  },
  computed: {
    classSet() {
      let classResult = `cookie-button cookie-button--${this.type}`;
      return classResult;
    }
  },
  methods: {
    handleClick() {
      this.$emit('click');
    }
  }
}
</script>
  • index.js
// 配置对外引用
import Button from './Button.vue';
import './button.scss';

// 提供 install 方法
// 这里提供一次 install 是为了便于单独引入 buttton 组件时进行注册
Button.install = function(Vue) {
  Vue.component(Button.name, Button);
};

// 默认导出方式导出
export default Button;

这样我们就实现了一个简单的按钮组件。
我们到根目录的 index.js 下进行 install。在 index.js 中加入以下代码:

/* 组件库对外导出的组件集合,对整个组件进行导出 */

// 导入组件(用于注册所有组件)
import Button from './components/button';


// 定义组件列表
const componentsList = [
  Button
];

const install = function(Vue) {
  // 判断是否安装过
  if(install.installed) return;

  // 注册所有组件
  componentsList.map((component) => {
    Vue.component(component.name, component);
  })
}

if(typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  install,
  Button
}

然后我们进入我们的 examples 目录来展示我们的按钮组件,在 main.js 中全部引入

// 引入组件(注册所有)
import CookieUI from '../packages/index.js';
Vue.use(CookieUI);

在组件中使用按钮:

<div class="box"><ck-button type="primary" @click="testClick">基本按钮</ck-button></div>
<div class="box"><ck-button type="danger">危险按钮</ck-button></div>
<div class="box"><ck-button type="warning">警告按钮</ck-button></div>
<div class="box"><ck-button type="info">信息按钮</ck-button></div>
  • 注意: <ck-button> 这个跟你写的组件的 name 属性相关(比如我这里就是 Button.vue 里面的 name 属性),名字只要符合规范即可。

5.gif

2、modal 组件

这种组件不需要 Vue.component() 方法来注册,比如常见的 Toast、Dialog,我这儿是直接绑定到 Vue 原型上,在项目里面可以直接使用 this 调用。

6.png

  • toast.scss
// 定义的变量
@import '../../style/common/variable.scss';
// 使用的弹性布局
@import '../../style/mixins/flex_style.scss';
// 动画相关的样式
@import '../../style/mixins/animation.scss';

.cookie-toast--mask {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 100;
  @include flex-all-center; // Sass 的混入语法
  .cookie-toast--dialog {
    max-width: 80vw;
    background-color: rgba(0, 0, 0, 1);
    padding: 16px;
    box-sizing: border-box;
    border-radius: 4px;
    animation: zoomIn .3s ease-out 0s forwards;
  }
  .cookie-toast--content {
    font-size: $font-size-s;
    color: #fff;
  }
}

@include anima-zoomIn
  • Toast.vue
<template>
  <div v-if="show" class="cookie-toast--mask">
    <div class="cookie-toast--dialog">
      <p class="cookie-toast--content">{{ message }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'ck-toast',
  data() {
    return {
      show: false,
      message: ''
    }
  },
  methods: {

  }
}
</script>
  • index.js
import Vue from 'vue';
import toastComponent from './Toast.vue';
import './toast.scss';

const toastConstructor =  Vue.extend(toastComponent);
let instance;

/**
 * 打开 toast 
 * @param options {Object}  消息内容 options.message,不可省略。停留时间 options.duration,可省略,默认为 2000(毫秒)
**/
let toast = function(options = {}) {
  if(!instance) {
    instance = new toastConstructor({
      el: document.createElement('div')
    });
  }

  if(instance.show === true) return;

  instance.message = options.message;
  instance.show = true;
  document.body.appendChild(instance.$el)

  let timer = setTimeout(() => {
    instance.show = false;
    clearTimeout(timer);
  }, options.duration || 2000);
}

export default toast;

这样,一个 Toast 就完成了,然后我们需要到 index.js 里面注册

/* 组件库对外导出的组件集合,对整个组件进行导出 */

// 导入组件(用于注册所有组件)
import Button from './components/button';
import Toast from './components/toast';

// 定义组件列表
const componentsList = [
  Button
];

const install = function(Vue) {
  // 判断是否安装过
  if(install.installed) return;

  // 注册所有组件
  componentsList.map((component) => {
    Vue.component(component.name, component);
  })

  Vue.prototype.$toast = Toast;
}

if(typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default {
  install,
  Button,
  Toast,
}

这样我们就注册好了 toast,结合上面 mian.js 引入 button 的那段代码,我们可以在项目里面这样使用:

this.$toast({message: 'Hello,Toast 演示', duration: 1500});

6.gif

另付 Dialog 演示,代码这里不再贴出来,后面我会把代码传到 github,需要的自取。

8.gif

注:后期我把 packages 目录下的 index.js 改为了 cookieui.js。

webpack 打包

组件库开发完毕,我们是需要发布到 npm 上供其他人使用的,不然单独提出来的意义也不大,所以我们首先要做的的就是将 UI 组件库打包,这儿还是借助了 webpack,它专门有针对 library 进行设置。

1、打包准备

这种方式是把所有相关的打包到 js 中,然后把样式单独抽离出来,形成 css 文件,最终你打包下来的目录就是一个 js 和一个 css,我们把它发布到 npm,当别人下载下来过后,引入方式大概就变成这种样子(这里只是举个例子):

import CookieUI from 'cookie-ui';
import '../cookie-ui/index.css';
Vue.use(CookieUI);

首先我们在 build 的目录下新建三个文件 webpack.lib.base.jswebpack.lib.prod.jswebpack.lib.prod.disperse.js

  • webpack.lib.base.js 通用的基本配置
  • webpack.lib.prod.js 打包所有组件
  • webpack.lib.prod.disperse.js 分组件打包

  • webpack.lib.base.js
// 库打包的主要配置
// 引入 vue-loader 插件
const VueLoaderPlugin = require('vue-loader/lib/plugin');
// 引入清除打包后文件的插件(最新版的需要解构,不然会报不是构造函数的错,而且名字必须写 CleanWebpackPlugin)
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // 我们打包组件库时不需要把 Vue 打包进去
  externals: {
    'vue': {
      root: 'Vue',
      commonjs: 'vue',
      commonjs2: 'vue',
      amd: 'vue',
    }
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'babel-loader'
          }	
        ]
      },
      {
        test: /\.vue$/,
        use: [
          {
            loader: 'vue-loader',
            options: {
              compilerOptions: {
                preserveWhitespace: false
              }
            }
          }
        ]
      },
      {
        test: /\.(jpe?g|png|gif)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 5120,
              esModule: false,
              fallback: 'file-loader',
              name: 'images/[name].[ext]'
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new VueLoaderPlugin()
  ],
  resolve: {
		alias: {
      'vue$': 'vue/dist/vue.runtime.esm.js',
    },
    extensions: ['*', '.js', '.vue']
	}
};
  • webpack.lib.prod.js
// 打包所有
// node.js 里面自带的操作路径的模块
const path = require("path");
const merge = require('webpack-merge');
const webpackLibBaseConfig = require('./webpack.lib.base.js');
// 用于提取 css 到文件中
const miniCssExtractPlugin = require('mini-css-extract-plugin');
// 用于压缩 css 代码
const optimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');

module.exports = merge(webpackLibBaseConfig, {
  mode: 'production',
  devtool: 'source-map',
  entry: {
    cookieui: path.resolve(__dirname, "../packages/cookieui.js")
  },
  output: {
    // 打包过后的文件的输出的路径
    path: path.resolve(__dirname, "../lib"),
    // 打包后生成的 js 文件
    filename: "[name].js",
    publicPath: "/",
    library: 'cookieui',
    libraryTarget: 'umd',
    libraryExport: 'default',
    umdNamedDefine: true
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
          {
            loader: miniCssExtractPlugin.loader, // 使用 miniCssExtractPlugin.loader 代替 style-loader
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass')
            }
          },
          {
            loader: 'postcss-loader'
          }
        ]
      },
    ]
  },
  plugins: [
    // 新建 miniCssExtractPlugin 实例并配置
    new miniCssExtractPlugin({
      filename: '[name].css'
    }),
    // 压缩 css
    new optimizeCssnanoPlugin({
      sourceMap: true,
      cssnanoOptions: {
        preset: ['default', {
          discardComments: {
            removeAll: true,
          },
        }],
      },
    }),
  ]
})
  • webpack.lib.prod.disperse.js
// 用于对组件单独打包,便于按需加载
// 用于拷贝的插件
const copyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
const miniCssExtractPlugin = require('mini-css-extract-plugin');
const optimizeCssnanoPlugin = require('@intervolga/optimize-cssnano-plugin');
const merge = require('webpack-merge');
const webpackLibBaseConfig = require('./webpack.lib.base.js');
// 引入入口配置文件
const entryConfig = require('../packages/entry_config.js');

//定义入口
let entry =  {};
entryConfig.configList.map((item) => {
  let componentName = item.name.toLowerCase();
  entry[componentName] = path.resolve(__dirname, '../packages/components/' + componentName + '/index.js');
});

module.exports = merge(webpackLibBaseConfig, {
  mode: 'production',
  devtool: '#source-map',
  entry,
  output: {
    // 打包过后的文件的输出的路径
    path: path.resolve(__dirname, "../lib/packages"),
    // 打包后生成的 js 文件
    // 解释下这个[name]是怎么来的,它是根据你的 entry 命名来的,入口叫啥,出口的[name]就叫啥
    filename: "[name]/index.js",
    // 我这儿目前还没有资源引用
    publicPath: "/",
    library: '[name]',
    libraryTarget: 'umd',
    libraryExport: 'default',
    umdNamedDefine: true
  },
  module: {
    rules: [
      {
        test: /\.(scss|sass)$/,
        use: [
          {
            loader: miniCssExtractPlugin.loader, // 使用 miniCssExtractPlugin.loader 代替 style-loader
          },
          {
            loader: 'css-loader',
          },
          {
            loader: 'sass-loader',
            options: {
              implementation: require('dart-sass')
            }
          },
          {
            loader: 'postcss-loader'
          }
        ]
      },
    ]
  },
  plugins: [
    // 新建 miniCssExtractPlugin 实例并配置
    new miniCssExtractPlugin({
      filename: '[name]/style.css'
    }),
    // 压缩 css
    new optimizeCssnanoPlugin({
      sourceMap: true,
      cssnanoOptions: {
        preset: ['default', {
          discardComments: {
            removeAll: true,
          },
        }],
      },
    }),
  ]
})

同时在 packages 目录下新建一个 entry_config.js,这个是用来单独打包组件时的配置

module.exports = {
  configList: [
    {
      name: 'button',
      author: 'LEE'
    },
    {
      name: 'toast',
      author: 'LEE'
    },
    {
      name: 'dialog',
      author: 'LEE'
    }
  ]
}

注:上面涉及的相关的包如果你没有安装的话,手动安装一次即可。

这样一来,webpack 就配置完毕了,现在我们还需要修改 package.json 里面的 script,加上以下几句:

"lib:all": "webpack --config ./build/webpack.lib.prod.js",
"lib:disp": "webpack --config ./build/webpack.lib.prod.disperse.js"

2、分别打包

接下来我们分别执行上面的命令,然后你会发现目录下有个 lib 目录,生成项目如下:

9.png

npm 发布

1、发布准备

  • 首先你得有一个 npm 的账号,到官网注册一个即可。 npm
  • 修改你目录下的 package.json

这个根据你情况来写,一般来说 name、version、main 这几个属性不可省略。同时你得 name 不能跟 npm 上其它开发者发布的包重名,像我这个 cookie-ui 就重复了,所以我改成了 vue-cookie-ui。-_-!。。。这里我给个大概参数配置,需要看完整的到仓库自取哈

{
  "name": "vue-cookie-ui",
  "version": "1.0.0",
  "description": "A Personal Learning UI library For Vue",
  "main": "lib/cookieui.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --config ./build/webpack.dev.js",
    "build": "webpack --config ./build/webpack.prod.js",
    "lib:all": "webpack --config ./build/webpack.lib.prod.js",
    "lib:disp": "webpack --config ./build/webpack.lib.prod.disperse.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/cookiepool/cookie-ui.git"
  },
  "keywords": [
    "UI",
    "Vue",
    "UI-Library"
  ],
  "author": "LEE",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/cookiepool/cookie-ui/issues"
  },
  "homepage": "https://github.com/cookiepool/cookie-ui#readme",
  /* 省略代码 */
}
  • 新建一个.npmignore 文件,用来忽略不发布的文件
touch .npmignore

加入以下代码

.DS_Store
node_modules
/dist
/build

# local env files
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*

examples/
packages/
public/
babel.config.js
build
postcss.config.js
教程.MD
.gitignore
*.map
*.html

2、开始发布

前面注册好了和配置好 package.json 等工作后,在根目录打开 bash,输入

npm login

这里你按照提示登录好账号即可,登录成功过后,再来一个发布命令即可

npm publish

发布成功过后你就可以到 npm 查到自己的包了。

10.png

从 npm 下载并引用组件

我们发布到 npm 后就可以从 npm 下载并使用了

npm i vue-cookie-ui

下载完成后去我们的项目里面引用(main.js)

全部引入

// 引入组件(注册所有)
import CookieUI from 'vue-cookie-ui';
import 'vue-cookie-ui/lib/cookieui.css';
Vue.use(CookieUI);

11.gif

按需引入

// 按需加载
// 引入组件
import Button from 'vue-cookie-ui/lib/packages/button';
import 'vue-cookie-ui/lib/packages/button/style.css';
Vue.use(Button)
// 引入 modal(Dialog 和 Toast 都要这样注册)
import Toast from 'vue-cookie-ui/lib/packages/toast';
import 'vue-cookie-ui/lib/packages/toast/style.css';
Vue.prototype.$toast = Toast;

这里我只按需引入了 Toast、Button,Dialog 没有引入,演示你会发现 Toast 正常,Dialog 无法工作并出现了报错。

12.gif

13.png

借助 babel-plugin-component 按需引入

安装依赖

npm install babel-plugin-component

在 babel.config.js 中加入以下代码:

plugins: [[
  // 配置按需引入插件 babel-plugin-component
  "component",
  {
    // 库的名字为 VUI
    "libraryName": "vue-cookie-ui",
    // 存放库文件的文件夹为 lib/packages
    "libDir": "lib/packages",
  }
]]

然后你就可以这样引入了,插件会自动帮你转换路径

// 使用 babel-plugin-component
import { Button, Toast, Dialog } from 'vue-cookie-ui';
Vue.use(Button);
Vue.prototype.$toast = Toast;
Vue.prototype.$dialog = Dialog;

结语

到这里就告一段落了,特别感谢 nutui 的源代码,给了很多参考,如有错误,还请多多包涵,并指出错误。前期在社区上也找了许多开发组件库的文章,也感谢这些开源分享的大佬。如果帮助到了你点个赞再走吧!

这里附上代码的 github 地址: cookie-ui

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

╄→承喏

暂无简介

文章
评论
27 人气
更多

推荐作者

夢野间

文章 0 评论 0

百度③文鱼

文章 0 评论 0

小草泠泠

文章 0 评论 0

zhuwenyan

文章 0 评论 0

weirdo

文章 0 评论 0

坚持沉默

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文