2018 React Redux 入门教程

译文链接, 原文链接
老实说学习 Redux 真的很有挫败感,虽然 Redux 的源码很小(< 2kb),然而其文档却庞大无比,老实说让人害怕!即便吾辈看了阮一峰写的 Redux 入门教程,然而还是在第二篇就 GG 了。纵然了解了概念,然而却不知如何使用,便是如此了。。。这篇翻译过来的教程吾辈感觉还不错,所以也便是转发一下好啦
吾辈也跟着教程写了一个 Redux Demo,仅供参考。

我希望这是学习 React Redux 最简单的入门教程。

我一开始学习 Redux 的时候我想找到最浅显易懂的入门教程。

尽管有大量的资源,但是我对于 Redux 的一些概念依旧搞不清楚。

我知道什么是 state ,但是 actions, action creators 和 reducers 又是什么鬼?我被搞晕掉了。

然后我也搞不明白 React 和 Redux 是怎样结合起来的。

前端时间开始着手写 React Redux 教程的过程中我理解了许多。

我通过写这篇教程自学 Redux 的基本概念。我也希望这可以帮助所有正在学习 React 和 Redux 的人。

适用人群

如果满足下列条件,那么本教程正是你要找的:

  • 良好的 Javascript, ES6, React 基础
  • 你希望以最简单的方式学习 Redux

你可以学到什么

通过这篇教程你可以学到:

  • Redux 是什么
  • 怎样结合 React 使用 Redux

搭建 React 开发环境

看这篇教程你需要有扎实的 Javascript, ES6, React 基础。

开始之前,让我们来简单搭建一个 React 开发环境。

你可以选择 webpack 3 或者 Parcel。

如果选用 Parcel 你可以参考这个链接 Setting up React with Parcel ,或者从我的 Github 上克隆仓库:

git clone git@github.com:valentinogagliardi/minimal-react-parcel.git

如果你想用 webpack 3 可以参考这个教程 How to set up React, Webpack 3, and Babel ,或者从我的 Github 上克隆仓库:

git clone git@github.com:valentinogagliardi/minimal-react-webpack.git

什么是 state ?

理解 Redux 之前你首先需要理解什么是 state 。

如果你曾经写过 React 的 state , 应该不足为奇。

我猜你应该写过如下的 React 状态组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react'
class ExampleComponent extends Component {
constructor() {
super()
this.state = {
articles: [
{ title: 'React Redux Tutorial for Beginners', id: 1 },
{ title: "Redux e React: cos'è Redux e come usarlo con React", id: 2 },
],
}
}
render() {
const { articles } = this.state
return (
<ul>
{articles.map(el => (
<li key={el.id}>{el.title}</li>
))}
</ul>
)
}
}

上述组件是基于 Javascript ES6 class 的。

每个 React 状态组件持有自己的 state 。在 React 中 state 持有数据。组件渲染这组数据展示给用户。

React 提供了一个 setState 方法来更新组件内部的 state 。

我们通常这么用:

  1. 从组件内部持有的 state 中获取数据渲染
  2. 通过 React 的 setState 方法更新 state

Redux 解决了什么问题 ?

应用规模比较小的时候,在 React 组件内部维护 state 很方便。

但在复杂一点的业务场景下这么做就不太合适了。组件中会填满各种管理更新 state 的方法,臃肿不堪。前端不应该知道业务逻辑。

那么,有没有其它管理 state 的方式呢?Redux 就是其中的一个解决方案。

Redux 解决了一开始我们可能还不太清楚的问题:它为每一个 React 组件明确提供它所需要的 state 。

Redux 统一在一个地方维护 state 。

通过 Redux ,获取和管理 state 的逻辑也和 React 隔离开来。

这中方式的好处可能还不太明显。当你用上它后就会发现它的威力。

下一节内容我们聊一下为什么以及什么时候我们需要用 Redux。

我要学 Redux 么 ?

想学习 Redux 但是没搞定?

Redux 的确吓坏了许多初学者。但是不必因此而感到担心。

Redux 没有那么难。关键是:不要随波逐流地去使用它。

你开始学习 Redux 应该是出于自发的动机和热情。

我学习 Redux 是因为:

我 100% 地渴望知道 Redux 是怎么工作的
我想要提升我的 React 技能
我想成为更牛 X 的 React 开发者
React/Redux 当下是黄金组合
Redux 是一种思想,它和框架无关。学习了它可以在任何地方(比如 Vue JS,Angular 中)使用。

我要用 Redux 么 ?

没有 Redux 构建复杂的 React 应用也是可行的。只是有一些成本。

用 Redux 也是有一定成本的:它加入了另外一个抽象层。但是我我认为是利大于弊的。

另一个困惑初学者的问题是:我怎么判断什么时候我需要使用 Redux 呢?

如果你没有经验判定是否需要用 Redux 来管理 state ,我有如下建议使用的场景:

多个 React 组件需要访问同一个 state 但是又没有父子关系
需要通过 props 向下向多个组件传递 state

如果还是找不到感觉,别怕,我也一样。

Dan Abramov 说过『 Flux 就像眼镜:你需要的时候你自然会知道 』。

事实上对于我而言也正是如此。

Dan Abramov 写了篇很棒的文章来帮助我们理解: Redux 的使用场景

dev.to 上也有一篇 Dan 关于这一问题的 讨论

看一下 Mark Erikson 整理的 Redux 学习资源

顺便说下,Mark 的博客被视为 Redux 的最佳实践

Dave Ceddia 也分享过关于 Redux 干了什么?以及它的使用场景 的话题。

在深入学习之前,花些时间去搞明白 Redux 解决了什么问题。然后再决定要不要学。

要清楚在小型的应用中使用 Redux 没什么好处。只有在大型应用下才能发挥它的威力。但不管如此,学习 Redux 解决问题的思路总不是什么坏事。

下一节,我们介绍一下内容:

Redux 的基本原则
Redux 和 React 怎么结合

了解 Redux store

Actions, Reducers 我都知道。但有件事我搞不明白:这些动态的片段是怎么结合到一起的?

有小黄人相助不成?

是 store 把所有流动的片段协调地结合到 Redux 中。在 Redux 中 store 就像人类的大脑:这是种魔法。

Redux store 是一切的基础:整个应用的 state 都在 store 中维护。

如果我们把 Redux 的工作模式想象成人类的大脑。 state 就相当于存在于大脑(store)中的一段回忆。

所以开始使用 Redux 的时候我们需要创建一个 store 来存放 state。

切换到你的开发目录,安装 Redux:

1
2
3
cd minimal-react-webpack/

npm i redux --save-dev

创建一个目录来存放 store 相关逻辑:

1
mkdir -p src/js/store

在 src/js/store 中创建 index.js 并初始化:

1
2
3
4
5
6
7
8
// src/js/store/index.js

import { createStore } from 'redux'
import rootReducer from '../reducers/index'

const store = createStore(rootReducer)

export default store

createStore 就是创建 Redux store 的方法。

createStore 接收一个 reducer 作为第一个参数,就是代码中的 rootReducer。

你也可以传递一个初始 state 进 createStore。 但是大多数时候你不必这么做。传递初始 state 在服务端渲染时比较有用。当然,state 来自 reducers。

现在我们知道 reducer 做了啥了。reducer 生成了 state 。 state 并不是手动创建的。

带着这些概念我们继续我们第一个 Redux reducer。

了解 Redux reducers

初始 state 在 服务端渲染 中很有用,它必须完全从 reducers 中产生。

reducer 是个什么鬼?

reducer 就是个 javascript 函数。它接收两个参数:当前的 state 和 action 。

Redux 的第三条原则中强调:state 必须是不可变的。这也是为什么 reducer 必须是一个纯函数。纯函数就是指给有明确的输入输出的函数。

在传统的 React 应用中我们通过 setState 方法来改变 state 。 在 Redux 中不可以这么做。

创建一个 reducer 不难,就是个包含两个参数的 javascript 普通函数。

在我们的例子中,我们将创建一个简单的 reducer, 并向他传递初始 state 作为第一个参数。第二个参数我们将提供一个 action 。到目前为止 reducer 除了返回初始状态什么都不会做。

创建 reducer 根目录

1
mkdir -p src/js/reducers

然后在 src/js/reducers 创建一个名为 index.js 的新文件:

1
2
3
4
5
6
7
8
9
// src/js/reducers/index.js

const initialState = {
articles: [],
}

const rootReducer = (state = initialState, action) => state

export default rootReducer

我承诺这是篇尽可能简明的教程。所以我们仅有一个 reducer :它除了返回初始 state 什么都不处理。

注意下初始 state 是怎样作为 默认参数 传递的。

下一节我们将结合一个 action 来让这个应用更加有趣。

了解 Redux actions

reducers 无疑是 Redux 中最重要的概念。reducers 用来生产应用的 state 。

但是 reducers 怎么知道什么时候去生产下一个 state 呢?

Redux 的第二条原则中说:改变 state 的唯一方式就是向 store 发送一个信号 。 这个 信号 指的就是 action 。

怎么改变这个不可更改的状态呢?你不能。返回的 state 是当前 state 的副本结合新数据后的一个全新的 state 。这点必须要明确。

比较欣慰的是 actions 就是简单的 javascript 对象。它长这样:

1
2
3
type: ‘ADD_ARTICLE’,

payload: { name: ‘React Redux Tutorial for Beginners’, id: 1 }

每一个 action 都要有一个 type 属性来描述要对 state 做怎样的改变。

你也可以指定一个叫 payload 的属性。在上面的例子中, payload 指代一篇新的文章。reducer 接下来将把这篇新文章加到 state 中去。

最佳实践是将每一个 action 都通过函数包裹起来。这个函数就是 action creator。

让我们来把这些都串起来创建一个简单的 action 。

创建 actions 目录:

1
mkdir -p src/js/actions

在 src/js/actions 目录中创建名为 index.js 的文件:

1
2
3
// src/js/actions/index.js

export const addArticle = article => ({ type: 'ADD_ARTICLE', payload: article })

type 属性就是个简单的字符串。reducer 将根据这个字符串决定怎么处理生成下一个 state 。

由于字符串很容易重复产生冲突,所以最好在常量中统一定义 action types 。

这个方法可以避免一些很难排查的错误。

创建一个新目录:

1
mkdir -p src/js/constants

