avatar

目录
面试手记

1. promise简单实现

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function _Promise(fn) {
let state = 'pending',
value = null,
callbacks = [];

this.then = function(onFulfilled) {
callbacks.push(onFulfilled);
return this; // 链式调用
};
function resolve(result) {
state = 'fulfilled';
callbacks.forEach(function (callback) {
callback(value);
});
}
fn(resolve);
}

//例1
function getUserId() {
return new _Promise(function(resolve) {
//异步请求
http.get(url, function(results) {
resolve(results.id)
})
})
}
getUserId().then(function(id) {
//一些处理
})

2. react性能优化

React 16 加载性能优化指南

  1. 首屏 -> 避免空白:在app root节点中加入loading提示;
  2. 首屏 -> 内容渲染:
    • 可优化资源:
      1. 基础框架:如 React、Vue 等,这些基础框架的代码是不变的,除非升级框架;
      2. Polyfill:对于使用了 ES2015+ 语法的项目来说,为了兼容性,polyfill 是必要的存在;
      3. 业务基础库:业务的一些通用的基础代码,不属于框架,但大部分业务都会使用到;
      4. 业务代码:特点是具体业务自身的逻辑代码;
    • 缓存基础框架:制定合适的缓存策略,四种缓存的优先级:cache-control > expires > etag > last-modified;
    • 使用动态 polyfill:
      1. 去掉构建中静态的 polyfill,换而使用 polyfill.io 这样的动态 polyfill 服务,保证只有在需要时,才会引入 polyfill;
      2. 编译到 ES2015+ ,提升代码运行效率,利用《script type=”module”》标签检测是否支持信语法;
        html
        1
        2
        3
        4
        <script type="module" src="main.es2016.js;"></script>
        <script nomodule src="main.es5.js"></script>
        // 1. 新浏览器识别module和nomodule,会只加载main.es2016.js;
        // 2. 旧浏览器不识别module和nomodule,会只加载main.es5.js;
    • 使用 SplitChunksPlugin 自动拆分业务基础库;
    • 业务代码:
      1. 使用 Tree Shaking 减少业务代码体积,去除没有用到的代码;
      2. Code Splitting “懒加载”代码,改成动态import的形式(React Loadable组件);
      3. shouldComponent()避免不必要的diff;或者启用PureComponent,自带shouldComponent浅比较props和state;
      4. list类型的同级节点加key属性;
    • 页面图片懒加载:监听scroll(节流);

React 框架级别性能优化

参考: 漫谈前端性能,突破React应用瓶颈

  1. 任务分片(React Fiber):基于浏览器对 requestIdleCallback 和 requestAnimationFrame 这两个API 的支持,React 团队实现新的调度策略 — Fiber reconcile。
  2. 结合 Web worker:
    • React 结合 Web worker:
      1. 标准的 React 应用由两部分构成:
        • React core:负责绝大部分复杂的 Virtual DOM 计算;
        • React-Dom:负责与浏览器真实 DOM 交互来展示内容。
      2. 我们尝试在 Web worker 中运行 React Virtual DOM 的相关计算。即将 React core 部分移入 Web worker 线程中。
      3. worker线程postMessage通信的成本决定更新少部分节点的性能不好;
    • Redux 结合 Web worker:
      1. 将 Redux 中 reducer 复杂的纯计算过程放在 worker 线程里;
      2. 在实现层面,借助 Redux 库的 enchancer 设计,完成了抽象封装;

web前端性能优化

  1. 内容优化
    • 减少HTTP请求数:合并多个CSS文件和js文件,利用CSS Sprites整合图像,合理设置HTTP缓存等。
    • 使用内容分发网络(CDN)
    • 使用Ajax缓存
    • 避免重定向
    • 延迟加载组件,预加载组件
    • 最小化iframe的数量
  2. CSS优化
    • 将CSS代码放在HTML页面的顶部
    • 避免使用CSS表达式
  3. javascript优化
    • 将JavaScript脚本放在页面的底部
    • 谨慎使用with,避免使用eval Function函数
    • 最小化DOM的访问:使用JavaScript访问DOM元素比较慢

