SQL参数化查询

SQL注入想必是每个人都听过,其原理和XSS攻击很相似,都是把用户的输入当做程序去执行。防御办法也很类似,就是对用户的输入进行转义,但是同样转义十分麻烦,因为SQL注入攻击的方式和变种实在太多,转义需要考虑到的情况也复杂多变;而另外一种方式就是使用参数化查询–Prepared Statements。

SQL注入

在先介绍参数化查询的时候我们先复习一下SQL注入,上面提到其原理是把用户的输入当做了SQL语句程序的一部分去执行,因为我们经常使用字符串拼接来构建SQL语句。

在这里开始演示一下(使用MySQL):

在我的数据库中我数据库中我建立了一个名为urls的表,其结构和数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> desc urls;
+-------------+------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+------------------+------+-----+-------------------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| url | varchar(255) | NO | | | |
| insert_time | timestamp | NO | | CURRENT_TIMESTAMP | |
| tid | bigint(20) | YES | | NULL | |
+-------------+------------------+------+-----+-------------------+----------------+
mysql> select * from urls;
+----+-----------------------+---------------------+------+
| id | url | insert_time | tid |
+----+-----------------------+---------------------+------+
| 1 | http://www.limoer.cc | 0000-00-00 00:00:00 | NULL |
| 2 | http://baidu.com/news | 0000-00-00 00:00:00 | NULL |
| 3 | http://do.io | 0000-00-00 00:00:00 | NULL |
| 5 | http://github.iod | 0000-00-00 00:00:00 | NULL |
+----+-----------------------+---------------------+------+

该表有4字段并且有4条记录,现在我们如果想要查询id=1的那条记录,应该这样写:select * from urls where id=1。执行该条语句,正确返回结果,现在我们修改一下这条语句,改成:select * from urls where id=1 and 1=1,执行这条语句,同样没问题,返回结果正常;我们接下来再把and 改成 or再执行,结果出乎我们的意料,我们把所有的记录都查询了出来,id=1的限定条件失效了。至于如何导致其失效,是因为or后面的条件1=1是恒等的,所以前面的限定条件已经不重要了,and也是如此,我们想要获取正确的结果,那么and后面的限定条件必须要正确才可以。

说到这里,其实我们就已经进行了一次SQL注入的攻击,并且窃取了数据库的所有记录(更严重的删库、窃取管理员密码也很easy)!

其实不光是上面演示到的使用and or来进行SQL注入,还有很多神奇的SQL语法让SQL注入有了可乘之机,例如我们常用的union等等。

解决办法

如果我们把上面情景放在实际开发过程中,我们可能现在有一个输入框,用户可以输入任意一个数据来查看某条记录,
服务端的SQL语句也许是这样的:select * from urls where id=${userInput}。如果某个淘气的用户不遵守约定输入了非数字,例如10 or 1=1,SQL语句拼接过后就成了这样:select * from urls where id=10 or 1=1,表中的信息一次被完全暴露!

针对上面的情况,我最想想到的不是转义输入也不是使用参数化查询,而是针对本问题,我们直接对其进行输入验证即可,既然其必须限定用户输入数字,那么在进行SQL拼接之前,对用户输入进行验证即可!

例如,在Node.js环境下,我们可以使用parseInt(userInput)就可以完成对用户输入进行强制性的验证。

第二种也就是最常用的解决办法就是转义,和防御XSS攻击一样,我们需要构建用于转义的函数,对用户的输入进行转义,还是上面的那个例子:

1
select * from urls where `id`= ${id};

如果用户输入1 or 1=1,那么毫无疑问将会导致一次非常严重的SQL注入攻击,现在假设我们已经写好了我们的转义函数escape,我们只需要在进行字符串拼接之前,做一次转义即可。 例如对于用户的输入1 or 1=1经过转义后变成了'1 or 1=1',经过SQL拼接过后则变成了:

1
select * from urls where `id`='1 or 1=1';

不出意外,我们得到了正确的结果。

关于转义函数escape如何实现,这里就不不再多说,很多数据库的驱动工具都带有相应的工具函数,我们在实际开发过程中一定要注意对用户的输入进行转义,来避免SQL注入攻击;当然,如果你使用参数化查询的话,就完全没有必要了。

参数化查询

最开始提到参数化查询的时候,我提到了Prepared Statements也就是预处理语句,其实我们可以把参数化查询理解为预处理,我们把完整的一次SQL查询分成两部分,第一步是预先查询,第二步使用参数得到结果。具体该怎么理解呢,还是接着上面的那个例子,现在我们使用参数化查询执行select * from urls where id=1。其分为两步,第一步执行select * from urls where id=?,注意这里的?,其实代表了未来将要传入的参数;第二步,传入用户的输入作为具体的id值,并且输出结果。这里要注意,因为执行完第一步的时候期待第二步传入的是一个用户的id(这里必须是数字),这时候用户传入的非法输入就不会生效,这也就从根本上杜绝了了SQL注入攻击。

好了,参数化查询(预处理)可以完全避免SQL注入,其还有其他的优点例如更加可读(相比于字符串拼接),多次查询性能会有提升(因为会对预处理语句进行缓存再利用)等。

说了这么多,那么如何使用参数化查询呢?很简单,使用一个支持该特性的数据库连接工具就可以了,比如我们下面要演示的Node环境下MySQL的参数化查询。

Demo

我们在Node环境下进行演示,首先通过npm install mysql2命令安装数据库连接工具,这里是mysql2,能够支持参数化查询。

