搞轮子:TabBar标签栏的自救

说到 TabBar 标签栏,我们自然会想到放在 App 底部的标签栏。同时,因为之前开发 Tabs 标签页时,曾经写过一篇文章( 组件开发:从业务到组件Tabs ),讲诉开发 Tabs 的问题和困难。现在遇到的 TabBar 与之大差不差,为什么还要写一篇呢?

原因有二:一是为回顾知识,二是 TabBar 有必要写的知识点

无论是 Tabs 还是 TabBar 组件,都采用父组件控制标签和改变标签,子项提供具体内容的形式

// Tabs
<Tabs value={value}
    onChange={(index) => {
        index && setValue(index);
    }}
    >
    <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
    <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
    <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>
// TabBar
<TabBar
    activeKey={activeKey}
    onChange={(key: any) => {
        setActiveKey(key);
    }}
    >
    <TabBar.Item
        itemKey="home"
        title="主页"
        icon={<IconTabbarHome />}
        />
    <TabBar.Item
        itemKey="financial"
        title="理财"
        icon={<IconTabbarFinancial />}
        />
    <TabBar.Item
        itemKey="user"
        title="我的"
        icon={<IconTabbarUser />}
        />
</TabBar>

思绪源码

他们都用了 React.Children ,这是我们要回顾的。

官网:React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

而 TabBar 不同的是,他多了 React.cloneElementReact.isValidElement

React.cloneElement 介绍

React.cloneElement(element, [config], [...children]);

官网:以 element 元素为样板克隆并返回新的 React 元素,config 中应包含新的 props,key 或 ref 。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,如果在 config 中未出现 key 或 ref,那么原始元素的 key 和 ref 将被保留

简单来说,他是个拷贝API,一般与 React.Children 配合,在原 children 上加上其他属性

React.isValidElement 介绍

React.isValidElement(object);

官网:验证对象是否为 React 对象,返回值为 true 或 false

回过头看 TabBar。我们要做的是,不仅要赋予 TabBar.Item 本来就要有的属性,而且要多给它几个属性,如:

  • selected:判断它是否被选中。因为 TabBar 控制选择的 key
  • onChange:点击后的回调,这里要做”是否能切换标签“的判断,所以要处理

结合三个 React 的顶层 API 后的代码:

...
const items = React.Children.map(children, (element, index) => {
    if (!React.isValidElement(element)) return null;
    return cloneElement(element, {
        key: index,
        title: element.props.title,
        icon: element.props.icon,
        itemKey: element.props.itemKey || index,
        className: element.props.className,
        style: element.props.style,
        selected: getSelected(index, element.props.itemKey),
        onChange: () => onChildChange(element.props.itemKey),
    });
});
...

具体代码可以去看 jing-ui 的源代码,这里做分析,先把它所有的子项都遍历,判断它是否是 React 元素,如果不是,返回 null,如果是拷贝子项原来的数据,并给予两个新的 props。其中 getSelected 代码如下:

const getSelected = (index: number, itemKey: string | number) => {
  if (!activeKey) {
    if (!defaultActiveKey && index === 0) {
      return true;
    }
    return defaultActiveKey === itemKey;
  }
  return activeKey === itemKey;
};

传入当前子项的索引值以及子项(TabBar.Item)的 itemKey 值,判断 TabBar 的 属性 activeKey 是否不存在,再判断 defaultActiveKey是否有或者索引值是否为0,如果都满足,说明默认选中的key没有,那就赋予第一个子项选中;如果 defaultActiveKey 有,就与 itemKey 匹配;再如果 activeKey 存在,就让它与 itemKey 匹配。其实看代码就能明白

onChildChange 代码:

const onChildChange = (value: string | number) => {
  if (isFunction(beforeChange)) {
    let canClick = beforeChange(value);
    if (canClick) {
      canClick.then(() => {
        if (typeof onChange === "function") {
          onChange(value);
        }
      });
    }
    return;
  }
  if (isFunction(onChange)) {
    onChange(value);
  }
};

beforeChange 属性指:切换标签前的回调函数,返回 false 课阻止切换

安全区域的解决

两种方案

一是塞 padding-bottom 样式

  • .jing-safe-area-bottom {
      padding-bottom: constant(safe-area-inset-bottom);
      padding-bottom: env(safe-area-inset-bottom);
    }
    
  • 这个方法在 TabBar 上无效,因为 TabBar 注定是底部固定的

二是包个容器,给它一个 height

  • .iphonex-extra-height {
      height: constant(safe-area-inset-bottom);
      height: env(safe-area-inset-bottom);
    }
    
  • 这个方法解决了

在 props 上我们相对应提供

  • enableSafeArea:是否开启底部安全区适配,设置 fixed 时默认开启
  • fixed:是否固定在底部,默认固定

具体代码如下:

const enableSafeArea = () => safeAreaInsetBottom ?? fixed;
if (enableSafeArea()) {
    return (
        {/* 提供父容器包裹 */}
        <div className={classnames(`${prefixCls}-container`)}>
            <div className={classnames(prefixCls, className)}>{items}</div>
            <div className="jing-iphonex-extra-height" />
        </div>
    );
}

总结

TabBar 与 Tabs 的不同之处在于,它用了 React.cloneElement,赋予了 (TabBar 中的)children 新的属性,这样我们就能再 TabBar 上控制它是否在切换前使用回调函数,方便我们后续实际业务中的操作

芝麻开门,显示全文!

从 Redux 说起,到手写,再到状态管理

学习一个东西之前,首先在大脑中积累充分的“疑惑感”。即弄清面临的问题到底是什么,再浏览方法本身之前,最好先使劲问问都想到什么方法。一个公认的事实是,你对问题的疑惑越大,在之前做的自己的思考越多,当看到解答之后印象就越深刻

先说结论

  1. Redux 是状态管理库,也是一种架构
  2. Redux 与 React 无关,但它是为了解决 React 组件中状态无法共享而出的一种解决方案
  3. 单纯的 Redux 只是一个状态机, store 中存放了所有的状态 state,要想改变里面的状态 state,只能 dispatch 一个动作
  4. 发出去的 action 需要用 reducer 来处理,传入 state 和 action,返回新的 state
  5. subscribe 方法可以注册回调方法,当 dispatch action 的时候会执行里面的回调
  6. Redux 其实是一个发布订阅模式
  7. Redux 支持 enhancer,enhancer 其实就是一个装饰器模式,传入当前的 createStore,返回一个增强的 createStore
  8. Redux 使用 applyMiddleware 函数支持中间件,它的返回值其实就是一个 enhancer
  9. Redux 的中间件也是一个装饰器模式,传入当前的 dispatch,返回一个增强了的 dispatch
  10. 单纯的 Redux 是没有 View 层的

