0%

最近在和一些年轻的js开发者的交流中,发现大家对于一些ECMAScript标准仍然有一些误区

const

在一些网络文章中,经常可以看到对于const的一些描述,大多数都是说明: 一旦声明则不可进行修改的常量

我就经常会用这个来分辨一些只是浅尝辄止的开发者,也是我面试中的一个非常小的问题:

当我声明了一个常量数组后,我还可以向内推送元素么?

可惜很大一部分的同学都会回答我不能,因为声明了就不能改变。实际上,这里不能改变的不是值,而是指向的内存地址。

箭头函数

往往一些简单的特性所隐藏的功能则经常带来一些问题,我认识的一些开发者极其热衷于使用箭头函数,哪怕是在第三方的库中。

他们简单的认为箭头函数仅仅是简写了function关键字,然而当他们在一些内部函数或回调函数中尝试使用箭头函数的时候却发现,再也取不到相应this了。

Promise

Proimse普及之前,我们的代码经常是这样的:

1
2
3
4
5
6
7
a.do('sth', ra => {
b.do('sth', rb => {
c.do('sth', rc => {
...
})
})
})

拜异步特性所赐,我们的代码经常写的像锥子。

这时候Promise横空出世,然而周围的开发者90%居然会这么写:

1
2
3
4
5
6
7
a.then(ra => {
b.then(rb => {
c.then(rc => {
...
})
})
})

说实话,写的时候就没有疑问这和之前有什么区别么?

1
2
3
4
a
.then(ra => b)
.then(rb => c)
.then(rc => { ... })

解决回调嵌套地狱,才是Promise褒受欢迎的原因之一。

async

曾经有个同事向我强力推荐koa, 当时公司内部用express比较多,我就问他,为什么要用koa呢?

回答居然是koa能用async……

async并不是一个指向性那么强的特性,实际上很多地方都能简单的用到,比如express:

1
2
3
app.get('*', async (req, res) => {
// do sth
})

还有的朋友问过我,我都使用了这个特性了,为什么代码还是不像我想象中的运行呢?

实际上async只是一个语法糖,只是让你用同步代码的编写方式来异步代码而已,ES6中其他大部分的特性同理。

Class

React充分应用着class的特性,虽然我没有仔细阅读过源码去看具体的实现是否和ES6中的完全相符,我经常看到这样的代码:

1
2
3
4
5
class Component extend React.Component {
constructor (props) {
super(props)
}
}

实际上哪怕你不声明,class也会隐式的生成并调用构造函数。

还是Class

这仍然是原型链的一种实现,不要想太多了。

结尾

我觉得最有用的特性现在看来还是尾递归优化……

Swoole

Swoole是我经常在使用的PHP的协程高性能网络通信引擎,非常好用,为PHP又提供了许多出乎意料的使用拓展,如携程异步等。并且是由C/C++语言编写,作为一个扩展安装使用非常的简单。

我在工作之余尝试将Swoole和公司框架Gini进行共同使用,效果不错,可见两者都符合低耦合的设计理念。简单的增加了一个index.php来作为程序的主入口,就非常简单的使用起了大部分功能。

例如在接受http请求方面:

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
public function request($req, $res) {
$header = $req->header;
$_SERVER = array_merge($_SERVER, $req->server);

// TODO: 传入res对象使后续可以对res对象进行操作
$uri = trim($_SERVER['request_uri'], '/');
$result = \Gini\CGI::request($uri, [
'header' => $header,
'get' => $req->get,
'post' => $req->post,
'files' => [], // 暂且先不考虑file
'route' => $uri,
'method' => $_SERVER['request_method'],
'swoole' => $this->server, // swoole_server 对象
'raw' => $req->rawContent(),
])->execute();

// 防止php://output过多挤压supervisor.log
ob_start();
$result->output();
ob_end_clean();

$res->status(http_response_code());
$res->end(J($result->content()));
}

就非常简单的可以将Swoole传递过来的请求,分发到Gini框架的路由当中去,再做各自的逻辑处理。

但是当我部署到生产环境的时候,就出现了一件比较尴尬的事情,跨域警告了。

跨域

