基礎知識
App如何通過網絡請求數據?
客戶服務器模型
- App 通過一個 URL 向特定的主機發送一個網絡請求加載需要的資源。URL 一般是使用 HTTP(HTTPS)協議,該協議會通過 IP(或域名)定位到資源所在的主機,然后等待主機處理和響應。
- 主機通過本次網絡請求指定的端口號找到對應的處理軟件,然后將網絡請求轉發給該軟件進行處理(處理的軟件會運行在特定的端口)。針對 HTTP(HTTPS)請求,處理的軟件會隨著開發語言的不同而不同,如 Java 的 Tomcat、PHP 的 Apache、.net 的 IIS、Node.js 的 JavaScript 運行時等)
- 處理軟件針對本次請求進行分析,分析的內容包括請求的方法、路徑以及攜帶的參數等。然后根據這些信息,進行相應的業務邏輯處理,最后通過主機將處理后的數據返回(返回的數據一般為 JSON 字符串)。
- App 接收到主機返回的數據,進行解析處理,最后展示到界面上。
- 發送請求獲取資源的一方稱為客戶端。接收請求提供服務的一方稱為服務端。
基本概念
URL
- Uniform Resource Locator(統一資源定位符),表示網絡資源的地址或位置。
- 互聯網上的每個資源都有一個唯一的 URL,通過它能找到該資源。
- URL 的基本格式
協議://主機地址/路徑
。
HTTP/HTTPS
- HTTP—HyperTextTransferProtocol:超文本傳輸協議。
- HTTPS—Hyper Text Transfer Protocol over Secure Socket Layer 或 Hypertext Transfer Protocol Secure:超文本傳輸安全協議。
請求方法
- 在 HTTP/1.1 協議中,定義了 8 種發送 HTTP 請求的方法,分別是
GET、POST、HEAD、PUT、DELETE、OPTIONS、TRACE、CONNECT
。 - 最常用的是 GET 與 POST。
響應狀態碼
狀態碼 | 描述 | 含義 |
---|---|---|
200 | Ok | 請求成功 |
400 | Bad Request | 客戶端請求的語法出現錯誤,服務端無法解析 |
404 | Not Found | 服務端無法根據客戶端的請求找到對應的資源 |
500 | Internal Server Error | 服務端內部出現問題,無法完成響應 |
請求響應過程
請求響應過程
JSON
- JavaScript Object Notation。
- 一種輕量級的數據格式,一般用于數據交互。
- 服務端返回給 App 客戶端的數據,一般都是 JSON 格式。
語法
- 數據以鍵值對
key : value
形式存在。 - 多個數據由
,
分隔。 - 花括號
{}
保存對象。 - 方括號
[]
保存數組。
key與value
- 標準 JSON 數據的 key 必須用雙引號
""
。 - JSON 數據的 value 類型:
- 數字(整數或浮點數)
- 字符串(
"
表示) - 布爾值(true 或 false)
- 數組(
[]
表示) - 對象(
{}
表示) - null
解析
- 厘清當前 JSON 數據的層級關系(借助于格式化工具)。
- 明確每個 key 對應的 value 值的類型。
- 解析技術
- Codable 協議(推薦)。
- JSONSerialization。
- 第三方框架。
URLSession
使用步驟
- 創建請求資源的 URL。
- 創建 URLRequest,設置請求參數。
- 創建 URLSessionConfiguration 用于設置 URLSession 的工作模式和網絡設置。
- 創建 URLSession。
- 通過 URLSession 構建 URLSessionTask,共有 3 種任務。
(1)URLSessionDataTask:請求數據的 Task。
(2)URLSessionUploadTask:上傳數據的 Task。
(3)URLSessionDownloadTask:下載數據的 Task。 - 啟動任務。
- 處理服務端響應,有 2 種方式。
(1)通過 completionHandler(閉包)處理服務端響應。
(2)通過 URLSessionDataDelegate(代理)處理請求與響應過程的事件和接收服務端返回的數據。
基本使用
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// get()
// post()
}
func get() {
// 1. 確定URL
let url = URL(string: "http://v.juhe.cn/toutiao/index?type=top&key=申請的key")
// 2. 創建請求
let urlRequest = URLRequest(url: url!)
// cachePolicy: 緩存策略,App最常用的緩存策略是returnCacheDataElseLoad,表示先查看緩存數據,沒有緩存再請求
// timeoutInterval:超時時間
// let urlRequest = URLRequest(url: url!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 5)
let config = URLSessionConfiguration.default
// 3. 創建URLSession
let session = URLSession(configuration: config)
// 4. 創建任務
let task = session.dataTask(with: urlRequest) { data, _, error in
if error != nil {
print(error!)
} else {
if let data = data {
print(String(data: data, encoding: .utf8)!)
}
}
}
// 5. 啟動任務
task.resume()
}
func post() {
let url = URL(string: "http://v.juhe.cn/toutiao/index")
var urlRequest = URLRequest(url: url!)
// 指明請求方法
urlRequest.httpMethod = "POST"
// 指明參數
let params = "type=top&申請的key"
// 設置請求體
urlRequest.httpBody = params.data(using: .utf8)
let config = URLSessionConfiguration.default
// delegateQueue決定了代理方法在哪個線程中執行
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
let task = session.dataTask(with: urlRequest)
task.resume()
}
}
// MARK:- URLSessionDataDelegate
extension ViewController: URLSessionDataDelegate {
// 開始接收數據
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
// 允許接收服務器的數據,默認情況下請求之后不接收服務器的數據即不會調用后面獲取數據的代理方法
completionHandler(URLSession.ResponseDisposition.allow)
}
// 獲取數據
// 根據請求的數據量該方法可能會調用多次,這樣data返回的就是總數據的一段,此時需要用一個全局的Data進行追加存儲
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
let result = String(data: data, encoding: .utf8)
if let result = result {
print(result)
}
}
// 獲取結束
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
} else {
print("=======成功=======")
}
}
}
注意:如果網絡請求是 HTTP 而非 HTTPS,默認情況下,iOS 會阻斷該請求,此時需要在 Info.plist 中進行如下配置。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
下載數據
class ViewController: UIViewController {
// 下載進度
@IBOutlet var downloadProgress: UIProgressView!
// 下載圖片
@IBOutlet var downloadImageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
download()
}
func download() {
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/wall.png")!
let request = URLRequest(url: url)
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
let task = session.downloadTask(with: request)
task.resume()
}
}
extension ViewController: URLSessionDownloadDelegate {
// 下載完成
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// 存入沙盒
let savePath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!
// 文件類型根據下載的內容決定
let fileName = "\(Int(Date().timeIntervalSince1970)).png"
let filePath = savePath + "/" + fileName
print(filePath)
do {
try FileManager.default.moveItem(at: location, to: URL(fileURLWithPath: filePath))
// 顯示到界面
DispatchQueue.main.async {
self.downloadImageView.image = UIImage(contentsOfFile: filePath)
}
} catch {
print(error)
}
}
// 計算進度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
DispatchQueue.main.async {
self.downloadProgress.setProgress(Float(totalBytesWritten) / Float(totalBytesExpectedToWrite), animated: true)
}
}
}
上傳數據
上傳數據需要服務端配合,不同的服務端代碼可能會不一樣,下面的上傳代碼適用于本人所寫的服務端代碼。
- 數據格式。
上傳數據格式
- 實現。
class ViewController: UIViewController {
let YFBoundary = "AnHuiWuHuYungFan"
@IBOutlet var uploadInfo: UILabel!
@IBOutlet var uploadProgress: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
upload()
}
func upload() {
// 1. 確定URL
let url = URL(string: "http://172.20.53.240:8080/AppTestAPI/UploadServlet")!
// 2. 確定請求
var request = URLRequest(url: url)
// 3. 設置請求頭
let head = "multipart/form-data;boundary=\(YFBoundary)"
request.setValue(head, forHTTPHeaderField: "Content-Type")
// 4. 設置請求方式
request.httpMethod = "POST"
// 5. 創建NSURLSession
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: OperationQueue())
// 6. 獲取上傳的數據(按照固定格式拼接)
var data = Data()
let header = headerString(mimeType: "image/png", uploadFile: "wall.png")
data.append(header.data(using: .utf8)!)
data.append(uploadData())
let tailer = tailerString()
data.append(tailer.data(using: .utf8)!)
// 7. 創建上傳任務 上傳的數據來自getData方法
let task = session.uploadTask(with: request, from: data) { _, _, error in
// 上傳完畢后
if error != nil {
print(error!)
} else {
DispatchQueue.main.async {
self.uploadInfo.text = "上傳成功"
}
}
}
// 8. 執行上傳任務
task.resume()
}
// 開始標記
func headerString(mimeType: String, uploadFile: String) -> String {
var data = String()
// --Boundary\r\n
data.append("--" + YFBoundary + "\r\n")
// 文件參數名 Content-Disposition: form-data; name="myfile"; filename="wall.jpg"\r\n
data.append("Content-Disposition:form-data; name=\"myfile\";filename=\"\(uploadFile)\"\r\n")
// Content-Type 上傳文件的類型 MIME\r\n\r\n
data.append("Content-Type:\(mimeType)\r\n\r\n")
return data
}
// 結束標記
func tailerString() -> String {
// \r\n--Boundary--\r\n
return "\r\n--" + YFBoundary + "--\r\n"
}
func uploadData() -> Data {
let image = UIImage(named: "wall.png")
let imageData = image!.pngData()
return imageData!
}
}
extension ViewController: URLSessionTaskDelegate {
// 上傳進去
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
DispatchQueue.main.async {
self.uploadProgress.setProgress(Float(totalBytesSent) / Float(totalBytesExpectedToSend), animated: true)
}
}
// 上傳出錯
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error)
}
}
}
WKWebView
- 用于加載 Web 內容的控件。
- 使用時必須導入
WebKit
模塊。
基本使用
- 加載網頁。
// 創建URL
let url = URL(string: "https://www.abc.edu.cn")
// 創建URLRequest
let request = URLRequest(url: url!)
// 創建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加載網頁
webView.load(request)
- 加載本地資源。
// 文件夾路徑
let basePath = Bundle.main.path(forResource: "localWeb", ofType: nil)!
// 文件夾URL
let baseUrl = URL(fileURLWithPath: basePath, isDirectory: true)
// html路徑
let filePath = basePath + "/index.html"
// 轉成文件
let fileContent = try? NSString(contentsOfFile: filePath, encoding: String.Encoding.utf8.rawValue)
// 創建WKWebView
let webView = WKWebView(frame: UIScreen.main.bounds)
// 加載html
webView.loadHTMLString(fileContent! as String, baseURL: baseUrl)
注意:如果是本地資源是文件夾,拖進項目時,需要勾選
Create folder references
,然后用Bundle.main.path(forResource: "文件夾名", ofType: nil)
獲取資源路徑。
與JavaScript交互
創建WKWebView
lazy var webView: WKWebView = {
// 創建WKPreferences
let preferences = WKPreferences()
// 開啟JavaScript
preferences.javaScriptEnabled = true
// 創建WKWebViewConfiguration
let configuration = WKWebViewConfiguration()
// 設置WKWebViewConfiguration的WKPreferences
configuration.preferences = preferences
// 創建WKUserContentController
let userContentController = WKUserContentController()
// 配置WKWebViewConfiguration的WKUserContentController
configuration.userContentController = userContentController
// 給WKWebView與Swift交互起一個名字:callbackHandler,WKWebView給Swift發消息的時候會用到
// 此句要求實現WKScriptMessageHandler
configuration.userContentController.add(self, name: "callbackHandler")
// 創建WKWebView
var webView = WKWebView(frame: UIScreen.main.bounds, configuration: configuration)
// 讓WKWebView翻動有回彈效果
webView.scrollView.bounces = true
// 只允許WKWebView上下滾動
webView.scrollView.alwaysBounceVertical = true
// 設置代理WKNavigationDelegate
webView.navigationDelegate = self
// 返回
return webView
}()
創建HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0,user-scalable=no"/>
</head>
<body>
iOS傳過來的值:<span id="name"></span>
<button onclick="responseSwift()">響應iOS</button>
<script type="text/javascript">
// 給Swift調用
function sayHello(name) {
document.getElementById("name").innerHTML = name
return "Swift你也好!"
}
// 調用Swift方法
function responseSwift() {
// 這里的callbackHandler是創建WKWebViewConfiguration是定義的
window.webkit.messageHandlers.callbackHandler.postMessage("JavaScript發送消息給Swift")
}
</script>
</body>
</html>
兩個協議
- WKNavigationDelegate:判斷頁面加載完成,只有在頁面加載完成后才能在實現 Swift 調用 JavaScript。WKWebView 調用 JavaScript:
// 加載完畢以后執行
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// 調用JavaScript方法
webView.evaluateJavaScript("sayHello('WebView你好!')") { (result, err) in
// result是JavaScript返回的值
print(result, err)
}
}
- WKScriptMessageHandler:JavaScript 調用 Swift 時需要用到協議中的一個方法來。JavaScript 調用 WKWebView:
// Swift方法,可以在JavaScript中調用
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}
ViewController
class ViewController: UIViewController {
// 懶加載WKWebView
...
// 加載本地html
let html = try! String(contentsOfFile: Bundle.main.path(forResource: "index", ofType: "html")!, encoding: String.Encoding.utf8)
override func viewDidLoad() {
super.viewDidLoad()
// 標題
title = "WebView與JavaScript交互"
// 加載html
webView.loadHTMLString(html, baseURL: nil)
view.addSubview(webView)
}
}
// 遵守兩個協議
extension ViewController: WKNavigationDelegate, WKScriptMessageHandler {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
...
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
...
}
}
SFSafariViewController
- iOS 9 推出的一種 UIViewController,用于加載與顯示 Web 內容,打開效果類似 Safari 瀏覽器的效果。
- 使用時必須導入
SafariServices
模塊。
import SafariServices
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
showSafariViewController()
}
func showSafariViewController() {
// URL
let url = URL(string: "https://www.baidu.com")
// 創建SFSafariViewController
let sf = SFSafariViewController(url: url!)
// 設置代理
sf.delegate = self
// 顯示
present(sf, animated: true, completion: nil)
}
}
extension ViewController: SFSafariViewControllerDelegate {
// 點擊左上角的完成(done)
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
print(#function)
}
// 加載完成
func safariViewController(_ controller: SFSafariViewController, didCompleteInitialLoad didLoadSuccessfully: Bool) {
print(#function)
}
}