AmamiRyoin's Blog

守得云开见月明


  • 首页

  • 关于

  • 标签

  • 归档
  • High一下

React16新特性解读

发表于 2018-11-01

React16新特性解读

这两天逛推特的时候偶然关注了一下某前端大佬,进而发现了react团队的Dan的推特(我的微信头像,帅的一批),又进而发现react16更新了新的特性(表示已经很久没有关注过前端的技术趋势了),感觉新特性还是比较有意思的,所以写个blog来记录下,以下内容均为官网内容的整理和个人的总结,如有不对的地方欢迎指正。

这里分两块分析

  • Hooks
  • 与redux的运用(useReducer)

Hooks

这次的新特性是Hooks(钩子)。什么是钩子函数,我的理解是触发某种条件就会触发的函数。

钩子函数的运用其实是很广泛的,react中的生命周期函数其实都是钩子函数。这次的新特性中Hooks分了两种,一种叫做Basic Hooks基本钩子函数,另一种叫做Additional Hooks额外钩子函数,Hooks只能用在函数式组件中,而不能用在类组件中。

Basic Hooks

有以下几种:

  • useState
  • useEffect
  • useContext

useState

useState这个钩子取代了之前的setState方法以及对this.state的定义,这意味着你将不再需要在constructor里写this.state和使用setState方法。
另外官方文档中说道你可以多次使用这些钩子函数,也就是说一个function里面你可以写多个useState和useEffect

1
2
3
4
5
6
7
8
9
10
11
12
13
function Counter() {
//第一个参数代表默认值,它将赋给count,setCount则会取到一个更新方法
//(有个疑问,如果我有多个state参数,那么我是用一个对象包起来呢,还是说写多个useState钩子。。。。)
const [count, setCount] = useState(0);
return (
<>
Count: {count}
<button onClick={() => setCount(0)}>Reset</button>
<button onClick={() => setCount(count => count + 1)}>+</button>
<button onClick={() => setCount(count => count - 1)}>-</button>
</>
);
}

useEffect

useEffect这个钩子是生命周期函数ComponentDidMount和ComponentDidUpdate的替代,在这个钩子中你可以写对state的操作。
官方说只要是render之后都会走这个钩子。

1
2
3
4
5
6
7
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
// Update the document title using the browser API
document.title = `You clicked ${count} times`; //更改title
//这里有个问题,本来我想在didMount里写的逻辑现在在didUpdate里也会有,怎么破,条件限制?
});

这段代码的官方解释如下:

1
2
3
4
5
6
We declare the count state variable, and then we tell React we need to use an effect. 
We pass a function to the useEffect Hook. This function we pass is our effect.
Inside our effect, we set the document title using the document.title browser API.
We can read the latest count inside the effect because it’s in the scope of our function.
When React renders our component, it will remember the effect we used, and then run our effect after updating the DOM.
This happens for every render, including the first one.

下面是详细对比:

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
//这段代码表示的是获取一个朋友登录状态,是否在线
class FriendStatusWithCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0, isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}

componentDidMount() {
document.title = `You clicked ${this.state.count} times`;

//这个ChatAPI是一个外界的Api,不要在意这个,从方法名来看就是用了订阅和取消订阅来个方法来改变friend.id的值

ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}

componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}

handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function FriendStatusWithCounter(props) {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});

const [isOnline, setIsOnline] = useState(null);
//问题是这样写不会订阅取消太频繁吗?
useEffect(() => {
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});

function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
// ...
}

官方的思想是利用钩子函数应该是让我们把代码分割成一个个的doing块,而不是按照生命周期来分割代码,组件的effects应该按照代码顺序来执行(可能表达的不是很对)。

原文如下:

1
2
Hooks lets us split the code based on what it is doing rather than a lifecycle method name. 
React will apply every effect used by the component, in the order they were specified.

再来看componentDidUpdate方法,它其实有两个默认参数prevProps和prevState,用来表示前一个props和state。所以useEffect也同样有这么个东西,即第二个参数(array类型)。

官方代码如下:

1
2
3
4
5
6
7
8
9
10
11
//原来的写法
componentDidUpdate(prevProps, prevState) {
if (prevState.count !== this.state.count) {
document.title = `You clicked ${this.state.count} times`;
}
}

//useEffect的写法
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);

解释一下这个[count],当渲染组件的时候第一次进入useEffect的时候会将你传入的第二个参数进行保存,当下次进来的时候useEffect会去判断这次的count和上次的count时候不同,从而起到原本componentDidUpdate的效果。

官方解释如下:

1
2
3
4
5
6
7
8
9
In the example above, we pass [count] as the second argument. 
What does this mean? If the count is 5, and then our component re-renders with count still equal to 5,
React will compare [5] from the previous render and [5] from the next render.
Because all items in the array are the same (5 === 5), React would skip the effect.
That’s our optimization.

When we render with count updated to 6, React will compare the items in the [5] array from the previous
render to items in the [6] array from the next render. This time, React will re-apply the effect because 5 !== 6.
If there are multiple items in the array, React will re-run the effect even if just one of them is different.

useContext

官方文档对这个钩子函数的描述比较少,可以看下官方的解释。

1
2
3
4
5
6
7
8
const context = useContext(Context);

/*
Accepts a context object (the value returned from React.createContext) and returns the current context value,
as given by the nearest context provider for the given context.

When the provider updates, this Hook will trigger a rerender with the latest context value.
*/

这个钩子函数会接收一个React.createContext返回的Context对象,并且将当前最近的provider的context的值返回,当provider更新时,这个钩子函数将会触发最新的context进行重绘

这里补充下React.createContext这个16版本的新特性。

因为React是单向数据流,所以数据的传递都是从上之下传递的,当我父节点的值想要传递个孙节点的时候一般来讲要通过子节点,
如下图


图是老图,图中方法名不是新特性的方法名,这里只是讲个概念

所以有了context(上下文)这个东西,
如下图

图是老图,图中方法名不是新特性的方法名,这里只是讲个概念

React.createContext会返回一个包含Provider和Consumer这两个组件的对象,在Provider组件中设置的value将能够在Consumer组件中使用,也就是说你在父组件外包一层Provider组件,将父组件的值传给Provider的value,将孙组件外层包Consumer组件,孙组件就可以拿到父组件的值。这里要注意的是Consumer接收的是一个函数对象。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let {Provider,Consumer} = React.createContext();
<Provider value={{a:1,b:2}}>
<Father>
<Son>
<GrandChild/>
</Son>
</Father>
<Provider>
function GrandChild(){
return(
<Consumer>
{(context) => (
<span>{context.a}{context.b}</span>
)}
</Consumer>
)
}

现在回到useContext,很明确了useContext可以让你直接拿到最近的Provider的value值,作为context用在你当前的组件里,不需要再去写Consumer这个组件。

Additional Hooks

有以下几种:

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeMethods
  • useMutationEffect
  • useLayoutEffect

这里就不全都介绍了,介绍几个我认为比较重要的钩子函数。

useReducer

这个比较重要,是数据传递方面的钩子函数,将在后面的与redux的运用写一起。

useMemo

官方解释如下:

1
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

