WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
45 stars 10 forks source link

Taro #59

Open WangShuXian6 opened 5 years ago

WangShuXian6 commented 5 years ago

Taro

一套基于 NodeJS 遵循 React 语法规范的多端统一开发框架

https://github.com/NervJS/taro https://taro.js.org/ https://nervjs.github.io/taro/ https://github.com/NervJS/awesome-taro

https://github.com/NervJS/awesome-taro. 多端统一开发框架 Taro 优秀学习资源汇总


安装

/** Quick Start With NPM Or Yarn **/
$ sudo npm install -g @tarojs/cli
$ sudo yarn global add @tarojs/cli

更新 Taro

更新 taro-cli 工具:

# taro
$ taro update self
# npm 
npm i -g @tarojs/cli@latest 
# yarn 
yarn global add @tarojs/cli@latest

更新项目中 Taro 相关的依赖,这个需要在你的项目下执行。

$ taro update project

使用命令创建模板项目

taro init myApp

NPM 5.2+ 也可在不全局安装的情况下使用 npx 创建模板项目:

npx @tarojs/cli init myApp

目前 Taro 已经支持微信/百度/支付宝小程序、H5 以及 ReactNative 等端的代码转换,针对不同端的启动以及预览、打包方式并不一致。


微信小程序

微信开发者工具的项目设置:

设置关闭 ES6 转 ES5 功能 设置关闭上传代码时样式自动补全 设置关闭代码压缩上传

需要自行下载并打开微信开发者工具,然后选择项目根目录进行预览 微信小程序编译预览及打包:

# npm script
$ npm run dev:weapp
$ npm run build:weapp
# 仅限全局安装
$ taro build --type weapp --watch
$ taro build --type weapp
# npx 用户也可以使用
$ npx taro build --type weapp --watch
$ npx taro build --type weapp

百度小程序

选择百度小程序模式,需要自行下载并打开百度开发者工具,然后在项目编译完后选择项目根目录下 dist 目录进行预览。 百度小程序编译预览及打包:

# npm script
$ npm run dev:swan
$ npm run build:swan
# 仅限全局安装
$ taro build --type swan --watch
$ taro build --type swan
# npx 用户也可以使用
$ npx taro build --type swan --watch
$ npx taro build --type swan

支付宝小程序

选择支付宝小程序模式,需要自行下载并打开支付宝小程序开发者工具,然后在项目编译完后选择项目根目录下 dist 目录进行预览。

支付宝小程序编译预览及打包:

# npm script
$ npm run dev:alipay
$ npm run build:alipay
# 仅限全局安装
$ taro build --type alipay --watch
$ taro build --type alipay
# npx 用户也可以使用
$ npx taro build --type alipay --watch
$ npx taro build --type alipay

H5

H5 模式,无需特定的开发者工具,在执行完下述命令之后即可通过浏览器进行预览。

H5 编译预览及打包:

# npm script
$ npm run dev:h5
# 仅限全局安装
$ taro build --type h5 --watch
# npx 用户也可以使用
$ npx taro build --type h5 --watch

React Native

React Native 端运行需执行如下命令,React Native 端相关的运行说明请参见 React Native 教程。

# npm script
$ npm run dev:rn
# 仅限全局安装
$ taro build --type rn --watch
# npx 用户也可以使用
$ npx taro build --type rn --watch

Taro-UI

npm install taro-ui 或者使用自定义主题版本 npm install taro-ui@next

安装最新预览版 npm install taro-ui@next

Noticebar 默认换行 禁止换行需要增加 css

white-space:nowrap;

环境判断

process.env.TARO_ENV

WangShuXian6 commented 5 years ago

虽然 this.props 由 Taro 本身设置以及 this.state 具有特殊的含义,但如果需要存储不用于视觉输出的东西,则可以手动向类中添加其他字段。

如果你不在 render() 中使用某些东西,它就不应该在状态中。


Taro.getEnv() 与 process.env.TARO_ENV https://github.com/NervJS/taro/issues/1080


Taro.getEnv() 返回 'WEAPP' | 'WEB' | 'RN' | 'SWAN' | 'ALIPAY'

process.env.TARO_ENV 返回 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn'



>Taro.getEnv() 这个 API 设计的本意应该是用来运行时环境判断, 而 process.env.TARO_ENV 是在编译时替换成字符串
***

>横向滚动
![scrollx](https://user-images.githubusercontent.com/30850497/49996332-b382c000-ffc9-11e8-9869-adc683b4f6d3.jpg)
WangShuXian6 commented 5 years ago

React 核心语法

JSX 是 React 的核心组成部分,React 认为组件化才是正确的代码分离方式,它要比模板与组件逻辑分开的方式更好,所以就有了 JSX 语法。它把 HTML 模板直接嵌入到 JS 代码里面,这样就做到了模板和组件关联。JSX 允许在 JS 中直接使用 XML 标记的方式来声明界面

render 函数返回了一些用括号包住的 XML 结构的界面描述,这其实就是该组件的界面描述。里面的写法和 HTML 并没有多大的差别。不同的地方主要是可以在里面进行事件绑定,表达变量,实现简单 JS 逻辑等,即在 JS 里写 HTML。而变量、简单 JS 逻辑都是需要用 {} 包裹起来。另外 HTML 的 class 属性因为是 Javascript 的保留字,所以需要写成 className。

在 JSX 里使用 JS 是有限制的,只能使用一些表达式,不能定义变量,使用 if/else 等,你可以用提前定义变量;用三元表达式来达到同样的效果。

列表渲染,一般是用数组的 map 函数。正如上面的例子,把需要列表渲染的数据使用 map 函数,返回所需要的 JSX 代码。而在事件绑定上,使用 on + 事件名称


props

父组件传给子组件的数据,会挂载在子组件的 this.props


state

state 与 props 不同,是属于组件自己内部的数据状态,一般在 constructor 构造函数里初始化定义 state

class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {name: 'aotu,taro!'};
}
render() {
return <h1>Hello, {this.state.name}</h1>;
}
}

当 state 需要变化时,是不允许随便更改的,需要调用 this.setState 来进行更改,否则视图没法进行更新 只把跟组件内部视图有关联的数据,变量放在 state 里面,以此避免不必要的渲染。


当 state 需要变化时,是不允许随便更改的,需要调用 this.setState 来进行更改,否则视图没法进行更新
``

组件的生命周期

组件的生命周期,指的是一个 React 组件从挂载,更新,销毁过程中会执行的生命钩子函数

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUpdate(nextProps, nextState) {}

  componentWillReceiveProps(nextProps) {}  

  componentDidUpdate(prevProps, prevState) {}

  shouldComponentUpdate(nextProps, nextState) {}

  componentWillUnmount() {}

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

constructor,顾名思义,组件的构造函数。一般会在这里进行 state 的初始化,事件的绑定等等

