brianway

brianway 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 brianway.github.io 编辑
编辑

服务器端研发爱好者.
阅读最新博文请前往 brianway.github.io

个人动态

brianway 赞了文章 · 2019-12-29

《Maven实战》阅读总结(三)Maven生命周期与插件

Maven生命周期与插件

除了坐标、依赖以及仓库之外,Maven另外两个核心概念是生命周期和插件。命令行的输入往往对应生命周期,而生命周期是抽象的,实际行为由插件完成。

何为生命周期

在Maven出现之前,项目构建的生命周期就已经存在,清理、编译、测试及部署。但公司与公司间,项目与项目间,往往使用不同方式做类似的工作。
Maven的生命周期对所有的构建过程进行抽象和统一。这个生命周期包括项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。
Maven的生命周期是抽象的,实际任务都交由插件来完成。这种思想与设计模式中的模板方法(Template Method)非常相似,生命周期是模板方法,而插件就是子类覆写的方法。

总结:Maven的生命周期定义了项目构建的流程,而插件负责实现流程中每个步骤,最后将插件与生命周期流程中每个步骤绑定即可完成项目构建。在这个过程中插件可以在多个项目中复用,而通过更换插件绑定亦可实现差异化构建。

生命周期详解

Maven拥有三套相互独立的生命周期。

生命周期阶段(phase)说明
cleanpre-clean执行清理前需要完成的工作
clean清理上一次构建生成的文件
post-clean执行清理后需要完成的工作
defaultvaildate验证,确保当前配置和POM内容是有效的,包含对POM文件树的验证。
intianlize初始化,执行构建生命周期的主任务之前的初始化
generate-sources生成源码,代码生成器生成后期阶段中处理或编译的源代码
proccess-sources处理源码,提供解析、修改和转换源码。常规源码和生成的源码都可以再这里处理
generate-resoureces生成资源,生成非源码资源,通常包括元数据文件和配置文件
process-resources处理资源,处理非源码资源,修改、转换和重定位资源都能在这阶段发生
compile编译,编译源码。编译过的类被放到目标目录树中
process-classes处理类,处理类文件转换和增强步骤。字节码交织器和常用工具常在这一阶段操作
generate-test-sources生成测试源码,生成要操作的单元测试代码
process-test-sources处理测试源码,在编译前对测试源码执行任何必要的处理。修改、转换或复制源代码
generate-test-resources生成测试资源,生成与测试相关的非源码资源
process-test-resources处理测试资源,处理、转换或重新定位于测试相关的资源
test-compile测试编译,编译单元测试的源码
process-test-classes处理测试类,对编译生成文件做后期处理(Maven2.0.5及以上)
test测试,运行编译过的单元测试并累计结果
prepare-package执行打包前的所有操作(Maven2.1及以上)
package打包,将可执行的二进制文件打包到一个分布式归档文件中,如jar或war
pre-integration-test前集成测试,准备集成测试,将归档文件部署到一个服务器上执行
integration-test集成测试,执行真正的集成测试,指在一个受到一定控制的模拟的真实部署环境中测试代码
post-integration-test后集成测试,解除集成测试准备,涉及环境重置或重新初始化
verify检验,检验可部署归档文件的有效性和完整性,通过后,将安装该归档
install安装,将项目包安装到本地仓库,供其他项目依赖
deploy部署,将项目发布到远程仓库,供其他开发人员与项目共享
sitepre-site执行一些在生成项目站点之前需要完成的工作
site生成项目站点文档
peo-site执行一些在生成项目站点之后需要完成的工作
site-deploy将生成的项目站点发布到服务器上

生命周期详解

从命令行执行Maven任务的最主要方式就是调用Maven的生命周期阶段。
Ps:Maven的三套生命周期是相互独立的,而一个生命周期阶段是有前后依赖关系的。
如:
mvn clean:该命令调用clean生命周期的clean阶段,实际执行clean生命周期中的pre-clean,clean阶段
mvn test:该命令调用default生命周期的test阶段,实际执行default生命周期中vaildate至test所有阶段
mvn clean install:该命令调用clean生命周期的clean阶段以及default生命周期的install阶段,实际执行clean生命周期中的pre-clean,clean阶段,以及default生命周期的vaildate至install所有阶段。
mvn clean deploy site-deploy:该命令调用clean生命周期的clean阶段、default生命周期的deploy阶段以及site生命周期的site-deploy阶段,实际执行实际执行clean生命周期中的pre-clean,clean阶段、default生命周期的所有阶段以及site生命周期的所有阶段。
由于Maven中主要生命周期阶段不多,而常用Maven命令都是基于这些阶段简单组合而成,因此只要对Maven生命周期有基本的理解,就可以正确而熟练地使用Maven命令

插件目标

Maven的核心仅定义了抽象的生命周期,具体的任务是交由插件完成,插件以独立的构件形式存在。
对于插件本身,为了代码复用,它往往具备多个功能,而每个功能都统称为插件目标(Plugin Goal)。
如:maven-dependency-plugin,基于项目依赖做很多事情。
1 帮助分析项目依赖,帮助找出潜在的无用依赖;
2 列出项目依赖树,帮助分析依赖来源
3 列出项目已解析的依赖 等...
这些任务有很多代码可以复用。因此,这些功能聚集在一个插件中,每个功能就是一个插件目标。
插件目标使用语法:
1 完整命令 mvn groupId:artifactId:version:goal 如 mvn org.apache.maven.plugins:maven-dependency-plugin:3.0.2:tree
2 简化version mvn groupId:artifactId:goal 如 mvn org.apache.maven.plugins:maven-dependency-plugin:tree
3 使用插件前缀 mvn 插件前缀:goal 如 mvn dependency:tree

插件绑定

Maven的生命周期与插件目标相互绑定,用以完成实际的构建任务。

内置绑定

为了使用户几乎不用任何配置就能构建Maven项目,Maven在核心为一些主要的生命周期阶段绑定了很多插件的目标,
当用户通过命令行调用生命周期时,对应的插件目标就会执行相应的任务。
如下图(default生命周期的阶段与插件目标的绑定关系由项目打包类型决定(packaging元素)下图以jar包构建为例

生命周期阶段(phase)内置插件执行任务
cleanpre-clean
cleanmaven-clean-plugin:clean删除项目的输出目录
post-clean
defaultvaildate
intianlize
generate-sources
proccess-sources
generate-resoureces
process-resourcesmaven-resources-plugin:resources复制主资源文件至主输出目录
compilemaven-compiler-plugin:compile编译主代码至主输出目录
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resourcesmaven-resources-plugin:testResources复制测试资源文件至测试输出目录
test-compilemaven-compiler-plugin:testCompile编译测试代码至测试输出目录
process-test-classes
testmaven-surefire-plugin:test执行测试用例
prepare-package
packagemaven-jar-plugin:jar创建项目jar包
pre-integration-test
integration-test
post-integration-test
verify
installmaven-install-plugin:install将项目输出构件安装到本地仓库
deploymaven-deploy-plugin:deploy将项目输出构件安装到远程仓库
sitepre-site
sitemaven-site-plugin:site生成项目站点
peo-site
site-deploymaven-site-plugin:deploy将项目站点部署到远程服务器上

Ps:空白的生命周期阶段,默认没有绑定任何插件,因此也没有任何实际行为
除默认打包类型jar外,常见的打包类型还有war、pom、maven-plugin、ear等。查看相关类型插件绑定官网http://maven.apache.org/guide...

自定义绑定

除了内置绑定以外,用户可以选择将某个插件目标绑定到生命周期的某个阶段,能让Maven项目在构建过程中执行更多更富特色的任务。
如maven-source-plugin:jar-no-fork,能够将项目主代码打成jar文件。将其绑定到default生命周期的verify阶段上,在执行完继承测试后和安装构件之前创建源码jar包。配置如下

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.0.1</version>
            <executions>    <!-- 插件执行配置 -->
                <execution>    <!-- executions下每个execution子元素可以配置执行一个任务 -->
                    <id>attach-sources</id>    <!-- 配置任务id -->
               <!-- <phase>verify</phase> -->  <!-- phase元素配置绑定生命周期阶段 -->
                    <goals>    <!-- 配置要执行的插件目标 -->
                        <goal>jar-no-fork</goal>
                    </goals>
                <execution>
            </executions>
        </plugin>
    </plugins>
</build>

完成自定义插件绑定后,运行mvn verify即可。
在上述代码中,注释了phase元素也能实现绑定,原因是很多插件目标在编写时已经定义了默认绑定阶段,可以使用maven-help-plugin查看插件详细信息,了解插件目标的默认绑定阶段。
mvn help:describe -Dplugin=groupId:artifactId:version

当插件目标绑定到不同的生命周期时,其执行顺序会由生命周期阶段的先后顺序决定。如果多个目标被绑定到同一个阶段,这些插件声明的先后顺序决定目标的执行顺序。

插件配置

完成插件目标与生命周期绑定后,用户还可以配置插件目标的参数,进一步调整插件目标所执行的任务,以满足项目的需求。几乎所有Maven插件目标都有一些可配置参数,可通过命令行和POM配置等方式来配置参数。

1)命令行插件配置

使用-D参数,并伴随一个参数名=参数值的形式,来配置参数
(命令行参数是由插件参数的表达式(Expression)决定,并非所有插件目标参数都有表达式,只能在POM中配置)
如maven-surrfire-plugin插件提供了一个maven.test.skip参数,当其值为true时,就会跳过执行测试。
mvn install -Dmaven.test.skip=true

2)POM中插件全局配置

并非所有插件参数都适合从命令行配置,有些参数的值从项目构建到项目发布都不会改变,或很少改变,在POM文件中一次性配置显然比重复在命令行输入要方便。
如mvan-compiler-plugin 可以配置全局参数 实现compile以及testCompile任务都能使用全局配置。

<build>
    <plugins>
        <plugin>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
            <configuration>    <!-- 声明插件全局配置 所有基于该插件目标的任务,都会使用这些配置 -->
                ...
            </configuration>
        </plugin>
    </plugins>
</builds>

3)POM中插件任务配置

除了为插件配置全局的参数,还可以为某个插件任务配置特定的参数。
如maven-antrun-plugin 可以配置插件任务参数,使run目标任务在不同生命周期输出不同的语句。

<build>
    <plugins>
        <plugin>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
            <executions>
                <execution>
                    <id>...</id>
                    <phase>...</phase>
                    <goals>
                        <goal>...</goal>
                    </goals>
                    <configuration>    <!-- 插件任务一配置 -->
                        ...
                    </configuration>
                </execution>
                <execution>
                    <id>...</id>
                    <phase>...</phase>
                    <goals>
                        <goal>...</goal>
                    </goals>
                    <configuration>    <!-- 插件任务二配置 -->
                        ...
                    </configuration>
                </execution>
                ...
            </executions>
        </plugin>
    </plugins>
</builds>

获取插件信息

仅仅理解如何配置使用插件是不够的,实现一个构建任务,用户需知道去哪找到合适的插件,并详细了解该插件的配置点。由于Maven的插件非常多,而且这其中大部分没有完善的文档,因此使用正确的插件并进行正确的配置,不容易。

1)在线插件信息

基本上所有的Maven插件都来自于apache和Codehaus。

apache说明官方插件,用户多,稳定性好
详细列表http://maven.apache.org/plugi...
下载地址http://repo1.maven.org/maven2...
Codehaus说明文档和可靠性相对较差,遇到问题,往往需要自己看源码
详细列表http://mojo.codehaus.org/plug...
下载地址http://repository.codehaus.or...

虽然并非所有插件都提供了完善的文档,但一些核心插件的文档还是非常丰富的。
一般来说,通过阅读插件在文档中的使用介绍和实例,就应该能够在自己的项目中很好地使用该插件。
当我们需要了解非常细节的目标参数时,就需要进一步访问该插件每个目标的文档。文档详细解释了该参数的作用、类型等信息。

2)使用maven-help-plugin插件

除了访问在线的插件文档之外,还可以借助maven-help-plugin来获取插件的详细信息。
执行maven-help-plugin的describe目标,指定要查询的插件的坐标,可查询插件的坐标,前缀(Goal Prefix),目标信息
mvn help:describe -Dplugin = (groupId:artifactId[:version] | Goal Prefix) -Dgoal = goal -Ddetail

从命令行调用插件

mvn -h 显示mvn命令帮助,可以看到如下信息:

usage:mvn [options] [<goal(s)]>] [<phase(s)>]
Options:
...

options表示可用的选项,除了选项之外,mvn命令后面可以添加一个到多个goal和phase,分别指插件目标和生命周期阶段。mvn命令可以激活生命周期阶段,从而执行那些绑定在生命周期阶段上的插件目标。也可以直接执行插件目标,因为有些插件目标不适用于生命周期阶段,如maven-help-plugin:describe。
直接执行插件目标语法 在上文中已提及,可在插件目标中查看。

插件解析机制

在命令行中执行插件目标,可使用插件前缀替代坐标,方便用户使用和配置插件。
Maven的这一特性是双刃剑,虽然它简化了插件的使用和配置,但如果出现问题,用户很难定位出问题的插件构件。
如mvn help:system 执行了什么插件。它的坐标是什么。这与Maven的插件解析机制有关。

1)插件仓库

与依赖构件一样,插件构件同样基于坐标存储在Maven仓库中,在需要的时候,Maven会先从本地仓库寻找插件,不存在则从远程仓库找到插件,下载到本地仓库使用。
插件的远程仓库不同于依赖的远程仓库,配置方式也不同,插件仓库配置如下:

