Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions packages/shared/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,117 @@ export const PLATFORM_CONFIG_MAP = {
type: PLATFORM_TYPE.QUICK
},
}

export const TT_SPECIFIC_COMPONENTS = new Set([
'page-container',
'slot',
'custom-wrapper',
'clue-order-form',
'aweme-group',
'pay-button',
'address-area',
'consume-card',
'aweme-data',
'rate-button',
'store-area',
'inline-payment-panel',
'aweme-user-card',
'aweme-live-book',
'draw-ad',
'lynx-view',
'flow-ad',
'ai-agent-chat',
'component'
])

export const DEFAULT_COMPONENTS = new Set<string>([
'view',
'scroll-view',
'swiper',
'cover-view',
'cover-image',
'icon',
'text',
'rich-text',
'progress',
'button',
'checkbox',
'form',
'input',
'label',
'picker',
'picker-view',
'picker-view-column',
'radio',
'radio-group',
'checkbox-group',
'slider',
'switch',
'textarea',
'navigator',
'audio',
'image',
'video',
'camera',
'live-player',
'live-pusher',
'map',
'canvas',
'open-data',
'web-view',
'swiper-item',
'movable-area',
'movable-view',
'functional-page-navigator',
'ad',
'block',
'import',
'official-account',
'editor'
])

export const UNITLESS_PROPERTIES_SET = new Set([
'animation-iteration-count',
'border-image-outset',
'border-image-slice',
'border-image-width',
'box-flex',
'box-flex-group',
'box-ordinal-group',
'column-count',
'columns',
'flex',
'flex-grow',
'flex-positive',
'flex-shrink',
'flex-negative',
'flex-order',
'grid-area',
'grid-row',
'grid-row-end',
'grid-row-span',
'grid-row-start',
'grid-column',
'grid-column-end',
'grid-column-span',
'grid-column-start',
'font-weight',
'line-clamp',
'line-height',
'opacity',
'order',
'orphans',
'tab-size',
'widows',
'z-index',
'zoom',
// SVG-related properties
'fill-opacity',
'flood-opacity',
'stop-opacity',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-miterlimit',
'stroke-opacity',
'stroke-width',
])
22 changes: 22 additions & 0 deletions packages/shared/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,25 @@ export function indent (str: string, size: number): string {
})
.join('\n')
}

export enum TTRenderType {
V1 = 1,
V2 = 2,
}

declare const tt: any
let ttUseV2TTDom: boolean | undefined

export function isEnableTTDom() {
// 目前仅对于 react 支持 ttdom
if (process.env.TARO_ENV !== 'tt' || process.env.FRAMEWORK !== 'react' || typeof tt === 'undefined') {
return false
}
if (ttUseV2TTDom !== undefined) return ttUseV2TTDom

const ttMode = tt.getRenderMode ? tt.getRenderMode() : TTRenderType.V1

ttMode === TTRenderType.V2 && tt.__$enableTTDom$__ ? (ttUseV2TTDom = true) : (ttUseV2TTDom = false)

return ttUseV2TTDom
}
97 changes: 67 additions & 30 deletions packages/taro-react/src/props.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { convertNumber2PX, FormElement } from '@tarojs/runtime'
import { capitalize, internalComponents, isFunction, isNumber, isObject, isString, PLATFORM_TYPE, toCamelCase } from '@tarojs/shared'
import { convertNumber2PX, eventHandlerTTDom, FormElement, setInnerHTML } from '@tarojs/runtime'
import { capitalize, internalComponents, isEnableTTDom, isFunction, isNumber, isObject, isString, PLATFORM_TYPE, toCamelCase, UNITLESS_PROPERTIES_SET } from '@tarojs/shared'

import type { Style, TaroElement } from '@tarojs/runtime'

Expand All @@ -8,6 +8,7 @@ import type { Style, TaroElement } from '@tarojs/runtime'
export type Props = Record<string, unknown>

const IS_NON_DIMENSIONAL = /aspect|acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i
const IS_DATASET_OR_ARIA = /^(data|aria)-/