为什么出现 Redux?

我们默认使用 React 技术栈,当页面少且简单时,完全没必要使用 Redux。Redux 的出现,是为了应对复杂组件的情况。即当组件复杂到三层甚至四层时(如下图),组件 4 想改变组件 1 的状态

react 组件树

按照 React 的做法,状态提升,将状态提升至同一父组件(在图中为祖父组件)。但层级一多,根组件要管理的 state 就很多了,不方便管理。

所以当初有了 context(React 0.14 确定引入),通过 context 能实现”远房组件“的数据共享。但它也有缺点,使用 context 意味着所有的组件都可以修改 context 里面的状态,就像谁都可以修改共享状态一样,导致程序运行的不可预测,这不是我们想要的

facebook 提出了 Flux 解决方案,它引入了单向数据流的概念(没错,React 没有单向数据流的概念,Redux 是集成了 Flux 的单向数据流理念),架构如下图所示:

Flux 流程图

这里不表 Flux。简单理解,在 Flux 架构中,View 要通过 Action (动作)通知 Dispatcher(派发器),Dispatcher 来修改 Store,Store 再修改 View

Flux 的问题或者说缺点在哪?

store 之间存在依赖关系、难以进行服务器端渲染、 stores 混杂了逻辑和状态

笔者在学习的 React 技术栈时是 2018 年,那是已然流行 React + Redux 的解决方案,Flux 已经被淘汰了,了解 Flux 是为了引出 Redux

Redux 的出现

Redux 主要解决状态共享问题

官网:Redux 是 JavaScript 状态容器,它提供可预测的状态管理

它的作者是 Dan Abramov

其架构为:

Redux 流程图

可以看得出,Redux 只是一个状态机,没有 View 层。其过程可以这样描述:

  • 自己写一个 reducer(纯函数,表示做什么动作会返回什么数据)
  • 自己写一个 initState(store 初始值,可写可不写)
  • 通过 createStore 生成 store,此变量包含了三个重要的属性
    • store.getState:得到唯一值(使用了闭包老哥)
    • store.dispatch:动作行为(改变 store 中数据的唯一指定属性)
    • store.subscribe:订阅(发布订阅模式)
  • 通过 store.dispatch 派发一个 action
  • reducer 处理 action 返回一个新的 store
  • 如果你订阅过,当数据改变时,你会收到通知

按照行为过程,我们可手写一个 Redux,下文在表,先说特点

三大原则

  • 单一数据源
    • 整个应用的 全局 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中
  • State 是只读的
    • 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生时间的普通对象
  • 使用纯函数来执行修改
    • 为了描述 action 如何改变 state tree,你需要编写纯的 reducers

三大原则是为了更好地开发,按照单向数据流的理念,行为变得可回溯

让我们动手写一个 Redux 吧

手写 redux

按照行为过程和原则,我们要避免数据的随意修改、行为的可回溯等问题

基础版:23 行代码让你使用 redux

export const createStore = (reducer, initState) => {
  let state = initState;
  let listeners = [];

  const subscribe = (fn) => {
    listeners.push(fn);
  };

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach((fn) => fn());
  };

  const getState = () => {
    return state;
  };

  return {
    getState,
    dispatch,
    subscribe,
  };
};

搞个测试用例

import { createStore } from "../redux/index.js";

const initState = {
  count: 0,
};

const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        ...state,
        count: state.count + 1,
      };
    case "DECREMENT":
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

const store = createStore(reducer, initState);

store.subscribe(() => {
  let state = store.getState();
  console.log("state", state);
});

store.dispatch({
  type: "INCREMENT",
});

PS:俺是在 node 中使用 ES6 模块,需要升级 Node 版本至 13.2.0

第二版:难点突破:中间件

普通的 Redux 只能做最基础地根据动作返回数据,dispatch 只是一个取数据的命令,例如:

dispatch({
  type: "INCREMENT",
});
// store 中的 count + 1

但在开发中,我们有时候要查看日志、异步调用、记录日常等

怎么办,做成插件

在 Redux 中,类似的概念叫中间件

中间件

Redux 的 createStore 共有三个参数

createStore([reducer], [initial state], [enhancer]);

第三个参数为 enhancer,意为增强器。它的作用就是代替普通的 createStore,转变成为附加上中间件的 createStore。打几个比方:

  • 托尼·斯塔克本来是一个普通有钱人,加上增强器(盔甲)后,成了钢铁侠
  • 中央下发一笔救灾款,加上增强器(大小官员的打点)后,到灾民手上的钱只有一丢丢
  • 路飞用武装色打人,武装色就是一个中间件

enhancer 要做的就是:东西还是那个东西,只是经过了一些工序,加强了它。这些工序由 applyMiddleware 函数完成。按照行业术语,它是一个装饰器模式。它的写法大致是:

applyMiddleware(...middlewares);
// 结合 createStore,就是
const store = createStore(reudcer, initState, applyMiddleware(...middlewares));

所以我们需要先对 createStore 进行改造,判断当有 enhancer 时,我们需传值给中间件

export const createStore = (reducer, initState, enhancer) => {
    if (enhancer) {
        const newCreateStore = enhancer(createStore)
        return newCreateStore(reducer, initState)
    }

	let state = initState;
    let listeners = [];
    ...
}

如果有 enhancer 的话,先传入 createStore 函数,生成的 newCreateStore 和原来的 createStore 一样,会根据 reducer, initState 生成 store。可简化为:

if (enhancer) {
  return enhancer(createStore)(reducer, initState);
}

PS:为什么要写成这样,因为 redux 是按照函数式写法来写的

为什么 createStore 可以被传值,因为函数也是对象,也可以作为参数传递(老铁闭包了)

这样我们的 applyMiddleware 自然就明确了

const applyMiddleware = (...middlewares) => {
    return (oldCreateStore) => {
        return (reducer, initState) => {
            const store = oldCreateStore(reducer, initState)
            ...
        }
    }
}

这里的 store 表示的是普通版中的 store,接下来我们要增强 store 中的属性

我愿称之为:五行代码让女人为我花了 18 万

