-
AJAX、Fetch、axiosAJAXAJAX可以在不更新全局的情况下更新局部页面。通过在与服务器进行数据交换,可以使网页实现异步更新。AJAX的原理就是通过XHR对象来向服务器发起异步请求,从服务器获得数据,然后用JS来操作DOM更新页面。领导想找小李汇报一下工作,就委托秘书去叫小李,自己就接着做其他事情,直到秘书告诉他小李已经到了,最后小李跟领导汇报工作。Ajax请求数据流程与“领导想找小李汇报一下工作”类似,上述秘书就相当于XMLHttpRequest对象,领导相当于浏览器,响应数据相当于小李。浏览器可以发送HTTP请求后,接着做其他事情,等收到XHR返回来的数据再进行操作。创建AJAX123456789101112// 1. 创建 XMLHttpRequest 实例let xhr = XMLHttpRequest()// 2. 打开和服务器的连接xhr.open('get', 'URL')// 3.发送xhr.send()// 4. 接收变化。xhr.onreadystatechange = () => { if(xhr.readyState == 4 && xhr.status == 200){ // readyState: ajax 状态,status:http 请求状态 console.log(xhr.responseText); //响应主体 }}• 创建AJAX实例:let xhr = new XMLHttpRequest()• 打开请求,配置请求前的配置项:xhr.open([http method], [url], [async], [userName], [userPass])1. http methods 请求方式:post,get,delete,put,head,options,trace,connect2. url:想服务器请求的路径3. async:是否为异步请求4. userName、userPass:用户名与密码• 通过XMLHttpRequest.open()方法与服务器建立连接• 发送请求:XMLHttpRequest.send() 方法中如果 Ajax 请求是异步的则这个方法发送请求后就会返回,如果Ajax请求是同步的,那么请求必须知道响应后才会返回。• 通过XMLHttpRequest对象的onreadystatechange事件监听服务器端的通信状态• 接收数据并进行处理• 将处理后的结果更新到页面上AJAX的缺点:• 本是针对MVC架构,不符合前端MVVM的浪潮• 基于原生的XHR开发• 配置和调用方式混乱axios原理axios是使用promise封装的ajax,它内部有两个拦截器,分别是request拦截器和response拦截器。• 请求拦截器的作用是在请求发送之前进行一些操作,例如在每个请求体上加入token• 响应拦截器的作用是接收到响应后做的一些操作,例如登录失效后需要重新登录跳转到登录页axios的特点• 由浏览器端发起请求,在浏览器中创建XHR• 支持promise API• 监听请求和返回• 更好的格式化,自动将数据转换为json数据• 安全性更高,可抵御CSRF攻击axios常用的方法axios常用的方法有get、post、put、patch、delete等。其中get和post返回的都是promise对象,可以使用promise方法axios.get(url[, config]):get请求用于列表和信息查询1234567891011axios.get('apiURL', { param: { id: 1 } // param 中的的键值对最终会 ? 的形式,拼接到请求的链接上,发送到服务器。}).then(res => { console.log(res);}).catch( error => { console.log(error)}axios.delete(url[, config]):删除123456axios.delete('apiURL', { params: { id: 1 }, timeout: 1000})axios.post(url[, data[, config]]):post请求用于信息的添加123456789axios.post('apiURL',{ user: '小新', age: 18}).then( res => { console.log(res);}).catch( error => { console.log(error)}axios.put(url[, data[, config]]):更新操作123axios.put('apiURL', { name: '小新',})axios.patch(url[, data[, config]]):更新操作12345axios.patch('apiURL', { id: 13,},{ timeout: 1000,})put和patch的区别patch方法用来更新局部资源,假设我们有一个UserInfo,里面有userId,userName,userGender等10个字段。可你的编辑功能因为需求,在某个特别的页面里只能修改userName,这个时候就可以使用patch。put也适用于更新数据,但必须提供完整的资源对象。axios相关配置• url:用于请求服务器的url• method:请求方法,默认为get• baseURL:会自动加到url前面• proxy:用于配置代理• transformRequest:允许在服务器发送请求之前修改请求数据axios拦截器执行顺序问题• 请求拦截:axios的请求拦截器会先执行最后指定的回调函数,再依次向前执行• 响应拦截:axios的响应拦截器会先执行最先执行的回调函数,再依次向前执行例如:1234567891011121314151617181920212223242526272829axios.interceptors.request.use(config => { console.log(`请求拦截1`); return config;});axios.interceptors.request.use(config => { // 在发送请求之前做些什么 console.log(`请求拦截2`); return config;}); // 添加响应拦截器 axios.interceptors.response.use(response => { // 对响应数据做点什么 console.log(`成功的响应拦截1`); return response.data;}); // 添加响应拦截器 axios.interceptors.response.use(response => { // 对响应数据做点什么 console.log(`成功的响应拦截2`); return response;}); // 发送请求 axios.get('/posts') .then(response => { console.log('成功了'); }) 执行结果为console.log("请求拦截2");console.log("请求拦截1");console.log("成功的响应拦截1");console.log("成功的响应拦截2");console.log("成功了");为什么axios中需要拦截器在SPA应用中,通常会使用token进行用户身份认证,这就要求每次请求必须携带用户的身份信息,针对这个需求,为了避免在每个请求中单独处理,我们可以通过封装统一的request函数来为每隔请求统一添加token信息。但如果想为某些请求添加缓存时间或者控制某些请求的调用频率的话,我们就需要不断地修改request函数来扩展对应的功能。此时,如果在考虑对响应进行统一处理,我们的request函数将变得越来越庞大,也越来越难维护。所以axios为我们提供了拦截器。为什么请求拦截2会在请求拦截1之前执行呢?在axios源码中将发送请求分为了请求拦截器、发送请求、响应拦截器、相应回调,通过Promise的链式调用将这些部分结合起来了,这样就得到了发送请求拿到数据的全部过程。下面分析源码:• 代码开始构建了一个config配置对象,用于第一次执行Promise返回一个成功的Promise• 最核心的数组chain,这个数组中保存了请求拦截器、响应拦截器和发送请求函数。该数组中间放的是发送请求的函数,左边放的是请求拦截器,右边放的是响应拦截器。在第一步中返回的Promise对象,将遍历chain数组逐一执行里面的函数,并返回新的Promise对象• 往数组中添加请求拦截函数,依照axios请求的执行顺序,请求拦截器应该在发送请求之前执行,故应该添加在发送请求函数的前面,使用unshift方法• 往数组中添加响应拦截器函数,依照axios请求的执行顺序,响应拦截器应该在发送请求之后执行,故应该添加在发送请求函数的后面,所以使用的是数组的push方法• Promise遍历执行,每次从chain中取出两个 函数执行(一个成功回调,一个失败回调)• 最后返回一个Promise对象,用于执行响应数据的回调fetchfetch是http请求数据的方式,它使用Promise,但不使用回调函数。fetch采用模块化设计,通过数据流处理数据,对于请求大文件或网速慢的情况相当有用。默认情况下fetch不会接收或发送cookies。优点:• 采用模块化思想,将输入、输出、状态跟踪分离• 基于promise,返回一个promise对象缺点:• 过于底层,有很多状态码没有进行封装• 无法阻断请求• 兼容性差无法检测请求进度Fetch、ajax与axios的区别• 传统的ajax利用的是HMLHttpRequest这个对象,和后端进行交互。• 而JQury ajax是对原生XHR的封装,多请求间有嵌套的话就会出现回调地狱的问题。• axios使用promise封装XHR,解决了回调地狱的问题。而Fetch没有使用XHR,使用的是promiseFetch和Ajax比有什么优点Fetch使用的是promise,方便使用异步,没有回调地狱的问题。总结Ajax是一种web数据交互的方式,它可以使页面在不重新加载的情况下请求数据并进行局部更新,它内部使用了XHR来进行异步请求。Ajax在使用XHR发起异步请求时得到的是XML格式的数据,如果想要JSON格式,需要进行额外的转换;Ajax本身针对的是MVC框架,不符合现在的MVVM架构;Ajax有回调地狱问题;Ajax的配置复杂而Fetch是XHR的代替品,它基于Promise实现的,并且不使用回调函数,它采用模块化结构设计,并使用数据流进行传输,对于大文件和网速慢的情况非常友好。但是Fetch不会对请求和响应进行监听;不能阻断请求;过于底层,对一些状态码没有封装;兼容性差。axios是基于Promise对XHR进行封装,它内部封装了两个拦截器,分别是请求拦截器和响应拦截器。请求拦截器用于在请求发出之前进行一些操作,比如:设置请求体,携带Cookie、token等;响应拦截器用于在得到响应后进行一些操作,比如:登录失效后跳转到登录页面重新登录。axios有get、post、put、patch、delete等方法。axios可以对请求和响应进行监听;返回Promise对象,可以使用Promise的API;返回JSON格式的数据;由浏览器发起请求;安全性更高,可以抵御CSRF攻击。axios源码分析axios的执行流程• 使用axios.create创建单独的实例,或直接使用axios实例• 对于axios调用进入到request()中进行处理• 执行请求拦截器• 请求数据转换器,将传入的数据进行处理,比如JSON.stringify(data)• 执行适配器,判断是浏览器端还是node端,以执行不同的方法• 响应数据转换器,对服务器端的数据进行处理,比如JSON.parse(data)• 执行响应拦截器,对服务器端数据进行处理,比如token失效跳转到登录页• 返回数据入口文件(lib/axios.js)导出的axios就是 实例化后的对象,还在其上挂载create方法,以供创建独立的实例,实现实例之间互不影响。12345678910111213// 创建实例过程的方法function createInstance(defaultConfig) { return instance;}// 实例化var axios = createInstance(defaults); // 创建独立的实例,隔离作用域axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig));};// 导出实例module.exports = axios;createInstance()123456789101112131415161718function createInstance(defaultConfig) { // 实例化,创建一个上下文 var context = new Axios(defaultConfig); // 平时调用的 get/post 等等请求,底层都是调用 request 方法 // 将 request 方法的 this 指向 context(上下文),形成新的实例 var instance = bind(Axios.prototype.request, context); // Axios.prototype 上的方法 (get/post...)挂载到新的实例 instance 上, // 并且将原型方法中 this 指向 context utils.extend(instance, Axios.prototype, context); // Axios 属性值挂载到新的实例 instance 上 // 开发中才能使用 axios.default/interceptors utils.extend(instance, context); return instance;}createInstance执行流程:• 通过构造函数Axios创建实例context,作为下面request方法的上下文(this指向)• 将Axios.prototype.request方法作为实例使用,并把this指向context,形成新的实例instance• 将构造函数Axios.prototype上的方法挂载到新的实例instance上,然后将原型各个方法中的this指向context,这样才能使用get、post等方法• 将Axios的属性挂载到instance上可以看到axios不是简单的创建实例context,而是在context上进行this绑定形成新的实例,然后将Axios属性和请求方法挂载到新的实例上拦截器(lib/core/InterceptorManager.js)拦截器涉及一个属性和三个方法:• handler:存放use注册的回调函数• use:注册成功和失败的回调函数• eject:删除注册过的函数• forEach:遍历回调函数123456789101112131415161718192021222324252627282930function InterceptorManager() { // 存放 use 注册的回调函数 this.handlers = [];} InterceptorManager.prototype.use = function use(fulfilled, rejected, options) { // 注册成功和失败的回调函数 this.handlers.push({ fulfilled: fulfilled, rejected: rejected, ... }); return this.handlers.length - 1;}; InterceptorManager.prototype.eject = function eject(id) { // 删除注册过的函数 if (this.handlers[id]) { this.handlers[id] = null; }}; InterceptorManager.prototype.forEach = function forEach(fn) { // 遍历回调函数,一般内部使用多 utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } });};dispatchRequest(lib/core/dispatchRequest.js)dispatchRequest主要做了以下操作:• transformRequest: 对 config 中的 data 进行加工,比如对 post 请求的 data 进行字符串化(JSON.stringify(data))• adapter:适配器,包含浏览器端 xhr 和 node 端的 http• transformResponse: 对服务端响应的数据进行加工,比如 JSON.parse(data)取消请求(lib/cancel/CancelToken.js)12345678910111213var CancelToken = axios.CancelToken;var source = CancelToken.source();axios.get('/user/12345', { cancelToken: source.token}).catch(function(thrown) { if (axios.isCancel(thrown)) { console.log('Request canceled', thrown.message); } else { // 处理错误 }});// 取消请求(message 参数是可选的)source.cancel('Operation canceled by the user.');• CancelToken 挂载 source 方法用于创建自身实例,并且返回 {token, cancel}• token 是构造函数 CancelToken 的实例,cancel 方法接收构造函数 CancelToken 内部的一个 cancel 函数,用于取消请求• 创建实例中,有一步是创建处于 pengding 状态的 promise,并挂在实例方法上,外部通过参数 cancelToken 将实例传递进 axios 内部,内部调用 cancelToken.promise.then 等待状态改变• 当外部调用方法 cancel 取消请求,pendding 状态就变为 resolve,即取消请求并且抛出 reject(message)总结• 为了支持 axios() 简洁写法,内部使用 request 函数作为新实例• 使用 promsie 链式调用的巧妙方法,解决顺序调用问题• 数据转换器方法使用数组存放,支持数据的多次传输与加工• 适配器通过兼容浏览器端和 node 端,对外提供统一 api• 取消请求这块,通过外部保留 pendding 状态,控制 promise 的执行时机到此这篇关于一文掌握ajax、fetch和axios的区别对比的文章就介绍到这了转载自https://www.jb51.net/article/250151.htm
-
【转载华为云社区】Promise 是异步编程的一种解决方案。Promise/** * 属性 */ Promise.length Promise.prototype /** * 方法 */ Promise.all(iterable) // 所有成功触发成功 任何失败触发失败 Promise.race(iterable) // 任意一个成功或失败后触发 Promise.reject(reason) Promise.resolve(value) /** * 原型 */ Promise.prototype.constructor //方法 Promise.prototype.catch(onRejected) Promise.prototype.then(onFulfilled, onRejected) Promise.prototype.finally(onFinally)Promise 有三种状态pending: 初始状态,既不是成功,也不是失败状态。resolve: 意味着操作成功完成。(resoloved)reject: 意味着操作失败。pendingpending 是初始状态,执行 resolve/reject 会进入对应状态,如果不执行,责一直为 pending 状态例如下面代码,promise 将一直在 pending 状态,不会执行 then/catch.new Promise(function (resolve, reject) { }) .then(res => console.log(res)) .catch(err => console.log(err))resolveresolve 意味着操作成功完成, 如果有 .then,值会传入 .then 的第一个参数函数里。如new Promise(function (resolve, reject) { resolve(1) }) .then(res => console.log(res))then 的第一个参数是成功的回调,第一个参数的返回值会影响接下来链的去向。第一个参数的返回值一般有三种情况无返回值:会去执行下一个 .then ,没有参数返回值非promise:调用下一个then的函数,参数为返回值返回值为promise:根据promise的执行结果,执行 下一个then/catch,如果一直是pending,则不执行下一个then/catch例如想要在当前 then 终止,可以这样操作: .then((res) => new Promise(() => {}))rejectreject 意味着操作失败。使用 .catch 会捕获到错误信息。与代码报错(如 undefined.a)不同的是, 代码报错如果不使用 catch 捕获,会向外传递,最终传递到根结点;而 reject 属于 promise 错误,即使不使用 catch 捕获也不会对全局有影响。用 promise 实现 fetch先来看几个问题:如果请求 code 404, 会走 then 还是 catch? (答案:then)控制台能看到一行 404 的错误, 为什么还是走 then 不是 catch 呢如果请求跨域失败,走 then 还是 catch?(答案:catch)同样是控制台看到错误,两者有什么区别呢?跨域失败的报错, 和 then 中 undefined.a 报错,如果都不 catch,后者在 react 脚手架开发环境页面会蹦,两者有什么区别?带着这几个问题,来看看 fetch。fetch 返回值是 promise,所以有三种状态 pending、resolve、reject.pending: 请求中resolve: 请求成功(code 200/404/500 等, 非 200 控制台输出错误)reject: 请求失败(跨域失败、连接超时、无网络等,控制台输出错误)我们还发现,请求失败时,只能 catch 到最后一行错误, 如图捕获后 为什么 404 在控制台看到错误,还走 then, resolve 如何实现实现有几个难点,throw 后面代码不会执行;先报错,后执行 then;catch 后错误不会打印在控制台;试了下,Promise.reject('xxx') 这样的报错方式虽然是微观任务,但是总是在.then之后才在控制台输出,更像是宏观任务。所以也加个setTImeout宏观任务调至后面。var fetch = function () { return new Promise(function (resolve, reject) { setTimeout(function () { if ('请求成功 200') { resolve('Response数据结构'); } else if ('请求成功 404,500等') { Promise.reject('GET xxxxxxxx 404'); setTimeout(function () { resolve('Response数据结构'); }); } }) }) }请求失败 例如跨域失败 reject 如何实现呢同样加个 setTimeoutvar fetch = function () { return new Promise(function (resolve, reject) { setTimeout(function () { if ('请求成功 200') { resolve('Response数据结构'); } else if ('请求成功 404,500等') { Promise.reject('GET xxxxxxxx 404'); setTimeout(function () { resolve('Response数据结构'); }); } else if ('请求失败') { Promise.reject('Access to fetch xxxxx with CORS disabled.'); Promise.reject('GET xxxxx net::ERR_FAILED'); setTimeout(function () { reject('TypeError: Failed to fetch'); }); } }) }) }还是有些问题,我们实现的因为在promise 中,错误会有前缀 Uncaught (in promise)。浏览器客户端应该有更好的实现方式。最后总结一下 fetch 的三种情况pending: 请求中resolve: 请求成功(code 200: 调用 resolve 返回数据; code: 404/500 等, 先抛错,再调用 resolve 返回数据。)reject: 请求失败(跨域失败、连接超时、无网络等,先控制台抛错,再调用 reject)抛错均不影响代码执行,与 undefined.a 不同。whosmeya.com文章来源: www.cnblogs.com,作者:whosmeya,版权归原作者所有,如需转载,请联系作者。原文链接:https://www.cnblogs.com/whosmeya/p/13189761.html
-
var code = HWH5.getAuthCode(); //alert(code); console.log(code);返回:Promise {}__proto__: Promisecatch: function()constructor: function()finally: function()then: function()__proto__: Object__defineGetter__: function()__defineSetter__: function()__lookupGetter__: function()__lookupSetter__: function()__proto__: nullconstructor: function()hasOwnProperty: function()isPrototypeOf: function()propertyIsEnumerable: function()toLocaleString: function()toString: function()valueOf: function()__proto__: null求救!
-
Javascript有很多强大的功能,其中一个就是它可以轻松的搞定异步编程。Node.js用回调函数代替了事件,使异步编程在js领域更加流行。但当更多的程序开始使用异步编程时,事件和回调函数确不能满足开发者想要做的所有事情,而Promise就是这些问题的解决方案。 Promise Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。 多么强大的东西都有浏览器兼容性的问题: 优点和缺点 优点:可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 缺点:首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 Promise 的生命周期 每一个Promise都会经历一个短暂的生命周期:先是处于进行中pending(Promise对象的初始状态,等到任务的完成或者被拒绝)状态,此时操作未完成,所以它也是未处理的;一旦异步操作执行结束,promise就会进入以下两种状态中的一种:Resolved又称fulfilled(任务执行完成并且成功的状态);Rejected(任务执行完成并且失败的状态)。 Promise的状态只可能从Pending状态转到Resolved状态或者Rejected状态,而且不能逆向转换,同时Resolved状态和Rejected状态也不能相互转换。 Then()方法 所有的Promise都有then()方法,且同一个Promise对象可以注册多个then方法,它接受两个可选参数:第一个是当Promise的状态变为resolved时要调用的函数;第而个是当Promise的状态变为rejected时要调用的函数。注意事项:如果省略这两个参数,或者提供非函数,那么将创建一个没有其他处理程序的新Promise,只是采用 Promise 的最终状态,then() 被调用。 如果省略第一个参数或提供的不是函数,创建的新 Promise 简单地采用 Promise 的完成状态,then()被调用(如果它变为完成)。 如果省略第二个参数或提供的不是函数,创建的新 Promise 简单地采用 Promise 的拒绝状态,then()被调用(如果它被拒绝)。 Then()方法返回的是一个新的promise对象,因此可以采用链式写法; 下面这个例子使用then方法依次指定了两个回调函数,第一个函数执行完,执行第二个回调函数,实现依次打印1,2,3 catch()方法 Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法。Promise.catch()方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。 上面代码中,promise抛出一个错误,就被catch()方法指定的回调函数捕获。 一般总是建议,Promise 对象后面要跟catch()方法,这样可以处理 Promise 内部发生的错误。Catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。 将其他对象变为Promise对象 promise.resovle()和Promise.reject()方法,可以将不是Promise对象作为参数,返回一个Promise对象。不同的是Promise.resovle()返回的是完成态的Promise,Promise.reject()创建已拒绝的Promise。 Promise.resovle()和Promise.reject()方法都可以接受非Promise的thenable对象作为参数。如果传入一个非Promise的Thenable对象,返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则以该值为成功状态返回promise对象; 有两种情形: 1.假设传入的参数没有一个.then方法,那么这个返回的Promise对象变成了resolve状态,其resolve的值就是这个对象本身。 2.假设传入的参数带有一个then方法(称为thenable对象), 那么将这个对象的类型变为Promise,其then方法变成Promise.prototype.then方法。 响应多个Promise Promise有一个"静态方法"——Promise.all(注意并非是promise.prototype), 这个方法接受一个元素是Promise对象的数组。这个方法也返回一个Promise对象,如果数组中所有的Promise对象都resolve了,那么这些resolve的值将作为一个数组作为Promise.al()l这个方法的返回值的(Promise对象)的resolve值,之后可以被then方法处理。如果数组中任意的Promise被reject,那么该reject的值就是Promise.al()l方法的返回值的reject值。then方法的第一个回调函数接收的resolve值(如上所述,是一个数组)的顺序和Promise.all中参数数组的顺序一致,而不是按时间顺序排序。 还有一个和Promise.all()相类似的方法Promise.race(),它同样接收一个数组,只不过它只接受第一个被resolve的值。 Promise的兴起,解决了在异步方法调用中,会出现回调函数一环扣一环的情况。不仅代码写起来美观,而且问题复杂的时候,阅读代码的人也容易以理解。 [color=rgb(51,51,51)]
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签