跳到主要内容

基于跨平台的svg组件编写一个svg编辑器

· 阅读需 3 分钟
duxapp
duxapp框架作者

duxapp 提供了一套跨平台的 SVG 编辑器组件,支持在多种环境中创建和编辑 SVG 图形。该编辑器包含以下核心功能:

  • 插入图片
  • 绘制自由路径
  • 添加文本
  • 创建基本形状(矩形、圆形、线条等)
  • 对元素进行移动、缩放和旋转操作

效果图

快速开始​

import { chooseMedia } from '@/duxapp/utils/net/util'
import {
Header,
TopView,
Column,
SvgEditor,
SvgEditorController,
useSvgEditorController,
px
} from '@/duxui'

export default function SvgEditorExample() {
// 获取编辑器控制器
const context = useSvgEditorController()

return (
<TopView>
<Header title='SVG编辑器' />
<Column grow className='m-3 bg-white'>
{/* 编辑器画布 */}
<SvgEditor
width='100%'
height='100%'
{...context.editor}
/>
</Column>
{/* 编辑器控制面板 */}
<SvgEditorController
{...context.controller}
selectImage={selectImage}
className='m-3 bg-white r-1'
style={{ height: px(100), marginTop: 0 }}
/>
</TopView>
)
}

// 选择图片的回调函数
const selectImage = async () => {
const res = await chooseMedia('image', { count: 1 })
return res[0].path
}

核心组件说明​

SvgEditor 组件​

编辑器主画布,接收以下主要属性:

  • defaultValue: 初始SVG内容

  • onChange: 内容变更回调

  • mode: 当前编辑模式(path/text/ellipse/line/rect)

  • 各形状的样式属性(pathProps/textProps等)

SvgEditorController 组件​

提供可视化操作界面,主要属性:

  • selectImage: 图片选择回调函数

  • 继承自Column组件的布局属性

useSvgEditorController Hook​

用于获取编辑器状态和控制方法:

const { editor, controller } = useSvgEditorController()

返回的两个属性分别用于绑定 SvgEditor 组件 和 SvgEditorController 组件

最后​

编写这个组件主要目的是为了检验跨平台的Svg组件的功能完整性,同时将源码开放,供大家使用或者学习参考

开发文档
GitHub

duxapp中主题系统是如何实现动态切换的

· 阅读需 9 分钟
duxapp
duxapp框架作者

在旧版本的duxapp,支持主题功能,但是那时候的主体是静态配置的,并不支持动态切换,新版本,在旧的静态主题基础上扩展,实现了动态主题切换

旧版本静态主题​

在之前的版本中已经支持主题功能,在用户配置用,使用模块的 theme 字段配置主题,像下面这样

// configs/config/index.js
option: {
// 基础模块
duxapp: {
theme: {
primaryColor: '#CDDE00',
secondaryColor: '#FDD000',
successColor: '#34a853',
warningColor: '#fbbc05',
dangerColor: '#ea4335',
pageColor: '#fafbf8'
}
}
}

配置了这些主题参数,会通过一个脚本转化为scss变量被加入到全局scss变量中,就像下面这样

$duxappPrimaryColor: #CDDE00;
$duxappSecondaryColor: #FDD000;
$duxappSuccessColor: #34a853;
$duxappDangerColor: #ea4335;
$duxappWarningColor: #fbbc05;
$duxappPageColor: #fafbf8;

然后你就能在任何scss文件中调用这些变量,例如 duxappStyle 的全局scss中调用这些变量,当然也不局限于这个文件,任何的scss都能调用这些变量

// src/duxappStyle/app.scss
.bg-primary {
background-color: $duxappPrimaryColor;
}

.bg-secondary {
background-color: $duxappSecondaryColor;
}

.bg-success {
background-color: $duxappSuccessColor;
}

然后在你的项目中就可以调用这些全局类名就能获得对应的样式

<View className='bg-primary' />

如果要编写 style 的时候获取这些主题参数,可以像下面这样

import { duxappTheme } from '@/duxapp'

<View style={{ backgroundColor: duxappTheme.primaryColor }} />
  • 关于如何定制自己模块的主题,查看这个文档

新版本的动态主题​

为了最小的升级成本,系统未对之前的主题系统进行大改,而是在当前的主题系统模式上进行简单调整就能使用

之前所有的内容在新的主题系统中都是生效的,如果你要切换到动态主题,只需要在用户配置中,配置多套主题即可

  • 将当前的模块配置中的 theme 移动到 themes.light (每个模块的配置都需要同样的操作)
  • 在 themes 里面新增一个主题配置 dark,表示夜间模式的主题
信息
  • 如果没有指定 themeConfig.default light将会作为默认主题,
  • 在你配置 dark 的主题的时候,如果和默认配置 light 相同的配置,你可以不配置,只需要与 light 不同的部分即可
  • 新增 themeConfig 配置项目,参考下面的示例,需要在里面配置主题列表 themes (必须配置) 其他三个选项为可选配配置,dark 用于指定页面模式主题,light 用于指定白天模式主题,default 指定默认主题
  • 只有当夜间模式和白天模式两个主题都存在的情况下系统才会跟随系统主题进行系统切换
  • 除了 light dark 你还可以配置更多的主题,通过 theme.setMode(主题) 切换
const config = {
option: {
// 基础模块
duxapp: {
themeConfig: {
themes: {
light: {
name: '明亮主题',
color: '#fff'
},
dark: {
name: '暗黑主题',
color: '#333'
}
},
// dark: 'dark',
// light: 'light',
// default: 'light'
},
themes: {
light: {
primaryColor: '#E70012',
secondaryColor: '#0092e8',
successColor: '#34a853',
warningColor: '#fbbc05',
dangerColor: '#ea4335',
pageColor: '#F7F9FC',

textColor1: '#373D52',
textColor2: '#73778E',
textColor3: '#A1A6B6',
textColor4: '#FFF',
header: {
color: '#fff', // 仅支持rgb hex值,请勿使用纯单词 设置为数组将显示一个渐变按钮
textColor: '#000', // 文本颜色
showWechat: true, // 微信公众号是否显示header
showWap: true, // h5是否显示header
}
},
dark: {
pageColor: '#1E1E1E',

whiteColor: '#181818',
blackColor: '#fff',
lineColor: '#1F1F1F',

textColor1: '#FFF',
textColor2: '#A1A6B6',
textColor3: '#73778E',
textColor4: '#373D52',
header: {
color: '#121212',
textColor: '#fff'
},
loading: {
dark: '#fff',
blank: '#7a7a7a'
}
}
}
},
duxui: {
themes: {
light: {
button: {
radiusType: 'round'
}
},
dark: {
tabBar: {
nameColor: '#888',
nameHoverColor: '#fff'
}
}
}
}
}
}

export default config

动态切换​

默认情况下只需要你配置了 light 和 dark 两个主题,程序就能跟随系统自动切换

如果你需要手动切换,下面是一动态切换主题的示例代码,参考这个进行开发,theme 是基础模块导出的工具

信息
  • 动态切换(包括自动切换)现在仅支持 小程序 H5端,其他平台还在开发中
  • 不支持的平台会按照配置的默认主题显示
import { Header, ScrollView, TopView, GroupList, theme, Button } from '@/duxuiExample'

export default function ThemeExample() {

const mode = theme.useMode(true)

const modes = theme.useModes()

return <TopView>
<Header title='Theme' />
<ScrollView>
<GroupList>
<GroupList.Item title='主题切换功能' desc='主题切换当前仅支持小程序和H5端,其他端还在努力开发中'
className='gap-3'
>
{
modes.map(item => <Button
type='primary'
plain={item.mode !== mode}
key={item.name}
onClick={() => item.switch()}
size='l'
>{item.name}</Button>)
}
</GroupList.Item>
</GroupList>
</ScrollView>
</TopView>
}

如何实现的​

动态主题在不同的平台使用了不同的实现方案,具体来说,小程序 H5端使用了css变量,RN端使用了插件动态修改组件代码实现动态切换

小程序 H5端​

小程序 H5端 css 变量 的实现流程

  • duxapp-cli 先将用户配置的主题名称进行统计并存储
  • duxapp-cli 根据用户主题配置生成主题scss文件 src/duxapp/userTheme/index.scss,这个文件会自动被 TopView组件引用
  • 编写一个 theme-loader 将其插入到 webpack 的 loader 中,在处理 scss 之前,先解析动态主题,目的是将调用到scss主题变量的代码,替换为调用css变量
  • 主题系统通过一个改变类型动态切换css变量的流程

RN端​

  • duxapp-cli 先将用户配置的主题名称进行统计并存储
  • duxapp-cli 根据用户主题配置生成主题js文件 src/duxapp/userTheme/index.rn.js
  • 编写一个 theme-loader 将其插入到 webpack 的 loader 中,在处理 scss 之前,先解析动态主题,目的是将调用到scss主题变量的代码,替换为调用css变量
  • 使用 patch-package 修改了 rn 端相关插件,主要改变的功能是,解析 theme-loader 生成的css变量,并替换成主题变量,并且将之前的 styleSheet 修改为函数的方式导出,函数接受一个主题名称,传入不同的主题名称,返回不同的 styleSheet
  • 修改插件,让插件在函数组件的头部,插入一个 hook ,这个 hook 会监听主题改变,并更新 styleSheet 并且在主题更新后,重新渲染组件

因为 React 组件的编写形式太过灵活多变,因此最后一步,在函数组件头部插入一个hook的操作,并不一定所有组件都能插入,局具体使用的显示参考这个说明

最后​

duxapp的主题系统经历了从静态配置到动态切换的演进过程:

  • 兼容性:新版本完美兼容旧版的静态主题配置

  • 灵活性:支持多主题配置和动态切换

  • 跨平台:针对不同平台采用最优实现方案

  • 易用性:提供简洁的API和配置方式

开发者可以根据项目需求选择合适的主题方案,通过简单的配置即可实现强大的主题功能。未来版本将继续优化各平台的兼容性和性能表现。

主题开发文档

开发文档
GitHub

在Taro中开发一个跨端Svg组件,同时支持小程序、H5、React Native

· 阅读需 7 分钟
duxapp
duxapp框架作者

Taro系列中一直没有跨端的绘图工具,小程序端支持canvas但是不支持svg,RN端有 react-native-svg 支持svg,但是没有很好原生的canvas插件,社区的canvas都是基于WebView实现的,或者skia,这个插件的书写方式和canvas有较大的差异

所以开发了这个兼容小程序、H5和React Native 的Svg组件,来实现跨端绘图

前言​