<pluginRepositories>
    <pluginRepository>
        ...    <!-- 此处与依赖远程仓库配置一样,可参考阅读总结二查看 -->
    </pluginRepository>
    ...
</pluginRepositoties>

Maven内置了插件仓库指向中央仓库,并关闭了对SNAPSHOT的支持。
一般来说,中央仓库所包含的插件完全能够满足我们的需要,因此也不需要配置其他的插件仓库,只有在很少的情况下,项目使用的插件无法在中央仓库找到,或者自己编写了插件,可以参考上述配置,在POM中或settings.xml中 加入其他插件仓库。

2)插件的默认groupId

在POM中配置插件时,如果该插件是Maven官方插件(groupId为org.apache.maven.plugins),则可以省略groupId配置。
不推荐使用这一特性,只省略一行配置,但会让团队中不熟悉Maven的成员感到费解。

3)解析插件版本

同样为了简化插件的配置和使用,用户可没有提供插件的版本,Maven会自动解析插件版本。
首先,Maven在超级POM中为所有核心插件设定了版本,超级POM是所有Maven项目的父POM,所有项目都继承了这个超级POM配置。所以用户使用插件未设定插件版本的情况有以下几种:

1 核心插件:通过超级POM继承设定版本
2 非核心插件:通过仓库元数据 groupId/artifactId/maven-metadata.xml ,遍历并归并本地仓库和远程仓库的仓库元数据,根据latest和release计算出插件的版本。Maven3之后使用release,避免使用latest获取到快照版本,因为快照版本的频繁更新会导致插件行为的不稳定。

4)解析插件前缀

mvn命令行支持使用插件前缀来简化插件的调用。Maven是如何通过插件前缀获取插件的坐标的?
插件前缀与groupId:artifactId是一一对应的,这种匹配关系存储在仓库元数据中,该仓库员数据位于groupId/maven-metadate.xml,Maven在解析插件仓库元数据时,会默认使用org.apache.maven.plugins和org.codehaus.mojo两个groupId,可以再settings.xml中配置其他groupId

<settings>
    ...
    <pluginGroups>
        <pluginGroup>...</pluginGroup>
    </pluginGroups>
    ...
<settings>

插件仓库元数据中存储了所有插件前缀与group:artifactId的对应关系
插件仓库元数据检查顺序为:apache -> codehaus -> 用户自定义插件组 -> 都不包含该前缀,则报错

查看原文

赞 5 收藏 4 评论 0

brianway 赞了文章 · 2019-12-05

对React children 的深入理解

React的核心为组件。你可以像嵌套HTML标签一样嵌套使用这些组件,这使得编写JSX更加容易因为它类似于标记语言。

当我刚开始学习React时,当时我认为“使用 props.children 就这么回事,我知道它的一切”。我错了。。

因为我们使用的事JavaScript,我们会改变children。我们能够给它们发送特殊的属性,以此来决定它们是否进行渲染。让我们来探究一下React中children的作用。

子组件

我们有一个组件 <Grid /> 包含了几个组件 <Row /> 。你可能会这么使用它:

<Grid>
  <Row />
  <Row />
  <Row />
</Grid>

这三个 Row 组件都成为了 Gridprops.children 。使用一个表达式容器,父组件就能够渲染它们的子组件:

class Grid extends React.Component {
  render() {
    return <div>{this.props.children}</div>
  }
}

父组件也能够决定不渲染任何的子组件或者在渲染之前对它们进行操作。例如,这个 <Fullstop /> 组件就没有渲染它的子组件:

class Fullstop extends React.Component {
  render() {
    return <h1>Hello world!</h1>
  }
}

不管你将什么子组件传递给这个组件,它都只会显示“Hello world!”

任何东西都能是一个child

React中的Children不一定是组件,它们可以使任何东西。例如,我们能够将上面的文字作为children传递我们的 <Grid /> 组件。

<Grid>Hello world!</Grid>

JSX将会自动删除每行开头和结尾的空格,以及空行。它还会把字符串中间的空白行压缩为一个空格。

这意味着以下的这些例子都会渲染出一样的情况:

<Grid>Hello world!</Grid>

<Grid>
  Hello world!
</Grid>

<Grid>
  Hello
  world!
</Grid>

<Grid>

  Hello world!
</Grid>

你也可以将多种类型的children完美的结合在一起:

<Grid>
  Here is a row:
  <Row />
  Here is another row:
  <Row />
</Grid>

child 的功能

我们能够传递任何的JavaScript表达式作为children,包括函数。

为了说明这种情况,以下是一个组件,它将执行一个传递过来的作为child的函数:

class Executioner extends React.Component {
  render() {
    // See how we're calling the child as a function?
    //                        ↓
    return this.props.children()
  }
}

你会像这样的使用这个组件

<Executioner>
  {() => <h1>Hello World!</h1>}
</Executioner>

当然,这个例子并没什么用,只是展示了这个想法。

假设你想从服务器获取一些数据。你能使用多种方法实现,像这种将函数作为child的方法也是可行的。

<Fetch url="api.myself.com">
  {(result) => <p>{result}</p>}
</Fetch>

不要担心这些超出了你的脑容量。我想要的是当你以后遇到这种情况时不再惊讶。有了children什么事都会发生。

操作children

如果你看过React的文档你就会说“children是一个不透明的数据结构”。从本质上来讲, props.children 可以使任何的类型,比如数组、函数、对象等等。

React提供了一系列的函数助手来使得操作children更加方便。

循环

两个最显眼的函数助手就是 React.Children.map 以及 React.Children.forEach 。它们在对应数组的情况下能起作用,除此之外,当函数、对象或者任何东西作为children传递时,它们也会起作用。

class IgnoreFirstChild extends React.Component {
  render() {
    const children = this.props.children
    return (
      <div>
        {React.Children.map(children, (child, i) => {
          // Ignore the first child
          if (i < 1) return
          return child
        })}
      </div>
    )
  }
}

<IgnoreFirstChild /> 组件在这里会遍历所有的children,忽略第一个child然后返回其他的。

<IgnoreFirstChild>
  <h1>First</h1>
  <h1>Second</h1> // <- Only this is rendered
</IgnoreFirstChild>

在这种情况下,我们也可以使用 this.props.children.map 的方法。但要是有人讲一个函数作为child传递过来将会发生什么呢?this.props.children 会是一个函数而不是一个数组,接着我们就会产生一个error!

err

然而使用 React.Children.map 函数,无论什么都不会报错。

<IgnoreFirstChild>
  {() => <h1>First</h1>} // <- Ignored ?
</IgnoreFirstChild>

计数

因为this.props.children 可以是任何类型的,检查一个组件有多少个children是非常困难的。天真的使用 this.props.children.length ,当传递了字符串或者函数时程序便会中断。假设我们有个child:"Hello World!" ,但是使用 .length 的方法将会显示为12。

这就是为什么我们有 React.Children.count 方法的原因

class ChildrenCounter extends React.Component {
  render() {
    return <p>React.Children.count(this.props.children)</p>
  }
}

无论时什么类型它都会返回children的数量

// Renders "1"
<ChildrenCounter>
  Second!
</ChildrenCounter>

// Renders "2"
<ChildrenCounter>
  <p>First</p>
  <ChildComponent />
</ChildrenCounter>

// Renders "3"
<ChildrenCounter>
  {() => <h1>First!</h1>}
  Second!
  <p>Third!</p>
</ChildrenCounter>

转换为数组

如果以上的方法你都不适合,你能将children转换为数组通过 React.Children.toArray 方法。如果你需要对它们进行排序,这个方法是非常有用的。

class Sort extends React.Component {
  render() {
    const children = React.Children.toArray(this.props.children)
    // Sort and render the children
    return <p>{children.sort().join(' ')}</p>
  }
}
<Sort>
  // We use expression containers to make sure our strings
  // are passed as three children, not as one string
  {'bananas'}{'oranges'}{'apples'}
</Sort>

上例会渲染为三个排好序的字符串。

sort

执行单一child

如果你回过来想刚才的 <Executioner /> 组件,它只能在传递单一child的情况下使用,而且child必须为函数。

class Executioner extends React.Component {
  render() {
    return this.props.children()
  }
}

我们可以试着去强制执行 propTypes ,就像下面这样

Executioner.propTypes = {
  children: React.PropTypes.func.isRequired,
}

这会使控制台打印出一条消息,部分的开发者将会把它忽视。相反的,我们可以使用在 render 里面使用 React.Children.only

class Executioner extends React.Component {
  render() {
    return React.Children.only(this.props.children)()
  }
}

这样只会返回一个child。如果不止一个child,它就会抛出错误,让整个程序陷入中断——完美的避开了试图破坏组件的懒惰的开发者。

编辑children

我们可以将任意的组件呈现为children,但是任然可以用父组件去控制它们,而不是用渲染的组件。为了说明这点,让我们举例一个 能够拥有很多 RadioButton 组件的 RadiaGroup 组件。

RadioButtons 不会从 RadioGroup 本身上进行渲染,它们只是作为children使用。这意味着我们将会有这样的代码。

render() {
  return(
    <RadioGroup>
      <RadioButton value="first">First</RadioButton>
      <RadioButton value="second">Second</RadioButton>
      <RadioButton value="third">Third</RadioButton>
    </RadioGroup>
  )
}

这段代码有一个问题。input 没有被分组,导致了这样:

为了把 input 标签弄到同组,必须拥有相同的name 属性。当然我们可以直接给每个RadioButtonname 赋值

<RadioGroup>
  <RadioButton name="g1" value="first">First</RadioButton>
  <RadioButton name="g1" value="second">Second</RadioButton>
  <RadioButton name="g1" value="third">Third</RadioButton>
</RadioGroup>

但是这个是无聊的并且容易出错。我们可是拥有JavaScript的所有功能的!

改变children的属性

RadioGroup 中我们将会添加一个叫做 renderChildren 的方法,在这里我们编辑children的属性

class RadioGroup extends React.Component {
  constructor() {
    super()
    // Bind the method to the component context
    this.renderChildren = this.renderChildren.bind(this)
  }

  renderChildren() {
    // TODO: Change the name prop of all children
    // to this.props.name
    return this.props.children
  }

  render() {
    return (
      <div className="group">
        {this.renderChildren()}
      </div>
    )
  }
}

让我们开始遍历children获得每个child

renderChildren() {
  return React.Children.map(this.props.children, child => {
    // TODO: Change the name prop to this.props.name
    return child
  })
}

我们如何编辑它们的属性呢?

永恒地克隆元素

这是今天展示的最后一个辅助方法。顾名思义,React.cloneElement 会克隆一个元素。我们将想要克隆的元素当作第一个参数,然后将想要设置的属性以对象的方式作为第二个参数。

const cloned = React.cloneElement(element, {
  new: 'yes!'
})

现在,clone 元素有了设置为 "yes!" 的属性 new

这正是我们的 RadioGroup 所需的。我们克隆所有的child并且设置name 属性

renderChildren() {
  return React.Children.map(this.props.children, child => {
    return React.cloneElement(child, {
      name: this.props.name
    })
  })
}

最后一步就是传递一个唯一的 nameRadioGroup

<RadioGroup name="g1">
  <RadioButton value="first">First</RadioButton>
  <RadioButton value="second">Second</RadioButton>
  <RadioButton value="third">Third</RadioButton>
</RadioGroup>

没有手动添加 name 属性给所有的 RadioButton ,我们只是告诉了 RadioGroup 所需的name而已。

总结

Children使React组件更像是标记而不是 脱节的实体。通过强大的JavaScript和一些React帮助函数使我们的生活更加简单。

文章同步于个人小站

查看原文

赞 29 收藏 19 评论 1

brianway 赞了文章 · 2019-09-05

在本地开发环境(Mac)中安装自签名证书,启用https

0. 前言

今天一大早上班Chrome浏览器提示已经自动升级(Version 63)需要重启浏览器,重启之后发现本地的开发环境打开不,原因是新版的浏览器强制将http转换成https了,而我本地的开发环境没有启用https。解决方案有两种:

  1. 将本地开发环境的域名(例如test.local)加入浏览的一个白名单,告诉浏览器该域名不需要强制启用https.
  2. 在本地生成一个自签名证书,并启用https.

第一种方式比较简单,在浏览器地址栏中输入chrome://net-internals/#hsts。在Delete domain 栏的输入框中输入要http访问的域名,然后点击delete地按钮,即可完成配置。然后你可以在Query domain栏中搜索刚才输入的域名,点击逗query地按钮后如果提示逗Not found即为成功。但是,我同事们用此方法都成功了,就我没有成功,无奈。。。遂研究第二种解决方案。

对于第二种解决方案,网上找了几个中文教程,但是新版本的浏览器已经不再适用。最后还是在强大的Google的帮助下找到此篇英文博客。https://deliciousbrains.com/https-locally-without-browser-privacy-errors/ 有兴趣的可以阅读原文,本文也并没有逐字翻译原文。教程也只适用于Mac系统,Windows系统还没有研究。

本教程假设本地域名为test.local

1. 生成自签名证书

  • 创建生成证书所需的配置文件

创建生成证书所需的配置文件,文件内容如下:

[ req ]

default_bits        = 2048
default_keyfile     = server-key.pem
distinguished_name  = subject
req_extensions      = req_ext
x509_extensions     = x509_ext
string_mask         = utf8only

[ subject ]

countryName                 = Country Name (2 letter code)
countryName_default         = US

stateOrProvinceName         = State or Province Name (full name)
stateOrProvinceName_default = NY

localityName                = Locality Name (eg, city)
localityName_default        = New York

organizationName            = Organization Name (eg, company)
organizationName_default    = Example, LLC

commonName                  = Common Name (e.g. server FQDN or YOUR name)
commonName_default          = Example Company

emailAddress                = Email Address
emailAddress_default        = test@example.com

[ x509_ext ]

subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer

basicConstraints       = CA:FALSE
keyUsage               = digitalSignature, keyEncipherment
subjectAltName         = @alternate_names
nsComment              = "OpenSSL Generated Certificate"

[ req_ext ]

subjectKeyIdentifier = hash

basicConstraints     = CA:FALSE
keyUsage             = digitalSignature, keyEncipherment
subjectAltName       = @alternate_names
nsComment            = "OpenSSL Generated Certificate"

[ alternate_names ]

DNS.1       = test.local

注意将文件的最后一行的改成自己的域名DNS.1=test.local。将文件保存并命名为test.local.conf

  • 生成证书,在命令行中运行:
openssl req -config test.local.conf -new -sha256 -newkey rsa:2048 -nodes -keyout test.local.key -x509 -days 365 -out test.local.crt

生成证书时,会有一系列问题需要填写,其他的问题都可以敲回车直接跳过,只将common name填写成你的域名,例如:

Common Name (e.g. server FQDN or YOUR name) []:test.local

命令运行成功会在当前目录下生成两个文件:test.local.crt, test.local.key

2. Nginx配置

关键的Nginx配置如下,其他部分省略:

server {
    listen 80;
    listen 443 ssl http2;
    server_name test.local;
    
    ssl on
    ssl_certificate     /etc/nginx/ssl/test.local.crt;
    ssl_certificate_key /etc/nginx/ssl/test.local.key;
    
    ...
}

3. 本机配置

重启nginx后,打开Chrome浏览器输入https://test.local,此时浏览器应该会提示Your connection is not private。打开浏览器调试工具,选择security选项卡,显示如下:
图片描述

将红框中的证书图标拖到桌面,会在桌面生成一个以cer为后缀的文件,双击文件,打开Keychain Access(需要输入密码)
图片描述

之后会打开一个列表:
图片描述

找到test.local的证书并双击,打开如下对话框:
图片描述

点击红框中的下拉菜单,将其设置为Always trust,然后关闭对话框(会再次要求输入密码确认)。完成之后,刷新浏览器页面即可正常打开,并且显示已经正常启用https。

参考文章:

https://deliciousbrains.com/h...
https://zhidao.baidu.com/ques...
查看原文

赞 6 收藏 7 评论 0

brianway 赞了文章 · 2019-03-06

Promise--优雅解决回调嵌套

最近一直在用空余时间研究node,当我写了一个简单的复制一个文件夹中的文件到另一个位置的时候,我看到了所谓的回调地狱,虽然只是四五个回调嵌套,但是这已经让我感到害怕,我写这么简单的一个小demo就写成这样,那稍微复杂点儿还了得?记得在看ES6的时候,里面提到过一种新的解决回调的方式---Promise,而且在node中也常用这个解决大量嵌套,所以这几天花了点儿时间看了看Promise,让我对Promise的认识更加清晰,所以写一些东西总结一下。

Promise状态的理解

new Promise实例化的Promise对象有三个状态:

  • “has-resolution” - Fulfilled

    • reslove(成功时),调用onFulfilled

  • "has-rejection" - Rejected

    • reject(失败时)。调用Rejected

  • "unresolve" - Pending

    • 既不是resolve也不是reject状态,也就是Promise刚刚被创建后的初始化状态。

图片描述

note:

  1. 在Chrome中输出resolve可以得到Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined},可以看出[[PromiseStatus]]中存储的就是Promise的状态,但是并没有公开访问[[PromiseStatus]]的用户API,所以暂时还无法查询其内部状态。

  2. Promise中的then的回调只会被调用一次,因为Promise的状态只会从Pending变为Fulfilled或者Rejected,不可逆。

Promise的使用

在使用Promise实现有序执行异步的基本格式如下:

//defined Promise async function
function asyncFun(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            reslove(/*reslove parameter*/);
        }else{
            reject(new Error(/*Error*/));
        }
    })
}

//use Promise&then
asyncFun().then(/*function*/).then(/*function*/)...

reslove方法的参数就是要传给回调函数的参数,即resolve将运行得到的结果传出来,而then接受该参数给回调继续执行后面的,如果这个then的中的函数还会返回Promise,则会重复执行该步骤直到结束。

reject方法的参数一般是包含了reject原因的Error对象。rejectresolve一样,也会将自己的参数传出去,接收该参数的是then的第二个fun或者是catch。其实.catch只是Promise.then(onFulfilled,onRejected)的别名而已。

快捷创建Promise

一般情况下我们会使用new Promise来创建prmise对象,除此之外我们也可以使用Promise.reslovePromise.reject来直接创建,例如Promise.resolve(42)可以认为是以下代码的语法糖

new Promise((reslove)=>{
    reslove(42);
});

这段代码可以让这个Promise对象立即进入resolve状态,并将42传递给后面then里所指定的onFulfilled函数。此外Promise.resolve还有一个作用,就是将非Promise对象转换为Promise对象。

Promise.reject(value)与之类似。

Promise.then()的异步调用带来的思考

var promise = new Promise(function (resolve){
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then(function(value){
    console.log(value); // 3
});
console.log("outer promise"); // 2

/*输出:
"inner promise"
"outer promise"
42
*/

从以上的这段代码我们可以看出Promise.then()是异步调用的,这也是Promise设计上规定的,其原因在于同步调用和异步调用同时存在会导致混乱

以上那段代码如果在调用onReady之前DOM已经载入的话,对回调函数进行同步调用,如果在调用onReady之前DOM还没有载入的话,通过注册DOMContentLoader事件监听器来对回调进行异步调用。这会导致该代码在源文件中不同位置输出不同的结果,关于这个现象,有如下几点:

  • 绝对不能对异步函数(即使在数据已经就绪)进行同步调用

  • 如果对异步回调函数进行同步调用,处理顺序可能会与预期不符,带来意外的结果

  • 对异步回调函数进行同步调用,还可能导致栈溢出或者异常处理错乱等问题

  • 如果想在将来的某个时刻调用异步回调,可以使用setTimeout等异步API

所以以上代码应该使用 setTimeout(fn, 0)进行调用。

function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        setTimeout(fn, 0);
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

所以在Promise中then是异步的。

Promise链式调用

各个Task相互独立

如果想实现Promise的链式调用,要求每次链式调用都返回Promise。所以每个异步执行都需要使用Promise包装,这里有一个误区:每个thencatch会返回也会反回一个新的Promise,但是这仅仅实现了链式调用,如果不将异步操作用Promise进行包装,依然不行。下面的例子就是错误的

function pro1(){
    return new Promise((reslove,reject)=>{
        if(reslove){
            setTimeout(()=>{console.log(1000)},1000);
            reslove();
        }
    })
}

function pro2(){
    setTimeout(()=>{console.log(2000)},2000);
}

function pro3(){
    setTimeout(()=>{console.log(3000)},3000);
}

pro1().then(pro2).then(pro3);
//or
function pro1(){
    setTimeout(()=>{console.log(1000)},1000);
}

Promise.resolve().then(pro1).then(pro2).then(pro3);

上面的写法有两处错误:

  1. 虽然在第一个函数返回了一个Promise,但是由于后面的异步操作并没有被Promise包装,所以并不会起任何作用,正确的做法是每一个异步操作都要被Promise包装

  2. resolve()调用的时机不对,resolve需要在异步操作执行完成后调用,所以需要写在异步操作内部,如果像上面那样写在异步操作外面,则不会起作用。

所以正确写法如下:

//直接返回Promise
function pro1(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(1000);resolve();},1000);
        
    })
}
function pro2(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(5000);resolve();},5000);
        
    });
}
function pro3(){
    return new Promise((resolve,reject)=>{
        setTimeout(()=>{console.log(500);resolve();},500);
    })
}
pro1().then(pro2).then(pro3);

//or使用Promise.reslove()

function pro1(cb){setTimeout(()=>{console.log(1000);cb()},1000)};
function pro2(cb){setTimeout(()=>{console.log(3000);cb()},3000)};
function pro3(cb){setTimeout(()=>{console.log(500);cb()},500)};


Promise.resolve()
       .then(()=>new Promise(resolve=>pro1(resolve)))
       .then(()=>new Promise(resolve=>pro2(resolve)))
       .then(()=>new Promise(resolve=>pro3(resolve)));

各个Task需要参数的传递

在Promise的链式调用中,有可能各个task之间存在相互依赖,例如TaskA想给TaskB传递一个参数,像下面这样:

/*例1.使用Promise.resolve()启动*/
let task1 = (value1)=>value1+1;
let task2 = (value2)=>value2+2;
let task3 = (value3)=>{console.log(value3+3)};

Promise.resolve(1).then(task1).then(task2).then(task3);//console => 7


/*例2.普通的返回一个Promise*/
function task1(value1){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value1+1);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

function task2(value2){
  return new Promise((resolve,reject)=>{
    if(resolve){
      resolve(value2+2);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}
function task3(value3){
  return new Promise((resolve,reject)=>{
    if(resolve){
      console.log(value3+3);
    }else{
      throw new Error("throw Error @ task1");
    }
  });
}

task1(1).then(task2).then(task3);//console => 7

关于reslovereject有以下两点说明:

  • reslove函数的作用是将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果作为参数传递出去

  • reject函数的作用是将Promise对象状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时候调用,并将异步操作报出的错误作为参数传递出去

所以从上面的例子和它们的用法可以看出,如果想要传递给后面task有两种方法:

  • 如果使用Promise.resolve()启动Promise,则像例1中那样在需要传递的参数前面加return即可。

  • 如果是利用Promise包装了任务,则把想要传递给下一个task的参数传入resolve()即可。

特别说明:如果需要resolve()往后传递多个参数,不能直接写resolve(a1,a2,a3),这样只能拿到第一个要传的参数,需要以数组或对象去传递

let obj = {a1:a1,a2:a2,a3:a3};
resolve(obj)
//or
let arr =[a1,a2,a3];
resolve(arr);

thencatch返回新的Promise

在Promise中无论是then还是catch方法,都会返回返回一个新的Promise对象。

图片描述

var aPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = aPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

所以像下面这样将链式调用分开写是不成功的

// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
    resolve(100);
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    console.log("1: " + value); // => 100
});

由于每次调用then方法都会返回一个新的Promise,所以导致最终输出100而不是100 2 2。

Promise.all()的使用

有时候需要多个彼此没有关联的多个异步任务全部执行完成后再执行后面的操作,这时候就需要用到Promise.all(),它接收一个Promise的对象的数组作为参数,当这个数组里的所有Promise对象全部变成resolve或者reject的时候,它才会去调用后面的.then()

这里需要说明一点,两个彼此无关的异步操作会同时执行,每个Promise的结果(即每个返回的Promise的resolve或reject时传递的参数)和传递给Promise.all的Promise数组的顺序一致。也就是说,假设有两个异步操作TaskA和TaskB,如果传入顺序为Promise.all([TaskA,TaskB]),则执行完成后传给.then的顺序为[TaskA,TaskB]。

function setTime(time){
  return new Promise((resolve)=>{
    setTimeout(()=>resolve(time),time);
  })
}

let startTime = Date.now();

Promise.all([setTime(1),setTime(100),setTime(200)])
       .then((value)=>{
         console.log(value);    //[1,100,200]
         console.log(Date.now() - startTime); //203
       });

从上面函数的输出值可以看出Promise.all()里的异步操作是同时执行的而且传给.then()的顺序和Promise.all()里的顺序一样。最终执行时间约为200ms,为什么不是200ms,这里涉及到关于setTimeout的精准问题,不在这里讨论。

Promise.race()的使用

Promise.rance()的用法与Promise.all()类似,不同的地方在于Promise.all()是在接收到的所有Promise都变为FulFilled或者Rejected状态之后才会继续进行后面的处理,而Promise.rance()只要有一个Promise对象进入FullFilled或者Rejected状态,就会继续进行后续处理。这相当于Promise.all()进行运算而Promise.rance()进行运算。但是这里有一点需要注意一下:

var taskA = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskA');
            resolve('this is taskA');
        }, 4);
    });
var taskB = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is taskB');
            resolve('this is taskB');
        }, 1000);
    });

Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);
});

/*
输出结果:
this is taskA
this is taskA
this is taskB
*/

从这里可以看出,在第一个Promise变为FulFiled状态运行then里的回调后,后面的Promise并没有停止运行,而是接续执行。也就是说, Promise.race 在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行。

Promise的reject和异步操作error的理解

function ReadEveryFiles(file){
    return new Promise((resolve,reject)=>{
        if(resolve){
            fs.readFile(`${__dirname}/jQuery/${file}`,(err,data)=>{
                if(err){
                    console.log(err);
                }else{
                    let obj = {data:data,file:file};
                    resolve(obj);
                }
            });
        }else{
            //promise reject error
        }
    });
}

这里的readFile的error和Promise的reject不一样,一个是readFile过程中导致的错误,而另一个是Promise做处理的时候导致的错误,可以这样理解,假设读取文件成功了,但是Promise还需要讲这个异步操作得到的数据拿到处理,在Promise做这些操作的时候可能出错。