componentWillMount,是当组件在进行挂载操作前,执行的函数,一般紧跟着 constructor 函数后执行

componentDidMount,是当组件挂载在 dom 节点后执行。一般会在这里执行一些异步数据的拉取等动作

shouldComponentUpdate,返回 false 时,组件将不会进行更新,可用于渲染优化

componentWillReceiveProps,当组件收到新的 props 时会执行的函数,传入的参数就是 nextProps ,你可以在这里根据新的 props 来执行一些相关的操作,例如某些功能初始化等

componentWillUpdate,当组件在进行更新之前,会执行的函数

componentDidUpdate,当组件完成更新时,会执行的函数,传入两个参数是 prevProps 、prevState

componentWillUnmount,当组件准备销毁时执行。在这里一般可以执行一些回收的工作,例如 clearInterval(this.timer) 这种对定时器的回收操作


小程序的生命周期

小程序的生命周期分为页面的生命周期和整个应用的生命周期。

应用的生命周期主要有onLaunch、onShow、onHide

onLaunch 是当小程序初始化完成时,会触发 onLaunch(全局只触发一次); onShow 是当小程序启动,或从后台进入前台显示,会触发 onShow; onHide 是当小程序从前台进入后台,会触发 onHide;

页面的生命周期会比较多一些,有onLoad、onReady、onShow、onHide、onUnload

onLoad 是监听页面加载的函数 onReady 是监听页面初次渲染完成的函数 onShow 是监听页面显示的函数 onHide 是监听页面隐藏的函数 onUnload 是监听页面卸载的函数

WangShuXian6 commented 5 years ago

在 Taro 中使用 Redux

为了更方便地使用 Redux,Taro 提供了与 react-redux API 几乎一致的包 @tarojs/redux 来让开发人员获得更加良好的开发体验。

开发前需要安装 redux 和 @tarojs/redux ,开发者可自行选择安装 Redux 中间件,本文以如下中间件为例:

$ yarn add redux @tarojs/redux redux-logger
# 或者使用 npm
$ npm install --save redux @tarojs/redux redux-logger

Todolist 快速上手 Redux

通过目录划分我们的store/reducers/actions

分别在三个文件夹里创建index.js,作为三个模块的入口文件


// store/index.js

import { createStore, applyMiddleware } from 'redux'

// 引入需要的中间件
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'

// 引入根reducers
import rootReducer from '../reducers'

const middlewares = [
  thunkMiddleware,
  createLogger()
]

// 创建 store
export default function configStore () {
  const store = createStore(rootReducer, applyMiddleware(...middlewares))
  return store
}

首先,定义好store,然后在app.js中引入。使用@tarojs/redux中提供的Provider组件将前面写好的store接入应用中,这样一来,被Provider包裹的页面都能访问到应用的store。

Provider 组件使组件层级中的 connect() 方法都能够获得 Redux store。


import Taro, { Component } from '@tarojs/taro'
import { Provider } from '@tarojs/redux'

import configStore from './store' import Index from './pages/index'

import './app.scss'

const store = configStore()

class App extends Component { ... render () { return (

)

} }


***
>新建constants文件夹来定义一系列所需的action type常量。例如 Todos 我们可以先增加ADD和DELETE两个action type来区分新增和删除 Todo 指令

```ts
// src/constants/todos.js

export const ADD = 'ADD'
export const DELETE = 'DELETE'

创建处理这两个指令的reducer


// src/reducers/index.js

import { combineReducers } from 'redux' import { ADD, DELETE } from '../constants/todos'

// 定义初始状态 const INITIAL_STATE = { todos: [ {id: 0, text: '第一条todo'} ] }

function todos (state = INITIAL_STATE, action) { // 获取当前todos条数,用以id自增 const todoNum = state.todos.length

switch (action.type) {
// 根据指令处理todos case ADD:
return { ...state, todos: state.todos.concat({ id: todoNum, text: action.data }) } case DELETE: let newTodos = state.todos.filter(item => { return item.id !== action.id })

  return {
    ...state,
    todos: newTodos
  }
default:
  return state

} }

export default combineReducers({ todos })

***
>在action中定义函数对应的指令
```ts
// src/actions/index.js

import { ADD, DELETE } from '../constants/todos'

export const add = (data) => {
  return {
    data,
    type: ADD
  }
}

export const del = (id) => {
  return {
    id,
    type: DELETE
  }
}

在 Todos 应用的主页使用相应action修改并取得新的store数据了。来看一眼 Todos 的index.js


// src/pages/index/index.js

import Taro, { Component } from '@tarojs/taro' import { View, Input, Text } from '@tarojs/components' import { connect } from '@tarojs/redux' import './index.scss'

import { add, del } from '../../actions/index'

class Index extends Component { config = { navigationBarTitleText: '首页' }

constructor () { super ()

this.state = {
  newTodo: ''
}

}

saveNewTodo (e) { let { newTodo } = this.state if (!e.detail.value || e.detail.value === newTodo) return

this.setState({
  newTodo: e.detail.value
})

}

addTodo () { let { newTodo } = this.state let { add } = this.props

if (!newTodo) return

add(newTodo)
this.setState({
  newTodo: ''
})

}

delTodo (id) { let { del } = this.props del(id) }

render () { // 获取未经处理的todos并展示 let { newTodo } = this.state let { todos, add, del } = this.props

const todosJsx = todos.map(todo => {
  return (
    <View className='todos_item'><Text>{todo.text}</Text><View className='del' onClick={this.delTodo.bind(this, todo.id)}>-</View></View>
  )
})

return (
  <View className='index todos'>
    <View className='add_wrap'>
      <Input placeholder="填写新的todo" onBlur={this.saveNewTodo.bind(this)} value={newTodo} />
      <View className='add' onClick={this.addTodo.bind(this)}>+</View>
    </View>
    <View>{ todosJsx }</View>  
  </View>
)

} }

export default connect (({ todos }) => ({ todos: todos.todos }), (dispatch) => ({ add (data) { dispatch(add(data)) }, del (id) { dispatch(del(id)) } }))(Index)



***
>在搭建类似商城这样的大型应用,我们非常建议你采用 Redux 管理数据状态,而譬如开发单页应用这类小型的站点,使用 Redux 则有可能会增加你的工作量。
WangShuXian6 commented 5 years ago

Taro 设计思想及架构

小程序的数据驱动模板更新的思想与实现机制,与 React 类似; React 采用 JSX 作为自身模板,JSX 相比字符串模板来说更自由,更自然,更具表现力,不需要依赖字符串模板的各种语法糖,也能完成复杂的处理

在 Taro 中采用的是编译原理的思想,所谓编译原理,就是一个对输入的源代码进行语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。

ast


小程序和 Web 端上组件标准与 API 标准有很大差异,这些差异仅仅通过代码编译手段是无法抹平的,例如你不能直接在编译时将小程序的 直接编译成

,因为他们虽然看上去有些类似,但是他们的组件属性有很大不同的,仅仅依靠代码编译,无法做到一致,同理,众多 API 也面临一样的情况。针对这样的情况,Taro 采用了定制一套运行时标准来抹平不同平台之间的差异。

这一套标准主要以三个部分组成,包括标准运行时框架、标准基础组件库、标准端能力 API,其中运行时框架和 API 对应 @taro/taro,组件库对应 @tarojs/components,通过在不同端实现这些标准,从而达到去差异化的目的。

在所有端中,我们挑选了微信小程序的组件库和 API 来作为 Taro 的运行时标准,因为微信小程序的文档非常完善,而且组件与 API 也是非常丰富,同时最重要的是,百度小程序以及支付宝小程序都是遵循的微信小程序的标准,这样一来,Taro 在实现这两个平台的转换上成本就大大降低了。

WangShuXian6 commented 5 years ago

CLI 原理及不同端的运行机制

taro-cli 负责 Taro 脚手架初始化和项目构建的的命令行工具 https://www.npmjs.com/package/@tarojs/cli

Taro 工程

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build
├── docs
├── lerna-debug.log
├── lerna.json        // Lerna 配置文件
├── package.json
├── packages
│   ├── eslint-config-taro
│   ├── eslint-plugin-taro
│   ├── postcss-plugin-constparse
│   ├── postcss-pxtransform
│   ├── taro
│   ├── taro-async-await
│   ├── taro-cli
│   ├── taro-components
│   ├── taro-components-rn
│   ├── taro-h5
│   ├── taro-plugin-babel
│   ├── taro-plugin-csso
│   ├── taro-plugin-sass
│   ├── taro-plugin-uglifyjs
│   ├── taro-redux
│   ├── taro-redux-h5
│   ├── taro-rn
│   ├── taro-rn-runner
│   ├── taro-router
│   ├── taro-transformer-wx
│   ├── taro-weapp
│   └── taro-webpack-runner
└── yarn.lock

Taro 项目主要是由一系列 NPM 包组成,位于工程的 Packages 目录下。它的包管理方式和 Babel 项目一样,将整个项目作为一个 monorepo 来进行管理,并且同样使用了包管理工具 Lerna。

Lerna 是一个用来优化托管在 Git/NPM 上的多 package 代码库的工作流的一个管理工具,可以让你在主项目下管理多个子项目,从而解决了多个包互相依赖,且发布时需要手动维护多个包的问题。

https://lernajs.io/

Packages 目录下十几个包中,最常用的项目初始化与构建的命令行工具 Taro CLI 就是其中一个。在 Taro 工程根目录运行 lerna publish 命令之后,lerna.json 里面配置好的所有的包会被发布到 NPM 上。


taro-cli 包的目录结构如下: 主要目录有:/bin、/src、/template

./ ├── bin // 命令行 │ ├── taro // taro 命令 │ ├── taro-build // taro build 命令 │ ├── taro-update // taro update 命令 │ └── taro-init // taro init 命令 ├── package.json ├── node_modules ├── src │ ├── build.js // taro build 命令调用,根据 type 类型调用不同的脚本 │ ├── config │ │ ├── babel.js // Babel 配置 │ │ ├── babylon.js // JavaScript 解析器 babylon 配置 │ │ ├── browser_list.js // autoprefixer browsers 配置 │ │ ├── index.js // 目录名及入口文件名相关配置 │ │ └── uglify.js │ ├── creator.js │ ├── h5.js // 构建h5 平台代码 │ ├── project.js // taro init 命令调用,初始化项目 │ ├── rn.js // 构建React Native 平台代码 │ ├── util // 一系列工具函数 │ │ ├── index.js │ │ ├── npm.js │ │ └── resolve_npm_files.js │ └── weapp.js // 构建小程序代码转换 ├── templates // 脚手架模版 │ └── default │ ├── appjs │ ├── config │ │ ├── dev │ │ ├── index │ │ └── prod │ ├── editorconfig │ ├── eslintrc │ ├── gitignore │ ├── index.js // 初始化文件及目录,copy模版等 │ ├── indexhtml │ ├── npmrc │ ├── pagejs │ ├── pkg │ └── scss └── yarn-error.log


>用到的核心库

>tj/commander.js Node.js - 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。

>jprichardson/node-fs-extra - 在 Node.js 的 fs 基础上增加了一些新的方法,更好用,还可以拷贝模板。

>chalk/chalk - 可以用于控制终端输出字符串的样式。

>SBoudrias/Inquirer.js - Node.js 命令行交互工具,通用的命令行用户界面集合,可以和用户进行交互。

>sindresorhus/ora - 实现加载中的状态是一个 Loading 加前面转起来的小圈圈,成功了是一个 Success 加前面一个小钩钩。

>SBoudrias/mem-fs-editor - 提供一系列 API,方便操作模板文件。

>shelljs/shelljs - ShellJS 是 Node.js 扩展,用于实现 Unix shell 命令执行。

>Node.js child_process - 模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大 200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。

#### Taro Init 命令主要的流程

***
>Taro 命令是怎样添加进去的呢?其原因在于 package.json 里面的 bin 字段:
```JSON
"bin": {
    "taro": "bin/taro"
  },

上面代码指定,Taro 命令对应的可执行文件为 bin/taro 。NPM 会寻找这个文件,在 [prefix]/bin 目录下建立符号链接。在上面的例子中,Taro 会建立符号链接 [prefix]/bin/taro。由于 [prefix]/bin 目录会在运行时加入系统的 PATH 变量,因此在运行 NPM 时,就可以不带路径,直接通过命令来调用这些脚本。

关于prefix,可以通过npm config get prefix获取。

$ npm config get prefix
/usr/local

通过下列命令可以更加清晰的看到它们之间的符号链接:

$ ls -al `which taro`
lrwxr-xr-x  1 chengshuai  admin  40  6 15 10:51 /usr/local/bin/taro -> ../lib/node_modules/@tarojs/cli/bin/taro

Taro 子命令 命令关联与参数解析 这里就不得不提到一个有用的包:tj/commander.js ,Node.js 命令行接口全面的解决方案,灵感来自于 Ruby's commander。可以自动的解析命令和参数,合并多选项,处理短参等等,功能强大,上手简单。具体的使用方法可以参见项目的 README。

https://github.com/SBoudrias/Inquirer.js https://github.com/tj/commander.js/ https://github.com/commander-rb/commander

更主要的,commander 支持 Git 风格的子命令处理,可以根据子命令自动引导到以特定格式命名的命令执行文件,文件名的格式是 [command]-[subcommand],例如:

taro init => taro-init
taro build => taro-build

/bin/taro 文件内容不多,核心代码也就那几行 .command() 命令:


#! /usr/bin/env node

const program = require('commander') const {getPkgVersion} = require('../src/util')

program .version(getPkgVersion()) .usage(' [options]') .command('init [projectName]', 'Init a project with default templete') .command('build', 'Build a project with options') .command('update', 'Update packages of taro') .parse(process.argv)


>init,build ,update等命令都是通过.command(name, description)方法定义的,然后通过 .parse(arg) 方法解析参数。具体可以查看 Commander.js API 文档。
>http://tj.github.io/commander.js/

>注意第一行#!/usr/bin/env node,有个关键词叫 Shebang
>http://smilejay.com/2012/03/linux_shebang/

***

>参数解析及与用户交互

>这里使用的是 SBoudrias/Inquirer.js 来处理命令行交互
>https://github.com/SBoudrias/Inquirer.js/

>用法
```ts
const inquirer = require('inquirer')  // npm i inquirer -D