如下:

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
const mysql2 = require('mysql2');
const conn = mysql2.createConnection({
host: 'localhost',
user: 'admin',
password: '123',
database: 'news'
});
// 不使用任何防护手段(将导致SQL注入攻击)
const userInput = '1 or 1=1';
conn.query(
`select * from urls where id=${userInput}`,
(err, result) => {
console.log(result);
}
);
// 使用转义(这里默认进行了转义)
conn.query(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result) => {
console.log(result);
}
);
// 使用参数化查询
conn.execute(
'select * from urls where `id`=?',
['1 or 1=1'],
(err, result, fields) => {
console.log(result);
}
);

尾巴

关于SQL注入和参数化查询就介绍到这里,如果你觉得参数化查询两步走我说得并不明确,你可以使用抓包工具来加深理解;还有最后的Demo,其实query和execute的区别就是一个支持了参数化查询而另外一个不支持;如果你运行Demo,仔细看,区别就藏在里面(Tips:B & T);最后,请总是使用参数化查询!

分享到 评论

使用react-transition-group实现路由切换动画

我们在使用React开发SPA的时候,使用react-router可以完成路由切换,但是这样路由切换是非常生硬的。有什么解决办法呢?我们可以使用react-transition-group来实现自定义的路由切换效果。

需要注意的是react-transition-group目前有两个版本,v1和v2版本的差距十分巨大,本教程使用的是最新的V2版本,你可以使用npm install --save react-transition-group来安装,如果想安装v1版本,则只需使用npm install --save react-transition-group@1.x命令即可。

react-transition-group主要提供三个组件TransitionTransitionGroupCSSTransition。从名字当中我们知道TransitionGroup作为一个容器组件,而其它两个组件才是实现动画的关键。这里我只介绍CSSTransition如何使用以及其注意的点。如需了解更多react-transition-group,请查看官方文档

CSSTransition

这个组件主要是使用css来控制组件的转场。它使用了在缓动中appearenterexit的三个状态,并且提供钩子类让我们自定义效果。

我们常用到的类有:

.className-enter
.className-enter.className-enter-active
.className-exit
.className-exit.className-exit-active

这里className是你自定义动画的名称,和V1版本大体相同的钩子类,只不过把leave改成了更加语义化的exit,这里需要注意。

CSSTransition有多个十分重要的属性:

  1. classNames属性接收一个字符串类名,注意这里是classNames而不是className
  2. timeout用于规定动画执行的时间,如果enterexit的持续时间相同的话可以使用timeout={number}即可,如果持续时间不一样,则timeout接收一个字典,两个键分别是enter和exit。
  3. 其他参数例如onEnteronExit你可以自定义逻辑在动画进行到某个阶段后触发。
  4. 动画进行的阶段:enter->entering->entered->exit->exiting->exited

例子

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

let App = () => (
<BrowserRouter>
<Route render={({location}) => {
return <div>
<Route exact path="/" render={() => (
<Redirect to="/home"/>
)}/>
<TransitionGroup>
<CSSTransition
key={location.pathname.split('/')[1]}
classNames="transitionWrapper" // 这里一定要注意的是:classNames 而不是className
timeout={400}
mountOnEnter={true}
unmountOnExit={true}
>
<div className="wrapper">
<Switch location={location}>
<Route exact path="/home" component={Home}/>
<Route path="/inspiration" render={() =><NavLink style={{marginRight: '20px', marginLeft: '20px'}} to="/home">HOME</NavLink>
}/>
<Route path="/mood" render={() => <h1>this is page3 mood!</h1>}/>
</Switch>
</div>
</CSSTransition>
</TransitionGroup>
</div>
}}/>
</BrowserRouter>
);
分享到 评论

使用antd和css-modules冲突的解决办法

在暑假做项目实训的时候前端就使用到React构建并且使用了Ant Design作为组件库,当时就使用了extract-text-webpack-plugin把css单独抽离出来成为一个单独的css文件并引入,当时就遇到一个问题,当我使用css-loader来处理css时,并不能处理自定义的css,但是我把CSS直接写进组件中是可行的,由于当时项目比较小并且时间比较赶,就直接使用了这种方式,在开发过程中也有苦说不清,但总算是完成了。

最近想写一点东西,又用到antd了,当然是相同的问题,只不过时过境迁,我有足够多的时间来处理这个遗留下来的问题。可是即使有那么多的时间,可是还是踩坑无数,最终还是完成了。相信遇到这个问题的并不止我一个人,这里就先记录下来,希望能对你有所帮助。

解决办法

经过查询和思考,解决这样的问题最好是单独处理antdCSS和自定义的CSS。好了问题解决办法已经很明显了,我们需要些两个不同的规则来出来css,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: 'css-loader'
})
},
{
test: /\.css$/,
exclude: /node_modules/,
use: 'css-loader'
},
...
plugins: [
new ExtractTextPlugin('style.css')
]

上面的代码我建立了两规则分别处理自定义css和antd 预定义css,我们可以正常的使用import './style.css'的形式引入css,但是我们查看页面,并没有加载我们自定义的css。

好吧,既然这样再试试css-modules的方式算了,我们把第二个规则改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
...
{
test: /\.css$/,
exclude: /node_modules/,
use: [
loader:'css-loader',
options: {
modules: true,
localIndentName: '[local]--[hash:base64:5]'
}
]
}

现在我们可以通过import style from './style.css'的形式引入自定义css,并且通过style.className的形式给元素设置类。这次倒好,直接build不成功了,我一气之下索性不搞了;为了继续捣鼓下去,我直接又把CSS写在组件中了,直到我要使用react-transition-group来做路由切换动画,不得不倒回来解决。这次比以往更加冷静,我仔细阅读了extract-text-webpack-plugin的readme过后,恍然大悟,原来我们可以在一个项目中使用多个ExtractTextPlugin实例来生成多个css文件!好了,这次还是通过两个规则处理css,并且构建两个css文件,一个是自定义的css,一个是antd css,问题迎刃而解,又可以开心的捣鼓了!