在小程序上面,是不支持直接编写svg代码的,但是有间接的方式让他支持,就是使用Image渲染svg,或者使用css的背景图渲染一个svg,但是这样写有一定的局限性,例如要支持动画、组件事件,将svg保存到本地等都无法实现

所以我选择了使用Canvas来模拟开发svg的相关组件支持,在RN端则是使用现在已经有的 react-native-svg 这个插件,因此我们只需要在小程序和H5端模拟 react-native-svg 组件的功能,组件属性及其功能完全按照 react-native-svg 规范进行开发

当前组件属性支持​

Svg中有大量的组件,用来绘制各种图形或者图片

已经支持的组件​

  • Svg
    width height viewBox preserveAspectRatio
  • Rect
    x y width height
  • Circle
    cx cy r
  • Ellipse
    cx cy rx ry
  • Line
    x1 y1 x2 y2
  • Image
    x y width height preserveAspectRatio
  • Text
    x y dx dy fontSize fontWeight fontFamily textAnchor fontStyle
  • TSpan
    x y dx dy fontSize fontWeight fontFamily textAnchor fontStyle
  • Polyline
    points
  • Polygon
    points
  • Path
    d
  • Defs
  • Use
    href x y
  • G
  • LinearGradient
    x1 y1 x2 y2
  • RadialGradient
    cx cy rx ry fx fy
  • Stop
    offset stopColor stopOpacity
  • ClipPath

支持的公共属性​

  • id
  • style
  • opacity
  • fill
  • fillOpacity
  • stroke
  • strokeWidth
  • strokeOpacity
  • strokeLinecap
  • strokeLinejoin
  • strokeDasharray
  • strokeDashoffset
  • clipPath
  • origin
  • originX
  • originY
  • translate
  • translateX
  • translateY
  • rotation
  • scale
  • scaleX
  • scaleY
  • skew
  • skewX
  • skewY
  • transform (如果要使用变换动画,需要使用这个属性实现,其他变换属性RN端不支持动画)

动画支持​

因为选择了 react-native-svg 作为开发参考,因此在动画支持方面,也参照了RN的相关API Animated 和 Easing

下面是一个循环动画的使用示例

效果图

import {
Svg, Rect,
Animated, Easing
} from '@/duxui/components/Svg'
import { useEffect, useRef } from 'react'

const AnimatedRect = Animated.createAnimatedComponent(Rect)

const Loop = () => {

const primary = duxappTheme.primaryColor
const secondary = duxappTheme.secondaryColor

const x = useRef(new Animated.Value(0)).current

const size = 50

useEffect(() => {
setTimeout(() => {
Animated.loop(
Animated.timing(x, {
toValue: 4,
duration: 3000,
easing: Easing.bounce,
useNativeDriver: false
})
).start()
}, 500)
}, [x])

const width = pxNum(702)
const height = pxNum(360)
return <GroupList.Item title='循环动画' desc='利用 interpolate 将动画值映射到任意范围'>
<Svg width={width} height={height}>
<AnimatedRect
x={x.interpolate({
inputRange: [0, 1, 2, 3, 4],
outputRange: [0, width - size, width - size, 0, 0]
})}
y={x.interpolate({
inputRange: [0, 1, 2, 3, 4],
outputRange: [0, 0, height - size, height - size, 0]
})}
width={size}
height={size}
rx={10} ry={10} stroke={secondary} fill={primary}
/>
</Svg>
</GroupList.Item>
}

组件事件​

为了实现组件事件,模拟 React Native 的 PanResponder API,在小程序和H5端实现了相关的功能

大多数组件都支持以下触摸事件

  • onPress
  • onPressIn
  • onPressOut
  • onLongPress

下面两个示例,一个展示点击事件,一个事件结合动画实现拖拽效果

效果图

事件示例​

下面的代码我们能看到点击不同的组件将获得对应组件的事件,这三个形状是有覆盖关系的

const Event = () => {

return <GroupList.Item title='组件事件' desc='点击对应的图形会有对应的点击事件'>
<Svg width={100} height={100}>
<Circle
cx='50%'
cy='50%'
r='38%'
fill='red'
onPress={() => toast('点击圆形')}
/>
<Rect
x='20%'
y='20%'
width='60%'
height='60%'
fill='blue'
onPress={() => toast('点击正方形')}
/>
<Path
d='M50,5L20,99L95,39L5,39L80,99z'
fill='pink'
onPress={() => toast('点击五角星')}
/>
</Svg>
</GroupList.Item>
}

结合动画​

const TouchEvent = () => {
const primary = duxappTheme.primaryColor
const secondary = duxappTheme.secondaryColor

const movePan = useRef(new Animated.ValueXY({ x: 10, y: 10 }, { useNativeDriver: false })).current

const moveEvent = useRef(PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: () => {
movePan.setOffset({
x: movePan.x._value,
y: movePan.y._value
})
},
onPanResponderMove: (e, gestureState) => {
movePan.setValue({ x: gestureState.dx, y: gestureState.dy })
},
onPanResponderRelease: () => {
movePan.flattenOffset()
}
})).current

const moveOriginPan = useRef(new Animated.ValueXY({ x: 150, y: 50 }, { useNativeDriver: true })).current

const moveOriginEvent = useRef(PanResponder.create({
onMoveShouldSetPanResponder: () => true,
onPanResponderMove: (e, gestureState) => {
moveOriginPan.setValue({ x: gestureState.dx + 150, y: gestureState.dy + 50 })
},
onPanResponderRelease: () => {
Animated.timing(moveOriginPan, {
toValue: { x: 150, y: 50 },
duration: 600,
easing: Easing.bounce,
useNativeDriver: true
}).start()
}
})).current

return <GroupList.Item title='结合动画' desc='蓝色拖拽,红色拖拽后回弹'>
<Svg width={pxNum(702)} height={pxNum(300)}>
<Rect width={pxNum(702)} height={pxNum(300)} fill='#fff' />
<RectAnimated
width={50}
height={50}
fill={secondary}
{...moveEvent.panHandlers}
x={movePan.x}
y={movePan.y}
/>
<RectAnimated
width={50}
height={50}
fill={primary}
{...moveOriginEvent.panHandlers}
x={moveOriginPan.x}
y={moveOriginPan.y}
// translateX={moveOriginPan.x}
// translateY={moveOriginPan.y}
/>
</Svg>
</GroupList.Item>
}

转换为图片​

为了方便导入为图片,还封装了一个 SvgToImage 组件,专门将Svg导出为图片方便下一步的处理,请前往文档查看具体的使用方法

继续​

Svg相关功能是duxui库中提供的一个组件,详情请查看duxui文档

当前UI库中的二维码(QRCode)、签名(Sign) 组件都是使用这个Svg组件来实现的

如果你对这个项目有兴趣,可以查看文档,继续了解详情

Svg文档:http://duxapp.com/docs/duxui/svg

框架文档:http://duxapp.com

GitHub:https://github.com/duxapp

如何在 duxapp 中开发一个兼容 RN 的动画库

· 阅读需 7 分钟
duxapp
duxapp框架作者

Taro 一直以来都没有一个能兼容 RN 的动画方案,duxapp 中扩展了 createAnimation 方法,让这个方法兼容了 RN 端,下面让我们来看看实现思路

createAnimation方法​

这个方法是用来创建一个动画实例的,使用方法像下面这样,每次 step 创建一组动画,每组动画同时执行,执行完一组继续执行下一组,直到所有的动画执行完

const an = createAnimation()
.translate(50, 50).rotate(180).step()
.translate(0, 0).rotate(0).step()
.export()

将创建的结果设置给 View 的 animation 属性,这个动画就能被执行了

在 Taro 里面这个方法目前兼容小程序 和 H5 端,我们要实现的就是让他兼容 RN,这样后面要给我们的组件加动画就更加简单了,不用在要加动画的组件 H5 写一套,RN 再写一套

RN的动画方案​

RN 里面的动画和 createAnimation 这个方式可以说是天差地别,下面来看这个实现淡入动画的代码,这是一个官方示例

import React, { useRef, useEffect } from 'react';
import { Animated, Text, View } from 'react-native';

const FadeInView = (props) => {
const fadeAnim = useRef(new Animated.Value(0)).current // 透明度初始值设为0

React.useEffect(() => {
Animated.timing( // 随时间变化而执行动画
fadeAnim, // 动画中的变量值
{
toValue: 1, // 透明度最终变为1,即完全不透明
duration: 10000, // 让动画持续一段时间
}
).start(); // 开始执行动画
}, [fadeAnim])

return (
<Animated.View // 使用专门的可动画化的View组件
style={{
...props.style,
opacity: fadeAnim, // 将透明度绑定到动画变量值
}}
>
{props.children}
</Animated.View>
);
}

好在 RN 的动画库本身是很强大的,我们要做的是在 RN 端模拟实现一个 createAnimation, 还是有办法解决的

实现​

要实现 RN,我们需要两步

  • 1、编写一个类,用来模拟 createAnimation 方法,通过这个类创建一些动画数据
  • 2、将这个类创建的数据传递给一个自定义组件,这个组件里面将这些数据解析成动画,并执行这些动画

创建 Animation 类​

这个类就比较简单,模拟 createAnimation 每一个方法即可,并在 export() 之后生成一个数据并返回,因代码过长,下面是部分代码展示

export const create = option => new Animation(option)

class Animation {

constructor(option = {}) {
if (!option.duration) {
option.duration = 400
}
if (!option.timingFunction) {
option.timingFunction = 'linear'
}
if (!option.delay) {
option.delay = 0
}
if (!option.transformOrigin) {
option.transformOrigin = '50% 50% 0'
}
this.defaultOption = option
}

result = []

current = {}

export() {
const res = this.result
this.result = []
return res
}

step(option) {
if (Object.keys(this.current).length) {
this.result.push({
option: {
...this.defaultOption,
...option
},
action: this.current
})
this.current = {}
}
return this
}

set(name, value) {
this.current[name] = value
return this
}

translate(x, y) {
this.translateX(x)
return this.translateY(y)
}

translate3D(x, y, z) {
this.translateX(x)
this.translateY(y)
return this.translateZ(z)
}

translateX(val) {
return this.set('translateX', val)
}

translateY(val) {
return this.set('translateY', val)
}

translateZ(val) {
return this.set('translateZ', val)
}
}

创建组件实现动画​

这个地方相对会复杂一些,其中的难点有几个

  • 在小程序上的动画,是会根据 css 的默认值去执行变化的,但是 RN 上的默认值需要在动画中设置,因此,需要获取这个默认值
  • 将动画拆分成适合 RN 端动画组件的 style 属性

