cross-env

不同平台命令有兼容性问题,可以通过一些第三方包解决

rimraf

删除文件、文件夹

mkdirp

创建文件夹

cross-env

不同平台(window linux unix)命令行传递参数有兼容性问题,通过cross-env可以解决各个平台的兼容性问题

安装

1
2
3
4
5
$ yarn add cross-env --dev

或者

$ npm install cross-env --save-dev

使用

1
2
3
4
5
6
// package.jsxon
...
"scripts": {
"dev": "cross-env NODE_ENV=development node ./builder/dev-server.js",
},
...

NODE_ENV=development 常用参数,构建工具以此区分不同的模式,开发模式(development 或 dev)还是生产环境(production 或 pro);
具体需要什么 参数=值 以项目为准;

react相关面试题

React

  1. state props 的理解,什么数据适合放入state中?
  2. setState函数有几种参数形式?this.state无法立即获取setState设置的数据原因?
  3. 组件间通信方式
  4. react有几个周期函数,各个周期函数的作用是什么(适合写什么样的逻辑)?
  5. diff算法?
  6. React性能优化

代码规范

  1. esLint?
  2. esLint如何结合IDE、GIT使用?

Redux

  1. 为什么要使用Redux,什么数据适合放入Redux中?
  2. Redux原理(如何实现的)?

CSS

  1. css预处理器
  2. css Module

ES6

  1. let const 为什么不用var?
  2. 箭头函数?
  3. 解构赋值、函数的reset参数?
  4. 函数式变成?
  5. class
  6. promise
  7. 异步回调地狱解决方案

webpack

  1. 配置、常用插件
  2. 命令行兼容性
  3. 各种常见loader
  4. 各个版本了解程度

react-router

  1. 配置文件过大,容易冲突,如何解决?
  2. hash、H5方式,用哪种?区别是什么?

其他

  1. 组件化、模块化
  2. 前后端分离
  3. GraphQL、restful
  4. HTTP Code
  5. 组件卸载,ajax打断
  6. ajax、fetch封装,封装了什么?
  7. 异常处理
  8. yarn vs npm
  9. 异常处理、错误上报
  10. 测试:e2e unit
  11. weex、RN、微信小程序

前端与产品约定

原型&文档标准

良好的原型&文档能有效的减小开发人员理解歧义,降低开发人员与产品之间的沟通。

是否考虑只出原型,将文档相关内容对应的标注在原型上?

原型标准

原型要满足一定的标准,方便其他人员查看;

  • 原型的布局要尽量与产品一致;
  • 左侧导航最好为系统菜单,如果不作为系统菜单,原型上要提供系统菜单;
  • 页面与菜单对应,详情、修改等页面没有菜单,要标明页面的菜单状态;
  • 原型上菜单可点击跳转到具体的页面;
  • 当前页面的弹窗在当前页面画出,用连线指明是哪个按钮对应的弹窗;
  • 页面元素对应使用相应组件,不要使用input做label展示,不要把radio做为checkbox使用;
  • 能使用页面元素体现的状态,尽量不要使用文字说明,比如input为disabled,可以通过灰底input框表示;
  • 无用的页面,元素要删除;
  • 不同版本最好独立整理出一个原型,只包含此版本的功能,方便查阅;

文档标准

  • 良好统一的排版布局;
  • 与原型对应;
  • 表单字段表明校验规则;
  • 变更部分使用不同颜色字体,并标记是哪个版本变更;

需求评审

产品整理完需求后,一般会进行需求评审,开发及领导会评审需求的可行性;

前端需求评审需要确认的内容如下:

  • 菜单与页面的对应关系;
  • 各个页面的交互是否合理;
  • 页面布局、交互技术实现是否可行;
  • 表单元素校验规则是否明确;

nginx操作

nginx 操作

反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

安装

1
sudo apt-get install nginx

启动

1
2
进入服务器nginx目录:cd /etc/nginx/
执行:sudo nginx

查看进程

1
ps -ef|grep nginx

停止

waiting for the worker processes to finish serving current requests

1
sudo nginx -s quit 或 sudo kill -s QUIT 1628(进程编号)

without waiting for the worker processes to finish serving current requests

1
sudo nginx -s stop

重启

更改配置文件之后需要重启

1
sudo nginx -s reload

Once the master process receives the signal to reload configuration, it checks the syntax validity of the new configuration file and tries to apply the configuration provided in it. If this is a success, the master process starts new worker processes and sends messages to old worker processes, requesting them to shut down. Otherwise, the master process rolls back the changes and continues to work with the old configuration. Old worker processes, receiving a command to shut down, stop accepting new connections and continue to service current requests until all such requests are serviced. After that, the old worker processes exit.

一旦主线程得到重启命令,它会先校验配置是否正确,如果正确,主线程会启动新的worker线程,关闭旧的worker线程。否则,主线程会回滚到原先的配置,旧的worker线程会得到一个shut down命令,停止接受新连接,但是会继续服务当前请求,当所有的请求处理完之后,会退出。

官网这段话什么意思?如果配置错误,master会roll back,但是worker还是会exit?

pid文件丢失问题解决方法

1
wangshubin@pc:/etc/nginx$ sudo nginx -c /etc/nginx/nginx.conf

nginx.config配置

这个文件中的配置基本上是没动,只是添加两个include

具体的配置在sites-enabled和sites-available中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154

user wangshubin;

worker_processes 4;

pid /run/nginx.pid;

events {

worker_connections 768;

# multi_accept on;

}

http {

##

# Basic Settings

##

sendfile on;

tcp_nopush on;

tcp_nodelay on;

keepalive_timeout 65;

types_hash_max_size 2048;

client_max_body_size 10m;

# server_tokens off;

# server_names_hash_bucket_size 64;

# server_name_in_redirect off;

include /etc/nginx/mime.types;

default_type application/octet-stream;

##

# Logging Settings

##

access_log /var/log/nginx/access.log;

error_log /var/log/nginx/error.log;

##

# Gzip Settings

##

gzip on;

gzip_disable "msie6";

# gzip_vary on;

# gzip_proxied any;

# gzip_comp_level 6;

# gzip_buffers 16 8k;

# gzip_http_version 1.1;

# gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

##

# nginx-naxsi config

##

# Uncomment it if you installed nginx-naxsi

##

#include /etc/nginx/naxsi_core.rules;

##

# nginx-passenger config

##

# Uncomment it if you installed nginx-passenger

##

#passenger_root /usr;

#passenger_ruby /usr/bin/ruby;

##

# Virtual Host Configs

##

include /etc/nginx/conf.d/*.conf;

include /etc/nginx/sites-enabled/*;

}

#mail {

# # See sample authentication script at:

# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript

#

# # auth_http localhost/auth.php;

# # pop3_capabilities "TOP" "USER";

# # imap_capabilities "IMAP4rev1" "UIDPLUS";

#

# server {

# listen localhost:110;

# protocol pop3;

# proxy on;

# }

#

# server {

# listen localhost:143;

# protocol imap;

# proxy on;

# }

#}

sites-available文件夹中,inc.eking99.com文件中的内容
sites-enabled文件夹中保存的文件就是sites-available其中的文件
sites-enabled表明sites-available中哪个”网站”是激活状态
sites-enabled中的文件会删除,或者从sites-available中拷贝过来,是一个软链接

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

# Enumerate all the Tornado servers here

upstream inc_eking_tornadoes {

server 127.0.0.1:8081;

server 127.0.0.1:8082;

}

proxy_next_upstream error;

server {

listen 80;

server_name inc.eking99.com;

# Allow file uploads

client_max_body_size 100M;

location ^~ /s/ {

alias /home/admin/deploy/inc-eking-web/public/;

if ($query_string) {

expires max;

}

}

#location = /favicon.ico {

# rewrite (.*) /s/favicon.ico;

#}

#location = /robots.txt {

# rewrite (.*) /s/robots.txt;

#}

location / {

proxy_read_timeout 1800;

proxy_pass_header Server;

proxy_set_header Host $http_host;

proxy_redirect false;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Scheme $scheme;

proxy_pass http://inc_eking_tornadoes;

}

}

nginxWebSocket配置

两种方式

  1. 修改nginx.config:

    1
    2
    3
    4
    map $http_upgrade $connection_upgrade{
    default upgrade;
    "" close;
    }

    具体网站配置文件:

    1
    2
    3
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  2. 将这两个配置都写到具体网站配置文件中。(将map…放到server配置上面)

平时处理nginx用到的一些东西:

一、具体用法

ln -s 源文件 目标文件

当我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在某个固定的目录,放上该文件,然后在其它的目录下用ln命令链接(link)它就可以,不必重复的占用磁盘空间,只生成目标文件的一个镜像。

例如:ln -s /tmp/less /usr/local/bin/less

二、注意:

(1)ln命令会保持你每一处连接文件的同步性,不论更改源文件还是目标文件,另一处文件也会有相 同的改动。

(2)ln命令分为软连接和硬链接(无参数-s)。与软连接不同的是,硬链接会在你选定的位置上生成一个与原来文件大小相同的文件。无论是软连接还是硬链接都具有文件的同步性。

(3)当一个存储空间,具有几个硬链接时,删除其中的一个,并不会对存储空间进行操作,所以其它的硬链接不会受到影响。

(4)ln默认时间里硬链接(无参数-s)。

原先 www.eking99.com nginx配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;

root /home/admin/deploy/www-eking99-web;
index index.html index.htm;

server_name www.eking99.com;

location / {
}
}

server {
listen 80;
server_name eking.mobi www.eking.mobi eking.umail.cn;
rewrite ^/(.*) http://www.eking99.com/$1 permanent;
}

www.eking.com nginx配置:

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

#all the Tornado servers here
upstream www_eking99_tornadoes {
server 127.0.0.1:9090;
}

proxy_next_upstream error;

server {
listen 80;
server_name www.eking99.com eking99.com
# Allow file uploads
client_max_body_size 15M;

location ^~ /s/ {
alias /home/admin/deploy/www-eking-web/public/;
if ($query_string) {
expires max;
}
}

location = /favicon.ico {
rewrite (.*) /s/favicon.ico;
}
location = /robots.txt {
rewrite (.*) /s/robots.txt;
}

location / {
proxy_pass_header Server;
proxy_set_header Host $http_host;
proxy_redirect off; #老版本是false
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_pass http://www_eking99_tornadoes;
}
}

一般服务器nginx位置:

1
2
3
/etc/nginx
sites-available为可用配置,但是不是实际跑的配置
sites-enable 为当前启用的配置,是sites-available的软连接

nginx目录中文件及其作用:

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
conf.d

mime.types

nginx.conf

sites-enabled

fastcgi_params

naxsi_core.rules

proxy_params

uwsgi_params

koi-utf

naxsi.rules

scgi_params

win-utf

koi-win

naxsi-ui.conf.1.4.1

sites-available

前后端分离 ngnix配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 服务地址
upstream api_service {
server localhost:8080;
keepalive 2000;
}
#
server {
listen 80;
server_name localhost;
location / {
root /home/app/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; #react-router 防止页面刷新出现404
}
location ^~/api { // 代理ajax请求,前端的ajax请求配置了统一的baseUrl = ‘/api’
proxy_pass http://api_service/;
proxy_set_header Host $http_host;
proxy_set_header Connection close;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Server $host;
}
}

web打印

web打印

window.print()直接打印

打印分页

如果table 使用规范的结构,打印自动分页时,每一行都会有表头、表尾;

如果需要强制分页,通过添加空div.pageBreak,样式如下

1
2
3
<style type="text/css">
  .pageBreak{ page-break-after:always;}
</style>

区分打印内容,不打印内容

不需要打印的内容,但是页面需要显示,添加no-printclass类;
需要打印的内容,但是页面不需要显示,添加just-printclass类

1
2
3
4
5
6
7
8
9
10
11
12
13
.just-print {
display: none !important;
}

@media print {
.no-print {
display: none !important;
}

.just-print {
display: block !important;
}
}

关于组件化

组件开发原则

遵循一定的原则,可以设计出简单易用、易于扩展的组件;

文件组织

每个(业务)组件使用的资源:(css)less、js、jsx、图片、字体等都放入一个文件夹下。

单一权责 组件的功能界限

掌握好组件拆分粒度,每一个组件要处理一个核心问题。提高重用性、灵活度,不要为了拆分而拆分,重点是满足业务需求,而不是炫技。

  1. 组件只关心自己的业务逻辑,一些交互,通过触发回调(props.onClickprops.onChange等)的方式,交给调用者来处理;

弹框操作封装

较少字段的表单,最好可以封装到modal中,让用户在一个页面中完成添加、修改操作

  • 如果弹框中的内容通用性不大,直接封装到modal中,如果后期有通用性需求,再拆分,一般情况下,modal中的内容不会有通用性,基本上都是以modal形式存在;
  • 如果弹框中内容有公用情况,可以分别定义两个组件,一个是基础组件,一个是基础组件+弹框,分多个个文件定义;
  • antd 中的modal有正常的声明周期,也就是willMount,didMount会在页面加载时触发一次,以后每次modal被重新打开都不触发,多个操作公用一个modal就会出现数据互相干扰;import {Modal} from 'zk-tookit/antd',zk-tookit中的modal,对antd 的 modal进行了扩展,添加了 beforeOpen方法,每次打开都会触发,可以做modal的初始化工作;也避免了modal没有被打开,modal的willMout,didMount也触发,造成的一些操作浪费;

组件常用数据命名

组件只负责显示数据,至于数据由哪儿来的,外部通过props传入,一般情况下,很少在组件内发请求获取数据,但是也要视情况而定.数据名称有统一约定,可以降低沟通成本。

  1. 核心数据:
    1. 列表:dataSource;
    2. 对象:data;
    3. 基础数据:value;
  2. 加载:loading;
  3. 是否可见:visible;
  4. 是否禁用:disabled;
  5. 提交回调:onSubmit;
  6. 确定回调:onOk;
  7. 取消回调:onCancel;
  8. 事件回调:onXxx(onChage,onClick等等);

props类型和默认值

要申明props类型并注释属性的作用,通过props类型声明,即可知晓组件有哪些props;合理设置(不是全部设置)组件的默认props,可以有效的简化外部传入props的判断工作、以简化组件的调用传参;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, {Component} from 'react';
import PropTypes from 'prop-types';

class A extends Component{
static propTypes{
loading: PropTypes.bool, // 页面加载状态
};

static defaultProps{
loading: false,
};

state = {};

render(){
...
}
}

基础业务数据组件

基础业务数据,经常以radioselectcheckbox等形式被其他业务组件调用,提供一些组件:XxxSelect、XxxCheckbox、XxxRadio、XxxTransfer等,方便其他组件调用;

设计原则:

  1. 组件内部发送ajax请求,获取数据,其他组件只管调用即可,不必关心数据;
  2. 外部提供数据,通过dataSource属性传入,一般不会用到,如果有的组件用到再扩展即可;
  3. onLoad(dataSource, res)回调,将ajax请求返回的数据、res暴露给其他组件,其他组件有可能会使用到数据;
  4. dataFilter(dataSource, res) => dataSource回调,其他组件用来对ajax返回的数据进行过滤;
  5. showDisabled属性,默认false:是否显示禁用项,有些基础业务数据,有禁用启用操作,通过此字段表明是否显示禁用项;禁用项不可选择,label上显示禁用图标;
  6. 修改数据时,禁用项回显问题,如果修改的数据就是禁用的,不处理会显示value,而不是label;
  7. showAll属性,默认false:显示所有数据,不分禁用与启用,都是正常显示;

第三方组件再次封装

有时候,第三方组件不能满足业务需求,需要进行一次包装,包装后只是功能的增强,不要修改第三方组件原有的功能;

  1. 单独包装一个第三方组件,要将props也传递给第三方组件,比如zk-tookit/antd中的Modal组件,只是添加了beforeOpen回调,并将所有的props也传递给antd的modal,保持antd的modal原有功能;
  2. 组合封装多个第三方组件,所有第三方组件相应的声明对应的props属性,比如ListPage封装,用到了Table,listPage组件提供tableProps属性,所有表格相关的属性,都通过tableProps传递;form的getFieldDecorator所需参数,都通过decorator传递等等;

基础数据组件

比如:星期、男女、是否等硬编码数据

可以定义一个文件夹,分别定义数据以及各个组件,比如:weeks.js、WeekCheckbox.jsx、WeekRadio.jsx、WeekSelect.jsx;具体有什么组件基于需求而定,可以用到一个扩展一个。

其他

  1. 一些key-value相关的数据,一般定义为:[{value: '1', label: '星期一'}, ...],以valuelabel为关键字。
  2. 组件扩展,小版本内要注意向前兼容,参数要保证只增不减,同时用FIXME标记好哪些是不好的设计,等到大版本更新时,进行重构;
  3. 合理区分展示组件、容器组件;

props处理

抽取部分封装时用到的props属性,其他的原封不动赋值给html标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
render(){
const {
type, shape, size, className, htmlType, ...others,
} = this.props;
// 使用type 等一些属性,干些事情
...
return (
<button
{...others}
type={htmlType || 'button'}
...
>
...
</button>
);
}

componentWillReceiveProps周期函数

获取新旧props进行对比,将props转化成内部state等操作;

判断chilren是什么组件

A.jsx

1
2
3
4
5
6
7
export default class A extends React.Component{
static __IS_A = true; // 添加一个静态变量,作为标记
...
render(){
...
}
}

B.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
export default class B extends React.Component{
render(){
const {children} = this.props;
let hasA = false;
React.Children.map(children, item => {
// 从type中获取静态变量,作为判断依据
if(item && item.type && item.type.__IS_A) hasA = true;
});
return (
<div>{children}</div>
);
}
}

C.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import A from 'path/to/A';
import B from 'path/to/B';
export default class C extends React.Component{
render(){
return (
<B>
<A/>
<span>...</span>
...
</B>
);
}
}

一些实用小工具

  • classNames className处理工具
  • omit 浅拷贝,删除指定属性

propTypes 的使用

通用组件要声明propTypes,复杂结构(对象,数组内部包含对象)要通过PropTypes.shape明确声明内部结构

  • 通过propTypes可以清晰看出组件需要哪些属性,没个属性都是什么类型、结构;
  • 有些IDE可以基于propTypes给出提示;
  • 方便调用;

前端leader工作任务

前端leader工作任务

项目搭建

  1. 基于不同的项目进行技术选型;
  2. 基于以往总结、了解到的最佳实践进行架构设计;

团队培养

  1. 技术指导;
  2. 疑难问题解答;
  3. 分享最佳实践;
  4. 提升团队技术水平;

项目开发

  1. 理解需求,工作任务拆分,分配到组员;驳回其他部门不合理需求;
  2. 项目核心难点功能开发;
  3. 总结并记录最佳实践,新开项目时,重新完善架构;
  4. 保证项目进度,代码质量;
  5. 为其他成员兜底,完成其他成员无法完成的任务;
  6. review 团队成员代码,给出指导建议;确保代码质量;
  7. 组件化、模块化规划,将复杂的需求,拆分成模块、组件;安排或亲自进行代码编写;

    工作总结

  8. 按时总结工作完成情况,上报领导;

前后端分离开发约定

前后端分离开发约定

遵守约定,可以避免一些不必要的歧义,降低沟通成本。

前后端配合开发流程

划分前后端工作界限,避免前后端工作节奏被相互影响;并行开发之前,一定要确定好接口!!!

  • 前后端理解需求,前端进行部分页面开发,技术难点攻克;后端进行需求分析、数据库设计,确定绝大部分字段名称、类型、各式、描述等;
  • 基于原型/文档,后端设计接口,基于restfull形成swagger接口文档,前端确认后端接口是否符合前端需求。需要确认内容如下:
    • 每一个页面都需要哪些请求,哪些是以前提供过,哪些没有提供过;
    • 请求方法类型,确认使用get还是post等;
    • 请求的url;
    • 请求参数结构、参数名、类型;
    • 返回数据结构、变量名、类型;
  • 前端基于约定好的swagger文档进行页面开发;后端进行业务开发,提供真实数据接口。接口规范定义之后,后期如有变动及时沟通;
  • 前后端进行联调:后端验证接口是否正确,前端提供技术支持;

前后端界限(任务分工)

前端负责展示,后端负责数据处理;不要让前端进行数据的拼接,筛选等操作。

  • 前端
    • 展示后端传递数据,一般不需进行处理就可以直接展示;
    • 处理用户交互,收集数据发送ajax请求;
  • 后端工作
    • code 转 name,同时保留原先的code,前端转换会发送过多的请求,影响展现性能;
    • 页面中多个零散请求,如果可行,合并成一个请求,减少页面请求数量,提高页面展现性能;

ajax约定

数据交互统一使用JSON。

请求约定

基于restfull设计请求url。

  • 正确区分get post put del 等请求含义:
    • get: 获取服务端数据;
    • post: 保存数据到服务端;
    • put: 修改服务端数据;
    • del: 删除服务端数据;
  • 列表页分页参数:
    • 页容量:pageSize
    • 页码:pageNum;
  • 同一个数据出现在任何位置,要统一参数名;比如:房间号roomNo,不要出现roomNumroomCode等其他参数名;
  • 如无特殊需求,传递时间数据,要统一时间格式;
  • 后端需要的参数,要声明字段名、类型、各式(时间)、描述(中文说明字段含义);

成功时返回数据结构(版本一)

适用于接口需要支撑多端情况,通过message可以统一多端提示,如果提示信息需要更改,只改接口,各个端不需要修改,不需要发布。

  • code:业务逻辑结果编码,前端一般用不到,也可以通过code约定成功还是失败;
  • message:后端提供的信息,一般用于展现给用户,错误提示或者成功提示;成功提示如果后端统一给出,那么可以统一各个端(pc、android、ios)的提示信息,要区分开发、生产模式,开发过程中给出完整的错误提示,便于定位错误,生产环境给出用户友好提示并隐藏错误细节;
  • error:后端详细异常对象(或者字符串),用来体现具体的后端错误信息,方便定位问题,只非生产环境提供,生产环境不提供,以免暴露后端细节;
  • data:数据,单个数据为json对象,多条数据为json数组,数据变量命名规则如下:
    • 数据变量驼峰式命名;
    • 数据不必加前缀,比如用户查询,直接返回idnameaccount即可,直接数据对象的属性,不必添加user前缀,不要写成userIduserNameuserAccount
    • 如果是其他对象属性,需要加前缀区分,比如user对象中有角色id,角色id要命名成roleId
    • 同一个数据,无论出现在哪个接口里面,要保证命名统一,减少歧义及沟通成本;比如:roomNum房号,不要出现roomNoroomCode等多个命名;
    • 码表型数据,对应code要转成对应的中文,命名以Name结尾,同时保留原有code(多数业务需要用到原code),比如:roomCode,同时提供roomCodeName用于显示;
    • 时间用于展示,直接格式化成要显示的格式,最好提供原时间的时间戳。如果不提供格式化后的数据,需要提供时间戳,前端可以自己格式化,不要提供其他格式日期,前端格式化时,容易出错;
    • 列表数据,每条件记录以id为唯一标识;一定要存在id属性;
    • 每一个字段要给出描述,不常见字段前端并不知道对应的是什么,沟通成本比较高;
  • total:总条数,分页页面会用到;
  • 其他约定字段等;

成功时返回数据结构(版本二)

适用于接口只对接一端,后端只需要给出错误信息,成功提示前端提供。

  • 直接包含数据:
    • 单个数据为js对象;
    • 多条数据为js数组;
  • 分页查询:
    • list存放数据;
    • total存放总条数;

失败时返回数据结构

接口调用失败、后端逻辑验证失败等,后端要返回错误信息;

  • 所有非预期成功结果都视为失败,比如校验错误、越权操作等
  • 后端要基于不同的开发模式,返回不同的错误提示,生产环境返回友好的用户提示;其他环境返回用户提示+具体错误信息,方便调试。
  • 合理使用http状态码,前端会根据不同的http状态码进行不同的操作,详见如下 http状态码约定;

http状态码约定:

基于不同的状态码,前端会有相应的封装

  • 200:正常业务逻辑,不包含校验错误等业务逻辑,200一定是预期的成功;
  • 400:业务逻辑错误,校验错误,后端主动抛出的错误等,也可以约定为其他cod,视情况而定;
  • 401:需要登录,前端发现ajax返回401,会跳转到登录页面;
  • 403:无权操作,前端一般会给出类似“无权操作”的提示;
  • 404:资源不存在,前端一般会给出类似“您访问的资源不存在”的提示;
  • 500:服务端异常,前端一般会给出“未知系统错误,请联系管理员”;

后端解决跨域

跨域问题由服务端配置或NG代理进行解决,更简单一些。只是开发过程中会遇到跨域问题,实际生产环境使用NG,解决了跨域问题;

  • header一些设置,以nodejs为例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    app.all('*', function(req, res, next) {
    res.header("Access-Control-Allow-Origin", config().allow_origin);
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Access-Control-Allow-Credentials", "true");
    res.header("X-Powered-By", ' 3.2.1')
    res.header("Content-Type", "application/json;charset=utf-8");
    next();
    });
  • cookie问题: ajax跨域请求中的cookie问题

    • 后端Access-Control-Allow-Origin不能设置成*,要指定具体的域名;
    • 前端ajax请求,要设置withCredentialstrue,允许跨域的cookie访问;

通过配置proxy解决跨域

前端开发会启动一个开发模式的服务,如果这个服务支持proxy配置,可以通过配置proxy解决跨域问题

1
2
3
4
5
"proxy": {
"/api": {
"target": "http://localhost:3000"
}
}

前端最佳实践

前端最佳实践

项目怎么都能写,但是如何写的优雅?如何省时省力?如何构建大型项目,而不乱?需要总结一些最佳实践~

核心问题

  • 如何进行组件的划分
  • 组件如何方便的获取(后端)数据
  • 组件如何方便的修改(后端)数据

封装的目的是什么?

  • 包装复杂逻辑
  • 提供简单接口
  • 方便调用
  • 简化开发
  • 少写代码
  • 功能统一
  • 样式统一
  • 代码风格统一
  • 代码生成
  • 可维护性
  • 扩展性
  • 健壮性
  • 可读性

前端目录结构组织

前提要划分好需求的各个模块,基于模块/功能点对应的建立models/services,否则大型项目,需求越来越多,最终会导致models/services混乱,models命名与其包含的内容不符等问题尽量避免;

如下为src中的目录

components

通用业务组件

models

数据层,与后端交互,为组件提供数据;

services

主要进行一些ajax请求,供models层调用,获取后端数据;
是否可以考虑与models层合并?

pages

页面,只有pages中的文件与redux连接?;
接受models层数据,组合components完成用户交互、数据展示等;

layouts

布局,提供整体的结构,头部底部等通用布局组件;

避免组件频繁渲染

如果一个复杂的页面,组件A嵌套的层次比较深,任何父级组件的重新渲染,都会导致A组件的重新渲染,通过shouldComponentUpdate周期函数,进行优化,避免没有必要的重复渲染;考虑是否使用immutable-js

基础数据放入redux

基础数据,其他业务页面会经常用到,简化获取基础数据方式,有利于各个业务页面获取/使用基础数据;特殊业务的请求是否可以考虑再业务组件中直接发起?不必经过redux?

比如客户数据,提供getCustomer action,用来获取客户数据,如果某个业务组件用到客户数据,直接从redux中获取;

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
export function mapStateToProps(state){
return {
customers: state.customer.dataSource,
customersLoading:: state.customer.loading,
};
}
...
componentWillMount(){
const {
$actions: {customer},
} = this.props;

customer.getCustomer();
}

render(){
const {
customers,
customersLoading,
otherLoading,
} = this.props;

if(customerLoading || otherLoading) {
// show loading...
}
}
...

提供filter工具

提供filter工具,用来转换数据显示,比如id转换成对应的label,用于页面显示

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

export function filterGender(gender) {
const genders = {
male: '男',
female: '女',
m: '男',
f: '女',
M: '男',
F: '女',
};

return genders[gender] || '未知';
}

复杂页面拆分

部分页面会很复杂,如果所有内容都写到一个文件,将增加修改/维护难度;

可以基于页面结构/功能点,合理的将页面拆分成各个小组件,适当的使用redux进行数据管理;

通用业务组件抽离

通用的业务组件抽离到components目录中

构建自动化

前端构建过程要自动化,基本通过几个命令搞定,核心命令有如下几个:

1
2
3
4
5
6
7
8
9
10
11
// 安装所有依赖
$ yarn

// 启动开发模式
$ yarn start 或 yarn dev

// 生产构建
$ yarn build

// 测试
$ yarn test

ajax封装

只关心请求,成功提示、异常提示要自动化,简单配置就OK;

1
ajax.put('/user/123', params, {successTip: '保存成功!', errorTip: '保存失败!'});

函数/组件编写原则

  • 可用

    编写一个函数/组件最核心也是最低的要求,要达到可用;

  • 健壮

    最基本的兼容性处理、边界处理、异常处理、用户输入校验;

  • 可靠

    尽可能在任何情况下,都返回一个可靠的结果,哪怕是异常情况下;

  • 宽容

    对需求宽容、对用户宽容、对调用者宽容、对维护者宽容;如果函数需要的是数字,调用者传入的是字符串数字是否允许等;

  • 精益求精

    可读性、扩展性是否良好?性能怎么样?

大型项目

做大型项目时,一定要组织好基础业务组件,很多模块都会用到的组件,维护好了,可以省很多事儿;

认证

单页面应用,一般会涉及跨域问题,cookie有时候不是很适合,最好使用tooken方式,适用面更广,但是浏览器与App不同,要做好安全性方面的工作。团队中有多个项目,最好统一认证方式,可以减少工作量,降低沟通成本;

GraphQL

GraphQL

中文官网

英文官网

React Apollo

  1. 对API中的数据提供了一套易于理解的完整描述;
  2. 客户端可以准确的获取所需数据,所需结构,前端可定制性较强,所需数据基本可以自己组织;
  3. 获取的数据没有冗余,后端也会有相应得性能优化(不获取的字段,不会执行后端相应得解析函数);
  4. API容易演进,提供不同版本的数据;
  5. 一个请求获取多个资源,降低HTTP请求数量;
  6. 容易提供强大的开发工具;
  7. 以模块为单位进行数据组织;(如何划分是个难点)

restful api

目前我们项目存在的问题:

  1. 字段没有中文描述,不常见字段不知道对应的是什么;
  2. 不同人开发的、不同接口中,同一内容,命名不同,比如 【房号】有的人(接口)中为roomNum,有的为roomNo;
  3. 有的接口返回的数据并不利于前端展示,前端需要做转换;
  4. 接口是哪位后端开发的并不知道、是否开发完成也不知道;
  5. 日期相关参数,格式不统一;
  6. swagger 文档中接口越来越多,查找有些困难;
  7. 很多数据存在无意义前缀,比如roomType : {roomTypeId, roomTypeName, …},这里直接写成:roomType:{id, name, …},使用时,一般形式为:roomType.id, roomType.name,没个字段就不必添加前缀,显得冗余;

git tag

主要操作:

创建附注标签

1
$ git tag -a v0.1.2 -m “0.1.2版本”

删除标签

误打或需要修改标签时,需要先将标签删除,再打新标签。

1
$ git tag -d v0.1.2 # 删除标签

将v0.1.2标签提交到git服务器

1
$ git push origin v0.1.2

将本地所有标签一次性提交到git服务器

1
$ git push origin --tags

列出标签

1
2
$ git tag # 在控制台打印出当前仓库的所有标签
$ git tag -l ‘v0.1.*’ # 搜索符合模式的标签

打标签

git标签分为两种类型:轻量标签和附注标签。轻量标签是指向提交对象的引用,附注标签则是仓库中的一个独立对象。建议使用附注标签。

创建轻量标签

1
$ git tag v0.1.2-light

创建附注标签

1
$ git tag -a v0.1.2 -m “0.1.2版本”

创建轻量标签不需要传递参数,直接指定标签名称即可。
创建附注标签时,参数a即annotated的缩写,指定标签类型,后附标签名。参数m指定标签说明,说明信息会保存在标签对象中。

切换到标签

与切换分支命令相同

1
$ git checkout [tagname]

查看标签信息

用git show命令可以查看标签的版本信息:

1
$ git show v0.1.2

删除标签

误打或需要修改标签时,需要先将标签删除,再打新标签。

1
$ git tag -d v0.1.2 # 删除标签

参数d即delete的缩写,意为删除其后指定的标签。

给指定的commit打标签

打标签不必要在head之上,也可在之前的版本上打,这需要你知道某个提交对象的校验和(通过git log获取)。

补打标签

1
$ git tag -a v0.1.1 9fbc3d0

标签发布
通常的git push不会将标签对象提交到git服务器,我们需要进行显式的操作:

1
2
$ git push origin v0.1.2 # 将v0.1.2标签提交到git服务器
$ git push origin --tags # 将本地所有标签一次性提到git服务器

注意:如果想看之前某个标签状态下的文件,可以这样操作

  1. git tag 查看当前分支下的标签

  2. git checkout v0.21 此时会指向打v0.21标签时的代码状态,(但现在处于一个空的分支上)

  3. cat test.txt 查看某个文件

refer to:http://www.csser.com/dev/580.html

数组选出几个数相加等于固定值

转自:从数组中选出和等于固定值的n个数(JavaScript实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/**
调用说明:
array: 数据源数组。必选。
sum: 相加的和。必选。
tolerance: 容差。如果不指定此参数,则相加的和必须等于sum参数,指定此参数可以使结果在容差范围内浮动。可选。
targetCount: 操作数数量。如果不指定此参数,则结果包含所有可能的情况,指定此参数可以筛选出固定数量的数相加,假如指定为3,那么结果只包含三个数相加的情况。可选。
返回值:返回的是数组套数组结构,内层数组中的元素是操作数,外层数组中的元素是所有可能的结果。
*/
function getCombBySum(array,sum,tolerance,targetCount){
var util = {
/*
get combination from array
arr: target array
num: combination item length
return: one array that contain combination arrays
*/
getCombination: function(arr, num) {
var r=[];
(function f(t,a,n)
{
if (n==0)
{
return r.push(t);
}
for (var i=0,l=a.length; i<=l-n; i++)
{
f(t.concat(a[i]), a.slice(i+1), n-1);
}
})([],arr,num);
return r;
},
//take array index to a array
getArrayIndex: function(array) {
var i = 0,
r = [];
for(i = 0;i<array.length;i++){
r.push(i);
}

return r;
}
},logic = {
//sort the array,then get what's we need
init: function(array,sum) {
//clone array
var _array = array.concat(),
r = [],
i = 0;
//sort by asc
_array.sort(function(a,b){
return a - b;
});
//get all number when it's less than or equal sum
for(i = 0;i<_array.length;i++){
if(_array[i]<=sum){
r.push(_array[i]);
}else{
break;
}
}

return r;
},
//important function
core: function(array,sum,arrayIndex,count,r){
var i = 0,
k = 0,
combArray = [],
_sum = 0,
_cca = [],
_cache = [];

if(count == _returnMark){
return;
}
//get current count combination
combArray = util.getCombination(arrayIndex,count);
for(i = 0;i<combArray.length;i++){
_cca = combArray[i];
_sum = 0;
_cache = [];
//calculate the sum from combination
for(k = 0;k<_cca.length;k++){
_sum += array[_cca[k]];
_cache.push(array[_cca[k]]);
}
if(Math.abs(_sum-sum) <= _tolerance){
r.push(_cache);
}
}

logic.core(array,sum,arrayIndex,count-1,r);
}

},
r = [],
_array = [],
_targetCount = 0,
_tolerance = 0,
_returnMark = 0;

//check data
_targetCount = targetCount || _targetCount;
_tolerance = tolerance || _tolerance;

_array = logic.init(array,sum);
if(_targetCount){
_returnMark = _targetCount-1;
}

logic.core(_array,sum,util.getArrayIndex(_array),(_targetCount || _array.length),r);

return r;
}

两个数相加等于固定的值,参考http://taop.marchtea.com/02.02.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 声明两个变量做为数组的指针,一个从0开始,一个从length-1开始。。。。
var arr=[1, 2, 4, 5, 6, 4];
var t = 5;
function twoSun(data, sum) {
var length = data.length;
var begin = 0;
var end = length - 1;
while(begin < end) {
var currSum = data[begin] + data[end];
if(currSum === sum) {
console.log(data[begin], data[end]);

begin++;
end--;
// break;
}else{
if(currSum < sum){
begin++;
}else{
end--;
}
}
}
}

twoSun(arr, t);

bind polyfill

老版本浏览器可能不支持bind函数,这里做一个polyfill。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if(!Function.prototype.bind){
Function.prototype.bind = function(obj){
var slice = [].slice,
//bind 函数是可以传参数的。比如:this.click.bind(this,1,2,3)
args = slice.call(arguments, 1),
self = this,
nop = function(){},
bound = function(){
return self.apply(this instanceof nop ? this:(obj||{}),
args.concat(slice.call(arguments)));
};
nop.prototype = self.prototype;
bound.prototype = new nop();
return bound;
};
}

通过时间戳获取索引的一个有趣的算法题

一个数组,是个数字:var arr = [0, 1, 2,…8, 9],设计一个函数,要求三秒内调用次函数,返回的数字都是一个,如果两次调用间隔超过三秒,则返回不同的数字,不允许使用全局计数变量。

实现思路,通过时间戳分段来实现,3000毫秒一个段,arr.length为一个区间,比如:[0, 3000] (3000, 6000] (6000, 9000]…,获取当前时间戳,计算当前时间戳属于哪个区段,即可得出索引值,具体算法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function getId(arr){
var t = new Date().getTime();
var index = parseInt(t/3000) % arr.length;
return arr[index];
}

// 合并成一行代码为:

function getId(arr){
return arr[parseInt(new Date().getTime()/3000)%arr.length];
}

Vue2.0 跨组件事件

组件化经常会涉及到跨组件通信,可以通过发布订阅实现,Vue中可以使用一个空的vue实例座位数据总线,实现组件间通信,不需要使用第三方的发布订阅库。

1
2
3
4
5
6
7
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})

实际项目中可以这样封装一下,搞个mixins:

1
2
3
4
5
6
7
8
9
10
11
12
13
const eventBus = new Vue();
const mixins = {
data (){
return {
eventBus,
};
},
}
Vue.mixin(mixins);

// 各个组件中就可以通过如下方式来使用
this.eventBus.$emit(...);
this.eventBus.$on(...);

JS获取窗滚动条位置

如果项目中使用jQuery,请使用jQuery相关方法

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
/**
* 获取指定window中滚动条的偏移量,如未指定则获取当前window
* 滚动条偏移量
*
* @param {window} w 需要获取滚动条偏移量的窗口
* @return {Object} obj.x为水平滚动条偏移量,obj.y为竖直滚动条偏移量
*/
function getScrollOffset(w) {
w = w || window;
// 如果是标准浏览器
if (w.pageXOffset != null) {
return {
x: w.pageXOffset,
y: w.pageYOffset
};
}

// 老版本IE,根据兼容性不同访问不同元素
var d = w.document;
if (d.compatMode === 'CSS1Compat') {
return {
x: d.documentElement.scrollLeft,
y: d.documentElement.scrollTop
}
}

return {
x: d.body.scrollLeft,
y: d.body.scrollTop
};
}
console.log(getScrollOffset());

JS获取窗口尺寸

项目中如果使用了jQuery,可以使用jQuery相关方法。

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
/**
* 查询指定窗口的视口尺寸,如果不指定窗口,查询当前窗口尺寸
**/
function getViewportSize(w) {
w = w || window;

// IE9及标准浏览器中可使用此标准方法
if ('innerHeight' in w) {
return {
width: w.innerWidth,
height: w.innerHeight
};
}

var d = w.document;
// IE 8及以下浏览器在标准模式下
if (document.compatMode === 'CSS1Compat') {
return {
width: d.documentElement.clientWidth,
height: d.documentElement.clientHeight
};
}

// IE8及以下浏览器在怪癖模式下
return {
width: d.body.clientWidth,
height: d.body.clientHeight
};
}
console.log(getViewportSize());

JS解析query-string

可以使用的一个第三方开源库query-string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 解析query string转换为对象,一个key有多个值时生成数组
*
* @param {String} query 需要解析的query字符串,开头可以是?,
* 按照application/x-www-form-urlencoded编码
* @return {Object} 参数解析后的对象
*/
function parseQuery(query) {
var result = {};

// 如果不是字符串返回空对象
if (typeof query !== 'string') {
return result;
}

// 去掉字符串开头可能带的?
if (query.charAt(0) === '?') {
query = query.substring(1);
}

var pairs = query.split('&');
var pair;
var key, value;
var i, len;

for (i = 0, len = pairs.length; i < len; ++i) {
pair = pairs[i].split('=');
// application/x-www-form-urlencoded编码会将' '转换为+
key = decodeURIComponent(pair[0]).replace(/\+/g, ' ');
value = decodeURIComponent(pair[1]).replace(/\+/g, ' ');

// 如果是新key,直接添加
if (!(key in result)) {
result[key] = value;
}
// 如果key已经出现一次以上,直接向数组添加value
else if (isArray(result[key])) {
result[key].push(value);
}
// key第二次出现,将结果改为数组
else {
var arr = [result[key]];
arr.push(value);
result[key] = arr;
} // end if-else
} // end for

return result;
}

function isArray(arg) {
if(Array.isArray){
return Array.isArray(arg);
}
if (typeof arg === 'object') {
return Object.prototype.toString.call(arg) === '[object Array]';
}
return false;
}

console.log(parseQuery('sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8'));

JS判断是否是函数

设计项目中可以使用如下封装或者使用第三方库的实现,比如loadsh,jQuery等isFunction方法

1
2
3
4
5
6
7
8
9
10
11
function isFunction(arg) {
if (arg) {
if (typeof (/./) !== 'function') {
return typeof arg === 'function';
} else {
return Object.prototype.toString.call(arg) === '[object Function]';
}
}
return false;
}
console.log(isFunction(function(){}));

JS时间监听器

一般都使用jQuery 少数情况会 用到原生,可以考虑使用如下封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function addListener(el, type, listener, useCapture){
if (window.addEventListener) {
el.addEventListener(type, listener, useCapture);
return listener;
}
if (window.attachEvent){
// 标准化this,event,target
var wrapper = function () {
var event = window.event;
event.target = event.srcElement;
listener.call(el, event);
};

el.attachEvent('on' + type, wrapper);
return wrapper;
}
}

addListener(document.getElementById('test'), 'click', function(e){
console.log(e);
});

JS判断是否是数组

设计项目中可以使用如下封装或者使用第三方库的实现,比如loadsh,jQuery等isArray方法

1
2
3
4
5
6
7
8
9
function isArray(arg) {
if(Array.isArray){
return Array.isArray(arg);
}
if (typeof arg === 'object') {
return Object.prototype.toString.call(arg) === '[object Array]';
}
return false;
}

js 深克隆

设计项目中可以使用如下封装或者使用第三方库的实现,比如loadsh

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
/**
* javascript 深度克隆函数
*/
function deepClone(obj) {
var _toString = Object.prototype.toString;

// null, undefined, non-object, function
if (!obj || typeof obj !== 'object') {
return obj;
}

// DOM Node
if (obj.nodeType && 'cloneNode' in obj) {
return obj.cloneNode(true);
}

// Date
if (_toString.call(obj) === '[object Date]') {
return new Date(obj.getTime());
}

// RegExp
if (_toString.call(obj) === '[object RegExp]') {
var flags = [];
if (obj.global) { flags.push('g'); }
if (obj.multiline) { flags.push('m'); }
if (obj.ignoreCase) { flags.push('i'); }

return new RegExp(obj.source, flags.join(''));
}

var result = Array.isArray(obj) ? [] :
obj.constructor ? new obj.constructor() : {};

for (var key in obj ) {
result[key] = deepClone(obj[key]);
}

return result;
}

function A() {
this.a = a;
}

var a = {
name: 'qiu',
birth: new Date(),
pattern: /qiu/gim,
container: document.body,
hobbys: ['book', new Date(), /aaa/gim, 111]
};

var c = new A();
var b = deepClone(c);
console.log(c.a === b.a);
console.log(c, b);

前端监控

一个开源库:https://github.com/fex-team/alogs

使用图片发送get请求,上报信息,由于浏览器对图片有缓存,同样的请求,图片只会发送一次,避免重复上报。

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
var entry = {};
function report(url, data) {
if (!url || !data) {
return;
}

// @see http://jsperf.com/new-image-vs-createelement-img
var image = document.createElement('img');

var items = [];
for (var key in data) {
if (data[key]) {
items.push(key + '=' + encodeURIComponent(data[key]));
}
}

var name = 'img_' + (+new Date());
entry[name] = image;
image.onload = image.onerror = function () {
console.log(arguments);
entry[name] =
image =
image.onload =
image.onerror = null;
delete entry[name];
};

image.src = url + (url.indexOf('?') < 0 ? '?' : '&') + items.join('&');
}

web 安全

  1. 用户输入的数据,不要不经过转义直接展示到页面上,防止其中有些js被执行,一般的用户输入的数据,页面渲染的时候都是被转义过得,即,用户输入什么就展示什么,不会被执行,但是特殊情况除外,比如富文本编辑器,对于富文本的内容一定要做好过滤。
  2. 防止记录用户登录的cookie被劫持,这个cookie要设置HttpOnly

android颜色管理

优秀的app设计,一般只有几种颜色,各个组件的颜色都是统一的,比如统一的字体颜色,统一的背景颜色,统一的border等,如果设计良好,可以基于所有的设计图,整理出android项目所需要color。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 主色调 -->
<color name="colorPrimary">#f65037</color>
<!-- 主色调深色 -->
<color name="colorPrimaryDark">#d34202</color>
<!-- 输入框获得焦点、单复选框选中、按钮激活颜色 -->
<color name="colorAccent">#ff0000</color>

<color name="white">#ffffff</color>
<color name="textColorPrimary">#eeeeee</color>
<color name="textColorPrimaryDark">#666666</color>
<color name="windowBackground">#ffffff</color>
<color name="navigationBarColor">#000000</color>
</resources>

nodejs 获取本机ip地址

1
2
3
4
5
6
7
8
9
10
11
12
exports = module.exports = function getIPAddress() {
var interfaces = require('os').networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
}

前端开发需要注意的一些问题

高保真还原设计图

  1. 开发之前要对设计图进行开发角度的分析,有疑问及时跟设计沟通解决;多注意一些空白页,异常数据情况,是否有相应的设计图。
  2. 开发过程中,要严格按照设计图进行编码,高保真还原设计图;
  3. 开发完成之后,让设计确认实现结果,发现问题,及时解决。
  4. 要根据自己的开发经验,确定设计是否合理,扁平化的图标,是否建议设计使用font-icon

高保真还原产品需求

  1. 开发之前认真参加需求评审,站在前端的角度考虑需求的合理性,有问题及时提出;
  2. 开发过程中发现需求的细节问题,及时跟产品沟通解决;
  3. 开发完成之后,要对照产品原型,自测自己开发的功能是否百分百符合需求。

空白页

尤其是列表页,在没有内容时,要显示类似“暂无数据”这样的提示,开发过程中要注意这一细节,如果没有设计,要跟设计沟通,让设计提供相关页面;如果没有文案,要跟产品确认提示文案。

按钮交互样式

这个属于用户体验方面的细节,用户进行了任何操作,界面上要给予及时的反馈,让用户知道,他进行了什么样的操作。发送请求时,按钮置为disabled(或者loading),防止重复发请求。

pc端:

  1. 鼠标移入移出按钮,要有交互反馈,可以通过:hover实现。
  2. 鼠标点击返回,可以通过:active实现。

移动端:

  1. 点击按钮:可以通过:active实现。

文字过长如何显示

设计有时会按照理想状况出设计图,但是如果文字过长如何显示,是直接截断?还是显示省略号?可以跟设计、产品沟通确认

图片

  1. 图片在保证清晰度的情况下,尽量压缩,移动端不同屏幕考虑提供2x 3x等规格的图片
  2. 图片裁剪,拼接@200_w(阿里的oss),要根据实际的场景,合理拼接参数
  3. 图片是否需要懒加载
  4. 图片加载loading站位图
  5. 图片加载失败站位图

title和页面icon

<title>Title</title> <link rel="icon">之前都是写死的开发的时候需要注意下。跨平台的时候是统一还是显示平台或者商户的title和icon需要和产品沟通一下。

请求

  1. 适当显示loading
  2. 发起请求时,对应按钮disabled(或者loading),防止重复请求
  3. 请求失败,错误提示

