@hyext/popup
TypeScript icon, indicating that this package has built-in type declarations

2.6.1 • Public • Published

@hyext/popup

虎牙浮窗类型小程序布局展示方案

Installation

使用前,请先向hy-ext@huya.com发送邮件(内容请带上extUuid)申请浮窗权限。

$ npm i @hyext/popup

Usage

Configuration

首先我们需要在项目根目录的project.config.json文件的compileNodeModules加上@hyext/popup字段。

{
  "builder": {
    "name": "@hyext/builder-beyond",
    "config": {
      "compileNodeModules": [
        "@hyext/popup"
      ]
    }
  }
}

Create PopupLayout

首先我们需要根据我们的业务需求, 创建一个 PopupLayout,步骤如下:

import { withPopupLayout, LayoutSwitch } from '@hyext/popup'

const WidgetMeasureBox = withPopupLayout(
  rootLayoutInfo => {
    const isLandscape = rootLayoutInfo.isLandscape

    // 例如我这个小挂件在公屏的左下角,宽高不指定,使用业务容器宽高。
    return {
      right: isLandscape ? 68 : 18, // 在横屏的时候距离右方68,竖屏的时候是18。
      top: isLandscape ? 'center' : '68%' // 在横屏的时候垂直方向居中,竖屏的时候距离底部68%的距离。
    }
  },
  {
    defaultWidth: 95, // 为了和内部容器的hycss对齐,这里会做一层转换,以750宽度的设计稿为例,按1:1的比例widget的宽高。
    defaultHeight: 95,
    id: 'widget',
    border: true // 调试用的, prod可以设置为false。
  }
)

const FullScreenMeasureBox = withPopupLayout(
  rootLayoutInfo => {
    // 例如我这个内部组件需要占满全屏

    return {
      left: 0,
      top: 0,
      width: '100%',
      height: '100%'
    }
  },
  {
    defaultWidth: 0, // 由于你已经把width & height设置为‘100%’, 内部容器的宽高会与rootLayoutInfo的大小一致, 所以这里设置不设置宽高都无所谓。
    defaultHeight: 0,
    id: 'fullScreen',
    border: true // 调试用的。prod可以设置为false
  }
)

Declarative

对应声明式的代码风格,我们可以接着上下文这样使用它们:

// 连接上下文 WidgetMeasureBox FullScreenMeasureBox
const widgetLayoutName = 'widget-layout'
const fullScreenLayoutName = 'full-screen-layout'

function App() {
  const [layoutName, setLayoutName] = useState(widgetLayoutName)

  return (
    <>
      <Button onPress={() => setLayoutName(fullScreenLayoutName)}>
        切换全屏
      </Button>
      <LayoutSwitch currentLayoutName={layoutName}>
        <WidgetMeasureBox name={widgetLayoutName}>
          <MyWidgetComponent></MyWidgetComponent>
        </WidgetMeasureBox>
        <FullScreenMeasureBox
          name={fullScreenLayoutName}
        > 
          <MyFullScreentComponent></MyFullScreentComponent>
        </FullScreenMeasureBox>
      </LayoutSwitch>
    </>
  )
}

note: LayoutSwitch只能用currentLayoutName。

Programmatic

对应编程式的代码风格,我们可以接着上下文这样使用它们:

// 连接上下文 WidgetMeasureBox FullScreenMeasureBox
const widgetLayoutName = 'widget-layout'
const fullScreenLayoutName = 'full-screen-layout'

function App() {
  return (
    <>
      <Button onPress={() => setLayoutName(fullScreenLayoutName)}>
        切换全屏
      </Button>
      <LayoutSwitch defaultLayout={layoutName}>
        <WidgetMeasureBox name={widgetLayoutName}>
          <MyWidgetComponent></MyWidgetComponent>
        </WidgetMeasureBox>
        <FullScreenMeasureBox
          name={fullScreenLayoutName}
        > 
          <MyFullScreentComponent></MyFullScreentComponent>
        </FullScreenMeasureBox>
      </LayoutSwitch>
    </>
  )
}

// 重点看这部分
function MyWidgetComponent(props) {
  const layout = props.layout

  return (
    <Button onPress={() => layout.replace(fullScreenLayoutName)}>切换回全屏</Button>
  )
}

function MyFullScreentComponent(props) {
  const layout = props.layout

  return (
    <Button onPress={() => layout.replace(widgetLayoutName)}>切换回小挂件</Button>
  )
}

