Weekbin

一个厉害的前端新手

LV0
已经发布0篇文章,距离下一等级还需发布1篇文章

自建组件库

前言:本文介绍如何做一个简单的 Vue UI 组件库 DEMO 项目。简单地介绍利用脚手架搭建目录框架结构,到开发组件,到测试组件(暂无),再到发布到 npm 的一整个 流程。

撇开组件库中可能用到的复杂技术不谈,做一个简单的玩具项目还是很简单的(既不需要适配,也不考虑兼容性,更不考虑性能),不是一座能直接压死人的大山。

一、创建项目

1. 安装 Vue Cli 脚手架

参考 Vue CLI 官方文档 进行安装

复制
npm install -g @vue/cli
# OR
yarn global add @vue/cli

2. 利用脚手架创建项目

创建项目步骤一

创建项目步骤二

3. 改造项目结构

将 src 目录重命名为 examples,此文件夹主要用于日后组件库的文档展示以及测试页面;新建 packages 目录,用于存放所有组件文件,在目录下新建若干个文件夹,单 个组件放在一个文件夹内,每个文件夹内都包含一个 index.ts 文件用于单组件注册,在 packages 目录下包含一个 index.ts 用于全局注册,注册所有组件库的组件;新 建 typings 文件夹,用于存放所有 ts 定义文件,将原存放于根目录下的两个 shims.d.ts 文件移入该文件夹;在项目根目录下新建 vue.config.js 文件,修改相关配置 以让修改目录结构后的项目跑起来。

修改后目录结构如下:

创建项目步骤三

vue.config.js 的配置内容,配置 index 中的入口地址,指向 examples 文件夹下的 main.ts 文件;添加 chainWebpack 的配置,将新建的 packages 目录加入编译。

复制
// vue.config.js
const path = require('path')

module.exports = {
  pages: {
    index: {
      entry: 'examples/main.ts',
      template: 'public/index.html',
      filename: 'index.html'
    }
  },
  chainWebpack: config => {
    config.module.rule('js').include.add(path.resolve(__dirname, 'packages')).end().use('babel').loader('babel-loader')
  }
}

4. 总结

dist 是打包后的文件夹,也是发布到 npm 的文件夹(现在暂时打包不起来),examples 是文档与测试的文件夹,packages 是组件的文件夹,typings 是存放定义的文件 夹。

至此,运行 yarn && yarn dev 应该是可以把项目跑起来了。

二、组件开发

由于真正的组件库开发这一块内容非常多而且非常复杂,所以这里就只写一个简单的 Tag 组件,意思到位,具体想玩什么样的逻辑与样式,可以自行拓展。

1. 创建组件目录

新建 Tag 文件夹,Tag 文件夹下新建 src 目录与 index.ts 文件,src 文件夹下创建 Tag.vue。

2. 开发组件

具体代码如下:

复制
// packages/Tag/src/Tag.vue
<template>
  <span :class="['wkb-tag', type]"><slot /></span>
</template>

<script>
export default {
  name: 'Tag',
  props: {
    type: {
      type: String,
      default: 'primary'
    }
  }
}
</script>