什么是跨域呢,前端可能接触的比较多,这并不是服务器做出的限制,而是浏览器的安全策略。

浏览器为了防止不同域之间发生不安全的数据交互或者攻击,会默认阻止这种行为。但是有些时候我们的应用的确是部署在不同的域下,这时候我们就要了解跨域的原理了。

当我们打开调试工具的时候,会发现在跨域请求之前,我们会看到一个类似这样的请求

1
2
Request URL: http://path.to/api
Request Method: OPTIONS

但是没有任何的消息体进行返回,然后就报错了,不能跨域。

实际上,浏览器在阻止跨域请求之前,会向目标服务器发送一个这样的请求,来询问服务器:什么样子的请求可以跨域啊?

然后服务器就会将自己的配置告诉浏览器,来方便做后续的判断。这也就是网上大部分的解决方案有效的原因了,比如nginx:

1
2
3
4
add_header Access-Control-Allow-Origin http://path.to;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;

实际上就是告诉了浏览器,我允许跨域的Origin, Headers, Methods等,如果请求符合,则就可以进行正常的跨域请求。

解决

这个在传统的PHP开发中很好解决,我们一般会在Web服务器如nginxapache中进行修改解决,但是Swoole实际上自己充当了一部分Http服务器的角色,所以我们需要稍微做一下改动。

上面说到了,具体能否跨域,主要取决于OPTIONS请求所返回的信息,所以我们可以在这里加以拦截:

1
2
3
4
5
6
7
8
public function request($req, $res) {
if ($req->server['request_method'] == 'OPTIONS') {
$res->status(http_response_code());
$res->end();
return;
}
...
}

这样就可以终止后面的路由解析行为,防止后面的逻辑再做其他的处理,然后像nginx配置的那样,我们需要加上我们希望返回的头内容:

1
2
3
4
5
6
7
8
9
10
public function request($req, $res) {
// 跨域OPTIONS返回
$res->header('Access-Control-Allow-Origin', '*');
$res->header('Access-Control-Allow-Methods', 'GET, POST, DELETE, PUT, PATCH, OPTIONS');
$res->header('Access-Control-Allow-Headers', 'Authorization, User-Agent, Keep-Alive, Content-Type, X-Requested-With');
if ($req->server['request_method'] == 'OPTIONS') {
$res->status(http_response_code());
$res->end();
return;
}

这里我简单的加入了几个常用的MethodsHeaders,这样实际上我们Swoole在接收到浏览器的跨域侦测时,就能返回正常的信息并继续跨域请求了。

之前在社交平台上随手给一个朋友回答了个小问题,发现按需加载和代码分割的概念非常容易混淆,也无法对前端应用的性能进行实质的提升

按需加载

按需加载其实说起来非常高大上但是其实很简单,element官网上就有简单的说明

可以看到其使用了一个很简单的特性,ES6的import特性,可以简单的为我们引入需要的组件等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/

new Vue({
el: '#app',
render: h => h(App)
});

我们可以看到,其中在引入element-ui的时候,仅仅引入了我们所需要的两个组件。

element-ui为我们提供了许多丰富的组件,但是可能在日常开发中我们只能用到一小部分,那么这个特性就会很好的帮助我们进行第三方组件引入的最小化了。

不过我遇到的这位朋友使用的方法让我意想不到,给我描述的情况是,他的确只引入了需要的组件,但是整个前端应用还是非常的庞大,导致首屏加载仍然非常缓慢。

仔细询问发现,他只是单纯使用了按需加载的属性,将所有需要用到的组件都像实例那样进行了引入。实际上,如果仅仅单纯的地使用这个特性,在使用了80%以上组件的情况下,结果和整体引入ui库也并无区别。

代码分割

在我看来,按需加载实际上和代码分割的辅助功能是相辅相成的,在没有代码分割之下,所有的按需加载实际上仍然会将所有的第三方库全部压缩到一个工程文件中,导致该文件下载缓慢,运行缓慢。

实际上这一概念已经存在很久了无论是:

vue-router 路由懒加载

亦或是

react code-splitting

中都有提到,当然他们主要也是借用的webpack的一项特性code-splitting

我们在这篇介绍中可以看到,webpack为我们提供了一种动态import的功能:

1
import(/* webpackChunkName: "lodash" */ 'lodash');

就像文章中所说的,这种写法会帮我们将引入的文件单独进行一个文件的分割打包,这样我们庞大的应用就会被我们精心地拆成一个个单独的js文件,互不影响,异步加载,极大地节省了我们的首屏加载时间。

这里和按需加载是相辅相成的,我们可以把我们需要使用的第三方组件分布在我们的各个小组件中,并让这些组件进行代码分割,从而达到最大化优化效果的目的。

同样我们可以看到这个语法中有一行注释:

1
/* webpackChunkName: "lodash" */

webpack会根据我们注释中所写的ChunkName也就是我们为这个包起得名字,将相同名字的包打入到一个文件中,这样哪怕我们希望打入一个包的组件之间没有继承关系,也能在这里进行处理。

优化

代码分割和按需加载已经很大程度上为我们进行了前端优化了,我们还需要优化什么呢?

我的一些同事在代码分割上做的不是很理智,这里我们先介绍一个webpack推荐的分析辅助插件webpack-bundle-analyzer

个人觉得这是一款非常好用的插件,只要进行简单的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '0.0.0.0',
analyzerPort: 8181,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
})
]
}