好了,show you the code!:

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
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractANTDCSS = new ExtractTextPlugin('[name]-antd.css');
const extractNormalCSS = new ExtractTextPlugin('[name]-normal.css');

module.exports = {
...
module:{
rules: [
{
test: /\.css$/,
include: /node_modules/,
loader: extractANTDCSS.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {modules: false}
}]
})
},
{
test: /\.css$/,
exclude: /node_modules/,
use: extractNormalCSS.extract({
fallback: 'style-loader',
use: [{
loader: 'css-loader',
options: {
modules: true,
localIndentName: '[local]--[hash:base64:5]'
}
}]
})
}
]
},
plugins: [
extractANTDCSS,
extractNormalCSS,
...
]
}

以上的两个loader会生成两个css文件,分别是vendor-antd.cssmain-normal.css,我们只需要在正确的位置引入这两个css文件就好了!

尾巴

我在前面提到我把CSS直接写在元素/组件的style标签中,其实这种方式实不可取的,它会让你在编码和代码review中苦不堪言,因为一旦项目变得很大,当你想修改某个样式的时候,花在定位CSS的时间是非常多的;并且,可读性和可复用性也会大打折扣;而且我们经常在写样式的过程中使用的各种选择器、伪类、伪元素都无法发挥其灵活的作用。所以,无论你是以何种方式写前端,请尽量不以这种方式写CSS。

当然,我们也要从性能上去考虑。因为css是在页面解析正式前就加载好了的(写在header)里面,在我们再解析页面的时候,加载速度就会变得更快;再有,如果我们使用把CSS写在组件中后,无可避免的会产生更多的重绘和回流,这会严重影响渲染性能。比如我们使用JS修改我们在style属性中标明的样式,那么必然会触发一次repaint。

好了,到此打住!如果你想学习reflow和repaint,点击这里,也许会帮助你!

分享到 评论

Three.js

最近貌似Node又有了新的fork ayo.js(怎么读!哎呦?),加之前端一不留神就出框架的节奏,在2016年就开始用Next(wtf!你能看出来其是一个前端框架?)来命名,以后恐怕就得future.js、plus.js的节奏…贵圈真乱啊!

当然当然,这和我们今天的主角three.js并没有太大的关系,比起这些看了名字不知所云的xxx.js,Three.js这个就和明显了,其是一个3d JavaScript库,更准确的说是用JavaScript编写的WebGL三方库,那么什么是WebGL呢?这个我不解释,有兴趣的小伙伴可以去探索。

作为我最想学却一直学不会的技术之一,WebGL的确对于大部分的前端猿们来说有些复杂和繁琐了,早些时候我花了大量的时间去啃API,学习如何使用,可到目前脑子还是一团乱麻。既然这样的话,我们得另辟蹊径,不能因为有困难就放弃学习不是!所以我了解到了Three.js,其化繁为简,做同样的事,其只需要少于1/5的代码量就可以完成,并且API也十分通俗易懂,学习难度降低了不少,可以让我们关注使用WebGL创造而不是痛苦的学习和编码。

如果你还不理解WebGL是什么,这是官方文档上的原话:

WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 3D and 2D graphics within any compatible web browser without the use of plug-ins. WebGL does so by introducing an API that closely conforms to OpenGL ES 2.0 that can be used in HTML5 canvas elements.

如果你对Three.js比较有兴趣的话可以直接进去官网,其中首页展示了很多featured projects,个人比较喜欢这个Paper Planes

你也可以去gayhub把Three.js代码download下来,里面有很多很多(大约几百个例子)可供学习,当然如果你想学习Three.js,来百度云下载,这是目前少有的全方面介绍Three.js的书籍。

好了,差不多介绍完该跑了。但是我好想发现了我竟然连副标题都没取,好吧,还是再多讲一会儿,为了彰显Three.js的简单易用的特性,我讲决定再写一个全面但是简单的例子,并且配上必要的讲解。

例子?不存在的!

这是一个很小的例子,它将会展示Three.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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title>使用Three.js</title>
<script src='./three.js'></script>
</head>
<body>
<div id="three-container"></div>
<script type='text/javascript'>
// three.js中有几个非常重要的知识点,为了构成一个3D程序,我们至少需要以下几部分。
// 1. Scene 场景,用于承载一些必要元素
let scene = new THREE.Scene();
// 2. Camera 相机(此相机非你想的那个相机哦!)
// Three.js中提供了两种相机,透视相机和正交相机,这里使用的是透视相机(类似于人眼看到的)
let camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerWidth, 1, 1000);
camera.position.set(-20, 40, 20);
// 3. renderer 渲染器, 也可以在canvas中渲染,但是复杂场景可能有性能问题
let renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x708090);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMapEnabled = true;
// 4. 物体
let cubegeo = new THREE.CubeGeometry(10, 10, 10);
// 5. 材质 用于物体表面,不同材质包含不同特性,可设置颜色等。
let material = new THREE.MeshLambertMaterial({
color: 0xffffff
});
// 组合物体与材质成为一个网格
let cube = new THREE.Mesh(cubegeo, material);
// 设置物体能够产生光源阴影
cube.castShadow = true;
scene.add(cube);
// 6. 光源 Three.js中存在多种光源
let light = new THREE.SpotLight(0xfffff);
light.position.set(-30, 40, -20);
light.castShadow = true;
scene.add(light);
// 设置相机看向场景远点(空间坐标系原点)
camera.lookAt(scene.position);
// 添加到HTML中
document.getElementById('three-container').appendChild(renderer.domElement);
// 为了更加直观,这里设置一下空间坐标系
let axes = new THREE.AxisHelper(30);
scene.add(axes);
// 动起来吧!添加动画
function animation() {
// 比如移动转动方块, 这里设置在x、y轴转动平面
cube.rotation.x += 0.1;
cube.rotation.y += 0.1;
requestAnimationFrame(animation);
renderer.render(scene, camera);
}
requestAnimationFrame(animation);
</script>
</body>
</html>

尾巴

即使是这样一个简单的例子,我如今也没有办法在不参考官方文档的情况下一口气写下来,原因无非在于,虽然其简化了开发,但是概念还是偏多并且需要记住每个API也是在有难度。

但是,如果我们能够十分清楚的理解制作3D应用的流程,至少是使用Three.js的流程,按照流程十分有条理的写下去,代码总归是十分清晰的。

time waiting for no one,这是我最近在看《穿越时空的少女》看到的。对啊,时间不等人,珍惜好为说不多的’自由’而’枯燥’的时间吧!

分享到 评论

从Decorator到Mobx

最近在开发一款视频编辑器,其中就用到了Mobx作为状态管理工具。Mobx中很重要的概念例如可观察(observable)的状态,可计算(computed)的值都用到了decorator(当然在使用Mobx时可以不用)。Decorator作为ES7引入的新特性,用于给类/属性添加新行为。对于不少初学者而言,可能对其并不是很了解,所以在这里从装饰器开始,聊聊我对Decorator和Mobx的理解。如果你正在学习Mobx,希望能对你快速上手Mobx能有所帮助。

先说装饰器(Decorator)

装饰器是ES7中引入的,其目的在于修改类/方法的行为。例如我们可以在不修改“类”的情况下为其增加新的功能。

例如:我们定义了一个学生“类”,其中有nameage两个属性,以及showInfo一个方法。

1
2
3
4
5
6
7
8
9
class Student {
constructor(name, age) {
this.name = name;
this.age = age;
}
showInfo = () => {
console.log(`name:${this.name}, age: ${this.age}`)
}
}

如果此时我们想为这个类添加一个属性school用于标明学校,,在不修改“类”的情况下,我们可以使用装饰器这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function addSchool(target) {
target.prototype.school = 'SDU';
}
@addId
class Student {
// ...
}

/**
@decorator
class A{}
等价于
A = decorator(A);
*/

let limoer = new Student('limoer', 21);
console.log(limoer.school); // > SDU

addSchool()给Student“类”的原型对象上添加了一个属性,现在所有实例都可以取到school这个属性。

更深入一步,上面看到用于装饰的函数只接收一个目标“类”作为参数,如果我们有多个参数的话,可以写成高阶函数的形式(即返回一个函数)。同样是上面的例子,现在学校由参数指定,我们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
function addSchool(school_name) {
return function(target) {
target.prototype.school = school_name;
}
}

@addSchool('CQMU')
class Student {
// ...
}

let lin = new Student('lin', 20);
console.log(lin.school); // > CQMU

装饰器不但可以装饰“类”,也可以对方法(…属性)进行修饰,使用的方式类似于对“类”的修饰,不过用于修饰的函数接收三个参数,target将要被修饰的对象, name被修饰的属性名, descriptor被修饰的属性的描述对象(ES5中详细介绍过)。 写一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

function showCount(target, name, descriptor) {
let prev = descriptor.value;
descriptor.value = function() {
console.log('count:' + StudentList.list.length);
prev.call(null, arguments);
}
return descriptor;
}

class StudentList {

static list = ['limoer', 'lin'];

@showCount
showNames () {
console.log(StudentList.list.join(' '));
}
}

let list = new StudentList();
list.showNames(); // count:2 \n limoer lin

上面的代码给StudentList类的showNames方法添加了打印数量的功能,并且是在不改变原有“类”结构的情况下。

说明,在现有的浏览器环境和Node都不能运行上面的代码(暂不支持装饰器),如果想运行的话,可以借用babel 并且使用相关插件(babel-plugin-transform-decorators-legacy)的前提下进行compile,之后就可以进行了。推荐开发过程中webpack和babel结合使用,效果更佳!

好了,关于Decorator简单介绍到此到一段落,更多的相关知识请自行发掘和学习。接下来,是时候了解并使用Mobx了!

Mobx?想说爱你不容易!

在文章最开头谈到我在最近的学习开发中使用了Mobx作为状态管理工具,最主要的原因是其相比Redux,学习和快速上手成本的确消了很多,并且它足够简单。但是在后来的开发过程中,虽然其可以没有redux中action,也不存在reducer,更是告别了单一而庞大的store,我们可以定义多个state用于保存状态,让每个状态或者是每个类属性添加注解,让其编程可观察的状态,而为了能够自动的更新值,我们可以通过使用computed这个装饰器或者autorun函数来完成。可是,在使用过程中,定义多少个状态,每个状态的结构又是如何,等等等等,都困扰着我,远没有使用redux来得清晰和直观。这也许是因为我对mobx目前刚好达到基本使用的程度,并没有深入的了解。基于此,接下来,我只谈谈Mobx入门,至于该如何优雅的使用,请自行摸索。

几个概念

  1. 可观察的状态

这也许是Mobx最基础也是最重要的概念了。我们可以使用Mobx提供的observable装饰器,让基本的数据结构(数组、对象、原始值等)变成可观察的。使用的方式如下:

1
2
3
4
5
6
7
8
9
10
let TimeState = observable({
currentTime: Date.now()
})
TimeState.set("currentTime", new Date().toString());

class AppState {
@observable list = ['limoer', 'lin'];
}
let state = new AppState();
console.log(state.list.length); // > 2