export const applyMiddleware = (...middlewares) => {
  return (oldCreateStore) => {
    return (reducer, initState) => {
      const store = oldCreateStore(reducer, initState);
      // 以下为新增
      const chain = middlewares.map((middleware) => middleware(store));
      // 获得老 dispatch
      let dispatch = store.dispatch;
      chain.reverse().map((middleware) => {
        // 给每个中间件传入原派发器,赋值中间件改造后的dispatch
        dispatch = middleware(dispatch);
      });
      // 赋值给 store 上的 dispatch
      store.dispatch = dispatch;
      return store;
    };
  };
};

现在写几个中间件来测试一下

// 记录日志
export const loggerMiddleware = (store) => (next) => (action) => {
  console.log("this.state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};

// 记录异常
export const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (error) {
    console.log("错误报告", error);
  }
};

// 时间戳
export const timeMiddleware = (store) => (next) => (action) => {
  console.log("time", new Date().getTime());
  next(action);
};

引入项目中,并运行

import { createStore, applyMiddleware } from "../redux/index.js";
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from "./middleware.js";

const initState = {
  count: 0,
};

const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        ...state,
        count: state.count + 1,
      };
    case "DECREMENT":
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware)
);

store.subscribe(() => {
  let state = store.getState();
  console.log("state", state);
});

store.dispatch({
  type: "INCREMENT",
});

运行发现已经实现了 redux 最重要的功能——中间件

测试代码

来分析下中间件的函数式编程,以 loggerMiddleware 为例:

export const loggerMiddleware = (store) => (next) => (action) => {
  console.log("this.state", store.getState());
  console.log("action", action);
  next(action);
  console.log("next state", store.getState());
};

在 applyMiddleware 源码中,

const chain = middlewares.map((middleware) => middleware(store));

相当于给每个中间件传值普通版的 store

let dispatch = store.dispatch;
chain.reverse().map((middleware) => (dispatch = middleware(dispatch)));

相当于给每个中间件在传入 store.dispatch,也就是 next,原 dispatch = next。这个时候的中间件已经本成品了,代码中的 (action) => {...} 就是函数 const dispatch = (action) => {}。当你执行 dispatch({ type: XXX }) 时执行中间件这段(action) => {...}

PS:柯里化一开始比较难理解,用多习惯就慢慢能懂

第三版:结构复杂化与拆分

中间件理解起来或许有些复杂,先看看其他的概念换换思路

一个应用做大后,单靠一个 JavaScript 文件来维护代码显然是不科学的,在 Redux 中,为避免这类情况,它提供了 combineReducers 来整个多个 reducer,使用方法如:

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
});

combinReducers 中传入一个对象,什么样的 state 对应什么样的 reducer。这就好了,那么 combinReducers 怎么实现呢?因为比较简单,不做多分析,直接上源码:

export const combinReducers = (...reducers) => {
  // 拿到 counter、info
  const reducerKey = Object.keys(reducers);
  // combinReducers 合并的是 reducer,返回的还是一个 reducer,所以返回一样的传参
  return (state = {}, action) => {
    const nextState = {};
    // 循环 reducerKey,什么样的 state 对应什么样的 reducer
    for (let i = 0; i < reducerKey.length; i++) {
      const key = reducerKey[i];
      const reducer = reducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      nextState[key] = nextStateForKey;
    }
    return nextState;
  };
};

同级目录下新建一个 reducer 文件夹,并新建 reducer.jsinfo.jsindex.js

// reducer.js
export default (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        count: state.count + 1,
      };
    case "DECREMENT": {
      return {
        count: state.count - 1,
      };
    }
    default:
      return state;
  }
};
// info.js
export default (state, action) => {
  switch (action.type) {
    case "SET_NAME":
      return {
        ...state,
        name: action.name,
      };
    case "SET_DESCRIPTION":
      return {
        ...state,
        description: action.description,
      };
    default:
      return state;
  }
};

合并导出

import counterReducer from "./counter.js";
import infoReducer from "./info.js";

export { counterReducer, infoReducer };

我们现在测试一下

import {
  createStore,
  applyMiddleware,
  combinReducers,
} from "../redux/index.js";
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from "./middleware.js";
import { counterReducer, infoReducer } from "./reducer/index.js";

const initState = {
  counter: {
    count: 0,
  },
  info: {
    name: "johan",
    description: "前端之虎",
  },
};

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
});

const store = createStore(
  reducer,
  initState,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware)
);

store.dispatch({
  type: "INCREMENT",
});

combinReducers 也完成了

测试代码

既然拆分了 reducer,那么 state 是否也能拆分,并且它是否需要传,在我们平时的写法中,一般都不传 state。这里需要两点改造,一是每个 reducer 中包含了它的 state 和 reducer;二是改造 createStore,让 initState 变得可传可不传,以及初始化数据

// counter.js 中写入对应的 state 和 reducer
let initState = {
  counter: {
    count: 0,
  },
};

export default (state, action) => {
  if (!state) {
    state = initState;
  }
  switch (action.type) {
    case "INCREMENT":
      return {
        count: state.count + 1,
      };
    case "DECREMENT": {
      return {
        count: state.count - 1,
      };
    }
    default:
      return state;
  }
};
// info.js
let initState = {
  info: {
    name: "johan",
    description: "前端之虎",
  },
};

export default (state, action) => {
  if (!state) {
    state = initState;
  }
  switch (action.type) {
    case "SET_NAME":
      return {
        ...state,
        name: action.name,
      };
    case "SET_DESCRIPTION":
      return {
        ...state,
        description: action.description,
      };
    default:
      return state;
  }
};

改造 createStore

export const createStore = (reducer, initState, enhancer) => {

    if (typeof initState === 'function') {
        enhancer = initState;
        initState = undefined
    }
    ...
    const getState = () => {
        return state
    }
	// 用一个不匹配任何动作来初始化store
    dispatch({ type: Symbol() })

    return {
        getState,
        dispatch,
        subscribe
    }
}

主文件中

import { createStore, applyMiddleware, combinReducers } from "./redux/index.js";
import {
  loggerMiddleware,
  exceptionMiddleware,
  timeMiddleware,
} from "./middleware.js";
import { counterReducer, infoReducer } from "./reducer/index.js";

const reducer = combinReducers({
  counter: counterReducer,
  info: infoReducer,
});

const store = createStore(
  reducer,
  applyMiddleware(loggerMiddleware, exceptionMiddleware, timeMiddleware)
);

console.dir(store.getState());

到此为止,我们已经实现了一个七七八八的 redux 了

完整体的 Redux

退订

