ZhengXingchi / ZhengXingchi.github.io

Apache License 2.0
0 stars 0 forks source link

格物院H5 #98

Open ZhengXingchi opened 4 years ago

ZhengXingchi commented 4 years ago

思考

由于在微信H5中使用,所以最开始考虑采用react-WeUI

React WeUI文档

参考weui-react项目实战新心得 itmifen/bookdrift github地址

后来考虑到移动端多端的问题,决定使用Taro来书写H5

ZhengXingchi commented 4 years ago

Taro

参考项目Taro 兼容 h5 踩坑指南

Taro,多端适配方案

qit-team/taro-yanxuan github项目网易严选实例

ZhengXingchi commented 4 years ago

egg 与 taro

PresByter2015/shopping-applet lqrCode/Comic-Inn egg branch以及taro branch

ZhengXingchi commented 4 years ago

taro的一些坑

升级到最新CLI:taro update self 升级到最新依赖库:taro update project 请参考 "常用 CLI 命令"中"更新" 章节:https://taro-docs.jd.com/taro/docs/GETTING-STARTED.html Taro 环境及依赖检测

ZhengXingchi commented 4 years ago

taro项目

更多资源

EasyTuan/taro-msparis

lsqy/taro-music

bozaigao/Taro-demo

qit-team/taro-yanxuan

ZhengXingchi commented 4 years ago

taro dva

Taro + dva 使用小结(搭建配置过程)

手摸手教你 Taro+dva+Hooks 快速开发小程序

ZhengXingchi commented 4 years ago

taro使用文档

渐进式入门教程

Taro 介绍

ZhengXingchi commented 4 years ago

taro服务器端渲染

请问,h5的服务器端渲染如何实现

ZhengXingchi commented 4 years ago

operation not permitted, uv_cwd

当删除某个项目的时候,vscode没有退出重新打开,就运行vscode里面内嵌的terminal就会报这个错。

ZhengXingchi commented 4 years ago

动态修改背景色字体

taro如何动态修改背景色字体

ZhengXingchi commented 4 years ago

@tarojs/cli2.1.5升级到3.0.5

  1. Super expression must either be null or a function import Taro, { Component, Config } from '@tarojs/taro' 改为 import React, { Component } from 'react'

  2. _react.default.createContext is not a function when using react-redux

    import { Provider } from "@tarojs/redux";
    改为
    import { Provider } from "react-redux";
    import { connect } from "@tarojs/redux";
    改为
    import { connect } from "react-redux";
  3. @tarojs/components' does not contain an export named 'View'. 升级 "@tarojs/components"到"3.0.5",

  4. Cannot use the decorators and decorators-legacy plugin together decorators-legacy与decorators重复了,decorators-legacy是babel6中的transform,babel 7不需要了吧

  5. The node type SpreadProperty has been renamed to SpreadElement

  6. router.esm.js?c53f:1569 Uncaught (in promise) Error: Route not found at next (router.esm.js?c53f:1569) at eval (router.esm.js?c53f:1580) 很可能是app.config.ts中该page没有定义

    export default {
    pages: [
    'pages/index/index',
    // 'pages/bookDetail/index'
    ],
    window: {
    backgroundTextStyle: 'light',
    navigationBarBackgroundColor: '#fff',
    navigationBarTitleText: 'WeChat',
    navigationBarTextStyle: 'black'
    }
    }
  7. Failed to compile.

./node_modules/taro-ui/dist/h5/components/accordion/index.js Module not found: Can't resolve 'nervjs' in '/xxxxx/node_modules/taro-ui/dist/h5/components/accordion'

初始化项目中taro用的是3.0版本,但是taro-ui默认安装的是2.0的版本,所以需要指定版本:yarn add taro-ui@3.0.0-alpha 参考初始化新项目 引入taro-ui 编译报错

ZhengXingchi commented 4 years ago

taro使用习惯

添加hooks功能 参考NervJS/taro-v2ex-hooks

添加dva功能
参考lsqy/taro-music DvaJS 官网 EasyTuan/taro-msparis 具体参考了 taro+dva的实践 dva上手-简单例子学习dva

添加typescript 有个isssue看看react-redux types give errors when used as decorators

ZhengXingchi commented 4 years ago

分辨率适配

最易懂的Android屏幕适配解决方案--总结版

翻页H5全分辨率适配最佳实践

设计图的宽度应该是按逻辑像素走吗?iphonex是375,iphone8plus是414,几乎没有逻辑像素宽度是750的手机,为什么设计图做成750的?

H5移动多终端适配全解 - 从原理到方案 关于移动端适配,你必须要知道的 为什么设计稿是750px

移动端适配

我对移动端适配的了解

关于移动端适配,你必须要知道的

移动适配问题总结方案