web安全

  1. 因输入/输出值转义不完全引发的安全漏洞:
    • SQL注入;
    • 跨站脚本攻击(XSS) + 会话劫持::利用没转义的输出构建script标签,将用户信息发送给自己的服务器,获取用户数据(cookie),就可以伪装成用户发送请求了;
    • 跨站点请求伪造(CSRF):利用img.src的属性,在用户不知情的情况下,发送请求(如购物);
  2. 其他安全漏洞:
    • 密码破解:穷举法,字典攻击;
    • 点击劫持(iframe透明覆盖);
    • Dos攻击:利用大量请求造成资源过载;

算法

  1. 冒泡排序
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    this.bubbleSort = function() {
    const length = array.length;
    for (let i = 0; i < length; i++) {
    for (let j = 0; j < length - 1 - i; j++) {
    if (array[j] > array[j + 1]) {
    [array[j], array[j + 1]] = [array[j + 1], array[j]];
    }
    }
    }
    };
  2. 快速排序:在数组中间选择一个主元,交换两边的元素,让主元左边的元素小于主元,右边的元素大于主元;再对两边重复这个步骤;
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 易理解版(阮一峰):没有考虑时间空间复杂度,保留核心思想-分治
    const quickBrief = function(array) {
    if (array.length <= 1) return array;
    const pivotIndex = Math.floor(array.length / 2);
    const pivot = array.splice(pivotIndex, 1)[0];
    const left = [];
    const right = [];
    for (let i = 0; i < array.length; i++) {
    if (array[i] < pivot) {
    left.push(array[i]);
    } else {
    right.push(array[i]);
    }
    }
    return quickBrief(left).concat([pivot], quickBrief(right));
    };
  3. 归并排序
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function merge(leftArr, rightArr) {
    const resultArr = [];
    while (leftArr.length && rightArr.length) {
    resultArr.push(leftArr[0] < rightArr[0] ? leftArr.shift() : rightArr.shift());
    }
    return resultArr.concat(leftArr, rightArr);
    }

    function mergeSort(arr) {
    if (arr.length < 2) return arr;
    const mid = Math.floor(arr.length / 2);
    return merge(mergeSort(arr.slice(0, mid)), mergeSort(arr.slice(mid)));
    }

三栏布局

参考:三栏布局

  1. float布局;
  2. position: absolute;
  3. display: table;
  4. display: flex;
  5. display: grid;

http 2.0 (SPDY协议是Google提出的)

  1. header压缩:HTTP/2使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
  2. 二进制分帧:HTTP 2.0 的所有帧都采用二进制编码
  3. 多路复用:有了新的分帧机制后,HTTP/2 不再依赖多个TCP 连接去实现多流并行了;HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接(每个域名一个连接)即可。
  4. 服务端推送

实现超出整数存储范围的两个大正整数相加

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const num1 = '1118911';
const num2 = '111135781';


function sumString(a, b) {
const aList = a.split('');
const bList = b.split('');
let full = 0;
const resultList = [];

while (aList.length || bList.length || full) {
let temp = ~~aList.pop() + ~~bList.pop() + full;
full = 0;
if (temp > 9) {
full = Math.floor(temp / 10);
temp = temp % 10;
}
resultList.unshift(temp);
}

return resultList.join('');
}

console.log(sumString(num1, num2));

函数式编程

  1. 纯函数:无副作用(不改变参数,不依赖环境,不进行请求,I/O操作);如map, slice等;
  2. 声明式代码
    javascript
    1
    2
    3
    4
    5
    6
    7
    // 命令式
    const makes = [];
    for (let i = 0; i < cars.length; i++) {
    makes.push(cars[i].make);
    }
    // 声明式
    const makes = cars.map(function(car){ return car.make; });
  3. 函数柯里化
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    // 普通写法
    const add = function(x, y) {
    return x + y;
    }
    // curry
    const addCurry = x => y => (x + y);
    const addTen = addCurry(10);
    const num = addTen(1);
  4. 函数组合
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const toUpperCase = x => x.toUpperCase();
    const exclaim = x => x + '!';
    // 普通
    const shout = x => exclaim(toUpperCase(x));
    console.log(shout('what happened!'));
    // 函数组合
    const compose = (...funcList) => x => funcList.reduce((pre, cur) => cur(pre), x);
    const shoutCompose = compose(toUpperCase, exclaim);
    console.log(shoutCompose('what happened!'));
  5. Point Free:函数无须提及将要操作的数据是什么样的
    javascript
    1
    2
    3
    4
    5
    6
    // 非 pointfree,因为提到了数据:word
    const snakeCase = function (word) {
    return word.toLowerCase().replace(/\s+/ig, '_');
    };
    // pointfree
    const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

CSS层叠顺序

层叠顺序 (层叠次序, 堆叠顺序, Stacking Order) 描述的是元素在同一个层叠上下文中的顺序规则,从层叠的底部开始,共有七种层叠顺序:

  1. 背景和边框:形成层叠上下文的元素的背景和边框。
  2. 负z-index值:层叠上下文内有着负z-index值的定位子元素,负的越大层叠等级越低;
  3. 块级盒:文档流中块级、非定位子元素;
  4. 浮动盒:非定位浮动元素;
  5. 行内盒:文档流中行内、非定位子元素;
  6. z-index: 0:z-index为0或auto的定位元素, 这些元素形成了新的层叠上下文;
  7. 正z-index值:z-index 为正的定位元素,正的越大层叠等级越高;

JavaScript设计模式

  1. 单例模式
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    * 在执行当前 Single 只获得唯一一个对象
    */
    var Single = (function(){
    var instance;
    function init() {
    //define private methods and properties
    //do something
    return {
    //define public methods and properties
    };
    }

    return {
    // 获取实例
    getInstance:function(){
    if(!instance){
    instance = init();
    }
    return instance;
    }
    }
    })();

    var obj1 = Single.getInstance();
    var obj2 = Single.getInstance();

    console.log(obj1 === obj2);
  2. 工厂模式
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Animal(opts){
    var obj = new Object();
    obj.name = opts.name;
    obj.color = opts.color;
    obj.getInfo = function(){
    return '名称:'+obj.name +', 颜色:'+ obj.color;
    }
    return obj;
    }
    var cat = Animal({name: '波斯猫', color: '白色'});
    cat.getInfo();
  3. 构造函数模式
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * 构造一个动物的函数
    */
    function Animal(name, color){
    this.name = name;
    this.color = color;
    this.getName = function(){
    return this.name;
    }
    }
    // 实例一个对象
    var cat = new Animal('猫', '白色');
    console.log( cat.getName() );
  4. 发布订阅模式
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /**
    * 发布订阅模式
    */
    var EventCenter = (function(){
    var events = {};
    /*
    {
    my_event: [{handler: function(data){xxx}}, {handler: function(data){yyy}}]
    }
    */
    // 绑定事件 添加回调
    function on(evt, handler){
    events[evt] = events[evt] || [];
    events[evt].push({
    handler:handler
    })
    }
    function fire(evt, arg){
    if(!events[evt]){
    return
    }
    for(var i=0; i < events[evt].length; i++){
    events[evt][i].handler(arg);
    }
    }
    function off(evt){
    delete events[evt];
    }
    return {
    on:on,
    fire:fire,
    off:off
    }
    }());

    var number = 1;
    EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'次');
    });
    EventCenter.off('click'); // 只绑定一次
    EventCenter.on('click', function(data){
    console.log('click 事件' + data + number++ +'次');
    });

    EventCenter.fire('click', '绑定');
  5. 模块模式
    javascript
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * 模块模式 = 封装大部分代码,只暴露必需接口
    */
    var Car = (function(){
    var name = '法拉利';
    function sayName(){
    console.log( name );
    }
    function getColor(name){
    console.log( name );
    }
    return {
    name: sayName,
    color: getColor
    }
    })();
    Car.name();
    Car.color('红色');
  6. 代理模式(利用ES6的proxy代理)

vue和react的区别

  1. jsx语法(all in js)和模板语法;
  2. 状态数据不可变(函数式)和监听数据对象的变化;
  3. Fiber分块执行;
  4. 相互借鉴:reselect和computed;

浏览器兼容

  1. 不同浏览器的标签默认的外补丁和内补丁不同
    解决:css里 *{margin:0;padding:0;}
  2. 几个img标签放在一起的时候,默认有间距
    解决:使用float属性为img布局
  3. 块属性标签float后,又有横行的margin情况下,在ie6显示margin比设置的大
    解决:设置display:inline;
  4. 当标签的高度设置小于10px,在IE6、IE7中会超出自己设置的高度
    解决:超出高度的标签设置overflow:hidden,或者设置line-height的值小于你的设置高度
  5. 标签最低高度设置min-height不兼容
    解决:如果我们要设置一个标签的最小高度200px,需要进行的设置为:{min-height:200px; height:auto !important; height:200px; overflow:visible;}
  6. js相关就用polyfill和babel;