note: LayoutSwitch只能用defaultLayout。

Compoments

LayoutSwitch

浮窗 UI 展示控件

Props

Name Type Required Default Description
defaultLayout string/null false void 默认 layout 的名字,使用编程式风格时存在
currentLayoutName string/null false void 当前 layout 名字,使用声明式风格时存在
children React.ReactElement true void 对应的 PopupLayout 组件

PopupLayout

一个 HOC 组件,当发生任何场景切换的时候(不限于横竖屏切换,业务层切换等等),负责计算浮窗位置和大小。

Props

Name Type Required Default Description
name string true void 该 layout 的名字

withPopupLayout

withPopupLayout(options, hocOptions)会创建一个PopupLayout组件。

options 参数说明
Name Type Required Default Description
WithPopupLayoutOptions UserLayoutOptions/UserLayoutOptionsFN true void withPopupLayout 选项

WithPopupLayoutOptions可以是一个对象 UserLayoutOptions, 含有 2 种类型属性,第一种是位置属性,另一种是大小属性

位置属性包含:topbottomleftright, 有 4 种组合:top & lefttop & rightbottom & leftbottom & right,它们都可以接受number or 百分比 or center三种值。

大小属性包含:widthheight, 它们是非必填的,可以接受number or 百分比,在不填的时候,会使用内部容器的宽高作为根容器的宽高。

WithPopupLayoutOptions亦可以是一个函数:(layout:OnLayoutChangeCallbackRes) => UserLayoutOptions,OnLayoutChangeCallbackRes 数据结构,在 Hooks 环节中有所提及,函数返回的是UserLayoutOptions

hocOptions 参数说明
Name Type Required Default Description
defaultWidth number true void 默认业务容器的宽
defaultHeight number true void 默认业务容器的高
id string false void HOC 的 displayName
border boolean false void HOC 内部容器是否加边框,用来 debug 的

PopupLayout 会向它的子组件传入一个layout对象,方便子组件可以使用编程式的风格操作 layout,访问方式:props.layout,其接口如下:

  • layout.replace(v: string): Promise<void> - 切换其他 layout

  • layout.show(): Promise<void> - 展示本 layout

  • layout.hide(): Promise<void> - 隐藏本 layout

其中 layout.show()layout.hide()是对于自身组件来说的展示和隐藏,只针对已渲染组件。

Hooks

useLayoutChange(deps: any[], cb: UserLayoutOptionsFN): ChangedResult

由于LayoutSwitch的渲染方案粒度太粗,一更新就整个页面更新,而使用useLayoutChangehook返回的状态去渲染UI会更加灵活,例如用在营收浮窗面板,此方案更加。

demo:

// 不建议在函数内部改变 react 的 state, state 可通过 hook 返回的结果改变
function renderLayout(res: OnLayoutChangeCallbackRes) {
 const { isLandscape } = res
  if (isLandscape) {
    return {
      layout: 'landscape',
      left: 'center',
      top: 'center',
      width: '50%',
      height: 100
    }
  } else {
    return {
      layout: 'not-landscape',
      left: '10%',
      top: '5%',
      width: '50%',
      height: 100
    }
  }
}

function Demo() {
  // deps is useEffect deps
  const { layout, status, error, isLandscape } = useLayoutChange(deps, renderLayout)

  // 可以利用临界状态 做一些事情 让UI切换更加平滑
  if (status === 'none' || status === 'updating') return null

  if (status === 'error') {
    return <Abnor type="error" desc={error.message} />
  }
  
  // 可以根据用户定义的 layout 名字,映射不同的UI
  switch (layout) {
    case: 'not-landscape'
      return <NotLandscapeUI>
    case: 'landscape'
      return <LandscapeUI>
    default: 
      return null
  }
}

参数说明

export type OnLayoutChangeCallbackRes = {
  isLandscape: boolean
  screenHeight: number
  screenWidth: number
}

export type UserLayoutOptions = {
  layout?: string
  left?: number | string
  top?: number | string
  right?: number | string
  bottom?: number | string
  width?: number | string
  height?: number | string
  alpha?: number
  visible?: boolean
}

export type UserLayoutOptionsFN = (
  layout: OnLayoutChangeCallbackRes
) => UserLayoutOptions | Promise<UserLayoutOptions>


export type ChangeResult = {
 status: LayoutStatus
 layout?: string
 error: null | Error
 setedLayoutOptions: SetLayoutOptions
} & OnLayoutChangeCallbackRes