const subscribe = (fn) => {
  listeners.push(fn);
  return () => {
    const index = listeners.indexOf(listener);
    listeners.splice(index, 1);
  };
};

中间件拿到的 store

现在的中间件能拿到完整的 store,他甚至可以修改我们的 subscribe 方法。按照最小开放策略,我们只用给 getState 即可,修改下 applyMiddleware 中给中间件传的 store

// const chain = middlewares.map(middleware => middleware(store))
const simpleStore = { getState: store.getState };
const chain = middlewares.map((middleware) => middleware(simpleStore));

compose

在我们的 applyMiddleware 中,把 [A, B, C] 转换成 A(B(C(next))),效果是:

const chain = [A, B, C];
let dispatch = store.dispatch;
chain.reverse().map((middleware) => {
  dispatch = middleware(dispatch);
});

Redux 提供了一个 compose ,如下

const compose = (...funcs) => {
  if (funcs.length === 0) {
    return (args) => args;
  }
  if (funcs.length === 1) {
    return funcs[0];
  }
  return funcs.reduce(
    (a, b) =>
      (...args) =>
        a(b(...args))
  );
};

2 行代码 replaceReducer

替换当前的 reudcer ,使用场景:

  • 代码分割
  • 动态加载
  • 实时 reloading 机制
const replaceReducer = (nextReducer) => {
  reducer = nextReducer;
  // 刷新一次,广播 reducer 已经替换,也同样把默认值换成新的 reducer
  dispatch({ type: Symbol() });
};

bindActionCreators

bindActionCreators 是做什么的,他通过闭包,把 dispatch 和 actionCreator 隐藏起来,让其他地方感知不到 redux 的存在。一般与 react-redux 的 connect 结合

这里直接贴源码实现:

const bindActionCreator = (actionCreator, dispatch) => {
  return function () {
    return dispatch(actionCreator.apply(this, arguments));
  };
};

export const bindActionCreators = (actionCreators, dispatch) => {
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch);
  }

  if (typeof actionCreators !== "object" || actionCreators === null) {
    throw new Error();
  }

  const keys = Object.keys(actionCreators);
  const boundActionCreators = {};
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const actionCreator = actionCreators[key];
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators;
};

以上,我们就已经完成了 Redux 中所有的代码。大体上这里 100 多行的代码就是 Redux 的全部,真 Redux 无非是加了些注释和参数校验

总结

我们把与 Redux 相关的名词列出来,梳理它是做什么的

  • createStore
    • 创建 store 对象,包含 getState、dispatch、subscribe、replaceReducer
  • reducer
    • 纯函数,接受旧的 state、action,生成新的 state
  • action
    • 动作,是一个对象,必须包括 type 字段,表示 view 发出通知告诉 store 要改变
  • dispatch
    • 派发,触发 action ,生成新的 state。是 view 发出 action 的唯一方法
  • subscribe
    • 订阅,只有订阅了,当派发时,会执行订阅函数
  • combineReducers
    • 合并 reducer 成一个 reducer
  • replaceReudcer
    • 代替 reducer 的函数
  • middleware
    • 中间件,扩展 dispatch 函数

砖家曾经画过一张关于 Redux 的流程图

流程图

换种思考方式理解

我们说过, Redux 只是一个状态管理库,它是由数据来驱动,发起 action,会引发 reducer 的数据更新,从而更新到最新的 store

与 React 结合

拿着刚做好的 Redux,放到 React 中,试试什么叫 Redux + React 集合,注意,这里我们先不使用 React-Redux,单拿这两个结合

先创建项目

npx create-react-app demo-5-react

引入手写的 redux 库

App.js 中引入 createStore,并写好初始数据和 reducer,在 useEffect 中监听数据,监听好之后当发起一个 action 时,数据就会改变,看代码:

import React, { useEffect, useState } from "react";
import { createStore } from "./redux";
import "./App.css";

const initState = {
  count: 0,
};

const reducer = (state, action) => {
  switch (action.type) {
    case "INCREMENT":
      return {
        ...state,
        count: state.count + 1,
      };
    case "DECREMENT":
      return {
        ...state,
        count: state.count - 1,
      };
    default:
      return state;
  }
};

const store = createStore(reducer, initState);

function App() {
  const [count, setCount] = useState(store.getState().count);

  useEffect(() => {
    const unsubscribe = store.subscribe(() => {
      setCount(store.getState().count);
    });
    return () => {
      if (unsubscribe) {
        unsubscribe();
      }
    };
  }, []);

  const onHandle = () => {
    store.dispatch({
      type: "INCREMENT",
    });
    console.log("store", store.getState().count);
  };
  return (
    <div className="App">
      <div>{count}</div>
      <button onClick={onHandle}>add</button>
    </div>
  );
}

export default App;

点击 button 后,数据跟着改变

效果图

PS:虽然我们可以用这种方式订阅 store 和改变数据,但是订阅的代码重复过多,我们可以用高阶组件将他提取出去。这也是 React-Redux 所做的事情

与原生 JS+HTML 结合

我们说过,Redux 是个独立于 Redux 的存在,它不仅可在 Redux 充当数据管理器,还可以在原生 JS + HTML 中充当起职位

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="container">
      <div id="count">1</div>
      <button id="btn">add</button>
    </div>
    <script type="module">
      import { createStore } from "./redux/index.js";

      const initState = {
        count: 0,
      };

      const reducer = (state, action) => {
        switch (action.type) {
          case "INCREMENT":
            return {
              ...state,
              count: state.count + 1,
            };
          case "DECREMENT":
            return {
              ...state,
              count: state.count - 1,
            };
          default:
            return state;
        }
      };

      const store = createStore(reducer, initState);

      let count = document.getElementById("count");
      let add = document.getElementById("btn");
      add.onclick = function () {
        store.dispatch({
          type: "INCREMENT",
        });
      };
      // 渲染视图
      function render() {
        count.innerHTML = store.getState().count;
      }
      render();
      // 监听数据
      store.subscribe(() => {
        let state = store.getState();
        console.log("state", state);
        render();
      });
    </script>
  </body>
</html>

效果如下:

效果图

状态生态

我们从 Flux 说到 Redux,再从 Redux 说了各种中间件,其中 React-saga 就是为解决异步行为而生的中间件,它主要采用 Generator(生成器)概念,比起 React-thunk 和 React-promise,它没有像其他两者将异步行为放在 action creator 上,而是把所有的异步操作看成“线程”,通过 action 触发它,当操作完成后再次发出 action 作为输出