这个钩子函数官方讲的也很简单,并没有具体的例子,这个函数会返回一个memoized的value,它有两个入参,分别是”create” function和一个array类型的参数,array将作为输入传给useMemo,useMemo将会保存这个值,当array中的值发生变化时才会去走”create” function这个函数方法从而更新memoizedValue,这样就避免了rerender过程中昂贵的计算。
个人demo如下,因为没有官方例子,所以自己写了一个:

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
import React, { useState, useEffect, useMemo } from 'react'
function Toggle () {
const step = 2;
const [count, setCount] = useState(0)
const changeCount = (count,step)=>{
console.log("ccccccccccc\n",count)
return function(){
return count+step
};
}
let memoizedValue = useMemo(()=>{
changeCount(count,step)
},[count,step])

//很遗憾这个memoizedValue没有打印出值,所以上述内容中的create打了引号,事实上官方也是打引号的,但官方没有解释这个create是什么函数方法
console.log(memoizedValue)
return (
<div>
<br/>
<p>Memo</p>
<span>{memoizedValue}</span>
<button onClick={() => setCount(count===3?count:count+1)}>//每次点击改变count的值,count改变后会去触发useMemo
click
</button>
</div>
)
}

export default Toggle

上述例子很简单就是点击改变count的值从而去触发useMemo,当count为3的时候count的值就不变了,useMemo就不会去执行changeCount这个方法,这个方法就是上述的create function

useCallback

这个钩子函数和useMemo一样,有两个入参,一个是callBack方法,一个是array类型的inputs,也是只有当inputs中的值改变时才回去执行callBack方法,官方例子如下:

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

同样它返回一个memoized类型的Callback(日了,这个memoized到底是什么),同样目的也是为了防止rerender的时候走这些方法函数,这个useCallback有个好处就是你可以将对于子组件的操作放callBack里,起到一个类似生命周期函数shouldComponentUpdate的功能。

这个demo没写,因为与useMemo功能类似就不搞了(不想写,好烦,要写子组件和孙组件)。

useRef

作用和之前this.refs是一样的,官方demo如下,就不赘述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
function TextInputWithFocusButton() {
const inputEl = useRef();
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}

关于新特性的hooks就这么多了,没讲到的是我觉得写代码的时候不经常用的,其实我ref都不想写,因为本来就不推荐用这个东西。

与redux的运用(useReducer)

我觉得新特性的hooks与redux的运用才是重点,然而当前官方文档的重心是在useState和useEffect上,因为官方花了大篇幅来讲解useEffect和useState。
现阶段我也只是摸索阶段,可能有讲的不对的地方。

useReducer

新特性中有个useReducer,这个让熟悉redux的人眼前一亮,难道不需要第三方库就能解决react难受的数据的传递问题了么。
先来看下官方的useReducer的例子:

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
const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case 'reset':
return initialState;
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
}
}

function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'reset'})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}

可以看出这个demo和我们写redux的时候十分相似,不,应该说简直一毛一样。
这里我就不赘述redux了,直接讲useReducer。
从上面的例子可以看出,useReducer接收了两个参数,reducer和initialSate,并且返回了对应的一个数组将值解构给了state和dispatch,下面通过按钮操作了state的值进行对应的变化。
这里可以看下useReducer的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var ReactCurrentOwner = {
curren:null,
currentDispatch:null
}
function resolveDispatcher() {
var dispatcher = ReactCurrentOwner.currentDispatcher;
!(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component.') : void 0;
return dispatcher;
}
function useReducer(reducer, initialState, initialAction) {
var dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialState, initialAction);
}

惊呆了,userReducer只有四行代码,但是追溯下去发现有问题啊,dispacher哪来的useReducer,其实官方文档里说了,上面的userReducer的代码只是一个简版(react.development.js中),真正的useReducer代码如下(react-dom.development.js中),很复杂的一段代码,这里就不讲了,太复杂(毕竟我是菜鸡,Dan的那种层次还达不到)。通过打印useReducer的返回值,可以看到,返回数组的第一个元素是state,第二个是一个function dispatchAction,这和redux十分相似。

再来看下上面的demo的代码结构,结合redux的写法,可以发现demo中其实是把reducer和action写在一起了,这其实是和redux的理念冲突的,因为redux提倡在reducer中不做不纯净的操作,希望将赋值和对数据的操作分开,因此,一般对于数据的操作都写在action中,reducer中只是取值赋值操作。

另外一个严厉的问题就是异步怎么写?


总结

新特性也是刚出来,还不是正式版,所以我也只是对于当前beta版本做一些解读,如有不对欢迎指正。

editBy AmamiRyoin

js实现图片转成ASCII码

发表于 2018-06-27

js实现图片转成ASCII码

之前有朋友问我微信头像(ASCII码构成的头像)怎么搞的,感觉很有意思,所以趁着项目转测没啥bug把这个写了。

canvas实现

直接上代码了,不啰嗦。

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
<body>
<input type="file"/>
<canvas id="canvas"></canvas>
<div id="img"></div>
<script>
$("input").on('input',function(){
let _this = this;
var file = $("input")[0].files[0];
var reader = new FileReader();
reader.onload=function(){
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var img = new Image();
img.src = this.result;
img.onload =function(){
this.width = this.width*0.2;
this.height = this.height*0.2;//图片像素太大所以将图片品质压缩下
invert(this,this.width,this.height);
};
function invert(img,w,h) {
canvas.width = w;
canvas.height = h;
context.drawImage(img,0,0,w,h);
//获取图片对象以及元素点的数组
var img1 = context.getImageData(0, 0, w, h);
var data = img1.data;
//转换灰度图
var arr=["M","N","H","Q","$","O","C","?","7",">","!",":","–",";","."];
var result=[];
for (var i = 0, len = data.length; i < len; i += 4) { //一个像素点对应数组种四个元素
var avg=(data[i]+data[i+1]+data[i+2])/3;
var num=Math.floor(avg/18); //arr长度为14,要让num落在0-14中,而rgb为0-255,因此除18
result.push(arr[num]);
if((i+4)%(w*4)==0&&i!=0){ //ASCII码换行
result.push("<br/>")
}
}
document.getElementById("img").innerHTML=result.join();
}
}
reader.readAsDataURL(file)
})
</script>
</body>

综上,不解释了都是些很简单的东西。

edit by AmamiRyoin

Antd的一些经验总结

发表于 2018-06-15

Ant Design

这个月赶着做了一个汽车金融的后台评估系统,技术栈用的react+react-redux+react-router+antd+webpack,昨天新版本上线后总算有时间来总结总结了。Antd是蚂蚁金服的一个UI库,提供了各种各样的UI组件,方便开发者开发,提高开发效率。这里就总结一下在使用antd组件时遇到的一些问题。

Form表单

对于antd中form表单的使用其实碰到了很多问题(毕竟之前一直用的不多),对于实际业务中的需求来说,它基本满足了所有需求。

表单的初始值(回显)

实际业务中经常有编辑的操作,编辑操作必定要将之前编辑的数据展示在表单上,即回显。关于默认值的设置,antd不允许对表单组件用value或defaultValue两个属性来设置(仅仅针对使用了getFieldDecorator方法进行了绑定的组件),而推荐使用form属性下的setFieldsValue这个方法来设置默认值。由于不能用value或defaultValue来设值,所以对应的对于每个使用了getFieldDEcorator绑定了的组件可以用getFieldValue方法来获取相应的组件的值。

表单校验规则

实际业务中也经常要对用户输入的内容进行校验,除了校验表单是否必填,也会对表单输入内容做一些规则上的校验比如对于手机号码的校验,身份证的校验等等,这里可以通过form表单下的getFieldDecorator方法来进行设置,该方法有两个参数,如下:

1
getFieldDecorator(id, options) // 其中id是表单对应的name,options中可以设置一些参数用于对表单的限制

另外想要进行的规则校验可以写在options下的rules属性,该属性是一个数组,如下:

1
2
3
4
5
6
7
8
9
10
<FormItem {...formItemLayout} label={'银行卡号'}>
{getFieldDecorator('cardId', {
initialValue: id=='add'?'':this.state.editData.accountNo,
rules: [{type:'string', required: true, message: '请输入银行卡号'},{
validator: this.checkFormBankId,
}],
})(
<Input placeholder="请输入银行卡号" maxLength="21"></Input>
)}
</FormItem>

在rules数组中有两个子元素,第一个中的type表示该字段的类型,通过这个方法包裹的组件的默认值的变量类型是不一样的,这个时候就可以通过这个字段来设置接收的变量类型,默认是string类型,除了string还可以选择如下类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
string: Must be of type string. This is the default type.
number: Must be of type number.
boolean: Must be of type boolean.
method: Must be of type function.
regexp: Must be an instance of RegExp or a string that does not generate an exception when creating a new RegExp.
integer: Must be of type number and an integer.
float: Must be of type number and a floating point number.
array: Must be an array as determined by Array.isArray.
object: Must be of type object and not Array.isArray.
enum: Value must exist in the enum.
date: Value must be valid as determined by Date
url: Must be of type url.
hex: Must be of type hex.
email: Must be of type email.

required表示该字段是否必填,message表示提示信息,当字段校验出错时会提示对应的内容。
validator就是我们所需要做的规则校验,对应的校验规则可以写在你的方法中。

Table表格

对于antd中的Table表格操作的最多的其实是带checkbox的表格

保留上一页的勾选

在开发过程中碰到过这种情况,在表格中勾选需要操作的选项后翻页,发现上页勾选的位置上这一页上也被勾选了,这显然是不科学的,造成这种现象的原因是有两点,一是表格数据是接口请求的并非一次性返回给你,因此下页数据请求过来后上页的数据没了,表格内部机制无法拿到的对应的行;二是没有给Table组件设置rowKeys属性,rowKeys属性可以用来让Table内部机制进行对行数据的筛选,一般rowKeys属性用行id来设置,这样可以保证每一行的rowkey不同,从而得到区分,代码如下:

1
Table className="che300-table" rowSelection={rowSelection} rowKey="projectId" columns={columns} dataSource={this.props.searchAftloanPkgData} pagination={pkgPaConf}></Table>

树形组件

对于antd的树形组件我一开始是很懵逼的,官方的文档demo写的不是很直观。

如何定义每一个节点的key值

树形组件的难点在于如何构建树形节点的key值,官方demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<Tree
checkable
defaultExpandedKeys={['0-0-0', '0-0-1']}
defaultSelectedKeys={['0-0-0', '0-0-1']}
defaultCheckedKeys={['0-0-0', '0-0-1']}
onSelect={this.onSelect}
onCheck={this.onCheck}
>
<TreeNode title="parent 1" key="0-0">
<TreeNode title="parent 1-0" key="0-0-0" disabled>
<TreeNode title="leaf" key="0-0-0-0" disableCheckbox />
<TreeNode title="leaf" key="0-0-0-1" />
</TreeNode>
<TreeNode title="parent 1-1" key="0-0-1">
<TreeNode title={<span style={{ color: '#1890ff' }}>sss</span>} key="0-0-1-0" />
</TreeNode>
</TreeNode>
</Tree>

由于树形结构的层级结构,key的设置并不容易,所以前后端对于数据结构的讨论较多,具体什么数据结构就不说了,讲不清。。。。唯一的痛点就是数据结构,其他没什么。

换肤,主题色的更改

antd是默认的蓝白风格,这次后台系统是黄黑风格,因此需要对antd进行换肤,具体的教程官方也有,有两种方式,一种是用新的样式覆盖,另一种是用webpack,这里选择使用webpack,通过loader进行less全局变量的替换,代码如下:

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
{
loader: require.resolve('less-loader'),
options: {
modifyVars: {
"@font-size-base": '14px',
"@table-padding-vertical": '4px',
"@table-padding-horizontal": '4px',
"@table-header-bg": '#e9e9e9',
"@primary-color": "#ffc847",
"@btn-primary-color": "#333",
"@text-color": '#fff',
"@layout-body-background": '#2b2b2b',
"@label-color": '#fff',
"@input-bg": '#333',
"@input-placeholder-color": '#666',
"@table-row-hover-bg": '#534e41',
"@component-background": '#333',
"@tag-default-bg": '#ffc847',
"@tag-default-color": '#fff',
"@item-hover-bg": '#484235',
"@background-color-base": '#484235',
"@item-active-bg": '#484235',
"@disabled-color": '#ffc847',
"@table-header-bg": '#595959',
"@heading-color": '#fff',
"@text-color-secondary": '#fff',
"@btn-default-color": '#333',
"@border-color-split": '#595959',
"@border-color-base": '#666',
"@background-color-active": '#ffc847'
},
},
}

edit by AmamiRyoin

h5、微信、小程序三端环境判断

发表于 2018-05-11

h5、微信、小程序三端环境判断

这次的需求代码中存在兼容以上三端的情况,下文就如何判断这三个环境作详解(H5就不说了,没有判断的必要)。

微信环境的判断

对于微信可以通过全局变量WeixinJSBridge来判断当前代码是否在微信环境下,即

1
window.WeixinJSBridge ? '在微信' :'不在微信'

但上述变量WeixinJSBridge并不会在执行代码时立刻挂载在window对象上,因此这个并不是一个最稳妥的判断方法,除非你执行上述代码的时候能保证window对象下已经挂载了这个变量。在不能确定是否存在这个变量时如果要判断当前是否在微信环境下,这里还有另外一个方法,代码如下:

1
/MicroMessenger/.test(navigator.userAgent) ? '在微信' :'不在微信'

微信环境下的navigator.userAgent这个字符串中会存在MicroMessenger这个子字符串,因此可以通过上述方法在不确定WeixinJSBridge是否已挂载的情况下来判断是否是微信环境。

小程序环境的判断

对于小程序来说也有一个全局变量可以用来判断是否是小程序环境————window.__wxjs_environment

1
window.__wxjs_environment ? '在小程序' :'不在小程序'

它和WeixinJSBridge一样并不能在第一时间获取到,中间也有一个挂载的过程,如果在尚未挂载之前要对环境进行判断则需要监听事件。

由于小程序的执行环境在微信程序中,所以可以通过监听WeixinJSBridgeReady事件来进行判断,代码如下:

1
2
3
4
5
6
7
8
9
if(!window.WeixinJSBridge || !WeixinJSBridge.invoke) {  //首先判断当前是否存在微信桥
document.addEventListener('WeixinJSBridgeReady', function(){ //微信桥不存在则监听微信桥准备事件
if(window.__wxjs_environment==='miniprogram'){ //当微信桥挂在上了之后则判断当前微信环境是否为小程序
console.log("在小程序")
}else{
console.log("在微信")
}
}, false)
}

综上为判断三端环境的详细说明

edit By AmamiRyoin

js移动端调用摄像头拍照并实现上传

发表于 2018-05-11

js移动端调用摄像头拍照并实现上传

前言

这次的需求功能中有一个用到手机拍照的功能,用户通过手机拍照从而识别图片中的信息,这个功能必然需要将图片上传到服务器,服务端实现图片的识别。(以下代码结合Vue)

需求分析

  • 图片拍摄

js代码调用手机摄像头可以通过input标签的file类型来实现,代码实现如下:

1
<input type="file" multiple="multiple" accept="image/*" capture="camera" @change="getImage" ref="file">

通过这个方式可以成功调用手机的摄像头和相册选取功能(红米5只支持相册选取,小米的奇葩手机,磨人);

  • 图片上传