此处代码过长,可以前往 github 查看,或者使用 duxapp 创建项目之后就能看到

使用​

动画库写好之后我们就能给我们原有的一些组件进行改造了,例如 PullView,在这之前,这个组件是针对 RN 端和其他端写了两套代码的,现在只需要一套代码就实现了,下面的示例代码展示了这个组件动画实现方式

import { isValidElement, cloneElement, forwardRef, useState, useEffect, useRef, useImperativeHandle, useCallback } from 'react'
import { View } from '@tarojs/components'
import { asyncTimeOut, nextTick, px, pxNum, transformStyle, useBackHandler } from '@/duxapp/utils'
import { Absolute } from '../Absolute'
import { Animated } from '../Animated'
import './index.scss'

export const PullView = forwardRef(({
side = 'bottom',
style,
overlayOpacity = 0.5,
children,
masking = true,
group,
onClose,
modal,
mask = modal,
duration = 200
}, ref) => {

const [mainAn, setMainAn] = useState(Animated.defaultState)

const [maskAn, setMaskAn] = useState(Animated.defaultState)

const ans = useRef()

const refs = useRef({})
refs.current.onClose = onClose
refs.current.overlayOpacity = overlayOpacity

const translate = siteTranslates[side]

const close = useCallback(async () => {
let an = ans.current.main
if (side === 'center' && process.env.TARO_ENV !== 'rn') {
an = an.translate('-50%', '-50%')
}
setMainAn(an[translate.key](pxNum(translate.value)).opacity(0).step(
process.env.TARO_ENV !== 'rn' ? {
transformOrigin: '25% 25% 0'
} : {}
).export())
setMaskAn(ans.current.mask.opacity(0).step().export())
await asyncTimeOut(duration)
refs.current.onClose?.()
}, [duration, side, translate.key, translate.value])

useBackHandler(close, !mask)

useImperativeHandle(ref, () => {
return {
close
}
})

useEffect(() => {
nextTick(() => {
if (!ans.current) {
ans.current = {
main: Animated.create({
duration,
timingFunction: 'ease-in-out'
}),
mask: Animated.create({
duration,
timingFunction: 'ease-in-out'
})
}
}
if (side === 'center') {
let an = ans.current.main.scale(1).opacity(1)
if (process.env.TARO_ENV !== 'rn') {
an = an.translateX('-50%').translateY('-50%')
}
setMainAn(an.step().export())
} else {
setMainAn(ans.current.main.translateX(0).translateY(0).opacity(1).step().export())
}
setMaskAn(ans.current.mask.opacity(refs.current.overlayOpacity).step().export())
})
}, [duration, side])

return <Absolute group={group}>
{masking && <Animated.View
animation={maskAn}
className='PullView'
>
<View className='PullView__other'
onClick={() => {
if (mask) {
return
}
close()
}}
></View>
</Animated.View>}
<Animated.View
animation={mainAn}
className={`PullView__main PullView__main--${side}`}
style={{
...style,
transform: transformStyle(side === 'center' ? {
translateX: '-50%',
translateY: '-50%',
scaleX: 0.4,
scaleY: 0.4
} : {
[translate.key]: px(translate.value)
})
}}
>
{
isValidElement(children) ?
cloneElement(children, {
style: {
...children.props.style,
...(side === 'center'
? {}
: side === 'bottom' || side === 'top'
? { width: '100%' }
: { height: '100%' })
}
}) :
children
}
</Animated.View>
</Absolute>
})

const siteTranslates = {
top: { key: 'translateY', value: -200 },
bottom: { key: 'translateY', value: 200 },
left: { key: 'translateX', value: -200 },
right: { key: 'translateX', value: 200 },
center: { key: 'scale', value: 0.4 }
}

最后​

当然这个动画也不是完美的,只是实现了一个基础的动画,甚至使用的时候还有诸多的限制,你可以点击下面的动画文档查看详细的使用方法以及限制

Animated 动画文档

开发文档
GitHub

duxapp框架是如何实现对鸿蒙的兼容,开发过程中遇到了哪些问题

· 阅读需 13 分钟
duxapp
duxapp框架作者

Taro 4.0 版本已经搭建好了兼容鸿蒙的基础框架,duxapp 是在这个基础上进一步开发,降低了开发者上手门槛,提供了统一的 UI 库。

duxapp 是一个模块化的多端开发框架,其中提供了多端 UI 库,能真正实现一套代码开发小程序、React Native、H5、鸿蒙

兼容鸿蒙之前​

在开始兼容鸿蒙之前,我们来讲一下,duxapp 是一个什么状况

1、样式兼容性​

现在 duxapp 已经兼容 小程序、React Native以及H5了,因为 React Native 端在这些端中兼容的样式是最少的,我们是按照React Native的标准去做,我们针对 H5 端和 小程序端的基础样式做了调整

需要将 View 组件调整为默认 Flex 竖向布局、默认的定位方式、模型盒、以及边框等,像下面这样:

/* H5 端 */
taro-view-core {
display: flex;
flex-direction: column;
position: relative;
border-style: solid;
border-width: 0;
}
input,
textarea,
taro-view-core {
box-sizing: border-box;
}
/* 小程序端 */
view {
display: flex;
flex-direction: column;
position: relative;
border-style: solid;
border-width: 0;
}
input,
textarea,
view {
box-sizing: border-box;
}

通过这样的调整,我们在项目中使用 View 组件的时候将默认使用 Flex 竖向布局,这样编写的 css 就能做到同时兼容多个端了

2、现有模块​

duxapp 框架中现在已提供了很多的公开模块了,这里介绍三个相关的基础模块,这三个模块在我们兼容鸿蒙的过程中都有很大的关联

  • duxapp

这个和框架名称相同的模块,是所有模块的根模块,所有的模块都需要依赖于这个模块

这个模块内提供了一些非常基础的组件、函数、配置等,且一些东西是必须使用的

用来包裹页面的容器组件 TopView
路由跳转函数 route
导出的用户配置

  • duxui

这个是提供的UI组件库,模块内提供了大量兼容多端的组件库,使用这些组件,能快速开发出兼容多端的应用

  • duxappReactNative

React Native端支持,要兼容 React Native,必须使用这个模块,模块内有 React Native 端的三方依赖、用户协议、版本更新、权限管理等

Taro 和鸿蒙的现状​

Taro 4.0 目前有两套实现鸿蒙的方案

  • @tarojs/plugin-platform-harmony-hybrid

webview方案,将 H5 运行在 webview 上,和小程序类似

  • @tarojs/plugin-platform-harmony-ets

duxapp 中为了更好的性能和体验,选择的的是这个方案

底层通过 arkts 以及 arkui 实现,将 Taro 的组件转化为对应的 arkui 里面对应的组件,并实现了 React 的运行时

例如 View 组件会通过 arkui 的 Flex、Row、Column 这三个组件来实现,会根据编写的不同样式选择不同的原生组件去渲染,从这里也大概能看出,鸿蒙的 arkui 里面也是 Flex 布局

兼容过程​

1、创建一个鸿蒙模块​

就像上面介绍的 React Native 端那样,鸿蒙端也需要创建一个模块,用来处理鸿蒙端的基础内容,例如用到的三方依赖、配置插件等

这个模块的名称为 duxappHarmony

2、添加需要的三方依赖​

目前鸿蒙端仅支持使用 vite 编译,因此鸿蒙端需要添加编译的相关的依赖,在模块内创建 package.json,内容如下

{
"scripts": {
"build:harmony": "duxapp runtime enterFile && duxapp harmony create && taro build --type harmony",
"dev:harmony": "npm run build:harmony -- --watch"
},
"dependencies": {
"@tarojs/plugin-platform-harmony-ets": "4.0.7",
"vite": "^5.4.10",
"terser": "^5.36.0",
"@tarojs/vite-runner": "4.0.5",
"@vitejs/plugin-react": "^4.3.3",
"vite-plugin-commonjs": "^0.10.3"
}
}

这个完整的文件中还包含了编译鸿蒙的两个命令

3、配置依赖项​

添加了依赖这些依赖并不会自动起作用,需要配置 Taro 编译配置,我们在这个模块内创建一个 Taro 的配置文件 taro.config.js,内容如下

import commonjs from 'vite-plugin-commonjs'

export default {
// 鸿蒙编译插件
plugins: [
'@tarojs/plugin-platform-harmony-ets'
],
harmony: {
compiler: {
// 指定 vite 编译
type: 'vite',
vitePlugins: [
commonjs()
]
},
projectPath: './dist/harmony'
}
}

这里面配置了平台插件、指定使用 vite 编译、设置了项目目录

这其中还加入了一个 vite 插件,因为 vite 默认不支持 commonjs,但是 duxapp 项目中使用了commonjs,所以加入了这个插件

到此基础环境配置好了,但是我们还需要创建鸿蒙工程文件,Taro 没有提供这个内容

4、在cli内处理鸿蒙工程文件​

在第 2 步的命令中,duxapp harmony create 有这个命令,我们就是通过这个命令来创建的鸿蒙工程文件,这样就不需要再通过鸿蒙的 IDE 再去手动操作创建了,这也是一个相对麻烦的过程

这个过程和 React Native 端是类似的,React Native端也是自动创建安卓和 ios 的工程文件,鸿蒙的工程文件创建会放在 dist/harmony 目录中

创建工程文件的过程中,可以通过配置或脚本对工程文件进行处理,例如鸿蒙端需要包名、版本号、app 名称等信息,都是通过配置文件来生成的

这里就不详细展开了,可以查看文档了解 鸿蒙入门文档

5、组件库兼容​

上面的步骤处理完之后,框架的东西就处理完了,但是仍然是不可用的,因为要编写一个兼容多端的项目,现有的组件,还需要针对鸿蒙端做相应的兼容,要兼容的组件包括基础模块中的组件和 duxui 模块中的组件

鸿蒙端兼容是一个很费事的过程,对鸿蒙端的兼容性 React Native还难,难就难在,鸿蒙有一个非标准的 Flex

在上面介绍了 View 组件会被在不同的情况编译的 Flex Row Column,具体来说,

  • 当 align-items 的属性值为 stretch、daseline 或者 flex-wrap 的属性值为 wrap会使用 Flex 组件
  • 否则当 flex-direction 为 row 或者 row-reverse 使用 Row 组件
  • 其他的情况使用 Column 组件

因为鸿蒙的 Row 和 Column 组件的align-items不支持 stretch和daseline,而 Row组件又不支持 wrap 换行

在使用 Row 或者 Column 的情况下,这和标准 Flex 布局基本没多大差别,问题就出在使用 Flex 组件这,这个组件有个奇怪的特性

