Phaoer


  • 首页

  • 标签

  • 分类

NGINX之HTTPS双向认证

发表于 2020-11-16 | 分类于 后端 , 网络协议

前言

大部分HTTPS的站点都只是单向认证,即只有客户端校验服务端。那么一种更安全的做法则是双向认证,即客户端服务端互相验证。
网上介绍https双向认证原理的帖子很多,就不做赘述!

重要

是否需要双向认证是服务端决定的

双向认证的客户端证书和服务端ssl证书没有关系

自签CA

首先ssl证书是受CA信任的三方机构颁发,并预装到主流浏览器中的,网站拥有合法的ssl证书可以规避浏览器的不安全警告,数据传输更安全。

但这里我们只探讨器原理,所以不去纠结ssl证书是否合法

创建根证书私钥:

1
openssl genrsa -out root.key 1024

创建根证书请求文件:

1
openssl req -new -out root.csr -key root.key

这里会让你填一些东西,随便填就行,我举个例子:
Country Name (2 letter code) [XX]:cn
State or Province Name (full name) []:sc
Locality Name (eg, city) [Default City]:cd
Organization Name (eg, company) [Default Company Ltd]:phaoer
Organizational Unit Name (eg, section) []:phaoer
Common Name (eg, your name or your servers hostname) []:root
Email Address []:
A challenge password []:
An optional company name []:

注意:在生成服务端和客户端请求文件的时候也会要求填写这些信息,Common Name填写域名即可

创建根证书(有效期10年):

1
openssl x509 -req -in root.csr -out root.crt -signkey root.key -CAcreateserial -days 3650

服务端证书

如果你的网站目前已经拥有合法的ssl证书,略过这一步即可。

生成服务端证书私钥:

1
openssl genrsa -out server.key 1024

生成服务端证书请求文件:

1
openssl req -new -out server.csr -key server.key

生成服务端公钥证书:

1
openssl x509 -req -in server.csr -out server.crt -signkey server.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650

客户端证书

生成客户端证书私钥:

1
openssl genrsa -out client.key 1024

生成客户端证书请求文件(各个参数需要和server相同):

1
openssl req -new -out client.csr -key client.key

生成客户端公钥证书:

1
openssl x509 -req -in client.csr -out client.crt -signkey client.key -CA root.crt -CAkey root.key -CAcreateserial -days 3650

nginx配置

1
2
3
4
5
6
......
ssl_certificate /你的证书路径/server.crt;
ssl_certificate_key /你的证书路径/server.key;
ssl_client_certificate /你的证书路径/root.crt;
ssl_verify_client on;
......

客户端证书提供给终端使用

浏览器

生成一个p12文件供终端安装

1
openssl pkcs12 -export -clcerts -in client.crt -inkey client.key -out client.p12

curl

curl双向认证请求需要将客户端证书和私钥转换成pem格式

crt转pem

1
2
openssl x509 -in client.crt -out client.der -outform der
openssl x509 -in client.der -inform der -outform pem -out client.pem

key转pem

1
2
openssl rsa -in client.key -out client.der -outform DER
openssl rsa -inform DER -outform PEM -in client.der -out clientkey.pem

reactHooks实现一个简易redux

发表于 2020-11-01 | 分类于 前端 , javascript , react

实现redux

顶层

  • createContext创建共享的上下文
  • useReducer使用类同redux
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React, {createContext, useContext, useReducer} from 'react';

const _STATE = createContext();
const _DISPATCH = createContext();

function App (){
const initStatus = {
count: 0
};

const reducer = (state, action) => {
switch (action.type) {
case 'add':
return {
...state, count: state.count + 1
};
case 'reduce':
return {
...state, count: state.count - 1
};
default:
throw 'Error';
}
}

const [state, dispatch] = useReducer(reducer, initStatus); //传入一个reducer函数和初始值

return <React.Fragment>
<_STATE.Provider value={state}>
<_DISPATCH.Provider value={dispatch}>
<Child />
</_DISPATCH.Provider>
</_STATE.Provider>
</React.Fragment>
}
......

需要使用状态的function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
......
const creator = (type, payload, error) => {
return {
type: type,
payload,
error
}
}
function Child(){
const state = useContext(_STATE); //获取共享的上下文
const dispatch = useContext(_DISPATCH);

return <React.Fragment>
<div>{state.count}</div>
<button onClick = {() => dispatch(creator("add"))}>增加</button>
<button onClick = {() => dispatch(creator("reduce"))}>减少</button>
</React.Fragment>
}
......