然后在 src/js/constants 目录下创建名为 action-types.js 的文件:

1
2
3
// src/js/constants/action-types.js

export const ADD_ARTICLE = 'ADD_ARTICLE'

然后打开 src/js/actions/index.js 用常量来替换字符串:

1
2
3
4
5
6
7
8
9
// src/js/actions/index.js

import { ADD_ARTICLE } from '../constants/action-types'

export const addArticle = article => ({
type: ADD_ARTICLE,

payload: article,
})

我们进一步完成了一个可运行的 Redux 应用。 接下来让我们重构下我们的 reducer 吧!

重构 reducer

进入下一步之前,让我们总结一下 Redux 的主要概念:

Redux store 就像大脑:它负责将所有流动的片段有机地整合进 Redux 中
应用中的 state 在 store 中以唯一且不可变对象的形式存在
一旦 store 接收到一个 action 他就会触发一个 reducer
reducer 返回下一个 state

reducer 是怎样构成的?

reducer 是一个 javascript 函数,它接收两个参数:state 和 action。

reducer 一般会包含一个 switch 语句(傻一点的话也可以用:if /else)。

reducer 根据 action type 产生下一个 state 。此外,当没有匹配到 action type 时它至少也要返回初始 state 。

当 action type 匹配到一个 case 分支的时候, reducer 将计算下一个 state 并返回一个全新的对象。下面是个例子:

1
2
3
4
5
6
7
8
9
10
// …

switch (action.type) {
case ADD_ARTICLE:
return { ...state, articles: [...state.articles, action.payload] }
default:
return state
}

// …

我们之前创建的 reducer 只返回了初始 state 什么都没做。让我们做点什么。

打开 src/js/reducers/index.js 按如下例子更新 reducer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ADD_ARTICLE } from '../constants/action-types'

const initialState = {
articles: [],
}

const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_ARTICLE:
return { ...state, articles: state.articles.push(action.payload) }
default:
return state
}
}

export default rootReducer

发现了什么?

尽管代码逻辑没有错,但这违背了 Redux 的主要原则: 不可变 。

Array.prototype.push 不是一个纯函数,它改变了原来的数组。

解决的方式很简单。用 Array.prototype.concat 替换 Array.prototype.push 来保持原始数组是不可变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ADD_ARTICLE } from '../constants/action-types'

const initialState = {
articles: [],
}

const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_ARTICLE:
return { ...state, articles: state.articles.concat(action.payload) }
default:
return state
}
}

export default rootReducer

还不够!可以用 扩展运算符 优化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ADD_ARTICLE } from '../constants/action-types'

const initialState = {
articles: [],
}

const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_ARTICLE:
return { ...state, articles: [...state.articles, action.payload] }
default:
return state
}
}

export default rootReducer

上述例子中初始 state 完全没受干扰。

最初的文章数组并没有改变。

初始 state 对象也没有改变。返回的 state 是初始 state 的一个副本。

在 Redux 中有两个关键点来防止类似的改变。

针对数组可以用 concat (), slice (), 和 … 操作符
针对对象可以用 Object.assign () and … 操作符

扩展运算符 在 webpack 3 中还是个实验性功能。需要安装一个 babel 插件来防止语法错误:

1
npm i —save-dev babel-plugin-transform-object-rest-spread

打开 .babelrc 更改配置:

1
2
3
4
{
"presets": ["env", "react"],
"plugins": ["transform-object-rest-spread"]
}

小贴士: reducer 随着应用的增长会变得臃肿。你可以拆分 reducer 进不同的函数,然后通过 combineReducers 将他们结合起来。

下一节我们将通过 console 控制台把玩一下 Redux 。搞起!

Redux store 中的方法

这节内容将会很快过完,我保证。

我想借助浏览器的控制台让你快速理解下 Redux 是怎样工作的。

Redux 本身是一个很小的类库 (2KB)。它暴露了 一些简单的 API 让我们来管理 state 。其中最重要的方法就是:

  • getState 用于获取应用的 state
  • dispatch 用于触发一个 action
  • subscribe 用于监听 state 的变化

我们将借助浏览器的管理控制台试验上述方法。

我们需要创建全局变量将我们先前创建的 store 和 action 暴露出来。

打开 src/js/index.js 按如下所示更新代码:

1
2
3
4
5
6
7
import store from '../js/store/index'

import { addArticle } from '../js/actions/index'

window.store = store

window.addArticle = addArticle

然后运行一下:

1
npm start

在浏览器中打开 http://localhost:8080/ 并按 F12 打开管理控制台。

由于我们全局暴露了 store , 所以我们可以进入它的方法。试一下!

首先访问当前 state:

1
store.getState()

输出:

1
{articles: Array(0)}

没有文章。事实上我们还没有更新初始 state 。

让这变得更有趣些我们可以通过 subscribe 方法监听 state 的变化。

subscribe 方法接收一个回调函数,当 action 触发的时候该回调函数就会执行。触发 action 就意味着通知 store 我们想改变 state 。

通过如下操作注册回调函数:

1
store.subscribe(() => console.log('Look ma, Redux!!'))

要想改变 state 我们需要触发一个 action 。要触发一个 action 我们就需要调用 dispatch 方法 。

我们已经有了一个 action :addArticle 用来向 state 中新增文章。

让我们触发一下这个 action :

1
store.dispatch( addArticle({ name: ‘React Redux Tutorial for Beginners’, id: 1 }))

执行上述代码后你将看到:

1
Look ma, Redux!!

验证一下 state 有没有变:

1
store.getState()

输出会是:

1
{articles: Array(1)}

这就是个最简单的 Redux 原型。

难么?

稍微花点时间练习一下 Redux 的三个方法。在控制台中玩一把:

  • getState 用于获取应用的 state
  • dispatch 用于触发一个 action
  • subscribe 用于监听 state 的变化

这就是开始入坑所需要知道的全部内容。

有信心进入下一步了么?让我们把 React 和 Redux 串起来吧。

把 React 和 Redux 结合起来

学习了 Redux 后我发现它并没有想象中那么复杂。

我知道我可以通过 getState 方法获取当前 state ;通过 dispatch 触发一个 action ; 通过 subscribe 监听 state 的变化。

目前我还不知道怎么将 React 和 Redux 组合起来使用。

我问自己:我需要在 React component 中调用 getState 方法么? 在 React component 我怎么去触发 action ? 等等一些列问题。

Redux 是框架无关的。你可以在纯 Javascript 中使用它。或者结合 Angular,Redux 一起使用。 有许多第三方库可以实现把 Redux 和任何你喜欢的框架结合起来使用。

对于 React 而言, react-redux 就是这么一个第三方库。

首先让我们通过下面的命令安装一下:

1
npm i react-redux —save-dev

为了演示 React 和 Redux 是如何协同工作的,我们将构建一个超级简单的应用。这个应用由如下组件组成:

  • 一个 App 组件
  • 一个展示文章的 List 组件
  • 一个新增文章的 Form 组件

(这是个玩具应用,仅仅用来展示文章列表,新增文章条目。但是可以算一个学习 Redux 不错的开端。)

react-redux

react-redux 是一个 React 和 Redux 绑定库。这个类库将 React 和 Redux 高效地连接起来。

react-redux 的 connect 方法做了什么呢? 毫无疑问是将 React 的组件和 Redux 的 store 连接起来了。

你可以根据需要向 connect 方法传递两个或者三个参数。需要知道的最基本的东西就是:

  • mapStateToProps 函数
  • mapDispatchToProps 函数

react-redux 中 mapStateToProps 做了什么呢?就像它的名字一样:它将部分 Redux state 和 React 组件中的 Props 连接了起来。通过这层连接,React 组件就可以从 store 中获取它所需要的 state 了。

react-redux 中 mapDispatchToProps 又做了什么呢?和 mapStateToProps 类似,只不过它是连接的 actions 。它将 部分 Redux action 和 React 组件中的 Props 连接了起来。通过这层连接,React 组件就可以从触发 actions 了。

都清楚了么?如果没有,停下来重读一遍。我知道许多内容需要时间消化。我要着急,早晚会搞明白的。

接下来的部分我们就要大显生手了!

App 组件 和 Redux store

我们知道 mapStateToProps 将部分 Redux state 和 React 组件中的 Props 连接了起来。你可能想问:连接 React 和 Redux 这么做就够了吗?不,还不够。

要连接 React 和 Redux 我们还要用到 Provider。

Provider 是 react-redux 提供的一个高阶组件。

直白地说,就是 Provider 将整个 React 应用封装起来,是它能够感知到整个 Redux store 的存在。

为什么这么做?我们知道在 Redux 中 store 管理着一切。React 必须通过 store 来访问 state , 触发 actions 。

了解了以上这些,就打开 src/js/index.js ,修改如下:

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import store from '../js/store/index'
import App from '../js/components/App'
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'),
)

看到没有?Provider 将整个应用包裹起来。并将 store 作为属性传入。

现在让我们创建一个 App 组件。没什么特别的:App 组件导入一个 List 组件并渲染。

创建一个目录来存放组件:

1
mkdir -p src/js/components

在 src/js/components 目录下创建名为 App.js 的文件。

1
2
3
4
5
6
7
8
9
// src/js/components/App.js

import React from 'react'

import List from './List'

const App = () => Articles

export default App

花时间看下组件的构成:

1
2
3
4
5
6
7
import React from 'react'

import List from './List'

const App = () => <List />

export default App

然后继续创建 List 组件。

List 组件 和 Redux state

目前为止我们没做特殊的操作。

但是 List 组件需要和 Redux store 产生交互。

简单总结一下:连接 React 组件 和 Redux 的关键就是 connect 方法。

Connect 方法接收至少一个参数。

由于我们想让 List 获取文章列表,因此我们需要让 state.articles 和组件连接起来。怎么做呢?用 mapStateToProps 参数来实现。

在 src/js/components 目录下创建名为 List.js 的文件。写入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/js/components/List.js

import React from 'react'
import { connect } from 'react-redux'

const mapStateToProps = state => {
return { articles: state.articles }
}
const ConnectedList = ({ articles }) => (
<ul className="list-group list-group-flush">
{articles.map(el => (
<li className="list-group-item" key={el.id}>
{el.title}
</li>
))}
</ul>
)
const List = connect(mapStateToProps)(ConnectedList)
export default List