type LayoutStatus = 'none' | 'updating' | 'updated' | 'error'

type SetLayoutOptions = typeof hyext.panel.setLayout

useLayoutChangeWithQueue(params: UseLayoutChangeWithQueueParams): ChangedResult

和 useLayoutChange 类似, 只是加强了 NOTICE 模式的支持, 同时修改了部分API:

  1. 当为NOTICE时, 仅有当onNoticeShow触发后 status 才会变为 updated
  2. 获取setLayoutOptions的函数, 入参增加了 error, errorOpts, expUI.
    • error: 初始值为null, 当发生错误时, 会被赋值为错误对象. 可以使用这个对象来达到fallback的效果. 在同一次onLayoutChange中, 最多只会尝试3次, 仍然失败后 status 变为 error
    • errorOpts: 出错时的配置
    • expUI: 是否应当显示实验组UI
  3. useLayoutChangeWithQueue 的参数改成了对象, 同时增加log参数
  4. useLayoutChangeWithQueue 的返回值增加了 extUI 和 onShowPayload
function renderLayout(info: GetLayoutOptsParams) {
 const { isLandscape, error, errorOpts, expUI } = info
  if (isLandscape) {
    return {
      layout: 'landscape',
      left: 'center',
      top: 'center',
      width: '50%',
      height: 100
    }
  } else {
    // 首先尝试使用NOTICE模式
    if (!error) {
      return {
        mode: 'NOTICE',
        liveroomPopupKey: 'superfans_autotrigger',
      }
    } else {
            // 失败则使用setLayout
      return {
        layout: 'not-landscape',
        left: '10%',
        top: '5%',
        width: '50%',
        height: 100
      }
    }

  }
}
function Demo() {
  // deps is useEffect deps
  const { layout, status, error, isLandscape, expUI } = useLayoutChangeWithQueue({deps, getLayoutOpts: renderLayout, log: console.log})

  // 可以利用临界状态 做一些事情 让UI切换更加平滑
  if (status === 'none' || status === 'updating') return null

  if (status === 'error') {
    return <Abnor type="error" desc={error.message} />
  }
  
  // 可以根据用户定义的 layout 名字,映射不同的UI
  switch (layout) {
    case: 'not-landscape'
      return <NotLandscapeUI>
    case: 'landscape'
      return <LandscapeUI>
    default: 
      return null
  }
}

参数说明

/* interface UserLayoutOptions, 除了可以透传 setMode 的参数, 还可以透传 setModeWithQueue 的参数, 如
 * onShow/onNoticeHide/showTime/waitAfterResetNormal/skipResetNormal
 * 详见 https://git.huya.com/exc/web/hyext-miniapp/-/tree/master/packages/hyext-utils#getsetmodewithqueuefn
 */
type GetLayoutOptsParams = OnLayoutChangeCallbackRes & { expUI: boolean, retryCount: number, errorOpts?: UserLayoutOptions, error?: Error }

interface UseLayoutChangeWithQueueParams {
  getLayoutOpts: (params: GetLayoutOptsParams) => UserLayoutOptions | Promise<UserLayoutOptions>,
  deps?: any[],

  /* 新增入参 */
  log?: (msg: string) => void
}

type ChangedResult = {
  status: LayoutStatus

  // 以下数据在 status 不等于 ‘none’ 时存在
  error: Error | null
  layout?: string

  isLandscape?: boolean
  screenHeight?: number
  screenWidth?: number

  setedLayoutOptions: {
    visible: boolean
    x: number
    y: number
    alpha: number
    width: number
    height: number
  } | null

  /* 新增数据 */
  expUI: boolean // 是否展示实验组UI(当前用户属于实验组)
  onShowPayload: {} // onNoticeShow 触发时携带的数据, 也可以在 getLayoutOpts 函数中透传setModeWithQueue 的onShow参数
  onHidePayload: {} // onNoticeHide 触发时携带的数据, 也可以在 getLayoutOpts 函数中透传setModeWithQueue 的onNoticeHide参数
}

Package Sidebar

Install

npm i @hyext/popup

Weekly Downloads

85

Version

2.6.1

License

ISC

Unpacked Size

58 kB

Total Files

16

Last publish

Collaborators

  • hy-ext
  • wundereye
  • maizhiying
  • xiangwang123
  • zhangjiaheng
  • limingyi_100