图片上传这里就有问题了,直接上传图片是不现实的,现在的手机像素都很高,拍照的图片质量必然很大,直接上传服务器并不显示,在这里就需要将图片进行压缩。

图片压缩可以通过两种方式:

  • 一种是服务端进行操作,比如七牛云或者阿里云的图片处理,但是现在的场景下并不可能;
  • 另一种是客户端处理,通过canvas来降低图片质量,进行图片压缩。

很显然是选择下面一种,直接上代码不bb

HTML部分:

1
<canvas id="dealPic" style="display:none;"></canvas>

JS部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let _this = this;
let img = new Image();
var reader = new FileReader();
reader.onload = function(){
_this.base64Img = this.result; //在这里获取base64码
img.src = _this.base64Img;
img.onload=function(){
let canvas = document.getElementById('dealPic');
let context = canvas.getContext('2d');
context.scale(0.4,0.4);
canvas.width = img.width*0.4;
canvas.height = img.height*0.4;
context.drawImage(img, 0, 0, img.width*0.4, img.height*0.4); //缩小画布,毕竟手机拍下来的图片都是几千像素乘几千像素的大小
_this.base64Img = canvas.toDataURL(_this.$refs.file.files[0].type,0.5); //canvas生成后通过toDataURL获取canvas 处理后的其base64码,并自定义其画质,此处是0.5
let PicBlob = _this.getBlobBydataURI(_this.base64Img,_this.$refs.file.files[0].type); //转成文件流方便上传服务器
}
}
reader.readAsDataURL(_this.$refs.file.files[0]);

到此,通过实验已经将原7M大小的照片转成了200多K,画质上来讲差的也不是太多,这样上传图片服务器压力就小了很多很多,用户体验也就更好。

在压缩一步

其实200多K还是很大,虽然相比于原来的7M来说是小了很多,但是相信很多人和我一样希望能够在保证图片质量的情况下再缩小图片大小,这个是可行的,上面介绍了两种方法来压缩图片,我们完全可以在客户端压缩图片后再上传服务器来进行图片的处理。因为公司用的是七牛云,所以这里介绍下七牛云的服务端图片处理。

其实很简单就分两步:上传七牛云服务器和图片地址加参

上传七牛云就不说了,这里介绍下图片地址加参。

七牛云的图片处理服务为七牛云上的图片文件提供以下功能:

  • 对图片进行缩略操作
  • 图片缩放、裁剪、旋转
  • 获取图片格式、大小、色彩模型信息
  • 提供数码相机照片的可交换图像文件格式
  • 图片添加图片、文字、图文混合水印
  • 计算图片的平均色调

接口如下:

接口 简介
imageslim 图片瘦身(imageslim)将存储在七牛的JPEG、PNG格式的图片实时压缩而尽可能不影响画质。
imageView2 图片基本处理接口可对图片进行缩略操作,生成各种缩略图。imageView2 接口可支持处理的原图片格式有 psd、jpeg、png、gif、webp、tiff、bmp。

七牛提供了很多图片处理的接口,这里我用的是imageView2这个接口来进行图片操作;

接口规格

注意:接口规格不含任何空格与换行符。

1
2
3
4
5
6
imageView2/<mode>/w/<LongEdge>
/h/<ShortEdge>
/format/<Format>
/interlace/<Interlace>
/q/<Quality>
/ignore-error/<ignoreError>

上面一次往下是长,宽,输出格式,渐进显示,图片质量以及失败返回等参数。

其中 <mode> 分为如下几种情况:

接口 简介
/0/w/<LongEdge>/h/<ShortEdge> 限定缩略图的长边最多为,短边最多为,进行等比缩放,不裁剪。如果只指定 w 参数则表示限定长边(短边自适应),只指定 h 参数则表示限定短边(长边自适应)
/1/w/<Width>/h/<Height> 限定缩略图的宽最少为,高最少为,进行等比缩放,居中裁剪。转后的缩略图通常恰好是 x 的大小(有一个边缩放的时候会因为超出矩形框而被裁剪掉多余部分)。如果只指定 w 参数或只指定 h 参数,代表限定为长宽相等的正方图。
/2/w/<Width>/h/<Height> 限定缩略图的宽最多为,高最多为,进行等比缩放,不裁剪。如果只指定 w 参数则表示限定宽(长自适应),只指定 h 参数则表示限定长(宽自适应)。它和模式0类似,区别只是限定宽和高,不是限定长边和短边。从应用场景来说,模式0适合移动设备上做缩略图,模式2适合PC上做缩略图。
/3/w/<Width>/h/<Height> 限定缩略图的宽最少为,高最少为,进行等比缩放,不裁剪。如果只指定 w 参数或只指定 h 参数,代表长宽限定为同样的值。你可以理解为模式1是模式3的结果再做居中裁剪得到的。
/4/w/<LongEdge>/h/<ShortEdge> 限定缩略图的长边最少为,短边最少为,进行等比缩放,不裁剪。如果只指定 w 参数或只指定 h 参数,表示长边短边限定为同样的值。这个模式很适合在手持设备做图片的全屏查看(把这里的长边短边分别设为手机屏幕的分辨率即可),生成的图片尺寸刚好充满整个屏幕(某一个边可能会超出屏幕)。
/5/w/<LongEdge>/h/<ShortEdge> 限定缩略图的长边最少为,短边最少为,进行等比缩放,居中裁剪。如果只指定 w 参数或只指定 h 参数,表示长边短边限定为同样的值。同上模式4,但超出限定的矩形部分会被裁剪。

由于要考虑到图像识别的问题,图片不能被处理地太过变样,因此采取第一个接口/0/w/<LongEdge>/h/<ShortEdge>,图片长宽方面只是进行等比缩放。

上面说了除了长宽,还可以对质量进行处理,/q/0.5这个表示50%的质量,取值在0~1之间。

综上是这次移动端调用手机摄像头的经验之谈,踩坑之旅。

edit By AmamiRyoin

Redux异步处理之Redux-thunk与Redux-saga

发表于 2018-04-23

Redux异步处理之 Redux-thunk与Redux-saga

Redux异步

众所周知,redux中所有的dispatch都是同步的,而处理ajax或fetch之类的异步请求一般要分为三步。

Action:

需要写三个action来处理——fetching、fetched、error这三步,请看代码:

1
2
3
{ type:'FETCH_POSTS_REQUEST'}//发起请求
{ type:'FETCH_POSTS_FAILURE', payload: 'errorInfo' }//请求失败
{ type:'FETCH_POSTS_SUCCESS', payload:{data}//请求成功并且获取到数据

Reducer:

同样需要写三个对应的reduce来进行对state的处理以及合并,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let reducer = (state = initialState, action) => {
switch(action.type) {
//发起请求时reducer的处理
case 'FETCH_POSTS_REQUEST': {
return {...state, fetching: true}
break;
}
//请求失败时的处理
case 'FETCH_POSTS_FAILURE': {
return {...state, fetching: false, error: action.payload}
break;
}
//请求成功
case 'FETCH_POSTS_SUCCESS': {
return {...state, fetching: false, fetched: true, users: action.payload}
break;
}
}
return state;
}

如何进行dispatch

由于dispatch是同步操作,reducer中又是以纯函数的形式进行代码编写,不能往里面加入其它带有操作性质的代码,因此异步的操作我们需要借助中间件来完成。
redux中 applyMiddleware 用法如下:

1
2
3
4
const store = createStore(
reducer,
applyMiddleware(...middlewares)
);

为了了解中间件到底做了些什么,先来看下源码:

applyMiddleware:

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
function applyMiddleware() {
for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
middlewares[_key] = arguments[_key];
}//定义一个middlewares数组,长度和入参相同,并且将入参传入该数组中
return function (createStore) {//这里的creatStore即前面的creatStore方法
return function (reducer, preloadedState, enhancer) {//这里的enhancer可以就当成是中间件
var store = createStore(reducer, preloadedState, enhancer);
var _dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: function dispatch(action) {
return _dispatch(action);
}
};
chain = middlewares.map(function (middleware) {
return middleware(middlewareAPI);
});//我们可以看下chain里面是个什么东西
_dispatch = compose.apply(undefined, chain)(store.dispatch);//嵌套执行中间件的组合逻辑(因此才有中间件执行顺序一说)
return _extends({}, store, {
dispatch: _dispatch //说白了中间件实际上就是改造了dispatch
});
};
};
}

