nativeReady = false;
var jsoncommand = JSON.stringify(command);
var _temp = prompt(jsoncommand,‘’);
return true;
} else {
return false;
}
}

上面的代码有所删减,若需要执行完整的jsbridge功能,还需要做一些额外的配置。例如告知前端这段js代码已经注入成功的标记。

什么时候注入js合适?

如果做过WebView开发,并且需要和js交互的同学,大部分都会认为js在WebViewClient.onPageFinished()方法中注入最合适,此时dom树已经构建完成,页面已经完全展现出来[1](()[3](()。但如果做过页面加载速度的测试,会发现WebViewClient.onPageFinished()方法通常需要等待很久才会回调(首次加载通常超过3s),这是因为WebView需要加载完一个网页里主文档和所有的资源才会回调这个方法。能不能在WebViewClient.onPageStarted()中注入呢?答案是不确定。经过测试,有些机型可以,有些机型不行。在WebViewClient.onPageStarted()中注入还有一个致命的问题——这个方法可能会回调多次,会造成js代码的多次注入。

另一方面,从7.0开始,WebView加载js方式发生了一些小改变,官方建议把js注入的时机放在页面开始加载之后。援引官方的文档[^4](():

Javascript run before page load

Starting with apps targeting Android 7.0, the Javascript context will be reset when a new page is loaded. Currently, the context is carried over for the first page loaded in a new WebView instance.

Developers looking to inject Javascript into the WebView should execute the script after the page has started to load.

在[这篇文章](()中也提及了js注入的时机可以在多个回调里实现,包括:

  • onLoadResource
  • doUpdateVisitedHistory
  • onPageStarted
  • onPageFinished
  • onReceivedTitle
  • onProgressChanged

尽管文章作者已经做了测试证明以上时机注入是可行的,但他不能完全保证没有问题。事实也是,这些回调里有多个是会回调多次的,不能保证一次注入成功。

WebViewClient.onPageStarted()太早,WebViewClient.onPageFinished()又太迟,究竟有没有比较合适的注入时机呢?试试WebViewClient.onProgressChanged()?这个方法在dom树渲染的过程中会回调多次,每次都会告诉我们当前加载的进度。这不正是告诉我们页面已经开始加载了吗?考拉正是使用了WebViewClient.onProgressChanged()方法来注入js代码。

@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
if (null != mIWebViewClient) {
mIWebViewClient.onProgressChanged(view, newProgress);
}

if (mCallProgressCallback && newProgress >= mProgressFinishThreshold) {
DebugLog.d(“WebView”, "onProgressChanged: " + newProgress);
mCallProgressCallback = false;
// mJsApi不为null且允许注入js的情况下,开始注入js代码。
if (mJsApi != null && WebJsManager.enableJs(view.getUrl())) {
mJsApi.loadLocalJsCode();
}
if (mIWebViewClient != null) {
mIWebViewClient.onPageFinished(view, newProgress);
}
}
}

可以看到,我们使用了mProgressFinishThreshold这个变量控制注入时机,这与前面提及的当progress达到80的时候,加载出来的页面已经基本可用了是相呼应的。

达到80%很容易,达到100%却很难。

正是因为这个原因,页面的进度加载到80%的时候,实际上dom树已经渲染得差不多了,表明WebView已经解析了标签,这时候注入一定是成功的。在WebViewClient.onProgressChanged()实现js注入有几个需要注意的地方:

  1. 上文提到的多次注入控制,我们使用了mCallProgressCallback变量控制
  2. 重新加载一个URL之前,需要重置mCallProgressCallback,让重新加载后的页面再次注入js
  3. 注入的进度阈值可以自由定制,理论上10%-100%都是合理的,我们使用了80%。

H5页面、Weex页面与Native页面交互——KaolaRouter

H5页面、Weex页面与Native页面的交互是通过URL拦截实现的。在WebView中,WebViewClient.shouldOverrideUrlLoading()方法能够获取到当前加载的URL,然后把URL传递给考拉路由框架,便可以判断URL是否能够跳转到其他非H5页面,考拉路由框架在[《考拉Android客户端路由总线设计》](()一文中有详细介绍,但当时未引入Weex页面,关于如何整合三者的通信,后续文章会有详细介绍。

WebViewClient.shouldOverrideUrlLoading()中,根据URL类型做了判断:

public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (StringUtils.isNotBlank(url) && url.equals(“about:blank”)) { //js调用reload刷新页面时候,个别机型跳到空页面问题修复
url = getUrl();
}
url = WebViewUtils.removeBlank(url);
mCallProgressCallback = true;
//允许启动第三方应用客户端
if (WebViewUtils.canHandleUrl(url)) {
boolean handleByCaller = false;
// 如果不是用户触发的操作,就没有必要交给上层处理了,直接走url拦截规则。
if (null != mIWebViewClient && isTouchByUser()) {
// 先交给业务层拦截处理
handleByCaller = mIWebViewClient.shouldOverrideUrlLoading(view, url);
}
if (!handleByCaller) {
// 业务层不拦截,走通用路由总线规则
handleByCaller = handleOverrideUrl(url);
}
mRedirectProtected = true;
return handleByCaller || super.shouldOverrideUrlLoading(view, url);
} else {
try {
notifyBeforeLoadUrl(url);
// https://sumile.cn/archives/1223.html
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setComponent(null);
intent.setSelector(null);
mContext.startActivity(intent);
if (!mIsBlankPageRedirect) {
back();
}
} catch (Exception e) {
ExceptionUtils.printExceptionTrace(e);
}
return true;
}
}

private boolean handleOverrideUrl(final String url) {
RouterResult result = WebActivityRouter.startFromWeb(
new IntentBuilder(mContext, url).setRouterActivityResult(new RouterActivityResult() {
@Override
public void onActivityFound() {
if (!mIsBlankPageRedirect) {
// 路由拦截成功以后,为防止首次进入WebView产生白屏,因此加了保护机制
back();
}
}

@Override
public void onActivityNotFound() {

}
}));
return result.isSuccess();
}

代码里写了注释,就不一一解释了。

WebView下拉刷新实现

由于考拉使用的下拉刷新跟Material Design所使用的下拉刷新样式不一致,因此不能直接套用SwipeRefreshLayout。考拉使用的是一套改造过的[Android-PullToRefresh]((),WebView的下拉刷新,正是继承自PullToRefreshBase来实现的。

/**

  • 创建者:Square Xu
  • 日期:2017/2/23
  • 功能模块:webview下拉刷新组件
    */
    public class PullToRefreshWebView extends PullToRefreshBase {
    public PullToRefreshWebView(Context context) {
    super(context);
    }

public PullToRefreshWebView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public PullToRefreshWebView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs);
}

public PullToRefreshWebView(Context context, Mode mode) {
super(context, mode);
}

public PullToRefreshWebView(Context context, Mode mode, AnimationStyle animStyle) {
super(context, mode, animStyle);
}

@Override
public Orientation getPullToRefreshScrollDirection() {
return Orientation.VERTICAL;
}

@Override
protected KaolaWebview createRefreshableView(Context context, AttributeSet attrs) {
KaolaWebview kaolaWebview = new KaolaWebview(context, attrs);
//解决键盘弹起时候闪动的问题
setGravity(AXIS_PULL_BEFORE);
return kaolaWebview;
}

@Override
protected boolean isReadyForPullEnd() {
return false;
}

@Override
protected boolean isReadyForPullStart() {
return getRefreshableView().getScrollY() == 0;
}
}

考拉使用了全屏模式实现沉浸式状态栏及[滑动返回]((),全屏模式和WebView下拉刷新相结合对键盘的弹起产生了闪动效果,经过组内大神的研究与多次调试(感谢@俊俊),发现setGravity(AXIS_PULL_BEFORE)能够解决闪动的问题。

如何处理加载错误(Http、SSL、Resource)?

对于WebView加载一个网页过程中所产生的错误回调,大致有三种:

  • WebViewClient.onReceivedHttpError(webView, webResourceRequest, webResourceResponse)

任何HTTP请求产生的错误都会回调这个方法,包括主页面的html文档请求,iframe、图片等资源请求。在这个回调中,由于混杂了很多请求,不适合用来展示加载错误的页面,而适合做监控报警。当某个URL,或者某个资源收到大量报警时,说明页面或资源可能存在问题,这时候可以让相关运营及时响应修改。

  • WebViewClient.onReceivedSslError(webview, sslErrorHandler, sslError)

任何HTTPS请求,遇到SSL错误时都会回调这个方法。比较正确的做法是让用户选择是否信任这个网站,这时候可以弹出信任选择框供用户选择(大部分正规浏览器是这么做的)。但人都是有私心的,何况是遇到自家的网站时。我们可以让一些特定的网站,不管其证书是否存在问题,都让用户信任它。在这一点上,分享一个小坑。考拉的SSL证书使用的是GeoTrust的GeoTrust SSL CA - G3,但是在某些机型上,打开考拉的页面都会提示证书错误。这时候就不得不使用“绝招”——让考拉的所有二级域都是可信任的。

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (UrlUtils.isKaolaHost(getUrl())) {
handler.proceed();
} else {
super.onReceivedSslError(view, handler, error);
}
}

  • WebViewClient.onReceivedError(webView, webResourceRequest, webResourceError)

只有在主页面加载出现错误时,才会回调这个方法。这正是展示加载错误页面最合适的方法。然鹅,如果不管三七二十一直接展示错误页面的话,那很有可能会误判,给用户造成经常加载页面失败的错觉。由于不同的WebView实现可能不一样,所以我们首先需要排除几种误判的例子:

  1. 加载失败的url跟WebView里的url不是同一个url,排除;
  2. errorCode=-1,表明是ERROR_UNKNOWN的错误,为了保证不误判,排除
  3. failingUrl=null&errorCode=-12,由于错误的url是空而不是ERROR_BAD_URL,排除

@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);

// -12 == EventHandle.ERROR_BAD_URL, a hide return code inside android.net.http package
if ((failingUrl != null && !failingUrl.equals(view.getUrl()) && !failingUrl.equals(view.getOriginalUrl())) /* not subresource error*/
|| (failingUrl == null && errorCode != -12) /not bad url/
|| errorCode == -1) { //当 errorCode = -1 且错误信息为 net::ERR_CACHE_MISS
return;
}

if (!TextUtils.isEmpty(failingUrl)) {
if (failingUrl.equals(view.getUrl())) {
if (null != mIWebViewClient) {
mIWebViewClient.onReceivedError(view);
}
}
}
}