if (typeof conf.description !== 'string') {
      prompts.push({
        type: 'input',
        name: 'description',
        message: '请输入项目介绍!'
      })
}

prompt()接受一个问题对象的数据,在用户与终端交互过程中,将用户的输入存放在一个答案对象中,然后返回一个Promise,通过then()获取到这个答案对象。

借此,新项目的名称、版本号、描述等信息可以直接通过终端交互插入到项目模板中,完善交互流程。

当然,交互的问题不仅限于此,可以根据自己项目的情况,添加更多的交互问题。inquirer.js 强大的地方在于,支持很多种交互类型,除了简单的input,还有confirm、list、password、checkbox等

此外,你在执行异步操作的过程中,还可以使用 sindresorhus/ora 来添加一下 Loading 效果。使用 chalk/chalk 给终端的输出添加各种样式。 https://github.com/sindresorhus/ora https://github.com/chalk/chalk


WangShuXian6 commented 5 years ago

Taro ts 正确解析 state

type PageState = {test:string}

type IProps = PageStateProps & PageDispatchProps & PageOwnProps

interface MiToast {
    props: IProps;
    state:PageState // 关键
}
WangShuXian6 commented 5 years ago

小程序 本地图片预加载

import {ComponentClass} from 'react'
import Taro, {Component} from '@tarojs/taro'
import {View, Canvas, Image} from '@tarojs/components'
//import {connect} from '@tarojs/redux'

import './index.less'
import SpriteImage from '../../images/slideshow/main-sprite-3.png'
import A1Image from '../../images/slideshow/a_1.png'
import A2Image from '../../images/slideshow/a_2.png'
import A3Image from '../../images/slideshow/a_3.png'
import A4Image from '../../images/slideshow/a_4.png'
import A5Image from '../../images/slideshow/a_5.png'
import A6Image from '../../images/slideshow/a_6.png'

let loadedImageList: any[] = []

type PageStateProps = {}

type PageDispatchProps = {}

type PageOwnProps = {
  config: any
}

type PageState = {
  imageList: any[];
}

type IProps = PageStateProps & PageDispatchProps & PageOwnProps

interface Slideshow {
  props: IProps;
  state: PageState;
}

//@connect(({}) => ({}), (dispatch) => ({}))
class Slideshow extends Component {
  ctx

  constructor() {
    super(...arguments)
    this.state = {
      imageList: [],
    }
  }

  async componentDidMount() {
    this.ctx = Taro.createCanvasContext('canvas', this.$scope)
    const imageList = this.generateImageList()
    this.setState({imageList})
    console.log('图片加载开始')
    await this.checkImageLoad()
    console.log('图片加载完成')
  }

  componentWillReceiveProps() {

  }

  componentWillUnmount() {
  }

  componentDidShow() {

  }

  componentDidHide() {
  }

  prevent(e) {
    e.preventDefault()
    e.stopPropagation()
  }

  generateImageList() {
    return [
      SpriteImage,
      A1Image,
      A2Image,
      A3Image,
      A4Image,
      A5Image,
      A6Image,
    ]
  }

  imageLoaded(e) {
    console.log('success')
    loadedImageList.push('')
  }

  imageLoadedError(e) {
    console.warn('图片加载失败', e)
  }

  checkImageLoad() {
    return new Promise((resolve) => {
      let timer = setInterval(() => {
        if (loadedImageList.length === this.state.imageList.length) {
          clearInterval(timer)
          resolve(true)
        }
      }, 300)
    })
  }

  handleClick(){}

  render() {
    return (
      <View className='slideshow-container' onClick={this.prevent.bind(this)}>
        {
          this.state.imageList.map((image, index) => (
            <Image
              className='image'
              src={image}
              onLoad={this.imageLoaded.bind(this)}
              onError={this.imageLoadedError.bind(this)}
              key={index}/>
          ))
        }

        <Canvas
          className='main-canvas'
          canvasId="canvas"
          onClick={this.handleClick.bind(this)}
        />
      </View>
    )
  }
}

export default Slideshow as ComponentClass<PageOwnProps, PageState>
.main-canvas {
  width: 750px;
  height: 324px;
  background-color: #baf091;
}

.image{
  display: none;
}

小程序 远程图片预加载

import {ComponentClass} from 'react'
import Taro, {Component} from '@tarojs/taro'
import {View, Canvas, Image} from '@tarojs/components'
//import {connect} from '@tarojs/redux'

import './index.less'

const baseUrl = `https://cdn.xxx.com/wsx/slideshow/`
const slideList = [
  'main-sprite-3.png',
  'a_1.png',
  'a_2.png',
  'a_3.png',
  'a_4.png',
  'a_5.png',
  'a_6.png',
]

type PageStateProps = {}

type PageDispatchProps = {}

type PageOwnProps = {
  config: any
}

type PageState = {

}

type IProps = PageStateProps & PageDispatchProps & PageOwnProps

interface Slideshow {
  props: IProps;
  state: PageState;
}

//@connect(({}) => ({}), (dispatch) => ({}))
class Slideshow extends Component {
  ctx

  constructor() {
    super(...arguments)
    this.state = {

    }
  }

  async componentDidMount() {
    this.ctx = Taro.createCanvasContext('canvas', this.$scope)
    await this.asyncDownloadImage()
  }

  componentWillReceiveProps() {

  }

  componentWillUnmount() {
  }

  componentDidShow() {

  }

  componentDidHide() {
  }

  prevent(e) {
    e.preventDefault()
    e.stopPropagation()
  }

  handleClick(e) {
    console.log('e,', e)
  }

  async asyncDownloadImage() {
    Promise.all(this.downloadImagePromise(baseUrl, slideList))
      .then((data) => {
        console.log('加载图片成功', data)
      })
      .catch((error) => {
        console.warn('加载图片失败', error)
      })
  }

  downloadImagePromise(baseUrl, imageUrlList) {
    return imageUrlList.map((imageUrl) => {
      const url = `${baseUrl}${imageUrl}`
      console.log('url', url)
      return Taro.downloadFile({
        url: `${baseUrl}${imageUrl}`
      })
    })
  }