Flex组件主轴默认不设置时撑满父容器,Column、Row组件主轴不设置时默认是跟随子节点大小。

这是官方文档的描述,这是一个很奇怪的特性,且没有办法通过设置取消,这导致很多奇怪的布局行为,这个效果类似于给元素永久设置了个 flex: 1,且比这个好厉害,就算有多层父元素,他也能把外层父元素撑开,感觉它才是爹!

在做兼容的时候,我总结了关于这个特性的兼容方式:

当 flex 的 alitn-items 为 stretch 或者 daseline 时,需要给当前元素指定尺寸,在任何能不用 alitn-items 的这两个属性的时候,都不要用这两个属性。如果要使用stretch,建议父元素使用 alitn-items: flex-start,子元素使用 align-self: stretch,这样的形式

当然,除了 Flex 布局这个最大的问题之外,还有很多与标准不太一样的地方,我整理总结了放在 兼容性章节,在开发的时候需要特别注意

可以说我在兼容鸿蒙的过程中,大部分时间都是在做组件库的兼容,和这些样式斗智斗勇

开始使用​

通过上面这一系列的操作,duxapp 框架已经基本实现了兼容鸿蒙,且提供了大量可用的 UI 组件库

你可以快速初始化一个 UI 库的示例项目,你可以通过这个示例项目快速的查看到 UI 库在鸿蒙端以及其他端的效果

npx duxapp-cli create projectExample duxuiExample

在运行这个命令之前,你需要安装好 nodejs 20+ yarn git

创建项目后,进入项目运行以下命令

yarn dev:harmony --app=duxuiExample

编译完成后,使用 DevEco Studio 打开 dist/harmony 目录,这个目录就是一个原生鸿蒙项目

打开项目后,右上角工具栏区域,选择已经开启的虚拟机,并且点击右侧的运行按钮(三角符号),开始编译,编译后,会自动在虚拟机上启动这个 app

如果想继续开发项目,请查看鸿蒙入门文档

当前版本在 windows 系统上,仍然存在问题,能编译成功,但是启动 app 会报错,你可以先尝试使用其他系统进行编译,例如 Mac,这个问题将会在不久后修复

未来展望​

目前对鸿蒙的兼容取得了初步的成效,但是也还有少数一些组件和功能还未实现兼容,像下面这些,将逐步完善这些组件或功能

  • PullView Modal 等组件动画效果
  • Picker 组件暂不支持
  • Sign 签名组件待完善
  • 录音组件录音功能待完善
  • 微信模块相关功能暂不支持
  • 地图暂不支持
  • List、Detail 组件待完善

Taro 目前开发中的版本,将放弃使用鸿蒙的 Flex 布局方式,转而使用更底层的实现方式,使用了 React Native 的布局引擎 Yoga,用过 Yoga 引擎来来布局,将引擎的计算结果再赋值到具体的组件上,这样就能极大改善鸿蒙布局兼容性问题,等待 Taro 的这套方案开源后,duxapp 也将跟进

如果你有兴趣,请继续查看文档了解详情吧

开发文档
GitHub

Taro首个支持鸿蒙的 UI 库,同时还兼容 React Native、小程序、H5

· 阅读需 16 分钟
duxapp
duxapp框架作者

Taro 4.0 已经推出一段时间了,4.0 版本主要是支持了鸿蒙端的开发以及 Vite 编译工具的支持。duxapp 在这段时间也跟随 Taro 的脚步,实现的对鸿蒙端的支持,并且也将之前的 duxui 这个多端的 UI 库,对鸿蒙端实现了兼容。

duxui 组件库提供了 60+ 的组件支持,能快速帮助你完成业务。

现在使用这个 UI 库,不仅能开发鸿蒙,还能实现同时开发 React Native、小程序和 H5,也是目前唯一一个能兼容这么多端的 UI 库。

组件展示​

效果图

下面我将介绍这一切是如何做到的,这其中遇到了哪些问题

兼容鸿蒙之前​

在开始兼容鸿蒙之前,我们来讲一下,duxapp 是一个什么状况

1、样式兼容性​

现在 duxapp 已经兼容 小程序、React Native以及H5了,因为 React Native 端在这些端中兼容的样式是最少的,我们是按照React Native的标准去做,我们针对 H5 端和 小程序端的基础样式做了调整

需要将 View 组件调整为默认 Flex 竖向布局、默认的定位方式、模型盒、以及边框等,像下面这样:

/* H5 端 */
taro-view-core {
display: flex;
flex-direction: column;
position: relative;
border-style: solid;
border-width: 0;
}
input,
textarea,
taro-view-core {
box-sizing: border-box;
}
/* 小程序端 */
view {
display: flex;
flex-direction: column;
position: relative;
border-style: solid;
border-width: 0;
}
input,
textarea,
view {
box-sizing: border-box;
}

通过这样的调整,我们在项目中使用 View 组件的时候将默认使用 Flex 竖向布局,这样编写的 css 就能做到同时兼容多个端了

2、现有模块​

duxapp 框架中现在已提供了很多的公开模块了,这里介绍三个相关的基础模块,这三个模块在我们兼容鸿蒙的过程中都有很大的关联

  • duxapp

这个和框架名称相同的模块,是所有模块的根模块,所有的模块都需要依赖于这个模块

这个模块内提供了一些非常基础的组件、函数、配置等,且一些东西是必须使用的

用来包裹页面的容器组件 TopView
路由跳转函数 route
导出的用户配置

  • duxui

这个是提供的UI组件库,模块内提供了大量兼容多端的组件库,使用这些组件,能快速开发出兼容多端的应用

  • duxappReactNative

React Native端支持,要兼容 React Native,必须使用这个模块,模块内有 React Native 端的三方依赖、用户协议、版本更新、权限管理等

Taro 和鸿蒙的现状​

Taro 4.0 目前有两套实现鸿蒙的方案

  • @tarojs/plugin-platform-harmony-hybrid

webview方案,将 H5 运行在 webview 上,和小程序类似

  • @tarojs/plugin-platform-harmony-ets

duxapp 中为了更好的性能和体验,选择的的是这个方案

底层通过 arkts 以及 arkui 实现,将 Taro 的组件转化为对应的 arkui 里面对应的组件,并实现了 React 的运行时

例如 View 组件会通过 arkui 的 Flex、Row、Column 这三个组件来实现,会根据编写的不同样式选择不同的原生组件去渲染,从这里也大概能看出,鸿蒙的 arkui 里面也是 Flex 布局

兼容过程​

1、创建一个鸿蒙模块​

就像上面介绍的 React Native 端那样,鸿蒙端也需要创建一个模块,用来处理鸿蒙端的基础内容,例如用到的三方依赖、配置插件等

这个模块的名称为 duxappHarmony

2、添加需要的三方依赖​

目前鸿蒙端仅支持使用 vite 编译,因此鸿蒙端需要添加编译的相关的依赖,在模块内创建 package.json,内容如下

{
"scripts": {
"build:harmony": "duxapp runtime enterFile && duxapp harmony create && taro build --type harmony",
"dev:harmony": "npm run build:harmony -- --watch"
},
"dependencies": {
"@tarojs/plugin-platform-harmony-ets": "4.0.7",
"vite": "^5.4.10",
"terser": "^5.36.0",
"@tarojs/vite-runner": "4.0.5",
"@vitejs/plugin-react": "^4.3.3",
"vite-plugin-commonjs": "^0.10.3"
}
}

这个完整的文件中还包含了编译鸿蒙的两个命令

3、配置依赖项​

添加了依赖这些依赖并不会自动起作用,需要配置 Taro 编译配置,我们在这个模块内创建一个 Taro 的配置文件 taro.config.js,内容如下

// eslint-disable-next-line import/no-commonjs
const commonjs = require('vite-plugin-commonjs').default

export default {
// 鸿蒙编译插件
plugins: [
'@tarojs/plugin-platform-harmony-ets'
],
harmony: {
compiler: {
// 指定 vite 编译
type: 'vite',
vitePlugins: [
commonjs()
]
},
projectPath: './dist/harmony'
}
}

这里面配置了平台插件、指定使用 vite 编译、设置了项目目录

这其中还加入了一个 vite 插件,因为 vite 默认不支持 commonjs,但是 duxapp 项目中使用了commonjs,所以加入了这个插件

到此基础环境配置好了,但是我们还需要创建鸿蒙工程文件,Taro 没有提供这个内容

4、在cli内处理鸿蒙工程文件​

在第 2 步的命令中,duxapp harmony create 有这个命令,我们就是通过这个命令来创建的鸿蒙工程文件,这样就不需要再通过鸿蒙的 IDE 再去手动操作创建了,这也是一个相对麻烦的过程

这个过程和 React Native 端是类似的,React Native端也是自动创建安卓和 ios 的工程文件,鸿蒙的工程文件创建会放在 dist/harmony 目录中

创建工程文件的过程中,可以通过配置或脚本对工程文件进行处理,例如鸿蒙端需要包名、版本号、app 名称等信息,都是通过配置文件来生成的

这里就不详细展开了,可以查看文档了解 鸿蒙入门文档

5、组件库兼容​

上面的步骤处理完之后,框架的东西就处理完了,但是仍然是不可用的,因为要编写一个兼容多端的项目,现有的组件,还需要针对鸿蒙端做相应的兼容,要兼容的组件包括基础模块中的组件和 duxui 模块中的组件

鸿蒙端兼容是一个很费事的过程,对鸿蒙端的兼容性 React Native还难,难就难在,鸿蒙有一个非标准的 Flex

在上面介绍了 View 组件会被在不同的情况编译的 Flex Row Column,具体来说,

  • 当 align-items 的属性值为 stretch、daseline 或者 flex-wrap 的属性值为 wrap会使用 Flex 组件
  • 否则当 flex-direction 为 row 或者 row-reverse 使用 Row 组件
  • 其他的情况使用 Column 组件

因为鸿蒙的 Row 和 Column 组件的align-items不支持 stretch和daseline,而 Row组件又不支持 wrap 换行

在使用 Row 或者 Column 的情况下,这和标准 Flex 布局基本没多大差别,问题就出在使用 Flex 组件这,这个组件有个奇怪的特性

Flex组件主轴默认不设置时撑满父容器,Column、Row组件主轴不设置时默认是跟随子节点大小。

这是官方文档的描述,这是一个很奇怪的特性,且没有办法通过设置取消,这导致很多奇怪的布局行为,这个效果类似于给元素永久设置了个 flex: 1,且比这个好厉害,就算有多层父元素,他也能把外层父元素撑开,感觉它才是爹!

在做兼容的时候,我总结了关于这个特性的兼容方式:

当 flex 的 alitn-items 为 stretch 或者 daseline 时,需要给当前元素指定尺寸,在任何能不用 alitn-items 的这两个属性的时候,都不要用这两个属性。如果要使用stretch,建议父元素使用 alitn-items: flex-start,子元素使用 align-self: stretch,这样的形式

当然,除了 Flex 布局这个最大的问题之外,还有很多与标准不太一样的地方,我整理总结了放在 兼容性章节,在开发的时候需要特别注意

可以说我在兼容鸿蒙的过程中,大部分时间都是在做组件库的兼容,和这些样式斗智斗勇

开始使用​

通过上面这一系列的操作,duxapp 框架已经基本实现了兼容鸿蒙,且提供了大量可用的 UI 组件库

你可以快速初始化一个 UI 库的示例项目,你可以通过这个示例项目快速的查看到 UI 库在鸿蒙端以及其他端的效果

npx duxapp-cli create projectExample duxuiExample

在运行这个命令之前,你需要安装好 nodejs 20+ yarn git

创建项目后,进入项目运行以下命令

yarn dev:harmony --app=duxuiExample

编译完成后,使用 DevEco Studio 打开 dist/harmony 目录,这个目录就是一个原生鸿蒙项目

打开项目后,右上角工具栏区域,选择已经开启的虚拟机,并且点击右侧的运行按钮(三角符号),开始编译,编译后,会自动在虚拟机上启动这个 app

如果想继续开发项目,请查看鸿蒙入门文档

当前版本在 windows 系统上,仍然存在问题,能编译成功,但是启动 app 会报错,你可以先尝试使用其他系统进行编译,例如 Mac,这个问题将会在不久后修复

未来展望​

目前对鸿蒙的兼容取得了初步的成效,但是也还有少数一些组件和功能还未实现兼容,像下面这些,将逐步完善这些组件或功能

  • PullView Modal 等组件动画效果
  • Picker 组件暂不支持
  • Sign 签名组件待完善
  • 录音组件录音功能待完善
  • 微信模块相关功能暂不支持
  • 地图暂不支持
  • List、Detail 组件待完善

Taro 目前开发中的版本,将放弃使用鸿蒙的 Flex 布局方式,转而使用更底层的实现方式,使用了 React Native 的布局引擎 Yoga,用过 Yoga 引擎来来布局,将引擎的计算结果再赋值到具体的组件上,这样就能极大改善鸿蒙布局兼容性问题,等待 Taro 的这套方案开源后,duxapp 也将跟进

如果你有兴趣,请继续查看文档了解详情吧

开发文档
GitHub

duxui:基于Taro,兼容React Native、小程序、H5的多端UI库

· 阅读需 13 分钟
duxapp
duxapp框架作者

duxui是duxapp官方开发的一款兼容多端的UI组件库,兼容小程序、H5、React Native,库中提供了60+的组件,覆盖大部分使用场景

它能帮助你通过统一的组件样式,快速完成多端应用的开发,包括React Native端的APP开发

duxui已经被我用于很多项目中,其中包含APP的项目就开发了几十个了,你可以通过这个链接去查看或者体验这些APP,https://app.share.dux.plus/,这里只包含了其中一部分,其中一些APP可能因为停止运营,无法正常浏览

示例​

如果你想看到这些组件的展示效果,请根据不同的端进行查看

如果你想通过项目来查看这些组件的运行效果或者组件源码,使用下面这个命令创建一个UI库的示例代码

npx duxapp-cli create projectName duxuiExample

其中的 projectName 是要创建的项目名称,创建之后根命令行据提示进行下一步操作

使用​

这是duxapp的一个模块,需要在duxapp中使用才能使用这个模块,UI库的文档请查看https://duxapp.com/docs/duxui/start

如果你还没有这个模块,需要先安装duxui模块,如果你还不知道什么是duxapp框架,请先查看这篇文章了解

yarn duxapp app add duxui

安装之后在你的模块中导入即可使用,像下面这样

import { Column, Header, Image, px, Row, ScrollView, Tab, TabItem, Text, TopView } from '@/duxui'
import { GridIcon } from '@/gridOperator'

export default function LedgerDetail() {
return <TopView>
<Header title='企业台账详情' />
<ScrollView>
<Row className='mt-3 mh-3 bg-white r-1 p-3 gap-3 items-start'>
<Image style={{ width: px(80) }} square />
<Column grow className='mt-1 gap-1'>
<Text numberOfLines={1}>鄂尔多斯市九工建筑有限责任公司</Text>
<Text className='mt-1' size={18} color={2}>联系人:小阿呆 | 联系电话:010-4521-8652</Text>
<Text size={18} color={2}>所属网格片区:<Text type='primary'>轻纺织产业园</Text> 历史问题:<Text type='danger'>12个</Text></Text>
</Column>
<GridIcon className='self-center text-primary' size={56} name='biaoqianlanbodianhua' />
</Row>
<Column className='mt-3'>
<Tab>
{
tabs.map(item => <TabItem key={item.value} title={item.name} paneKey={item.value} />)
}
</Tab>
</Column>
<Column className='mt-3 mh-3 bg-white r-1 p-3'>
<Text size={1} style={{ lineHeight: px(42) }}>公司是适应市场经济社会化大生产的需要而形成的一种企业组织形式。中国的公司是指依照《中华人民共和国公司法》在中国境内设立的以营利为目的社团法人,包括有限责任公司和股份有限公司。 指企业的组织形式。以营利为目的的社团法人。在资本主义社会获得高度发展。我国在建国后对私营公司进行了社会主义改造。国营工、商、建筑、运输等部门中实行独立经济核算的经营管理组织和某些城市中按行业划分的专业管理机构,也通称公司。随着我国经济体制的改革,享有法人资格的各种公司纷纷设立,按章程从事自身的生产经营活动。 指企业的组织形式。以营利为目的的社团法人。在资本主义社会获得高度发展。我国在建国后对私营公司进行了社会主义改造。国营工、商、建筑、运输等部门中实行独立经济核算的经营管理组织和某些城市中按行业划分的专业管理机构,也通称公司。随着我国经济体制的改革,享有法人资格的各种公司纷纷设立,按章程从事自身的生产经营活动。 折叠</Text>
</Column>
</ScrollView>
</TopView>
}

const tabs = [
{ name: '基本信息', value: 0 },
{ name: '上报工单', value: 1 },
{ name: '历史问题', value: 2 }
]

组件库​

下面我来介绍一下这些组件和组件的功能

TopView 顶层容器​

TopView是作为页面的根组件使用的,在duxapp中,每个页面都需要存在这个组件,他用来实现duxapp的多项功能

Header 头部导航​

duxapp默认将头部设置为了自定义,每个页面面建议都使用一个头部组件作为标题的显示,且Header组件会控制状态栏颜色显示、H5页面标题显示

ScrollView 滚动容器​

这是对Taro ScrollView的封装,实现了多个端的下拉刷新功能,ScrollView仅支持垂直滚动,需要横向滚动使用 ScrollView.Horizontal

PullView 弹窗​

可以从上下左右4个方向弹出内容

Loading 加载动画​

用于展示loading动画,类似于ios那样的菊花

Absolute 绝对定位​

放在这个组件内的子元素,会被渲染在最外层,这是用TopView.add方法实现的

List 分页列表​

当你的列表接口采用 page 进行分页时,可以用这个组件快速实现列表页面,这个组件有用以下特性

  • 下拉刷新
  • 自动分页
  • 空组件展示
  • 自定义头部底部渲染
  • RN端使用 @shopify/flash-list 实现,拥有更好的性能
  • 多列支持

Layout 布局计算​

用来计算Layout所在的组件的位置、尺寸信息

ActionSheet 弹出菜单​

封装弹出菜单功能

Button 按钮​

按钮组件

Cell 单元格​

单元格组件用来展示竖向的菜单列表,默认拥有阴影属性

LinearGradient 线性渐变​

因为RN不支持通过css编写渐变,所以写了这个组件用于实现渐变功能

BoxShadow 阴影​

由于RN 安卓端对阴影的支持不太完善,使用这个组件可以实现阴影效果

Column flex竖向​

这是用于快速竖向布局的组件

Row flex横向​

这是用于快速横向布局的组件

Space 间距​

用于控制子元素的间距,通过 gap 实现,建议直接使用全局样式的 gap

Divider 分割线​

使用边框实现的分割线

Grid 宫格​

用于实现9宫格这样的布局方式

Card 卡片​

卡片布局,带有外边距,内边距、圆角、阴影的组件

Tab 选项卡​

选项卡切换,也可以用于表单项使用

TabBar 底部导航​

通常用于app首页的底部导航,要使用这个组件需要通过创建函数创建

Elevator 电梯楼层​

例如城市选择,可以通过城市名称首字母进行导航,快速选择到需要的城市,就可以用这个组件

下拉菜单,常见用于一些表单的筛选

Form 表单​

封装了表单组件,能快速完成表单的布局、表单验证、复杂对象表单

表单包含了一系列的组件或者方法,包括下面这些

  • Form 表单
  • FormItem 表单项
  • FormSubmit 表单提交
  • FormReset 表单重置
  • FormObject 对象表单
  • FormArray 数组表单
  • FormArrayAction 数组表单操作管理
  • useFormContext 获取表单上下文
  • useFormItemProxy 给表单代理值和事件

Input 输入框​

对Taro Input的封装,新增了一些属性,基本不带样式,需要自行编写样式

Textarea 多行文本​

对Taro Textarea的封装,新增了一些属性

Picker 选择器​

选择器包含了单列选择器和多列选择器

PickerDate 日期选择​

用于日期时间的选择

Radio 单选​

单选组件

Checkbox 多选​

多选组件

Switch 开关​

开关组件

Calendar 日历​

为兼容多端,所以编写的日历组件,支持日、周、范围选择,支持多种自定义日历的方式

Grade 评分​

评分组件

Cascade 级联选择​

一个功能丰富的级联选择器,支持单选、多选,支持多级选择、单级选择,等更多功能

CardSelect 卡片选择​

一个丰富样式的卡片类型的选择器,同时支持单选和多选模式

Upload 上传​

用于上传图片或者视频的组件

ModalForm 弹出表单​

用于将其他表单封装为一个弹出表单,例如单列选择器、日期选择器都是由这个组件封装的

这里包含了

  • ModalForm 单出单个表单,作为一个表单使用,放在 FormItem 中
  • ModalForms 弹出多个表单,独立内容,放在Form内任何位置

NumberKeyboard 数字键盘​

用于数字的输入,例如输入验证码,支付密码、电话号码、身份证等

