TeaCoder

生命不息,代码不止


  • 首页

  • 分类

  • 归档

  • 标签

  • 搜索
close

如何在CentOS7上安装Python3并设置本地编程环境

发表于 2018-07-08   |   分类于 后端

本文基于引用文章(见尾部)和个人搭建python3.7.0环境的过程进行总结和分享。

安装环境说明

  • 主机:centos7
  • 安装python版本:3.7.0

安装依赖环境

1
# yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel

-y参数:自动选择,不弹出选择对话,默认yes

若不先安装依赖环境,在后面编译的步骤会报错缺少依赖,如:找不到zlib包

1
zipimport.ZipImportError: can't decompress data; zlib not available

注意:上面尾部的包libffi-devel,是python3.7版本的新依赖,若此包没安装,编译时会报错

1
ModuleNotFoundError: No module named '_ctypes'

获取python源文件

1
wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz

解压缩

1
tar -zxvf Python-3.7.0.tgz

执行configure检测安装平台特征

./configure是用来检测你的安装平台的目标特征的。比如它会检测你是不是有CC或GCC,并不是需要CC或GCC,它是个shell脚本。

这一步一般用来生成 Makefile,为下一步的编译做准备

1
2
cd Python-3.7.0
./configure

不出意外,会提示

1
configure: error: no acceptable C compiler found in $PATH

原因是没有安装gcc,因为python是用C写的,所以需要用gcc进行编译,所以需要先安装gcc

1
2
# 顺道把c++编译器也安装了
yum install gcc gcc-c++

再次执行./configure

几分钟之内发现一系列的checking日志被打出(觉得检测时间太久此步可忽略):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
checking linux/tipc.h presence... yes
checking for linux/tipc.h... yes
checking linux/random.h usability... yes
checking linux/random.h presence... yes
checking for linux/random.h... yes
checking spawn.h usability... yes
checking spawn.h presence... yes
checking for spawn.h... yes
checking util.h usability... no
checking util.h presence... no
checking for util.h... no
checking alloca.h usability... yes
checking alloca.h presence... yes
checking for alloca.h... yes
checking endian.h usability... yes ....

ok,没有报错进入下一步编译

执行make进行编译,执行make install进行安装

1
2
# (注:make install足够的权限)
make && make install
  • make:编译,可能遇到的错误:make * 没有指明目标并且找不到 makefile。 没有Makefile,解决方法是要先./configure 一下生成Makefile。

  • make install:将程序安装至系统中。如果原始码编译无误,且执行结果正确,便可以把程序安装至系统预设的可执行文件存放路径。如果用bin_PROGRAMS宏的话,程序会被安装至/usr/local/bin这个目录。

查看是否安装成功:

1
2
python3 -V // 输出 Python3.7.0
python -V // 输出 python2.7.6

设置虚拟环境

成功安装python后,我们需要为python项目创建虚拟环境。

虚拟环境为Python项目创建一个隔离空间,确保每个项目都有自己的一组依赖项,这些依赖项不会破坏任何其他项目。

设置编程环境使我们能够更好地控制Python项目以及如何处理不同版本的包。在使用第三方软件包时,这一点尤为重要。

新建项目目录

1
2
3
cd ~
mkdir myproject
cd myproject

运行以下命令来创建独立环境:

1
python3 -m venv my_env

本质上,此命令创建一个新目录(在本例中称为my_env),其中包含我们可以使用以下ls命令查看的一些项:

1
bin include lib lib64 pyvenv.cfg

到此为止,环境创建完成,要使用该虚拟环境,还需要执行激活虚拟环境命令

1
source my_env/bin/activate

可以看到linux操作提示符前缀变为

1
(my_env) [root@localhost myproject]#

这个前缀让我们知道环境my_env当前是活动的,这意味着当我们在这里创建程序时,它们将只使用这个特定环境的设置和包。

注意:我们使用python3创建的虚拟环境,在虚拟环境类,我们的python版本默认为python3.7.0,退出虚拟环境后python默认版本仍为2.7版本。

1
(my_env) [root@localhost bin]# python -V // 输出Python 3.7.0

1
[root@localhost ~]# python -V // 输出Python 2.7.5

想退出python虚拟环境,只需执行deactivate命令

1
(my_env) [root@localhost myproject]# deactivate

参考链接:https://www.digitalocean.com/community/tutorials/how-to-install-python-3-and-set-up-a-local-programming-environment-on-centos-7

移动端H5多页开发拍门砖经验

发表于 2018-05-17   |   分类于 前端

H5多页
两年前刚接触移动端开发,刚开始比较疑惑,每次遇到问题都是到社区里提问或者吸取前辈的经验分享,感谢热衷于分享的开发者为前端社区带来欣欣向上的生命力。本文结合先前写的文章和开发经验分享给大家,希望也能帮助刚步入移动端开发的新人解惑。以下会以其中一个以公积金页面开发项目作为例子,介绍移动端的一些常见问题和使用Vuejs作为lib进行多页开发的经验。

移动端自适应布局

在项目中移动端最常用的自适应布局方案就是flexbox结合rem。规范的分栏式使用flexbox,其他大部分不规则视图使用rem,对于rem最常用的方案就是淘宝开源的可伸缩布局方案。

根据设备设备像素比设置scale的值(scale = 1 / deviceRatio),这样可以保持视口device-width始终等于设备物理像素,接着根据屏幕大小动态计算根字体大小,具体是将屏幕划分为100等分,每份为a,1rem就等于10a。

标注

通常我们会拿到750宽的设计稿,这是基于iPhone6的物理分辨率。有的设计师也许会偷懒,设计图上面没有任何的标注,如果我们边开发边量尺寸,无疑效率是比较低的。要么让设计师标注上,要么自食其力。

如果设计师实在没有时间,推荐使用markman进行标注,免费版阉割了一些功能(比如无法保存本地)不过基本满足了我们的需求了。

后来我发现比markman更好的标注工具PxCook,该工具可以显示PSD设计图中的图层的样式代码,对于前端来说简直方便极了。

标注完成后开始写我们的样式,使用了淘宝的lib-flexible库之后,我们的根字体基准值就为750/100*10 = 75px。此时我们从图中若某个标注为100px,那么css中就应该设置为100/75 = 1.333333rem。所以为了提高开发效率,可以使用px转化为rem的插件。下面是sublimeText和Vscode的转换插件:

px转rem插件

  • sublimeText插件:rem-unit
    rem-unit

  • Vscode插件: cssrem
    pxtorem

使用rem的几点总结

  • 在所有的单位中,font-size推荐使用px,然后结合媒体查询进行重要节点的控制,这样可以满足突出或者弱化某些字体的需求,而非整体调整。
  • 众向的单位可以全部使用px,横向的使用rem,因为移动设备宽度有限,而高度可以无限向下滑动。但这也有特例,比如对于一些活动注册页面,需要在一屏幕内完全显示,没有下拉,这时候所有众向或者横向都应该使用rem作为单位。如图:

shili

左图的表单高度单位由于下边空距较大,使用px在不同屏幕显示更加;而右边的活动注册页由于不能出现滚动条,所有的众向高度、margin、padding都应该使用rem。

  • border、box-shadow、border-radius等一些效果应该使用px作为单位。

手机状态栏和浏览器导航栏的影响

之前发布的文章中,有个SF的前端小伙伴提出的问题:
文中作者有重点强调布局全部铺满,和下方与很多空隙的处理方案是不同的,在工作中我遇到这种情况,设计师的设计稿宽度为750×1334,但实际的展示高度并没有那么多,因为上方有导航栏还包括手机自己的状态栏展示,所以整体高度就达不到750,但是设计师设计稿是严格按照750进行设计的,这种情况下使用rem,严格按照设计师尺寸进行还原就会出现屏幕出现滚动条情况,请问针对这种情况您是怎么处理的?是从设计稿上规范,还是从开发上有相应的措施

依旧以我的分享界面为例:
展示高度不同通常发生在微信及浏览器端,因为前者没有地址栏和工具栏,这样显示高度通常会和设计师设计的视图吻合。那如果按照纯padding,margin即使全部使用rem,在浏览器端依旧会超出一屏高度,对于分享页面这种不是我们想要看到的。这时候就要做出取舍,我对主体区域采用绝对定位,这样上面间隙虽然小,不过仍能保持在一个屏幕高度显示。若采用margin padding在设置,必然已出现滚动条。当然这样的前提是依赖设计图的,通常设计师会为了空间感有保留一定的间隙,也不会将主要对象高度设的过高,否则太撑满也不好看,开发上如果设计图宽高没有在一定界限之内,超出也无法避免,不过我们这种分享界面通常是通过微信分享好友,通过浏览器打开的视图效果出现滚动条其实也不怎么影响不是么?
下面附上微信端和浏览器端的效果图:

微信端:微信端

浏览器端: 浏览器端

Vuejs作为lib开发移动端页面

为何不使用SPA模式

一般移动端使用vue是为了数据交互频繁而且快速开发的页面,为什么不使用单页SPA开发模式,原因大概几点。

  • 为了快速开发,快速上线
  • 项目其他成员不熟悉SPA,不熟悉webpack
  • 参与项目时项目已使用多页开发,短时间无法重构

