Press UI 应用于普通Vue项目的实践

[toc]

1. 开始

Press UI 是一套基于 uni-app 的组件库,是项目内孵化的,但并不与任何业务绑定的底层组件库。

目前 Press UI 主要有三方面功能:

  • 基础组件,提供与 Vant 相同API的组件,比如 Button、Picker 等共60多个
  • 业务组件,在基础组件上搭建的、业务中沉淀的组件,目前有10多个
  • 核心逻辑,包含路由寻址、IM模块封装等

下图是示例二维码,分别为H5、微信小程序、QQ小程序。

在做一个普通Vue项目(非uni-app环境)需求时,需要一个组件,在 Press UI 已经有了,可不可以直接用 Press UI 内的呢?这样就不用写两套代码了。

进而想一下,能否让所有组件都支持普通Vue项目呢?

2. 思路

其实,uni-app的组件也是SFC,相比普通H5项目,有以下不同:

  1. 支持条件编译,是跨平台的关键部分
  2. 包含uni-app内置组件,比如
    • scroll-view
    • swiper
    • swiper-item
    • view
    • image
    • rich-text
    • ...
  3. 包含uni-app提供的API,比如
    • uni.createSelectorQuery()
    • uni.vibrateLong
    • uni.showToast
    • ...
  4. 包含与小程序对齐的生命周期,如onShowonLoad

现在分析下如何抹平上面几点,达到一套代码走天下。

  1. 对于条件编译,可以实现一套一样的,专门针对于非 uni-app 项目。

看了下 uni-app 的源码,其条件编译是在preprocess魔改的。

  1. 对于uni-app内置组件,有两种处理办法:
  • H5端直接不用内置组件,完全用H5的Tag或Vue语法处理。

  • 用内置组件,但在非uni-app场景下,实现内置组件的polyfill

Press UI 同时采用了这两种方法,对于一些简单的,可直接替代的,用第一种方法:

  • view => div
  • text => span
  • image => img
  • uni-shadow-root => div
  • rich-text => v-html

对于相对复杂的组件,用的第二种方法,比如自己实现了scroll-viewswiperswiper-item

  1. 对于uni-app提供的API,Press UI 在非uni-app环境实现了对应的polyfill

  2. 对于非Vue原生支持的生命周期,可替换为Vue自身的。

3. 实现

工欲善其事,必先利其器。对于组件库,示例和文档就是"器"。

Press UI 要想跨第n+1端,需要一个环境来调试并检验成果。由于uni-appvue-cli-service注入了插件,所以这里另起了一个示例项目,把 Press UI 当submodule引入。

然后实现了与uni-app一模一样的条件编译工具 ifdef-loader,使用方式与uni-app相同,并沉到业务底层。

并且实现了scroll-viewswiper等组件,提供与uni-app相同的属性和事件,另外,对组件涉及的uni-app相关的api也进行了兼容。

下面是 Press UI 在非uni-app环境下的示例:

4. 遇到的问题

uni-app使用的Vue是修改过的,并非Vue官方版本,因为在渲染等方面有差异。

4.1. uni-shadow-root

uni-shadow-root替换成div有坑点,可能会导致一些场景无法滑动,比如press-tab

小程序下uni-shadow-rootdisplay默认值是inline,也就是子元素展示高度(比如内容1700px)不会超过父元素高度(比如300px),但是换成div后,对应的viewdisplay属性值为block,其高度是子元素完整的高度(1700px)。

4.2. 插值语法

uni-app项目中,插值前后的空格是被抹掉的,而Vue是保留的。举个例子:

html 复制代码
<span>
  {{ text }}
</span>

uni-app中会被渲染成:

html 复制代码
<span>{{ text }}</span>

而在普通Vue项目中,会被渲染成:

html 复制代码
<span> {{ text }} </span>

这个不同在white-space被设置成pre-wrap等属性时显示出来。

4.3. image

image替换为img标签时,需处理属性mode

4.4. IntersectionObserver

这个API在Calendar组件中用到了,当月份滑动的时候,父组件获取当前月份,来展示对应的标题。

具体是如何判断呢?如果某个月份的顶部小于父组件的顶部时,意味着它滑到了当前视口。

