Germinate's Blog


  • 首页

  • 归档
Germinate's Blog

IE浏览器中文参数导致400 Bad Request问题研究

发表于 2018-02-05 |

问题描述

本人在使用Jboss-eap-7.1(内核是Wildfly)时,向服务器发送带中文url参数的Get请求时,服务器会返回400错误。而且应用根本接收不到该请求的信息,在连接层面就被拦截了,没有将请求转给应用处理。不止是Wildfly,tomcat的7,8版本也存在一样的情况。

问题探究

经过实验,发现IE浏览器6, 8, 11版本均存在该问题,但chrome浏览器正常,微软的Edge浏览器也是正常的。除了中文,如果参数含有‘<’, ‘{‘等特殊字符时,IE浏览器也会报400错误。经过查找资料和抓包分析,发现是编码问题导致了两个浏览器的结果不同,但不是大家一般认为的字符集的原因。下面是不同浏览器针对GET请求URL编码情况的分析。对于GET URL http://example.com/test?lover=樊

  • chrome浏览器先使用UTF-8字符集编码URL中的所有字符,再使用 %编码(Percent-Encoding)将字节转换为%HH(%+两个十六进制数字)的形式,这样可以保证URL中全部是标准ASCII字符。樊的UTF-8编码为{E6, A8, 8A},实际发送的请求是 http://example.com/test?lover=%E6%A8%8A
  • IE浏览器会使用GBK字符集编码URL中的参数,但不会转换为%编码。樊的GBK编码为{B7, AE} 。实际发送的请求时,直接将樊字对应的GBK字节作为请求的内容。

以上分析也许不太容易理解,我们再从字节的角度详细解释一下。
chrome请求抓包内容
以上是chrome浏览器发送请求的抓包内容,尾部的参数是=%E6%A8%8A,实际发送的字节为{ 3D, 25, 45, 36, 25, 41, 38, 25, 38, 41, 38, 41 }。’=’ 的ASCII码为3D,’%’ 的ASCII码为25,’E’ 的ASCII码为45,以此类推。
IE请求抓包内容
以上是IE浏览器发送请求的抓包内容,尾部的参数是=樊,实际发送的字节为{ 3D, B7, AE }。B7和AE都是超出RFC 3986规范允许的字节。

%编码是使用字符集编码后,再将每个超出规范允许范围的字节转换为 %+字节对应的十六进制 的形式。
另外,IE中有一个选项,“发送UTF-8 URL”。选中之后,参数还是以GBK格式发送。这个仅仅对URL路径中的字符生效,选中就是UTF-8,不勾选就是GBK。但对于URL参数没有作用,永远都使用GBK字符集。IE会对路径中的字符做%编码,URL参数不做。

根本原因

所以问题的根本原因是IE没有遵循RFC 3986规范,未对URL参数使用 %编码,而是直接将字符对应的字节码发送到服务器。Wildfly服务器端的安全机制对此类URL做了拦截。它在该版本做了更新,遵循RFC 3986规范,对于url中含有非法字符的请求,直接予以拒绝,并返回400错误。相关代码摘录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private static final boolean[] ALLOWED_TARGET_CHARACTER = new boolean[256];
………… ………… …………
for(int i = 0; i < 256; ++i) {
if(i < 32 || i > 126) {
ALLOWED_TARGET_CHARACTER[i] = false;
} else {
switch ((char)i) {
case '\"':
case '#':
case '<':
case '>':
case '\\':
case '^':
case '`':
case '{':
case '|':
case '}':
ALLOWED_TARGET_CHARACTER[i] = false;
break;
default:
ALLOWED_TARGET_CHARACTER[i] = true;
}
}
}
………… ………… …………
while (buffer.hasRemaining()) {
char next = (char) (buffer.get() & 0xFF);
if(!allowUnescapedCharactersInUrl && !ALLOWED_TARGET_CHARACTER[next]) {
throw new BadRequestException(UndertowMessages.MESSAGES.invalidCharacterInRequestTarget(next));
}
………… ………… …………
}

可以看出,对于ASCII码在32-125之外的字符,是不允许的,直接抛出异常。对于该范围内的字符,有10个不允许出现。
根据代码,可以解释上述中文的问题。由GBK编码规范可知,字符的第一个字节肯定大于127,不被服务器接受,所以返回了400错误。
所以,问题并不是字符集导致的,而是%编码导致的。即使使用GBK编码中文字符,只要将字符的每个字节都转换为%HH的形式,依然能正确发送请求。至于可能导致的乱码问题,也有解决方式。但最好根据规范使用UTF-8。
根据参考资料2显示,Tomcat7,8也严格执行了RFC 3986规范,阻止不安全字符。但我认为该文章对原因的分析是不准确的。

Jboss下解决方案

根据UNDERTOW-1185的ISSUE,Undertow在该版本做了更新,增加了一个开关选项 ALLOW_UNESCAPED_CHARACTERS_IN_URL ,当配置为true时,就不再根据规范限制url中的字符。相应地,Jboss eap根据JBEAP-13710的ISSUE,将在JBEAP-13744更新Undertow版本,预计会在Jboss-eap-7.1.1版本中见到新的配置项。
但如果有条件,最好还是使用encodeUrl()编码一下,可以避免很多问题。与之相关的资料非常多,本文不再赘述。

参考资料

  1. URL编码问题&乱码根源
  2. IE6-IE11 Get请求参数带中文tomcat 返回400错误并显示:Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
Germinate's Blog

Jboss eap7.1 配置部署入门

发表于 2018-02-02 |

本文介绍了Linux系统下Jboss-eap-7.1服务器 standalone 模式的配置部署入门。90%的内容来自jboss的官方配置文档,只不过从初学者的角度对配置过程进行了梳理,介绍了部署应用时普遍会用到的配置项。参考本文,大多数应用可以在Jboss-eap-7.1上成功运行。
下文出现的命令、代码和文件地址中,${JBOSS_HOME}指代Jboss-eap-7.1所处的根目录。

安装与启动、停止

安装:所谓“安装”,就是把下载好的包解压到某个地址即可。
启动:进入${JBOSS_HOME}/bin目录,执行 standalone.sh 脚本,看到输出started in XXXXms,就证明服务器已经成功启动。
停止:服务器的进程通常都是后台运行。停止时,在${JBOSS_HOME}/bin目录下执行 ./jboss-cli.sh --connect,进入命令行管理页面,输入shutdown命令,即可停止服务器。(也可以合并为一条语句:./jboss-cli.sh --connect command=:shutdown)
配置:修改${JBOSS_HOME}/bin下的 standalone.conf 文件,可以改变Jboss启动的配置。
在文件中加入 JAVA_HOME="/path/jdk",可以指定使用的jdk,而不必依赖于系统默认的java环境变量。
在文件中加入 LANG=Zh_CN.GB18030,可以设定语言环境为中文
修改JAVA_OPTS,可以设置JVM的启动参数

基本概念

  • jboss-eap-7.1有两种运行模式,standalone——单机模式 和 domain——集群模式。本文介绍了单机模式的部署。
  • jboss启动服务器时,默认读取的配置文件是${JBOSS_HOME}/standalone/configuration/standalone.xml。也可以指定读取其他配置文件,命令是./standalone.sh --server-config=standalone-custom.xml
  • jboss-cli(command line interface),是jboss的命令行管理工具。执行${JBOSS_HOME}/bin/jboss-cli.sh即可进入。通过命令行实现部署卸载应用、配置系统设置和执行管理任务的功能。使用该工具修改系统配置时,最终也会作用到standalone.xml中。直接改xml的方式非常直观,所以更受本人青睐,本文所有配置示例均是直接修改xml。
  • Management Console,管理控制台。是jboss提供的web管理系统,地址在http://localhost:9990/console/App.html, 所有的操作均可通过jboss-cli实现,本文不再介绍该工具。

standalone.xml配置

地址配置

默认的配置,服务器启动后只能通过localhost访问。通过网卡IP地址访问时,需修改配置文件。找到下面的配置片段

1
2
3
4
5
6
7
8
<interfaces>
<interface name="management">
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
</interface>
<interface name="public">
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
</interface>
</interfaces>

将 public 标签下的地址修改为网卡ip地址,这样就能通过IP地址访问。也可以设置为0.0.0.0,机器上所有可用的ip地址均可以访问。
不建议修改 management 标签下的地址,个人认为远程的管理存在一定风险,还是直接在机器上做比较好。

端口配置

修改监听的端口,找到下面的配置片段:

1
2
3
4
5
6
7
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
………… ………… …………
<socket-binding name="http" port="${jboss.http.port:8080}"/>
<socket-binding name="https" port="${jboss.https.port:8443}"/>
………… ………… …………
</socket-binding-group>

根据需要修改不同协议监听的端口。管理控制台的端口默认是9990,也可以在此处修改。

数据源配置

数据源配置如下面代码所示,标签的含义非常明晰,不再针对标签做解释说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<subsystem xmlns="urn:jboss:domain:datasources:5.0">
<datasources>
………… ………… …………
<datasource jndi-name="java:jboss/datasources/MyDB" pool-name="MyDB" enabled="true" use-java-context="true" use-ccm="true">
<connection-url>jdbc:jtds:sybase://10.2.55.16:11100/testDB;charset=cp936</connection-url>
<driver>jtds124</driver>
<pool>
<min-pool-size>1</min-pool-size>
<max-pool-size>20</max-pool-size>
<prefill>true</prefill>
</pool>
<security>
<user-name>user</user-name>
<password>password</password>
</security>
<validation>
<valid-connection-checker class-name="org.jboss.jca.adapters.jdbc.extensions.sybase.SybaseValidConnectionChecker"/>
<check-valid-connection-sql>SELECT 1</check-valid-connection-sql>
<validate-on-match>true</validate-on-match>
<exception-sorter class-name="org.jboss.resource.adapter.jdbc.vendor.SybaseExceptionSorter"/>
</validation>
<timeout>
<blocking-timeout-millis>30000</blocking-timeout-millis>
<idle-timeout-minutes>1</idle-timeout-minutes>
</timeout>
<statement>
<track-statements>true</track-statements>
</statement>
</datasource>
<drivers>
………… ………… …………
<driver name="jtds124" module="net.sourceforge.jtds.jdbc.124">
<xa-datasource-class>net.sourceforge.jtds.jdbcx.JtdsDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>

配置好数据源后,需要将数据库驱动jar包放在 ${JBOSS_HOME}/modules 下正确的位置,才能成功绑定数据源。驱动的放置目录要和driver标签中module的值保持一致,以上面的配置为例,驱动jar包的路径应该是
${JBOSS_HOME}/modules/modules/net/sourceforge/jtds/jdbc/124/main/
main不要体现在属性中,124文件夹下必须要有main文件夹,jar包在main文件夹中。
main文件夹下还要有module.xml,配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="net.sourceforge.jtds.jdbc.124">
<resources>
<resource-root path="jtds-1.2.4.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
<module name="javax.transaction.api"/>
<module name="org.jboss.as.connector"/>
</dependencies>
</module>

module.xml中module标签的 name 属性要和standalone.xml中driver标签的 module 属性保持一致。
符合JDBC标准的数据源如此就配置完成。但本例中使用的是jtds,所以还要做额外的操作,否则无法正确绑定数据源。在驱动jar包的META-INF文件夹中新建 services 文件夹,然后新建名为 java.sql.Driver 的文件,在文件第一行写下Driver的路径。本例中,java.sql.Driver文件的内容如下:
net.sourceforge.jtds.jdbc.Driver
经过上述配置,在启动服务器时,看到输出Started Driver service with driver-name = jtds124,证明数据源配置成功。

Undertow容器配置

经过下面的配置操作,修改了接收http请求的设置、容器默认的字符集,移除了jboss默认的欢迎页面,开启了http访问的日志以及输出http请求和响应的信息。
找到如下配置片段,默认配置为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<subsystem xmlns="urn:jboss:domain:undertow:4.0">
<buffer-cache name="default"/>
<server name="default-server">
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<filter-ref name="server-header"/>
<filter-ref name="x-powered-by-header"/>
<http-invoker security-realm="ApplicationRealm"/>
</host>
</server>
<servlet-container name="default">
………… ………… …………
</servlet-container>
<handlers>
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers>
<filters>
………… ………… …………
</filters>
</subsystem>

修改后的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<subsystem xmlns="urn:jboss:domain:undertow:4.0">
<buffer-cache name="default"/>
<server name="default-server">
<http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true" url-charset="GBK" decode-url="false"/>
<https-listener name="https" socket-binding="https" security-realm="ApplicationRealm" enable-http2="true"/>
<host name="default-host" alias="localhost">
<!-- <location name="/" handler="welcome-content"/> -->
<filter-ref name="server-header"/>
<filter-ref name="x-powered-by-header"/>
<http-invoker security-realm="ApplicationRealm"/>
<filter-ref name="request-dumper"/>
<access-log pattern="combined" directory="${jboss.home.dir}/standalone/log"/>
</host>
</server>
<servlet-container name="default" default-encoding="GBK" use-listener-encoding="true">
………… ………… …………
</servlet-container>
<!-- <handlers>
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers> -->
<filters>
………… ………… …………
<filter name="request-dumper" class-name="io.undertow.server.handlers.RequestDumpingHandler" module="io.undertow.core"/>
</filters>
</subsystem>

  1. 修改http请求设置:在http-listener标签中,增加了url-charset和decode-url。其中url-charset默认使用的UTF-8,可以根据需要修改。decode-url指是否使用字符集解码url和参数,默认为true。设为false则交由后续代码进行解码处理。http-listener的详细配置属性点击这里。
  2. 修改容器默认字符集:在servlet-container标签中,default-encoding设置所有应用的字符集(默认为utf-8),use-listener-encoding指是否使用listener定义的编码。servlet-container的详细配置属性点击这里。
  3. 移除Jboss默认欢迎页面:配置文件中注释的部分,就是jboss默认欢迎页面的配置。直接注释就是将其移除,也可以将默认地址映射到某个应用上。
  4. 开启http访问日志:在host标签下增加access-log这一标签,即可将http请求的信息记在日志中,便于调试。directory是日志存放的目录,默认文件名为access_log.log。pattern是要日志中记录的信息,预定义了common和combined两种模式,combined的信息丰富一些。自定义的输出模式详见此处。
  5. 输出http请求和响应的信息:在filters标签下增加名为request-dumper的filter,在host标签下增加命名一致的filter-ref,就能在jboss日志中输出每一个http请求的request和response信息,便于调试。

注:对于上面的操作1,早期版本中,在extensions和management标签之间增加下面的配置代码,可以实现同样的效果。但自从wildfly 8 之后,jboss放弃tomcat容器转而使用undertow,所以下面的配置不再起作用了。

1
2
3
4
<system-properties>
<property name="org.apache.catalina.connector.URI_ENCODING" value="GBK"/>
<property name="org.apache.catalina.connector.USE_BODY_ENCODING_FOR_QUERY_STRING" value="true"/>
</system-properties>

部署应用

下面将介绍三种部署模式,但官方推荐在生产环境中,使用第三种手动模式,前两种可以应用于开发环境。
应用部署相关的默认配置如下:

1
2
3
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
</subsystem>

scan-interval是每隔多少毫秒扫描deployments文件夹下的变化。如果设定的值小于1,则只在启动时扫描。

全自动模式

在默认配置下,将war包上传至${JBOSS_HOME}/standalone/deployments,服务器会在启动时和每5000毫秒间隔,检查deployments下的文件变化,部署应用。

半自动模式

增加属性 auto-deploy-zipped="false"
上传应用myApp.war至deployments文件夹后,在deployments文件夹下新建一个文件,命名为myApp.war.dodeploy,服务器检测到这个文件后,则会开始执行部署。更多配置详情点击这里。

手动模式

增加属性scan-enabled="false"
此模式是官方推荐的生产环境部署应用方式,无需将应用上传至deployments文件夹下。首先保证myApp.war在服务器上,假设其路径为/usr/me/myApp.war。
启动服务器,并使用./jboss-cli.sh --connect命令进入命令行管理界面。执行命令deploy /usr/me/myApp.war部署应用。取消部署时,在命令行管理界面执行 undeploy myApp.war。
应用的数据会在${JBOSS_HOME}/standalone/data/content下,并且standalone.xml最下方会出现deployments标签,显示已经部署的应用。

题外话

既然是一个入门级的教程,就介绍下jboss一系列的产品。因为改名,导致jboss系列越来越乱,下面做一个简单梳理。
Undertow: Jboss自主研发的Servlet容器。
Jboss AS(Application Server): Jboss社区版的早期版本,由于和Jboss eap名称相近,在Jboss AS7之后就改名为Wildfly。内部封装的容器是基于Tomcat的升级版。
WildFly: 版本号从WildFly 8.0开始,由Jboss AS改名而来。内部封装的容器是Undertow。
Jboss EAP(Enterprise Application Platform):Jboss的企业版。会将社区版中验证过的,成熟的技术引入该版本,支持周期长。6的版本以Jboss AS7为基础;7的版本以Wildfly为基础。

参考资料

  1. 非jdbc驱动配置services
Germinate's Blog

react-navigation自定义StackNavigator页面跳转动画

发表于 2017-07-07 |

2018.02.09更新:
react-navigation内置跳转动画的路径发生了改变,由
react-navigation/src/views/CardStackStyleInterpolator 改为
react-navigation/src/views/CardStack/CardStackStyleInterpolator

————————————————————— 分割线 —————————————————————

使用StackNavigator跳转页面时,react-navigation根据平台的不同内置了相应的跳转动画。
内置的跳转动画在react-navigation/src/views/CardStack/CardStackStyleInterpolator中,共三种。forHorizontal:从右向左进入、forVertical:从下向上进入、forFadeFromBottomAndroid:从底部淡出。
关于修改默认的跳转动画或者自定义动画效果,以下给出两个场景。

修改整个栈内的页面跳转动画

这种场景是,一个栈内所有页面的跳转使用相同的动画效果,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import CardStackStyleInterpolator from 'react-navigation/src/views/CardStack/CardStackStyleInterpolator';
const CustomerNavigator = StackNavigator({
ScreenKey: { screen: MyScreen },
// other screens
}, {
transitionConfig: () => {
screenInterpolator: CardStackStyleInterpolator.forVertical,
transitionSpec: {
duration: 250,
easing: Easing.bounce,
timing: Animated.timing,
},
},
});

在安卓上运行时,默认的跳转动画效果是forFadeFromBottomAndroid,经过上述配置,CustomerNavigator 内的页面切换时,会使用forVertical效果。
transitionSpec内可以配置与动画相关的属性。

通过传递参数决定某页面的跳转动画

在一个StackNavigator内,可能某些页面需要用forHorizontal的跳转方式,另一些需要用forVertical的跳转方式,以下是解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import CardStackStyleInterpolator from 'react-navigation/src/views/CardStack/CardStackStyleInterpolator';
const TransitionConfiguration = () => ({
screenInterpolator: (sceneProps) => {
const { scene } = sceneProps;
const { route } = scene;
const params = route.params || {};
const transition = params.transition || 'forHorizontal';
return CardStackStyleInterpolator[transition](sceneProps);
},
});
const CustomerNavigator = StackNavigator({
ScreenKey: { screen: MyScreen },
// other screens
}, {
transitionConfig: TransitionConfiguration,
});

假如希望CustomerNavigator内的某个页面使用forVertical的跳转动画效果,调用this.props.navigate('SomeScreenKey', { transition: 'forVertical' });切换页面即可。

总结

本文均使用react-navigation自带的跳转动画,因为这三种跳转方式可以满足很多需求,希望将来能内置更丰富的效果。
react-navigation支持自定义的跳转动画效果,获取sceneProps中的layout,position,scene属性,以及scene中的index属性,就能完成自定义动画的开发。具体可以参考这篇文章的内容。如果想对其动画原理有更深的了解,可以学习这篇文章。

参考资料

  1. React Native Navigation, custom Scene (Screen) Transitions and interpolations
  2. issue#1187–Add support for custom transitionConfig
Germinate's Blog

react-navigation 为StackNavigator页面设置初始化props的三种方式

发表于 2017-06-30 |

使用react-navigation的StackNavigator跳转页面时,有给页面设置初始化props的需求。根据应用场景不同,本文总结了三种方式:

场景一:所有页面设置初始props

如果要为所有页面设置初始props,且应用程序最外层的导航是StackNavigator,直接用screenProps即可。对官方文档摘录如下:

1
2
3
4
5
6
const SomeStack = StackNavigator({
// config
});
<SomeStack
screenProps={{ propKey: propValue }}
/>

在页面中使用this.props.screenProps即可获取screenProps的内容。

场景二:子StackNavigator下的所有页面设置初始props

这种场景更为常见。多个StackNavigator嵌套时,如果要为子StackNavigator下所有页面设置初始props,同样使用screenProps这个属性,但需参照以下写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const setScreenProps = SomeComponent =>
class extends SomeComponent {
render() {
return <SomeComponent {...this.props} screenProps={{ propKey: propValue }} />;
}
};
const innerStack = StackNavigator({
// config
});
const outerStack = StackNavigator({
innerStackKey: { screen: setScreenProps(innerStack) },
// other screen
});

首先定义了一个函数setScreenProps,为StackNavigator添加screenProps属性。然后在outerStack的定义中,使用这个函数做跳转入口。
这样设置之后,innerStack下的所有页面,都可以使用this.props.screenProps获取screenProps的内容。

场景三:某个特定页面设置初始props

这种场景无需使用screenProps,直接添加组件的属性即可,代码示例如下:

1
2
3
4
const SomeStack = StackNavigator({
someScreenKey: { screen: props => <SomeScreenView {...props} myProp="value" /> },
// other screen
});

this.props.myProp就可以获取设置的属性。注意,必须参照上述代码。踩过的坑有:

  1. screen: <SomeScreenView myProp="value" /> 直接输出RN组件,会报错。react-navigation在此处有校验,必须传入一个函数。(ES6的class本质也是函数)
  2. screen: () => <SomeScreenView myProp="value" /> 这种写法,SomeScreenView将没有navigation。

参考资料

  1. issue#935–Passing regular props when navigating
  2. issue#740–Setting screen specific props / params

以上内容参考 react-navigation github的issue。当学习使用新的、快速发展的技术时,github的issue比Google和Stack Overflow好用一百倍!

Germinate's Blog

flex布局时,设置alignItem属性导致元素不显示的问题探究

发表于 2017-06-19 |

最近在使用react native的过程中,遇到一个问题。父子元素均使用flex布局,子元素按设置的比例“分享”父元素的空间,显示效果符合预期。但如果父元素设置了 alignItem: center ,子元素就不再显示(在子元素使用alignSelf: center 亦是如此)。下面用一个简单的例子直观地描述问题。

问题描述

布局很简单,蓝色父容器占满屏,黄色、红色两个子元素上下排列,分享父容器空间。

  • 默认情况的布局代码:
    1
    2
    3
    4
    <View style={{ flexDirection: 'column', flex: 1, backgroundColor: 'blue' }}>
    <View style={{ backgroundColor: 'yellow', flex: 1 }} />
    <View style={{ backgroundColor: 'red', flex: 1 }} />
    </View>

正常情况

  • 父容器设置alignItem: center , 子元素不显示
    1
    2
    3
    4
    <View style={{ alignItems: 'center', flexDirection: 'column', flex: 1, backgroundColor: 'blue' }}>
    <View style={{ backgroundColor: 'yellow', flex: 1 }} />
    <View style={{ backgroundColor: 'red', flex: 1 }} />
    </View>

子元素不显示

  • 子元素必须要指定宽度,或者元素内部有撑起容器的内容,才能显示。
    1
    2
    3
    4
    5
    6
    <View style={{ alignItems: 'center', flexDirection: 'column', flex: 1, backgroundColor: 'blue' }}>
    <View style={{ width: 100, backgroundColor: 'yellow', flex: 1 }} />
    <View style={{ backgroundColor: 'red', flex: 1 }}>
    <Text>Hello World!</Text>
    </View>
    </View>

子元素有宽度

这个问题困扰了我很久,按我原先的理解,元素设置了flex: 1,就应该占满剩余空间,但结果却和预想的不一样。最终在这里找到了答案。

问题探究

让我豁然开朗的就是w3schools中的一句话:

The align-items property vertically aligns the flexible container’s items when the items do not use all available space on the cross-axis.

根本原因是从句的内容,当子元素不需要使用交叉轴的所有空间时,才应该设置这个属性。
该属性默认的是stretch ,填充交叉轴的全部长度。如果设置其他属性,例如center,就需要显式地指定元素在交叉轴方向上的大小,或者有内容,否则元素就不会显示。

此处还纠正了我一个错误的认识。设置flex属性时,并不是弹性填满元素剩余的可用空间,仅仅是在主轴方向分配剩余空间,至于显示大小,还要靠交叉轴的配合。举个例子:

1
2
3
4
<View style={{ alignItems: 'center', flexDirection: 'column', flex: 1, backgroundColor: 'blue' }}>
<View style={{ width: 100, backgroundColor: 'yellow', flex: 1 }} />
<View style={{ backgroundColor: 'red', flex: 1 }} />
</View>

给上方元素指定宽度,下方元素没有宽度和内容。显示效果如下图:
上方元素指定宽度
可以看到,虽然下方元素没有显示,但在主轴方向上还是占据了一半空间(设置了flex:1 属性)。所以主轴的剩余空间还是按比例分配了,只是交叉轴长度为0,导致下方元素不显示。

总结

虽然是针对React Native的探究,但经过我的简单测试,网页上的flex布局也遵循这个原理。
这次学习的过程,让我有两个很深刻的体会。

  1. 不能只关注效果,忽略了场景。因为弹性盒模型上手非常容易, alignItem属性描述又通俗易懂——元素在交叉轴方向上的对齐方式。所以就忽略了使用这个属性的具体场景。对于技术的使用,效果很重要,但适用的场景也必须清楚才行。 (仔细想想自己有点傻,当然是不使用交叉轴所有空间时,对齐方式才有意义,都占满了还指定对齐方式干嘛?)
  2. 一筹莫展时先去看看官方文档。学习新技术时,优秀的博客或教程有时比官方文档更易上手,所以我们往往忽略了官方文档。即使去阅读了,作为一个经验不足的初学者,文档的一些内容不是很理解,或者在阅读过程中忽略了看似没用但实则重要的说明。当有了一定经验,碰到棘手的问题时,再去看官方文档,可能有新的理解,问题便迎刃而解。
Germinate's Blog

React Native使用Chrome调试全局(GLOBAL)变量

发表于 2017-02-10 |

使用React Native开发过程中,将本地存储的模块react-native-storage放入了全局中,期望使用chrome调试时,可以像网页调试一样直接在控制台调试api,这样比在应用程序中调试方便很多。
但是,在控制台中,GLOBAL是未定义的,更别提访问全局的变量了。经过查找资料,发现了问题所在。
React Native 的chrome调试工具,使用Web Worker运行app的程序,但调试界面默认使用网页页面的运行环境。Web Worker有自己的运行环境,与页面的运行环境不共享变量,所以无法访问到GLOBAL。解决方法非常简单,把运行环境切换为debuggerWorker.js即可。如下图所示:
切换运行环境
之后,就可以使用GLOBAL中的变量了。
效果

Germinate's Blog

GBK与UTF-8编码错误转换后,无法再正确恢复

发表于 2016-12-31 |

阅读本文之前,需要了解GBK与UTF-8的编码知识,相关内容可以参考上一篇博文。

字符集错误转换导致的问题

UTF-8格式编码的字节流,按GBK字符集转换为字符串,会出现乱码,这很正常。但将其重新转为字节流,再用UTF-8字符集转为字符串,还是乱码。这就让我产生了疑惑,虽然使用错误的字符集必然导致乱码,但字节的信息并没有改变,因此再转为字节流,用正确的字符集解码,应该得到正常的字符串。但事实是,被错误字符集转换过的字符串,无法恢复到原来的字符集。

问题的根本原因

造成该问题的根源是字节发生了变化。GBK或UTF-8遇到无法解析的字符时,会使用特殊的字符代替,因此造成原有字节信息的丢失,无法恢复。

错误转换的分析

UTF-8 → GBK

对于一串UTF-8编码的字节流,使用GBK进行解码。连续两个大于127的字节被认为是一个GBK编码的字符;若只读到一个大于127的字节,便发生错误,无法解析。此时,用字符‘?’代替错误字节,ASCII码是63。
以“樊”字为例,UTF-8编码使用三个字节表示该字符,字节码为[11100110, 10101000, 10001010]([e6, a8, 8a])。使用GBK解码时,读到第一个字节大于127,则取两个字节解析为一个GBK字符。前两个字节e6 8a被解析为GBK字符——妯。 第三个字节无法解析,所以赋值为?,最后的结果是 妯?。
可以看出,最后一个字节的信息丢失了,由8a变成3F,即使把结果再转换为字节流,也无法用utf-8字符集正确解析了。

GBK → UTF-8

对于一串GBK编码的字节流,使用UTF-8解码。UTF-8对于字节的格式有严格要求,当解析某个字符失败时,使用‘�’(UTF-8编码为EF BF BD)代替。
继续以“樊”字为例,其GBK字节码为[10110111, 10101110]([B7, AE])。使用UTF-8解码时,根据规则,要求10开头的字节之前,必须有字节标识一个字符的长度,所以两个字节都无法解析。最后的字符串是��。
可以看出,所有的字节信息都丢失了,因此无法再使用GBK解析该字符串。
注意,UTF-8用�替换,是以字符为单位的。例如[11100110, 10101000, 01000001]使用UTF-8解码,得到的结果是�A,而不是��A。根据第一个字节的格式,UTF-8期望将三个字节转换为一个字符。但最后一个字节不符合要求,所以前两个字节被一个�代替。而不是每个字节都被�代替。

参考资料

  1. 把gbk的编码按utf-8来解码,可能导致的不可恢复的错误——Chinainvent
Germinate's Blog

UTF-8与GBK字符集解析

发表于 2016-12-29 |

字符集的三个要素

  • 字符表:要显示的字符的集合(例如所有的英文字符、所有的汉字)
  • 字符编码:字符表中的字符对应的二进制表示
  • 字符集:定义了一种规则,将一串二进制数字解释为一个字符

Unicode编码

Unicode是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。目前已经编码了12万+的字符。—— 维基百科
简单来说,Unicode就是给世界上存在的字符都赋予唯一的二进制编码。

Unicode直接做字符集的问题

因为Unicode要编码所有可能的字符,那么每个字符占用的字节长度就会变多。以英文为例,一个英文字符使用ASCII码只需要一个字节,而用Unicode需要4个字节,甚至更多。并且前面的字节都是0。这样纯英文文件的存储大小就会成倍扩大,是极大的浪费。

UTF-8编码

一种变长的编码方式,基于Unicode的一种实现。它使用1–4个字节表示一个字符,根据不同的字符变化字节的长度,可以节省空间。编码规则如下:

  • 对于单字节的字符,字节的第一位设为0,后面7位是这个字符的unicode码。ASCII码表中的字符,UTF-8编码与其ASCII码保持一致。
  • 对于n个字节的字符(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位均设为10。剩余的二进制位,从后向前填上这个符号的unicode码。用0填充未使用的二进制位。
Unicode范围 编码格式 剩余位数
000000 – 00007F 0xxxxxxx 7位
000080 – 0007FF 110xxxxx 10xxxxxx 11位
000800 – 00FFFF 1110xxxx 10xxxxxx 10xxxxxx 16位
010000 – 10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 21位

根据上表可以看出,UTF-8剩余可以填的位数,决定了对应Unicode编码的范围。 下面举个转换的例子:
“樊”的unicode是6A0A(0110 1010 0000 1010),查表可知,6A0A属于第三行的范围,因此”樊”的UTF-8编码需要三个字节,即格式是 1110xxxx 10xxxxxx 10xxxxxx 。然后,从6A 0A的最后一个二进制位开始,从后向前填入格式中的x,多出的位补0。最终,”樊”的UTF-8编码是11100110 10101000 10001010(E6 A8 8A)
由此可以总结出字符用UTF-8编码的规律:

  • 4个字节的UTF-8十六进制编码一定是以F开头
  • 3个字节的UTF-8十六进制编码一定是以E开头
  • 2个字节的UTF-8十六进制编码一定是以C或D开头
  • 1个字节的UTF-8十六进制编码一定是以小于8的数字开头

注:UTF-8并没有编码所有的Unicode的字符,只包含了第0号平面(plane)和部分1号平面的字符。这属于更深层次的探究,感兴趣的读者可以通过UTF-8与Unicode字符平面映射了解。

中文编码

GB2312

该字符集使用2个字节表示一个字符。简单地理解,一个小于127的字节的意义与ASCII码相同,但两个大于127的字节连在一起时,就是GB2312编码的字符。以下是两个字节的具体范围:
第一个字节的范围是0xA1(161)–0xF7(247) ; 第二个字节的范围是0xA1(161)–0xFE(254)
共收录了6763个汉字,还包括拉丁字母、希腊字母、日文字符。并对 ASCII 里已有的数字、标点、字母赋予了两个字节的编码,这就是”全角”字符,而小于127的就称为”半角”字符。

GBK

由于GB2312仅包含了常用汉字,没有编码生僻字以及繁体字,GBK就对其进行了扩展。简单地理解,只要第一个字节大于127,不管后面的字节是否大于127,就是GBK编码的字符。以下是两个字节的具体范围:
第一个字节的范围是0x81(129)–0xFE(254) ; 第二个字节范围一部分在0x40(64)–0x7E(126),另一部分在0x80(128)–0xFE(254)
这样扩展之后的编码方案被称为 GBK 标准,GBK包括了GB2312 的所有内容,提供了23940个编码,使用了21886个。

中文编码的存在价值

UTF-8编码汉字通常需要三个字节,而GBK只需要两个字节,所以对于纯中文、不考虑国际化,且对流量和存储大小比较敏感的应用,可以使用GBK编码节省存储空间和传输流量。

参考资料

  1. 字符编码笔记:ASCII,Unicode和UTF-8 —— 阮一峰
  2. UTF-8 —— 维基百科
  3. 汉字内码扩展规范 —— 维基百科
Shuhao Zhang

Shuhao Zhang

Compile-Link-Run-Success!

8 日志
© 2016 - 2018 Shuhao Zhang
由 Hexo 强力驱动
主题 - NexT.Pisces
本站访问量次