function* helloWorldGenerator() {
  yield "hello";
  yield "world";
  yield "ending";
}

const helloWorld = helloWorldGenerator();

hewlloWorld.next(); // { value: 'hello', done: false }
hewlloWorld.next(); // { value: 'world', done: false }
hewlloWorld.next(); // { value: 'ending', done: true }
hewlloWorld.next(); // { value: undefined, done: true }

简单来说:遇到 yield 表达式,就暂停执行后面的操作,并将紧跟 yield 后面的那个表达式的值,作为返回值 value,等着下一个调用 next 方法,再继续往下执行

Dva

Dva 是什么?

官网:Dva 首先是一个基于 Redux + Redux-saga 的数据流方案。为了简化开发体验,Dva 还额外内置了 react-router 和 fetch,所以可以理解为一个轻量级的应用框架

简单来说,它是整合了现在最流行的数据流方案,即一个 React 技术栈:

dva = React-Router + Redux + Redux-saga + React-Redux

它的数据流图为:

Dva 流程图

view dispatch 一个动作,改变 state(即 store),state 与 view 绑定,响应 view

其他不表,可去 Dva 官网查看,这里讲讲 Model ,它包含了 5 个属性

  • namespace
    • model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 . 的方式创建多层命名空间
  • state
    • 初始值
  • reducers
    • 纯函数,以 key/value 格式定义 reducer。用于处理同步擦做,唯一可以修改 state 的地方,由 action 触发
    • 格式为:(state, action) => newState[(state, action) => newState, enhancer]
  • effects
    • 处理异步操作和业务逻辑,以 key/value 格式定义 effect
    • 不直接修改 state。由 action 触发
    • call:执行异步操作
    • put:发出一个 Action,类似于 dispatch
  • subscriptions
    • 订阅
    • app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 链接、 keyboard 输入、history 路由变化、geolocation 变化等等

Mobx

View 通过订阅也好,监听也好,不同的框架有不同的技术,总之 store 变化, view 也跟着变化

Mobx 使用的是响应式数据流方案。后续会单独写一篇,此篇太长,先不写

补充:单向数据流

先介绍 React 中数据传递,即通信问题

  • 向子组件发消息
  • 向父组件发消息
  • 向其他组件发消息

React 只提供了一种通信方式:传参。

即父传值给子,子不能修改父传的数据,props 具有不可修改性。子组件想把数据传给父组件怎么办?通过 props 中的事件来传值通知父组件

仓库地址:https://github.com/johanazhu/jo-redux

芝麻开门,显示全文!

三句话测试你是否懂git

同事和组长的一番对话引起了笔者对 git 的思考

先介绍一下我司小工坊式的 git 提交流程,本地打包,删除 dist 文件,重建 dist 文件,git add .git commit -m 'XX'git push origin 分支名

和传统公司的 git 提交不同,我司打包是本地打包,而且是把 dist 文件直接上传到仓库

事故现象

同事把代码推上去后,浏览器访问的还是原来的 js 和 css。

同事说:组长,需要你把 dist 删掉,重新再从仓库里拉一下最新的

组长:git 提交后不就把原来的 dist 替换了吗,你让我删 dist 有什么意义

扯皮了一会儿,组长还是删了然后重新拉,没想到好了

组长说:你的 dist 现在是最新的,所以现在就好了

同事具体说了什么笔者忘记了,大致上在辩护 git 提交不会把原来的 dist 文件删除问题,不过他没说服组长,组长也没说服他,反正已经安全上线而不了了之。

我正好在旁边听到了,要是两年前我也许会一直提出问题参与辩论,申援同事。但笔者没动,不是怕 PUA,而是表达能力太差,即使是对的,也说不好。其根本原因是笔者对这块知识了解的不深刻,所以不敢说大话

理论知识

按照理论知识,你 push 整个 dist 文件,即使远程仓库中有 dist,也不会把整个 dist 文件夹替换,只会替换其中相同的数据,而因为打了 hash 值,所以 css 和 js 都是不同的,所以一直这样做,dist 中的文件会越来越多,而因为 index.html 文件只有一个,所以不会出现替换了还引用之前文件的问题,如果出现,清除下浏览器的缓存就能解决

实战检验

因为生产环境和测试环境发布代码流程不同,所以先要把环境配置成一致先

需要做的事情很简单,把 nginx 中指向仓库地址,到时候从远端拉下代码即可

先修改 nginx 中的配置

server {
    listen 7000;
    # root /usr/share/nginx/html/dist
    root /home/xxx/dist
    ...
}

再检查一下 nginx 配置是否 ok

nginx -t

接着重启 nginx

nginx -s reload

接着把代码提交到远端仓库,再上服务器进入 /home/xxx 目录下,git pull origin XX ,进入 dist 文件,查看打包后的 js

原js的hash值

我们修改在项目中打印一些日志,表示文件改动,这样 build 之后会打出不同 hash 的 js

git push origin XX

再次登录服务器,进入 /home/xxx 目录,再拉代码git pull origin xx

再次提交后的代码

发现,umi.b0f5511b.js 被删掉了,新生成的 umi.f8280c0e.js 在其中,dist 中是干净的源文件,这是为什么呢?

你 build 之后,是先删掉 dist 文件,生成的是一个干净的 dist,然后我的操作是:

  • git add .
  • git commit -m ‘XX’
  • git push origin ‘XX 分支’

我的操作中没有 pull 代码,而是直接 push 代码,这就意味着 dist 就是我本地的 dist,而非合并之后的

想想这种做法的缺点是多人开发时,pull 别人的代码后,merge 之后还要重新 build,才能再次提交

好险,还好没有逞英雄

谨言慎行是一辈子的学问

三句话测试你是否懂 git

这触发了笔者对 git 的新认知,结合平时经验,笔者觉得三个问题能测试别人对 git 的理解程度

  1. 你和同事基于同一 commit 开发,后续合并时,如何按照时间顺序显示提交记录
    • git rebase master XX(分支)
    • 获得更优雅的提交树
  2. 代码如何回滚
    • git reset —hard XX
    • 把当前代码指向另一个 commit 上
  3. 你开发代码,提交了好几个 commit,后续使用 git reset --hard xxxxx 把代码指针指回原始 commit ,并在这个 commit 上开发了一个功能,并提交了一个 commit,怎么找回之前提交的那好几个 commit
    • 首先使用 git reflog ,它能展示你之前所有的 git 操作
      • 比较 git log,它不仅包括了 git log 上的操作,而且它记录了被删除的 commit 记录和 reset 操作
    • git reset --hard XX
      • 将 git 指针指向回到原始代码前的那个 commit
    • git cherry-pick XX
      • 合并二次开发时的 commit
      • cherry-pick 意为取出,将二次开发时的 commit 取出放入主分支上