css像素与物理像素

HTMLCSS学习笔记(二十)-- 移动端项目准备工作

ZhengXingchi commented 4 years ago

图片延迟加载库

图片延迟加载库Layzr

如何处理h5高清屏图片?

ZhengXingchi commented 4 years ago

如何处理 h5 高清屏图片?

1. srcset

语法:在元素上添加srcset属性。srcset的值是一个用逗号分隔的列表。列表中的每个项包含一张图片的路径并且按倍数(例如,1x,2x,3x...)提供多张分辨率的图片

参考:https://github.com/YIXUNFE/blog/issues/25

2. media query

@media only screen and (-Webkit-min-device-pixel-ratio: 1.5),
only screen and (-moz-min-device-pixel-ratio: 1.5),
only screen and (-o-min-device-pixel-ratio: 3/2),
only screen and (min-device-pixel-ratio: 1.5){

}

3. image-set

参考:http://www.jianshu.com/p/67ce3e7dd157

也可以参考HTML5之图片在Retina屏的常用几种处理方式

ZhengXingchi commented 4 years ago

Promise的API

Promise.all()
Promise.allSettled()
Promise.any()
Promise.prototype.catch()
Promise.prototype.finally()
Promise.prototype.then()
Promise.race()
Promise.reject()
Promise.resolve()
ZhengXingchi commented 4 years ago

翻页效果

js实现翻页效果 参考js--翻书效果的实现 turn.js Make a flip book with HTML5 github地址

ghxin66/turn.js turn.js翻书效果的学习

react翻页

ZhengXingchi commented 4 years ago

动态ReactDOM.render创建的组件无法同步props

参考动态ReactDOM.render创建的组件无法同步props 如果想自动同步,可以利用react的生命周期函数,手动在state更新、其他组件渲染完毕之后调用一次dynamic()

ZhengXingchi commented 4 years ago

taro的h5项目没有onReachBottom方法

参考js监听页面的scroll事件,当移到底部时触发事件

//页面拉到底时自动加载更多
$(window).scroll(function(event){
var wScrollY = window.scrollY; // 当前滚动条位置
var wInnerH = window.innerHeight; // 设备窗口的高度(不会变)
var bScrollH = document.body.scrollHeight; // 滚动条总高度
if (wScrollY + wInnerH >= bScrollH) {
showMore();
}
});

taro的h5项目没有onPullDownRefresh方法

参考前端页面如何实现下拉刷新

js使用scroll下拉刷新

js使用scroll下拉刷新

ZhengXingchi commented 4 years ago

移动1px问题

$grey: #9f9fab;
.tab {
    width: 100%;
    height: 40px;
    line-height: 40px;
    @include border-1px($grey);
}

/* 实现移动端 1px , 根据不同 dpr 去缩放 */
@mixin border-1px($color) {
  position: relative;
  @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5) {
    &::after {
      transform: scaleY(0.7);
      content: '';
      width: 100%;
      border-bottom: 1px solid $color;
      height: 1px;
      position: absolute;
      bottom: 0;
    }
  }
  @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) {
    &::after {
      transform: scaleY(0.5);
      content: '';
      width: 100%;
      border-bottom: 1px solid $color;
      height: 1px;
      position: absolute;
      bottom: 0;
    }
  }
}

解决移动端 1px 问题

Mars/solutions/border-1px.md

移动端1px问题的解决办法

移动端1px问题处理方法

7 种方法解决移动端 Retina 屏幕 1px 边框问题

ZhengXingchi commented 4 years ago

滚动条滚动到父组件的顶部

document.getElementById(..).scrollTop = 0;

ZhengXingchi commented 4 years ago

taro的tabbar

taro自定义顶部导航条/底部Tabbar Taro中自定义小程序tabBar

微信小程序——底部导航tabBar

ZhengXingchi commented 4 years ago

原生js判断某个元素是否滚动到底部

document.querySelector('.content').addEventListener('scroll',function () {
        //读取内容区域的真实高度(滚动条高)
//        console.log(this.scrollHeight);
        //读取滚动条的位置
//        console.log(this.scrollTop);
        //设置滚动到的位置
//            this.scrollTop=800;
        //读取元素的高度
//        console.log(this.clientHeight)
        //意思就是内容总体的高度 - 滚动条的偏移值  === 元素的高度(包含内边)但不包含外边距,边框,以及滚动条
        if(this.scrollHeight-this.scrollTop===this.clientHeight){
            console.log("到达底部");
        }
    })
ZhengXingchi commented 4 years ago

taro Loading chunk

