30分钟ES6从陌生到熟悉

作者: 叶小钗

前言

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

这句话基本涵盖了为什么会产生ES6这次更新的原因——编写复杂的大型应用程序。

回顾近两年的前端开发,复杂度确实在快速增加,近期不论从系统复杂度还是到前端开发人员数量应该达到了一个饱和值,换个方式说,没有ES6们的前端代码依旧可以写很多复杂的应用,而ES6的提出更好的帮们解决了很多历史遗留问题,另一个角度ES6让JS更适合开发大型应用,而不用引用太多的库了。

本文,简单介绍几个ES6核心概念,个人感觉只要掌握以下新特性便能愉快的开始使用ES6做代码了!

这里的文章,请配合着阮老师这里的教程,一些细节阮老师那边讲的好得多:http://es6.ruanyifeng.com/docs/class-extends

除了阮老师的文章还参考:http://www.infoq.com/cn/articles/es6-in-depth-arrow-functions

PS:文中只是个人感悟,有误请在评论提出

模块Module的引入

都说了复杂的大型应用了,所以们第一个要讨论的重要特性就是模块概念,们做一个复杂的项目必定需要两步走:

① 分得开,并且需要分开

② 合得起来

们普遍认为没有复杂的应用,只有分不开的应用,再复杂的应用,一旦可以使用组件化、模块化的方式分成不同的小单元,那么其难度便会大大降低,模块化是大型、复杂项目的主要拦路虎。为了解决这个问题,社区制定了一些模块加载方案,对于浏览器开发来说,们用的最多的是AMD规范,也就是大家熟知的requireJS,而ES6中在语音标准层面实现了模块功能,用以取代服务端通信的CommonJS和AMD规范,成为了通用的规范,多说无益,们这里上一段代码说明:

 1 /*
 2 validate.js 多用于表单验证
 3 */
 4 export function isEmail (text) {
 5     var reg = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
 6     return reg.test(text);
 7 }
 8 
 9 export function  isPassword (text) {
10     var reg = /^[a-zA-Z0-9]$/;
11     return reg.test(text);
12 }

那么们现在想在页面里面使用这个工具类该怎么做呢:

 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8 <!-- 请注意这里type=module才能运行 -->
 9 <script type="module">
10     import  from './validate.js';
11     var e1 = 'dddd';
12     var e2 = 'yexiaochai@qq.com'
13     console.log(isEmail(e1))
14     console.log(isEmail(e2))
15 </script>
16 </body>
17 </html>

ES6中的Module提出,在这里看来是想在官方完成之前requireJS干的工作,这里也有一些本质上的不一样:

① requireJS是使用加载script标签的方式载入js,没有什么限制

② import命令会被js引擎静态分析,先于模块其他语句执行

以上特性会直接给们带来一些困扰,比如原来们项目控制器会有这么一段代码:

1 var viewId = ''; //由浏览器获取试图id,url可能为?viewId=booking|list|...
2 //如果不存在则需要构建,记住构建时需要使用viewdata继承源view
3 requirejs(viewId, function(View) {
4     //执行根据url参数动态加载view逻辑
5 })

前面说过了,import命令会被js引擎静态分析,先于模块其他语句执行,所以们在根本不能将import执行滞后,或者动态化,做不到的,这种写法也是报错的:

if (viewId) {
  import view from './' + viewId;
}

这种设计会有利于提高编译器效率,但是之前的动态业务逻辑就不知道如何继续了?而ES6如果提供import的方法,们变可以执行逻辑:

1 import(viewId, function() {
2     //渲染页面
3 })

事实上他也提供了:

现在看起来,JS中的模块便十分完美了,至于其中一些细节,便可以用到的时候再说了

ES6中的类Class

们对们的定位一直是非常清晰的,们就是要干大项目的,们是要干复杂的项目,除了模块概念,类的概念也非常重要,们之前用的这种方式实现一个类,们来温故而知新。

当一个函数被创建时,Function构造函数产生的函数会隐式的被赋予一个prototype属性,prototype包含一个constructor对象

