Houser's Blog

Technology && Art


  • 首页

  • 导航

  • 标签

  • 分类

  • 关于我

  • 归档

  • 代码片段

  • 搜索

vscode中开启emmet对jsx的支持

发表于 2018-03-03 | 分类于 笔记

在用户配置文件里添加如下配置, class会自动转化为className

1
2
3
4
"emmet.syntaxProfiles": {
"javascript": "jsx"
},
"emmet.triggerExpansionOnTab": true,

react-mobx-less-router架构起手

发表于 2018-03-02

使用create-react-app创建项目

创建项目

create-react-app project

抛出配置文件

npm run eject

mobx

安装mobx

npm install mobx mobx-react –save

mobx中文文档

启用装饰器语法

在MobX 中使用 ES.next 装饰器是可选的。本章节将解释如何(避免)使用它们。

使用装饰器的优势:

样板文件最小化,声明式代码。
易于使用和阅读。大多数 MobX 用户都在使用。

Babel中启用装饰器

安装依赖
npm i –save-dev babel-plugin-transform-decorators-legacy

编辑package.json,添加plugins,修改后如下

1
2
3
4
5
6
7
8
"babel": {
"presets": [
"react-app"
],
"plugins": [
"transform-decorators-legacy"
]
},

在index.js文件中注入store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import { HashRouter } from 'react-router-dom';
import { Provider } from 'mobx-react';
import ResumeStore from './store/ResumeStore';
let stores = {
ResumeStore,
};

ReactDOM.render((
<Provider {...stores}>
<HashRouter>
<App />
</HashRouter>
</Provider>
),document.getElementById('root'));
registerServiceWorker();

配置react-router

安装react-router

npm install –save react-router

App.js文件中配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { Component } from 'react';
import './App.less';
import {
Route,
Switch,
} from 'react-router-dom'
import Index from './page/Index';

class App extends Component {
render() {
return (
<div>
<Switch>
<Route path="/" component={Index} />
</Switch>
</div>
);
}
}

export default App;

配置 less

安装依赖

npm install less-loader less –save-dev

修改配置文件

修改 config文件夹下的 webpack.config.dev.js 和 webpack.config-prod.js

  1. test: /.css$/ 改为 /.(css|less)$/
  2. test: /.css$/ 的 use 数组配置增加 less-loader

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
test: /\.(css|less)$/,
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
options: {
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebookincubator/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
autoprefixer({
browsers: [
'>1%',
'last 4 versions',
'Firefox ESR',
'not ie < 9', // React doesn't support IE8 anyway
],
flexbox: 'no-2009',
}),
],
},
},
{
loader: require.resolve('less-loader') // compiles Less to CSS
}
],
}

【翻译】没有常春藤学校学位的我是如何获得微软、亚马逊、推特的offer

发表于 2018-03-02

原文:How I landed offers from Microsoft, Amazon, and Twitter without an Ivy League degree

JavaScript有用的代码片段

发表于 2018-02-20 | 分类于 笔记

参考 JavaScript有用的代码片段和trick

浮点数取整

1
2
3
4
5
const x = 123.4545;
x >> 0; // 123
~~x; // 123
x | 0; // 123
Math.floor(x); // 123

注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。

1
2
3
> Math.floor(-12.53); // -13
> -12.53 | 0; // -12
>

生成6位数字验证码

1
2
3
4
5
6
7
8
9
10
11
// 方法一
('000000' + Math.floor(Math.random() * 999999)).slice(-6);

// 方法二
Math.random().toString().slice(-6);

// 方法三
Math.random().toFixed(6).slice(-6);

// 方法四
'' + Math.floor(Math.random() * 999999);

url查询参数转json格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES6
const query = (search = '') => ((querystring = '') => (q => (querystring.split('&').forEach(item => (kv => kv[0] && (q[kv[0]] = kv[1]))(item.split('='))), q))({}))(search.split('?')[1]);

// 对应ES5实现
var query = function(search) {
if (search === void 0) { search = ''; }
return (function(querystring) {
if (querystring === void 0) { querystring = ''; }
return (function(q) {
return (querystring.split('&').forEach(function(item) {
return (function(kv) {
return kv[0] && (q[kv[0]] = kv[1]);
})(item.split('='));
}), q);
})({});
})(search.split('?')[1]);
};

query('?key1=value1&key2=value2'); // es6.html:14 {key1: "value1", key2: "value2"}