抛开使用单页的架构,开发多页应用时,一个页面交互逻辑与一个Vue实例对应。

基于接口返回数据的属性注入

“基于接口返回数据的属性注入”是个人创建的话术,抛开此概念,先说一下表单数据的绑定方式。

表单的数据绑定

一个重要的点是有几份表单就分开几个表单对象进行数据绑定。

以上图公积金查询为例,由于不同城市会有不同的查询要素,可能登陆方式只有一种,也可能有几种。比如上图有三种登陆方式,在使用vue布局时,有两种方案。

  • 1、 只建立一个表单用于数据绑定,点击按钮触发判断
  • 2、有几种登陆方式建立几个表单,用一个字段标识当前显示的表单

由于使用第三方的接口,一开始也没有先进行接口返回数据结构的查看,采用了第一种错误的方式,错误一是每种登陆方式下面的登陆要素的数量也不同,错误二是数据绑定在同一个表单data下,当用户在用户名登陆方式输入用户名密码后,切换到客户号登陆方式,就会出现数据错乱的情况。

解决完布局问题后,我们需要根据设计图定义一些状态,比如当前登陆方式的切换、同意授权状态的切换、按钮是否可以点击的状态、是否处于请求中的状态。当然还有一些app穿过来的数据,这里就忽略了。

1
2
3
4
5
6
7
8
data: {
tags: {
arr: [''],
activeIndex: 0
},
isAgreeProxy: true,
isLoading: false
}

接着审查一下接口返回的数据,推荐使用chrome插件postman,比如呼和浩特的登陆要素如下:

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
{
"code": 2005,
"data": [
{
"name": "login_type",
"label": "身份证号",
"fields": [
{
"name": "user_name",
"label": "身份证号",
"type": "text"
},
{
"name": "user_pass",
"label": "密码",
"type": "password"
}
],
"value": "1"
},
{
"name": " login_type",
"label": "公积金账号",
"fields": [
{
"name": "user_name",
"label": "公积金账号",
"type": "text"
},
{
"name": "user_pass",
"label": "密码",
"type": "password"
}
],
"value": "0"
}
],
"message": "登录要素请求成功"
}

可以看到呼和浩特有两种授权登陆方式,我们在data中定义了一个loginWays,初始为空数组,接着methods中定义一个请求接口的函数,里面就是基于返回数据的基础上为上面fields对象注入一个input字段用于绑定,这就是所谓的基于接口返回数据的属性注入。

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
methods: {
queryloginWays: function(channel_type, channel_code) {
var params = new URLSearchParams();
params.append('channel_type', channel_type);
params.append('channel_code', channel_code);
axios.post(this.loginParamsProxy, params)
.then(function(res) {
console.log(res);
var code = res.code || res.data.code;
var msg = res.message || res.data.message;
var loginWays = res.data.data ? res.data.data : res.data;
// 查询失败
if (code != 2005) {
alert(msg);
return;
}
// 添加input字段用于v-model绑定
loginWays.forEach(function(loginWay) {
loginWay.fields.forEach(function(field) {
field.input = '';
})
})
this.loginWays = loginWays;
this.tags.arr = loginWays.map(function(loginWay) {
return loginWay.label;
})
}.bind(this))
}
}

即使返回的数据有我们不需要的数据也没有关系,这样保证我们不会遗失进行下一步登陆所需要的数据。

这样多个表单绑定数据问题解决了,那么怎么进行页面间数据传递?如果是app传过来,那么通常使用URL拼接的方式,使用window.location.search获得queryString后再进行截取;如果通过页面套入javaWeb中,那么直接使用”${字段名}”就能获取,注意要js中获取java字段需要加双引号。

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
computed: {
// 真实姓名
realName: function() {
return this.getQueryVariable('name') || ''
},
// 身份证
identity: function() {
return parseInt(this.getQueryVariable('identity')) || ''
},
/*If javaWeb
realName: function() {
return this.getQueryVariable('name') || ''
},
identity: function() {
return parseInt(this.getQueryVariable('identity')) || ''
}*/
},
methods: {
getQueryVariable: function(variable) {
var query = window.location.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) == variable) {
return decodeURIComponent(pair[1]);
}
}
console.log('Query variable %s not found', variable);
}
}

关于前端跨域调试

在进行接口请求时,我们的页面通常是在sublime的本地服务器或者vscode本地服务器预览,所以请求接口会遇到跨域的问题,如果使用Gulp进行打包,可以使用插件http-proxy-middleware,或者使用nginx。

使用Gulp

在项目构建的时候通常我们源代码会放在src文件夹下,然后使用gulp进行代码的压缩、合并、图片的优化(根据需要)等等,我们会使用gulp。

解决跨域的问题可以用gulp-connect结合http-proxy-middleware,此时我们在gulp-connect中的本地服务器进行预览调试。

gulpfile.js如下: 开发过程使用gulp server:dev命令,监听文件改动并使用livereload刷新,并且代理src目录;使用gulp命令进行打包;使用gulp server:dist代理dist生产目录。

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
var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var autoprefixer = require('gulp-autoprefixer');
var useref = require('gulp-useref');
var connect = require('gulp-connect');
var proxyMiddleware = require('http-proxy-middleware');
// 开发跨域代理 将localhost:8088/api 映射到 https://api.xxxxx.com/
gulp.task('server:dev', ['listen'], function() {
var middleware = proxyMiddleware(['/api'], {
target: 'https://api.xxxxx.com/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
});
connect.server({
root: env == 'dev' ? './src' : './dist',
port: 8088,
livereload: true,
middleware: function(connect, opt) {
return [middleware]
}
});
});
// 打包后跨域代理
gulp.task('server:dist', ['listen'], function() {
var middleware = proxyMiddleware(['/api'], {
target: 'https://api.xxxxx.com/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
});
connect.server({
root: './dist',
port: 8088,
livereload: true,
middleware: function(connect, opt) {
return [middleware]
}
});
});
gulp.task('html', function() {
gulp.src('src/*.html')
.pipe(useref())
.pipe(gulp.dest('dist'));
});
gulp.task('css', function() {
gulp.src('src/css/main.css')
.pipe(concat('main.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/css/'));
gulp.src('src/css/share.css')
.pipe(concat('share.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/css/'));
gulp.src('src/vendors/css/*.css')
.pipe(concat('vendors.min.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/vendors/css'));
return gulp
});
gulp.task('js', function() {
return gulp.src('src/vendors/js/*.js')
.pipe(concat('vendors.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/vendors/js'));
});
gulp.task('img', function() {
gulp.src('src/imgs/*')
.pipe(gulp.dest('dist/imgs'));
});
gulp.task('listen', function() {
gulp.watch('./src/css/*.css', function() {
gulp.src(['./src/css/*.css'])
.pipe(connect.reload());
});
gulp.watch('./src/js/*.js', function() {
gulp.src(['./src/js/*.js'])
.pipe(connect.reload());
});
gulp.watch('./src/*.html', function() {
gulp.src(['./src/*.html'])
.pipe(connect.reload());
});
});
gulp.task('default', ['html', 'css', 'js', 'img']);

使用nginx

在nginx配置使用proxy_pass,需要注意一点:
如果在proxy_pass后面的url加/,表示绝对根路径;如果没有/,表示相对路径,把匹配的路径部分也给代理走。

1
2
3
4
5
6
7
8
9
10
server {
listen 80;
server_name localhost;
root [Your project root];
index index.html index.htm default.html default.htm;
location ^~/api {
proxy_pass https://api.xxxxx.com/;
}
}

公众号网页的调试

如果你开发的H5基于微信jsSDK,你一定接触过微信授权域名,微信会将授权数据传给一个回调页面,而回调页面必须在你配置的域名下(含子域名)。比如我们获取用户的openid操作。而微信配置域名回去该域名根目录下检测一个xxx_verify_xxx.txt文件,确保该域名是属于你的。

所以要想在微信开发调试工具中获取openid,我们需要使用一种叫做内网穿透的工具。下面是自己比较常用的两个工具:

  • ngrok
  • 花生壳

ngrok

ngrok执行命令

1
ngrok -config ngrok.cfg start web

在ngrok.exe目录需要一个配置文件ngrok.cfg
以下是配置示例:

1
2
3
4
5
6
7
8
server_addr: "tunnel.cn:4443"
trust_host_root_certs: false
tunnels:
web:
subdomain: "xxx"
proto:
http: 8086
https: 8086

启动后xxx.tunnel.cn:4443会指向你本地的8086端口,将xxx_verify_xxx.txt文件放到8086端口根目录即可配置授权域名成功。

花生壳

花生壳免费版对于个人开通仅需6元,然后每月会提供给你1G的流量,免费版不支持80端口,最多支持两个域名,需要下载桌面客户端。

添加域名映射很简单,免费版无法配置自定义域名,由花生壳自动生成。
花生壳
配置成功后启动客户端可查看当前的状态

感谢阅读,欢迎任何形式的技术提问和交流!

  • 邮箱:me@huzerui.com
  • 知乎:晚风轻拂
  • 掘金

前端都了解的配色策略

发表于 2017-10-15   |   分类于 前端 , 设计

在色彩设计应用中,我们对颜色不同程度的理解,影响到设计页面的表现。可是这跟前端有什么联系么?不是设计师的事情么?