写在最后

这几天开始用Promise写了一些东西,发现其实如果用Promise,会使得代码量加大,因为每一个异步都要被Promise封装,但是这样换来的却是更加容易的维护,所以还是值得的,当代码写完后,我们很容易就能看出代码的执行过程,相对于原来用嵌套去写要直观许多,而如果想要解决Promise的代码量过大的问题,我们可以使用Generator函数,另外,在ES7标准中推出了更加牛的异步解决方案Async/Await,关于它们,我将会在随后继续深入。

参考

JavaScript Promise迷你书(中文版)
ECMAScript 6 入门---Promise对象

查看原文

赞 16 收藏 37 评论 10

brianway 收藏了文章 · 2019-01-18

Java 正则表达式详解

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009162306

1. 正则表达式

1.1 什么是正则表达式

正则表达式
: 定义一个搜索模式的字符串。

正则表达式可以用于搜索、编辑和操作文本。

正则对文本的分析或修改过程为:首先正则表达式应用的是文本字符串(text/string),它会以定义的模式从左到右匹配文本,每个源字符只匹配一次。

1.2 示例

正则表达式匹配
this is text精确匹配字符串 "this is text"
this\s+is\s+text匹配单词 "this" 后跟一个或多个空格字符,后跟词 "is" 后跟一个或多个空格字符,后跟词 "text"
^\d+(\.\d+)?^ 定义模式必须匹配字符串的开始,d+ 匹配一个或多个数字,? 表明小括号内的语句是可选的,\. 匹配 ".",小括号表示分组。例如匹配:"5"、"1.5" 和 "2.21"

2. 正则表达式的编写规则

2.1 常见匹配符号

正则表达式描述
.匹配所有单个字符,除了换行符(Linux 中换行是 \n,Windows 中换行是 \r\n
^regex正则必须匹配字符串开头
regex$正则必须匹配字符串结尾
[abc]复选集定义,匹配字母 a 或 b 或 c
[abc][vz]复选集定义,匹配字母 a 或 b 或 c,后面跟着 v 或 z
[^abc]当插入符 ^ 在中括号中以第一个字符开始显示,则表示否定模式。此模式匹配所有字符,除了 a 或 b 或 c
[a-d1-7]范围匹配,匹配字母 a 到 d 和数字从 1 到 7 之间,但不匹配 d1
XZ匹配 X 后直接跟着 Z
X|Z匹配 X 或 Z

2.2 元字符

元字符是一个预定义的字符。

正则表达式描述
\d匹配一个数字,是 [0-9] 的简写
\D匹配一个非数字,是 [^0-9] 的简写
\s匹配一个空格,是 [ \t\n\x0b\r\f] 的简写
\S匹配一个非空格
\w匹配一个单词字符(大小写字母、数字、下划线),是 [a-zA-Z_0-9] 的简写
\W匹配一个非单词字符(除了大小写字母、数字、下划线之外的字符),等同于 [^\w]

2.3 限定符

限定符定义了一个元素可以发生的频率。

正则表达式描述举例
*匹配 >=0 个,是 {0,} 的简写X* 表示匹配零个或多个字母 X,.* 表示匹配任何字符串
+匹配 >=1 个,是 {1,} 的简写X+ 表示匹配一个或多个字母 X
?匹配 1 个或 0 个,是 {0,1} 的简写X? 表示匹配 0 个或 1 个字母 X
{X}只匹配 X 个字符\d{3} 表示匹配 3 个数字,.{10} 表示匹配任何长度是 10 的字符串
{X,Y}匹配 >=X 且 <=Y 个\d{1,4} 表示匹配至少 1 个最多 4 个数字
*?如果 ? 是限定符 *+?{} 后面的第一个字符,那么表示非贪婪模式(尽可能少的匹配字符),而不是默认的贪婪模式

2.4 分组和反向引用

小括号 () 可以达到对正则表达式进行分组的效果。

模式分组后会在正则表达式中创建反向引用。反向引用会保存匹配模式分组的字符串片断,这使得我们可以获取并使用这个字符串片断。

在以正则表达式替换字符串的语法中,是通过 $ 来引用分组的反向引用,$0 是匹配完整模式的字符串(注意在 JavaScript 中是用 $& 表示);$1 是第一个分组的反向引用;$2 是第二个分组的反向引用,以此类推。

示例:

package com.wuxianjiezh.demo.regex;

public class RegexTest {

    public static void main(String[] args) {
        // 去除单词与 , 和 . 之间的空格
        String Str = "Hello , World .";
        String pattern = "(\\w)(\\s+)([.,])";
        // $0 匹配 `(\w)(\s+)([.,])` 结果为 `o空格,` 和 `d空格.`
        // $1 匹配 `(\w)` 结果为 `o` 和 `d`
        // $2 匹配 `(\s+)` 结果为 `空格` 和 `空格`
        // $3 匹配 `([.,])` 结果为 `,` 和 `.`
        System.out.println(Str.replaceAll(pattern, "$1$3")); // Hello, World.
    }
}

上面的例子中,我们使用了 [.] 来匹配普通字符 . 而不需要使用 [\\.]。因为正则对于 [] 中的 .,会自动处理为 [\.],即普通字符 . 进行匹配。

2.4.1 仅分组但无反向引用

当我们在小括号 () 内的模式开头加入 ?:,那么表示这个模式仅分组,但不创建反向引用。

示例:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "img.jpg";
        // 分组且创建反向引用
        Pattern pattern = Pattern.compile("(jpg|png)");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
        }
    }
}

运行结果:

jpg
jpg

若源码改为:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "img.jpg";
        // 分组但不创建反向引用
        Pattern pattern = Pattern.compile("(?:jpg|png)");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
        }
    }
}

运行结果:

jpg
Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 1
    at java.util.regex.Matcher.group(Matcher.java:538)
    at com.wuxianjiezh.regex.RegexTest.main(RegexTest.java:15)

2.4.2 分组的反向引用副本

Java 中可以在小括号中使用 ?<name> 将小括号中匹配的内容保存为一个名字为 name 的副本。

示例:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "@wxj 你好啊";
        Pattern pattern = Pattern.compile("@(?<first>\\w+\\s)"); // 保存一个副本
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
            System.out.println(matcher.group("first"));
        }
    }
}

运行结果:

@wxj 
wxj 
wxj 

2.5 否定先行断言(Negative lookahead)

我们可以创建否定先行断言模式的匹配,即某个字符串后面不包含另一个字符串的匹配模式。

否定先行断言模式通过 (?!pattern) 定义。比如,我们匹配后面不是跟着 "b" 的 "a":

a(?!b)

2.6 指定正则表达式的模式

可以在正则的开头指定模式修饰符。

  • (?i) 使正则忽略大小写。
  • (?s) 表示单行模式("single line mode")使正则的 . 匹配所有字符,包括换行符。
  • (?m) 表示多行模式("multi-line mode"),使正则的 ^$ 匹配字符串中每行的开始和结束。

2.7 Java 中的反斜杠

反斜杠 \ 在 Java 中表示转义字符,这意味着 \ 在 Java 拥有预定义的含义。

这里例举两个特别重要的用法:

  • 在匹配 .{[(?$^* 这些特殊字符时,需要在前面加上 \\,比如匹配 . 时,Java 中要写为 \\.,但对于正则表达式来说就是 \.
  • 在匹配 \ 时,Java 中要写为 \\\\,但对于正则表达式来说就是 \\

注意:Java 中的正则表达式字符串有两层含义,首先 Java 字符串转义出符合正则表达式语法的字符串,然后再由转义后的正则表达式进行模式匹配。

2.8 易错点示例

  • [jpg|png] 代表匹配 jpgpng 中的任意一个字符。
  • (jpg|png) 代表匹配 jpgpng

3. 在字符串中使用正则表达式

3.1 内置的字符串正则处理方法

在 Java 中有四个内置的运行正则表达式的方法,分别是 matches()split())replaceFirst()replaceAll()。注意 replace() 方法不支持正则表达式。

方法描述
s.matches("regex")当仅且当正则匹配整个字符串时返回 true
s.split("regex")按匹配的正则表达式切片字符串
s.replaceFirst("regex", "replacement")替换首次匹配的字符串片段
s.replaceAll("regex", "replacement")替换所有匹配的字符

3.2 示例

示例代码:

package com.wuxianjiezh.regex;

public class RegexTest {

    public static void main(String[] args) {
        System.out.println("wxj".matches("wxj"));
        System.out.println("----------");

        String[] array = "w x j".split("\\s");
        for (String item : array) {
            System.out.println(item);
        }
        System.out.println("----------");

        System.out.println("w x j".replaceFirst("\\s", "-"));
        System.out.println("----------");

        System.out.println("w x j".replaceAll("\\s", "-"));
    }
}

运行结果:

true
----------
w
x
j
----------
w-x j
----------
w-x-j

4. 模式和匹配

Java 中使用正则表达式需要用到两个类,分别为 java.util.regex.Patternjava.util.regex.Matcher

第一步,通过正则表达式创建模式对象 Pattern

第二步,通过模式对象 Pattern,根据指定字符串创建匹配对象 Matcher

第三步,通过匹配对象 Matcher,根据正则表达式操作字符串。

来个例子,加深理解:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String text = "Hello Regex!";

        Pattern pattern = Pattern.compile("\\w+");
        // Java 中忽略大小写,有两种写法:
        // Pattern pattern = Pattern.compile("\\w+", Pattern.CASE_INSENSITIVE);
        // Pattern pattern = Pattern.compile("(?i)\\w+"); // 推荐写法
        Matcher matcher = pattern.matcher(text);
        // 遍例所有匹配的序列
        while (matcher.find()) {
            System.out.print("Start index: " + matcher.start());
            System.out.print(" End index: " + matcher.end() + " ");
            System.out.println(matcher.group());
        }
        // 创建第两个模式,将空格替换为 tab
        Pattern replace = Pattern.compile("\\s+");
        Matcher matcher2 = replace.matcher(text);
        System.out.println(matcher2.replaceAll("\t"));
    }
}

运行结果:

Start index: 0 End index: 5 Hello
Start index: 6 End index: 11 Regex
Hello    Regex!

5. 若干个常用例子

5.1 中文的匹配

[\u4e00-\u9fa5]+ 代表匹配中文字。

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "閑人到人间";
        Pattern pattern = Pattern.compile("[\\u4e00-\\u9fa5]+");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

运行结果:

閑人到人间

5.2 数字范围的匹配

比如,匹配 1990 到 2017。

注意:这里有个新手易范的错误,就是正则 [1990-2017],实际这个正则只匹配 01279 中的任一个字符。

正则表达式匹配数字范围时,首先要确定最大值与最小值,最后写中间值。

正确的匹配方式:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "1990\n2010\n2017";
        // 这里应用了 (?m) 的多行匹配模式,只为方便我们测试输出
        // "^1990$|^199[1-9]$|^20[0-1][0-6]$|^2017$" 为判断 1990-2017 正确的正则表达式
        Pattern pattern = Pattern.compile("(?m)^1990$|^199[1-9]$|^20[0-1][0-6]$|^2017$");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

运行结果:

1990
2010
2017

5.3 img 标签的匹配

比如,获取图片文件内容,这里我们考虑了一些不规范的 img 标签写法:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "<img  data-original='aaa.jpg' /><img data-original=bbb.png/><img data-original=\"ccc.png\"/>" +
                "<img data-original='ddd.exe'/><img data-original='eee.jpn'/>";
        // 这里我们考虑了一些不规范的 img 标签写法,比如:空格、引号
        Pattern pattern = Pattern.compile("<img\\s+data-original=(?:['\"])?(?<src>\\w+.(jpg|png))(?:['\"])?\\s*/>");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("src"));
        }
    }
}

运行结果:

aaa.jpg
bbb.png
ccc.png

5.4 贪婪与非贪婪模式的匹配

比如,获取 div 标签中的文本内容:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "<div>文章标题</div><div>发布时间</div>";
        // 贪婪模式
        Pattern pattern = Pattern.compile("<div>(?<title>.+)</div>");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("title"));
        }

        System.out.println("--------------");

        // 非贪婪模式
        pattern = Pattern.compile("<div>(?<title>.+?)</div>");
        matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("title"));
        }
    }
}

运行结果:

文章标题</div><div>发布时间
--------------
文章标题
发布时间

6. 推荐两个在线正则工具

7. 参考

Java Regex - Tutorial

查看原文

brianway 收藏了文章 · 2019-01-16

Redux入门教程(快速上手)

满满的干货,耐心看完,你会发现redux原来这么可爱。

典型的Web应用程序通常由共享数据的多个UI组件组成。通常,多个组件的任务是负责展示同一对象的不同属性。这个对象表示可随时更改的状态。在多个组件之间保持状态的一致性会是一场噩梦,特别是如果有多个通道用于更新同一个对象。

举个小栗子,一个带有购物车的网站。在顶部,我们用一个UI组件显示购物车中的商品数量。我们还可以用另一个UI组件,显示购物车中商
品的总价。如果用户点击添加到购物车按钮,则这两个组件应立即更新当前的数据。如果用户从购物车中删除商品、更改数目、使用优惠券或者更改送货地点,则相关的UI组件都应该更新出正确的信息。
可以看到,随着功能范围的扩大,一个简单的购物车将会很难保持数据同步。