而constructor便是该新函数对象(constructor意义不大,但是可以帮们找到继承关系)

每个函数都会有一个prototype属性,该属性指向另一对象,这个对象包含可以由特定类型的所有实例共享的属性和方法

每次实例化后,实例内部都会包含一个[[prototype]](proto)的内部属性,这个属性指向prototype

① 们通过isPrototypeOf来确定某个对象是不是的原型
② hasOwnPrototype 可以检测一个属性是存在实例中还是原型中,该属性不是原型属性才返回true
```
var Person = function (name, age) {
    this.name = name; this.age = age; }; Person.prototype.getName = function () { return this.name; }; var y = new Person('叶小钗', 30);

为了方便,使用,们做了更为复杂的封装:

 1 var arr = [];
 2 var slice = arr.slice;
 3 
 4 function create() {
 5   if (arguments.length == 0 || arguments.length > 2) throw '参数错误';
 6 
 7   var parent = null;
 8   //将参数转换为数组
 9   var properties = slice.call(arguments);
10 
11   //如果第一个参数为类(function),那么就将之取出
12   if (typeof properties[0] === 'function')
13     parent = properties.shift();
14   properties = properties[0];
15 
16   function klass() {
17     this.initialize.apply(this, arguments);
18   }
19 
20   klass.superclass = parent;
21   klass.subclasses = [];
22 
23   if (parent) {
24     var subclass = function () { };
25     subclass.prototype = parent.prototype;
26     klass.prototype = new subclass;
27     parent.subclasses.push(klass);
28   }
29 
30   var ancestor = klass.superclass && klass.superclass.prototype;
31   for (var k in properties) {
32     var value = properties[k];
33 
34     //满足条件就重写
35     if (ancestor && typeof value == 'function') {
36       var argslist = /^\s*function\s*\(([^\(\)]*?)\)\s*?\{/i.exec(value.toString())[1].replace(/\s/i, '').split(',');
37       //只有在第一个参数为$super情况下才需要处理(是否具有重复方法需要用户自己决定)
38       if (argslist[0] === '$super' && ancestor[k]) {
39         value = (function (methodName, fn) {
40           return function () {
41             var scope = this;
42             var args = [function () {
43               return ancestor[methodName].apply(scope, arguments);
44             } ];
45             return fn.apply(this, args.concat(slice.call(arguments)));
46           };
47         })(k, value);
48       }
49     }
50 
51     klass.prototype[k] = value;
52   }
53 
54   if (!klass.prototype.initialize)
55     klass.prototype.initialize = function () { };
56 
57   klass.prototype.constructor = klass;
58 
59   return klass;
60 }

View Code

这里写一个demo:

 1 var AbstractView = create({
 2   initialize: function (opts) {
 3     opts = opts || ;
 4     this.wrapper = opts.wrapper || $('body');
 5 
 6     //事件集合
 7     this.events = ;
 8 
 9     this.isCreate = false;
10 
11   },
12   on: function (type, fn) {
13     if (!this.events[type]) this.events[type] = [];
14     this.events[type].push(fn);
15   },
16   trigger: function (type) {
17     if (!this.events[type]) return;
18     for (var i = 0, len = this.events[type].length; i < len; i++) {
19       this.events[type][i].call(this)
20     }
21   },
22   createHtml: function () {
23     throw '必须重写';
24   },
25   create: function () {
26     this.root = $(this.createHtml());
27     this.wrapper.append(this.root);
28     this.trigger('onCreate');
29     this.isCreate = true;
30   },
31   show: function () {
32     if (!this.isCreate) this.create();
33     this.root.show();
34     this.trigger('onShow');
35   },
36   hide: function () {
37     this.root.hide();
38   }
39 });
40 
41 var Alert = create(AbstractView, {
42 
43   createHtml: function () {
44     return '<div class="alert">这里是alert框</div>';
45   }
46 });
47 
48 var AlertTitle = create(Alert, {
49   initialize: function ($super) {
50     this.title = '';
51     $super();
52 
53   },
54   createHtml: function () {
55     return '<div class="alert"><h2>' + this.title + '</h2>这里是带标题alert框</div>';
56   },
57 
58   setTitle: function (title) {
59     this.title = title;
60     this.root.find('h2').html(title)
61   }
62 
63 });
64 
65 var AlertTitleButton = create(AlertTitle, {
66   initialize: function ($super) {
67     this.title = '';
68     $super();
69 
70     this.on('onShow', function () {
71       var bt = $('<input type="button" value="点击" />');
72       bt.click($.proxy(function () {
73         alert(this.title);
74       }, this));
75       this.root.append(bt)
76     });
77   }
78 });
79 
80 var v1 = new Alert();
81 v1.show();
82 
83 var v2 = new AlertTitle();
84 v2.show();
85 v2.setTitle('是标题');
86 
87 var v3 = new AlertTitleButton();
88 v3.show();
89 v3.setTitle('是标题和按钮的alert');

View Code

ES6中直接从标准层面解决了们的问题,他提出了Class关键词让们可以更好的定义类,们这里用们ES6的模块语法重新实现一次:

 1 export class AbstractView {
 2     constructor(opts) {
 3         opts = opts || ;
 4         this.wrapper = opts.wrapper || $('body');
 5         //事件集合
 6         this.events = ;
 7         this.isCreate = false;
 8     }
 9     on(type, fn) {
10         if (!this.events[type]) this.events[type] = [];
11         this.events[type].push(fn);
12     }
13     trigger(type) {
14         if (!this.events[type]) return;
15         for (var i = 0, len = this.events[type].length; i < len; i++) {
16             this.events[type][i].call(this)
17         }
18     }
19     createHtml() {
20         throw '必须重写';
21     }
22     create() {
23         this.root = $(this.createHtml());
24         this.wrapper.append(this.root);
25         this.trigger('onCreate');
26         this.isCreate = true;
27     }
28     show() {
29         if (!this.isCreate) this.create();
30         this.root.show();
31         this.trigger('onShow');
32     }
33     hide() {
34         this.root.hide();
35     }
36 }
37 export class Alert extends AbstractView {
38     createHtml() {
39         return '<div class="alert">这里是alert框</div>';
40     }
41 }
42 export class AlertTitle extends Alert {
43     constructor(opts) {
44         super(opts);
45         this.title = '';
46     }
47     createHtml() {
48         return '<div class="alert"><h2>' + this.title + '</h2>这里是带标题alert框</div>';
49     }
50     setTitle(title) {
51         this.title = title;
52         this.root.find('h2').html(title)
53     }
54 }
55 export class  AlertTitleButton extends AlertTitle {
56     constructor(opts) {
57         super(opts);
58         this.on('onShow', function () {
59             var bt = $('<input type="button" value="点击" />');
60             bt.click($.proxy(function () {
61                 alert(this.title);
62             }, this));
63             this.root.append(bt)
64         });
65     }
66 }
```
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Title</title>
 6 </head>
 7 <body>
 8 <script type="text/javascript" src="zepto.js"></script>
 9 