可试想一下如果某些情况下我们需要脱离脱离设计师,比如我们需要做些赏心悦目的示例分享,除了将交互、逻辑做好,没有优雅的配色和设计,可能你的分享会显得苍白无力,下面直入正题。

颜色理论(Color theory)

三原色:指色彩中不能再分解的三种基本颜色,我们通常说的三原色,即红、黄、蓝。三原色可以混合出所有的颜色,同时相加为黑色,黑白灰属于无色系。色彩中颜料调配三原色混合色为黑色。

红+黄=橙、黄+蓝=绿、蓝+红=紫

色环(Color Wheel):又称色轮、色圈,是将可见光区域的颜色以圆环来表示,为色彩学的一个工具,一个基本色环通常包括12种不同的颜色。
色环形成
色坏

配色策略

在理解的色环之后,在前人的总结和指引下,就衍生出了一些配色策略。注意每一种配色策略都可以因饱和度、明度的变化产生数不清的配色方案。比如下面的相似色两种配色方案,基本颜色就是四种(主色、辅色、点睛、背景)。

可以发现它们对应颜色的色相变化值都不超过30,在提升或降低饱和度之后形成的颜色变化从而形成不同配色方案。

配色策略提供的一种指引,具体的颜色值确定靠的更多是灵感和视觉审美。

单色(Monochromatic)

Monochromatic

相似色(Analogous)

Analogous
Analogous

互补色(Complementary)

如:红绿对比、蓝橙、黄紫对比
Complementary

分割互补色(Split complementary)

Split complementary

三角对立(Triadic )

Triadic

矩形(Tetradic)

Tetradic

关于主色调、辅色调、点睛色、背景色

主色调
页面色彩的主要色调、总趋势,其他配色不能超过该主要色调的视觉面积。(背景白色不一定根据视觉面积决定,可以根据页面的感觉需要。)

辅色调
仅次与主色调的视觉面积的辅助色,是烘托主色调、支持主色调、起到融合主色调效果的辅助色调。

点睛色
在小范围内点上强烈的颜色来突出主题效果,使页面更加鲜明生动。

背景色
衬托环抱整体的色调,协调、支配整体的作用。

配色工具

Adobe出品的一个在线配色工具,上边列出了海量的配色方案供我们使用,如果你不满足,也可以试下上传图片建立自己的配色方案,只需上传一张图片,网站便可自动提取图片颜色生成配色方案。

网页版地址:https://color.adobe.com/zh/create/color-wheel/
除了创建自己的配色方案,可以参考他人的配色方案并且提供下载编辑的功能,十分好用。
 explore

Kuler也有PS插件,使用方式参考这篇文章教你使用最受欢迎的配色小工具Kuler。

其他配色工具:
Spectrum:Mac + iOS 配色工具
Paletton:The Color Scheme Designer
Color Scheme Designer:高级在线配色工具

更多的配色工具参考:推荐配色工具、国外最好的22个配色网站

网页设计配色文章

一、秒变配色高手!怎么都不会错的6条网页设计配色原则
二、网页设计常用色彩搭配表
三、如何巧用色彩打造动人心弦的网页设计

基于bmob后端云微信小程序开发

发表于 2017-10-14   |   分类于 前端

人的一生90%的时间都在做着无聊的事情,社会的发展使得我们的闲暇时间越来越多,我们把除了工作的其他时间放在各种娱乐活动上 。

程序员有点特殊,他们把敲代码看成娱乐活动的一部分,以此打发时间的不占少数。这不最近无聊搞了一个口袋吉他小程序,使用bmob后端云提供数据存储服务,除吉他谱图片,其他图片存储在七牛。

关于bmob小程序开发文档请戳这里,文档详细简练,主要是缩短了开发周期,不过对于复杂的项目,还是推荐使用自己服务器提供数据服务。

使用微信扫描二维码预览

qrcode
源码地址:https://github.com/alex1504/wx-guita_tab

下面分点分享下小程序的开发过程中的关键点及感受,说明:

  1. 小程序标签统称组件,Html标签统称元素。
  2. 部分内容会与vuejs及jQuery作对比

使用iconfont字体图标

新建项目并添加图标
iconfont
在app.wxss中以unicode方式引入

1
2
3
4
5
6
7
8
@font-face {
font-family: 'iconfont'; /* project id 431644 */
src: url('//at.alicdn.com/t/font_431644_aahynh26y6lp7gb9.eot');
src: url('//at.alicdn.com/t/font_431644_aahynh26y6lp7gb9.eot?#iefix') format('embedded-opentype'),
url('//at.alicdn.com/t/font_431644_aahynh26y6lp7gb9.woff') format('woff'),
url('//at.alicdn.com/t/font_431644_aahynh26y6lp7gb9.ttf') format('truetype'),
url('//at.alicdn.com/t/font_431644_aahynh26y6lp7gb9.svg#iconfont') format('svg');
}

定义通用icon样式,定义伪元素

1
2
3
4
5
6
7
.icon{
display: inline-block;
font-family: 'iconfont';
}
.icon-home::before{
content: "\e600";
}

使用

1
<view class="icon icon-home"</view>

小程序事件绑定及处理器

小程序并没有类似vuejs的v-model进行双向绑定,使用bindinput类似jQuery监听input事件在事件处理器中更新数据,通过event对象e.data.value即可获得input的值。

1
2
// bindconfirm监听键盘回车事件,focus属性聚焦渲染组件时会自动弹出手机软键盘
<input type='text' placeholder='歌曲名 / 歌手' bindinput='bindSearchInput' bindconfirm='onSearch' focus></input>

1
2
3
4
5
bindSearchInput(e) {
this.setData({
searchTxt: e.detail.value
})
}

小程序中的事件处理器并不能像vue一样传入参数,因为事件处理器只有一个默认的参数event对象,在for循环的组件中如果要想获取元素绑定的id,可以通过和jQuery相同的方式绑定data属性。

1
2
3
4
5
6
7
8
<!-- 轮播图 -->
<swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}">
<block wx:for="{{banner_list}}" wx:key="{{index}}">
<swiper-item bindtap="navigateToDetail" data-id="{{item.href}}">
<image src="{{item.image}}" class="slide-image" mode="widthFix"></image>
</swiper-item>
</block>
</swiper>

获取id:

1
2
3
4
//事件处理函数
navigateToDetail: function (e) {
const id = e.currentTarget.dataset.id;
}

阻止事件冒泡

1
bindtap、bindlongtap、bindtouchstart、bindtouchmove、bindtouchend、bindtouchcancle

对应阻止冒泡事件将bind用catch替代

setData

小程序的视图更新需要调用setData修改绑定数据,直接对数据进行修改是不会触发视图层更新的。setData接受一个对象,为需要添加或修改的属性。属性名有点特殊,[]中的值会被识别为变量,因此如果要对对象数组中的某个属性进行修改,只能预先拼接好属性名。
错误做法:

1
2
3
4
5
6
// 视图不更新
this.data.searchSongs[index].love_flag': 2
// SyntaxError: unknown: Unexpected token
this.setData({
'searchSongs[' + index + '].love_flag': 2
})

正确做法:

1
2
3
4
5
6
setSongFlag(e) {
// 注意setData属性名[]中的非整数值会被识别为变量
let key = 'searchSongs[' + index + '].love_flag'
this.setData({
[key]: 2
})

关于image组件

小程序wxss的background-image及image组件都不支持本地url
在H5的开发中,通常我们会将页面一些不需要根据容器大小来选择显示方式的图片使用img标签,需要一些特殊显示方式的使用background。但小程序只需要image组件便可。它提供的mode属性和背景定义图片及img元素控制图片显示方式对比

mode属性 background-size html img元素
scaleToFill 100%,100%(默认) width:100%;height:100%
aspectFit contain js实现
aspectFill cover js实现
widthFix 100%, auto width: 100%;

其他的top、bottom、right、left等不缩放图片调整位置的属性与background-position作用相同,img元素则只能通过定位控制。

小程序API异步方案

如果没有强迫症,小程序API使用默认回调的方式即可;另外由于小程序只支持es6,不支持async及await,也可以将API封装成promise的方式,参考funnycoder的这篇文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function promiseify(func) {
return (args = {}) => {
return new Promise((resolve, reject) => {
func.call(wx, Object.assign(args, {
success: resolve,
fail: reject,
}));
})
}
}
for (const key in wx) {
if (Object.prototype.hasOwnProperty.call(wx, key) && typeof wx[key] === 'function') {
wx[`_${key}`] = promiseify(wx[key]);
}
}

小程序问题

  • 调试器没有css快捷提示功能和颜色面板,影响布局及颜色调整效率(随性派)
  • 无法引入第三方js库
  • 内置组件单调,没有考虑字体数量比较多时的自适应情况
  • 不支持跳转外部链接
  • 背景图片或者image组件不能用本地图片

关于小程序审发布或更新

小程序上线需要经过审核、发布两个过程。
审核通过后有全量更新、或者分阶段发布,小程序才会更新,首次发布没有选项。

全量发布:即时向全量微信用户发布新版小程序。
分阶段发布:新版小程序将在15天内以开发者自定义比例,向微信用户发布更新
详情见知乎:发布小程序时选择全量发布和分阶段发布是什么意思?

不得不说小程序审核速度是非常快的,即便是个人申请(相比以企业账号申请会有应用服务类型限制),通常小程序没有涉及政策不允许的内容或者超过小程序允许的应用服务类型,都是可以顺利通过,初次体验,即便在国庆期间,也是有工作团队进行审核,审核时间通常在几小时内。

未完待续

一直关注着小程序,一直处于不愠不火的状态,但微信团队一直坚持在更新。从小程序的历史更新日志可以看到,无论是开发工具、基础库、与原生硬件交互API都在不断的更新或者修复异常bug,有时间希望做些与硬件交互更有趣的小程序和大家分享。

这个简易小程序将加入评论功能,用户系统功能、曲谱本地收藏、分享、改善图片加载、滑动位置保存等功能及问题,借此熟悉小程序开发以便做出更有趣的东西出来,因此本篇文章随开发过程持续更新。
希望这篇分享对你有所帮助,更希望能与同样热爱前端的你交流心得体会抑或工作经历、困扰等,欢迎知乎私信或邮件交流。

知乎:https://www.zhihu.com/people/huzerui
邮箱:me@huzerui.com

关于Blob、FileReader、FormData的那些事

发表于 2017-07-05   |   分类于 前端

Blob对象是什么

从MDN上有这样一段描述:

一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式。 File 接口基于Blob,继承 blob功能并将其扩展为支持用户系统上的文件。

引用中的关键词是保存着原始数据的类似文件的对象,所谓类似文件的对象可以理解为这种对象本身不是文件,但可以从原始数据解析出文件数据。

既然要解析,就需要知道Blob对象的类型,所以创建Blob的时候第二个参数就是配置项,我们可以配置所有MIME类型,比如将配置项的type制定为’text/xml’,’text/plain’,’text/css’等,Blob构造函数语法如下(参考MDN):

1
var aBlob = new Blob( array, options );

  • array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob.
  • options 是一个可选的Blob熟悉字典,它可能会指定如下两种属性:
    • type,默认值为 “”,它代表了将会被放入到blob中的数组内容的MIME类型。
    • endings,默认值为”transparent”,它代表包含行结束符\n的字符串如何被输出。 它是以下两个值中的一个: “native”,代表行结束符会被更改为适合宿主操作系统文件系统的惯例,或者 “transparent”, 代表会保持blob中保存的结束符不变

Blob创建出来,那JS种如何读取或者说从原始数据解析出文件数据呢?从Blob中读取内容的唯一方法是使用FileReader。

FileReader对象

故名思议,FileReader对象用于读取文件数据,所谓的文件数据其实就是Blob对象和File对象(因为File对象继承于Blob对象)。
由于文件的读取时间基于文件的大小,所以FileReader读取的过程为设计为异步,如下:

1
2
3
4
5
6
7
var fileReader = new FileReader();
FileReader.addEventListener('onload', function(e){
// 读取的数据保存在fileReader实例的result属性
var data = e.target.result
// Todo...
})
fileReader.readAsDataURL(file)

这个过程跟预加载图片的过程相同,生成实例->监听->开始加载,上面的例子以读取文件为例,使用readAsDataURL的方法,FileRader还有三种读取为其他数据类型的方法:

方法名 参数 描述
readAsBinaryString file 将文件读取为二进制编码
readAsBinaryArray file 将文件读取为二进制数组
readAsText file[, encoding] 按照格式将文件读取为文本,encode默认为UTF-8
readAsDataURL file 将文件读取为DataUrl

另外还有一个abort方法用于阻止文件读取

我们知道Image对象读取图像的事件有onload、onerror、onabort,而FileReader除了这三个事件,还新增了三个对过程的监听事件,onloadstart、onprogress、onloadend,但实际上新增的事件使用的并不多,主要用于大文件读取时进度条实现的需求上。

在onprogress的事件处理器中,有一个ProgressEvent对象,这个事件对象实际上继承了Event对象,提供了三个只读属性:

  • lengthComputable
  • loaded (已读取的字节数)
    • total (总字节数)

其中事件的lengthComputable属性代表文件总大小是否可知。如果 lengthComputable 属性的值是 false,那么意味着总字节数是未知并且 total 的值为零。

看到这些是否感觉似曾相识,“蓦然回首,那人却在灯火阑珊处”。没错,使用ajax上传文件的时候也有同样的事件钩子,XMLHttpRequest Level 2中,xhr的progress事件用于异步请求进度的监听,上传的进度事件绑定在xhr的upload属性中,在异步上传文件时我们可以这样监听进度:

1
2
3
4
5
6
7
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var percentComplete = (e.loaded / e.total) * 100;
$progress.css('width', percentComplete + '%');
}
};

同样地,我们只需要为onprogress事件添加处理器就获取文件读取的进度。

1
2
3
4
function progressHandler(e) {
var percentLoaded = Math.round((e.loaded / e.total) * 100);
$progress.css('width', percentLoaded + '%');
}

FileReader和文件密不可分,既然说到文件上传,就再说一下多文件上传、拖拽上传

FormData与文件上传

多文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var fileInput = document.getElementById("myFile");
var files = fileInput.files;
var formData = new FormData();
for(var i = 0; i < files.length; i++) {
var file = files[i];
formData.append('files[]', file, file.name);
xhr.open("POST", "/upload.php");
xhr.onload = function(){
if(this.status === 200){
//对请求成功的处理
}
}
// 如果要监听进度
xhr.upload.onprogress = progressHandler
xhr.send(formData);
xhr = null;
}

这是将所有的文件发送一次异步请求,而我们监听的进度也是所有文件上传的总进度,如果我们需要单独监听单个文件的上传进度,只需改成递归的方式依次发送请求。

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
var fileInput = document.getElementById("myFile");
var files = fileInput.files;
var i = 0;
var uploadFile = function(){
var formData = new FormData();
formData.append(files[i]);
xhr.open("POST", "/upload.php");
xhr.onload = function(){
if(this.status === 200){
//进行下一次请求
i++;
if(i != files.length){
uploadFile()
}
}
}
// 如果要监听进度
xhr.upload.onprogress = progressHandler
xhr.send(formData);
xhr = null;
}

拖拽上传

拖拽上传只需了解HTML5基本的拖拽事件即可,参考MDN的HTML 拖放 API。

  • drag : 元素被拖拽时由拖拽元素频繁触发的事件
  • dragstart : 拖拽时开始时由拖拽元素触发的事件
  • dragend : 拖拽结束时触发由拖拽元素的事件
  • dragover : 当拖拽元素进入放置区域时由放置元素频繁触发的事件(每隔几百毫秒就会触发一次)
  • dragenter : 当拖拽元素进入放置区域时由放置元素触发的事件
  • dragleave : 当拖拽元素离开放置区域时由放置元素触发的事件
  • drop : 当拖拽元素在放置区域放置时由放置元素触发的事件

持续触发的事件:drag和dragover
发生在拖拽元素的事件:drag、dragstart、dragend
发生在放置元素的事件:dragover 、dragenter 、dragleave、drop
事件触发次序:dragstart -> drag -> dragenter -> dragover -> dragleave -> drop -> dragend

当我们拖放文件到浏览器中时,浏览器默认的行为是浏览器将当前页面重定向到被拖拽元素所指向的资源上,因此需要阻止dragenter、dragover、drop的默认行为,这样才能使drop事件被触发。(最好同时阻止冒泡)

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 dropArea;
dropArea = document.getElementById("dropArea");
dropArea.addEventListener("dragenter", handleDragenter, false);
dropArea.addEventListener("dragover", handleDragover, false);
dropArea.addEventListener("drop", handleDrop, false);
function handleDragenter(e) {
/*若要兼容ie
window.event? window.event.cancelBubble = true : e.stopPropagation();
window.event? window.event.returnValue = false : e.preventDefault();*/
e.stopPropagation();
e.preventDefault();
}
function handleDragover(e) {
e.stopPropagation();
e.preventDefault();
}
function handleDrop(e) {
e.stopPropagation();
e.preventDefault();
var dt = e.dataTransfer;
var files = dt.files;
// FileReader将图片读取为dataUrl并立刻展示..省略
}

值得注意的是:触发dragstart事件后,其他元素的mousemove、mouseover、mouseenter、mouseleaver、mouseout事件均不会被触发。

上面拖拽回调中的事件对象,继承自 MouseEvent 对象,它的dataTransfer属性保存着拖拽对象的相关信息。

DataTransfer对象有几个重要的属性,其中files属性保存文件的数据。
effectAllowed 和 dropEffect 最主要的作用是,用于配置拖拽操作过程中鼠标指针的类型以便提示用户后续可执行怎样的操作;其次的作用是,控制 drop 事件的触发与否。当显示禁止的指针样式时,将无法触发目标元素的 drop 事件。

注意:只能在dragstart中设置effectAllowed,只能在dragover中设置dropEffect

拖拽后通过FileReader读取立刻显示图片优化体验,因为用户可能需要确定是否更换图片,然后单击按钮才将图片,这样可以防止不必要的请求,接下来就是上文提到的FormData上传文件,这里不再赘述。

vuejs开发H5页面总结

发表于 2017-07-03   |   分类于 前端