compose:

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
/*
* Composes single-argument functions from right to left. The rightmost
* function can take multiple arguments as it provides the signature for
* the resulting composite function.
*
* @param {...Function} funcs The functions to compose.
* @returns {Function} A function obtained by composing the argument functions
* from right to left. For example, compose(f, g, h) is identical to doing
* (...args) => f(g(h(...args))).
*/
function compose() {
for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
funcs[_key] = arguments[_key];
}
if (funcs.length === 0) {
return function (arg) {
return arg;
};
}
//如果长度为1,返回该函数
if (funcs.length === 1) {
return funcs[0];
}
//核心语句reduce嵌套执行所有的函数最后返回一个最终函数
return funcs.reduce(function (a, b) {
return function () {
return a(b.apply(undefined, arguments));
};
});
}

综上其实compose嵌套执行的函数实际上就是类似下面这个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function f1(next) {
return function() {
console.log('f1 start')
next()
console.log('f1 end')
}
}
function f2(next) {
return function() {
console.log('f2 start')
next()
console.log('f2 end')
}
}
function f() {
console.log('heart')
}
f1(f2(f))() //所以最后的综合逻辑函数就是类似这种M1(M2(dispatch))(action)

creatStore:

中间件有关的主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
......

redux的applyMiddle就分析到这边,在了解了这个得基础上,下面就来介绍两个常用的异步流处理的中间件,redux-thunk和 redux-saga,本文就这两个中间件进行探讨和比较其中优劣。

认识Redux-thunk

redux-thunk的官方文档是这样介绍的:

1
从异步的角度,Thunk 是指一切都就绪的会返回某些值的函数。你不用传任何参数,你只需调用它,它便会返回相应的值。

本栗子结合thunk的使用方式如下:

1
2
3
4
5
6
7
8
9
10
//此处dispatch的不是一个action对象而是一个函数
store.dispatch((url)=>(dispatch)=>{
dispatch({
type:'FETCH_POSTS_REQUEST'
});//发起请求(象征性的触发一下,表示我准备开始请求了,其实个人感觉不写都无所谓,只是改变了一下fetching的值而已#滑稽)
fetch(url).then(data => dispatch({
type:'FETCH_POSTS_SUCCESS',
payload:data
}));//请求成功后更新state
})

因此,从上述代码可以看出redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数,并且触发dispatch都是从UI进行触发,逻辑都是写在UI。

那么为什么thunk可以传递一个函数,这里我们来看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
//这里的意思是指如果这个action是函数,那么就执行它,否则next
return next(action);//将此action传递到下一个middleware
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;

但由此可以看出redux-thunk的缺点:

  • action 虽然扩展了,但因此变得复杂,后期可维护性降低;
  • thunks 内部测试逻辑比较困难,需要mock所有的触发函数( 主要因素);
  • 协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;
  • 业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部。(主要因素);

完整代码

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
import {applyMiddleware, createStore} from 'redux';
import axios from 'axios';
import thunk from 'redux-thunk';
const initialState = { fetching: false, fetched: false, users: [], error: null }
const reducer = (state = initialState, action) => {
switch(action.type) {
case 'FETCH_POSTS_REQUEST': {
return {...state, fetching: true}
break;
}
case 'FETCH_POSTS_FAILURE': {
return {...state, fetching: false, error: action.payload}
break;
}
case 'FETCH_POSTS_SUCCESS': {
return {...state, fetching: false, fetched: true, users: action.payload}
break;
}
}
return state;
}
const middleware = applyMiddleware(thunk);
store.dispatch((dispatch) => {
dispatch({type: 'FETCH_POSTS_REQUEST'});
axios.get('./data.json').then((response) => {
dispatch({type: 'FETCH_POSTS_SUCCESS', payload: response.data})
})
.catch((err) => {
dispatch({type: 'FETCH_POSTS_FAILURE', payload: err})
})
});

认识Redux-saga

首先什么是saga?
saga,这个术语常用于CQRS架构,代表查询与责任分离。

saga的作者是个搞房地产的,业余时间写写代码,和阮一峰有点像。

官方的介绍是这样的:

1
2
redux-saga 是一个旨在使应用程序副作用(即数据获取等异步操作和获取浏览器缓存等非纯函数操作)更容易管理,执行更有效率,容易测试和解决错误的库。这个构思模型是一个saga就像一个应用程序中仅仅用于解决这些副作用的单线程。
redux-saga是一个redux中间件,具有完全访问redux应用状态,也可以迅速处理redux action,这意味着这个线程可以被主程序中的正常redux action启动、暂停和取消。它利用了es6之中的generators的特点来使这些异步数据流容易读取、写入和测试。这样做,这些异步数据流看起来就像是你的标准的同步JavaScript代码。(类似于async/await),除此之外,generators有一些我们需要的令人惊叹的特征。

redux-saga 将异步任务进行了集中处理,且方便测试。

所有的东西都必须被封装在 sagas 中。sagas 包含3个部分,用于联合执行任务:

  • worker saga :做所有的工作,如调用 API,进行异步请求,并且获得返回结果。
  • watcher saga :监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务。
  • root saga :立即启动 sagas 的唯一入口

使用方法

  • 使用createSagaMiddleware方法创建saga 的Middleware,然后在创建的redux的store时,使用applyMiddleware函数将创建的sagaMiddleware实例绑定到store上,最后可以调用saga Middleware的run函数来执行某个或者某些Middleware。
  • 在saga的Middleware中,可以使用takeEvery或者takeLatest等API来监听某个action,当某个action触发后,saga可以使用call、fetch等api发起异步操作,操作完成后使用put函数触发action,同步更新state,从而完成整个State的更新。
    首先需要启动saga,启动saga一般都写在入口文件中,下面是个栗子:
1
2
3
4
5
6
7
8
9
10
11
12
13
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';
import appReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(appReducer,applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);//saga一旦执行就会永远执行下去
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);

然后,就可以在 sagas 文件夹中集中写 saga 文件了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { take, fork, call, put } from 'redux-saga/effects';
//执行函数即work saga
function* fetchUrl(url) {
try{//利用try-catch来捕获异常
const data = yield call(fetch, url); // 指示中间件调用 fetch 异步任务
yield put({ type: 'FETCH_POSTS_SUCCESS', payload:data }); // 指示中间件发起一个 action 到 Store
}catch(e){
yield put({ type: 'FETCH_POSTS_FAILURE', payload:error })
}
}
// 监听函数即watch saga
function* watchFetchRequests() {
while(true) {
const action = yield take('FETCH_POSTS_REQUEST'); // 指示中间件等待 Store 上指定的 action,即监听 action
yield fork(fetchUrl, action.url); // 指示中间件以无阻塞调用方式执行 fetchUrl
}
}