获取URL参数

1
2
3
4
5
6
7
8
function getQueryString(key){
var reg = new RegExp("(^|&)"+ key +"=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r!=null){
return unescape(r[2]);
}
return null;
}

n维数组展开成一维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var foo = [1, [2, 3], ['4', 5, ['6',7,[8]]], [9], 10];

// 方法一
// 限制:数组项不能出现`,`,同时数组项全部变成了字符数字
foo.toString().split(','); // ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

// 方法二
// 转换后数组项全部变成数字了
eval('[' + foo + ']'); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// 方法三,使用ES6展开操作符
// 写法太过麻烦,太过死板
[1, ...[2, 3], ...['4', 5, ...['6',7,...[8]]], ...[9], 10]; // [1, 2, 3, "4", 5, "6", 7, 8, 9, 10]

// 方法四
JSON.parse(`[${JSON.stringify(foo).replace(/\[|]/g, '')}]`); // [1, 2, 3, "4", 5, "6", 7, 8, 9, 10]

// 方法五
const flatten = (ary) => ary.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []);
flatten(foo); // [1, 2, 3, "4", 5, "6", 7, 8, 9, 10]

// 方法六
function flatten(a) {
return Array.isArray(a) ? [].concat(...a.map(flatten)) : a;
}
flatten(foo); // [1, 2, 3, "4", 5, "6", 7, 8, 9, 10]

日期格式化

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
// 方法一
function format1(x, y) {
var z = {
y: x.getFullYear(),
M: x.getMonth() + 1,
d: x.getDate(),
h: x.getHours(),
m: x.getMinutes(),
s: x.getSeconds()
};
return y.replace(/(y+|M+|d+|h+|m+|s+)/g, function(v) {
return ((v.length > 1 ? "0" : "") + eval('z.' + v.slice(-1))).slice(-(v.length > 2 ? v.length : 2))
});
}

format1(new Date(), 'yy-M-d h:m:s'); // 17-10-14 22:14:41

// 方法二
Date.prototype.format = function (fmt) {
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)){
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (var k in o){
if (new RegExp("(" + k + ")").test(fmt)){
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
}
}
return fmt;
}

new Date().format('yy-M-d h:m:s'); // 17-10-14 22:18:17

匿名函数自执行写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
( function() {}() );
( function() {} )();
[ function() {}() ];

~ function() {}();
! function() {}();
+ function() {}();
- function() {}();

delete function() {}();
typeof function() {}();
void function() {}();
new function() {}();
new function() {};

var f = function() {}();

1, function() {}();
1 ^ function() {}();
1 > function() {}();

数字字符转数字

1
2
var a = '1';
+a; // 1

最短的代码实现数组去重

1
[...new Set([1, "1", 2, 1, 1, 3])]; // [1, "1", 2, 3]

面向学生的微信开发建议

发表于 2018-02-09

微信开发学习建议

了解微信开发

微信功能的实现主要依托期微信公众平台,即订阅号、服务号、企业号、小程序。功能对比。功能说明。

开发文档是主要参考文档,有所有能用的功能和实现说明

认证与未认证的订阅号/服务号接口权限区别很大。

学习过程中可以开通测试账号,无需认证就具备所有高级接口权限

开发建议

准备

  • 购买学生版服务器
    • 腾讯云
    • 阿里云 相对偏贵,性能好
  • 装linux系统,推荐 ubuntu
  • 推荐服务器软件管理软件 amh
  • 注册个人域名并备案
  • 通过Github管理个人项目
  • 使用Google搜索(翻墙方案很多,建议自己找两三个同学合伙搭建翻墙服务器,推荐VPS服务商 vultr $2.5/月, 工具 shadowsocks)

学习流程

  • 注册微信公众平台测试号
  • 搭建服务器运行环境,解析域名 ( 必须,微信开发有域名安全机制,并能正常访问 )
  • 部署基础项目
  • 实现微信登录功能
  • 实现JS-SDK功能
  • 实现微信支付功能( 需要以公司名义开通,流程教复杂 )

调试技巧

  • 微信web开发者工具 在chrome的基础上输出更多调试信息
  • chrome 学会用谷歌浏览器调试
  • 真机调试 页面在微信上对显示效果跟电脑上不完全一样,存在兼容性问题,这个时候,如果在本地开发,可以把本地启动对服务器暴露到局域网,用手机通过ip访问

开发模式:

微信网页开发 (最常用)