<style scoped lang="scss">
.wkb-tag {
  font-weight: 500;
  font-size: 18px;
  line-height: 24px;
  text-transform: uppercase;
  padding: 2px 10px;
  border-radius: 10.5px;

  &.primary {
    color: #387dff;
    background-color: rgba(#387dff, 0.2);
  }

  &.success {
    color: #23b899;
    background-color: rgba(#23b899, 0.2);
  }

  &.danger {
    color: #fe7c4b;
    background-color: rgba(#fe7c4b, 0.2);
  }
}
</style>
复制
// packages/Tag/index.ts
import { PluginObject } from 'vue'
import WkbTag from './src/Tag.vue'

const injectInstallObject: PluginObject<null> = {
  install(Vue) {
    Vue.component('wkb-tag', WkbTag)
  },
  pluginName: 'wkb-tag'
}

const WkbComponent = Object.assign(WkbTag, injectInstallObject)

if (typeof window !== 'undefined' && window.Vue) {
  injectInstallObject.install(window.Vue as any)
}

export default WkbComponent
复制
// packages/index.ts
import { PluginFunction } from 'vue'
import WkbTag from './Tag'

const WkbComponentList = [WkbTag]

const install: PluginFunction<null> = function(Vue) {
  WkbComponentList.forEach(component => {
    Vue.component(component.pluginName, component)
  })
}

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

export default { install }

3. 调用该组件

组件开发完成后可以在 examples 文件夹下撰写使用文档及做一些基础测试,确保组件的功能正常。

方便起见,直接使用全局注册的方式注册组件,代码如下:

复制
// examples/App.vue
<template>
  <div id="app">
    <div class="wrapper">
      <h1>WKB-UI</h1>
      <div class="tag-group">
        <h2>Tag 标签</h2>
        <wkb-tag type="primary">堆溢出</wkb-tag>
        <wkb-tag type="success">堆溢出</wkb-tag>
        <wkb-tag type="danger">堆溢出</wkb-tag>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'

export default Vue.extend({})
</script>
复制
// examples/main.ts
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import WkbUI from '../packages'

Vue.use(WkbUI)

Vue.config.productionTip = false

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

成功调用的话,可以在浏览器看到如下内容:

创建项目步骤三

4. 总结

至此,一个 Tag.vue 组件就开发完了,既可以但组件注册,也可以全局统一注册。

三、发布

1. 发布前准备工作

在根目录新建一个 .npmignore 文件,配置一下不上传到 npm 的内容,我的配置如下:

复制
// .npmignore
.DS_Store
examples/
packages/
public/
vue.config.js
babel.config.js
.prettierrc
.eslintrc.js
.editorconfig
.browserslistrc

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

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

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

配置完了之后还差最后一步,记得 yarn build 打包出来一个 dist 文件夹,最终发布到 npm 上的其实只要这个 dist 文件夹(typings)就够了。

我们还需要最后配置一下 yarn build 的命令,因为默认是将这个项目编译成一个应用而不是一个插件,我们要修改配置使其变为一个插件。

参考 vue 官网相关介绍 进行如下配置,将 build 的入口指向 packages 下的 index.ts:

复制
{
  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build --target lib packages/index.ts",
    "lint": "vue-cli-service lint"
  }
}

2. 使用 github 管理项目

自行操作,顺便完善下项目根目录下的 README,介绍一下组件库的使用方式。

3. 发布到 npm

很简单其实也就两部操作,如果没有 npm 账号的话首先先去 npm 官网 注册一个。

先登录:

npm login
Username: 你的用户名
password: 你的密码
email: 填你自己的邮箱就行

登陆完了,发布:

npm publish --access=public

四、坑

1. 环境及依赖安装的问题

自行想办法用代理解决该问题。

2. TS 相关

组件注册的问题

使用 js 开发时,通常会使用下面的方法来注册组件。

复制
import Button from './src/Button.vue'

Button.install = function (Vue, config) {
  // 处理 config 的相关逻辑
  Vue.component(Button.name, Button)
}

诚然,使用 js 开发时并不会出现什么问题,一切都在正常的运行。但是当使用 ts 时,坑就出现了:

  1. 首先 ts 的单页组件一般会用 Vue.extend() 来导出,这就导致导出的对象是拿不到 name 属性的,也就是以上代码的 Button.name 会出错,会错误的显示为构造 函数上的名称 VueComponent

  2. 其次,Button 的类型被自动推断为VueConstructor<Button>,这也就意味着如果想直接在该属性上添加 install 方法会直接报 错Property 'install' does not exist on type 'ExtendedVue<Vue, unknown, unknown, unknown, Record<never, any>>'.

  3. 甚至如果想全局注册组件,Vue.use() 方法会报出重载错误 Property 'install' is missing in type 'VueConstructor<Record<never, any> & Vue>' but required in type 'PluginObject<any>'. ,这是因为导出的模块被 错误的推导为 VueConstructor<Button>,而其本身是不具备 install 方法的。

解决方法如下:

复制
// Button.vue
import Vue from 'vue'

export default Vue.extend({
  name: 'WbkButton'
})

// index.ts
import { PluginObject } from 'vue'
import WkbButton from './src/Button.vue'

// 此处的泛型为 install 第二个参数的类型,本组件用不到所以设置为了 null,可以更具自己的需求做响应的更改
const injectInstallObject: PluginObject<null> = {
  install(Vue) {
    Vue.component('wkb-button', WkbButton)
  }
}

const WkbComponent = Object.assign(WkbButton, injectInstallObject)

export default WkbComponent

// main.ts 全局注册
import WkbButton from '../packages/Button/index'

Vue.use(WkbButton) // Vue.use<T> 的泛型同样也是定义的 use 方法的第二个参数,即传入 install 方法的第二个参数

// App.vue 局部注册
import Vue from 'vue'
import WkbButton from '../packages/Button'

export default Vue.extend({
  components: {
    'wkb-button': WkbButton
  }
})

3. 发布时的问题

npm 源不对

如果你使用了淘宝镜像或者 nrm 来管理你的 npm 源,请切换到 npm 本身来。

仓库的属性不对

检查 packages.json 中,name 在 npm 上是唯一且不存在已经发布了的包的;private 属性一定得是 false,私有的包不付费是发布不上去的。对于第一种情况,建议使 用私人名作为前缀,如 @weekbin/package-name,使用这种情况发布的包,在 npm publish 时要特别声明我的包时公开的,npm publish --access=public。

up-to-top