在这篇文章中,我将介绍Redux框架,它可以帮助你以简单易用的方式构建复杂项目并进行维护。为了使学习更容易,我们将使用一个简化的购物车项目来学习Redux的工作原理。你需要至少熟悉React库,因为你以后需要将其与Redux集成。

学习前提

在我们开始以前,确保你熟悉以下知识:

同时,确保你的设备已经安装:

什么是Redux

Redux是一个流行的JavaScript框架,为应用程序提供一个可预测的状态容器。Redux基于简化版本的Flux框架,Flux是Facebook开发的一个框架。在标准的MVC框架中,数据可以在UI组件和存储之间双向流动,而Redux严格限制了数据只能在一个方向上流动。 见下图:

图片描述

在Redux中,所有的数据(比如state)被保存在一个被称为store的容器中 → 在一个应用程序中只能有一个。store本质上是一个状态树,保存了所有对象的状态。任何UI组件都可以直接从store访问特定对象的状态。要通过本地或远程组件更改状态,需要分发一个action分发在这里意味着将可执行信息发送到store。当一个store接收到一个action,它将把这个action代理给相关的reducerreducer是一个纯函数,它可以查看之前的状态,执行一个action并且返回一个新的状态。

理解不变性(Immutability)

在我们开始实践之前,需要先了解JavaScript中的不变性意味着什么。在编码中,我们编写的代码一直在改变变量的值。这是可变性。但是可变性常常会导致意外的错误。如果代码只处理原始数据类型(numbers, strings, booleans),那么你不用担心。但是,如果在处理Arrays和Objects时,则需要小心执行可变操作。
接下来演示不变性

  • 打开终端并启动node(输入node)。
  • 创建一个数组,并将其赋值给另一个变量。
> let a = [1, 2, 3]
> let b = a
> b.push(8)
> b
[1, 2, 3, 8]
> a
[1, 2, 3, 8]

可以看到,更新数组b也会同时改变数组a。这是因为对象和数组是引用数据类型 → 这意味着这样的数据类型实际上并不保存值,而是存储指向存储单元的指针。
将a赋值给b,其实我们只是创建了第二个指向同一存储单元的指针。要解决这个问题,我们需要将引用的值复制到一个新的存储单元。在Javascript中,有三种不同的实现方式:

  1. 使用Immutable.js创建不可变的数据结构。
  2. 使用JavaScript库(如UnderscoreLodash)来执行不可变的操作。
  3. 使用ES6方法执行不可变操作。

本文将使用ES6方法,因为它已经在NodeJS环境中可用了,在终端中,执行以下操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = Object.assign([],a)
[ 1, 2, 3 ]
> b.push(8)
> b
[ 1, 2, 3, 8 ] // b output
> a
[ 1, 2, 3 ] // a output

在上面的代码中,修改数组b将不会影响数组a。我们使用Object.assign()创建了一个新的副本,由数组b指向。我们也可以使用操作符(...)执行不可变操作:

> a = [1,2,3]
[ 1, 2, 3 ]
> b = [...a, 4, 5, 6]
[ 1, 2, 3, 4, 5, 6 ]
> a
[ 1, 2, 3 ]

我不会深入这个主题,但是这里还有一些额外的ES6功能,我们可以用它们执行不可变操作:

配置Redux

配置Redux开发环境的最快方法是使用create-react-app工具。在开始之前,确保已经安装并更新了nodejsnpmyarn。我们生成一个redux-shopping-cart项目并安装Redux

create-react-app redux-shopping-cart

cd redux-shopping-cart
yarn add redux # 或者npm install redux

首先,删除src文件夹中除index.js以外的所有文件。打开index.js,删除所有代码,键入以下内容:

import { createStore } from "redux";

const reducer = function(state, action) {
  return state;
}

const store = createStore(reducer);

让我解释一下上面的代码:

  1. 首先,我们从redux包中引入createStore()方法。
  2. 我们创建了一个名为reducer的方法。第一个参数state是当前保存在store中的数据,第二个参数action是一个容器,用于:

    • type - 一个简单的字符串常量,例如ADD, UPDATE, DELETE等。
    • payload - 用于更新状态的数据。
  3. 我们创建一个Redux存储区,它只能使用reducer作为参数来构造。存储在Redux存储区中的数据可以被直接访问,但只能通过提供的reducer进行更新。

注意到,我在第二点中所提到state。目前,state为undefined或null。要解决这个问题,需要分配一个默认的值给state,使其成为一个空数组:

const reducer = function(state=[], action) {
  return state;
}

让我们更进一步。目前我们创建的reducer是通用的。它的名字没有描述它的用途。那么我们如何使用多个reducer呢?我们将用到Redux包中提供的combineReducers函数。修改代码如下:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const cartReducer = function(state=[], action) {
  return state;
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

在上面的代码中,我们将通用的reducer修改为productReducercartReducer。创建这两个空的reducer是为了展示如何在一个store中使用combineReducers函数组合多个reducer。

接下来,我们将为reducer定义一些测试数据。修改代码如下:

// src/index.js

…

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const cartReducer = function(state=initialState, action) {
  return state;
}

…

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

我们使用store.getState()在控制台中打印出当前的状态。你可以在终端中执行npm start或者yarn start来运行dev服务器。并在控制台中查看state

图片描述

现在,我们的cartReducer什么也没做,但它应该在Redux的存储区中管理购物车商品的状态。我们需要定义添加、更新和删除商品的操作(action)。我们首先定义ADD_TO_CART的逻辑:

// src/index.js

…

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

…

我们继续来分析一下代码。一个reducer需要处理不同的action类型,因此我们需要一个SWITCH语句。当一个ADD_TO_CART类型的action在应用程序中分发时,switch中的代码将处理它。
正如你所看到的,我们将action.payload中的数据与现有的state合并以创建一个新的state。

接下来,我们将定义一个action,作为store.dispatch()的一个参数。action是一个Javascript对象,有一个必须的type和可选的payload。我们在cartReducer函数后定义一个:

…
function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
…

在这里,我们定义了一个函数,返回一个JavaScript对象。在我们分发消息之前,我们添加一些代码,让我们能够监听store事件的更改。

…
let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

unsubscribe();

接下来,我们通过分发消息到store来向购物车中添加商品。将下面的代码添加在unsubscribe()之前:

…
store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

下面是整个index.js文件:

// src/index.js

import { createStore } from "redux";
import { combineReducers } from 'redux';

const productsReducer = function(state=[], action) {
  return state;
}

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

const ADD_TO_CART = 'ADD_TO_CART';

const cartReducer = function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}

function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

let store = createStore(rootReducer);

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

保存代码后,Chrome会自动刷新。可以在控制台中确认新的商品已经添加了。

图片描述

组织Redux代码

index.js中的代码逐渐变得冗杂。我把所有的代码都写在index.js中是为了起步时的简单易懂。接下来,我们来看一下如何组织Redux项目。首先,在src文件夹中创建一下文件和文件夹:

src/
├── actions
│ └── cart-actions.js
├── index.js
├── reducers
│ ├── cart-reducer.js
│ ├── index.js
│ └── products-reducer.js
└── store.js

然后,我们把index.js中的代码进行整理:

// src/actions/cart-actions.js

export const ADD_TO_CART = 'ADD_TO_CART';

export function addToCart(product, quantity, unitCost) {
  return {
    type: ADD_TO_CART,
    payload: { product, quantity, unitCost }
  }
}
// src/reducers/products-reducer.js

export default function(state=[], action) {
  return state;
}
// src/reducers/cart-reducer.js

import  { ADD_TO_CART }  from '../actions/cart-actions';

const initialState = {
  cart: [
    {
      product: 'bread 700g',
      quantity: 2,
      unitCost: 90
    },
    {
      product: 'milk 500ml',
      quantity: 1,
      unitCost: 47
    }
  ]
}

export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    default:
      return state;
  }
}
// src/reducers/index.js

import { combineReducers } from 'redux';
import productsReducer from './products-reducer';
import cartReducer from './cart-reducer';

const allReducers = {
  products: productsReducer,
  shoppingCart: cartReducer
}

const rootReducer = combineReducers(allReducers);

export default rootReducer;
// src/store.js

import { createStore } from "redux";
import rootReducer from './reducers';

let store = createStore(rootReducer);

export default store;
// src/index.js

import store from './store.js';
import { addToCart }  from './actions/cart-actions';

console.log("initial state: ", store.getState());

let unsubscribe = store.subscribe(() =>
  console.log(store.getState())
);

store.dispatch(addToCart('Coffee 500gm', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));

unsubscribe();

整理完代码之后,程序依然会正常运行。现在我们来添加修改和删除购物车中商品的逻辑。修改cart-actions.jscart-reducer.js文件:

// src/reducers/cart-actions.js
…
export const UPDATE_CART = 'UPDATE_CART';
export const DELETE_FROM_CART = 'DELETE_FROM_CART';
…
export function updateCart(product, quantity, unitCost) {
  return {
    type: UPDATE_CART,
    payload: {
      product,
      quantity,
      unitCost
    }
  }
}

export function deleteFromCart(product) {
  return {
    type: DELETE_FROM_CART,
    payload: {
      product
    }
  }
}
// src/reducers/cart-reducer.js
…
export default function(state=initialState, action) {
  switch (action.type) {
    case ADD_TO_CART: {
      return {
        ...state,
        cart: [...state.cart, action.payload]
      }
    }

    case UPDATE_CART: {
      return {
        ...state,
        cart: state.cart.map(item => item.product === action.payload.product ? action.payload : item)
      }
    }

    case DELETE_FROM_CART: {
      return {
        ...state,
        cart: state.cart.filter(item => item.product !== action.payload.product)
      }
    }

    default:
      return state;
  }
}

最后,我们在index.js中分发这两个action

// src/index.js
…
// Update Cart
store.dispatch(updateCart('Flour 1kg', 5, 110));

// Delete from Cart
store.dispatch(deleteFromCart('Coffee 500gm'));
…

保存完代码之后,可以在浏览器的控制台中检查修改和删除的结果。

使用Redux工具调试

如果我们的代码出错了,应该如何调试呢?

Redux拥有很多第三方的调试工具,可用于分析代码和修复bug。最受欢迎的是time-travelling tool,即redux-devtools-extension。设置它只需要三个步骤。

  • 首先,在Chrome中安装Redux Devtools扩展。
  • 然后,在运行Redux应用程序的终端里使用Ctrl+C停止服务器。并用npm或yarn安装redux-devtools-extension包。
yarn add redux-devtools-extension
  • 一旦安装完成,我们对store.js稍作修改:
// src/store.js
import { createStore } from "redux";
import { composeWithDevTools } from 'redux-devtools-extension';
import rootReducer from './reducers';

const store = createStore(rootReducer, composeWithDevTools());

export default store;

我们还可以把src/index.js中日志相关的代码删除掉。返回Chrome,右键单击该工具的图标,打开Redux DevTools面板:

图片描述

图片描述

可以看到,Redux Devtools很强大。你可以在action, statediff(方法差异)之间切换。选择左侧面板上的不同action,观察状态树的变化。你还可以通过进度条来播放actions序列。甚至可以通过工具直接分发操作信息。具体的请查看文档

集成React

在本文开头,我提到Redux可以很方便的与React集成。只需要简单的几步。

  • 首先,停止服务器,并安装react-redux包:
yarn add react-redux
  • 接下来,在index.js中加入React代码。我们还将使用Provider类将React应用程序包装在Redux容器中:
// src/index.js
…
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';

const App = <h1>Redux Shopping Cart</h1>;

ReactDOM.render(
  <Provider store={store}>
    { App }
  </Provider> ,
  document.getElementById('root')
);
…

目前,已经完成了集成的第一部分。可以启动服务器以查看效果。第二部分涉及到使用刚刚安装的react-redux包中的几个方法。通过这些方法将React组件与Redux的storeaction相关联。此外,还可以使用ExpressFeathers这样的框架来设置API。API将为我们的应用程序提供对数据库服务的访问。

感谢网友整理了本文的相关代码,如需要,请移步这里

在Redux中,我们还可以安装其他一些包,比如axios等。我们React组件的state将由Redux处理,确保所有组件与数据库API的同步。想要更进一步的学习,请看Build a CRUD App Using React, Redux and FeathersJS

总结

我希望本文能对你有所帮助。当然,还有很多相关的内容需要学习。例如,处理异步操作、身份验证、日志记录等。如果觉得Redux适合你,可以看看以下几篇文章:

这篇文章是看到比较简明的Redux教程。当然也是翻译过来哒,文中提到了很多延伸文章,我还在一个个学习当中,遇到不错的依然会翻译给大家的。

?喜欢的话记得收藏哦!

查看原文

brianway 赞了文章 · 2018-12-28

antd组件使用进阶及踩过的坑

更多我对Antd的使用及思考,请参考:antd-doddle

扯点犊子

一晃眼,两个月过去了,自己从一家不大不小的屌丝公司跳到一家被具有纯正互联网血液的公司。从以前的围绕jQuery、Echarts为主技术栈开展工作,到现在以React、Antd为主技术栈开发业务;但不是所有的业务antd都能支持,所以有时得自己动手,在antd上做一层浅封装。
文章中提到的示例都可以在codeBox找到:codeBox

自定义表单组件

Antd的Form表单介绍一节中,提到过自定义表单控件。其实例是关于货币价值转换的,如下图所示:
image
当我们在我们的页面中需要频繁的用到某一个组合类型的组件,而Antd又不支持时,最好的做法就是对Antd组件做一层浅封装行成一个独立的组件,当然也可以使用html 自有的表单元素进行封装,只是这样做出来费事,且样式和整个页面没有那么容易统一。封装的注意事项在上面的截图中已经一一列出:核心就是value的处理,与onChange事件的支撑,接下来将以一个实例来操作说明。