好了,最简单的例子就是这样,我们使用ES5和ES6 decorator的方式分别创建了两个state,第一个state我们适应装饰器让一个对象(Map)变得可观察,而第二个我们则是对一个“类”属性(为一个数组)进行了修饰,让其变成可观察的。

这里值得注意的是,如果一个数据结构变得可观察,那么其类型也会发生改变,例如我们让一个数据变得可观察,此时其已经变成了一个 Observable Array, 这是一种Mobx定义的数据结构,拥有其独特的API,此时使用Array.isArray(state.list)讲返回false,因为Observable Array 并不是一种数组类型。

好了,当看到这里,你是否有这样一个疑问:让一个数据结构变得可观察,其作用到底在哪里呢?其实很简单,我们都知道Mobx是React的小伙伴,其目的是在于替换React本身的state,我们都知道对于React而言,如果一旦state发生改变,就将导致页面更新并且重新渲染,基于此,让数据结构变得可观察,其目的是在于当被观察的数据发生改变,React也能做出相应的更新和重绘操作等,并且,这样的重绘是经过Mobx优化的,只进行必要的重绘来增加性能!

  1. 可计算值

可计算值是通过现有状态和其它可计算值派生出来的值。这很好理解,我们在使用React的时候,往往要通过state衍生出很多的值,例如如果state的一部分是一个数组,那么我们通过衍生得到的数组长度就是一个计算值,并且在Mobx中,一旦可观察的state或者其他computed value 发生改变,可计算值就会重新计算。其实,在实际的React项目中,我们在很多地方都使用到了计算值。

还是上面AppState的例子,现在我们给其增加一个计算值,

1
2
3
4
5
6
7
8
9
10
class AppState {
@observable list = ['limoer', 'lin'];
@computed get count() {
return this.list.length;
}
}
let state = new AppState();
console.log(state.count); // > 2
state.list.push('lindo');
console.log(state.count); // > 3

count是一个计算值,一旦list发生变化,其就会自动重新计算,可以保证,count的值每次都是最新的,并且都是等于list数组的长度。

  1. autorun

其作用和函数名一样好理解,其会自动执行;autorun其本身是一个响应式函数,其使用到的依赖关系state/computed value等一旦发生改变,其就会自动执行一次,效果和计算值类似,但是计算值和autorun的应用场景是不一样的,computed value通常会产生一个新值而autorun达到某种目的而不产生新值,例如生成日志,处理网络请求等。
还是上面的例子,我们继续扩展:

1
2
3
4
class AppState {
// ...省略前面的代码
let logcount = autorun(() => {console.log('count: ' + this.count)});
}

这里我们在autorun中使用了computed value, 一旦发生count改变,就会自动打印出新的count值;当然,初始化state实例对象的时候,就会先执行一次。

  1. action

动作是用来修改状态的。并且只应该对修改状态的函数使用action,要使用动作很简单,使用@action修饰一个函数或者使用action(fn),把要修饰的函数作为参数即可。继续上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13

class AppState {
// 省略上面的代码
@action.bound
addOne(name) {
this.list.push(name);
}
// 或者
@action
addOne = (name) => {
this.list.push(name);
}
}

上面我们定义了一个函数,用于向列表中添加一个姓名。请注意,ES6 class的写法无法自动绑定到对象,所以使用`@action.bound` 或者是使用ES6中引入的箭头函数(推荐)。

与React使用

  1. observer
    observer是由mobx-react包(需独立安装)提供的用于让组件变成响应式组件的decorator。官方文档中写到:它用 mobx.autorun 包装了组件的 render 函数以确保任何组件渲染中使用的数据变化时都可以强制刷新组件。
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
import React, { Component } from 'react';
import { render } from 'react-dom';
// 其余依赖省略
@observer
class NameList extends Component {
addUser = (e) => {
e.preventDefault();
if(this.uname.value){
this.props.appstate.addOne(this.uname.value);
}else{
console.log('must input user name!');
}
}
render() {
return <div>
<ul>
{
this.props.appstate.list.map((index, name) => {
return <li key={index + 10}>{name}</li>
})
}
</ul>
<div>
<p>当前用户人数:{this.props.appstate.count}</p>
<label for="uname">姓名</label>
<input type="text" name="uname" ref={(ref) => this.uname = ref}/>
<button onClick={this.addUser}>+</button>
</div>
</div>
}
}

render(<NameList appstate={appstate} />, document.getElementById('app'));

上面是一个响应式组件的例子,结合了上面定义的状态,我们可以查看所有的姓名、数量,并且可以通过点击按钮来改变state。其实observer对非响应式组件仍然有效,同样是上面的例子:

1
2
3
4
5
6
7
const List = observer(({appstate}) => {
return <ul>
appstate.list.map((index, name) => {
return <li key={index + 19}>{name}</li>
})
</ul>
})

好了,对于observer的介绍就告一段落,更多的Mobx和React连接的方式,以及Mobx提供的生命钩子函数等相关知识你可以查看官方文档来了解。

尾巴

自从放了暑假回了家,效率下降特别多,在学校的时候以为回家可以安心学习,到了家才知道一切都变了,该做的事情还没做,还有更多的知识要学习。所以,早早回学校也许是一个不错的选择!所以再过几天,就要启程回学校了,在最后一年里,期待所有的努力都没有白费,期待一个新(好)的开始!

分享到 评论

响应式布局的那些事

响应式设计在如今的web开发过程中已经是必不可少,它可以针对不同的设备环境对页面进行调整,并且可以在PC端和移动端达到很好效果的情况下,不用开发多套页面,可以提升开发速度,可维护性打打增强。

响应式布局