配置项我这边不再赘述了,运行NODE_ENV=production ANALYZ=true npm_config_report=true npm run build后则会为我们生成一个直观的build报告:

我们可以看到,每一个文件都被分割成了和自身大小比例相等的块,抛去这些最大的引入,我们可以看到右下角有一些几乎看不清的小引入,这些都是同事开发的功能组件,非常的小。

根据这个分析,我们可以得到目前占用体积最大的组件是哪几个,如果过多的占用了我们的加载时间,则应该进行继续分割优化。

但是如果我们的组件非常的小,如右下方的组件,不足1KB,个人觉得就不用进行拆分了,和其父组件在一起就可以,否则这种细碎组件拆分过多,加载性能上没有太多提升反而加大了http请求的压力。

JavaScript的ES2015标准已经被普及了很久了,众多的前后端应用也已经争先恐后地支持了这一标准,其中有一条也是最后一条很有意思,叫做尾递归优化

不得不先说一下栈

栈是一个比较基础的数据结构,大家也广为熟悉。不过可能使用起来不会被感觉到。

栈可以被比喻为学生时代的判卷,做得快的同学(或者交白卷的同学)的卷子往往会最先放在讲台上,然后后面交卷的同学卷子会盖在之前同学的卷子上,最后做的慢的同学(或者仍然是交白卷的同学)的卷子会在最上面,这种有序堆叠卷子的行为被称为入栈

大家交完卷子的时候,老师会把卷子都抱走然后挨个判卷,最后交的卷子会被最先判到,按照顺序一张一张判完,这种行为叫做出栈

当然也有老师边交卷边判卷的,这也是被栈允许的,并不是一定要全部入栈后才能出栈,但是仍然在老师拿卷子的那个时间点,拿到的是最后交的卷子。

这种顺序被我们称为先入后出或者后入先出……what ever……

Javascript中实际上也是存在着很多栈的调用的,比如常见的数组操作,借用MDN的例子:

1
2
3
4
5
var animals = ['pigs', 'goats', 'sheep'];

animals.push('cows')
console.log(animals);
// 输出 ["pigs", "goats", "sheep", "cows"]

我们可以看到利用数组的push方法,我们将cows加入到了数组的末尾,这就是入栈

1
2
3
4
5
6
var animals = ['pigs', 'goats', 'sheep'];

console.log(animals());
// 输出 "sheep"
console.log(animals);
// 输出 ["pigs', 'goats"]

pop方法则为我们弹出了数组最后一个元素sheep,这就是出栈

调用栈

为什么要介绍栈

我们在编写代码的时候,做最多的事情很可能就是调用一个又一个的函数、方法来打成我们的目的,这里实际上就是在不停地使用栈的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let end = () => {
console.log('end');
}

let output = name => {
console.log(name);
};

let start = name => {
console.log('my name is');
output(name);
end();
};
start('huangStomach');