List 组件接收一个名为 articles 的属性, 它是一个 articles 数组的副本。这个数组寄生于我们之前创建的 Redux state 内。它来自 reducer :

1
2
3
4
5
6
7
8
9
10
11
12
const initialState = {
articles: [],
}

const rootReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_ARTICLE:
return { ...state, articles: [...state.articles, action.payload] }
default:
return state
}
}

然后我们就可以用这个属性在 JSX 中生成一个文章列表了:

1
2
3
4
5
6
7
{
articles.map(el => (
<li className="list-group-item" key={el.id}>
{el.title}
</li>
))
}

小提示 可以用 React PropTypes 对属性进行校验。

最后组件被导出为 List。List 是无状态组件和 Redux store 连接之后的产物。

无状态组件没有内部 state 。数据是通过属性传入的。

依旧很困惑?我也是。理解 connect 的工作原理需要花点时间。别害怕,慢慢来。

我建议你停下来花点时间看下 connect 和 mapStateToProps 。

搞懂了我们就进入下一章的学习。

Form 组件 和 Redux actions

Form 组件比 List 组件稍微复杂一点。它是用于创建文章的表单。

另外它是一个状态组件。

状态组件是指在组件内部维护自身 state 的组件 。

状态组件?我们一直说要用 Redux 来管理 state 。 为什么现在又说要让 Form 自己去维护 state ?

即使在使用 Redux 的时候,使用有状态的组件也是完全可以的,两者并不冲突。

并不是所有的 state 都必须在 Redux 中管理。

在这个例子中我不想其他任何组件知道 Form 组件内部的 state 。

这么做很好。

这个组件做什么事呢?

该组件包含了一些在表单提交时更新本地 state 的逻辑。

另外它接收一个 Redux action 作为属性传入其中。用这种方式它可以通过触发 addArticle action 来更新全局的 state。

在 src/js/components 下建一个名为 Form.js 的文件。它看起来像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// src/js/components/Form.js

import React, { Component } from 'react'
import { connect } from 'react-redux'
import uuidv1 from 'uuid'
import { addArticle } from '../actions/index'

const mapDispatchToProps = dispatch => {
return {
addArticle: article => dispatch(addArticle(article)),
}
}

class ConnectedForm extends Component {
constructor() {
super()
this.state = {
title: '',
}
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}

handleChange(event) {
this.setState({ [event.target.id]: event.target.value })
}

handleSubmit(event) {
event.preventDefault()
const { title } = this.state
const id = uuidv1()
this.props.addArticle({ title, id })
this.setState({ title: '' })
}

render() {
const { title } = this.state

return (
<form onSubmit={this.handleSubmit}>
<div className="form-group">
<label htmlFor="title">Title</label>

<input
type="text"
className="form-control"
id="title"
value={title}
onChange={this.handleChange}
/>
</div>

<button type="submit" className="btn btn-success btn-lg">
SAVE
</button>
</form>
)
}
}

const Form = connect(
null,
mapDispatchToProps,
)(ConnectedForm)

export default Form

怎么描述这个组件呢?除了 mapDispatchToProps 和 connect 它就是标准的 React 的那套东西。

mapDispatchToProps 将 Redux actions 和 React 属性 连接起来。这样组件就可以向外发送 action 了。

你可以看下 action 是怎样在 handleSubmit 方法中被触发的:

1
2
3
4
5
6
7
8
9
// ...
handleSubmit(event) {
event.preventDefault();
const { title } = this.state;
const id = uuidv1();
this.props.addArticle({ title, id }); // Relevant Redux part!!
// ...
}
// ...

最后组件被导出为 Form 。 Form 就是组件和 Redux store 连接之后的产物。

注意:当 mapStateToProps 不存在的时候,connect 的第一个参数必须为 null,就像例子中的 Form 一样。 否则,你会得到 TypeError: dispatch is not a function. 的警告。

我们的组件部分就都结束了。

更新下 App , 把 Form 组件加进来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react'
import List from './List'
import Form from './Form'

const App = () => (
<div className="row mt-5">
<div className="col-md-4 offset-md-1">
<h2>Articles</h2>
<List />
</div>
<div className="col-md-4 offset-md-1">
<h2>Add a new article</h2>
<Form />
</div>
</div>
)

export default App

运行一下:

1
npm start

打开 http://localhost:8080

你将看到如下界面:

没什么特别的,但可以展示 React 和 Redux 是如何工作的。

左边的 List 组件和 Redux store 相连。它将在你新建文章的时候刷新渲染。

哈哈!

你可以在 这里 查看源码。

总结

我希望你可以从这篇教程中学到一些东西。我尽力把例子写的足够简单。我希望可以在下面的评论中倾听你的反馈。

Redux 有许多模板和移动部件。别灰心。拿起来把玩,花点时间去吸收它所有的概念。

我逐步从零开始慢慢地理解了 Redux 。相信你也可以做到。

当然,也请花点时间想清楚为什么需要在你的项目中引用 Redux。

无论怎样:学习 Redux 都是 100% 值得的。

Redux 并不是状态管理的唯一方式。Mobx 是另一个有趣的可选方案。我也在关注 Apollo Client。谁将胜出?只有时间知道答案。

函数式编程的一些资源

Redux 使大多数初学者感到害怕,是因为它是围绕函数式编程和纯函数展开的。

我只能是推荐一些资源给大家,因为函数式编程超出了本指南的范围。 下面是一些函数式编程和纯函数相关的资源:

Redux 中的异步 actions

我不确定谈论异步 actions 是否合适。

大多数 Redux 初学者都很难仅学习纯粹的 Redux。 在 Redux 中处理复杂的异步 action 还是比较费劲的。

当你了解了 Redux 的核心概念后可以去读一下 Matt Stow 写的 A Dummy’s Guide to Redux and Thunk in React。这是篇关于 Redux 怎样用 redux-thunk 处理 API 请求的非常不错的介绍。

谢谢阅读,码得愉快~

2016 年里做前端是怎样一种体验

本文转载自 SegmentFault,英文原文在 How it feels to learn JavaScript in 2016
吾辈觉得颇为有趣,便转载了一下(也算是前端开发的悲哀之处了吧)
附:吾辈觉得至今为止,前端工具链仍然没有好太多,工具链过多导致配置复杂,入门难度极大。
附:现在都 2018 了,然而吾辈的公司现在还是用 jQuery,虽然吾辈目前在内部强推了 VueJS #笑哭
附:吾辈这里对内容添加了一些样式,便于阅读时不那么疲劳。