顺带提一下useEffect

1
2
3
4
useEffect(() => {
//do something
}, []);
//因为hooks函数每一次的render产生的函数和变量都是不一样的,所以当useEffect依赖某个变量时需要在第二个参数传入

如何快速构建一个兼容IE9的react应用

发表于 2020-10-02 | 分类于 前端 , javascript , react

前言

按理说都2020了不应该再去谈react的构建这些东西,但是仍有人向我询问这些东西,所以这里细说一下如何最快速的构建一个兼容IE9的react应用

create-react-app

create-react-app是一个官方支持的创建 React 单页应用程序的方法。它提供了一个零配置的现代构建设置。

使用

npx或yarn create

1
2
npx create-react-app demo
yarn create react-app demo

安装兼容IE的依赖

1
yarn add react-app-polyfill core-js

为何是react-app-polyfill和core-js

关于react-app-polyfill的解释可以自行参看官方文档。
至于core-js的引用是因为React 16 依赖集合类型 Map 和 Set ,需要为IE < 11这一类的旧浏览器提供一个全局的 polyfill,core-js只是一个选择,你可以使用其他的polyfill。

shim和polyfill?

首先你需要知道的是这两者都是为了兼容而生,而polyfill可以理解为一个shim库。
两者的区别在于,shim的实现方式是产生一个新的api去实现兼容,而polyfill则是使用符合当前浏览器规范的语法去实现旧的api。

配置

index.js

找到 /你的项目名/src/index.js

在首行添加

1
2
3
4
5
//index.js
import 'core-js/es';
import 'react-app-polyfill/ie9';
import 'react-app-polyfill/stable';
......

package.json

找到 /你的项目名/package.json

为browserslist添加IE配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//package.json
{
......
"browserslist": {
"production": [
......
"ie >= 9"
],
"development": [
......
"ie >= 9"
]
}
}

至此,一个兼容IE9的react应用就告成了,当然create-react-app还有很多实用的功能,自行参考官网即可。

react-native App更新方案

发表于 2020-08-12 | 分类于 前端 , APP , Hybrid , Android

前言

用react-native(一下简称RN)开发的app的更新方案有很多,其中比较火的是热更新方案,有官方推荐的pushy和微软的code-push
文档很详细,接入也比较简单
这里主要介绍一种最传统的更新方案,也是很多原生开发在使用的方案——全量更新

全量更新

顾名思义,即每次更新通过http去下载新版本包去然后去做一个覆盖安装,这种做法在更新迭代中会避免很多不必要的麻烦,而且在这个5G都要到来的时代,网络资源大小的限制也显得不那么重要。

步骤

获取当前APP版本号

react-native是不提供获取版本号模块的,所以我们需要自己去写一个原生模块去获取当前app的版本号。
传送门:原生模块

新建一个模块文件夹

新建一个文件夹在android/app/src/main/java/com/your-app-name/下,这里我取名叫appinfo

新建一个类

在appinfo新建一个类为AppInfoModule.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.your-app-name.appinfo;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

import com.facebook.react.uimanager.IllegalViewOperationException;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;

import java.util.Map;
import java.util.HashMap;

public class AppInfoModule extends ReactContextBaseJavaModule {

public AppInfoModule(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return "AppInfo"; //后面需要调用的模块名
}

@ReactMethod
public void getVersion(Callback successCallback) { //后面需要调用的方法名,不需要传参
try {
PackageInfo info = getPackageInfo();
if(info != null){
successCallback.invoke(info.versionName); //这里我是一回调函数的形式返回,也可以用promise的方式
} else {
successCallback.invoke("");
}
} catch (IllegalViewOperationException e){
successCallback.invoke("");
}
}

private PackageInfo getPackageInfo(){
PackageManager manager = getReactApplicationContext().getPackageManager();
PackageInfo info = null;
try{
info = manager.getPackageInfo(getReactApplicationContext().getPackageName(),0);
return info;
} catch (Exception e){
e.printStackTrace();
} finally {
return info;
}
}
}

注册模块

用于在JavaScript 中访问,新建一个AppInfoPackage.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.your-app-name.appinfo;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AppInfoPackage implements ReactPackage {

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}

@Override
public List<NativeModule> createNativeModules(
ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new AppInfoModule(reactContext)); //你的模块
return modules;
}

}

暴露方法

暴露该模块,修改android/app/src/main/java/com/your-app-name/MainApplication.java文件的getPackages方法

1
2
3
4
5
6
7
8
9
10
import com.your-app-name.appinfo.AppInfoPackage;
......
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
packages.add(new AppInfoPackage()); //导入注册好的模块
return packages;
}
......

JavaScript调用

1
2
3
4
NativeModules.AppInfo.getVersion((version) => {
console.log(version);
//output 1.1.0
});

版本校验

写一个版本比较接口,把获取到的版本号提交上去,以此来判断是否需要更新

提供一个php的版本比较方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private function compareVersion($v1,$v2) 
{
if(empty($v2)){
return false;
}
$l1 = explode('.',$v1);
$l2 = explode('.',$v2);
$len = count($l1) > count($l2) ? count($l1): count($l2);
for ($i = 0; $i < $len; $i++) {
$n1 = $l1[$i] ? $l1[$i] : 0;
$n2 = $l2[$i] ? $l2[$i] : 0;
if ($n1 > $n2) {
return true;
} else if ($n1 < $n2) {
return false;
}
}
return false;
}

下载新版本

版本校验需要更新,得到更新的地址后去下载保存到本机

获取读取存储的权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
......
(async () => {
try {
const permissions = [
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
];

const granteds = await PermissionsAndroid.requestMultiple(permissions);
if (granteds["android.permission.ACCESS_FINE_LOCATION"] === "granted" && granteds["android.permission.READ_EXTERNAL_STORAGE"] === "granted" && granteds["android.permission.WRITE_EXTERNAL_STORAGE"] === "granted") {
} else {
ToastAndroid.show('授权被拒绝', ToastAndroid.LONG)
}
} catch (err) {
ToastAndroid.show(err.toString(), ToastAndroid.LONG);
}
})();

react-native-fs下载

接入文档react-native-fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import RNFS from 'react-native-fs';

const downloadDest = `${RNFS.DownloadDirectoryPath}/app.apk`;

const options = {
fromUrl: UPDATEURL, //更新包的地址
toFile: downloadDest, //下载后保存的地址
background: true,
begin: (res) => {
}
};
const ret = RNFS.downloadFile(options);
ret.promise.then(res => {
// 下载成功
}).catch(err => {
ToastAndroid.show(err, ToastAndroid.LONG);
});

安装

下载成功后,考虑到用户体验,还得有一个自动安装吧~
当然去打开指定路径的文件也是需要我们自己写原生模块的。

按照上面的流程,我新建了update模块和UpdateModule.java类
这里流程很上面的获取版本号一致,我直接贴UpdateModule.java的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package com.your-app-name.update;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;

import android.content.Intent;
import android.net.Uri;
import android.os.Build;
//使用FileProvider需要一定的配置
import androidx.core.content.FileProvider;

import java.io.File;
import java.util.Map;
import java.util.HashMap;

public class UpdateModule extends ReactContextBaseJavaModule {
private ReactApplicationContext context;
public UpdateModule(ReactApplicationContext reactContext) {
super(reactContext);
context = reactContext;
}

@Override
public String getName() {
return "Update";
}

@ReactMethod
public void installApp(final String path, Callback successCallback) {
try {
if (Build.VERSION.SDK_INT >= 24) { //android 7.0以后,处于安全考虑必须使用FileProvider打开文件
Uri contentUri = FileProvider.getUriForFile(context, "com.your-app-name.fileprovider", new File(path));

Intent intent = new Intent(Intent.ACTION_VIEW)
.setDataAndType(contentUri, "application/vnd.android.package-archive");

intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

context.startActivity(intent);
} else {
Intent intent = new Intent(Intent.ACTION_VIEW).setDataAndType(Uri.parse("file://" + path), "application/vnd.android.package-archive").setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
successCallback.invoke("安装成功");
} catch (Exception e) {
successCallback.invoke("安装失败");
}
}
}

更新下逻辑代码,下载成功后自动安装

1
2
3
4
5
6
7
8
9
10
......
ret.promise.then(res => {
// 下载成功
NativeModules.Update.installApp(downloadDest, (res) => {
// 安装成功
});
}).catch(err => {
ToastAndroid.show(err, ToastAndroid.LONG);
});
......

路由器之UPnP协议

发表于 2020-08-10 | 分类于 后端 , 网络协议

前言

通用即插即用(英语:Universal Plug and Play,简称UPnP)是由“通用即插即用论坛”(UPnP™ Forum)推广的一套网络协议。该协议的目标是使家庭网络(数据共享、通信和娱乐)和公司网络中的各种设备能够相互无缝连接,并简化相关网络的实现。UPnP通过定义和发布基于开放、因特网通讯网协议标准的UPnP设备控制协议来实现这一目标。
————百度百科

实例

目前市面上大部分路由器都支持UPnP协议,那么我们在开发关于路由器的项目时,都会去获取路由器的一些相关信息,这时候UPnP协议就派上用场了。

关于UPnP的原理解析和介绍,网络上的文章太多了,这里就不再赘述。
直接上干货