  render() {
    return (
      <View className='slideshow-container' onClick={this.prevent.bind(this)}>
        <Canvas
          className='main-canvas'
          canvasId="canvas"
          onClick={this.handleClick.bind(this)}
        />
      </View>
    )
  }
}

export default Slideshow as ComponentClass<PageOwnProps, PageState>
WangShuXian6 commented 5 years ago

Taro-UI

适配 h5

https://taro-ui.aotu.io/#/docs/quickstart https://taro-ui.aotu.io/#/docs/questions

增加配置项:

h5: {
  esnextModules: ['taro-ui']
}
WangShuXian6 commented 5 years ago

Taro 不同平台 打包到不同目录

config/index.js


const outputRootStrtegy = {
h5: 'dist_h5',
weapp: 'dist_weapp',
alipay: 'dist_alipay',
swan: 'dist_swan',
['undefined']: 'dist'
}
const env = JSON.parse(process.env.npm_config_argv)['cooked'][1].split(':')[1]
const outputRoot = outputRootStrtegy[env]

const config = { projectName: 'time-paper-mini', date: '2018-12-11', designWidth: 750, deviceRatio: { '640': 2.34 / 2, '750': 1, '828': 1.81 / 2 }, sourceRoot: 'src', outputRoot: outputRoot, plugins: { babel: { sourceMap: true, presets: [ 'env' ], plugins: [ 'transform-decorators-legacy', 'transform-class-properties', 'transform-object-rest-spread' ] } }, defineConstants: {}, copy: { patterns: [], options: {} }, weapp: { module: { postcss: { autoprefixer: { enable: true, config: { browsers: [ 'last 3 versions', 'Android >= 4.1', 'ios >= 8' ] } }, pxtransform: { enable: true, config: {} }, url: { enable: true, config: { limit: 10240 // 设定转换尺寸上限 } } } } }, h5: { publicPath: '/', staticDirectory: 'static', module: { postcss: { autoprefixer: { enable: true } } }, h5: { esnextModules: ['taro-ui'] } } }

module.exports = function (merge) { if (process.env.NODE_ENV === 'development') { return merge({}, config, require('./dev')) } return merge({}, config, require('./prod')) }

WangShuXian6 commented 5 years ago

Taro h5 防止跨域 devServer 配置

/config/dev.js


const isH5 = process.env.CLIENT_ENV === 'h5'
const HOST = '"https://test.xxx.com/api/"'

module.exports = { env: { NODE_ENV: '"development"' }, defineConstants: { HOST: isH5 ? '"/api"' : HOST }, weapp: {}, h5: { devServer: { proxy: { '/api/': { target: JSON.parse(HOST), pathRewrite: { '^/api/': '/' }, changeOrigin: true, secure: false, logLevel: "debug" } } } } }


>/config/index.js
```ts
const outputRootStrtegy = {
  h5: 'dist_h5',
  weapp: 'dist_weapp',
  alipay: 'dist_alipay',
  swan: 'dist_swan',
  tt: 'dist_tt',
  ['undefined']: 'dist'
}
const env = JSON.parse(process.env.npm_config_argv)['cooked'][1].split(':')[1]
const outputRoot = outputRootStrtegy[env]

const config = {
  projectName: 'time-paper-mini',
  date: '2018-12-11',
  designWidth: 750,
  deviceRatio: {
    '640': 2.34 / 2,
    '750': 1,
    '828': 1.81 / 2
  },
  sourceRoot: 'src',
  outputRoot: outputRoot,
  plugins: {
    babel: {
      sourceMap: true,
      presets: [
        'env'
      ],
      plugins: [
        'transform-decorators-legacy',
        'transform-class-properties',
        'transform-object-rest-spread'
      ]
    }
  },
  defineConstants: {},
  copy: {
    patterns: [],
    options: {}
  },
  weapp: {
    module: {
      postcss: {
        autoprefixer: {
          enable: true,
          config: {
            browsers: [
              'last 3 versions',
              'Android >= 4.1',
              'ios >= 8'
            ]
          }
        },
        pxtransform: {
          enable: true,
          config: {}
        },
        url: {
          enable: true,
          config: {
            limit: 10240 // 设定转换尺寸上限
          }
        }
      }
    }
  },
  h5: {
    publicPath: '/',
    staticDirectory: 'static',
    module: {
      postcss: {
        autoprefixer: {
          enable: true
        }
      }
    },
    output: {
      filename: 'js/[name].[hash].js',
      chunkFilename: 'js/[name].[chunkhash].js'
    },
    imageUrlLoaderOption: {
      limit: 5000,
      name: 'static/images/[name].[hash].[ext]'
    },
    miniCssExtractPluginOption: {
      filename: 'css/[name].[hash].css',
      chunkFilename: 'css/[name].[chunkhash].css'
    },
    esnextModules: ['taro-ui']
  }
}

module.exports = function (merge) {
  if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'))
  }
  return merge({}, config, require('./prod'))
}

使用

/src/api.js

const API = process.env.NODE_ENV === 'production' ? 'xxx' : 'test'
const isH5 = process.env.TARO_ENV === 'h5'
export const BaseUrl = isH5 ? `http://127.0.0.1:10086/api/` : `https://${API}.xxxxx.com/api/`
WangShuXian6 commented 5 years ago

Taro 使用图标字体,app.less全局样式

页面 类

static options = {
    addGlobalClass: true
  };

,如果是函数式组件, 函数名.options = {addGlobalClass: true}

import { View, Text } from "@tarojs/components";

import "./TabIcon.less";

export default function TabIcon(icon) {
  return (
    <View className="tab-icon-container">
      <View
        className={"tab-icon " + icon.iconClass}
        style={{ color: "blue" }}
      ></View>
      <Text className={icon.active ? "tab-name active" : "tab-name"}>
        {icon.iconName}
      </Text>
    </View>
  );
}

TabIcon.options = { addGlobalClass: true };
WangShuXian6 commented 5 years ago

Taro 上传图片 [h5]

需要自己根据mime type来生成个随机文件名

现在 chooseImage 还不能返回完整的文件名

Taro.uploadFile 上传参数

  const buildUploadImageOption = chooseImageRes => {
    const extInfo = extName.mime(chooseImageRes.tempFiles[0].type);
    const tempName = Math.ceil(Math.random() * 10000);
    let fileName = `${tempName}.${extInfo[0].ext}`;

    return {
      url: UploadImageApi,
      filePath: chooseImageRes.tempFilePaths[0],
      name: "img",
      fileName,
      formData: {}
    };
  };

一个上传组件

import Taro from "@tarojs/taro";
// import { useState } from "@tarojs/taro";

import "./TakePhoto.less";
import { View } from "@tarojs/components";
import extName from "ext-name";

const IMAGE_EXTS = ["image/jpg", "image/jpeg", "image/png"];

const BaseUrl =
  process.env.NODE_ENV === "production"
    ? "https://prod.xxx.com"
    : "https://test.xxx.com";

const UploadImageApi = BaseUrl + "/api/UploadImg";

export default function TakePhoto({ onTakePhoto }) {
  const takePhoto = async () => {
    Taro.chooseImage({ count: 1 })
      .then(async res => {
        console.log(res);
        if (!checkImage(res)) return false;

        await asyncUploadImage(res);
        onTakePhoto(res.tempFiles[0].path);
      })
      .catch(error => {
        console.warn(error);
        Taro.showToast({
          title: "请重新选择图片",
          icon: "none",
          duration: 2000
        });
      });
  };

  const checkImage = res => {
    if (
      !res.tempFiles ||
      !res.tempFiles[0] ||
      !res.tempFiles[0].type ||
      !IMAGE_EXTS.includes(res.tempFiles[0].type)
    ) {
      Taro.showToast({
        title: "请选择 jpg/jpeg/png 类型的图片",
        icon: "none",
        duration: 2000
      });
      return false;
    } else {
      return true;
    }
  };

  const buildUploadImageOption = chooseImageRes => {
    const extInfo = extName.mime(chooseImageRes.tempFiles[0].type);
    const tempName = Math.ceil(Math.random() * 10000);
    let fileName = `${tempName}.${extInfo[0].ext}`;

    return {
      url: UploadImageApi,
      filePath: chooseImageRes.tempFilePaths[0],
      name: "img",
      fileName,
      formData: {}
    };
  };

  const asyncUploadImage = async chooseImageRes => {
    if (
      !chooseImageRes ||
      !chooseImageRes.tempFilePaths ||
      !chooseImageRes.tempFilePaths[0]
    )
      return false;
    const newOption = buildUploadImageOption(chooseImageRes);

    return new Promise(resolve => {
      Taro.uploadFile(newOption)
        .then(res => {
          console.log(res);
          if (isUploadSuccess(res)) {
            resolve(res);
          } else {
            resolve(false);
          }
        })
        .catch(error => {
          console.warn("error--", error);
          Taro.showToast({
            title: "上传错误" + JSON.stringify(error),
            icon: "none"
          });
          resolve(false);
        });
    });
  };

  const isUploadSuccess = response => {
    const newData = JSON.parse(response.data);
    return (
      response.statusCode === 200 && newData.code === 2000 && newData.img_path
    );
  };

  return (
    <View className="take-photo-container">
      <View className="icon-upload take-photo" onClick={takePhoto}></View>
    </View>
  );
}

style

            <Image src={editedImage} className="edited-image" style={{width:width,height:height}}></Image>
WangShuXian6 commented 5 years ago

Taro h5 端 资源相对路径

h5: {
    publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
WangShuXian6 commented 5 years ago

Taro v1.3.19 配置云函数

目录结构

|--client [客户端根目录-在此目录下执行编译]  
|--|--node_modules [客户端依赖]  
|--|--config []  
|--|--|--dev.js []  
|--|--|--index.js []  
|--|--|--prod.js []  
|--|--src [客户端源码]  
|--|--.editorconfig []  
|--|--.eslintrc []  
|--|--global.d.ts []  
|--|--package.json []  
|--|--tsconfig.json []  
|--|--.gitignore []  
|--|--.gitlab-ci.yml []  
|--|--.variables.yml []  
|--|--.npmrc []  
|--|--mini.project.json [微信小程序配置]  
|--|--project.config.json [淘宝小程序配置]  
|--cloud [微信 云函数 源码]  
|--|--functions [云函数文件夹]  
|--|--|--login [云函数名称-在此目录安装依赖-执行编译]  
|--|--|--|--node_modules [独立的云函数依赖]  
|--|--|--|--index.ts []  
|--|--|--|--package.json []  
|--server [淘宝 云函数 源码]  
|--|--login [云函数名称-在此目录安装依赖-执行编译]  
|--|--|--node_modules []  
|--|--|--cloud.json []  
|--|--|--index.js []  
|--|--|--package.json []  
|--dist_alipay [编译后的客户端文件-淘宝端]  
|--|--client [客户端根目录]  
|--|--|--app.js []  
|--|--server [云函数根目录-从外部手动拷贝]  
|--|--|--login []  
|--|--|--|--node_modules []  
|--|--|--|--cloud.json []  
|--|--|--|--index.js []  
|--|--|--|--package.json []  
WangShuXian6 commented 4 years ago

Taro 增加自定义环境 prerelease env

config/prerelease.js

module.exports = {
  env: {
    NODE_ENV: '"prerelease"'
  },
  defineConstants: {

  },
  weapp: {},
  h5: {

  }
};

config/index.js

// 省略
module.exports = function (merge) {
  if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'))
  }
  if (process.env.NODE_ENV === 'prerelease') {
    return merge({}, config, require('./prerelease'))
  }
  return merge({}, config, require('./prod'))
}

package.json

"scripts": {
    "build:weapp": "taro build --type weapp",
    "build:swan": "taro build --type swan",
    "build:alipay": " taro build --type alipay",
    "build:tt": "taro build --type tt",
    "build:h5": "taro build --type h5",
    "build:rn": "taro build --type rn",
    "dev:weapp": "npm run build:weapp -- --watch",
    "dev:swan": "npm run build:swan -- --watch",
    "dev:alipay": "npm run  build:alipay -- --watch",
    "dev:tt": "npm run build:tt -- --watch",
    "dev:h5": "npm run build:h5 -- --watch",
    "dev:rn": "npm run build:rn -- --watch",
    "pre:weapp": "NODE_ENV=prerelease taro build --type weapp --watch",
    "pre:swan": "NODE_ENV=prerelease  taro build --type swan --watch",
    "pre:alipay": "NODE_ENV=prerelease   taro build --type alipay --watch",
    "pre:tt": "NODE_ENV=prerelease  taro build --type tt --watch",
    "pre:h5": "NODE_ENV=prerelease  taro build --type h5 --watch",
    "pre:rn": "NODE_ENV=prerelease  taro build --type rn --watch",
    "mock:taobao": "json-server --watch src/mock/taobao/db.json"
  },

执行

npm run pre:weapp

显示

console.log('process.env.NODE_ENV',process.env.NODE_ENV)

// process.env.NODE_ENV prerelease
WangShuXian6 commented 2 months ago

ScrollView

ScrollView 的高度必须固定才能触发ScrollView的onScrollToUpper滚动到顶部事件

      <ScrollView
        className={styles['patient-chat-scrollview']}
        scrollY
        scrollWithAnimation
        onScrollToUpper={handleScrollToUpper}
      >
        <PatientChatList messages={messages} />
        <View className={'bottom-blank'}></View>
        <View className={styles['test-height']}></View>
      </ScrollView>
  & > .patient-chat-scrollview {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: $scrollHeight;
    background: #f3f3f5;
    white-space: nowrap;
    & > .bottom-blank {
      width: 100%;
      height: 100px; // 94px;
    }
  }

ScrollView 的同级别如果动态增加了元素,那么会导致ScrollView自动滚动到顶部

WangShuXian6 commented 1 month ago

Taro React 小程序 长期订阅组件

组件

效果

image 取消后重复展示N次: image

useSubscribeMessage.ts

import { useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { createLogger } from '@/utils/common'
const logger = createLogger()

const SUBSCRIBE_MESSAGE_SUCCESS = 'requestSubscribeMessage:ok'

interface Props {
  tmplIds: string[]
  lazy: boolean
}

interface Return {
  subscribeMessage: () => Promise<boolean>
}
export const useSubscribeMessage = ({ tmplIds, lazy }: Props): Return => {
  const subscribeMessage = (tmplIds: string[]): Promise<boolean> => {
    return new Promise((resolve, reject) => {
      Taro.requestSubscribeMessage({
        tmplIds: tmplIds,
        entityIds: [],
        success: (res) => {
          const { errMsg } = res
          if (errMsg === SUBSCRIBE_MESSAGE_SUCCESS) {
            // 检查每个模板消息的订阅状态
            const results = tmplIds.map((tmplId) => res[tmplId])
            const isAllAccept = results.every((status) => status === 'accept')
            if (isAllAccept) {
              resolve(true)
            } else {
              logger.warn('用户拒绝订阅部分或全部消息')
              resolve(false)
            }
          } else {
            resolve(false)
          }
        },
        fail: (error) => {
          resolve(false)
        },
        complete: (res: TaroGeneral.CallbackResult) => {}
      })
    })
  }

  const run = async () => {
    const result = await subscribeMessage(tmplIds)
    logger.info('run result:', result)
    return result
  }

  useEffect(() => {
    if (lazy) return
    run()
  }, [lazy])
  return {
    subscribeMessage: run
  }
}

useSubscribeMessageModal.tsx

import { useEffect } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { useSubscribeMessage } from './useSubscribeMessage'
import { createLogger } from '@/utils/common'
import { emitter, useFDialog } from '@/components/common/FDialog/useFDialog'

const logger = createLogger()
const subscribeMessageFlag = 'subscribeMessageFlag'

interface Props {
  tmplIds: string[]
  lazy: boolean
}

interface Return {
  subscribeMessage: () => Promise<boolean>
  rendeSubscribeMessageTip: (msg: string) => JSX.Element
}
export const useSubscribeMessageModal = ({ tmplIds, lazy }: Props): Return => {
  const { subscribeMessage } = useSubscribeMessage({ tmplIds, lazy: true })
  const { render: rendeSubscribeMessageTip, setVisible: showSubscribeMessageTip } = useFDialog(
    subscribeMessageFlag,
    true
  )

  const subscribeMessageModal = async (): Promise<boolean> => {
    let isSubscribe = false
    let attempt = 0
    const maxAttempts = 999 // 设置最大尝试次数,避免无限循环

    while (!isSubscribe && attempt < maxAttempts) {
      attempt++
      isSubscribe = await subscribeMessage()
      logger.info(`第${attempt}次订阅尝试,结果:`, isSubscribe)

      if (!isSubscribe) {
        logger.warn('用户拒绝订阅,显示二次弹窗')
        showSubscribeMessageTip(true)

        await new Promise((resolve) => {
          const handleConfirm = async () => {
            emitter.off(subscribeMessageFlag, handleConfirm)
            resolve(null)
          }
          emitter.on(subscribeMessageFlag, handleConfirm)
        })
      }
    }

    if (!isSubscribe) {
      logger.warn('用户多次拒绝订阅,放弃请求')
    }

    return isSubscribe
  }

  const run = async (): Promise<boolean> => {
    return await subscribeMessageModal()
  }

  useEffect(() => {
    if (lazy) return
    run()
  }, [lazy])
  return {
    subscribeMessage: run,
    rendeSubscribeMessageTip
  }
}

createLogger

type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR'
const isProduction = process.env.NODE_ENV === 'production'

const logLevels: Record<LogLevel, { level: LogLevel; style: string }> = {
  DEBUG: { level: 'DEBUG', style: 'background-color: #25cbe9; color: white;' },
  INFO: { level: 'INFO', style: 'background-color: #28a745; color: white;' },
  WARN: { level: 'WARN', style: 'background-color: #ffc107; color: black;' },
  ERROR: { level: 'ERROR', style: 'background-color: #dc3545; color: white;' }
}

const getCurrentTimestamp = (): string => {
  const now = new Date()
  const year = now.getFullYear()
  const month = (now.getMonth() + 1).toString().padStart(2, '0')
  const day = now.getDate().toString().padStart(2, '0')
  const hours = now.getHours().toString().padStart(2, '0')
  const minutes = now.getMinutes().toString().padStart(2, '0')
  const seconds = now.getSeconds().toString().padStart(2, '0')
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

export const createLogger = (isEnabled: boolean = !isProduction) => {
  const log = (level: LogLevel, message: unknown, ...optionalParams: unknown[]): void => {
    if (!isEnabled) return
    //console.trace() // 显示堆栈追踪
    const timestamp = getCurrentTimestamp()
    const { style } = logLevels[level]

    // Format message depending on its type
    let formattedMessage: string
    if (typeof message === 'string') {
      formattedMessage = message
    } else {
      try {
        formattedMessage = JSON.stringify(message, null, 2)
      } catch (error) {
        console.error('解析日志失败:', error)
        formattedMessage = ''
      }
    }

    console.log(`%c[${timestamp}] [${level}] ${formattedMessage}`, style, ...optionalParams)
  }

  return {
    debug: (message: unknown, ...optionalParams: unknown[]): void => {
      log(logLevels.DEBUG.level, message, ...optionalParams)
    },
    info: (message: unknown, ...optionalParams: unknown[]): void => {
      log(logLevels.INFO.level, message, ...optionalParams)
    },
    warn: (message: unknown, ...optionalParams: unknown[]): void => {
      log(logLevels.WARN.level, message, ...optionalParams)
    },
    error: (message: unknown, ...optionalParams: unknown[]): void => {
      log(logLevels.ERROR.level, message, ...optionalParams)
    }
  }
}

// 示例使用
// const isProduction = process.env.NODE_ENV === 'production';
// const logger = createLogger();

// logger.debug("This is a debug message");
// logger.info({ key: "value", anotherKey: [1, 2, 3] });
// logger.warn(["This", "is", "a", "warning", "message"]);
// logger.error(new Error("This is an error message"));

useFDialog

import React, { useState } from 'react'
import FDialog from './index'
import { EventEmitter } from 'events'
const emitter = new EventEmitter()

interface Return {
  render: (msg: string) => React.ReactElement
  setVisible: React.Dispatch<React.SetStateAction<boolean>>
}

export const useFDialog = (
  emitFlag?: string,
  hideCancelButton: boolean = false,
  hideConfirmButton: boolean = false
) => {
  const [visible, setVisible] = useState(false)

  const handleClose = () => {
    setVisible(false)
  }

  const handleConfirm = () => {
    if (!emitFlag) return
    emitter.emit(emitFlag, '')
  }

  const render = (msg: string) => {
    return (
      <FDialog
        onClose={handleClose}
        msg={msg}
        visible={visible}
        hideCancelButton={hideCancelButton}
        hideConfirmButton={hideConfirmButton}
        setVisible={setVisible}
        onConfirm={handleConfirm}
      />
    )
  }

  return {
    render,
    setVisible
  }
}

export { emitter }

FDialog

import React, { useState } from 'react'
import { Cell, Dialog } from '@nutui/nutui-react-taro'
import { on } from 'events'
interface Props {
  msg: string
  visible: boolean
  setVisible: React.Dispatch<React.SetStateAction<boolean>>
  hideConfirmButton: boolean
  hideCancelButton: boolean
  onConfirm?: () => void
  onCancel?: () => void
  onClose: () => void
}

const FDialog = ({
  msg,
  visible,
  setVisible,
  hideConfirmButton,
  hideCancelButton,
  onClose,
  onConfirm,
  onCancel
}: Props) => {
  const handleConfirm = () => {
    setVisible(false)
    onConfirm && onConfirm()
  }
  const handleCancel = () => {
    setVisible(false)
    onCancel && onCancel()
  }
  return (
    <>
      {/* <Cell
        title="提示"
        onClick={() => {
          setVisible(true)
        }}
      /> */}
      <Dialog
        onClose={onClose}
        className="test-dialog"
        title="提示"
        visible={visible}
        hideConfirmButton={hideConfirmButton}
        hideCancelButton={hideCancelButton}
        closeIcon={false}
        closeIconPosition="top-right"
        style={{
          '--nutui-dialog-close-color': '#8c8c8c'
        }}
        onConfirm={handleConfirm}
        onCancel={handleCancel}
      >
        {msg}
      </Dialog>
    </>
  )
}
export default FDialog

使用

import { lazy, useState } from 'react'
import Taro, { useDidShow } from '@tarojs/taro'
import { View, Text, RichText, Button } from '@tarojs/components'
import { useSubscribeMessageModal } from '../common/hooks'
import { TemplateID } from '../common/config'
import { createLogger } from '@/utils/common'
const logger = createLogger()

const tmplIds = [TemplateID.ConsultationReminder]

const Page = () => {
  const { subscribeMessage, rendeSubscribeMessageTip } = useSubscribeMessageModal({
    tmplIds,
    lazy: true
  })

  const handleButtonClick = async () => {
    const isSubscribe = await subscribeMessage()
    if (!isSubscribe) {
      logger.warn('用户拒绝订阅')
      return
    }
  }

  return (
    <View>
      <Button onClick={handleButtonClick}>我同意</Button>
      {rendeSubscribeMessageTip('请确认允许“aaa”向您发送“问诊提醒”')}
    </View>
  )
}

export default Page
WangShuXian6 commented 1 month ago

版本号 hooks

点击UI即消失 image

hooks

src\hooks\useVersionDisplay.tsx

import React, { useEffect } from 'react'
import Taro from '@tarojs/taro'
import { render, unmountComponentAtNode } from '@tarojs/react'
import { View, Text, RootPortal } from '@tarojs/components'
import { document } from '@tarojs/runtime'

const isProduction = process.env.NODE_ENV === 'production'

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// 示例:生成一个在 1 到 10 之间的随机整数
//const randomInt = getRandomInt(1, 10);
//console.log(randomInt);

const VersionDisplay = ({ version, onClose }: { version: string; onClose: () => void }) => {
  const containerStyle = {
    position: 'fixed' as 'fixed',
    top: 0,
    left: 0,
    width: '100vw',
    padding: '10px',
    backgroundColor: 'rgba(0, 0, 0, 0.7)',
    color: '#fff',
    textAlign: 'center' as 'center',
    zIndex: 9999
  }

  const handleClick = (event) => {
    event.stopPropagation() // 阻止事件冒泡
    onClose() // 关闭版本号 UI
  }

  return (
    <RootPortal>
      <View style={containerStyle} onClick={handleClick}>
        <Text>版本号: {version}</Text>
      </View>
    </RootPortal>
  )
}

export const destroy = (node) => {
  const currentPages = Taro.getCurrentPages()
  const currentPage = currentPages[currentPages.length - 1]
  const path = currentPage.$taroPath
  const pageElement = document.getElementById(path)

  unmountComponentAtNode(node)
  pageElement?.removeChild(node)
}

export const useVersionDisplay = (version: string) => {
  console.log(`版本号: ${version}`)
  const init = () => {
    console.log(`版本号: ${version}`)
    if (isProduction) return
    const id = `version-display-${getRandomInt(10000,10000000)}`
    let view = document.getElementById(id)

    const currentPages = Taro.getCurrentPages()
    const currentPage = currentPages[currentPages.length - 1]
    const path = currentPage.$taroPath
    const pageElement = document.getElementById(path)

    const handleClose = () => {
      view && destroy(view)
    }

    if (!view) {
      // 如果节点不存在,则创建并挂载
      view = document.createElement('view')
      view.id = id
      render(<VersionDisplay version={version} onClose={handleClose} />, view)
      pageElement?.appendChild(view)
    } else {
      // 如果节点已存在,更新版本号和样式
      render(
        <VersionDisplay version={version} onClose={handleClose} />,
        view
      )(
        // 重置 z-index 使其显示
        view.style as any
      ).zIndex = '9999'
    }
  }
  useEffect(() => {
    try {
      init()
    } catch (error) {
      console.error('渲染版本号UI错误:', error)
    }
    // 不再移除节点,避免报错
    return () => {
      // 如果需要在组件卸载时隐藏或处理节点,可以在这里处理
    }
  }, [version])
}

使用

import { useVersionDisplay } from '@/hooks/useVersionDisplay';

const MyApp = () => {
  useVersionDisplay('1.0.0');

  return (
    <View>
      {/* 其他内容 */}
    </View>
  );
};

export default MyApp;
WangShuXian6 commented 1 month ago

text 换行

    & > .f-notice-bar-text {
      width: 333px;
      font-family: PingFangSC, PingFang SC;
      font-weight: 400;
      font-size: 12px;
      //color: #d3700d;
      line-height: 17px;
      text-align: left;
      font-style: normal;
      word-break: break-all;
      word-wrap: break-word;
      white-space: normal;
    }