问:最近我接手了一个新的 Web 项目,不过老实说我已经好久没碰过这方面的代码了。听说前端的技术栈已经发生了极大的变革,不知道你现在是不是仍然处于最前沿的开发者阵列?
答:准确来说,过去俗称的写网页的,现在应该叫做 Front End Engineer,我确实属于这所谓的前端工程师。并且我才从 JSConfReactConf 面基回来,因此我觉得我觉得我还是了解目前 Web 前端领域最新的面貌的。
问:不错不错,我的需求其实也不复杂,就是从后端提供的 REST 风格的 EndPoint 来获取用户活动数据并且将其展示在前端界面上。并且需要以列表形式展示,同时,列表要支持筛选排序等操作,对了,还要保证前端数据和服务端保持一致。按照我现在的理解,我打算用 jQuery 来抓取与展现数据,你觉得咋样?
答:不不不,现在估计已经没多少人使用 jQuery 了吧。你可以试试 React,毕竟这是 2016 年了啊。
问:额,好吧,那啥是 React 啊?
答:这是个非常不错的源自 Facebook 的前端库,它能够帮你便捷地响应界面事件,同时保证项目层级的可控性与还说得过去的性能。
问:不错不错,那我是不是就可以用 React 来展示数据了呢?
答:话是这么说没错,不过你需要添加 ReactReact DOM 依赖项到你的页面中去。
问:等等,React 不是一个库吗?为啥要添加两个依赖呢?
答:不要急,前者是 React 的核心库,后面呢算是 Facebook 操作的辅助库,这样就能让你用 JSX 来描述你的界面布局了。
问:JSX?啥是 JSX
答:JSX 是一个类似于 XMLJavaScript 语法扩展,它是另一种描述 Facebook 的方式,可以认为是 HTML 的替代品。
问:等等,HTML 咋啦?
答:都 2016 了,直接用 HTML 早就过时了。
问:好吧,那是不是我把两个库添加到项目中我就可以使用 React 了?
答:额,还要一些小的工具,你需要添加 Babel 到你的项目中,这样你就能用了。
问:又是一个库?Babel 又是什么鬼?
答:你可以把 Babel 认为是一个转译工具,可以将某个特定版本的 JavaScript 转译为任意版本的 JavaScript。你可以选择不使用 Babel,不过那也就意味着你只能用烦人的 ES5 来编写你的项目了。不过既然都是 2016 了,我建议你还是使用最新的 ES2016+ + 语法吧。
问:ES5ES2016++?我已经迷茫了,ES5ES2016+ + 又是啥?
答:ES5ECMAScript 2015 的缩写,也是现在被绝大部分浏览器所支持的 JavaScript 语法。
问:ECMAScript
答:是的,你应该知道 JavaScript 最早于 1995 年提出,而后在 1999 年第一个正式版本定稿。之后的十数年里 JavaScript 的发展一直很凌乱,不过经过七个版本之后已经逐步清晰了。
问:7 个版本?那么 ES5ES2016+ 又是第几个版本呢?
答:是的,分别指第五个版本与第七个版本。
问:等等,那第六个版本呢?
答:你说 ES6?估计我刚才没有讲明白,ECMAScript 的每个版本都是向前兼容的,当你使用 ES2016+ + 的时候也就意味着你在使用之前所有版本的所有特性啦。
问:原来是这样啊,那为啥一定要用 ES2016+ 而不是 ES6 呢?
答:是的,你可以使用 ES6,不过如果你要使用 asyncawait 这些特性,你就要去用 ES2016+ 了。否则你就还不得不去使用 ES6Generator 来编写异步代码了。
问:我现在彻底迷糊了,我只是想简单地从服务端加载些数据而已,之前只需要从 CDN 加载下 jQuery 的依赖库,然后用 Ajax 方法来获取数据即可,为啥我现在不能这么做呢?
答:别傻了,每个人都知道一味使用 jQuery 的后果就是让你的代码变得一团乱麻,这都 2016 了,没人再想去面对这种头疼的代码了。
问:你说的是有道理,那现在我是不是就把这三个库加载进来,然后用 HTMLTable 来展示这些数据?
答:嗯,你可以选择一个模块打包工具将这三个依赖库打包到一个文件中。
问:额,啥是模块打包工具啊?
答:这个名词在不同的环境下指代也不同,不过在 Web 开发中我们一般将支持 AMDCommonJS 的工具称为模块打包工具。
问:AMDCommonJS 又是?
答:它们是用于描述 JavaScript 库与类之间交互的接口标准,你有听过 exports 与 requires 吗?你可以根据 AMD 或者 CommonJS 的规范来定义多个 JavaScript 文件,然后用类似于 Browserify 的工具来打包它们。
问:原来是这样,那 Browserify 是啥呢?
答:Browserify 最早是为了避免人们把自己的依赖一股脑放到 NPM Registry 中构建的,它最主要的功能就是允许人们将遵循 CommonJS 规范的模块打包到一个文件中。
问:NPM Registry
答:这是一个很大的在线仓库,允许人们将代码与依赖以模块方式打包发布。
问:就像 CDN 一样?
答:还是有很大差异的,它更像一个允许人们发布与下载依赖库的中心仓库。
问:哦,我懂了,就像 Bower 一样啊。
答:对哒,不过 2016 年了,同样没啥人用 Bower 了。
问:嗯嗯,那我这时候应该从 npm 库中下载依赖了是吧?
答:是的,譬如如果你要用 React 的话,你可以直接用 Npm 命令来安装 React,然后导入到你的项目中,现在绝大部分主流的 JavaScript 库都支持这种方式了。
问:嗯嗯,就像 Angular 一样啊。
答:不过 Angular 也是 2015 年的流行了,现在像 VueJS 或者 RxJS 这样的才是小鲜肉,你想去学习它们吗?
问:不急不急,我们还是先多聊聊 React 吧,贪多嚼不烂。我还想确定下,是不是我从 npm 下载了 React 然后用 Browserify 打包就可以了?
答:是的。
问:好的,不过每次都要下载一大堆依赖然后打包,看起来好麻烦啊。
答:是的,不过你可以使用像 Grunt 或者 Gulp 或者 Broccoli 这样的任务管理工具来自动运行 Browserify。对了,你还可以用 Mimosa
问:GruntGulpBroccoliMimosa?我们到底在讨论啥?
答:不方,我们在讨论任务管理工具,不过同样的,这些工具也是属于 2015 年的弄潮儿。现在我们流行使用 Webpack 咯。
问:Makefiles? 听起来有点像是一个 C 或者 C++ 项目啊。
答:没错,不过很明显 Web 的演变之路就是把所有事情弄复杂,然后再回归到最基础的方式。估计不出几年你就要在 Web 中写汇编代码了。
问:额,你刚才好像提到了 Webpack
答:是的,这是一个兼顾了模块打包工具与任务运行器的打包工具,有点像 Browserify 的升级版本。
问:嗷嗷,这样啊,那你觉得哪个更好点呢?
答:这个因人而异了,不过我个人是更加偏好于 Webpack,毕竟它不仅仅支持 CommonJS 规范,还支持 ES6 的模块规范。
问:好吧,我已经被 CommonJS/ES6 这些东西彻底搞乱了。
答:很多人都是这样,多了,你可能还要去了解下 SystemJS
问:天哪,又是一个新名词,啥是 SystemJS 呢?
答:不同于 BrowserifyWebpack 1.xSystemJS 是一个允许你将多个模块分封于多个文件的动态模块打包工具,而不是全部打包到一个大的文件中。
问:等等,不过我觉得按照网络优化规范我们应该将所有的库打包到一个文件中。
答:是的,不过 HTTP/2 快要来了,并发的 HTTP 请求已经不是梦。
问:额,那时候是不是就不需要添加 React 的依赖库了?
答:不一定,你可以将这些依赖库从 CDN 中加载进来,不过你还是需要引入 Babel 的吧。
问:额,我刚才好像说错了话。
答:是的,如果按照你所说的,你需要在生产环境下将所有的 Babel-core 引入,这样会无端端增加很多额外的性能消耗。
问:好吧,那我到底应该怎么做呢?
答:我个人建议是用 TypeScript+Webpack+SystemJS+Babel 这一个组合。
问:TypeScript?我一直以为我们在说的是 JavaScript
答:是的,TypeScriptJavaScript 的超集,基于 ES6 版本的一些封装。你应该还没忘记 ES6 吧?
问:我以为我们刚才说到的 ES2016+ + 就是 ES6 的超集了。为啥我们还需要 TypeScript 呢?
答:因为 TypeScript 允许我们以静态类型语言的方式编写 JavaScript,从而减少运行时错误。都 2016 了,添加些强类型不是坏事。
问:原来 TypeScript 是做这个的啊!
答:是的,还有一个就是 Facebook 出品的 Flow
问:Flow 又是啥?
答:FlowFacebook 出品的静态类型检测工具,基于函数式编程的 OCaml 构建。
问:OCamel函数式编程
答:你没听过吗?函数式编程高阶函数Currying? 纯函数?
问:我一无所知。
答:好吧,那你只需要记得函数式编程在某些方面是优于 OOP 的,并且我们在 2016 年应该多多使用呦。
问:等等,我在大学就学过了 OOP,我觉得挺好的啊。
答:是的,OOP 确实还有很多可圈可点的地方,不过大家已经认识到了可变的状态太容易引发未知问题了,因此慢慢的所有人都在转向不可变数据与函数式编程。在前端领域我们可以用 Rambda 这样的库来在 JavaScript 中使用函数式编程了。
问:你是不是专门一字排开名词来了?Ramda 又是啥?
答:当然不是啦,Rambda 是类似于 Lambda 的库,源自 David Chambers
问:David Chambers
答:David Chambers 是个很优秀的程序员,他是 Rambda 的核心贡献者之一。如果你要学习函数式编程的话,你还应该关注下 Erik Meijer
问:Erik Meijer
答:另一个函数式编程领域的大神与布道者。
问:好吧,还会让我们回到 React 的话题吧,我应该怎么使用 React 来抓取数据呢?
答:额,React 只是用于展示数据的,它并不能够帮你抓取数据。
问:我的天啊,那我怎么来抓取数据呢?
答:你应该使用 fetch 来从服务端获取数据。
问:fetch
答:是的,fetch 是浏览器原生基于 XMLHttpRequests 的封装。
问:那就是 AJAX 咯?
答:AJAX 一般指仅仅使用 XMLHttpRequests,而 fetch 允许你基于 Promise 来使用 AJAX,这样就能够避免 Callback hell 了。
问:Callback hell?
答:是的,每次你向服务器发起某个异步请求的时候,你必须要添加一个异步回调函数来处理其响应,这样一层又一层地回调的嵌套就是所谓的 Callback hell 了。
问:好吧,那 Promise 就是专门处理这个哩?
答:没错,你可以用 Promise 来替换传统的基于回调的异步函数调用方式,从而编写出更容易理解与测试的代码。
问:那我现在是不是直接使用 fetch 就好了啊?
答:是啊,不过如果你想要在较老版本的浏览器中使用 fetch,你需要引入 fetch Polyfill,或者使用 RequestBluebird 或者 Axios
问:来啊,互相伤害吧,你还是直接告诉我我还需要了解多少个库吧!
答:这可是 JavaScript 啊,可是有成千上万个库的。而且不少库还很大呢,譬如那个嵌了一张 Guy Fieri 图片的库。
问:你是说 Guy Fieri? 我听说过,那 BluebirdRequestAxios 又是啥呢?
答:它们可以帮你执行 XMLHttpRequests 然后返回 Promise 对象。
问:难道 jQueryAJAX 方法不是返回 Promise 吗?
答:请忘掉 jQuery 吧,用 fetch 配合上 Promise,或者 async/await 能够帮你构造合适的控制流。
问:这是你第三次提到 await 了,这到底是个啥啊?
答:awaitES7 提供的关键字,能够帮你阻塞某个异步调用直到其返回,这样能够让你的控制流更加清晰,代码的可读性也能更上一层楼。你可以在 Babel 中添加 stage-3 preset,或者添加 syntax-async-functions 以及 transform-async-to-generator 这两个插件。
问:好麻烦啊。
答:是啊,不过更麻烦的是你必须先预编译 TypeScript 代码,然后用 Babel 来转译 await
问:为啥?难道 TypeScript 中没有内置?
答:估计在下一个版本中会添加该支持,不过目前的 1.7 版本的 TypeScript 目标是 ES6,因此如果你还想在浏览器中使用 await,你必须要先把 TypeScript 编译为 ES6,然后使用 Babel 转译为 ES5
问:我已经无话可说了。
答:好吧,其实你也不用想太多,首先你基于 TypeScript 进行编码,然后将所有使用 fetch 的模块转译为 ES6,然后再使用 Babelstage-3 preset 来对 await 等进行 Polyfill,最后使用 SystemJS 来完成加载。如果你打算使用 fetch 的话,还可以使用 BluebirdRequest 或者 Axios
问:好,这样说就清晰多了,是不是这样我就达到我的目标了?
答:额,你的应用需要处理任何的状态变更吗?
问:我觉得不要把,我只是想展示数据。
答:那还行,否则的话你还需要了解 FluxRedux 等等一系列的东西。
问:我不想再纠结于这些名词了,再强调一遍,我只是想展示数据罢了。
答:好吧,其实如果你只是想展示数据的话,你并不需要 React,你只需要一个比较好的模板引擎罢了。
问:你在开玩笑?
答:不要着急,我只是告诉你你可以用到的东西。
问:停!
答:我的意思是,即使你仅仅打算用个模板引擎,还是建议使用下 TypeScript+SystemJS+Babel
问:好吧,那你还是推荐一个模板引擎吧!
答:有很多啊,你有对哪种比较熟悉吗?
问:唔,好久之前用了,记不得了。
答:jTemplates?jQote?PURE?
问:没听过,还有吗?
答:TransparencyJSRenderMarkupJS?KnockoutJS?
问:还有吗?
答:PlatesJS?jQuery-tmpl?Handlebars?
问:好像最后一个有点印象。
答:Mustache?underscore
问:好像更晚一点的。
答:Jade?DustJS?
问:不。
答:DotJS?EJS?
问:不。
答:Nunjucks?ECT?
问:不。
答:Mah?Jade?
问:额,还不是。
答?难道是 ES6 原生的字符串模板引擎。
问:我估计,这货也需要 ES6 吧。
答:是啊。
问:需要 Babel
答:是啊。
问:是不是还要从 npm 下载核心模块?
答:是啊。
问:是不是还需要 BrowserifyWebpack 或者类似于 SystemJS 这样的模块打包工具?
答:是啊。
问:除了 Webpack,还需要引入任务管理器。
答:是啊。
问:我是不是还需要某个函数式编程语言,或者强类型语言?
答:是啊。
问:然后如果用到 await 的话,还需要引入 Babel
答:是啊。
问:然后就可以使用 fetchPromise 了吧?
答:别忘了 Polyfill fetch,Safari 目前还不能原生支持 fetch
问:是不是,学完这些,就 OK 了?
答:额,目前来看是的,不过估计过几年我们就需要用 Elm 或者 WebAssembly 咯~
问:我觉得,我还是乖乖去写后端的代码吧。
答:Python 大法好!