在 redux-saga 中的基本概念就是:sagas 自身不真正执行副作用(如函数 call),但是会构造一个需要执行副作用的描述。中间件会执行该副作用并把结果返回给 generator 函数。
对于sages ,采用 Generator 函数来 yield Effects(包含指令的文本对象)。Generator 函数的作用是可以暂停执行,再次执行的时候从上次暂停的地方继续执行。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。你可以通过使用 effects API 如 fork,call,take,put,cancel 等来创建 Effect。

关于Effect官方是这样解释的:

在 redux-saga 的世界里,Sagas 都用 Generator 函数实现。我们从 Generator 里 yield 纯 JavaScript 对象以表达 Saga 逻辑。 我们称呼那些对象为 Effect。Effect 是一个简单的对象,这个对象包含了一些给 middleware 解释执行的信息。 你可以把 Effect 看作是发送给middleware 的指令以执行某些操作(调用某些异步函数,发起一个 action 到 store)。

对上述例子的说明:

  • 引入的 redux-saga/effects 都是纯函数,每个函数构造一个特殊的对象,其中包含着中间件需要执行的指令,如:call(fetchUrl, url) 返回一个类似于 {type: CALL, function: fetchUrl, args: [url]} 的对象。
  • 在 watcher saga watchFetchRequests中:
    首先 yield take(‘FETCH_REQUEST’) 来告诉中间件我们正在等待一个类型为 FETCH_REQUEST 的 action,然后中间件会暂停执行
    wacthFetchRequests generator 函数,直到 FETCH_REQUEST action 被 dispatch。一旦我们获得了匹配的 action,中间件就会恢复执行 generator 函数。下一条指令 fork(fetchUrl, action.url) 告诉中间件去无阻塞调用一个新的 fetchUrl 任务,action.url 作为 fetchUrl 函数的参数传递。中间件会触发 fetchUrl generator 并且不会阻塞 watchFetchRequests。当fetchUrl 开始执行的时候,watchFetchRequests会继续监听其它的 watchFetchRequests actions。当然,JavaScript 是单线程的,redux-saga 让事情看起来是同时进行的。
  • 在 worker saga fetchUrl 中,call(fetch,url) 指示中间件去调用 fetch 函数,同时,会阻塞fetchUrl 的执行,中间件会停止 generator 函数,直到 fetch 返回的 Promise 被 resolved(或 rejected),然后才恢复执行 generator 函数。

这里我们可以来看一波源码,搞清楚这个Effect到底是什么。
对于Effect对象的定义,写在了 redux-saga/src/internal/io.js 文件中,下面是Effect的定义。

1
const effect = (type, payload) => ({ [IO]: true, [type]: payload });

很简单就一句话,表明这个effect其实就是返回了一个对象。

接下来看看所谓的put和call到底是个什么东西

put
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function put(channel, action) {
if (process.env.NODE_ENV === 'development') {
if (arguments.length > 1) {
check(channel, is.notUndef, 'put(channel, action): argument channel is undefined')
check(channel, is.channel, `put(channel, action): argument ${channel} is not a valid channel`)
check(action, is.notUndef, 'put(channel, action): argument action is undefined')
} else {
check(channel, is.notUndef, 'put(action): argument action is undefined')
}
}
if (is.undef(action)) {
action = channel
channel = null
}
return effect(PUT, { channel, action })
}
call
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function getFnCallDesc(meth, fn, args) {
if (process.env.NODE_ENV === 'development') {
check(fn, is.notUndef, `${meth}: argument fn is undefined`)
}
let context = null
if (is.array(fn)) {
[context, fn] = fn
} else if (fn.fn) {
({ context, fn } = fn)
}
if (context && is.string(fn) && is.func(context[fn])) {
fn = context[fn]
}
if (process.env.NODE_ENV === 'development') {
check(fn, is.func, `${meth}: argument ${fn} is not a function`)
}
return { context, fn, args }
}
export function call(fn, ...args) {
return effect(CALL, getFnCallDesc('call', fn, args))
}

出乎意料都是只返回了一个纯对象( 先不管细节 )。
effect返回的纯对象由于generate函数的机制会将yield的控制权交给外部,用来给generator外层的执行容器task( 这东西我讲不清楚所以就不讲了 )发送一个信号,告诉task该做什么。task在接收到effect发出的指令后将会执行下面这段函数。

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
function next(arg, isErr) {
// Preventive measure. If we end up here, then there is really something wrong
if (!mainTask.isRunning) {
throw new Error('Trying to resume an already finished generator')
}
try {
let result
if (isErr) {
result = iterator.throw(arg)
} else if (arg === TASK_CANCEL) {
/**
getting TASK_CANCEL automatically cancels the main task
We can get this value here
- By cancelling the parent task manually
- By joining a Cancelled task
**/
mainTask.isCancelled = true
/**
Cancels the current effect; this will propagate the cancellation down to any called tasks
**/
next.cancel()
/**
If this Generator has a `return` method then invokes it
This will jump to the finally block
**/
result = is.func(iterator.return) ? iterator.return(TASK_CANCEL) : { done: true, value: TASK_CANCEL }
} else if (arg === CHANNEL_END) {
// We get CHANNEL_END by taking from a channel that ended using `take` (and not `takem` used to trap
End of channels)
result = is.func(iterator.return) ? iterator.return() : { done: true }
} else {
result = iterator.next(arg)//这里将会执行generator并将结果赋值给result
}
if (!result.done) {//这里会判断这个generator是否执行完毕
runEffect(result.value, parentEffectId, '', next) //这里的runEffect就是各种执行结果的返回(全部流程到此结束)
} else {
/**
This Generator has ended, terminate the main task and notify the fork queue
**/
mainTask.isMainRunning = false
mainTask.cont && mainTask.cont(result.value)
}
} catch (error) {
if (mainTask.isCancelled) {
log('error', `uncaught at ${name}`, error.message)
}
mainTask.isMainRunning = false
mainTask.cont(error, true)
}
}

Redux-saga优点

  • 声明式 Effects:所有的操作以JavaScript对象的方式被 yield,并被 middleware 执行。使得在 saga 内部测试变得更加容易,可以通过简单地遍历 Generator 并在 yield 后的成功值上面做一个 deepEqual 测试。
  • 高级的异步控制流以及并发管理:可以使用简单的同步方式描述异步流,并通过 fork(无阻塞) 实现并发任务。
  • 架构上的优势:将所有的异步流程控制都移入到了 sagas,UI 组件不用执行业务逻辑,只需 dispatch action 就行,增强组件复用性。

Edit by AmamiRyoin

日常Js学习汇总(2)

发表于 2018-04-13

日常Js学习汇总(2)

最近搞项目改版,存在新老需求,这次就项目内遇到的一些问题和学习到的新知识做一个笔记。

cookie

主要是设置cookie和删除cookie的问题,一般设置cookie可以如下设置

1
document.cookie = `${"cookie名"}=${"cookie值"};expires=${"cookie生命时间"};domain=${"存储域名"};path=${"相对路径"}`;

或者用jquery.cookie这个库来设置(主要就是方便点,不用拼那么长字符串),如下

1
$.cookie("cookie名","cookie值",{expires:'cookie生命时间',domain:'存储域名',path:'相对路径'});

实现上没什么难度,但是在项目中代码执行后偏偏没有在控制台中看到存入的cookie,很蛋疼。

最后发现问题出在domain上。项目里是区分环境的,不同的环境存入不同的域名,其实这也不是问题,问题是我设置的是测试环境的域名,但是我看代码效果的时候去的是docker。。。。。瞬间感觉自己的智商被清零了。。。。。。主要还是当初试着在控制台里输入了上述代码,控制台里确实也存入了和当前域名不一样的域名的cookie,被这个坑惨了,而且至今都不知道为什么在控制台写可以存不同域名的cookie但是代码里就存不进去。