JavaScript继承

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// ES5 组合继承
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag;
}
function Sub(){
this.subFlag = false;
Super.call(this);
}
Sub.prototype = new Super(); // Super后面加不加括号,结果一样
Sub.prototype.getSubFlag = function() {
return this.subFlag;
}
var obj = new Sub();
console.log(obj.getSubFlag());

// ES6 class
class Super {
constructor() {
this.flag = true;
}
getFlag() {
return this.flag;
}
}
class Sub extends Super {
constructor() {
super();
this.subFlag = false;
}
getSubFlag() {
return this.subFlag;
}
}
var obj = new Sub();
console.log(obj.getSubFlag());

// JavaScript继承本质:无类继承
const father = {
flag: true,
getFlag() {
return this.flag;
},
};
const child = {
childFlag: false,
getAllFlag() {
return this.childFlag;
},
};
Object.setPrototypeOf(child, father);
console.log(child.getAllFlag());

斐波那契序列

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 1. 递归(普通版)
function Fibonacci1(n) {
if (n <= 2) return 1;
return Fibonacci1(n - 1) + Fibonacci1(n - 2);
}

// 2. 递归(尾调用优化)
function Fibonacci2(n, ac1 = 1, ac2 = 1) {
if (n <= 2) { return ac2 };
return Fibonacci2(n - 1, ac2, ac1 + ac2);
}

// 3.循环版
function Fibonacci3(n){
if (n===1 || n===2) {
return 1;
}
let ac1 = 1, ac2 = 1;
for (let i = 2; i < n; i++){
[ac1, ac2] = [ac2, ac1 + ac2];
}
return ac2;
}

// 4. 生成器版
function Fibonacci4(n) {
function* fibonacci() {
let [prev, curr] = [1, 1];
while (true) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}

if (n === 1 || n === 2) return 1;
let ac = 0;
const fibo = fibonacci();
for (let i = 2; i < n; i++) {
ac = fibo.next().value;
}
return ac;
}

git分支管理策略

  1. master分支:稳定主分支,最后合并,发布版本;
  2. dev分支:开发分支;
  3. featrue分支:开发新特性;
  4. bugfix分支:修改bug;

bootstrap特点

  1. 跨浏览器:可以兼容所有现代浏览器,包括比较诟病的IE7、8;
  2. 响应布局;
  3. 提供了全面的组件,包括:导航,标签,按钮,表单等;jquery插件;
  4. 支持sass动态样式;

new和bind的相关实现

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function _new(fn){
return function() {
var o = { '__proto__': fn.prototype };
fn.apply(o, arguments);

return o;
};
}

function Person(name) {
this.name = name;
}
const person = _new(Person)('aa');
console.log(person);