通过微信打开的页面能使用微信app提供的功能,相当于在常见页面里拓展一些功能,可以把微信理解成具有特殊功能的浏览器

常用的有

  • 微信登录:订阅号/服务号都支持
  • 微信支付:服务号支持
  • JS-SDK:微信打开的页面通过js调取微信app的拥有的功能,常用的有
    • 分享页面
    • 上传下载图片
    • 扫一扫

微信交互开发

在公众号界面通过聊天的形式进行交互

  • 接受消息
  • 回复消息
  • 通知消息

APP开发

在非微信app中调起微信登录和微信支付

开发流程

  • 确定需求,注册合适的公众号
  • 注册域名,服务器
  • 配置微信公众号 ( 根据需要使用的微信功能进行配置,开发文档有说明 )
  • 选取合适的语言和框架
  • 搭建开发环境
    • 服务器软件 ( Apache / Nginx )
    • 语言运行环境
    • 版本管理系统( 方便代码管理和多人开发 Git / Svn )
  • 开发
  • 测试 ( 测试覆盖面要全,极端值也需要测试 )
  • 上线

主要微信功能说明

阿里云视频上传在React中的实现

发表于 2017-12-20

在原项目基础上进行了删减,用了antd UI库,能够显示当前上传进度,采用阿里云点播凭证方式上传,不是OSS的上传模式,可以根据官方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
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
import React, {Component} from 'react';
import {Button, Col, Form, Icon, Input, message, Modal, Row, Select, Switch, Upload} from 'antd';
import {
loadServiceCourseDataSet,
loadUploadVideoAuth,
reloadUploadVideoAuth,
updateServiceCourseItem
} from '../../../service/course';
import LazyLoad from 'react-lazy-load';

const FormItem = Form.Item;

// 创建 上传实例 变量,多次尝试后放在这里才靠谱
var uploader;

class Update extends Component {
constructor(props) {
super(props);
this.state = {
fileList: [],
aliVideoAuthDto: {
requestId: '',
uploadAddress: '',
uploadAuth: '',
videoId: ''
},
upload_progress: ''
}
}

componentDidMount() {

let _this = this;

uploader = new VODUpload({
// 文件上传失败
'onUploadFailed': function (uploadInfo, code, message) {
message.fail('上传失败,请稍后再试');
//console.log("onUploadFailed: file:" + uploadInfo.file.name + ",code:" + code + ", message:" + message);
},
// 文件上传完成
'onUploadSucceed': function (uploadInfo) {
_this.setState({ uploading: false})
message.success('上传成功');
//console.log("onUploadSucceed: " + uploadInfo.file.name + ", endpoint:" + uploadInfo.endpoint + ", bucket:" + uploadInfo.bucket + ", object:" + uploadInfo.object);
},
// 文件上传进度
'onUploadProgress': function (uploadInfo, totalSize, uploadedSize) {
//console.log("onUploadProgress:file:" + uploadInfo.file.name + ", fileSize:" + totalSize + ", percent:" + Math.ceil(uploadedSize * 100 / totalSize) + "%");
_this.setState({ upload_progress: Math.ceil(uploadedSize * 100 / totalSize) + "%"})
},
// STS临时账号会过期,过期时触发函数
'onUploadTokenExpired': function () {
message.success('上传凭证过期,请重试');
//console.log("onUploadTokenExpired");
},
// 开始上传
'onUploadstarted': function (uploadInfo) {
_this.setState({ uploading: true });
uploader.setUploadAuthAndAddress(uploadInfo, _this.state.aliVideoAuthDto.uploadAuth, _this.state.aliVideoAuthDto.uploadAddress);
}
});
uploader.init();
}

normFile = (e) => {
console.log('Upload event:', e);
if (Array.isArray(e)) {
return e.file;
}
return e && e.fileList;
}

doUpload = () => {
console.log('start');
uploader.startUpload();
};

render() {
const {getFieldDecorator} = this.props.form;

const formItemLayout = {
labelCol: {
xs: {span: 24},
sm: {span: 4},
},
wrapperCol: {
xs: {span: 24},
sm: {span: 18},
},
};

const uploadProps = {
action: '//jsonplaceholder.typicode.com/posts/',
onRemove: (file) => {
this.setState(({fileList}) => {
const index = fileList.indexOf(file);
const newFileList = fileList.slice();
newFileList.splice(index, 1);
return {
fileList: newFileList,
};
});
},
beforeUpload: (file) => {
let userData = '{"Vod":{"UserData":"{"IsShowWaterMark":"false","Priority":"7"}"}}';

console.log(file);

this.setState({videoSize: file.size})

uploader.addFile(file, null, null, null, userData);

// 获取上传凭证
loadUploadVideoAuth({
courseItemId: this.props.data.id,
videoName: file.name,
videoTitle: file.name,
videoTags: file.name,
videoDesc: file.name,
}).then(data => {
this.setState({aliVideoAuthDto: data.data.aliVideoAuthDto});
});

this.setState(({fileList}) => ({
fileList: [...fileList, file],
}));

return false;
},
fileList: this.state.fileList,
};

return (
<Modal title="更新" visible={this.props.show} onCancel={this.props.onCancel} footer={null} width={'80%'}>
<Row type='flex' style={{marginBottom: '5px'}}>

<Col span={24}>
<FormItem {...formItemLayout} label="课程视频">
<Upload {...uploadProps}>
<Button>
<Icon type="upload"/> 选择文件
</Button>
</Upload>
<Button type="primary" onClick={this.doUpload} disabled={this.state.fileList.length === 0}
loading={this.state.uploading}>
{this.state.uploading ? this.state.upload_progress : '开始上传'}
</Button>
</FormItem>
</Col>
</Row>
<FormItem wrapperCol={{span: 12, offset: 4}}>
<Button type="primary" onClick={this.handleSubmit}>提交更新</Button>
</FormItem>
</Modal>
)
}
}

export default Form.create()(Update);

阿里云视频点播在React中的实现

发表于 2017-12-20

在原项目组件上进行了删减,保留核心代码,不一定能直接运行,需要根据情况调整代码

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
import React, { Component } from 'react';
import { hashHistory } from 'react-router';

class CourseDetail extends Component {
constructor(props) {
super(props);
this.state = {};

this.player = null; // 创建播放器实例变量
}


componentDidMount() {
// 初始化播放器,
this.player = new Aliplayer({
id: 'Ali_Player', // 容器id
vid: '',
width: "100%", // 播放器宽度
playauth: '',
cover: `${IMG_DOMAIN}${data.data.serviceCourseDto.serviceCourse.coverUrl}`,
autoplay: false,
rePlay: false,
skinLayout:[{"name":"H5Loading","align":"cc"},
{"name":"errorDisplay","align":"tlabs","x":0,"y":0},
{"name":"infoDisplay","align":"cc"},
{"name":"controlBar","align":"blabs","x":0,"y":0,"children":[{"name":"progress","align":"tlabs","x":0,"y":0},
{"name":"timeDisplay","align":"tl","x":10,"y":24}]}]
});

this.player.on('play',()=>{
if(this.state.cur_courseitem.courseItemId){
this.player.play();
} else {
this.player.pause();
Toast.info('请选择章节');
}
})
}

handlePlay = (courseItemId) => {
// 播放事件,此时获取播放凭证,也可以在挂载的时候获取播放凭证
loadVideoPalyAuth({courseItemId}).then(data => {
this.setState({
cur_courseitem: {
...this.state.cur_courseitem,
videoId: data.data.aliVideoPlayAuthDto.videoId,
playauth: data.data.aliVideoPlayAuthDto.playAuth,
courseItemId: courseItemId
}
},()=>{
if (this.player){
this.player.dispose();
}

this.player = new Aliplayer({
id: 'Ali_Player', // 容器id
vid: data.data.aliVideoPlayAuthDto.videoId,
width: "100%", // 播放器宽度
playauth: data.data.aliVideoPlayAuthDto.playAuth,
autoplay: false,
rePlay: false,
});
});
})
}

render() {
return (
<div>
<button onClick={this.handlePlay}>播放</button>
<div className="prism-player" id='Ali_Player' />
</div>
);
}
}

export default CourseDetail;

个人总结的react脚手架

发表于 2017-09-07 | 分类于 原创

react-cli

react应用的起步工程和总结 Github传送门

特点

采用时下流行的技术栈,视图:react,状态管理:redux,路由:react-router 作为基础,配合webpack等开发工具而搭建的项目架构,适用于中小型项目,也可以在此基础上进行调整适合更多类型的项目。

同时还会提供react开发相关的资料、疑问、解决方案等,希望对开发者有所帮助,也对自己的技术有所提升。欢迎大家提供建议