最近参与了APP内嵌H5页面的开发,这次使用vuejs替代了jQuery,仅仅把vuejs当做一个库来使用,效率提高之外代码可读性更强,在此分享一下自己的一些开发中总结的经验。

关于布局方案

当拿到设计师给的UI设计图,前端的首要任务就是布局和样式,相信这对于大部分前端工程师来说已经不是什么难题了。移动端的布局相对PC较为简单,关键在于对不同设备的适配。之前介绍了一篇关于移动端rem布局方案,这大致是网易H5的适配方案。不过实践中发现淘宝开源的可伸缩布局方案效果更好且更容易使用。

网易云的方案总结为:根据屏幕大小 / 750 = 所求字体 / 基准字体大小比值相等,动态调节html的font-size大小。

淘宝的方案总结为:根据设备设备像素比设置scale的值,保持视口device-width始终等于设备物理像素,接着根据屏幕大小动态计算根字体大小,具体是将屏幕划分为10等分,每份为a,1rem就等于10a。

通常我们会拿到750宽的设计稿,这是基于iPhone6的物理分辨率。有的设计师也许会偷懒,设计图上面没有任何的标注,如果我们边开发边量尺寸,无疑效率是比较低的。要么让设计师标注上,要么自食其力。如果设计师实在没有时间,推荐使用markman进行标注,免费版阉割了一些功能(比如无法保存本地)不过基本满足了我们的需求了。

标注完成后开始写我们的样式,使用了淘宝的lib-flexible库之后,我们的根字体基准值就为750/100*10 = 75px。此时我们从图中若某个标注为100px,那么css中就应该设置为100/75 = 1.333333rem。所以为了提高开发效率,可以使用px转化为rem的插件。如果你使用sublimeText,可以用 rem-unit
rem-unit
如果你用vscode编辑器,推荐 cssrem
pxtorem

使用rem单位注意以下几点:

  1. 在所有的单位中,font-size推荐使用px,然后结合媒体查询进行重要节点的控制,这样可以满足突出或者弱化某些字体的需求,而非整体调整。
  2. 众向的单位可以全部使用px,横向的使用rem,因为移动设备宽度有限,而高度可以无限向下滑动。但这也有特例,比如对于一些活动注册页面,需要在一屏幕内完全显示,没有下拉,这时候所有众向或者横向都应该使用rem作为单位。如图:
    rem-desc
    左图的表单高度单位由于下边空距较大,使用px在不同屏幕显示更加;而右边的活动注册页由于不能出现滚动条,所有的众向高度、margin、padding都应该使用rem。
  3. border、box-shadow、border-radius等一些效果应该使用px作为单位。

基于接口返回数据的属性注入

可能大家不明白什么叫”基于接口返回数据的属性注入”,在此之前,先说一下表单数据的绑定方式,一个重要的点是有几份表单就分开几个表单对象进行数据绑定。

已上图公积金查询为例,由于不同城市会有不同的查询要素,可能登陆方式只有一种,也可能有几种。比如上图有三种登陆方式,在使用vue布局时,有两种方案。一是只建立一个表单用于数据绑定,点击按钮触发判断;而是有几种登陆方式建立几个表单,用一个字段标识当前显示的表单。由于使用第三方的接口,一开始也没有先进行接口返回数据结构的查看,采用了第一种错误的方式,错误一是每种登陆方式下面的登陆要素的数量也不同,错误二是数据绑定在同一个表单data下,当用户在用户名登陆方式输入用户名密码后,切换到客户号登陆方式,就会出现数据错乱的情况。

解决完布局问题后,我们需要根据设计图定义一些状态,比如当前登陆方式的切换、同意授权状态的切换、按钮是否可以点击的状态、是否处于请求中的状态。当然还有一些app穿过来的数据,这里就忽略了。

1
2
3
4
5
6
7
8
data: {
tags: {
arr: [''],
activeIndex: 0
},
isAgreeProxy: true,
isLoading: false
}

接着审查一下接口返回的数据,推荐使用chrome插件postman,比如呼和浩特的登陆要素如下:

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
{
"code": 2005,
"data": [
{
"name": "login_type",
"label": "身份证号",
"fields": [
{
"name": "user_name",
"label": "身份证号",
"type": "text"
},
{
"name": "user_pass",
"label": "密码",
"type": "password"
}
],
"value": "1"
},
{
"name": " login_type",
"label": "公积金账号",
"fields": [
{
"name": "user_name",
"label": "公积金账号",
"type": "text"
},
{
"name": "user_pass",
"label": "密码",
"type": "password"
}
],
"value": "0"
}
],
"message": "登录要素请求成功"
}

可以看到呼和浩特有两种授权登陆方式,我们在data中定义了一个loginWays,初始为空数组,接着methods中定义一个请求接口的函数,里面就是基于返回数据的基础上为上面fields对象注入一个input字段用于绑定,这就是所谓的基于接口返回数据的属性注入。

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
methods: {
queryloginWays: function(channel_type, channel_code) {
var params = new URLSearchParams();
params.append('channel_type', channel_type);
params.append('channel_code', channel_code);
axios.post(this.loginParamsProxy, params)
.then(function(res) {
console.log(res);
var code = res.code || res.data.code;
var msg = res.message || res.data.message;
var loginWays = res.data.data ? res.data.data : res.data;
// 查询失败
if (code != 2005) {
alert(msg);
return;
}
// 添加input字段用于v-model绑定
loginWays.forEach(function(loginWay) {
loginWay.fields.forEach(function(field) {
field.input = '';
})
})
this.loginWays = loginWays;
this.tags.arr = loginWays.map(function(loginWay) {
return loginWay.label;
})
}.bind(this))
}
}

即使返回的数据有我们不需要的数据也没有关系,这样保证我们不会遗失进行下一步登陆所需要的数据。

这样多个表单绑定数据问题解决了,那么怎么进行页面间数据传递?如果是app传过来,那么通常使用URL拼接的方式,使用window.location.search获得queryString后再进行截取;如果通过页面套入javaWeb中,那么直接使用”${字段名}”就能获取,注意要js中获取java字段需要加双引号。

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
computed: {
// 真实姓名
realName: function() {
return this.getQueryVariable('name') || ''
},
// 身份证
identity: function() {
return parseInt(this.getQueryVariable('identity')) || ''
},
/*If javaWeb
realName: function() {
return this.getQueryVariable('name') || ''
},
identity: function() {
return parseInt(this.getQueryVariable('identity')) || ''
}*/
},
methods: {
getQueryVariable: function(variable) {
var query = window.location.search.substring(1);
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) == variable) {
return decodeURIComponent(pair[1]);
}
}
console.log('Query variable %s not found', variable);
}
}

关于前端跨域调试

在进行接口请求时,我们的页面通常是在sublime的本地服务器或者vscode本地服务器预览,所以请求接口会遇到跨域的问题。
在项目构建的时候通常我们源代码会放在src文件夹下,然后使用gulp进行代码的压缩、合并、图片的优化(根据需要)等等,我们会使用gulp。这里解决跨域的问题可以用gulp-connect结合http-proxy-middleware,此时我们在gulp-connect中的本地服务器进行预览调试。
gulpfile.js如下: 开发过程使用gulp server命令,监听文件改动并使用livereload刷新;使用gulp命令进行打包。

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
var gulp = require('gulp');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var autoprefixer = require('gulp-autoprefixer');
var useref = require('gulp-useref');
var connect = require('gulp-connect');
var proxyMiddleware = require('http-proxy-middleware');
// 定义环境变量,若为 dev,则代理src目录; 若为prod,则代理dist目录
var env = 'prod'
// 跨域代理 将localhost:8088/api 映射到 https://api.shujumohe.com/
gulp.task('server', ['listen'], function() {
var middleware = proxyMiddleware(['/api'], {
target: 'https://api.shujumohe.com/',
changeOrigin: true,
pathRewrite: {
'^/api': '/'
}
});
connect.server({
root: env == 'dev' ? './src' : './dist',
port: 8088,
livereload: true,
middleware: function(connect, opt) {
return [middleware]
}
});
});
gulp.task('html', function() {
gulp.src('src/*.html')
.pipe(useref())
.pipe(gulp.dest('dist'));
});
gulp.task('css', function() {
gulp.src('src/css/main.css')
.pipe(concat('main.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/css/'));
gulp.src('src/css/share.css')
.pipe(concat('share.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/css/'));
gulp.src('src/vendors/css/*.css')
.pipe(concat('vendors.min.css'))
.pipe(autoprefixer({
browsers: ['last 2 versions'],
cascade: false
}))
.pipe(gulp.dest('dist/vendors/css'));
return gulp
});
gulp.task('js', function() {
return gulp.src('src/vendors/js/*.js')
.pipe(concat('vendors.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/vendors/js'));
});
gulp.task('img', function() {
gulp.src('src/imgs/*')
.pipe(gulp.dest('dist/imgs'));
});
gulp.task('listen', function() {
gulp.watch('./src/css/*.css', function() {
gulp.src(['./src/css/*.css'])
.pipe(connect.reload());
});
gulp.watch('./src/js/*.js', function() {
gulp.src(['./src/js/*.js'])
.pipe(connect.reload());
});
gulp.watch('./src/*.html', function() {
gulp.src(['./src/*.html'])
.pipe(connect.reload());
});
});
gulp.task('default', ['html', 'css', 'js', 'img']);