芝麻开门,显示全文!

搞轮子:从业务到组件Tabs

前言

因为之前为 UI 库由我来做且维护,但缺点是集成在项目中,不便于其他项目使用,且组件被同事改了之后会导致之前沿用的组件乱掉。所以 UI 由独立库维护是最好的。正逢项目不急,就将其排上了日期,其中写了

皮肤概念解决的是 UI 库目前适用于两个主色调、次色调不同的项目

css 定位解决的是 Tag 组件的 border 边框让尺寸变长问题

现在要提取 Tab 组件,发现原来的组件不太合理

业务中的 Tab 组件

业务效果如图所示,滑动它,能连带着切换下划线和内容

image-20211115163300514

后端返回数据结构

总结构:

image-20211115163114381

详细结构:

image-20211115163050904

当时的写法:

<Tab
  ref={tabRef}
  data={tagGroupProductList}
  sticky={true}
  selected={selectedTab}
  onChange={onHandleChangeTab}
  onClick={onClickToBtn}
/>

组件 Tab 分为三部分,主 Tab、TabItem、TabContext

image-20211115163152899

在 Tab 中,集成了 TabItem 和 TabContext

TabItem:头部图片+文字

TabContext:内容卡片,支持滑动

// 伪代码,此为 Tab
<div className="jing-tab" style={style} ref={ref}>
  // 遍历<TabItem></TabItem>
  // 塞入当前下标页的数据,展示 TabContext <TabContext />
</div>

又提供滑动后的回调 changeSwiper。当滑动后,意味着组件头部的位置改变,也意味着 context 数据的改变。虽然写的有点饶,但我想表达的是原来的组件的业务和逻辑绑定密切

这种写法是封装好了样式,只要传 props 即可,相当于木偶组件,使用者传入数据即可。

<Tab
  ref={tabRef}
  data={tagGroupProductList} // 传入一次性参数
  sticky={true}
  selected={selectedTab}
  onChange={onHandleChangeTab}
  onClick={onClickToBtn}
/>

缺点是子组件不能传参数,你不能控制子组件,当遇到类似的 Tab(标签页)效果,但内容结构不一致就会导致不可用。说白了,这个 Tab 只适用于当前页面的效果,没有普世性

为了组件的可拓展性,业务和项目尽量分开

重构组件

我一开始的设想是希望做成这样:

<Tabs swiper={true} active="0">
  <Tab title="标签1">内容1</Tab>
  <Tab title="标签2">内容2</Tab>
  <Tab title="标签3">内容3</Tab>
</Tabs>

Tabs 控制标签页是否能滑动,能否点击,滑动到那个标签页等等,Tab 控制当前标签的标题和内容

但这样,Tabs 和 Tab 就分离成了两个组件,有必要分离吗?用到标签页(Tabs)的时候必然会用 Tab,所以我后面改造成了

<Tabs value="{value}">
  <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
  <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
  <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>

Tab 嫁接在 Tabs 的子组件中

思路与做法

Tabs 的作用是为了外界的 props

Tab 的作用是展示数据,如 title 和 content

思路有了,怎么做?

Tabs 的属性包括但不限于:

  • value:绑定当前选中标签的标识符

  • swipeable:是否开启手势滑动切换

  • sticky:是否使用粘性定位布局

  • disabled:是否禁用标签

  • swipeThreshold:滚动阈值

拆开了 Tabs 和 Tab 的结构是怎么样的

const Panel = (props) => {
    const classes = classnames(prefixCls, props.className, {
        [`${prefixCls}--active`]: !!props.selected,
        [`${prefixCls}--disabled`]: !!props.disabled,
    });
    return (
        <div className="jing-tabs__panel" style={props.style} role="tabpanel">
            {props.children}
        </div>
    )
}

const Tabs = (props) => {
  return (
    <div className="jing-tabs">
      <div className="jing-tabs__wrap">
          React.Children.map(children, (item: any, index: number) => {
            // 简化
          return (
             <Title ...item.props/>
          );
    })
      </div>
      <div className="jing-tabs__content">
         // 遍历 React.Children,将Children中的数据塞入 TabPanel 中
          React.Children.map(children, (item,index) => (
           <TabPanel {...item.props}/>
          ))
      </div>
    </div>
  )
}

Tabs.Panel = Panel

这里有个知识点:React.Children

什么是 React.Children?

React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

React.Children.map 的使用方法

React.Children.map(children, function[(thisArg)])

所以在代码中

React.Children.map(children, (item, index) => <TabPanel {...item.props} />);

(item, index) => <TabPanel /> 表示的是 children 中的每个 Tabs.Panel

联想到组件

<Tabs value={value}>
  <Tabs.Panel title="标签1">内容 1</Tabs.Panel>
  <Tabs.Panel title="标签2">内容 2</Tabs.Panel>
  <Tabs.Panel title="标签3">内容 3</Tabs.Panel>
</Tabs>

在这个案例中,React.Children 表示 children,即三个 Tabs.Panel,并且将其遍历

(item, index) => <TabPanel /> ,就一一对应每个 TabPanel

所以 children 中的内容必须是 Tans.Panel 组件,这样才不会出差错

同理,我们可以把标题抽离出来,成一”木偶组件”,React.Children.map 时传入所需要的数据

这里需要注意的是,Tabs.Panel 组件承担了 Tabs.Title 传值的作用,其 title、img 等都是传给 Tabs.Title 的参数。