技术栈

  • react
  • redux
  • react-router
  • mock
  • pace
  • whatwg-fetch

项目构建

目录结构

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
.
├── README.md
├── build #构建生成目录
│   ├── bundle
│   └── index.html
├── package-lock.json
├── package.json
├── src #源代码文件
│   ├── action.js #action创建函数文件
│   ├── components #组件目录 *1
│   │   ├── layout
│   │   └── notify
│   ├── http.js #所有fetch请求 *2
│   ├── index.html #webpack生成html的模板 *3
│   ├── index.js #入口文件
│   ├── reducer.js #reducer *4
│   ├── router.js #路由定义
│   ├── routes #路由对应的page目录
│   │   ├── App.js #入口文件 *5
│   │   ├── index #一个页面对应一个文件夹 *6
│   │   │   ├── Index.js
│   │   │   └── index.scss
│   │   └── login
│   │   ├── Login.js
│   │   └── login.scss
│   ├── static #静态文件夹
│   │   └── logo.jpg
│   ├── store.js #store创建文件 *7
│   └── utils #工具集
│   ├── config.js #项目配置
│   ├── mock.js #本地mock数据
│   ├── pace.css #首屏加载动画css
│   ├── pace.js #首屏加载动画js
│   └── theme.scss #主题sass变量
├── webpack.config.js #webpack开发配置文件
└── webpack.production.config.js #webpack构建配置文件

目录结构说明

  1. components 每一个组件对应一个文件夹,包含该组件js以及css,更小的组件也放在该文件夹下
  2. 将所有的请求独立出来放在一个文件里,每个fetch请求封装成一个回调函数并export
  3. 采用自定义html模板进行打包便于引用cdn等文件,或者其他自定义操作
  4. 项目复杂的时候可以创建reducer文件夹,进一步拆分
  5. 每个页面都是该组件的字组件,便于引入例如通知等全局组件
  6. 每个页面对应一个文件夹,因为一个页面包含的组件较多,利于拆分
  7. 独立出来store是便于在非组件的文件中操作reducer的store

开发说明

克隆项目:

1
git clone https://github.com/Houserqu/react-cli.git

进入项目目录安装依赖:

1
npm i

开发:

1
npm run dev

构建:

1
npm run build

曲折回学路

发表于 2017-09-05 | 分类于 随笔

时隔五个月,终于再次回到学校,从去年12月份检查出病到现在,居然已经有十余月,真的不知道是如何走到现在的。
这次病魔缠身,对我身体和精神带来了巨大打击,对家人也是带来了沉重的负担,感触颇深,希望对生命有重新的认识吧。不彻底痛苦一次,就不会知道生活恶习所引发的蝴蝶效应。

虽然已经回到学校,但似乎还没有那么顺利上课,治疗时间还没有达到规定,只好按规定办事了。

左手静脉血管上扎的疤痕依旧清晰可见,一天大把的药还需要服用,革命尚未成功!只能告诫自己好好爱惜身体吧。

这次回校真的激动万分,见到了等待已久的她,见到了高谈阔论的室友。生活很美好!

fetch post formData

发表于 2017-08-18 | 分类于 笔记

当使用fetch用表单的方式post json类型的数据时候,需要注意几个问题

header

设置header 的 ‘Content-Type’,’application/x-www-form-urlencoded;charset=utf-8’

序列化json

尝试过多种方式,需要处理成 ‘username=admin&password=password’这种方式才能被正确的识别成 formData格式,可以在浏览器查看具体的请求体
采用类似 new FormData() 方式会被处理成——WebKitFormBoundary

具体示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//序列化json
const formBody = Object.keys(paramsArray).map(key=>encodeURIComponent(key)+'='+encodeURIComponent(paramsArray[key])) .join('&');

var headers = new Headers();
headers.set('Content-Type','application/x-www-form-urlencoded;charset=utf-8');

fetch('api/auth/login',{
method:'post',
mode:'cors',
credentials: "include",
headers,
body: formBody
}).then((response)=>{
return response.json();
}).then((responseData)=>{
console.log(responseData);
});

参考:
四种常见的 POST 提交数据方式
how to post a x-www-form-urlencoded request from react-native
How to make a post request with JSON data in application/x-www-form-urlencoded

1…345
Houser

Houser

43 日志
6 分类
44 标签
GitHub 知乎
© 2015 - 2021 Houser
鄂ICP备15011479号-3
由 Hexo 强力驱动
主题 - NexT.Pisces