关于异步函数的回调问题

发表于 2017-03-30   |   分类于 前端

我们在项目中经常需要根据异步请求结果进行相关操作,以往常用的方法是callback,但现在如果结合babel编译,用promise或者async会更加优雅。如下是错误的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 异步之后进行相关操作的错误做法
function asyncFun(){
var flag = false;
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res){
if(res.code == 1){
flag = true
}
})
return flag; // 这里始终返回false,因为异步请求不阻塞后面代码执行
}
if(asyncFun()){
// 由于flag始终为false,很明显这里永远不会执行
}

解决方案有三种:传统回调方式,ES5Promise对象,ES6 async/await方式

回调函数方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 定义一个异步函数
function asyncFun(cb){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res){
cb && cb(res)
})
}
asyncFun(function(res){
// 根据异步请求返回的结果进行相关操作
if(res.code == 1){
}
})

使用ES6引入的promise对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function asyncFun(){
return new Promise(function(resolve, reject){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res){
resolve(res)
})
})
}
asyncFun().then(function(res){
if(res.code == 1){
// 你要进行相关的操作
}
})

如果后面的操作不只一次,你可以定义多个返回Promise对象的函数,像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function asyncFun2(res){
// 这里可以获取asyncFun传过来的res
return new Promise(function(resolve, reject){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res2){
resolve(res2)
})
})
}
function asyncFun3(res2){
// 这里可以获取asyncFun2传过来的res2
return new Promise(function(resolve, reject){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res3){
resolve(res3)
})
})
}
asyncFun().then(asyncFun2).then(asyncFun3)

说明:

  • 这种写法十分类似jQuery的链式写法,只要返回Promise对象,就拥有then()及catch()方法,后者用于捕捉回调运行时发生的错误。
  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve方法和reject方法。如果异步操作成功,则用resolve方法将Promise对象的状态变为“成功”(即从pending变为resolved);如果异步操作失败,则用reject方法将状态变为“失败”(即从pending变为rejected)
  • then方法其实有两个参数,第一个参数作为上一步操作resolve时的回调函数,如果上一步操作后状态变为rejected,则以第二个参数作为回调函数

使用ES7关键字async/await——异步终极解决方案

ES7 中有了更加标准的解决方案,新增了async/await两个关键词,async可以声明一个异步函数,此函数需要返回一个 Promise对象。await 可以等待一个 Promise 对象 resolve,并拿到结果。
现在要实现上面的需求,可以这样:

基本规则

  • async 表示这是一个async函数,await只能用在这个函数里面。
  • await 表示在这里等待promise返回结果了,再继续执行。
  • await 后面跟着的应该是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getData(){
return new Promise(function(resolve,reject){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(res){
resolve(res)
})
})
}
async function asyncFun(){
var res = await getData();
if(res.code == 1){
//进行相关操作
}
}

使用async/await捕获异常

这里说下捕获异步操作中的异常问题,既然then不用写,那么catch也可以不用了,可以直接用标准的try/catch语法捕捉错误。

常犯的错误是使用try/catch捕获异步函数的异常,这时候因为try/catch块执行完了,异步函数很可能还没有完成,异常还没有被实际抛出。当异常被抛出的时候,由于catch块已经执行完了,此时就会导致进程崩溃。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 错误的示例
function getData(){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(){
// 模拟异常
null();
})
}
try{
getData();
console.log('我会被打印');
}catch(e){
console.log('error')
}
// '我会被打印'
// undefined
// Uncaught TypeError: null is not a function

可以看到异步请求中的异常无法被正常捕获,’error’并没有被打印出来,console.log(‘我会被打印’)仍然执行了,使用async/await可以很容易解决这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getData(){
return new Promise(function(resolve,reject){
$.ajax({
url: 'https://api.github.com/',
type: 'get'
}).done(function(){
// 模拟异常
reject('error')
})
})
}
async function asyncFun(){
try{
await getData();
// 这里以下的代码不被执行
console.log('我不会被打印');
}catch(err){
console.log(err);
}
}
async(); // Promise{}
// error

总结:

  1. 从回调到promise再到async/await解决步式操作需求的问题,可以看到代码易读性明显增强了,async/await不仅去除了嵌套问题,也不用再用链式写法的then了。
  2. async/await只是一套语法糖,其他语言的async/await可能是协程或者多线程编程的语法糖,JS本身是单线程的,async/await与传统的callback或者promise执行起来并无两样
  3. ES7 async/await仍未成标准,最新版谷歌已经支持,但大部分浏览器仍没有实现,要使用它可以用babel编译成浏览器支持的ES5。

Vuejs2.0 Demo

发表于 2017-02-04   |   分类于 前端

前段时间利用闲暇时间学习Vuejs并且做了一个实例,主要使用vue-router实现路由控制,vuex对程序进行状态管理,axios发送http请求。对数据在组件间的传递、webpack打包工具、数据的双向绑定有更深刻的了解和认识。
预览图

项目在线地址:http://www.huzerui.com/vue2.0-demo/
Github地址:https://github.com/alex1504/vue2.0-demo

扫描二维码预览:
二维码地址

更新记录:

2017.1.13 主导航电影、音乐、图书、图片使用router跳转电影模块使用tab菜单切换各个列表模块下拉滚动加载图片模块使用flex布局实现瀑布流效果
2017.1.17 增加了电影详情模块,优化路由跳转
2017.1.18 增加了登录、登出模块,使用leancloud数据存储功能
2017.1.19 增加了图片详情模块,增加了新的生产依赖vue-touch
2017.2.6 新增用户注册模块

开发依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-preset-es2015": "^6.0.0",
"cross-env": "^3.0.0",
"css-loader": "^0.25.0",
"file-loader": "^0.9.0",
"mockjs": "^1.0.1-beta3",
"node-sass": "^4.2.0",
"sass": "^0.5.0",
"sass-loader": "^4.0.0",
"style-loader": "^0.13.1",
"vue-loader": "^10.0.0",
"vue-style-loader": "^1.0.0",
"vue-template-compiler": "^2.1.0",
"webpack": "^2.1.0-beta.25",
"webpack-dev-server": "^2.1.0-beta.9"

生产环境依赖:

1
2
3
4
5
6
7
"axios": "^0.15.3",
"vue": "^2.1.0",
"vue-material": "^0.5.2",
"vue-router": "^2.1.1",
"vue-swipe": "^2.0.2",
"vue-touch": "^2.0.0-beta.3",
"vuex": "^2.1.1"

使用的接口:

豆瓣电影图书接口、gankio图片接口、网易云音乐专辑及搜索接口(见components中请求部分)

说明:

  • Vue-material中的spinner组件在某些浏览器无法正常显示,在chrome以及微信中体验效果较好。
  • 由于众所周知的原因,Vue-material中的原生icon使用了阿里的iconfont替代。(已经在许多歌项目使用,表示图标非常丰富,调用方式多样)
  • 由于ithub访问速度原因,初次加载需稍等片刻。
  • 登录及注册使用了Leancloud的后端云服务,关于如何使用你可以点这里查看LeanCloud搭建的各类应用演示。

关于使用Leancloud接口

细心的小伙伴应该会发现,使用LN接口必须使用AV.init对接口进行初始化,接收的参数是你的APPID和APPScret,也就是说即使我们的项目不开源,通过前端审查脚本也可以知道你的应用id及密钥,进而可以轻松调取你的接口做坏坏的事情。
为此如果你需要纯前后端分离的进行接口调用,LN给予的安全方案是配置web安全域名,也就是只有安全域名才能访问你的接口。
安全域名
另外更稳妥的做法是传统开发APP的方式,将APPID和APPScret在后台代码中对接口进行初始化,前端就审查不到了,比如koa就可以单独为静态文件设置公开访问的静态目录,对于敏感数据文件可以设置存放在安全的前端无法访问的目录下面。
在有后端对接口实现复杂的逻辑封装的时候,推荐LN官方推出的LeanEngine运行方案,有Nodejs、Pythod、PHP、Java四个版本。

感触:

  1. MVVM三分天下,如果你希望一个轻巧、易上手、文档详尽的框架,选择Vuejs
  2. 如果你十分熟练jQuery的用法,在Vuejs中你很容易发现一些遗传着“经典”的足迹
  3. 仔细发现,如果你学习过js设计模式,你会觉得Vuejs的设计很像其中一种模式——单例模式
  4. 如果你需要做一个小型应用,不妨使用后端云服务(比如LeanCloud、Bomb、maxLeap),既减少你的开发时间也减少开发成本;但大型的应用和数据敏感型的应用肯定是不推荐用别人的服务器的。
  5. ES6还没完全支持,ES7就发布了,关于js逐渐融合各种语言优良特性不断发展的前景个人是十分看好的,毕竟js创始人Brendan Eich仅用几天时间就完成了它的设计。在开发过程中也确实因为js的某些缺陷增大了开发的代码量,不得不用一些设计模式去弥补。关于ES6、ES7的新特性,会在今后逐渐学习并记录。
  6. 见过一些reactNative制作的应用,体验和原生APP无任何差别,有Vue Native么?不妨给它加层包装,叫做weex,后面也会慢慢地了解和学习。

常用的Lodash方法

发表于 2017-01-22

