Deacon的小站

今天的天气也挺不错

为了实现低代码平台,我写了一个拖拽库,顺便学习如何从0发布一个开源库

拖拽库github地址:github.com/bpuns/easy-…

拖拽库说明文档:bpuns.github.io/easy-dnd-do…

前言

一年前,我接到一个需求,需要开发一个低代码平台,调研了下市面上适用于React的拖拽库,本人都感觉使用起来过于复杂,并且体积都挺大的,我就想用原生的拖拽Api,但是原生Api在复杂场景下(无限嵌套,嵌套规则复杂),是不可用的,如果想了解具体遇到的问题,可以看 巨详细!帮你踩坑HTML5拖拽api(下) 这篇文章。因此,我就想基于 Drag and Drop api 封装的一套可以应用与复杂场景下的拖拽库

并且这个库最好不要和任何的前端框架绑定,这样方便移植和扩展,因为是自己封装的,因此可以掌握所有实现细节,开发过程中如果遇到了性能瓶颈,也能最快的找到解决方案,而且我也没完整的发布过一个开源库,正好学习下整个过程

拖拽库实现思路

拖拽上下文

页面中可能会有很多可以拖拽的区域,并且拖拽区域之间互不影响,那么就需要为每个拖拽区域创建一个上下文,那么就可以提供一个方法,用于上下文的创建,为了到时候方便扩展,可以传一个配置项作为参数。

interface ProviderConfig { } 

/**
 * 创建拖拽作用域下的基本数据
 * @param {*} config 配置项
 * @returns
 */
function createProvider(config?: ProviderConfig): Context;

拖拽类型

在现实生活中,我们之所以知道一个物品可以放到另外一个物品中,比如食物剩余的包装袋需要放到垃圾桶中,电脑需要放到电脑包里。所以抽到代码中,需要给可以拖拽的dom元素添加一个类型,给需要放置的元素设置允许放置的类型有哪些

// 创建拖拽对象
const drag = useDrag({
  config: {
    type: '类型'
  }
})

// 创建放置对象
const drop = useDrop({
  config: {
    acceptType: [ '类型' ]
  }
})

canDrop

光有类型不够,还需要给Drop添加一个方法,判断当前是否允许放置,比如当前Drop元素内部可能只能放一个元素

const drop = useDrop({
  config: {
    canDrop: () => true | false
  }
})

Drag api设计

Drag Api的事件需要和h5事件保持一致

const drag = useDrag({
  config: {
    type: '类型',
    context
  },
  dragStart(){
    console.log('开始拖拽')
  },
  drag(){
    console.log('拖拽中')
  },
  dragEnd(){
    console.log('结束拖拽')
  }
})

Drop api设计

Drop Api的事件需要和h5事件保持一致

const drop = useDrop({
  config: {
    context,
    // 判断是否允许拖拽
    canDrop: () => true | false,
    // 使用Set替换数组
    acceptType: new Set([ '类型' ]),
  },
  dragEnter(){
    console.log('拖拽元素进入')
  },
  dragOver(){
    console.log('拖拽元素在当前元素范围内移动')
  },
  dragLeave(){
    console.log('拖拽元素离开')
  },
  drop(){
    console.log('拖拽元素放置')
  }
})

Vue 与 React 支持

因为在Vue和React中都有上下文的概念,那就好办了,提供个组件包裹需要拖拽的元素,用于传递上下文

  • Vue
<template>
  <dnd-provider>
    <component />
  </dnd-provider>
</template>

<script setup lang="ts">
import { DndProvider } from 'easy-dnd/vue'
</script>
  • React
import { DndProvider } from 'easy-dnd/react'

function Example1() {
  return (
    <DndProvider>
      <Component />
    </DndProvider>
  )
}

因为拖拽和放置都需要作用在dom上,那么可以Drag与Drop都可以提供个方法,用于dom的绑定

  • Vue
<template>
  <div :ref="drag.dragRef" class="a">
    盒子A
  </div>
</template>

<script setup lang="ts">

import { useDrag } from 'easy-dnd/vue'