一个带远程搜索的下拉选择组件

2018.12月更新:随着Select组件comobox模式在新的版本中被舍弃,和autoComplete组件的出现。这个组件也进行了重写。但整体逻辑没有改变,主要时改变了激活弹出框和关闭弹出框的逻辑。具体可参见我的github项目:React进阶
seri
这个组件的大致实现需求如上面动态图所示。产品需求就是需要一个编辑框,这个框在用户点击输入时,需要弹出一个搜索框,根据用户的输入远程搜索获取数据形成一个下拉列表,供用户选择。这在jquery时代,这是一个很常见的需求,也有很多的组件可选择,但在Antd的组件库中,没有完全匹配的,但有及其相似功能的,比如:

image

这个组件与产品的需求契合度已经达到了80%, 但是产品说了搜索输入框需要与编辑输入框分开,并且有明显的区别,ok,那就费点事,把Antd组件稍微做一下改变嘛。
image
所以简单分解一下,需要用到Input,Icon, Select这三种组件,具体实现可以查看SandBox上的源码及示例。说一下自己遇到的难点:

支持双向绑定

<FormItem type="inline" label="员工姓名">
  {getFieldDecorator('search',{
    initialValue: { name: 'Dom' }
  })(
    <OriginSearch {...modalProps} />
  )}
</FormItem>

在Antd的Form表单组件中,如果需要做数据双向绑定,就需要用到其提供的getFieldDecorator方法来装饰组件,而我们自己封装的组件要支持这个特性的话,如最开始提到的,我们需要使用onChange方法来触发装饰器值的同步。

在我们为一个组件添加了装饰器后,可以查看props明显发现多了id,onChange, value三个属性,value属性是用于获取我们在initialValue设定的值的,而onChange方法是用于同步值,实现双向绑定。所以在我们这个组件中,当用户从下拉框中选择一个选项后,我们需要调用onChange方法去同步值,代码如下所示:

handleSelect(value, option) {
  const { handleSelect } = this.props;
  const { seachRes } = this.state;
  // 初始化基础信息
  const selectValue = handleSelect(value, option, seachRes) || value;
  this.triggerChange(selectValue);
  this.setState({ value: selectValue });
  this.handleCloseSearch();
}
triggerChange(value) {
  const { onChange } = this.props;
  // 调用装饰器方法去同步选中后的值
  onChange && onChange(value);
}

点击组件以外的地方收起组件

这看似是个很容易实现的需求,但因为Antd所有的弹框组件都用了同一套方法,其弹框Dom树并不是挂载在Select输入框的父节点上,而是直接挂载在Body节点上,所以想用冒泡的机制来实现就不可能了。所以就和投机用了点击事件的节点名称来判断,看具体实现:

componentDidUpdate(prevProps, prevState) {
  const { isShowSearch } = this.state;
  const bodyNode = document.querySelector("body");
  if (isShowSearch !== prevState.isShowSearch) {
    // 状态切换的时候才改变状态
    if (isShowSearch) {
      document
        .querySelector(".js-origin-search .ant-select-search__field")
        .focus();
      bodyNode.addEventListener("click", this.handleChangeVisible);
    } else {
      bodyNode.removeEventListener("click", this.handleChangeVisible);
    }
  }
}
handleChangeVisible(event) {
  const { isShowSearch } = this.state;
  event = event || window.event;
  const elem = event.target;
  let inComponentClick = false;
  // 当搜索框框被打开时,点击空白处搜索框收起;由于antd的下拉列表是挂载在body下,而非搜索框节点下的某一子节点,所以
  // 无法采用阻止冒泡的方式来避免body下的click事件被响应,所以只有靠判断被点击的节点类,来判断body的click事件是否响应
  if (
    (this.searchInputElement && this.searchInputElement.contains(elem)) ||
    elem.className.indexOf("ant-select-dropdown") !== -1
  ) {
    inComponentClick = true;
  }
  // 当点击事件为非下拉列表选中事件,切搜索框为展开时,触发搜索框收起方法;
  !inComponentClick && isShowSearch && this.handleCloseSearch();
}

虽然这只是一次很简单的封装,但其包含的知识点还是非常多的。自己还封装过日期多选,日期选择增加至今,地址地区联合选择器这种,从实现上其实都是一个思路,在这一个SandBox项目中都能看到。

组件奇特使用方式(持续更新)

动态更新表单组件Required参数,但验证没有同步

加入现在有这样一个需求,用户需要选择自己的性别(男,女,其他),当用户选择其他时,下面说明项由非必填变为必填。这看似是一个很简单的需求,在用户选择其他时,将isRequired变量变为true就行了,看起来好像,大概,貌似成功了。但是自己在antd组件的应用上,发现,当你把isRequired置为true,label标签前面会加上一个*,但这只是一个假象,当你填上数据再删除时,antd组件这时并不会自动验证,并触发提示词显示。但是,这些都没有。所谓的isRequired置为true,并没有达到真正想要的效果,测试代码如下,我猜测antd的这个机制和他的initValue更新机制相似,只有在组件初始化的时候会设置一次初始值,后面都是组件内部state参数进行状态切换,原以为用resetFields可以解决,最后发现不能。但是巧的方法没有,不代表笨办法也没有。

<FormItem
  label="性别"
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
>
  {getFieldDecorator('gender', {
    rules: [{ required: true, message: 'Please select your gender!' }],
  })(
    <Select
      placeholder="Select a option and change input text above"
      onChange={this.handleSelectChange}
    >
      <Option value="male">male</Option>
      <Option value="female">female</Option>
    </Select>
  )}
</FormItem>
<FormItem
  label="说明"
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
>
  {getFieldDecorator('note', {
    rules: [{ required: isRequired, message: 'Please input your note!' }],
  })(
    <Input />
  )}
</FormItem>

最后想出的最好的解决办法就是动态销毁重新挂载这个组件,我们可以通过动态设定key值,来保证状态的同步。其实这是重新渲染了一个新的组件来替换。

<FormItem
  label="说明"
  key={isRequired}
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
> 

2019.01.06日更:
上面的问题,在自己使用3.9的版本上没有再出现了,但是好像又出现了一个更大的表单动态校验问题。看下图,需求和上面差不多:
2303313742-5c317658312cd_articlex
当选择启用时,原因必填。其他时,变为非必填。现在出现的情况时,必填时触发错误提醒,但将状态置为禁用时,label的必填属性虽然被重置了,但错误提醒仍然存在,有些奇怪。
基于上面的现象,我去基友社区搜了一下Issuse,果然时存在的:form动态校验问题,幸运的时,antd大佬也给出了解决方案:使用form.validateFields([key], { force: true })来解决

实践后的幡然醒悟

用了两个多月,其实Antd自己本身没啥坑,只是由于我们组现在使用的版本是2.9,但自己习惯于看3.x的版本文档,所以多次在一个地方徘徊好久,总以为是自己代码实现有问题,其实是2.9版本还没有实现。

总结一

在Select组件上,2.9与3.x就有较大的差异:

  1. Select 的option必须带有不同的Key值,且value值也不能有相同的,比如在远程搜索加载员工列表时,就会出现同名的情况,所以这时的value就不能只用名字,得用value加工号或则其他值来代替。
  2. Select 的 onchange事件在3.x版本以前回调函数只有value值,没有option回调参数。
  3. Select 的notFundContent属性可配置结合Spin实现加载动画,但在版本3以下,该配置对于comobox模式无效(其文档未对这个特性(Bug)做说明)。。。
  4. Select 的 onSelect事件在3.x以后也有较大改动,其option参数包含的内容作了很大调整,在2.9版本还可以通过option.props.index获取选择的索引,在3.x版本只能间接通过设置key为index,然后通过获取key值来获取index;
  5. Select 组件渲染出来的下拉列表是没有挂载在Select组件父节点上的,其是采用绝对定位,挂载在body节点上的。。。所有用父节点做筛选是无法获取的。

总结二

另外在表单组件自校验validator的使用上,有一个隐藏的少有人知的使用方法是:

<FormItem {...formItemLayout} label="确认密码">
    {
        getFieldDecorator('confirmPassword', {
            rules: [{
                    required: true,
                    message: '请再次输入以确认新密码',
                }, {
                    validator: this.handleConfirmPassword
                }],
        })(<Input type="password" />)
    }
</FormItem> 
handleConfirmPassword(rule, value, callback) {
    if (value && value.length < 5 ) {
        callback('长度不足');
        return;
    }
    // Note: 必须总是返回一个 callback,否则 validateFieldsAndScroll 无法响应
    callback()
}

总结三

当我们使用getFieldDecorator并用initialValue设定初始值时,当我们改变组件的值时,组件表现出的值也改变了,但这个值并不是initialValue设定的,其是组件内部的state值保持的,如果需要继续用initialValue来设定组件的值,我们需要调用resetFields方法使initialValue有效;

总结四:Table设置width无效

Antd组件个人觉得最好用的功能就是Table,其配合pagination可以直接实现前端分页,在有些使用场景可以大大提高使用体验。但是Table也有坑(其实也是css一个隐形知识点),就是有时你会发现你为一列设置了width,但是并没有鸟用。

{
  key: 'userId',
  name: '用户ID',
  value: 'asddsddsfsfsdfsdfsdfsfsfdsfsfsfsfsfddefgervwerbvw'
  width: 80
}

就像上面的这种数据,设置了80的宽度,但最后撑开差不多是300。最后的最后,记起了又一个css属性叫 word-break ,来历就是在浏览器中,纯数字或者纯字母的字符串,他的显示默认是不换行的,就算他已经超出了这一行的边际,就是这么叼的一个属性。所以Table也受这个影响,由于我要展示的内容中是纯字母,纵然我设置了width,依然没什么鸟用。需要设置与td相关联的table样式,加上word-break:break-all这样的解药。

总结四:Select(下拉弹框类)组件页面滚动时,下拉内容与弹出父组件分离

image
如上图所示,外层页面一滚动,下拉框与下拉内容就分离了,分离,离,离了。这个出现原就是因为ANTD所有的弹框都是默认挂载在body下面,然后绝对定位的。所以滚动body内容, 就会造成与弹出触发的那个组件错位。幸好在3.0以后ANTD对这个bug做出了一个解决方案,就是增加了getPopupContainer属性,可以让下拉内容挂载任何存在的dom节点上,并相对其定位。具体用例请查看官方示例

Hooks自定义组件报cannot be given refs

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()

随着React hooks不断的应用,在antd中,用hooks自定义的组件,会报如上所示的refs无法获取,这是antd form双向绑定对ref有需要。因为ref和key一样,不是通过prop来传递的,react对其有特殊的处理。好在这个可以通过forwardRef来解决,官方文档,看一个示例:

export default function FundSelect(props) {
    return (
        <YourComponent {...props}>
    )
}

使用forwardRef转发Ref

function FundSelect(props, ref) {
    return (
        <YourComponent ref={ref} {...props}>
    )
}

export default forwardRef(FundSelect);

getFieldDecorator包裹下的Switch组件无法显示为true状态

<FormItem {...formItemLayout} label="是否显示">
     {getFieldDecorator('enable', {
      initialValue: true,})
(<Switch />)}
 </FormItem>

发现在getFieldDecorator包裹下的Switch组件无法显示为true的状态,3.23版本报以下提示信息:

warning.js:6 Warning: [antd: Switch] value is not validate prop, do you mean checked?

查找文档得知Switch组件是通过checked的属性来显示状态的,所以需要一个额外的属性valuePropName

<FormItem {...formItemLayout} label="是否显示文档">
            {getFieldDecorator('showDocument', {
              initialValue: true,
              valuePropName: 'checked'
            })(<Switch />)}
</FormItem>

文章首发于: http://closertb.site

