天天修改

天天修改 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织 www.agzgz.com 编辑
编辑

大前端,小程序,全栈开发

个人动态

天天修改 赞了文章 · 10月16日

聊聊 React 两个状态管理库 Redux & Recoil

State Management in React Apps | WalkingTree Technologies

背景

React 是一个十分优秀的UI库, 最初的时候, React 只专注于UI层, 对全局状态管理并没有很好的解决方案, 也因此催生出类似Flux, Redux 等优秀的状态管理工具。

随着时间的演变, 又催化了一批新的状态管理工具。

简单整理了一些目前主流的状态管理工具:

  1. Redux
  2. React Context & useReducer
  3. Mobx
  4. Recoil
  5. react-sweet-state
  6. hox

这几个都是我接触过的,Npm 上的现状和趋势对比

image.png

image.png

毫无疑问,ReactRedux 的组合是目前的主流。

今天5月份, 一个名叫 Recoil.js 的新成员进入了我的视野,带来了一些有趣的模型和概念,今天我们就把它和 Redux 做一个简单的对比, 希望能对大家有所启发。

正文

先看 Redux:

Redux

React-Redux 架构图:

image.png

这个模型还是比较简单的, 大家也都很熟悉。

先用一个简单的例子,回顾一下整个模型:

actions.js

export const UPDATE_LIST_NAME = 'UPDATE_NAME';

reducers.js

export const reducer = (state = initialState, action) => {
    const { listName, tasks } = state;
    switch (action.type) {
        case 'UPDATE_NAME': {
            // ...
        }        
    default: {
            return state;
        }
    }
};

store.js

import reducers from '../reducers';
import { createStore } from 'redux';
const store = createStore(reducers);
export const TasksProvider = ({ children }) => (
    <Provider store={store}>
       {children}
    </Provider>
);

App.js

import { TasksProvider } from './store';
import Tasks from './tasks';
const ReduxApp = () => (
    <TasksProvider>
       <Tasks />
   </TasksProvider>
);

Component

// components
import React from 'react';
import { updateListName } from './actions';
import TasksView from './TasksView';

const Tasks = (props) => {
    const { tasks } = props;
    return (
        <TasksView tasks={tasks} />
    );
};

const mapStateToProps = (state) => ({
  tasks: state.tasks
});

const mapDispatchToProps = (dispatch) => ({
    updateTasks: (tasks) => dispatch(updateTasks(tasks))
});

export default connect(mapStateToProps, mapDispatchToProps)(Tasks);

当然也可以不用connect, react-redux 提供了 useDispatch, useSelector 两个hook, 也很方便。

import { useDispatch, useSelector } from 'react-redux';
const Tasks = () => {
    const dispatch = useDispatch();
    const name = useSelector(state => state.name);
    const setName = (name) => dispatch({ type: 'updateName', payload: { name } });
    return (
        <TasksView tasks={tasks} />
    );
};

image.png

整个模型并不复杂,而且redux 还推出了工具集redux toolkit,使用它提供的createSlice方法去简化一些操作, 举个例子:

// Action
export const UPDATE_LIST_NAME = 'UPDATE_LIST_NAME';

// Action creator
export const updateListName = (name) => ({
    type: UPDATE_LIST_NAME,
    payload: { name }
});

// Reducer
const reducer = (state = 'My to-do list', action) => {
    switch (action.type) {
        case UPDATE_LIST_NAME: {
            const { name } = action.payload;
            return name;
        }

        default: {
            return state;
        }
    }
};

export default reducer;

使用 createSlice

// src/redux-toolkit/state/reducers/list-name
import { createSlice } from '@reduxjs/toolkit';

const listNameSlice = createSlice({
    name: 'listName',
    initialState: 'todo-list',
    reducers: {
        updateListName: (state, action) => {
            const { name } = action.payload;
            return name;
        }
    }
});

export const {
    actions: { updateListName },
} = listNameSlice;

export default listNameSlice.reducer;

通过createSlice, 可以减少一些不必要的代码, 提升开发体验。

尽管如此, Redux 还有有一些天然的缺陷

  1. 概念比较多,心智负担大。
  2. 属性要一个一个 pick,计算属性要依赖 reselect。还有魔法字符串等一系列问题,用起来很麻烦容易出错,开发效率低。
  3. 触发更新的效率也比较差。对于connect到store的组件,必须一个一个遍历,组件再去做比较,拦截不必要的更新, 这在注重性能或者在大型应用里, 无疑是灾难。

对于这个情况, React 本身也提供了解决方案, 就是我们熟知的 Context.

Image for post

<MyContext.Provider value={/* some value */}>

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

给父节点加 Provider 在子节点加 Consumer,不过每多加一个 item 就要多一层 Provider, 越加越多:

Recoil - Ideal React State Management Library? - DEV

而且,使用Context 问题也不少。

对于使用 useContext 的组件,最突出的就是问题就是 re-render.

不过也有对应的优化方案: React-tracked.

稍微举个例子:

// store.js
import React, { useReducer } from 'react';
import { createContainer } from 'react-tracked';
import { reducers } from './reducers';

const useValue = ({ reducers, initialState }) => useReducer(reducer, initialState);
const { Provider, useTracked, useTrackedState, useUpdate } = createContainer(useValue);

export const TasksProvider = ({ children, initialState }) => (
    <Provider reducer={reducer} initialState={initialState}>
        {children}
    </Provider>
);

export { useTracked, useTrackedState, useUpdate };

对应的,也有 hooks 版本:

const [state, dispatch] = useTracked();
const dispatch = useUpdate();
const state = useTrackedState();

// ...

Recoil

Recoil.js 提供了另外一种思路, 它的模型是这样的:

Image for post

在 React tree 上创建另一个正交的 tree,把每片 item 的 state 抽出来。

每个 component 都有对应单独的一片 state,当数据更新的时候对应的组件也会更新。

Recoil 把 这每一片的数据称为 Atom,Atom 是可订阅可变的 state 单元。

这么说可能有点抽象, 看个简单的例子吧:

// index.js

import React from "react";
import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <App />
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

Recoil Root

Provides the context in which atoms have values. Must be an ancestor of any component that uses any Recoil hooks. Multiple roots may co-exist; atoms will have distinct values within each root. If they are nested, the innermost root will completely mask any outer roots.

可以把 RecoilRoot 看成顶层的 Provider.

Atoms

假设, 现在要实现一个counter:

Image for post

先用 useState 实现:

import React, { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

export default App;

再用 atom 改写一下:

import React from "react";
import { atom, useRecoilState } from "recoil";

const countState = atom({
  key: "counter",
  default: 0,
});

const App = () => {
  const [count, setCount] = useRecoilState(countState);
  
  return (
    <div className="app">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <div>Count is {count}</div>
    </div>
  );
};

export default App;

看到这, 你可能对atom 有一个初步的认识了。

那 atom 具体是个什么概念呢?

Atom

简单理解一下,atom 是包含了一份数据的集合,这个集合是可共享,可修改的。

组件可以订阅atom, 可以是一个, 也可以是多个,当 atom 发生改变时,触发再次渲染。

const someState = atom({
    key: 'uniqueString',
    default: [],
});

每个atom 有两个参数:

  • key:用于内部识别atom的字符串。相对于整个应用程序中的其他原子和选择器,该字符串应该是唯一的
  • default:atom的初始值。

atom 是存储状态的最小单位, 一种合理的设计是, atom 尽量小, 保持最大的灵活性。

Recoil 的作者, 在 ReactEurope video 中也介绍了以后一种封装定atom 的方法:

export const itemWithId =
    memoize(id => atom({
        key: `item${id}`,
        default: {...},
    }));

Selectors

官方描述:

“A selector is a pure function that accepts atoms or other selectors as input. When these upstream atoms or selectors are updated, the selector function will be re-evaluated.”

selector 是以 atom 为参数的纯函数, 当atom 改变时, 会触发重新计算。

selector 有如下参数:

  • key:用于内部识别 atom 的字符串。相对于整个应用程序中的其他原子和选择器,该字符串应该是唯一的.
  • get:作为对象传递的函数{ get },其中get是从其他案atom或selector检索值的函数。传递给此函数的所有atom或selector都将隐式添加到selector的依赖项列表中。
  • set?:返回新的可写状态的可选函数。它作为一个对象{ get, set }和一个新值传递。get是从其他atom或selector检索值的函数。set是设置原子值的函数,其中第一个参数是原子名称,第二个参数是新值。

看个具体的例子:

import React from "react";
import { atom, selector, useRecoilState, useRecoilValue } from "recoil";

const countState = atom({
  key: "myCount",
  default: 0,
});

const doubleCountState = selector({
  key: "myDoubleCount",
  get: ({ get }) => get(countState) * 2,
});

const inputState = selector({
  key: "inputCount",
  get: ({ get }) => get(doubleCountState),
  set: ({ set }, newValue) => set(countState, newValue),
});

const App = () => {
  const [count, setCount] = useRecoilState(countState);
  const doubleCount = useRecoilValue(doubleCountState);
  const [input, setInput] = useRecoilState(inputState);
  
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <button onClick={() => setCount(count - 1)}>Decrease</button>
      <input type="number" value={input} onChange={(e) => setInput(Number(e.target.value))} />
      <div>Count is {count}</div>
      <div>Double count is {doubleCount}</div>
    </div>
  );
};

export default App;

比较好理解, useRecoilStateuseRecoilValue 这些基础概念可以参考官方文档

另外, selector 还可以做异步, 比如:

  get: async ({ get }) => {
    const countStateValue = get(countState);
    const response = await new Promise(
      (resolve) => setTimeout(() => resolve(countStateValue * 2)),
      1000
    );
    return response;
  }

不过对于异步的selector, 需要在RecoilRoot加一层Suspense:

ReactDOM.render(
  <React.StrictMode>
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>}>
          <App />
      </React.Suspense>
    </RecoilRoot>
  </React.StrictMode>,
  document.getElementById("root")
);

Redux vs Recoil

模型对比:

image.png

Recoil 推荐 atom 足够小, 这样每一个叶子组件可以单独去订阅, 数据变化时, 可以达到 O(1)级别的更新.

Recoil 作者 Dave McCabe一个评论中提到:

Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1).
useReducer is equivalent to useState in that it works on a particular component and all of its descendants, rather than being orthogonal to the React tree.

Rocil 可以做到 O(1) 的更新是因为,当atom数据变化时,只有订阅了这个 atom 的组件需要re-render。

不过, 在Redux 中,我们也可以用selector 实现同样的效果:

// selector
const taskSelector = (id) => state.tasks[id];

// component code
const task = useSelector(taskSelector(id));

不过这里的一个小问题是,state变化时,taskSelector 也会重新计算, 不过我们可以用createSelector 去优化, 比如:

import { createSelector } from 'reselect';

const shopItemsSelector = state => state.shop.items;

const subtotalSelector = createSelector(
  shopItemsSelector,
  items => items.reduce((acc, item) => acc + item.value, 0)
)

写到这里, 是不是想说,就这? 扯了这么多, Rocoil 能做的, Redux 也能做, 那要你何用?

哈哈, 这个确实有点尴尬。

不过我认为,这是一种模式上的改变,recoil 鼓励把每一个状态做的足够小, 任意组合,最小范围的更新。

而redux, 我们的习惯是, 把容器组件连接到store上, 至于子组件,哪怕往下传一层,也没什么所谓。

我想,Recoil 这么设计,可能是十分注重性能问题,优化超大应用的性能表现。

目前,recoil 还处于玩具阶段, 还有大量的 issues 需要处理, 不过值得继续关注。

最后

感兴趣的朋友可以看看, 做个todo-list体验一下。

希望这篇文章能帮到你。

才疏学浅,文中若有错误, 欢迎指正。

参考资料

  1. http://react.html.cn/docs/context.html#reactcreatecontext
  2. https://recoiljs.org/docs/basic-tutorial/atoms
  3. https://www.emgoto.com/react-state-management/
  4. https://medium.com/better-programming/recoil-a-new-state-management-library-moving-beyond-redux-and-the-context-api-63794c11b3a5
查看原文

赞 17 收藏 9 评论 0

天天修改 赞了文章 · 10月6日

前端实用小工具(URL参数截取、JSON判断、数据类型检测、版本号对比等)

背景

在日常开发中,我们经常会用一些工具类方法来实现业务逻辑 下面列举几种最常用的

URL截取参数

//直接调用输入想要截取的参数名称几个
export function getParamFromUrl(key) {
    if (key === undefined) return null;
    let search = location.search.substr(1);
    let mReg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)');
    let mValue = search.match(mReg);
    if (mValue != null) return unescape(mValue[2]);
    return null;
}
//示例
let city = getParamFromUrl('city');

JSON是否为空判断

//输入想要检测的json数据 如果为空返回返回false
export function isNullObject(model) {
  if (typeof model === "object") {
    let hasProp = false;
    for (const prop in model) {
        hasProp = true;
        break;
    }
    if (hasProp) {
        return false;
    }
    return true;
  } else {
      throw "model is not object";
  }
}