响应式布局的一种实现方式的原理是使用CSS3新引入的Media Query来调整元素在不同分辨率下的显示效果,并且通过JavaScript进行交互。总结起来,响应式布局有以下几个需要注意的点:

  1. 设置Viewport

我们知道,在移动设备中,页面被放置在虚拟的窗口中,这个窗口也称作视口(Viewport),对于未进行移动端适配或者是未进行响应式设计的页面,往往页面的宽高都会大于移动设备的宽高,所以为了能够在移动设备上进行页面交互,缩放是不可避免的,但是频繁的放大缩小带来的浏览体验肯定不会好。所以,在响应式设计的第一步,就是要禁止移动设备的缩放,这很容易实现,我们只需要在html页面中的head元素下添加一个meta标签用于规定禁止缩放就可以了:

1
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maxinum-scale=1.0" />
  1. 使用Media Query

媒介查询才是响应式布局的关键所在,我们使用Media Query 来实现在不同尺寸下使用不同的样式。Media Query的规则有很多,例如@media screen and (max-width: 980px){...}就表示了在980px下的屏幕下使用在此定义的各种样式,同样还有min-widthorientation(设备方向)等属性,我们需要按需进行设置。

  1. 使用JavaScript

如果能做到上面的两点,在一般情况下,响应式布局是可以实现的。但是如果在布局的过程中需要改变交互,那么JavaScript久必须派上用场了。例如一个菜单栏,在十分小的屏幕下需要折叠,那么就需要用到JavaScript。

Code

上面是我能够想到的响应式布局的一些要点,在实际学习过程中,我并没有在一些项目中使用相应式设计的方式(貌似很悲哀…)。在目前移动为先的时代,为移动端做更好的优化是不可避免的,无论是使用重新写一套移动端页面,还是使用响应式布局,或者使用其他的例如Flex Box来进行布局。作为一个工作在浏览器端的🐒,这都是我们必须具备的素质。

好了,写一个简单的小例子吧。如果你从未接触过响应式布局,那么希望接下来的code会帮助你更快地了解并应用它。

我们来写一个菜单栏,其HTML结构‍如下:

1
2
3
4
5
6
7
8
9
<div id="nav">
<ul id="nav-list">
<li><a href="#" id="home">Home</a></li>
<li><a href="#" id="topic">Topic</a></li>
<li><a href="#" id="today">Today</a></li>
<li><a href="#" id="about">About</a></li>
<li><a href="#" id="concat">Concat</a></li>
</ul>
</div>

CSS如下:

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
* {
margin: 0;
padding: 0;
}
#nav {
position: relative;
}
#nav-list ul li {
list-style: none;
box-sizing: border-box;
width: 20%;
}

#nav-list ul li a {
display: block;
text-align: center;
text-decoration: none;
color: #FFF;
line-height: 4em;
font-size: 1.4em;
}

#nav-list ul li:nth-child(1) a {
background-color: #bcbcbc;
}
#nav-list ul li:nth-child(2) a {
...
/*添加背景色*/
}
#nav-list ul li:nth-child(1) a:hover {
background-color: rgba(188, 188, 188, .8);
/*添加鼠标移上去的样式*/
}
#nav-list ul li:nth-child(1) a::before {
content: ''
/*使用伪类来添加图标字体等*/
}

现在,我们来为屏幕宽度小于768px写一个样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@media screen and (max-width: 768px) {
ul li a::before {
font-size: 20px;
line-height: 60px;
}

ul li a {
font-size: 0;
height: 60px;
}
/*
上面的的样式指明了再768px宽度及以下,我们设置a标签的font-size为0,不显示字体。
设置伪元素所在的图标字体的行高等于a标签的宽度,使其垂直居中。
通过上面的简单设置,我们在小于768px跨度的屏幕下,对于该菜单就只能看到图标了。
*/
}

接着,我们可以为更窄的屏幕设置折叠菜单,我们通过css来绘制折菜单,使用JS来显示和隐藏。具体的实现这里就不贴出来了。

ok,到此为止,我们已经写好了一个响应式菜单栏了(虽然…)。

分享到 评论

Demo--Canvas with React

一个使用Canvas处理图片的Demo,使用React + webpack + Redux 的技术栈,非常适合初学者,希望你喜欢!

为什么

学习canvas已经有一阵子了,忙完了计组课设,考完了数据挖掘,终于有时间来做一点小Demo来巩固自己所学的知识了。就像上面介绍的那样,这是一个使用Canvas进行图片处理的Demo,其可以选择本地图片,改变其R G B 以及透明度,然后可以选择保存到本地。并且为了重温很久没碰的React,前端使用了React,使用Redux进行数据的管理(虽然简单到没必要使用),并且使用了css modules 以便直接在组件中使用css。当然这一切都是在使用webpack进行编译打包的情况下。

这个Demo十分简单,特别适合React初学者食用,相信会对你的React学习有所帮助!

如何运行

  1. 从我的github上clone到本地;
  2. 进入Demo根目录, 执行npm init 安装依赖;
  3. 安装完毕后,执行 npm run build 进行构建;
  4. 在Chrome浏览器(下载功能只能在Chrome中使用,所以…)中打开index.html。

至此,你可以体验这个简单的Demo了。

像什么

如果你觉得在你的机器上run很麻烦,或者你只是想看看长得怎么样。

在浏览器器中打开,是这个样子的:

初始化效果

我承认的确很简单,简单到显得简陋了!接下来你可以选左下角的选择文件按钮来选择任何一张图片,比如我选择了一张图片后:

初始化图片

任何被选中的图片都会被居中显示,宽高都会适应600*400的图片操作区域。现在,可以对图片进行操作了:

处理后

我们选择对图片的R、G、B、以及透明度进行调整,实时调整的效果将会在左侧的图片区域实时显示出来。

