我們將構(gòu)建一個(gè)簡(jiǎn)單卻真實(shí)的評(píng)論框,你可以將它放入你的博客,類似disqus、livefyre、facebook提供的實(shí)時(shí)評(píng)論的基礎(chǔ)版。
我們將提供以下內(nèi)容:
一個(gè)展示所有評(píng)論的視圖
一個(gè)提交評(píng)論的表單
用于構(gòu)建自定制后臺(tái)的接口鏈接(hooks)
同時(shí)也包含一些簡(jiǎn)潔的特性:
評(píng)論體驗(yàn)優(yōu)化: 評(píng)論在保存到服務(wù)器之前就展現(xiàn)在評(píng)論列表,因此用戶體驗(yàn)很快。
實(shí)時(shí)更新: 其他用戶的評(píng)論將會(huì)實(shí)時(shí)展示。
Markdown格式: 用戶可以使用MarkDown格式來(lái)編輯文本。
雖然它不是入門教程的必需品,但接下來(lái)我們會(huì)添加一個(gè)功能,發(fā)送 POST
ing請(qǐng)求到服務(wù)器。如果這是你熟知的事并且你想創(chuàng)建你自己的服務(wù)器,那么就這樣干吧。而對(duì)于另外的一部分人,為了讓你集中精力學(xué)習(xí),而不用擔(dān)憂服務(wù)器端方面,我們已經(jīng)用了以下一系列的語(yǔ)言編寫了簡(jiǎn)單的服務(wù)器代碼 - JavaScript(使用 Node.js),Python和Ruby。所有代碼都在GitHub。你可以查看代碼或者下載 zip 文件來(lái)開始學(xué)習(xí)。
開始使用下載的教程,只需開始編輯 public/index.html
。
在這個(gè)教程里面,我們將使用放在 CDN 上預(yù)構(gòu)建好的 JavaScript 文件。打開你最喜歡的編輯器,創(chuàng)建一個(gè)新的 HTML 文檔:
<!-- index.html --> <html> <head> <title>Hello React</title> <script src="http://fb.me/react- {{site.react_version}}.js" rel="external nofollow" ></script> <script src="http://fb.me/JSXTransformer- {{site.react_version}}.js" rel="external nofollow" ></script> <script src="http://code.jquery.com/ jquery-1.10.0.min.js" rel="external nofollow" ></script> </head> <body> <div id="content"></div> <script type="text/jsx"> // Your code here </script> </body> </html>
在本教程其余的部分,我們將在此 script 標(biāo)簽中編寫我們的 JavaScript 代碼。
注意:
因?yàn)槲覀兿牒?jiǎn)化 ajax 請(qǐng)求代碼,所以在這里引入 jQuery,但是它對(duì) React 并不是必須的。
React 中全是模塊化、可組裝的組件。以我們的評(píng)論框?yàn)槔覀儗⒂腥缦碌慕M件結(jié)構(gòu):
- CommentBox - CommentList - Comment - CommentForm
讓我們構(gòu)造 CommentBox
組件,它只是一個(gè)簡(jiǎn)單的 <div>
而已:
// tutorial1.jsvar CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> Hello, world! I am a CommentBox. </div> ); } }); React.render( <CommentBox />, document.getElementById('content') );
首先你注意到 JavaScript 代碼中 XML 式的語(yǔ)法語(yǔ)句。我們有一個(gè)簡(jiǎn)單的預(yù)編譯器,用于將這種語(yǔ)法糖轉(zhuǎn)換成純的 JavaScript 代碼:
// tutorial1-raw.jsvar CommentBox = React.createClass({displayName: 'CommentBox', render: function() { return ( React.createElement('div', {className: "commentBox"}, "Hello, world! I am a CommentBox." ) ); } }); React.render( React.createElement(CommentBox, null), document.getElementById('content') );
JSX 語(yǔ)法是可選的,但是我們發(fā)現(xiàn) JSX 語(yǔ)句比純 JavaScript 更加容易使用。閱讀更多關(guān)于 JSX 語(yǔ)法的文章。
我們通過(guò) JavaScript 對(duì)象傳遞一些方法到 React.createClass()
來(lái)創(chuàng)建一個(gè)新的React組件。其中最重要的方法是render
,該方法返回一顆 React 組件樹,這棵樹最終將會(huì)渲染成 HTML。
這個(gè) <div>
標(biāo)簽不是真實(shí)的DOM節(jié)點(diǎn);他們是 React div
組件的實(shí)例。你可以認(rèn)為這些就是React知道如何處理的標(biāo)記或者一些數(shù)據(jù)。React 是安全的。我們不生成 HTML 字符串,因此默認(rèn)阻止了 XSS 攻擊。
你沒(méi)有必要返回基本的 HTML。你可以返回一個(gè)你(或者其他人)創(chuàng)建的組件樹。這就使得 React 變得組件化:一個(gè)關(guān)鍵的前端維護(hù)原則。
React.render()
實(shí)例化根組件,啟動(dòng)框架,注入標(biāo)記到原始的 DOM 元素中,作為第二個(gè)參數(shù)提供。
讓我們?yōu)?nbsp;CommentList
和 CommentForm
構(gòu)建骨架,這也會(huì)是一些簡(jiǎn)單的 <div>
:
// tutorial2.jsvar CommentList = React.createClass({ render: function() { return ( <div className="commentList"> Hello, world! I am a CommentList. </div> ); } });var CommentForm = React.createClass({ render: function() { return ( <div className="commentForm"> Hello, world! I am a CommentForm. </div> ); } });
下一步,更新 CommentBox
組件,使用這些新的組件:
// tutorial3.jsvar CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList /> <CommentForm /> </div> ); } });
注意我們是如何混合 HTML 標(biāo)簽和我們創(chuàng)建的組件。HTML 組件就是普通的 React 組件,就像你定義的一樣,只有一點(diǎn)不一樣。JSX 編譯器會(huì)自動(dòng)重寫 HTML 標(biāo)簽為 React.createElement(tagName)
表達(dá)式,其它什么都不做。這是為了避免全局命名空間污染。
讓我們創(chuàng)建我們的第三個(gè)組件,Comment
。我們想傳遞給它作者名字和評(píng)論文本,以便于我們能夠?qū)γ恳粋€(gè)獨(dú)立的評(píng)論重用相同的代碼。首先讓我們添加一些評(píng)論到 CommentList
:
// tutorial4.jsvar CommentList = React.createClass({ render: function() { return ( <div className="commentList"> <Comment author="Pete Hunt">This is one comment</Comment> <Comment author="Jordan Walke">This is *another* comment</Comment> </div> ); } });
請(qǐng)注意,我們已經(jīng)從父節(jié)點(diǎn) CommentList
組件傳遞給子節(jié)點(diǎn) Comment
組件一些數(shù)據(jù)。例如,我們傳遞了 Pete Hunt(通過(guò)一個(gè)屬性)和 This is one comment (通過(guò)類似于XML的子節(jié)點(diǎn))給第一個(gè) Comment
。從父節(jié)點(diǎn)傳遞到子節(jié)點(diǎn)的數(shù)據(jù)稱為 props,是屬性(properties)的縮寫。
讓我們創(chuàng)建評(píng)論組件。通過(guò) props,就能夠從中讀取到從 CommentList
傳遞過(guò)來(lái)的數(shù)據(jù),然后渲染一些標(biāo)記:
// tutorial5.jsvar Comment = React.createClass({ render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> {this.props.children} </div> ); } });
在 JSX 中通過(guò)將 JavaScript 表達(dá)式放在大括號(hào)中(作為屬性或者子節(jié)點(diǎn)),你可以生成文本或者 React 組件到節(jié)點(diǎn)樹中。我們?cè)L問(wèn)傳遞給組件的命名屬性作為 this.props
的鍵,任何內(nèi)嵌的元素作為 this.props.children
。
Markdown 是一種簡(jiǎn)單的格式化內(nèi)聯(lián)文本的方式。例如,用星號(hào)包裹文本將會(huì)使其強(qiáng)調(diào)突出。
首先,添加第三方的 Showdown 庫(kù)到你的應(yīng)用。這是一個(gè)JavaScript庫(kù),處理 Markdown 文本并且轉(zhuǎn)換為原始的 HTML。這需要在你的頭部添加一個(gè) script 標(biāo)簽(我們已經(jīng)在 React 操練場(chǎng)上包含了這個(gè)標(biāo)簽):
<!-- index.html --> <head> <title>Hello React</title> <script src="http://fb.me/react-{{site.react_version}}.js" rel="external nofollow" ></script> <script src="http://fb.me/JSXTransformer-{{site.react_version}}.js" rel="external nofollow" ></script> <script src="http://code.jquery.com/jquery-1.10.0.min.js" rel="external nofollow" ></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js" rel="external nofollow" ></script> </head>
下一步,讓我們轉(zhuǎn)換評(píng)論文本為 Markdown 格式,然后輸出它:
// tutorial6.jsvar converter = new Showdown.converter();var Comment = React.createClass({ render: function() { return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> {converter.makeHtml(this.props.children.toString())} </div> ); } });
我們?cè)谶@里唯一需要做的就是調(diào)用 Showdown 庫(kù)。我們需要把this.props.children
從 React 的包裹文本轉(zhuǎn)換成 Showdown 能處理的原始的字符串,所以我們顯示地調(diào)用了toString()
。
但是這里有一個(gè)問(wèn)題!我們渲染的評(píng)論在瀏覽器里面看起來(lái)像這樣:“<p>
This is <em>
another</em>
comment</p>
”。我們想這些標(biāo)簽真正地渲染成 HTML。
那是 React 在保護(hù)你免受 XSS 攻擊。這里有一種方法解決這個(gè)問(wèn)題,但是框架會(huì)警告你別使用這種方法:
// tutorial7.jsvar converter = new Showdown.converter();var Comment = React.createClass({ render: function() { var rawMarkup = converter.makeHtml(this.props.children.toString()); return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author} </h2> <span dangerouslySetInnerHTML={{"{{"}}__html: rawMarkup}} /> </div> ); } });
這是一個(gè)特殊的 API,故意讓插入原始的 HTML 變得困難,但是對(duì)于 Showdown,我們將利用這個(gè)后門。
記住: 使用這個(gè)功能,你會(huì)依賴于 Showdown 的安全性。
到目前為止,我們已經(jīng)在源代碼里面直接插入了評(píng)論數(shù)據(jù)。相反,讓我們渲染一小塊JSON數(shù)據(jù)到評(píng)論列表。最終,數(shù)據(jù)將會(huì)來(lái)自服務(wù)器,但是現(xiàn)在,寫在你的源代碼中:
// tutorial8.jsvar data = [ {author: "Pete Hunt", text: "This is one comment"}, {author: "Jordan Walke", text: "This is *another* comment"} ];
我們需要用一種模塊化的方式將數(shù)據(jù)傳入到 CommentList
。修改 CommentBox
和 React.render()
方法,通過(guò) props 傳遞數(shù)據(jù)到 CommentList
:
// tutorial9.js var CommentBox = React.createClass({ render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.props.data} /> <CommentForm /> </div> ); } }); React.render( <CommentBox data={data} />, document.getElementById('content') );
現(xiàn)在數(shù)據(jù)在 CommentList
中可用了,讓我們動(dòng)態(tài)地渲染評(píng)論:
// tutorial10.jsvar CommentList = React.createClass({ render: function() { var commentNodes = this.props.data.map(function (comment) { return ( <Comment author={comment.author}> {comment.text} </Comment> ); }); return ( <div className="commentList"> {commentNodes} </div> ); } });
就是這樣!
讓我們用一些從服務(wù)器獲取的動(dòng)態(tài)數(shù)據(jù)替換硬編碼的數(shù)據(jù)。我們將移除數(shù)據(jù)屬性,用獲取數(shù)據(jù)的URL來(lái)替換它:
// tutorial11.jsReact.render( <CommentBox url="comments.json" />, document.getElementById('content') );
這個(gè)組件和前面的組件是不一樣的,因?yàn)樗仨氈匦落秩咀约?。該組件將不會(huì)有任何數(shù)據(jù),直到請(qǐng)求從服務(wù)器返回,此時(shí)該組件或許需要渲染一些新的評(píng)論。
到目前為止,每一個(gè)組件都根據(jù)自己的 props 渲染了自己一次。props
是不可變的:它們從父節(jié)點(diǎn)傳遞過(guò)來(lái),被父節(jié)點(diǎn)“擁有”。為了實(shí)現(xiàn)交互,我們給組件引進(jìn)了可變的 state。this.state
是組件私有的,可以通過(guò)調(diào)用this.setState()
來(lái)改變它。當(dāng)狀態(tài)更新之后,組件重新渲染自己。
render()
methods are written declaratively as functions of this.props
and this.state
. 框架確保UI始終和輸入保持一致。
當(dāng)服務(wù)器獲取數(shù)據(jù)的時(shí)候,我們將會(huì)用已有的數(shù)據(jù)改變?cè)u(píng)論。讓我們給 CommentBox
組件添加一個(gè)評(píng)論數(shù)組作為它的狀態(tài):
// tutorial12.jsvar CommentBox = React.createClass({ getInitialState: function() { return {data: []}; }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
getInitialState()
在組件的生命周期中僅執(zhí)行一次,設(shè)置組件的初始化狀態(tài)。
當(dāng)組件第一次創(chuàng)建的時(shí)候,我們想從服務(wù)器獲取(使用GET方法)一些JSON數(shù)據(jù),更新狀態(tài),反映出最新的數(shù)據(jù)。在真實(shí)的應(yīng)用中,這將會(huì)是一個(gè)動(dòng)態(tài)功能點(diǎn),但是對(duì)于這個(gè)例子,我們將會(huì)使用一個(gè)靜態(tài)的JSON文件來(lái)使事情變得簡(jiǎn)單:
// tutorial13.json[ {"author": "Pete Hunt", "text": "This is one comment"}, {"author": "Jordan Walke", "text": "This is *another* comment"} ]
我們將會(huì)使用jQuery幫助發(fā)出一個(gè)一步的請(qǐng)求到服務(wù)器。
注意:因?yàn)檫@會(huì)變成一個(gè)AJAX應(yīng)用,你將會(huì)需要使用一個(gè)web服務(wù)器來(lái)開發(fā)你的應(yīng)用,而不是一個(gè)放置在你的文件系統(tǒng)上面的一個(gè)文件。如上所述,我們已經(jīng)在GitHub上面提供了幾個(gè)你可以使用的服務(wù)器。這些服務(wù)器提供了你學(xué)習(xí)下面教程所需的功能。
// tutorial13.jsvar CommentBox = React.createClass({ getInitialState: function() { return {data: []}; }, componentDidMount: function() { $.ajax({ url: this.props.url, dataType: 'json', success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
在這里,componentDidMount
是一個(gè)在組件被渲染的時(shí)候React自動(dòng)調(diào)用的方法。動(dòng)態(tài)更新的關(guān)鍵點(diǎn)是調(diào)用this.setState()
。我們把舊的評(píng)論數(shù)組替換成從服務(wù)器拿到的新的數(shù)組,然后UI自動(dòng)更新。正是有了這種響應(yīng)式,一個(gè)小的改變都會(huì)觸發(fā)實(shí)時(shí)的更新。這里我們將使用簡(jiǎn)單的輪詢,但是你可以簡(jiǎn)單地使用WebSockets或者其它技術(shù)。
// tutorial14.jsvar CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } }); React.render( <CommentBox url="comments.json" pollInterval={2000} />, document.getElementById('content') );
我們?cè)谶@里所做的就是把AJAX調(diào)用移到一個(gè)分離的方法中去,組件第一次加載以及之后每隔兩秒鐘,調(diào)用這個(gè)方法。嘗試在你的瀏覽器中運(yùn)行,然后改變comments.json
文件;在兩秒鐘之內(nèi),改變將會(huì)顯示出來(lái)!
現(xiàn)在是時(shí)候構(gòu)造表單了。我們的CommentForm
組件應(yīng)該詢問(wèn)用戶的名字和評(píng)論內(nèi)容,然后發(fā)送一個(gè)請(qǐng)求到服務(wù)器,保存這條評(píng)論。
// tutorial15.jsvar CommentForm = React.createClass({ render: function() { return ( <form className="commentForm"> <input type="text" placeholder="Your name" /> <input type="text" placeholder="Say something..." /> <input type="submit" value="Post" /> </form> ); } });
讓我們使表單可交互。當(dāng)用戶提交表單的時(shí)候,我們應(yīng)該清空表單,提交一個(gè)請(qǐng)求到服務(wù)器,然后刷新評(píng)論列表。首先,讓我們監(jiān)聽表單的提交事件和清空表單。
// tutorial16.jsvar CommentForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if (!text || !author) { return; } // TODO: send request to the server this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function() { return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /> <input type="text" placeholder="Say something..." ref="text" /> <input type="submit" value="Post" /> </form> ); } });
React使用駝峰命名規(guī)范的方式給組件綁定事件處理器。我們給表單綁定一個(gè)onSubmit
處理器,用于當(dāng)表單提交了合法的輸入后清空表單字段。
在事件回調(diào)中調(diào)用preventDefault()
來(lái)避免瀏覽器默認(rèn)地提交表單。
我們利用Ref
屬性給子組件命名,this.refs
引用組件。我們可以在組件上調(diào)用getDOMNode()
獲取瀏覽器本地的DOM元素。
當(dāng)用戶提交評(píng)論的時(shí)候,我們需要刷新評(píng)論列表來(lái)加進(jìn)這條新評(píng)論。在CommentBox
中完成所有邏輯是合適的,因?yàn)?code style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; padding: 2px 4px; color: rgb(45, 133, 202); background-color: rgb(249, 242, 244);">CommentBox擁有代表評(píng)論列表的狀態(tài)(state)。
我們需要從子組件傳回?cái)?shù)據(jù)到它的父組件。我們?cè)诟附M件的render
方法中做這件事:傳遞一個(gè)新的回調(diào)函數(shù)(handleCommentSubmit
)到子組件,綁定它到子組件的onCommentSubmit
事件上。無(wú)論事件什么時(shí)候觸發(fā),回調(diào)函數(shù)都將會(huì)被調(diào)用:
// tutorial17.jsvar CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleCommentSubmit: function(comment) { // TODO: submit to the server and refresh the list }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } });
當(dāng)用戶提交表單的時(shí)候,讓我們?cè)?code style="box-sizing: border-box; font-family: Menlo, Monaco, Consolas, 'Courier New', monospace; font-size: 13px; border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; padding: 2px 4px; color: rgb(45, 133, 202); background-color: rgb(249, 242, 244);">CommentForm中調(diào)用這個(gè)回調(diào)函數(shù):
// tutorial18.jsvar CommentForm = React.createClass({ handleSubmit: function(e) { e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if (!text || !author) { return; } this.props.onCommentSubmit({author: author, text: text}); this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function() { return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /> <input type="text" placeholder="Say something..." ref="text" /> <input type="submit" value="Post" /> </form> ); } });
現(xiàn)在回調(diào)函數(shù)已經(jīng)就緒,唯一我們需要做的就是提交到服務(wù)器,然后刷新列表:
// tutorial19.jsvar CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleCommentSubmit: function(comment) { $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: comment, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } });
我們的應(yīng)用現(xiàn)在已經(jīng)完成了所有功能,但是在你的評(píng)論出現(xiàn)在列表之前,你必須等待請(qǐng)求完成,感覺(jué)很慢。我們可以提前添加這條評(píng)論到列表中,從而使應(yīng)用感覺(jué)更快。
// tutorial20.jsvar CommentBox = React.createClass({ loadCommentsFromServer: function() { $.ajax({ url: this.props.url, dataType: 'json', success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, handleCommentSubmit: function(comment) { var comments = this.state.data; var newComments = comments.concat([comment]); this.setState({data: newComments}); $.ajax({ url: this.props.url, dataType: 'json', type: 'POST', data: comment, success: function(data) { this.setState({data: data}); }.bind(this), error: function(xhr, status, err) { console.error(this.props.url, status, err.toString()); }.bind(this) }); }, getInitialState: function() { return {data: []}; }, componentDidMount: function() { this.loadCommentsFromServer(); setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } });
你剛剛通過(guò)一些簡(jiǎn)單步驟夠早了一個(gè)評(píng)論框。了解更多關(guān)于為什么使用React的內(nèi)容,或者深入學(xué)習(xí)API參考,開始專研!祝你好運(yùn)!
更多建議: