WangShuXian6 / blog

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

Taro Next #97

Open WangShuXian6 opened 4 years ago

WangShuXian6 commented 4 years ago

Taro Next

https://taro-docs.jd.com/taro/next/docs/migration.html

CLI 工具安装

首先,你需要使用 npm 或者 yarn 全局安装@tarojs/cli,或者直接使用npx:

使用 npm 安装 CLI

$ npm install -g @tarojs/cli@next
# OR 使用 yarn 安装 CLI
$ yarn global add @tarojs/cli@next
# OR 安装了 cnpm,使用 cnpm 安装 CLI
$ cnpm install -g @tarojs/cli@next

注意事项

值得一提的是,如果安装过程出现sass相关的安装错误,请在安装mirror-config-china后重试。

$ npm install -g mirror-config-china

项目初始化

使用命令创建模板项目

$ taro init myApp

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

$ npx @tarojs/cli init myApp

依赖迁移

项目里只需要加几个依赖,把

import Taro, { useEffect, useLayoutEffect, useReducer, useState, useContext, useRef, useCallback, useMemo, useRouter, useScope, useTabItemTap, useResize, useReachBottom, usePullDownRefresh, useDidHide, useDidShow, usePageScroll } from '@tarojs/taro'

换成

import React, { Component, useEffect, useLayoutEffect, useReducer, useState, useContext, useRef, useCallback, useMemo, } from 'react'
import Taro, { Current, useRouter, useScope, useTabItemTap, useResize, useReachBottom, usePullDownRefresh, useDidHide, useDidShow, usePageScroll } from '@tarojs/taro'

就可以完成迁移,其他代码都不用动

WangShuXian6 commented 4 years ago

多平台独立目录编译配置

config/index.js

const path = require("path");

const outputRootStrtegy = {
  h5: "dist_h5",
  weapp: "dist_weapp",
  alipay: "dist_alipay/client",
  swan: "dist_swan",
  tt: "dist_tt",
  jd: "dist_jd",
  ["undefined"]: "dist"
};

const env = process.env.TARO_ENV;
const outputRoot = outputRootStrtegy[env];

const config = {
  projectName: "myAppnext",
  date: "2020-2-14",
  designWidth: 750,
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2
  },
  sourceRoot: "src",
  outputRoot: outputRoot,
  plugins: [],
  defineConstants: {},
  copy: {
    patterns: [],
    options: {}
  },
  framework: "react",
  mini: {
    postcss: {
      pxtransform: {
        enable: true,
        config: {}
      },
      url: {
        enable: true,
        config: {
          limit: 1024 // 设定转换尺寸上限
        }
      },
      cssModules: {
        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
        config: {
          namingPattern: "module", // 转换模式,取值为 global/module
          generateScopedName: "[name]__[local]___[hash:base64:5]"
        }
      }
    }
  },
  h5: {
    publicPath: "/",
    staticDirectory: "static",
    postcss: {
      autoprefixer: {
        enable: true,
        config: {
          browsers: ["last 3 versions", "Android >= 4.1", "ios >= 8"]
        }
      },
      cssModules: {
        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
        config: {
          namingPattern: "module", // 转换模式,取值为 global/module
          generateScopedName: "[name]__[local]___[hash:base64:5]"
        }
      }
    }
  }
};

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

package.json

{
  "name": "myAppnext",
  "version": "1.0.0",
  "private": true,
  "description": "next test",
  "templateInfo": {
    "name": "default",
    "typescript": true,
    "css": "less"
  },
  "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",
    "build:qq": "taro build --type qq",
    "build:quickapp": "taro build --type quickapp",
    "build:jd": "taro build --type jd",
    "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",
    "dev:qq": "npm run build:qq -- --watch",
    "dev:quickapp": "npm run build:quickapp -- --watch",
    "dev:jd": "npm run build:jd -- --watch"
  },
  "browserslist": [
    "last 3 versions",
    "Android >= 4.1",
    "ios >= 8"
  ],
  "author": "",
  "dependencies": {
    "@babel/runtime": "^7.7.7",
    "@tarojs/components": "3.0.0-alpha.2",
    "@tarojs/runtime": "3.0.0-alpha.2",
    "@tarojs/taro": "3.0.0-alpha.2",
    "@tarojs/react": "3.0.0-alpha.2",
    "react": "^16.10.0",
    "@tbmp/mp-cloud-sdk": "^1.2.2",
    "dayjs": "^1.8.20",
    "regenerator-runtime": "^0.11.1"
  },
  "devDependencies": {
    "@types/webpack-env": "^1.13.6",
    "@types/react": "^16.0.0",
    "@tarojs/mini-runner": "3.0.0-alpha.2",
    "@babel/core": "^7.8.0",
    "babel-preset-taro": "3.0.0-alpha.2",
    "eslint-config-taro": "3.0.0-alpha.2",
    "eslint": "^6.8.0",
    "eslint-plugin-react": "^7.8.2",
    "eslint-plugin-import": "^2.12.0",
    "eslint-plugin-react-hooks": "^1.6.1",
    "stylelint": "9.3.0",
    "@typescript-eslint/parser": "^2.x",
    "@typescript-eslint/eslint-plugin": "^2.x",
    "typescript": "^3.7.0"
  }
}

.editorconfig

# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

.eslintrc

{
  "extends": ["taro"],
  "rules": {
    "no-unused-vars": ["error", { "varsIgnorePattern": "Taro" }],
    "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx", ".tsx"] }]
  },
    "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "useJSXTextNode": true,
    "project": "./tsconfig.json"
  }
  }

.gitignore

node_modules
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# production
/build
dist/

# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

# temp
.temp/
.rn_temp/
deploy_versions/

npm-debug.log*
yarn-debug.log*
yarn-error.log*

/
# 
dist/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store

dist/
dist_weapp/
dist_alipay/
dist_swan/
dist_tt/
dist_h5/
deploy_versions/
.temp/
.rn_temp/
node_modules/
.DS_Store
.idea/

.npmrc

registry=https://registry.npm.taobao.org
disturl=https://npm.taobao.org/dist
sass_binary_site=https://npm.taobao.org/mirrors/node-sass/
phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/
electron_mirror=https://npm.taobao.org/mirrors/electron/
chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver
operadriver_cdnurl=https://npm.taobao.org/mirrors/operadriver
selenium_cdnurl=https://npm.taobao.org/mirrors/selenium
node_inspector_cdnurl=https://npm.taobao.org/mirrors/node-inspector
fsevents_binary_host_mirror=http://npm.taobao.org/mirrors/fsevents/

babel.config.js

// babel-preset-taro 更多选项和默认值:
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
module.exports = {
  presets: [
    ['taro', {
      framework: 'react',
      ts: true
    }]
  ]
}

global.d.ts

declare module '*.png';
declare module '*.gif';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.svg';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.styl';

// @ts-ignore
declare const process: {
  env: {
    TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq';
    [key: string]: any;
  }
}

project.config.json

{
  "miniprogramRoot": "./dist_alipay/client",
  "projectname": "app",
  "description": "app",
  "appid": "touristappid",
  "setting": {
    "urlCheck": true,
    "es6": false,
    "postcss": false,
    "minified": false
  },
  "compileType": "miniprogram"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "removeComments": false,
    "preserveConstEnums": true,
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "noImplicitAny": false,
    "allowSyntheticDefaultImports": true,
    "outDir": "lib",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "strictNullChecks": true,
    "sourceMap": true,
    "baseUrl": ".",
    "rootDir": ".",
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "allowJs": true,
    "resolveJsonModule": true,
    "typeRoots": [
      "node_modules/@types",
      "global.d.ts"
    ]
  },
  "exclude": [
    "node_modules",
    "dist"
  ],
  "compileOnSave": false
}

README.md

## 开发前必备

>Taro v3.0.0-alpha.2

>https://taro-docs.jd.com/taro/next/docs/GETTING-STARTED.html

### CLI 工具安装

>全局安装```@tarojs/cli```,或者直接使用npx:

```bash
# 使用 npm 安装 CLI
npm install -g @tarojs/cli@next
# OR 使用 yarn 安装 CLI
yarn global add @tarojs/cli@next
# OR 安装了 cnpm,使用 cnpm 安装 CLI
cnpm install -g @tarojs/cli@next

注意事项

值得一提的是,如果安装过程出现sass相关的安装错误,请在安装mirror-config-china后重试。

npm install -g mirror-config-china

文件目录

src/adapters

适配器

faceAdapter

src/assets

静态资源 图片 字体

src/components

组件

src/config

配置,常量

src/pages

页面

src/services

服务[网络通信]

src/store

全局单例存储

src/types

类型定义

src/utils

工具函数

WangShuXian6 commented 4 years ago

Taro 组件

异形 轮播图

image

import React, { Component, useEffect, useLayoutEffect, useReducer, useState, useContext, useRef, useCallback, useMemo, } from 'react'
import Taro, { useRouter, useTabItemTap, useResize, useReachBottom, usePullDownRefresh, useDidHide, useDidShow, usePageScroll } from '@tarojs/taro'
import { View, Text, Swiper, SwiperItem, Image } from '@tarojs/components'

import { DeepWriteable } from '@/utils/tsUtils'

import { useHomeQueryResponse } from '@/graphql/query/useHome'

import './Banner.less'

type Props = {
  banneres: DeepWriteable<useHomeQueryResponse['userBanners']>;
}
export default function Banner({ banneres = [] }: Props) {
  const [current, setCurrent] = useState<number>(0)
  useEffect(() => {
    console.log('banneres---2:', banneres)
  }, [banneres])

  const change = (e) => {
    setCurrent(e?.detail?.current)
  }

  return (
    <View className='w-banner-container'>
      <Swiper
        className='w-swiper'
        indicatorColor='#999'
        indicatorActiveColor='#333'
        circular
        indicatorDots
        autoplay
        previousMargin='144rpx'
        nextMargin='144rpx'
        onChange={change.bind(this)}
      >
        {banneres && banneres.map((banner, index) => {
          return (
            <SwiperItem key={index} >
              <Image src={banner.cover} className={current === index ? 'image' : 'image small'} mode='aspectFill' />
            </SwiperItem>
          )
        })}
      </Swiper>
    </View>
  )
}
@import "../../style/color.less";
@import "../../style/size.less";

.w-banner-container {
  width: 100%;
  height: 41vw;
  .w-swiper {
    width: 100%;
    height: 190px;
    .image {
      width: 459px;
      height: 190px;
      border-radius: 10px;
      background-color: @gray-1;
    }
    .small {
      transform: scale3d(0.857, 0.857, 0.857);
    }
  }
}

WangShuXian6 commented 5 months ago

Taro 图表

Taro 使用 Echarts:taro-react-echarts

安装

npm install taro-react-echarts

导入组件

import Echarts from 'taro-react-echarts'

定制下载 Echarts js库 https://echarts.apache.org/zh/builder.html

import { useEffect, useRef } from 'react'
import Echarts, { EChartOption, EchartsHandle } from 'taro-react-echarts'
//@ts-ignore
//import echarts from '../lib/echarts.min'
//@ts-ignore
import echarts from '../lib/echarts'
import styles from "./index.module.scss";

interface Props{

}

const HealthRecords= ({}:Props) => {

  const echartsRef = useRef<EchartsHandle>(null)
  const option: EChartOption = {
    legend: {
      top: 50,
      left: 'center',
      z: 100,
    },
    tooltip: {
      trigger: 'axis',
      show: true,
      confine: true,
    },
    xAxis: {
      type: 'category',
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    },
    yAxis: {
      type: 'value',
    },
    series: [
      {
        data: [150, 230, 224, 218, 135, 147, 260],
        type: 'line',
      },
    ],
  }
  return <Echarts echarts={echarts} option={option} ref={echartsRef} />
};

export default HealthRecords;

Taro 使用 taro-f2-react

domisooo/taro-f2-react

安装

yarn add taro-f2-react @antv/f2 -S

详细使用文档参考 https://f2.antv.antgroup.com/ @antv/f2

使用

关键

config/index.ts

const config = {
  compiler: {
    type: 'webpack5',
    prebundle: {
      enable: false,
    },
  },
}

否则可能会报错“ https://github.com/domisooo/taro-f2-react/issues/3

页面 src/pages/echarts/index.tsx


import React, { useCallback, useRef } from "react";
import { View, Text, Button, Image } from "@tarojs/components";
import F2Canvas from "taro-f2-react";
import { Chart, Interval } from "@antv/f2";
import "./index.less";

const data = [ { genre: "Sports", sold: 275 }, { genre: "Strategy", sold: 115 }, { genre: "Action", sold: 120 }, { genre: "Shooter", sold: 350 }, { genre: "Other", sold: 150 }, ];

const Index = () => { return ( <View style={{ width: "100%", height: "260px" }}>

</View>

); };

export default Index;


### 注意
>如果出现错误:折线图平移:拖动图表事件未定义 Event is not defined #21
taro-f2-react 错误详情:https://github.com/domisooo/taro-f2-react/issues/21

或者等待官方 antvis 修复 https://github.com/antvis/F2/issues/1980

解决方案1:等待 官方 antvis 修复。

解决方案2: 锁定版本
```json
{
  ...
  "dependencies": {
    "@antv/f2": "4.0.51",
    "taro-f2-react": "1.1.1"
  }
}

滚动后获取 ScrollBar 当前的区间 range

image

src\pages\mock\data2.ts


export const data2 = [
{
title: 'Bohemian Rhapsody',
artist: 'Queen',
release: 1975,
year: '1999',
rank: '1',
count: 978
},
{
title: 'Hotel California',
artist: 'Eagles',
release: 1977,
year: '1999',
rank: '2',
count: 1284
},
{
title: 'Child In Time',
artist: 'Deep Purple',
release: 1972,
year: '1999',
rank: '3',
count: 1117
},
{
title: 'Stairway To Heaven',
artist: 'Led Zeppelin',
release: 1971,
year: '1999',
rank: '4',
count: 1132
},
{
title: 'Paradise By The Dashboard Light',
artist: 'Meat Loaf',
release: 1978,
year: '1999',
rank: '5',
count: 1187
},
{
title: 'Yesterday',
artist: 'The Beatles',
release: 1965,
year: '1999',
rank: '6',
count: 909
},
{
title: 'Angie',
artist: 'The Rolling Stones',
release: 1973,
year: '1999',
rank: '8',
count: 1183
},
{
title: 'Bridge Over Troubled Water',
artist: 'Simon & Garfunkel',
release: 1970,
year: '1999',
rank: '9',
count: 1111
},
{
title: 'A Whiter Shade Of Pale',
artist: 'Procol Harum',
release: 1967,
year: '1999',
rank: '10',
count: 1190
},
{
title: 'Hey Jude',
artist: 'The Beatles',
release: 1968,
year: '1999',
rank: '11',
count: 1037
},
{
title: 'House Of The Rising Sun',
artist: 'The Animals',
release: 1964,
year: '1999',
rank: '13',
count: 543
},
{
title: 'Goodnight Saigon',
artist: 'Billy Joel',
release: 1983,
year: '1999',
rank: '14',
count: 748
},
{
title: 'Dancing Queen',
artist: 'ABBA',
release: 1976,
year: '1999',
rank: '16',
count: 1111
},
{
title: 'Another Brick In The Wall',
artist: 'Pink Floyd',
release: 1979,
year: '1999',
rank: '17',
count: 1266
},
{
title: 'Sunday Bloody Sunday',
artist: 'U2',
release: 1985,
year: '1999',
rank: '18',
count: 1087
},
{
title: 'Tears In Heaven',
artist: 'Eric Clapton',
release: 1992,
year: '1999',
rank: '21',
count: 435
},
{
title: 'Old And Wise',
artist: 'The Alan Parsons Project',
release: 1982,
year: '1999',
rank: '24',
count: 945
},
{
title: 'Losing My Religion',
artist: 'R.E.M.',
release: 1991,
year: '1999',
rank: '25',
count: 415
},
{
title: 'School',
artist: 'Supertramp',
release: 1974,
year: '1999',
rank: '26',
count: 1011
},
{
title: 'Who Wants To Live Forever',
artist: 'Queen',
release: 1986,
year: '1999',
rank: '30',
count: 836
},
{
title: 'Everybody Hurts',
artist: 'R.E.M.',
release: 1993,
year: '1999',
rank: '31',
count: 301
},
{
title: 'Over De Muur',
artist: 'Klein Orkest',
release: 1984,
year: '1999',
rank: '32',
count: 1166
},
{
title: 'Paint It Black',
artist: 'The Rolling Stones',
release: 1966,
year: '1999',
rank: '33',
count: 1077
},
{
title: 'The Winner Takes It All',
artist: 'ABBA',
release: 1980,
year: '1999',
rank: '35',
count: 926
},
{
title: 'Candle In The Wind (1997)',
artist: 'Elton John',
release: 1997,
year: '1999',
rank: '37',
count: 451
},
{
title: 'My Heart Will Go On',
artist: 'Celine Dion',
release: 1998,
year: '1999',
rank: '41',
count: 415
},
{
title: 'The River',
artist: 'Bruce Springsteen',
release: 1981,
year: '1999',
rank: '48',
count: 723
},
{
title: 'With Or Without You',
artist: 'U2',
release: 1987,
year: '1999',
rank: '51',
count: 816
},
{
title: 'Space Oddity',
artist: 'David Bowie',
release: 1969,
year: '1999',
rank: '59',
count: 1344
},
{
title: 'Stil In Mij',
artist: 'Van Dik Hout',
release: 1994,
year: '1999',
rank: '65',
count: 373
},
{
title: 'Nothing Compares 2 U',
artist: "Sinead O'Connor",
release: 1990,
year: '1999',
rank: '90',
count: 426
},
{
title: 'Wonderful Tonight',
artist: 'Eric Clapton',
release: 1988,
year: '1999',
rank: '91',
count: 515
},
{
title: 'Blowing In The Wind',
artist: 'Bob Dylan',
release: 1963,
year: '1999',
rank: '94',
count: 323
},
{
title: 'Eternal Flame',
artist: 'Bangles',
release: 1989,
year: '1999',
rank: '96',
count: 495
},
{
title: 'Non Je Ne Regrette Rien',
artist: 'Edith Piaf',
release: 1961,
year: '1999',
rank: '106',
count: 178
},
{
title: 'Con Te Partiro',
artist: 'Andrea Bocelli',
release: 1996,
year: '1999',
rank: '109',
count: 362
},
{
title: 'Conquest Of Paradise',
artist: 'Vangelis',
release: 1995,
year: '1999',
rank: '157',
count: 315
},
{
title: 'White Christmas',
artist: 'Bing Crosby',
release: 1954,
year: '1999',
rank: '218',
count: 10
},
{
title: "(We're gonna) Rock Around The Clock",
artist: 'Bill Haley & The Comets',
release: 1955,
year: '1999',
rank: '239',
count: 19
},
{
title: 'Jailhouse Rock',
artist: 'Elvis Presley',
release: 1957,
year: '1999',
rank: '247',
count: 188
},
{
title: 'Take Five',
artist: 'Dave Brubeck',
release: 1962,
year: '1999',
rank: '279',
count: 204
},
{
title: "It's Now Or Never",
artist: 'Elvis Presley',
release: 1960,
year: '1999',
rank: '285',
count: 221
},
{
title: 'Heartbreak Hotel',
artist: 'Elvis Presley',
release: 1956,
year: '1999',
rank: '558',
count: 109
},
{
title: 'One Night',
artist: 'Elvis Presley',
release: 1959,
year: '1999',
rank: '622',
count: 71
},
{
title: 'Johnny B. Goode',
artist: 'Chuck Berry',
release: 1958,
year: '1999',
rank: '714',
count: 89
},
{
title: 'Unforgettable',
artist: "Nat 'King' Cole",
release: 1951,
year: '1999',
rank: '1188',
count: 20
},
{
title: 'La Mer',
artist: 'Charles Trenet',
release: 1952,
year: '1999',
rank: '1249',
count: 24
},
{
title: 'The Road Ahead',
artist: 'City To City',
release: 1999,
year: '1999',
rank: '1999',
count: 262
},
{
title: 'What It Is',
artist: 'Mark Knopfler',
release: 2000,
year: '2000',
rank: '545',
count: 291
},
{
title: 'Overcome',
artist: 'Live',
release: 2001,
year: '2001',
rank: '879',
count: 111
},
{
title: 'Mooie Dag',
artist: 'Blof',
release: 2002,
year: '2003',
rank: '147',
count: 256
},
{
title: 'Clocks',
artist: 'Coldplay',
release: 2003,
year: '2003',
rank: '733',
count: 169
},
{
title: 'Sunrise',
artist: 'Norah Jones',
release: 2004,
year: '2004',
rank: '405',
count: 256
},
{
title: 'Nine Million Bicycles',
artist: 'Katie Melua',
release: 2005,
year: '2005',
rank: '23',
count: 250
},
{
title: 'Rood',
artist: 'Marco Borsato',
release: 2006,
year: '2006',
rank: '17',
count: 159
},
{
title: 'If You Were A Sailboat',
artist: 'Katie Melua',
release: 2007,
year: '2007',
rank: '101',
count: 256
},
{
title: 'Viva La Vida',
artist: 'Coldplay',
release: 2009,
year: '2009',
rank: '11',
count: 228
},
{
title: 'Dochters',
artist: 'Marco Borsato',
release: 2008,
year: '2009',
rank: '25',
count: 268
},
{
title: 'Need You Now',
artist: 'Lady Antebellum',
release: 2010,
year: '2010',
rank: '210',
count: 121
},
{
title: 'Someone Like You',
artist: 'Adele',
release: 2011,
year: '2011',
rank: '6',
count: 187
},
{
title: 'I Follow Rivers',
artist: 'Triggerfinger',
release: 2012,
year: '2012',
rank: '79',
count: 167
},
{
title: 'Get Lucky',
artist: 'Daft Punk',
release: 2013,
year: '2013',
rank: '357',
count: 141
},
{
title: 'Home',
artist: 'Dotan',
release: 2014,
year: '2014',
rank: '82',
count: 76
},
{
title: 'Hello',
artist: 'Adele',
release: 2015,
year: '2015',
rank: '23',
count: 29
}
]

`src\pages\f3\index.tsx`
```tsx
import React, { useCallback, useRef } from 'react'
import { View, Text, Button, Image } from '@tarojs/components'
import F2Canvas from 'taro-f2-react'
import { Axis, Chart, Interval, Line, Point, ScrollBar } from '@antv/f2'
import './index.less'

import { data2 } from '../mock/data2'
import { useTouchHandlers, Point as PointType } from './useTouchHandlers'

type ScrollBarType = typeof ScrollBar

const Index = () => {
  const scrollBarRef = useRef<any>()

  const handleSwipeLeft = (point: PointType) => {
    const { state } = scrollBarRef?.current || {}
    const { range } = state || {}
    console.log('左划', point)
    console.log(scrollBarRef?.current)
    console.log(range) //例如 [0.4,0.6] 表示当前的区间。即ScrollBar组件的range当前值
  }
  const handleSwipeRight = (point: PointType) => {
    const { state } = scrollBarRef?.current || {}
    const { range } = state || {}
    console.log('右划', point)
    console.log(scrollBarRef?.current)
    console.log(range) //例如 [0.4,0.6] 表示当前的区间。即ScrollBar组件的range当前值
  }
  const { handleTouchStart, handleTouchEnd } = useTouchHandlers(handleSwipeLeft, handleSwipeRight)

  return (
    <View
      style={{ width: '100%', height: '260px' }}
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
    >
      <F2Canvas>
        <Chart data={data2}>
          <Axis field="release" tickCount={5} nice={false} />
          <Axis field="count" />
          <Line x="release" y="count" />
          <Point x="release" y="count" />
          <ScrollBar ref={scrollBarRef} mode="x" range={[0.1, 0.3]} />
        </Chart>
      </F2Canvas>

      <Text>{}</Text>
    </View>
  )
}

export default Index

src\pages\f3\useTouchHandlers.ts


import { useRef } from 'react'

export type Point={ x:number y:number }

interface TouchHandlers { handleTouchStart: (e) => void handleTouchEnd: (e) => void }

export const useTouchHandlers = ( handleSwipeLeft?: (point:Point) => void, handleSwipeRight?: (point:Point) => void ): TouchHandlers => { const startXRef = useRef<number | null>(null) const startYRef = useRef<number | null>(null)

const handleTouchStart = (e) => { const touch = e.changedTouches[0] startXRef.current = touch.clientX startYRef.current = touch.clientY }

const handleTouchEnd = (e) => { const touch = e.changedTouches[0] const endX = touch.clientX const endY = touch.clientY

if (startXRef.current === null || startYRef.current === null) return

const diffX = endX - startXRef.current
const diffY = endY - startYRef.current

const point={
  x:endX,
  y:endY
}

if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 30) {
  // Horizontal swipe detected
  if (diffX < 0) {
    // Left swipe
    handleSwipeLeft && handleSwipeLeft(point)
  } else {
    // Right swipe
    handleSwipeRight && handleSwipeRight(point)
  }
}

startXRef.current = null
startYRef.current = null

}

return { handleTouchStart, handleTouchEnd } }

WangShuXian6 commented 5 months ago

小程序分包

分包根目录

配置路径必须包含index.tsx文件即 index src/pages/healthRecords/index.tsx


import { useEffect, useRef } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View, Button } from '@tarojs/components'
import styles from './index.module.scss'

const Index = () => { return index }

export default Index


>`src/pages/healthRecords/home/index.tsx`
>配置路径必须包含`index.tsx`文件即 `home/index`
```tsx
import { useEffect, useRef } from 'react'
import Taro, { useRouter } from '@tarojs/taro'
import { View, Button } from '@tarojs/components'
import styles from './index.module.scss'

const Home = () => {
  return <View>home</View>
}

export default Home

配置 app.config.ts

src/app.config.ts

export default defineAppConfig({
  pages: [
    'pages/index/index', //首页
  ],
  // lazyCodeLoading: 'requiredComponents',
  subPackages: [

    {
      root: "pages/healthRecords",
      name: "healthRecords",
      pages: ["index","home/index"]
    }
  ],
  window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#fff',
    navigationBarTitleText: '122',
    navigationBarTextStyle: 'black',
    // navigationStyle: 'custom',
  },
  // 获取坐标控制
  requiredPrivateInfos: [
    'getLocation',
    'onLocationChange',
    'startLocationUpdate',
    'chooseLocation',
    'chooseAddress',
  ],
  // 需要跳转的小程序appid
  navigateToMiniProgramAppIdList: ['wx8735a8a39cf58b5e', 'wx2e4b495f6111764c'],
  permission: {
    'scope.userLocation': {
      desc: '你的位置信息将用于小程序位置接口的效果展示',
    },
  },
  tabBar: {
    custom: true,
    color: '#999999',
    selectedColor: '#6e183e',
    backgroundColor: '#f8f8f8',
    list: [
      {
        pagePath: 'pages/index/index',
        text: '首页',
      },
      {
        pagePath: 'pages/personalCenterTab/index',
        text: '个人中心',
      },
    ],
  },
});
WangShuXian6 commented 4 months ago

state 与 ref 的监测测试

测试 hooks 中的 模拟请求

node v22.4.0

测试无论嵌套多少hooks,组件,都可以监测 useState 值变更,无法监测useRef值变更

package.json


{
"name": "taro-test",
"version": "1.0.0",
"private": true,
"description": "test",
"templateInfo": {
"name": "taro-hooks@2x",
"typescript": true,
"css": "Less",
"framework": "React"
},
"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",
"build:qq": "taro build --type qq",
"build:jd": "taro build --type jd",
"build:quickapp": "taro build --type quickapp",
"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",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch",
"dev:quickapp": "npm run build:quickapp -- --watch"
},
"browserslist": [
"last 3 versions",
"Android >= 4.1",
"ios >= 8"
],
"author": "",
"dependencies": {
"@antv/f2": "^5.5.1",
"@babel/runtime": "^7.7.7",
"@taro-hooks/plugin-react": "2",
"@taro-hooks/shared": "2",
"@tarojs/components": "3.6.32",
"@tarojs/helper": "3.6.32",
"@tarojs/plugin-framework-react": "3.6.32",
"@tarojs/plugin-platform-alipay": "3.6.32",
"@tarojs/plugin-platform-h5": "3.6.32",
"@tarojs/plugin-platform-jd": "3.6.32",
"@tarojs/plugin-platform-qq": "3.6.32",
"@tarojs/plugin-platform-swan": "3.6.32",
"@tarojs/plugin-platform-tt": "3.6.32",
"@tarojs/plugin-platform-weapp": "3.6.32",
"@tarojs/react": "3.6.32",
"@tarojs/runtime": "3.6.32",
"@tarojs/shared": "3.6.32",
"@tarojs/taro": "3.6.32",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"taro-f2-react": "^1.2.0",
"taro-hooks": "2",
"taro-react-echarts": "^1.2.2"
},
"devDependencies": {
"@babel/core": "^7.8.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@tarojs/cli": "3.6.32",
"@tarojs/taro-loader": "3.6.32",
"@tarojs/webpack5-runner": "3.6.32",
"@types/node": "^18.15.11",
"@types/react": "^18.0.0",
"@types/webpack-env": "^1.13.6",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
"babel-plugin-import": "^1.13.3",
"babel-preset-taro": "3.6.32",
"eslint": "^8.12.0",
"eslint-config-taro": "3.6.32",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-react": "^7.8.2",
"eslint-plugin-react-hooks": "^4.2.0",
"postcss": "^8.4.18",
"react-refresh": "^0.11.0",
"stylelint": "9.3.0",
"ts-node": "^10.9.1",
"typescript": "^4.1.0",
"webpack": "^5.78.0"
},
"engines": {
"node": ">=12.0.0"
}
}

>`src/pages/query/components/TestQueryEffect.tsx`
```tsx
import React, { useEffect } from "react";
import useQuery from "../utils/useQuery";
import { mockApiCall } from "../utils/mock";
import { View, Text, Button, Image } from "@tarojs/components";
import { useEnv, useNavigationBar, useModal, useToast } from "taro-hooks";

const TestSingQuery = () => {
  const { data, error, isLoading, exec } = useQuery<string, { userId: number }>(
    mockApiCall,
    { userId: 123 }
  );

  useEffect(() => {
    console.log(`监测 TestSingQuery data 更新:${data}`);
  }, [data]);

  return (
    <View>
      {isLoading && <Text>Loading...</Text>}
      {error && <Text>Error: {error.message}</Text>}
      {data && <Text>Data: {data}</Text>}
      <Button onClick={() => exec({ userId: 456 })}>
        Refetch with new params
      </Button>
    </View>
  );
};

export default TestSingQuery;

src/pages/query/hooks/useTestHookQuery.ts


//import useQuery from "../utils/useQuery";
import { mockApiCall } from "../utils/mock";
import { useEffect, useRef, useState } from "react";

const useTestHookQuery = (params: unknown, lazy = false) => { const [data, setData] = useState(); const dataRef = useRef(); const [error, setError] = useState(); const [isLoading, setIsLoading] = useState(false);

const exec = async (newParams: unknown) => { setIsLoading(true); mockApiCall(newParams || params) .then((response) => { console.log(响应 response:); console.table(response); setData(response); dataRef.current = response; }) .catch((error) => { setError(error as Error); }) .finally(() => { setIsLoading(false); }); };

useEffect(() => { if (lazy) return; exec(params); }, []);

useEffect(() => { // useState的值 可以监测变更 console.log(监测 useTestHookQuery data:-data${data}}); console.log(监测 useTestHookQuery data:-dataRef:${dataRef.current}}); }, [data]);

useEffect(() => { // useRef的值 无法监测变更 console.log(监测 useTestHookQuery dataRef:-data${data}}); console.log(监测 useTestHookQuery dataRef:-dataRef:${dataRef.current}}); }, [dataRef]);

return { data, error, isLoading, exec, }; };

export default useTestHookQuery;


>`src/pages/query/utils/mock.ts`
```tsx
// 模拟请求函数
export const mockApiCall = (params: unknown): Promise<string> => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve(`Received params: ${JSON.stringify(params)}`);
      }, 2000);
    });
  }

src/pages/query/utils/useQuery.ts


import { useState, useEffect } from 'react'

export interface QueryResult<T, P> { data: T | null error: Error | null isLoading: boolean exec: (p: P) => void }

// 通用请求 hooks const useQuery = <T, P>(queryFn: (params: P) => Promise, params: P): QueryResult<T, P> => { const [data, setData] = useState<T | null>(null) const [error, setError] = useState<Error | null>(null) const [isLoading, setIsLoading] = useState(false)

const exec = async (newParams: P) => { setIsLoading(true) // try { // const response = await queryFn(newParams || params) // console.log(response--${JSON.stringify(response)}) // setData(response) // } catch (error) { // setError(error as Error) // } finally { // setIsLoading(false) // }

queryFn(newParams || params).then((response)=>{
  console.log(`queryFn response-${JSON.stringify(response)}`)
  setData(response)
}).catch((error)=>{
  setError(error as Error)
}).finally(()=>{
  setIsLoading(false)
})

}

useEffect(() => { console.log('queryFn 123') exec(params) }, [])

useEffect(() => { console.log(' queryFn 111111111data:', data) }, [data])

return { data, error, isLoading, exec } }

export default useQuery


>`src/pages/query/index.tsx`
```tsx
import React, { useCallback, useEffect } from "react";
import { View, Text, Button, Image } from "@tarojs/components";
import { useEnv, useNavigationBar, useModal, useToast } from "taro-hooks";
import TestSingQuery from "./components/TestQueryEffect";
import useTestHookQuery from "./hooks/useTestHookQuery";
import "./index.less";

const Index = () => {
  const { data, exec } = useTestHookQuery({ a: 2222 }, true);
  useEffect(() => {
    exec({ a: 333 });
  }, []);

  return (
    <View className="wrapper">
      {/* <TestSingQuery /> */}
      {data}
    </View>
  );
};

export default Index;

image

WangShuXian6 commented 4 months ago

Taro Issues

不支持的语法

未严格测试


if (isLoading) return <View>加载中...</View>
if (error) return <View>错误: {error.message}</View>

if (data?.result === ResultStatus.Failure) { return 错误: {data.msg} }


会报错:
```lua
.._src_runtime_connect.ts:373 React 出现报错,请打开编译配置 mini.debugReact 查看报错详情:https://docs.taro.zone/docs/config-detail#minidebugreact
onError @ .._src_runtime_connect.ts:373
waitAppWrapper @ .._src_runtime_connect.ts:192
value @ .._src_runtime_connect.ts:377
getDerivedStateFromError @ .._src_runtime_connect.ts:118
Df.c.payload @ vendors.js? [sm]:18494
ud @ vendors.js? [sm]:18444
gg @ vendors.js? [sm]:18519
Sh @ vendors.js? [sm]:18593
Rh @ vendors.js? [sm]:18581
Qh @ vendors.js? [sm]:18581
Gh @ vendors.js? [sm]:18581
Lh @ vendors.js? [sm]:18572
Eh @ vendors.js? [sm]:18570
workLoop @ vendors.js? [sm]:20134
flushWork @ vendors.js? [sm]:20107
performWorkUntilDeadline @ vendors.js? [sm]:20401
setTimeout (async)
schedulePerformWorkUntilDeadline @ vendors.js? [sm]:20447
performWorkUntilDeadline @ vendors.js? [sm]:20406
setTimeout (async)
schedulePerformWorkUntilDeadline @ vendors.js? [sm]:20447
requestHostCallback @ vendors.js? [sm]:20456
unstable_scheduleCallback @ vendors.js? [sm]:20309
Dh @ vendors.js? [sm]:18598
Z @ vendors.js? [sm]:18569
Ad @ vendors.js? [sm]:18567
df @ vendors.js? [sm]:18485
(anonymous) @ ._src_hooks_useQuery.ts:30
Promise.then (async)
_callee$ @ ._src_hooks_useQuery.ts:28
tryCatch @ vendors.js? [sm]:21565
(anonymous) @ vendors.js? [sm]:21653
(anonymous) @ vendors.js? [sm]:21594
asyncGeneratorStep @ vendors.js? [sm]:20971
_next @ vendors.js? [sm]:20985
(anonymous) @ vendors.js? [sm]:20990
(anonymous) @ vendors.js? [sm]:20982
exec @ ._src_hooks_useQuery.ts:16
(anonymous) @ ._src_hooks_useQuery.ts:40
Gg @ vendors.js? [sm]:18540
Fh @ vendors.js? [sm]:18587
(anonymous) @ vendors.js? [sm]:18583
workLoop @ vendors.js? [sm]:20134
flushWork @ vendors.js? [sm]:20107
performWorkUntilDeadline @ vendors.js? [sm]:20401
setTimeout (async)
schedulePerformWorkUntilDeadline @ vendors.js? [sm]:20447
performWorkUntilDeadline @ vendors.js? [sm]:20406
setTimeout (async)
schedulePerformWorkUntilDeadline @ vendors.js? [sm]:20447
requestHostCallback @ vendors.js? [sm]:20456
unstable_scheduleCallback @ vendors.js? [sm]:20309
Dh @ vendors.js? [sm]:18598
Z @ vendors.js? [sm]:18569
Ad @ vendors.js? [sm]:18567
enqueueForceUpdate @ vendors.js? [sm]:18448
E.forceUpdate @ vendors.js? [sm]:19842
mount @ .._src_runtime_connect.ts:226
mount @ .._src_runtime_connect.ts:271
(anonymous) @ .._src_dsl_common.ts:300
vendors.js? [sm]:18493 Error: Minified React error #300; visit https://reactjs.org/docs/error-decoder.html?invariant=300 for the full message or use the non-minified dev environment for full errors and additional helpful warnings.
    at Ke (vendors.js? [sm]:18473)
    at cg (vendors.js? [sm]:18515)
    at Sh (vendors.js? [sm]:18593)
    at Rh (vendors.js? [sm]:18581)
    at Qh (vendors.js? [sm]:18581)
    at Gh (vendors.js? [sm]:18581)
    at Lh (vendors.js? [sm]:18572)
    at Eh (vendors.js? [sm]:18570)
    at workLoop (vendors.js? [sm]:20134)
    at flushWork (vendors.js? [sm]:20107)(env: macOS,mp,1.06.2405020; lib: 2.25.3)

image

作为值使用的枚举类型,需要使用完整路径导入

import { Blood } from '@/pages/healthRecords/utils/types/bloodPressure'

错误 虽然 index 中也引入了枚举 export * from './bloodPressure'

import { Blood } from '@/pages/healthRecords/utils/types/index

process 无法再运行时解析[需要进一步探明原因和解决方案]

因为这是编译时替换,运行时已经不存在process对象, 且webpack5不在前端环境中保留process对象,仅在node环境中存在。

可用

const appid=process.env.TARO_APP_VPID

不可用 会报错 process 未定义

const vpid: string = bMockMode
? process.env.TARO_APP_VPID
: vpidInUrl || visitorInfo.VISIT_PATIENT_ID

webpack 不建议在前端项目中使用 process

https://github.com/NervJS/taro/issues/12764

无法使用枚举的值

新定义的枚举值需要重新运行编译命令npm run dev:weapp 疑似taro将枚举静态替换

export enum UploadNumberType {
  Camera = '1',
  Album = '2'
}

const a=UploadNumberType.Camera //重新编译后才正常,否则 报错 UploadNumberType 未定义

Ref 类型的值不能作为属性传递

因为不会触发重新渲染,也不会触发更新

WangShuXian6 commented 4 months ago

自定义颜色的彩色日志

基础

const a='123'
console.log(`%c ${a}`,'background-color: #25cbe9;color:white; ')
console.info(`%c ${a}`,'background-color: #25cbe9;color:white; ')

image

增强

注意 在 taro中 process.env.NODE_ENV 在编译时直接静态替换 为 .env 指定的值,所以每次使用 process.env.NODE_ENV 后需要重新执行 npm run dev:weapp 等编译命令使其生效,否则该变量将会未定义导致报错或在所有环境该值为假。

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

    const timestamp = getCurrentTimestamp()
    const { style } = logLevels[level]

    // Format message depending on its type
    let formattedMessage: string
    if (typeof message === 'string') {
      formattedMessage = message
    } else {
      formattedMessage = JSON.stringify(message, null, 2)
    }

    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(!isProduction);

// 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"));

image

使用日志

引入并初始化

import { createLogger } from '@/utils/common'
const logger = createLogger()

非生产环境默认启用日志

生产环境默认关闭日志

可手动指定是否显示日志

const logger = createLogger(false)//所有环境都关闭日志

使用

// 示例使用
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"));
WangShuXian6 commented 4 months ago

Taro Typescript 类型

路由

路由参数

路由中 xxxx/index?vpid=123 包含 vpid 参数


export type RouterParams = {
vpid: string
}

const AnswerCatalogPage = () => { const { vpid: vpidInUrl } = useRouter().params //新版taro已支持泛型参数 const { vpid: vpidInUrl } = useRouter().params as unknown as RouterParams//旧版taro hack写法,不需要再使用 ...... }

WangShuXian6 commented 4 months ago

请求封装

useQuery

类型

src\utils\query\type.ts


// QueryOptions 接口支持泛型
export interface QueryOptions<
UrlParams extends object| undefined = undefined,
RequestBody extends object | undefined = undefined
{
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
params?: UrlParams
data?: RequestBody
headers?: Record<string, string>
debounce?: number
autoFetch?: boolean
}

// QueryResult 接口支持泛型 export interface QueryResult< ResponseBody, UrlParams extends object| undefined = undefined, RequestBody extends object | undefined = undefined

{ data: ResponseBody | null error: Error | null statusCode: number | null isLoading: boolean refetch: (options?: Partial<QueryOptions<UrlParams, RequestBody>>) => Promise }

// ApiResponseConfig 接口 export interface ApiResponseConfig { isSuccessful: (data: ResponseBody) => boolean getErrorMessage: (statusCode: number, data: ResponseBody) => string }

// 定义 ApiConfig 接口 export interface ApiConfig { baseUrl: string headers: Record<string, string> timeout?: number withCredentials?: boolean }

export interface GetConfig<UrlParams extends object| undefined = undefined> { url: string urlParams?: UrlParams }


### 配置

#### 默认配置
>`src\utils\query\apiConfig.ts`
```ts
import { ApiConfig, ApiResponseConfig, GetConfig } from './type'

// 默认 API 配置
const defaultConfig: ApiConfig = {
  baseUrl: process.env.TARO_APP_API || '',//https://ax
  headers: {
    'Content-Type': 'application/json',
    Authorization: 'Bearer yourToken'
  },
  timeout: 5000,
  withCredentials: false
}

// 动态获取 API 配置
export const getApiConfig = <UrlParams extends object>(config: GetConfig<UrlParams>): ApiConfig => {
  // 这里可以添加逻辑以动态调整配置,例如基于环境变量或用户会话信息
  return {
    ...defaultConfig,
    baseUrl: config.url || defaultConfig.baseUrl
  }
}

// 默认的业务规则配置对象
export const defaultApiResponseConfig: ApiResponseConfig<unknown> = {
  isSuccessful: (data) => true,
  getErrorMessage: (statusCode, data) => `Failed with status code ${statusCode}`
}

业务配置

src\utils\query\apiConfigFW.ts


import { encodeToLower, generateSignedUrl } from '../str'
import { getDevMode, getStore } from '../utils'
import Taro from '@tarojs/taro'
import { ApiConfig, ApiResponseConfig, GetConfig } from './type'

//会修改url export function getFWRequestConfig<UrlParams extends object | undefined = undefined>( config: GetConfig ): ApiConfig { let { url, urlParams = {} } = config const access_token = Taro.getStorageSync('access_token')

// 根据请求的不同设置特定的请求头和参数 const headers: Record<string, string> = { 'Content-Type': 'application/json', 'FW-Account-Id': getStore('AccountId'), 'fw-device-platform': process.env.TARO_APP_FW_DEVICE_PLATFORM || 'web', 'fw-push-token': getStore('openId'), 'fw-dev-mode': getDevMode() || '' }

if (url.includes('MiniProgram') || url.includes('GetAccessToken')) { headers['content-type'] = 'application/x-www-form-urlencoded'

const timestamp = new Date().getTime()
let sign
let queryString = ''

if (url.includes('GetAccessToken')) {
  sign = generateSignedUrl({ ...urlParams, timestamp })
} else {
  sign = generateSignedUrl({ ...urlParams, access_token, timestamp })
  queryString = Object.keys(urlParams)
    .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(urlParams[key])}`)
    .join('&')
}

url += `?${queryString}&timestamp=${timestamp}&sign=${sign}`
if (access_token) {
  url += `&access_token=${encodeToLower(access_token)}`
}

}

// 构造请求配置对象 const requestConfig: ApiConfig = { headers, baseUrl: process.env.TARO_APP_API || '', //'https://apxlt', //process.env.TARO_APP_API timeout: 50000, withCredentials: true }

return requestConfig }

// 默认的业务规则配置对象 export const defaultApiResponseConfigFW: ApiResponseConfig = { isSuccessful: (data) => data.result === 1, getErrorMessage: (statusCode, data) => data.msg || Failed with status code ${statusCode} }


### useQuery
>`src\utils\query\useQuery.ts`
```ts
import { useEffect, useState, useRef, useCallback } from 'react'
import Taro from '@tarojs/taro'
import { defaultApiResponseConfigFW, getFWRequestConfig } from './apiConfigFW'
import { ApiConfig, ApiResponseConfig, GetConfig, QueryOptions, QueryResult } from './type'
import { defaultApiResponseConfig, getApiConfig } from './apiConfig'
import { createLogger } from '@/utils/common'
const logger = createLogger()

// Function to convert object to URL query parameters string
const toUrlParams = (urlParams: Record<string, any>): string => {
  const queryString = Object.keys(urlParams)
    .map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(urlParams[key]))
    .join('&')
  return queryString ? `?${queryString}` : ''
}

// useQuery 钩子支持泛型
export function useQuery<
  ResponseBody,
  UrlParams extends object | undefined = undefined,
  RequestBody extends object | undefined = undefined
>(
  endpoint: string,
  initialOptions?: QueryOptions<UrlParams, RequestBody>,
  apiConfigInput: ApiConfig | ((config: GetConfig<UrlParams>) => ApiConfig) = getFWRequestConfig,
  responseConfig: ApiResponseConfig<ResponseBody> = defaultApiResponseConfigFW
): QueryResult<ResponseBody, UrlParams, RequestBody> {
  const apiConfig =
    typeof apiConfigInput === 'function'
      ? apiConfigInput({ url: endpoint, urlParams: initialOptions?.params })
      : apiConfigInput
  const [data, setData] = useState<ResponseBody | null>(null)
  const [error, setError] = useState<Error | null>(null)
  const [statusCode, setStatusCode] = useState<number | null>(null)
  const [isLoading, setLoading] = useState<boolean>(false)
  const lastCall = useRef<{ timestamp: number; key: string }>({ timestamp: 0, key: '' })

  const fetchData = useCallback(
    async (options: QueryOptions<UrlParams, RequestBody>): Promise<ResponseBody> => {
      const now = Date.now()
      const debounce = options.debounce || 300 // 默认防抖时间为 300 毫秒
      const requestKey = `${endpoint}-${JSON.stringify(options.params || {})}-${JSON.stringify(
        options.data || {}
      )}`

      if (now - lastCall.current.timestamp < debounce && lastCall.current.key === requestKey) {
        return Promise.reject(new Error('Too many requests in a short time'))
      }

      lastCall.current = { timestamp: now, key: requestKey }
      setLoading(true)
      setError(null)

      const { method = 'GET', headers, params, data } = options
      const requestHeaders = { ...apiConfig.headers, ...headers }
      const urlParams = method === 'GET' && params ? toUrlParams(params) : ''
      const fullUrl = endpoint.startsWith('http')
        ? endpoint + urlParams
        : `${apiConfig.baseUrl}${endpoint}${urlParams}`
      const requestBody = method !== 'GET' ? data : undefined

      try {
        const response = await Taro.request<ResponseBody>({
          url: fullUrl,
          method,
          data: requestBody,
          header: requestHeaders,
          timeout: apiConfig.timeout,
          credentials: apiConfig.withCredentials ? 'include' : 'omit'
        })

        setStatusCode(response.statusCode)
        if (
          response.statusCode >= 200 &&
          response.statusCode < 300 &&
          responseConfig.isSuccessful(response.data)
        ) {
          setData(response.data)
          setError(null)
          setLoading(false)
          return response.data
        } else {
          const msg = responseConfig.getErrorMessage(response.statusCode, response.data)
          const newError = new Error(msg)
          setError(newError)
          setData(null)
          setLoading(false)
          return Promise.reject(newError)
        }
      } catch (err) {
        setError(err as Error)
        setData(null)
        setLoading(false)
        return Promise.reject(err)
      }
    },
    [apiConfig, endpoint, responseConfig]
  )

  useEffect(() => {
    if (initialOptions?.autoFetch) {
      fetchData(initialOptions).catch(console.error)
    }
    return () => {}
  }, [initialOptions?.autoFetch])

  const refetch = useCallback(
    async (options: Partial<QueryOptions<UrlParams, RequestBody>> = {}) => {
      const {
        method,
        params = {},
        data = {},
        headers = {},
        debounce,
        autoFetch
      } = initialOptions || {}

      const {
        method: newMethod,
        params: newParams = {},
        data: newData = {},
        headers: newHeaders = {},
        debounce: newDebounce,
        autoFetch: newAutoFetch
      } = options || {}

      const mergedParams: UrlParams = { ...params, ...newParams } as UrlParams
      const mergedData = { ...data, ...newData } as RequestBody

      const mergeOptions: QueryOptions<UrlParams, RequestBody> = {
        method: newMethod || method,
        params: mergedParams,
        data: mergedData,
        headers: { ...headers, ...newHeaders },
        debounce: newDebounce !== undefined ? newDebounce : debounce,
        autoFetch: newAutoFetch !== undefined ? newAutoFetch : autoFetch
      }
      // logger.info('options:', options)
      // logger.info('initialOptions:', initialOptions)
      // logger.info('mergedParams:', mergedParams)
      // logger.info('mergedData:', mergedData)
      // logger.info('mergeOptions:', mergeOptions)
      return fetchData(mergeOptions)
    },
    [fetchData, initialOptions]
  )

  return { data, error, statusCode, isLoading, refetch }
}

useGet

src\utils\query\useGet.ts

import { QueryResult } from './type' import { useQuery } from './useQuery'

export const useGet = <ResponseBody, UrlParams extends object>( endpoint: string, params?: UrlParams, headers?: Record<string, string>, debounce?: number, autoFetch: boolean = false ): QueryResult<ResponseBody, UrlParams, undefined> => { return useQuery<ResponseBody, UrlParams, undefined>(endpoint, { method: 'GET', params, headers, debounce, autoFetch }) }


### usePost
>`src\utils\query\usePost.ts`
```ts
import { QueryResult } from './type'
import { useQuery } from './useQuery'

export const usePost = <ResponseBody, RequestBody extends object>(
  endpoint: string,
  data?: RequestBody,
  headers?: Record<string, string>,
  debounce?: number,
  autoFetch: boolean = false
): QueryResult<ResponseBody, undefined, RequestBody> => {
  return useQuery<ResponseBody, undefined, RequestBody>(endpoint, {
    method: 'POST',
    data,
    headers,
    debounce,
    autoFetch
  })
}

usePut

src\utils\query\usePut.ts

import { QueryResult } from './type' import { useQuery } from './useQuery'

export const usePut = <ResponseBody, RequestBody extends object | undefined = undefined>( endpoint: string, data?: RequestBody, headers?: Record<string, string>, debounce?: number, autoFetch: boolean = false ): QueryResult<ResponseBody, undefined, RequestBody> => { return useQuery<ResponseBody, undefined, RequestBody>(endpoint, { method: 'PUT', data, headers, debounce, autoFetch }) }


### useDelete
>`src\utils\query\useDelete.ts`
```ts

import { QueryResult } from './type'
import { useQuery } from './useQuery'

export const useDelete = <ResponseBody, UrlParams extends object>(
  endpoint: string,
  params?: UrlParams,
  headers?: Record<string, string>,
  debounce?: number,
  autoFetch: boolean = false
): QueryResult<ResponseBody, UrlParams, undefined> => {
  return useQuery<ResponseBody, UrlParams, undefined>(endpoint, {
    method: 'DELETE',
    params,
    headers,
    debounce,
    autoFetch
  })
}

示例

``


import Taro from '@tarojs/taro'
import { useState } from 'react'
import { useGet, usePost, usePut, useDelete } from '../' // 假设这些钩子定义在 'hooks' 文件中

// API 响应的数据类型 export interface MyDataType { id: number title: string content: string createdAt: string }

// API GET 请求的参数类型 interface MyParamsType { id: number }

// POST 请求的体结构 interface MyPostBodyType { title: string content: string }

// PUT 请求的体结构,假设与 POST 类似 interface MyPutBodyType { title: string content: string }

const MyComponent = () => { // 使用自定义钩子处理 API 请求 const { data: getData, error: getError, isLoading: getLoading, refetch: refetchGet } = useGet<MyDataType, MyParamsType>('/data', { id: 123 }, {}, 300, true)

const { data: postData, error: postError, isLoading: postLoading, refetch: refetchPost } = usePost<MyDataType, MyPostBodyType>('/data', { title: '标题', content: 'Hello World' })

const { data: putData, error: putError, isLoading: putLoading, refetch: refetchPut } = usePut<MyDataType, MyPutBodyType>('/data/123', { title: '标题', content: 'Updated Content' })

const { data: deleteData, error: deleteError, isLoading: deleteLoading, refetch: refetchDelete } = useDelete<MyDataType, MyParamsType>('/data/123')

return (

{getLoading ? ( Loading data... ) : getError ? ( Error loading data: {getError.message} ) : ( Data Loaded:
{JSON.stringify(getData, null, 2)}
)} {postLoading ? ( Sending data... ) : postError ? ( Error posting data: {postError.message} ) : ( Data Posted:
{JSON.stringify(postData, null, 2)}
)} {putLoading ? ( Updating data... ) : putError ? ( Error updating data: {putError.message} ) : ( Data Updated:
{JSON.stringify(putData, null, 2)}
)} {deleteLoading ? ( Deleting data... ) : deleteError ? ( Error deleting data: {deleteError.message} ) : ( Data Deleted:
{JSON.stringify(deleteData, null, 2)}
)}

) }

export default MyComponent


>``
```ts
import { View, Button, Input, Text } from '@tarojs/components'
import { useState } from 'react'
import { useQuery } from '../useQuery' // 假设这个路径是你的 useQuery 钩子路径

interface User {
  id: number
  name: string
}

const FetchUsersComponent = () => {
  const [search, setSearch] = useState('')
  const { data, error, isLoading, refetch } = useQuery<User[], { name?: string }>('/users', {
    autoFetch: false
  })

  const handleFetchClick = () => {
    refetch({ params: { name: search } }) // 根据当前输入框的内容发起搜索
  }

  return (
    <View>
      <Input onInput={(e) => setSearch(e.detail.value)} placeholder="输入用户名进行搜索" />
      <Button onClick={handleFetchClick}>加载用户</Button>
      {isLoading && <Text>加载中...</Text>}
      {error && <Text>错误:{error.message}</Text>}
      {data && data.map((user) => <View key={user.id}>{user.name}</View>)}
    </View>
  )
}

export default FetchUsersComponent
import React from 'react'
import Taro from '@tarojs/taro'
import { useState } from 'react'
import { useGet, usePost, usePut, useDelete } from '../'
import { Button, View, Text, Textarea, Input } from '@tarojs/components'

export interface MyDataType {
  id: number
  title: string
  content: string
  createdAt: string
}

// API GET 请求的参数类型
interface MyParamsType {
  id: number
}

// POST 请求的体结构
interface MyPostBodyType {
  title: string
  content: string
}

// PUT 请求的体结构,假设与 POST 类似
interface MyPutBodyType {
  title: string
  content: string
}

