Loading Offline Resources in WKWebView with a Local Web Server
基于 LocalWebServer 实现 WKWebView 离线资源加载
1. Background
The author mentioned in the article 《WKWebView》 that WKWebView performs network requests in a process independent of the app process, and the requested data does not pass through the main process. Therefore, using NSURLProtocol directly on WKWebView cannot intercept the request. Therefore, if you need to intercept requests, a feasible solution is to use the private API exposed by Apple's open source Webkit2 source code (see section 3 of the original article for details: NSURLProtocol issue). However, using private API will inevitably bring about the following problems:
Audit risk
When intercepting http/https, the post request body is lost
If ajax hook is used, there may be post header character length limit, Put type request exception, etc.
From this point of view, before the arrival of iOS11 WKURLSchemeHandler [Explore], the private API was not so perfect. Fortunately, through searching, we found that the iOS system has the ability to build a server, and it is theoretically possible to implement WKWebView offline resource loading.
2. Analysis
Local web server based on iOS currently has the following relatively complete frameworks:
CocoaHttpServer (Supports iOS, macOS and various network scenarios)
GCDWebServer (based on iOS, does not support https and webSocket)
Telegraph (Swift implementation, more complete functions than the above two categories)
Because most APPs currently support ATS, and most domestic project codes are still implemented using OC, this article will conduct experiments based on CocoaHttpSever. Telegraph was born to supplement the shortcomings of CocoaHttpSever and GCDWebServer. For pure Swift projects, it is recommended to use Telegraph.
- (void)viewDidLoad { [super viewDidLoad]; //Setup WKWebView WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init]; WKUserContentController *controller = [[WKUserContentController alloc] init]; configuration.userContentController = controller; configuration.processPool = [[WKProcessPool alloc] init]; _wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration]; _wkWebView.navigationDelegate = self; _wkWebView.UIDelegate = self; [self.view addSubview:_wkWebView]; //Start local web server [[LocalWebServerManager sharedInstance] start]; //Local request which use local resource [self loadLocalRequest]; //Remote request which use local resource // [self loadRemoteRequest]; }
Add a referenced resource directory (the blue folder) to the project, then create index.html and hi.js in that directory.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
<!DOCTYPE html> <html> <head> <metahttp-equiv="Content-Type"content="text/html; charset=utf-8" /> <title>Hello</title> <scripttype="text/javascript"src="http://localhost:60000/hi.js"></script> </head> <bodybgcolor="#4F8FFF"> <center> <h1><br><br><br><br><br><br>Congratulations! The server is running successfully!</h1> <h5><br>Click the button below to try it out</h5> <inputtype='button'value='Calls the method ' in the local js resourceonclick='invokeAlert()'/> </center> </body> </html>
1 2 3
functioninvokeAlert() { alert('Perfect!') }
Access http://localhost:60000/index.html through WKWebView.
After the above 4 steps, you can start the iOS local http service and access local html resources through WKWebView.
However, the purpose of this article is "Implementing WKWebView offline resource loading based on LocalWebServer". The so-called offline resource loading refers to: the page resources are on the remote server, and some resources in the page are in the iOS local sandbox. Based on the above example, that is, index.html should be run on remote services such as nginx or apache, and hi.js should be stored in the resource directory of the app.
Is this feasible? Let's give it a try. Rename index.html to demo.html and configure it in http://smallfan.net for direct access
As mentioned before: supporting ATS has become a correct and effective choice.
In Safari and Apple WebKit: In https pages, http requests are not allowed, otherwise they will be blocked.
So if https is already supported, then http://localhost:60000/hi.js in the page must also support https. The problem is that for the https page, we can use a legal certificate issued by the CA for two-way or one-way authentication, but localhost is not a legal host, which means we need to implement a self-signed certificate for it.
4.1 Implement local server self-signed certificate
Because iOS development uses MacOS, the following behavior assumes that OpenSSL has been installed on the system by default.
First go to OpenSSL official website to download the latest version, unzip it and find CA.sh in the app directory, copy it to the root directory, and then run
1
% sh ~/CA.sh -newca
After running, a demoCA directory will be generated, which stores the CA's certificate and private key. At the same time, remember the authorization password you set. Create a directory
At this time, the terminal will ask you to fill in the area, name and other information. Common Name This item must be filled in localhost or 127.0.0.1. If you fill in 127.0.0.1, then the request on the page can only use https://127.0.0.1:60000 and cannot be used. https://localhost:60000, and vice versa.
Since then, the self-signed certificate has been generated.
4.2 Configure self-signed certificate
Import the P12 file into the project's resource directory. Then create MyHttpConnection as a subclass of HttpConnection and override - (BOOL)isSecureServer and sslIdentityAndCertificates.
In fact, this method of implementing WKWebView offline loading is a strange trick. Because of Apple's closed ecosystem, it is often difficult to optimize some existing things, and we can only continue to explore. From this perspective, Python is more fun, 23333…
Regarding some issues in local web server, the author has not had time to conduct more in-depth research in the future. Here are some possible issues. Everyone is welcome to discuss them:
Resource access permission security issues
When switching between the front and backend of the app, the service restart performance is time consuming.
When the service is running, power and CPU usage issues
Multi-threading and disk IO issues
Finally, throw out a Demo, Biu... Demo address: LocalWebServer