image-20200929171431032

数据类型检测

//检测变量的数据类型
export function getParamType(item) {
    if (item === null) return null;
    if (item === undefined) return undefined;
    return Object.prototype.toString.call(item).slice(8, -1);
}
//返回String Function Boolean Object Number

image-20200929171150164

获取cookie

//获取document下cookie的具体某个参数值
export function getCookie(key) {
    if (key === undefined) {
        return undefined;
    }
    let cookies = document.cookie;
    let mReg = new RegExp('(^|;)\\s*' + key + '=([^;]*)(;|$)');
    let mValue = cookies.match(mReg);
    let ret = undefined;
    if (mValue != null) {
        ret = unescape(mValue[2]);
    }
    if (ret !== undefined) {
        ret = ret.replace(/^\"|\'/i, '').replace(/\"|\'$/i, '');
    }
    return ret;
}

image-20200930103240035

版本号对比

一般在做APP端开发的时候需要用到一些版本控制,那么就需要针对版本号来进行对比,高版本或者低版本做一些特殊的逻辑处理,下面就是提供版本对比的方法

//传入要对比的版本号,一般前面一个传入当前的版本号,后面一个写上要对比的版本号
export function versionCompare(higher, lower) {
    let sep = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '.';

    let higherAry = higher.split(sep),
        lowerAry = lower.split(sep);
    let l = Math.max(higherAry.length, lowerAry.length);
    for (let i = 0; i < l; i++) {
        let high = parseInt(higherAry[i] || 0);
        let low = parseInt(lowerAry[i] || 0);
        if (high > low) {
            return 1;
        }
        if (high < low) {
            return -1;
        }
    }
    return 0;
}
//返回值  higher > lower: 1;higher = lower: 0;higher < lower:-1

image-20200930103754427

数组去重

export function arrayUniq(array){
    let temp = []; 
    for(var i = 0; i < array.length; i++){
        if(temp.indexOf(array[i]) == -1){
            temp.push(array[i]);
        }
    }
    return temp;
}

image-20200930104914363

iPhone X系列机型判断

export function isIphoneX() {
    // iPhone X、iPhone XS
    var isIPhoneX =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 3 &&
        window.screen.width === 375 &&
        window.screen.height === 812;
    // iPhone XS Max
    var isIPhoneXSMax =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 3 &&
        window.screen.width === 414 &&
        window.screen.height === 896;
    // iPhone XR
    var isIPhoneXR =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 2 &&
        window.screen.width === 414 &&
        window.screen.height === 896;
    if (isIPhoneX || isIPhoneXSMax || isIPhoneXR) {
        return true;
    }
    return false;
}
查看原文

赞 28 收藏 21 评论 1

天天修改 发布了文章 · 9月16日

监听浏览器的前进和后退按钮

项目需要兼容PC/H5,需要监听浏览器的前进和后退按钮点击并分别处理逻辑

监听popstate

popstate关键字做监听方法,能够实时拦截用户点击前进按钮和后退按钮的操作,但不能够区分用户到底点击的是前进按钮还是后退按钮

监听popstate

window.addEventListener("popstate", function (evt) {
  callback(evt)
}, false)

pushState推送历史记录

window.history.pushState({}, '页面标题', 'urlpath')

监听前进/后退按钮

为了满足需求,我们需要为每一个pushState的参数添加一个time参数, 每一次(前进/后退)操作对比对time值,并找到历史数据中的对应下标,从而确定用户点击的是前进按钮还是后退按钮

let localHistory = []
let param = {
  id: 'uniqid',
  url: 'uniqurl'
  title: '页面标题',
  time: (new Date()).getTime()
}

window.history.pushState(param, param.title, param.url)
localHistory.push(param)

上述代码分别在window原生history和localHistory存储了一份相同的数据,重复上述操作(多次跳转链接)

let popstateTime = 0

function getHistoryItem(time){
  let index = findHistoryIndex.call({time})  // 从localHistory列表中查找
  let historyIndex = index - 1 // 取当前event.state对应的历史数据的上一条数据
  return findHistoryItem.call(historyIndex)
}

function setPopstateTime(time){
  let historyItem = getHistoryItem(time)
  that.popstateTime = (historyItem && historyItem.time) || popstateTime || 0
}

window.addEventListener("popstate", function (evt) {
  trigger(evt)
}, false)

function trigger(e){
  let state = e.state
  let time = state.time
  if (popstateTime === 0) {  // 第一次一定是后退按钮
    setPopstateTime(e.state.time)
    // redirectBack()
  } else {
    if (e.state.time >= this.popstateTime) {
      // 使前进无效
    } else {
      setPopstateTime(e.state.time)
      // redirectBack()
    }
  }
}

上述DEMO针对后退按钮执行响应方法,并不会响应用户点击前进按钮

查看原文

赞 0 收藏 0 评论 0

天天修改 发布了文章 · 9月7日

把react组件封装成JS对象

aotoo

aotoo是一个react的封装库,将react组件js实例化

GITHUB源码

INSTALL

yarn add @aotoo/aotoo
#
npm install @aotoo/aotoo

USAGE 1

将原生React组件封装成JS对象

import createComponent from '@aotoo/aotoo'

class Test extends React.Component {
  render(){
    return (
      <div className='container' onClick={this.env.onTest}>
        {this.state.title}
      </div>
    )
  }
}

const testInstance = createComponent(Test, {
  data: {
    title: 'some text'
  },
  onTest(e){
    console.log('do something')
  }
})

function Container(props){
  setTimeout(()=>{
    testInstance.setData({
      title: 'change state.title'
    })
  }, 3000)

  return (
    <testInstance.UI />
  )
}

ReactDOM.render(<Container />, document.getElementById('root'))

USAGE 2

使用配置数据生成实例

import createComponent from '@aotoo/aotoo'

const test = createComponent(
  {
    data: {
      title: 'some text',
      onClick: 'onTest?user=jack'
    },
    onTest(e, param, inst){
      console.log(param) // {user: jack}
      inst.setData({
        title: 'change state.title'
      })
    },
    changeTitle() {
      this.setData({
        title: 'change title again'
      })
    }
  },

  // template
  function(state, props){
    return (
      <div onClick={state.onClick}>{state.title}</div>
    )
  }
)

setTimeout(() => {
  test.changeTitle()
}, 3000);

ReactDOM.render(<test.UI />, document.getElementById('root'))

通用属性

属性类型说明
$$idString类似于$('#id')的id
createdFunction生命周期,同小程序组件
attachedFunction生命周期,同小程序组件
readyFunction生命周期,同小程序组件
didUpdateFunction每次更新后触发

通用API

方法类型说明
parent(p)查找父级
getData()获取元素数据
show()显示该组件
hide()隐藏该组件
destory()销毁该组件
render(p)渲染组件,与直接写jsx一致
attr(p1, p2)设置/获取data-*属性

item

引入@aotoo/aotoo后,会生成全局变量ui_item和全局方法组件UI_item, item组件将会生成一个div的html结构

配置生成组件ui_item

import '@aotoo/aotoo'

const itemConfig = {
  title: '标题',
  onClick: 'changeTitle?title=新的标题',
  changeTitle(e, param, inst){
    inst.update({
      title: param.title
    })
  }
}

const item = ui_item(itemConfig)

ReactDOM.render(<item.UI />, document.getElementById('root'))

使用React方法组件UI_item

import '@aotoo/aotoo'

function changeTitle(e){
  this.update({
    title: '新的标题'
  })
}

const JSX = <UI_item title='标题' onClick={changeTitle}/>

item属性

属性类型说明
$$idString类似于$('#id')的id
titleString/Object/Arrayitem结构
imgString/Object/Arrayitem结构
attrObjectdata-*属性
bodyArrayitem结构,子集均为item
footerArrayitem结构
dotArrayitem结构
itemClassString自定义样式
itemStyleString自定义样式
methodsObject自定义方法
onXXXXString/Functionall events
createdFunction生命周期,同小程序组件
attachedFunction生命周期,同小程序组件
readyFunction生命周期,同小程序组件

item API 方法

方法参数说明
reset(p)恢复初始数据
update(p, callback)更新数据
setData(p, callback)与update相同
attr(p1, p2)设置/获取data-*属性
addClass(p, callback)新增样式类
removeClass(p, callback)移除样式类名
hasClass(p)检测样式类名
css(p)自定义样式
toggleClass(p, callback)切换样式类名
siblings(p)查找兄弟元素
parent(p)查找父级
getData()获取元素数据
show()显示该组件
hide()隐藏该组件
destory()销毁该组件
render(p)渲染组件,与直接写jsx一致

list

引入@aotoo/aotoo后,会生成全局变量ui_list和全局方法组件UI_list, list组件将会生成一组div的html结构(基于item组件)

配置生成组件ui_list

const listConfig = {
  data: [
    {title: 'JACK', onClick: 'onItemClick?user=jack'},
    {title: 'ROSE', onClick: 'onItemClick?user=rose'}
  ],
  listClass: 'list-class',
  onItemClick(e, param, inst){
    if (param.user === 'jack') {
      this.update({
        'data[0].title': 'JACK LOVE ROSE'
      })
    }
  }
}

const list = ui_list(listConfig)  

ReactDOM.render(<list.UI />, document.getElementById('root'))
  

使用React方法组件UI_list

import {$$} '@aotoo/aotoo'

function itemClick(e, param, inst){
  if (param.user === 'jack') {
    this.update({
      'data[0].title': 'JAKE LOVE ROSE'
    })
  }
}

const listData = [
  {title: 'JACK', onClick: 'onItemClick?user=jack'},
  {title: 'ROSE'}
]

const JSX = <UI_list 
  $$id='mylist' 
  data={listData} 
  onItemClick={itemClick}
/>

setTimeout(() => {
  $$('#mylist').update({
    'data[1].title': 'ROSE LOVE JACK TOO'
  })
}, 4000);

ReactDOM.render(JSX, document.getElementById('root'))

tree

tree组件是list组件的超集,通过扁平数据输出层次性的HTML结构,可支持多层次数据

const listConfig = {
  data: [
    {title: '广东省', idf: 'gd'},
    {title: '广州市', parent: 'gd', idf: 'gz'},
      {title: '天河区', parent: 'gd', parent: 'gz'},
      {title: '白云区', parent: 'gd', parent: 'gz'},
      {title: '越秀区', parent: 'gd', parent: 'gz'},
    {title: '深圳市', parent: 'gd'},
    {title: '东莞市', parent: 'gd'},

    {title: '湖南省', idf: 'hn'},
    {title: '长沙市', parent: 'hn'},
    {title: '衡阳市', parent: 'hn'},
  ],
  mode: 'tree'
}

const tree = ui_list(listConfig)  

ReactDOM.render(<tree.UI />, document.getElementById('root'))
空格不是必须的,为展现数据层次

list属性

属性类型说明
$$idString类似于$('#id')的id
dataArraylist子集合
headerJSX列表头部
footerJSX列表底部
listClassString列表样式类
listStyleString列表内联样式
itemClassString批量设置子项样式类
itemMethodObject批量设置子项事件方法
methodsObject设置实例方法
modeString列表类型

list API 方法

方法参数说明
reset(p)恢复初始数据
update(p, callback)更新数据
setData(p, callback)与update相同
insert(query, pay)插入数据
append(pay)追加数据
prepend(pay)前置数据
remove(query)删除数据
attr(p1, p2)设置/获取data-*属性
addClass(p, callback)新增样式类
removeClass(p, callback)移除样式类名
hasClass(p)检测样式类名
css(p)自定义样式
toggleClass(p, callback)切换样式类名
parent(p)查找父级
getData()获取元素数据
show()显示该组件
hide()隐藏该组件
destory()销毁该组件
render(p)渲染组件,与直接写jsx一致

关注我们,后续完善文档

查看原文

赞 0 收藏 0 评论 0

天天修改 赞了文章 · 8月17日

@babel/preset-env 与@babel/plugin-transform-runtime 使用及场景区别

之前在用babel 的时候有个地方一直挺晕的,@babel/preset-env@babel/plugin-transform-runtime都具有转换语法的能力, 并且都能实现按需 polyfill ,但是网上又找不到比较明确的答案, 趁这次尝试 roullp 的时候试了试.

如果我们什么都不做, 没有为babel 编写参数及配置, 那babel 并没有那么大的威力, 它什么都不会做, 正是因为各个预设插件的灵活组合、赋能, 让 babel 充满魅力, 创造奇迹

首先是 @babel/preset-env

@babel/preset-env

这是一个我们很常用的预设, 几乎所有的教程和框架里都会让你配置它, 它的出现取代了 preset-es20** 系列的babel 预设, 你再也不需要繁杂的兼容配置了。 每出一个新提案就加一个? 太蠢了。

有了它, 我们就可以拥有全部, 并且! 它还可以做到按需加载我们需要的 polyfill。 就是这么神奇。
但是吧, 它也不是那么自动化的, 如果你要是不会配置,很有可能就没有用起它的功能

不管怎么养, 首先试一下,眼见为实

首先创建一个 index.js ,内容如下, 很简单

function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
console.log(arr)

然后我们在根目录下创建一个 .babelrc文件, 帮我们刚刚说的预设加进去

{
  "presets": [
    ["@babel/preset-env"]
  ]
}

然后我我们打包一下(这里我用的是roullup)

看一下产出的结果
2019-12-03-21-00-52

我们可以看到, 它babel帮我们做了这几件事情:

  1. 转换箭头函数
  2. const 变为 var

奇怪, 为什么 babel 不帮我们转换 map ? 还有 promise 这些也都是es6的特性呀

嗯~,会不会是我们的目标浏览器不对, babel 觉得不需要转换了, 会不会是这样, 那我们加一个 .browserslistrc 试一下

那就。让我们在根目录下创建一个 .browserslistrc
2019-12-03-21-06-54

好。现在让我们再打包一次.
2019-12-03-21-07-43

咦, 没什么效果。 跟刚刚一样啊。 说明不是目标浏览器配置的问题, 是babel 做不了这个事。

因为默认 @babel/preset-env 只会转换语法,也就是我们看到的箭头函数、const一类。
如果进一步需要转换内置对象、实例方法,那就得用polyfill, 这就需要你做一点配置了,

这里有一个至关重要的参数 "useBuiltIns",他是控制 @babel/preset-env 使用何种方式帮我们导入 polyfill 的核心, 它有三个值可以选

entry

这是一种入口导入方式, 只要我们在打包配置入口 或者 文件入口写入 import "core-js" 这样一串代码, babel 就会替我们根据当前你所配置的目标浏览器(browserslist)来引入所需要的polyfill 。

像这样, 我们在 index.js 文件中加入试一下core-js

// src/index.js
import "core-js";
function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
console.log(arr)

babel配置如下

[
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ]
}