mescroll插件问题

新需求中有个顶部下拉刷新,底部上拉加载的功能,这里使用了mescroll这个插件,demo示例如下

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
var mescroll = new MeScroll("mescroll", {
down: {
auto: false, //是否在初始化完毕之后自动执行下拉回调callback; 默认true
callback: downCallback //下拉刷新的回调
},
up: {
auto: true, //是否在初始化时以上拉加载的方式自动加载第一页数据; 默认false
isBounce: false, //此处禁止ios回弹,解析(务必认真阅读,特别是最后一点): http://www.mescroll.com/qa.html#q10
callback: upCallback, //上拉回调,此处可简写; 相当于 callback: function (page) { upCallback(page); }
toTop:{ //配置回到顶部按钮
src : "../res/img/mescroll-totop.png", //默认滚动到1000px显示,可配置offset修改
//offset : 1000
}
}
});
function downCallback(){
//联网加载数据
getListDataFromNet(0, 1, function(data){
//联网成功的回调,隐藏下拉刷新的状态
mescroll.endSuccess();
//设置列表数据
setListData(data, false);
}, function(){
//联网失败的回调,隐藏下拉刷新的状态
mescroll.endErr();
});
}
function upCallback(page){
//联网加载数据
getListDataFromNet(page.num, page.size, function(curPageData){
//联网成功的回调,隐藏下拉刷新和上拉加载的状态;
//mescroll会根据传的参数,自动判断列表如果无任何数据,则提示空;列表无下一页数据,则提示无更多数据;
console.log("page.num="+page.num+", page.size="+page.size+", curPageData.length="+curPageData.length);

//方法一(推荐): 后台接口有返回列表的总页数 totalPage
//mescroll.endByPage(curPageData.length, totalPage); //必传参数(当前页的数据个数, 总页数)

//方法二(推荐): 后台接口有返回列表的总数据量 totalSize
//mescroll.endBySize(curPageData.length, totalSize); //必传参数(当前页的数据个数, 总数据量)

//方法三(推荐): 您有其他方式知道是否有下一页 hasNext
//mescroll.endSuccess(curPageData.length, hasNext); //必传参数(当前页的数据个数, 是否有下一页true/false)

//方法四 (不推荐),会存在一个小问题:比如列表共有20条数据,每页加载10条,共2页.如果只根据当前页的数据个数判断,则需翻到第三页才会知道无更多数据,如果传了hasNext,则翻到第二页即可显示无更多数据.
mescroll.endSuccess(curPageData.length);

//设置列表数据
setListData(curPageData, true);
}, function(){
//联网失败的回调,隐藏下拉刷新和上拉加载的状态;
mescroll.endErr();
});
}

示例代码中并没有什么问题,但是实际运用中由于要判断是否存在下一页的情况,这里就会使用到mescroll.endSuccess这个方法(接口数据中并没有返回total或是pageTotal);

其实没有total或者pagetotal也不是什么大问题,完全可以通过pageSize来判断是否存在下一页数据,坑就坑在它的加载中这段标签也不是插件内部自动控制的,这个标签也需要用mescroll.endSuccess这个方法来进行判断,否则当前页加载完之后下次再拖到底部时就不会触发上拉加载方法。

微信小程序的支付功能跳转

由于这次需求中将m站和小程序搞在一起,准备将m站嵌在小程序的web-view中,然而这里有个问题,那就是支付功能,小程序内部不允许调用微信接口的支付功能,只能使用调用小程序自己的支付接口,这意味着原本m站的支付功能将失效,而如果调用小程序的支付接口那就必须从内嵌的m站页面中将参数传给小程序,所以后来就想了一个办法,通过webview跳转时的url把支付所需参数传给小程序,从而调用小程序的支付功能。

具体实现是m站内部判断出当前环境是小程序还是h5页面,小程序则将支付所需参数通过调用小程序的跳转方法跳到一个空白的web-veiw上将url的参数带出,并在这个新的web-view上进行支付功能的实现,如果不是小程序就还是用原来的微信支付方式支付。小程序具体代码如下:

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
onLoad: function (options) {
var that = this;
//页面加载调取微信支付(原则上应该对options的携带的参数进行校验)
that.requestPayment(options);
},
//根据 obj 的参数请求wx 支付
requestPayment: function (obj) {
//获取options的订单Id
var orderId = obj.orderId;
//调起微信支付
wx.requestPayment({
//相关支付参数
'timeStamp': obj.timestamp,
'nonceStr': obj.nonceStr,
'package': 'prepay_id=' + obj.prepay_id,
'signType': obj.signType,
'paySign': obj.paySign,
//小程序微信支付成功的回调通知
'success': function (res) {
//定义小程序页面集合
var pages = getCurrentPages();
//当前页面 (wxpay page)
var currPage = pages[pages.length - 1];
//上一个页面 (index page)
var prevPage = pages[pages.length - 2];
//通过page.setData方法使index的webview 重新加载url 有点类似于后台刷新页面
//此处有点类似小程序通过加载URL的方式回调通知后端 该订单支付成功。后端逻辑不做赘述。
prevPage.setData({
url: "https://xxxxxxxxxx.com/wx_isPayment.jhtml?orderId=" + orderId + '&ispay=0',

}),
//小程序主动返回到上一个页面。即从wxpay page到index page。此时index page的webview已经重新加载了url 了
//微信小程序的page 也有栈的概念navigateBack 相当于页面出栈的操作
wx.navigateBack();
},
//小程序支付失败的回调通知
'fail': function (res) {
console.log("支付失败"),
console.log(res)
var pages=getCurrentPages();
var currPage = pages[pages.length - 1];
var prevPage = pages[pages.length - 2];
console.log("准备修改数据")
prevPage.setData({
url: "https://xxxxxxxxxx/wx_isPayment.jhtml?orderId=" + orderId + '&ispay=0' ,
}),
console.log("准备结束页面")
wx.navigateBack();
}
})
}

以上就是这次项目中所获取的经验。

edit by AmamiRyoin

React杂谈之Immutable

发表于 2018-03-27

React杂谈之Immutable.js

Immutable.js是什么,有何意义

immutable从英文翻译过来叫做不可改变的,所以Immutable.js就是用来生成不可变的数据的。

Javascript的缺陷

众所周知在javascript中,对象一般是可变的,如下:

1
2
3
4
let foo={a: 1}; 
let bar=foo;
bar.a=2;
console.log(foo.a) // 2

这里可以看出foo的a属性其实也跟着bar中的a属性发生了改变。

Immutable的定义

这里就直接引用官方的原话了:

Immutable 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。

那如果通过immutable来对对象进行操作会怎样呢,下面来看代码以便于更好地理解上面一段话:

1
2
3
4
import Immutable from 'immutable';
let foo = Immutable.Object({a:1});
let bar = foo.set('a',2); // 定义一个bar变量,并且将一个新的Immutable返回给它
console.log(foo.get('a')); // 获取foo对象(Immutable)的a属性,结果是1

从上面的代码可以看出虽然赋值给bar的时候对foo进行了修改操作,但是foo中的属性值并没有发生变化,这也就印证了上述所说‘Immutable 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象’的这句话。

Immutable的意义

那么说了这么多,这个Immutable的意义究竟在哪里。这里主要分为以下几点:

  • Immutable 降低了 Mutable 带来的复杂度

    可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。

    1
    2
    3
    4
    5
    function touchAndLog(touchFn) {
    let data = { key: 'value' };
    touchFn(data);
    console.log(data.key);
    }

    在上述代码中你不可能知道到底会打印出什么,因为我们无法确定touchFn对data到底做了哪些操作。那如果这个data是个Immutable类型的数据呢,毫无疑问打印出来是value。

  • 节省内存

    Immutable.js 使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。

  • 可以做到对于数据应用的时间旅行等功能

    因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。

  • 函数式编程

    Immutable 本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