function isEventName (s: string) {
return s[0] === 'o' && s[1] === 'n'
Expand Down Expand Up @@ -125,6 +126,17 @@ function setEvent (dom: TaroElement, name: string, value: unknown, oldValue?: un
eventName = 'tap'
}

if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
if (isFunction(oldValue)) {
(dom as any).removeEventListener(`bind${eventName}`, dom[`__${eventName}__`])
}
if (isFunction(value)) {
dom[`__${eventName}__`] = eventHandlerTTDom.bind(null, dom, value)
dom.addEventListener(`bind${eventName}`, dom[`__${eventName}__`], isCapture)
}
Comment on lines +129 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

TT DOM 事件缓存键会让冒泡/捕获监听互相覆盖。

这里把缓存固定在 dom[\${eventName}`]onClickonClickCapture` 会落到同一个键上,后注册的会覆盖前一个;后续更新或卸载其中一个 prop 时,也会把另一个监听误删/遗留。

🔧 建议修复
 function setEvent (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) {
   const isCapture = name.endsWith('Capture')
+  const handlerKey = `__${name}__`
   let eventName = name.toLowerCase().slice(2)
   if (isCapture) {
     eventName = eventName.slice(0, -7)
   }
@@
   if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
     if (isFunction(oldValue)) {
-      (dom as any).removeEventListener(`bind${eventName}`, dom[`__${eventName}__`])
+      (dom as any).removeEventListener(`bind${eventName}`, dom[handlerKey], isCapture)
     }
     if (isFunction(value)) {
-      dom[`__${eventName}__`] = eventHandlerTTDom.bind(null, dom, value)
-      dom.addEventListener(`bind${eventName}`, dom[`__${eventName}__`], isCapture)
+      dom[handlerKey] = eventHandlerTTDom.bind(null, dom, value)
+      dom.addEventListener(`bind${eventName}`, dom[handlerKey], isCapture)
     }
     return
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/taro-react/src/props.ts` around lines 129 - 136, The TT DOM handler
cache currently reuses dom[`__${eventName}__`] for both capture and bubble
handlers, causing onClick and onClickCapture to override each other and
incorrect removals; change the cache key to include the capture flag (e.g.
`__${eventName}__capture` vs `__${eventName}__bubble` or append isCapture) so
eventHandlerTTDom bindings are stored separately, and ensure removeEventListener
reads the same capture-aware key before removing so
addEventListener/removeEventListener use the same stored function per
(eventName,isCapture) pair.

return
}

if (isFunction(value)) {
if (oldValue) {
dom.removeEventListener(eventName, oldValue as any, process.env.TARO_PLATFORM !== PLATFORM_TYPE.HARMONY ? false : undefined)
Expand Down Expand Up @@ -228,42 +240,50 @@ function setProperty (dom: TaroElement, name: string, value: unknown, oldValue?:
) {
// skip
} else if (name === 'style') {
if (/harmony.*cpp/.test(process.env.TARO_ENV || '')) {
return dom.setAttribute('_style4cpp', value)
}
const style = dom.style
if (isString(value)) {
style.cssText = value
if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
if (isString(value)) {
dom.setAttribute('style', value)
} else if (isObject(value)) {
dom.setAttribute('style', styleObjectToCss(value as StyleValue))
}
} else {
if (isString(oldValue)) {
style.cssText = ''
oldValue = null
if (/harmony.*cpp/.test(process.env.TARO_ENV || '')) {
return dom.setAttribute('_style4cpp', value)
}
const style = dom.style
if (isString(value)) {
style.cssText = value
} else {
if (isString(oldValue)) {
style.cssText = ''
oldValue = null
}

if (isObject<StyleValue>(oldValue)) {
for (const i in oldValue) {
if (!(value && i in (value as StyleValue))) {
// Harmony特殊处理
if (process.env.TARO_PLATFORM === PLATFORM_TYPE.HARMONY && i === 'position' && oldValue[i] === 'fixed') {
// @ts-ignore
dom.setLayer(0)
if (isObject<StyleValue>(oldValue)) {
for (const i in oldValue) {
if (!(value && i in (value as StyleValue))) {
// Harmony特殊处理
if (process.env.TARO_PLATFORM === PLATFORM_TYPE.HARMONY && i === 'position' && oldValue[i] === 'fixed') {
// @ts-ignore
dom.setLayer(0)
}
setStyle(style, i, '')
}
setStyle(style, i, '')
}
}
}

if (isObject<StyleValue>(value)) {
for (const i in value) {
if (!oldValue || !isEqual(value[i], (oldValue as StyleValue)[i])) {
// Harmony特殊处理
if (process.env.TARO_PLATFORM === PLATFORM_TYPE.HARMONY && i === 'position') {
if (value[i] === 'fixed' || (value[i] !== 'fixed' && oldValue?.[i])) {
// @ts-ignore
dom.setLayer(value[i] === 'fixed' ? 1 : 0)
if (isObject<StyleValue>(value)) {
for (const i in value) {
if (!oldValue || !isEqual(value[i], (oldValue as StyleValue)[i])) {
// Harmony特殊处理
if (process.env.TARO_PLATFORM === PLATFORM_TYPE.HARMONY && i === 'position') {
if (value[i] === 'fixed' || (value[i] !== 'fixed' && oldValue?.[i])) {
// @ts-ignore
dom.setLayer(value[i] === 'fixed' ? 1 : 0)
}
}
setStyle(style, i, value[i])
}
setStyle(style, i, value[i])
}
}
}
Expand All @@ -275,10 +295,17 @@ function setProperty (dom: TaroElement, name: string, value: unknown, oldValue?:
const oldHtml = (oldValue as DangerouslySetInnerHTML)?.__html ?? ''
if (newHtml || oldHtml) {
if (oldHtml !== newHtml) {
dom.innerHTML = newHtml
if (process.env.TARO_ENV === 'tt' && isEnableTTDom()) {
setInnerHTML(dom, newHtml)
} else {
dom.innerHTML = newHtml
}
}
}
} else if (!isFunction(value)) {
if (process.env.TARO_ENV === 'tt' && isEnableTTDom() && !IS_DATASET_OR_ARIA.test(name)) {
name = toCamelCase(name)
}
if (value == null) {
dom.removeAttribute(name)
} else {
Expand All @@ -297,3 +324,13 @@ function setPseudo(dom: TaroElement, name: '::after' | '::before', value: StyleV
dom.set_pseudo_before(value)
}
}

function styleObjectToCss(style: StyleValue) {
return Object.entries(style)
.map(([key, value]) => {
const kebabCaseKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
const cssValue = typeof value === 'number' && !UNITLESS_PROPERTIES_SET.has(kebabCaseKey) ? `${value}px` : value === null ? '' : value
return `${kebabCaseKey}: ${cssValue};`
})
.join(' ')
}
Comment on lines +328 to +336
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

styleObjectToCssundefined 值会生成无效 CSS。

当 style 对象中某个属性值为 undefined 时(如 { color: undefined }),条件 value === null 不匹配,typeof value === 'number' 也不匹配,最终 cssValueundefined,生成 color: undefined; 这样的无效 CSS 字符串。

🔧 建议修复
 function styleObjectToCss(style: StyleValue) {
   return Object.entries(style)
+    .filter(([, value]) => value != null)
     .map(([key, value]) => {
       const kebabCaseKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
       const cssValue = typeof value === 'number' && !UNITLESS_PROPERTIES_SET.has(kebabCaseKey) ? `${value}px` : value === null ? '' : value
       return `${kebabCaseKey}: ${cssValue};`
     })
     .join(' ')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function styleObjectToCss(style: StyleValue) {
return Object.entries(style)
.map(([key, value]) => {
const kebabCaseKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
const cssValue = typeof value === 'number' && !UNITLESS_PROPERTIES_SET.has(kebabCaseKey) ? `${value}px` : value === null ? '' : value
return `${kebabCaseKey}: ${cssValue};`
})
.join(' ')
}
function styleObjectToCss(style: StyleValue) {
return Object.entries(style)
.filter(([, value]) => value != null)
.map(([key, value]) => {
const kebabCaseKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
const cssValue = typeof value === 'number' && !UNITLESS_PROPERTIES_SET.has(kebabCaseKey) ? `${value}px` : value === null ? '' : value
return `${kebabCaseKey}: ${cssValue};`
})
.join(' ')
}
🤖 Prompt for AI Agents
In `@packages/taro-react/src/props.ts` around lines 328 - 336, In
styleObjectToCss, undefined property values produce invalid CSS like "color:
undefined;"; update the function (styleObjectToCss) to treat undefined the same
as null by either filtering out entries with value === undefined before mapping
or by changing the cssValue logic to check value === null || value === undefined
so undefined becomes '' (or skips the property), keeping the existing numeric px
logic that uses UNITLESS_PROPERTIES_SET for kebabCaseKey.

Loading
Loading