项目发布之前

  1. 多余的调试(console.log、alert)代码是否已删除(尤其是alert)。检查方法:可以全局搜索一下,发现调试代码,及时删除
  2. 安装第三方包是否同时修改了package.json(—save,—save-dev参数未加引起的)。检查方法:将本地的node_moduls文件夹删除,重新npm install 启动项目,查看是否缺包。
  3. 生产环境的构建配置是否正确。检查方法:一般测试环境和线上环境使用的配置相同,测试环境没问题,一般问题不大;还可以本地模拟正式环境,进行一次构建。
  4. 微信调试是否关闭。检查方法:wx.config中的debug是否为false
  5. 第三方库的调试是否关闭。检查方法:以vue为例,在webpack配置中,设置new webpack.DefinePlugin({‘process.env’: ‘production’}),可以去掉调试信息,有的库是提供全局方法,具体情况具体分析。

1px边框实现

iphone手机,设置border:1px solid #000,实际显示要比一像素要宽,具体原因请自己Google,
通过transform: scale(0.5)可以实现1像素border,具体封装如下:
注意:

  • 要显示border的元素需要设置:position:relative或者position:absolute;
  • 同时只能设置一个top或者bottom(一个left或者right)
    因为top和left是使用:after,left和right使用的是before;
  • 这种方式无法设置border-radius;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    @borderDefaultWidth: 1px;
    @borderDefaultColor: #e8e8e8;

    .border-top(@width: @borderDefaultWidth, @color: @borderDefaultColor) {
    &:after {
    top: 0;
    .border-top-bottom-style(@width, @color);
    }
    .media()
    }

    .border-bottom(@width: @borderDefaultWidth, @color: @borderDefaultColor) {
    &:after {
    bottom: 0;
    .border-top-bottom-style(@width, @color);
    }
    .media()
    }

    .border-right(@width: @borderDefaultWidth, @color: @borderDefaultColor) {
    &:before {
    right: 0;
    .border-left-right-style(@width, @color);
    }
    .media()
    }

    .border-left(@width: @borderDefaultWidth, @color: @borderDefaultColor) {
    &:before {
    left: 0;
    .border-left-right-style(@width, @color);
    }
    .media()
    }

    .border-top-bottom-style(@width, @color) {
    content: "\20";
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    -webkit-transform-origin: 50% 100%;
    transform-origin: 50% 100%;
    height: @width;
    background-color: @color;
    }

    .border-left-right-style(@width, @color) {
    content: "\20";
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    -webkit-transform-origin: 100% 50%;
    transform-origin: 100% 50%;
    width: @width;
    background-color: @color;
    }

    .media() {
    @media only screen and (-webkit-min-device-pixel-ratio: 2) {
    &:after {
    -webkit-transform: scaleY(0.5);
    transform: scaleY(0.5);
    }
    }

    @media only screen and (-webkit-min-device-pixel-ratio: 3) {
    &:after {
    -webkit-transform: scaleY(0.33333);
    transform: scaleY(0.33333);
    }
    }

    @media only screen and (-webkit-min-device-pixel-ratio: 4) {
    &:after {
    -webkit-transform: scaleY(0.25);
    transform: scaleY(0.25);
    }
    }
    }

使用方法:

1
2
3
4
5
6
@import '../path-to/mixins.less';
div{
display: relative;
.border-top();
.border-left();
}

js滚动动画

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
/**
* 滚动动画函数
* @param el 元素对象或者元素id
* @param targetPosition 滚动条目标位置
* @param duringTime 持续时间 默认300
* @param onComplete 滚动完成触发
*/
export function scrollAnimate({el, targetPosition, duringTime = 300, onComplete}) {
if (typeof el === 'string') {
el = document.getElementById(el);
}

if (!targetPosition) return;

const startTime = (new Date()).getTime();

function animate() {
const nowTime = (new Date()).getTime();
const elapsed = nowTime - startTime;
const fraction = elapsed / duringTime;
const position = targetPosition - el.scrollTop;
if (fraction < 1) {
el.scrollTop += fraction * position * Math.sin(Math.PI / 2);
setTimeout(animate, Math.min(25, duringTime - elapsed));
} else {
el.scrollTop = targetPosition;
if (onComplete) {
onComplete();
}
}
}

animate();
}

金钱格式化

开发过程中经常能遇到对金钱格式化的需求,这里整理一个方法

es5 版本

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
/**
* 将数值四舍五入后格式化成金额形式
*
* @param num 数值(Number或者String)
* @param options.prefix 金钱前缀,默认为空,一般为 ¥ 或 $
* @param options.decimalNum 保留小数点个数,默认为2 一般为 0 1 2
* @param options.splitSymbol 格式化分割符,默认为英文逗号,分隔符必须是单字符
* @return 金额格式的字符串,如'¥1,234,567.45'
* @type String
*/
function formatCurrency(num, options) {
if (options === undefined) options = {};
var prefix = options.prefix || '';
var decimalNum = options.decimalNum;
var splitSymbol = options.splitSymbol;
var centsPercent = 100;
if (splitSymbol === undefined) splitSymbol = ',';
if (decimalNum !== 0 && decimalNum !== 1 && decimalNum !== 2) decimalNum = 2;
if (decimalNum === 0) {
centsPercent = 1;
}
if (decimalNum === 1) {
centsPercent = 10;
}
num = num.toString().replace(/\$|\,/g, '');
if (isNaN(num)) num = "0";
var sign = (num == (num = Math.abs(num))) ? '' : '-';
num = Math.floor(num * centsPercent + 0.50000000001);
var cents = num % centsPercent;
num = Math.floor(num / centsPercent).toString();
if (cents < 10 && decimalNum === 2) {
cents = "0" + cents;
}

for (var i = 0; i < Math.floor((num.length - (1 + i)) / 3); i++) {
num = num.substring(0, num.length - (4 * i + 3)) + splitSymbol + num.substring(num.length - (4 * i + 3));
}
if (decimalNum === 0) {
return prefix + sign + num;
}
return prefix + sign + num + '.' + cents;
}

console.log(formatCurrency(9090900.55533));
// output: 9,090,900.56

console.log(formatCurrency(9090900.55533, {
prefix: '¥',
decimalNum: 2,
splitSymbol: ','
}));
// output: ¥9,090,900.56

es6 版本

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

/**
* 将数值四舍五入后格式化成金额形式
*
* @param num 数值(Number或者String)
* @param options 可选参数
* @param options.prefix 金钱前缀,默认为空,一般为 ¥ 或 $
* @param options.decimalNum 保留小数点个数,默认为2 一般为 0 1 2
* @param options.splitSymbol 格式化分割符,默认为英文逗号,分隔符必须是单字符
* @return 金额格式的字符串,如'¥1,234,567.45'
* @type String
*/
function formatCurrency(num, options = {}) {
let {decimalNum, splitSymbol} = options;
const {prefix = ''} = options;
let centsPercent = 100;

if (splitSymbol === undefined) splitSymbol = ',';
if (decimalNum !== 0 && decimalNum !== 1 && decimalNum !== 2) decimalNum = 2;
if (decimalNum === 0) centsPercent = 1;
if (decimalNum === 1) centsPercent = 10

num = num.toString().replace(/\$|,/g, '');
if (isNaN(num)) num = '0';
const sign = (num === Math.abs(num).toString()) ? '' : '-';
num = Math.abs(num);
num = Math.floor((num * centsPercent) + 0.50000000001);
let cents = num % centsPercent;
num = Math.floor(num / centsPercent).toString();
if (cents < 10 && decimalNum === 2) {
cents = `0${cents}`;
}
for (let i = 0; i < Math.floor((num.length - (1 + i)) / 3); i++) {
const endPosition = (4 * i) + 3;
num = num.substring(0, num.length - endPosition)
+ splitSymbol + num.substring(num.length - endPosition);
}
if (decimalNum === 0) {
return prefix + sign + num;
}
return `${prefix}${sign}${num}.${cents}`;
}

Karma 使用

为什么使用Karma

  • 真实浏览器
  • 多浏览器
  • 开发过程中本地运行
  • 持续集成服务器中运行
  • 每次保存自动运行
  • 终端运行
  • 生成测试报告
  • 模块化,es6支持

安装

1
2
3
4
$ npm install karma --save-dev

// 可以全局使用karma命令
$ sudo npm install -g karma-cli

JS 正则表达式

JavaScript 权威指南第6版摘录一些表格

正则表达式中的直接量字符.png
正则表达式的字符类.png
正则表达式的重复字符语法.png
正则表达式的选择、分组和引用字符.png
正则表达式中的锚字符.png
正则表达式修饰符.png

字符串可以使用正则的四个方法

1
2
3
4
'abc'.search(/abc/);
'abc'.match(/abc/);
'abc'.split(/b/);
'abc'.replace(/abc/, 'def');

正则对象RegExp属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
// 五个属性
// source global ignoreCase multiLine lastIndex

// 两个方法
// test
// exec
var pattern = /Java/g;
var text = 'JavaScript is more fun than Java!';
var result;
while((result = pattern.exec(text)) !== null){
console.log("Matched '" + result[0] + "'" + " at Position " + result.index + "; next search begins at " + pattern.lastIndex);
}

React PropTypes

良好的组件,要定义好propTypes

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
static propTypes =  {
// 可以声明 prop 为指定的 JS 基本类型。默认
// 情况下,这些 prop 都是可传可不传的。
optionalArray: React.PropTypes.array,
optionalBool: React.PropTypes.bool,
optionalFunc: React.PropTypes.func,
optionalNumber: React.PropTypes.number,
optionalObject: React.PropTypes.object,
optionalString: React.PropTypes.string,

// 所有可以被渲染的对象:数字,
// 字符串,DOM 元素或包含这些类型的数组。
optionalNode: React.PropTypes.node,

// React 元素
optionalElement: React.PropTypes.element,

// 用 JS 的 instanceof 操作符声明 prop 为类的实例。
optionalMessage: React.PropTypes.instanceOf(Message),

// 用 enum 来限制 prop 只接受指定的值。
optionalEnum: React.PropTypes.oneOf(['News', 'Photos']),

// 指定的多个对象类型中的一个
optionalUnion: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.number,
React.PropTypes.instanceOf(Message)
]),

// 指定类型组成的数组
optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number),

// 指定类型的属性构成的对象
optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number),

// 特定形状参数的对象
optionalObjectWithShape: React.PropTypes.shape({
color: React.PropTypes.string,
fontSize: React.PropTypes.number
}),

// 以后任意类型加上 `isRequired` 来使 prop 不可空。
requiredFunc: React.PropTypes.func.isRequired,

// 不可空的任意类型
requiredAny: React.PropTypes.any.isRequired,

// 自定义验证器。如果验证失败需要返回一个 Error 对象。不要直接
// 使用 `console.warn` 或抛异常,因为这样 `oneOfType` 会失效。
customProp: function(props, propName, componentName) {
if (!/matchme/.test(props[propName])) {
return new Error('Validation failed!');
}
}
}

基于rem的html font-size 写法

rem是相对于html的font-size的大小单位,在做移动端,不同屏幕下自适应很好用,下面是基于不同分辨率,生成html font-size大小的代码

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
/*
* 375屏幕为 20px,以此为基础计算出每一种宽度的字体大小
* 375以下不变,375以上等比放大
*/

@baseWidth: 375px;
@baseFont: 20px;

html {
font-size: @baseFont; //默认当做320px宽度的屏幕来处理
}

@bps: 400px, 414px, 480px; // 支持其他分辨率,在这里追加即可

.loop(@i: 1) when (@i <= length(@bps)) { //注意less数组是从1开始的
@bp: extract(@bps, @i);
@font: @bp/@baseWidth*@baseFont;
@media only screen and (min-width: @bp){
html {
font-size: @font !important;
}
}
.loop((@i + 1));
};
.loop;

转换成css为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
html {
font-size: 20px;
}
@media only screen and (min-width: 400px) {
html {
font-size: 21.33333333px !important;
}
}
@media only screen and (min-width: 414px) {
html {
font-size: 22.08px !important;
}
}
@media only screen and (min-width: 480px) {
html {
font-size: 25.6px !important;
}
}

注:less 转换方法 sudo npm install less -g lessc styles.less

js一些工具方法

平时用到的一些小方法,整理起来备用

数组乱序

1
2
3
4
5
var arr = [0, 1, 2, 3, 4, 5, 6];
arr.sort(function () {
return 0.5 - Math.random();
})
console.log(arr);

随机数

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取 min~max之间的一个随机整数
* @param min
* @param max
* @returns {*}
*/
function getRandomNum(min, max) {
var range = max - min;
var rand = Math.random();
return (min + Math.round(rand * range));
}

日期格式化扩展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// es6版本

/**
* 日期格式化函数
* 年:y,1-4个占位符
* 月:M,日:d,时:h,分:m,秒:s,季度:q, 1-2 个占位符
* 毫秒:S,1个占位符
* 例:
* (new Date()).format('yyyy-MM-dd hh:mm:ss.S') ---> 2016-09-05 10:18:32.976
* (new Date()).format('yyyy-M-d h:m:s.S') ---> 2016-9-5 10:18:32.976
* @param date
* @param format
* @returns {*}
*/
function formatDate(date, format = 'yyyy-MM-dd') {
const symbolMap = {
'M+': date.getMonth() + 1, // 月
'd+': date.getDate(), // 日
'h+': date.getHours(), // 时
'm+': date.getMinutes(), // 分
's+': date.getSeconds(), // 秒
'q+': Math.floor((date.getMonth() + 3) / 3), // 季度
S: date.getMilliseconds(), // 毫秒
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, String(date.getFullYear()).substr(4 - RegExp.$1.length));
}
for (const k of Object.keys(symbolMap)) {
if (new RegExp(`(${k})`).test(format)) {
format = format.replace(RegExp.$1, (RegExp.$1.length === 1) ? (symbolMap[k]) : (`00${symbolMap[k]}`.substr(String(symbolMap[k]).length)));
}
}
return format;
}

// es5版本

/**
* 日期格式化函数
* 年:y,1-4个占位符
* 月:M,日:d,时:h,分:m,秒:s,季度:q, 1-2 个占位符
* 毫秒:S,1个占位符
* 例:
* (new Date()).format('yyyy-MM-dd hh:mm:ss.S') ---> 2016-09-05 10:18:32.976
* (new Date()).format('yyyy-M-d h:m:s.S') ---> 2016-9-5 10:18:32.976
* @param date
* @param format
* @returns {*}
*/
function dateFormat(date, format) {
var symbolMap = {
"M+": date.getMonth() + 1, //月
"d+": date.getDate(), //日
"h+": date.getHours(), //时
"m+": date.getMinutes(), //分
"s+": date.getSeconds(), //秒
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in symbolMap) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (symbolMap[k]) : (("00" + symbolMap[k]).substr(("" + symbolMap[k]).length)));
}
}
return format;
}

/**
* 扩展Date,添加格式化功能
* 年:y,1-4个占位符
* 月:M,日:d,时:h,分:m,秒:s,季度:q, 1-2 个占位符
* 毫秒:S,1个占位符
* 例:
* (new Date()).format('yyyy-MM-dd hh:mm:ss.S') ---> 2016-09-05 10:18:32.976
* (new Date()).format('yyyy-M-d h:m:s.S') ---> 2016-9-5 10:18:32.976
* @param format
* @returns {*}
*/
Date.prototype.format = function (format) {
var symbolMap = {
"M+": this.getMonth() + 1, //月
"d+": this.getDate(), //日
"h+": this.getHours(), //时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in symbolMap) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, (RegExp.$1.length == 1) ? (symbolMap[k]) : (("00" + symbolMap[k]).substr(("" + symbolMap[k]).length)));
}
}
return format;
};

根据给定时间,获取给定时间当前周,前一周,后一周的所有日期

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
/**
* 根据给定时间,获取给定时间当前周,前一周,后一周的所有日期
* @param date
* @returns {{activeDates: Array, prevDates: Array, nextDates: Array}}
*/
function getWeekDateRange(date) {
var activeDates = [];
var prevDates = [];
var nextDates = [];
var nowTime = date.getTime();
var day = date.getDay();
var oneDayLong = 24 * 60 * 60 * 1000;

for (var i = -7; i < 14; i++) {
var dateTiem = nowTime + (i - day) * oneDayLong;
var d = new Date(dateTiem);
if (i < 0) {
prevDates.push(d);
} else if (i < 7) {
activeDates.push(d);
} else {
nextDates.push(d);
}

}

return {
activeDates: activeDates,
prevDates: prevDates,
nextDates: nextDates,
}
}

去除字符串前后,左侧,右侧空格

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var testStr = '   aaa bbb   ';
// 去掉前后空格
var str = testStr.replace(/(^\s*)|(\s*$)/g, "");

// 去掉左侧空格
var str = testStr.replace(/(^\s*)/g, "");

// 去掉右侧空格
var str = testStr.replace(/(\s*$)/g, "");

// 扩展string对象
String.prototype.trim = function() {
return this.replace(/(^\s*)|(\s*$)/g, "");
}

String.prototype.leftTrim = function() {
return this.replace(/(^\s*)/g, "");
}

String.prototype.rightTrim = function() {
return this.replace(/(\s*$)/g, "");
}

格式化电话号码

1
2
3
4
5
6
7
8
9
10
function formatPhoneNumber (phoneNum) {
phoneNum = phoneNum.toString();
var regFormatted = /^(\d{3})(\d{1,4})(\d{1,4}$)?/g;
var pattern = '$1-$2-$3';
// 第二个replace解决 186-88-,多一个‘-’的问题
return phoneNum.replace(regFormatted, pattern).replace(/^(\d{3}-\d{1,4})(-)$/, '$1');
}
console.log(18688888888); // 186-8888-8888
console.log('18688888888'); // 186-8888-8888
console.log(18688);// 186-88

获取当前日期的前后几天的日期

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 getOffsetDate(n) {
var d = new Date();
var year = d.getFullYear();
var mon = d.getMonth() + 1;
var day = d.getDate();
if (day >= n) {
if (mon > 1) {
mon = mon - 1;
}else {
year = year - 1;
mon = 12;
}
}
d.setDate(d.getDate() + n);
year = d.getFullYear();
mon = d.getMonth() + 1;
day = d.getDate();
return [
year + "" + (mon < 10 ? ('0' + mon) : mon) + "" + (day < 10 ? ('0' + day) : day),
(mon < 10 ? ('0' + mon) : mon) + "-" + (day < 10 ? ('0' + day) : day)
];
}
// 如果当前日期是 2016-12-28
console.log(getOffsetDate(1)); // ["20161229", "12-29"] 当前日期的后一天
console.log(getOffsetDate(-3)); // ["20161225", "12-25"] 当前日期的前三天

自定义webpack loader简化react-router按需加载写法

使用react-router时,需要在开始异步获取组件/获取完成/组件是否需要渲染等时刻加入hock(使用发布订阅发送消息),按需加载写法本来就比较繁琐,加上这些hock,更加繁琐了:

1
2
3
4
5
6
7
8
9
10
11
{
path: '/dev/components',
getComponent: (nextState, cb) => {
startFetchingComponent();
require.ensure([], (require) => {
if (!shouldComponentMount(nextState)) return;
endFetchingComponent();
cb(null, connectComponent(require('./sys-component/SysComponent')));
});
},
},

其中除了path: '/dev/components','./sys-component/SysComponent' 其他信息,各个route写法都一样,就算复制粘贴,相同代码也占了绝大部分。

注:其中startFetchingComponent shouldComponentMount endFetchingComponent 方法会使用发布订阅发送消息,connectComponent用来链接redux,这些hock是贴近我的具体业务加的,具体情况,随机应变。

想要的结果

简化写法如下就能实现如上功能:

1
2
3
4
{
path: '/dev/components',
asyncComponent: './sys-component/SysComponent',
},

自定义routes-loader实现

webpack 自定义loader请参考:如何开发一个 Webpack Loader

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
function getComponentString(componentPath) {
return "getComponent: (nextState, cb) => {"
+ "startFetchingComponent();"
+ "require.ensure([], (require) => {"
+ "if (!shouldComponentMount(nextState)) return;"
+ "endFetchingComponent();"
+ "cb(null, connectComponent(require('" + componentPath + "')));"
+ "});"
+ "},";
}

module.exports = function (source, other) {
this.cacheable();
var routesStrTemp = source;
var patt = /asyncComponent:[ ]*['"]([^'"]+)['"][,]/gm;
var isRoutes = false;
var block = null;
while ((block = patt.exec(source)) !== null) {
isRoutes = block[0] && block[1];
if (isRoutes) {
routesStrTemp = routesStrTemp.replace(block[0], getComponentString(block[1]));
}
}
if (isRoutes) {
routesStrTemp = "import connectComponent from 'src/utils/connectComponent.js';\n"
+ "import {startFetchingComponent, endFetchingComponent, shouldComponentMount} from 'src/utils/route-utils';"
+ routesStrTemp;
this.callback(null, routesStrTemp, other);
} else {
this.callback(null, source, other);
}
};

webpack 配置

由于这是对原routes.js文件的修改,故routes-loader加在preLoaders中,在loaders执行之前,做好这些转换工作。

1
2
3
4
5
6
7
8
9
10
preLoaders: [
...
{
test: /routes\.js$/,
loader: path.join(__dirname, './routes-loader'), // 这里要使用绝对路径
include: projectRoot,
exclude: /node_modules/
}
...
],

es6 Mixin模式的实现

可以用来实现多继承,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function mix(...mixins) {
class Mix {}

for (let mixin of mixins) {
copyProperties(Mix, mixin);
copyProperties(Mix.prototype, mixin.prototype);
}

return Mix;
}

function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}

上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

1
2
3
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}

参考链接:
http://es6.ruanyifeng.com/#docs/class#Mixin模式的实现

ubuntu开发环境搭建

开发环境搭建:

git

1
sudo apt-get install git

生成ssh key

  • 生成key,执行如下命令,然后一路回车即可

    1
    ssh-keygen -t rsa -C “xxx@163.com”
  • 复制id_rsa.pub中内容到远程服务器

    1
    2
    cd ~/.ssh
    cat id_rsa.pub
  • 进入远程服务器,编辑authorized_keys文件,加入生成的id_rsa.pub

    1
    vi /home/git/.ssh/authorized_keys

安装nodejs

1
2
3
4
sudo apt-get install -y build-essential
curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo ln -s /usr/bin/nodejs /usr/bin/node

升级npm

1
sudo npm update npm -g

安装java8

1
2
3
4
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer -y
java -version

安装maven3

官方下载最新的apache-maven-3.x.y-bin.tar.gz

1
2
3
4
5
6
7
8
解压
sudo tar zxvf apache-maven-3.3.9-bin.tar.gz -C /usr/local/

sudo vi /etc/profile
加入:export PATH=/usr/local/apache-maven-3.3.9/bin:$PATH

注销后:
mvn -v

微信夸号支付问题

场景是这样的,我在A公众号开发了一个web支付页面,订阅号B中访问时,支付就会出现夸号支付错误

解决方案

  1. 捕获支付错误,弹出支付二维码,二维码是后端生成的一个微信支付二维码,内容类似:weixin://wxpay/xxx?xxx=xx
  2. 引导用户长按二维码,识别并支付,
  3. 用户支付成功之后,由于前端无法捕获回调,弹出框提供一个‘已完成支付’之类的按钮,让用户点击,完成支付。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    wx.chooseWXPay({
    ... // 一大堆配置
    success: function (res) {
    ... // 支付成功,跳转之类的逻辑
    },
    fail: function (res) {
    // res.errMsg 为 chooseWXPay: fail 并没有什么卵用
    ... // 支付失败,弹出支付二维码,引导用户长按二维码支付。
    }
    });

参考链接:
http://blog.sina.com.cn/s/blog_13f5b445f0102vsev.html

关于React ref的使用

React应用中,有时需要操作真实得dom节点,比如使用一些第三方,非React插件的时候,需要直接操作dom,React中,可以使用ref获取;

1
2
3
4
5
6
7
8
9
10
11
componentDidMount(){
const myInput = this.refs.myInput;
}

render(){
return (
<div>
<input ref="myInput"/>
</div>
)
}

还可以这样写:

1
2
3
4
5
6
7
8
9
10
11
componentDidMount(){
const myInput = this.myInput;
}

render(){
return (
<div>
<input ref={(view) => this.myInput = view}/>
</div>
)
}

组件嵌套得时候有个坑:父组件只能获取直接子节点的ref

1
2
3
4
5
6
7
8
9
10
11
12
13
componentDidMount(){
// 这里是无法获取到的,由于input得直接父组件是SomeComponent,这时input的ref并未执行
// 可以在SomeComponent 的 componentDidMount中获取到myInput
const myInput = this.myInput;
}

render(){
return (
<SomeComponent>
<input ref={(view) => this.myInput = view}/>
</SomeComponent>
)
}