Java 中 String 转 LocalDateTime 出现错误

场景

在 Java 中使用 LocalDateTime 解析 String 失败

代码如下

1
2
final LocalDateTime result = LocalDateTime.parse("2000-01-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
log.info("result: {}", result);

抛出异常

1
java.time.format.DateTimeParseException: Text '2000-01-01' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {},ISO resolved to 2000-01-01 of type java.time.format.Parsed

吾辈也在 SegmentFault 上提出了这个问题,然而直到写出这篇记录时然而没有人告诉吾辈答案。。。

解决

先转换为 LocalDate 再二次转换

吾辈首先找到了一种笨方法

  1. 先解析为 LocalDate
  2. LocalDate 转换为 LocalDateTime
1
2
3
final LocalDateTime localDateTime = LocalDate.parse("2018-12-11", DateTimeFormatter.ISO_DATE).atStartOfDay();
assertThat(localDateTime)
.isNotNull();

使用 DateTimeFormatter 先解析,然后转换为 LocalDateTime

  1. 使用 DateTimeFormatter.ISO_DATE 解析文本并得到 TemporalAccessor 对象
  2. 使用 temporalAccessor.get 方法获取指定属性
  3. 使用 LocalDateTime.of 构造一个 LocalDateTime 对象
1
2
3
4
5
6
7
8
9
10
11
final TemporalAccessor temporalAccessor = DateTimeFormatter.ISO_DATE.parse("2018-12-11");
final LocalDateTime localDateTime = LocalDateTime.of(
secureGet(temporalAccessor, ChronoField.YEAR),
secureGet(temporalAccessor, ChronoField.MONTH_OF_YEAR),
secureGet(temporalAccessor, ChronoField.DAY_OF_MONTH),
secureGet(temporalAccessor, ChronoField.HOUR_OF_DAY),
secureGet(temporalAccessor, ChronoField.MINUTE_OF_HOUR),
secureGet(temporalAccessor, ChronoField.SECOND_OF_MINUTE),
secureGet(temporalAccessor, ChronoField.NANO_OF_SECOND)
);
log.info("localDateTime: {}", localDateTime);

secureGet 是吾辈自定义的一个工具方法,具体看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 安全获取时间的某个属性
*
* @param temporalAccessor 需要获取的时间对象
* @param chronoField 需要获取的属性
* @return 时间的值,如果无法获取则默认为 0
*/
private static int secureGet(TemporalAccessor temporalAccessor, ChronoField chronoField) {
if (temporalAccessor.isSupported(chronoField)) {
return temporalAccessor.get(chronoField);
}
return 0;
}

使用 DateTimeFormatterBuilder 构建器

吾辈在 StackOverflow 找到了一个好的方法

  1. 使用 DateTimeFormatterBuilder 构建 DateTimeFormatter 对象
  2. 赋予可选匹配项默认值(非常重要
  3. 使用 LocalDateTime.parse 进行解析
1
2
3
4
5
6
7
8
9
10
final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd[['T'hh][:mm][:ss]]")
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.toFormatter();
final LocalDateTime localDateTime = LocalDateTime.parse("2018-12-11", formatter);
assertThat(localDateTime)
.isNotNull();

最后一种方法满足了吾辈的需求,所以,也便是在这里记录一下啦

Git 错误 Reset 恢复

场景

今天在帮同事操作 Git 的时候,因为没有清楚理解意思,吾辈错误撤回了一些提交。

具体使用的命令是

1
git reset dd256c7d66ad2e9671cbd47650ffddc4267ca7d5

感觉吾辈今天不能撤销这个错误操作的话,怕是今天别想走出公司了吧 #笑

解决

当然,吾辈没有添加 --hard 参数,想来仓库还是有救的。之后使用 Google 搜索了一下相关的内容,找到了 git 版本恢复命令 reset,然后吾辈便尝试进行了恢复。

  1. 找到使用 git reset 之前的最后一次提交的 commit id

    1
    2
    # 查看 git 记录的所有操作,包括回退操作也会记录
    git reflog
  2. 使用 git reset --hard 回退

    1
    2
    # 回退到指定提交,但不会将之后提交混入到未提交的内容
    git reset --hard dd256c7d66ad2e9671cbd47650ffddc4267ca7d5
  3. 使用 git log 检查最后一次提交是否恢复

    1
    2
    # 这时可以看到最后一次提交已经恢复了
    git log

嘛,只是偶然遇到的一个错误,吾辈也便稍微记录一下好了

在 VSCode 中使用路径别名也有提示

场景

最近在学 ReactJS,遇到了一个很奇怪的问题。当吾辈在 webpack 配置中配置了路径别名之后,VSCode 再输入路径便没有了提示。

路径别名配置

1
2
3
4
5
6
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
'@': path.resolve(__dirname, '../src'),
},

吾辈也安装了 Path Intellisense 插件,然而这毫无意义,仍然是只有在相对路径的情况下才会提示。

解决

抱着这个疑问,吾辈稍微去搜索了一下。然后,找到了 webpack 自定义别名后,VScode 路径提示问题 这个问题。在下面的回答中,吾辈找到了答案。

注:这里已采纳的答案实际上应该是复制少了一个括号导致实际使用会出错,不过确实是正确答案。

在项目根目录下添加 jsconfig.json 并添加以下配置

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}

然后重启 VSCode,之后,一切便恢复了理想状态!

原因

那么,jsconfig.json 到底是什么神奇的东西,为什么能影响到 VSCode 的提示呢?吾辈找到了 VSCode 官网上的文档,文档上对此的说明是:VSCode 大部分功能都是开箱即用,然而有些却需要进行一些基本的配置才能获得最佳体验,jsconfig 就是用来配置 JavaScript 语言的相关功能。

所以,原因明了了,这是 VSCode 内置的功能,就是为了便于开发的。而我们仅仅需要做一些简单的配置,即可使用这些功能。
吾辈也使用 tsc 命令生成了一份 jsconfig.json(由 tsconfig.json 改名),泥萌可以复制并按照自己的需求修改其中的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
{
"compilerOptions": {
/* Basic Options */
"target": "ES2018" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */

/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */

/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./" /* Base directory to resolve non-absolute module names. */,
"paths": {
"@/*": ["src/*"]
} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */,
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */

/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
}
}

SpringBoot 集成 Thymeleaf 模板引擎

场景

最近开始了一个新的项目,后端使用了 SpringBoot。因为没有进行前后端分离,所以还需要模板引擎。经过调查,我们放弃 JSP/JSTL 而选择了 SpringBoot 默认推荐的 Thymeleaf

附:不要吐槽 JSP/JSTL 很老,吾辈自己都觉得很老,然而公司不允许前后端分离,无解。。。(或许有?)

实现

创建项目

使用 springboot.io 创建项目,选择 WebThymeleaf 依赖,生成的 build.gradle 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
plugins {
id 'org.springframework.boot' version '2.1.3.RELEASE'
id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.rxliuli.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

添加公共 js 依赖管理

公共 JavaScript 依赖: templates/common/common-lib-js.html

1
2
3
4
5
6
7
8
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="common-lib-js">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js"></script>
</div>
</body>
</html>

添加公共顶部

公共的顶部: templates/common/common-header.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<body>
<header th:fragment="common-header" id="common-header">
<style>
#common-header {
height: 100px;
width: 100%;
}

#common-header .text-center {
text-align: center;
}
</style>
<h1 class="text-center">这里是公共顶部</h1>
</header>
</body>
</html>

添加公共底部

公共的底部: templates/common/common-footer.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<body>
<header th:fragment="common-footer" id="common-footer">
<style>
#common-footer {
height: 100px;
width: 100%;
}

#common-footer .text-center {
text-align: center;
}
</style>
<h1 class="text-center">这里是公共底部</h1>
</header>
</body>
</html>

在页面中引入

下面在页面中引入看看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<style>
.text-center {
text-align: center;
}
</style>
<title>首页</title>
</head>
<body>
<div th:replace="common/common-header::common-header"></div>
<main>
<p class="text-center">这里是页面单独的内容部分</p>
</main>
<div th:replace="common/common-footer::common-footer"></div>
<div th:replace="common/common-lib-js::common-lib-js"></div>
<script>
console.log($)
</script>
</body>
</html>

效果图

效果图

可以看到 common-lib-js, common-header, common-footer 都已经引入成功

注意,我们在页面中引入的顺序是

  1. common-header: 公共头部
  2. 页面自定义 HTML 内容
  3. common-footer: 公共底部
  4. common-lib-js: 公共 JavaScript 依赖
  5. 页面自定义 JavaScript 脚本

主要遵循下面几个原则

  • JavaScript 必须在 HTML body 结尾处引入,避免加载的速度问题
  • 自定义的 JavaScript 必须在公共的 JavaScript 之后引入,避免依赖找不到

更进一步

难道每个页面我们都需要引入这些公共的文件么?有什么更好的方法么?例如每个页面只要写单独的部分,在渲染的时候 自动 将页面中的单独部分渲染到某个布局页面中。
很遗憾的是,Thymeleaf 本身并未提供这个功能。然而,Thymeleaf 已经有人做出了第三方的库以提供此功能。