10 <!-- 请注意这里type=module才能运行 -->
11 <script type="module">
12 import  from './es6class.js';
13 var v1 = new Alert();
14 v1.show();
15 var v2 = new AlertTitle();
16 v2.show();
17 v2.setTitle('是标题');
18 var v3 = new AlertTitleButton();
19 v3.show();
20 v3.setTitle('是标题和按钮的alert');
21 </script>
22 </body>
23 </html>

这里的代码完成了与上面一样的功能,而代码更加的清爽了。

ES6中的函数

们这里学习ES6,由大到小,首先讨论模块,其次讨论类,这个时候理所当然到了们的函数了,ES6中函数也多了很多新特性或者说语法糖吧,首先们来说一下这里的箭头函数

箭头函数

//ES5
$('bt').click(function (e) {
    //doing something
})
//ES6
$('bt').click(e => {
    //doing something
})

有点语法糖的感觉,有一个很大不同的是,箭头函数不具有this属性,箭头函数直接使用的是外部的this的作用域,这个想不想用看个人习惯吧。

参数新特性

ES6可以为参数提供默认属性

1 function log(x, y = 'World') {
2   console.log(x, y);
3 }
4 
5 log('Hello') // Hello World
6 log('Hello', 'China') // Hello China
7 log('Hello', '') // Hello