  • 1.使用udp多播向239.255.255.250:1900发送一条ssdp搜索报文

    1
    M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN:"ssdp:discover"\r\nMX:5\r\nST: upnp:rootdevice\r\n
  • 2.监听239.255.255.250:1900,如果网络存在一个UPnP设备的话,设备必须发送响应信息

  • 3.解析响应消息,获得LOCATION的值,会得到一个xml的地址,即根设备(路由器)的描述信息地址

代码示例

node

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
const socket = dgram.createSocket('udp4');

socket.on('error', (err) => {
socket.close();
});

socket.bind(1900, () => {
socket.setMulticastTTL(128);
socket.addMembership('239.255.255.250');
socket.setMulticastLoopback(true);

const msg = toByteArray('M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN:"ssdp:discover"\r\nMX:5\r\nST: upnp:rootdevice\r\n');

socket.send(msg, 0, msg.length, 1900, '239.255.255.250', function (error, bytes) {
});
});

socket.on('message', function(data, rinfo) {
socket.close();

var str = String.fromCharCode.apply(null, new Uint8Array(data));
const ClearBr = (key) => {
key = key.replace(/<\/?.+?>/g,"");
key = key.replace(/[\r\n]/g, "");
return key;
}

const xmlAddress = ClearBr(str).match(/LOCATION: (\S*).xml/)[1] + ".xml";

fetch(xmlAddress).then(res => {
return res.text();
}).then(res => {
console.log(res);
const modelName = res.match(/<modelName>(\S*)<\/modelName>/); //路由器型号
});
});

function toByteArray (obj){
var uint = new Uint8Array(obj.length);
for (var i = 0, l = obj.length; i < l; i++){
uint[i] = obj.charCodeAt(i);
}
return new Uint8Array(uint);
}

异步解决方案async & await 及在Gulp和WebPack中的使用

发表于 2020-07-28 | 分类于 前端 , node , 构建工具

前言

  • async & await 是目前为止最好的异步解决方案
  • 从回调函数的形式 =》es6提出的promise =》es8提出的async & await
  • async & await将异步处理做到了极致 => 用同步代码的方式来处理异步

举例

下面我们来简单的看一个示例:存在异步请求A和B,当A的返回值为success执行B

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
var A = () => {
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve("success");
},3000);
})
}

var B = () => {
return new Promise(function(resolve, reject){
setTimeout(function(){
resolve("done");
//reject("error");
},1000);
})
}

//Promise + then
A().then(res => {
if (res == "success") {
return B();
}else{
console.log("error");
}
})
.then(res => {
console.log(res);
})
.catch(error => {
console.log(error);
});

//async
(async () => {
try {
const a = await A();
if (a == "success") {
const b = await B();
console.log(b);
}else{
console.log("error");
}
} catch (error) {
console.log(error);
}
})();

从上面的示例可以看到async & await的优势:

  • 处理我们常遇到的条件判别和中间值显得异常清晰
  • 处理逻辑不需要再新建匿名函数,大大减少代码量,结构清晰
  • 错误捕获可以使用try/catch结构轻松捕获

好处说完了,但是因为大佬是es8的新特性,浏览器还没支持。因此下面会介绍怎么在最常用的两种构建工具(gulp和webpack)中配合babel来真正的在我们的生产环境使用它

在构建工具中的使用

webpack篇

需要用到的包

1
2
npm i --save-dev webpack webpack-cli babel-loader babel-core babel-preset-env babel-plugin-transform-runtime
npm i --save babel-runtime

webpack遵循commonJs规范,所以我们完全不需要去单独处理各模块的依赖。
在这个模块化的年代,或许这也是我更钟爱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
// webpack.config.js
module.exports = {
entry:{
main: __dirname + "/index.js"
},
output:{
path: __dirname + "/dist",
filename: "index.bundle.js"
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
}
]
}
}

.babelrc