如何操作cookie?

Cookie默认情况下是不需要做处理的,如果有特殊需求,如针对某个页面设置额外的Cookie字段,可以通过代码来控制。下面列出几个有用的接口:

  • 获取某个url下的所有Cookie:CookieManager.getInstance().getCookie(url)
  • 判断WebView是否接受Cookie:CookieManager.getInstance().acceptCookie()
  • 清除Session Cookie:CookieManager.getInstance().removeSessionCookies(ValueCallback<Boolean> callback)
  • 清除所有Cookie:CookieManager.getInstance().removeAllCookies(ValueCallback<Boolean> callback)
  • Cookie持久化:CookieManager.getInstance().flush()
  • 针对某个主机设置Cookie:CookieManager.getInstance().setCookie(String url, String value)

如何调试WebView加载的页面?

在Android 4.4版本以后,可以使用Chrome开发者工具调试WebView内容[^5](()。调试需要在代码里设置打开调试开关。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true);
}

开启后,使用USB连接电脑,加载URL时,打开Chrome开发者工具,在浏览器输入

chrome://inspect

可以看到当前正在浏览的页面,点击inspect即可看到WebView加载的内容。

WebView优化

除了上面提到的基本操作用来实现一个完整的浏览器功能外,WebView的加载速度、稳定性和安全性是可以进一步加强和提高的。下面从几个方面介绍一下WebView的优化方案,这些方案可能并不是都适用于所有场景,但思路是可以借鉴的。