至于不定参数撒的,这里没有多过多的使用,等项目遇到再说吧,如果研究的太细碎,反而不适合们开展工作。

let、const和var

之前的js世界里,们定义变量都是使用的var,别说还真挺好用的,虽有会有一些问题,但是对于熟悉js特性的小伙伴都能很好的解决,一般记住:变量提升会解决绝大多数问题。

就能解决很多问题,而且真实项目中,们会会避免出现变量出现重名的情况所以有时候大家面试题中看到的场景在实际工作中很少发生,只要不刻意臆想、制造一些难以判断的场景,其实并不会出现多少BUG,不能因为想考察人家对语言特性的了解,就做一些容易容易忘掉的陷阱题。

无论如何,var 声明的变量受到了一定诟病,事实上在强类型语言看来也确实是设计BUG,但是完全废弃var的使用显然不是js该做的事情,这种情况下出现了let关键词。

let与var一致用以声明变量,并且一切用var的地方都可以使用let替换,新的标准也建议大家不要再使用var了,let具有更好的作用域规则,也许这个规则是边界更加清晰了:

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1

这里是一个经典的闭包问题:

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

因为i在全局范围有效,共享同一个作用域,所以i就只有10了,为了解决这个问题,们之前会引入闭包,产生新的作用域空间(好像学名是变量对象,给忘了),但是那里的i跟这里的i已经不是一个东西了,但如果将var改成let,上面的答案是符合预期的。可以简单理解为每一次”{}“,let定义的变量都会产生新的作用域空间,这里产生了循环,所以每一次都不一样,这里与闭包有点类似是开辟了不同的空间。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

这里因为内部重新声明了i,事实上产生了3个作用域,这里一共有4个作用域指向,let最大的作用就是js中块级作用域的存在,并且内部的变量不会被外部所访问,所以之前为了防止变量侮辱的立即执行函数,似乎变得不是那么必要了。

之前们定义一个常量会采用全部大写的方式:

var NUM = 10;

为了解决这个问题,ES6引入了const命令,让们定义只读常量,这里不对细节做过多研究,直接后续项目实践吧,项目出真知。

生成器Generators

ES6中提出了生成器Generators的概念,这是一种异步编程的解决方案,可以将其理解为一种状态机,封装了多个内部状态,这里来个demo:

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// 
hw.next()
// 
hw.next()
// 
hw.next()
// 

这个yield(产出)类似于之前的return,直观的理解就是一个函数可以返回多次了,或者说函数具有"顺序状态”,yield提供了暂停功能。这里想写个代码来验证下期中的作用域状态:

function* test(){
    let i = 0;
    setTimeout(function() {
        i++;
    }, 1000);
    yield i;
    yield i++;
    return i
}

let t = test();
console.log(t.next());

setTimeout(function() {
    console.log(t.next());
}, 2000);
console.log(t.next());

//
//
//

之前们写一个城市级联的代码,可能会有些令人蛋疼:

 1 $.get('getCity', , function(province) {
 2     let pid = province[0];
 3     //根据省id获取城市数据
 4     $.get('getCity', , function(city) {
 5         let cityId = city[0];
 6         //根据市级id获取县
 7         $.get('getCity', , function(city) {
 8             //do smt.
 9         });
10     });
11 });

这个代码大家应当比较熟悉了,用promise能从语法层面解决一些问题,这里简单介绍下promise。

Promise

Promise是一种异步解决方案,有些同事认为其出现就是为了们代码变得更好看,解决回调地狱的语法糖,ES6将其写入了语音标准,提供了原生Promise对象。Promise为一容器,里面保存异步事件的结果,他是一个对象具有三个状态:pending(进行中)、fulfilled(已成功)、rejected(已失败),这里还是来个简单代码说明:

1 function timeout(ms) {
2   return new Promise((resolve, reject) => {
3     setTimeout(resolve, ms, 'done');
4   });
5 }
6 
7 timeout(100).then((value) => {
8   console.log(value);
9 });

实例化Promise时,第一个回调必须提供,是进行转为成功时候会执行,第二个也是一个函数失败时候调用,非必须,这里来个demo:

 1 let  timeout = function (ms) {
 2     return new Promise(function (resolve) {
 3         setTimeout(resolve, ms);
 4     });
 5 };
 6 
 7 timeout(1000).then(function () {
 8     return timeout(1000).then(function () {
 9         let s = '大家';
10         console.log(s)
11         return s;
12     })
13 
14 }).then(function (data) {
15     return timeout(1000).then(function () {
16         let s = data + '好,';
17         console.log(s)
18         return s;
19     })
21     return timeout(1000).then(function () {
22         let s = data + '是叶小钗';
23         console.log(s)
24         return s;
25     });
27     console.log(data)
28 });

如果们请求有依赖的话,第一个请求依赖于第二个请求,代码就可以这样写:

 1 let getData = function(url, param) {
 2     return new Promise(function (resolve) {
 3         $.get(url, param, resolve );
 4     });
 5 }
 6 getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data) {
 7     console.log('获取了省数据,们马上根据省数据申请市数据', data);
 8     return getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) {
 9         console.log(data1);
10         return '是市数据';
11     })
12 
14     //前面的参数传过来了
15     console.log(data);
16     console.log('获取了市数据,们马上根据市数据申请县数据');
17     getData('http://api.kuai.baidu.com/city/getstartcitys?callback=?').then(function (data1) {
18         console.log(data1);
19     });
20 })

如此便可以避免多层嵌套了,关于Promise的知识点还很多,们遇到复杂的工作场景再拿出来说吧,对他的定位就是一个语法糖,将异步的方式变成同步的写法,骨子里还是异步,上面们用Promise解决回调地狱问题,但是回调地狱问题遇到的不多,却发现Promise一堆then看见就有点烦,们的Generator函数似乎可以让这个情况得到缓解。

但是暂时在实际工作中没有找到更好的使用场景,这里暂时到这里,后面工作遇到再详述,对这块不是很熟悉也不妨碍们使用ES6写代码。

代理

代理,其实就是你要做什么帮你做了就行了,一般代理的原因都是,需要做点手脚,或者多点操作,或者做点"赋能”,如们常常包装setTimeout一般:

1 let timeout = function (ms, callback) {
2     setTimeout(callback, ms);
3 }