const MyComponent = () => {
  const [postInput, setPostInput] = useState({ title: '', content: '' })
  const [deleteId, setDeleteId] = useState<number | null>(null)

  const {
    data: getData,
    error: getError,
    isLoading: getLoading,
    refetch: refetchGet
  } = useGet<MyDataType, MyParamsType>('/data', { id: 123 }, {}, 300, true)

  const {
    data: postData,
    error: postError,
    isLoading: postLoading,
    refetch: refetchPost
  } = usePost<MyDataType, MyPostBodyType>('/data', {
    title: 'Hello World',
    content: 'This is a new post'
  })

  const {
    data: putData,
    error: putError,
    isLoading: putLoading,
    refetch: refetchPut
  } = usePut<MyDataType, MyPutBodyType>('/data/123', {
    title: 'Updated Title',
    content: 'Updated Content'
  })

  const {
    data: deleteData,
    error: deleteError,
    isLoading: deleteLoading,
    refetch: refetchDelete
  } = useDelete<MyDataType, MyParamsType>('/data/123')

  const handleInputChange = (field, value) => {
    setPostInput((Viewv) => ({ ...Viewv, [field]: value }))
  }

  const handleSubmit = () => {
    // Explicitly passing the latest data to refetchPost
    refetchPost({ data: postInput })
  }

  const handleDelete = () => {
    if (deleteId) {
      refetchDelete({ params: { id: deleteId } })
    }
  }

  return (
    <View>
      <View>
        <Text>Data Loaded:</Text>
        <View>{JSON.stringify(getData, null, 2)}</View>
        <Button onClick={() => refetchGet({ params: { id: 123 } })}>Refresh Data</Button>
      </View>
      <View>
        <Text>Data Posted:</Text>
        <View>{JSON.stringify(postData, null, 2)}</View>
      </View>

      <View>
        <Input
          onInput={(e) => handleInputChange('title', e.detail.value)}
          placeholder="Enter title"
        />
        <Textarea
          onInput={(e) => handleInputChange('content', e.detail.value)}
          placeholder="Enter content"
        />
        <Button onClick={handleSubmit}>Submit New Post</Button>
        <Text>Data Posted:</Text>
        <View>{JSON.stringify(postData, null, 2)}</View>
      </View>

      <View>
        <Button
          onClick={() =>
            refetchPut({
              data: { title: 'Updated Again', content: 'Further updated content' }
            })
          }
        >
          Update Data
        </Button>
      </View>
      <View>
        <Text>Data Deleted:</Text>
        <View>{JSON.stringify(deleteData, null, 2)}</View>
        <Button onClick={() => refetchDelete()}>Delete Data</Button>
      </View>

      <View>
        {/* 其他组件代码不变 */}
        <Input
          type="number"
          onInput={(e) => setDeleteId(Number(e.detail.value))}
          placeholder="Enter ID to delete"
        />
        <Button onClick={handleDelete}>Delete Data</Button>
        {deleteLoading ? (
          <Text>Deleting data...</Text>
        ) : deleteError ? (
          <Text>Error deleting data: {deleteError.message}</Text>
        ) : (
          <Text>Data Deleted: {JSON.stringify(deleteData, null, 2)}</Text>
        )}
      </View>
    </View>
  )
}

export default MyComponent
import { View, Text } from '@tarojs/components'
import { useQuery } from '../useQuery'

// 定义要获取的数据类型
interface UserData {
  id: number
  name: string
  email: string
}

// 定义请求参数类型
interface GetUserParams {
  userId: number
}

const UserComponent = () => {
  const params: GetUserParams = { userId: 1 }
  const { data, error, isLoading, refetch } = useQuery<UserData, GetUserParams>('/users', {
    method: 'GET',
    params
  })

  if (isLoading) {
    return <Text>Loading...</Text>
  }

  if (error) {
    return (
      <View>
        <Text>Error: {error.message}</Text>
        <Text onClick={() => refetch()}>Retry</Text>
      </View>
    )
  }

  return (
    <View>
      <Text>User Name: {data?.name}</Text>
      <Text>User Email: {data?.email}</Text>
    </View>
  )
}

export default UserComponent
import { View, Text } from '@tarojs/components'
import { useQuery } from '../useQuery'

const MyComponent = () => {
  const { refetch } = useQuery<{ id: number; name: string }>('/data')

  const handleRefetch = () => {
    refetch()
      .then((data) => {
        console.log('Fetched data:', data)
      })
      .catch((error) => {
        console.error('Error fetching data:', error)
      })
  }

  const handleRefetchTry = async () => {
    try {
      const result = await refetch()
      console.log('测试useQuery2 refetch 返回:', result)
    } catch (error) {
      console.log('测试useQuery2 refetch 异常:', error)
    }
  }

  return <button onClick={handleRefetch}>Refetch Data</button>
}
import { View, Text, Button } from '@tarojs/components';
import { useDelete } from '../useDelete';

const DeletePostComponent = () => {
  const { data, error, isLoading, refetch } = useDelete<{ deleted: boolean },Record<string,string>>('/posts/1');

  if (isLoading) return <Text>Deleting...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;
  return (
    <View>
      <Text>Post Deleted: {data?.deleted ? 'Yes' : 'No'}</Text>
      <Button onClick={() => refetch()}>Delete Again</Button>
    </View>
  );
};

export default DeletePostComponent;
import { View, Text } from '@tarojs/components';
import { useGet } from '../useGet';

interface UserData {
  id: number;
  name: string;
  email: string;
}

interface UserParams {
  userId: number;
}

const GetUserComponent = () => {
  const params: UserParams = { userId: 1 };
  const { data, error, isLoading } = useGet<UserData, UserParams>('/user', params);

  if (isLoading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;
  return <View><Text>Name: {data?.name}</Text><Text>Email: {data?.email}</Text></View>;
};
import { View, Text, Button } from '@tarojs/components';
import { usePost } from '../usePost';

interface PostData {
  title: string;
  content: string;
}

const PostComponent = () => {
  const postData: PostData = { title: "New Post", content: "Hello World" };
  const { data, error, isLoading, refetch } = usePost<{ success: boolean; id: number },PostData>('/posts', postData);

  if (isLoading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;
  return (
    <View>
      <Text>Post Created: {data?.success ? 'Yes' : 'No'}</Text>
      <Button onClick={() => refetch()}>Create Again</Button>
    </View>
  );
};

export default PostComponent;
import { View, Text, Button } from '@tarojs/components';
import { usePut } from '../usePut';

interface UpdateData {
  title: string;
}

const UpdatePostComponent = () => {
  const updateData: UpdateData = { title: "Updated Title" };
  const { data, error, isLoading, refetch } = usePut<{ updated: boolean },UpdateData>('/posts/1', updateData);

  if (isLoading) return <Text>Updating...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;
  return (
    <View>
      <Text>Post Updated: {data?.updated ? 'Yes' : 'No'}</Text>
      <Button onClick={() => refetch()}>Update Again</Button>
    </View>
  );
};

export default UpdatePostComponent;
WangShuXian6 commented 4 months ago

Toast

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

let toastIdCounter = 0; // 计数器用于生成唯一 ID

interface ToastProps {
  visible: boolean
  message: string
  duration: number
  icon?: string
}

const Toast = ({ visible, message, duration = 2000, icon }: ToastProps) => {
  const containerStyle = {
    position: 'fixed' as 'fixed',
    top: 0,
    left: 0,
    width: '100vw',
    height: '100vh',
    display: visible ? 'flex' : 'none',
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: visible ? 'rgba(0,0,0,0.3)' : 'rgba(0,0,0,0)',
    transition: 'all ease-in-out 0.1s'
  }

  const descStyle = {
    display: 'flex',
    justifyContent: 'center',
    alignItems: 'center',
    maxWidth: '80vw',
    minHeight: '50px',
    backgroundColor: 'rgba(0,0,0,0.8)',
    color: '#fff',
    padding: '10px 20px',
    borderRadius: '5px'
  }

  return (
    <RootPortal>
      <View style={containerStyle}>
        {icon && <Image style={{ marginRight: '10px' }} src={icon} />}
        <Text style={descStyle}>{message}</Text>
      </View>
    </RootPortal>
  )
}

export const TaroToast = ({ message, duration = 2000, icon = '' }) => {
  const id = `toast-${toastIdCounter++}`; // 生成唯一 ID
  const view = document.createElement('view');
  view.id = id;

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

  render(<Toast visible={true} message={message} duration={duration} icon={icon} />, view);
  pageElement?.appendChild(view);

  if (duration !== 0) {
    setTimeout(() => {
      destroyToast(view);
    }, duration);
  }

  if (duration === 0) {
    return () => destroyToast(view);
  }
}

export const destroyToast = (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 toast = (message: string, duration = 2000) => {
  TaroToast({ message, duration });
}

//使用示例
// import { toast } from '@/components/common'

// const handleToast=()=>{
//   toast('提示1')
//   toast('提示2')
//   toast('提示3')
//   setTimeout(()=>{
//     toast('提示4')
//   },10)
// }

使用

使用示例
import { toast } from '@/components/common'

const handleToast=()=>{
  toast('提示1')
  toast('提示2')
  toast('提示3')
  setTimeout(()=>{
    toast('提示4')
  },10)
}

图片