InputCode 验证码密码输入​

通常需要配合 NumberKeyboard 组件来实现验证码的输入功能

LicensePlate 车牌号输入​

车牌号输入组件

Text 文本​

对Text的封装,对主题色、字号等进行了主题配置,对常用css的快捷属性封装

Image 图片​

对图片的封装,实现了预览,多图预览,在RN端使用 expo-image 实现,提供更好的性能

Badge 徽标​

用于展示未读消息数量,红点

Tag 标签​

标签展示

Avatar 头像​

显示一个头像或者头像组

HtmlView 富文本显示​

用于显示富文本

Step 步骤条​

例如快递更新日志,就可以用这个组件来实现

Empty 空数据​

某些列表数据为空时可以用这个组件显示

Status 角标状态​

显示在卡片四个角的状态

LongPress 长按​

长按事件封装

TouchableOpacity 触摸反馈​

点击具有不透明效果的组件

将内容弹出,显示在屏幕中间

在元素做所在位置弹出一个菜单

loading 显示加载动画​

显示加载中动画

message 消息通知​

显示一个消息通知

confirm 确认弹框​

异步调用一个确认弹框

Sign 签名​

手写签名组件

HorseLanternLottery 跑马灯抽奖​

抽奖组件,可以随机结果,也可以异步指定抽奖结果

总结​

可以看到,组件库很丰富,覆盖的使用场景很多

详细的使用方法,请前往开发文档查看

开发文档:http://duxapp.com

GitHub:https://github.com/duxapp

duxapp对RN端开发进行了哪些改动,如何提升开发效率

· 阅读需 12 分钟
duxapp
duxapp框架作者

Taro的React Native端开发提供了两种开发方式,一种是将壳和代码分离,一种是将壳和代码合并在一起开发

信息

duxapp中更进一步,你不需要太关注壳子什么的,你只需要安装好安卓和ios的编译环境,用一个命令就能编译apk或者ios,并且这个编译的过程和duxapp的模块化理念高度绑定,通过指定 --app= 指定不同的入口,就能打包出不同的项目,就像下面这样

# 编译 duxuiExample 的安卓调试版本
yarn android --app=duxuiExample

# 编译 duxuiExample 的IOS调试版本
yarn ios --app=duxuiExample

# 编译成功后启动Metro代码编译服务
yarn start --app=duxuiExample

下面我来详细介绍一下,在duxapp中是如何对RN进行优化的

配置化​

对于Taro的壳子,或者原生React Native,都会存在 android ios这两个文件夹,而在duxapp中,这些文件夹的内容是自动生成的,那么对于需要在这些文件夹中修改的配置内容,例如包名、版本号、新架构开关等,都通过配置文件的方式配置了,而不需要需修改具体的文件

这个配置文件是项目配置文件夹下的 configs/duxuiExample/duxapp.rn.js,其中 duxuiExample 就是我通过--app=duxuiExample 指定的入口模块

这个配置文件的内容就像下面这样,可以清晰的看到,对安卓配置了包名、名称、版本号等信息,IOS同样如此

export default {
android: {
appid: 'cn.duxapp.duxui',
appName: 'duxUI库',
versionCode: 2,
versionName: '1.1.0',
keystore: {
storeFile: 'duxui.keystore',
keyAlias: 'duxui',
storePassword: 'TN62eyasJAKm2ksD',
keyPassword: 'TN62eyasJAKm2ksD'
}
},
ios: {
BundleId: 'cn.duxapp.duxui',
appName: 'duxUI库',
versionCode: 1,
versionName: '1.0.0',
team: '',
plist: {
'duxapp/Info.plist': {
NSCameraUsageDescription: 'duxUI库需要拍照用于APP内图片上传更换头像',
NSContactsUsageDescription: 'duxapp需要访问你的通讯录,将客户信息保存到通讯录中',
NSLocalNetworkUsageDescription: 'App需要访问你的本地网络,用于和服务器建立连接',
NSLocationAlwaysAndWhenInUseUsageDescription: '使用你的位置信息用于地图定位和位置选择',
NSLocationAlwaysUsageDescription: '使用你的位置信息用于地图定位和位置选择',
NSLocationWhenInUseUsageDescription: '使用你的位置信息用于地图定位和位置选择',
NSPhotoLibraryAddUsageDescription: 'duxUI库需要保存宣传图到你的相册用于分享',
NSPhotoLibraryUsageDescription: 'duxUI库需要访问相册用于APP内图片上传更换头像',
}
}
}
}

内容复制​

上面这个配置文件已经解决了大部分打包需要用到的配置,但是你开发过RN的话你会看出来,证书他是一个文件,这里只指定了证书文件名称,但是并未指定证书具体内容,还有打包一个app,它总是需要一个app图标的,包括安卓和ios的图标,那么这些内容,可以通过配置文件中的copy文件夹,将这些项目文件复制到安卓或者ios对应的文件位置

这个文件夹内容看起来是这样的

copy结构

那么你又会发现,好像这些文件的结构,以及如何生成这些文件,又是一个头疼的问题,duxapp-cli,帮你解决了这个麻烦的问题,只需要两个简单的命令,就可以自动创建这个些文件

首先是安卓证书文件,需要注意的是,这里是指定--config=,而不是指定 --app=

yarn duxapp android keystore --config=duxuiExample

创建成功后,需要手动将命令行打印的配置内容,放进duxapp.rn.js相应位置

然后是logo创建,需要将你项目的logo文件放在配置文件根目录,也就是 configs/duxuiExample/logo.png

yarn duxapp rn logo --config=duxuiExample

命令使用成功后,他会自动把logo放进对应位置,你就不需要进一步操作了

这样是不是就简单起来了,下面来看看,要如何使用第三方插件,例如微信插件、高德地图插件等

信息

以上所有提到的 duxuiExample 都是以 UI库示例 这个模块项目来举例的,在你的项目中根据实际情况替换

三方模块​

你的项目或多或少都要用一些第三方的插件,React Native基础模块中已经包含了很多基础常用插件,你可以通过三方模块查看到,包含的基础插件

传统的方法是将他们添加到 package.json 依赖中,然后根据文档内容修改安卓或者ios文件夹对应的内容,在duxapp中提供了另外一种方式来实现第三方插件的使用

像这个react-native-view-shot安装方式很简单的插件,他只要求你将他添加到 package.json 的依赖中就可以使用了

那么我们结合模块,在你需要用到这个功能的模块配置文件中,一样的添加上这个依赖即可,像下面这个duxui模块的package.json文件一样

{
"dependencies": {
"b-validate": "^1.5.3",
"react-native-view-shot": "~3.8.0",
"react-native-fast-shadow": "~0.1.1",
"array-tree-filter": "^2.1.0"
}
}

其实开源的大多数第三方插件都是这样的,只需要添加到依赖中,重新打包就能用了,但是很少数的插件,他就是要改一些安卓或者ios里面的原生内容,像微信插件,它需要的改动还挺多的,我根据他文档需求,列举了下面这些

安卓:

  • 添加 proguard
  • 新建 WXEntryActivity.java 用于回调处理
  • 新建 WXPayEntryActivity.java 用于支付回调处理
  • 添加 <package android:name="com.tencent.mm" /> 用于跳转到微信的白名单
  • 添加 .wxapi.WXEntryActivity
  • 添加 .wxapi.WXPayEntryActivity

ios:

  • 由于插件bug,需要添加 pod 依赖项 pod 'WechatOpenSDK'
  • 修改 AppDelegate.h 入口文件
  • 修改 AppDelegate.mm 文件进行一些处理
  • 在 Info.plist 添加 Schemes 和 BundleURLTypes 和 applinks
  • 在项目配置中,添加 UniversalLink

其他:

  • 通过patch修复当前版本的一个bug

首先还是要在模块中添加依赖 package.json 文件

{
"dependencies": {
"react-native-wechat-lib": "^3.0.4",
"wechat-jssdk": "^5.1.0"
}
}

那么在duxapp前面提到,安卓和ios文件夹的内容都是自动生成的,我又是如何处理这些修改的呢?这里就需要用到 duxapp-cli 提供的模块更新脚本来处理

针对微信插件的处理脚本文件位于 src/wechat/update/index.js,这个文件的内容是下面这样的

// eslint-disable-next-line import/no-commonjs
export default ({ config }) => {
const { android, option } = config
return {
// 描点插入
insert: {
'android/app/proguard-rules.pro': {
'content': `
##### 微信 ######
-keep class com.tencent.mm.opensdk.** { *; }
-keep class com.tencent.wxop.** { *; }
-keep class com.tencent.mm.sdk.** { *; }`
},
'ios/Podfile': {
'podEnd': ` pod 'WechatOpenSDK'`
},
'ios/duxapp/AppDelegate.h': {
import: ' #import "WXApi.h"',
'appDelegate.protocol': ' ,WXApiDelegate'
},
'ios/duxapp/AppDelegate.mm': {
import: '#import <React/RCTLinkingManager.h>',
appDelegate: `// react-native-wechat-lib start

- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
return [WXApi handleOpenURL:url delegate:self];
}

- (BOOL)application:(UIApplication *)application
continueUserActivity:(NSUserActivity *)userActivity
restorationHandler:(void(^)(NSArray<id<UIUserActivityRestoring>> * __nullable
restorableObjects))restorationHandler {
// 触发回调方法
[RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
return [WXApi handleOpenUniversalLink:userActivity
delegate:self];
}

// Universal Links 配置文件, 没使用的话可以忽略。
// ios 9.0+
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
options:(NSDictionary<NSString*, id> *)options
{
// Triggers a callback event.
// 触发回调事件
[RCTLinkingManager application:application openURL:url options:options];
return [WXApi handleOpenURL:url delegate:self];
}
// react-native-wechat-lib end`
}
},
create: {
'android/app/src/main/java/cn/duxapp/wxapi/WXEntryActivity.java': `package ${android.appid}.wxapi;

import android.app.Activity;
import android.os.Bundle;
import com.wechatlib.WeChatLibModule;

public class WXEntryActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WeChatLibModule.handleIntent(getIntent());
finish();
}
}
`,
'android/app/src/main/java/cn/duxapp/wxapi/WXPayEntryActivity.java': `package ${android.appid}.wxapi;

import android.app.Activity;
import android.os.Bundle;
import com.wechatlib.WeChatLibModule;

public class WXPayEntryActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WeChatLibModule.handleIntent(getIntent());
finish();
}
}
`
},
android: {
xml: {
'app/src/main/AndroidManifest.xml': {
tag: {
queries: {
child: '<package android:name="com.tencent.mm" />'
}
},
attr: {
'android:name=".MainApplication"': {
child: `<activity
android:name=".wxapi.WXEntryActivity"
android:label="@string/app_name"
android:exported="true"
android:taskAffinity="${android.appid}"
android:launchMode="singleTask"
/>
<activity
android:name=".wxapi.WXPayEntryActivity"
android:label="@string/app_name"
android:exported="true"
/>`
}
}
}
}
},
ios: {
plist: {
'duxapp/Info.plist': {
CFBundleURLTypes: [
{
CFBundleTypeRole: 'Editor',
CFBundleURLName: 'weixin',
CFBundleURLSchemes: [
option?.wechat?.appid || 'wx'
]
}
],
LSApplicationQueriesSchemes: ['weixin', 'wechat', 'weixinULAPI']
},
'duxapp/duxapp.entitlements': {
'com.apple.developer.associated-domains': [
`applinks:${option?.wechat?.applinks || 'duxapp.com'}`
]
}
}
}
}
}