(https://github.com/closertb/c...

查看原文

赞 40 收藏 29 评论 4

brianway 赞了回答 · 2018-09-10

解决webpack 打包,babel 编译 es6 语法,如何兼容 ie8 浏览器呢?

vue只兼容ie9以上的浏览器,并且需要引入babel-polyfill才能支持到ie9


vue以及vuex可能需要Promise等低版本ie浏览器没有提供的类,所以需要加入babel-polyfill

1.先安装babel-polyfill

npm install babel-polyfill --save

2.在入口文件(main.js)最上方将其import进来

import 'babel-polyfill'
// 下面是别的代码,不需要动
import Vue from 'vue'
import App from './App'

关注 3 回答 3

brianway 收藏了文章 · 2018-03-05

Java 正则表达式详解

版权声明:本文由吴仙杰创作整理,转载请注明出处:https://segmentfault.com/a/1190000009162306

1. 正则表达式

1.1 什么是正则表达式

正则表达式
: 定义一个搜索模式的字符串。

正则表达式可以用于搜索、编辑和操作文本。

正则对文本的分析或修改过程为:首先正则表达式应用的是文本字符串(text/string),它会以定义的模式从左到右匹配文本,每个源字符只匹配一次。

1.2 示例

正则表达式匹配
this is text精确匹配字符串 "this is text"
this\s+is\s+text匹配单词 "this" 后跟一个或多个空格字符,后跟词 "is" 后跟一个或多个空格字符,后跟词 "text"
^\d+(\.\d+)?^ 定义模式必须匹配字符串的开始,d+ 匹配一个或多个数字,? 表明小括号内的语句是可选的,\. 匹配 ".",小括号表示分组。例如匹配:"5"、"1.5" 和 "2.21"

2. 正则表达式的编写规则

2.1 常见匹配符号

正则表达式描述
.匹配所有单个字符,除了换行符(Linux 中换行是 \n,Windows 中换行是 \r\n
^regex正则必须匹配字符串开头
regex$正则必须匹配字符串结尾
[abc]复选集定义,匹配字母 a 或 b 或 c
[abc][vz]复选集定义,匹配字母 a 或 b 或 c,后面跟着 v 或 z
[^abc]当插入符 ^ 在中括号中以第一个字符开始显示,则表示否定模式。此模式匹配所有字符,除了 a 或 b 或 c
[a-d1-7]范围匹配,匹配字母 a 到 d 和数字从 1 到 7 之间,但不匹配 d1
XZ匹配 X 后直接跟着 Z
X|Z匹配 X 或 Z

2.2 元字符

元字符是一个预定义的字符。

正则表达式描述
\d匹配一个数字,是 [0-9] 的简写
\D匹配一个非数字,是 [^0-9] 的简写
\s匹配一个空格,是 [ \t\n\x0b\r\f] 的简写
\S匹配一个非空格
\w匹配一个单词字符(大小写字母、数字、下划线),是 [a-zA-Z_0-9] 的简写
\W匹配一个非单词字符(除了大小写字母、数字、下划线之外的字符),等同于 [^\w]

2.3 限定符

限定符定义了一个元素可以发生的频率。

正则表达式描述举例
*匹配 >=0 个,是 {0,} 的简写X* 表示匹配零个或多个字母 X,.* 表示匹配任何字符串
+匹配 >=1 个,是 {1,} 的简写X+ 表示匹配一个或多个字母 X
?匹配 1 个或 0 个,是 {0,1} 的简写X? 表示匹配 0 个或 1 个字母 X
{X}只匹配 X 个字符\d{3} 表示匹配 3 个数字,.{10} 表示匹配任何长度是 10 的字符串
{X,Y}匹配 >=X 且 <=Y 个\d{1,4} 表示匹配至少 1 个最多 4 个数字
*?如果 ? 是限定符 *+?{} 后面的第一个字符,那么表示非贪婪模式(尽可能少的匹配字符),而不是默认的贪婪模式

2.4 分组和反向引用

小括号 () 可以达到对正则表达式进行分组的效果。

模式分组后会在正则表达式中创建反向引用。反向引用会保存匹配模式分组的字符串片断,这使得我们可以获取并使用这个字符串片断。

在以正则表达式替换字符串的语法中,是通过 $ 来引用分组的反向引用,$0 是匹配完整模式的字符串(注意在 JavaScript 中是用 $& 表示);$1 是第一个分组的反向引用;$2 是第二个分组的反向引用,以此类推。

示例:

package com.wuxianjiezh.demo.regex;

public class RegexTest {

    public static void main(String[] args) {
        // 去除单词与 , 和 . 之间的空格
        String Str = "Hello , World .";
        String pattern = "(\\w)(\\s+)([.,])";
        // $0 匹配 `(\w)(\s+)([.,])` 结果为 `o空格,` 和 `d空格.`
        // $1 匹配 `(\w)` 结果为 `o` 和 `d`
        // $2 匹配 `(\s+)` 结果为 `空格` 和 `空格`
        // $3 匹配 `([.,])` 结果为 `,` 和 `.`
        System.out.println(Str.replaceAll(pattern, "$1$3")); // Hello, World.
    }
}

上面的例子中,我们使用了 [.] 来匹配普通字符 . 而不需要使用 [\\.]。因为正则对于 [] 中的 .,会自动处理为 [\.],即普通字符 . 进行匹配。

2.4.1 仅分组但无反向引用

当我们在小括号 () 内的模式开头加入 ?:,那么表示这个模式仅分组,但不创建反向引用。

示例:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "img.jpg";
        // 分组且创建反向引用
        Pattern pattern = Pattern.compile("(jpg|png)");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
        }
    }
}

运行结果:

jpg
jpg

若源码改为:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "img.jpg";
        // 分组但不创建反向引用
        Pattern pattern = Pattern.compile("(?:jpg|png)");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
        }
    }
}

运行结果:

jpg
Exception in thread "main" java.lang.IndexOutOfBoundsException: No group 1
    at java.util.regex.Matcher.group(Matcher.java:538)
    at com.wuxianjiezh.regex.RegexTest.main(RegexTest.java:15)

2.4.2 分组的反向引用副本

Java 中可以在小括号中使用 ?<name> 将小括号中匹配的内容保存为一个名字为 name 的副本。

示例:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "@wxj 你好啊";
        Pattern pattern = Pattern.compile("@(?<first>\\w+\\s)"); // 保存一个副本
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
            System.out.println(matcher.group(1));
            System.out.println(matcher.group("first"));
        }
    }
}

运行结果:

@wxj 
wxj 
wxj 

2.5 否定先行断言(Negative lookahead)

我们可以创建否定先行断言模式的匹配,即某个字符串后面不包含另一个字符串的匹配模式。

否定先行断言模式通过 (?!pattern) 定义。比如,我们匹配后面不是跟着 "b" 的 "a":

a(?!b)

2.6 指定正则表达式的模式

可以在正则的开头指定模式修饰符。

  • (?i) 使正则忽略大小写。
  • (?s) 表示单行模式("single line mode")使正则的 . 匹配所有字符,包括换行符。
  • (?m) 表示多行模式("multi-line mode"),使正则的 ^$ 匹配字符串中每行的开始和结束。

2.7 Java 中的反斜杠

反斜杠 \ 在 Java 中表示转义字符,这意味着 \ 在 Java 拥有预定义的含义。

这里例举两个特别重要的用法:

  • 在匹配 .{[(?$^* 这些特殊字符时,需要在前面加上 \\,比如匹配 . 时,Java 中要写为 \\.,但对于正则表达式来说就是 \.
  • 在匹配 \ 时,Java 中要写为 \\\\,但对于正则表达式来说就是 \\

注意:Java 中的正则表达式字符串有两层含义,首先 Java 字符串转义出符合正则表达式语法的字符串,然后再由转义后的正则表达式进行模式匹配。

2.8 易错点示例

  • [jpg|png] 代表匹配 jpgpng 中的任意一个字符。
  • (jpg|png) 代表匹配 jpgpng

3. 在字符串中使用正则表达式

3.1 内置的字符串正则处理方法

在 Java 中有四个内置的运行正则表达式的方法,分别是 matches()split())replaceFirst()replaceAll()。注意 replace() 方法不支持正则表达式。

方法描述
s.matches("regex")当仅且当正则匹配整个字符串时返回 true
s.split("regex")按匹配的正则表达式切片字符串
s.replaceFirst("regex", "replacement")替换首次匹配的字符串片段
s.replaceAll("regex", "replacement")替换所有匹配的字符

3.2 示例

示例代码:

package com.wuxianjiezh.regex;

public class RegexTest {

    public static void main(String[] args) {
        System.out.println("wxj".matches("wxj"));
        System.out.println("----------");

        String[] array = "w x j".split("\\s");
        for (String item : array) {
            System.out.println(item);
        }
        System.out.println("----------");

        System.out.println("w x j".replaceFirst("\\s", "-"));
        System.out.println("----------");

        System.out.println("w x j".replaceAll("\\s", "-"));
    }
}

运行结果:

true
----------
w
x
j
----------
w-x j
----------
w-x-j

4. 模式和匹配

Java 中使用正则表达式需要用到两个类,分别为 java.util.regex.Patternjava.util.regex.Matcher

第一步,通过正则表达式创建模式对象 Pattern

第二步,通过模式对象 Pattern,根据指定字符串创建匹配对象 Matcher

第三步,通过匹配对象 Matcher,根据正则表达式操作字符串。

来个例子,加深理解:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String text = "Hello Regex!";

        Pattern pattern = Pattern.compile("\\w+");
        // Java 中忽略大小写,有两种写法:
        // Pattern pattern = Pattern.compile("\\w+", Pattern.CASE_INSENSITIVE);
        // Pattern pattern = Pattern.compile("(?i)\\w+"); // 推荐写法
        Matcher matcher = pattern.matcher(text);
        // 遍例所有匹配的序列
        while (matcher.find()) {
            System.out.print("Start index: " + matcher.start());
            System.out.print(" End index: " + matcher.end() + " ");
            System.out.println(matcher.group());
        }
        // 创建第两个模式,将空格替换为 tab
        Pattern replace = Pattern.compile("\\s+");
        Matcher matcher2 = replace.matcher(text);
        System.out.println(matcher2.replaceAll("\t"));
    }
}

运行结果:

Start index: 0 End index: 5 Hello
Start index: 6 End index: 11 Regex
Hello    Regex!

5. 若干个常用例子

5.1 中文的匹配

[\u4e00-\u9fa5]+ 代表匹配中文字。

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "閑人到人间";
        Pattern pattern = Pattern.compile("[\\u4e00-\\u9fa5]+");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

运行结果:

閑人到人间

5.2 数字范围的匹配

比如,匹配 1990 到 2017。

注意:这里有个新手易范的错误,就是正则 [1990-2017],实际这个正则只匹配 01279 中的任一个字符。

正则表达式匹配数字范围时,首先要确定最大值与最小值,最后写中间值。

正确的匹配方式:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "1990\n2010\n2017";
        // 这里应用了 (?m) 的多行匹配模式,只为方便我们测试输出
        // "^1990$|^199[1-9]$|^20[0-1][0-6]$|^2017$" 为判断 1990-2017 正确的正则表达式
        Pattern pattern = Pattern.compile("(?m)^1990$|^199[1-9]$|^20[0-1][0-6]$|^2017$");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group());
        }
    }
}

运行结果:

1990
2010
2017

5.3 img 标签的匹配

比如,获取图片文件内容,这里我们考虑了一些不规范的 img 标签写法:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "<img  data-original='aaa.jpg' /><img data-original=bbb.png/><img data-original=\"ccc.png\"/>" +
                "<img data-original='ddd.exe'/><img data-original='eee.jpn'/>";
        // 这里我们考虑了一些不规范的 img 标签写法,比如:空格、引号
        Pattern pattern = Pattern.compile("<img\\s+data-original=(?:['\"])?(?<src>\\w+.(jpg|png))(?:['\"])?\\s*/>");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("src"));
        }
    }
}

运行结果:

aaa.jpg
bbb.png
ccc.png

5.4 贪婪与非贪婪模式的匹配

比如,获取 div 标签中的文本内容:

package com.wuxianjiezh.regex;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexTest {

    public static void main(String[] args) {
        String str = "<div>文章标题</div><div>发布时间</div>";
        // 贪婪模式
        Pattern pattern = Pattern.compile("<div>(?<title>.+)</div>");
        Matcher matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("title"));
        }

        System.out.println("--------------");

        // 非贪婪模式
        pattern = Pattern.compile("<div>(?<title>.+?)</div>");
        matcher = pattern.matcher(str);
        while (matcher.find()) {
            System.out.println(matcher.group("title"));
        }
    }
}

运行结果:

文章标题</div><div>发布时间
--------------
文章标题
发布时间

6. 推荐两个在线正则工具

7. 参考

Java Regex - Tutorial

查看原文

brianway 发布了文章 · 2017-09-29

如何准备校招技术面试

如何准备校招技术面试

标签 : 面试


[TOC]


2017 年互联网校招已近尾声,作为一个非 CS 专业的应届生,零 ACM 经验、零期刊论文发表,我通过自己的努力和准备,从找实习到校招一路运气不错,面试全部通过,谨以此文记录我的校招感悟。

写在前面