const drag = useDrag({
  config: {
    type: 'A'
  },
  dragStart: () => {
    console.log('A 开始拖拽')
  },
  dragEnd: () => {
    console.log('A 结束拖拽')
  }
})

</script>
  • React
import { useDrag } from 'easy-dnd/react'

function A() {

  // 创建拖拽实例
  const drag = useDrag(() => ({
    config: {
      type:  'A'
    },
    // 拖拽开始的回调
    dragStart: () => {
      console.log('A 开始拖拽')
    },
    // 拖拽结束的回调
    dragEnd: () => {
      console.log('A 结束拖拽')
    }
  }))

  return (
    <div
      // 与dom绑定
      ref={drag.dragRef}
    >
      盒子A
    </div>
  )
}

编写打包脚本

随便找个打包工具,我这个库使用的是rollup

import { rollup } from 'rollup'
import typescript from 'rollup-plugin-typescript2'
import { terser } from 'rollup-plugin-terser'

rollup({
    // 入口配置
    input:   entry,
    // 输出配置
    output: {},
    // 插件配置
    plugins: [
      // 代码压缩
      terser({
        ie8:      false,
        compress: true,
        format:   {
          quote_style: 1
        }
      }),
      // typescript支持
      typescript()
    ],
    // 配置要排除哪些文件,
    external: []
  })

详细配置文件可以看这里

使用vuePress编写Doc并发布到GitHub Pages上

一个开源库,一定需要一个说明文档,那么这里我使用的是 VuePress,官方的介绍如下

VuePress 是一个以 Markdown 为中心的静态网站生成器。你可以使用 Markdown在新窗口打开 来书写内容(如文档、博客等),然后 VuePress 会帮助你生成一个静态网站来展示它们

文档编写完成之后,如果你有自己的服务器,可以把静态资源部署到自己的服务器上,像我这样的白嫖党,可以选择发布到 GitHub Pages 上,可以看Github的官方文档来部署你的说明文档

部署完成的效果就是下面这样啦

image.png

编写使用demo

打包脚本和文档有了之后,就需要准备demo了,因为有些人是不会看文档的,直接看demo基本上就可以很快的把一个库的使用方法学会,因此我准备了在 纯原生环境下使用,在Vue下使用,在React下使用的代码示例,点击这里 查看

image.png

npm发布

我选择再编写了个js脚本,帮我把package.json,README.md文件全部拷贝到打包文件夹下,这样我就不用编写 .npmignore文件了

  • scripts/build.js
import { join } from 'node:path'
import process from 'node:process'
import { rollup } from 'rollup'
......

async function buildPackage(_package) {

  console.log(`${name}:开始构建`)

  // 构建代码 ......

  console.log(`${name}:构建结束`)

}

; (async function () {
  // 清空原有的输出路径
  await fs.emptyDir(BUILD_PATH)
  // 打包
  await Promise.allSettled(packages.map(buildPackage))
  // 复制需要提交到npm上的配置文件
  copyNpmConfig()
})()

function copyNpmConfig() {
  // 复制package.json
  const desJson = JSON.parse(fs.readFileSync(join(PROJECT_PATH, 'package.json'), 'utf-8'))
  const newPackageJson = {
    name:       desJson.name,
    version:    desJson.version,
    keywords:   desJson.keywords,
    author:     desJson.author,
    repository: desJson.repository
  }
  fs.writeFileSync(join(BUILD_PATH, 'package.json'), JSON.stringify(newPackageJson, null, 2))
  console.log('package.json完成')
  // 复制README.md
  fs.copyFileSync(
    join(PROJECT_PATH, 'README.md'),
    join(BUILD_PATH, 'README.md')
  )
  console.log('复制README.md完成')
}

最后就是调用推送命令发布。当然,如果你实在懒的话,也可以编写node脚本帮你提交

$ npm publish

最后

这个拖拽库是真实可用的,并不是整活,它已经用在了我私人项目中,并且已经使用了快1年多的时间,目前已经趋于稳定,欢迎大家使用并提出宝贵意见!!

后续展望

目前拖拽没有动画效果,并且还有基于这个库横向/纵向/网格拖拽的组件可以直接使用,后续可以加上