1.添加依赖项

1
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.3.0'

2.添加布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html
lang="zh-CN"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>layout</title>
</head>
<body>
<!--公共的头部-->
<div th:replace="common/common-header::common-header"></div>
<!--页面自定义的 HTML-->
<div layout:fragment="html"></div>
<!--公共的尾部-->
<div th:replace="common/common-footer::common-footer"></div>
<!--公共的 js 依赖-->
<div th:replace="common/common-lib-js::common-lib-js"></div>
<!--页面的 js 依赖-->
<div layout:fragment="js"></div>
</body>
</html>

3.使用布局文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html
lang="zh-CN"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorator="common/layout"
>
<head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<style>
.text-center {
text-align: center;
}
</style>
<title>首页</title>
</head>
<body>
<main layout:fragment="html">
<p class="text-center">这里是页面单独的内容部分</p>
</main>
<script layout:fragment="js">
console.log($)
</script>
</body>
</html>

再次刷新,将看到与直接引入有着相同的效果!

Java 使用 FTP/SFTP

场景

项目中需要使用 FTP,所以做了简单的 FTP/SFTP 封装,此处仅做一下记录。

注:这里并未实现连接池管理,生产环境强烈建议手动实现连接池以提高性能!

UML 图像说明

形状

注:此处参考自 IDEA UML 图中的颜色

  • 蓝色:类/步骤
  • 黄色:字段
  • 红色:函数
  • 紫色:配置

图形

  • 长方形:类/配置文件/依赖项
  • 圆角长方形:字段/函数/对象/变量
  • 箭头:拥有/向下依赖的意思

目标

封装简单的通用操作

  • 上传单个文件
  • 上传使用 InputStream(内存操作)
  • 下载单个文件
  • 下载得到 InputStream(内存操作)
  • 创建目录
  • 递归创建目录
  • 删除单个文件/空目录
  • 获取指定目录下的文件信息列表
  • 获取文件/目录信息
  • 递归获取文件/目录信息
  • 递归删除目录
  • 监听目录变化(内部使用)
  • 异步上传后等待结果

思路

  1. 定义顶层接口 FtpOperator,具体实现由子类(BasicFtpOperatorImpl, SftpOperatorImpl)完成
  2. 定义顶层配置文件基类 FtpClientConfig,包含着 ftp 连接必须的一些东西,具体细节在子类配置中 BasicFtpClientConfig, SftpClientConfig
  3. 添加工厂类 FtpOperatorFactory,根据不同子类的配置对象创建不同的 ftp 操作对象,并且一经创建就可以永久性使用
  4. 添加 FtpWatchConfig, FtpWatch, FtpWatchFactory FTP 监听器
  5. 添加集成 SpringBoot 中,读取 application.yml 中的配置,并创建不同的 FtpOperator 暴露给外部使用,动态初始化 FTP 监视器

注:这里使用 FTP 监视器的原因是为了避免每次上传数据后都要单独监听 FTP 目录的变化,造成 FTP 多线程连接数量过多
注:这里的并未实现 FTPClient 及 Jsch 的对象池管理,所以仅可参考实现,生产环境中仍需进行修改!

图解如下

图解

实现

具体的代码吾辈就不贴到这里了,全部的代码已经放到 GitHub 的公共仓库 上了。

FTP 使用

FtpOperator API 图解
FtpOperator API 图解

上传部分流程图解
上传部分流程图解

使用 FtpOperator 进行基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@RunWith(SpringRunner.class)
@SpringBootTest
public class FtpSpringConfigTest {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private FtpOperator ftp;

@Test
public void put() throws UnsupportedEncodingException {
// 上传数据
final ByteArrayInputStream is = new ByteArrayInputStream("测试数据".getBytes("UTF-8"));
final boolean result = ftp.put(is, "/test.txt");
assertThat(result)
.isTrue();
}

@Test
public void exist() {
// 判断数据是否存在于 ftp 服务器
final boolean exist = ftp.exist("/test.txt");
assertThat(exist)
.isTrue();
}

@Test
public void get() {
// 从 ftp 服务器上下载数据
ftp.get("/test.txt", is -> {
try {
final List<String> list = IOUtils.readLines(is);
log.info("list: {}", list);
assertThat(list)
.isNotEmpty();
} catch (IOException e) {
throw new RuntimeException(e);
}

});
}

@Test
public void mkdir() {
// 创建文件夹
assertThat(ftp.mkdir("/test"))
.isTrue();
}

@Test
public void mkdirR() {
// 递归创建文件夹
assertThat(ftp.mkdirR("/test/test2/test3"))
.isTrue();
}

@Test
public void ls() {
// 获取目录下的文件信息列表
final List<Stat> list = ftp.ls("/");
log.info("list: {}", list.stream()
.map(Stat::getPath)
.collect(Collectors.joining("\n")));
assertThat(list)
.isNotEmpty();
}

@Test
public void lsr() {
// 获取目录下的文件信息列表
final List<Stat> list = ftp.lsR("/");
log.info("list: {}", list.stream()
.map(Stat::getPath)
.collect(Collectors.joining("\n")));
assertThat(list)
.isNotEmpty();
}

@Test
public void rm() {
// 删除单个文件
assertThat(ftp.rm("/test.txt"))
.isTrue();
}

@Test
public void rmdir() {
// 删除指定空目录
assertThat(ftp.rmdir("/test/test2/test3"))
.isTrue();
}

@Test
public void rmdirR() {
// 递归删除指定目录
assertThat(ftp.rmdirR("/test"))
.isTrue();
}
}

使用 FtpOperator 上传文件并监听结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RunWith(SpringRunner.class)
@SpringBootTest
public class FtpSpringConfigTest extends BaseTest {
private final Logger log = LoggerFactory.getLogger(getClass());
@Autowired
private FtpOperator ftp;
@Test
public void watch() throws InterruptedException, UnsupportedEncodingException {
// 监听新文件 /test.md 的出现
final String path = "/test.md";
ftp.watch((Predicate<String>) str -> str.equals(path))
.thenAcceptAsync(stat -> {
log.warn("stat: {}", stat);
assertThat(ftp.exist(stat.getPath()))
.isNotNull();
});
// 创建测试文件
final ByteArrayInputStream is = new ByteArrayInputStream("测试数据".getBytes("UTF-8"));
log.warn("test file upload completed!");
assertThat(ftp.put(is, path))
.isTrue();
// 注意,这里有一个问题就是如果程序结束的太快,那么更新将变得不可能的!
Thread.sleep(2000);
// 删除测试文件
ftp.rm(path);
}
}

那么,关于 Java 中使用 FTP/SFTP 便到此为止啦

Java 优雅的拷贝对象属性

场景

在 Java 项目中,经常遇到需要在对象之间拷贝属性的问题。然而,除了直接使用 Getter/Stter 方法,我们还有其他的方法么?
当然有,例如 Apache Common Lang3BeanUtils,然而 BeanUtils 却无法完全满足吾辈的需求,所以吾辈便自己封装了一个,这里分享出来以供参考。

  • 需要大量复制对象的属性
  • 对象之间的属性名可能是不同的
  • 对象之间的属性类型可能是不同的

目标

简单易用的 API

  • copy: 指定需要拷贝的源对象和目标对象
  • prop: 拷贝指定对象的字段
  • props: 拷贝指定对象的多个字段
  • exec: 执行真正的拷贝操作
  • from: 重新开始添加其他对象的属性
  • get: 返回当前的目标对象
  • config: 配置拷贝的一些策略

思路

  1. 定义门面类 BeanCopyUtil 用以暴露出一些 API
  2. 定义每个字段的操作类 BeanCopyField,保存对每个字段的操作
  3. 定义 BeanCopyConfig,用于配置拷贝属性的策略
  4. 定义 BeanCopyOperator 作为拷贝的真正实现

图解

图解

实现

注:反射部分依赖于 joor, JDK1.8 请使用 joor-java-8

定义门面类 BeanCopyUtil 用以暴露出一些 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* java bean 复制操作的工具类
*
* @author rxliuli
*/
public class BeanCopyUtil<F, T> {
/**
* 源对象
*/
private final F from;
/**
* 目标对象
*/
private final T to;
/**
* 拷贝的字段信息列表
*/
private final List<BeanCopyField> copyFieldList = new LinkedList<>();
/**
* 配置信息
*/
private BeanCopyConfig config = new BeanCopyConfig();

private BeanCopyUtil(F from, T to) {
this.from = from;
this.to = to;
}

/**
* 指定需要拷贝的源对象和目标对象
*
* @param from 源对象
* @param to 目标对象
* @param <F> 源对象类型
* @param <T> 目标对象类型
* @return 一个 {@link BeanCopyUtil} 对象
*/
public static <F, T> BeanCopyUtil<F, T> copy(F from, T to) {
return new BeanCopyUtil<>(from, to);
}

/**
* 拷贝指定对象的字段
*
* @param fromField 源对象中的字段名
* @param toField 目标对象中的字段名
* @param converter 将源对象中字段转换为目标对象字段类型的转换器
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String fromField, String toField, Function<? super Object, ? super Object> converter) {
copyFieldList.add(new BeanCopyField(fromField, toField, converter));
return this;
}

/**
* 拷贝指定对象的字段
*
* @param fromField 源对象中的字段名
* @param toField 目标对象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String fromField, String toField) {
return prop(fromField, toField, null);
}

/**
* 拷贝指定对象的字段
*
* @param field 源对象中与目标对象中的字段名
* @param converter 将源对象中字段转换为目标对象字段类型的转换器
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field, Function<? super Object, ? super Object> converter) {
return prop(field, field, converter);
}

/**
* 拷贝指定对象的字段
*
* @param field 源对象中与目标对象中的字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> prop(String field) {
return prop(field, field, null);
}

/**
* 拷贝指定对象的多个字段
*
* @param fields 源对象中与目标对象中的多个字段名
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> props(String... fields) {
for (String field : fields) {
prop(field);
}
return this;
}

/**
* 执行真正的拷贝操作
*
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> exec() {
new BeanCopyOperator<>(from, to, copyFieldList, config).copy();
return this;
}

/**
* 重新开始添加其他对象的属性
* 用于在执行完 {@link #exec()} 之后还想复制其它对象的属性
*
* @param from 源对象
* @param <R> 源对象类型
* @return 一个新的 {@link BeanCopyUtil} 对象
*/
public <R> BeanCopyUtil<R, T> from(R from) {
return new BeanCopyUtil<>(from, to);
}

/**
* 返回当前的目标对象
*
* @return 当前的目标对象
*/
public T get() {
return to;
}

/**
* 配置拷贝的一些策略
*
* @param config 拷贝配置对象
* @return 返回 {@code this}
*/
public BeanCopyUtil<F, T> config(BeanCopyConfig config) {
this.config = config;
return this;
}
}