写作动机

  • 记录自己的经历、反思与总结
  • 在互联网上看到了很多前人的经验帖,作为回馈,也分享自己的经验
  • 赚一点打赏(可扫描我个人网站里的二维码 http://brianway.github.io/about/)

你可以从这篇文章获取哪些信息

  • 如何看待校招面试
  • 后台研发 为例,校招技术面试的考核范围和难度
  • 如何提升自己的技术内功(长期)
  • 如何有针对性地做面试准备(短期)
  • 面试交谈中的一些技巧
  • 一线互联网公司的校招面试流程

这篇文章不能带给你什么

  • 本文并不是速成宝典/突击手册
  • 本文不会罗列面试原题和题解

我的面试经历

春招找实习,投了三家(阿里,腾讯,美团),全部拿到实习 Offer,去了阿里。

  • 阿里:菜鸟网络;3 面技术电面 + 1 面 HR 视频面。二面布置了一个工程小作业,做了两天
  • 腾讯:投的后台开发(事业群无意向),被转到 SNG 运营开发;简历评级 S,2 面电话面试 + 线路现场面试 2 面技术 + 1 面 HR
  • 美团:美团餐饮平台;2 面电话面试 + HR 直接通知 offer。历时一周多,最效率

秋招为了稳妥地转正,拿一个好的评级,在专心准备转正面试,错过了提前批,正式批投了五家,面了三家(阿里,腾讯,网易),基本都是 special offer:

  • 阿里:菜鸟网络;成功转正,评级不错
  • 腾讯:投的微信 Web 开发,被调到 IEG 后台开发(不知道为什么多了一个阿里实习,我的简历评级反而从 S 变成了 A+);在线笔试 + 现场 2 技术面 + 1 HR面。HR 面时我期望薪资提的 sp 的价格,offer 报批中。
  • 网易:跨境电商(考拉海购);在线笔试 + 现场 2 技术面 + 1 HR 面。HR 说技术面评价不错,问题不大。结合身边同学,我应该是 sp。网易面试体验最好,面试官(尤其一面那位)很专业,很佩服
  • 头条(放弃面试):来校园宣讲,免笔试直接面试,没去
  • 网易游戏(放弃面试):计费应用系统开发;电话通知笔试过了,手里有 3 个 offer 了,就主动告知不面了。

如何看待校招面试

招聘,对公司而言,是寻找劳动力;对员工而言,是寻找未来的同事。所以考核的东西肯定有如下几点:

  • 技术能力:招你进来是干活的,公司不是慈善机构,不养闲人,所以你需要 证明你能胜任这项工作
  • 学习能力/潜力:要能干活的走社招就行了,应届的优势就是潜力,你得 向公司证明值得培养你
  • 软技能:招你进来是和其他人共事的,所以为人处事、沟通能力、是否合群、三观等等, 你的“调性”要和公司/组里的吻合

所以很多人挂的原因就很明显了:技术能力不佳,挂;看不到学习的悟性,只会已有技能,挂;聊天思路不清晰,沟通障碍,挂…………

当然,招聘本来就是一个双向选择的过程,公司在选择你的同时,你也在选择公司,比如工作要拼还是要 balance,这种事见仁见智。

技术面试考核的技术范围

虽然面试考核的东西很多,但技术面试最主要的还是考核技术能力,记得学长告诉过我:“技术基础和项目经历,至少得有一个拿得出手”,所以下面谈谈所谓的“技术基础”。

我投的所有岗位都是 Java 研发,所以就以 Java 研发为例,以文字的形式写出大致的知识图谱/思维导图。很多我就只点到为止,不可能面面俱到。

  • 算法和数据结构基础

    • 数据结构:数组,链表,哈希表,堆,队列,栈,二叉树,B树/B+树,红黑树,图(研发问图不多)
    • 常见的排序算法(冒泡,插入,快排,堆排,归并排序...)
    • 简单的动态规划问题(背包,上楼梯)
    • 各种时间空间复杂度分析
  • Java 基础

    • 最基本的语言基础:语法,关键字含义,面向对象....
    • 集合类(ArrayList, HashMap, ConcurrentHashmap...等等)
    • 多线程(锁,CAS,线程池,concurrent 包下的类)
    • 语言特性:反射,动态代理,泛型,Java 8 新特性
    • IO (装饰器模式,NIO)
    • JVM:内存模型,垃圾回收,类加载机制
  • Java Web

    • Tomcat,Servlet,JSP,Cookie/Session 等基本概念
    • 框架使用和原理:Spring(AOP,IoC),MyBatis 等
  • 数据库(主要是 MySQL)

    • 基本 SQL 语句,索引优化
    • 存储引擎(InnoDB,MyISAM),索引原理
    • 事务 ACID,隔离级别
    • 分库分表,主从复制,读写分离
  • 计算机网络

    • OSI 7 层模型和 TCP 4 层模型
    • 传输层:TCP/IP 相关知识,和 UDP 比较
    • HTTP 协议:报文结构,POST/GET 方法
    • 网络编程 (socket, NIO, select 等)
  • 操作系统

    • 基本 Linux 的操作指令
    • 进程/线程比较,进程间通信方式,P/V 操作
    • 磁盘调度,虚拟内存
    • 死锁,中断

1.这么多东西,从哪学起呢?

我自己的学习路线规划:技能的主线是 Java 语言基础 -> Spring 框架开发业务 -> 分布式系统解决高并发,基础方面 算法,网络协议,操作系统 带着学

2.学到什么程度呢?

  • 概念性的程度:了解是啥,能够口述含义
  • 使用的程度:可以熟练使用,比如调用 API,写 SQL 查询
  • 原理的程度:能够讲清楚底层实现
  • 提出见解的程度:从任意技术可以引申出相关技术,并能够分析联系和区别,提出自己的见解和体会

以上四种程度逐层加深,当然越深越好,通常前两种程度只能称之为“了解”;而写上简历迎接考核的,至少需要掌握到「原理」的程度

比如,Java 你至少得看过常用类的 JDK 源码;数据库只会增删改查肯定不够,起码得把底层索引结构讲清楚;常见算法起码能讲清楚关键步骤,分析时间空间复杂度,并且 bug free 地写出来。

技术储备是一切的基础,如果基础都不会,那后面的简历撰写,临场引导面试官等技巧根本无从下手,只有被虐的份

如何提升自己的技术内功

“冰冻三尺非一日之寒”,功夫在平时,与其背面经寄希望于碰原题,不如扎实学习,以不变应万变。

可以通过 看书+记笔记 的形式来学习,书上的代码可以对着敲一敲,学习笔记和代码可以发到 GitHub 上,用来展示自己的学习热情。

下面是我这一年多来看过的书,每一本我读之前都做了充分的调研和筛选,豆瓣评分一般都在 7~9 分:

  • 《算法》(第四版)图以前章节
  • 《剑指 Offer》
  • 《Java 编程思想》
  • 《Java多线程编程核心技术》(高洪岩 著)
  • 《Java 并发编程实战》
  • 《深入理解 Java 虚拟机》
  • 《Java 8 实战》
  • 《鸟哥的 Linux 私房菜》
  • 《MySQL 必知必会》
  • 《Maven 实战》
  • 《图解 HTTP》
  • 《敏捷软件开发》
  • 《架构探险-从零开始写 Java Web 框架》
  • 《Spring 3.x 企业应用开发实战》
  • 《Head First 设计模式》
  • 《大型网站技术架构》(李智慧 著)
  • 《大型网站系统与 Java 中间件实践》

少看了一本《深入理解计算机系统》,所以被问到系统相关问题就很虚。

附上买书的截图

书1

书2

我整理了一些学习笔记和源码:

如何有针对性地做面试准备

首先 最重要的就是写简历,找工作就像相亲,简历就是颜值,在这个看脸的社会,只有长得过关,别人才愿意深入发掘你。从我面试的感受来说,通过与否,简历可以占到 50% 以上。

建议尽早的开始准备简历,因为写简历时才会发现自己的简历没什么东西可写,才会有一种危机感。如果等到校招才开始写,就晚了。

怎么写一个好的简历网上很多帖子,就不赘述了,这里简单说几点:

  • 亮点:名校本硕,专业排名,大赛获奖,名企实习。(如果一个没有只能自求多福)
  • 技能:这些可能是面试官的提问点,不熟别写
  • 项目:技术栈,难点,贡献/产出
  • 事实:一万句“我学习能力很强”不如一句“我专业排名第一”,一万句“我爱打游戏”不如一句“我王者农药打了 1000 多盘,每天投入 4 小时以上”

项目经历的提炼可以从以下几个方面着手准备,这些问题都可以事先想好,多口述演练,以免临场紧张出错

  • 基本问题

    • 项目简介
    • 项目详解
    • 项目用到的技术
  • 开放问题

    • 遇到的困难及解决
    • 项目的优缺点及改进
    • 收获

没有项目经历可以自己找找开源的项目做,或者找一些自己感兴趣的方向做一些玩具项目,世上无难事,只怕有心人。

面试技巧

面试本质是信息沟通,是你向面试官展示你能胜任这个岗位,而不是一问一答。你应该尽量主导面试,引导面试官,而不是被动答题。即便同一个众人皆知的问题,也不应止步于答出来即可,要争取尽量回答的和别人不一样,展现出自己独到的研究和体会。

所以很多人疑惑:为什么我每一问都答上来了,为啥还挂了?多半就是自我感觉良好,其实答的不咋样,或者就是被面试官牵着鼻子走,给人一种是在背面经的感觉。其实很多问题没有标准答案,面试官看的是你解决问题的思路,思考问题的方式,而不是这个问题答案是什么

我从自己被问过的问题和反思中简单说几点:

  • 1.不止步于问题,多展示自己会的

比如最经典一个问题:输入一个网址到看到页面,经历了哪些过程。如果只是回答 DNS,OSI 的几层协议,那格局未免太小,同时会把面试官引导往网络协议提问,接踵而至的可能就是 TCP/IP(三次握手,TIME_WAIT,滑动窗口),HTTP(keep-alive,HTTPS)等一系列协议细节,就把自己的展示机会限制在很小的范围了。

而这个问题其实可以从网站架构的层面先宏观描述,如 DNS,负载均衡,静态页面/动态页面,数据库访问,缓存,甚至前端的浏览器渲染等等,这样你可以聊得东西就很多,同时证明你有实际的项目经验,知道业界是如何做的,然后再回头把网络层面的东西简单提一提。

再比如被问到有哪些 hash 方法?因为一般都是问哈希冲突解决方法,很多人会说开放地址法,链表法等等。至于 hash 方法,真的有点懵逼,凭下意识说了一个取模(取余数),答到这肯定不够,于是我把 Java 几个基本类型的包装类 Integer, Boolean, String 的 hashcode 方法的实现说了一遍,一方面作为例子填充回答,另一方面,表示我研究过 JDK 源码

  • 2.结合例子,不要背书

譬如,被问到 AOP,IoC 这些概念时,不要官方的解释背一遍,最好结合自己的使用经验,把使用场景和感受到的好处说一下,会比较生动。

  • 3.从更高的层面去总结和阐释问题

之前被问到“倒排索引”的问题时,我只是举了个例子把这个概念讲清楚了,后来觉得并不是太好,只是停留在解释的层面,如果能首先提纲挈领的来一句“一般的索引,是文档到单词的映射;而倒排索引,是单词到文档的映射”,整个回答的高度就不一样了。

再比如“二叉树搜索和哈希查找的区别”,如果能答出搜索二叉树是基于比较的排序,所以时间复杂度为 O(log n),哈希查找是一个函数映射,所以能做到 O(1),说明你不是背面经的。

  • 4.不会的先把丑话说在前面

面试中难免会遇到不会的,这时千万不要不懂装懂,如果觉得有想法、能说一点,可以先坦诚承认自己不会,但愿意尝试回答一下,这样即使说错了,也不会被怼,因为你已经把场子捡开了。

比如当问到“JVM 如何标记要回收的内存”,我其实记不得了,就说不清楚,但标记无非就那么几种方法,所以提了两种:要么在每块内存留一点标识位(比如一个字节)来标识,要么另外用一个调度表之类的存储结构来统一记录和管理。这样还可以和面试官进一步交流学习。

  • 5.复盘反思

每一场面完都要反思总结,看看哪里答得不好。如果是知识性的缺陷,就赶快补起来;知道答案但答得不好的,就重新组织措辞和表达方式;即使是回答上来的,也可以反思一下如何答得更好。

当然,所有技巧的前提,是认真踏实的基础准备

一线互联网公司的校招面试流程

  • 笔试

我觉得很多公司的笔试是过场,主要还是根据简历刷人。因为我笔试从来没全部 AC 过,但都通过笔试了,认识的一些朋友三题 AC,却没过笔试。

  • 电话面试

有的大公司会全程电话面试(如阿里),有的是先电话面试筛选简历,然后通知现场面(如网易)。电话面试的好处就是不用手写代码,缺点就是语言很多时候传递的信息是不够的,很多面部反馈、手势和图例都无法展示。

  • 现场面试

大多数公司还是会安排现场面试,要么安排你去公司面试(大公司会报销路费),要么安排公司员工到你学校来,通常就是 2 面技术面+1 面 HR 面 。每个公司风格不同,比如腾讯一般是一天一面,战线有三天,而网易是一天面完。

另外,其实校招的战线已经从每年的 9 月份提前到了 3 月份,尤其是阿里,实习生留用会占很大一个比例。所以希望认清紧迫的形式,越早准备越好。3 月能不能找到一个好的实习,对找工作影响还是挺大的,毕竟,如果没实习经历,那面试只能全程怼基础了,而基础是个无底洞。而且,手里是否有一个保底的转正 offer,你面试的心态是不一样的。

一些建议

  • 简历越早投越好:就像《中国好声音》导师转椅子一样,刚开始觉得不错的就啪啪啪四转,后来坑不够了,越来越纠结
  • 只投一种岗位:我全程只投了“Java 研发岗/服务器端开发”,这样可以统一准备,避免战线过长
  • 分梯度精投几个公司:不要海投,这样会疲于奔命,劳民伤财还没有时间准备。可以根据自己的能力,投几个不同档次的公司(如:高于自己水平的,希望比较大的,很有把握的,这几档),争取拿到保底的,冲刺心仪的,而不是看到一个公司招人就去面试。

最后祝各位找工作顺利,这篇文章主要是写给学弟学妹的,希望尽早重视起来,开始准备。如果读完觉得有收获的话,欢迎给我打赏(http://brianway.github.io/about/)

查看原文

赞 10 收藏 15 评论 2

认证与成就

  • 获得 210 次点赞
  • 获得 11 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 9 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • shepher

    Shepher 是一款 ZooKeeper 的管理工具。在小米公司,我们用它作为配置管理中心。

  • webporter

    webporter 是一个基于垂直爬虫框架 webmagic 的 Java 爬虫应用,旨在提供一套完整的数据爬取,持久化存储和可视化展示的实践样例。技术栈: Java+Elasticsearch+Kibana

  • java-learning

    java&javaweb学习笔记,内容主要是对一些基础特性和编程细节进行总结整理

注册于 2015-11-06
个人主页被 1.5k 人浏览