上述代码中,我们先调用了start函数,并在内部调用了output函数和end函数。当我们在调用start函数的时候,系统会为我们分配一块内存,存放我们调用的函数和它的参数:

1
2
3
[
['start', { name: 'huangStomach' }]
]

当然这么写只是打个比方,它会被计算机放入到内存块中。然后,我们调用了output函数,同样,计算机也会分配一块内存:

1
2
3
4
[
['output', { name: 'huangStomach' }],
['start', { name: 'huangStomach' }]
]

它会被放在第一个内存块上面,形成一个。这时候我们打印了name,这个函数也被执行完成并弹出栈。接下来我们会执行第二个函数end

1
2
3
4
[
['end'],
['start', { name: 'huangStomach' }]
]

它会打印end并完成自己的使命然后被弹出,这时候栈内只剩下一个元素:

1
2
3
[
['start', { name: 'huangStomach' }]
]

然后我们的start方法也彻底的完成了使命,栈也被清空了。

递归调用栈

这和尾递归优化又有什么关系呢

比如我们可能有这样一个函数:

1
2
3
4
5
let factorial = (n, acc = 1) {
"use strict";
if (n <= 1) return acc;
return factorial(n - 1, n * acc);
}

这个函数是用来求阶乘的,我们可以看到在函数的末尾我们又调用了自身,这被称为一个递归调用函数。

有时候我们不会使用遍历而是使用递归,可能是习惯问题可能是为了代码更加优美,但实际上两种方法都能为我们解决同样的问题。

当我们使用递归的时候,可以看到我们在函数内部不停的调用函数,这就会形成,当我们传入的参数是10可能还可以非常快的计算出来,但是如果我们传入100000,则会报错Maximum call stack size exceeded也就是stackoverflow

当你冥思苦想,不断地为BUG唏嘘不已的时候,路过的扫地大妈会拍拍你的肩膀,告诉你:栈溢出了

为什么会这样呢?因为函数调用栈的原因,过多的递归调用则会使栈不停地堆叠,直到超出系统的安全限制,我们不得不修改我们的代码,或者干脆切换到遍历。

但是在ES2015中,我们拥有了尾递归优化,当我们处于ES2015环境并开启了严格模式后,尾递归优化就启动了。

当递归函数的末尾仅有对自身函数的调用的时候,系统则会进行检测,发现可以对之前的进行复用,我们不用再不停地进行入栈操作,递归也真正有了用武之地。

在目前的大部分设计中,管理系统仍然不可忽视的一项重要功能就是侧边功能栏,然而在侧边功能栏上的开发,也绕过了许多的弯路

js侧边栏

在我新手时期,也写过许多的侧边栏,侧边栏的要求很简单,和屏幕高度保持一致,并且功能项能在内部进行滚动,当时的我也是煞费苦心,为了能够准确地自适应,不得不用js去进行实现

1
2
3
4
5
6
$(window).resize(function(){
let sidebar = $('#dashboard-sidebar');
sidebar.css('height', 'auto');
let h = Math.max(sidebar.outerHeight(), $(window).height() - $('#header').height());
sidebar.css('height', h);
}).resize();

可以说是非常的愚蠢了,每次都要在窗口变动的时候重新计算,会有视觉上的延误。

并且在首屏加载完成之前,还要忍受一个不正常的侧边栏等待渲染。

绝对定位侧边栏

在后续的开发中,觉得js实现侧边栏实在是太过愚蠢,不停地寻找解决方案的时候,发现实际上绝对定位是可以为我们解决问题的。

给侧边栏定好宽度,然后进行绝对定位,css会自动帮我们将高度和屏幕自适应,在很长一段时间内为我解决了问题。

1
2
3
4
5
6
position: fixed;
top: 0;
bottom: 0;
left: 0;
background-color: #444F61;
overflow-y: auto;

很简单就完成了一个自适应的侧边栏,唯一的缺点就是因为使用了绝对定位,其他的元素得注意是否要纷纷绕道。

现在的侧边栏

后来由于绝对定位也是为我带来了一定程度上的困扰,一旦我修改侧边栏,其他元素也要纷纷跟着移动,每次修改起来也是令人很头疼,所以又开始研究其他的解决方案。

后来在使用bootstrap的时候,也沿用了它所使用的一个css属性值 vh

很担心兼容性的我去MDN上进行了一下兼容性的查询,IE9就已经支持了。另外如果侧边栏之上还有顶部导航栏,仍然可以使用计算属性:

1
2
height: 100vh;
height: calc(100vh - 60px);

除了vh以外,也有很多类似的属性值如:vwvminvmax,都是进行自适应开发不错的选择。

在日常的后端开发中, 不可避免的就是要处理各种文件上传的需要, 随之而来的就是各种下载的需求, 我遇到的大部分开发者都会考虑到在上传的时候做简单的文件校验(有的也没有做), 然而在文件下载的问题上, 则采用依赖于web服务器的静态文件传输, 经常造成很多的麻烦。

上传

大部分开发者实际上在上传的时候都会对文件进行校验, 并且为了安全性考虑甚至会将文件更换名称。但是更换名称后简单的依赖于web服务器进行下载则会让用户拥有一种我的文件被篡改过的感觉(文件名发生了变化)。

依赖于web服务器的文件上传,则可以认为是相对依赖于浏览器的url的一种操作,一旦我们将web应用部署在二级目录下,如:

1
https://origin/path/

则很容易出现存储问题,比如我们存储的目录很可能因为这个原因需要多嵌套一层path目录

下载

下载遇到的问题同理,最经常遇到的则是静态文件仅仅通过web服务器来做下载,不方便做权限管理,并且容易发生盗用资源的现象,其他的web应用用着你的资源,但是却耗费着你的带宽。

另一个则和上传的问题一样,下载的时候也避免不了遇到二级目录的问题,这时候web服务器很容易就带着二级目录去做寻找。

并且在资源进行删除的时候,其相关的静态文件如果没有进行处理也不好做清理,容易造成垃圾文件堆叠。

解决

我个人在这方面也是有吃过很多的亏,上面的问题全部都碰到过,最后决定使用各种编程语言中读取文件的api来进行解决:

  • 文件名更换
  • 二级目录的问题
  • 权限
  • 下载

其中文件名更换非常的简单,我习惯于在文件进行存储时,对其旧的文件名进行持久化存储,以PHP为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

// DATA_DIR 为web应用根目录的相对路径常量

$uniqid = uniqid();
$path = DATA_DIR . "/attached/{$uniqid}";

$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$name = date("YmdHis") . '_' . rand(10000, 99999) . '.' . $ext;

if (is_dir($path) && is_uploaded_file($file['tmp_name'])
&& move_uploaded_file($file['tmp_name'], "{$path}/{$name}")) {
$old = $file['name'];
$dir = $path;
$path = "{$path}/{$name}";
$mime = $file['type'];
}

我们可以获得文件相关的四个变量,其中$old则为我们解决了文件名更换的问题,我们会将用户文件的文件名进行存储,而在我们本地则使用了$name来重命名保证安全文件

同时,二级目录问题也得到了解决,这里使用的是我们自定义的目录$path,不会受到部署的影响。

在下载的时候,我们也可以依赖于编程语言提供的api:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
// APP_PATH 为web应用根目录的绝对路径常量

$article; // 假设我们的数据库映射对象是一篇文章
if (!$article->id) $this->redirect('error/404');
$fullpath = APP_PATH . '/' . $article->path;

header("Content-Type: {$article->mime}");
header('Accept-Ranges: bytes');
header('Accept-Length:' . filesize($fullpath));
header("Content-Disposition: attachment; filename=\"{$article->old}\"");
ob_clean();
echo file_get_contents($fullpath);
exit;

这里由于是编程语言的实现,那么我们依赖于权限的问题也得到了解决,如第5行中代码返回404错误一样,我们也可以进行逻辑判断并返回401错误

并且同时,这里的下载也会从我们的数据库里面寻找文件,不用再被web服务器的二级目录所困扰,并且如果更加深入一下,额外的代码也可以降低我们的资源被盗用的风险。