Lodash目前非常流行的JavaScript的工具集合框架,被前端开发者广泛地使用,下面是一些平常开发过程中常用的函数。

在介绍函数前,对lodash函数名应有如下的了解:

  • 含sorted的方法接受有序数组
  • 同名含By的方法额外接受一个迭代器参数
  • 同名含Last的方法返回的是符合筛选条件元素的最后一个

对象扩展:

_.assign

1
2
3
4
5
var foo = { a: "a property" };
var bar = { b: 4, c: "an other property" }
var result = _.assign({ a: "an old property" }, foo, bar);
// result => { a: 'a property', b: 4, c: 'an other property' }

防抖函数

_.debounce

在触发防抖函数的事件后面加个延迟时间,如果在该延迟时间内没有继续触发事件,那么执行防抖函数。设想一个场景,电梯门打开的时候,如果在2s内陆续有人按电梯按钮,那么每次电梯都会再重新等待2s,直至2s中没人按按钮再触发关门操作。

1
2
3
4
5
6
function validateEmail() {
// Validate email here and show error message if not valid
}
var emailInput = document.getElementById("email-field");
emailInput.addEventListener("keyup", _.debounce(validateEmail, 500));

迭代函数n次

  • 返回:每次返回结果构成的数组
  • 应用:生成大量测试数据
1
2
3
4
5
6
function getRandomInteger() {
return Math.round(Math.random() * 100);
}
var result = _.times(5, getRandomNumber);
// result => [64, 70, 29, 10, 23]

查找数组中的特定对象

接收多个对象属性查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var users = [
{ firstName: "John", lastName: "Doe", age: 28, gender: "male" },
{ firstName: "Jane", lastName: "Doe", age: 5, gender: "female" },
{ firstName: "Jim", lastName: "Carrey", age: 54, gender: "male" },
{ firstName: "Kate", lastName: "Winslet", age: 40, gender: "female" }
];
var user = _.find(users, { lastName: "Doe", gender: "male" });
// user -> { firstName: "John", lastName: "Doe", age: 28, gender: "male" }
var underAgeUser = _.find(users, function(user) {
return user.age < 18;
});
// underAgeUser -> { firstName: "Jane", lastName: "Doe", age: 5, gender: "female" }

设置及获取对象属性

根据对象路径进行设置或获取

  • _set方法:如果路径不存在,会自动创建路径。像下面的例子,使用该方法不会再出现“Cannot set property ‘items’ of undefined” error.”这种错误
  • _get方法:如果对象路径不存在,返回undefined而不会报错,第三个参数接收默认值。
1
2
3
4
5
var bar = { foo: { key: "foo" } };
_.set(bar, "foo.items[0]", "An item");
// bar => { foo: { key: "foo", items: ["An item"] } }
var name = _.get(bar, "name", "John Doe");
// name => John Doe

将对象数组重组成对象映射

  • 只要服务器返回的是个集合对象,就可以通过该方法将集合转成对象映射
  • 第二个参数也可以是函数,函数的第一个参数默认是数组中的一个对象

如从100篇文章中选取出id为34abc的文章,当获取到文章数组后,怎么做?

1
2
3
4
5
6
7
8
9
10
11
12
var posts = [
{ id: "1abc", title: "First blog post", content: "..." },
{ id: "2abc", title: "Second blog post", content: "..." },
// more blog posts
{ id: "34abc", title: "The blog post we want", content: "..." }
// even more blog posts
];
posts = _.keyBy(posts, "id");
var post = posts["34abc"]
// post -> { id: "34abc", title: "The blog post we want", content: "..." }

遍历集合元素并返回特定格式的对象

  • 通过 iteratee 遍历集合中的每个元素。 每次返回的值会作为下一次 iteratee 使用。 如果没有提供accumulator(第三个参数,即初始值),则集合中的第一个元素作为 accumulator。
  • 常见的错误是忽略了返回结果(return result)和没有传入函数的第三个参数(默认值)

下面的例子从数组筛选符合年龄条件并由年龄进行分组的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var users = [
{ name: "John", age: 30 },
{ name: "Jane", age: 28 },
{ name: "Bill", age: 65 },
{ name: "Emily", age: 17 },
{ name: "Jack", age: 30 }
]
var reducedUsers = _.reduce(users, function (result, user) {
if(user.age >= 18 && user.age <= 59) {
(result[user.age] || (result[user.age] = [])).push(user);
}
return result;
}, {});
// reducedUsers -> {
// 28: [{ name: "Jane", age: 28 }],
// 30: [{ name: "John", age: 30 }, { name: "Jack", age: 30 }]
// }

深拷贝

_.cloneDeep()

1
2
3
4
5
6
7
8
9
10
11
var original = { foo: "bar" };
var copy = original;
copy.foo = "new value";
// copy -> { foo: "new value" } Yeah!
// original -> { foo: "new value" } Oops!
var original = { foo: "bar" };
var copy = _.cloneDeep(original);
copy.foo = "new value";
// copy -> { foo: "new value" } Yeah!
// original -> { foo: "bar" } Yeah!

浅拷贝

_.clone()

对于对象内部的引用类型不会拷贝,而是指向同一个存在堆中的地址

1
2
3
4
5
var objects = [{ 'a': 1 }, { 'b': 2 }];
var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true

数组去重

uniq() 与 . sortedUniq()

1
2
3
var sortedArray = [1, 1, 2, 3, 3, 3, 5, 8, 8];
var result = _.sortedUniq(sortedArray);
// -> [1, 2, 3, 5, 8]

方法 说明
_.uniq 数组去重
_.sortedUniq 对于已排序的数组性能更高

向有序数组中插入元素,依旧保证有序

返回:插入元素在数组中的索引

方法:都含有sorted表示接受有序数组

  • _.sortedIndex
  • _.sortedIndexBy
  • _.sortedLastIndex
  • _.sortedLastIndexBy

含Last方法和不含Last方法的区别是:
Last方法在保持有序的前提下会把value插进最大的那个位置

1
2
3
4
5
6
7
var objects = [{ 'x': 4 }, { 'x': 5 }];
_.sortedLastIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
// => 1
_.sortedLastIndexBy(objects, { 'x': 4 }, 'x');
// => 1

参考及延伸

Lodash官方文档(4.17.4):https://lodash.com/docs/4.17.4
Lodash中文文档(3.10.1):http://lodashjs.com/docs/
Lodash/fp模块:https://github.com/lodash/lodash/wiki/FP-Guide
什么是Lodash/fp模块:http://www.cnblogs.com/legendlee/p/5601524.html
Webpack按需打包lodash:http://www.tuicool.com/articles/niyeMrN

Gulp前端自动化工具笔记

发表于 2016-11-07   |   分类于 前端

gulp
某个深夜独自在房间深造,在不断键盘敲击的“Alt+Tab”和“F5”许久,我开始疲惫了,于是从冰箱抡起一杯可乐。畅饮之余,去Google寻求仙人指路,仙人找到了nodejs为我指点了迷津,并递给我一杯可乐和吸管,原来“她”就在这里,她的名字叫“Gulp”。

她虽然只是一杯插着吸管的饮料,可却能在烈日炎炎的夏天舒缓你焦灼的身心,在寒冷的冬天给你雪中送炭似的温暖,给你不一样的编码体验。

拾起一杯Gulp

gulpjs是一个前端构建工具,API简单,基于任务流,无需像Grunt复杂的配置。

安装

在安装了node.js的基础上使用命令行或git即可安装。
全局安装:

1
$ npm install --global gulp

作为项目的开发依赖安装(-dev),如果作为项目依赖,则只需(–save)

1
$ npm install --save-dev gulp

根目录创建gulpfile.js

1
2
3
4
5
var gulp = require('gulp');
gulp.task('default', function() {
// 将你的默认的任务代码放在这
});

运行

如果省略了task参数,则gulp === gulp default

1
gulp [task]? // []中存放参数,?参考js正则,代表可有可无

畅饮Gulp