当前 .browserslistrc 文件(更改目标浏览器为 Chrome 是为了此处演示更直观,简洁), 我们只要求兼容 chrome 50版本以上即可(当下最新版本为78)

Chrome > 50

那打包后如何呢?
2019-12-03-21-46-14

恐怖如斯啊,babel把我们填写的 import "core-js"替换掉, 转而导入了一大片的polyfill, 而且都是一些我没有用到的东西。

那我们提升一下目标浏览器呢? 它还会导入这么多吗?
此时, 我们把目标浏览器调整为比较接近最新版本的 75(当下最新版本为78)

// .browserslistrc

Chrome > 75

此刻打包后引入的 polyfill 明显少了好多。
2019-12-03-21-51-46

但同样是我们没用过的。
这也就是印证了上面所说的, 当 useBuiltIns 的值为 entry 时, @babel/preset-env 会按照你所设置的目标浏览器在入口处来引入所需的 polyfill,
不管你需不需要。

如此,我们可以知道, useBuiltIns = entry 的优点是覆盖面积就比较广, 一股脑全部搞定, 但是缺点就是打出来的包就大了多了很多没有用到的 polyfill, 并且还会污染全局

useage

这个就比较神奇了, useBuiltIns = useage 时,会参考目标浏览器(browserslist) 和 代码中所使用到的特性来按需加入 polyfill