与React全家桶摩擦出的火花

在React中使用Immutable的好处

其实下面这张图更方便理解:

Immutable

当数据发生改变的时候,只有关联节点会被修改,其他节点则会被复制一份,最终产生一个新的数据树;

由于react中我们只要执行了this.setState()方法,不管state的值是否发生变化都会重新执行一遍render方法进行重新渲染,所以在react中有一个名为shouldComponentUpdate的钩子函数,它会根据你返回的布尔值来进行判断是否重新进行渲染,代码如下:

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
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}

shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}

上面的代码是官方给的例子,然而可以看出来如果state结构异常复杂的话(比如对象的嵌套)这种比较是没有用的,这里存在着浅比较的问题这个钩子函数也会相应的变得复杂。(这里不明白的话可以去看shouldComponentUpdate的源码,对于嵌套对象的比较是用的Object.is()方法进行的比较)

对于这种情况,immutable就是一个非常好的解决方案,由于immutable的不可变性,我们可以非常轻松的在shouldComponentUpdate中进行prevProps和nextProps的比较。

Immutable中提供is()方法来进行Immutable对象间的比较,它比较的是两个对象的 hashCode 或 valueOf,由于Immutable内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。

Immutable与Redux摩擦出的火花

综上所述,为了避免浅比较存在的问题并且能够在redux数据流中进行react的渲染优化,我们可以将Immutable引入进来,引入Immutable就意味着要修改原本的数据结构类型,这就意味着我们要修改Action和Reducer以及组件中所有用到state的地方(工作量稍微有点庞大),对于Reducer的修改 这里可以引入redux-immutable这个库。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//store.js

import { createStore } from 'redux';
import { combineReducers } from 'redux-immutablejs';

import Immutable from 'immutable';
import * as reducers from './reducers';

const reducer = combineReducers(reducers);
const state = Immutable.fromJS({});

const store = reducer(state);
export default createStore(reducer, store);
`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//reducers.js

import { createReducer } from 'redux-immutablejs'
const initialState = Immutable.fromJS({ isAuth: false })

/**
* Reducer domain that handles authentication & authorization.
**/
export default createReducer(initialState, {
[LOGIN]: (state, action) => state.merge({
isAuth: true,
token: action.payload.token
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//container.js

const mapStateToProps = state => ({
todos: state.get('todos').get('items')
})

const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(TodoActions, dispatch)
})

export default connect(
mapStateToProps,
mapDispatchToProps
)(App)

对于container你也可以直接映射一个Immutable对象给state,通过get方法传递到元素中去,这里随便个人的喜好了。

结束

edit by AmamiRyoin

HTML与Css小技巧(1)

发表于 2018-03-24

HTML与Css小技巧(1)

前言

俗话说能用css解决的问题千万别用js,这里主要就给各位大佬介绍下不常用的或者说是新奇的css小技巧。

css 混合模式

熟悉PS的人应该都知道混合模式,实际上在canvas和svg中也有涉及,而在css中混合模式被称为mix-blend-mode,属性如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mix-blend-mode: normal;          //正常
mix-blend-mode: multiply; //正片叠底
mix-blend-mode: screen; //滤色
mix-blend-mode: overlay; //叠加
mix-blend-mode: darken; //变暗
mix-blend-mode: lighten; //变亮
mix-blend-mode: color-dodge; //颜色减淡
mix-blend-mode: color-burn; //颜色加深
mix-blend-mode: hard-light; //强光
mix-blend-mode: soft-light; //柔光
mix-blend-mode: difference; //差值
mix-blend-mode: exclusion; //排除
mix-blend-mode: hue; //色相
mix-blend-mode: saturation; //饱和度
mix-blend-mode: color; //颜色
mix-blend-mode: luminosity; //亮度

mix-blend-mode: initial; //初始
mix-blend-mode: inherit; //继承
mix-blend-mode: unset; //复原

额,具体的什么效果我就不说了,程序员当然都是直接上代码啊!

请选择以下选项改变mix-blend-mode属性

日常Js学习汇总(1)

发表于 2018-03-24

日常Js学习汇总(1)

前言

去年11月份本来就打算开始写博客的结果拖到现在,也是服了自己的执行力了,不过总算也是开始写起来了。如果觉得写的不错,各位看官还请打个赏呗#滑稽
上个月刚跳槽换公司,面试时候出的笔试题当初做的一脸懵逼啊,倒不是说难,反而可以说是太基础,基础到平时大家都不会怎么去用(只是以我个人的观点来看,我承认自己基本功不是很扎实#流下了弱者的泪水),这里就凭着本人不算强的记忆力,把这些题目分享下。

Js基础之Number对象

Number 对象是原始数值的包装对象。

创建 Number 对象的语法:

1
2
var myNum=new Number(value);
var myNum=Number(value);
参数

参数 value 是要创建的 Number 对象的数值,或是要转换成数字的值。

返回值

当 Number() 和运算符 new 一起作为构造函数使用时,它返回一个新创建的 Number 对象。如果不用 new 运算符,把 Number() 作为一个函数来调用,它将把自己的参数转换成一个原始的数值,并且返回这个值(如果转换失败,则返回 NaN)。

Number 对象属性

  • constructor ——–返回对此对象的 Number 函数的引用。

  • MAX_VALUE ——–表示的最大的数。

  • Min_VALUE ——–表示的最小的数。

  • NaN ——–非数字值。(可以理解为Not a Number)

  • NEGATIVE_INFINITY ——–负无穷大,溢出时返回该值。

  • POSITIVE_INFINITY ——–正无穷大,溢出时返回该值。

  • prototype ———原型。

Number 对象方法

  • toString ———把数字转换为字符串,使用指定的基数。

  • toLocaleString ——–把数字转换为字符串,使用本地数字格式顺序。

  • toFixed ——–把数字转换为字符串,结果的小数点后有指定位数的数字。

  • toExponential ——–把对象的值转换为指数计数法。(这个不查手册还真不知道还有这种方法)

  • toPrecision ——–把数字格式化为指定的长度。(这个也是查了手册才知道的)

  • valueOf ——–返回一个 Number 对象的基本数字值。

对于Number对象的描述

在 JavaScript 中,数字是一种基本的数据类型。JavaScript 支持 Number 对象,该对象是原始数值的包装对象。在必要时,JavaScript 会自动地在原始数据和对象之间转换。构造函数 Number() 可以不与运算符 new 一起使用,而直接作为转化函数来使用。以这种方式调用 Number() 时,它会把自己的参数转化成一个数字,然后返回转换后的原始数值(或 NaN)。构造函数通常还用作 5 个有用的数字常量的占位符,这 5 个有用的数字常量分别是可表示的最大数、可表示的最小数、正无穷大、负无穷大和特殊的 NaN 值。 注意,这些值是构造函数 Number() 自身的属性,而不是单独的某个 Number 对象的属性。

1
2
3
4
var big = Number.MAX_VALUE //正解

var n= new Number(2);
var big = n.MAX_VALUE; //错误用法,这里的变量n是new了Number对象,所以这里的n是Number的一个构造函数,并不是Number对象,所以它没有MAX_VALUE这个属性
12
AmamiRyoin

AmamiRyoin

20 日志
14 标签
RSS
GitHub Twitter
© 2022 AmamiRyoin
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4