1
2
3
4
5
6
7
//babel 配置
{
"plugins": ["transform-runtime"], //注意plugin 优先级高于presets
"presets": [
["env"]
]
}

Gulp篇

正确姿势

这是gulp + browserify正确使用方式,如若你是gulp新手请参照下面的简单版本
这里先讲述正确使用方法

你需要用到的包:

1
2
npm i --save-dev gulp gulp-uglify gulp-buffer vinyl-source-stream babelify babel-core babel-preset-env babel-plugin-transform-runtime browserify
npm i --save babel-runtime

这里babel-runtime相当于是我们的polyfill,我们使用transform-runtime来动态的去引入需要的部分,避免污染

很多人会疑问为什么会使用到browserify?下面敲重点:

  • 这得从babel谈起,通俗点说,babel可以将浏览器还没支持的es的新写法编译为浏览器支持的语法。
  • 而es中新增的api则不会被转换,这时候我们就得引入polyfill解决。
  • 而babel转换的代码其实是遵循commonJs规范的,浏览器环境显然不支持,因此再用browserify处理各模块间的关系即可。

然后又有人问了gulp-buffer vinyl-source-stream是什么鬼?下面还是重点:
这里我只做基本的介绍,这属于gulp的高级技巧,想刨根问底的童鞋请先了解以下知识

  • buffer
  • stream(流)
  • gulp

​下面是简单介绍​:

  • 首先,gulp使用了vinyl-fs,而vinyl-fs使用的核心是vinyl,vinyl 可以创建一个文件描述对象,通过接口可以取得该文件所对应的数据(Buffer类型)、cwd路径、文件名等等。
  • 因此按照我们入乡随俗的道理,要想使用gulp是不是得把可读流转换为vinyl file object?那么vinyl-source-stream(可以将普通流转换为vinyl file object stream),vinyl-source-buffer(可以将普通流转换为vinyl file object buffer)它们就来了,按需使用即可。
  • vinyl file object拥有一个属性来表示里面是buffer还是流,gulp默认使用buffer。像一些gulp的插件也是基于buffer的实现(比如gulp-uglify),因此gulp-buffer它来了。反之基于流的插件也可以使用gulp-streamify转换成流再进行处理。

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//gulpfile.js
var gulp = require('gulp'),
babelify = require('babelify'),
notify = require('gulp-notify'), //按需使用
browserify = require('browserify'),
buffer = require('gulp-buffer'), //vinyl-buffer也可
uglify = require('gulp-uglify'),
source = require('vinyl-source-stream'); //vinyl-source-buffer 按需使用

gulp.task('default', function () {
browserify('./index.js')
.transform("babelify", {plugins: ["transform-runtime"],presets: [["env"]]}) //babel处理
.bundle() //处理模块依赖
.pipe(source("index.js")) //转换为vinyl文件对象
.pipe(buffer()) //转换为buffer以便进行代码压缩
.pipe(uglify())
.pipe(gulp.dest('./dist'))
.pipe(notify({
message: 'javascript compile complete'
}));
});

简单版本,不推荐

gulp-browserify已经被官方拉入黑名单,作者也停止维护了。新手入门可以使用

你需要用到的包

1
2
npm i --save-dev gulp gulp-babel babel-core babel-preset-env babel-plugin-transform-runtime gulp-browserify
npm i --save babel-runtime

gulp-browserify来自动处理模块依赖

gulpfile.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//gulpfile.js
var gulp = require('gulp'),
babel = require('gulp-babel'),
notify = require('gulp-notify'),
browserify = require('gulp-browserify');

gulp.task('default', function () {
gulp.src('./index.js')
.pipe(babel())
.pipe(browserify())
.pipe(gulp.dest('./dist'))
.pipe(notify({
message: 'javascript compile complete'
}));
});
1
// babel 配置同webpack篇

参考文章:
6-reasons-why-javascripts-async-await-blows-promises

为什么放弃jQuery

发表于 2020-07-20 | 分类于 前端 , javascript , jquery

前言

首先,直接操作dom的编程方式已经成为过去,数据驱动才是正途!
其次,不一定非要舍弃jQuery,只是可以,选择权在自己手上!
下面只是就事论事

落幕

2018年7月 全球最大的同性交友社区(Github)被微软收购后的 52 天,改版并放弃了 jQuery 。这一举动让我等搬运工陷入了沉思。

Why?