定义每个字段的操作类 BeanCopyField,保存对每个字段的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
* 拷贝属性的每一个字段的选项
*
* @author rxliuli
*/
public class BeanCopyField {
private String from;
private String to;
private Function<? super Object, ? super Object> converter;

public BeanCopyField() {
}

public BeanCopyField(String from, String to, Function<? super Object, ? super Object> converter) {
this.from = from;
this.to = to;
this.converter = converter;
}

public String getFrom() {
return from;
}

public BeanCopyField setFrom(String from) {
this.from = from;
return this;
}

public String getTo() {
return to;
}

public BeanCopyField setTo(String to) {
this.to = to;
return this;
}

public Function<? super Object, ? super Object> getConverter() {
return converter;
}

public BeanCopyField setConverter(Function<? super Object, ? super Object> converter) {
this.converter = converter;
return this;
}
}

定义 BeanCopyConfig,用于配置拷贝属性的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
/**
* 拷贝属性的配置
*
* @author rxliuli
*/
public class BeanCopyConfig {
/**
* 同名的字段自动复制
*/
private boolean same = true;
/**
* 覆盖同名的字段
*/
private boolean override = true;
/**
* 忽略 {@code null} 的源对象属性
*/
private boolean ignoreNull = true;
/**
* 尝试进行自动转换
*/
private boolean converter = true;

public BeanCopyConfig() {
}

public BeanCopyConfig(boolean same, boolean override, boolean ignoreNull, boolean converter) {
this.same = same;
this.override = override;
this.ignoreNull = ignoreNull;
this.converter = converter;
}

public boolean isSame() {
return same;
}

public BeanCopyConfig setSame(boolean same) {
this.same = same;
return this;
}

public boolean isOverride() {
return override;
}

public BeanCopyConfig setOverride(boolean override) {
this.override = override;
return this;
}

public boolean isIgnoreNull() {
return ignoreNull;
}

public BeanCopyConfig setIgnoreNull(boolean ignoreNull) {
this.ignoreNull = ignoreNull;
return this;
}

public boolean isConverter() {
return converter;
}

public BeanCopyConfig setConverter(boolean converter) {
this.converter = converter;
return this;
}
}

定义 BeanCopyOperator 作为拷贝的真正实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 真正执行 copy 属性的类
*
* @author rxliuli
*/
public class BeanCopyOperator<F, T> {
private static final Logger log = LoggerFactory.getLogger(BeanCopyUtil.class);
private final F from;
private final T to;
private final BeanCopyConfig config;
private List<BeanCopyField> copyFieldList;

public BeanCopyOperator(F from, T to, List<BeanCopyField> copyFieldList, BeanCopyConfig config) {
this.from = from;
this.to = to;
this.copyFieldList = copyFieldList;
this.config = config;
}

public void copy() {
//获取到两个对象所有的属性
final Map<String, Reflect> fromFields = Reflect.on(from).fields();
final Reflect to = Reflect.on(this.to);
final Map<String, Reflect> toFields = to.fields();
//过滤出所有相同字段名的字段并进行拷贝
if (config.isSame()) {
final Map<ListUtil.ListDiffState, List<String>> different = ListUtil.different(new ArrayList<>(fromFields.keySet()), new ArrayList<>(toFields.keySet()));
copyFieldList = Stream.concat(different.get(ListUtil.ListDiffState.common).stream()
.map(s -> new BeanCopyField(s, s, null)), copyFieldList.stream())
.collect(Collectors.toList());
}
//根据拷贝字段列表进行拷贝
copyFieldList.stream()
//忽略空值
.filter(beanCopyField -> !config.isIgnoreNull() || fromFields.get(beanCopyField.getFrom()).get() != null)
//覆盖属性
.filter(beanCopyField -> config.isOverride() || toFields.get(beanCopyField.getTo()).get() == null)
//如果没有转换器,则使用默认的转换器
.peek(beanCopyField -> {
if (beanCopyField.getConverter() == null) {
beanCopyField.setConverter(Function.identity());
}
})
.forEach(beanCopyField -> {
final String fromField = beanCopyField.getFrom();
final F from = fromFields.get(fromField).get();
final String toField = beanCopyField.getTo();
try {
to.set(toField, beanCopyField.getConverter().apply(from));
} catch (ReflectException e) {
log.warn("Copy field failed, from {} to {}, exception is {}", fromField, toField, e.getMessage());
}
});
}
}

使用

使用流程图

使用流程图

测试

代码写完了,让我们测试一下!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
public class BeanCopyUtilTest {
private final Logger log = LoggerFactory.getLogger(getClass());
private Student student;
private Teacher teacher;

@Before
public void before() {
student = new Student("琉璃", 10, "女", 4);
teacher = new Teacher();
}

@Test
public void copy() {
//简单的复制(类似于 BeanUtils.copyProperties)
BeanCopyUtil.copy(student, teacher).exec();
log.info("teacher: {}", teacher);
assertThat(teacher)
.extracting("age")
.containsOnlyOnce(student.getAge());
}

@Test
public void prop() {
//不同名字的属性
BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec();
assertThat(teacher)
.extracting("name", "age", "sex")
.containsOnlyOnce(student.getRealname(), student.getAge(), false);
}

@Test
public void prop1() {
//不存的属性
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name2")
.exec()
.get())
.extracting("age", "sex")
.containsOnlyOnce(student.getAge(), false);
}

@Test
public void from() {
final Teacher lingMeng = new Teacher()
.setName("灵梦")
.setAge(17);
//测试 from 是否覆盖
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec()
.from(lingMeng)
.exec()
.get())
.extracting("name", "age", "sex")
.containsOnlyOnce(lingMeng.getName(), lingMeng.getAge(), false);
}

@Test
public void get() {
//测试 get 是否有效
assertThat(BeanCopyUtil.copy(student, teacher)
.prop("sex", "sex", sex -> Objects.equals(sex, "男"))
.prop("realname", "name")
.exec()
.get())
.extracting("name", "age", "sex")
.containsOnlyOnce(student.getRealname(), student.getAge(), false);
}

@Test
public void config() {
//不自动复制同名属性
assertThat(BeanCopyUtil.copy(new Student().setAge(15), new Teacher())
.config(new BeanCopyConfig().setSame(false))
.exec()
.get())
.extracting("age")
.containsOnlyNulls();
//不覆盖不为空的属性
assertThat(BeanCopyUtil.copy(new Student().setAge(15), new Teacher().setAge(10))
.config(new BeanCopyConfig().setOverride(false))
.exec()
.get())
.extracting("age")
.containsOnlyOnce(10);
//不忽略源对象不为空的属性
assertThat(BeanCopyUtil.copy(new Student(), student)
.config(new BeanCopyConfig().setIgnoreNull(false))
.exec()
.get())
.extracting("realname", "age", "sex", "grade")
.containsOnlyNulls();
}

