在项目中,针对接口的高频调用,需要封装高效且易用的公共方法,进而很大程度上提升代码规范质量及编码效率。封装应该解决的问题:
先看一段接口配置文件
在配置文件 api.js 中通过调用 reqMethod
方法构造接口函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// api.js
import req from './reqMethod.js'; // 封装接口方法
const baseUrl = 'https://www.域名1.com'; // 配置默认域名
const reqMethod = function () {
let arg = Array.from(arguments);
arg.splice(3, 0, baseUrl);
return req.apply(null, arg);
};
export const getBrandmenus = params => reqMethod('GET', `/api/path`, params);
export const editNickname = params => reqMethod('GET', '/api/path', params);
export const addFavouriteCar = params => reqMethod('GET', '/api/path', params);
export const delFavouriteCar = params => reqMethod('GET', '/api/path', params);
因为
const
特性保证了 API 接口名称的唯一性(多人开发不会出现命名冲突),并保证了接口配置集中在 api.js 文件中方便统一管理维护。
将接口配置挂载到全局对象上
1
2
3
4
// app.ux
const injectRef = Object.getPrototypeOf(global) || global;
import * as api from './api.js';
injectRef.API = api;
调用示例
在页面中可以直接使用 asycn/await
方式调用全局 API 方法获取接口数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
async getDataList() {
try {
const res = await API.getBrandmenus({
sessionid: deviceInfo.deviceId
});
this.list = res.result.map(item => {
item.date = UTILS.Formate(item.date, 'YYYY-MM-DD');
return item;
});
} catch (error) {
console.log(error);
}
}
}
reqMethod(method, url, params, baseUrl, stateDetection, showPrompt)
。
增加 基础路径
参数,可支持多域名配置。(注:reqMethod
方法入参是完整 URL 时会覆盖默认域名配置)
返回状态检测: 默认:是,业务规定 返回数据中 returncode = 0
为正常请求。
是否弹窗提示: 默认:是,404、500、超时等是否弹窗提示。
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
// reqMethod.js
import fetch from '@system.fetch';
import prompt from '@system.prompt';
function reqMethod(method, url, params = {}, baseUrl, stateDetection = true, showPrompt = true) {
fixXiaomiParamsBug(params);
url = /^http/.test(url) ? url : baseUrl + url; // 扩展支持多域名配置
params = sign(params);
return request(method, url, params, stateDetection, showPrompt);
}
function request(method, url, params, stateDetection = true, showPrompt = true) {
return new Promise((resolve, reject) => {
fetch.fetch({
url: url,
data: params,
method: method,
success: (res) => {
try {
if (res.code !== 200) {
prompt.showToast({
message: '网络错误'
});
reject(res);
return;
}
const data = JSON.parse(res.data);
if (data.returncode === 0) {
resolve(data);
return;
} else {
if (!stateDetection) {
resolve(data);
return;
}
if (showPrompt) {
prompt.showToast({
message: data.message
});
}
reject(res);
}
} catch (error) {
reject(res);
}
},
fail: function (res, code) {
prompt.showToast({
message: '网络错误'
});
reject(res);
}
});
});
}
// 小米 1030 版本bug 小数点后超过3位的数字会被截取,解决方法转换成字符串传递
function fixXiaomiParamsBug(params) {
if (typeof params === 'object') {
for (let key in params) {
if (typeof params[key] === 'number') {
const x = String(params[key]).indexOf('.') + 1; //小数点的位置
const y = String(params[key]).length - x; //小数的位数
if (y > 3) params[key] = String(params[key]);
}
}
}
}
function sign(obj) {
// 加密签名方法
}
export default reqMethod;
sign
方法为签名方法,fixXiaomiParamsBug
修复 小米 1030 版本bug,小数点后超过 3 位的数字会被截取,解决方法为转换成字符串传递。
因为快应用对 rpk 有 1M 尺寸的限制,我们的业务在 3.0 初期版本时一度达到 900K 的尺寸,缩小 rpk 尺寸成为我们的首要任务。
除了压缩图片,适量地使用网络图片,提取公共组件和方法外,我们还发现:在快应用的模板文件中,如果多个页面通过 import
方式引入相同公共 js 文件,最后这个文件会被多次打包到 rpk 文件中,也就是说构建工具不会提取页面之间的重复引入,在公共模块使用频率较高的情况下会大幅增加包的体积。
将公共方法挂载到全局作用域上,模板中直接调用全局方法。最终打包的结果中只包含一份公共 js 的引入。
入口文件 app.ux
我们将 utils 文件夹下的方法挂在到全局 UTILS
下,对于高频使用的方法比如 API
方法可提取出来单独挂载,缩短调用路径。
1
2
3
4
5
6
7
// app.ux
const injectRef = Object.getPrototypeOf(global) || global; // 获取全局对象
import * as Utils from './utils/index.js'; // 引入公共方法
const { api } = Utils;
injectRef.UTILS = Utils; // 挂载到全局对象
injectRef.API = api; // API 方法是对 fetch 方法的封装含有 sign 和 token
在业务代码中的调用方式,如:index.ux
在模板中可直接通过 API.getBrandmenus
获取接口数据, UTILS.Formate
方法对日期做格式化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// index.ux
export default {
async getDataList() {
try {
const res = await API.getBrandmenus({ // 直接调用全局 API 方法获取接口数据。
sessionid: deviceInfo.deviceId
});
this.list = res.result.map(item => {
item.date = UTILS.Formate(item.date, 'YYYY-MM-DD'); // 调用全局公共方法 UTILS 下的 Formate 格式化时间
return item;
});
} catch (error) {
console.log(error);
}
}
}
在快应用中很多系统能力 API 都是以 callback 形式提供。比如获取地理位置API:
1
2
3
4
5
6
7
8
9
10
11
12
geolocation.getLocation({
success: function(data) {
console.log(
`handling success: longitude = ${data.longitude}, latitude = ${
data.latitude
}`
)
},
fail: function(data, code) {
console.log(`handling fail, code = ${code}`)
}
})
在业务中如果使用 callback 形式很容易写出回调地狱并且不利于代码整洁,我们可以通过一个简单的方法将 callback 形式的 API 转换成 Promise 模式的,这样业务中就可以使用 promise
或者 async/await
形式调用了。
在我们的业务中有一个 promiseAPI.js
的公共方法,负责将 callback 转换成 Promise。
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
// promiseAPI.js
import storage from '@system.storage';
import device from '@system.device';
import network from '@system.network';
import geolocation from '@system.geolocation';
const promiseFactory = (pointer, params = {}) => {
return new Promise((resolve, reject) => {
params = Object.assign({
success: (data) => { resolve(data); },
fail: (err, code) => { reject(err, code) }
}, params);
pointer(params);
});
};
// 获取设备信息。
export const getDeviceInfo = () => promiseFactory(device.getInfo);
// 获取设备Id。
export const getDeviceId = () => promiseFactory(device.getId, { type: ["device", "mac"] });
// 读取存储内容。
export const getStorage = (key) => promiseFactory(storage.get, { key });
// 修改存储内容。
export const setStorage = (key, value) => promiseFactory(storage.set, { key, value });
// 清空存储内容。
export const clearStorage = (key, value) => promiseFactory(storage.clear);
// 获取网络类型。
export const getNetworkType = () => promiseFactory(network.getType);
// 获取地理位置。
export const getLocation = () => promiseFactory(geolocation.getLocation, { timeout: 3000 });
业务代码中调用方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.ux
import * as promiseApi from './promiseAPI.js';
const getDeviceIdMethods = async () => {
try {
const data = await promiseApi.getDeviceId();
systemInfo = Object.assign({}, systemInfo, {
'deviceId': data.device,
'IMEI': data.device,
'macAddress': data.mac
});
} catch (error) {
console.log('获取设备ID失败');
}
};
一个内容丰富的选项卡,通常会包含许多页签内容。 tabs 系统组件默认会直接加载所有页签内容,导致 JS 线程持续忙于渲染每个页签,无法响应用户点击事件等,降低用户体验,为此我们在官方给出的 demo 基础上做出了一些优化。官方DEMO
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
<import name="original-tab" src="./original.ux"></import>
<import name="recommend-tab" src="./recommend.ux"></import>
<import name="chejiahao-tab" src="./chejiahao.ux"></import>
<template>
<div class="container">
<tabs onchange="onChangeTabIndex" index="{{currentindex}}" class="tab">
<tab-bar class="tab-header" mode="scrollable">
<stack class="tab-header__item" for="{{tabHeadList}}" @click="clickTabBar($idx)">
<text class="tab-header__text {{currentindex == $idx ? 'tab-header__text--active' : ''}}">{{$item.title}}</text>
<div class="tab-header__line {{currentindex == $idx ? 'tab-header__line--active' : ''}}"></div>
</stack>
</tab-bar>
<tab-content class="tab-content">
<div class="tab-content-section">
<trend-tab page-show="{{tabHeadList[0].isShow}}"></trend-tab>
</div>
<div class="tab-content-section">
<recommend-tab page-show="{{tabHeadList[1].isShow}}"></recommend-tab>
</div>
<div class="tab-content-section">
<original-tab page-show="{{tabHeadList[2].isShow}}"></original-tab>
</div>
</tab-content>
</tabs>
</div>
</template>
<script>
export default {
data() {
return {
tabHeadList: [{
title: '热榜',
pv: 'hotlist',
isShow: false
},
{
title: '推荐',
pv: 'recommend',
isShow: false
},
{
title: '原创',
pv: 'original',
isShow: false
}
],
currentindex: -1
}
},
onShow() {
this.watchCurrentIndexChange(this.currentindex, null);
},
onHide() {
this.watchCurrentIndexChange(null, this.currentindex);
},
async onInit() {
this.$watch('currentindex', 'watchCurrentIndexChange');
this.currentindex = 1; // 可以根据业务 设置某一个tab为默认显示tab
},
watchCurrentIndexChange(newVal, oldVal) {
if (oldVal !== null && oldVal >= 0) { // 判断条件:排除首次点击是
this.tabHeadList.splice(oldVal, 1, Object.assign({}, this.tabHeadList[oldVal], {
isShow: false
}));
PV_TRACK.endTime(this.tabHeadList[oldVal].pv);
}
if (newVal !== null && oldVal >= 0) { // 排除离开页面时阻止触发
this.tabHeadList.splice(newVal, 1, Object.assign({}, this.tabHeadList[newVal], {
isShow: true
}));
PV_TRACK.startTime(this.tabHeadList[newVal].pv);
}
},
clickTabBar(index) {
this.currentindex = index
},
onChangeTabIndex(evt) {
this.currentindex = evt.index;
},
}
</script>
trend-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
<template>
<div class="flex-coloum">
<list class="feed-list">
<list-item for="list" type="feedcard">
<text>{{$item.title}}</text>
</list-item>
</list>
</div>
</template>
<script>
export default {
props: [
'pageShow'
],
data() {
return {
list: []
}
},
onInit() {
this.$watch('pageShow', 'watchPageShowChange');
},
watchPageShowChange(newV, oldV) {
if (newV && !this.list.length) { // 组件监听到 `page-show` 参数为 `true` 时请求数据
this.getDataList('加载首屏');
}
},
async getDataList(model) {
// ... 请求数据
this.list = [1,2,3,4];
},
}
</script>
我们来分析下上面代码相比官方 demo 的调整。
首先进入 tab 页后所有 tab-content
下的组件都会渲染,但并不会请求接口,因为每一个组件都没有数据相当于空模板,这一改动经过实测,效果与效率均有所提升。
当 tab 组件切换或者被点击时 currentindex
的值发生改变触发监听器 watchCurrentIndexChange
执行,并修改相应 tab-content
下组件(这里用 trend-tab
组件举例)的参数 page-show
。
trend-tab
组件通过监听到 pageShow
参数触发 watchPageShowChange
方法。
当 pageShow
为 true
时请求数据,渲染列表。
当用户切换到下一个频道时当前 trend-tab
组件的 pageShow
值为 false
还可以做一些业务操作(比如视频频道 inline 播放的 video 可以暂停)。
当用户再次滑回显示 trend-tab
组件时 page-show
参数为 true
,但不满足 !this.list.length
条件,所以不会再次请求接口。
1
2
3
4
5
6
onShow() {
this.watchCurrentIndexChange(this.currentindex, null);
},
onHide() {
this.watchCurrentIndexChange(null, this.currentindex);
}
在模板中
onShow
和onHide
直接调用watchCurrentIndexChange
方法是出于上报 PV 的考虑,配合watchCurrentIndexChange
方法内的判断条件过滤一些特殊情况,可以监听用户在任意一个频道的停留时长,以及在任一频道跳出时,通过触发页面的onHide
事件进行统计上报的操作。
PV_TRACK 是我们内部的针对快应用设计的一套统计 SDK,借鉴了轻粒子快应用统计的思路,再结合我们自身的特点。
统计 PV 时需要发送 2 个请求:进入页面时发送 开始时间
,离开时发送 离开时间
,用于统计在线时长。
startTime
用于进入页面时发送开始时间。
调用示例:
1
2
3
4
5
6
onShow() {
PV_TRACK.startTime('页面标识', this.auto_open_from);
}
onHide() {
PV_TRACK.endTime('页面标识', this.auto_open_from);
}
page_show
v2.0 新增 startTime
语法糖,传参可省略 auto_open_from
参数,用于进入页面时发送开始时间,适用于一个页面对应一个 PV 签的场景。
相对应的,带有 tab 或一个页面因参数对应多个 PV 签的情况使用 startTime
和 endTime
编程的方式上报。
调用示例:
1
2
3
4
5
6
onShow() {
PV_TRACK.page_show(this, { uid: '业务扩展字段' });
}
onHide() {
PV_TRACK.page_show(this, { uid: '业务扩展字段' });
}
V2 新增 全局调用方法
PV_TRACK
,无需传递auto_open_from
参数,方法会从this
中获取。注:上报的 PV name 是当前页面的router.name
。
用于发送点击事件统计。
调用示例:
1
PV_TRACK.click(page, event);
以上是我们 PV 中暴露的一些基础 API,数据统计对一个项目的长期发展和持续性优化非常重要。
在我们的业务中有一些页面需要用到设备信息,但这些获取设备信息的方法多数都是异步的需要用户授权后才可获取,举个例子,之家快应用的车系综述页需要获取地理位置信息后,给出用户所在地的车源信息,此时该页面将会自行获取地理位置,但是 PV统计也获取了地理位置信息,导致程序中有两个不同位置的方法同时调用获取设备信息。
这暴露出 3 个问题:
解决方法:抽象获取设备信息公共方法,需要获取设备信息的需求依赖获取设备信息公共方法执行完毕后调用,解决共用数据保证调用顺序。
实现获取设备信息 从 PV 方法中剥离:
getDeviceData
方法返回获取设备基础信息的 Promise (包括 device.getInfo
、device.getId
、 network.getType
、geolocation.getLocation
)。
deviceData
用户存储 getDeviceData
方法执行后返回的数据。
PV 方法中建立了发送队列,在 pv.setInfo(res)
方法被调用前不会上报统计,待调用 pv.setInfo(res)
后 pv 方法会连同传入的设备信息按照顺序上报。
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
const { PV, getDeviceData } = Utils;
const pv = new PV();
injectRef.PV_TRACK = pv;
let deviceData = null;
let deviceCompleteArray = [];
export default {
onCreate() {
// 快应用启动统计
pv.send({
cate: 'clt',
event: 'kuai_app_launch_clt'
});
getDeviceData().then(res => {
deviceData = res;
console.log('################ 获取设备信息 #################');
pv.setInfo(res);
while (deviceCompleteArray.length) {
const resolve = deviceCompleteArray.shift();
resolve(res);
}
}).catch(err => {
console.log(err);
});
},
getDeviceInfo() {
return new Promise((resolve, reject) => {
if (!deviceData) {
deviceCompleteArray.push(resolve);
} else {
resolve(deviceData);
}
});
}
};
业务代码中实现等待设备信息逻辑:
1
2
3
4
5
6
7
8
async onInit() {
try {
await this.$app.$def.getDeviceInfo();
await this.getList();
} catch (error) {
console.log(error);
}
}
this.$app.$def.getDeviceInfo()
返回一个 Promise,并插入 deviceCompleteArray
数组中。deviceCompleteArray
数组中每一项的 resolve
,实现阻塞代码的后续执行。1
2
3
4
while (deviceCompleteArray.length) {
const resolve = deviceCompleteArray.shift();
resolve(res);
}
至此实现多入口调用集中依赖同一个获取方法的功能。
上面总结的一些小方法和思路应用到项目中可以提升开发效率,在项目中我们遵循开发规范可以保证快应用项目的可维护性和扩展性,未来我们将会持续打磨和优化代码,并更多的输出一些我们在项目开发过程中的经验。
]]>https://www.utorrent.com/intl/zh_cn/downloads/linux。
下载 µTorrent Server for Ubuntu 12.04 解压到指定目录中。
运行 ./utserver
文件
./utserver &
可以启动后台运行。 通过 jobs 查看后台正在运行的进程。
在浏览器输入 http://localhost:8080/gui/
,帐户为 admin
密码为空,就可以登录 uTorrent Web 控制台了。
1
sudo apt-get install nginx
所有的配置文件都在/etc/nginx
下,并且每个虚拟主机已经安排在了/etc/nginx/sites-available
下
程序文件在/usr/sbin/nginx
日志放在了/var/log/nginx
中
并已经在/etc/init.d/
下创建了启动脚本nginx
默认的虚拟主机的目录设置在了/var/www/nginx-default
(有的版本 默认的虚拟主机的目录设置在了/var/www
, 请参考/etc/nginx/sites-available
里的配置)
1
2
3
sudo /etc/init.d/nginx start # 启动
sudo nginx -s reload # 重启
编辑 /etc/nginx/sites-available/default
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server {
listen 80; # 端口
server_name localhost; # 服务名
charset utf-8; # 避免中文乱码
root /dev/shm/update; # 显示的根索引目录,注意这里要改成你自己的,目录要存在
location / {
autoindex on; # 开启索引功能
autoindex_exact_size off; # 关闭计算文件确切大小(单位bytes),只显示大概大小(单位kb、mb、gb)
autoindex_localtime on; # 显示本机时间而非 GMT 时间
if ($request_filename ~* ^.*?\.(txt|doc|pdf|rar|gz|zip|docx|exe|xlsx|ppt|pptx|zip)$){
add_header Content-Disposition: 'attachment;';
}
}
}
nethogs 可以查看实时进程网络占用。
安装:sudo apt install nethogs
查看网络状态: nethogs eth0
即 nethogs + 网卡名称,双击table会出现备选网卡名称
1
df -h
解压:
1
unzip FileName.zip
压缩:
1
zip FileName.zip DirName
zip -r example_service.zip example_service/
-r 将指定的目录下的所有子目录以及文件一起处理
服务器端口:自己设定(如不设定,默认从 9000-19999 之间随机生成)
密码:自己设定(如不设定,默认为 teddysun.com)
加密方式:自己设定(如不设定,默认为 aes-256-gcm)
备注:脚本默认创建单用户配置文件,如需配置多用户,安装完毕后参照下面的教程示例手动修改配置文件后重启即可。
https://github.com/shadowsocks/shadowsocks-windows/releases
使用root用户登录,运行以下命令:
1
2
3
wget --no-check-certificate -O shadowsocks.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks.sh
chmod +x shadowsocks.sh
./shadowsocks.sh 2>&1 | tee shadowsocks.log
1
2
3
4
5
6
7
8
Congratulations, Shadowsocks-python server install completed!
Your Server IP :your_server_ip
Your Server Port :your_server_port
Your Password :your_password
Your Encryption Method:your_encryption_method
Welcome to visit: https://teddysun.com/342.html
Enjoy it!
使用root用户登录,运行以下命令:
1
./shadowsocks.sh uninstall
最近2周的时间由于工作不忙,一直在看有关GraphQL
的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC
风格的接口。后来转做了前端开发,从实现接口者变成了调用接口者,接触最多的当属REST
风格的接口。因此在这段学习GraphQL
的过程中,并且也尝试使用它以全栈的角度做了一个小项目,在这个过程中,一直在思考它对比前两者在API
设计的整体架构体系中的各个指标上,孰优孰劣。
其实在使用和学习的过程中,有很多伟德伟诺官网都对比过它们的异同,但是大部分伟德伟诺官网并没有从一个相对客观的角度来对比,更多是为了突显一个的优点而刻意指出另外一个的缺点。这让我想到一句话,脱离业务情景谈bv伟德登录入口就是耍流氓。
昨天订阅的GraphQL Weekly
中推送的一个视频正好是讲关于它们这三者的,于是就点进去看了看,发现质量还是不错的,于是就想整理出来,分享给大家。
原视频地址(油管地址,自备梯子):这里
如果没有梯子的话直接看我整理的东西也可以,我觉的应该都覆盖到视频中所讲的重点内容了。
当然,这些内容如果分开来讲,每一块内容所涉及的东西都够写一本书了,这里仅仅是简单归纳和整理,从宏观的角度来对比它们的异同,从而能够在日后面临bv伟德登录入口选型时,有一个更佳明确的决策方向。
先简单介绍下RPC
,它是Remote Procedure Call(远程过程调用)
的简称。一般基于RPC
协议所设计的接口,是基于网络采用客户端/服务端的模式完成调用接口的。
function explosion
关于RPC
的优点其实很好理解,就是因为它性能高同时又很简单,但是我认为这是对于接口提供者来讲的(因为它的高耦合性)。
但是如果从接口调用者的角度来看,高耦合性就变成了缺点,因为高耦合意味着调用者必须要足够了解系统本身的实现才能够完成调用,比如:
breaking change
(违背开闭原则)RPC
所暴露接口仅仅会暴露函数的名称和参数等信息,对于函数之间的调用关系无法提供,这意味着调用者必须足够了解系统,从能够知道如何正确的调用这些接口,但是对于接口调用者往往不需要了解过多系统内部实现细节关于上面的第二点,为了减少breaking change
,我之前实现接口的时候一般都会引入版本的概念,就是在暴露接口的方法名中加入版本号,一开始效果确实不错,但是随后就不知不觉的形成了function explosion
,和视频中主讲人所举例的例子差不多,贴一下视频中的截图感受一波:
当前REST风格的API架构方式已经成了主流解决方案了,相比较RPC,它的主要不同之处在于,它是对于资源(Resource)的模型化而非步骤(Procedure)。
REST的优点基本解决了RPC中存在的问题,就是解耦,从而使得前后端分离成为可能。接口提供者在修改接口时,不容易造成breaking-change,接口调用者在调用接口时,往往面向数据模型编程,而省去了了解接口本身的时间成本。
但是,我认为REST当前最大的问题在于虽然它利用http
的动词约束了接口的暴露方式,同时增强了语义,但是却没有约束接口如何返回数据的最佳实践,总让人感觉只要是返回json格式的接口都可以称作REST。
我在实际工作中,经常会遇到第二条缺点所指出的问题,就是接口返回的数据冗余度很高,但是却缺少我真正需要的数据,因此不得已只能调用其他接口或者直接和后端商议修改接口,并且这种问题会在web端和移动端共用一套接口中被放大。
当前比较好的解决方案就是规范化返回数据的格式,比如json-schema或者自己制定的规范。
GraphQL是近来比较热门的一个bv伟德登录入口话题,相比REST和RPC,它汲取了两者的优点,即不面向资源,也不面向过程,而是面向数据查询(ask for exactly what you want)。
同时GraphQL本身需要使用强类型的Schema来对数据模型进行定义,因此相比REST它的约束性更强。
鉴于GraphQL这两个星期我也仅仅是做了一些简单地使用和了解,仅仅说一下感受。
首先值得肯定的是,在某些程度上确实解决了REST的缺点所带来的问题,同时配套社区建议的各种工具和库,相比使用REST风格,全栈开发体验上升一个台阶。
但是这个看起来很好的东西为什么没有火起来呢?我觉的最主要的原因是因为GraphQL所带来的好处,大部分是对于接口调用者而言的,但是实现这部分的工作却需要接口提供者来完成。
同时GraphQL的最佳实践场景应当是类似像Facebook这样的网站,业务逻辑模型是图状数据结构,比如社交。如果在一些业务逻辑模型相对简单的场景,使用GraphQL确实不如使用REST来得简单明了、直截了当。
另外一方面是GraphQL的使用场景相当灵活,在我自己的调研项目中,我是把它当做一个类似ORM的框架来使用的,在别人的一些伟德伟诺官网中,会把它当做一个中间层来做渐进式开发和系统升级。这应当算是另外一个优点。
下面根据要设计的API类型给予一些bv伟德登录入口选型建议。
如果是Management API
,这类API的特点如下:
这种情景使用REST + JSON API
可能会更好。
如果是Command or Action API
,这类API的特点如下:
这种情况使用RPC
就足够了。
如果是Internal Micro Services API
,这类API的特点如下:
这种情景仍然建议使用RPC
。
如果是Micro Services API
,这类API的特点如下:
这种情景使用RPC
或者REST
均可。
如果是Data or Mobile API
,这类API的特点是:
这种场景无疑GraphQL
是最好的选择。
提供一张表格来总览它们之间在不同指标下的表现:
最后引用人月神话中的观点no silver bullet
,在bv伟德登录入口选型时需要具体情况具体分析,不过鉴于GraphQL的灵活性,把它与RPC和REST配置使用,也是不错的选择。
]]>
最近2周的时间由于工作不忙,一直在看有关GraphQL
的东西,前后端均有涉及,由于我之前做过后端开发,当时实现的接口的大体是符合RPC
风格的接口。后来转做了前端开发,从实现接口]]>
youtube-dl官网:https://yt-dl.org/
项目地址:https://github.com/rg3/youtube-dl
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
Usage: youtube-dl [OPTIONS] URL [URL...]
Options:
通用选项:
-h, --help 打印帮助文档
--version 打印版本信息
-U, --update 更新到最新版(需要权限)
-i, --ignore-errors 遇到下载错误时跳过
--abort-on-error 遇到下载错误时终止
--dump-user-agent 显示当前使用的浏览器(User-agent)
--list-extractors 列出所有的提取器(支持的网站)
--extractor-descriptions 同上
--force-generic-extractor 强制使用通用提取器下载
--default-search PREFIX 使用此前缀补充不完整的URLs,例如:"ytsearch2 yt-dl" 从youtube搜索并下载两个关于yt-dl视频. 使用"auto"youtube-dl就会猜一个,一般效果等价于"ytsearch"("auto_warning"猜测时加入警告).我已知支持的PREFIX:ytsearch (youtube), ytsearchdate (youtube), yvsearch (yahoo videos), gvsearch (google videos)
--ignore-config 不读取配置文件,当时用了全局配置文件/etc/youtube-dl.conf:不再读取 ~/.config/youtube-dl/config (%APPDATA%/youtube-dl/config.txt on Windows)
--config-location PATH 使用指定路径下的配置文件
--flat-playlist 列出列表视频但不下载
--mark-watched 标记看过此视频 (YouTube only)
--no-mark-watched 不标记看过此视频 (YouTube only)
--no-color 打印到屏幕上的代码不带色
网络选项:
--proxy URL 使用HTTP/HTTPS/SOCKS协议的代理.如:socks5://127.0.0.1:1080/.
--socket-timeout SECONDS 放弃连接前等待时间
--source-address IP 绑定的客户端IP地址
-4, --force-ipv4 所有连接通过IPv4
-6, --force-ipv6 所有连接通过IPv6
地理限制:
--geo-verification-proxy URL 使用此代理地址测试一些有地理限制的地址
--geo-bypass 绕过地理限制通过伪装X-Forwarded-For HTTP头部的客户端ip (实验)
--no-geo-bypass 不 绕过地理限制通过伪装X-Forwarded-For HTTP头部的客户端ip (实验)
--geo-bypass-country CODE 强制绕过地理限制通过提供准确的ISO 3166-2标准的国别代码(实验) 注:以上三个实验参数实测未成功
视频选择:
--playlist-start NUMBER 指定列表中开始下载的视频(默认为1)
--playlist-end NUMBER 指定列表中结束的视频(默认为last)
--playlist-items ITEM_SPEC 指定列表中要下载的视频项目编号.如:"--playlist-items 1,2,5,8"或"--playlist-items 1-3,7,10-13"
--match-title REGEX 下载标题匹配的视频(正则表达式或区分大小写的字符串)
--reject-title REGEX 跳过下载标题匹配的视频(正则表达式或区分大小写的字符串)
--max-downloads NUMBER 下载NUMBER个视频后停止
--min-filesize SIZE 不下载小于SIZE的视频(e.g. 50k or 44.6m)
--max-filesize SIZE 不下载大于SIZE的视频(e.g. 50k or 44.6m)
--date DATE 仅下载上传日期在指定日期的视频
--datebefore DATE 仅下载上传日期在指定日期或之前的视频 (i.e. inclusive)
--dateafter DATE 仅下载上传日期在指定日期或之后的视频 (i.e. inclusive)
--min-views COUNT 不下载观影数小于指定值的视频
--max-views COUNT 不下载观影数大于指定值的视频
--match-filter FILTER 通用视频过滤器. Specify any key (see help for -o for a list of available keys) to match if the key is present, !key to check if the key is not present, key > NUMBER (like "comment_count > 12", also works with >=, <, <=, !=, =) to compare against a number,key = 'LITERAL' (like "uploader = 'Mike Smith'", also works with !=) to match against a string literal and & to require multiple matches. Values which are not known are excluded unless you put a question mark (?) after the operator. For example, to only match videos that have been liked more than 100 times and disliked less than 50 times (or the dislike functionality is not available at the given service), but who also have a description, use --match-filter "like_count > 100 & dislike_count <? 50 & description" .
--no-playlist 当视频链接到一个视频和一个播放列表时,仅下载视频
--yes-playlist 当视频链接到一个视频和一个播放列表时,下载视频和播放列表
--age-limit YEARS 下载合适上传年限的视频
--download-archive FILE 仅下载档案文件中未列出的影片,已下载的记录ID
--include-ads 同时下载广告(实验)
下载选项:
-r, --limit-rate RATE 最大bps (e.g. 50K or 4.2M)
-R, --retries RETRIES 重试次数 (默认10), or "infinite".
--fragment-retries RETRIES 一个分段的最大重试次数(default is 10), or "infinite" (DASH, hlsnative and ISM)
--skip-unavailable-fragments 跳过不可用分段(DASH, hlsnative and ISM)
--abort-on-unavailable-fragment 放弃某个分段当不可获取时
--keep-fragments 下载完成后,将下载的片段保存在磁盘上; 片段默认被删除
--buffer-size SIZE 设置缓冲区大小buffer (e.g. 1024 or 16K) (default is 1024)
--no-resize-buffer 不自动调整缓冲区大小.默认情况下自动调整
--playlist-reverse 以相反的顺序下载播放列表视频
--playlist-random 以随机的顺序下载播放列表视频
--xattr-set-filesize Set file xattribute ytdl.filesize with expected file size (experimental)
--hls-prefer-native 使用本机默认HLS下载器而不是ffmpeg
--hls-prefer-ffmpeg 使用ffmpeg而不是本机HLS下载器
--hls-use-mpegts 使用TS流容器来存放HLS视频,一些高级播放器允许在下载的同时播放视频
--external-downloader COMMAND 使用指定的第三方下载工具,当前支持:aria2c,avconv,axel,curl,ffmpeg,httpie,wget
--external-downloader-args ARGS 给第三方下载工具指定参数,如:--external-downloader aria2c --external-downloader-args -j8
文件系统选项:
-a, --batch-file FILE 文件中包含需要下载的URL
--id 仅使用文件名中的视频ID
-o, --output TEMPLATE Output filename template, see the "OUTPUT TEMPLATE" for all the info
--autonumber-start NUMBER 指定%(autonumber)s的起始值(默认为1)
--restrict-filenames 将文件名限制为ASCII字符,并避免文件名中的“&”和空格
-w, --no-overwrites 不要覆盖文件
-c, --continue 强制恢复部分下载的文件。 默认情况下,youtube-dl仅在可能时将恢复下载。
--no-continue 不要恢复部分下载的文件(从头开始重新启动)
--no-part 不使用.part文件 - 直接写入输出文件
--no-mtime 不使用Last-modified header来设置文件最后修改时间
--write-description 将视频描述写入.description文件
--write-info-json 将视频元数据写入.info.json文件
--write-annotations 将视频注释写入.annotations.xml文件
--load-info-json FILE 包含视频信息的JSON文件(使用“--write-info-json”选项创建)
--cookies FILE 文件从中读取Cookie(经测试,export cookies插件可以使用,但firebug导出的cookies导致错误,chrome下请用cookies.txt)注意:不同平台windows、Linux、OSX之间需要转换CE LF才能使用!
--cache-dir DIR 文件存储位置。youtube-dl需要永久保存一些下载的信息。默认为$XDG_CACHE_HOME/youtube-dl或/.cache/youtube-dl。目前,只有YouTube播放器文件(对于具有模糊签名的视频)进行缓存,但可能会发生变化。
--no-cache-dir 不用缓存
--rm-cache-dir 删除所有缓存文件
缩略图:
--write-thumbnail 把缩略图写入硬盘
--write-all-thumbnails 将所有缩略图写入磁盘
--list-thumbnails 列出所有可用的缩略图格式
详细/模拟选项:
-q, --quiet 激活退出模式
--no-warnings 忽略警告
-s, --simulate 不下载不存储任何文件到硬盘,模拟下载模式
--skip-download 不下载视频
-g, --get-url 模拟下载获取视频直连
-e, --get-title 模拟下载获取标题
--get-id 模拟下载获取id
--get-thumbnail 模拟下载获取缩略图URL
--get-description 模拟下载获取视频描述
--get-duration 模拟下载获取视频长度
--get-filename 模拟下载获取输出视频文件名
--get-format 模拟下载获取输出视频格式
-j, --dump-json 模拟下载获取JSON information.
-J, --dump-single-json 模拟下载获取每条命令行参数的JSON information.如果是个播放列表,就获取整个播放列表的JSON
--print-json 下载的同时获取视频信息的JSON
--newline 进度条在新行输出
--no-progress 不打印进度条
--console-title 在控制台标题栏显示进度
-v, --verbose 打印各种调试信息
--dump-pages 打印下载下来的使用base64编码的页面来调试问题(非常冗长)
--write-pages 将下载的中间页以文件的形式写入当前目录中以调试问题
--print-traffic 显示发送和读取HTTP流量
-C, --call-home 联系youtube-dl服务器进行调试
--no-call-home 不联系youtube-dl服务器进行调试
解决方法:
--encoding ENCODING 强制指定编码(实验)
--no-check-certificate 禁止HTTPS证书验证
--prefer-insecure 使用未加密的连接来检索有关视频的信息(目前仅支持YouTube)
--user-agent UA 指定user agent
--referer URL 指定自定义的referer,仅限视频来源于同一网站
--add-header FIELD:VALUE 指定一个自定义值的HTTP头文件,使用分号分割,可以多次使用此选项
--bidi-workaround 围绕缺少双向文本支持的终端工作。需要在PATH中有bidiv或fribidi可执行文件
--sleep-interval SECONDS 在每次下载之前休眠的秒数,或者每次下载之前的随机睡眠的范围的下限(最小可能的睡眠秒数)与-max-sleep-interval一起使用。
--max-sleep-interval SECONDS 每次下载前随机睡眠范围的上限(最大可能睡眠秒数)。只能与--min-sleep-interval一起使用。
视频格式选项:
-f, --format FORMAT 视频格式代码,查看"FORMAT SELECTION"获取所有信息
--all-formats 获取所有视频格式
--prefer-free-formats 开源的视频格式优先,除非有特定的请求
-F, --list-formats 列出请求视频的所有可用格式
--youtube-skip-dash-manifest 不要下载关于YouTube视频的DASH清单和相关数据
--merge-output-format FORMAT 如果需要合并(例如bestvideo + bestaudio),则输出到给定的容器格式。mkv,mp4,ogg,webm,flv之一。如果不需要合并,则忽略
字幕选项:
--write-sub 下载字幕文件
--write-auto-sub 下载自动生成的字幕文件 (YouTube only)
--all-subs 下载所有可用的字幕
--list-subs 列出所有字幕
--sub-format FORMAT 字幕格式,接受格式偏好,如:"srt" or "ass/srt/best"
--sub-lang LANGS 要下载的字幕的语言(可选)用逗号分隔,请使用--list-subs表示可用的语言标签
验证选项:
-u, --username USERNAME 使用ID登录
-p, --password PASSWORD 账户密码,如果此选项未使用,youtube-dl将交互式地询问。
-2, --twofactor TWOFACTOR 双因素认证码
-n, --netrc 使用.netrc认证数据
--video-password PASSWORD 视频密码(vimeo, smotri, youku)
Adobe Pass Options:
--ap-mso MSO Adobe Pass多系统运营商(电视提供商)标识符,使用--ap-list-mso列出可用的MSO
--ap-username USERNAME MSO账号登录
--ap-password PASSWORD 账户密码,如果此选项未使用,youtube-dl将交互式地询问。
--ap-list-mso 列出所有支持的MSO
后处理选项:
-x, --extract-audio 将视频文件转换为纯音频文件(需要ffmpeg或avconv和ffprobe或avprobe)
--audio-format FORMAT 指定音频格式: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "best" by default;-x存在时无效
--audio-quality QUALITY 指定ffmpeg/avconv音频质量,为VBR插入一个0(best)-9(worse)的值(默认5),或者指定比特率
--recode-video FORMAT 必要时将视频转码为其他格式(当前支持: mp4|flv|ogg|webm|mkv|avi)
--postprocessor-args ARGS 给后处理器提供这些参数
-k, --keep-video 视频文件在后处理后保存在磁盘上; 该视频默认被删除
--no-post-overwrites 不要覆盖后处理文件; 默认情况下,后处理文件将被覆盖
--embed-subs 在视频中嵌入字幕(仅适用于mp4,webm和mkv视频)
--embed-thumbnail 将缩略图嵌入音频作为封面艺术
--add-metadata 将元数据写入视频文件
--metadata-from-title FORMAT 从视频标题中解析附加元数据,如歌曲标题/艺术家。格式语法和--output相似.也可以使用带有命名捕获组的正则表达式。解析的参数替换现有值。Example: --metadata-from-title "%(artist)s - %(title)s" matches a title like "Coldplay - Paradise". Example (regex): --metadata-from-title "(?P<artist>.+?) - (?P<title>.+)"
--xattrs 将元数据写入视频文件的xattrs(使用dublin core 和 xdg标准)
--fixup POLICY 自动更正文件的已知故障。never(不做警告), warn(只发出警告), detect_or_warn (默认;如果可以的话修复文件,否则警告)
--prefer-avconv 后处理时相较ffmpeg偏向于avconv
--prefer-ffmpeg 后处理优先使用ffmpeg
--ffmpeg-location PATH ffmpeg/avconv程序位置;PATH为二进制所在文件夹或者目录.
--exec CMD 在下载后对文件执行命令,类似于find -exec语法.示例:--exec'adb push {} /sdcard/Music/ && rm {}'
--convert-subs FORMAT 转换字幕格式(当前支持: srt|ass|vtt)
youtube-dl使用Python编写的工具,所以系统里没有Python的话,先安装Python。youtube-dl需要Python 2.6以上的版本。
1
sudo apt-get install python2.7
1
yum install -y python python-devel
下载地址:https://github.com/rg3/youtube-dl
Linux, macOS:
1
2
sudo curl -L https://yt-dl.org/downloads/latest/youtube-dl -o /usr/local/bin/youtube-dl
sudo chmod a+rx /usr/local/bin/youtube-dl
如果没有 curl 可以使用 wget
1
2
sudo wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl
sudo chmod a+rx /usr/local/bin/youtube-dl
FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec
1.升级系统 (CentOS)
1
2
3
sudo yum install epel-release -y
sudo yum update -y
sudo shutdown -r now
2.安装Nux Dextop Yum 源
由于CentOS没有官方FFmpeg rpm软件包。但是,我们可以使用第三方YUM源(Nux Dextop)完成此工作。
1) CentOS 7
1
2
sudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm
2) CentOS 6
1
2
sudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro
sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el6/x86_64/nux-dextop-release-0-2.el6.nux.noarch.rpm
3.安装FFmpeg 和 FFmpeg开发包
1
sudo yum install ffmpeg ffmpeg-devel -y
4.测试是否安装成功
1
ffmpeg
5.如果你想了解更多关于FFmpeg使用方面的资料,可以输入:
1
ffmpeg -h
例子:
使用FFmpeg将mp3转为ogg
1
ffmpeg -i MLKDream_64kb.mp3 -c:a libvorbis -q:a 4 MLKDream_64kb.ogg
使用FFmpeg将flv转为mp4
1
ffmpeg -i beeen.flv -y -vcodec copy -acodec copy beeen.mp4
1
sudo add-apt-repository ppa:djcj/hybrid
1
sudo apt-get update
1
sudo apt-get install ffmpeg
使用帮助命令查看其用法:
1
youtube-dl -h
一些常用的参数:
youtube-dl –list-extractors #查看支持网站列表
youtube-dl -U #程序升级
youtube-dl –get-format URL #获取视频格式
youtube-dl -F URL #获取所有格式(目前仅支持YouTube),例如:
1
youtube-dl -F http://www.youtube.com/watch?v=n-BXNXvTvV4
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
[youtube] Setting language
[youtube] n-BXNXvTvV4: Downloading video webpage
[youtube] n-BXNXvTvV4: Downloading video info webpage
[youtube] n-BXNXvTvV4: Extracting video information
Available formats:
: mp4 [1080x1920]
: webm [1080x1920]
: mp4 [720x1280]
: webm [720x1280]
: flv [480x854]
: webm [480x854]
: flv [360x640]
: mp4 [360x640]
: webm [360x640]
: flv [240x400]
: 3gp [240x320]
: 3gp [144x176]
: mp4 [1080p] (DASH Video)
: mp4 [720p] (DASH Video)
: mp4 [480p] (DASH Video)
: mp4 [360p] (DASH Video)
: mp4 [240p] (DASH Video)
: mp4 [192p] (DASH Video)
: mp4 [256k] (DASH Audio)
: webm [256k] (DASH Audio)
: mp4 [128k] (DASH Audio)
: webm [128k] (DASH Audio)
: mp4 [48k] (DASH Audio)
youtube-dl -f format URL #下载指定格式的视频,这里以下载1080p原画质量的视频格式为例:
1
youtube-dl -f 137 http://www.youtube.com/watch?v=n-BXNXvTvV4
推荐使用SS代理。非全局模式,请在命令后面加 --proxy 'socks5://127.0.0.1:1080'
(1080是端口号,我的端口是1080,您的端口请按照您自己设置的填写)
例如: youtube-dl --proxy 'socks5://127.0.0.1:1080' [URL]
1) 查看视频所有类型,只看不下载
youtube-dl -F [url]
或者
youtube-dl –list-formats [url]
这是一个列清单参数,执行后并不会下载视频,但能知道这个目标视频都有哪些格式存在,这样就可以有选择的下载啦!
下载指定质量的视频和音频并自动合并
youtube-dl -f [format code] [url]
通过上一步获取到了所有视频格式的清单,最左边一列就是编号对应着不同的格式.
由于YouTube的1080p及以上的分辨率都是音视频分离的,所以我们需要分别下载视频和音频,可以使用137+140这样的组合.
如果系统中安装了ffmpeg的话, youtube-dl 会自动合并下下好的视频和音频, 然后自动删除单独的音视频文件
youtubd-dl –write-sub [url] //这样会下载一个vtt格式的英文字幕和mkv格式的1080p视频下来
youtube-dl –write-sub –skip-download [url] //下载单独的vtt字幕文件,而不会下载视频
youtube-dl –write-sub –all-subs [url] //下载所有语言的字幕(如果有的话)
youtube-dl –write-auto-sub [url] //下载自动生成的字幕(YouTube only)
youtube-dl –list-subs [url] //列出所有可用字幕
获取所有语言的字幕列表:’http://video.google.com/timedtext?hl=en&v=hRfHcp2GjVI&type=list‘
获取字幕,在视频有官方字幕的情况下:’http://www.youtube.com/api/timedtext?lang=%s&v=%s&name=%s‘
「上传字幕」和「机器字幕」是不互相「兼容」的,有「上传字幕」的视频是没有「机器字幕」的,当然一个视频也可能「上传字幕」和「机器字幕」都没有
youtube-dl -f [format code] [palylist_url] //这种方式可以下载制定清晰度的mp4视频
youtube-dl [playlist_url] //下载视频列表,这种方式下载的视频可能是mkv格式或者webm格式
youtube-dl -cit [playlist_url] //下载视频列表,这种方式下载的视频可能是mkv格式或者webm格式
youtube-dl –yes-playlist [url] //当链接为视频列表,则下载该列表视频,跟上面的一样,可能是mkv或者webm格式
1
youtube-dl -f 137+140 https://www.youtube.com/watch?v=Zaem7Ok3PjE
1
youtube-dl -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio' --merge-output-format mp4 https://www.youtube.com/watch?v=Zaem7Ok3PjE
使用选项 --download-archive FILE
youtube-dl 读取并添加到不再下载的文件列表。每次成功下载文件时,该视频ID都会添加到FILE。
您可以按如下方式使用它:
1
youtube-dl --download-archive downloaded.txt --no-post-overwrites -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/bestvideo+bestaudio' --merge-output-format mp4 https://www.youtube.com/watch?v=Zaem7Ok3PjE
他会记录当前频道中已经下载过的视频,这样方便你随时删除已经下载过的视频防止重新下载。
1
JSON.stringify($$('#thumbnail').map(item => item.href))
youtube-dl官网:https://yt-dl.org/
项目地址:
在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。
更多的形式下采用的基于 Token 的验证机制,JWT 本质的也是一种 Token,但是其中又有些许不同。
JWT 及时 JSON Web Token,它是基于 RFC 7519 所定义的一种在各个系统中传递紧凑和自包含的 JSON 数据形式。
JSON Web Token 由三部分组成使用 . 分割开:
一个 JWT 形式上类似于下面的样子:
xxxxx.yyyy.zzzz
Header 一般由两个部分组成:
alg 是是所使用的 hash 算法例如 HMAC SHA256 或 RSA,typ 是 Token 的类型自然就是 JWT。
1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}
然后使用 Base64Url 编码成第一部分。1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<second part>.<third part>
这一部分是 JWT 主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims 的实体一般包含用户和一些元数据,这些 claims 分成三种类型:reserved, public, 和 private claims。
-(保留声明)reserved claims :预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject), aud(audience) 等。
这里都使用三个字母的原因是保证 JWT 的紧凑
-(公有声明)public claims : 这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。
-(私有声明)private claims : 这个部分是共享被认定信息中自定义部分。
一个 Pyload 可以是这样子的:
1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
这部分同样使用 Base64Url 编码成第二部分。
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.<third part>
在创建该部分时候你应该已经有了 编码后的 Header 和 Payload 还需要一个一个秘钥,这个加密的算法应该 Header 中指定。
一个使用 HMAC SHA256 的例子如下:
1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
这个 signature 是用来验证发送者的 JWT 的同时也能确保在期间不被篡改。
所以,做后你的一个完整的 JWT 应该是如下形式:
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意被 . 分割开的三个部分
在用户使用证书或者账号密码登入的时候一个 JSON Web Token 将会返回,同时可以把这个 JWT 存储在local storage、或者 cookie 中,用来替代传统的在服务器端创建一个 session 返回一个 cookie。
当用户想要使用受保护的路由时候,应该要在请求得时候带上 JWT ,一般的是在 header 的 Authorization
使用 Bearer
的形式,一个包含的 JWT 的请求头的 Authorization 如下:
1
Authorization: Bearer <token>
这是一中无状态的认证机制,用户的状态从来不会存在服务端,在访问受保护的路由时候回校验 HTTP header 中 Authorization 的 JWT,同时 JWT 是会带上一些必要的信息,不需要多次的查询数据库。
这种无状态的操作可以充分的使用数据的 APIs,甚至是在下游服务上使用,这些 APIs 和哪服务器没有关系,因此,由于没有 cookie 的存在,所以在不存在跨域(CORS, Cross-Origin Resource Sharing)的问题。
JWT 在各个 Web 框架中都有 JWT 的包可以直接使用,下面使用 Flask 和 Express 作为例子演示。
下面会使用 httpie 作为演示工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
HTTPie: HTTP client, a user-friendly cURL replacement.
- Download a URL to a file:
http -d example.org
- Send form-encoded data:
http -f example.org name="bob" profile-picture@"bob.png"
- Send JSON object:
http example.org name="bob"
- Specify an HTTP method:
http HEAD example.org
- Include an extra header:
http example.org X-MyHeader:123
- Pass a user name and password for server authentication:
http -a username:password example.org
- Specify raw request body via stdin:
cat data.txt | http PUT example.org
这里的演示是 Flask-JWT
的 Quickstart内容。
安装必要的软件包:
1
2
pip install flask
pip install Flask-JWT
一个简单的 DEMO:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from flask import Flask
from flask_jwt import JWT, jwt_required, current_identity
from werkzeug.security import safe_str_cmp
class User(object):
def __init__(self, id, username, password):
self.id = id
self.username = username
self.password = password
def __str__(self):
return "User(id="%s")" % self.id
users = [
User(1, "user1", "abcxyz"),
User(2, "user2", "abcxyz"),
]
username_table = {u.username: u for u in users}
userid_table = {u.id: u for u in users}
def authenticate(username, password):
user = username_table.get(username, None)
if user and safe_str_cmp(user.password.encode("utf-8"), password.encode("utf-8")):
return user
def identity(payload):
user_id = payload["identity"]
return userid_table.get(user_id, None)
app = Flask(__name__)
app.debug = True
app.config["SECRET_KEY"] = "super-secret"
jwt = JWT(app, authenticate, identity)
@app.route("/protected")
@jwt_required()
def protected():
return "%s" % current_identity
if __name__ == "__main__":
app.run()
首先需要获取用户的 JWT:
1
2
3
4
5
6
7
8
9
10
% http POST http://127.0.0.1:5000/auth username="user1" password="abcxyz" ~
HTTP/1.0 200 OK
Content-Length: 193
Content-Type: application/json
Date: Sun, 21 Aug 2016 03:48:41 GMT
Server: Werkzeug/0.11.10 Python/2.7.10
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"
}
使用 @jwt_required()
装饰器来保护你的 API
1
2
3
4
@app.route("/protected")
@jwt_required()
def protected():
return "%s" % current_identity
这时候你需要在 HTTP 的 header 中使用 Authorization: JWT <token>
才能获取数据
1
2
3
4
5
6
7
8
% http http://127.0.0.1:5000/protected Authorization:"JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZGVudGl0eSI6MSwiaWF0IjoxNDcxNzUxMzIxLCJuYmYiOjE0NzE3NTEzMjEsImV4cCI6MTQ3MTc1MTYyMX0.S0825N6IliQb65QoJfUXb3IGq-j9OVJpHBh-bcUz_gc"
HTTP/1.0 200 OK
Content-Length: 12
Content-Type: text/html; charset=utf-8
Date: Sun, 21 Aug 2016 03:51:20 GMT
Server: Werkzeug/0.11.10 Python/2.7.10
User(id="1")
不带 JWT 的时候会返回如下信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
% http http://127.0.0.1:5000/protected ~
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 125
Content-Type: application/json
Date: Sun, 21 Aug 2016 03:49:51 GMT
Server: Werkzeug/0.11.10 Python/2.7.10
WWW-Authenticate: JWT realm="Login Required"
{
"description": "Request does not contain an access token",
"error": "Authorization Required",
"status_code": 401
}
Auth0 提供了 express-jwt 这个包,在 express 可以很容易的集成。
1
2
3
4
5
npm install express --save
npm install express-jwt --save
npm install body-parser --save
npm install jsonwebtoken --save
npm install shortid --save
本例子中只是最简单的使用方法,更多使用方法参看 express-jwt
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
const express = require("express");
const expressJwt = require("express-jwt");
const bodyParser = require("body-parser");
const jwt = require("jsonwebtoken");
const shortid = require("shortid");
const app = express();
app.use(bodyParser.json());
app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));
app.use(function (err, req, res, next) {
if (err.name === "UnauthorizedError") {
res.status(401).send("invalid token");
}
});
app.post("/login", function(req, res) {
const username = req.body.username;
const password = req.body.password;
if (!username) {
return res.status(400).send("username require");
}
if (!password) {
return res.status(400).send("password require");
}
if (username != "admin" && password != "password") {
return res.status(401).send("invaild password");
}
const authToken = jwt.sign({username: username}, "secret");
res.status(200).json({token: authToken});
});
app.post("/user", function(req, res) {
const username = req.body.username;
const password = req.body.password;
const country = req.body.country;
const age = req.body.age;
if (!username) {
return res.status(400).send("username require");
}
if (!password) {
return res.status(400).send("password require");
}
if (!country) {
return res.status(400).send("countryrequire");
}
if (!age) {
return res.status(400).send("age require");
}
res.status(200).json({
id: shortid.generate(),
username: username,
country: country,
age: age
})
})
app.listen(3000);
express-jwt
作为 express 的一个中间件,需要设置 secret
作为秘钥,unless 可以排除某个接口。
默认的情况下,解析 JWT 失败会抛出异常,可以通过以下设置来处理该异常。
1
2
3
4
5
6
app.use(expressJwt({secret: "secret"}).unless({path: ["/login"]}));
app.use(function (err, req, res, next) {
if (err.name === "UnauthorizedError") {
res.status(401).send("invalid token");
}
});
/login
忽略的 JWT 认证,通过这个接口获取某个用户的 JWT
1
2
3
4
5
6
7
8
9
10
11
12
% http POST http://localhost:3000/login username="admin" password="password" country="CN" age=22
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 143
Content-Type: application/json; charset=utf-8
Date: Sun, 21 Aug 2016 06:57:42 GMT
ETag: W/"8f-iMzAS1K5StDQgtNnVSvqtQ"
X-Powered-By: Express
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M"
}
不使用 JWT 的时候
1
2
3
4
5
6
7
8
9
10
% http POST http://localhost:3000/user username="hexiangyu" password="password" ~
HTTP/1.1 401 Unauthorized
Connection: keep-alive
Content-Length: 13
Content-Type: text/html; charset=utf-8
Date: Sun, 21 Aug 2016 07:00:02 GMT
ETag: W/"d-j0viHsPPu6FaNJ6cXoiFeQ"
X-Powered-By: Express
invalid token
使用 JWT 就可以成功调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
% http POST http://localhost:3000/user Authorization:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNDcxNzYyNjYyfQ.o5RFJB4GiR28HzXbSptU6MsPwW1tSXSDIjlzn7erG0M" username="hexiangyu" password="password" country="CN" age=22
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 66
Content-Type: application/json; charset=utf-8
Date: Sun, 21 Aug 2016 07:04:34 GMT
ETag: W/"42-YnGYuyDLxpVUexEGEcQj1g"
X-Powered-By: Express
{
"age": "22",
"country": "CN",
"id": "r1sFMCUc",
"username": "hexiangyu"
}
JSON Web Token Introduction
IANA JSON Web Token
Flask-JWT
express-jwt
本文转自链接:http://blog.zhengxiaowai.cc/post/safe-jwt-restful-api.html
]]>在如今 RESTful 化的 API 接口下,cookie-session 已经不能很好发挥其余热保护好你的 API 。
更多的形]]>
vue-cli 3.0
版本的新增的一些功能对开发独立组件/库带来的便利。
如下面代码,提供一个id作为挂载点,vue将组件直接渲染到 app 对应的div上,下面我们将介绍下具体是如何操作的。
1
2
3
4
5
6
7
8
<div id="app"></div>
<script src="https://cdn.bootcss.com/vue/2.5.15/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="./commentSystem.umd.min.js"></script>
<script>
new Vue({render: h => h(window["commentSystem"])}).$mount('#app');
</script>
vue-cli 3.0 提供了构建目标的选项,可以将一个组件打包成一个UMD格式的库对外暴漏。
运行:1
vue-cli-service build --target lib --name myLib [entry]
结果:1
2
3
4
5
6File Size Gzipped
dist/myLib.umd.min.js 13.28 kb 8.42 kb
dist/myLib.umd.js 20.95 kb 10.22 kb
dist/myLib.common.js 20.57 kb 10.09 kb
dist/myLib.css 0.33 kb 0.23 kb
也可以使用 vue ui 提供的图形界面完成打包:
vue-cli 3.0 提供的构建目标的功能非常实用,在 2.0 版本时我们如果想发布一个组件需要自己手动修改 webpack 配置。对这部分有兴趣的可以移步如何在 npm 上发布你的 vue 组件
在评论模块中,组件需要和其他组件的交互状态,通过 props
和 emit
传递状态过于繁琐,引入状态管理必不可少。
为了便于打包库组件状态管理相对于官方示例来说会有一些小的调整。
1
2
3
4
5
6
7
8
9
// CommentSystem.vue 文件
import Vue from 'vue';
import Vuex, { mapState } from 'vuex';
Vue.use(Vuex);
export default {
name: 'CommentSystemComponent',
store: store
}
官方示例把 store
引入在 app.vue
中,因为我们打包的根节点在 CommentSystem.vue
组件上,所以 store
在 CommentSystem.vue
中引入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
// http://www.autohome.com.cn/comment/Articlecomment.aspx?articleid=899378&replyid=61166916
window.ahCSConfig = {
_appid: 'cms', // 区分来源,例如 pc/m/app
appid: 1, // 业务线 ID
contentid: 595293, // 内容 ID
reply: {
tips: '汽车之家温馨提示您:留言中请不要恶意攻击国家、其他用户及工作人员,不要发布任何广告性质的留言,我们会在第一时间永久封杀违反以上规定的ID。流畅沟通、观点鲜明、善意提醒是我们推崇的良性留言氛围。',
},
comment: {
tips: '留言中所有与交易和团购相关的信息均为虚假信息,与汽车之家无关,请勿相信。'
}
}
</script>
<script src="./dist/comment-system.umd.min.js"></script>
通过全局变量传入配置参数,注意脚本顺序。
1
2
3
4
5
6
7
8
9
10
var rootComponent = new Vue({
render: h => h(window["comment-system"])
}).$mount('#app');
ahCSConfig.$comment = rootComponent.$children[0].$children[0];
document.querySelector('#btn').addEventListener('click', function() {
ahCSConfig.$comment.$store.commit('SET_PICTURE_DIALOG_DATA', {
visible: true,
info: 'http://www3.autoimg.cn/newsdfs/g24/M02/26/26/80x0_0_autohomecar__ChcCL1p5uC-AFrOcABBbFVoFSGA574.jpg'
});
}, false);
页面向组件通信,通过获取组件对象并commit相关消息来实现。
1
2
3
4
// config 配置中配置项
event: ({type, payload}) => {
console.log('组件对外暴漏commit方法', type, payload);
}
1
2
3
4
5
6
7
8
9
10
11
12
// store.js 中对应触发逻辑
const ahCSConfig = window.ahCSConfig;
const eventInterceptors = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
ahCSConfig && ahCSConfig.event && ahCSConfig.event(mutation);
});
};
export default new Vuex.Store({
plugins: [eventInterceptors]
})
通过配置项中添加
event
方法,vue组件内每次 commit 都会调用 event 方法并传入type
和payload
。
在评论系统中会用大 Dialog
、Message
、Button
、Pagination
等组件。这些组件我们可以选择一个UI库引入,也可以自行开发。
element-ui
支持按需引入:
借助 babel-plugin-component
,我们可以只引入需要的组件,以达到减小项目体积的目的。
首先,安装 babel-plugin-component
:
1
npm install babel-plugin-component -D
然后,将 .babelrc
修改为:
1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js
中写入以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
* Vue.use(Button)
* Vue.use(Select)
*/
new Vue({
el: '#app',
render: h => h(App)
});
在评论系统中,借鉴了 element 的源码,自行实现了这几个组件。
axios 官方对跨域推荐的解决方式是设置CORS,在我们的业务中很多接口无法设置CORS,所以需要引入jsonp包解决这个问题,对此我们提供了一个接口的请求封装。
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
/**
* 这是一个请求方法
* @param {string} method 请求方式
* @param {string} url 请求连接
* @param {object} params 请求参数
* @return {Promise} 返回 Promise
*/
function reqMethod(method, url, obj, config = {}) {
// jsonp
if (method.toLocaleLowerCase() === 'jsonp') {
let q = Object.assign({}, obj, { '_': new Date().getTime() });
return new Promise((resolve, reject) => {
let path = url + '?' + qs.stringify(q);
jsonp(path, { timeout: axios.defaults.timeout }, function(err, data) {
if (err) {
MsgToast('err');
reject(err);
}
MsgToast({
data,
requestURL: path
});
if (data.returncode !== undefined && data.returncode !== 0) {
console.warn('JSONP 响应拦截器拦截 状态码异常:', data);
return Promise.reject(new Error('状态码异常'));
}
resolve(data);
});
});
}
let modeKey = ['post', 'put'].includes(method.toLowerCase()) ? 'data' : 'params';
return new Promise((resolve, reject) => {
axios({
method: method.toLowerCase(),
url: url,
[modeKey]: obj || {},
...config
}).then((response) => {
resolve(response.data, url);
}).catch((error) => {
reject(error);
});
});
}
在 vue-cli 3.0 的构建环境下很容将一个组件输出为一个UMD的库,通过不同的引入方式可以支持在传统业务和 vue项目中引入,这符合vue的一贯思路 The Progressive
JavaScript Framework
,行动起来吧,是时候选择用 vue 完成你的开发任务了。
vue-cli 3.0
版本的]]>
作者 Kid 蚂蚁金服·数据体验bv伟德登录入口团队
随着我们解决的场景越来越专业化和复杂化,大型SPA应用的流行,前端承担的职责越来越多。代码的质量和系统的完整性越来越难把握。很容易导致迭代着迭代着发现代码改不动了。最后只能新起炉灶,重新开发。归根到底在于复杂度的失控
,本文会尝试分析其中的问题以及从前端如何应用领域模型
开发的角度给出一些建议。
我们的系统架构精心设计过,按照标准的系统分层来管理复杂度。逻辑层
,展示层
,数据层
。每一层都精心设计。我们抽象出独立的类来放通用逻辑。对着代码不断地重构,将有复用能力的点进行抽象。为什么需求的变动还是能经常摧毁我们的设计呢。
原因在于:
软件本身是为了管理复杂度,我们现在面对的问题域错综复杂。为了创建真正好用的软件,开发者必须有一整套与之相关的知识体系。为此要知道的知识的广度让人生畏。一旦我们不能理解问题域,我们就没法做到控制问题域的复杂性。
当复杂性失去控制的时候,开发人员就无法理解软件。当领域的复杂度没有得到解决时,基础bv伟德登录入口再好的构思也无济于事。
上面我所描述的设计都是bv伟德登录入口层面
的设计。我们很容易抽象出一个独立的类来放通用逻辑,可是很难给它业务上的定义
!这个通用类只有bv伟德登录入口维度上的通用。
问题在于bv伟德登录入口维度上的通用很容易被业务摧毁
。需求上的变动或者膨胀,bv伟德登录入口维度的通用很容易被摧毁。举个例子,页面变化了,某个视图组件被复用了,他可能就要被提取到上层的common目录。也就是bv伟德登录入口模型立刻需要重新设计,然后就是重构,重构成工程师喜欢的简洁的样子。然后需求再变化,再重构….陷入了怪圈。
并且这个阶段我们很难保证重构的高效进行,有个理论叫破窗户
理论。一幢年老的大楼,一旦第一扇窗户破了,就会立刻给人一种年久失修,腐败的迹象。就像是一辆车,一旦第一个车窗坏了,里面很快就会遭到破坏。
里面的根本原因就是我们设计的bv伟德登录入口模型
与领域模型
不匹配。于是每次需求的改动,映射到bv伟德登录入口模型的改动可能就是极大的工作量。甚至根本改不动,在业务压力很大的时候,我们只能告诉产品经理,这个可以做,但是我们需要2个月。结局很可能就是需求方的妥协,牺牲用户的利益。导致产品越来越难用。
任何项目都会丢失知识,外包出去的系统可能只交回了代码,知识没有传递回来。离职了,转岗了,一旦出于某种原因人们没有口头传递知识,知识就丢失了。
丢失的知识也会导致系统越来越难维护,新同学不知道对于通用逻辑的改动会发生什么事情,代码最终变成了“石油坑”,越陷越深,最终无法自拔。
以上这三个问题归根到底,就是我们没有在前端代码里把我们业务描述清楚。我们很多情况下是视图驱动
,而不是业务驱动
。很多时候只关心页面长什么样子,发了什么请求拿了什么数据。于是在业务概念上每个人理解的深度都不同。解这个问题可能采用新的领域驱动设计的开发方式会比较合适。
领域模型是跨前端-后端-产品-设计的统一的语言。统一的语言既可以形成统一的理解,也可以促进领域模型的不断精化。也能迫使开发人员学习重要的业务原理,而不是机械的功能开发。产品经理也会不断提炼知识,升华自身理解。如果没有一个统一的,有共识的结构化的模型,一定会让项目很快的僵化,最后变成维护代价极高的遗留系统。
领域模型很多情况下都是由后端同学建立的,前端同学如何指导开发呢?我对于我们系统的演进过程进行了总结,希望能给大家一些灵感:
我们在进行前端设计之前要搞懂我们要开发的业务含义。除了自己理解建立模型之外我们可以寻求后端同学的帮助。拿到他们的领域模型
,弄清他们的模块划分。他们其实是业务逻辑的最终实现方,我们可以直接借鉴他们的模型,这样也可以保证前后端对于业务模型的理解一致。
我们要绘制出前端的领域模型图,这个图与后端的领域模型图一致程度很高,但绝不是一样
的。通常比后端模型简单。比如页面需要进行一项任务的配置,这个配置在后端模型里可能会被解释的相当复杂(会被拿去做一些同环比之类的复杂操作),但是在我们前端模型里,他的业务功能就是简单的任务配置而已~
如图,这一点是必须落到代码上的核心
!!一定要根据对应的前端领域模型在代码中分离出单独的领域层。模型必须与实现紧密结合,一定要指导设计,并落到代码上成为最终产品的一部分
。
还需要强调的是领域层的建设一定不是两个页面同时发了个请求,于是把这个请求抽出来,给与一个领域的名字。他一定被提前建立好
的。在开始进行前端设计之前就被设计出来的一层。
我们要将所有页面组件与模块内的业务行为都抽离出来,放在合适的领域模块中。只要是业务行为,一定有一个领域模块可以落
。如果不行就是领域模型设计的不合理。
要明白,驱动领域层分离的目的并不是页面被复用,这一点在思想上
一定要转化过来。领域层并不是因为被多个地方复用而被抽离。它被抽离的原因是:
稳定
的(页面以及与页面绑定的模块都是不稳定的)解耦
的(页面是会耦合的,页面的数据会来自多个接口,多个领域)极高复杂度
,值得单独管理(view层处理页面渲染以及页面逻辑控制,复杂度已经够高,领域层解耦可以轻view层。view层尽可能轻量
是我们架构师cnfi主推的思路)以层为单位
是可以被复用
的(你的代码可能会抛弃某个bv伟德登录入口体系,从vue转成react,或者可能会推出一个移动版,在这些情况下,领域层这一层都是可以直接复用)衍进
(模型存在的目的是让人们聚焦,聚焦的好处是加强了前端团队对于业务的理解,思考业务的过程才能让业务前进)这里想引用下我们leader导演的话说,我们的竞争力绝不仅仅只是前端,我们的竞争力在于我们是数据部门的前端,在于我们对于数据业务的理解。只有对于业务有深层次的理解,才能将系统带到正确的轨道上来。
接口约定尽量由前端主导
,毕竟接口是给前端使用,前端来设计接口比较合理。而且在约定的过程中,前端同学又多了一次熟悉后端是如何分模块的机会。必须通过看后端同学的数据库和整体的设计文档来约定接口路径和变量名称,也能够让前后端同学对于系统的各部分的命名一致。
我们在类,方法,模块命名时要直指业务核心
,保持与领域模型的一致。比如一条员工数据记录可能会被翻译成 inputRec或者employeeData, inputRec其实就是一个计算机思维的术语,而employeeData才是直指问题领域。这个错误其实很容易犯,我们开发的程序员思维根深蒂固
。
确保团队内部所有同学都要熟悉系统的模型。尤其是对于要熟悉并修改代码的新同学,先向他们分享我们系统的领域模型之后再介绍bv伟德登录入口架构。工作开展的重点的不同会导致编程世界观的不同。这样子会让新同学养成习惯,在进行bv伟德登录入口决断之前先判断是否符合现有的模型。不断的思考模型,才能够帮助我们业务成长。
领域驱动设计对于降低项目的复杂度上是明显效果的,而且将前端的代码业务逻辑和视图逻辑解耦。可以做到业务逻辑层的复用。加深了前端同学对于业务的理解和思考,可以促进业务发展。这种分层思想并不局限在某个框架下,建议大家尝试下~
对我们团队感兴趣的可以关注专栏,关注github或者发送简历至’tao.qit####alibaba-inc.com‘.replace(‘####’, ‘@’),欢迎有志之士加入~
原文地址:https://github.com/ProtoTeam/blog/blob/master/201806/2.md
]]>作者 Kid 蚂蚁金服·数据体验bv伟德登录入口团队
随着我们解决的场景越来越专业化和复杂化,大型SPA应用的流行,前端承担的职责越来越多。代码的质量和系统的完整性越来越难把握。很容易导致迭代着迭代着发现代码改不动了。]]>
Modern mode
等等。下面我们重点关注下 Modern mode
是什么,如何实现的。
使用 Babel
我们能够利用 ES2015 中最新的语言特性,但这也意味着我们必须通过转换和 添加 polyfille
来支持旧浏览器。这些转换后的代码通常比原生 ES2015+ 代码更冗长,并且解析和运行较慢。鉴于当今大多数现代浏览器对原生 ES2015+ 有着不错的支持,而我们不得不将数据量更大和效率底下的代码发送给浏览器,因为我们必须支持那些旧的浏览器。
Vue CLI 提供了一个 Modern mode
来帮助您解决这个问题。用以下命令进行生产时:
1
vue-cli-service build --modern
Vue CLI 构建两个版本的 js 包:一个面向支持现代浏览器的原生 ES2015+ 包,以及一个针对其他旧浏览器的包。
但最酷的部分是没有特殊的部署要求。生成的HTML文件中自动适配。 这个方式采用了Phillip Walton 伟德伟诺官网中讨论的bv伟德登录入口方案:
在支持原生 ES2015+ 的浏览器中,js会通过 <script type="module">
加载,并且可以使用 <link rel="modulepreload">
预加载。
在不支持的浏览器中使用 <script nomodule>
来加载编译版本,并且这会被支持ES模块的浏览器所忽略。
Safari 10 中有一个小问题这里已经解决,可以自动加载。
对比 Hello World 应用(vue 初始化的 Demo)使用这种模式打包出来的文件,通过现代模式输出的包(以后简称现代包)已经小了16%。在生产中,现代包通常会显著的提升 parse 速度和加载性能。
在浏览器环境语法特性检测还没有一个特别好的解决方案,随着一些新的 JavaScript 语法的出现,单凭特性检测来检查新语法的支持程度很是棘手。尽管如此对于 ES2015+ 的基本语法特性检测我们还是有办法的。解决之道便是 <script type="module">
。
大部分开发者认为 <script type="module">
是用来加载 ES 模块的,但是这里使用是 <script type="module">
的特性——加载浏览器可以处理的、使用 ES2015+ 语法的 JavaScript 文件。
换句话说,每个支持 <script type="module">
的浏览器都支持你所熟知的大部分 ES2015+ 语法,例如:
<script type="module">
的浏览器也支持 async 和 await 函数。<script type="module">
的浏览器也支持 Class 类。<script type="module">
的浏览器也支持 arrow functions。<script type="module">
的浏览器也支持 fetch 、Promises、Map、Set 等更多 ES2015+ 语法。因此,唯一需要做的就是为不支持 <script type="module">
的浏览器提供一个降级方案。对于支持 <script type="module">
的浏览器会忽略 <script nomodule></script>
方式引入的脚本,如下代码:
1
2
3
4
// 支持的浏览器 会加载 app.js, 不支持的浏览器因为 type 值不是 text/javascript 所以脚本并不会被加载。
<script type="module" src="app.js"></script>
// 支持的浏览器 会忽略配置 `nomodule` 属性的脚本加载,不支持的浏览器会正常加载。
<script src="app-legacy.js" nomodule></script>
下面看一下vue打包出来的代码:
1
2
3
4
5
6
7
8
9
10
11
12
// ...
<link as="style" href=""/css/app.6166f93b.css" rel="preload">
<link as="script" href="/js/app.4e3e948a.js" rel="modulepreload">
<link as="script" href="/js/chunk-vendors.fcf87964.js" rel="modulepreload">
<link href="/css/app.6166f93b.css" rel="stylesheet">
// ...
<script type="module" src="/js/chunk-vendors.fcf87964.js"></script>
<script type="module" src="/js/app.4e3e948a.js"></script>
<script>!function () { var e = document, t = e.createElement("script"); if (!("noModule" in t) && "onbeforeload" in t) { var n = !1; e.addEventListener("beforeload", function (e) { if (e.target === t) n = !0; else if (!e.target.hasAttribute("nomodule") || !n) return; e.preventDefault() }, !0), t.type = "module", t.src = ".", e.head.appendChild(t), t.remove() } }();</script>
<script src="/js/chunk-vendors-legacy.ea74b83d.js" nomodule></script>
<script src="/js/app-legacy.854b5bc1.js" nomodule></script>
之前说到现代浏览器中都可以通过 <script type="module">
来实现 ES2015+ 的特性检测针对性的加载脚本,但是 Safari 10 除外,这里的一段脚本是修复 safari 10 上 nomdoule
的表现不同的:
1
<script>!function () { var e = document, t = e.createElement("script"); if (!("noModule" in t) && "onbeforeload" in t) { var n = !1; e.addEventListener("beforeload", function (e) { if (e.target === t) n = !0; else if (!e.target.hasAttribute("nomodule") || !n) return; e.preventDefault() }, !0), t.type = "module", t.src = ".", e.head.appendChild(t), t.remove() } }();</script>
如果你对 vue-cli 3 是如何实现这块的感兴趣可以查看源码。
为了生成不同环境的js文件,你需要2个 babel-loader targets
配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// resolve targets
let targets
if (process.env.VUE_CLI_BABEL_TARGET_NODE) {
// running tests in Node.js
targets = { node: 'current' }
} else if (process.env.VUE_CLI_BUILD_TARGET === 'wc' || process.env.VUE_CLI_BUILD_TARGET === 'wc-async') {
// targeting browsers that at least support ES2015 classes
// https://github.com/babel/babel/blob/master/packages/babel-preset-env/data/plugins.json#L52-L61
targets = {
browsers: [
'Chrome >= 49',
'Firefox >= 45',
'Safari >= 10',
'Edge >= 13',
'iOS >= 10',
'Electron >= 0.36'
]
}
} else if (process.env.VUE_CLI_MODERN_BUILD) {
// targeting browsers that support <script type="module">
targets = { esmodules: true }
} else {
targets = rawTargets
}
ES2015+ 浏览器支持目标只需配置
babel-loader
参数targets = { esmodules: true }
即可。
preload 作为一个新的web标准,旨在提高性能,为web开发人员提供更细粒度的加载控制。preload 使开发者能够自定义资源的加载逻辑,且无需忍受基于脚本的资源加载器带来的性能损失。
preload 还有许多其他好处。使用 as 来指定将要预加载的内容的类型,将使得浏览器能够:
在 modulepreload
诞生前,还没有一种很好的声明式预加载模块的方法。Chrome 从 64 版本后 开始 “实验性的支持这个特征”。<link rel="modulepreload">
是 <link rel="preload">
的特定模块版本,解决了后者的一些问题。
总结:
启用该模式会自动构建两个版本的 js 包,针对支持现代浏览器的原生 ES2015+ 包,和针对其他旧浏览器的包,生成的 HTML 会通过 <script type="module">
和 <script nomodule>
进行自动降级,不需要任何特殊部署配置。原生 ES2015 包几乎不需要任何 polyfill 和编译,代码尺寸更小,现代浏览器 parse 和运行也更快。
拓展阅读:
ES6 Modules in Chrome M61+
ECMAScript modules in browsers
ES6 Modules in Depth
Deploying ES2015+ Code in Production Today
Preloading modules
Modern mode
等等。下]]>
vue-cli
是 vue 官方团队推出的一款快速开发 vue
项目的构建工具,具有开箱即用并且提供简洁的自定义配置等功能。 vue-cli
从 2.0 到 3.0 的升级有太多的新东西可以说了,但是不可能在本文中列举所有的内容,这篇伟德伟诺官网作为一个对比 2.0 升级功能的导读,让你快速了解 3.0 更新的内容。
创建项目命令的变化。
1
vue create my-project
自定义功能配置包括以下功能:
可以注意到 3.0 版本直接新加入了 TypeScript 以及 PWA 的支持。
选择好后,可以把以上配置存储为预设值,以后通过 vue-cli 创建的其他项目将都采用刚才的配置。
我们对比发现 vue-cli 3.0 默认项目目录相比 2.0 来说精简了很多。
config
和 build
文件夹。static
文件夹,新增 public
文件夹,并且 index.html
移动到 public
中。src
文件夹中新增了 views
文件夹,用于分类 视图组件 和 公共组件。从 3.0 版本开始,在项目的根目录放置一个 vue.config.js
文件, 可以配置该项目的很多方面。
vue.config.js 应该导出一个对象,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module.exports = {
baseUrl: '/',
outputDir: 'dist',
lintOnSave: true,
compiler: false,
// 调整内部的 webpack 配置。
// 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/webpack.md
chainWebpack: () => {},
configureWebpack: () => {},
// 配置 webpack-dev-server 行为。
devServer: {
open: process.platform === 'darwin',
host: '0.0.0.0',
port: 8080,
https: false,
hotOnly: false,
// 查阅 https://github.com/vuejs/vue-doc-zh-cn/vue-cli/cli-service.md#配置代理
proxy: null, // string | Object
before: app => {}
}
....
}
调整 webpack 配置最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象,该对象将会被 webpack-merge 合并入最终的 webpack 配置。
示例代码:配置 webpack 新增一个插件。
1
2
3
4
5
6
7
8
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}
修改插件选项的参数你需要熟悉 webpack-chain
的 API 并阅读一些源码以便了解如何权衡这个选项的全部配置项,但是它给了你比直接修改 webpack 配置中的值更灵活且安全的方式。
1
2
3
4
5
6
7
8
9
10
// vue.config.js
module.exports = {
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
return [/* new args to pass to html-webpack-plugin's constructor */]
})
}
}
注意:当我们更改一个webpack配置时候,可以通过
vue inspect > output.js
输出完整的配置清单,注意它输出的并不是一个有效的 webpack 配置文件,而是一个用于审查的被序列化的格式。
Babel 可以通过 .babelrc
或 package.json
中的 babel 字段进行配置。
ESLint 可以通过 .eslintrc
或 package.json
文件中的 eslintConfig
字段进行配置。
你可能注意到了 package.json
中的 browserslist
字段指定了该项目的目标浏览器支持范围。
public
目录的调整。vue 约定 public/index.html
作为入口模板会通过 html-webpack-plugin
插件处理。在构建过程中,资源链接将会自动注入其中。除此之外,vue-cli 也自动注入资源提示(preload/prefetch
), 在启用 PWA
插件时注入 manifest/icon
链接, 并且引入(inlines) webpack runtime / chunk manifest 清单已获得最佳性能。
在 JavaScript 或者 SCSS 中通过相对路径引用的资源会经过 webpack
处理。放置在 public
文件的资源可以通过绝对路径引用,这些资源将会被复制,而不经过 webpack
处理。
小提示:图片最好使用相对路径经过 webpack 处理,这样可以避免很多因为修改网站根目录导致的图片404问题。
在 3.0 版本中,选择启用 TypeScript 语法后,vue 组件的书写格式有特定的规范。
示例代码:
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
import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from 'vue-property-decorator'
const s = Symbol('baz')
@Component
export class MyComponent extends Vue {
@Emit()
addToCount(n: number){ this.count += n }
@Emit('reset')
resetCount(){ this.count = 0 }
@Inject() foo: string
@Inject('bar') bar: string
@Inject(s) baz: string
@Model('change') checked: boolean
@Prop()
propA: number
@Prop({ default: 'default value' })
propB: string
@Prop([String, Boolean])
propC: string | boolean
@Provide() foo = 'foo'
@Provide('bar') baz = 'bar'
@Watch('child')
onChildChanged(val: string, oldVal: string) { }
@Watch('person', { immediate: true, deep: true })
onPersonChanged(val: Person, oldVal: Person) { }
}
以上代码相当于
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
const s = Symbol('baz')
export const MyComponent = Vue.extend({
name: 'MyComponent',
inject: {
foo: 'foo',
bar: 'bar',
[s]: s
},
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
propA: Number,
propB: {
type: String,
default: 'default value'
},
propC: [String, Boolean],
},
data () {
return {
foo: 'foo',
baz: 'bar'
}
},
provide () {
return {
foo: this.foo,
bar: this.baz
}
},
methods: {
addToCount(n){
this.count += n
this.$emit("add-to-count", n)
},
resetCount(){
this.count = 0
this.$emit("reset")
},
onChildChanged(val, oldVal) { },
onPersonChanged(val, oldVal) { }
},
watch: {
'child': {
handler: 'onChildChanged',
immediate: false,
deep: false
},
'person': {
handler: 'onPersonChanged',
immediate: true,
deep: true
}
}
})
更多详细内容请关注这里;
当我们选择启用 PWA 功能时,在打包生成的代码时会默认生成 service-worker.js
和 manifest.json
相关文件。如果你不了解 PWA,点击这里查看;
需要注意的是 在
manifest.json
生成的图标信息,可以在public/img
目录下替换。
默认情况 service-worker
采用的是 precache
,可以通过配置 pwa.workboxPluginMode
自定义缓存策略。详情
配置示例
1
2
3
4
5
6
7
8
9
10
11
12
// Inside vue.config.js
module.exports = {
// ...其它 vue-cli 插件选项...
pwa: {
workboxPluginMode: 'InjectManifest',
workboxOptions: {
// swSrc 中 InjectManifest 模式下是必填的。
swSrc: 'dev/sw.js',
// ...其它 Workbox 选项...
},
},
};
vue-cli 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在编写你的应用上,而不必花好几天去纠结配置的问题。与此同时,它也为每个工具提供了调整配置的灵活性。
https://github.com/vuejs/vue-cli/
https://github.com/vuejs/vue-cli/blob/dev/docs/README.md
vue-cli
是 vue 官方团队推出的一款快速开发 vue
项目的构建工具,具有开箱即用并且提供简洁的自定义配置等功能。 vue-cli
从 2.0 到 3.0 的升级有太多的新东西可以说了,]]>
当你开发出一款NB累累的 Vue 组件,并希望其他开发者在的项目中使用它。你将怎么分享组件给他们使用呢?
在这篇伟德伟诺官网中我将告诉你如何准备你的组件,打包并发布到 NPM。我将使用一个示例项目演示以下内容:
我创建了这个简单的时钟组件,我会把它发布到 NPM 上。也许它不是你见过的最酷的组件,但它足以演示。
这是组件文件。这里没有什么特别的东西,但是请注意,我正在引入 moment
库用于格式化时间。
从你的包中排除依赖关系很重要,稍后我们会看到。
Clock.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div></div>
</template>
<script>
import moment from 'moment';
export default {
data() {
return {
time: Date.now()
}
},
computed: {
display() {
return moment(this.time).format("HH:mm:ss");
}
},
created() {
setInterval(() => {
this.time = Date.now();
}, 1000);
}
}
</script>
我为将组件发布到 NPM 上所做的大部分准备工作都是由 Webpack 完成的。 下面示例代码是本文中将要添加的基本 Webpack 配置。 如果您以前使用过Vue和Webpack,可以忽略没什么特别的地方:
webpack.config.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
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: path.resolve(__dirname + '/src/Clock.vue'),
output: {
path: path.resolve(__dirname + '/dist/'),
filename: 'vue-clock.js'
},
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
include: __dirname,
exclude: /node_modules/
},
{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.css$/,
loader: 'style!less!css'
}
]
},
plugins: [
new webpack.optimize.UglifyJsPlugin( {
minimize : true,
sourceMap : false,
mangle: true,
compress: {
warnings: false
}
})
]
};
externals 配置选项可以配置,从 Webpack 打包的代码中排除某个特定的依赖。 我不希望我的组件中包含依赖关系,因为那会使打包出来的代码变得臃肿,并可能导致用户环境中的版本冲突。 用户必须自己安装依赖项。
在这个例子中,我的包依赖于 moment
库。 为了确保它不会被打包,在 WebPACK 配置中我将指定 moment 为一个 external
(外部引用):
webpack.config.js
1
2
3
4
5
6
7
module.exports = {
...
externals: {
moment: 'moment'
},
...
}
在Vue.js中,用户可能通过两种途经使用组件。 首先是,浏览器引入:
1
<script type="text/javascript" src="vue-clock.js"></script>
其次,基于Node.js的开发环境,例如:
1
import VueClock from 'vue-clock';
理想情况下,我希望用户能够在任意一个环境中使用 Vue Clock。 不幸的是,这些环境需要将代码以不同方式打包,这意味着我必须设置两个不同的构建配置。
为此,我将创建两个单独的Webpack配置。 这比听起来更容易,因为配置几乎相同。 首先,我将创建一个常见的配置对象,然后使用 webpack-merge
将其包含在两个环境配置中:
webpack.config.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
const webpack = require('webpack');
const merge = require('webpack-merge');
const path = require('path');
var commonConfig = {
output: {
path: path.resolve(__dirname + '/dist/'),
},
module: {
loaders: [ ... ]
},
externals: { ... },
plugins: [ ... ]
};
module.exports = [
// Config 1: For browser environment
merge(commonConfig, {
}),
// Config 2: For Node-based development environments
merge(commonConfig, {
})
];
常见的配置与之前完全一样(我简写了大部分内容以节省空间),除了我已经删除的 entry
和 output.filename
选项。 我将在单独的构建配置中单独配置它们。
浏览器不能像 Node 那样,可以从另一个 JavaScript 模块文件引入。但是它们可以使用像AMD这样的脚本加载器,为了最大限度地简化,我让组件脚本作为一个全局变量,这样可以更简单地添加。
另外,我不希望用户必须想很多才能弄清楚如何使用组件。我会这样做的,当用户引入脚本时,组件可以很容易地注册为全局组件。Vue的插件系统将有助于实现这个功能。
我的目标是这个简单的设置:
index.html
1
2
3
4
5
6
7
8
9
<body>
<div id="app">
<vue-clock></vue-clock>
</div>
<script type="text/javascript" src="vue-clock.js"></script>
<script type="text/javascript">
Vue.use(VueClock);
</script>
</body>
首先,我将创建一个插件包装器,以方便安装组件:
plugin.js
1
2
3
4
5
6
7
import Clock from './Clock.vue';
module.exports = {
install: function (Vue, options) {
Vue.component('vue-clock', Clock);
}
};
该插件全局注册组件,因此用户可以在其应用程序的任何位置调用 clock 组件。
现在,我使用plugin.js
路径作为浏览器构建的入口点。我会输出一个名为 vue-clock.min.js
的文件,这对用户来说是最明显的。
1
2
3
4
5
6
7
8
9
module.exports = [
merge(config, {
entry: path.resolve(__dirname + '/src/plugin.js'),
output: {
filename: 'vue-clock.min.js',
}
}),
...
];
Webpack 可以以各种不同的方式导出你的脚本,例如:作为一个对象,作为一个全局变量,遵循AMD或CommonJS规范的模块。您可以使用 libraryTarget
选项指定。
对于浏览器打包方式,我将使用 window
导出方式。 我也可以使用 UMD
来获得更多的灵活性,但是由于我已经创建了两个打包方式,所以我只是将这个打包方式限制在浏览器中。
我还将库名称指定为 VueClock
。 这意味着当浏览器引入包时,它将挂载在全局上 window.VueClock
。
1
2
3
4
5
output: {
filename: 'vue-clock.min.js',
libraryTarget: 'window',
library: 'VueClock'
}
为了允许用户在 Node 开发环境中使用组件,我将使用 UMD 方式打包输出提供给 Node 环境使用。 UMD是一种灵活的模块类型,可以在各种不同的脚本加载器和环境中使用代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = [
...
merge(config, {
entry: path.resolve(__dirname + '/src/Clock.vue'),
output: {
filename: 'vue-clock.js',
libraryTarget: 'umd',
// These options are useful if the user wants to load the module with AMD
library: 'vue-clock',
umdNamedDefine: true
}
})
];
注意,在Node 环境中使用单个文件组件作为入口,不需要使用插件包装器。这样可以更灵活的引入:
1
2
3
4
5
6
7
import VueClock from 'vue-clock';
new Vue({
components: {
VueClock
}
});
在发布到NPM之前,我将设置我的 package.json 文件。 有关每个选项的详细说明,请访问 npmjs.com。
package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"name": "vue-clock-simple",
"version": "1.0.0",
"description": "A Vue.js component that displays a clock.",
"main": "dist/vue-clock.js",
"scripts": {
"build": "rimraf ./dist && webpack --config ./webpack.config.js"
},
"author": "Anthony Gore",
"license": "MIT",
"dependencies": {
"moment": "^2.18.1"
},
"repository": { ... },
"devDependencies": { ... }
}
我省略了这个文件的大部分,但重点要注意的是:
"main": "dist/vue-clock.js"
。 这指向 Node bundle 文件,确保模块加载器知道文件的准确路径,即1
import VueClock from 'vue-clock' // this resolves to dist/vue-clock.js
现在我的组件设置正确,可以在NPM上发布。 我不会重复的在这里说明,因为它们在npmjs.com上很好地介绍。
结果如下:
https://github.com/anthonygore/vue-clock-simple/blob/master/webpack.config.js
https://vuejsdevelopers.com/2017/07/31/vue-component-publish-npm/
当你开发出一款NB累累的 Vue 组件,并希望其他开发者在的项目中使用它。你将怎么分享组件给他们使用呢?
在这篇伟德伟诺官网中我将告诉你如何准备你的组件,打包并发布到 NPM。我将使用一个示例项目演示以下内容:
我创建了这个简单的时钟组件,我会把它发布到 NPM 上。也许它不是你见过的最酷的组件,但它足以演示。
这是组件文件。这里没有什么特别的东西,但是请注意,我正在引入 moment
库用于格式化时间。
从你的包中排除依赖关系很重要,稍后我们会看到。
vue-cli 作为 Vue 官方推荐的项目构建脚手架,它提供了开发过程中常用的,热重载,构建,调试,单元测试,代码检测等功能。我们本次的异步远端组件将基于 vue-cli 开发。
远端代码应该存储在一个可访问的 URL 上,这样我们通过 Axios 类似的 HTTP client 请求这个链接拿到源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Axios from 'axios';
export default {
name: 'SyncComponent',
props: {
// 父组件提供请求地址
url: {
type: String,
default: ''
}
},
data() {
return {
resData: ''
};
},
async mounted() {
if (!this.url) return;
const res = await Axios.get(this.url); // 我们在组件挂载完成时,请求远端代码并存储结果。
this.resData = res.data;
}
};
以上是基础代码 为了方便 一下例子中 我将省略重复的代码部分。
这部分有些繁琐,涉及到多个问题:
.vue
模板 或 ES.next
语法,模块需要编译后才可以使用。处理这部分比较简单,我们自己定义一个webpack配置文件来打包这些模板。
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
// 在 build 目录下新建 webpack.sync-components.prod.conf.js 文件
const webpack = require('webpack');
const path = require('path');
const utils = require('./utils');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
// 此处引入要打包的组件
entry: {
componentA: resolve('/src/views/component-a.vue')
},
// 输出到静态目录下
output: {
path: resolve('/static/'),
filename: '[name].js',
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: false, // ****** vue-loader v13 更新 默认值为 true v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
loaders: utils.cssLoaders({
sourceMap: true,
extract: false // css 不做提取
}),
transformToRequire: {
video: 'src',
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
// 压缩JS
new webpack.optimize.UglifyJsPlugin({
compress: false,
sourceMap: true
}),
// 压缩CSS 注意不做提取
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
})
]
};
至此我们的模块已经被编译成框架可以识别的文件。
new Function
。
1
2
3
4
5
6
7
async mounted() {
if (!this.url) return;
const res = await Axios.get(this.url);
let Fn = Function;
this.mode = new Fn(`return ${res.data}`)();
}
有两种可能会导致这个问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue-loader v13 esModule 更新 默认值为 true, v12及之前版本为 false, 此项配置影响 vue 自身异步组件写法以及 webpack 打包结果
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
esModule: false
... 以下省略千军万码
}
}
// UglifyJs 需要取消变量名替换配置,此配置并不会极大影响压缩率
new webpack.optimize.UglifyJsPlugin({
compress: false,
sourceMap: true
})
至此 远程组件就被引入到框架中了。
这里有一个问题,从 view组件
到 远程异步加载组件
再到 实际业务组件
通信一共三层,中间层 远程异步组件
作为公共组件不可被修改,需要 view组件
直接向 实际业务组件
通信。vuex 和 eventBus 方案都过于繁琐,这里我们采用 $attrs 和 $listeners(vue v2.4+), 来实现 “fallthrough”(vue组件跨层级通信)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 修改 sync-component.vue 组件
// 新增 v-bind="$attrs" v-on="$listeners"
<component
:is="mode"
v-bind="$attrs"
v-on="$listeners">
</component>
// inheritAttrs: true
export default {
name: 'SyncComponent',
props: {
// 父组件提供请求地址
url: {
type: String,
default: ''
}
},
inheritAttrs: true
... 以下省略千军万码
}
我们不希望看到远端组件和框架中存在较大库或插件的重复的引入,这部分内容尚处在实践阶段,主要思路是把公共库挂载到Vue原型链上实现组件公共复用 Vue.prototype.$xxx
。
1
2
3
4
// 全局添加 axios 对象
import axios from 'axios';
Vue.prototype.$http = axios;
引入的远程组件可以访问到框架中的公共包了,这时候还需要配置 webpack 使远程组件打包时不要包含公共包的代码。
1
2
3
4
5
6
// webpack.sync-components.prod.conf.js 添加
externals: {
vue: 'vue',
'element-ui': 'element-ui',
axios: 'axios'
}
这部分我们直接用一个全局变量做字典,存储 以 请求地址:数据
为子项的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async mounted() {
if (!this.url) return;
// Cache 缓存 根据 url 参数
if (!window.SyncComponentCache) {
window.SyncComponentCache = {};
}
let res;
if (!window.SyncComponentCache[this.url]) {
window.SyncComponentCache[this.url] = Axios.get(this.url);
res = await window.SyncComponentCache[this.url];
} else {
res = await window.SyncComponentCache[this.url];
}
let Fn = Function;
this.mode = new Fn(`return ${res.data}`)();
console.log(this.mode);
}
至此,异步远程组件就可以加载并和框架进行通信了。
本文中的源码请访问 github 获取,组件已经发布到 NPM 上,可以直接安装。
拓展阅读:
如何在 npm 上发布你的 vue 组件
作者简介 导演 蚂蚁金服数据前端
蚂蚁金服数据平台前端团队主要负责多个数据相关的PC Web单页面应用程序,业务复杂度类比Excel等桌面应用,业务前端代码量在几万行~几十万行,随着产品不断完善,破百万指日可待。管理好10万行级甚至百万行级代码的前端应用,是我们团队的核心挑战之一。
接下来的系列伟德伟诺官网,我会尝试从以下几个角度介绍我们团队应对挑战的方法:
团队的架构方案是多个产品经历一年的持续迭代,不断摸索出来的一套适合本团队数据产品业务场景的架构方案,架构方案中还存在尚未解决的痛点和有争议的部分需要持续优化,不保证这套架构适合您的产品。
先介绍下我们团队的产品特点:
架构的目的是管理复杂度,将复杂问题分而治之、有效管理,我们的具体方法如下:
这里的“页面级”粒度指一个路由映射的组件
划分原则:
继续细分粒度,然后将可复用模块或组件抽离到公共区域
数据模型根据职责分成两类:
领域模型是业务数据,往往要持久化到数据库或localStorage中,属于可跨模块复用的公共数据,如:
领域模型作为公共数据,建议统一存放在一个叫做Domain Model Layer的架构独立分层中(前端业界一般对这层的命名为ORM层)。
下沉到Domain Model Layer(领域模型层)有诸多利处:
应用状态模型是与视图相关的状态数据,如:
这些数据与具体的视图模块或业务功能强相关,建议存放在业务模块的Model中。
组件根据职责划分为两类:
容器型组件是与store直连的组件,为展示型组件或其它容器组件提供数据和行为,尽量避免在其中做一些界面渲染相关的事情。
展示型组件独立于应用的其它部分内容,不关心数据的加载和变更,保持职责单一,仅做视图呈现和最基本交互行为,通过props
接收数据和回调函数输出结果,保证接收的数据为组件数据依赖的最小集。
一个有成百上千展示型组件的复杂系统,如果展示型组件粒度切分能很好的遵循高内聚低耦合和职责单一原则的话,可以沉淀出很多可复用的通用业务组件。
按照上面三点合并同类项后,业务架构图变更为
模块粒度逐渐细化,会带来更多的跨模块通信诉求,为避免模块间相互耦合、确保架构长期干净可维护,我们规定:
我们建议将跨模块通信的逻辑代码放在父模块中,或者在一个叫做Mediator层中单独维护。
最终得到我们团队完整的业务逻辑架构图:
刚刚从空间维度讲了架构管理的方案,现在从时间维度说说应用的数据流转 — Redux单向数据流。
Redux架构的设计核心是单向数据流,应用中所有的数据都应该遵循相同的生命周期,确保应用状态的可预测性。
每个Action都会对应一个数据处理函数,即Reducer。特别强调,Reducer必须是纯函数(pure function),这个规定带来一个非常大的好处,数据处理层代码变的非常容易写单元测试。
纯函数的特征是入参相同的情况下,返回值恒等,举个栗子🌰:
纯函数:
1
2
3
function add(a, b) {
return a + b;
}
非纯函数:
1
2
3
4
function now() {
let now = new Date();
return now;
}
函数中如果包含 Math.random
,new Date()
, 异步请求等内容,且影响到最终结果的返回,即为非纯函数。
Store 数据存放的地方,store保存从进入页面开始所有Action操作生成的数据状态(state),每次Action引发的数据变更都必须生成一个新的state对象,且确保旧的state对象不被修改。这样做可以保证
应用的状态的可预测、可追溯,也方便设计Redo/Undo功能。
我们团队使用轻量级的immutable方案immutability-helper
,相比完全拷贝一份(deep clone)性能更优、存储空间利用率更高。
immutability-helper
的API不够友好,我们写了一个库immutability-helper-x增强它的易用性。
immutability-helper API风格:
1
2
3
4
5
6
7
8
9
import update from 'immutability-helper';
const newData = update(myData, {
x: {
y: {
z: { $set: 7 }
}
},
});
immutability-helper-x API风格:
1
2
3
import update from 'immutability-helper-x';
const newData = update.$set(myData, 'x.y.z', 7);
React/Redux是一种典型的数据驱动的开发框架(Data-Driven-Development),在开发中,我们可以将更多的精力集中在数据(领域模型+状态模型)的操作和流转上,再也不用被各种繁琐的DOM操作代码困扰,当Store变更时,React/Redux框架会帮助我们自动的统一渲染视图。
监听Store变更刷新视图的功能是由react-redux完成的:
context
属性向后代组件提供(provide)store对象;严格遵循架构规范和单向数据流规范,可以保证我们的前端应用在比较粗的粒度上的可维护性和扩展性,对于更细的粒度的代码,我们组织童鞋学习和分享《设计模式》 和 《重构 - 改善既有代码的设计》,持续打磨和优化自己的代码,未来团队会持续输出这方面的系列伟德伟诺官网。
本篇先聊前端通用架构,具体模块的业务架构、架构遵循的原则、团队架构组的架构评审流程等内容会在接下来的系列伟德伟诺官网中阐述。感兴趣的同学关注专栏或者发送简历至 tao.qit###alibaba-inc.com,欢迎有志之士加入~
伟德伟诺官网来源:如何管理好10万行代码的前端单页面应用
]]>作者简介 导演 蚂蚁金服数据前端
蚂蚁金服数据平台前端团队主要负责多个数据相关的PC Web单页面应用程序,业务复杂度类比Excel等桌面应用,业务前端代码量在几万行~几十万行,随着产品不断完善,破百万指日]]>
Git是一个很好的版本管理工具,不过相比于传统的版本管理工具,学习成本比较高。
实际开发中,如果团队成员比较多,开发迭代频繁,对Git的应用比较混乱,会产生很多不必要的冲突或者代码丢失等。
就像代码需要代码规范一样,使用Git进行代码管理同样需要一个清晰的流程和规范, Git Flow就是一个被广泛认可的Git使用最佳实践。
Git Flow是Vincent Driessen提出的一个分支管理的策略,http://nvie.com/posts/a-successful-git-branching-model/
应用这个规范可以使得版本库的演进保持简洁,主干清晰,各个分支有不同的职责,在很大程度上减少冲突的产生。
Git Flow通过对分支的管理,实现版本迭代的清晰。
这个流程图是应用Git Flow的标准流程,可以看到,不同的分支在产品研发和上线的不同阶段有不同的作用,扮演了不同的角色。
结合图片,简单介绍一下不同分支的职责。
这个分支是发布到生产环境的代码,这个分支只能从其他分支合并,不能在这个分支直接修改。
这个分支是主开发分支,包含所有要发布到下一个Release的代码,这个主要合并自其他分支,比如Feature分支。
Feature 分支主要用来开发一个新的功能,一旦开发完成,合并回Develop分支,并且进入下一个Release,Feature分支可以选择删除或者保留。
当需要发布一个新Release的时候,基于Develop分支创建一个Release分支,Release分支在测试过程中可能会修改,完成Release后,合并到Master和Develop分支。
当在Production发现新的Bug时候,需要创建一个Hotfix分支, 完成Hotfix后,合并回Master和Develop分支,所以Hotfix的改动会进入下一个Release。
我们在应用Git Flow的时候,也遇到了一些问题,比如开发结束后,在develop分支进行merge开发分支操作,出现冲突如果不能很好的解决,容易对develop分支的代码造成污染。
下面是实际开发中使用的流程,在Feature分支上合并develop代码,然后合并到develop分支上,流程更加清晰,冲突优先在开发分支解决。
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
//新建分支
git checkout develop
git pull origin develop
git checkout -b myfeature
//在分支上开发
git add ***
git commit -m "*****"
//在分支开发过程中合并develop分支到本分支(先把自己的工作commit到本地)
git checkout develop
git pull origin develop
git checkout myfeature
git merge develop
(如果没有冲突,就继续开发,如果有冲突,执行下面过程)
首先在本地解决冲突,再把冲突解决commit
git add ***
git commit -m "*****"
//在分支开发结束,需要将本分支合并到develop分支(先把自己的工作commit到本地)
git checkout develop
git pull origin develop
git merge myfeature
(如果没有冲突,就推送到远程)
git push origin develop
(如果有冲突,则解决冲突,再commit,并推送到远程:)
git add ***
git commit -m "*****"
git push origin develop
应用Git Flow的目的是更好的进行版本管理和持续集成,有些细节并不一定要遵循这个模型,可以根据团队规模进行简单的调整,适合的才是最好的。
]]>Git是一个很好的版本管理工具,不过相比于传统的版本管理工具,学习成本比较高。
实际开发中,如果团队成员比较多,开发迭代频繁,对Git的应用比较混乱,会产生很多不必要的冲突或者代码丢失等。
就]]>
本规范提供了一种统一的编码规范来编写 Vue.js 代码。这使得代码具有如下的特性:
本指南为 De Voorhoede 参考 RiotJS 编码规范 而写。
this
赋值给 component
变量this.$parent
this.$refs
始终基于模块的方式来构建你的 app,每一个子模块只做一件事情。
Vue.js 的设计初衷就是帮助开发者更好的开发界面模块。一个模块是应用程序中独立的一个部分。
每一个 Vue 组件(等同于模块)首先)必须专注于解决一个单一的问题,独立的、可复用的、微小的 和 可测试的。
如果你的组件做了太多的事或是变得臃肿,请将其拆分成更小的组件并保持单一的原则。一般来说,尽量保证每一个文件的代码行数不要超过 100 行。也请保证组件可独立的运行。比较好的做法是增加一个单独的 demo 示例。
组件的命名需遵从以下原则:
同时还需要注意:
app-
前缀作为命名空间: 如果非常通用的话可使用一个单词来命名,这样可以方便于其它项目里复用。1
2
3
4
5
6
7
8
9
<!-- 推荐 -->
<app-header></app-header>
<user-list></user-list>
<range-slider></range-slider>
<!-- 避免 -->
<btn-group></btn-group> <!-- 虽然简短但是可读性差. 使用 `button-group` 替代 -->
<ui-slider></ui-slider> <!-- ui 前缀太过于宽泛,在这里意义不明确 -->
<slider></slider> <!-- 与自定义元素规范不兼容 -->
Vue.js 的表达式是 100% 的 Javascript 表达式。这使得其功能性很强大,但也带来潜在的复杂性。因此,你应该尽量保持表达式的简单化。
如果你发现写了太多复杂并难以阅读的行内表达式,那么可以使用 method 或是 computed 属性来替代其功能。
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
<!-- 推荐 -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script type="text/javascript">
export default {
computed: {
month() {
return this.twoDigits((new Date()).getUTCMonth() + 1);
},
year() {
return (new Date()).getUTCFullYear();
}
},
methods: {
twoDigits(num) {
return ('0' + num).slice(-2);
}
},
};
</script>
<!-- 避免 -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>
虽然 Vue.js 支持传递复杂的 JavaScript 对象通过 props 属性,但是你应该尽可能的使用原始类型的数据。尽量只使用 JavaScript 原始类型(字符串、数字、布尔值)和函数。尽量避免复杂的对象。
组件的每一个属性单独使用一个 props,并且使用函数或是原始类型的值。
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 推荐 -->
<range-slider
:values="[10, 20]"
min="0"
max="100"
step="5"
:on-slide="updateInputs"
:on-end="updateResults">
</range-slider>
<!-- 避免 -->
<range-slider :config="complexConfigObject"></range-slider>
在 Vue.js 中,组件的 props 即 API,一个稳定并可预测的 API 会使得你的组件更容易被其他开发者使用。
组件 props 通过自定义标签的属性来传递。属性的值可以是 Vue.js 字符串(:attr="value"
或 v-bind:attr="value"
)或是不传。你需要保证组件的 props 能应对不同的情况。
验证组件 props 可以保证你的组件永远是可用的(防御性编程)。即使其他开发者并未按照你预想的方法使用时也不会出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<input type="range" v-model="value" :max="max" :min="min">
</template>
<script type="text/javascript">
export default {
props: {
max: {
type: Number, // 这里添加了数字类型的校验
default() { return 10; },
},
min: {
type: Number,
default() { return 0; },
},
value: {
type: Number,
default() { return 4; },
},
},
};
</script>
this
赋值给 component
变量在 Vue.js 组件上下文中,this
指向了组件实例。因此当你切换到了不同的上下文时,要确保 this
指向一个可用的 component
变量。
换句话说,不要在编写这样的代码 const self = this;
,而是应该直接使用变量 component
。
this
赋值给变量 component
可用让开发者清楚的知道任何一个被使用的地方,它代表的是组件实例。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
<script type="text/javascript">
export default {
methods: {
hello() {
return 'hello';
},
printHello() {
console.log(this.hello());
},
},
};
</script>
<!-- 避免 -->
<script type="text/javascript">
export default {
methods: {
hello() {
return 'hello';
},
printHello() {
const self = this; // 没有必要
console.log(self.hello());
},
},
};
</script>
按照一定的结构组织,使得组件便于理解。
name
属性。借助于 vue devtools 可以让你更方便的测试。组件结构化
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
<template lang="html">
<div class="Ranger__Wrapper">
<!-- ... -->
</div>
</template>
<script type="text/javascript">
export default {
// 不要忘记了 name 属性
name: 'RangeSlider',
// 组合其它组件
extends: {},
// 组件属性、变量
props: {
bar: {}, // 按字母顺序
foo: {},
fooBar: {},
},
// 变量
data() {},
computed: {},
// 使用其它组件
components: {},
// 方法
watch: {},
methods: {},
// 生命周期函数
beforeCreate() {},
mounted() {},
};
</script>
<style scoped>
.Ranger__Wrapper { /* ... */ }
</style>
Vue.js 提供的处理函数和表达式都是绑定在 ViewModel 上的,组件的每一个事件都应该按照一个好的命名规范来,这样可以避免不少的开发问题,具体可见如下 为什么。
Vue.js 支持组件嵌套,并且子组件可访问父组件的上下文。访问组件之外的上下文违反了基于模块开发的第一原则。因此你应该尽量避免使用 this.$parent
。
Vue.js 支持通过 ref
属性来访问其它组件和 HTML 元素。并通过 this.$refs
可以得到组件或 HTML 元素的上下文。在大多数情况下,通过 this.$refs
来访问其它组件的上下文是可以避免的。在使用的的时候你需要注意避免调用了不恰当的组件 API,所以应该尽量避免使用 this.$refs
。
this.$refs
来实现。this.$ref
而不是 JQuery
、document.getElement*
、document.queryElement
。1
2
3
4
5
<!-- 推荐,并未使用 this.$refs -->
<range :max="max"
:min="min"
@current-value="currentValue"
:step="1"></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
<!-- 使用 this.$refs 的适用情况-->
<modal ref="basicModal">
<h4>Basic Modal</h4>
<button class="primary" @click="$refs.basicModal.close()">Close</button>
</modal>
<button @click="$refs.basicModal.open()">Open modal</button>
<!-- Modal component -->
<template>
<div v-show="active">
<!-- ... -->
</div>
</template>
<script>
export default {
// ...
data() {
return {
active: false,
};
},
methods: {
open() {
this.active = true;
},
hide() {
this.active = false;
},
},
// ...
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 如果可通过 emited 来做则避免通过 this.$refs 直接访问 -->
<template>
<range :max="max"
:min="min"
ref="range"
:step="1"></range>
</template>
<script>
export default {
// ...
methods: {
getRangeCurrentValue() {
return this.$refs.range.currentValue;
},
},
// ...
};
</script>
Vue.js 的组件是自定义元素,这非常适合用来作为样式的根作用域空间。可以将组件名作为 CSS 类的命名空间。
使用组件名作为样式命名的前缀,可基于 BEM 或 OOCSS 范式。同时给 style 标签加上 scoped 属性。加上 scoped 属性编译后会给组件的 class 自动加上唯一的前缀从而避免样式的冲突。
1
2
3
4
5
6
7
8
9
<style scoped>
/* 推荐 */
.MyExample { }
.MyExample li { }
.MyExample__item { }
/* 避免 */
.My-Example { } /* not scoped to component or module name, not BEM compliant */
</style>
使用 Vue.js 组件的过程中会创建 Vue 组件实例,这个实例是通过自定义属性配置的。为了便于其他开发者使用该组件,对于这些自定义属性即组件API应该在 README.md
文件中进行说明。
README.md
是标准的我们应该首先阅读的文档文件。代码托管网站(GitHub、Bitbucket、Gitlab 等)会默认在仓库中展示该文件作为仓库的介绍。在模块目录中添加 README.md
文件:
1
2
3
4
range-slider/
├── range-slider.vue
├── range-slider.less
└── README.md
在 README 文件中说明模块的功能以及使用场景。对于 vue 组件来说,比较有用的描述是组件的自定义属性即 API 的描述介绍。
range slider 组件可通过拖动的方式来设置一个给定范围内的数值。
该模块使用 noUiSlider 来实现夸浏览器和 touch 功能的支持。
<range-slider>
支持如下的自定义属性:
min
max
values
values="[10, 20]"
. Defaults to [opts.min, opts.max]
.step
on-slide
(values, HANDLE)
格式的参数。 如: on-slide={ updateInputs }
, component.updateInputs = (values, HANDLE) => { const value = values[HANDLE]; }
.on-end
(values, HANDLE)
格式的参数。如需要自定义 slider 的样式可参考 noUiSlider 文档)
添加 index.html
文件作为组件的 demo 示例,并提供不同配置情况的效果,说明组件是如何使用的。
代码校验可以保持代码的统一性以及追踪语法错误。.vue 文件可以通过使用 eslint-plugin-html
插件来校验代码。你可以通过 vue-cli
来开始你的项目,vue-cli
默认会开启代码校验功能。
为了校验工具能够校验 *.vue
文件,你需要将代码编写在 <script>
标签中,并使组件表达式简单化,因为校验工具无法理解行内表达式,配置校验工具可以访问全局变量 vue
和组件的 props
。
ESLint 需要通过 ESLint HTML 插件来抽取组件中的代码。
通过 .eslintrc
文件来配置 ESlint,这样 IED 可以更好的理解校验配置项:
1
2
3
4
5
6
7
8
9
10
11
{
"extends": "eslint:recommended",
"plugins": ["html"],
"env": {
"browser": true
},
"globals": {
"opts": true,
"vue": true
}
}
运行 ESLint
1
eslint src/**/*.vue
JSHint 可以解析 HTML(使用 --extra-ext
命令参数)和抽取代码(使用 --extract=auto
命令参数)。
通过 .jshintrc
文件来配置 ESlint,这样 IED 可以更好的理解校验配置项。
1
2
3
4
{
"browser": true,
"predef": ["opts", "vue"]
}
运行 JSHint1
jshint --config modules/.jshintrc --extra-ext=html --extract=auto modules/
注:JSHint 不接受 vue
扩展名的文件,只支持 html
。
Fork 和提 PR 以帮助我们改进或者可以给我们提 Issue。
1)下载redis安装包
可去官网http://redis.io ,也可通过wget命令:
1
wget http://download.redis.io/redis-stable.tar.gz
2)解压
1
tar –zxvf redis-stable.tar.gz
3) 编译、安装
1
cd redis-stable
1
make
如果提示 gcc command
不识别,请自行安装 gcc
;
如果提示couldn’t execute tcl : no such file or dicrectory
,请自行安装tcl
;
如果提示
请执行 make distclean
,然后再 make
Make成功之后,会在src目录下多出一些文件,如下
可手动拷贝redis-server、redis-cli、redis-check-aof、redis-check-dump等至/usr/local/bin目录下,也可执行make install,此处执行make install
可查看,/usr/local/bin
下已有这些文件。
注意:若此时执行 redis-server –v
(查看版本命令),若提示 redis-server command not found
,则需要将 /usr/local/bin
目录加到环境变量,如何添加,此处不做详细介绍,可查看修改 /etc/profile
,(查看环境变量命令:echo $PATH
)
正常如下
至此,redis安装完成,接着配置。
1) 创建配置文件目录,dump file 目录,进程pid目录,log目录等
配置文件一般放在 /etc/
下,创建redis目录
1
2
3
cd /etc/
mkdir redis
ll
查看创建的redis目录
~
dump file、进程pid、log目录等,一般放在 /var/
目录下
,1
2
3
4
5
6
7cd /var/
mkdir redis
cd redis
mkdir data log run
至此,目录创建完毕
2) 修改配置文件,配置参数
首先拷贝解压包下的 redis.conf
文件至 /etc/redis
查看 /etc/redis/redis.conf
1
2
3
cd /etc/redis/
ll
打开redis.conf
文件
修改端口(默认6379)
修改pid目录为新建目录
修改dump目录为新建目录
修改log存储目录为新建目录
3) 持久化
默认rdb,可选择是否开启aof,若开启,修改配置文件 appendonly
4) 启动redis,查看各目录下文件
查看进程
redis已启动
查看dump, log, pid等
发现只有日志,没有dump和pid信息,是因为当前redis服务仍然是console模式运行的,且没有数据存储操作
停止redis服务,修改配置文件使得redis在 background
运行
改成yes
,保存,重启redis服务
1
2
redis-cli shutdown
redis-server /etc/redis/redis.conf
查看pid信息,如下
查看dump信息
若配置了aof持久化方式,data目录下还会有aof的相关文件
5) 客户端连接redis
默认端口6379
6) 至此,redis基础配置完毕,若有其他相关配置调整,可查找文档再修改
1) 创建redis启动脚本
拷贝解压包下 utils下redis
启动脚本至 /etc/init.d/
1
cp redis_init_script /etc/init.d/
修改脚本名称(也可不修改)为redis
查看 ll
修改脚本pid及conf路径为实际路径
生产环境下,配置时,配置文件、pid等最好加上端口标识,以便区分,如
保存
退出
至此,在 /etc/init.d/
目录下,已经可以通过 service redis start/stop
命令启动和关闭redis
若在其他目录下,不能够使用这2个命令,请继续配置2),添加权限
2) 给启动脚本添加权限
1
chmod +x /etc/init.d/redis
实际命令,根据目录的不同,会不一样
相应的删除权限是
1
chmod –x /etc/init.d/redis
如果需要在开机的时候,redis服务自动启动,可继续3)
3) 设置自启动
1
chkconfig redis on
如果运行报错,提示
是因为没有在启动脚本里加入redis启动优先级信息,可添加如下
再次执行chkconfig redis on
,成功
至此,自启动配置完毕
修改redis配置文件,redis.conf。
1.注释掉原来的持久化规则1
2
3#save 900 1
#save 300 10
#save 60 10000
2.设置为空1
save ""
然后重启redis服务即可。
redis.conf
中 maxmemory
选项,该选项是告诉redis当使用了多少物理内存后就开始拒绝后续的写入请求,该参数能很好的保护redis不会因为使用了过多的物理内存后而导致swap,最终严重影响性能甚至崩溃。
已本机为例,本机内存为16G,业务需求5G redisu, 设置如下:1
2
3maxmemory 5368709120
或者
maxmemory 5gb
maxmemory 参数为
<bytes>
, 计算公式 5G 1024 1024 * 1024 或者直接写 5gb
设置了maxmemory
的选项,redis内存使用达到上限。可以通过设置LRU算法来删除部分key,释放空间。默认是按照过期时间的,如果set时候没有加上过期时间就会导致数据写满maxmemory。
如果不设置maxmemory或者设置为0,64位系统不限制内存,32位系统最多使用3GB内存。
LRU是Least Recently Used 近期最少使用算法。
如果设置了 maxmemory
,一般都要设置过期策略。打开Redis的配置文件有如下描述,Redis有六种过期策略:
那么打开配置文件,添加如下一行,使用volatile-lru的过期策略:1
maxmemory-policy volatile-lru
保存文件退出,重启redis服务。
查找列出的信息中 used_memory
字段:
1
2
3
4
5
6
7
# Memory
used_memory:1324304
used_memory_human:1.26M
used_memory_rss:9826304
used_memory_rss_human:9.37M
used_memory_peak:1958208
used_memory_peak_human:1.87M
注意单位依然是 <bytes>
,本机 redis 使用量为 1.26M
修改redis的配置文件,将所有bind信息全部屏蔽。1
2
3# bind 192.168.1.100 10.0.0.1
# bind 192.168.1.8
# bind 127.0.0.1
修改完成后,需要重新启动redis服务。
如果有防火墙相关配置需要开启redis相关端口。
修改redis的配置文件 requirepass
字段。1
requirepass "uis.cc"
程序源路径: /temp/redis-3.2.4/
程序路径: /usr/local/bin/redis-server
配置文件路径: /etc/redis/redis.conf
pidfile路径: /var/redis/run/redis.pid
dump路径: /var/redis/data
logfile路径: /var/redis/log/redis.log
端口: 6379
客户端快速链接:redis-cli -h 10.10.10.10 -p 6379
服务方式启动 :service redis start/stop
路径方式启动:
启动redis命令: redis-server /etc/redis/redis.conf
停止redis命令: redis-cli shutdown
相关伟德伟诺官网:
http://blog.csdn.net/ludonqin/article/details/47211109
https://my.oschina.net/dxqr/blog/711578
http://blog.csdn.net/tanzhang78/article/details/52073440
1)下载redis安装包
mongo
进入数据库命令窗口。
MongoDB use DATABASE_NAME
用于创建数据库。该命令如果数据库不存在,将创建一个新的数据库, 否则将返回现有的数据库。
use DATABASE语句的基本语法如下:
1
> use DATABASE_NAME
如果想创建一个数据库名称为 <mydb>
, 那么 use DATABASE
语句应该如下:1
2> use mydb
switched to db mydb
要检查当前选择的数据库使用命令 db
1
2
> db
mydb
如果想查询数据库列表,那么使用命令 show dbs
.
1
2
3
> show dbs
local 0.78125GB
test 0.23012GB
所创建的数据库(mydb)不存在于列表中。要显示的数据库,需要至少插入一个文档进去。
1
2
3
4
5
> db.movie.insert({"name":"yiibai tutorials"})
> show dbs
local 0.78125GB
mydb 0.23012GB
test 0.23012GB
MongoDB的默认数据库是test。 如果没有创建任何数据库,那么集合将被保存在测试数据库。
MongoDB db.dropDatabase()
命令用于删除现有的数据库。
dropDatabase()指令的基本语法如下:
1
db.dropDatabase()
这将删除选定的数据库。如果没有选择任何数据库,那么它会删除默认的“test”数据库
如果想删除新的数据库 <mydb>
, 那么 dropDatabase()
命令将如下所示:
1
2
3
4
>use mydb
switched to db mydb
>db.dropDatabase()
>{ "dropped" : "mydb", "ok" : 1 }
MongoDB 的 db.createCollection(name, options)
用于创建集合。 在命令中, name
是要创建集合的名称。 Options
是一个文档,用于指定集合的配置。
选项参数是可选的,所以需要指定集合的唯一名字。
createCollection()方法的基本语法如下
1
2
3
4
>use test
switched to db test
>db.createCollection("mycollection")
{ "ok" : 1 }
可以通过使用 show collections
命令来检查创建的集合1
2
3>show collections
mycollection
system.indexes
1
2
>db.createCollection("mycol", { capped : true, autoIndexID : true, size : 6142800, max : 10000 } )
{ "ok" : 1 }
在MongoDB中并不需要创建集合。 当插入一些文档 MongoDB 会自动创建集合。
1
2
3
4
5
6
>db.yiibai.insert({"name" : "yiibai"})
>show collections
mycol
mycollection
system.indexes
yiibai
MongoDB 的 db.collection.drop()
用于从数据库中删除集合。
drop()
命令的基本语法如下1
db.COLLECTION_NAME.drop()
下面给出的例子将删除给定名称的集合:mycollection1
2
3
4>use mydb
switched to db mydb
>db.mycollection.drop()
true
将数据插入到MongoDB集合,需要使用MongoDB 的 insert()
方法。
insert()命令的基本语法如下:1
2
3
4
5
6
7
8
9
10
11>db.COLLECTION_NAME.insert(document)
例子
>db.mycol.insert({
_id: ObjectId(7df78ad8902c),
title: 'MongoDB Overview',
description: 'MongoDB is no sql database',
by: 'yiibai tutorials',
url: 'http://www.yiibai.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
})
这里 mycol 是我们的集合名称,它是在之前的教程中创建。如果集合不存在于数据库中,那么MongoDB创建此集合,然后插入文档进去。
在如果我们不指定_id
参数插入的文档,那么 MongoDB 将为文档分配一个唯一的ObjectId
。
_id
是12个字节十六进制数在一个集合的每个文档是唯一的。 12个字节被划分如下:
1
_id: ObjectId(4 bytes timestamp, 3 bytes machine id, 2 bytes process id, 3 bytes incrementer)
要以单个查询插入多个文档,可以通过文档 insert() 命令的数组方式。
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
>db.post.insert([
{
title: 'MongoDB Overview',
description: 'MongoDB is no sql database',
by: 'yiibai tutorials',
url: 'http://www.yiibai.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 100
},
{
title: 'NoSQL Database',
description: 'NoSQL database doesn't have tables',
by: 'yiibai tutorials',
url: 'http://www.yiibai.com',
tags: ['mongodb', 'database', 'NoSQL'],
likes: 20,
comments: [
{
user:'user1',
message: 'My first comment',
dateCreated: new Date(2013,11,10,2,35),
like: 0
}
]
}
])
要从集合查询MongoDB数据,需要使用MongoDB的 find()
方法。
find()方法的基本语法如下1
>db.COLLECTION_NAME.find()
find()
方法将在非结构化的方式显示所有的文件。 如果显示结果是格式化的,那么可以用pretty()
方法。
1
>db.mycol.find().pretty()
####例子
1
2
3
4
5
6
7
8
9
10
>db.mycol.find().pretty()
{
"_id": ObjectId(7df78ad8902c),
"title": "MongoDB Overview",
"description": "MongoDB is no sql database",
"by": "yiibai tutorials",
"url": "http://www.yiibai.com",
"tags": ["mongodb", "database", "NoSQL"],
"likes": "100"
}
除了find()
方法还有findOne()
方法,仅返回一个文档。
RDBMS Where子句等效于MongoDB
查询文档在一些条件的基础上,可以使用下面的操作
{<key>:<value>}
{<key>:{$lt:<value>}}
{<key>:{$lte:<value>}}
{<key>:{$gt:<value>}}
{<key>:{$gte:<value>}}
{<key>:{$ne:<value>}}
在 find()
方法,如果您传递多个键通过”,”将它们分开,那么MongoDB对待它就如 AND
条件一样。基本语法如下所示:1
>db.mycol.find({key1:value1, key2:value2}).pretty()
下面给出的例子将显示所有教程含“yiibai tutorials”和其标题是“MongoDB Overview”
1
2
3
4
5
6
7
8
9
10
>db.mycol.find({"by":"yiibai tutorials","title": "MongoDB Overview"}).pretty()
{
"_id": ObjectId(7df78ad8902c),
"title": "MongoDB Overview",
"description": "MongoDB is no sql database",
"by": "yiibai tutorials",
"url": "http://www.yiibai.com",
"tags": ["mongodb", "database", "NoSQL"],
"likes": "100"
}
对于上面给出的例子相当于where
子句:where by='yiibai tutorials' AND title='MongoDB Overview'
。可以传递任何数目的键-值对在find子句。
要查询基于OR条件的文件,需要使用$or关键字。OR的基本语法如下所示:1
2
3
4
5
6
7>db.mycol.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()
下面给出的例子将显示所有撰写含有 ‘yiibai tutorials’ 或是标题为 ‘MongoDB Overview’ 的教程
1
2
3
4
5
6
7
8
9
10
>db.mycol.find({$or:[{"by":"tutorials point"},{"title": "MongoDB Overview"}]}).pretty()
{
"_id": ObjectId(7df78ad8902c),
"title": "MongoDB Overview",
"description": "MongoDB is no sql database",
"by": "yiibai tutorials",
"url": "http://www.yiibai.com",
"tags": ["mongodb", "database", "NoSQL"],
"likes": "100"
}
下面给出的例子显示有喜欢数大于100 的文档,其标题要么是 ‘MongoDB Overview’ 或 ‘yiibai tutorials’. 等效于SQL的where子句:where likes>10 AND (by = 'yiibai tutorials' OR title = 'MongoDB Overview')
1
2
3
4
5
6
7
8
9
10
>db.mycol.find("likes": {$gt:10}, $or: [{"by": "yiibai tutorials"}, {"title": "MongoDB Overview"}] }).pretty()
{
"_id": ObjectId(7df78ad8902c),
"title": "MongoDB Overview",
"description": "MongoDB is no sql database",
"by": "yiibai tutorials",
"url": "http://www.yiibai.com",
"tags": ["mongodb", "database", "NoSQL"],
"likes": "100"
}
MongoDB的update()
和save()
方法用于更新文档到一个集合。 update()方法将现有的文档中的值更新,而save()方法使用传递到save()方法的文档替换现有的文档。
MongoDB Update() 方法 语法
1
>db.COLLECTION_NAME.update(SELECTIOIN_CRITERIA, UPDATED_DATA)
考虑mycol集合有如下数据。1
2
3{ "_id" : ObjectId(5983548781331adf45ec5), "title":"MongoDB Overview"}
{ "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
{ "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}
下面的例子将设置其标题“MongoDB Overview”的文件为新标题为“New MongoDB Tutorial”1
2
3
4
5>db.mycol.update({'title':'MongoDB Overview'},{$set:{'title':'New MongoDB Tutorial'}})
>db.mycol.find()
{ "_id" : ObjectId(5983548781331adf45ec5), "title":"New MongoDB Tutorial"}
{ "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
{ "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Tutorial Overview"}
默认情况下,MongoDB将只更新单一文件,更新多,需要一个参数 ‘multi’ 设置为 true。1
>db.mycol.update({'title':'MongoDB Overview'},{$set:{'title':'New MongoDB Tutorial'}},{multi:true})
save() 取代方法,通过 save()方法取代新文档
mongodb 的 save()方法如下所示的基本语法:1
2>db.COLLECTION_NAME.save({_id:ObjectId(),NEW_DATA})
例子
下面的例子将替换该文件_id '5983548781331adf45ec7'
1
2
3
4
5
6
7
8
9>db.mycol.save(
{
"_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai New Topic", "by":"Yiibai Yiibai"
}
)
>db.mycol.find()
{ "_id" : ObjectId(5983548781331adf45ec5), "title":"Yiibai Yiibai New Topic", "by":"Yiibai Yiibai"}
{ "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
{ "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}
MongoDB 的 remove()
方法用于从集合中删除文档。remove()方法接受两个参数。一个是标准缺失,第二是justOne标志
deletion criteria : 根据文件(可选)删除条件将被删除。
justOne : (可选)如果设置为true或1,然后取出只有一个文档。
remove()方法的基本语法如下1
>db.COLLECTION_NAME.remove(DELLETION_CRITTERIA)
考虑mycol集合有如下数据。1
2
3{ "_id" : ObjectId(5983548781331adf45ec5), "title":"MongoDB Overview"}
{ "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
{ "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Yiibai Overview"}
下面的例子将删除所有的文件,其标题为 ‘MongoDB Overview’1
2
3
4>db.mycol.remove({'title':'MongoDB Overview'})
>db.mycol.find()
{ "_id" : ObjectId(5983548781331adf45ec6), "title":"NoSQL Overview"}
{ "_id" : ObjectId(5983548781331adf45ec7), "title":"Yiibai Toturials Overview"}
只删除一个
如果有多个记录,并要删除仅第一条记录,然后在 remove()
方法设置参数 justOne
。1
>db.COLLECTION_NAME.remove(DELETION_CRITERIA,1)
如果没有指定删除条件,则MongoDB将从集合中删除整个文件。这相当于SQL的 truncate 命令。1
2>db.mycol.remove()
>db.mycol.find()
mongo
进入数据库命令窗口。
MongoDB use DATABASE_NAME
用于创建数据库。该命令如果数据库不存在,将创]]>
编译和安装本地插件,你可能需要安装编译工具:
1
yum install gcc-c++ make
在 centos 等 linux 系统中用 root 用户运行 RHEL, 来安装 Node.js v4:
1
curl --silent --location https://rpm.nodesource.com/setup_4.x | bash -
Node.js v6:
1
curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -
Node.js 0.10:
1
curl --silent --location https://rpm.nodesource.com/setup | bash -
然后运行:
1
yum -y install nodejs
检查一下版本号看是否安装成功1
2node -v
v4.5.0
先切换到root用户一些需要的依赖包1
yum install libtool automake autoconf gcc-c++ openssl-devel
下载源代码自己编译以下代码中的tar.gz包根据node.js官网上的版本来定,比如我现在最新是 0.10.29
1
2
3
4
5
6
7
cd /usr/local/src
wget https://nodejs.org/dist/v4.5.0/node-v4.5.0.tar.gz
tar zxvf node-v4.5.0.tar.gz
cd node-v4.5.0
./configure
make
make install
检查一下版本号看是否安装成功1
2node -v
v4.5.0
用自带的包管理先删除一次1
yum remove nodejs npm -y
依次类推,看你的操作系统用什么包管理,如果你是用 brew 安装的 node 需要用 brew 先删除一次
进入 /usr/local/lib
删除所有 node
和 node_modules
文件夹
进入 /usr/local/include
删除所有 node
和 node_modules
文件夹
检查 ~
文件夹里面的 local
lib
include
文件夹,然后删除里面的所有node
和node_modules
文件夹
可以使用以下命令查找:
1
2
find ~/ -name node
find ~/ -name node_modules
进入 /usr/local/bin
删除 node
的可执行文件
以下步骤可选:
删除: /usr/local/bin/npm
删除: /usr/local/share/man/man1/node.1
删除: /usr/local/lib/dtrace/node.d
删除: rm -rf /home/[homedir]/.npm
删除: rm -rf /home/root/.npm
]]>编译和安装本地插件,你可能需要安装编译工具:
1
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
1
brew update
1
brew install mongodb
1
brew services start mongodb
1
brew services start mongodb
1
mongod --config /usr/local/etc/mongod.conf
express()
用来创建一个Express的程序。express()
方法是express模块导出的顶层方法。1
2var express = require('express');
var app = express();
express.static
是Express中唯一的内建中间件。它以server-static模块为基础开发,负责托管 Express 应用内的静态资源。
参数root
为静态资源的所在的根目录。
参数options
是可选的,支持以下的属性:
Last-Modified
头部。可能的取值有false
和true
。如果你想获得更多关于使用中间件的细节,你可以查阅Serving static files in Express。
app
对象一般用来表示Express程序。通过调用Express模块导出的顶层的express()
方法来创建它:1
2
3
4
5
6
7
8var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.send('hello world!');
});
app.listen(3000);
app
对象具有以下的方法:
它还有一些属性设置,这些属性可以改变程序的行为。获得更多的信息,可以查阅Application settings。
app.locals
对象是一个javascript对象,它的属性就是程序本地的变量。1
2
3
4
5app.locals.title
// => 'My App'
app.locals.email
// => 'me@myapp.com'
一旦设定,app.locals
的各属性值将贯穿程序的整个生命周期,与其相反的是res.locals
,它只在这次请求的生命周期中有效。
在程序中,你可以在渲染模板时使用这些本地变量。它们是非常有用的,可以为模板提供一些有用的方法,以及app
级别的数据。通过req.app.locals
(具体查看req.app),Locals可以在中间件中使用。1
2
3app.locals.title = 'My App';
app.locals.strftime = require('strftime');
app.locals.email = 'me@myapp.com';
app.mountpath
属性是子程序挂载的路径模式。
一个子程序是一个
express
的实例,其可以被用来作为路由句柄来处理请求。
1
2
3
4
5
6
7
8
9
var express = require('express');
var app = express(); // the main app
var admin = express(); // the sub app
admin.get('/', function(req, res) {
console.log(admin.mountpath); // /admin
res.send('Admin Homepage');
});
app.use('/admin', admin); // mount the sub app
它和req对象的baseUrl属性比较相似,除了req.baseUrl
是匹配的URL路径,而不是匹配的模式。如果一个子程序被挂载在多条路径模式,app.mountpath
就是一个关于挂载路径模式项的列表,如下面例子所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14var admin = express();
admin.get('/', function(req, res) {
console.log(admin.mountpath); // ['adm*n', '/manager']
res.send('Admin Homepage');
});
var secret = express();
secret.get('/', function(req, res) {
console.log(secret.mountpath); // /secr*t
res.send('Admin secret');
});
admin.use('secr*t', secret); // load the 'secret' router on '/secr*t', on the 'admin' sub app
app.use(['/adm*n', '/manager'], admin); // load the 'admin' router on '/adm*n' and '/manager' , on the parent app
当子程序被挂载到父程序时,mount
事件被发射。父程序对象作为参数,传递给回调方法。1
2
3
4
5
6
7
8
9
10
11var admin = express();
admin.on('mount', function(parent) {
console.log('Admin Mounted');
console.log(parent); // refers to the parent app
});
admin.get('/', function(req, res) {
res.send('Admin Homepage');
});
app.use('/admin', admin);
app.all
方法和标准的app.METHOD()
方法相似,除了它匹配所有的HTTP动词。
对于给一个特殊前缀映射一个全局的逻辑处理,或者无条件匹配,它是很有效的。例如,如果你把下面内容放在所有其他的路由定义的前面,它要求所有从这个点开始的路由需要认证和自动加载一个用户。记住这些回调并不是一定是终点:loadUser
可以在完成了一个任务后,调用next()
方法来继续匹配随后的路由。1
app.all('*', requireAuthentication, loadUser);
或者这种相等的形式:1
2app.all('*', requireAuthentication);
app.all('*', loadUser);
另一个例子是全局的白名单方法。这个例子和前面的很像,然而它只是限制以/api
开头的路径。1
app.all('/api/*', requireAuthentication);
路由HTTP DELETE
请求到有特殊回调方法的特殊的路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果不能满足当前路由的处理条件,那么你可以传递控制到随后的路由。1
2
3app.delete('/', function(req, res) {
res.send('DELETE request to homepage');
});
设置类型为布尔的设置名为name
的值为false
,此处的name
是app settings table中各属性的一个。调用app.set('foo', false)
和调用app.disable('foo')
是等价的。
比如:1
2
3app.disable('trust proxy');
app.get('trust proxy');
// => false
返回true
如果布尔类型的设置值name
被禁用为false
,此处的name
是app settings table中各属性的一个。1
2
3
4
5app.disabled('trust proxy');
// => true
app.enable('trust proxy');
app.disabled('trust proxy');
// => false
设置布尔类型的设置值name
为true
,此处的name
是app settings table中各属性的一个。调用app.set('foo', true)
和调用app.enable('foo')
是等价的。1
2
3app.enable('trust proxy');
app.get('trust proxy');
// => true
返回true
如果布尔类型的设置值name
被启动为true
,此处的name
是app settings table中各属性的一个。1
2
3
4
5app.enabled('trust proxy');
// => false
app.enable('trust proxy');
app.enabled('trust proxy');
// => true
注册给定引擎的回调,用来渲染处理ext文件。
默认情况下,Express需要使用require()
来加载基于文件扩展的引擎。例如,如果你尝试渲染一个foo.jade
文件,Express在内部调用下面的内容,同时缓存require()
结果供随后的调用,来加速性能。
1
app.engine('jade', require('jade').__express);
使用下面的方法对于那些没有提供开箱即用的.__express
方法的模板,或者你希望使用不同的模板引擎扩展。
比如,使用EJS模板引擎来渲染.html
文件:1
app.engine('html', require('ejs').renderFile);
在这个例子中,EJS提供了一个.renderFile
方法,这个方法满足了Express规定的签名规则:(path, options, callback)
,然而记住在内部它只是ejs.__express
的一个别名,所以你可以在不做任何事的情况下直接使用.ejs
扩展。
一些模板引擎没有遵循这种规范,consolidate.js库映射模板引擎以下面的使用方式,所以他们可以无缝的和Express工作。1
2
3var engines = require('consolidate');
app.engine('haml', engines.haml);
app.engine('html', engines.hogan);
获得设置名为name
的app设置的值,此处的name
是app settings table中各属性的一个。
如下:1
2
3
4
5
6app.get('title');
// => undefined
app.set('title', 'My Site');
app.get('title');
// => 'My Site'
路由HTTP GET
请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。1
2
3app.get('/', function(req, res) {
res.send('GET request to homepage');
});
绑定程序监听端口到指定的主机和端口号。这个方法和Node
中的http.Server.listen()是一样的。1
2
3var express = require('express');
var app = express();
app.listen(3000);
通过调用express()
返回得到的app
实际上是一个JavaScript的Function
,被设计用来作为一个回调传递给Node HTTP servers
来处理请求。这样,其就可以很简便的基于同一份代码提供http和https版本,所以app没有从这些继承(它只是一个简单的回调)。1
2
3
4
5
6var express = require('express');
var https = require('https');
var http = require('http');
http.createServer(app).listen(80);
https.createServer(options, app).listen(443);
app.listen()
方法是下面所示的一个便利的方法(只针对HTTP协议):1
2
3
4app.listen = function() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};
路由一个HTTP请求,METHOD
是这个请求的HTTP方法,比如GET
,PUT
,POST
等等,注意是小写的。所以,实际的方法是app.get()
,app.post()
,app.put()
等等。下面有关于方法的完整的表。
获取更多信息,请看routing guide。
Express支持下面的路由方法,对应与同名的HTTP方法:
如果使用上述方法时,导致了无效的javascript的变量名,可以使用中括号符号,比如,
app['m-search']('/', function ...
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没有满足当前路由的处理条件,那么传递控制到随后的路由。
本API文档把使用比较多的HTTP方法
app.get()
,app.post
,app.put()
,app.delete()
作为一个个单独的项进行说明。然而,其他上述列出的方法以完全相同的方式工作。
app.all()
是一个特殊的路由方法,它不属于HTTP协议中的规定的方法。它为一个路径加载中间件,其对所有的请求方法都有效。
1
2
3
4
app.all('/secret', function (req, res) {
console.log('Accessing the secret section...');
next(); // pass control to the next handler
});
给路由参数添加回调触发器,这里的name
是参数名或者参数数组,function
是回调方法。回调方法的参数按序是请求对象,响应对象,下个中间件,参数值和参数名。
如果name
是数组,会按照各个参数在数组中被声明的顺序将回调触发器注册下来。还有,对于除了最后一个参数的其他参数,在他们的回调中调用next()
来调用下个声明参数的回调。对于最后一个参数,在回调中调用next()
将调用位于当前处理路由中的下一个中间件,如果name
只是一个string
那就和它是一样的(就是说只有一个参数,那么就是最后一个参数,和数组中最后一个参数是一样的)。
例如,当:user
出现在路由路径中,你可以映射用户加载的逻辑处理来自动提供req.user
给这个路由,或者对输入的参数进行验证。1
2
3
4
5
6
7
8
9
10
11
12app.param('user', function(req, res, next, id) {
User.find(id, function(error, user) {
if (err) {
next(err);
}
else if (user){
req.user = user;
} else {
next(new Error('failed to load user'));
}
});
});
对于Param
的回调定义的路由来说,他们是局部的。它们不会被挂载的app或者路由继承。所以,定义在app
上的Param
回调只有是在app
上的路由具有这个路由参数时才起作用。
在定义param
的路由上,param
回调都是第一个被调用的,它们在一个请求-响应循环中都会被调用一次并且只有一次,即使多个路由都匹配,如下面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14app.param('id', function(req, res, next, id) {
console.log('CALLED ONLY ONCE');
next();
});
app.get('/user/:id', function(req, res, next) {
console.log('although this matches');
next();
});
app.get('/user/:id', function(req, res) {
console.log('and this mathces too');
res.end();
});
当GET /user/42
,得到下面的结果:
1
CALLED ONLY ONCE although this matches and this matches too
1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.param(['id', 'page'], function(req, res, next, value) {
console.log('CALLED ONLY ONCE with', value);
next();
});
app.get('/user/:id/:page', function(req. res, next) {
console.log('although this matches');
next();
});
app.get('/user/:id/:page', function (req, res, next) {
console.log('and this matches too');
res.end();
});
当执行GET /user/42/3
,结果如下:1
CALLED ONLY ONCE with 42
CALLED ONLY ONCE with 3
although this matches
and this mathes too
下面章节描述的
app.param(callback)
在v4.11.0之后被弃用。
通过只传递一个回调参数给app.param(name, callback)
方法,app.param(naem, callback)
方法的行为将被完全改变。这个回调参数是关于app.param(name, callback)
该具有怎样的行为的一个自定义方法,这个方法必须接受两个参数并且返回一个中间件。
这个回调的第一个参数就是需要捕获的url的参数名,第二个参数可以是任一的JavaScript对象,其可能在实现返回一个中间件时被使用。
这个回调方法返回的中间件决定了当URL中包含这个参数时所采取的行为。
在下面的例子中,app.param(name, callback)
参数签名被修改成了app.param(name, accessId)
。替换接受一个参数名和回调,app.param()
现在接受一个参数名和一个数字。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23var express = require('express');
var app = express();
app.param(function(param, option){
return function(req, res, next, val) {
if (val == option) {
next();
}
else {
res.sendStatus(403);
}
}
});
app.param('id', 1337);
app.get('/user/:id', function(req, res) {
res.send('Ok');
});
app.listen(3000, function() {
console.log('Ready');
});
在这个例子中,app.param(name, callback)
参数签名保持和原来一样,但是替换成了一个中间件,定义了一个自定义的数据类型检测方法来检测user id
的类型正确性。1
2
3
4
5
6
7
8
9
10
11
12
13
14app.param(function(param, validator) {
return function(req, res, next, val) {
if (validator(val)) {
next();
}
else {
res.sendStatus(403);
}
}
});
app.param('id', function(candidate) {
return !isNaN(parseFloat(candidate)) && isFinite(candidate);
});
在使用正则表达式来,不要使用
.
。例如,你不能使用/user-.+/
来捕获user-gami
,用使用[\\s\\S]
或者[\\w\\>W]
来代替(正如/user-[\\s\\S]+/
)。 1
2
3
4
5
6
7
8//captures '1-a_6' but not '543-azser-sder'
router.get('/[0-9]+-[[\\w]]*', function);
//captures '1-a_6' and '543-az(ser"-sder' but not '5-a s'
router.get('/[0-9]+-[[\\S]]*', function);
//captures all (equivalent to '.*')
router.get('[[\\s\\S]]*', function);
通过这个方法可以得到app
典型的路径,其是一个string
。1
2
3
4
5
6
7
8
9
10var app = express()
, blog = express()
, blogAdmin = express();
app.use('/blog', blog);
app.use('/admin', blogAdmin);
console.log(app.path()); // ''
console.log(blog.path()); // '/blog'
console.log(blogAdmin.path()); // '/blog/admin'
如果app
挂载很复杂下,那么这个方法的行为也会很复杂:一种更好用的方式是使用req.baseUrl
来获得这个app的典型路径。
路由HTTP POST
请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。1
2
3app.post('/', function(req, res) {
res.send('POST request to homepage')
});
路由HTTP PUT
请求到有特殊回调的特殊路径。获取更多的信息,可以查阅routing guide。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没能满足当前路由的处理条件,那么传递控制到随后的路由。1
2
3app.put('/', function(req, res) {
res.send('PUT request to homepage');
});
通过callback
回调返回一个view
渲染之后得到的HTML文本。它可以接受一个可选的参数,可选参数包含了这个view
需要用到的本地数据。这个方法类似于res.render()
,除了它不能把渲染得到的HTML文本发送给客户端。
将
app.render()
当作是可以生成渲染视图字符串的工具方法。在res.render()
内部,就是使用的app.render()
来渲染视图。如果使能了视图缓存,那么本地变量缓存就会保留。如果你想在开发的过程中缓存视图,设置它为
true
。在生产环境中,视图缓存默认是打开的。
1
2
3
4
5
6
7
app.render('email', function(err, html) {
// ...
});
app.render('email', {name:'Tobi'}, function(err, html) {
// ...
});
返回一个单例模式的路由的实例,之后你可以在其上施加各种HTTP动作的中间件。使用app.route()
来避免重复路由名字(因此错字错误)–说的意思应该是使用app.router()
这个单例方法来避免同一个路径多个路由实例。1
2
3
4
5
6
7
8
9
10
11
12
13var app = express();
app.route('/events')
.all(function(req, res, next) {
// runs for all HTTP verbs first
// think of it as route specific middleware!
})
.get(function(req, res, next) {
res.json(...);
})
.post(function(req, res, next) {
// maybe add a new event...
})
给name
设置项赋value
值,name
是app settings table中属性的一项。
对于一个类型是布尔型的属性调用app.set('foo', ture)
等价于调用app.enable('foo')
。同样的,调用app.set('foo', false)
等价于调用app.disable('foo')
。
可以使用app.get()
来取得设置的值:1
2app.set('title', 'My Site');
app.get('title'); // 'My Site'
Application Settings
如果name
是程序设置之一,它将影响到程序的行为。下边列出了程序中的设置。
/Foo
和/foo
处理是一样。false
来禁用query parser
,或者设置simple
,extended
,也可以自己实现query string
解析函数。simple
基于Node
原生的query
解析,querystring。/foo
和/foo/
的路由处理是一样。app
在一个反向代理的后面,使用x-Forwarded-*
来确定连接和客户端的IP地址。注意:X-Forwarded-*
头部很容易被欺骗,所有检测客户端的IP地址是靠不住的。trust proxy
默认不启用。当启用时,Express尝试通过前端代理或者一系列代理来获取已连接的客户端IP地址。req.ips
属性包含了已连接客户端IP地址的一个数组。为了启动它,需要设置在下面trust proxy options table中定义的值。trust proxy
的设置实现使用了proxy-addr
包。如果想获得更多的信息,可以查阅它的文档view
所在的目录或者目录数组。如果是一个数组,将按在数组中的顺序来查找view
。X-Powered-By:Express
HTTP头部Options for trust proxy
settings
查阅Express behind proxies来获取更多信息。
如果为true
,客户端的IP地址作为X-Forwarded-*
头部的最左边的条目。如果为false
,可以理解为app
直接与英特网直连,客户端的IP地址衍生自req.connection.remoteAddress
。false
是默认设置。
一个IP地址,子网,或者一组IP地址,和委托子网。下面列出的是一个预先配置的子网名列表。
127.0.0.1/8
, ::1/128
169.254.0.0/16
, fe80::/10
10.0.0.0/8
, 172.16.0.0/12
, 192.168.0.0/16
, fc00::/7
使用下面方法中的任何一种来设置IP地址:
app.set(‘trust proxy’, ‘loopback’) // specify a single subnet
app.set(‘trust proxy’, ‘loopback, 123.123.123.123’) // specify a subnet and an address
app.set(‘trust proxy’, ‘loopback, linklocal, uniquelocal’) // specify multiple subnets as CSV
app.set(‘trust proxy’, [‘loopback’, ‘linklocal’, ‘uniquelocal’]) // specify multiple subnets as an array
当指定IP地址之后, 这个IP地址或子网会被设置了这个IP地址或子网的app
排除在外, 最靠近程序服务的没有委托的地址将被看做客户端IP地址。
信任从反向代理到app中间小于等于n跳的连接为客户端。
客户自定义委托代理信任机制。如果你使用这个,请确保你自己知道你在干什么。
app.set(‘trust proxy’, function (ip) {
if (ip === ‘127.0.0.1’ || ip === ‘123.123.123.123’) return true; // trusted IPs
else return false;
})
Options for etag
settingsETag
功能的实现使用了etag包。如果你需要获得更多的信息,你可以查阅它的文档。
设置为true
,启用weak ETag。这个是默认设置。设置false
,禁用所有的ETag。
strong
,使能strong ETag。如果是weak
,启用weak
ETag。客户自定义ETag
方法的实现. 如果你使用这个,请确保你自己知道你在干什么。
app.set(‘etag’, function (body, encoding) {
return generateHash(body, encoding); // consider the function is defined
})
挂载中间件方法到路径上。如果路径未指定,那么默认为”/“。
一个路由将匹配任何路径如果这个路径以这个路由设置路径后紧跟着”/“。比如:
app.use('/appale', ...)
将匹配”/apple”,”/apple/images”,”/apple/images/news”等。中间件中的
req.originalUrl
是req.baseUrl
和req.path
的组合,如下面的例子所示。 1
2
3
4
5
6app.use('/admin', function(req, res, next) {
// GET 'http://www.example.com/admin/new'
console.log(req.originalUrl); // '/admin/new'
console.log(req.baseUrl); // '/admin'
console.log(req.path);// '/new'
});
在一个路径上挂载一个中间件之后,每当请求的路径的前缀部分匹配了这个路由路径,那么这个中间件就会被执行。
由于默认的路径为/
,中间件挂载没有指定路径,那么对于每个请求,这个中间件都会被执行。1
2
3
4
5// this middleware will be executed for every request to the app.
app.use(function(req, res, next) {
console.log('Time: %d', Date.now());
next();
});
中间件方法是顺序处理的,所以中间件包含的顺序是很重要的。1
2
3
4
5
6
7
8
9// this middleware will not allow the request to go beyond it
app.use(function(req, res, next) {
res.send('Hello World');
});
// this middleware will never reach this route
app.use('/', function(req, res) {
res.send('Welcome');
});
路径可以是代表路径的一串字符,一个路径模式,一个匹配路径的正则表达式,或者他们的一组集合。
下面是路径的简单的例子。
// will match paths starting with /abcd
app.use(‘/abcd’, function (req, res, next) {
next();
})
// will match paths starting with /abcd and /abd
app.use(‘/abc?d’, function (req, res, next) {
next();
})
// will match paths starting with /abcd, /abbcd, /abbbbbcd and so on
app.use(‘/ab+cd’, function (req, res, next) {
next();
})
// will match paths starting with /abcd, /abxcd, /abFOOcd, /abbArcd and so on
app.use(‘/ab*cd’, function (req, res, next) {
next();
})
// will match paths starting with /ad and /abcd
app.use(‘/a(bc)?d’, function (req, res, next) {
next();
})
// will match paths starting with /abc and /xyz
app.use(/\/abc|\/xyz/, function (req, res, next) {
next();
})
// will match paths starting with /abcd, /xyza, /lmn, and /pqr
app.use([‘/abcd’, ‘/xyza’, /\/lmn|\/pqr/], function (req, res, next) {
next();
})
方法可以是一个中间件方法,一系列中间件方法,一组中间件方法或者他们的集合。由于router
和app
实现了中间件接口,你可以像使用其他任一中间件方法那样使用它们。
app.use(function (req, res, next) {
next();
})
router
是有效的中间件。var router = express.Router();
router.get(‘/‘, function (req, res, next) {
next();
})
app.use(router);
Express
程序是一个有效的中间件。var subApp = express();
subApp.get(‘/‘, function (req, res, next) {
next();
})
app.use(subApp);
var r1 = express.Router();
r1.get(‘/‘, function (req, res, next) {
next();
})
var r2 = express.Router();
r2.get(‘/‘, function (req, res, next) {
next();
})
app.use(r1, r2);
var r1 = express.Router();
r1.get(‘/‘, function (req, res, next) {
next();
})
var r2 = express.Router();
r2.get(‘/‘, function (req, res, next) {
next();
})
app.use(‘/‘, [r1, r2]);
function mw1(req, res, next) { next(); }
function mw2(req, res, next) { next(); }
var r1 = express.Router();
r1.get(‘/‘, function (req, res, next) { next(); });
var r2 = express.Router();
r2.get(‘/‘, function (req, res, next) { next(); });
var subApp = express();
subApp.get(‘/‘, function (req, res, next) { next(); });
app.use(mw1, [mw2, r1, r2], subApp);
下面是一些例子,在Express
程序中使用express.static
中间件。
为程序托管位于程序目录下的public
目录下的静态资源:1
2// GET /style.css etc
app.use(express.static(__dirname + '/public'));
在/static
路径下挂载中间件来提供静态资源托管服务,只当请求是以/static
为前缀的时候。1
2// GET /static/style.css etc.
app.use('/static', express.static(express.__dirname + '/public'));
通过在设置静态资源中间件之后加载日志中间件来关闭静态资源请求的日志。1
2app.use(express.static(__dirname + '/public'));
app.use(logger());
托管静态资源从不同的路径,但./public
路径比其他更容易被匹配:1
2
3app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/files'));
app.use(express.static(__dirname + '/uploads'));
req
对象代表了一个HTTP请求,其具有一些属性来保存请求中的一些数据,比如query string
,parameters
,body
,HTTP headers
等等。在本文档中,按照惯例,这个对象总是简称为req
(http响应简称为res
),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
如下例子:1
2
3app.get('/user/:id', function(req, res) {
res.send('user' + req.params.id);
});
其实你也可以这样写:1
2
3app.get('/user/:id', function(request, response) {
response.send('user' + request.params.id);
});
在Express 4
中,req.files
默认在req
对象中不再是可用的。为了通过req.files
对象来获得上传的文件,你可以使用一个multipart-handling
(多种处理的工具集)中间件,比如busboy
,multer
,formidable
,multipraty
,connect-multiparty
或者pez
。
这个属性持有express
程序实例的一个引用,其可以作为中间件使用。
如果你按照这个模式,你创建一个模块导出一个中间件,这个中间件只在你的主文件中require()
它,那么这个中间件可以通过req.app
来获取express的实例。
例如:1
2// index.js
app.get("/viewdirectory", require('./mymiddleware.js'));
1
2
3
4
// mymiddleware.js
module.exports = function(req, res) {
res.send('The views directory is ' + req.app.get('views'));
};
一个路由实例挂载的Url路径。1
2
3
4
5
6var greet = express.Router();
greet.get('/jp', function(req, res) {
console.log(req.baseUrl); // greet
res.send('Konichiwa!');
});
app.use('/greet', greet);
即使你使用的路径模式或者一系列路径模式来加载路由,baseUrl
属性返回匹配的字符串,而不是路由模式。下面的例子,greet
路由被加载在两个路径模式上。1
app.use(['/gre+t', 'hel{2}o'], greet); // load the on router on '/gre+t' and '/hel{2}o'
当一个请求路径是/greet/jp
,baseUrl
是/greet
,当一个请求路径是/hello/jp
,req.baseUrl
是/hello
。req.baseUrl
和app
对象的mountpath属性相似,除了app.mountpath
返回的是路径匹配模式。
在请求的body中保存的是提交的一对对键值数据。默认情况下,它是undefined
,当你使用比如body-parser
和multer
这类解析body
数据的中间件时,它是填充的。
下面的例子,给你展示了怎么使用body-parser
中间件来填充req.body
。1
2
3
4
5
6
7
8
9
10
11var app = require('express');
var bodyParser = require('body-parser');
var multer = require('multer');// v1.0.5
var upload = multer(); // for parsing multipart/form-data
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({extended:true})); // for parsing application/x-www-form-urlencoded
app.post('/profile', upload.array(), function(req, res, next) {
console.log(req.body);
res.json(req.body);
});
当使用cookie-parser
中间件的时候,这个属性是一个对象,其包含了请求发送过来的cookies
。如果请求没有带cookies
,那么其值为{}
。1
2
3// Cookie: name=tj
req.cookies.name
// => "tj"
获取更多信息,问题,或者关注,可以查阅cookie-parser。
指示这个请求是否是新鲜的。其和req.stale
是相反的。
当cache-control
请求头没有no-cache
指示和下面中的任一一个条件为true
,那么其就为true
:
if-modified-since
请求头被指定,和last-modified
请求头等于或者早于modified
响应头。if-none-match
请求头是*
。if-none-match
请求头在被解析进它的指令之后,不匹配etag
响应头(完全不知道什么鬼)。1
2
req.fresh
// => true
包含了源自Host
HTTP头部的hostname
。
当trust proxy
设置项被设置为启用值,X-Forwarded-Host
头部被使用来代替Host
。这个头部可以被客户端或者代理设置。1
2
3// Host: "example.com"
req.hostname
// => "example.com"
当trust proxy
设置项被设置为启用值,这个属性包含了一组在X-Forwarded-For
请求头中指定的IP地址。不然,其就包含一个空的数组。这个头部可以被客户端或者代理设置。
例如,如果X-Forwarded-For
是client
,proxy1
,proxy2
,req.ips
就是["clinet", "proxy1", "proxy2"]
,这里proxy2
就是最远的下游。
req.url
不是一个原生的Express
属性,它继承自Node’s http module。
这个属性很像req.url
;然而,其保留了原版的请求链接,允许你自由地重定向req.url
到内部路由。比如,app.use()
的mounting
特点可以重定向req.url
跳转到挂载点。1
2
3// GET /search?q=something
req.originalUrl
// => "/search?q=something"
一个对象,其包含了一系列的属性,这些属性和在路由中命名的参数名是一一对应的。例如,如果你有/user/:name
路由,name
属性可作为req.params.name
。这个对象默认值为{}
。1
2
3// GET /user/tj
req.params.name
// => "tj"
当你使用正则表达式来定义路由规则,捕获组的组合一般使用req.params[n]
,这里的n
是第几个捕获租。这个规则被施加在无名通配符匹配,比如/file/*
的路由:1
2
3// GET /file/javascripts/jquery.js
req.params[0]
// => "javascripts/jquery.js"
包含请求URL的部分路径。1
2
3// example.com/users?sort=desc
req.path
// => "/users"
当在一个中间件中被调用,挂载点不包含在
req.path
中。你可以查阅app.use()获得跟多的信息。
请求的协议,一般为http
,当启用TLS加密,则为https
。
当trust proxy
设置一个启用的参数,如果存在X-Forwarded-Proto
头部的话,其将被信赖和使用。这个头部可以被客户端或者代理设置。1
2req.ptotocol
// => "http"
一个对象,为每一个路由中的query string
参数都分配一个属性。如果没有query string
,它就是一个空对象,{}
。1
2
3
4
5
6
7
8
9
10
11
12
13// GET /search?q=tobi+ferret
req.query.q
// => "tobi ferret"
// GET /shoes?order=desc&shoe[color]=blue&shoe[type]=converse
req.query.order
// => "desc"
req.query.shoe.color
// => "blue"
req.query.shoe.type
// => "converse"
当前匹配的路由,其为一串字符。比如:1
2
3
4app.get('/user/:id?', function userIdHandler(req, res) {
console.log(req.route);
res.send('GET')
})
前面片段的输出为:1
2
3
4
5
6
7
8
9
10
11
12
13
14{ path:"/user/:id?"
stack:
[
{ handle:[Function:userIdHandler],
name:"userIdHandler",
params:undefined,
path:undefined,
keys:[],
regexp:/^\/?$/i,
method:'get'
}
]
methods:{get:true}
}
一个布尔值,如果建立的是TLS的连接,那么就为true
。等价与:1
'https' == req.protocol;
当使用cookie-parser
中间件的时候,这个属性包含的是请求发过来的签名cookies
,不签名的并且为使用做好了准备(这句真不知道怎么翻译了…)。签名cookies
驻留在不同的对象中来体现开发者的意图;不然,一个恶意攻击可以被施加在req.cookie
值上(它是很容易被欺骗的)。记住,签名一个cookie
不是把它藏起来或者加密;而是简单的防止篡改(因为签名使用的加密是私人的)。如果没有发送签名的cookie
,那么这个属性默认为{}
。1
2
3// Cookie: user=tobi.CP7AWaXDfAKIRfH49dQzKJx7sKzzSoPq7/AcBBRVwlI3
req.signedCookies.user
// => "tobi"
为了获取更多的信息,问题或者关注,可以参阅cookie-parser。
指示这个请求是否是stale
(陈旧的),它与req.fresh
是相反的。更多信息,可以查看req.fresh。1
2req.stale
// => true
请求中域名的子域名数组。1
2
3// Host: "tobi.ferrets.example.com"
req.subdomains
// => ["ferrets", "tobi"]
一个布尔值,如果X-Requested-With
的值为XMLHttpRequest
,那么其为true
,其指示这个请求是被一个客服端库发送,比如jQuery
。1
2req.xhr
// => true
检查这个指定的内容类型是否被接受,基于请求的Accept
HTTP头部。这个方法返回最佳匹配,如果没有一个匹配,那么其返回undefined
(在这个case下,服务器端应该返回406和”Not Acceptable”)。type
值可以是一个单的MIME type
字符串(比如application/json
),一个扩展名比如json
,一个逗号分隔的列表,或者一个数组。对于一个列表或者数组,这个方法返回最佳项(如果有的话)。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// Accept: text/html
req.accepts('html');
// => "html"
// Accept: text/*, application/json
req.accepts('html');
// => "html"
req.accepts('text/html');
// => "text/html"
req.accepts(['json', 'text']);
// => "json"
req.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
req.accepts('image/png');
req.accepts('png');
// => undefined
// Accept: text/*;q=.5, application/json
req.accepts(['html', 'json']);
// => "json"
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
返回指定的字符集集合中第一个的配置的字符集,基于请求的Accept-Charset
HTTP头。如果指定的字符集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
返回指定的编码集合中第一个的配置的编码,基于请求的Accept-Encoding
HTTP头。如果指定的编码集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
返回指定的语言集合中第一个的配置的语言,基于请求的Accept-Language
HTTP头。如果指定的语言集没有匹配的,那么就返回false。
获取更多信息,或者如果你有问题或关注,可以参阅accepts。
返回指定的请求HTTP头部的域内容(不区分大小写)。Referrer
和Referer
的域内容可互换。1
2
3
4
5
6
7
8req.get('Content-type');
// => "text/plain"
req.get('content-type');
// => "text/plain"
req.get('Something')
// => undefined
其是req.header(field)
的别名。
如果进来的请求的Content-type
头部域匹配参数type
给定的MIME type
,那么其返回true
。否则返回false
。1
2
3
4
5
6
7
8
9
10
11
12
13
14// With Content-Type: text/html; charset=utf-8
req.is('html');
req.is('text/html');
req.is('text/*');
// => true
// When Content-Type is application/json
req.is('json');
req.is('application/json');
req.is('application/*');
// => true
req.is('html');
// => false
获取更多信息,或者如果你有问题或关注,可以参阅type-is。
过时的。可以在适合的情况下,使用
req.params
,req.body
或者req.query
。
返回当前参数name
的值。1
2
3
4
5
6
7
8
9
10// ?name=tobi
req.param('name')
// => "tobi"
// POST name=tobi
req.param('name')
// => "tobi"
// /user/tobi for /user/:name
req.param('name')
// => "tobi"
按下面给出的顺序查找:
可选的,你可以指定一个defaultValue
来设置一个默认值,如果这个参数在任何一个请求的对象中都不能找到。
直接通过
req.params
,req.body
,req.query
取得应该更加的清晰-除非你确定每一个对象的输入。Body-parser
中间件必须加载,如果你使用req.param()
。详细请看req.body。
res
对象代表了当一个HTTP请求到来时,Express
程序返回的HTTP响应。在本文档中,按照惯例,这个对象总是简称为res
(http请求简称为req
),但是它们实际的名字由这个回调方法在那里使用时的参数决定。
例如:1
2
3app.get('/user/:id', function(req, res) {
res.send('user' + req.params.id);
});
这样写也是一样的:1
2
3app.get('/user/:id', function(request, response) {
response.send('user' + request.params.id);
});
这个属性持有express
程序实例的一个引用,其可以在中间件中使用。res.app
和请求对象中的req.app
属性是相同的。
布尔类型的属性,指示这个响应是否已经发送HTTP头部。1
2
3
4
5app.get('/', function(req, res) {
console.log(res.headersSent); // false
res.send('OK'); // send之后就发送了头部
console.log(res.headersSent); // true
});
一个对象,其包含了响应的能够反应出请求的本地参数和因此只提供给视图渲染,在请求响应的周期内(如果有的话)–我要翻译吐了。否则,其和app.locals
是一样的。(不知道翻译的什么…)
这个参数在导出请求级别的信息是很有效的,这些信息比如请求路径,已认证的用户,用户设置等等。1
2
3
4
5app.use(function(req, res, next) {
res.locals.user = req.user;
res.locals.authenticated = !req.user.anonymous;
next();
});
res.append()方法在
Expresxs
4.11.0以上版本才支持。
在指定的field
的HTTP头部追加特殊的值value
。如果这个头部没有被设置,那么将用value
新建这个头部。value
可以是一个字符串或者数组。
注意:在res.append()
之后调用app.set()
函数将重置前面设置的值。1
2
3res.append('Lind', ['<http://localhost>', '<http://localhost:3000>']);
res.append('Set-Cookie', 'foo=bar;Path=/;HttpOnly');
res.append('Warning', '199 Miscellaneous warning');
设置HTTP响应的Content-Disposition
头内容为”attachment”。如果提供了filename
,那么将通过res.type()
获得扩展名来设置Content-Type
,并且设置Content-Disposition
内容为”filename=”parameter。1
2
3
4
5
6res.attachment();
// Content-Disposition: attachment
res.attachment('path/to/logo.png');
// Content-Disposition: attachment; filename="logo.png"
// Content-Type: image/png
设置name
和value
的cookie
,value
参数可以是一串字符或者是转化为json字符串的对象。
options是一个对象,其可以有下列的属性。
/
。HTTPS
协议使用。res.cookie()所作的都是基于提供的
options
参数来设置Set-Cookie
头部。没有指定任何的options
,那么默认值在RFC6265
中指定。
使用实例:1
2res.cookie('name', 'tobi', {'domain':'.example.com', 'path':'/admin', 'secure':true});
res.cookie('remenberme', '1', {'expires':new Date(Date.now() + 90000), 'httpOnly':true});
maxAge
是一个方便设置过期时间的方便的选项,其以当前时间开始的毫秒数来计算。下面的示例和上面的第二条功效一样。1
res.cookie('rememberme', '1', {'maxAge':90000}, "httpOnly":true);
你可以设置传递一个对象作为value
的参数。然后其将被序列化为Json字符串,被bodyParser()
中间件解析。1
2res.cookie('cart', {'items':[1, 2, 3]});
res.cookie('cart', {'items':[1, 2, 3]}, {'maxAge':90000});
当我们使用cookie-parser
中间件的时候,这个方法也支持签名的cookie。简单地,在设置options
时包含signed
选项为true
。然后res.cookie()
将使用传递给cookieParser(secret)
的密钥来签名这个值。1
res.cookie('name', 'tobi', {'signed':true});
根据指定的name
清除对应的cookie。更多关于options
对象可以查阅res.cookie()
。1
2res.cookie('name', 'tobi', {'path':'/admin'});
res.clearCookie('name', {'path':'admin'});
传输path
指定文件作为一个附件。通常,浏览器提示用户下载。默认情况下,Content-Disposition
头部”filename=”的参数为path
(通常会出现在浏览器的对话框中)。通过指定filename
参数来覆盖默认值。
当一个错误发生时或者传输完成,这个方法将调用fn
指定的回调方法。这个方法使用res.sendFile()
来传输文件。1
2
3
4
5
6
7
8
9
10
11
12res.download('/report-12345.pdf');
res.download('/report-12345.pdf', 'report.pdf');
res.download('report-12345.pdf', 'report.pdf', function(err) {
// Handle error, but keep in mind the response may be partially-sent
// so check res.headersSent
if (err) {
} else {
// decrement a download credit, etc.
}
});
结束本响应的过程。这个方法实际上来自Node
核心模块,具体的是response.end() method of http.ServerResponse。
用来快速结束请求,没有任何的数据。如果你需要发送数据,可以使用res.send()和res.json()这类的方法。1
2res.end();
res.status(404).end();
进行内容协商,根据请求的对象中Accept
HTTP头部指定的接受内容。它使用req.accepts()来选择一个句柄来为请求服务,这些句柄按质量值进行排序。如果这个头部没有指定,那么第一个方法默认被调用。当不匹配时,服务器将返回406
“Not Acceptable”,或者调用default
回调。Content-Type
请求头被设置,当一个回调方法被选择。然而你可以改变他,在这个方法中使用这些方法,比如res.set()
或者res.type()
。
下面的例子,将回复{"message":"hey"}
,当请求的对象中Accept
头部设置成”application/json”或者”/json”(不过如果是`/*`,然后这个回复就是”hey”)。1
2
3
4
5
6
7
8
9
10
11
12
13
14res.format({
'text/plain':function() {
res.send('hey')'
},
'text/html':function() {
res.send('<p>hey</p>');
},
'application/json':function() {
res.send({message:'hey'});
},
'default':function() {
res.status(406).send('Not Acceptable');
}
})
除了规范化的MIME类型之外,你也可以使用拓展名来映射这些类型来避免冗长的实现:1
2
3
4
5
6
7
8
9
10
11res.format({
text:function() {
res.send('hey');
},
html:function() {
res.send('<p>hey</p>');
},
json:function() {
res.send({message:'hey'});
}
})
返回field
指定的HTTP响应的头部。匹配是区分大小写。1
2res.get('Content-Type');
// => "text/plain"
发送一个json的响应。这个方法和将一个对象或者一个数组作为参数传递给res.send()
方法的效果相同。不过,你可以使用这个方法来转换其他的值到json,例如null
,undefined
。(虽然这些都是bv伟德登录入口上无效的JSON)。1
2
3res.json(null);
res.json({user:'tobi'});
res.status(500).json({error:'message'});
发送一个json的响应,并且支持JSONP。这个方法和res.json()
效果相同,除了其在选项中支持JSONP回调。1
2
3
4
5
6
7
8res.jsonp(null)
// => null
res.jsonp({user:'tobi'})
// => {"user" : "tobi"}
res.status(500).jsonp({error:'message'})
// => {"error" : "message"}
默认情况下,jsonp的回调方法简单写作callback
。可以通过jsonp callback name设置来重写它。
下面是一些例子使用JSONP响应,使用相同的代码:1
2
3
4
5
6
7
8
9// ?callback=foo
res.jsonp({user:'tobo'})
// => foo({"user":"tobi"})
app.set('jsonp callback name', 'cb')
// ?cb=foo
res.status(500).jsonp({error:'message'})
// => foo({"error":"message"})
连接这些links
,links
是以传入参数的属性形式提供,连接之后的内容用来填充响应的Link HTTP头部。1
2
3
4res.links({
next:'http://api.example.com/users?page=2',
last:'http://api.example.com/user?page=5'
});
效果:1
2Link:<http://api.example.com/users?page=2>;rel="next",
<http://api.example.com/users?page=5>;rel="last"
设置响应的Location
HTTP头部为指定的path
参数。1
2
3res.location('/foo/bar');
res.location('http://example.com');
res.location('back');
当path
参数为back
时,其具有特殊的意义,其指定URL为请求对象的Referer
头部指定的URL。如果请求中没有指定,那么其即为”/“。
Express传递指定的URL字符串作为回复给浏览器响应中的
Location
头部的值,不检测和操作,除了当是back
这个case时。浏览器有推导预期URL从当前的URL或者指定的URL,和在Location
指定的URL的责任;相应地重定向它。(我也不知道翻译的什么…)
重定向来源于指定path
的URL,以及指定的HTTP status codestatus
。如果你没有指定status
,status code默认为”302 Found”。1
2
3
4res.redirect('/foo/bar');
res.redirect('http://example.com');
res.redirect(301, 'http://example.com');
res.redirect('../login');
重定向也可以是完整的URL,来重定向到不同的站点。1
res.redirect('http://google.com');
重定向也可以相对于主机的根路径。比如,如果程序的路径为http://example.com/admin/post/new
,那么下面将重定向到http://example.com/admim
:1
res.redirect('/admin');
重定向也可以相对于当前的URL。比如,来之于http://example.com/blog/admin/
(注意结尾的/
),下面将重定向到http://example.com/blog/admin/post/new
。1
res.redirect('post/new');
如果来至于http://example.com/blog/admin
(没有尾部/
),重定向post/new
,将重定向到http://example.com/blog/post/new
。如果你觉得上面很混乱,可以把路径段认为目录(有’/‘)或者文件,这样是可以的。相对路径的重定向也是可以的。如果你当前的路径为http://example.com/admin/post/new
,下面的操作将重定向到http://example.com/admin/post
:1
res.redirect('..');
back
将重定向请求到referer,当没有referer
的时候,默认为/
。1
res.redirect('back');
渲染一个视图,然后将渲染得到的HTML文档发送给客户端。可选的参数为:
locals
,定义了视图本地参数属性的一个对象。callback
,一个回调方法。如果提供了这个参数,render
方法将返回错误和渲染之后的模板,并且不自动发送响应。当有错误发生时,可以在这个回调内部,调用next(err)
方法。本地变量缓存使能视图缓存。在开发环境中缓存视图,需要手动设置为true;视图缓存在生产环境中默认开启。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// send the rendered view to the client
res.render('index');
// if a callback is specified, the render HTML string has to be sent explicitly
res.render('index', function(err, html) {
res.send(html);
});
// pass a local variable to the view
res.render('user', {name:'Tobi'}, function(err, html) {
// ...
});
发送HTTP响应。body
参数可以是一个Buffer
对象,一个字符串,一个对象,或者一个数组。比如:1
2
3
4
5res.send(new Buffer('whoop'));
res.send({some:'json'});
res.send('<p>some html</p>');
res.status(404).send('Sorry, we cannot find that!');
res.status(500).send({ error: 'something blew up' });
对于一般的非流请求,这个方法可以执行许多有用的的任务:比如,它自动给Content-Length
HTTP响应头赋值(除非先前定义),也支持自动的HEAD和HTTP缓存更新。
当参数是一个Buffer
对象,这个方法设置Content-Type
响应头为application/octet-stream
,除非事先提供,如下所示:1
2res.set('Content-Type', 'text/html');
res.send(new Buffer('<p>some html</p>'));
当参数是一个字符串,这个方法设置Content-Type
响应头为text/html
:1
res.send('<p>some html</p>');
当参数是一个对象或者数组,Express使用JSON格式来表示:1
2res.send({user:'tobi'});
res.send([1, 2, 3]);
res.sendFile()
从Express v4.8.0
开始支持。
传输path
指定的文件。根据文件的扩展名设置Content-Type
HTTP头部。除非在options
中有关于root
的设置,path
一定是关于文件的绝对路径。
下面的表提供了options
参数的细节:
Last-Modified
头部为此文件在系统中的最后一次修改时间。设置false
来禁用它当传输完成或者发生了什么错误,这个方法调用fn
回调方法。如果这个回调参数指定了和一个错误发生,回调方法必须明确地通过结束请求-响应循环或者传递控制到下个路由来处理响应过程。
下面是使用了所有参数的使用res.sendFile()
的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25app.get('/file/:name', function(req, res, next) {
var options = {
root:__dirname + '/public',
dotfile:'deny',
headers:{
'x-timestamp':Date.now(),
'x-sent':true
}
};
var fileName = req.params.name;
res.sendFile(fileName, options, function(err) {
if (err) {
console.log(err);
res.status(err.status).end();
}
else {
console.log('sent', fileName);
}
});
});
res.sendFile
提供了文件服务的细粒度支持,如下例子说明:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15app.get('/user/:uid/photos/:file', function(req, res) {
var uid = req.params.uid
, file = req.params.file;
req.user.mayViewFilesFrom(uid, function(yes) {
if (yes) {
res.sendFile('/upload/' + uid + '/' + file);
}
else {
res.status(403).send('Sorry! you cant see that.');
}
});
})
获取更多信息,或者你有问题或者关注,可以查阅send。
设置响应对象的HTTP status code
为statusCode
并且发送statusCode
的相应的字符串形式作为响应的Body。1
2
3
4res.sendStatus(200); // equivalent to res.status(200).send('OK');
res.sendStatus(403); // equivalent to res.status(403).send('Forbidden');
res.sendStatus(404); // equivalent to res.status(404).send('Not Found');
res.sendStatus(500); // equivalent to res.status(500).send('Internal Server Error')
如果一个不支持的状态被指定,这个HTTP status依然被设置为statusCode
并且用这个code的字符串作为Body。1
res.sendStatus(2000); // equivalent to res.status(2000).send('2000');
设置响应对象的HTTP头部field
为value
。为了一次设置多个值,那么可以传递一个对象为参数。1
2
3
4
5
6
7res.set('Content-Type', 'text/plain');
res.set({
'Content-Type':'text/plain',
'Content-Length':'123',
'ETag':'123456'
})
其和res.header(field [,value])
效果一致。
使用这个方法来设置响应对象的HTTP status。其是Node中response.statusCode的一个连贯性的别名。1
2
3res.status(403).end();
res.status(400).send('Bad Request');
res.status(404).sendFile('/absolute/path/to/404.png');
设置Content-Type
HTTP头部为MIME type,如果这个指定的type能够被mime.lookup确定。如果type
包含/
字符,那么设置Content-Type
为type
(我已经晕了)。1
2
3
4
5res.type('.html'); // => 'text/html'
res.type('html'); // => 'text/html'
res.type('json'); // => 'application/json'
res.type('application/json'); // => 'application/json'
res.type('png'); // => image/png:
设置Vary
响应头为field
,如果已经不在那里。(不懂什么意思)1
res.vary('User-Agent').render('docs');
一个router
对象是一个单独的实例关于中间件和路由。你可以认为其是一个”mini-application”(迷你程序),其具有操作中间件和路由方法的能力。每个Express
程序有一个内建的app路由。
路由自身表现为一个中间件,所以你可以使用它作为app.use()
方法的一个参数或者作为另一个路由的use()
的参数。
顶层的express
对象有一个Router()
方法,你可以使用Router()
来创建一个新的router
对象。
如下,可以创建一个路由:1
var router = express.Router([options]);
options
参数可以指定路由的行为,其有下列选择:
/Foo
和/foo
一样。res.params
。如果父路由参数和子路由参数冲突,子路由参数优先。/foo
和/foo/
被路由一样对待处理你可以将router
当作一个程序,可以在其上添加中间件和HTTP路由方法(例如get
,put
,post
等等)。1
2
3
4
5
6
7
8
9
10
11// invoked for any requests passed to this router
router.use(function(req, res, next) {
// .. some logic here .. like any other middleware
next();
});
// will handle any request that ends in /events
// depends on where the router is "use()'d"
router.get('/events', function(req, res, next) {
// ..
});
你可以在一个特别的根URL上挂载一个路由,这样你就以将你的各个路由放到不同的文件中或者甚至是mini的程序。1
2// only requests to /calendar/* will be sent to our "router"
app.use('/calendar', router);
这个方法和router.METHOD()
方法一样,除了这个方法会匹配所有的HTTP动作。
这个方法对想映射全局的逻辑处理到特殊的路径前缀或者任意匹配是十分有用的。比如,如果你放置下面所示的这个路由在其他路由的前面,那么其将要求从这个点开始的所有的路由进行验证操作和自动加载用户信息。记住,这些全局的逻辑操作,不需要结束请求响应周期:loaduser
可以执行一个任务,然后调用next()
来将执行流程移交到随后的路由。1
router.all('*', requireAuthentication, loadUser);
相等的形式:1
2router.all('*', requireAuthentication)
router.all('*', loadUser);
这是一个白名单全局功能的例子。这个例子很像前面的,不过其仅仅作用于以/api
开头的路径:1
router.all('/api/*', requireAuthentication);
router.METHOD()
方法提供了路由方法在Express
中,这里的METHOD
是HTTP方法中的一个,比如GET
,PUT
,POST
等等,但router
中的METHOD是小写的。所以,实际的方法是router.get()
,router.put()
,router.post()
等等。
你可以提供多个回调函数,它们的行为和中间件一样,除了这些回调可以通过调用next('router')
来绕过剩余的路由回调。你可以使用这个机制来为一个路由设置一些前提条件,如果请求没有满足当前路由的处理条件,那么传递控制到随后的路由。
下面的片段可能说明了最简单的路由定义。Experss转换path字符串为正则表达式,用于内部匹配传入的请求。在匹配的时候,是不考虑Query strings
,例如,”GET /“将匹配下面的路由,”GET /?name=tobi”也是一样的。1
2
3router.get('/', function(req, res) {
res.send('Hello World');
});
如果你对匹配的path有特殊的限制,你可以使用正则表达式,例如,下面的可以匹配”GET /commits/71dbb9c”和”GET /commits/71bb92..4c084f9”。1
2
3
4
5router.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res) {
var from = req.params[0];
var to = req.params[1];
res.send('commit range ' + from + '..' + to);
});
给路由参数添加回调触发器,这里的name
是参数名,function
是回调方法。回调方法的参数按序是请求对象,响应对象,下个中间件,参数值和参数名。虽然name
在bv伟德登录入口上是可选的,但是自Express V4.11.0之后版本不推荐使用(见下面)。
不像
app.param()
,router.param()
不接受一个数组作为路由参数。
例如,当:user
出现在路由路径中,你可以映射用户加载的逻辑处理来自动提供req.user
给这个路由,或者对输入的参数进行验证。1
2
3
4
5
6
7
8
9
10
11
12router.param('user', function(req, res, next, id) {
User.find(id, function(error, user) {
if (err) {
next(err);
}
else if (user){
req.user = user;
} else {
next(new Error('failed to load user'));
}
});
});
对于Param
的回调定义的路由来说,他们是局部的。它们不会被挂载的app或者路由继承。所以,定义在router
上的param
回调只有是在router
上的路由具有这个路由参数时才起作用。
在定义param
的路由上,param
回调都是第一个被调用的,它们在一个请求-响应循环中都会被调用一次并且只有一次,即使多个路由都匹配,如下面的例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14router.param('id', function(req, res, next, id) {
console.log('CALLED ONLY ONCE');
next();
});
router.get('/user/:id', function(req, res, next) {
console.log('although this matches');
next();
});
router.get('/user/:id', function(req, res) {
console.log('and this mathces too');
res.end();
});
当GET /user/42
,得到下面的结果:1
CALLED ONLY ONCE
although this matches
and this matches too
下面章节描述的
router.param(callback)
在v4.11.0之后被弃用。
通过只传递一个回调参数给router.param(name, callback)
方法,router.param(naem, callback)
方法的行为将被完全改变。这个回调参数是关于router.param(name, callback)
该具有怎样的行为的一个自定义方法,这个方法必须接受两个参数并且返回一个中间件。
这个回调的第一个参数就是需要捕获的url的参数名,第二个参数可以是任一的JavaScript对象,其可能在实现返回一个中间件时被使用。
这个回调方法返回的中间件决定了当URL中包含这个参数时所采取的行为。
在下面的例子中,router.param(name, callback)
参数签名被修改成了router.param(name, accessId)
。替换接受一个参数名和回调,router.param()
现在接受一个参数名和一个数字。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
26var express = require('express');
var app = express();
var router = express.Router();
router.param(function(param, option){
return function(req, res, next, val) {
if (val == option) {
next();
}
else {
res.sendStatus(403);
}
}
});
router.param('id', 1337);
router.get('/user/:id', function(req, res) {
res.send('Ok');
});
app.use(router);
app.listen(3000, function() {
console.log('Ready');
});
在这个例子中,router.param(name. callback)
参数签名保持和原来一样,但是替换成了一个中间件,定义了一个自定义的数据类型检测方法来检测user id
的类型正确性。1
2
3
4
5
6
7
8
9
10
11
12
13
14router.param(function(param, validator) {
return function(req, res, next, val) {
if (validator(val)) {
next();
}
else {
res.sendStatus(403);
}
}
});
router.param('id', function(candidate) {
return !isNaN(parseFloat(candidate)) && isFinite(candidate);
});
返回一个单例模式的路由的实例,之后你可以在其上施加各种HTTP动作的中间件。使用app.route()
来避免重复路由名字(因此错字错误)–后面这句不知道说的什么鬼,大概的意思就是避免同一个路径有两个路由实例。
构建在上面的router.param()
例子之上,下面的代码展示了怎么使用router.route()
来指定各种HTTP方法的处理句柄。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32var router = express.Router();
router.param('user_id', function(req, res, next, id) {
// sample user, would actually fetch from DB, etc...
req.user = {
id:id,
name:"TJ"
};
next();
});
router.route('/users/:user_id')
.all(function(req, res, next) {
// runs for all HTTP verbs first
// think of it as route specific middleware!
next();
})
.get(function(req, res, next) {
res.json(req.user);
})
.put(function(req, res, next) {
// just an example of maybe updating the user
req.user.name = req.params.name;
// save user ... etc
res.json(req.user);
})
.post(function(req, res, next) {
next(new Error('not implemented'));
})
.delete(function(req, res, next) {
next(new Error('not implemented'));
})
这种方法重复使用单个/usrs/:user_id
路径来添加了各种的HTTP方法。
给可选的path
参数指定的路径挂载给定的中间件方法,未指定path
参数,默认值为/
。
这个方法类似于app.use()
。一个简单的例子和用例在下面描述。查阅app.use()获得更多的信息。
中间件就像一个水暖管道,请求在你定义的第一个中间件处开始,顺着中间件堆栈一路往下,如果路径匹配则处理这个请求。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
27var express = require('express');
var app = express();
var router = express.Router();
// simple logger for this router`s requests
// all requests to this router will first hit this middleware
router.use(function(req, res, next) {
console.log('%s %s %s', req.method, req.url, req.path);
next();
})
// this will only be invoked if the path starts with /bar form the mount ponit
router.use('/bar', function(req, res, next) {
// ... maybe some additional /bar logging ...
next();
})
// always be invoked
router.use(function(req, res, next) {
res.send('hello world');
})
app.use('/foo', router);
app.listen(3000);
对于中间件function
,挂载的路径是被剥离的和不可见的。关于这个特性主要的影响是对于不同的路径,挂载相同的中间件可能对代码不做改动,尽管其前缀已经改变。
你使用router.use()
定义中间件的顺序很重要。中间们是按序被调用的,所以顺序决定了中间件的优先级。例如,通常日志是你将使用的第一个中间件,以便每一个请求都被记录。1
2
3
4
5
6
7var logger = require('morgan');
router.use(logger());
router.use(express.static(__dirname + '/public'));
router.use(function(req, res) {
res.send('Hello');
});
现在为了支持你不希望记录静态文件请求,但为了继续记录那些定义在logger()
之后的路由和中间件。你可以简单的将static()
移动到前面来解决:1
2
3
4
5router.use(express.static(__dirname + '/public'));
router.use(logger());
router.use(function(req, res){
res.send('Hello');
});
另外一个确凿的例子是从不同的路径托管静态文件,你可以将./public
放到前面来获得更高的优先级:1
2
3app.use(express.static(__dirname + '/public'));
app.use(express.static(__dirname + '/files'));
app.use(express.static(__dirname + '/uploads'));
router.use()
方法也支持命名参数,以便你的挂载点对于其他的路由而言,可以使用命名参数来进行预加载,这样做是很有益的。
express()
用来创建一个Express的程序。express()
为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的bv伟德登录入口点,包括但不限于前端bv伟德登录入口。
当然可以, H5 火了这么久,涵盖了各个方面的bv伟德登录入口。
对于视频录制,可以使用强大的 webRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的bv伟德登录入口,缺点是只在 PC 的 chrome 上支持较好,移动端支持不太理想。
对于视频播放,可以使用 HLS(HTTP Live Streaming)协议播放直播流, ios 和 android 都天然支持这种协议,配置简单,直接使用 video 标签即可。
webRTC 兼容性:
video 标签播放 hls 协议视频:
<video controls autoplay>
<source src="http://10.66.69.77:8080/hls/mystream.m3u8" type="application/vnd.apple.mpegurl" />
<p class="warning">Your browser does not support HTML5 video.</p>
</video>
简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。
每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。
.m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件:
1
2
3
4
5
6
7
#EXTM3U m3u文件头
#EXT-X-MEDIA-SEQUENCE 第一个TS分片的序列号
#EXT-X-TARGETDURATION 每个分片TS的最大的时长
#EXT-X-ALLOW-CACHE 是否允许cache
#EXT-X-ENDLIST m3u8文件结束符
#EXTINF 指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效
mystream-12.ts
ts 文件:
HLS 的请求流程是:
1 http 请求 m3u8 的 url。
2 服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。
3 客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。
简单流程:
我们知道 hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟。参考资料:https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/FrequentlyAskedQuestions/FrequentlyAskedQuestions.html
当视频直播可大致分为:
1 视频录制端:一般是电脑上的音视频输入设备或者手机端的摄像头或者麦克风,目前以移动端的手机视频为主。
2 视频播放端:可以是电脑上的播放器,手机端的 native 播放器,还有就是 h5 的 video 标签等,目前还是已手机端的 native 播放器为主。
3 视频服务器端:一般是一台 nginx 服务器,用来接受视频录制端提供的视频源,同时提供给视频播放端流服务。
简单流程:
当首先明确几个概念:
视频编码: 所谓视频编码就是指通过特定的压缩bv伟德登录入口,将某个视频格式的文件转换成另一种视频格式文件的方式,我们使用的 iphone 录制的视频,必须要经过编码,上传,解码,才能真正的在用户端的播放器里播放。
编解码标准: 视频流传输中最为重要的编解码标准有国际电联的H.261、H.263、H.264,其中 HLS 协议支持 H.264 格式的编码。
音频编码: 同视频编码类似,将原始的音频流按照一定的标准进行编码,上传,解码,同时在播放器里播放,当然音频也有许多编码标准,例如 PCM 编码,WMA 编码,AAC 编码等等,这里我们 HLS 协议支持的音频编码方式是AAC编码。
下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤:
1 音视频的采集,ios 中,利用 AVCaptureSession和AVCaptureDevice 可以采集到原始的音视频数据流。
2 对视频进行 H264 编码,对音频进行 AAC 编码,在 ios 中分别有已经封装好的编码库来实现对音视频的编码。
3 对编码后的音、视频数据进行组装封包;
4 建立 RTMP 连接并上推到服务端。
ps:由于编码库大多使用 c 语言编写,需要自己使用时编译,对于 ios,可以使用已经编译好的编码库。
x264编码: https://github.com/kewlbear/x264-ios
faac编码: https://github.com/fflydev/faac-ios-build
ffmpeg编码: https://github.com/kewlbear/FFmpeg-iOS-build-script
关于如果想给视频增加一些特殊效果,例如增加滤镜等,一般在编码前给使用滤镜库,但是这样也会造成一些耗时,导致上传视频数据有一定延时。
简单流程:
和之前的 x264 一样,ffmpeg 其实也是一套编码库,类似的还有 Xvid,Xvid 是基于 MPEG4 协议的编解码器,x264是基于 H.264 协议的编码器, ffmpeg 集合了各种音频,视频编解码协议,通过设置参数可以完成基于 MPEG4,H.264 等协议的编解码,demo 这里使用的是 x264 编码库。
Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议,现在属于 Adobe。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。
这里列举一下 hls 和 rtmp 对比:
简所谓推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的,https://github.com/runner365/LiveVideoCoreSDK
简简单的推流服务器搭建,由于我们上传的视频流都是基于 rtmp 协议的,所以服务器也必须要支持 rtmp 才行,大概需要以下几个步骤:
1 安装一台 nginx 服务器。
2 安装 nginx 的 rtmp 扩展,目前使用比较多的是https://github.com/arut/nginx-rtmp-module
3 配置 nginx 的 conf 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rtmp {
server {
listen 1935; #监听的端口
chunk_size 4000;
application hls { #rtmp推流请求路径
live on;
hls on;
hls_path /usr/local/var/www/hls;
hls_fragment 5s;
}
}
}
4 重启 nginx,将 rtmp 的推流地址写为 rtmp://ip:1935/hls/mystream,其中 hls_path 表示生成的 .m3u8 和 ts 文件所存放的地址,hls_fragment 表示切片时长,mysteam 表示一个实例,即将来要生成的文件名可以先自己随便设置一个。更多配置可以参考:https://github.com/arut/nginx-rtmp-module/wiki/
根据以上步骤基本上已经实现了一个支持 rtmp 的视频服务器了。
简单来说,直接使用 video 标签即可播放 hls 协议的直播视频:
1
2
3
4
<video autoplay webkit-playsinline>
<source src="http://10.66.69.77:8080/hls/mystream.m3u8" type="application/vnd.apple.mpegurl" />
<p class="warning">Your browser does not support HTML5 video.</p>
</video>
需要注意的是,给 video 标签增加 webkit-playsinline 属性,这个属性是为了让 video 视频在 ios 的 uiwebview 里面可以不全屏播放,默认 ios 会全屏播放视频,需要给 uiwebview 设置 allowsInlineMediaPlayback=YES。 业界比较成熟的 videojs,可以根据不同平台选择不同的策略,例如 ios 使用 video 标签,pc 使用 flash 等。
简根据以上步骤,笔者写了一个 demo,从实现 ios 视频录制,采集,上传,nginx 服务器下发直播流,h5 页面播放直播视频者一整套流程,总结出以下几点比较坑的地方:
1 在使用 AVCaptureSession 进行采集视频时,需要实现 AVCaptureVideoDataOutputSampleBufferDelegate 协议,同时在- (void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection 捕获到视频流,要注意的是 didOutputSampleBuffer 这个方法不是 didDropSampleBuffer 方法,后者只会触发一次,当时开始写的是 didDropSampleBuffer 方法,差了半天才发现方法调用错了。
2 在使用 rtmp 推流时,rmtp 地址要以 rtmp:// 开头,ip 地址要写实际 ip 地址,不要写成 localhost,同时要加上端口号,因为手机端上传时是无法识别 localhos t的。
这里后续会补充上一些坑点,有的需要贴代码,这里先列这么多。
目前,腾讯云,百度云,阿里云都已经有了基于视频直播的解决方案,从视频录制到视频播放,推流,都有一系列的 sdk 可以使用,缺点就是需要收费,如果可以的话,自己实现一套也并不是难事哈。
demo地址:https://github.com/lvming6816077/LMVideoTest/
]]>为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的bv伟德登录入口点,包括但不限于前端bv伟德登录入口。
当然可以, H]]>