随着ECMAScript标准的更新,原生js已经可代替jQuery,给我babel一个面子,不要再说兼容不支持的话!
下面举两个比较常用的方面来说明:

  • jQuery Api可以使用原生轻松实现
  • 使用 fetch 来代替 ajax;
    Fetch API 是基于 Promise 设计,更符合目前的异步解决方案。什么是目前的异步解决方案?翻看上一篇async & await

具体实现对比(部分)

选择器

1
2
3
4
5
//jq
$("#a .b");

//Native
document.querySelectorAll("#a .b");

插入

1
2
3
4
5
6
7
8
9
10
11
12
 //jq
$(selector).append(html);
//prepend
//before
//after

//Native
document.querySelectorAll(selector).insertAdjacentHTML("beforeend", html);
//beforebegin 开始标签之前 before
//afterbegin 开始标签之后 prepend
//beforeend 结束标签之前 append
//afterbegin 结束标签之后 after

类操作

1
2
3
4
5
6
7
8
9
10
11
 //jq
$(selector).addClass(className);
//hasClass
//removeClass
//toggleClass

//Native
document.querySelectorAll(selector).classList.add(className, className, className...);
//contains(className) 检测是否存在
//removeClass(className, className, className...) 删除
//toggle(className, true|false) 切换

ajax

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
//jq
$.ajax({
//.................
});

//Native
(async () => {
try{
const res = await req(url, {headers: {Accept: "text/html"}});
if(......){
.......
}
} catch {

}
}());

function req(url, data) { //自己封装,仅供参考
let sendurl = url;
let setting = {
method: data.type ? data.type : "get",
headers: new Headers({
"Accept": "application/json, text/javascript, */*; q=0.01",
"Pragma": "no-cache",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
"X-Requested-With": "XMLHttpRequest"
}),
credentials: "include"
};

if(data.headers){
const headers = setting.headers;
for(let attr in data.headers){
if(headers.has(attr)){
headers.set(attr, data.headers[attr]);
} else {
headers.append(attr, data.headers[attr]);
}
}
}

if (setting.method == "post") {
setting.body = data.param ? paramData(data.param) : "";
} else {
sendurl = sendurl + (data.param ? "?" + paramData(data.param) : "");
}

return fetch(sendurl, setting).then((response) => {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
throw new Error(response.statusText);
}
}).then((response) => {
const accept = setting.headers.get("accept");
if(accept.includes("/json")){
return response.json();
} else if(accept.includes("/html")){
return response.text();
}
});
}

原生实现类jQuery的库

上手才是关键,下面实现了部分jQuery的Api(代码构建编译部分省略),可能功能有所差异,但是就是这个味儿~

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
<!DOCTYPE html>
<html lang="en">
<title>test</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<head>
</head>
<body>
<div class="test" actid="111"></div>
</body>
<script>
class xy {
constructor(selector){
this.dom = document.querySelectorAll(selector);
this.length = document.querySelectorAll(selector).length;
}

eq(index){
this.dom = [this.dom[index]];
return this;
}

attr(name){
return this.dom[0].getAttribute(name);
}

children(index){
let childrenList = [];
for(let i = 0;i < this.dom.length;i++){
childrenList.push(this.dom[i].children);
}

let arr = [];
for(let i = 0;i < childrenList.length;i++){
arr.push(index != undefined ? childrenList[i][index] : childrenList[i]);
}

return arr.length > 1 ? arr : arr[0];
}

append(htmlStr){
// for(let i = 0;i < this.dom.length;i++){
// this.dom[i].insertAdjacentHTML("beforeend", htmlStr);
// }
this.circulationFun("dom.insertAdjacentHTML('beforeend', '" + htmlStr + "')");
}

html(htmlStr){
this.circulationFun("dom.innerHTML = '" + htmlStr.replace(/\s*/g,"") + "'");
}

remove(){
this.circulationFun("dom.parentNode.removeChild(dom)");
}

addClass(className){
this.classFun("add", className);
}

removeClass(className){
this.classFun("remove", className);
}

toggleClass(className){
this.circulationFun("dom.classList.toggle('" + className + "')");
}

classFun(order, className){
const classArr = className.split(" ");
let code = "dom.classList." + order + "(";
for(let i = 0;i < classArr.length;i++){
code += "'" + classArr[i] + "',";
}
code = code.substr(0, code.length - 1);
code += ")";
this.circulationFun(code);
}

circulationFun(code){
for(let i = 0;i < this.dom.length;i++){
new Function("dom", "i", code)(this.dom[i], i);
}
}

//你可以不停的去扩展
}

