gaara

gaara 查看完整档案

杭州编辑  |  填写毕业院校杭州又拍云  |  开发 编辑填写个人主网站
编辑

这个人很懒,什么也没留下

个人动态

gaara 收藏了文章 · 2019-09-12

H5与Native交互之JSBridge技术

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
    
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
    
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
    
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作,首发于有赞技术博客: http://tech.youzan.com/jsbridge/

查看原文

gaara 赞了文章 · 2019-09-12

H5与Native交互之JSBridge技术

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
    
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
    
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
    
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            var iframe = document.createElement('iframe');
            iframe.style.width = '1px';
            iframe.style.height = '1px';
            iframe.style.display = 'none';
            iframe.src = url;
            document.body.appendChild(iframe);
            setTimeout(function() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作,首发于有赞技术博客: http://tech.youzan.com/jsbridge/

查看原文

赞 100 收藏 178 评论 4

gaara 赞了文章 · 2019-02-25

从印刷学的观点略谈字体显示的现在和未来

来自于这个问题。

文字是人类最容易加工、查询、保存,并且最为准确的表意工具。内容为王,追究文字的观感,对人类阅读文字材料的工作是相当重要的。

乔布斯在做苹果之前就有研究字体设计的经历,这在之后也成为了苹果设计思想的一部分。现在看来,字体设计的独到之处,也是苹果为什么成为苹果的一个意义非常深的理由。

注:

  1. 信心一贯不高,因此略谈成性。有错误请不要见怪。
  2. 图没做,有时间会补的。
  3. 本文并不刻意区分Retina屏幕视网膜屏幕两个名词。

衬线和非衬线

前端设计师可能会熟悉,写font-family字体列表时,最后一定要有一个serifsans-serif兜底。这两个名称就代表了采用浏览器的默认衬线和默认无衬线字体,作为前边都匹配不到时的最后选择。

衬线(serif)这个概念来源于字体排版印刷学,代表除了笔画本来必要的形状之外,在笔画起始端、末端和关键节点处多出来的装饰形状。衬线体的代表是宋体Times。例如“口”这个字,必要的形状只是一个方块。但用宋体写一个“口”,可以看到在四个角都有多出来的形状。

非衬线(sans-serif, sans是哪个语言的否定前缀来着?)就是相反的意思,字形只包含笔画的必要形状。非衬线体的代表是黑体Arial

衬线字体和非衬线字体
▲ 衬线字体和非衬线字体

衬线体的优点是能够改善大段文字的阅读效果。不知道你是否看过这样一个笑话一般的实验:

把一段话的英文单词,只保留首末字母不动,中间全部打乱,仍然能够正确阅读:

Aoccdrnig to a rscheearch at Cmabrigde Uinervtisy, it deosn't mttaer in waht oredr the ltteers in a wrod are, the olny iprmoetnt tihng is taht the frist and lsat ltteer be at the rghit pclae. The rset can be a total mses and you can sitll raed it wouthit porbelm. Tihs is bcuseae the huamn mnid deos not raed ervey lteter by istlef, but the wrod as a wlohe. Amzanig!

“剑桥大学出品”这个也许是编的,这段文字本身的结论其实也不完全准确。正确的结论应该表述为:存在于上下文中的单词,关键音节保持不动,其他部分打乱之后,仍然几乎不会影响阅读的准确性。

(例如:第一个词Aoccdrnig,其实不光Ag不动,在According断开位置的音节也没有动,所以才容易阅读)

提起这个是说明,人类在阅读文字和图形的时候,都有识别关键点的天然习惯。只要关键点对上了,人类对图形的判断就决定了个八九不离十。而剩余的部分,哪怕看起来占的面积再多,也只有让人类更加确信一点儿的价值。

衬线字就抓住了人类的这个思维特点。人类对字形的认识来源于书写,而书写的关键在于:(1)起点和终点、(2)笔画必须通过/折返的关键点。(例如,我们写一个数字7,重点就在于左上角起头,右上角折弯,正下方收笔。至于两条笔画是直是弯,弧度多少,并没有太多的意义。)

在人类需要花费时间阅读大量文字时,每个文字的阅读时间就变得重要。不同的文字虽然笔画有差异,但决定字形的关键点,其实都不会非常多。并且笔画是1维的线条,多一条少一条比较敏感;而关键点是没有维度的点,多几个感觉也不太明显。

所以使用衬线字体,允许人类迅速抓住字形的关键点,就可以用快速、均匀的速度读出每一个文字。衬线字体阅读文字的时间如果画一条曲线,会是比较平直的。相反,如果用非衬线字体,就会出现简单字读得快,复杂字读得慢的现象,时间曲线是起伏明显的。选择哪个更加舒服,这很清楚。

但衬线体的麻烦在于:不好对付少量大号的文字。因为这时文字量少,意义一目了然,识别字形的时间已经不重要了。而在字号放大之后,衬线字体就会由于多余的图形“花眼”而显得难看。所以现在报纸和书籍的标题,一般都倾向于非衬线体。

衬线体排正文,非衬线体打标题,这呈现了一种天然互补的形态。

字体光栅化:妥协与混乱

一切都随着显示器的发明而改变了。

计算机显示与印刷学的不一致

一切都源于dpi (dot per inch = 点每英寸),这项表征图形点阵疏密程度的指标的区别。

印刷技术可以轻易达到250-300dpi的密度,因此可以轻易的在一个小空间内,表现出字体很多的细节。印刷品拿普通放大镜看,细节也是清楚的,只有拿40X以上的网点显微镜才能看到印刷的点阵。

但显示器则不然。从诞生到现在,普通显示器的dpi也不过70-150的水平,因此人眼凑近了看就能看到正方形网格的点阵。显示器无法表现很精细的小型图案,这是一个现实的困难。

点阵渲染,矢量抗锯齿渲染,和矢量对点阵的倒逼

由于首先产生的显示器是单色(甚至没有灰度),分辨率也很低的,所以点阵字形就是一种当然的想法了——每个字符占用一定范围的点阵,每个点只有黑和白两种状态。

随着屏幕进入灰度和彩色时代,以及分辨率的增大,显示和打印大号字的需求开始显现。这时人类就发现:对每个字号都做一套点阵,过于费时费力。并且字号大空间就充足,字形可以更加接近字体的本征,个别点的差异并不重要。总之,投入人类技巧去设计点阵字体越来越亏本。

人类很懒,最喜欢用自动化解决一切。所以这时用数学曲线表示形状的矢量字形,很自然就被提了出来。矢量字形可以缩放到任意大小,但缩放之后毕竟仍然是数学描述,想要在屏幕的点阵网格上显示躲不了光栅化一步,确定屏幕上的哪些点落在字形的曲线之内。

但显示器毕竟精细度不够,单纯的光栅化必然产生锯齿。因此让字形边缘产生灰度,不限于黑白两色的抗锯齿技术,随着电脑性能发展也被提了出来。

对于大字的点阵渲染与抗锯齿渲染
▲ 对于大字的点阵渲染与抗锯齿渲染

但戏剧性的是,有了抗锯齿技术,矢量字形在小号字上的表现也得到了改善。小号字只要引入灰度,不是纯的黑白,就能大大增加信息量和表现力。哪怕不设计点阵字形,直接用矢量字形渲染也能够辨认。

对于小字的点阵渲染与抗锯齿渲染
▲ 对于小字的点阵渲染与抗锯齿渲染,小字的抗锯齿渲染虽然模糊,但仍是可辨的

所以……是否可以完全抛弃小号字的点阵字形呢?很可惜的是,这个尝试的效果并不完美:单纯的矢量渲染,会让整个字“毛茸茸”的一片,并不十分容易辨认。

面对这个问题,就产生了认可改造两种平行的道路,看下一节。

微软与苹果:是接近印刷学,还是创造显示学?

这一段我说的不好。要看《Joel谈软件》中的一节,大师对苹果和微软的字体抗锯齿技术做出的分析。

苹果选择认可,去追近印刷学——保持简单,保持统一,原样就是原样。

微软选择改造,去创造新的显示学——将点阵字体的优势,引入矢量显示的系统当中,单纯为了屏幕显示的效果去做各种各样的微调(workaround)。

实践不同,但都没有错。并且有一点最终相同的地方是:都是在显示器dpi不足的情况下做的妥协

独特的计算机文化:衬线与非衬线的倒错

点阵字形由于一般是塞在小空间里,所以设计中需要发挥很多的技巧,让有限的空间中字形怎样既保证清晰可辨,又尽量追求美感。

在这种现实压力下,点阵字形(尤其对于小字)往往会丢失一些字体的本征特点。例如从DOS时代就开始使用的经典16px宋体,在“一”、“口”等简单字的边角,也许还能留下1像素的衬线。但在“霸”、“编”等稍复杂的字上就表现不出衬线了,甚至对于“饕”、“餮”等字连清晰表现全部的笔画都不太可能。

「此处加16px点阵渲染下这几个字的图」

在矢量抗锯齿渲染之下,面临的问题也类似:衬线只能渲染出一些很浅很小的灰度点,到头来会让字在整体上显得很“脏”,显示效果还不如非衬线的清晰可读。

这也就是说,对于大篇幅正文使用的小字衬线字形几乎不太可行,非衬线字形基本上没有商量。——而这时,文章标题为了突出显示,就必须和正文产生明显区别,所以更习惯于用衬线字。

计算机上标题衬线字,正文非衬线字的习惯用法,相对于印刷学的习惯,是一个非常有趣的倒错。

这种现象在西文网页更加常见。多逛逛洋人的报纸、电视媒体的网站,就总能看到。

中文字体之惑

西文很简单,但中文复杂的要死。就比如说:在12px的字高下,西文真的能勉强做出点阵版的Times衬线字,并且衬线还很明显。而中文根本没有指望。

复杂的东西必定难以产生、难以组合和难以变化。积重难返,知道这个“重”在何处,才能知道如果发生了变革会“变”在哪里。

Windows“宋体”:历史的堆叠造成的二元性

Windows的宋体是一个各种成果堆叠,所制造出来的怪胎。

DOS时代,最开始用点阵字体来表示汉字的时候,是没有明确的“字体”一说的。而开始使用矢量字体时,由于中文印刷学的指导,在“宋仿楷黑”中“宋”自然而然的排在了第一位。

Windows在这一点上犯了糊涂。Windows希望将所有旧的默认字体,组合成单一的默认字体,因此就造成了宋体既是矢量的衬线字,又是点阵的非衬线字的怪象。(微软顺手还引入了另一个怪象,就是宋体在西文上还是最不常用的等宽字体……唉……真是业界毒瘤)

Windows曾经多次修改过宋体的点阵部分的字形,最近一次是Win7的修改,但多年过去都不敢颠覆这个问题,把小字的渲染还原成正确的矢量渲染。习惯成自然,Windows宋体的这个现象恐怕是要一直存在下去,直到Windows和微软公司生命的尽头了。

微软雅黑:姗姗来迟的大一统

不过Windows Vista和7也带来了微软壮士断腕和“曲线救国”的决心:虽然宋体改不了,但总可以新做一个默认字体,在非衬线上保持统一,来结束这场怪象!

这就是我们熟知的微软雅黑。这是一个点阵、矢量和西文部分都精心设计的,一套非常优秀而全面的纯非衬线字体。

M$为了微软雅黑花了不少的$,江湖传闻每个字100$。不管是真是假,在目前看来,这绝对是值得的。微软现在大多数的UI设计,都受惠于字体回归科学的努力。

可惜微软毕竟是个靠吃老本为生的技术后进者——这个努力,相比Mac和Linux都晚了一步。Mac和Linux的中文,一开始就干脆的采用了非衬线体。并且主要依赖矢量渲染,点阵渲染只是用于微调。

Retina:一场伟大的革命

一切又随着Retina屏幕的问世而改变了。

人类不断的在显示器上追求更高的dpi,终于让显示器的dpi赶上甚至超越了印刷的水平。Retina屏幕我记得已经实现了300+的dpi。

形象的来说,我们在普通屏幕上看印刷稿件,需要放大到300%以上,检查所有细节无误才算有保证。但在Retina屏幕上,只需100%的原样,就能真正的所见即所得——见到的细节有多少,印出的细节就有多少。

Retina屏幕:让无数精妙的旧技巧欲哭无泪的破坏者

量变引发质变。dpi增加一点,一般只是图像变小一点。但dpi增加到一定程度,就会导致一个恐怖的质变:**屏幕上的单个像素点,从此以后就不再可辨了**!!!

这也就是说,以下历史悠久的光栅渲染技术,在Retina屏幕上就被无情的淘汰掉了:

  • 所有的点阵字型 (无差别全部废弃)
  • 矢量渲染小字时的修正技术 (Retina屏幕下,像素数永远足够,不存在小字的概念)
  • 有1px精细边框的小图标
  • 无抗锯齿的Bresenham直线绘制法 (单点和1像素的直线都不能画了)
  • ……(一时想不起来,不过必然还有很多)

可以说这是非常可惜的事情:这些不单单是成熟的技术,更是一门门本身就充满着工程之美的技艺

这些代表着人类创造力的美妙技艺,最终消失在历史当中,这虽然是技术的进步,但也不得不说是艺术上的的遗憾。

视网膜屏显示学:思想亟待追赶技术的蓝海

不破不立。但先破和后立之间,一般都存在着一道鸿沟。

现在的Retina也是如此。虽然技术上达到了这样的水平,但在软件设计上,可以说我们还没有足够的思想和工具,去完全把Retina屏幕的优势全都用好。(这一点我只知道结论,举不出例子,非常抱歉)

视网膜屏上如何显示?这必将又是一门有待研究开发,通过不断迭代才能得到认识的新科学。

未来将会如何?

随着Retina彻底颠覆了我们对“计算机显示”的认识,我们就必须思考:在新的显示技术的推动下,我们的显示学究竟会有什么样的未来?

预测A:计算机显示学消亡,印刷学统一一切?

显示器达到了印刷的细密度之后,印刷的所有技巧就可以全部套用到显示器上。这样,未来的显示学也就等同于印刷学和平面设计学,显示技术就是可以动的印刷技术,不再需要两套并行的方法论。

预测B:计算机继续创造全新的显示学,仍然与印刷学保持并列?

人类对计算机显示学,已经形成了难以动摇的习惯。则将来人类在视网膜屏幕的时代,仍将发展计算机显示学所产生的方法,仍将尊重人类在上一时代已有的审美观。最终形成一套不完全像印刷学,但可以和印刷学并列的新的显示科学?

前路在何方?

不知道。

我只能看到的是:显示技术的革命,这又将是一个必然发生,但无比漫长的进化过程。

人类花了几十年,创建了以肉眼可辨光栅为技术基础和行事前提的显示科学。没有理由不相信,人类还需要再用上几十年的时间,才能将这一套旧科学彻底的送进博物馆。

前路漫漫,只有不要停止思考和尝试,才是唯一的发展之道。

查看原文

赞 14 收藏 14 评论 4

gaara 回答了问题 · 2018-01-30

HTTP缓存 服务器返回的304, 为什么被代理修改成了200?

服务器 (图中的 Server)返回 304,是因为客户端(图中的 Cache 服务器)已经有缓存。

浏览器(图中的 Client3)本地没有缓存,所以在请求服务端(图中的 Cache 服务器)时,响应 200

关注 7 回答 4

gaara 赞了文章 · 2018-01-28

OOD、DIP、IOC、DI、依赖注入容器(即 控制反转容器,IOC Container)

1. 名词介绍

  1. OOD,面向对象设计

  2. DIP,依赖倒置(软件设计原则)

  3. IOC,控制反转(软件设计模式)

  4. DI,依赖注入

  5. IOC Container,控制反转容器,也是依赖注入容器

2. 组成部分

  1. 服务清单(功能清单,service list)

  2. 服务(高层类,service ,对外提供服务)

  3. 服务提供者(底层类,service provider ,实际提供服务的对象)

2. 依赖倒置原则(DIP)

2.0 介绍

依赖倒置原则,它转换了依赖,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口

详细介绍请点我

2.1 场景描述

提供一个计算机储存的服务。需要根据不同的用户需求,使用不同的存储设备。

2.2 没有遵循依赖倒置原则的例子

2.2.1 定义好服务提供者(实际提供服务)

// 定义一个 硬盘存储类 (服务提供者)
class HardDiskStorage {
    public function saveToHardDisk(){
        
    }
    
    public function readFromHardDisk(){
        
    }
}

// 定义一个 U盘存储类(服务提供者)
class UStorage {
    public function saveToU(){
        
    }
    
    public function readFromU(){
        
    }
}

2.2.2 定义 服务(对外提供服务的对象)

/**
 * 定义一个 ComputerStorage 类(存储服务)
 */ 

// 第一种:使用硬盘作为提供实际服务的对象
class ComputerStorage {
    protected $_storage = null;
    
    function __construct(){
        $this->_storage = new HardDiskStorage();
    }
    
    public function save(){
        $this->_storage->saveToHardDisk();
    }
    
    public function read(){
        $this->_storage->readFromHardDisk();
    }
}

// 第二种:使用 U 盘作为提供实际服务的对象
class ComputerStorage {
    protected $_storage = null;
    
    function __construct(){
        $this->_storage = new UStorage();
    }
    
    public function save(){
        $this->_storage->saveToU();
    }
    
    public function read(){
        $this->_storage->readFromU();
    }
}

// 读取
$cs = new ComputerStorage();
$cs->read();

2.2.3 代码分析

根据上面的代码,当切换服务提供者时,服务类的代码需要做较多的改动。服务(ComputerStorage)本省作为一个高层类,对外提供访问,却受制于提供具体服务的服务提供者(HardDiskStorageUStorage)定义的实现(saveToHardDisksaveToUreadFromHardDiskreadFromU),高层模块依赖底层模块实现,违背了依赖倒置原则。

2.3 遵循依赖倒置原则的例子

2.3.1 场景

2.1 介绍中场景。

2.3.2 定义服务清单(高层模块定义接口)

interface ServiceList {
    public function save();
    public function read();
}

2.3.3 定义服务提供者

// 硬盘
class HardDiskStorage implements ServiceList {
    public function save(){
        
    }
    
    public function read(){
        
    }
}

// U 盘
class UStorage implements ServiceList {
    public function save(){
        
    }
    
    public function read(){
        
    }
}

2.3.4 定义服务

class ComputerStorage {
    protected $_storage = null;
    
    function __construct(){
        $this->_storage = new HardDiskStorage();        
    }
    
    public function save(){
        $this->_storage->save();
    }
    
    public function read(){
        $this->_storage->read();
    }
}

$cs = new ComputerStorage();
$cs->read();

2.3.5 代码分析

上述代码中,事先定义了好了服务清单(接口,ServiceList),然后服务提供者实现这些接口(HardDiskStorageUStorage),服务(ComputerStorage)只需要切换服务提供者即可(HardDiskStorageUStorage),完全无需理会他们的实现(readFromHardDiskreadFromU...等)。高层模块不依赖于底层模块定义的实现,遵循了依赖倒置原则

3. 控制反转(IOC) + 依赖注入(DI)

3.0 介绍

控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取

详细介绍请点我

3.1 场景

2 场景

3.2 没有实现控制反转的例子

2 中的例子就是没有实现控制反转的例子。2ComputerStorage 获取依赖(HardDiskStorageUStorage)的途径都是在 contruct 构造函数中获取的,即 类内部实例化依赖获取。

3.3 实现控制反转的例子

以下代码是根据 2 中的代码做了些许的调整。

class ComputerStorage {
    protected $_storage = null;
    
    /**
     * 内部只获取依赖的实例
     */
    public function setStorage($storage){
        $this->_storage = $storage;
    }

    public function save(){
        $this->_storage->save();
    }
    
    public function read(){
        $this->_storage->read();
    }
}

// 外部实例化依赖
$hardDiskStorage = new HardDiskStorage();

$cs = new ComputerStorage();
// 注入依赖
$cs->setStorage($hardDiskStorage);

4. 依赖注入容器(IOC 容器)

4.0 场景

2 场景。

4.1 使用 IOC容器

class Container {
    // 注册表
    protected static $_registry = null;
    
    // 保存到注册表
    public static function set($classname , Callable $create){
        self::$_registry[$classname] = $create;
    }
    
    // 获取注册表对应类的实例
    public static function get($key){
        call_user_func(self::$_registry[$key]);
    }
}

class ComputerStorage {
    protected $_storage = null;
    
    function __construct(){
        $this->_storage = Container::get('HardDiskStorage');
    }
    
    public function read(){
        $this->_storage->read();
    }
    
    public function save(){
        $this->_storage->save();
    }
}

/**
 * 注册依赖
 */
Container::set('HardDiskStorage' , function(){
    return new HardDiskStorage();
});

Container::set('UStorage' , function(){
    return new UStorage();
});

// 测试
$cs = new ComputerStorage();

$cs->read();
查看原文

赞 3 收藏 5 评论 0

gaara 赞了文章 · 2017-12-24

30行Python代码刷王者荣耀金币

原理

王者荣耀的冒险模式里有个挑战模式,第一次过关可以获得比较多的金币,后面重新挑战还是会获得少量金币,这不算是bug,你不嫌烦手动蛮力也可以刷金币。

推荐关卡:陨落的废都 - 魔女回忆

此关卡使用纯输出英雄20秒左右可以打BOSS,50秒左右可以通关,每次重复通关可以获得奖励19金币。在开挂前建议你手动通关体验一下。此为游戏原理。

简单来说,需要执行以下步骤:

  1. 界面打开至挑战关卡:陨落的废都 - 魔女回忆 【点击下一步】
  2. 进入阵容调整界面,提前安排好阵容。【点击闯关】
  3. 进入挑战界面。【点击右上角-自动-等待挑战结束】
  4. 进入挑战完成界面。【点击屏幕继续】
  5. 进入关卡奖励界面。【点击再次挑战】
  6. 进入阵容调整界面,循环至步骤1或步骤2【貌似取决于游戏区和版本】

只要你能模拟屏幕点击就可以完成刷金币的脚本,在安卓模拟界面点击最简单的方式就是使用ADB发送命令,不需要root手机,不需要安装第三方软件,方便快捷。ADB命令点击屏幕坐标[x, y] 可以使用命令:

adb shell input tap x y

iOS用户请直接查阅 github项目地址,原理和微信跳一跳外挂相似。

准备

  • 本脚本适用于安卓游戏区,需要真实安卓手机。
  • 手机需开启USB调试模式,允许电脑调试。
  • 电脑需安装好安卓驱动,一般豌豆荚或者各种管家可以自动帮你装好。
  • 电脑需要有ADB工具集,很多方式可以获取。
  • ADB工具需要加入环境变量PATH中,方便随时调用。
  • 电脑上需要安装Python,因为这是我选择的脚本语言。

专业的开发测试人员,也可以参考我的另外两篇博客:

如果只是为了刷金币,只需要安装好驱动和ADB工具即可。

步骤

如果万事具备,那么步骤就非常简单。

环境检测

  1. 用USB连接手机,如果弹出警告,请允许电脑调试手机。
  2. 使用命令 adb devices 检验adb和手机状态已经就绪。
$ adb devices
List of devices attached
b******4        device

模拟点击屏幕,比如你可以打开画图软件,然后运行命令:

adb shell input tap 500 500

如果如果一切OK,那么你将看到画图软件在坐标(500,500)的位置有一个点。

代码实现

通关需要点击的屏幕位置是固定的,加上注释我们只需要不到30行代码就可以完成。

def tap_screen(x, y):
    os.system('adb shell input tap {} {}'.format(x, y))

def do_money_work():
    print('#0 start the game')
    tap_screen(1600, 970)
    sleep(3)

    print('#1 ready, go!!!')
    tap_screen(1450, 910)
    sleep(15)

    print('#2 auto power on!')
    tap_screen(1780, 40)

    for i in range(25):
        tap_screen(1000, 500)
        sleep(1)

    print('#3 do it again...\n')
    tap_screen(1430, 980)
    sleep(3)

然后我们写一个主函数来循环刷钱。

if __name__ == '__main__':
    for i in range(repeat_times):
        print('round #{}'.format(i + 1))
        do_money_work()

拿来主义

如果你喜欢拿来主义,请访问本文项目地址:

然后:

  1. 下载项目中的 kog.py 到本地,iOS则为 kog_iOS.py
  2. 将游戏打开,进入挑战模式,魔女回忆,阵容调整界面。
  3. 根据手机性能和分辨率,调整kog.py中的参数。(手机分辨率,刷金次数等等)
  4. 运行以下命令,手机上就可以查看实时运行效果。
python kog.py

注意:

  1. 每周金币上限4200,需要接近4个小时,不建议一次刷满,手机和你都要休息。
  2. 铭文,手机性能,英雄选择都会影响通关速度,自己微调等待时间。
  3. 如果你不想被USB数据线束缚,可以考虑使用无线连接Android真机

声明

本脚本纯属娱乐和探索的心得,如果你因为违反了游戏规则导致被封号,我概不负责。

关于作者:Python技术爱好者,目前从事测试开发相关工作,转载请注明原文出处。欢迎关注我的博客 https://betacat.online,你可以到我的公众号中去当吃瓜群众。

Betacat.online

查看原文

赞 90 收藏 250 评论 33

gaara 回答了问题 · 2017-12-23

从数据库中删除数据后,跳转页面时就卡住了

  1. 代码没有对 Content.find() 为空时进行判断,为空的话,应该是需要单独 resolve 空数组
  2. exec 方法里只对存在 doc 和存在 err 进行了处理,如果没有 err 且 doc 为空,也会有问题

关注 2 回答 1

认证与成就

  • 获得 194 次点赞
  • 获得 90 枚徽章 获得 4 枚金徽章, 获得 37 枚银徽章, 获得 49 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2012-12-15
个人主页被 919 人浏览