taro在使用Taro.navigateBack时候有时候会loading chunk报错,这时候不太明白为什么,但是可以考虑加一个钩子函数,类似[网络问题导致至chunk fail,没有对应钩子函数处理](https://github.com/NervJS/taro/issues/5238)

ZhengXingchi commented 4 years ago

手机调试

vConsole <script> (function() { const script = document.createElement('script') script.src = 'https://cdn.bootcss.com/vConsole/3.2.2/vconsole.min.js' document.body.appendChild(script) script.onload = function () { var vConsole = new VConsole(); } })() </script> 抓包 Charles Fiddler

ZhengXingchi commented 3 years ago

唤醒App

H5唤起APP指南

步骤一、下载插件
npm install callapp-lib --save
npm install timers --save

步骤二、引入插件
import CallApp from "callapp-lib";
import { setTimeout } from 'timers';

步骤三、在methods 内定义点击事件
openWx () {
        const options = {
            scheme: { protocol: "weixin" },
            intent: {
              package: "com.tencent.mm",
              scheme: "weixin"
            },
            //apple store
            appstore: "https://itunes.apple.com/cn/app//id414478124?mt=8",
            //应用宝
            yinyongbao:"https://android.myapp.com/myapp/detail.htm?apkName=com.tencent.mm",
            fallback: "https://a.app.qq.com/o/simple.jsp?pkgname=com.tencent.mm" //唤端失败后跳转的地址
          };
          const callLib = new CallApp(options);
          callLib.open({});
}
ZhengXingchi commented 3 years ago

微信支付

如何在测试服务绕过微信的域名检测可以参考QA如何解决当前页面的 URL 未注册 解决方法 我们可以使用 charles 绕过微信的域名检测,让微信“误以为”页面的域名在 staging ,实际上页面是从 dev 环境中获取。假设 staging 域名为 http://a.b.com,dev 1 环境域名为 a-dev1.b.com,我们要实现查看 dev1页面并完成支付,需要做如下操作:

1、进入 charles > Tools > Map Remote > Add,填写参数值 2、手机连上代理,在微信内打开链接 http://a.b.com,此时不但可查看 a-dev1.b.com 环境下的页面,还能完成支付。 image

ZhengXingchi commented 3 years ago

taro 怎么配置多个环境

[taro怎么配置多个环境](https://github.com/NervJS/taro/issues/4807)

ZhengXingchi commented 3 years ago

打包体积过大

[打包体积太大微信开发者工具点击预览提示超过2000kb](https://github.com/NervJS/taro/issues/6479) [Taro打包h5在config/index.js中如何配置HtmlWebpackplugins插件,2.0.7可以用,现在2.2.9不能用了](https://github.com/NervJS/taro/issues/7024)

[taro@2.0.6 addChunkPages问题](https://github.com/NervJS/taro/issues/5645) [taro@2.0.5如何将echart.js文件分离出公共组件(common.js),并在子包中引用](https://github.com/NervJS/taro/issues/5628)

关于小程序不能大于2M的解决方案

采用小程序拆包方案 小程序分包(Taro分包案例)

关于配置splitChunks

webpack里的optimization.splitChunks使用问题

webpack4系列教程(六):使用SplitChunksPlugin分割代码

Webpack 配置:optimization.splitChunks.chunks

ZhengXingchi commented 3 years ago

动态tabbar

[Taro里面有什么方法能有动态的tabbar?](https://github.com/NervJS/taro/issues/2260)

ZhengXingchi commented 3 years ago

响应式布局

Learn CSS Grid in 5 minutes - A tutorial for beginners

ZhengXingchi commented 3 years ago

微信小程序

getApp什么意思 参考官方文档 独立分包

ZhengXingchi commented 3 years ago

REM布局-自动计算根fontsize

/* 根据窗口宽度自动计算html基准字体大小,用于移动端弹性布局 */
(function() {
    var docEl = document.documentElement,
        docBody = document.body,
        baseFontSize = 100,//为了方便,这样rem=px/100
        pageMaxWidth = 750,
        rootHtml = document.getElementsByTagName('html')[0],
        resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',
        recalc = function() {
            var clientWidth = clientWidth = docEl.clientWidth || docBody.clientWidht;

            //最大宽度显示为750的宽度
            if (clientWidth > pageMaxWidth) {
                clientWidth = pageMaxWidth
            }

            if (!clientWidth) return;
            rootHtml.style.fontSize = baseFontSize * (clientWidth / pageMaxWidth) + 'px';
        };
    if (!window.addEventListener) return;

    window.addEventListener(resizeEvt, recalc, false);

    recalc();
})();
ZhengXingchi commented 3 years ago

小程序使用 Taro 和原生混合开发方案的探索

[小程序使用 Taro 和原生混合开发方案的探索](https://github.com/rottenpen/blog/issues/22)

ZhengXingchi commented 3 years ago

浏览器退出事件监听

参考JS之onunload、onbeforeunload事件详解 移动端检测微信浏览器返回,关闭,进入后台操作

背景:最近做一个倒计时记录学习时长项目,需要在用户点击浏览器的返回按钮或者直接关闭浏览器,或者直接退出微信或者进入后台时记录下当前页面的进度,下次进去从上次退出的地方开始倒计时。一开始想的很简单直接监测浏览器的返回事件window.onbeforeunload,在安卓和pc上可以监测到,但是iOS上监测不到

解决办法:根据百度相关文档,发现iOS端检测需要用pagehide去检测,于是修改代码如下

var u = navigator.userAgent;
            var isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; //g
            var isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端

 if (isAndroid) {
                                //这个是安卓操作系统
                                window.onbeforeunload = function () {
                                    //窗口关闭前
                                   ...暂停倒计时并且ajax请求记录到数据库
                                };
                            } else if (isIOS) {
                                //这个是ios操作系统
                                window.addEventListener('pagehide', function () {
                                           ...暂停倒计时并且ajax请求记录到数据库
                                });
                            } else {
                                //这个pc
                                window.onbeforeunload = function () {
                                    //窗口关闭前
                                    c      ...暂停倒计时并且ajax请求记录到数据库
                                };
                            }

调试是打断点iOS还是无法监测到,当时郁闷了很长时间,苹果官方文档明明写了用这个能监测到,但是我居然监测不到,一度以为是系统版本低,升级到最新系统发现还是监测不到pagehide,无法记录数据到数据库,最后翻遍百度在一个帖子的不起眼的评论里看到了一句话说把ajax请求改成同步试试,我抱着试试的态度修改了代码

//这个是ios操作系统
                                window.addEventListener('pagehide', function () {
                                    $.ajaxSetup({
                                        async: false//关闭异步
                                    });
                                    ...ajax请求记录数据到数据库
                                });
                                window.addEventListener('pageshow', function () {
                                    $.ajaxSetup({
                                        async: true//页面显示时恢复异步
                                    });
                                });

发布测试居然监测到了,郁闷已久的问题终于解决了,虽然不知道什么原因,但是解决了就是好事

监测浏览器返回、关闭、退出、进入后台完整代码

if (isAndroid) {
                                //这个是安卓操作系统
                                window.onbeforeunload = function () {
                                    //窗口关闭前
                                    ...ajax操作
                                };
                            } else if (isIOS) {
                                //这个是ios操作系统
                                window.addEventListener('pagehide', function () {
                                    $.ajaxSetup({
                                        async: false
                                    });
                                   ...ajax操作
                                });
                                window.addEventListener('pageshow', function () {
                                    $.ajaxSetup({
                                        async: true
                                    });
                                });
                            } else {
                                //这个pc
                                window.onbeforeunload = function () {
                                    //窗口关闭前
                                    ...ajax操作
                                };
                            }

                            //window.onunload = function () {
                            //    //窗口关闭后
                            ...ajax操作
                            //};
                            window.addEventListener("popstate", function (e) {
                              ...ajax操作
                            }, false);
                            if (typeof document.hidden !== "undefined") {
                                hidden = "hidden";
                                visibilityChange = "visibilitychange";
                            } else if (typeof document.mozHidden !== "undefined") {
                                hidden = "mozHidden";
                                visibilityChange = "mozvisibilitychange";
                            } else if (typeof document.msHidden !== "undefined") {
                                hidden = "msHidden";
                                visibilityChange = "msvisibilitychange";
                            } else if (typeof document.webkitHidden !== "undefined") {
                                hidden = "webkitHidden";
                                visibilityChange = "webkitvisibilitychange";
                            }
                            document.addEventListener(visibilityChange, function () {
                                console.log("当前页面是否被隐藏:" + document[hidden]);
                                if (document[hidden]) {//页面被隐藏,进入后台运行时监测
                                    ...ajax操作
                                }
                                else {
                                    ...页面重新进入前台时操作,比如恢复倒计时
                                }
                            }, false);

安卓手机关闭浏览器监听不到,具体看下一期https://www.cnblogs.com/stubborn-donkey/p/12978860.html

上一篇文章说了在安卓和iOS微信浏览器监测页面关闭返回的方法,当时测试时是有效果的,后来在安卓端不起作用了,只有浏览器中没有页面缓存重新加载时,才有作用

初步判断是页面首次加载时window.onbeforeunload起作用了,下一次进入读取的缓存页面,window.onbeforeunload不起作用了

window.addEventListener('pagehide', function () {})在iOS上没有问题,但是在安卓上不管第一次进入还是从缓存读取都不起作用,应该是浏览器兼容问题

通过debugmm.qq.com/?forcex5=true或者debugstbs.qq.com开启微信浏览器为X5内核后,发现安卓端可以用了,window.addEventListener('pagehide', function () {})起作用了

debugmm.qq.com/?forcex5=false关闭X5内核后,又不起作用了

发现

window.addEventListener( 'blur', function() { console.log( 'blur' ); } ); window.addEventListener( 'focus', function() { console.log( 'focus' ); } ); 可以检测点击了关闭浏览器的功能,但是返回监测不到

经过测试发现 window.onbeforeunload = function () { //窗口关闭前 saveExam(that)

};能监测到返回

所以最后解决方式是在安卓里用

window.addEventListener( 'blur', function() { console.log( 'blur' ); } );结合window.onbeforeunload和 window.addEventListener("popstate", function (e) { iOS里用pagehide

ZhengXingchi commented 3 years ago

单例组件

参考React实现单例组件

问题背景 在工作中遇到了这样一个场景,写了个通用的弹窗组件,却在同一个页面中多次使用了该组件。当点击打开弹窗时,可想而知,一次性打开了多个弹窗,而业务需求只需要打开一个。

我个人在解决问题过程中的一些已废弃思路 我首先想到的是能不能像mobx的@observer一样用一个譬如@singleton来修饰组件类,然后在像正常组件一样在使用组件的地方使用标签名来使用该组件。google了大半小时,发现行不通,因为每在render方法里使用一个组件,React就会自动实例化一个组件类,所以React本身的设计其实完全不适用于单例

解决问题的核心思路 采用类似调用方法的形式而非组件标签的形式来调用组件 只能在一个特定的容器内render组件,从而保证单例

import React from 'react'
import { render, unmountComponentAtNode } from 'react-dom'
//具体单例类代码
export default class Singleton {
  constructor(component){
    this.dom = null;
    this.component = component;
    this.instance = null;
  }

  render(option) {
    if(!this.dom) {
      this.dom = document.createElement('div');
      document.body.appendChild(this.dom);
    }
    this.instance = ReactDOM.render(<this.component {...option}/>, this.dom);
  }

  destroy() {
    unmountComponentAtNode(this.dom);
  }
}

//使用例子
//在适当地方调用如下代码渲染组件
new Singleton(Component).show();

如何在React项目中单例化通用(全局,常用)组件 #82 https://github.com/jun-lu/blog/issues/82

如何在React项目中单例化通用(全局,常用)组件
在jQuery的年代,如果要做一个全局的加载状态,可能会设计下面这样的API

Loading.show();
let result = await ajax("http://xxx");
Loading.hide();

在ajax的前后分别调用Loading的显示和隐藏。简单清晰,可复用性强,

现在使用React,则会自然的变成下面这样。

this.setState({
  loading:true
});

let result = await ajax("http://xxx");

this.setState({
  loading:false
});

...
render(){
  let {loading} = this.state;
  return loading ? <Loading /> : <Other .../>
}

因为在React中,只能通过state或props的改变来驱动DOM更新,不能凭空插入节点到DOM树中,所以只能通过申明一个变量的方法来判断是否应该显示加载中状态,这看起来比较繁琐,每次个异步请求都得这样写一遍,不够简单和高效,是否有办法做到更简单的API呢?

包装基础组件(高阶组件)
这里我们申明一个可以代理传入组件的props.display属的高阶组件,并暴露show,hide方法。

let SingleComponents = [];

function higherOrderComponents(Component){

  let exp = null;
  class HigherOrderComponents extends React.Component{
    construcotr(props){
      super(props);

      this.state = {
        display:false
      }
      //实例化的时候保留引用
      exp = this;
    }

    render(){
      return <Component display={this.state.display} />
    }
  }

  SingleComponents.push(HigherOrderComponents);

  return {
    show(){
      exp.setState({
        display:true
      })
    },
    hide(){
      exp.setState({
        display:false
      })
    }
  }
}

上面的高阶组件除了代理props.display属性外, 还把新组件加入了全局的list,方便我们在初始化页面的时候,进行全局实例化。实例化以后他就是单例了。

看看我们如何利用高阶组件注册一个单例的 Loading .

//简单的加载中状态
class Loading extends React.Component{
  render(){
    return this.props.display ? <div>loading</div> : null
  }
}
//把Loading放入高阶组件
let GlobalLoading = higherOrderComponents(Loading);

Loading.show = GlobalLoading.show;
Loading.hide = GlobalLoading.hide;

站点首页进行如下渲染

render(){
  render <div>
    {
        //实例化所有已注册组件
        SingleComponents.map((Component, index)=>{
            return <Component key={index} />
        })
    }
  </div>
 }

而后我们就可以在页面中引入Loading组件,进行单例式的调用

import Loding from './Loading';

Loading.show();
let result = await ajax("http://xxx");
Loading.hide();

这里我们用一个Loding组件作为引子,实践了如何把react单例化,根据上面的方法,上面的代码只需要略加修改,即可扩大的项目的所有单例组件。比如我们的高阶组件可以代理所有的props,那么就可以单例化 alert, Confirm, Dialog 这些基础组件,而且这些基础组件的设计不受影响,依然可以是无状态组件。

备注:以上都是伪代码,仅做参考

参考react单例模式

react单例组件的实现方式
说到react的单例,大家可能一哈子就想到了像Alert啊,弹层啊、Confirm啊之类的。没毛病。单例嘛,就是全局唯一一个实例,不可能同时出现两个嘛。是的。极大部分业务情况下是这样的。所以,怎么实现一个单例组件,是个值得思考的问题。

因地制宜,我们的前提是react组件的单例。
使用react组件常见的套路是写jsx,直接声明式地将组件放在它该在地位置,如:

// ...
// ...
render() {

    return (
        <div className={wrapperClassNames}>
            <GoBack goBack = {props.goBack} />
            <h1 className = 'header-title'>{props.headerTitle}</h1>
            {
                rightOptions ? <div className = 'header-right'>{ rightOptions }</div> : null
            }
        </div>
    );
}
// ...
// ...

如果我们想根据某种状态来决定是否显示某个组件,可以三目。这样:
// ...
// ...
render() {

    return (
        <div className={wrapperClassNames}>
            {
                this.state.showModal ? <Modal /> : null
            }
        </div>
    );
}
// ...
// ...

是这样的吧?相信大家也都是这么用的。没没啥大毛病。

没啥毛病,意思是有点小毛病咯?
正如小标题,确实没啥大毛病,却有一些小毛病,我说说我在业务中遇到的问题。

动画直接丢失。好理解吧?我这个组件有进场、退场动画,在状态变化、变为不显示时,直接就被干掉了,退场动画写给谁看啊?
有多少个Modal,就要写多少次(除非把Modal的数据写在上层组件的state里,一并传给Modal)
跟单例组件有啥关系?
正如前面所说,使用React,就注定了对组件的使用是声明式的。声明式的组件也意味着满足条件时会直接render到页面上(虽然可以用state来判断是否显示组件,但这种方式直接导致动画失效,这里排除了这种情况)。一般来说,使用单例组件可以采用调用的形式,这里引用一个同事的Alert组件:
class Alert extends PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            show: props.show,
            onConfirm: props.onConfirm,
            content: props.content
        };
        this.timer = null;
        that = this;
    }

    componentWillUnmount() {
        if (container) {
            document.body.removeChild(container);
        }
    }

    render() {
        const { show, content, onConfirm } = this.state;
        const actions = [{
            text: '确定',
            callback: () => {
                onConfirm();
                this.setState({
                    show: false
                });
            }
        }];
        return (
            <Modal
                title ='提示'
                footer
                onHide = {() => false}
                actions = {actions}
                show = {show}
            >
                { content }
            </Modal>
        );
    }
}

Alert.propTypes = {
    show: PropTypes.bool,
    onConfirm: PropTypes.func,
    content: PropTypes.string
};

Alert.defaultProps = {
    show: false,
    onConfirm: () => true,
    content: ''
};

if (!isNodeEnv()) {
    container = document.createElement('div');
    document.body.appendChild(container);
    ReactDom.render(<Alert />, container);
}

export default {
    alert(config) {
        that.setState(Object.assign({}, config, { show: true }));
    }
};

如上,对外暴露的不再是一个组件,而是包含alert方法的对象。通过手动调用alert(config)的方式,实现了单例组件。这种方式也非常常见。但这种方式有一个弊端,是什么呢?思考下。

使用API调用的方式实现单例组件
是组件吧?那我们肯定要传一些参数对吧?(不要把Alert这个单例组件那来当话题背景,你可以想象一个模态框弹层组件,除了外面的掩层,里面的内容是不是得完全自己去写呀?)调用api的话,必须每次都把配置对象传入,可能是很大一个对象,如:

globalLayer.show({
    title: '测试',
    onHide: function() {},
    onClick: function() {},
    content: (<Component1> <Son/>  </Component1>)
    // .....
});

这样的配置。而且每次调用这个方法都得传一个大对象过去。是不是有点麻烦?

使用声明式组件实现单例
什么是声明组件?就是:

// ...
render() {
    <GlobalLayer show = {this.state.showLayer1}>
    </GlobalLayer>

    <GlobalLayer show = {this.state.showLayer2}>
    </GlobalLayer>

    <GlobalLayer show = {this.state.showLayer3}>
    </GlobalLayer>
}
// ...

类似这种的“直接render“。虽然看上去被直接render了,看上去应该有3个被塞入DOM了。但巧妙的就是GlobalLayer是一个高阶函数,它管理了自己的state中显示逻辑—-用这个state来控制children是否显示。这里贴一下我的高阶函数的实现:

import React, { PureComponent } from 'react';
import LazyRender from '../LazyRender'; // 这个是一个单独的用来懒渲染的高阶函数,可脱离本高阶函数独立使用
export default function (MyComponent) {

    class Wrapper extends PureComponent {

        static displayName = 'SelfDeleteWrapper';

        state = {
            showComponent: false // 用来控制是否显示内部组件
        };

        static getDerivedStateFromProps(props, state) {
            if (props.show && !state.showComponent) { // 准备展示组件
                return {
                    showComponent: true
                };
            }

            return null;
        }
        deleteComponent = () => { // 销毁组件
            console.log('1');
            this.setState({
                showComponent: false
            });
        }

        LazyComponent = LazyRender(MyComponent); // 缓存要渲染的东西

        render() {

            const {
                showComponent
            } = this.state;

            const {
                deleteComponent,
                LazyComponent
            } = this;

            return (
                showComponent ?
                    <LazyComponent {...this.props} __onDelete={deleteComponent} /> : null
            );
        }
    }

    return Wrapper;
}
关键点还是在于onDelete函数。在MyComponent触发了onHide函数或者被上层组件设置为show: false时会触发onDelete,使LazyComponent这个组件被react干掉。当然,触发__onDelete是在执行完MyComponent的退场动画后才触发的。这样保证了全局单一的实例。
ZhengXingchi commented 3 years ago

animation

参考[createAnimation 在iOS和Android两个平台表现不一致 #5261](https://github.com/NervJS/taro/issues/5261)

/* eslint-disable react/jsx-key */
/**
 * 竖直跑马灯, 匀速
 * 
 * 使用方式一:
 *      <Marquee data={...}/>
 * 
 * 使用方式二:
 *      <Marquee>
 *          ....
 *      </Marquee>
 *      
 */
import Taro, { Component } from '@tarojs/taro';
import { View, Text, ScrollView } from '@tarojs/components';
import PropTypes from 'prop-types';

export default class Marquee extends Component {
    static propTypes = {
        duration: PropTypes.number, // 动画持续时长(毫秒),优先级低于interval
        width: PropTypes.oneOfType([PropTypes.string,PropTypes.number]),    // 跑马灯组件宽度
        height: PropTypes.oneOfType([PropTypes.string,PropTypes.number]),   // 跑马灯组件高度
        data: PropTypes.array,    // 数据
        interval: PropTypes.number,     // 每条数据的展示时长(毫秒),优先级高于duration
    };
    static defaultProps = {
        duration: 10000,
        width: '100%',
        height: '100%',
        data: [],
        interval: 1000,
    };
    state = {
        marqueeAnimate: '',
    };
    marqueeAnimation = null;
    rersetAnimation = null;
    contentHeight = 10;
    isEnd = false;
    componentWillMount(){
        const {
            duration,
            data,
            interval
        } = this.props;
        let time = duration;
        if(data && data.length>0){
            time = data.length * interval;
            this.contentHeight = data.length * 30
        }
        this.marqueeAnimation = Taro.createAnimation({
            duration: time, 
            timingFunction: 'linear',
            delay: 0,
            transformOrigin: '50% 50%',
        });
        this.rersetAnimation = Taro.createAnimation({
            duration: 10, 
            timingFunction: 'step-start',
            delay: 0,
            transformOrigin: '50% 50%',
        });
    };
    componentWillReceiveProps(nextProps) {
        const {
            interval
        } = this.props;
        const oldData = this.props.data
        const nextData = nextProps.data;
        if((!oldData && !!nextData) || (oldData && oldData.length || 0) !== (nextData && nextData.length || 0)){
            console.log('接收新数据数据>>>', {oldData, nextData});
            const time = nextData.length * interval;
            this.contentHeight = nextData.length * 30
            this.marqueeAnimation = Taro.createAnimation({
                duration: time, 
                timingFunction: 'linear',
                delay: 0,
                transformOrigin: '50% 50%',
            });
            this._resetAnimate();
        }
    };
    componentDidShow() {
        const {
            data,
        } = this.props;
        console.log('数据>>>', data);
        if(data && data.length>0){
            this._startAnimate();
        }else{
            const _this= this;
            const query = Taro.createSelectorQuery();
            query.select('#marqueeContent').boundingClientRect();
            query.exec((res)=>{
                console.log('测量结果>>>', res);
                const { height } = res[0];
                _this.contentHeight = height;
                _this._startAnimate();
            })
        }
    };
    _startAnimate = () => {
        this.isEnd = false
        console.log('动画开始>>>', this.contentHeight);
        const {
            height,
        } = this.props;
        this.marqueeAnimation.translateY(-this.contentHeight).step();
        this.setState({
            marqueeAnimate: this.marqueeAnimation.export(),
        })
    };
    _resetAnimate = (e) => {
        this.isEnd = true;
        console.log('动画重置>>>', e);
        const {
            height,
        } = this.props;
        this.rersetAnimation.translateY(0).step();
        this.setState({
            marqueeAnimate: this.rersetAnimation.export(),
        })
    };
    _animateEnd = () => {
        if(this.isEnd){
            this._startAnimate();
        }else{
            this._resetAnimate();
        }
    };
    _stopAnamite = (e) => {
        console.log('触摸开始>>>', e);
        if(this.isEnd){
            this.rersetAnimation.export();
        }else{
            this.marqueeAnimation.export();
        }
    };
    _resumeAnamite = (e) => {
        console.log('触摸结束>>>', e);
        // if(this.isEnd){
        //     this._startAnimate();
        // }else{
        //     this._resetAnimate();
        // }
    };
    render() {
        const {
            height,
            width,
            data,
            textStyle
        } = this.props;
        const {
            marqueeAnimate
        } = this.state;
        const widthValue = typeof(width)==='string' ? width : Taro.pxTransform(width);
        const heightValue = typeof(height)==='string' ? height : Taro.pxTransform(height);
        if(data && data.length>0){
            return (
                <View onTouchStart={this._stopAnamite} onTouchEnd={this._resumeAnamite} style={{ width: widthValue, height: heightValue,  overflow: 'hidden' }}>
                    <ScrollView id='marqueeContent' animation={marqueeAnimate} onTransitionend={this._animateEnd}>
                        <View style={{ display: 'flex', flexDirection: 'column', paddingTop: Taro.pxTransform(height) }}>
                            {data.map((item,index)=>{
                                return <Text taroKey={String(index)} style={textStyle}>{ item }</Text>
                            })}
                        </View>
                    </ScrollView>
                </View> 
            )  
        }
        return (
            <View style={{ width: widthValue, height: heightValue, overflow: 'hidden' }}>
                <ScrollView id='marqueeContent' animation={marqueeAnimate} onTransitionend={this._animateEnd}>
                    <View style={{ display: 'flex', flexDirection: 'column', paddingTop: Taro.pxTransform(height) }}>
                        { this.props.children }
                    </View>
                </ScrollView>
            </View> 
        )
    }
}
ZhengXingchi commented 3 years ago

黑暗模式(深色模式)浅色模式

参考通过三种方式来快速实现深色模式 深色模式在 Web 端的适配方案 深色模式适配指南

方式一:通过 prefers-color-scheme 属性,这个媒体属性用于检测用户是否有将系统的主题色设置为浅色或深色

/* 浅色模式 */ 
    @media (prefers-color-scheme: light) { 
        body { 
            background-color: #fff; 
            color: #000; 
        } 
    } 
    /* 深色模式 */ 
    @media (prefers-color-scheme: dark) { 
        body { 
            background-color: #000; 
            color: #fff; 
        } 
    } 

不过需要通过 JS 去检测用户的浏览器是否支持

    if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 
      // dark mode 
    } 

方式二:通过当天的时间去选择模式(简单粗暴)

function getModeOnTime(){ 
    const date = new Date() 
    const hour = date.getHours() 
    if (hour < 5 || hour > 20) return 'dark' 
    return 'light' 
  } 

方式三:让用户自己选择当前是什么模式 总结:这三种方法可以组合起来弄,比如如果浏览器支持 prefers-color-scheme 属性,直接通过 prefers-color-scheme 属性来选择当前模式,并让用户可以切换和选择。如果用户的浏览器不支持 prefers-color-scheme 属性 那就通过当天的时间去选择模式,同时也是让用户可以切换和选择。

ZhengXingchi commented 3 years ago

子组件绑定原生事件类似vue的.native

外⾯设置了,⾥⾯要⽤。 ⽐如这样

 <div onClick={this.props.onClick}>
 <Menu style={{overflow:"auto",height:200}}>
 {
 this.recursion()
 }
 </Menu>
 </div>

React里面里面除了原生 HTML 元素之外,你自己的组件是不会自动触发什么onClick之类的事件的,需要你自己手动调用父组件通过props传入的方法。

ZhengXingchi commented 3 years ago

移动端穿透问题

移动端滚动穿透问题 解决移动端滚动穿透 [AtActionSheet在ios的webview上,会有滚动穿透的问题](https://github.com/NervJS/taro-ui/issues/286)

ZhengXingchi commented 3 years ago

使用nginx后的静态资源地址问题

webpack4.0 html和css里图片资源加载问题

ZhengXingchi commented 3 years ago

移动端引入的字体文件过大处理方法

移动端引入的字体文件过大处理方法