做组件需要注意什么

  1. 它的结构

    • 我对 标签页组件是拆分为 TabsTabs.PanelTabs.Title。其中 Tabs.Title 没有暴露出来,因为不需要
  2. 它有什么属性

    • swipeable:支持滑动
      • 如果需要滑动,那么就需要用到 Swiper 滑块组件
      • 当它滑动时,标题中的 line 也要跟着滚动到相应的位置
    • sticky:粘性布局
      • 同样,粘性布局在很多场景下都有用到,是否需要抽离成组件
    • swipeThreshold:滚动阈值
      • 当 children 中的数量(React.Children.Count)大于阈值时支持横向滚动
  3. 它的下标跟随怎么做

    1. 首先要先获取每个 Tabs.Title 的 ref,即获取它的 dom

      1. Tabs.Title 时转发(forwardRef) 此组件
      2. 使用 <Title ref={(el) => (tabsTitleRef.current[index] = el)} ... /> 获取每个 Tabs.Title 的 dom
    2. 补间动画

      1. 拿到点击后的值的 dom 的 坐标,transform: translateX(XX)
    3. 当达到滚动阈值后,tabsNavRefTabs.Title 的父容器) 移动,移动是有公式的

      • function scrollLeftTo() {
          cancelRaf(rafId);
          let count = 0;
          const from = scroller.scrollLeft;
          const frames =
            duration === 0 ? 1 : Math.round((duration * 1000) / 16);
        
          function animate() {
            scroller.scrollLeft += (to - from) / frames;
        
            if (++count < frames) {
              rafId = raf(animate);
            }
          }
        
          animate();
        }
        

其余的没什么难度了

总结

Tabs 标签页组件的核心在于什么?标签的标题和内容,点击标签时,内容切换到该页面以及当它可以滑动时,变化的是当前的下标。我们需要以它为切入点,当它变化时,下划线滚动。而点击 Title 或者滑动 Content 都会触发 setCurrentIndex(index),用 hooks 写完美解决”关注点问题“

芝麻开门,显示全文!

回调函数到promise再到理解async/await

推特上有人发了个”在 7 秒内理解 async/await“ 的视频,地址看这里

拆分成就是写法的变化:回调函数 -> promise -> async/await

js 天生支持异步,如果你的数据依赖于异步请求,那么需要在它的回调中获取,一旦写的多了,就形成了回调地狱,如下图所示

回调函数模式

后来,ES6 出了 promise,promise 的意思是承诺,情景如下:

未婚妻:你一定要回来!

出去打战的士兵:I promise

这样写法上就有个先后顺序,不用再嵌套,而是串联(但换行之后看起来也很清晰)

promise模式

promise 的问题在于,它的语境还是异步,当 getDate 拿到数据后做事情(then)

人的惯性思维是同步,即写就写了,开心就开心了,怒就怒了,没有说等五秒后再笑

所以 promise 是虽好,但可以再换个写法——async/await

async/await模式

async/awiat 是绑定在一起的,缺一不可

const a = await getData() 其中的 a 就是请求数据拿到的结果,从理解上更符合人的思维

芝麻开门,显示全文!

闲人闲谈之执行力

最近一年来,开始将博客公开,为的是在在简历上给人一个能持续输出的印象。但本身却不爱在网上更多公开自己的信息,以至于当同事知道自己的 Github 后

主要是执行力不行。春夏天还行,但今年记录了一下,十月份过后(也许是十一综合征),状态就不行了

想挽回,即使执行了计划,还是不能很好的完成它

怎么能做到很好的执行自己的计划

迷茫有三:

为什么哪些大牛能一直持续更新?

  • 勤奋。强迫自己做
  • 兴趣。因为热爱不在乎
  • 生活。不得已

为什么我做不到?

  • 自身不够勤奋
  • 做了之后没有成就感,即使我写了文章,写了知识地图,但是成就感获得不多。但自己又不是那种特别爱成就感的人

设计属于自己的课程?

兴趣很廉价,专注力才可贵

芝麻开门,显示全文!

scss不能用除法?

之前在项目中就遇到过,scss 不是用除法的问题,当时项目忙,没有及时处理,但心里一直有一根刺,像张爱玲的红玫瑰一样闹的我心烦,现在有时间就想拔掉这根刺

错误定位

因为运行项目后,用到除法,提示错误为

scss警告不能除法

点进官方的解决方案

有两种,一种是引用 @use "sass:math";,使用 math.div(100%, 24) 这类写法,另一种是全局下载sass-migrator 对目标文件进行转换

网上查了一番, bootstrap 鸡贼,换了个思路,用乘法代替,具体可看代码。但也应该会遇到必须使用除法的情况,从 PR 上看,没有看到必须用除法的场景,可惜一番

算了,再去查一番,发现用指定绑定的 sass 可以,是个日本佬写的方案 。所以大概三种方法解决

先用第一种方法解决,确实,在开发环境上不报错了,但是在 build 时,报 Error: Invalid CSS after "...ion-delay: math"

报错信息

第二种、第三种都不好使

PS:这里要批评一下自己,看报错信息就知道使用到了 node-sass,node-sass 不支持这种写法,但是当时自己没认真看报错信息,直接去 Google 了

换种思路

我的项目是基于 umi 开发,用 scss 是因为安装了它的插件:@umijs/plugin-sass

umijs/plugin-sass文档

我的 @umijs/plugin-sass 已经升级到最新版本,也就是说使用到了 Dart Sass,难道 Dart Sass 的问题?

在胡乱找的时候,发现了这篇文章 ,解惑了

This is because you need to use sass instead of node-sass. Remove node-sass and use sass instead and this error should go away

翻译过来就是

这是因为您需要使用sass而不是node-sass. 删除node-sass并使用sass,此错误应该消失。

我的做法

直接升级整个项目,粗暴

npm update

接着删掉 package.json 中的 node-sass(当初的自己害了自己)

再接着删掉整个 node_modules,再重新下载

rm -rf node_modules
yarn

结果通了,这样解决了一个心头刺

@umijs/plugin-sass 源码解读

在找问题的时候看了下 @umijs/plugin-sass 的源码,贴出来看一下

import { IApi, utils } from "umi";

export default (api: IApi) => {
  api.describe({
    config: {
      schema(Joi) {
        return Joi.object({
          implementation: Joi.any(),
          sassOptions: Joi.object(),
          prependData: Joi.alternatives(Joi.string(), Joi.func()),
          sourceMap: Joi.boolean(),
          webpackImporter: Joi.boolean(),
        });
      },
    },
  });

  api.chainWebpack((memo, { createCSSRule }) => {
    createCSSRule({
      lang: "sass",
      test: /\.(sass|scss)(\?.*)?$/,
      loader: require.resolve("sass-loader"),
      options: utils.deepmerge(
        {
          implementation: require("sass"),
        },
        api.config.sass || {}
      ),
    });
    return memo;
  });
};

介于对 webpack 的不熟悉,下面的说法不带有参考性

  • api.describe 不懂
  • api.chainWebpack 这段大概是对 webpack 的规则的一些写入,即是用 sass 写法

后续学习前端工程化系列的时候再对其做补充

扩展阅读

Sass 是采用 Ruby 语言编写的一款 CSS 预处理语言

Sass 和 Scss 其实就是同一种东西,我们平时都称之为 Sass,两者不同之处主要有两点:

  1. 文件扩展名不同,Sass 是以“.sass”后缀为扩展名,而 Scss 是以“.scss”后缀为扩展名。
  2. 语法书写方式不同,Sass 是以严格的缩进式语法规则来书写,不带大括号({})和分号(;),而 Scss 的语法书写和我们的 CSS 语法书写方式非常类似。

芝麻开门,显示全文!

css定位小技巧

最近在忙着做组件,发现之前的组件做的不好看,其关键在于我们没有 UI 标准,所有的标准由产品定,设计就出一个看上去大差不差的效果图(尺寸没有按标准来做,多少会差个几像素)。然后开发按照原型标准来做

这倒是的我们前端出的产品处于没有标准但看上去是一回事的状况

这不,在重构过程中,我发现之前的组件的不合理之处

在做 Tag 标签组件时,发现加 border 的标签比其他的都长了

image-20211111142921445

查看 CSS,发现是因为 border 的长度导致,因为这里有三个标签,加起来就有 6 个像素,所以看起来就很明显

使用 box-sizing: border-box 更改盒模型,回顾一下,盒模型有哪几种,

标准盒模型

  • width= content

IE 盒模型(怪异盒子)

  • width=content+border+padding

但问题是我所有的盒模型都已经格式化为 border-box。所以这个方法没有用

参考了一下同行的做法,这里给有赞一个赞

他们的做法是对主 UI 做相对定位,再在 before 中添加绝对定位,即

.Tag {
  position: relative;
  &::before {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    border: 1px solid;
    border-color: inherit;
    border-radius: inherit;
    content: "";
    pointer-events: none;
  }
}

这样就做到了在 主容器中(Tag 组件)实现边框的内嵌

芝麻开门,显示全文!

我的历史梳理

前言

在微博写周末闲谈,写着写着就超过了一千字,想着干脆写完整了,放在博客上,记录一下 27 岁时我的历史观

正文

最近这两周来,一直在看温铁军教授的视频,很受感触,从三农问题说到国家战略;从国际政治说到马列主义,再到中国特色社会主义;从东西方文明接触的始点说到现在的贸易战,再到中国的应对。总之,我的历史观有被重塑。强烈推荐朋友们去看一看

这里我抛砖引玉把我的所知所记写下来,东西在脑子里,想到哪写什么

秦始皇统一中国,二世被灭。起源于不在于秦的暴政,而是他“车同轨,书同文,统一度量尺”等一些列措施得罪了各种利益阶级。像车同轨,得罪的就是贵族,为什么呢?各个国家的车轨尺寸不同,你一统一,尺寸不同意味着身份不同;同理书同文得罪的是文人;统一度量尺得罪的是商人(商人可在各个国家套利)。秦统一为了管理整个天下,必须要中央集权,搞郡县制,不再搞分封,毕竟周朝的分封已经证明失败,但要把不统一改为统一,必然招到反噬

像现在 200 多个国家和地区,要是有强人想统一整个地球,即靠武力统一地球,实现统一语言、文字、标准,阻力肯定很大,也许根本推行不下去。秦那个时候就处于那样的状况,结果是屁股还没坐稳几年,就被灭了

到了汉朝,前几代皇帝休养生息,文景之治就在这时发生的,安稳了几年,国库里有了存款。汉武帝也是千古一帝,他想打匈奴了。再此之前,汉朝是要每年给匈奴钱,交不起就侵略,汉武帝觉得国库有钱了,不想给钱了,就硬气起来和他们干起来了

其实匈奴来犯也是有原因的,因为那个时候正处于小冰河时期,气候正好变冷,他们必须南下生存,也是迫不得已

汉武帝做到了每户人家没儿子的必须养马,有男丁的必须出一人,去打战时是一个人牵着两匹马上的,霍去病、卫青、李广就是那个时候的出来的英雄,因为他们打的是外来侵略者,所以在历史上的名气大,而秦国时期大将白起、蒙恬知道的人就少了(对老百姓而言)

匈奴被汉武帝打败后,还是要生活啊,怎么办,往西走,翻过喀尔巴阡山脉,进入东欧,跟欧洲的原始部落冲突,最终进入中欧、北欧一代,把这一代的日耳曼蛮族南压,罗马人打不过,允许日耳曼南下,进入意大利,最终是日耳曼蛮族灭了西罗马

简单来说,东方把匈奴(游牧民族)打到大漠以北,匈奴进入欧洲,引起欧洲动乱,使得西罗马灭亡(外因)。这次天气变化引起的打仗、让东西方文明都倒退了。东方倒退了 400 年,期间经历了东汉、三国、魏晋、十六国、南北朝,直到隋唐又一大一统;西方进入中世纪,直接倒退 1000 年,抗打击能力属实不行。

而在唐朝时期,听说又是因为天气原因。不过面对的是突厥。突厥分东西突厥。唐朝李世民照样打,打败了突厥后,吸纳了一部分,即新疆地区同胞,而另一部分突厥人往西跑,经过突厥走廊,并南下,到了今天的土耳其(安纳托利亚半岛),导致了东罗马(拜占庭)灭亡的一个外因

这就是两次文明冲突的历史梳理

温老讲课讲的极好,我一个对历史没多大兴趣的人都听得津津有趣,推荐大家可以去B站看一下他的课

芝麻开门,显示全文!

搞轮子:皮肤概念的几种方案

而且我还希望能有灰度模式和暗黑模式,皮肤概念孕育而生

大厂的做法

vant

ant-design

zarm

考察下来,zarm 最符合我的预期

按 zarm 的来

简单来说,使用 context 来做主题色,因为 context 能传递到任意组件,接下来就看怎么写

皮肤解决方案

https://segmentfault.com/a/1190000041195585?_ea=195391016

CSS 变量

:root {
  --bg-color: brown; // 定义颜色变量
}
.btn {
  // 直接使用颜色预定义的颜色变量
  background-color: var(--bg-color);
}

https://css-tricks.com/switch-font-color-for-different-backgrounds-with-css/

https://zhuanlan.zhihu.com/p/494460951

https://mp.weixin.qq.com/s/6bmqki5IPDlD4H7a7C1HXw

张鑫旭的换肤方案

https://www.bilibili.com/video/BV1kU4y1X7a8?vd_source=55c655c3b4aed7bb7a250da7eea13eb8

https://juejin.cn/post/7117911005841063944

芝麻开门,显示全文!