以下是node的http框架koa的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const router = require('koa-router')()
const send = require('koa-send');

router.get('/image', async ctx => {
const mongo = new Database()
const db = await mongo.init()
let platform = await db.collection('platform').findOne({ "_id": "10010" })
if (platform) {
ctx.attachment(platform.fileName);
await send(ctx, platform.fileName, { root: `${__dirname}/../${platform.filePath}` });
}
else {
ctx.body = {
data: {},
code: 400,
message: '获取失败'
}
}
db.close()
})

也很简单方便

开始

实际上官方提供给我们的教程已经非常清晰明确了reactjs.org

当我们安装好之后,就会拥有一个create-react-app对应模板的react应用

结构如下:

1
2
3
4
5
6
7
├── node_modules
├── package.json
├── public
├── README.md
├── src
└── yarn.lock

其中要注意的一点是,我们之后编写的代码不会直接被浏览器所识别,我们应尽量把所有需要react-loader为我们做转换的代码放置在src目录下

src目录结构:

1
2
3
4
5
6
7
8
.
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── registerServiceWorker.js

这个目录中会有很多的js文件和css文件

public目录结构:

1
2
3
├── favicon.ico
├── index.html
└── manifest.json

我们可以看到public目录结构更加像我们所熟悉的静态web应用的样子,我们访问的也是响应的index.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

我这里把注释删除掉了一些,实际上我们编写的代码会被替换到<div id="root"></div>中,我们可以在src目录下的index.js看到:

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

ReactDOM帮我们将App.js文件内的内容,渲染到了刚才所说的index.html

我们继续打开生成的App.js中我们可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}

export default App;

这样的文件结构或者这样的用法被我们习惯的称为组件

组件

虽然对于基础web或后端开发者来说可能有些不熟悉,但实际上概念是想通的。不得不说借助于ES6规范,使得js更加亲切了一些

以我写的最多的php为例:

1
2
3
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

实际上我们可以简单的理解为require引入资源,只不过react中不仅仅可以引入.js或者.jsx文件,也可以引入其他的静态资源。

下半部分可能会比较熟悉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}

export default App;

典型的class的设计,诸多高级语言都会拥有的特性,使得面向对象思想也更加的清晰。

在这个文件中我们甚至可以看到继承关键字extends,不太相同的是我们实现的只有render()方法,我们稍后再说,实际上和诸多语言的class一样,组件本身实际上也是提供构造函数的,这里因为没有具体使用所以官方为我们省略掉了,我们可以自己添加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends Component {
constructor(props) {
super(props);
}

render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
</div>
);
}
}

export default App;

这样我们就拥有了熟悉的构造函数,(其中的props参数之后再说),实际上我个人觉得,每个组件我们都可以当做是一个类来理解。

属性

那么说到对象,不可或缺的就是内部的属性。react中提供了组件内部属性操作的相关api,譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: 1,
};
}

render() {
return (
<div className="App">
</div>
);
}
}

export default App;

这里我先将render()内的多余代码删除了,可以看到我们在构造函数中为this赋予了新的属性state,官方文档中的解释就是:

State is similar to props,but it is private and fully controlled by the component

props后面再谈,可以看到state实际上是被识别为private的,我们可以理解为对象内的私有变量/属性,同时使用起来也比较简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: 1,
};
}

render() {
return (
<div className="App" onClick={() => this.setState({value: 'X'})}>
{this.state.value}
</div>
);
}
}

export default App;

我们可以看到,我们可以世界在html中使用this.state.value进行私有变量的使用,很像php中的动态脚本,不过我们想要修改私有属性的话不能直接去进行赋值,而是需要调用this.setState()

这种直接在html中写js代码的格式被称为jsx,我一般喜好将所有的使用jsx语法的文件后缀修改为.jsx

jsx语法中有许多要注意的地方,如上述代码中无法直接使用html的class属性,而是要使用className,同时注册元素方法的时候也较为不一样,这里为了简单的将方法内的this指向当前组件我们编写了一个箭头函数。

当有内部私有变量的时候,我们就会想象到另一个概念公共变量/属性,实际上就是props

类的公共属性我们可以在实例化好类之后直接进行赋值,回到我们的index.js文件:

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

实际上当我们import App from './App';的时候,就会产生一个类似于实例化一样的操作,我们在这里会获取到一个App对象,但是我们不会像其他语言一样进行简单的赋值,而是使用html属性一样的方式:

1
ReactDOM.render(<App name='wa' />, document.getElementById('root'));

我们赋予App的name属性则会注入到组件内部的props中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: 1,
};
}

render() {
return (
<div className="App" onClick={() => this.setState({value: 'X'})}>
{this.state.value}
<p>
{this.props.name}
</p>
</div>
);
}
}

export default App;

我们也可以像普通的使用类一样的扩展我们的组件:

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

anotherOne() {
console.log('another one');
}

changeValue(val = 'x') {
this.setState({value: val});
this.anotherOne();
}

render() {
return (
<div className="App" onClick={() => this.changeValue()}>
{this.props.name}
</div>
);
}
}

export default App;

实际上无论是vue还是react,其中的设计思想都是平时比较常用的,理解了他们相通的地方,学习使用起来就会更为容易。

在大多数的sql学习中,众多学习者(包括我),可能非常容易在学习中浅尝辄止,满足于基础的增删改查,进而往往忽略了sql为我们提供的更多高效的特性,从而无论在开发还是使用上都会造成不便

HAVING 子句

在日常使用中,使用较多的一种数据处理形式就是数据聚合,数据聚合令人第一时间想到的就是GROUP BY语句,具体使用可能如下:

1
2
3
SELECT COUNT(*) AS count,equipment_id 
FROM eq_record
GROUP BY equipment_id

这种简单的使用可以非常简单的帮我们归纳数据,获取例如获取每种数据在特定情形下出现过多少次

如果想要获取到出现次数大于1的数据集合呢?我反正是这样写过:

1
2
3
4
5
6
SELECT * FROM (
SELECT COUNT(*) AS count,equipment_id
FROM eq_record
GROUP BY equipment_id
) AS T1
WHERE count > 1

虽然可以得到想要的结果,不过效率非常的低,括号中的语句形成了一个子查询,我们还要再对子查询进行条件查询,效率很低

但是实际上,我们可以使用HAVING子句:

1
2
3
4
SELECT COUNT(*) AS count,equipment_id 
FROM eq_record
GROUP BY equipment_id
HAVING count > 1

这样写不仅仅优雅,并且还去掉了子查询,增加了查询的效率

NULL 的陷阱

很多的编程语言中都会包含布尔类型(bool 或者是 boolean),sql中也存在着相似的概念然而,sql中还包含着第三个值unknown,这被称为三值逻辑

当我们进行如下查询的时候,会返回给我们失败的结果:

1
2
3
SELECT *
FROM eq_record
WHERE dtend = NULL

这必然失败,但是为什么呢?大家肯定知道正确的写法:

1
2
3
SELECT * 
FROM eq_record
WHERE dtend IS NULL

之所以上面的语句无法正确执行,是因为WHERE子句实际上只会将条件中返回结果为true的行加入查询结果,但是对NULL的比较,均会返回unknown

举个例子,如果我们想获取全部的使用记录(虽然我们肯定不会这么写):

1
2
3
4
SELECT * 
FROM eq_record
WHERE dtend <> 1514736000
OR dtend = 1514736000

实际上dtendNULL的行是无法返回的,任何查询执行到NULL行的时候都会做类似如下转换:

1
2
3
4
SELECT * 
FROM eq_record
WHERE unknown
OR unknown

所以我们需要加一个条件

1
2
3
4
5
SELECT * 
FROM eq_record
WHERE dtend <> 1514736000
OR dtend = 1514736000
OR dtend IS NULL

甚至我们在CASE表达式里也需要改变,如下的语句是无法执行的:

1
2
3
4
CASE dtend
WHEN 1 THEN 'ok'
WHEN NULL THEN 'no'
END

改变为:

1
2
3
CASE WHEN dtend = 1 THEN 'ok'
WHEN dtend IS NULL THEN 'no'
END

经过这么多的例子,其实可以这样假定:
在允许的情况下,应当尽量避免NULL的使用