这个文件导出了一个函数,这个函数参数中的 config 就是当前项目的RN编译配置文件,这个文件中可以获取到了包名、版本号等信息

函数返回了一个对象,这个对象中的每一个key就代表不同的功能,下面一一介绍一下这些key

  • insert 用于将内容插入到指定文件的指定位置
  • create 用于将文件创建于指定位置
  • android 其中的xml用来处理合并安卓中的xml文件的,这是用 xmldom来实现的
  • ios 其中的plist是用来合并ios的plist配置文件的

关于这个脚本文件的详细内容需阅读 使用原生模块 了解详情

看了半天,是不是感觉这个模块处理也是挺复杂的,其实我已经封装了一些常用的原生模块,就像这个微信插件,你不需要再去实现一遍,你只需要安装这个微信模块并把他添加到你项目模块的依赖中就能使用了

yarn duxapp app add wechat

然后就像 duxuiExample 这个模块的配置文件一样,将 wechat 添加到依赖中,然后重新编译

{
"name": "duxuiExample",
"description": "ui库示例",
"version": "1.0.13",
"dependencies": [
"duxui",
"duxcms",
"amap",
"echarts",
"wechat"
]
}

还有更多的模块,请前往应用商店查看 https://cloud.dux.plus/apps

总结​

通过上面的说明,你已经基本了解了duxapp是如何处理RN端开发的,还有很多的详细的内容,还需要前往文档查看http://duxapp.com/docs/course/rn/start

再结合duxapp提供的ui库、工具库、全局样式等方法,就能很快的完成你的APP项目了

GitHub:https://github.com/duxapp

Taro使用模块化开发,以提升开发效率

· 阅读需 17 分钟
duxapp
duxapp框架作者

duxapp是基于Taro二次开发的模块化框架

使用这个框架,结合框架提供的UI库和工具库,能帮助你快速且高质量的完成项目,且能实现同时开发小程序、H5、APP(React Native),并且保证各个端的一致性

duxapp还针对APP开发(React Native)做了大量优化,大大降低了APP发开的难度,你可以阅读React Native教程,了解详情

下面让我来详细介绍如何使用duxapp

何为模块化​

什么是模块化?就像npm包一样,我们可以将一些通用的功能或页面编写在一个模块内,提供给多个项目来使用,以提高代码的复用性。

模块的概念在很多后端框架中很常见,它们可以在应用商店通过安装应用的方式来获得新功能,在前端框架中确很少见到类似的设计方案,当然你其实也可以理解为发布到npm就是一种模块化的设计,但是在Taro中很多功能他并不能发布到npm中,例如页面。页面需要放在项目中,当发布到npm之后就会无法使用

在duxapp框架中的模块化设计原理,和npm的依赖关系是类似的,每个模块有一个配置文件app.json,里面的依赖字段dependencies,用来填写我要用到的依赖,就像下面ui库示例这个模块的配置

{
"name": "duxuiExample",
"description": "ui库示例",
"version": "1.0.13",
"dependencies": [
"duxui",
"duxcms",
"amap",
"echarts",
"wechat"
]
}

和npm依赖不一样的是,这里的依赖不包含版本信息。因为页面等限制条件,你一个项目中,同一个模块无法存在两个不同的版本,因此并未设计指定版本号的功能

依赖关系是逐层查找的,就像npm一样,例如这里依赖的duxui模块,他的模块配置文件是这样的

{
"name": "duxui",
"description": "DUXUI库",
"version": "1.0.42",
"dependencies": [
"duxapp"
]
}

在duxui模块中他又使用了duxapp这个依赖,通过每个模块都去查找,我们最终整理出这样的依赖关系图

dependencies

那么最终当我们使用下面的命令编译duxuiExample模块的时候

# 调试小程序
yarn dev:weapp --app=duxuiExample
# 调试h5
yarn dev:h5 --app=duxuiExample

实际被编译的模块就包含下面这些

  • duxuiExample
  • duxcms
  • amap
  • echarts
  • wechat
  • duxui
  • duxappReactNative
  • duxapp

使用duxapp​

上面介绍了模块化的原理,现在我们来看看,具体要怎么使用这个框架

首先使用cli命令创建一个项目,中途会要求你选择模板,你可以选择 duxui 示例代码(包含所有组件的示例代码 支持RN端) 这个选项,和上面使用的示例一样

npx duxapp-cli create projectName

在使用这个命令之前,确保安装了以下工具和环境

安装后会自动安装项目依赖

进入项目目录projectName,使用上面提到的命令 yarn dev:weapp --app=duxuiExample 或者 yarn dev:h5 --app=duxuiExample 编译为小程序或者H5,使用开发者工具或者浏览器就能预览

可以看到编译命令是在Taro原有的命令基础上增加了 --app= 参数,参数用来指定一个模块,通常你都需要指定这个参数,因为你的项目中除了上面提到的模块之外,大多是时候还会存在其他模块,如果你不指定的话,他会把所有模块都打包进去

通过上面的描述可以看出,其实在一个项目中不是真的只有一个项目,在我的实际开发经验中,我是将很多项目放在一起开发的,我只需要通过 --app= 参数指定我的项目入口文件进行编译,他就是不同的项目

多个项目同时存在,如何保持他们不混乱呢,例如第三方npm依赖,每个项目可能都有不同的npm依赖,这通过下面的章节来介绍

模块​

在duxapp框架中,src目录下每个文件夹将被识别为一个模块,模块一般是像下面这样设计结构的

├── duxapp                      模块名称
│ ├── components 模块组件库
│ │ ├── ComponentName 组件
│ │ │ └── index.jsx
│ │ └── index.js 导出需要导出的组件
│ ├── config 配置目录
│ │ ├── route.js 路由配置文件(路径固定)
│ │ ├── theme.js 主题配置文件(路径固定)
│ │ └── themeToScss.js 主题转换函数(路径固定)
│ ├── pages 页面放置文件夹
│ │ └── index 页面文件夹
│ │ ├── index.jsx 页面
│ │ └── index.scss
│ ├── utils 工具库
│ │ ├── index.js 导出工具库
│ │ └── ...you util.js
│ ├── update 模块安装目录
│ │ ├── copy 需要复制到项目的文件(路径固定)
│ │ │ └── ...
│ │ └── index.js 安装脚本 主要针对RN端 插件安装方法(路径固定)
│ ├── app.js 模块入口文件
│ ├── app.json 模块配置文件 包括名称 依赖等(必须)
│ ├── app.scss 全局样式文件(次样式文件无需导入到js文件中,会自动注入全局)
│ ├── package.json 和项目的package.json相同,用于指定当前模块需要用到的三方依赖等
│ ├── changelog.md 更新日志(必须 如果发布)
│ ├── index.js 模块出口文件 可以导出组件和方法给其他模块使用
│ ├── index.html 如果是h5的项目可以自定义index.html,仅当作为入口模块时可用
│ ├── app.config.js 用于覆盖项目全局配置
│ ├── babel.config.js babel配置文件
│ ├── metro.config.js metro配置文件
│ ├── taro.config.js Taro编译配置文件
│ ├── taro.config.prod.js Taro 发布配置文件
│ ├── taro.config.dev.js Taro 调试配置文件
│ └── readme.md 自述文件(必须 如果发布)

关于模块目录的详细内容查看这个 模块结构 获取

模块配置​

在duxui这个模块中,它的配置文件是这样的

{
"name": "duxui",
"description": "DUXUI库",
"version": "1.0.42",
"dependencies": [
"duxapp"
]
}

我们看到,他有一个字段 npm,(新版本中,这个配置已经移动到模块的package.json文件中)它的内容和项目的 package.json 的配置是完全一样的,在模块中编写这个内容,将会和项目的 package.json 进行覆盖合并,那么你就可以通过模块来安装当前模块需要依赖了,每个模块中都可以指定这个依赖,他们会合并在一起

当你指定了不同的 --app= 入口模块之后,框架会根据你使用到的模块中的第三方依赖自动重新安装

在模块中还有很多类似的设计,用来编写配置或者文件,包括下面这些

  • app.scss 编写全局样式
  • index.html 如果是h5的项目可以自定义index.html,仅当作为入口模块时可用
  • app.config.js 用于覆盖项目全局配置
  • babel.config.js babel配置文件
  • metro.config.js metro配置文件
  • taro.config.js Taro编译配置文件

模块路由​

每个模块中都可以编写页面,当然这不是必选项,这些页面会被定义在自己的模块中

通过 modeName/config/route.js 定义当前的模块路由,例如 duxuiExample 的路由定义如下

/**
* login:是否需要登录
* platform:支持的平台(weapp, h5, rn)不配置支持所有
* subPackage:是否将其设置为分包
* home: 是否是主页 是主页的页面将会被排在前面
*/
export default {
pages: {
'duxuiExample/index': {
pages: {
index: {
home: true
}
}
},
'duxuiExample/example': {
pages: {
Button: {},
Cell: {},
Grid: {},
Divider: {},
Space: {},
// 更多未展示
}
}
}
}

路由的定义也是经过封装的,配置的时候是将一个文件夹作为一个对象来处理,这样我们能很方便的将某个文件夹进行分包等操作

使用UI库和全局样式编写页面​

在基础模块 duxapp 中提供了可以用于快速布局页面的全局样式,他就和 tailwindcss 类似,在结合UI组件,编写页面像下面这样的,可以看到我们不需要编写 scss 文件就能完成页面的编写

import { Avatar, Card, ScrollView, Column, Divider, Header, Text, TopView, Row, px, Image, nav, Tag } from '@/duxui'
import { useRequest, CmsIcon, saleHook, Qrcode } from '@/duxcmsSale'
import { setClipboardData } from '@tarojs/taro'