第四步,点击图片区下的按钮,就可以吧处理过的图片下载到本地了,我们打开下载后的图片和处理的图片进行对比,就像这样:
保存与对比

至此,我已经演示完了所有的功能。

不足

如果你细心一点的话,你会发现这个Demo还有很多问题:

  1. 我们导入任何宽高的图片,其都会被自适应到框中,所以处理后的图片品质会下降。
  2. 保存图片只能在Chrome浏览器中进行,已测试在Firfox中无法使用这个功能。
  3. 右侧的工具栏在选择新图片后不会被初始化。
  4. 功能单一。
  5. 界面简陋

你需要注意的是

如果你想学习React和canvas,那么我希望我的这个Demo会对你有所帮助,这里提几个需要注意的点,这些点也是我在开发过程中遇到的问题:

  1. 如何使用input file来选择一张图片并绘制到canvas中。
  2. 如何保存图片。
  3. 图片在React中绘制的时机。
  4. 如何使用redux进行数据管理,特别是如何使用带参数的action。
  5. 你所关注的。

未来

这虽然是一个很简单的Demo,但是我会在此基础上进行继续跟进,现在能想到的是解决上面提到的不足,比如设置两种模式,处理图片品质下降的问题;兼容主流浏览器;增添新功能;修改工具栏的状态初始化的bug;以及其它我以后能够想到并且我能够实现的。

以及…我目前有想法开发一个可交互的视频编辑器,有兴趣的同学可以关注下咯!

写到后面

还有不到3个小时我就21岁了,想想前面走过的20年,尤其是上大学的三年来,感慨颇多。谢天谢地,就算无论如何,我都完好无损的度过了。接下来的一岁中,我将面临人生中一个个重大的转折点,实习、毕业、工作、走向社会。从小到大,我对我所有的事情做出选择,接下来,也不例外。我做好准备了,并且一直在准备着!

共勉!

新!

5.28日

  1. 解决了再次选择图片工具栏初始化的问题;
  2. 工具栏的调节精度下沉到0.01;
  3. 修改页面细节。

现在看起来长这样!
新的页面

6.3日

  1. 同样的功能,不同的界面和实现方式,采用react但是去除redux使用state进行状态管理;
  2. 操作更加主流和人性化;
  3. 已知BUG,下载某些图片的时候可能会失败,暂不知原因。

新版地址:https://github.com/xiaomoer/picture-editor-with-canvas

看起来是这样的:

还有这样:

加油!

分享到 评论

初识 requestAnimationFrame

事情的起因是这样的,前段时间面试的时候面试官问我会canvas不,作为一名未来的前端猿,我只有过一点了解,后来居然收到了offer,当然在闲暇之余是要学习一下canvas,并且在学习过程中首次接触到了本文的主角requestAnimationFrame

web中实现动画

老实说,如果有人问我如何在web开发中实现动画,我第一时间想到的就是使用定时器setTimeout()或者setTimeInterval()来实现。其实实现的方式远远不止这一种,在CSS3的时代,我们实现动画有了更多的选择,比如使用关键帧动画,使用transition,我们也可以在canvas上绘图来实现动画;当然,还有requestAnimationFrame

使用setTimeout()/setInterval()实现的方式很简单,我前面有一篇文章就简要介绍了JS中的定时器。使用这种方式实现动画其实是有其性能瓶颈的,例如:

1
2
3
4
function animation(){
// do something
setTimeout(animation, 1000/60)
}

上面我们以60帧/秒的速度执行动画,但是如果浏览器不是60帧/秒,就会掉帧;并且由于JS单线程的特点,所有不能保证每一次执行回调都是1000/60毫秒;还有,当窗口处于非激活状态的时候,它同样可能会执行。

其实很好理解,作为定时器,setTimeout/setInterval并不是专门做动画的,存在各种各样的问题也是很好接受的,但是当我们认识到这种实现动画的方式的各种缺点时,我们也许会考虑另一种动画的实现方式,而requestAnimationFrame是一种更好的方案。

初识 requestAnimationFrame

当我们执行window.requestAnimationFrame(callback)的时候,浏览器会在下次重绘的时候执行回调函数,它会告诉浏览器马上就要执行动画了,而callback则是用于更新动画。

requestAnimationFrame使用起来很简单,通过递归不断来执行回调来更新画面从而让画面动起来,我们甚至不需为其指定动画执行的时间和帧率。其优点是:1)从名字上就可以看出这是一个专门用于实现动画的API,优化是自然少不了的;2)其如果处于非激活状态,会自动暂停执行,有效节省了CPU资源。

小实例

我们在做动画的时候,有时希望背景移动起来,结合目前正在学习的canvas,我们可以很轻易的做到这点.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let j = 0;
let image = document.querySelectorAll('img')[0];
function moveBackground(){
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.translate(10, 0);
ctx.drawImage(image, 0, 0);
j++;
if(j < 20){
requestAnimationFrame(moveBackground);
}
}
let moveBtn = document.getElementById('move');
moveBtn.onclick = function(e){
e.preventDefault();
requestAnimationFrame(moveBackground);
}

上面我们点击button的时候,开始执行动画,通过不断的坐标变换和清除重绘,达到背景图片向右移的效果。

最后,请注意,不是所有浏览器都支持该方法,所以你可能需要一个polyfill,关于如何实现这个polyfill,网络上的资源比较多了,这里就不在赘述。

分享到 评论