当然, 使用 useBuiltIns = useage, 还需要填写另一个参数 corejs 的版本号,

core-js 支持两个版本, 2 或 3, 很多新特性已经不会加入到 2 里面了, 比如: flat 等等最新的方法, 2 这个版本里面都是没有的, 所以建议大家用3

此时的 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ]
}

此时的 index.js


function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
console.log(arr)
console.log( hasNumber(2) )

此时的 .browserslistrc

> 1%
last 10 versions
not ie <= 8

打包后:
2019-12-03-22-59-03

nice ,够神奇, 我们用的几个新特性真的通通都加上了

这种方式打包体积不大,但是如果我们排除node_modules/目录,遇上没有经过转译的第三方包,就检测不到第三方包内部的 ‘hello‘.includes(‘h‘)这种句法,这时候我们就会遇到bug

false

剩下最后一个 useBuiltIns = false , 那就简单了, 这也是默认值 , 使用这个值时不引入 polyfill

@babel/runtime

这种方式会借助 helper function 来实现特性的兼容,
并且利用 @babel/plugin-transform-runtime 插件还能以沙箱垫片的方式防止污染全局, 并抽离公共的 helper function , 以节省代码的冗余

也就是说 @babel/runtime 是一个核心, 一种实现方式, 而 @babel/plugin-transform-runtime 就是一个管家, 负责更好的重复使用 @babel/runtime

@babel/plugin-transform-runtime 插件也有一个 corejs 参数需要填写

版本2 不支持内置对象 , 但自从Babel 7.4.0 之后,拥有了 @babel/runtime-corejs3 , 我们可以放心使用 corejs: 3 对实例方法做支持

当前的 .babelrc