/**
* 测试学生类
*/
private static class Student {
/**
* 姓名
*/
private String realname;
/**
* 年龄
*/
private Integer age;
/**
* 性别,男/女
*/
private String sex;
/**
* 年级,1 - 6
*/
private Integer grade;

public Student() {
}

public Student(String realname, Integer age, String sex, Integer grade) {
this.realname = realname;
this.age = age;
this.sex = sex;
this.grade = grade;
}

public String getRealname() {

return realname;
}

public Student setRealname(String realname) {
this.realname = realname;
return this;
}

public Integer getAge() {
return age;
}

public Student setAge(Integer age) {
this.age = age;
return this;
}

public String getSex() {
return sex;
}

public Student setSex(String sex) {
this.sex = sex;
return this;
}

public Integer getGrade() {
return grade;
}

public Student setGrade(Integer grade) {
this.grade = grade;
return this;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}

/**
* 测试教师类
*/
private static class Teacher {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别,true 男,false 女
*/
private Boolean sex;
/**
* 职位
*/
private String post;

public Teacher() {
}

public Teacher(String name, Integer age, Boolean sex, String post) {
this.name = name;
this.age = age;
this.sex = sex;
this.post = post;
}

public String getName() {
return name;
}

public Teacher setName(String name) {
this.name = name;
return this;
}

public Integer getAge() {
return age;
}

public Teacher setAge(Integer age) {
this.age = age;
return this;
}

public Boolean getSex() {
return sex;
}

public Teacher setSex(Boolean sex) {
this.sex = sex;
return this;
}

public String getPost() {
return post;
}

public Teacher setPost(String post) {
this.post = post;
return this;
}

@Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
}

如果没有发生什么意外,那么一切将能够正常运行!


好了,那么关于在 Java 中优雅的拷贝对象属性就到这里啦

使用 Java 实现 setTimeout/setInterval

场景

之前想把 Java 代码中使用回调函数的方法改成 Promise 风格,苦思冥想而不得其解。然而突发奇想之下,吾辈尝试在 Java 中实现 JavaScript 的 setTimeout/setInterval,并在之后想到了如何封装回调为 Promise,所以便先在此将这个想法的写出来以供参考。

Promise 是 ES6 添加的一个重要的元素,它将回调函数压平为了一级调用,并在 ES7 的 async/await 中彻底改变了异步的使用方式!

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class AsyncUtil {
private static final Logger log = LoggerFactory.getLogger(AsyncUtil.class);

/**
* 将当前线程休眠指定的时间
*
* @param millis 毫秒
*/
public static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

/**
* 实现 JavaScript 中的 setTimeout
* 注:由于 {@link CompletableFuture#cancel(boolean)} 方法的限制,该定时器无法强制取消
*
* @param ms 等待时间
* @return 异步对象
*/

public static CompletableFuture<Void> setTimeout(long ms) {
return CompletableFuture.runAsync(() -> sleep(ms));
}

/**
* 实现等待指定资源加载完成
* 注:由于 {@link CompletableFuture#cancel(boolean)} 方法的限制,该定时器无法强制取消
*
* @param condition 临界条件
* @return 异步对象
*/
public static CompletableFuture<Void> waitResource(Supplier<Boolean> condition) {
return CompletableFuture.runAsync(() -> {
while (!condition.get()) {
sleep(100);
}
});
}

/**
* 实现 JavaScript 中的 setInterval 周期函数
* 该方法并不是非常精确的定时器,仅适用于一般场景,如有需要请使用 {@link ScheduledExecutorService} 类
* 注:由于 {@link CompletableFuture#cancel(boolean)} 方法的限制,该定时器无法强制取消
*
* @param ms 间隔毫秒数
* @param runnable 回调函数
* @return 永远不会完成的异步对象
*/
public static CompletableFuture<Void> setInterval(long ms, Runnable runnable) {
return CompletableFuture.runAsync(() -> {
//noinspection InfiniteLoopStatement
while (true) {
try {
runnable.run();
sleep(ms);
} catch (Exception e) {
log.error("使用 setInterval 发生异常: ", e);
}
}
});
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class AsyncUtilTest {
private final Logger log = LoggerFactory.getLogger(getClass());

public static void main(String[] args) {
final AsyncUtilTest asyncUtilTest = new AsyncUtilTest();
asyncUtilTest.setTimeout();
asyncUtilTest.waitResource();
asyncUtilTest.setInterval();
AsyncUtil.sleep(4000);
}

@Test
public void setTimeout() {
log.info("setTimeout completed before time: {}", LocalDateTime.now());
AsyncUtil.setTimeout(1000)
.thenRunAsync(() -> log.info("setTimeout completed after time: {}", LocalDateTime.now()));
}

@Test
public void waitResource() {
log.info("waitResource completed before time: {}", LocalDateTime.now());
final AtomicInteger i = new AtomicInteger(1);
AsyncUtil.waitResource(() -> i.get() == 3)
.thenRunAsync(() -> log.info("waitResource completed after time: {}", LocalDateTime.now()));
AsyncUtil.sleep(2);
i.set(3);
}

@Test
public void setInterval() {
log.info("setInterval completed before time: {}", LocalDateTime.now());
final CompletableFuture<Void> future = AsyncUtil.setInterval(100, () -> log.info("setInterval in the loop, current time: {}", LocalDateTime.now()));
AsyncUtil.sleep(500);
future.complete(null);
AsyncUtil.sleep(1000);
}
}

Spring Mongo Data 使用

前置要求

本文假设你已经了解或知道以下技能,尤其而且是勾选的内容。

  • Gradle
  • SpringBoot
  • MongoDB
  • SpringBoot 集成 MongoDB

注:本文不谈 SpringBoot 如何整合 MongoDB,如果需要可以去吾辈的另一篇记录 SpringBoot 整合 Mybatis Plus/MongoDB 查看,并且本文以项目 spring-boot-mybatis-plus-mongo-example 为基础作为说明。

使用

注:Spring Data MongoDB 是 Spring Data 的一部分,下面统一使用 MongoData 来称呼。

继承 MongoRepository<T, ID> 使用命名方法

集成了 MongoData 之后,我们可以选择让 Dao 继承 MongoRepository<T, ID> 模板以获得通用方法,并且,可以通过特定方式的命名方法让 MongoData 来帮我们自动实现。

例如

1
2
3
4
5
6
7
8
9
10
@Repository
public interface UserInfoLogRepository extends MongoRepository<UserInfoLog, Long>, CustomUserInfoLogRepository {
/**
* 根据 id 查询用户日志信息
*
* @param id 查询的 id
* @return 用户日志
*/
UserInfoLog findUserInfoLogByIdEquals(Long id);
}

这个方法将会被 MongoData 自动实现,而我们做的只是让接口方法名符合 MongoData 的命名规范罢了。

这里来说明一下 findUserInfoLogByIdEquals 方法,将之拆分开来

  • find: 代表查询的意思,可以想象成 SQL 中的 select
  • UserInfoLog: 代表查询的类型,可以想象成 select 中的表名(非必须,默认为当前 MongoRepository 的实体泛型类)
  • By: 代表着条件的开始,可以想象成 SQL 中的 where
  • Id: 代表着条件中的字段,可以想象成 where 下的条件字段名
  • Equals: 代表条件的操作,可以想象成 where 下的条件操作,此处等价于 =

是的,MongoData 会自动根据方法名创建具体的实现,而我们要做的,仅仅是让方法名复合 MongoData 的规范而已。

甚至于,我们可以添加更多的条件,例如下面的 findUserInfoLogsByUserIdEqualsAndLogTimeGreaterThanEqualAndOperateRegex

1
2
3
4
5
6
7
8
9
/**
* 根据用户 id/记录时间/操作说明查询用户日志
*
* @param userId 用户 id
* @param logTime 记录时间
* @param operate 操作说明
* @return 用户日志
*/
List<UserInfoLog> findUserInfoLogsByUserIdEqualsAndLogTimeGreaterThanEqualAndOperateRegex(Long userId, LocalDateTime logTime, String operate);

当吾辈第一次看到这么长的方法名时(是的,足足有 71 个字符组成),也只能惊呼:Oh my Gad!
这对业务层的调用实在是太过于痛苦了,尤其而且能逼死强迫症(例如吾辈),所以下面就说一种更加灵活的解决方案!

使用 MongoOperations 创建更加灵活的查询

修改 UserInfoLogRepository 并定义 listByParam() 以替代上面的 findUserInfoLogsByUserIdEqualsAndLogTimeGreaterThanEqualAndOperateRegex() 方法

1
2
3
4
5
6
7
8
9
public interface UserInfoLogRepository {
/**
* 根据一些参数查询用户信息列表
*
* @param userInfoLog 参数对象
* @return 用户信息列表
*/
List<UserInfoLog> listByParam(UserInfoLog userInfoLog);
}

创建实现类 UserInfoLogRepositoryImpl 并实现 listByParam() 方法。这里注入 MongoOperations MongoDB 操作模板,它的实现类实际上是 MongoTemplate,然后使用 Criteria 定义复杂的查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Repository
public class UserInfoLogRepositoryImpl implements UserInfoLogRepository {
@Autowired
private MongoOperations mongoOperations;

@Override
public List<UserInfoLog> listByParam(UserInfoLog userInfoLog) {
final Criteria criteria = new Criteria();
if (userInfoLog.getUserId() != null) {
criteria.and("userId")
.is(userInfoLog.getUserId());
}
if (userInfoLog.getLogTime() != null) {
criteria.and("logTime")
.gte(userInfoLog.getLogTime());
}
if (userInfoLog.getOperate() != null) {
criteria.and("operate")
.regex(userInfoLog.getOperate());
}
return mongoOperations.find(new Query(criteria), UserInfoLog.class);
}
}

可以看到,listByParam() 相对而言更加优雅,不过代码量上也有增加就是了。事实上,对于复杂的查询,最好使用这种方式进行查询。

集合 MongoRepository 和 MongoOperations

总之,上面两种方式各有优缺点。MongoRepository 对于大部分常见的操作基本都可以正常替代,而 MongoOperations 比之灵活得多,我们是否只能鱼与熊掌不可兼得呢?
当然不是,MongoData 在设计之初便充分权衡过方便与灵活性的平衡点,所以,我们可以同时使用它们!

具体使用步骤如下

自定义更加复杂的 Dao 接口

该接口定义需要自己实现的方法,需要同时被 UserInfoLogRepositoryUserInfoLogRepositoryImpl 实现

1
2
3
4
5
6
7
8
9
public interface CustomUserInfoLogRepository {
/**
* 根据一些参数查询用户信息列表
*
* @param userInfoLog 参数对象
* @return 用户信息列表
*/
List<UserInfoLog> listByParam(UserInfoLog userInfoLog);
}

定义一些简单操作的 Dao 接口

注意,这里同时继承了 CustomUserInfoLogRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface UserInfoLogRepository extends MongoRepository<UserInfoLog, Long>, CustomUserInfoLogRepository {
/**
* 根据 id 查询用户日志信息
*
* @param id 查询的 id
* @return 用户日志
*/
@Nullable
UserInfoLog findUserInfoLogByIdEquals(Long id);

/**
* 根据用户 id/记录时间/操作说明查询用户日志
*
* @param userId 用户 id
* @param logTime 记录时间
* @param operate 操作说明
* @return 用户日志
*/
List<UserInfoLog> findUserInfoLogsByUserIdEqualsAndLogTimeGreaterThanEqualAndOperateRegex(Long userId, LocalDateTime logTime, String operate);
}

定义实现 UserInfoLogRepositoryImpl 类

数据仓库 UserInfoLogRepository 的实现类,但请务必注意,实现类继承的是 CustomUserInfoLogRepository 接口,而非本应该继承的接口。而且实现类的名字必须是基础接口名 + Impl,因为 MongoData 默认使用的实现类就是这个名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class UserInfoLogRepositoryImpl implements CustomUserInfoLogRepository {
@Autowired
private MongoOperations mongoOperations;

@Override
public List<UserInfoLog> listByParam(UserInfoLog userInfoLog) {
final Criteria criteria = new Criteria();
if (userInfoLog.getUserId() != null) {
criteria.and("userId")
.is(userInfoLog.getUserId());
}
if (userInfoLog.getLogTime() != null) {
criteria.and("logTime")
.gte(userInfoLog.getLogTime());
}
if (userInfoLog.getOperate() != null) {
criteria.and("operate")
.regex(userInfoLog.getOperate());
}
return mongoOperations.find(new Query(criteria), UserInfoLog.class);
}
}

常用 API

匹配标准:Criteria

方法名 参数 功能
and String 并且
not /Object
orOperator Criteria... 并且是其他标准
andOperator Criteria... 并且是其他标准
is Object 等于
ne Object 不等于
le Object 小于
let Object 小于或等于
gt Object 大于
gte Object 大于或等于
in Object.../Collection<?> 在其中
nin Object.../Collection<?> 不在其中
mod Number,Number 模运算
all Object.../Collection<?> 包含全部
size int 长度
exists boolean 存在
type int/Type... 结构化数据的类型
regex String/String,String/Pattern 正则
alike Example<?> 匹配到最像的
isEqual Object,Object 是否相等