们包装setTimeout往往是为了clearTimeout的时候能全部清理掉,其实就是拦截下,ES6提供了Proxy关键词用于设置代理器:

 1 var obj = new Proxy(, {
 2   get: function (target, key, receiver) {
 3     console.log(`getting $!`);
 4     return Reflect.get(target, key, receiver);
 5   },
 6   set: function (target, key, value, receiver) {
 7     console.log(`setting $!`);
 8     return Reflect.set(target, key, value, receiver);
 9   }
10 });
11 obj.count = 1
12 //  setting count!
13 ++obj.count
14 //  getting count!
15 //  setting count!
16 //  2
```
//target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为
var proxy = new Proxy(target, handler);

们这里继续写一个简单的demo:

 1 let person = {
 2     constructor: function(name, age = 20) {
 3         this.name = name;
 4         this.age = age
 5     },
 6     addAge: function() {
 7         this.age++;
 8     },
 9     getAge: function() {
10         return this.age;
11     }
12 }
13 
14 var proxy = new Proxy(person, {
15     get: function(target, property) {
16         console.log(arguments);
17         return target[property];
18     },
19     set: function(target, property) {
20         console.log(arguments);
21     }
22 });
23 
24 person.constructor('叶小钗', 30);
25 console.log(person.age)
26 console.log(proxy.age)

但是暂时没有发现比较好的业务场景,比如说,现在重写了一个实例的get方法,便能在一个全局容器中记录这个被执行了多少次,这里一个业务场景是:一次个页面连续发出了很多次请求,但是单页应用做页面跳转时候,需要将所有的请求句柄移除,这个似乎也不是代理完成的工作,于是要使用ES6写代码,似乎可以暂时忽略代理。

结语

有了以上知识,基本从程序层面可以使用ES6写代码了,但是工程层面还需要引入webpack等工具,这些们下次介绍吧。

刷新评论 刷新页面 返回顶部

原文创作:叶小钗

原文链接:https://www.cnblogs.com/yexiaochai/p/9359943.html

更多推荐

更多
  • Camel云原生-六、将应用部署到 Kubernetes 容器被抽象成称为pod的实体。pod 由一个或多个容器以及运行这些容器的必要配置组成。这种结构是 Kubernetes 真正精心安排的。pod ...
    Apache CN

  • Camel云原生-一、欢迎来到 Apache Camel 什么是系统集成?,业务还是集成逻辑?,云原生应用,什么是 ApacheCamel?,第四的,包装应用,摘要,集成逻辑,集成路由,交流和信息,表达式语言,Java 进化,微服务,开发要求,微文件规范,运行代码,快速汽车,优步罐,容器映像,
    Apache CN

  • Camel云原生-四、使用 Apache Camel 访问数据库 关系数据库,处理异常,摘要,JPA 的持久性,使用 JPA 的参数化查询,处理,尝试捕捉最终,错误处理程序,一个例外条款, 我们在实现 API 或集成时所做的大部分工作是移动数据。我们提供数据,消费数据,转换数据,复制数据,等等。这样,
    Apache CN

  • Camel云原生-五、使用 Apache Kafka 发送消息 面向消息的中间件,ApacheKafka,Camel 和 Kafka,摘要,概念和架构,安装和运行,测试安装,设置应用,首次测试,扩大消费者规模,偏移复位,单元测试应用, 在前面的章节中,我们主要关注跨应用通信的同步方法的使用,更具体地
    Apache CN

  • Camel云原生-二、开发 REST 集成 Camel DSLs,REST 和 OpenAPI,第一个应用:REST 文件服务器,摘要,REST 接口和 OpenAPI,可读性和逻辑重用,Beans 和处理器,述语,数据格式,类型转换器, 在上一章中,向您介绍了 Apache C
    Apache CN

  • Camel云原生-三、使用 Keycloak 保护 Web 服务 访问控制,用 Keycloak 保护 REST APIs,摘要,OAuth 2.0,OpenID 连接,凯克洛克,公开联系人列表 API,配置键盘锁,配置资源服务器,使用 Camel 消费 API, 我们一直在谈论 web 服务,方法是
    Apache CN

  • Kubernetes Serverless应用-十一、运行无服务器工作负载 不断发展的软件和平台,KubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetesKubernetes,无服务器工具
    Apache CN

  • Kubernetes Serverless应用-十、无服务器考虑事项 安全最佳实践,干燥 Kubernetes,保护无服务器服务,OpenFaaS,无内胎的,function函数,Apache OpenWhisk,分裂,结论,监视 Kubernetes,仪表板,谷歌云,Microsoft Azure,摘要
    Apache CN

  • Kubernetes Serverless应用-九、查看 OpenFaaS openfans 简介,在本地运行 openfans,OpenFaaS 命令行客户端,码头工人,启动 Minikube 集群,使用 helm 安装 openfans,你好世界!,openfans 用户界面和商店,普罗米修斯,摘要,
    Apache CN

  • Kubernetes Serverless应用-八、使用 Fission 启动应用 Fission 概述,安装先决条件,安装舵,安装 Fission 命令行界面,在本地运行 Fission,使用头盔发射 Fission,处理输出,启动我们的第一个功能,留言簿,Fission 命令,Fission 功能命令,创建命令,获
    Apache CN

  • 近期文章

    更多
    文章目录

      推荐作者

      更多