{
  "presets": [
    ["@babel/preset-env"]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

当前的 index.js

function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
console.log(arr)
console.log( hasNumber(2) )

打包后如下:
2019-12-04-00-01-39

我们看到使用 @babel/plugin-transform-runtime 编译后的代码和之前的 @babel/preset-env 编译结果大不一样了,
它使用了帮助函数, 并且赋予了别名 , 抽出为公共方法, 实现复用。 比如它用了 _Promise 代替了 new Promise , 从而避免了创建全局对象

上面两种方式一起用会怎么样

useage 和 @babel/runtime

useage 和 @babel/runtime 同时使用的情况下比较智能, 并没有引入重复的 polyfill

个人分析原因应该是: babel 的 plugin 比 prset 要先执行, 所以preset-env 得到了 @babel/runtime 使用帮助函数包装后的代码,而 useage 又是检测代码使用哪些新特性来判断的, 所以它拿到手的只是一堆 帮助函数, 自然没有效果了

实验过程如下:

当前index.js


function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
const hasNumber2 = (num) => [4, 5, 6, 7, 8, 9].includes(num)
console.log(arr)
console.log( hasNumber(2))
console.log( hasNumber2(3) )

当前 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "usage",
        "corejs": 3
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

打包结果:

2019-12-04-00-23-24

entry 和 @babel/runtime

跟 useage 的情况不一样, entry 模式下, 在经过 @babel/runtime 处理后不但有了各种帮助函数还引入了许多polyfill, 这就会导致打包体积无情的增大

个人分析: entry 模式下遭遇到入口的 import "core-js" 及就立即替换为当前目标浏览器下所需的所有 polyfill, 所以也就跟 @babel/runtime 互不冲突了, 导致了重复引入代码的问题, 所以这两种方式千万不要一起使用, 二选一即可

实现过程如下:

当前 index.js:

import "core-js"
function test() {
  new Promise()
}
test()
const arr = [1,2,3,4].map(item => item * item)
const hasNumber = (num) => [4, 5, 6, 7, 8].includes(num)
const hasNumber2 = (num) => [4, 5, 6, 7, 8, 9].includes(num)
console.log(arr)
console.log( hasNumber(2))
console.log( hasNumber2(3) )

当前 .babelrc

{
  "presets": [
    ["@babel/preset-env", 
      {
        "useBuiltIns": "entry"
      }
    ]
  ],
  "plugins": [
    [ 
      "@babel/plugin-transform-runtime", {
        "corejs": 3
      }
    ]
  ]
}

当前 .browserslistrc 的目标版本(为了减少打包后的文件行数为又改为chrome 了, 懂那个意思就行)

Chrome > 70

打包结果:

2019

总结

  1. @babel/preset-env 拥有根据 useBuiltIns 参数的多种polyfill实现,优点是覆盖面比较全(entry), 缺点是会污染全局, 推荐在业务项目中使用

    • entry 的覆盖面积全, 但是打包体积自然就大,
    • useage 可以按需引入 polyfill, 打包体积就小, 但如果打包忽略node_modules 时如果第三方包未转译则会出现兼容问题
  2. @babel/runtime 在 babel 7.4 之后大放异彩, 利用 corejs 3 也实现了各种内置对象的支持, 并且依靠 @babel/plugin-transform-runtime 的能力,沙箱垫片和代码复用, 避免帮助函数重复 inject 过多的问题, 该方式的优点是不会污染全局, 适合在类库开发中使用

    上面 1, 2 两种方式取其一即可, 同时使用没有意义, 还可能造成重复的 polyfill 文件

查看原文

赞 26 收藏 11 评论 12

天天修改 发布了文章 · 5月24日

微信小程序swiper的自适应高度

小程序组件swiper需要指定固定高度,但在某些场景中我们需要动态设置swiper的高度以完整展示swiper中的内容,比如高度不同的图片,笔者最近项目中的日历组件(31号有时会多出一行)等等,如何使swiper组件自适应高度呢?

翻阅了一些网上的例子,一般的解决方法是通过设置style.height来解决

<swiper
  style="{{style}}"
>
  <swiper-item></swiper-item>
</swiper>
  Page({
    data: {
      style: ''
    },
    
    onReady(){
      this.setData({style: 'height: 100px'})
    }
  })

问题:状态丢失

无状态内容通过上述设置可以达到要求,但类似于日历(横向滚动)这种需要保持状态的组件,会导致状态丢失(当然可以回填数据,但很麻烦)

在既要保持状态,同时又要动态设置swiper高度的要求下,最好是通过css来解决问题,这是第一印象,这样既能保持无渲染(刷新),然后高度又能定制

一番折腾过后,发现一般的css设置不能解决上述问题,因为不管直接设置swiper高度,或者被容器包裹,设置容器高度,都需要将高度固定,一旦固定高度就达不到要求

解决: CSS变量

在几乎不考虑兼容性(没有大量测试机)的情况下,使用css变量能够很好的解决上述要求,设置css变量不会导致结构重新渲染,属于css范畴的解决方法。

模板

<view class="box" style="{{boxStyle}}">
  <swiper class="container">
    <swiper-item></swiper-item>
  </swiper>
</view>

样式

.box{
  --box-height: 400px;
  --append-height: 0;
  width: 100vw;
  height: calc(var(--box-height) + var(--append-height))
}

.container{
  height: 100%;
  width: 100%;
}

js

Page({
  data: {
    boxStyle: ''
  },

  onReady(){
    if (...) {
      this.setData({boxStyle: '--append-height: 50px'})
    } else {
      this.setData({boxStyle: '--append-height: 0'})
    }
  }
})

上述设置,将动态设置CSS变量(由js控制好)来达到swiper自适应高度,现在我们的项目正在上线中,等待测试出bug,哈哈

欢迎关注github项目
关注下面的小程序查看最新的DEMO示例
xquery.png

查看原文

赞 0 收藏 0 评论 0

天天修改 发布了文章 · 5月20日

Git设置忽略(.gitignore)的一些技巧

记录一些git上碰到的麻烦事

设置忽略文件

git初始化文件夹后,添加.gitignore文件,并将需要忽略的文件或者文件夹填写到其中

/mtk/ # 过滤整个文件夹
*.zip # 过滤所有.zip文件
/mtk/do.c # 过滤某个具体文件

!src/   # 不过滤该文件夹
!*.zip   #不过滤所有.zip文件
!/mtk/do.c # 不过滤该文件

以斜杠/开头表示目录;
以星号*通配多个字符;
以问号?通配单个字符
以方括号[]包含单个字符的匹配列表;
以叹号!表示不忽略(跟踪)匹配到的文件或目录;

已提交的文件如何设置忽略

  1. 清空git catch的记录
git rm --cached User.php  

# 若是文件夹
git rm -r --cached folder/
  1. 清空git已索引的记录
git rm -f  User.php  

3.添加.gitignore记录
接着commit、提交

已忽略的文件如何重新提交

重新添加已经被忽略过的文件时,我们仅仅使用git add是不行的,因为git仓库中根本没有那个文件,这时候我们需要加上-f参数来强制添加到仓库中,然后在提交。比如上面设置了忽略排除的文件TokenGuard.php我们需要重新加入

git add -f /test/framework/src/nate/Auth/TokenGuard.php

接下来移除.gitignore记录
commit、提交

源码

GITHUB
/pages/form

下列小程序DEMO包含下拉菜单、通用型筛选列表、索引列表、markdown(包含表格)、评分组件、水果老虎机、折叠面板、双栏分类导航(左右)、刮刮卡、日历等组件

xquery.png

查看原文

赞 0 收藏 0 评论 0

天天修改 发布了文章 · 4月14日

小程序input的一些坑点

最近在开发中遇到的一些坑点

  1. 表单组件(input)如何阻止冒泡
  2. 在容器(fixed)中的input如何弹出键盘

阻止input冒泡

<view bind:tap="onTap" class="container">
    <input bindinput="onBindInput" type="text"/>
</view>

上例中input操作会冒泡到container,导致onTap响应执行

修正

<view bind:tap="onTap" class="container">
    <input bindinput="onBindInput" type="text" catch:tap="empty"/>
</view>

冒泡的问题是由input的tap事件导致,因此定义一个empty的空方法,使它响应input的catch:tap,来达到阻止input的冒泡的作用

在容器(fixed)中的input如何弹出键盘

<view class="container" style="position: fixed; bottom: 0">
    <input bindinput="onBindInput" type="text"/>
</view>

container组件在屏幕底部出现,点击Input组件时,弹出的键盘会遮盖input输入框

修正

<view class="container" style="position: fixed; bottom: 0; {{mystyle}}">
    <input bindinput="onBindInput" bindkeyboardheightchange="onkeybord" type="text"/>
</view>
Page({
    data: {
        mystyle: '',
    },
    
    onkeybord(e){
        let detail = e.detail
        let kbHeight = detail.height
        let tool = Pager.getElementsById('reminder-tool')
        if (kbHeight === 0) {
          this.setData({
              mystyle: ' '
          })
        }

        if (kbHeight && kbHeight > 0) {
            this.setData({
                mystyle: `bottom: ${kbHeight-40}px;`
            })
        }
    }
})

demo

xquery.png

查看原文

赞 0 收藏 0 评论 0

天天修改 发布了文章 · 3月31日

小程序双头slider选择器

小程序商城项目中,需要设置价格区间,或者在旅游行业项目中需要设置时间区间,双头选择器有比较好的交互效果

image.png

  • 支持设置单头、双头选择器
  • 支持自定义选择头的文字/图片
  • 支持自定义轴
  • 支持开启/关闭提示信息
  • 支持事件绑定

示例代码

https://github.com/webkixi/aotoo-xquery 
=> pages/sslider    

配置说明

wxml

<ui-item item="{{sliderConfig}}" />

js

const Pager = require('../../components/aotoo/core/index')
const mkSslider = require('../../components/modules/sslider/index')
Pager({
  data: {
    rangeValue: '00',
    sliderConfig: mkSslider({
      id: '', 
      max: 10,
      step: 1,
      value: [0, 10],
      blockSize: 30,
      button: [{}, {}],
      content: null,
      bindchange: null,
      bindchanging: null,
      smooth: true,
      tip: true,
      disable: false,
      frontColor: '#ccc',
      backColor: '#2b832b'
    }),
  },
})

配置参数

调用 'mkSslider(param)' 方法生成slider的配置

id
{String} 定义唯一id,可以在onReady中拿到实例

max
{Number} 设置最大值

step
{Number} 设置步进

value
{Array} 设置默认值

blockSize
{Number} 设置选择头的大小,默认30px

button
{Array} 数组长度===1,为单头slider,长度===2为双头slider

content
{String|Object|item} 设置轴内容,默认为一条2px的线段,支持设置文字/图片

bindchange
{Function} touchend绑定事件,返回value数组

bindchanging
{Function} touchmove绑定事件,实时返回value

smooth
{Boolean} touchmove时是否平滑渲染 默认 false

tip
{Boolean} 是否显示提示

disable
{Boolean} 是否设置无效,无效后不能有任何操作

frontColor
{String} 前景色,轴线条选中后的颜色,默认 #ccc

backColor
{String} 背景色,轴线条默认颜色,默认 #2b832b

如何使用

如何获取实例

Pager({
  data: {
    slideConfig: mkSslider({ id: 'abc' })
  },
  onReady() {
    console.log(this.abc.value)
  }
})

设置最大值/步进值

slideConfig: mkSslider({
  max: 1000,
  step: 50,
  value: [0, 1000]
})

设置提示tip

slideConfig: mkSslider({
  tip: true
})

设置默认值

slideConfig: mkSslider({
  value: [3, 8]
})

设置指示器大小

slideConfig: mkSslider({
  blockSize: 40 // 默认30
})

设置指示器文字/图片

slideConfig: mkSslider({
  button: ['爽', {img: {src: '/images/chat.png', itemStyle: 'width: 30px; border-raduis: 50%;'}}]
})

设置无效

slideConfig: mkSslider({
  disable: true // 默认有效
})

设置前景色/背景色

slideConfig: mkSslider({
  frontColor: 'red',
  backColor: 'blue'
})

设置绑定方法

slideConfig: mkSslider({
  bindchange() {}, // touchend响应
  bindchanging() {} // touchmove响应
})

如何设置单头slider

slideConfig: mkSslider({
  button: [{}]
})

GITHUB源码

示例小程序

xquery.png

查看原文

赞 0 收藏 0 评论 0

天天修改 发布了文章 · 3月31日

功能完善的小程序日历组件

小程序日历组件

日历组件,表单组件绝逼是前端开发的一个噩梦,尤其要做好一个旅游项目的日历,变态需求特别多,要在小程序中实现携程app的日历,还要兼顾性能问题。

image.png

  • 自定义横向/纵向日历
  • 自定义区间大小
  • 自定义日期内容
  • 指定节假日
  • 支持跨月显示

难点

  • 懒加载保证渲染性能
  • 通过配置实现纵向日历和横向日历
  • 阳历节日与农历节日与节气
  • 交互,尤其是区域选择的交互

示例代码

https://github.com/webkixi/aotoo-xquery 
=> pages/calendar    

配置说明

wxml

<ui-calendar dataSource="{{config}}" />

js

基本用法

const Pager = require('../../components/aotoo/core/index')
Pager({
  data: {
    config: {
      $$id: 'calendar',
      mode: 1,  // 纵向日历
      type: 'range',  // 区域选择
      tap: 'onTap', // page响应事件
      total: 365, // 定义从今天开始一年事件
      rangeCount: 28,  // 区选区间28天
      festival: true, // 开启节假日显示
      value: ['2019-12-24', '2020-01-05'],  // 默认值
      methods: { 
        // 响应 tap事件
        onTap(e, param, inst) {
          if (param.range === 'start') {
            inst.update({dot: [{title: '入住'}]})
          }
          if (param.range === 'end') {
            inst.update({dot: [{title: '离店'}]})
            setTimeout(() => {
              Pager.alert('离店,跳回页面')
            }, 1000);
          }
          console.log(param);
        }
      }
    }
  }
})

$$id
{String} 配置实例的Id

mode
{Number} 设置日历的展示模式,1=纵向日历 2=横向日历

type
{Number} single=单选日历, range=选择区间, multiple=多选日历

total
{Number} 设置日历从今天开始起需要跨多少天,如 180天,或者365天

start
{String} 设置起始日期,如:'2020-06-05'

date
{Object|Function} 定义附加日期内容

disable
{Boolean} 设置全局无效,所有日期均不能交互,权重低于单个日期设置的disable

rangeCount
{Number} 当type === 'range'时,rangeCount为区间大小,意味着区间允许选择多少天

rangeMode
{Number} 当正在做日期区间选择时,是否允许显示angeCount之外的日期 1=显示, 2=不显示

tap
{String} 响应日期元素的tap事件

value
{Array} 默认选中的日期,允许数组为空,如果type='single'则应该设置如['2020-06-05'],type='range'应该设置如['2020-06-03', '2020-06-05'], type='multiple'时,数组允许多值

data
{Array} 该数据会自动计算日期跨度数量(允许跨年设置),如果设置了该数据,则total无效,如设置为['2019-11-05', '2021-11-05'],自动计算日期为730天

festival
{Boolean|Array} 设置日历假期显示,支持显示指定假期

toolbox
{Object} 日历的扩展配置,允许设置一些高级功能,如日历是否允许跨月,特殊的range算法等等

toolbox.header
{Boolean} 是否显示日历的头部,一般用于横向日历时为true

toolbox.monthHeader
{Boolean} 是否显示日历的月头部,一般在纵向日历时为true

toolbox.rangeEdge
{Function} 默认值null,type='range'时,自定义range选择算法

toolbox.discontinue
{Boolean} 默认false,当日历有data数组构建时,缺少数据的月份会被忽略

如何设置

设置横向、纵向日历

let calenderConfig = {
  $$id: 'calendar',
  mode: 2, // 1,纵向日历 2,横向日历
  type: 'single', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  total: 180, // 所有日期选择天数
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
}

设置区间选择日历

该示例配置为仿携程的功能设置

let calendarConfig = {
  $$id: 'calendar',  //实例id
  mode: 1,  // 纵向日历
  type: 'range',  // 区间选择日历
  tap: 'onTap', // tap响应方法
  total: 365,  // 指定日历从今天开始总天数
  rangeCount: 28, // 区间范围
  rangeMode: 1, // 区间选择是否隐藏非区间的月份
  festival: true, // 是否显示节假日
  value: ['2020-04-03', '2020-04-09'],  // 默认值
  methods: { 
    // 定义响应方法  
    onTap(e, param, inst) {
      
      if (param.range === 'start') {  // 第一次点击时
        inst.update({dot: [{title: '入住'}]})
      }
      if (param.range === 'end') { // 第二次点击时
        inst.update({dot: [{title: '离店'}]})
      }
      console.log(param);
    }
  }
}

设置多选日历

支持选中多个日期

let calenderConfig = {
  $$id: 'calendar',
  mode: 2,
  type: 'multiple', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  total: 180, // 所有日期选择天数
  value: ['2020-04-03', '2020-04-09', '2020-04-10'],
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
}

据已知日期自动构建

此例中total无效,由两个给定的日期构建了三个月的日历

let calenderConfig = {
  $$id: 'calendar',
  mode: 2, // 1,纵向日历 2,横向日历
  type: 'single', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  total: 180, // 所有日期选择天数,此例中无效  
  data: [{"date":"2020-04-03"}, {"date":"2020-06-03"}],
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
},

根据已知日期自动构建,忽略无数据月份

此例中total无效, 由两个给定的日期构建了三个月的日历

let calenderConfig = {
  $$id: 'calendar',
  mode: 2, // 1,纵向日历 2,横向日历
  type: 'single', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  total: 180, // 所有日期选择天数,此例中无效
  data: [{"date":"2020-04-03"}, {"date":"2020-06-03"}],
  toolbox: {
    discontinue: true // 允许构建跨月日历
  },
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
},

构建节假日日历

允许指定节假日,指定节假日内容

  • festival: true

显示所有组件自带节日

  • festival: ['元旦节', '情人节', '劳动节', '冬至']

显示指定假日

  • festival: [{title: '春节', content: {dot: ['新年好']}}]

显示指定节日,并附加内容

let calenderConfig = {
  $$id: 'calendar',
  mode: 1, // 1,纵向日历 2,横向日历
  type: 'single', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  data: [{"date":"2020-09-03"}, {"date":"2020-12-28"}],
  festival: ['教师节', '圣诞节'],
  toolbox: {
    discontinue: true // 允许忽略无数据月份
  },
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
},

自定义日期内容

自定义日期内容有两种方法

  • 在data数据配置中加入'dot'数组属性

config.data = [{date: '2020-03-03', content: {dot: ['内容']}}]

  • 在date属性中配置
// 配置所有日期的附加内容   
config.date = {dot: ['自定义内容']}  

// 指定日期内容配置   
config.date = function(param){
  // 通过param的属性写逻辑 param.date, param.year, param.month, param.day ...
  if (param.date === '2020-8-13') {
    param.dot = ['附加内容']
    return param
  }
}

设置示例

let calenderConfig = {
  $$id: 'calendar',
  mode: 2, // 1,纵向日历 2,横向日历
  type: 'single', // single:单选  range: 区间选择  multiple:多选
  tap: 'onTap', // 回调事件
  date: function(param){
    if (param.month === 12 && param.day === 26) {
      param.dot = ['毛主席诞辰']
      return param
    }
    if (param.month === 9 && param.day === 10) {
      param.dot = [
        {title: '生日', itemStyle: 'font-size: 11px; color: blue;'},
        {title: '骗你的', itemStyle: 'font-size: 11px; color: #666'},
      ]
      return param
    }
    if (param.month === 9 && param.day === 20) {
      param.dot = [
        {title: '无效日期', itemStyle: 'font-size: 12px; color: red;'},
        {title: '不能交互', itemStyle: 'font-size: 12px; color: #666;'},
      ]
      param.disable = true
      return param
    }
  },
  toolbox: {
    discontinue: true
  },
  data: [{"date":"2020-09-03"}, {"date":"2020-12-28"}],
  methods: { // 响应方法
    onTap(e, param, inst) {
      console.log(param);
    }
  }
},

GITHUB源码

示例小程序

xquery.png

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 7 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • aotoo-xquery

    小程序开发工具库,UI库

  • aotoo-hub

    前端全栈构建工具

  • FKP-REST

    基于react,koa,webpack等等的全站JS框架

  • FKP-RN

    FKP-RN是一套兼容android和ios的react-native架构

  • FKP-RN-ROUTER

    FKP-RN-ROUTER是一个RN用的router库,通过npm install 安装

注册于 2015-06-03
个人主页被 215 人浏览