function $xy(selector){
return new xy(selector);
}

$xy(".test").attr("actid");
//output 111
</script>

PHP对于不同的Content-Type取值的处理方式

发表于 2020-05-02 | 分类于 后端 , php

HTTP协议 (Hyper Text Transfer Protocol)

HTTP是一个基于TCP/IP通信协议来传递数据,包括html文件、图像、结果等,即是一个客户端和服务器端请求和应答的标准。

HTTP协议特点

  • http无连接:限制每次连接只处理一个请求,服务端完成客户端的请求后,即断开连接。(传输速度快,减少不必要的连接,但也意味着每一次访问都要建立一次连接,效率降低)
  • http无状态:对于事务处理没有记忆能力。每一次请求都是独立的,不记录客户端任何行为。(优点解放服务器,但可能每次请求会传输大量重复的内容信息)
  • 客户端/服务端模型:客户端支持web浏览器或其他任何客户端,服务器通常是nginx或者apache等
  • 简单快速
  • 灵活:可以传输任何类型的数据

一次HTTP请求过程

域名 =》DNS域名解析 =》TCP三次握手建立连接 =》传输http报文 =》四次挥手断开连接

TCP三次握手过程

  • 第一次握手客户端需要发送一个syn=1告诉服务端需要联机,并且会同时发送一个随机生成的顺序号码(seq)。
  • 第二次握手由服务端发起,服务端需要回复客户端一个确认ACK,这个值为第一次客户端生成的顺序号码加1,另外也生成一个随机顺序号码给到客户端。
  • 第三次握手由客户端收到第二次握手的消息后,回复服务端一个确认消息ACK,这个值为服务端的顺序号码加1,也会发送一个seq。

    第一次握手,是客户端需要确认自己发消息没问题,服务端收到消息以后证明自己收消息没问题,这时他还需要证明自己发消息没问题所以他发起第二次握手。
    客户端收到第二次握手的消息后确认了自己的发消息和收消息都没问题,但这时候服务端只知道自己收消息没有问题,他第二次发出的消息他并不知道客户端是否成功接收了。
    所以有了第三次握手,告诉服务端我收到了,你发消息没问题。这时,就证明了客户端和服务端双方的收发都没有问题。

HTTP请求报文

一个HTTP请求报文由请求行(request line)、请求头部(header)和请求体(body)组成。

  • 请求行:请求方法、请求地址和协议版本
  • 请求头:主要说Content-Type(请求的与实体对应的MIME信息)
    • 常见取值 :
      application/x-www-form-urlencoded (使用jquery发送ajax请求会默认类型)
      application/json
      multipart/form-data (传输文件固定类型)
    • 后端如何接收:(PHP)
      取值为application/x-www-data-urlencoded情况下$_POST 和 php://input均有值。
      取值为 application/json 时$_POST无值。
      取值为multipart/form-data 时php://input无值。此时应该用$_POST来获取字段,$_FILES 来获取上传的文件信息
  • 请求体:
    • 常见类型 :
      application/x-www-form-urlencoded :a=1&b=2
      application/json :”{“a”:1,”b”:2}”

HTTP响应报文

HTTP响应报文主要由状态行、响应头、及响应数据组成。

  • 状态行:协议版本,状态码,状态码描述。
  • 响应头:主要说Content-Type
    • 常见值 :
      text/html : HTML格式
      text/plain :纯文本格式
      text/xml : XML格式
      image/gif :gif图片格式
      image/jpeg :jpg图片格式
      image/png:png图片格式

四次挥手断开

通过前面,我们已经知道ACK是用来应答的,SYN是用来代表连接的。而这里的FIN报文就是代表断开连接的,当主动方没有数据再需要传输给对方时,会向对方发起FIN报文,但这时候被动方不会立马断开连接,他只会回复一个ACK告诉主动方你发的FIN报文我收到了,很可能不会立马关闭,因为他可能还没处理完请求,可能还有消息需要发送,这时主动方进入FIN_WAIT_2状态。等被动发也没有消息需要发送了,这时候才会也发一个FIN给主动方,主动方回复以后,连接断开。

COOKIE

Http 协议中引入了 cookie 技术,用来解决 http 协议无状态的问题。通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态;Cookie会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。后端以 session 这样的机制来保存服务端的对象状态。

Nginx搭配Node来构建统计系统

发表于 2020-04-05 | 分类于 后端 , node

背景