实用的JavaScript技巧、经验总结

  1. 避免给一个未申明变量赋值,因为这会直接创建一个全局变量。

  2. 总是使用 ‘===’ 而不是 ‘==’,’===’会直接比较,而’==’必要时会进行类型转换等造成错误。

  3. 使用typeof instanceof 应当小心。

    1
    2
    3
    4
    typeof null // object
    function A(){}
    new A() instanceof A // true
    new A() instanceof Object // true
  4. arguments 对象转换成一个数组

    1
    2
    Array.ptototype.slice.call(arguments);
    Array.from(arguments) //ES6
  5. 验证一个参数是否是数组

    1
    Array.prototype.toString.call(a) === '[object Array]'
  6. 取得一个数组中最大值与最小值

    1
    2
    Math.max.apply(Math, arr);
    Math.min.apply(Math, arr);
  7. 使用splice删除数组中某一个/一些元素,而不是使用delete,如果使用delete的话,相当于只是把原值变为undefined

  8. 使用for .. of来遍历数组,使用for .. in 要避免遍历到原型上面的可枚举属性,使用hasOwnProperty()来检测

  9. 不要扩展Object.prototype,因为这会给所有(?)对象增加属性/方法,从而产生很多意想不到的行为和错误!

  10. 对于一个构造函数,总是使用 new进行构造函数调用,否则默认返回空(对象)。

  11. arguments.callee() 可执行当前函数,不推荐使用。

  12. 认识 ‘+’运算符, 对于对象而言,会转换成字符串,对于其他运算符则会尝试转成数字。

  13. 在使用if语句是,如果需要在条件中赋值,需要加上括号:
    `javascript
    if((x = y)){
    // do something
    }
    并且结果是否为真取决于y的真假。

  14. 判断一个数是否为NAN使用 x !== x,为true则该变量为NAN(NAN不等于自身)

分享到 评论

在Express中使用Cookie

文章来自于我在express框架上使用cookie引发的一些问题,但在具体介绍cookie以及如何正确的使用cookie之前,我觉得我有必要说一说cookie到底是什么。

Cookie是服务器保存在浏览器中的一段小(一般而言size<4KB)的文本信息,而浏览器每次想服务器发出请求,就会携带上这段信息。Cookie一般包含了key、value、到期时间、所属域名、所属路径等信息。

在浏览器中我们只需要使用document.cookie来得到当前页面所属的cookie。请注意,返回的cookie是以字符串形式存在的,不同的key-value之间通过’;’来分割,所以如果你想对齐进行进一步操作,需要相应的处理。

这里需要注意的是,document.cookie属性是可写的,这就意味着你可以手动添加cookie,使用document.cookie="name=value"的形式。注意,这里是添加,而不产生覆盖。

好了,关于cookie的属性的具体含义和用法,大家可以自行去了解。

我的问题

我在使用服务器端使用cookie的时候出现了问题,出现这样问题的原因很简单,首先我对cookie存在错误的理解,请务必注意,cookie是服务器发送给客户端,而客户端在发起请求的时候携带cookie而已。在正确认识cookie之后,并且成功的将cookie发送到浏览器过后,问题又来了,我在请求的时候,cookie却不能发送到服务端。我使用的是下面一段代码:

1
2
3
4
5
6
7
8
9
10
fetch('/login?'+stringify_data, {
method: 'GET'
}).then(function(res) {
return res.json();
}).then(function(json){
console.log(json.status);
}).catch(function(err) {
console.log('oh ! error!')
})
}

这里我使用了fetch API,在能够正确的发送请求的情况下,服务器无法读取到相应的cookie信息,同样在chrome开发者工具中查看请求头也发现请求并没有携带cookie信息。我想着一定是fetch API的问题,所以我赶快写了一个使用Ajax的请求,很显然,能够正确的发起携带cookie的请求。好吧,写到现在,我想的确是fetch API的问题了,阅读文档发现fetch API发送的请求默认是不带cookie的,必须手动设置(无论是出于什么样的考虑,但还是觉得坑)!好吧,问题迎刃而解,我们只需要在fetch函数第二个参数设置credentials: 'include'就可以发送cookie了/无奈!

在express中使用cookie

在express中使用cookie是一件十分惬意的事情,因为如果你使用cookie-parser中间件的话,那么我们只需要使用res.cookie(name, value[,options])就可以设置cookie了,关于options相关的参数可以自行学习!

如果想删除cookie,也很简单,使用res.clearCookie(name)就可以啦。

当然如果想获取请求头发过来的cookie,我们只需要使用req.cookies就可以了,这里返回的是一个JS对象,我们直接可以使用name来读取值,从而做进一步的操作。

在使用了cookie-parser中间件过后,在服务端操作cookie已经足够简单,并且cookie-parser不但提供了非签名使用的方式,还提供了签名的使用方式,具体使用是在使用中间件的时候添加一个secret,app.use(cookieParser('secret'))即可,当然,在获取cookie的时候使用 req.signedCookies属性就好了。

好了,如果你不想使用cookie-parser,我们也能够通过req.headers.cookies(感到罪恶所以不推荐/无奈)访问到cookie,如果想写cookie的话,使用res.setHeader(name, value)(再次感到罪恶)或者res.writeHead(status[,options])就可以了…

尾巴

一般情况下,文章末尾,我总会写一点鸡汤/无奈,这次也不例外。距离写上一篇博客已经过了很久了,起初有两篇想写的文章,一篇是在RN中使用Navigator,另外一篇则是介绍我自己正在学习的几种分类算法。可是当创建好文件准备开工时,我因为写一篇文章可能需要2-3个小时(我速度慢)或者是因为真的动笔写的时候反而觉得没什么要说的就放弃了。然后一段时间过后,或许是因为忙,或许是因为懒,或许是因为浮躁,就是没有去实践,没有去巩固,而把一切都抛之脑后,然后把前面学习到的忘得一干二净!

嗯,这的确的真实的!仅此而已!

END

分享到 评论