这个怎么破?组件化,将dom操作封装成一个独立的组件,比如图表(chart),可以把图表单独封装成组件,其他组件调用即可,即可避免组件嵌套ref无法获取的问题,又符合组件化规范,提高复用。

android APP常用图标尺寸规范

程序启动图标

  1. LDPI (Low Density Screen,120 DPI),其图标大小为 36 x 36 px。
  2. MDPI (Medium Density Screen, 160 DPI),其图标大小为 48 x 48 px。
  3. HDPI (High Density Screen, 240 DPI),其图标大小为 72 x 72 px。
  4. xhdpi (Extra-high density screen, 320 DPI),其图标大小为 96 x 96 px。
  5. xxhdpi(xx-high density screen, 480 DPI),其图标大小为144 x 144 px。

底部菜单图标

大屏 (hdpi)

  1. 完整图片(红色): 72 x 72 px
  2. 图标(蓝色): 48 x 48 px
  3. 图标外边框(粉色): 44 x 44 px

中屏 (mdpi)

  1. 完整图片: 48 x 48 px
  2. 图标: 32 x 32 px
  3. 图标外边框: 30 x 30 px

小屏(ldpi)

  1. 完整图片: 36 x 36 px
  2. 图标: 24 x 24 px
  3. 图标外边框: 22 x 22 px

弹出对话框顶部图标

  1. 小屏 24 x 24 px Low density screen (ldpi)
  2. 中屏 32 x 32 px Medium density screen (mdpi)
  3. 大屏 48 x 48 px High density screen (hdpi)

长列表内部列表项图标

  1. 小屏 24 x 24 px Low density screen (ldpi)
  2. 中屏 32 x 32 px Medium density screen (mdpi)
  3. 大屏 48 x 48 px High density screen (hdpi)

底部或顶部tab标签图标

大屏 (hdpi)

  1. 完整图片(红色): 48 x 48 px
  2. 图标(蓝色): 42 x 42 px

中屏 (mdpi)

  1. 完整图片: 32 x 32 px
  2. 图标: 28 x 28 px

小屏(ldpi)

  1. 完整图片: 24 x 24 px
  2. 图标: 22 x 22 px

底部状态栏图标

  1. ldpi (120 dpi) 18 x 18 px 小屏
  2. mdpi (160 dpi) 24 x 24 px 中屏
  3. hdpi (240 dpi) 36 x 36 px 大屏
  4. xhdpi (320 dpi) 48 x 48 px 特大屏

参考链接
http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0120/2331.html

关于vue中自定义事件的理解

事件不外乎两部分内容,监听/触发,下面根据这两部分,简单介绍一下vue中的自定义事件。

监听

$on()

  • 示例提供得方法,可以通过this.$on(event, callback)直接调用
  • 事件可以通过 this.$emit, this.$dispatch 或this.$broadcast触发。
    1
    2
    3
    4
    5
    6
    7
    ...
    ready() {
    this.$on('customer-event', () => {
    ...
    });
    },
    ...

$once()

  • 示例提供得方法,可以通过this.$once(event, callback)直接调用
  • 监听一个自定义事件,但是只触发一次,在第一次触发之后删除监听器。

v-on:event

当子组件触发了 “customer-event” 事件,父组件的 handleIt 方法将被调用。所有影响父组件状态的代码放到父组件的 handleIt 方法中;子组件只关注触发事件。

1
<child v-on:customer-event="handleIt"></child>

events

示例提供的方法,在创建实例时 events 选项简单地调用 $on

1
2
3
4
5
6
events: {
'customer-event'(msg) {
// 事件回调内的 `this` 自动绑定到注册它的实例上
this.messages.push(msg)
}
}

触发

$emit

当前组件内部触发事件,事件不会冒泡到父级,只有当前组件内部才可以捕获到

$dispatch

派发事件,事件沿着父链冒泡;

$broadcast

广播事件,事件向下传导给所有的后代。

参考链接:
http://cn.vuejs.org/api/#实例方法-事件

React Native StyleSheet 封装

React Native 的 app 应该对各个平台做适配,但各个平台的UI风格不同,很多情况下需要通过样式进行区分,不同平台应用不同的样式,下面的函数摘自FaceBook的f8app ,对react-native的StyleSheet做了封装,可以根据不同平台,应用不同的样式

js/common/F8StyleSheet.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
/**
* Copyright 2016 Facebook, Inc.
*
* You are hereby granted a non-exclusive, worldwide, royalty-free license to
* use, copy, modify, and distribute this software in source code or binary
* form for use in connection with the web services and APIs provided by
* Facebook.
*
* As with any software that integrates with the Facebook platform, your use
* of this software is subject to the Facebook Developer Principles and
* Policies [http://developers.facebook.com/policy/]. This copyright notice
* shall be included in all copies or substantial portions of the software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE
*
* @providesModule F8StyleSheet
* @flow
*/

'use strict';

import {StyleSheet, Platform} from 'react-native';

export function create(styles: Object): {[name: string]: number} {
const platformStyles = {};
Object.keys(styles).forEach((name) => {
let {ios, android, ...style} = {...styles[name]};
if (ios && Platform.OS === 'ios') {
style = {...style, ...ios};
}
if (android && Platform.OS === 'android') {
style = {...style, ...android};
}
platformStyles[name] = style;
});
return StyleSheet.create(platformStyles);
}

使用方法:

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
var StyleSheet = require('F8StyleSheet');

......

var styles = StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: 'transparent',
ios: {
paddingBottom: 6,
justifyContent: 'center',
alignItems: 'center',
},
android: {
paddingLeft: 60,
},
},
button: {
borderColor: 'transparent',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
ios: {
height: HEIGHT,
paddingHorizontal: 20,
borderRadius: HEIGHT / 2,
borderWidth: 1,
},
android: {
paddingBottom: 6,
paddingHorizontal: 10,
borderBottomWidth: 3,
marginRight: 10,
},
},
label: {
letterSpacing: 1,
fontSize: 12,
color: 'white',
},
deselectedLabel: {
color: 'rgba(255, 255, 255, 0.7)',
},
});

js构造树状数据结构

菜单,组织结构等场景经常会用到树状结构的数据,树状结构数据可以后端构造,也可以前端构造,如下是js方式构造数据,需要扁平数据结构为

1
2
3
4
5
6
[
{key: 1, parentKey: 0, text: '一级'},
{key: 2, parentKey: 1, text: '二级2'},
{key: 3, parentKey: 1, text: '二级3'},
...
]

key, parentKey, text 函数中会用到,如果后端给出的不是这三个字段,可以适当改造如下方法,或者做映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/**
* 检测某个节点是否有parent节点
* @param rows 所有节点
* @param row 需要判断得节点
* @returns {boolean}
*/
export function hasParent(rows, row) {
let parentKey = row.parentKey;
for (let i = 0; i < rows.length; i++) {
if (rows[i].key === parentKey) return true;
}
return false;
}

/**
* js构造树方法。
* @param rows 具有key,parentKey关系的扁平数据结构,标题字段为text
* @param parentNode 开始节点
* @returns {array}
*/
export function convertToTree(rows, parentNode) {
// 这个函数会被多次调用,对rows做深拷贝,否则会产生副作用。
rows = rows.map((row) => {
return assign({}, row);
});
parentNode = assign({}, parentNode);

let nodes = [];
if (parentNode) {
nodes.push(parentNode);
} else {
// 获取所有的顶级节点
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
if (!hasParent(rows, row.parentKey)) {
nodes.push(row);
}
}
}

// 存放要处理的节点
let toDo = nodes.map((v) => v);

while (toDo.length) {
// 处理一个,头部弹出一个。
let node = toDo.shift();
// 获取子节点。
for (let i = 0; i < rows.length; i++) {
let row = rows[i];
if (row.parentKey === node.key) {
let child = row;
let parentKeys = [node.key];
if (node.parentKeys) {
parentKeys = node.parentKeys.concat(node.key);
}
child.parentKeys = parentKeys;
let parentText = [node.text];
if (node.parentText) {
parentText = node.parentText.concat(node.text);
}
child.parentText = parentText;

if (node.children) {
node.children.push(child);
} else {
node.children = [child];
}
// child加入toDo,继续处理
toDo.push(child);
}
}
}
if (parentNode) {
return nodes[0].children;
}
return nodes;
}

获取滚动条宽度

1
2
3
4
5
6
7
8
9
10
11
12
function scrollBarWidth () {
var scrollDiv = document.createElement('div');
scrollDiv.style.position = 'absolute';
scrollDiv.style.top = '-9999px';
scrollDiv.style.width = '50px';
scrollDiv.style.height = '50px';
scrollDiv.style.overflow = 'scroll';
document.body.appendChild(scrollDiv)
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth
document.body.removeChild(scrollDiv)
return scrollbarWidth
}

HTML5本地存储

分为localStorage和sessionStorage,一个永久有效,一个session级别的,用法完全相同;每个网站5M,用于存储字符串(只能存字符串)。

支持情况

支持情况

检测

1
2
3
4
5
if(window.localStorage){
//code here
}else{
//code here
}

使用

推荐统一使用setItem getItem removeItem

赋值方法

1
2
3
localStorage.a = 3;//设置a为"3"
localStorage["a"] = "sfsf";//设置a为"sfsf",覆盖上面的值
localStorage.setItem("b","isaac");//设置b为"isaac"

取值方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a1 = localStorage["a"];//获取a的值
var a2 = localStorage.a;//获取a的值
var b = localStorage.getItem("b");//获取b的值
```

### 清除方法
```js
localStorage.removeItem("c");//清除c的值
localStorage.clear();//全部清除
```
### 遍历本地存储
这里会把所有的存储都遍历出来,无论是你用的,还是其他程序用的。key最好有个前缀,不容易重复的前缀,这样能区分开数据到底是什么类型
```js
var storage = window.localStorage; //类数组对象
for(var i = 0; i < storage.length; i++){
//key(i)获得相应的键,再用getItem()方法获得对应的值
var key = storage.key(i);
var value = storage.getItem(key);
}

异常处理

异常处理很重要,否则会出现莫名其妙得卡死,还不知道问题出在哪里)
主要是存储已满异常,可以考虑使用LRU(Least Recently Used最近最少使用)

1
2
3
4
5
6
7
8
9
10
try{
localStorage.setItem(key, value);
}catch(oException){
if(oException.name == 'QuotaExceededError'){
console.log('超出本地存储限额!');
//如果历史信息不重要了,可清空后再设置
localStorage.clear();
localStorage.setItem(key,value);
}
}

封装

如下是项目中使用的一个简单封装,storage.js,es6语法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* 本地存储封装,项目中其他地方不要直接使用localStorage和sessionStorage,统一使用封装。
* 简化接口,字符串json转换。
* 如果本地存储使用比较多,可以考虑封装LRU(Least Recently Used最近最少使用)
*/
export default {
global: {
get(key) {
return window.globalStorage ? window.globalStorage[key] : null;
},
set(key, jsonValue) {
window.globalStorage = window.globalStorage ? window.globalStorage : {};
window.globalStorage[key] = jsonValue;
},
remove(key) {
if (window.globalStorage) {
delete window.globalStorage[key];
}
},
removeAll() {
window.globalStorage = {};
},
},
local: {
get(key) {
const strValue = localStorage.getItem(key);
return JSON.parse(strValue);
},
set(key, jsonValue) {
const strValue = JSON.stringify(jsonValue);
localStorage.setItem(key, strValue);
},
remove(key) {
localStorage.removeItem(key);
},
removeAll() {
localStorage.clear();
},
},
session: {
get(key) {
const strValue = sessionStorage.getItem(key);
return JSON.parse(strValue);
},
set(key, jsonValue) {
const strValue = JSON.stringify(jsonValue);
sessionStorage.setItem(key, strValue);
},
remove(key) {
sessionStorage.removeItem(key);
},
removeAll() {
sessionStorage.clear();
},
},
};

// 使用
import Storage from './storage.js';
Storage.session.get(key);
Storage.session.set(key, jsonValue);

参考链接:
http://www.cnblogs.com/xiaowei0705/archive/2011/04/19/2021372.html

关于单页面应用的url处理

单页面应用越来越流行,单页面应用的开发页面跳转都需要前端来处理,配合H5的history API,可以不使用hash方式拼接url,但是带来一个问题,如果用户刷新当前页面,通过history修改过的url会正常的发送请求到后端,如果后端不做处理,就会返回404,用户体验不是很好。

单页面应用url处理方式

一般的单页面应用,都会使用一个前端router统一控制跳转,很少会自己通过history实现逻辑,一般成熟router都会提供hash,和history两种方式修改url。

  • hash方式,地址栏会拼接#xxx,后端不需要特殊配置,刷新前进后退等history相关操作都OK,但是地址栏也比较丑陋,无法使用锚点
  • history方式,通过pushState,replaceState改变地址栏,看起来就跟真实的地址一样,对于有url洁癖的同学来说,perfect!但是刷新需要后端做特殊支持

history方式,后端支持

后端如果发现是get请求,并且没有被处理,都跳转到index。

  • 通过服务器实现
1
2
3
4
5
6
7
8
9
10
11
Apache
In your vhost :

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
1
2
Nginx
rewrite ^(.+)$ /index.html last;
  • 通过代码实现可以在后端路由处做处理,所有未截获的get请求(非ajax请求),都跳转到index

个人比较喜欢代码方式,灵活性比较高,而且不用修改服务器配置

这样处理过,后端就不会存在404,将控制权交给了前端,当通过一个url进入页面时,后端统一跳转到index,前端通过路由匹配是否有对应页面,如果没有,前端跳转404

参考链接:
http://readystate4.com/2012/05/17/nginx-and-apache-rewrite-to-support-html5-pushstate/

shell命令符号& && ; ||作用

符号&

多个命令同时执行。

1
command1&command2&command3

符号&&

顺序执行,只有前面的命令执行成功,后面的命令才会执行。

1
command1&&command2&&command3

符号

书序执行,不管前面得命令是否成功,后面的命令继续执行。

1
command1;command2;command3

符号||

前面命令如果执行成功,不执行后面的命令,失败则执行后面的命令。

1
command1||command2||command3

注:&& 和 || 与程序的逻辑运算含义一致。

常用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
/* 块状元素水平居中 */
.auto{margin-left:auto; margin-right:auto;}
/* 清除浮动*/
.fix{*zoom:1;}
.fix:after{display:table; content:''; clear:both;}
/* 基于display:table-cell的自适应布局 */
.cell{display:table-cell; *display:inline-block; width:2000px; *width:auto;}
/* 双栏自适应cell部分连续英文字符换行 */
.cell2{overflow:hidden; _display:inline-block;}
/* 单行文字溢出虚点显 示*/
.ell{text-overflow:ellipsis; white-space:nowrap; overflow:hidden;}
/* css3过渡动画效果 */
.trans{
-webkit-transition:all .15s;
transition:all .15s;
}
/* 大小不定元素垂直居中 */
.dib_vm{display:inline-block; width:0; height:100%; vertical-align:middle;}
/* 加载中背景图片 - 如果您使用该CSS小库,务必修改此图片地址 */
.loading{background:url(about:blank) no-repeat center;}
/* 无框文本框文本域 */
.bd_none{border:0; outline:none;}
/* 绝对定位隐藏 */
.abs_out{position:absolute; left:-999em; top:-999em;}
.abs_clip{position:absolute; clip:rect(0 0 0 0);}
/* 按钮禁用 */
.disabled{outline:0 none; cursor:default!important; opacity:.4; filer:alpha(opacity=40); -ms-pointer-events:none; pointer-events:none;}
/*inline-block与float等宽列表*/
.inline_box{font-size:1em; letter-spacing:-.25em; font-family:Arial;}
.inline_two, .inline_three, .inline_four, .inline_five, .inline_six, .inline_any{display:inline-block; *display:inline; letter-spacing:0; vertical-align:top; *zoom:1;}
.float_two, .float_three, .float_four, .float_five, .float_six{float:left;}
.inline_two, .float_two{width:50%; *width:49.9%;}
.inline_three, .float_three{width:33.33333%; *width:33.3%;}
.inline_four, .float_four{width:25%; *width:24.9%;}
.inline_five, .float_five{width:20%; *width:19.9%;}
.inline_six, .float_six{width:16.66666%; *width:16.6%;}
.inline_fix{display:inline-block; width:100%; height:0; overflow:hidden;}

参考链接:
http://www.cnblogs.com/lyzg/p/5561001.html

简单按钮实现

按钮一般分为三种状态,普通 鼠标移入 点击,一般状态切换,只是改变背景颜色,字体颜色,鼠标移入通过:hover伪类实现,点击使用:active伪类实现,简单的代码如下:

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
.btn {
display: inline-block;
padding: 8px 10px;
background: blue;
border-radius: 5px;
color: #fff;
border: 0;
box-sizing: border-box;
outline: 0;
cursor: pointer;
text-decoration: none;
transition: .3s;
}
/* hover 要在active 前面*/

.btn:hover {
background: red;
}
.btn:active {
background: yellow;
color: #000;
}
/* loading状态禁用点击等事件,表单按钮直接加loading状态,不用判断是否loading状态,来确定是否发请求,本身loading状态的按钮就变成不可点击了 */
.btn.loading {
pointer-events: none;
}

效果如下:

a按钮

Git使用

官网地址
git的使用还是挺复杂的,由于精力有限,这里只是总结了一些简单的使用,满足日常工作即可,如果不能满足日常工作,查阅相关资料,再做完善

git安装

linux(ubuntu14)下安装

1
2
sudo apt-get install git
查看git版本 git --version

windows下安装

1
2
网上下载一个[git工具](http://rj.baidu.com/soft/detail/30195.html?ald)工具,安装完成即可在windows下使用git
常用的工具为:msysgit

修改配置

克隆下来的git仓库中有个.git的隐藏文件夹,其中的config文件为git的配置文件,编辑这个config文件可以修改配置。
也可以使用命令进行修改配置:

1
2
3
git config --global user.name ...
git config --global user.email ...
git config --global color.ui true :颜色,不设置,默认都是黑色

可视化工具

官网
smartgithg分为三个版本:

  1. 试用版30天
  2. 商业版,需要证书,有技术支持
  3. 个人版,不需要证书,没有技术支持

使用时候选择3 个人版。如果选择的是使用版,30天过期后,把home下的隐藏文件夹.smartgit删掉,重新启动smartgit可以重新选择版本

PyCharm连接Git

  1. File -> Close Project 进入“Welcome to PyCharm ”
  2. 点击VCS Check out from Version Control 选择Git
选项说明
  1. Vcs Repository URL:(远程Git地址)如ssh://git@192.168.1.136/srv/inc-eking-web.git
  2. Parent Directory:拷贝到本地的位置。
  3. DirectoryName:文件名称

pycharm本身自带了一些git的使用工具,不太好用,推荐使用smartgit

注:配置3之前生成证书(后续不需要输入密码)

git 忽略文件方法

在git文件夹(仓库)下,简历一个.gitignore(隐藏文件)即可。还有其他方法,这个方法,可以吧.gitignore文件提交到服务器上,其他用户可以pull,共享
.gitignore 的语法规范如下:

  1. 所有空行或者以注释符号 # 开头的行都会被 Git 忽略;
  2. 可以使用标准的 glob 模式匹配。 匹配模式最后跟反斜杠(/)说明要忽略的是目录。 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

glob 模式匹配:

  1. 星号匹配零个或多个任意字符;
  2. [abc] 匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);
  3. 问号(?)只匹配一个任意字符;
  4. [0-9a-zA-Z] 在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9a-zA-Z] 表示匹配所有 0 到 9 的数字和所有字母);
  5. 转义字符。

注:理论上来说,在要忽略的格式文件后面添加注释是允许的,但经过我的验证,结果发现这样子操作并不能达到预期的效果。

一个 .gitignore 例子。

1
2
3
4
5
6
7
8
9
10
11
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt

分支管理

分支可以协助不同的开发功能进行,比如发布会单拉出一个分支,bug修改会拉出一个分支,特性开发会拉出一个分支,不同的版本也会拉出一个分支,关于分支怎么管理,根据项目需求适当的拉分支。在分支上进行不同的开发,最总合并到一个分支上(一般是master分支),具体怎么管理分支跟Git工作流有关

Git工作流

一个好的工作流方式能很好地管理日常工作,工作流可以理解为分支的checkout与合并?有关工作流相关的资料,网上查阅吧,比较经典的是GitHub工作流?

常用命令

具体的命令使用请参考其他网站

创建git仓库

1
sudo git init --bare repertoryname.git

Git就会创建一个裸仓库(repertoryname),裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。

克隆远程仓库

1
git clone ssh://username@serveraddress:port/repertoryname.git

例如:

1
2
3
git clone  ssh://git@192.168.0.201/srv/inc-eking-web.git    缺省端口号,默认58001
git clone ssh://git@xxx.xxx.xxx.xxx:58011/srv/inc-eking-web.git
git clone ssh://git@www.example.com/srv/inc-eking-web.git

下拉数据

1
git pull origin 分支名

例如:

1
git pull origin master

提交数据

1
2
3
git add --all
git commit -m "My commit"
git push -u origin master

开分支

1
git branch 新分支名

例如,在master分支下,新开一个开发分支:

1
git branch dev

切换到新的分支

1
git checkout 分支名

例如,在master分支下,切换到新开的dev:

1
git checkout dev

开分支和切换分支合并到一个命令

1
git checkout -b 新分支名

例如,新开一个开发分支,并立即切换到该分支:

1
git checkout -b dev

切换到原分支

1
git checkout 原分支名

例如,切换回master:

1
git checkout master

注意:当前分支有修改,还未commit的时候,会切换失败,应当先commit,但可以不用push

合并分支

1
git merge 需要合并的分支名

例如,刚刚已经切换回master,现在需要合并dev的内容:

1
git merge dev

建议在GitLab(或者其他git系统)上面创建merge request的形式来进行分支的合并和代码审核。

查看本地分支列表

1
git branch -a

前面带remotes/origin 的,是远程分支

查看远程分支列表

1
git branch -r

向远程提交本地新开的分支

1
git push origin 新分支名

例如,刚刚在master下新开的dev分支:

1
git push origin dev

删除远程分支

1
git push origin :远程分支名

例如,删除刚刚提交到远程的dev分支:

1
git push origin :dev

删除本地分支

1
git branch 分支名称 -d

例如,在master分支下,删除新开的dev分支:

1
git branch dev -d

注意:如果dev的更改,push到远程,在GitLab(或者其他git系统)上面进行了merge操作,但是本地master没有pull最新的代码,会删除不成功,可以先git pull origin master,或者强制删除:git branch dev -D

更新分支列表信息

1
git fetch -p

git远程仓库搭建

可以使用git代码托管服务器,git的代码托管服务很多,名气最大的莫过于Github,其他还有GitLab、Bitbucket、CSDN-CODE、Git@OSC等等,如果代码比较重要,可以自己搭建git服务器,假设你已经有sudo权限的用户账号,下面,正式开始安装。

第一步,安装git:

1
sudo apt-get install git

第二步,创建一个git用户,用来运行git服务:

1
sudo adduser git

第三步,创建证书:

证书的作用:一些软件(比如git)要请求远程机器上的数据,可以使用证书,就不用每次请求数据时都输入远程机器的用户名和密码了.

  1. 终端根目录中输入:ssh-keygen -t rsa -C “wangshubin@eking.mobi“ 然后一路回车
  2. 进入.ssh目录 cd ~/.ssh (~是根目录)
  3. cat id_rsa.pub
  4. 远程连接其他机器:ssh eking@xxx.xxx.xxx.xxx 或者 sshpass -p ‘xxxxx’ ssh admin@xxx.xxx.xxx.xxx (可以将此条命令建成sh文件,如果有很多远程机器需要维护,这样可以方便链接)
  5. 进入远程机器的.ssh目录 cd /home/git/.ssh
  6. vi编辑authorized_keys文件(一般.ssh目录下就这一个文件) 将3步中生成的key添加到authorized_keys文件中,一行一个

注:cd /home/git/.ssh(git指的是用户名,每个用户下会有一个.ssh文件夹,这个文件夹只有经过相应的ssh操作才会存在,不存在可以手动添加一个,authorized_keys文件如果不存在可以添加一个.)

第四步,初始化Git仓库:

先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:

1
sudo git init --bare sample.git

Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹是为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常都以.git结尾。

然后,把owner改为git,让这个目录可以通过git用户访问:

1
sudo chown -R git:git sample.git

第五步,禁用shell登录:

出于安全考虑,第二步创建的git用户不允许登录shell,这可以通过编辑/etc/passwd文件完成。找到类似下面的一行:

1
git:x:1001:1001:,,,:/home/git:/bin/bash

改为:

1
git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

这样,git用户可以正常通过ssh使用git,但无法登录shell,因为我们为git用户指定的git-shell每次一登录就自动退出。

第六步,克隆远程仓库:

现在,可以通过git clone命令克隆远程仓库了,在各自的电脑上运行:

1
git clone git@server:/srv/sample.git

显示如下信息表明成功(前提这个库是空的):

1
2
Cloning into 'sample'...
warning: You appear to have cloned an empty repository.

其他

  1. 要方便管理公钥,用Gitosis;
  2. 要像SVN那样变态地控制权限,用Gitolite。

web弹框插件设计

弹框一般项目都会用到,项目中的所有弹框要做到统一,需要对弹框组件做单独的整理维护

一般弹框分为:alert、confirm、promp、toast这么几种

设计一个通用的modal,其他(任何弹框、滑出形式组件)的组件基于modal实现,统一底层的实现,便于后期统一维护和修改,统一控制,这一点很重要。其实是使用了外观模式。根据参数个数判断参数的作用,实现类似重载功能,下面是简单的实现原理:
具体参考framework7源码modals.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
app.modal = function(options){
……
}
alert = function (text, title, callbackOk) {
if (typeof title === 'function') {
callbackOk = arguments[1];
title = undefined;
}
return app.modal(options);
};
confirm = function (text, title, callbackOk, callbackCancel) {
if (typeof title === 'function') {
callbackCancel = arguments[2];
callbackOk = arguments[1];
title = undefined;
}
return app.modal(options);
};
prompt = function (text, title, callbackOk, callbackCancel,callbackOpened) {
if (typeof title === 'function') {
callbackCancel = arguments[2];
callbackOk = arguments[1];
callbackOpened = arguments[3];
title = undefined;
}
return app.modal(options);
};