一个互联网企业少不了对自身业务的数据访问量的监控和分析。统计行业,如今CNZZ和百度作为两大巨头,是第一优先选择。

但是这两方的覆盖面积太广,难免有时候会有些“疏漏”:

  • CNZZ和百度统计经常数据不一致
  • 经常出现数据缺失(比如对于站点根目录的PV,UV访问)

作为企业肯定是对于自身业务的数据是第一关注点,那么数据准确率和迭代速度就成为了优先选择。

统计系统v1.0.0

数据统计前端规则

这里主要是说日志收集和处理方向,统计规则我说了,反正就是围绕cookies和localStorage再添加各种限制展开。有兴趣的可以看我的repo=> Pure_statistics ,这里讲述了对PV和UV的收集规则。

日志收集和处理

  • 用静态资源作为“载体”
  • rsyslog解析并推送到MySQL

这就是统计系统的雏形

不过久而久之就出现了瓶颈,rsyslog不灵活,迭代过程十分痛苦。

取其精华,去其糟粕

保留Nginx处理静态资源访问的优势,用对字符串十分友好的Node来处理日志,解析字段。
虽然Nginx和Node都是基于事件驱动的服务模型,但是不得不承认Nginx对于静态访问的处理是优于Node的。
像普遍情况专门有一台NodeServer,在拓展业务上可以用Nginx做一个反向代理​,实现动静分离,发挥其各自的优势。​

进一步优化

nginx.conf

其实我们在处理日志的时候只需要去处理我们“载体”相关的日志,那么我们就得将统计日志和普通日志分离。

这里我介绍一种配置路由的方式来分离日志

  • 将你的载体放在某一个目录下,比如/tongji/
  • 利用location的普通字符匹配规则去匹配载体访问
  • 匹配后修改日志路径
1
2
3
4
5
6
7
location ^~ /tongji/ {
if ($time_iso8601 ~ "(\d{4})-(\d{2})-(\d{2})") {
set $time $1$2$3;
}
access_log 路径马赛克/$time-access.log static;
# static 规则按需配置.
}

这样在解析日志的时候可以避免冗余日志,节约是美德呀~
当然你还可以做很多事,比如匹配站点,避免非自己业务的访问干扰数据精确度等等

当然,如果你司足够壕,当我没说。

Node处理程序

剩下来的事情就是用Node去读日志了
createReadStream创建一个可读流交给 readline 逐行读取处理就行了,剩下的这就是JavaScript开发者的拿手戏了 => 字符串解析

请注意,node在处理大文件或者未知内存文件大小的时候千万不要使用readFile,会突破V8内存限制。
推荐使用stream + pipe去处理,其不受V8限制,如果不设计字符串的操作还可以纯粹用buffer去操作,当然此时便是受物理内存的限制了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');
const url = require('url');
const readline = require('readline');

console.time('cost-time');
const rl = readline.createInterface({
input: fs.createReadStream('路径马赛克/20200221-access.log')
});

let arr = [];

rl.on('line', (line) => {
//你自己的业务逻辑
});

rl.on('close', () => {
console.timeEnd('cost-time'); //假吧意思看个耗时
});

Antd 按需加载并使用自定义样式

发表于 2019-12-20 | 分类于 前端 , node , 构建工具

按需加载

为什么要按需加载?

  • 在使用antd的组件时会自动加载css
  • 大大减少打包后的文件体积

按照官网的介绍使用babel-plugin-loader即可实现,这里就不赘述

按需加载同时使用自定义样式

这里需要用到三个loader

1
npm install --save-dev style-loader css-loader url-loader

webpack配置

在配置的时候我们得写两套样式处理规则,分别处理antd样式和自己的样式

处理Antd样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ 
test: /\.css$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
options: {
importLoaders: 1
}
}
],
exclude: /app/, //这里要将自己的排除在规则外
}

处理自己的样式

1
2
3
4
5
6
7
8
9
10
11
12
{ 
test: /\.css$/,
use: [
{
loader: "style-loader",
},
{
loader: "css-loader",
}
],
exclude: /node_modules/, //这里要将Antd的样式排除
}

这里还需要注意的是在配置entry的时候,因为我们的antd已经按需加载了,所以无需再重复打包

12下一页

Irwin

web技术探讨总结,javascript疑难,nodejs应用,微信小程序,react-native实战,网络通信。

20 日志
15 分类
36 标签
GitHub E-Mail
© 2020 Irwin
Power By Hexo | NexT