EXISTS 谓词

无论在哪些地方,都能听到类似的结论:

能用EXISTS就不要用IN

的确,比如:

1
2
3
4
5
6
7
SELECT * 
FROM eq_reserv
WHERE id IN (
SELECT reserv_id
FROM eq_record
WHERE dtend = 0
)

这样的语句实际上执行效率很慢,IN会产生一个子查询,每个WHERE条件都会去扫描子查询中的所有数据,非常耗费资源

这里我们可以使用EXISTS 谓词:

1
2
3
4
5
6
7
SELECT * 
FROM eq_reserv AS a
WHERE EXISTS (
SELECT *
FROM eq_record AS b
WHERE a.id = b.reserv_id
)

这样比IN更快,如果我们在关联的列id上建立了索引,那么查询的时候不会查询实际的表,单独查询索引就可以了

如果使用了EXISTS那么不用像IN一样扫描全表,而是只要有一行满足条件则进行返回,比IN的效率高出很多

EXISTSIN不太一样,更像是简单的做关联,如果能做出关联则进行结果的返回

提高性能

避免排序

SQL中很大部分的内存消耗实际上都发生在排序中,虽然可能没有并且的使用ORDER BY,但是数据库内部仍然会进行隐式的排序,如使用:

1
2
3
4
5
GROUP BY
ORDER BY
SUM() COUNT() AVG() MAX() MIN()
DISTINCT
UNION INTERSECT EXCEPT

等等,都会触发排序,所以在提高性能的过程中,应当尽量避免使用排序,例如在使用UNION等的过程中,SQL会为了去除重复数据而进行排序,当我们知道不会有数据重复时,其实可以使用UNION ALL来避免排序

EXISTS的工作实际上也可以为我们去除重复数据,某种情况下我们可以使用其来代替DISTINCT从而避免排序

另外当我们需要使用极值函数(MAX() MIN())时,使用索引能避免进行排序

WHERE与HAVING

当使用了HAVING子句的时候,可能会有这样的困惑,既然HAVING可以为我们提升聚合的效率,是不是应该多使用HAVING?

实际上并不全是如此,如果条件允许的情况下,能写在WHERE子句中的条件应该尽量写在WHERE子句中,这样在GROUP BY进行聚合排序的时候才会降低排序的负担,从而提高性能,并且很多聚合后的视图是不会保有原本的索引的

正确使用索引

众所周知索引是能为我们大大提高查询效率的,但是很多时候我们并没有正确使用索引:

1
2
3
SELECT * 
FROM eq_record
WHERE dtend * 1.5 < 1514736000

上这里对索引进行了运算,然而这种转换数据库是不会帮我们做的,所以我们可以这样写:

1
2
3
SELECT * 
FROM eq_record
WHERE dtend < 1514736000 / 1.5

当使用索引的时候,条件表达式左侧应该尽量是原始字段,另外,使用NULL来做判断的时候,也是无法使用索引的

有些时候我们会用到联合索引,比如(user_id,equipment_id),这样的时候使用顺序则很重要

1
2
SELECT * FROM eq_record WHERE user_id = 1 AND equipment_id = 1
SELECT * FROM eq_record WHERE equipment_id = 1 AND user_id = 1

第二条语句仅仅是颠倒了顺序,就使得数据库无法充分使用索引从而变慢,所以当使用联合索引时应尽量让顺序和定义索引的时候保持一致

使用IN

有些时候我们迫不得已会使用IN谓词例如:

1
2
3
4
SELECT * 
FROM eq_sample
WHERE sender_id IN (SELECT id FROM user WHERE atime > 0)
AND lab_id IN (SELECT lab_id FROM user WHERE atime > 0)

我们这里使用了两个子查询(先不考虑是否可以转换使用EXISTS谓词),并且子查询的内容是一样的,如果我们这么执行,则会生成两个完全相同的子查询集合,所以我们可以这样写:

1
2
3
4
SELECT *
FROM eq_sample
WHERE (sender_id, lab_id)
IN (SELECT id, lab_id FROM user WHERE atime > 0)

则会让sql重复使用一个子查询,尽量提高效率