如下是基于F7改造的一个jQuery插件(样式使用的还是F7的样式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/*========================================================
* 弹出框
* =======================================================*/
// TODO F7有些方法没实现,暂时不需要
;
(function ($, window, document, undefined) {
var modalStack = [];
var _modalTemplateTempDiv = document.createElement('div');

function modal(params) {
params = params || {};
var modalHTML = '';
var buttonsHTML = '';
if (params.buttons && params.buttons.length > 0) {
for (var i = 0; i < params.buttons.length; i++) {
buttonsHTML += '<span class="modal-button' + (params.buttons[i].bold ? ' modal-button-bold' : '') + '">' + params.buttons[i].text + '</span>';
}
}
var titleHTML = params.title ? '<div class="modal-title">' + params.title + '</div>' : '';
var textHTML = params.text ? '<div class="modal-text">' + params.text + '</div>' : '';
var afterTextHTML = params.afterText ? params.afterText : '';
var noButtons = !params.buttons || params.buttons.length === 0 ? 'modal-no-buttons' : '';
var verticalButtons = params.verticalButtons ? 'modal-buttons-vertical' : '';
modalHTML = '<div class="modal ' + noButtons + ' ' + (params.cssClass || '') + '"><div class="modal-inner">' + (titleHTML + textHTML + afterTextHTML) + '</div><div class="modal-buttons ' + verticalButtons + '">' + buttonsHTML + '</div></div>';


_modalTemplateTempDiv.innerHTML = modalHTML;

var modal = $(_modalTemplateTempDiv).children();

$('body').append(modal[0]);

// Add events on buttons
modal.find('.modal-button').each(function (index, el) {
$(el).on('click', function (e) {
if (params.buttons[index].close !== false) closeModal(modal);
if (params.buttons[index].onClick) params.buttons[index].onClick(modal, e);
if (params.onClick) params.onClick(modal, index);
});
});
openModal(modal);
return modal;
}

function openModal(modal) {
modal = $(modal);
var isModal = modal.hasClass('modal');
if ($('.modal.modal-in:not(.modal-out)').length && isModal) {
modalStack.push(function () {
openModal(modal);
});
return;
}
// do nothing if this modal already shown
if (true === modal.data('f7-modal-shown')) {
return;
}
modal.data('f7-modal-shown', true);
modal.trigger('close', function () {
modal.removeData('f7-modal-shown');
});
if (isModal) {
modal.show();
modal.css({
marginTop: -Math.round(modal.outerHeight() / 2) + 'px'
});
}

if ($('.modal-overlay').length === 0) {
$('body').append('<div class="modal-overlay"></div>');
}
var overlay = $('.modal-overlay');
//Make sure that styles are applied, trigger relayout;
var clientLeft = modal[0].clientLeft;//这个不能删,删了actions动画没了.
// Trugger open event
modal.trigger('open');
// Classes for transition in
overlay.addClass('modal-overlay-visible');
modal.removeClass('modal-out').addClass('modal-in').transitionEnd(function (e) {
if (modal.hasClass('modal-out')) modal.trigger('closed');
else modal.trigger('opened');
});
return true;
}

function closeModal(modal) {
modal = $(modal || '.modal-in');
if (typeof modal !== 'undefined' && modal.length === 0) {
return;
}
var isModal = modal.hasClass('modal');
var overlay = $('.modal-overlay');
if (overlay && overlay.length > 0) {
overlay.removeClass('modal-overlay-visible');
}
modal.trigger('close');
modal.removeClass('modal-in').addClass('modal-out').transitionEnd(function (e) {
if (modal.hasClass('modal-out')) modal.trigger('closed');
else modal.trigger('opened');
modal.remove();
});
if (isModal) {
modalStackClearQueue();
}
return true;
}

function modalStackClearQueue() {
if (modalStack.length) {
(modalStack.shift())();
}
}

var modalTitle = '提示';
var modalButtonOk = '确定';
var modalButtonCancel = '取消';
var modalPreloaderTitle = '加载中';
$.extend({
alert: function (text, title, callbackOk) {
if (typeof title === 'function') {
callbackOk = arguments[1];
title = undefined;
}
return modal({
text: text || '',
title: typeof title === 'undefined' ? modalTitle : title,
buttons: [
{text: modalButtonOk, bold: true, onClick: callbackOk}
]
});
},
confirm: function (text, title, callbackOk, callbackCancel) {
if (typeof title === 'function') {
callbackCancel = arguments[2];
callbackOk = arguments[1];
title = undefined;
}
return modal({
text: text || '',
title: typeof title === 'undefined' ? modalTitle : title,
buttons: [
{text: modalButtonCancel, onClick: callbackCancel},
{text: modalButtonOk, bold: true, onClick: callbackOk}
]
});
},
showPreloader: function (title) {
return modal({
title: title || modalPreloaderTitle,
text: '<div class="preloader"></div>',
cssClass: 'modal-preloader'
});
},
hidePreloader: function () {
closeModal('.modal.modal-in');
},
showIndicator: function () {
//$('body').append('<div class="preloader-indicator-overlay"></div><div class="preloader-indicator-modal"><span class="preloader preloader-white"></span></div>');
//去掉全屏透明遮盖层
$('body').append('<div class="preloader-indicator-modal"><span class="preloader preloader-white"></span></div>');
},
hideIndicator: function () {
$('.preloader-indicator-overlay, .preloader-indicator-modal').remove();
},
toast: function (text, closeCallBack) {
var m = modal({
title: '',
text: text
});
if (closeCallBack) {
m.on("close", closeCallBack);
}
setTimeout(function () {
closeModal();
}, 1500);
return modal
},
actions: function (params) {
var modal, groupSelector, buttonSelector;

params = params || [];

if (params.length > 0 && !$.isArray(params[0])) {
params = [params];
}
var modalHTML;

var buttonsHTML = '';
for (var i = 0; i < params.length; i++) {
for (var j = 0; j < params[i].length; j++) {
if (j === 0) buttonsHTML += '<div class="actions-modal-group">';
var button = params[i][j];
var buttonClass = button.label ? 'actions-modal-label' : 'actions-modal-button';
if (button.bold) buttonClass += ' actions-modal-button-bold';
if (button.color) buttonClass += ' color-' + button.color;
if (button.bg) buttonClass += ' bg-' + button.bg;
if (button.disabled) buttonClass += ' disabled';
buttonsHTML += '<div class="' + buttonClass + '">' + button.text + '</div>';
if (j === params[i].length - 1) buttonsHTML += '</div>';
}
}
modalHTML = '<div class="actions-modal">' + buttonsHTML + '</div>';
_modalTemplateTempDiv.innerHTML = modalHTML;
modal = $(_modalTemplateTempDiv).children();
$('body').append(modal[0]);
groupSelector = '.actions-modal-group';
buttonSelector = '.actions-modal-button';

var groups = modal.find(groupSelector);
groups.each(function (index, el) {
var groupIndex = index;
$(el).children().each(function (index, el) {
var buttonIndex = index;
var buttonParams = params[groupIndex][buttonIndex];
var clickTarget;
if ($(el).is(buttonSelector)) clickTarget = $(el);
if ($(el).find(buttonSelector).length > 0) clickTarget = $(el).find(buttonSelector);

if (clickTarget) {
clickTarget.on('click', function (e) {
if (buttonParams.close !== false) closeModal(modal);
if (buttonParams.onClick) buttonParams.onClick(modal, e);
});
}
});
});
openModal(modal);
return modal;
},
closeModal: closeModal
});
})(jQuery, window, document);

使用IntelliJ系列IDE,导致webpack-dev-server监听文件失效问题

在使用 IntelliJ系列IDE(比如WebStorm idea等),开始webpack-dev-server,修改文件并保存,webpack-dev-server并没有反应,由于IntelliJ系列IDE会吧文件修改存在一个临时文件中,并不会改变原始文件,而webpack-dev-server监听的又是原始文件,so…

解决办法:just change a setting

如下图将 Use “safe write” (save changes to a temporary file first) 去掉勾选。

save-write

React 周期函数

对于React相关开发,理解React提供得几个周期函数尤为重要,这里简单总结一下,更多内容移步参考链接

React生命周期图

React生命周期图

周期函数说明

生命周期函数 被调用次数 能否使用setState() 说明
componentWillMount 1 首次渲染前的一些初始化操作,如果涉及到渲染之后的DOM操作,在componentDidMount进行。
render >=1 由于调用比较频繁,尽量少写逻辑,除了获取state,props属性之外的一些变量声明不要在这里做,大块的业务逻辑封装成方法,提出render,保持render清晰简洁。
componentDidMount 1 尽量不要在此方法中使用setState,初始化setState操作请在componentWillMount做。
componentWillReceiveProps(object nextProps) >=0 提供了接受新的props之后操作state得机会
shouldComponentUpdate(object nextProps, object nextState) >=0 返回false render不会执行,可以通过此方法优化性能。
componentWillUpdate(object nextProps, object nextState) >=0
componentDidUpdate(object prevProps, object prevState) >=0
componentWillUnmount 1

参考链接:
http://www.open-open.com/lib/view/open1446022707992.html

http://reactjs.cn/react/docs/component-specs.html

node 项目中的模块

项目中使用到的一些modules的介绍

ejs-mate

模板语言

oneapm

oneapm 是个用来监控网站性能的服务,性能监控解决方案

colors

get colors in your node.js console.

两种使用方式:

扩展String.prototype

1
2
3
4
5
6
7
require('colors');

console.log('hello'.green); // outputs green text
console.log('i like cake and pies'.underline.red) // outputs red underlined text
console.log('inverse the color'.inverse); // inverses the color
console.log('OMG Rainbows!'.rainbow); // rainbow
console.log('Run the trap'.trap); // Drops the bass

不扩展String.prototype

1
2
3
4
5
6
7
var colors = require('colors/safe');

console.log(colors.green('hello')); // outputs green text
console.log(colors.red.underline('i like cake and pies')) // outputs red underlined text
console.log(colors.inverse('inverse the color')); // inverses the color
console.log(colors.rainbow('OMG Rainbows!')); // rainbow
console.log(colors.trap('Run the trap')); // Drops the bass

loader

Node静态资源加载器。该模块通过两个步骤配合完成,代码部分根据环境生成标签。上线时,需要调用minify方法进行静态资源的合并和压缩。

express

基于 Node.js 平台,快速、开放、极简的 web 开发框架。

express-session

Simple session middleware for Express

passport

兼容express的身份认证中间件

loadash

javascript 的一个工具库,类似underscore的一个东西

csurf

Node.js CSRF protection middleware. Cross-site request forgery跨站请求伪造

compression

nodejs 压缩中间件,http请求返回的资源进行压缩?

body-parser

Node.js body parsing middleware.

connect-busboy

Connect middleware for busboy

errorhandler

helmet

Helmet是一系列帮助增强Node.JS之Express/Connect等Javascript Web应用安全的中间件。

一些著名的对Web攻击有XSS跨站脚本, 脚本注入 clickjacking 以及各种非安全的请求等对Node.js的Web应用构成各种威胁,使用Helmet能帮助你的应用避免这些攻击。

url

提取url的各种信息

response-time

This module creates a middleware that records the response time for requests in HTTP servers. The “response time” is defined here as the elapsed time from when a request enters this middleware to when the headers are written out to the client.

method-override

Lets you use HTTP verbs such as PUT or DELETE in places where the client doesn’t support it.

cookie parsing with signatures

MongoDB-从懵逼到入门

安装

根据官网进行安装。

ubuntu 15 安装完成之后无法使用解决办法

1
2
sudo apt-get install upstart-sysv
重启之后,就可以使用了。

启动&停止

官网上有相关文档,一切以官网为主

终端方式

1
2
3
4
# 启动
mongod
# 停止
关闭当前终端即可

后台运行方式

1
2
3
4
5
6
7
# 启动 添加--fork参数 --logpath:指定日志文件 --logappend:日志以追加方式记录
mongod --fork --logpath /var/log/mongodb.log --logappend

# 停止 通过admin数据库命令停止
mongo
use admin
db.shutdownServer()

卸载

http://askubuntu.com/questions/147135/how-can-i-uninstall-mongodb-and-reinstall-the-latest-version

1
2
3
sudo apt-get purge mongodb mongodb-clients mongodb-server mongodb-dev
sudo apt-get purge mongodb-10gen
sudo apt-get autoremove

常用shell命令

  • 查看当前数据库:db
  • 查看有那些数据库:show dbs
  • 切换数据库:use db_name
  • 查看集合:show collections
  • 查询:db.collection_name.find()
  • 插入:db.collection_name.insert(obj)
  • 更新:db.collection_name.update(condition,newObj)

导入导出

更多参考这里

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
导出数据库:
mongodump -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -o 保存导出文件路径

如果没有用户密码,可以去掉-u和-p。
如果导出本机的数据库,可以去掉-h。
如果是默认端口,可以去掉--port。
如果想导出所有数据库,可以去掉-d。

导入数据库:
mongorestore -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 --drop 文件存在路径

--drop的意思是,先删除所有的记录,然后恢复

导出表:
mongoexport -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -c 表名 -f 字段 -q 条件导出 --csv -o 文件名

-f 导出指字段,以字号分割,-f name,email,age导出name,email,age这三个字段
-q 可以根查询条件导出,-q '{ "uid" : "100" }' 导出uid为100的数据
--csv 表示导出的文件格式为csv的,这个比较有用,因为大部分的关系型数据库都是支持csv,在这里有共同点

导入表:
1.1,还原整表导出的非csv文件
mongoimport -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -c 表名 --upsert --drop 文件名
重点说一下--upsert,其他参数上面的命令已有提到,--upsert 插入或者更新现有数据

1.2,还原部分字段的导出文件
mongoimport -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -c 表名 --upsertFields 字段 --drop 文件名
--upsertFields根--upsert一样

1.3,还原导出的csv文件
mongoimport -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -c 表名 --type 类型 --headerline --upsert --drop 文件名
上面三种情况,还可以有其他排列组合的。

使用序列生成id

注:

  1. 分表分库,使用自增id的方式,会出现id重复的情况
  2. 要保证id的任何情况下的唯一性,最好不用数据库自增id
  3. 自增id有规律,容易被爬虫,(但是有些情况欢迎被爬,但是用户之类的信息就要避免被爬了。)
  4. 不局限于user_id,任何需要保护的id都可以使用
  5. 使用统一的一个表,维护各个表数据的id,保证id唯一的同时,可以对id进行混淆。

表设计:sequences

1
2
3
4
5
6
7
CREATE TABLE `sequences` (
`name` varchar(50) NOT NULL COMMENT '名称,可以等同于表名',
`name_space` varchar(50) NOT NULL COMMENT '名称空间',
`step` int(11) NOT NULL DEFAULT '1' COMMENT '步进长度',
`next_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '下一个id',
PRIMARY KEY (`name`,`name_space`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

算法(python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from collections import deque
import string
import random
from sqlalchemy import exc
from app.commons import stringutil
from configs.database_builder import DatabaseBuilder

class DatabaseSequenceFactory(object):
""" 所有Dao对象需要集成DatabaseTemplate
"""
sequences = dict() # 缓存系统中所有的id

def __init__(self, database_factory=None, alphabet=None):
self._default_db = DatabaseBuilder.get_default_db_instance() if database_factory is None else database_factory
self.alphabet = "wWBLCMPZbH2GFszTogYu93JNDfIlVmneRx6tkAvKU51r8XOidpchEjQqS7a40y" if alphabet is None else alphabet

def get_id(self, name_space, name): # 默认为当前项目的name_space 具体项目要修改这个默认值
sequence_key = "%r:%r" % (name_space, name)
sequence = self.sequences.get(sequence_key)
if sequence is None or len(sequence) == 0:
# id消费完了,重新获取一组
start_id, step = self._get_start_id_step(name_space, name)
sequence = deque(range(start_id, start_id+step))
self.sequences[sequence_key] = sequence

return sequence.popleft()

def get_obfuscated_id(self, name_space, name, alphabet=None, has_random=False, random_length=1, is_prefix=True):
result = stringutil.base62_encode(self.get_id(name_space, name),
alphabet if alphabet is not None else self.alphabet)
if has_random:
random_str = ''.join(random.sample(string.ascii_letters+string.digits, random_length))
result = random_str+'_'+result if is_prefix else result+'_'+random_str
return result

def _get_start_id_step(self, name_space, name):
"""

:param name_space:
:param name:
:return: tupple (start_id, step)
"""

with self._default_db.create_connection() as connection:
update_sql = "update sequences set next_id = last_insert_id(next_id+step) where name=%s and name_space=%s"
select_sql = "select last_insert_id()-step as start_id, step from sequences " +\
"where name=%s and name_space=%s"
result = connection.execute(update_sql, name, name_space)
if result.rowcount == 0:
raise exc.ArgumentError("sequence is not exist namespace:%r name:%r" % (name_space, name))
entity = connection.execute(select_sql, name, name_space).first()
return entity[0], entity[1]


if __name__ == "__main__":
DatabaseBuilder.run_mode = 'test'
for i in range(0, 100000):

print DatabaseSequenceFactory().get_id('aiwanr','hehe')
# print DatabaseSequenceFactory().get_obfuscated_id('aiwanr','users')
# print DatabaseSequenceFactory().get_obfuscated_id('aiwanr','users', has_random=True)
# print DatabaseSequenceFactory().get_obfuscated_id('aiwanr','users', has_random=True, random_length=2, is_prefix=False)

人员管理数据库设计

用户管理

视情况而定,三张表或四张表:

  • users 用户基本信息表,用于存放用户id,姓名、性别、头像等常用信息
  • accounts 用户账号表,users:accounts 1:n,用于存储用户账号、登录时间、登录ip等信息(多类型账号支持)
  • passwords 账号对应的密码 accounts:passwords n:1,多个账号都可以登录使用同一个密码。
  • profiles 用户其他信息,users:profiles 1:1,用于存放一些不常用的信息,比如邮箱,工作,职位等等。

为什么1:1的两个表,不直接合并成一个表?常用信息使用一个表,不常用信息使用一个表,可以提高查询速度,信息如果区分的好,单表的查询要远远多于两个表的关联查询,比如users表和profiles表,“用户名”这样的信息会频繁用到,而“单位”,则不是很常用。需要同时显示的时候,做一下关联查询即可。

用户敏感信息处理:

  1. 密码加密:用户密码进行加密处理
  2. 密码添加salt(一个随机字符串,每个用户有个唯一的salt),确保即使密码明文相同,加密过后的密码也不相同
  3. user_id,通过一个序列算法生成(详见《使用序列生成id》),防止分表分库user_id出现重复,防止恶意爬虫,获取有规律的user_id

一个python中的加密算法:

1
2
3
4
5
6
7
8
def _encrypted_password(password, salt):
"""
* 密码加密
* @param string password 密码
* @param string salt 混淆码
* @return string 加密后的密码
"""
return hashlib.md5(hashlib.md5(password).hexdigest() + salt).hexdigest()

users、accounts、passwords表创建语句:

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
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` varchar(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '用户id',
`name` varchar(50) NOT NULL COMMENT '姓名/昵称 用户姓名和昵称',
`avatar` varchar(200) NOT NULL,
`gender` char(1) NOT NULL DEFAULT 'N' COMMENT '性别: N-unknown M-man F-female',
`is_locked` tinyint(4) NOT NULL DEFAULT '0',
`created_at` bigint(20) NOT NULL,
`updated_at` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
KEY `index_user_id` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=111 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `accounts` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`user_id` varchar(36) CHARACTER SET ascii COLLATE ascii_bin NOT NULL DEFAULT '0',
`account_type` varchar(10) NOT NULL,
`name` varchar(80) NOT NULL,
`last_visit_ip` varchar(50) NOT NULL,
`last_login_ip` varchar(50) NOT NULL,
`fail_count` int(255) NOT NULL,
`locked_at` bigint(20) NOT NULL,
`created_at` bigint(20) NOT NULL,
`updated_at` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_accounts_actAc` (`account_type`,`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=160 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `passwords` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`account_id` bigint(20) unsigned NOT NULL,
`hashed_password` varchar(32) NOT NULL,
`salt` varchar(5) NOT NULL,
`created_at` bigint(20) NOT NULL,
`updated_at` bigint(20) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_passwords_aid` (`account_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=162 DEFAULT CHARSET=utf8mb4;

CSS Secrets阅读笔记

透明边框

1
2
3
4
5
div{
border: 10px solid hsla(0,0%,100%,.5);
background: white;
background-clip: padding-box;
}

主要属性:

  • 边框颜色使用hsla或者rgba
  • background-clip设置为padding-box;即背景为padding+content

background-clip三个值:

  • border-box 背景被裁剪到边框盒。
  • padding-box 背景被裁剪到内边距框。
  • content-box 背景被裁剪到内容框。

效果如下:



官方示例:http://play.csssecrets.io/translucent-borders

多重边框

box-shadow 方案

box-shadow语法:

box-shadow: h-shadow v-shadow blur spread color inset;

描述
h-shadow 必需。水平阴影的位置。允许负值。
v-shadow 必需。垂直阴影的位置。允许负值。
blur 可选。模糊距离。
spread 可选。阴影的尺寸。
color 可选。阴影的颜色。请参阅 CSS 颜色值。
inset 可选。将外部阴影 (outset) 改为内部阴影。
1
2
3
4
div{
background: yellowgreen;
box-shadow: 0 0 0 10px #655;
}

效果如下:



两个边框:

1
2
3
4
div{
background: yellowgreen;
box-shadow: 0 0 0 10px #655, 0 0 0 15px deeppink;
}


三个边框:

1
2
3
4
5
6
div{
background: yellowgreen;
box-shadow: 0 0 0 10px #655,
0 0 0 15px deeppink,
0 0 0 20px #632;
}



几点说明:

  • shadows会忽略box-sizing属性,阴影是加在盒子之外的,可以使用 box-shadow 的 inset 属性解决这个问题。
  • shadows会忽略hover click等鼠标事件,同样可以使用inset属性解决,如果有内容,可以使用padding属性,把shadows的空间留出来。
  • 不支持dashed边框

官方示例:http://play.csssecrets.io/multiple-borders

outline 方案

1
2
3
4
5
div{
background: yellowgreen;
border: 10px solid #655;
outline: 15px solid deeppink;
}

效果如下:



背景位置右下角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
方案一
div {
background: url(http://csssecrets.io/images/code-pirate.svg)
no-repeat bottom right #58a;
background-position: right 20px bottom 10px;
}
方案二
div {
background: url(http://csssecrets.io/images/code-pirate.svg)
no-repeat bottom right #58a;
background-origin: content-box;
}
方案三
div {
background: url(http://csssecrets.io/images/code-pirate.svg)
no-repeat bottom right #58a;
background-position: calc(100% - 20px) calc(100% - 10px);
}

内圆角

1
2
3
4
div {
outline: .6em solid #655;
box-shadow: 0 0 0 .4em #655; /* todo calculate max of this */
}

条纹背景

1
2
3
4
5
6
div{
background: #58a;
background-image: repeating-linear-gradient(30deg,
hsla(0,0%,100%,.1), hsla(0,0%,100%,.1) 15px,
transparent 0, transparent 30px);
}

随机条纹背景

1
2
3
4
5
6
7
8
div{
background: hsl(20, 40%, 90%);
background-image:
linear-gradient(90deg, #fb3 11px, transparent 0),
linear-gradient(90deg, #ab4 23px, transparent 0),
linear-gradient(90deg, #655 23px, transparent 0);
background-size: 83px 100%, 61px 100%, 41px 100%;
}

各种背景:http://lea.verou.me/css3patterns/ http://bennettfeely.com/gradients/

各种按钮:http://simurai.com/archive/buttons/

容器倾斜文字不倾斜

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
方案一
.button { transform: skewX(45deg); }
.button > div { transform: skewX(-45deg); }

.button {
display: inline-block;
padding: .5em 1em;
border: 0; margin: .5em;
background: #58a;
color: white;
text-transform: uppercase;
text-decoration: none;
font: bold 200%/1 sans-serif;
}
方案二
.button {
position: relative;
display: inline-block;
padding: .5em 1em;
border: 0; margin: .5em;
background: transparent;
color: white;
text-transform: uppercase;
text-decoration: none;
font: bold 200%/1 sans-serif;
}

.button::before {
content: ''; /* To generate the box */
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
z-index: -1;
background: #58a;
transform: skew(45deg);
}

菱形图片

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
方案一
.diamond {
width: 250px;
height: 250px;
transform: rotate(45deg);
overflow: hidden;
margin: 100px;
}

.diamond img {
max-width: 100%;
transform: rotate(-45deg) scale(1.42);
z-index: -1;
position: relative;
}
方案二
img {
max-width: 250px;
margin: 20px;
-webkit-clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
transition: 1s;
}

img:hover {
-webkit-clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

单边阴影

1
2
3
4
5
6
div {
width: 1.6in;
height: 1in;
background: #fb3;
box-shadow: 0 5px 4px -4px black;
}

绝对定位方式水平垂直居中

两个div,.parent 和 .child,.child 要相对于.parent水平垂直居中。

第一种方式:

1
2
3
4
5
6
7
8
9
10
11
.parent{
display:relative;
}
.child{
width:200px;
height:300px;
top:50%;
margin-top:-100px;
left:50%;
margin-left:-150px;
}

这种方式兼容性比较好,但是要指定.child得宽高,而且宽高修改之后要修改margin-top或margin-left。

第二种方式:

1
2
3
4
5
6
7
8
9
.parent{
position:relative;
}
.child{
position:absolute;
top:50%;
left:50%;
transform:translate(-50%, -50%);
}

这种方式ie9以上可用,不需要制定宽高,或者宽高随意指定,不用修改其他属性。

js数组操作

数组基本方法:

concat()

用于链接2个或多个数组,不改变现有数组,返回一个数组。

语法:arr.concat(arr1, arr2, arr3, …arrN);

1
2
3
4
5
6
7
8
var a1 = [1, 2, 3];
var a2 = [4, 5, 6];
var a3 = [7, 8, 9];

console.log(a1.concat(a2, a3)); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
console.log(a1); // [1, 2, 3]
console.log(a2); // [4, 5, 6]
console.log(a3); // [7, 8, 9]

join()

将数组的所有元素拼接成一个字符串,不改变现有数组,返回一个字符串。

语法:arr.join([separator]);

separator: 指定链接各个元素的符号,默认为英文逗号。

1
2
3
4
var arr = ['a', 'b', 'c'];

console.log(arr.join()); // a,b,c
console.log(arr.join('-')); // a-b-c

push()

数组尾部添加1个或者多个元素,改变原有数组,返回改变之后的数组长度。

语法:arr.push(elem1, elem2, …elemN)

如果参数为空,则直接返回数组长度,不对数组有任何操作

1
2
3
4
5
6
7
8
9
var arr = ['a', 'b', 'c'];

console.log(arr.push()); // 3
console.log(arr); // ["a", "b", "c"]
console.log(arr.push('d')); // 4
console.log(arr); // ["a", "b", "c", "d"]

console.log(arr.push('e', 'f')); // 6
console.log(arr); // ["a", "b", "c", "d", "e", "f"]

pop()

数组尾部删除1个元素,改变原有数组,返回被删除元素,如果数组为空,返回undefined。

语法:arr.pop()

1
2
3
4
5
6
7
8
var arr = ['a', 'b', 'c'];

console.log(arr.pop()); // c
console.log(arr); // ["a", "b"]

var arr2 = [];
console.log(arr2.pop()); // undefined
console.log(arr2); // []

reverse()

颠倒数组中元素得顺序,改变原有数组,返回颠倒完成之后的数组

语法:arr.reverse()

1
2
3
4
5
6
7
var arr = ['a', 'b', 'c'];

var arr2 = arr.reverse();

console.log(arr2); // ["c", "b", "a"]

console.log(arr); // ["c", "b", "a"]

unshift()

数组头部添加1个或多个元素,改变原有数组,返回改变之后的数组长度。

语法:arr.unshift(elem1, elem2, …elemN)

如果参数为空,则直接返回数组长度,不对数组有任何操作

1
2
3
4
5
6
7
8
9
var arr = ['a', 'b', 'c'];

console.log(arr.unshift()); // 3
console.log(arr); // ["a", "b", "c"]
console.log(arr.unshift('d')); // 4
console.log(arr); // ["d", "a", "b", "c"]

console.log(arr.unshift('e', 'f')); // 6
console.log(arr); // ["e", "f", "d", "a", "b", "c"]

注:unshift和push两个方法非常类似,只不过一个再头部添加,一个在尾部添加。

shift()

数组头部删除1个元素,改变原有数组,返回被删除元素,如果数组为空,返回undefined。

语法:arr.shift()

1
2
3
4
5
6
7
8
var arr = ['a', 'b', 'c'];

console.log(arr.shift()); // a
console.log(arr); // ["b", "c"]

var arr2 = [];
console.log(arr2.shift()); // undefined
console.log(arr2); // []

sort()

对数组的元素进行排序,改变原有数组,返回排序之后的数组。

语法:arr.sort([sortby])

如果参数为空,默认按字母得字符编码顺序排序。

sortby为接受两个参数的函数,返回用于说明这两个参数顺序得数字,比如参数为a,b:

a小于b,即排序之后a在b之前,sortby函数需要返回一个小于0的值;

a等于b,sortby函数需要返回一个等于0的值;

a大于b,即排序之后b在a之前,sortby函数需要返回一个大于0的值;

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
var arr = ['just', 'do', 'it', '!'];

console.log(arr.sort()); // ["!", "do", "it", "just"]
console.log(arr); // ["!", "do", "it", "just"]

var arr2 = [12, 28, 1, 99, 23, 27];

console.log(arr2.sort()); // [1, 12, 23, 27, 28, 99]

console.log(arr2.sort(function(a,b){
return a-b;
})); // [1, 12, 23, 27, 28, 99]

console.log(arr2.sort(function(a,b){
return b-a;
})); // [99, 28, 27, 23, 12, 1]

var arr3 = [
{
name: '张三',
age: 22
},
{
name: '李四',
age: 15
},
{
name: '王五',
age: 27
}
];

var arr4 = arr3.sort(function(a, b){
return a.age-b.age;
});
for(var i = 0;i<arr4.length;i++){
console.log(arr4[i].name, arr4[i].age);
}
/*
李四 15
张三 22
王五 27
*/

slice()

获取现有数组中指定元素,不改变原有数组,返回一个数组。

语法:arr.slice(start, end)

如果start, end 都缺省,返回与原数组相同的一个数组;

如果只传如start,缺省end,返回从start开始至原数组末尾所有元素组成的数组

具体传参方式参考如下例子:

1
2
3
4
5
6
7
8
9
10
var arr = [1, 2, 3, 4, 5, 6];

console.log(arr.slice()); // [1, 2, 3, 4, 5, 6]
console.log(arr.slice(0)); // [1, 2, 3, 4, 5, 6]
console.log(arr.slice(1)); // [2, 3, 4, 5, 6]
console.log(arr.slice(1, 3)); // [2, 3]
console.log(arr.slice(1, -2)); // [2, 3, 4] 5的位置相当于-2, 6的位置相当于-1
console.log(arr.slice(-3, -1)); // [4, 5] 4的位置是-3
console.log(arr.slice(4, 1)); // []
console.log(arr); // [1, 2, 3, 4, 5, 6]

splice()

插入, 删除, 替换数组元素,改变原有数组,返回删除的所有元素组成的数组。

语法:arr.splice(start, count, elem1, elem2, …elemN)

start:插入或者删除元素得起始位置

count:删除元素得个数

elem1:从start位置开始插入的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];

console.log(arr.splice()); // [] 相当于啥也没做
console.log(arr); // ["a", "b", "c", "d", "e", "f", "g"]
console.log(arr.splice(1)); // ["b", "c", "d", "e", "f", "g"] 从1开始到数组结尾的元素全部删除
console.log(arr); // ["a"]
console.log(arr.splice(1, 2)); // ["b", "c"] 从索引为1开始删除2个元素
console.log(arr); // ["a", "d", "e", "f", "g"]

arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
console.log(arr.splice(1, 0, 'h')); // [] 在1的位置插入h
console.log(arr); // ["a", "h", "b", "c", "d", "e", "f", "g"]

arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
console.log(arr.splice(1, 3, 'h', 'i'));// ["b", "c", "d"]从1的位置开始 删除3个元素,并插入h,i两个元素。
console.log(arr); // ["a", "h", "i", "e", "f", "g"]

toString()

将数组转化为一个字符串,不改变原有数组,返回一个字符串

语法:arr.toString()

1
2
3
var arr = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
console.log(arr.toString()); // a,b,c,d,e,f,g
console.log(arr); // ["a", "b", "c", "d", "e", "f", "g"]

ES5 数组的扩展

forEach()

遍历循环一个数组

语法:arr.forEach(func[, context])

func:一个函数,这个函数接受三个参数value, index, array,每次遍历的数组元素,元素对应的索引,原数组。

context:上下文,用来改变func中this的指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var arr = ['a', 'b', 'c'];
arr.forEach(function(value, index, array){
console.log(value, index, array);
});
/*
a 0 ["a", "b", "c"]
b 1 ["a", "b", "c"]
c 2 ["a", "b", "c"]
*/

// 只是占位得元素不会被forEach遍历
var arr = ['a', , 'c'];
console.log(arr.length); // 3
arr.forEach(function(value, index, array){
console.log(value, index, array);
});
/*
a 0 ["a", "b", "c"]
c 2 ["a", "b", "c"]
*/

map()

可以理解为映射,基于原数组映射出一个新数组,不改变原数组,返回一个新数组。
语法:arr.map(func[, context])

func:一个函数,此函数接受三个参数:value, index, array,必须要有返回值,所有的返回值会组成一个新数组。

context:上下文,用来改变func中this的指向。

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
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.map(function(value, index, array){
return value*value;
})
console.log(arr); // [1, 2, 3, 4, 5]
console.log(arr2); // [1, 4, 9, 16, 25]

var arr3 = [
{
name: '张三',
age: 22
},
{
name: '李四',
age: 15
},
{
name: '王五',
age: 27
}
];

console.log(arr3.map(function(v){
return v.name;
})); // ["张三", "李四", "王五"]

filter()

可以理解为‘过滤’,‘筛选’,不改变原数组,返回筛选之后的新数组。
语法:arr.filter(func[, context])

func:一个函数,此函数接受三个参数:value, index, array,返回值如果是true或者转化为true得值,则此次遍历的元素就会被加入返回得新数组中(筛选通过!)

context:上下文,用来改变func中this的指向。

1
2
3
4
5
6
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.filter(function(v){
return v>3;
});
console.log(arr2); // [4, 5]
console.log(arr); // [1, 2, 3, 4, 5]

some()

每次遍历只要有一次返回true,some函数就返回true,所有遍历都返回false,some返回false,不改变原有数组,返回boolean值。

语法:arr.some(func[, context])

func:一个函数,此函数接受三个参数:value, index, array

context:上下文,用来改变func中this的指向。

1
2
3
4
5
6
var arr = [1, 2, 3, 4, 5];
var result = arr.some(function(value, index, array){
return value>3;
});
console.log(result); // true 4和5 这两个元素大于3了,所有返回true。
console.log(arr); // [1, 2, 3, 4, 5]

every()

每次遍历只要有一次返回false,every函数就返回false,所有遍历都返回true,every返回true,不改变原有数组,返回boolean值。

语法:arr.every(func[, context])

func:一个函数,此函数接受三个参数:value, index, array

context:上下文,用来改变func中this的指向。

与some类似,但是every要求每个元素都满足要求,才返回true,some只要一个元素满足要求,就返回true

1
2
3
4
5
6
var arr = [1, 2, 3, 4, 5];
var result = arr.every(function(value, index, array){
return value>0;
});
console.log(result); // true 所有元素都大于0,所以返回true
console.log(arr); // [1, 2, 3, 4, 5]

indexOf()

查询元素在数组中的索引,跟str.indexOf类似。如果未查到,返回-1,否则返回元素所在数组中的索引。

语法:arr.indexOf(elem[, start])

elem:要查找得元素

start:开始位置,如果缺省,从开始位置查找(0)

1
2
3
4
var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
console.log(arr.indexOf('g')); // -1
console.log(arr.indexOf('c')); // 2
console.log(arr.indexOf('d', 5)); // -1

lastIndexOf()

与indexOf类似,只不过是从结尾开始查找,从右向左查找。

语法:arr.lastIndexOf(elem[, start])

elem:要查找得元素

start:开始位置,如果缺省,从数组末尾开始查找(arr.length-1)

1
2
3
4
var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
console.log(arr.lastIndexOf('g')); // -1
console.log(arr.lastIndexOf('c')); // 2
console.log(arr.lastIndexOf('d', 5)); // 3

reduce()

这个函数有些复杂,不知到怎么描述好,直接看例子吧:

语法:arr.reduce(func[, initialValue])

func:接受4个参数得函数,previous, current, index, array,之前值,当前值,索引,数组本身。func的返回值,会作为下一次迭代时previous的值。

initialValue:可选,表示初始值,如果指定initialValue,则作为初始时previous的值,如果缺省,数组得第一个元素为previous值,第二个元素为current。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var arr = [1, 2, 3, 4, 5];
var sum = arr.reduce(function(previous, current, index, array){
return previous + current;
});
console.log(sum); // 15

过程展开如下:
// 初始:
previous = initialValue = 1, current = 2;
// 第一次迭代 之后
previous = 1 + 2 = 3, current = 3;//current 向后移动一个元素
// 第二次迭代之后
previous = 3 + 3 = 6, current = 4;
// 第三次迭代之后
previous = 6 + 4 = 10, current = 5;
// 第四次迭代之后
previous = 10 + 5 = 15 ,current = undefined// 没有了,退出,返回15;

reduceRight()

和reduce类似,只不过从数组结尾开始,从右向左迭代。

ES6 数组的扩展

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。

下面是一个类似数组的对象,Array.from将它转为真正的数组。

1
2
3
4
5
6
7
8
9
10
11
12
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

1
2
3
4
5
6
7
8
9
10
11
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
});

// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}

上面代码中,querySelectorAll方法返回的是一个类似数组的对象,只有将这个对象转为真正的数组,才能使用forEach方法。

只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。

1
2
3
4
5
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

上面代码中,字符串和Set结构都具有Iterator接口,因此可以被Array.from转为真正的数组。

如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

1
2
Array.from([1, 2, 3])
// [1, 2, 3]

值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

1
2
3
4
5
6
7
// arguments对象
function foo() {
var args = [...arguments];
}

// NodeList对象
[...document.querySelectorAll('div')]

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

1
2
Array.from({ length: 3 });
// [ undefined, undefined, undefinded ]

上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。

1
2
3
const toArray = (() =>
Array.from ? Array.from : obj => [].slice.call(obj)
)();

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

1
2
3
4
5
6
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

下面的例子是取出一组DOM节点的文本内容。

1
2
3
4
5
6
7
let spans = document.querySelectorAll('span.name');

// map()
let names1 = Array.prototype.map.call(spans, s => s.textContent);

// Array.from()
let names2 = Array.from(spans, s => s.textContent)

下面的例子将数组中布尔值为false的成员转为0

1
2
Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]

另一个例子是返回各种数据的类型。

1
2
3
4
5
function typesOf () {
return Array.from(arguments, value => typeof value)
}
typesOf(null, [], NaN)
// ['object', 'object', 'number']

如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this

Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。

1
2
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']

上面代码中,Array.from的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。

Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于\uFFFF的Unicode字符,算作两个字符的bug。

1
2
3
function countSymbols(string) {
return Array.from(string).length;
}

Array.of()

Array.of方法用于将一组值,转换为数组。

1
2
3
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1

这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

1
2
3
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]

上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于2个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

1
2
3
4
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

Array.of方法可以用下面的代码模拟实现。

1
2
3
function ArrayOf(){
return [].slice.call(arguments);
}

数组实例的copyWithin()

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

1
Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。
  • start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。

这三个参数都应该是数值,如果不是,会自动转为数值。

1
2
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

上面代码表示将从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2。

下面是更多例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
var i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署TypedArray的copyWithin方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

数组实例的find()和findIndex()

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

1
2
[1, 4, -5, 10].find((n) => n < 0)
// -5

上面代码找出数组中第一个小于0的成员。

1
2
3
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10

上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

1
2
3
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2

这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。

1
2
3
4
5
[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

数组实例的fill()

fill方法使用给定值,填充一个数组。

1
2
3
4
5
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

1
2
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

上面代码表示,fill方法从1号位开始,向原数组填充7,到2号位之前结束。

数组实例的entries(),keys()和values()

ES6提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"

如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

1
2
3
4
5
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

数组实例的includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持。

1
2
3
[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

1
2
[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

1
2
3
if (arr.indexOf(el) !== -1) {
// ...
}

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相当运算符(===)进行判断,这会导致对NaN的误判。

1
2
[NaN].indexOf(NaN)
// -1

includes使用的是不一样的判断算法,就没有这个问题。

1
2
[NaN].includes(NaN)
// true

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

1
2
3
4
5
6
const contains = (() =>
Array.prototype.includes
? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value)
)();
contains(["foo", "bar"], "baz"); // => false

另外,Map和Set数据结构有一个has方法,需要注意与includes区分。

  • Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
  • Set结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)

数组的空位

数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

1
Array(3) // [, , ,]

上面代码中,Array(3)返回一个具有3个空位的数组。

注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

1
2
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

上面代码说明,第一个数组的0号位置是有值的,第二个数组的0号位置没有值。

ES5对空位的处理,已经很不一致了,大多数情况下会忽略空位。

  • forEach(), filter(), every()some()都会跳过空位。
  • map()会跳过空位,但会保留这个值
  • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// forEach方法
[,'a'].forEach((x,i) => log(i)); // 1

// filter方法
['a',,'b'].filter(x => true) // ['a','b']

// every方法
[,'a'].every(x => x==='a') // true

// some方法
[,'a'].some(x => x !== 'a') // false

// map方法
[,'a'].map(x => 1) // [,1]

// join方法
[,'a',undefined,null].join('#') // "#a##"

// toString方法
[,'a',undefined,null].toString() // ",a,,"

ES6则是明确将空位转为undefined

Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

1
2
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

扩展运算符(...)也会将空位转为undefined

1
2
[...['a',,'b']]
// [ "a", undefined, "b" ]

copyWithin()会连空位一起拷贝。

1
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

fill()会将空位视为正常的数组位置。

1
new Array(3).fill('a') // ["a","a","a"]

for...of循环也会遍历空位。

1
2
3
4
5
6
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1

上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。

entries()keys()values()find()findIndex()会将空位处理成undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

由于空位的处理规则非常不统一,所以建议避免出现空位。

数组推导

数组推导(array comprehension)提供简洁写法,允许直接通过现有数组生成新数组。这项功能本来是要放入ES6的,但是TC39委员会想继续完善这项功能,让其支持所有数据结构(内部调用iterator对象),不像现在只支持数组,所以就把它推迟到了ES7。Babel转码器已经支持这个功能。

1
2
3
4
var a1 = [1, 2, 3, 4];
var a2 = [for (i of a1) i * 2];

a2 // [2, 4, 6, 8]

上面代码表示,通过for...of结构,数组a2直接在a1的基础上生成。

注意,数组推导中,for...of结构总是写在最前面,返回的表达式写在最后面。

for...of后面还可以附加if语句,用来设定循环的限制条件。

1
2
3
4
5
6
7
8
9
10
var years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];

[for (year of years) if (year > 2000) year];
// [ 2006, 2010, 2014 ]

[for (year of years) if (year > 2000) if(year < 2010) year];
// [ 2006]

[for (year of years) if (year > 2000 && year < 2010) year];
// [ 2006]

上面代码表明,if语句要写在for...of与返回的表达式之间,而且可以多个if语句连用。

下面是另一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var customers = [
{
name: 'Jack',
age: 25,
city: 'New York'
},
{
name: 'Peter',
age: 30,
city: 'Seattle'
}
];

var results = [
for (c of customers)
if (c.city == "Seattle")
{ name: c.name, age: c.age }
];
results // { name: "Peter", age: 30 }

数组推导可以替代mapfilter方法。

1
2
3
4
5
6
7
[for (i of [1, 2, 3]) i * i];
// 等价于
[1, 2, 3].map(function (i) { return i * i });

[for (i of [1,4,2,3,-8]) if (i < 3) i];
// 等价于
[1,4,2,3,-8].filter(function(i) { return i < 3 });

上面代码说明,模拟map功能只要单纯的for...of循环就行了,模拟filter功能除了for...of循环,还必须加上if语句。

在一个数组推导中,还可以使用多个for...of结构,构成多重循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
var a1 = ['x1', 'y1'];
var a2 = ['x2', 'y2'];
var a3 = ['x3', 'y3'];

[for (s of a1) for (w of a2) for (r of a3) console.log(s + w + r)];
// x1x2x3
// x1x2y3
// x1y2x3
// x1y2y3
// y1x2x3
// y1x2y3
// y1y2x3
// y1y2y3

上面代码在一个数组推导之中,使用了三个for...of结构。

需要注意的是,数组推导的方括号构成了一个单独的作用域,在这个方括号中声明的变量类似于使用let语句声明的变量。

由于字符串可以视为数组,因此字符串也可以直接用于数组推导。

1
2
3
[for (c of 'abcde') if (/[aeiou]/.test(c)) c].join('') // 'ae'

[for (c of 'abcde') c+'0'].join('') // 'a0b0c0d0e0'

上面代码使用了数组推导,对字符串进行处理。

数组推导需要注意的地方是,新数组会立即在内存中生成。这时,如果原数组是一个很大的数组,将会非常耗费内存。

实用方法

删除元素

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
/**
* 根据元素,删除数据中的此元素,修改原素组,返回删除的元素
* @param array
* @param item
* @returns {Array.<T>|*}
*/
function removeByItem(array, item) {
if (!array.length) return;
var index = array.indexOf(item);
if (index > -1) {
return array.splice(index, 1);
}
}

/**
* 根据索引,删除数据中的元素,修改原素组,返回删除的元素
* @param array
* @param item
* @returns {Array.<T>|*}
*/
function removeByIndex(array, index) {
if (!array.length) return;
if (index < 0 || index > array.length - 1) return;
if (index > -1) {
return array.splice(index, 1);
}
}

参考链接:

http://www.cnblogs.com/tugenhua0707/p/5052808.html

http://www.zhangxinxu.com/wordpress/2013/04/es5%E6%96%B0%E5%A2%9E%E6%95%B0%E7%BB%84%E6%96%B9%E6%B3%95/

http://es6.ruanyifeng.com/#docs/array

webpack构建,js使用script标签引入

使用webpack构建大型项目的时候,如果js库引用过多,彼此之间依赖比较复杂,webpack构建过程非常的慢,而且生成得common.js非常大。整个项目中一些第三方得js库可以不用构建,直接在页面上通过script标签引入即可,只通过webpack打包编译业务代码。

我使用过react react-router ant-design等搭建了一个管理系统,光webpack构建就将近2分钟(而且是电脑配置很牛逼得情况下),打包成的common.js未压缩时2M多,压缩之后还有800k~900K,将react 等一些第三方的js通过script标签直接在页面上引入,降低了单个文件的大小,加快了webpack构建速度,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
output: {
...
'libraryTarget': 'var',
...
},
externals: {
'react': 'React',
'react-dom': 'ReactDOM',
'antd': 'antd',
"jquery": "jQuery"
},
...
/*
* 如果不使用CDN,拷贝externals文件到指定静态目录,webpack-dev-server也可以获取到这些文件。
* */
new CopyWebpackPlugin([
{from: 'src/static/antd-0.12.12.min.css'},
{from: 'src/static/antd-0.12.12.min.js'},
{from: 'src/static/react-0.14.6.min.js'},
{from: 'src/static/react-dom-0.14.6.min.js'}
])

页面上通过script引入js:

1
2
3
4
<script src="react-0.14.6.min.js"></script>
<script src="react-dom-0.14.6.min.js"></script>
<script src="antd-0.12.12.min.js"></script>
<script src="jquery.min.js"></script>

文件中的写法不变,依然使用es6的import:

1
2
3
4
import React from 'react';
import ReactDOM from 'react-dom';
import { Breadcrumb } from 'antd';
import $ from 'jquery'

使用node创建一个命令行工具 zk-init

安装

1
$ sudo npm install zk-init -g

使用

1
$ zk-init [template name]

在终端输入一个命令就能执行相关的操作,比如终端输入$ zk-init,就会的一个简单的jQuery开发的demo。输入$ zk-init jquery-webpack就会得到一个通过webpack构建得jQuery项目初始结构。

做这个小工具主要是学习一下“手脚架”是怎么写的。同时也想通过这个工具快速初始化出各种项目结构,比如我要写一个小demo的时候,需要新建一个文件夹,然后再创建htmlcssjs等,复杂一些得项目需要gulp grunt webpack等,还得从别的项目中把配置拷贝过来进行修改(配置我记不住。。。),通过这个工具可以省去一些繁琐的,没有技术含量的搭建过程。实现细节,这里就不详细介绍了可以直接去看源码

webpack 监听html文件

在使用webpack-dev-server时,js改变,webpack就会重新构建项目,并且浏览器会自动刷新,但是编辑html文件时,webpack-dev-server却毫无反应。

原因由于webpack(webpack-dev-server)只监听有依赖得相关文件,比如html等没有依赖的文件是不会被webpack watch监听的。

既然有依赖关系才会被webpack watch监听,那么就把html文件添加到webpack构建过程中去,就可以监听html文件了。本文介绍一个方法,仅供参考

最终效果

  • 开发过程中可以监听html文件,编辑html文件,浏览器会自动刷新;
  • 最终构建生成的线上文件不能有为了监听html,而加入的html依赖内容。

准备

webpack配置文件要能根据命令行参数区分不同的开发模式(runmod)。

为了兼容各个平台命令行参数的写法,这里使用cross-env模块

安装方法:

1
npm install cross-env --save-dev

使用方法,在 node 的package.json中定义一个scripts:

1
2
3
"scripts" :{
"dev" : "cross-env runmod=devserver webpack-dev-server --port 8086 --progress --inline"
}

终端输入npm run dev 即可运行如上命令

创建一个 watch-html.js文件:

1
2
3
4
5
// RUNMOD 是在webpack.config.js中配置的: new webpack.DefinePlugin({"RUNMOD": JSON.stringify(runmod)}),
if (RUNMOD === 'devserver') {
require('./index.html');// 这个引入只是让index.html可以被webpack watch 监听
require('./demo/demo.html');
}

webpack.config.js文件配置

webpack.config.js文件中可以使用 var runmod = process.env.runmod 获取到命令行传入得参数(runmod=devserver

通过new webpack.DefinePlugin({"RUNMOD": JSON.stringify(runmod)})方式可以把RUNMOD参数传给每一个通过webpack构建的js文件。参见watch-html.js

将watch-html.js文件加入entry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var _entry = {
//"index": ["./src/home/home.jsx", "./src/home/home-content.jsx"],//会合并成一个index.js
"index": './index.js',
"demo": './demo/demo.js'
};
if (runmod === 'devserver') {// 只有devserver模式,才加入监听html文件
_entry["watch-html"] = './watch-html.js';
}
...
module.exports = {
...
/*
* 入口文件配置
* */
entry: _entry,
...
}

将所需要进行监听得文件加入watch-html.js文件中就可以实现开发模式(devserver模式)下监听html文件,其他模式(线上 测试 等)不会生成额外的文件。

React组件间数据传递

组件间的数据管理,可以使用flux,redux,reflux等方案,如果不使用这些方案,组件间该如何传递数据?本文提供了几个办法。

组件关系

组件关系可以分为两种,一种是有层级关系得,即:父子组件;另一种是没有层级关系,即:没有公共的父级组件或者祖先组件。(感觉我在说废话)。

父组件给子组件传递数据

父组件可以通过props给子组件传递数据。子组件通过this.props可以获取到父级组件传递过来的数据。

子组件给父组件传递数据

父组件通过props,给子组件提供一个方法,方法是用来修改父组件数据,子组件调用方法,可以将子组件得数据传递给父组件;

代码片段如下:

1
2
3
4
5
let childData = {}
render(){
return <ChildComponent setParentData={(dataFromChild)=>childData=dataFromChild}/>
}
//子组件通过调用 this.props.setParentData(dataFromChild);即可将数据传给父级组件。

没有层级关系的组件间传递数据

可以使用发布订阅模式,进行数据通信;其实父子关系得组件间也可以使用发布订阅模式。flux,redux等解决方案,本质上其实也是发布订阅模式。

React Redux 理解

初步对Redux的理解,进行一下总结

组成部分

views

组件可以分为两类,component-views controller-views

components-views

这个名字是我自己起的,这类组件职责如下:

  • 接受父组件(controller-views)的props做展示;
  • 调用父组件的方法(通过props传递的),改变数据,一般组件中的事件(比如点击事件),通过pros调用父组件的方法。

controller-views

顾名思义,这个view有控制器的功能,主要的作用如下:

  • 包含组装其他组件(component-views),通过props为其他组件提供数据;
  • 通过props.dispatch调用actions中的方法,处理业务逻辑;
  • 通过props为子组件提供方法,供子组件调用。
  • 需要通过react-redux connect方法进行包装,将state转换成props。

actions

为controller-vers提供语义化API,actions中的方法一般只是单纯的返回一个action对象,action约定有个type属性,用来让reducers区分什么样的操作。

reducers

一些纯函数,接受两个参数,原state数据和action,返回处理好的state,函数中药注意,不要直接改变形参的state,通过assign方法,重新创建一个state,然后返回新建并处理过的state。

官方文档

英文文档

中文文档

分页实现方法

limit offset,count的方式实现分页

这种分页方式可以提供完整的分页信息,比如页码,总页数(需要计算),总记录数等。

前端需要传给后台两个参数:当前页,每页显示记录数(currentPage,pageSize)或者一个参数:当前页(currentPage),每页显示记录数(pageSize)由后台自己提供。

offest和count计算方式如下:

1
2
int offset = (currentPage-1)*pageSize;
int count = pageSize;

MySql分页查询语句如下

1
SELECT * FROM table LIMIT offset,count; -- offset 和 count替换成上面计算得结果。

查询总记录数(totalCount)Sql语句如下

1
SELECT count(*) FROM table

计算总页数

1
int totalCount = Math.ceil(totalCount/pageSize);//向上取整

preCursor nextCursor方式分页

这种方式分页不显示页码级总页数,总记录数等分页信息,适用于只有两个分页按钮(上一页,下一页),无限加载情况。

前端需要给后台传两个参数nextCursor(或者preCursor) 和 pageSize(pageSize也可以后台自己提供)

下一页(无限加载)对应的sql语句

1
SELECT * FROM table WHERE created_at>nextCursor ORDER BY created_at DESC LIMIT pageSize; -- nextCursor 和 pageSize 替换成对应的值

上一页对应的sql语句

1
SELECT * FROM table WHERE created_at<preCursor ORDER BY created_at DESC LIMIT pageSize; -- preCursor 和 pageSize 替换成对应的值

其中nextCursor和preCursor存的是created_at的一个值,比如一次行查出10条数据,那么nextCursor存的就是第十条数据的created_at值,preCursor存的就是第一条数据created_at值。preCursor 和 nextCursor 需要传给前端,便于下次翻页的时候前端再传给后端。

前端编码规范

通用规范请参阅如下链接,如有冲突,以本文为准。

工具

项目中可以使用ESLint,辅助实现规范

IntelliJ IDEA the Java IDE(IntelliJ系列) 配置:

.eslintrc.js配置文件项目中已经加入,相关包已经安装了,注意第一次使用时npm install eslint相关包

IDE 左上角 File -> Settings 打开settings,配置如下图
IntelliJ系列,ESLint配置图片

规范目的

为了提高团队开发效率,减少沟通障碍,以及便于后期维护扩展,特制定此规范。本规范一经大家确认,前端开发人员必须按照此规范进行前端开发。规范中如有不恰之处,请及时提出,经讨论后修改或完善此规范。

基本准则

模块化,组件化,语义化jsx(html),结构(jsx)表现(css或less)行为(js)分离。

文件规范

  1. 文件夹,文件命名,要使用语义化的英文,不允许使用缩写,数字。
  2. 架构相关,非业务文件归档至assets/src/framework
  3. 公共方法归档至assets/src/common,注意,这个文件夹存放公共的方法,而非组件,添加之后QQ或邮件通知全体前端开发人员;
  4. 公共自定义组件归档至assets/src/component,并写好README,QQ或邮件通知全体前端开发人员;
  5. 业务模块统一归档至assets/src/page
  6. 图片个数超过5个以上,要归档至当前模块文件夹下的imgs/文件夹中;
  7. 一个功能模块新建一个文件夹,一个模块相关的文件(html,css,js,jsx,images),归档至对应的模块文件夹中,当此模块不需要时,删除此模块对应的文件夹,即可将此模块相关文件全部移除;
  8. 文件夹命名:小写英文+连字符(减号),比如:account-manage
  9. 文件命名: 驼峰式命名,首字母大写:比如AccountManage.jsx;一个文件一个类,而且文件名和类名必须完全一致,默认export类名(或者变量名)与文件名完全一致。
  10. 添加修改页面,文件名以Edit结尾,列表页面,文件名以List结尾,详情页面以Detail结尾;

    1
    2
    3
    AccountEdit.jsx
    AccountDetail.jsx
    AccountList.jsx
  11. css/less命名:一个模块下基本就包含一个css/less文件,命名为style.css/style.less即可;

  12. 图片:小写英文+连字符(减号),比如:user-avatar.jpg
  13. 文件之间的依赖,统一使用ES6写法:import request from 'xxx.jsx'
  14. 统一使用ES6得模块写法(import,export),不要使用其他方式。

jsx

  1. 文件名必须和默认输出完全保持一致

    1
    2
    3
    4
    5
    6
    7
    8
    // file CheckBox.js or CheckBox.jsx
    class CheckBox {
    // ...
    }
    export default CheckBox;

    // good
    import CheckBox from './CheckBox';
  2. import必须写在文件的头部,未使用到的引入,要删除。

  3. export必须写在文件底部,通过export {aaa,bbb,Ccc as ccc }方式导出多个变量,通过export default aaa 导出默认变量。
  4. import各类型文件顺序

    • css/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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    // css/less文件
    import './style.less';
    //第三方模块
    import React from 'react';
    import {Link} from 'react-router'
    import assign from 'object-assign';
    import moment from 'moment';
    import {message, Button, Row, Col, Tabs, Progress } from 'antd'
    //自定义模块
    import Page from '../../framework/page/Page';
    import ajax from '../../framework/common/ajax'
    //图片
    import UserAvatar from './user-avatar.jpg'
    // 模块变量
    const TabPane = Tabs.TabPane;
    const ProgressLine = Progress.Line;

    //或者这样:
    // css/less文件
    import './style.less';
    //第三方模块
    import React from 'react';
    import {Link} from 'react-router'
    import assign from 'object-assign';
    import moment from 'moment';
    //自定义模块
    import Page from '../../framework/page/Page';
    import ajax from '../../framework/common/ajax'
    //图片
    import UserAvatar from './user-avatar.jpg'

    //一次性引入过多,最好使用如下方式,方便增,删
    import {
    message,
    Button,
    Row,
    Col,
    Tabs,
    Form,
    Modal,
    Input,
    InputNumber,
    DatePicker,
    Radio,
    Checkbox,//保留这个逗号,方便增,删
    } from 'antd'

    // 模块变量
    const TabPane = Tabs.TabPane;
    const ProgressLine = Progress.Line;
  5. 声明引用一个第三方模块的变量,必须使用const

    1
    2
    const TabPane = Tabs.TabPane;
    const ProgressLine = Progress.Line;
  6. 尽量使用const声明变量,其次使用let,不允许使用var

  7. React统一使用ES6写法

    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
    // 是否考虑创建一个BaseComponent,其他类继承BaseComponent?
    // BaseComponent中可以封装一些常用方法。
    class App extends React.Component{
    // 构造函数
    constructor(props){
    super(props);
    }
    // 初始化state,替代原getInitialState, 注意前面没有static
    state = {
    showMenu:false
    };
    // 替代原propTypes,指定props参数规范, 属性,注意前面有static,属于静态方法.
    static propTypes = {
        autoPlay: React.PropTypes.bool.isRequired
    }
    // 默认defaultProps,替代原getDefaultProps方法, 注意前面有static,属于静态方法.
    static defaultProps = {
    loading:false
    };
    componentDidMount() {
    // do something yourself...
    }
    // 事件的写法,这里要使用箭头函数,箭头函数不会改变this的指向,否则函数内,this指的就不是当前对象了
    // React.CreatClass方式React会自动绑定this,ES6写法不会.
    handleClick = (e)=>{
    this.setState();//这里的this指的还是App
    };
    render(){

    }
    }
  8. jsx类的结构顺序:

    • constructor
    • state
    • static propTypes
    • static defaultProps
    • 其他自定义属性或静态(类)属性
    • 周期函数componentDidMount等
    • 自定义函数handle系列等
    • 其他自定义辅助函数
    • render

    遵循这样的约定,方便快速查找各部分代码。

  9. jsx中事件属性,统一on开头,比如

    1
    <Sidebar onClick={this.handleClick} onToggle={this.handleToggle}></Sidebar>
  10. 事件处理函数命名,统一handle开头,驼峰式命名,比如:handleClick handleToggle

  11. 事件统一使用箭头函数,箭头行数不会改变向上文(this指向),不必使用bind了。

    1
    2
    3
    handleClick = (e) => {
    // Do something here!
    };
  12. 请求统一使用封装过的request

  13. 提示信息统一使用 src/common.jsx/MESSAGE_CONTENT中维护的内容。
    1
    2
    3
    4
    5
    6
    7
    // 变量命名 取名字比较费劲 可读性较好
    exports const MESSAGE_CONTENT = {
    OPERATE_SUCCESS:'操作成功',
    OPERATE_FAILED:'操作失败',
    LOGIN_SUCCESS:'登陆成功',
    STORE_NAME_CAN_NOT_BE_NULL:'门店不能为空'
    }

less/css

  1. 每个模块如果只有一个less/css文件,要命名为style.less/style.css
  2. 类名/id名 小写英文+连字符,比如:.user-avatar
  3. 各个模块的样式,以一个大的父级开始写起,避免各个模块样式冲突

    1
    2
    3
    4
    5
    6
    7
    8
    .account-manage{
    .block{
    ...
    }
    #some-id{
    ...
    }
    }
  4. 编写less时,避免嵌套多层(最多不要超过3层)。不要编写太过于复杂的less。

路由结构规范

特殊情况,不能按照规范实现,与各位leader商榷

1
2
3
4
/m/1 对应商户 1 为商户id
/s/2 对应门店 2 为门店id
多单词使用“_”链接,不要使用“-”,或其他特殊字符。
根据菜单结构,定义url结构,RestFull 约定,具体看下面例子。

例如:
菜单结构:

1
2
3
品牌
-门店
-门店管理

对应的菜单为

1
2
3
4
5
6
7
8
列表页:
http:localhost:8080/m/1/stores
详情页
http:localhost:8080/m/1/stores/12
添加页
http:localhost:8080/m/1/stores/new
修改页
http:localhost:8080/m/1/stores/12/edit

菜单结构

1
2
3
4
5
门店  # store
-订单 # order 一级
-外卖订单 # take_out 二级
-新订单 # new_order 可点击跳转页面
-所有订单 # all_order 可点击跳转页面

对应的菜单为:

1
2
3
4
5
6
7
8
列表页
http:localhost:8080/m/1/s/2/order/take_out/new_orders
详情页
http:localhost:8080/m/1/s/2/order/take_out/new_orders/12
添加页
http:localhost:8080/m/1/s/2/order/take_out/new_orders/new
修改页
http:localhost:8080/m/1/s/2/order/take_out/new_orders/21/edit

未完待续

Intellij IDEA Maven3 cannot resolve symbol

Intellij IDEA + Maven3 模块无法引入得问题,编辑器上显示红色下滑线,maven可以正常编译项目,项目也可以运行,就是编辑器有红色得提示,碍眼。网上搜索了一些解决办法,但是没什么鸟用。那就应该是缓存问题,清理一下重启Idea就Ok了:

1
File -> Invalidate Caches/Restart...

本地新建文件夹并提交到github上

先手动在github官网上创建一个空的仓库,名为repo-name

方法一:通过命令clone到本地,一般是还没有写任何代码,clone下来之后再写内容

1
2
3
git clone git@github.com:your-name/repo-name.git
or
git clone https://github.com/your-name/repo-name.git

注意:https方式ssh-key不起作用,每次push都要输入账号密码

方法二:本地已经写好文件了,然后才创建的github仓库。

1
2
3
4
5
6
cd 到本地文件夹
git init
git add --all
git commit -m "first commit"
git remote add origin git@github.com:your-name/repo-name.git
git push -u origin master

编辑完成之后,提交命令

1
2
3
git add --all
git commit -m '这里写注释'
git push origin master

npm scripts 跨平台写法

Unix 或者 Windows 的命令行脚本写法有些不同,在不精通这些脚本命令的情况下,可以通过一些工具达到跨平台,使用起来也非常简单.

一个package.json文件实例:源码戳这里

1
2
3
4
5
6
7
8
9
10
11
{
"devDependencies": {
"cross-env": "^1.0.7",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"rimraf": "^2.5.0"
},
"scripts": {
"my": "rimraf my && mkdirp my && ncp build my && cross-env MOD=pro node my.js"
}
}

相应的模块官网:

删除目录 rimraf

创建目录 mkdirp

传递参数 cross-env

复制 ncp

React ES6写法

React 最新版本支持ES6的写法,配合上Babel,写起来挺嗨的,总结一下ES6的一些写法:

React ES6+写法

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
// 多加一层继承, 可以在baseComponent中做一些文章.
class baseComponent extends React.Component{
// 构造函数
constructor(props){
super(props);
}
// ...其他公用代码,方法等封装
}
//定义类 并继承baseComponent
class App extends baseComponent{
// 构造函数
constructor(props){
super(props);
}
// 初始化state,替代原getInitialState, 注意前面没有static
state = {
showMenu:false
};
// 替代原propTypes 属性,注意前面有static,属于静态方法.
static propTypes = {
    autoPlay: React.PropTypes.bool.isRequired
}
// 默认defaultProps,替代原getDefaultProps方法, 注意前面有static
static defaultProps = {
loading:false
};
// 事件的写法,这里要使用箭头函数,箭头函数不会改变this的指向,否则函数内,this指的就不是当前对象了
// React.CreatClass方式React会自动绑定this,ES6写法不会.详见下一小节说明.
handleClick = (e)=>{
this.setState();//这里的this指的还是App
};
componentDidMount() {
// React内置的周期函数,这里要显示的调用父类的相应函数,
// 否则一旦父类中有封装,子类会把父类相应函数覆盖掉,不会执行父类的函数.
// 需要判断一下...父类一旦没有实现componentDidMount,这里直接调用就会报错,
// 最好是父类都实现相应的方法,子类就不用判断了.
if (super.componentDidMount) {
super.componentDidMount();
}
// do something yourself...
}
}

官方的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {count: props.initialCount};
}
tick() {
this.setState({count: this.state.count + 1});
}
render() {
return (
<div onClick={this.tick.bind(this)}>
Clicks: {this.state.count}
</div>
);
}
}
Counter.propTypes = { initialCount: React.PropTypes.number };
Counter.defaultProps = { initialCount: 0 };

React ES6 事件绑定

官方原话:

Autobinding: When creating callbacks in JavaScript, you usually need to explicitly bind a method to its instance such that the value of this is correct. With React, every method is automatically bound to its component instance. React caches the bound method such that it’s extremely CPU and memory efficient. It’s also less typing!

新的ES6写法如果要实现this还指向当前对象,有三种写法:个人感觉箭头函数写法最优雅.

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
第一种:this._handleClick.bind(this)

_handleClick(e) {
console.log(this);
}
render() {
return (
<div>
<h1 onClick={this._handleClick.bind(this)}>点击</h1>
</div>
);
}
第二种:this._handleClick = this._handleClick.bind(this)

constructor(props) {
super(props);
this._handleClick = this._handleClick.bind(this)
}
_handleClick(e) {
console.log(this);
}
render() {
return (
<div>
<h1 onClick={this._handleClick}>点击</h1>
</div>
);
}
第三种:_handleClick = (e) => {}

_handleClick = (e) => {
// 使用箭头函数(arrow function)
console.log(this);
}
render() {
return (
<div>
<h1 onClick={this._handleClick}>点击</h1>
</div>
);
}

React ES6 写法,做封装一些待解决的问题

  • 多层继承,props state等属性如何继承?
  • 一些周期函数子类如何自动调用?而不是super.componentDidMount()这种显示调用

模块模式

模块模式是用来封装逻辑并避免全局命名空间污染的好方法。 使用匿名函数可以做到这一点, 匿名函数也是JavaScript 中被证明最优秀的特性之一。 通常是创建一个匿名函数并立即执行它。 在匿名函数里的逻辑都在闭包里运行, 为应用中的变量提供了局部作用域和私有的运行环境 :

1
2
3
(function(){
/* ... */
})();

全局导入

模块内部的变量都是局部变量,全局命名空间无法访问,但是模块内部可以访问全局变量,模块中的变量很难一眼就看出来是全局变量还是局部变量,隐身的全局变量由于解释器要遍历作用域链的原因,会是程序变得更慢,局部变量的读取往往更快更高效。

将全局变量以函数参数的方式传入匿名函数,可以将全局变量导入模块中。这种方式比隐式的全局对象更加简洁而且速度更快 :

1
2
3
(function($){
/* ... */
})(jQuery);

模块中不要使用隐式全局变量,全局变量要通过参数传入匿名函数

全局导出

理想状况下应当使用尽可能少的全局变量, 但总会有某些特殊场景用到全局变量。 可以将页面的 window 导入我们的模块, 直接给它定义属性, 通过这种方式可以暴露全局变量 :

1
2
3
(function($, exports){
exports.Foo = "wem";
})(jQuery, window);

添加少量上下文

使用局部上下文是一种架构模块很有用的方法, 特别是当需要给事件注册回调函数时。

实际情况是, 模块中的上下文都是全局的, this 就是 window :

1
2
3
(function(){
assertEqual( this, window );
})();

如果想自定义作用域的上下文, 则需要将函数添加至一个对象中, 比如 :

1
2
3
4
5
6
7
(function(){
var mod = {};
mod.contextFunction = function(){
assertEqual( this, mod );
};
mod.contextFunction();
})();

在 contextFunction() 中的上下文不是全局的, 而是 mod 对象。 这时使用 this 就不必担心会创建全局变量了。 为了展示如何在实际中更好地使用这种技术, 我们对例子做进一步修改 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(function ($) {
var mod = {};
mod.load = function (func) {
$($.proxy(func, this));
};
mod.load(function () {
this.view = $("#view");
});
mod.assetsClick = function (e) {
// 处理点击
};
mod.load(function () {
this.view.find(".assets").click(
$.proxy(this.assetsClick, this)
);
});
})(jQuery);

这里新建了 load() 函数来处理回调, 当页面加载后执行它。 注意, 我们使用了 jQuery.proxy() 来确保回调是基于正确的上下文执行的。

然后, 当页面加载时, 我们给一个元素添加了点击事件监听, 这里使用了局部函数assetsClick() 作为回调函数。

摘自:《基于MVC的JavaScript Web富应用开发》

JS函数节流/截流

平时作网页的时候,会给window添加resize,scroll事件,这些事件触发频率比较高,如果事件实现比较复杂,性能较低,可以使用节流/截流方式

也不知道谁起的名,一个叫节流,一个叫截流

节流

每间隔一个固定的时间调用一次函数,以$(window).resize()为列:

1
2
3
4
5
6
7
8
9
10
11
$(function() {
var timer = 0;
$(window).resize(function() {
if (!timer) {
timer = setTimeout(function() {
//do something...
timer = 0;
}, 250)
}
}).trigger('resize');//页面初始化的时候立即调用一次
})

节流的一个封装

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 throttle = function (action, delay) {
let timeout = null
let lastRun = 0
return function () {
if (timeout) {
return
}
let elapsed = Date.now() - lastRun
let context = this
let args = arguments
let runCallback = function () {
lastRun = Date.now()
timeout = false
action.apply(context, args)
}
if (elapsed >= delay) {
runCallback()
}
else {
timeout = setTimeout(runCallback, delay)
}
}
}
$(window).resize(throttle(() => {
console.log('我会100ms被调用一次');
}, 100));

截流

实现一个搜索功能,用户输入停止300后触发一次搜索事件。

1
2
3
4
5
6
7
8
var searchTimeout,
searchDelay = 300;
$('#search').on('keyup', function (event) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(function () {
$ajaxForm.triggerHandler('submit');
}, searchDelay);
});

emmet语法

element标签名

1
2
3
div
...will produce:
<div></div>

IDid选择符

1
2
3
div#some-id 或者 #some-id
...will produce:
<div id="some-id"><div>

CLASS类选择符

1
2
3
div.some-class 或者 .some-class
...will produce:
<div class="some-class"></div>

> 嵌套,直接子节点

1
2
3
4
5
6
7
div>ul>li
...will produce:
<div>
<ul>
<li></li>
</ul>
</div>

+兄弟节点

1
2
3
4
5
div+p+bq
...will produce:
<div></div>
<p></p>
<blockquote></blockquote>

^切换上下文,返回上一级

1
2
3
4
5
6
7
div+div>p>span+em^bq
...will produce:
<div></div>
<div>
<p><span></span><em></em></p>
<blockquote></blockquote>
</div>

可以多个一起使用,每一个返回一级

1
2
3
4
5
6
7
div+div>p>span+em^^^bq
...will produce:
<div></div>
<div>
<p><span></span><em></em></p>
</div>
<blockquote></blockquote>

* 重复多个

1
2
3
4
5
6
7
8
9
ul>li*5
...will produce:
<ul>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>

()分组,用来分组子集,不推荐过多使用

1
2
3
4
5
6
7
8
9
10
11
12
13
div>(header>ul>li*2>a)+footer>p
...will produce:
<div>
<header>
<ul>
<li><a href=""></a></li>
<li><a href=""></a></li>
</ul>
</header>
<footer>
<p></p>
</footer>
</div>

()可以嵌套使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(div>dl>(dt+dd)*3)+footer>p
...will produce:
<div>
<dl>
<dt></dt>
<dd></dd>
<dt></dt>
<dd></dd>
<dt></dt>
<dd></dd>
</dl>
</div>
<footer>
<p></p>
</footer>

[]用户自定义属性

1
2
3
div[title="Hello world!"]
...will produce:
<div title="Hello world!" data-name="haha"></div>

说明:

  • 方括号内可以一次性写多个属性,以空格隔开,比如:[title=’H W’ data-name=haha]
  • 属性可以不写属性值,比如:[title data-name]
  • 属性值可以使用单引号或者双引号
  • 属性值如果没有空格,可以省略引号

$数字

1
2
3
4
5
6
7
8
9
ul>li.item$*5
...will produce:
<ul>
<li class="item1"></li>
<li class="item2"></li>
<li class="item3"></li>
<li class="item4"></li>
<li class="item5"></li>
</ul>

可以使用多个$,不足会使用0站位

1
2
3
4
5
6
7
8
9
ul>li.item$$$*5
...will produce:
<ul>
<li class="item001"></li>
<li class="item002"></li>
<li class="item003"></li>
<li class="item004"></li>
<li class="item005"></li>
</ul>

@控制数字开始值和顺序

@- 倒序

1
2
3
4
5
6
7
8
9
ul>li.item$@-*5
...will produce:
<ul>
<li class="item5"></li>
<li class="item4"></li>
<li class="item3"></li>
<li class="item2"></li>
<li class="item1"></li>
</ul>

@n 改变数字开始位置

1
2
3
4
5
6
7
8
9
ul>li.item$@3*5
...will produce:
<ul>
<li class="item3"></li>
<li class="item4"></li>
<li class="item5"></li>
<li class="item6"></li>
<li class="item7"></li>
</ul>

@-n 降序并改变开始位置

1
2
3
4
5
6
7
8
9
ul>li.item$@-3*5
...will produce:
<ul>
<li class="item7"></li>
<li class="item6"></li>
<li class="item5"></li>
<li class="item4"></li>
<li class="item3"></li>
</ul>

{}文本节点

1
2
3
a{Click me}
...will produce:
<a href="">Click me</a>

注意如下的区别(受>影响)

1
2
3
4
5
6
7
a{click}+b{here}
...will produce:
<a href="">click</a><b>here</b>

a>{click}+b{here}
...will produce:
<a href="">click<b>here</b></a>

缺省表签名

1
2
3
4
5
6
#some-id                  div#some-id
.some-class div.some-class
.wrap>.content div.wrap>div.content
em>.info em>span.info
ul>.item*3 ul>li.item*3
table>#row$*4>[colspan=2] table>tr#row$*4>td[colspan=2]

缺省标签名之后,emmet会根据父级标签确定表签名,一些例子:

  • li for ul and ol
  • tr for table, tbody, thead and tfoot
  • td for tr
  • option for select and optgroup

lorem 生成一段虚拟文字,一般用来快速填充html,查看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
lorem
...will produce:
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Alias et nemo repudiandae. Esse ex molestias numquam, possimus quaerat quas sint tempora? Asperiores dicta ducimus facilis impedit neque ratione ut vel!

lorem10 //生成10个单词
...will produce:
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Dolore, totam.

p*4>lorem
...will produce:
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Qui dicta minus molestiae vel beatae natus eveniet ratione temporibus aperiam harum alias officiis assumenda officia quibusdam deleniti eos cupiditate dolore doloribus!</p>
<p>Ad dolore dignissimos asperiores dicta facere optio quod commodi nam tempore recusandae. Rerum sed nulla eum vero expedita ex delectus voluptates rem at neque quos facere sequi unde optio aliquam!</p>
<p>Tenetur quod quidem in voluptatem corporis dolorum dicta sit pariatur porro quaerat autem ipsam odit quam beatae tempora quibusdam illum! Modi velit odio nam nulla unde amet odit pariatur at!</p>
<p>Consequatur rerum amet fuga expedita sunt et tempora saepe? Iusto nihil explicabo perferendis quos provident delectus ducimus necessitatibus reiciendis optio tempora unde earum doloremque commodi laudantium ad nulla vel odio?</p>

ul.generic-list>lorem10.item*4 //10是只每行10个单词
...will produce:
<ul class="generic-list">
<li class="item">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Nam vero.</li>
<li class="item">Laboriosam quaerat sapiente minima nam minus similique illum architecto et!</li>
<li class="item">Incidunt vitae quae facere ducimus nostrum aliquid dolorum veritatis dicta!</li>
<li class="item">Tenetur laborum quod cum excepturi recusandae porro sint quas soluta!</li>
</ul>

注意:

  • 空格是emmet停止转换的分隔符,一个emmt语句中,不能有空格
  • 不要写太复杂的语法,写太复杂了容易出错。

参考连接

emmet官网 http://docs.emmet.io/
emmet语法查询 http://emmet.evget.com/

css选择器

闲来无事,复习一下。

选择器 例子 描述 CSS版本
.class .active 类选择器 1
#id #wrap id选择器 1
* * 通配符,选择所有元素 2
element p 标签选择器 1
element,element div,p 逗号选择符,一次性选择多个元素 1
element element div p 空格选择符,选取所有后代元素 1
element>element div>p 大于号选择符,选取直接子节点 2
element+element div+p 加号选择符,紧接着之后的第一个兄弟元素(中间不能有其他元素),直接下一个。 2
element~element div~p 所有之后的兄弟元素,非兄弟元素不会选中 3
[attr] div[title] 含有title属性的div元素 2
[attr=value] div[title=good] 选择title属性为good的div元素 2
[attr~=value] div[title~=good] title属相包含good 单词 的div元素,比如title=’good boy!’可以,但是title=’goodboy’不可以 2
[attr竖线=value] div[title竖线=go] title属性以go开头的所有div元素,还不算是以某个xx开头,这个选择符只有以下几种情况可以:title=’go’ title=’go-‘ title=’go-od’ title=’go-od boy!’,需要连字符,这个选择器是不是为lang属性发明的? 2
[attr^=value] a[src^=”https”] 开头:选择其 src 属性值以 “https” 开头的所有a元素 3
[attr$=value] a[src$=”.pdf”] 结尾:选择其 src 属性以 “.pdf” 结尾的所有a元素 3
[attr*=value] a[src*=”abc”] 包含:选择其 src 属性中包含 “abc” 子串的所有a元素 3
:link a:link 选择所有未被访问的链接 1
:visited a:visited 选择所有已被访问的链接 1
:active a:active 选择活动链接 1
:hover a:hover 选择鼠标指针位于其上的链接 1
:focus input:focus 选择获得焦点的 input 元素 2
:first-letter p:first-letter 选择每个 p 元素的首字母 1
:first-line p:first-line 选择每个 p 元素的首行 1
:first-child p:first-child 选择属于父元素的第一个子元素的每个 p 元素 2
:before p:before 在每个 p 元素的内容之前插入内容 2
:after p:after 在每个 p 元素的内容之后插入内容 2
:lang(language) p:lang(it) 选择带有以 “it” 开头的 lang 属性值的每个p元素 2
:first-of-type p:first-of-type 选择属于其父元素的首个 p 元素 3
:last-of-type p:last-of-type 选择属于其父元素的最后 p 元素 3
:only-of-type p:only-of-type 选择属于其父元素唯一的 p 元素 3
:only-child p:only-child 选择属于其父元素的唯一子元素 3
:nth-child(n) p:nth-child(2) 选择属于其父元素的第二个子元素 3
:nth-last-child(n) p:nth-last-child(2) 同上,从最后一个子元素开始计数 3
:nth-of-type(n) p:nth-of-type(2) 选择属于其父元素第二个 p 元素 3
:nth-last-of-type(n) p:nth-last-of-type(2) 同上,但是从最后一个子元素开始计数。 3
:last-child p:last-child 选择属于其父元素最后一个子元素 3
:root :root 选择文档的根元素 3
:empty p:empty 选择没有子元素的 p 元素(包括文本节点) 3
:target #news:target 选择当前活动的 #news 元素 3
:enabled input:enabled 选择每个启用的 input 元素 3
:disabled input:disabled 选择每个禁用的 input 元素 3
:checked input:checked 选择每个被选中的 input 元素 3
:not(selector) :not(p) 选择非 p 元素的每个元素 3
::selection ::selection 选择被用户选取的元素部分 3

webpack文件名相关配置和按需加载

output.filename

filename是对应于entry里面生成出来的文件名。比如:

1
2
3
4
5
6
7
8
9
{
entry: {
"index": "./index.jsx"
},
output: {
filename: "[name].min.js",
chunkFilename: "[name].min.js"
}
}

生成出来的文件名为index.min.js。

output.chunkname

chunkname 是未被列在entry中,却又需要被打包出来的文件命名配置,一般在按需加载模块时候,会生成文件,这个文件名就是使用output.chunkname进行配置的.一般配置成:

1
2
3
4
output: {
filename: "[name].min.js",
chunkFilename: "[name].[chunkhash:8].min.js"
}

非entry,但是需要单独打包出来的文件名配置,添加[chunkhash:8] 每次文件内容改变后,文件名都会变.防止浏览器缓存不更新.

webpack按需加载写法

1
2
3
4
require.ensure(["modules/demo.jsx"], function(require) {
var a = require("modules/demo.jsx");
// ...
}, 'demo');

这样除了entry之外会生成的文件demo.asd88sss.min.js(asd88sss我乱写的,webpack会根据文件内容生成[chunkhash:8]).

require.ensure() API的第三个参数是给这个模块命名,否则 chunkFilename: “[name].[chunkhash:8].min.js” 中的 [name] 是一个自动分配的、可读性很差的id(就是一个数字)

结合React-Router按需加载写法

1
2
3
4
5
6
7
8
9
10
childRoutes: [
{ path: '/login',
getComponent: (location, cb) => {
require.ensure([], (require) => {
cb(null, require('../components/Login'))
})
}
}
// ...
]

react-router 代码片段 其实就是getComponent函数内部使用了require.ensure()

如果使用按需加载(require.ensure),本文件中就不要使用import再引入相应得模块,否这webpack不会单独打包出一个文件。

js状态机实现tab切换页面

tab页的状态机方式实现,不多说直接上代码
源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>jQuery 选项卡</title>
<style>
*{
padding:0;
margin:0;
}
body{
padding:15px;
}
#tabs{

}
#tabs>li{
width:100px;
height:30px;
line-height: 30px;
text-align: center;
float:left;
list-style-type: none;
border:1px solid #ccc;
border-bottom:0;
cursor:pointer;
}
#tabs>li:first-child{
border-top-left-radius:5px;
border-right:0;
}
#tabs>li:last-child{
border-top-right-radius:5px;
border-left:0;
}
#tabs>li.active,#tabs>li:hover{
background-color:#eee;
}
#tabs-content>div{
display:none;
width:500px;
height:200px;
border:1px solid #ccc;
border-bottom-right-radius: 5px;
border-bottom-left-radius: 5px;
border-top-right-radius: 5px;
}
#tabs-content>div.active{
display: block;
}
</style>