在了解的Gulp的一些插件和基本使用方法之后,我开始构建自己的gulpfile.js,这个流程可以实现如下功能。

  • 实时监听文件的变化,一旦保存浏览器自动刷新。
  • 自动编译scss文件
  • 合并样式文件、脚本文件
  • 压缩样式文件、脚本文件、图片
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
/*============================引入插件===============================*/
var gulp = require('gulp');
var plugins = require('gulp-load-plugins')({
rename: {
'gulp-ruby-sass': 'sass',
'gulp-clean-css': 'cleancss'
}
});
var browserSync = require('browser-sync').create();
var reload = browserSync.reload;
/*=============================发布任务==============================*/
gulp.task('html',function(){
return gulp.src('src/*.html')
.pipe(gulp.dest('dist/'));
});
gulp.task('css',function(){
// var cssFilter = plugins.filter(['src/**/*.css', '!src/css/vendor'], {restore: true});
return gulp.src('src/css/**/*.css')
.pipe(plugins.cleancss())
.pipe(gulp.dest('dist/css/'));
});
gulp.task('js',function(){
// var jsFilter = plugins.filter(['src/**/*.js', '!src/js/vendor'], {restore: true});
return gulp.src('src/**/*.js')
.pipe(plugins.uglify())
.pipe(gulp.dest('dist/'));
});
gulp.task('img',function(){
gulp.src('src/img/*')
.pipe(plugins.imagemin())
.pipe(gulp.dest('dist/img'));
});
gulp.task('dist',['html', 'css', 'js' , 'img'], function(){
return gulp.src(['src/res', 'src/mocks'])
.pipe(gulp.dest('dist/'));
});
/*=============================合并任务==============================*/
gulp.task('bundle',function(){
var vendor = {
css: ['src/css/vendor/jquery.fullpage.css'],
js: ['src/js/vendor/jquery.js','src/js/vendor/jquery.fullpage.js']
};
// 合并库css
gulp.src(vendor.css)
.pipe(plugins.concat('bundle.css'))
.pipe(gulp.dest('src/css/vendor/'));
// 合并库js
gulp.src(vendor.js)
.pipe(plugins.concat('bundle.js'))
.pipe(gulp.dest('src/js/vendor/'));
});
/*=============================服务器任务==============================*/
// 静态服务器 + 监听 scss/html 文件
gulp.task('server', ['sass','bundle'], function() {
browserSync.init({
server: './src'
});
gulp.watch('src/scss/**/*.scss', ['sass']);
gulp.watch('src/**/*.html', ['html']).on('change', reload);
gulp.watch('src/js/**/*.js', ['js']).on('change', reload);
});
/*=============================编译任务==============================*/
gulp.task('sass', function() {
// 编译库样式文件
plugins.sass('src/scss/vendor/*.scss',{sourcemap: true})
.on('error', plugins.sass.logError)
.pipe(plugins.sourcemaps.write())
.pipe(gulp.dest('src/css/vendor/'))
.pipe(reload({stream: true}));
// 编译项目样式文件
plugins.sass('src/scss/main.scss',{sourcemap: true})
.on('error', plugins.sass.logError)
.pipe(plugins.sourcemaps.write())
.pipe(gulp.dest('src/css/'))
.pipe(reload({stream: true}));
return gulp;
});
/*=============================默认任务==============================*/
gulp.task('default', ['dist']);

插件介绍

browserSync:浏览器实时相应文件更改并进行重新加载
gulp-load-plugins:gulp插件加载工具,可对gulp插件进行重命名。当使用插件过多时可以有效管理和组织插件。
gulp-ruby-sass: sass编译工具,同时生成sourcemap方便调试
gulp-clean-css: 压缩css工具
gulp-uglify: 压缩js工具
gulp-concat:css、js合并工具
gulp-imagemin: 图片压缩工具

流程说明

  • 在项目开发阶段,只存在src目录,所有开发均在src目录进行,直到项目开发完成,运行gulp命令则生成dist发布目录。
  • 项目开发过程中执行gulp server命令,即可实施监听文件变动从而实现浏览器自动刷新。

gulp server运行时,一旦文件重新保存,则触发以下任务:

  • scss-vendor文件夹中的样式文件会被编译并打包成bundle.css并输出到css-vendor
  • js-vendor中的库文件会被打包成bundle.js并输出到js-vendor
  • scss目录下同级的scss文件将被合并到main.scss文件中并被编译成main.css打包到css目录下面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-root
-dist //发布目录,执行gulp默认任务生成
-src
-css
vendor
...
main.css
-img
-js
vendor
main.js
-scss
vendor
...
main.scss
normalize.scss
index.html
...

品味Gulp

Gulp-API

以下为API介绍,[]中的参数为可选,options参数不太常用,故不作详细介绍。

gulp.task(name[, deps], fn)

定义一个使用 Orchestrator 实现的任务(task)。
name: 任务的名字,如果你需要在命令行中运行你的某些任务,那么,请不要在名字中使用空格。
deps: 一个包含任务列表的数组,这些任务会在你当前任务运行之前完成。

1
2
3
gulp.task('somename', function() {
// 做一些事
});

gulp.src(globs[, options])

输出(Emits)符合所提供的匹配模式(glob)或者匹配模式的数组(array of globs)的文件。 将返回一个 Vinyl files 的 stream 它可以被 piped 到别的插件中。

1
2
3
4
gulp.src('client/templates/*.jade')
.pipe(jade())
.pipe(minify())
.pipe(gulp.dest('build/minified_templates'));

gulp.dest(path[, options])

能被 pipe 进来,并且将会写文件。并且重新输出(emits)所有数据,因此你可以将它 pipe 到多个文件夹。如果某文件夹不存在,将会自动创建它。

1
2
3
4
5
gulp.src('./client/templates/*.jade')
.pipe(jade())
.pipe(gulp.dest('./build/templates'))
.pipe(minify())
.pipe(gulp.dest('./build/minified_templates'));

gulp.watch(glob[, opts, cb])

1
2
3
gulp.watch('js/**/*.js', function(event) {
console.log('File ' + event.path + ' was ' + event.type + ', running tasks...');
});

怎么少了gulp.pipe()?
生活中我们每天都在搞事情,事情等同于任务,但这个任务中不同的环节是有依赖关系的,比如完成一幅画涂颜料需要在绘画完成后才能开始,于是gulp.pipe()相当于提供一个管道,让我们将这些环节连接起来,最终完成一个完整的任务。

关于glob参数匹配规则

发起一项任务需要寻找文件对象,这就需要一些规则用于匹配文件。

匹配规则

1
2
3
4
5
6
7
8
9
* 匹配文件路径中的0个或多个字符,但不会匹配路径分隔符,除非路径分隔符出现在末尾
** 匹配路径中的0个或多个目录及其子目录,需要单独出现,即它左右不能有其他东西了。如果出现在末尾,也能匹配文件。
? 匹配文件路径中的一个字符(不会匹配路径分隔符)
[...] 匹配方括号中出现的字符中的任意一个,当方括号中第一个字符为^或!时,则表示不匹配方括号中出现的其他字符中的任意一个,类似js正则表达式中的用法
!(pattern|pattern|pattern) 匹配任何与括号中给定的任一模式都不匹配的
?(pattern|pattern|pattern) 匹配括号中给定的任一模式0次或1次,类似于js正则中的(pattern|pattern|pattern)?
+(pattern|pattern|pattern) 匹配括号中给定的任一模式至少1次,类似于js正则中的(pattern|pattern|pattern)+
*(pattern|pattern|pattern) 匹配括号中给定的任一模式0次或多次,类似于js正则中的(pattern|pattern|pattern)*
@(pattern|pattern|pattern) 匹配括号中给定的任一模式1次,类似于js正则中的(pattern|pattern|pattern)

示例:

1
2
3
4
5
6
7
8
9
10
11
* 能匹配 a.js,x.y,abc,abc/,但不能匹配a/b.js
*.* 能匹配 a.js,style.css,a.b,x.y
*/*/*.js 能匹配 a/b/c.js,x/y/z.js,不能匹配a/b.js,a/b/c/d.js
** 能匹配 abc,a/b.js,a/b/c.js,x/y/z,x/y/z/a.b,能用来匹配所有的目录和文件
**/*.js 能匹配 foo.js,a/foo.js,a/b/foo.js,a/b/c/foo.js
a/**/z 能匹配 a/z,a/b/z,a/b/c/z,a/d/g/h/j/k/z
a/**b/z 能匹配 a/b/z,a/sb/z,但不能匹配a/x/sb/z,因为只有**单独出现才能匹配多级目录
?.js 能匹配 a.js,b.js,c.js
a?? 能匹配 a.b,abc,但不能匹配ab/,因为它不会匹配路径分隔符
[xyz].js 只能匹配 x.js,y.js,z.js,不会匹配xy.js,xyz.js等,整个中括号只代表一个字符
[^xyz].js 能匹配 a.js,b.js,c.js等,不能匹配x.js,y.js,z.js

由上面可以看出gulp匹配的规则和正则大致相同。有一点要注意的是**匹配符号会深入所有的子目录里面,而出现路径分割符只会进入一级子目录中寻找匹配文件。

匹配模式

匹配模式分为:单一匹配、多种匹配、展开匹配

单一匹配

精确匹配:

1
gulp.src("./js/main.js")

模糊匹配:

1
gulp.src("**/*.js")

多种匹配

当有多种匹配模式(多个单一匹配)时可以使用数组

1
gulp.src(['js/*.js','css/*.css','*.html'])

使用数组的方式还有一个好处就是可以很方便的使用排除模式,在数组中的单个匹配模式前加上!即是排除模式,它会在匹配的结果中排除这个匹配,要注意一点的是不能在数组中的第一个元素中使用排除模式

1
2
gulp.src([*.js,'!b*.js']) //匹配所有js文件,但排除掉以b开头的js文件
gulp.src(['!b*.js',*.js]) //不会排除任何文件,因为排除模式不能出现在数组的第一个元素中

展开匹配

1
2
3
4
5
a{b,c}d 会展开为 abd,acd
a{b,}c 会展开为 abc,ac
a{0..3}d 会展开为 a0d,a1d,a2d,a3d
a{b,c{d,e}f}g 会展开为 abg,acdfg,acefg
a{b,c}d{e,f}g 会展开为 abdeg,acdeg,abdeg,abdfg
12…4
TeaCoder

TeaCoder

写精致代码,过简单生活

36 日志
6 分类
35 标签
RSS
掘金 GitHub 知乎
© 2015 - 2021 TeaCoder