export default function Sale() {

const [{ info = {}, day = {}, money, total = {} }] = useRequest('sale/index')

return <TopView>
<Header absolute title='推广中心' color='#FFFFFF' style={{ backgroundColor: 'transparent' }} />
<Image style={{ height: px(396) }} className='w-full absolute' src={require('./images/tui_bag.png')} />
<Row justify='between' items='center' style={{ marginTop: px(208) }} className='mt-3 ph-3'>
<Row items='center' justify='start'>
<Avatar url={info.avatar}>{info.nickname}</Avatar>
<Column className='mh-3'>
<Row items='center' className='gap-2'>
<Text size={33} bold color='#FFFFFF' >{info.nickname}</Text>
{!!info.level_name && <Tag type='primary' size='s'>{info.level_name}</Tag>}
</Row>
<Row className='mt-2'>
<Text color='#FFFFFF' size={1}>邀请码:{info.code}</Text>
<CmsIcon className='mh-2' size={36} name='copy' color='#FFFFFF' onClick={() => setClipboardData({ data: info.code })} />
</Row>
</Column>
</Row>
<CmsIcon size={60} name='QRcode1' color='#FFFFFF' onClick={Qrcode.show} />
</Row>
<ScrollView className='mt-3'>
<Card margin disableMarginTop>
<Row jtems='center' justify='between' className='gap-3'>
<Column justify='center' items='center' grow >
<Text bold type='primary'>{total.order_num || 0}</Text>
<Text color={2} size={2} className='mt-2'>直推订单</Text>
</Column>
<Column justify='center' items='center' grow>
<Text bold type='primary'>{total.user_num || 0}</Text>
<Text color={2} size={2} className='mt-2'>直推客户</Text>
</Column>
</Row>
<Row className='mt-2' jtems='center' justify='between'>
<Column justify='center' items='center' grow>
<Text bold type='primary'>{total.month_sale_money || 0}</Text>
<Text color={2} size={2} className='mt-2'>本月收益</Text>
</Column>
<Column justify='center' items='center' grow>
<Text bold type='primary'>{total.sale_money || 0}</Text>
<Text color={2} size={2} className='mt-2'>累计收益</Text>
</Column>
</Row>
</Card>
<Card shadow margin disableMarginTop onClick={() => nav('duxcmsAccount/cash/index')}>
<Row items='center' justify='between'>
<Text bold>佣金管理</Text>
<CmsIcon name='direction_right' size={32} />
</Row>
<Row className='mt-3' items='baseline'>
<Text size={2}>可提现佣金:</Text>
<Text className='mh-3' bold size={50}>{money || 0}</Text>
</Row>
</Card>
<Card margin disableMarginTop>
<Row items='center' justify='around'>
<Column items='center'>
<Text size={2}>今日预估收益</Text>
<Text className='mt-1' bold size={40} >{day.sale_money || 0}</Text>
</Column>
<Column items='center'>
<Text size={2}>今日有效订单</Text>
<Text className='mt-1' bold size={40} >{day.order_num || 0}</Text>
</Column>
<Column items='center'>
<Text size={2}>今日新增客户</Text>
<Text className='mt-1' bold size={40} >{day.user_num || 0}</Text>
</Column>
</Row>
</Card>

<Card margin className='gap-4'>
<Text size={4} bold>其他操作</Text>
<Row justify='between' items='center' onClick={() => nav('duxcmsSale/index/order')}>
<Text size={2} className='mh-2' bold>推广订单</Text>
<CmsIcon name='direction_right' size={32} />
</Row>
<Row justify='between' items='center' onClick={() => nav('duxcmsSale/index/customer')} >
<Text size={2} className='mh-2' bold>我的客户</Text>
<CmsIcon name='direction_right' size={32} />
</Row>
<saleHook.Render mark='index.menus' />
</Card>
<Row style={{ height: px(16) }}></Row>
</ScrollView >
</TopView>
}

为何获得更好的编辑体验,需要在vscode中安装 SCSS Everywhere 插件,他能识别到全局样式并给出编写提示

用户配置​

很多模块都是通用的,那么一些需要根据不同项目变化的内容,就不能写在模块中,而是要通过配置的形式来配置

项目配置放在项目根目录下的 configs 目录中,其中每个文件夹就是一个配置,文件中的index.js就是项目配置

像下面这个duxuiExample的配置,其中option中的每一项就是对应模块的配置

// import qiniu from './base/components/UploadFileManage/drive/qiniu'

const config = {
// 覆盖app.config.js 配置
appConfig: {
requiredPrivateInfos: [
'chooseLocation',
'getLocation',
'onLocationChange',
'startLocationUpdateBackground',
'chooseAddress'
]
},
// 调试配置
debug: {
// 在h5端开启vconsole调试功能
vconsole: false
},
// 模块配置 将会调用模块生命周期的option,将对应模块的参数传入
option: {
// 基础模块
duxapp: {
theme: {
primaryColor: '#E70012',
secondaryColor: '#E84C00',
successColor: '#34a853',
warningColor: '#fbbc05',
dangerColor: '#ea4335',
pageColor: '#F7F9FC',

textColor1: '#373D52',
textColor2: '#73778E',
textColor3: '#A1A6B6',
textColor4: '#FFF',
header: {
color: '#fff', // 仅支持rgb hex值,请勿使用纯单词 设置为数组将显示一个渐变按钮
textColor: '#000', // 文本颜色
showWechat: false, // 微信公众号是否显示header
showWap: true, // h5是否显示header
}
}
},
wechat: {
// 分享组件配置
share: {
open: true,
// 开启未定义的页面分享
pageSlef: {
// 包含这些页面分享自身 页面路径关键词匹配 include 优先级比 exclude 高,
// 可以配置exclude为空数组表示支持所有页面
// pageSlef优先级高于pageHome
// include: ['page/test'],
// 排除这些页面 不进行分享
exclude: []
},
// 开启未定义的页面分享到指定页面
pageHome: {
path: '',
params: {},
// 包含这些页面分享自身 页面路径关键词匹配
// include: [],
// 排除这些页面 不进行分享
// exclude: []
},
// 公共分享参数
common: {
title: 'DUXUI',
desc: '同时兼容小程序、H5、RN',
image: 'https://img.zhenxinhuixuan.com/weiwait/cropper/2lVCofRIu6Jl3jNebxCA6VkEMUeaobvLWFYMTiaG.jpg'
},
platform: {
app: {
// 配置分享到小程序的原始id 同时相当于开关
weappUserName: '',
// 配置分享到h5的url 同时相当于开关
h5Url: 'https://duxui.cn',
}
}
}
},
// 新php模块化系统
duxcms: {
request: {
origin: 'https://mock.dux.plus',
path: 'api', // 域名二级目录
secretId: '53368068',
secretKey: '6c278fbf1791fbed3ae79197de03f65f',
devOpen: false,
devToken: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJtZW1iZXIiLCJpYXQiOjE2ODc5Mzg3NDEsImV4cCI6MTY5MDUzMDc0MSwiaWQiOjZ9.kCb82Y3bgUJWUo_WYsUPO1cLYzF1OJdEWTKAj9iNlF0'
},
// 登录相关配置
loginConfig: {
// 手机号登录
phone: true,
// 邮箱登录
email: false,
// app微信登录
appWatch: true,
// 小程序微信登录
weappWatch: true,
// 名称
appName: 'duxui'
}
}
}
}

export default config

后端框架​

在我开发的项目中,后端框架是我的合伙人负责的,后端同样也是采用模块化的开发模式完成的,duxapp框架对后端框架已经打好了对接的基础,如果你考虑进一步提升后端的开发效率,可以考虑使用

后端开发文档:https://www.dux.cn/

总结​

这一篇文章已经很长了,duxapp框架中还有很多内容,无法在这里一一介绍,像下面这些

  • 模块主题
  • 模块安装与发布
  • 快速开发APP(React Native)
  • 请求上传
  • 路由系统
  • 开放模块(用户管理、微信、支付宝、页面设计器等)
  • 等等

请前往开发文档查看详细教程

开发文档:http://duxapp.com/

GitHub:https://github.com/duxapp

2024-09 升级到Taro4 RN0.75

· 阅读需 4 分钟
duxapp
duxapp框架作者

在此次更新之前的版本中,Taro使用的是3.6.7,RN使用的是0.71,在这次升级中将Taro和RN都更新到了最新版本,让我们来看看这次更新中都有哪些变化

完善的开发文档​

这次更新后,对duxapp进行了阶段性的总结,于是将开发文档进行了全面的完善,包括基础教程、基础模块教程、UI库教程等等模块的教程

点击导航栏去查看它们

模块更新​

  • 将duxui库的types进行了全面的完善
  • 将公共模块进行了全面更新以兼容新版本的RN和Taro
  • 将这些模块重新发布到了应用市场

Taro升级4.0​

Taro4.0主要是新增了对鸿蒙的支持、支持Vite编译等

这个版本对小程序和H5端的影响不是很大,但是对RN升级有影响,只有4.0的版本才能将RN升级到0.75

RN升级0.75​

从0.71到0.75的升级RN为我们带来了诸多好处

  • 开启新架构后依然能兼容不支持新架构的模块 所以在升级到0.75后,新架构是默认开启的,这可以为你的APP提供更好的性能
  • 更完善的样式布局,例如新增了对transform-origin、translate百分比、gap百分比、position: 'static'等样式的支持
  • 更多更新你可以查看 RN 博客

三方依赖更新​

  • 将RN的依赖都更新到了最新的版本
  • 移除了react-native-fs依赖,因为这个依赖已经不更新了,使用expo-file-system
  • 移除了rn-fetch-blob依赖,也是不更新,将其替换为react-native-blob-util
  • 新增了expo-image用于RN端图片的实现

导入方式变更​

不支持这种方式导入

import Taro from '@tarojs/taro'

需要修改为这种方式

import { showModal } from '@tarojs/taro'

CLI更新​

对cli更新,提供了更完善的支持

  • 新增模块 app.config.js 全局配置的支持
  • 对入口文件app.js修改为可选
  • 对路由配置文件config/route.js修改为可选
  • 优化创建项目命令
  • 安卓更新脚本支持对xml文件处理
  • 更新脚本支持创建文件
  • 自动更新项目公共文件,让你的项目保持最新

如何更新到最新版​

更新其实也比较简单,将你依赖的公共模块都升级到最新版本

例如你是用了duxui yarn duxapp add duxui,就更新它,然后重新编译即可

当然更新有风险,你可能还需要对现有代码进行一些兼容性修改才能正常运行,需要谨慎操作