ts 复制代码
initRect() {
  if (this.contentObserver != null) {
    this.contentObserver.disconnect();
  }
  const rootSelector = '.press-calendar__body';
  const selector = '.month';
  const threshold = [0, 0.1, 0.9, 1];
  const observeAll = true;

  if (intersectionObserverPloyFill({
    selector,
    options: {
      threshold,
      observeAll,
      root: document.querySelector(rootSelector),
    },
    callback: (changes) => {
      for (const change of changes) {
        if (change.boundingClientRect.top <= change.rootBounds.top) {
          this.subtitle = formatMonthTitle(+change.target.dataset.date);
        }
      }
    },
  })) {
    return;
  }


  const contentObserver = uni.createIntersectionObserver(this, {
    thresholds: threshold,
    observeAll,
  });

  this.contentObserver = contentObserver;
  contentObserver.relativeTo(rootSelector);

  contentObserver.observe(selector, (res) => {
    if (res.boundingClientRect.top <= res.relativeRect.top) {
      this.subtitle = formatMonthTitle(res.dataset.date);
    }
  });
},
ts 复制代码
export function intersectionObserverPloyFill({
  selector,
  callback,
  options,
}) {
  if (isNotInUni()) {
    const io = new IntersectionObserver(callback, options);
    const target = document.querySelectorAll(selector);
    target.forEach((element) => {
      io.observe(element);
    });
    return true;
  }
  return false;
}

4.5. 输入框高度自适应

textareaautosize属性在uni-app项目中是封装好的,如果自己实现呢?

可以监听输入框的scrollHeight,如果其发生了变化,那么说明输入的文字行数也发生了变化,然后把这个scrollHeight当作height赋值给textarea即可。

ts 复制代码
adjustSize() {
  const { input } = this.$refs;
  if (!(this.type === 'textarea' && this.autosize) || !input) {
    return;
  }

  const scrollTop = getRootScrollTop();
  input.style.height = 'auto';

  let height = input.scrollHeight;
  if (isObject(this.autosize)) {
    const { maxHeight, minHeight } = this.autosize;
    if (maxHeight) {
      height = Math.min(height, maxHeight);
    }
    if (minHeight) {
      height = Math.max(height, minHeight);
    }
  }

  if (height) {
    input.style.height = `${height}px`;
    setRootScrollTop(scrollTop);
  }
},

4.6. event.detail

uni-app中获取scrollTop等属性是通过event.detail,而普通Vue项目是通过event.target

4.7. dataset

uni-app会把event.target.dataset的数字转为number类型,而普通Vue项目一直是string

html 复制代码
<div
  v-for="(item,index) in (options)"
  :key="item.index"
  :data-index="index"
>
</div>

uni-page-head 兼容

uni-page-headposition: fixed的元素,高度44px,在它下面加个同等高度的uni-placeholder,这样下面的元素就不会顶上去了,好处是对其他元素样式无侵入。

scss 复制代码
.uni-placeholder {
  width: 100%;
  height: 44px;
  height: calc(44px + constant(safe-area-inset-top));
  height: calc(44px + env(safe-area-inset-top));
}

top-window

uni-app中如果有top-window,也就是uni-page-head那层级,计算boundingClientRect时,是会去除top-window的高度的,也就是会少44px

ts 复制代码
uni.createSelectorQuery().in(this);
item
  .select(`#seq-${this.curSelItem}`)
  .boundingClientRect((res) => {})
  .exec();

5. 效果

目前 Press UI 的schedule-treearea组件在普通Vue项目中使用,实现了一套代码n+1端复用,nuni-app赋予的,1是本次扩展的。

有些组件还实现了5端复用,除了H5、微信小程序、QQ小程序、普通H5外,还兼容了PC端。不过这种兼容并非是平台级别的,更多是UI展示和事件的处理。

相关推荐
孙 悟 空13 小时前
uni-app:监听页面返回,禁用返回操作
前端·javascript·uni-app
mosen86820 小时前
uniapp中uni.scss如何引入页面内或生效
前端·uni-app·scss
lyz24685920 小时前
uniapp popup弹窗组件的自定义使用方法
uni-app
沙尘暴炒饭20 小时前
uniapp 前端解决精度丢失的问题 (后端返回分布式id)
前端·uni-app
牛牛科技20 小时前
生产管理系统PHP+Uniapp源码
uni-app
Smile_ping20 小时前
uniapp——APP读取bin文件,解析文件的数据内容(一)
uni-app·uniapp 读取文件·app端读取bin文件
CDERP-plus20 小时前
uniapp 3分钟集成轮播广告图
uni-app·erp·erp移动端
Liberty_yes20 小时前
uniapp input苹果中文键盘输入拼音直接切换输入焦点监听失效
uni-app
街尾杂货店&20 小时前
webpakc介绍
uni-app
洗发水很好用1 天前
uniApp打包H5发布到服务器(docker)
uni-app