CandyWebCache

我们知道,在加载页面的过程中,js、css和图片资源占用了大量的流量,如果这些资源一开始就放在本地,或者只需要下载一次,后面重复利用,岂不美哉。尽管WebView也有几套缓存方案[^6]((),但是总体而言效果不理想。基于自建缓存系统的思路,由网易杭研研发的CandyWebCache项目应运而生。CandyWebCache是一套支持离线缓存WebView资源并实时更新远程资源的解决方案,支持打母包时下载当前最新的资源文件集成到apk中,也支持在线实时更新资源。在WebView中,我们需要拦截WebViewClient.shouldInterceptRequest()方法,检测缓存是否存在,存在则直接取本地缓存数据,减少网络请求产生的流量。

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, request);
}

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (WebSwitchManager.isWebCacheEnabled()) {
try {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, url);
return WebViewUtils.handleResponseHeader(resourceResponse);
} catch (Throwable e) {
ExceptionUtils.uploadCatchedException(e);
}
}
return super.shouldInterceptRequest(view, url);
}

除了上述缓存方案外,腾讯的QQ会员团队也推出了开源的解决方案[VasSonic]((),旨在提升H5的页面访问体验,但最好由前后端一起配合改造。这套整体的解决方案有很多借鉴意义,考拉也在学习中。

Https、HttpDns、CDN

将http请求切换为https请求,可以降低运营商网络劫持(js劫持、图片劫持等)的概率,特别是使用了http2后,能够大幅提升web性能,减少网络延迟,减少请求的流量。

HttpDns,使用http协议向特定的DNS服务器进行域名解析请求,代替基于DNS协议向运营商的Local DNS发起解析请求,可以降低运营商DNS劫持带来的访问失败。目前在WebView上使用HttpDns尚存在一定问题,网上也没有较好的解决方案([阿里云Android WebView+HttpDns最佳实践](()、[腾讯云HttpDns SDK接入](()、[webview接入HttpDNS实践](()),因此还在调研中。

另一方面,可以把静态资源部署到多路CDN,直接通过CDN地址访问,减少网络延迟,多路CDN保障单个CDN大面积节点访问失败时可切换到备用的CDN上。

WebView独立进程

WebView实例在Android7.0系统以后,已经可以选择运行在一个独立进程上[7](();8.0以后默认就是运行在独立的沙盒进程中[8]((),未来Google也在朝这个方向发展,具体的WebView历史可以参考上一篇文章[《如何设计一个优雅健壮的Android WebView?(上)》](()第一小节。

Android7.0系统以后,WebView相对来说是比较稳定的,无论承载WebView的容器是否在主进程,都不需要担心WebView崩溃导致应用也跟着崩溃。然后7.0以下的系统就没有这么幸运了,特别是低版本的WebView。考虑应用的稳定性,我们可以把7.0以下系统的WebView使用一个独立进程的Activity来包装,这样即使WebView崩溃了,也只是WebView所在的进程发生了崩溃,主进程还是不受影响的。

public static Intent getWebViewIntent(Context context) {
Intent intent;
if (isWebInMainProcess()) {
intent = new Intent(context, MainWebviewActivity.class);
} else {
intent = new Intent(context, WebviewActivity.class);
}
return intent;
}

public static boolean isWebInMainProcess() {
return android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
}

WebView免流

从去年开始,市场上出现了一批互联网套餐卡,如腾讯王卡、蚂蚁宝卡、京东强卡、阿里鱼卡、网易白金卡等,这些互联网套餐相比传统的运营商套餐来说,资费便宜,流量多,甚至某些卡还拥有特殊权限——对某些应用免流。如[网易白金卡]((),对于网易系与百度系的部分应用实现免流。

免流原理

市面上常见的免流应用,原理无非就是走“特殊通道”,让这一部分的流量不计入运营商的流量统计平台中。Android中要实现这种“特殊通道”,有几种方案。

  1. 微屁恩。目前运营商貌似没有采用这种方案,但确实是可行的。由于国情,不多介绍,懂的自然懂。
  2. 全局代理。把所有的流量中转到代理服务器中,代理服务器再根据流量判断是否属于免流流量。
  3. IP直连。走这个IP的所有流量,服务器判断是否免流。

WebView免流方案

对于上面提到的几种方案,native页面所有的请求都是应用层发起的,实际上都比较好实现,但WebView的页面和资源请求是通过JNI发起的,想要拦截请求的话,需要一些功夫。网罗网上的所有方案,目前觉得可行的有两种,分别是全局代理和拦截WebViewClient.shouldInterceptRequest()

全局代理

由于WebView并没有提供接口针对具体的WebView实例设置代理,所以我们只能进行全局代理。设置全局代理时,需要通知系统代理环境发生了改变,不幸地是,Android并没有提供公开的接口,这就导致了我们只能hook系统接口,根据不同的系统版本来实现通知的目的[9](()、[10](()。6.0以后的系统,尚未尝试是否可行,根据公司同事的反馈,和5.0系统的方案是一致的。

/**

  • Set Proxy for Android 4.1 - 4.3.
    */
    @SuppressWarnings(“all”)
    private static boolean setProxyJB(WebView webview, String host, int port) {
    Log.d(LOG_TAG, “Setting proxy with 4.1 - 4.3 API.”);

try {
Class wvcClass = Class.forName(“android.webkit.WebViewClassic”);
Class wvParams[] = new Class[1];
wvParams[0] = Class.forName(“android.webkit.WebView”);
Method fromWebView = wvcClass.getDeclaredMethod(“fromWebView”, wvParams);
Object webViewClassic = fromWebView.invoke(null, webview);

Class wv = Class.forName(“android.webkit.WebViewClassic”);
Field mWebViewCoreField = wv.getDeclaredField(“mWebViewCore”);
Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);

Class wvc = Class.forName(“android.webkit.WebViewCore”);
Field mBrowserFrameField = wvc.getDeclaredField(“mBrowserFrame”);
Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

Class bf = Class.forName(“android.webkit.BrowserFrame”);
Field sJavaBridgeField = bf.getDeclaredField(“sJavaBridge”);
Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

Class ppclass = Class.forName(“android.net.ProxyProperties”);
Class pparams[] = new Class[3];
pparams[0] = String.class;
pparams[1] = int.class;
pparams[2] = String.class;
Constructor ppcont = ppclass.getConstructor(pparams);

Class jwcjb = Class.forName(“android.webkit.JWebCoreJavaBridge”);
Class params[] = new Class[1];
params[0] = Class.forName(“android.net.ProxyProperties”);
Method updateProxyInstance = jwcjb.getDeclaredMethod(“updateProxy”, params);

updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));
} catch (Exception ex) {
Log.e(LOG_TAG, "Setting proxy with >= 4.1 API failed with error: " + ex.getMessage());
return false;
}

Log.d(LOG_TAG, “Setting proxy with 4.1 - 4.3 API successful!”);
return true;
}

/**

  • Set Proxy for Android 5.0.
    */
    public static void setWebViewProxyL(Context context, String host, int port) {
    System.setProperty(“http.proxyHost”, host);
    System.setProperty(“http.proxyPort”, port + “”);
    try {
    Context appContext = context.getApplicationContext();
    Class applictionClass = Class.forName(“android.app.Application”);
    Field mLoadedApkField = applictionClass.getDeclaredField(“mLoadedApk”);
    mLoadedApkField.setAccessible(true);
    Object mloadedApk = mLoadedApkField.get(appContext);
    Class loadedApkClass = Class.forName(“android.app.LoadedApk”);
    Field mReceiversField = loadedApkClass.getDeclaredField(“mReceivers”);
    mReceiversField.setAccessible(true);
    ArrayMap receivers = (ArrayMap) mReceiversField.get(mloadedApk);
    for (Object receiverMap : receivers.values()) {
    for (Object receiver : ((ArrayMap) receiverMap).keySet()) {
    Class clazz = receiver.getClass();
    if (clazz.getName().contains(“ProxyChangeListener”)) {
    Method onReceiveMethod = clazz.getDeclaredMethod(“onReceive”, Context.class, Intent.class);
    Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
    onReceiveMethod.invoke(receiver, appContext, intent);
    }
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

需要注意的是,在WebView退出时,需要重置代理。

拦截WebViewClient.shouldInterceptRequest()

拦截WebViewClient.shouldInterceptRequest()的目的是使用免流的第三种方案——IP替换。直接看代码。

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
WebResourceResponse resourceResponse = CandyWebCache.getsInstance().getResponse(view, request);
if (request.getUrl() != null && request.getMethod().equalsIgnoreCase(“get”)) {
Uri uri = request.getUrl();
String url = uri.toString();
String scheme = uri.getScheme().trim();
String host = uri.getHost();
String path = uri.getPath();
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(host)) {
return null;
}
// HttpDns解析css文件的网络请求及图片请求
if ((scheme.equalsIgnoreCase(“http”) || scheme.equalsIgnoreCase(“https”)) && (path.endsWith(“.css”)
|| path.endsWith(“.png”)
|| path.endsWith(“.jpg”)
|| path.endsWith(“.gif”)
|| path.endsWith(“.js”))) {
try {
URL oldUrl = new URL(uri.toString());
URLConnection connection;
// 获取HttpDns域名解析结果
List ips = HttpDnsManager.getInstance().getIPListByHostAsync(host);
if (!ListUtils.isEmpty(ips)) {
String ip = ips.get(0);
String newUrl = url.replaceFirst(oldUrl.getHost(), ip);
connection = new URL(newUrl).openConnection(); // 设置HTTP请求头Host域
connection.setRequestProperty(“Host”, oldUrl.getHost());
} else {
connection = new URL(url).openConnection(); // 设置HTTP请求头Host域
}
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(url);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
return new WebResourceResponse(mimeType, “UTF-8”, connection.getInputStream());
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return super.shouldInterceptRequest(view, reque 《大厂前端面试题解析+Web核心总结学习笔记+企业项目实战源码+最新高清讲解视频》无偿开源 徽信搜索公众号【编程进阶路】 st);

如何设计一个优雅健壮的Android WebView?(下相关推荐

  1. 如何设计一个优雅健壮的Android WebView?,吊打面试官系列

    }if (mIWebViewClient != null) {mIWebViewClient.onPageFinished(view, newProgress);} } } 可以看到,我们使用了`mP ...

  2. 如何设计一个优雅健壮的Android WebView?(上)

    原文链接 https://kaolamobile.github.io/2017/12/10/design-an-elegant-and-powerful-android-webview-part-on ...

  3. 如何设计一个优雅健壮的Android WebView?(上) 基于考拉电商平台的WebView实践

    前言 Android应用层的开发有几大模块,其中WebView是最重要的模块之一.网上能够搜索到的WebView资料可谓寥寥,Github上的开源项目也不是很多,更别提有一个现成封装好的WebView ...

  4. 用 Python 制作一个艺术签名小工具,给自己设计一个优雅的签名

    生活中有很多场景都需要我们签字(签名),如果是一些不重要的场景,我们的签名好坏基本无所谓了,但如果是一些比较重要的场景,如果我们的签名比较差的话,就有可能给别人留下不太好的印象了,俗话说字如其人嘛,本 ...

  5. 艺术签名python_用 Python 制作一个艺术签名小工具,给自己设计一个优雅的签名...

    生活中有很多场景都需要我们签字(签名),如果是一些不重要的场景,我们的签名好坏基本无所谓了,但如果是一些比较重要的场景,如果我们的签名比较差的话,就有可能给别人留下不太好的印象了,俗话说字如其人嘛,本 ...

  6. python绘制自己的名字_用 Python 制作一个艺术签名小工具,给自己设计一个优雅的签名...

    生活中有很多场景都需要我们签字(签名),如果是一些不重要的场景,我们的签名好坏基本无所谓了,但如果是一些比较重要的场景,如果我们的签名比较差的话,就有可能给别人留下不太好的印象了,俗话说字如其人嘛,本 ...

  7. Java开发面试技巧,如何设计一个优雅的RESTFUL的接口

    HTTP 里面有几个基本的方法.我们利用这些约定一些规范. 方法 作用 GET 获取数据 POST 插入数据 PUT 更新数据 DELECT 删除数据 从表中,如果我们可以清楚看到当我们的接口是关于获 ...

  8. rest 接口怎么传list_如何设计一个优雅的RESTFUL的接口

    show me the code and talk to me,做的出来更要说的明白 我是布尔bl,你的支持是我分享的动力! 一 .引入 设计接口是我们开发人员的日常操作.当我们把接口交给前端人员时, ...

  9. gratitude_用Photoshop设计一个优雅的“ Gratitude Log”登陆页面

    gratitude 在本教程中,我将指导您完成为虚构的"感谢日志"服务设计优雅的着陆页的过程. 我们将完全从头开始,立即使用Adobe Photoshop绘制布局. 我们将使用一些 ...

最新文章

  1. 第二十四课.循环神经网络RNN
  2. 天气之子电脑壁纸_天气之子这些美到窒息的头像壁纸
  3. PHP China杨格:PHP China 是开发者的“家”
  4. zendopcache代替APC效果不错
  5. 在python中、下列代码的输出是什么-Python 面试中 8 个必考问题
  6. Java 二维数组的初始化
  7. 教你些技巧,用 Python 自动化办公做一些有趣的事情!太方便了
  8. java正则匹配非html字符串_java正则表达式去除html中所有的标签和特殊HTML字符(以开头的)...
  9. python无条件跳转_python按按钮实现界面跳转_python实现界面跳转 - CSDN
  10. 刘徽割圆法转化成matlab,刘徽割圆术动画演示
  11. Spring的事务管理
  12. mysql有关时间教程_MySQL教程21-日期和时间类型
  13. windows 区域截屏以及延迟截屏
  14. 网页表格线框html,网页设计表格单元格线条及边框设置
  15. Junit4初始化错误
  16. Java面试题及答案2020,安卓java编程软件app
  17. 兔子繁殖问题Java实现
  18. wires hark使用
  19. 【题解】Cutting Woods
  20. 北京玉符飞扬科技面经(一面拿到offer)面试官是facebook的大牛

热门文章

  1. 20 个超酷的 HTML5/CSS3 应用及源码
  2. python例题——文件独特行数
  3. 小甲鱼python课后习题【16,17,18,19,20】
  4. 语音交互:4G之上的智能手机革命
  5. MUI页面及遇到的bug
  6. 360百科成自媒体平台 已成功吸引账号入驻 [冷笑话精选]
  7. Golang 通用连接池
  8. [源码和文档分享]Qt实现的宠物小精灵对战游戏阶段一-宠物小精灵的加入
  9. RocketMQ 在联想大数据中的应用简析
  10. esc键退出全屏 vue_Js 网页全屏(vue)-2020-08-26-亲测兼容 F11、ESC的全屏操作