</head>
<body>
<div id="tabs-wrap">
<ul id="tabs">
<li data-tab="users" class='active'>Users</li>
<li data-tab="groups">Groups</li>
<li data-tab="position">Position</li>
</ul>
<div style="clear:both;"></div>
<div id="tabs-content">
<div data-tab="users" class='active'>Users...</div>
<div data-tab="groups">Groups...</div>
<div data-tab="position">Position...</div>
</div>
</div>


<script src="jquery-1.10.2.min.js"></script>
<script type="text/javascript">
/*
通过自定义事件方式实现
选项卡状态切换回调彼此分离,代码更具有扩展性
js值改变相应的active类,显示隐藏通过css控制
*/
jQuery.fn.tabs = function(control){
var element = $(this);
control = $(control);
element.on('click', 'li', function(){
var tabName = $(this).attr('data-tab');
//点击选项卡时触发自定义事件
element.trigger('change.tabs', tabName);
});
//绑定自定义事件
element.on('change.tabs', function(e, tabName){
element.find('li').removeClass('active');
element.find('>[data-tab="'+tabName+'"]').addClass('active');
});
element.on('change.tabs', function(e, tabName){
control.find('>[data-tab]').removeClass('active');
control.find('>[data-tab="'+tabName+'"]').addClass('active')
});
var firstName = element.find('li:first').attr('data-tab');
element.trigger('change.tabs', firstName);
return this;
};
// 切换选项卡的动作和窗口的hash做关联,支持浏览器的前进后退按钮。
$('ul#tabs').on('change.tabs', function(e, tabName){
window.location.hash = tabName;
})
$(window).on('hashchange', function(){
var tabName = window.location.hash.slice(1);
$('ul#tabs').trigger('change.tabs', tabName);
})
// 初始化插件
$('ul#tabs').tabs('#tabs-content');
// 选中某个选项卡
$('ul#tabs').trigger('change.tabs', 'position');
</script>
</body>
</html>

html的viewport meta标签

移动设备上浏览器一般会加如下meta标签,会有更好的显示效果

1
<meta name="viewport" content="width=device-width, initial-scale=1.0">

注:width=device-width 会导致 iPhone 5 添加到主屏后以 WebApp 全屏模式打开页面时出现黑边 戳这里

content 参数:

  • width viewport 宽度(数值/device-width)
  • height viewport 高度(数值/device-height)
  • initial-scale 初始缩放比例
  • maximum-scale 最大缩放比例
  • minimum-scale 最小缩放比例
  • user-scalable 是否允许用户缩放(yes/no)
  • minimal-ui iOS 7.1 beta 2 中新增属性,可以在页面加载时最小化上下状态栏。这是一个布尔值,可以直接这样写:
    1
    <meta name="viewport" content="width=device-width, initial-scale=1, minimal-ui">

其他

而如果你的网站不是响应式的,请不要使用 initial-scale 或者禁用缩放。便于移动设备访问你的网站的时候,用户可以通过缩放查看你的网站。

1
<meta name="viewport" content="width=device-width,user-scalable=yes">

适配 iPhone 6 和 iPhone 6plus 则需要写:

1
2
<meta name="viewport" content="width=375">
<meta name="viewport" content="width=414">

不同手机viewport width尺寸

操作系统 屏幕尺寸 viewport width
android 4.7~5寸 360px
IOS 4.7~5寸(iPhone 6) 375px
android 5.5寸 400px
IOS 5.5寸(iPhone 6 plus) 414px

html常用meta标签

常用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
声明文档使用的字符编码,
<meta charset="utf-8">

优先使用 IE 最新版本和 Chrome
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />

360 使用Google Chrome Frame
<meta name="renderer" content="webkit">

页面关键词 keywords
<meta name="keywords" content="your keywords">

页面描述内容 description
<meta name="description" content="your description">

定义网页作者 author
<meta name="author" content="author,email address">

百度禁止转码
通过百度手机打开网页时,百度可能会对你的网页进行转码,脱下你的衣服,往你的身上贴狗皮膏药的广告,为此可在 head 内添加
<meta http-equiv="Cache-Control" content="no-siteapp" />

viewport 可以让布局在移动浏览器上显示的更好。 通常会写
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">

移动端的meta

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
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="format-detection"content="telephone=no, email=no" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="apple-mobile-web-app-capable" content="yes" /><!-- 删除苹果默认的工具栏和菜单栏 -->
<meta name="apple-mobile-web-app-status-bar-style" content="black" /><!-- 设置苹果工具栏颜色 -->
<meta name="format-detection" content="telphone=no, email=no" /><!-- 忽略页面中的数字识别为电话,忽略email识别 -->
<!-- 启用360浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 避免IE使用兼容模式 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- 针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓 -->
<meta name="HandheldFriendly" content="true">
<!-- 微软的老式浏览器 -->
<meta name="MobileOptimized" content="320">
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait">
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait">
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes">
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true">
<!-- UC应用模式 -->
<meta name="browsermode" content="application">
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app">
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no">
<!-- 适应移动端end -->

HTML DOCTYPE

介绍

DOCTYPE(Document Type),该声明位于文档中最前面的位置,处于 html 标签之前,此标签告知浏览器文档使用哪种 HTML 或者 XHTML 规范。

DTD(Document Type Definition) 声明以 <!DOCTYPE> 开始,不区分大小写,前面没有任何内容,如果有其他内容(空格除外)会使浏览器在 IE 下开启怪异模式(quirks mode)渲染网页。

作用

在 HTML中 doctype 有两个主要目的。

  • 对文档进行有效性验证,它告诉用户代理和校验器这个文档是按照什么 DTD 写的。这个动作是被动的,每次页面加载时,浏览器并不会下载 DTD 并检查合法性,只有当手动校验页面时才启用。
  • 决定浏览器的呈现模式,对于实际操作,通知浏览器读取文档时用哪种解析算法。如果没有写,则浏览器则根据自身的规则对代码进行解析,可能会严重影响 html 排版布局。浏览器有三种方式解析 HTML 文档。 非怪异(标准)模式 怪异模式 * 部分怪异(近乎标准)模式 关于IE浏览器的文档模式,浏览器模式,严格模式,怪异模式,DOCTYPE 标签,可详细阅读模式?标准!的内容。

格式

公共 DTD,名称格式为 注册//组织//类型 标签//语言,

  • 注册指组织是否由国际标准化组织(ISO)注册,+表示是,-表示不是。
  • 组织即组织名称,如:W3C。
  • 类型一般是 DTD。
  • 标签是指定公开文本描述,即对所引用的公开文本的唯一描述性名称,后面可附带版本号。
  • 语言是 DTD 语言的 ISO 639 语言标识符,如:EN 表示英文,ZH 表示中文。

类型

XHTML 1.0 可声明三种 DTD 类型。分别表示严格版本,过渡版本,以及基于框架的 HTML 文档。

  • HTML 4.01 strict 严格版本

    1
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
  • HTML 4.01 Transitional 过渡版本

    1
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  • HTML 4.01 Frameset 基于框架的 HTML 文档

    1
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">

最新 HTML5 推出更加简洁的书写,它向前向后兼容,推荐使用。

1
<!doctype html>

基于hexo 和 github 搭建的博客

hexo-blog

基于hexo搭建的博客 详细文档请参考官网

安装

1
2
3
4
npm install hexo-cli -g
git clone https://github.com/zkboys/hexo-blog.git
cd hexo-blog
npm install

修改网站信息

1
2
3
4
5
6
7
# Site
title: ZKBOYS
subtitle: I'm a 博客
description: 技术整理归档
author: WangShubin
language: zh-CN
timezone:

创建新文章

1
hexo new 'My new Post'

将会生成/source/_posts/My-new-Post.md文件,内容如下:

1
2
3
4
5
---
title: My new Post //文章列表标题 网页<title>
date: 2016-03-09 15:59:25
tags:
---

生成public静态资源(网站的实际内容)

1
hexo generate

删除public目录使用hexo clean

发布

修改_config.yml

1
2
3
4
5
deploy:
type: git
repo: <repository url>
branch: [branch]
message: [message]

安装发布插件

1
npm install hexo-deployer-git --save

发布

1
hexo deploy

npm scripts

clean + generate + deploy
package.json中加入

1
2
3
4
5
"scripts":{
"deploy":"hexo clean && hexo generate && hexo deploy",
"push":"git add --all && git commit -m 'blog update' && git push origin master",
"done":"npm run push && npm run deploy"
}

使用方法

1
2
3
4
5
6
发布
npm run deploy
提交
npm run push
提交并发布:
npm run done

注:如果项目没有任何更改,调用npm run push命令会报错

一些技术文档

NodeJS API
http://nodeapi.ucdok.com/#/api/

FreeMarker
模版语言
官方有中文文档下载连接
http://freemarker.incubator.apache.org/index.html

vue.js
http://cn.vuejs.org/
数据驱动的组件,为现代化的 Web 界面而生

sui
http://sui.taobao.org/sui/docs/index.html
SUI 是一套基于bootstrap开发的前端组件库,同时她也是一套设计规范。通过SUI,可以非常方便的设计和实现精美的页面

sui mobile
http://m.sui.taobao.org/
轻量,小巧且精美的UI库;方便迅速搭建手机H5应用

ant.design
http://ant.design/
Ant Design 是一个 UI 设计语言,是一套提炼和应用于企业级后台产品的交互语言和视觉体系。

zepto
http://gmu.baidu.com/doc/2.0.5/
http://zeptojs.com/
Zepto是一个轻量级的针对现代浏览器的JS库,兼容jQuery用法

webpack
http://webpack.github.io/
http://zhaoda.net/webpack-handbook/
前端构建工具

gulp
http://www.gulpjs.com.cn/docs
https://github.com/gulpjs/gulp/tree/master/docs
前端构建工具

React
http://reactjs.cn/
http://facebook.github.io/react/
用于构建用户界面的JAVASCRIPT库

React Flux
http://reactjs.cn/react/docs/flux-overview.html
http://facebook.github.io/flux/docs/overview.html
Flux是Facebook用来构建用户端的web应用的应用程序体系架构。

React Router
https://github.com/rackt/react-router/tree/master/docs

History
https://github.com/rackt/history

SuperAnger
http://visionmedia.github.io/superagent/
Super Agent is light-weight progressive ajax API crafted for flexibility, readability, and a low learning curve after being frustrated with many of the existing request APIs. It also works with Node.js!

Font Awesome
http://fontawesome.io/

《ECMAScript 6入门》
http://es6.ruanyifeng.com/
《ECMAScript 6入门》是一本开源的JavaScript语言教程,全面介绍ECMAScript 6新引入的语法特性。

Fetch
https://github.com/github/fetch
将会代替ajax(XHR)?