Function.prototype._bind = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fbound = function() {
var bindArgs = Array.prototype.slice.call(arguments);
// 平常调用时,环境为context
// new 构造函数方式调用时,这环境指向this
self.apply(this instanceof self ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承函数的原型中的值
fbound.prototype = this.prototype;
return fbound;
}

const foo = {
value: 1
};
function bar() {
console.log(this.value);
}
const bindFoo = bar.bind(foo);
bindFoo(); // 1

千分位

javascript
1
2
3
4
function toS(num) {
return num.toString().replace(/(?=(\B\d{3})+$)/g, ',');
}
console.log(toS(100000));

Javascript中valueOf与toString区别

  1. valueOf应用于运算,toString应用于显示.
  2. 在进行对象转换成字符串时(例如:alert(test)),将优先调用 toString,如果没有 toString 方法了,就调用 valueOf方法,如果tostring 和 valueOf都没重写,就按照 Object的toString 方法输出.
  3. 在进行强转字符串类型时将优先调用toString方法,强转为数字时优先调用valueOf。
  4. 在有运算操作符的情况下,valueOf的优先级高于toString。

git merge 和 git rebase 区别

参考

transform: translateZ(0)

gpu加速:元素本身使用transform和opacity做CSS动画的时候,会提前告诉GPU动画如何开始和结束及所需要的指令;所以会创建一个复合层(渲染层),并把页面所有的复合层发送给GPU;作为图像缓存,然后动画的发生仅仅是复合层间相对移动。

http请求方法

  1. GET:获取资源
  2. POST:传输实体文本
  3. PUT:传输文件
  4. DELETE:删除文件
  5. HEAD:获得报文首部
  6. OPTIONS:询问支持的方法
  7. TRACE:追踪路径
  8. CONNECT:要求用隧道协议连接代理

post四种content-type

  1. application/x-www-form-urlencoded (默认):
    • 参数形式:提交的数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码;
  2. multipart/form-data:
    • 使用表单上传文件时;
    • Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容;
    • 参数形式:消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 –boundary 开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 –boundary– 标示结束。
  3. application/json:
    • 各大浏览器都原生支持 JSON.stringify;
    • 可以方便的提交复杂的结构化数据,特别适合 RESTful 的接口;
    • 参数形式:{“title”:”test”,”sub”:[1,2,3]}
  4. text/xml:
    • 它是一种使用 HTTP 作为传输协议,XML 作为编码方式的远程调用规范;
    • XML 结构还是过于臃肿,一般场景用 JSON 会更灵活方便;

CSS实现等比例缩放的盒子:

  1. padding-bottom有一个非常重要的特性,即当它的值是百分比形式时,百分比的基数是其所在元素的父元素的宽度,而不是高度;
  2. 具体的解决方案是:将元素的高度height设为0,使其高度等于该元素的padding-bottom,再合理设置该元素padding-bottom的值

jQuery组件封装

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// html
<select name="select" class="select"></select>

// plugin
(function($) {
/**
* @desc 一个异步获取下拉框数据的组件
* @param options { object } 配置参数对象
* @param options.url { string } 获取数据路径
* @param options.textField { string } option的显示文本对应返回数据中的字段名
* @param options.valueField { string } option的value对应返回数据中的字段名
* @return target { jquery object } 返回jquery对象本身,提供链式调用
*/
$.fn.initSelect = function(options) {
const defaultOptions = {
textField: 'text',
valueField: 'value',
} // 用户可调配置
const target = this
let optionCode = ''

$.extend(options, defaultOptions)
$.ajax({
url: options.url,
method: 'GET',
success: function(res) {
const { data } = res
optionCode = data
.map(elem => (`<option value="${elem[options.valueField]}">${elem[options.textField]}</option>`))
.join('')
target.empty().html(optionCode)
},
})

return target
}
}(jQuery))

// logic
$(document).ready(function() {
$('.select').initSelect({ url: '/select' })
})

盒子碰撞

  1. 反证法比较容易:列出几种不相交的情况;

高阶组件

释义:高阶组件的概念应该是来源于JavaScript的高阶函数,高阶组件仅仅只是是一个接受组件组作输入并返回组件的函数;

  1. 代理方式;
  2. 继承方式;

可控组件和不可控组件(input)

  1. 非可控组件:
    • defaultValue用于非可控组件初始化input的value值;
    • 非约束性组件的value就是原生的DOM管理的,想要获取值的时候通过ref获取dom引用来获取值;
  2. 可控组件:
    • 由React管理了组件的value;
    • input的value指定为组件state中的值;
    • onchange指定输入事件处理函数,通过改变state将改变的值反应到view上;

react key

彩蛋:一周面试记录

总结:面试不易,且工作且珍惜!
9.27: 面试初尝试,知道可能会不顺利,但是没想到打击这么大!面试官说:你的简历写的很糟糕!回公司路上坐了第一次两层公交,小安慰!
9.28: 面了一个很小的公司,但是感觉方向不对,他们要招node相关方向:动画 函数式编程 callback全局异常处理
9.30: 电话面试,感觉紧张的不行,要对自己的项目和使用过的技术要熟悉!马上十一了,先放下出去好好玩吧。
10.10: 今天约了两个面试,感觉都还行,不过算法答得很烂!
10.15: 早上面试一个小公司,但是那公司的环境真的好棒呀!可是问的好多都不会,很多回答乱答!下午面试面了一半,另一人有事~失望…都预约了,怎么这样!!!
10.16: 早上一个教室网,面试了一会儿就让回了,感觉不受尊重!下午面头条,感觉大公司环境真的不一样!面试的要求也挺高,自己很多都记不太清楚,实在是水平有限!
10.17: 面试百度,感觉面试官很nice,面了三面,但中间一面回答不好,可惜了!
最后收到了2个offer,真是意外之得,开心!

文章作者: 盛顺炎
文章链接: https://www.shengshunyan.xyz/2018/11/01/%E9%9D%A2%E8%AF%95%E6%89%8B%E8%AE%B0/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 果实的技术分享
打赏
  • 微信
    微信
  • 支付寶
    支付寶

评论