Elixir 解析XML的几个坑

在Elixir 1.0.2中解析XML文档的时候,最开始是参考的这个视频, 总之在Google的搜索结果中掉进了无数多个坑后,终于爬出来了.

创建一个项目, 本文所述所有内容都是在文件test/xml_parsing_test.exs文件中完成, 并通过mix test执行测试用例:

root@c87c9967219c:~# mix new xml_parsing
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/xml_parsing.ex
* creating test
* creating test/test_helper.exs
* creating test/xml_parsing_test.exs

Your mix project was created successfully.
You can use mix to compile it, test it, and more:

    cd xml_parsing
    mix test

Run `mix help` for more commands.

把我拉出来的是下面这篇Wiki, 视频中的代码有几个问题:

  • defrecord关键字废弃了,必须用Record.defrecord
  • Record.defrecord不能定义在模块的外面了,必须定义在模块内部, 比如:

    1
    defmodule XmlParsingTest do
        use ExUnit.Case
        require Record
        Record.defrecord :xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl")
        Record.defrecord :xmlText, Record.extract(:xmlText, from_lib: "xmerl/include/xmerl.hrl")
        ...
    end
  • 获取节点值的方式变了

1
test "Test parsing xml document OLD" do
    {xml, _rest}        = :xmerl_scan.string(:binary.bin_to_list(sample_xml))
    [ title_element ]   = Enum.first(:xmerl_xpath.string('/blog/title', xml)).value
    [ title_text ]      = title_element.content
    title               = title_text.value
    assert title == 'Using xmerl module to parse xml document in elixir'
end
# 对比上下两个测试方法的区别
test "Test parsing xml document NEW" do
    {document, _} = :xmerl_scan.string(String.to_char_list(sample_xml))
    [element] = :xmerl_xpath.string('/blog/title/text()', document)
    assert xmlText(element, :value)  == 'Using xmerl module to parse xml document in elixir'
end

对比上面的代码, 我们通过Record.defrecord定义记录, 该记录是从Erlang的xmerl库中提取的(调用Record.extract), 该记录将作为测试模块的一个函数,我们可以通过下面的方法验证:

运行iex进入Elixir Shell, 并声明模块:

导入xmerl模块的记录为Elixir模块函数

1
test "Test parsing xml document" do
    # 解析XML文档
    {document, _} = :xmerl_scan.string(String.to_char_list(sample_xml))
    # XPATH查询
    [element] = :xmerl_xpath.string('/blog/title/text()', document)
    # 断言
    assert xmlText(element, :value)  == 'Using xmerl module to parse xml document in elixir'
end

完整的代码

https://gist.github.com/94ce9976fc52e04e572a

完整的项目代码

https://github.com/developerworks/xml_parsing

参考资料

  1. ELIXIR AND XML
    http://erlangcentral.org/wiki/index.php?title=Elixir_and_XML
  2. 028: Parsing XML
    http://elixirsips.com/episodes/028_parsing_xml.html
  3. 常用的XML文档解析模块
    https://github.com/h4cc/awesome-elixir#xml

使用Elixir Mix编译Ejabberd模块

本文是基于Github上的一个项目的实践, 该项目包含了Ejabberd 14.07的头文件, 直接把开发好的模块放在src目录下,并执行:

克隆项目:

1
# git clone https://github.com/scrogson/ejabberd_dev.git
Cloning into 'ejabberd_dev'...
remote: Counting objects: 39, done.
remote: Total 39 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (39/39), done.
Checking connectivity... done.

进入项目目录

1
cd ejabberd_dev

编译:

1
mix do deps.get, compile

即可编译模块, 编译的Beam文件生成到_build目录中.

TODO:

  • 开发一个Elixir Mix任务,把编译好的Beam文件复制到Ejabberd的部署目录
  • 开发一个Elixir Mix任务, 用于更新Ejabberd新版本的头文件到ejabberd_dev项目目录

Sequelize 定义索引

Node的ORM框架Sequelize 2.0版本支持在模型定义文件中定义索引的创建, 2.0之前的版本, 需要通过Migration的方式创建.

V1.7版本的创建方式

1
migration.addIndex('partner', ['name', 'cellphone'])

V2版本的创建方式

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
var moment = require('moment');
module.exports = function (sequelize, DataTypes) {
var partner = sequelize.define('partner', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: '合作伙伴ID',
allowNull: false
},
name: {
type: DataTypes.STRING(32),
allowNull: false,
comment: '合作伙伴名称,可以是个人也可以使组织机构',
description: '合作伙伴名称,可以是个人也可以使组织机构'
},
concat: {
type: DataTypes.STRING(16),
allowNull: false,
comment: '联系人姓名',
description: '联系人姓名'

},
cellphone: {
type: DataTypes.STRING(11),
allowNull: false,
comment: '联系人手机号码',
description: '联系人手机号码'
},
email: {
type: DataTypes.STRING(64),
allowNull: true,
comment: '联系人电子邮件地址',
description: '联系人电子邮件地址'
},
status: {
type: DataTypes.STRING(16),
allowNull: false,
defaultValue: 'PENDING', // 'APPROVED, REJECTED',
comment: '合作伙伴状态,PENDING:等待审核,APPROVED:已经核准,REJECTED:已拒绝',
description: '合作伙伴状态,PENDING:等待审核,APPROVED:已经核准,REJECTED:已拒绝'
}
}, {
comment: '合作伙伴信息表',
classMethods: {
associate: function (models) {
partner.hasMany(models.map);
}
},
getterMethods: {
created_at: function () {
return moment(this.getDataValue('created_at')).format('YYYY-MM-DD');
},
updated_at: function () {
return moment(this.getDataValue('updated_at')).format('YYYY-MM-DD HH:mm:ss');
},
deleted_at: function () {
return moment(this.getDataValue('deleted_at')).format('YYYY-MM-DD HH:mm:ss');
}
},
indexes: [
{
name: 'partner_unique_index_name',
unique: true,
method: 'BTREE',
fields: ['name']
},
{
name: 'partner_unique_index_cellphone',
unique: true,
method: 'BTREE',
fields: ['cellphone']
}
]
});
partner._status = {
PENDING: 'PENDING',
APPROVED: 'APPROVED',
REJECTED: 'REJECTED'
};
return partner;
};

OSM building

OSM Buildings 是一个OpenStreetMaps建筑几何体的可视化库.

龙湖北城天街OSM Buildings

  • 0.2.2 开始如下方法名称被重命名

    1
    loadData() -> load()
    setData() -> set()
    setStyle() -> style()
    setDate() -> date()
  • 与Leaflet集成

    1
    <head>
      <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css">
      <script src="http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
      <script src="OSMBuildings-Leaflet.js"></script>
    </head>
  • 初始化地图引擎并添加地图Tile层

    1
    // 瓦片模板
    new L.TileLayer(
    'http://{s}.tiles.mapbox.com/v3/osmbuildings.kbpalbpk/{z}/{x}/{y}.png',
    { attribution: 'Map tiles &copy; <a href="http://mapbox.com">MapBox</a>', maxNativeZoom: 19, maxZoom: 21 }
    ).addTo(map);
  • 设置GeoJSON数据

    1
    // ==========
    // GeoJSON层
    // ==========
    var geojsonLayer = L.geoJson([longforGeojsonFeature], {
      style: function (feature) {
        return feature.properties && feature.properties.style;
      },
      onEachFeature: function (feature, layer) {
        console.log(feature);
        layer.bindPopup(feature.properties.name);
      }
    }).addTo(map);
  • 集成OSM Buildings显示3D建筑物:

    1
    // ============================
    // 集成OSM Buildings显示3D建筑物
    // ============================
    // 加载数据
    var osmb = new OSMBuildings(map)
      .date(new Date(2014, 10, 8, 8, 0))
      .load()
      .click(function (e) {
        console.log('feature clicked:', e);
        osmb.getDetails(e.feature, function (json) {
          console.log(json.features[0].properties.tags);
          var popup = L.popup()
            .setLatLng([29.58170544840436, 106.53061509132385])
            .setContent(
                '<h3 style="margin: 5px 0">' +
                json.features[0].properties.tags.name + '</h3>' + '<br/>' +
                json.features[0].properties.tags.description
            )
            .openOn(map);
        });
      });
    // 加载层切换器
    L.control.layers({}, { Buildings: osmb }).addTo(map);

完整项目代码

1
git clone https://github.com/developerworks/leaflet-longfor.git
cd leaflet-longfor
bower install
npm install
# 启动Web服务器
python -m SimpleHTTPServer 8888 # Python 2.7
python -m http.server 8888      # Python 3
# 浏览器地址栏
http://localhost:8888/examples

参考资料

  1. Leafletjs API参考手册
    http://leafletjs.com/reference.html

  2. OSM Buidling Github项目地址
    https://github.com/kekscom/osmbuildings

  3. Leaflet 集成 OSM Buildings 显示 3D 建筑物
    http://www.cuitu.net/content/leaflet-ji-cheng-osm-buildings-xian-shi-3d-jian-zhu-wu

Semantic UI 入门

Semantic UI

基本用法

<head>内引入dist/semantic.min.css/dist/semantic.min.js两个文件

1
<link rel="stylesheet" type="text/css" href="/dist/semantic.min.css">
<script src="/dist/semantic.min.js"></script>

也可以使用单独的某个组件

1
<link rel="stylesheet" type="text/css" href="/dist/components/icon.css">

推荐用法

gulp install

1
git clone https://github.com/Semantic-Org/Semantic-UI.git
cd Semantic-UI
npm install
gulp

运行 gulp 进入交互式设置, 会问你一些问题, 仔细看, 仔细答.

1
gulp            // 安装后监视修改
gulp build      // 从源代码构建所有文件
gulp clean      // 清除dist目录
gulp watch      // 监视文件
gulp install    // 重新运行安装
gulp help       // 列举所有命令

移动3G网络下的流管理

这篇文章的起因是因为朋友开发的移动社交APP使用Ejabberd的时候遇到了3G网络频繁断开连接的问题.
由于3G网络的特性,在3G网络内的客户端IP可能是频繁变化的,你手持移动设备在不停的移动,遇到建筑物,
进入电梯等都可能导致3G网络突然中断.等你再次连接上服务器的时候,之前的状态就已经丢失了.

为了解决这类问题, XMPP基金会发布了一个XMPP扩展协议XEP-198 Stream Management 类支持XMPP会话的恢复.

Ejabberd在14.05之后内置了对流管理的支持, 所以要使服务器支持流管理, 必须升级到至少14.05, Ejabberd的默认配置是打开了流管理功能的.

下面是服务器需要支持的配置选项:

1
listen:
  -
    port: 5222
    module: ejabberd_c2s
    ##
    ## If TLS is compiled in and you installed a SSL
    ## certificate, specify the full path to the
    ## file and uncomment these lines:
    ##
    ## certfile: "/path/to/ssl.pem"
    ## starttls: true
    ##
    ## To enforce TLS encryption for client connections,
    ## use this instead of the "starttls" option:
    ##
    ## starttls_required: true
    ##
    ## Custom OpenSSL options
    ##
    ## protocol_options:
    ##   - "no_sslv3"
    ##   - "no_tlsv1"
    max_stanza_size: 65536
    shaper: c2s_shaper
    access: c2s
    zlib: true
    ##
    ## 打开(true)或关闭(false)Ejabberd的流管理(XEP 198)功能, 默认为true
    ##
    stream_management: true
    ##
    ## 消息确认队列, 用于存储客户端未确认的消息队列, 用于重传, 当超过此限制时,
    ## 客户端会话自动被Ejabberd终止, 有效值为正整数和`infinity`, 默认值为500
    ##
    max_ack_queue: 500
    ##
    ## 会话超时是重传
    ##
    ## 未被客户端确认的消息将在会话超时的时候重传(retransimission),
    ## 该行为通常是我们所期望的, 但是在某些环境下可能导致不期望的结果, 比如:
    ## 当一个消息同时发送给两个资源时, 如果一个资源超时, 那么另一个资源将收到服务器重传的消息,
    ## 这不是我们所期望的结果. 因此Ejabberd默认对此选项的设置为false, 含义为当会话超时后,
    ## 返回一个错误, 而不是重传消息
    ##
    ## 为了避免上述两个资源其中一个超时的副作用,可以:
    ## 1. 如果一个JID 只允许有一个 Resource同时在线, 那么我们可以把该选项设置为true
    ## 2. 如果一个JID 允许不止一个 Resource同时在线, 建议把该选项设置为false
    resend_on_timeout: false

要启用XMPP扩展协议XEP 198所描述的流恢复功能, 还需要客户端主动打开流恢复功能. 客户端要判断服务器是否支持流管理特性, 在流初始化阶段服务器会返回如下信息:

1
<stream:features xmlns="jabber:client"
    xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
  <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/>
  <session xmlns="urn:ietf:params:xml:ns:xmpp-session"/>
  <sm xmlns="urn:xmpp:sm:2"/>
  <sm xmlns="urn:xmpp:sm:3"/>
  <csi xmlns="urn:xmpp:csi:0"/>
  <c xmlns="http://jabber.org/protocol/caps"
    hash="sha-1"
    node="http://www.process-one.net/en/ejabberd/"
    ver="aIT+/ulfcbHXDKPkCA+iw9x5mU8="/>
  <register xmlns="http://jabber.org/features/iq-register"/>
</stream:features>

如果包含:

1
...
<sm xmlns="urn:xmpp:sm:2"/>
<sm xmlns="urn:xmpp:sm:3"/>
...

那么说明服务器是支持流管理特性的. 这个时候我们可以让客户端发送一个<enable>XML片段通知服务器端打开流管理.

1
<enable xmlns='urn:xmpp:sm:3' resume='true'/>

同时服务响应一个<enabled>标签,表示服务器的流管理已经打开.

1
<enabled xmlns="urn:xmpp:sm:3"
    id="g2gCbQAAABk5NDIyMTUxMzkxNDE4OTA0NzEwMjA1OTUxaANiAAAFimIADc4EYgAO508="
    resume="true"
    max="300"
    xmlns:stream="http://etherx.jabber.org/streams"
    version="1.0"/>

<enabled>标签上有几个重要的属性:

  • id 流管理的会话ID
  • resume 是否支持流恢复
  • max 会话超时时间

上述的流管理会话的建立过程必须是在通过了身份认证之和资源绑定之后, 如果未通过身份认证资源绑定尝试建立一个流管理会话将会得到一个<fail/>.

流的恢复

1
<resume
    xmlns='urn:xmpp:sm:3'
    h='some-sequence-number'
    previd='g2gCbQAAABk5NDIyMTUxMzkxNDE4OTA0NzEwMjA1OTUxaANiAAAFimIADc4EYgAO508='/>
  • 必须带上xmlns='urn:xmpp:sm:3'属性
  • h 表示断开连接之前最后一次从服务器收到的XML节的序列号
  • previd 上一次流会话的ID

客户端发送流恢复请求之后, 服务端会重新生成一个id

1
<enabled
    xmlns="urn:xmpp:sm:3"
    id="g2gCbQAAABozNDY3MjMxMTkyMTQxODkwNjE4MDM1NjIwM2gDYgAABYpiAA3Tw2IAAlr+"
    resume="true"
    max="300"
    xmlns:stream="http://etherx.jabber.org/streams" version="1.0"/>

我们看到服务器响应了一个<enabled>标签, 其属性id的值和之前是不同的.

客户端需要记录这个值, 以备下一次连接中断后的流恢复.

(译)理解Elixir宏第3部分

继续我们对宏的探索. 上一篇我概括了一些基本原理, 今天我们来讨论关于Elixir AST的一些细节.

追踪函数调用

发现AST结构

编写断言宏

序列化代码

(译)理解Elixir宏第2部分

这是理解Elixir宏系列文章的第二篇. 上一篇讨论了编译阶段Elixir宏

调用宏

要知道在编译过程的展开阶段最重要的事情是: 编译器调用各种宏(以及其他代码生成构造), 并产生最终的AST.

例如, 一个经典的对trace宏的使用示例如下:

1
# 定义了我们的应用模块
defmodule MyModule do
  # 包含了我们需要用到的代码调试和追踪模块
  require Tracer
  ...
  def some_fun(...) do
    # 调用具体的追踪模块调试代码
    Tracer.trace(...)
  end
end

Hygiene

上一章提到过, 宏默认是hygienic的. 其含义是: 宏引入的变量有其自己的私有作用域, 不会影响代码的其他部分. 这就是为什么我们能安全地在trace宏中引入result变量.

1
quote do
    result = unquote(expression_ast) # result 变量是宏的私有变量
    ...
end

该变量不会干扰调用这个宏的代码. 在调用宏的地方, 可以随意的声明你自己的result变量, 它不会被tracer宏中的result变量隐藏.

大多数时候hygiene是我们想要的效果, 但是也有例外. 有时候, 可能需要创建在调用者作用域内可用的变量. 下面我们通过Plug库的一个用例来演示, 我们如何使用Plug来制定路由:

1
get "/resource1" do
    send_resp(conn, 200, ...)
end
post "/resource2" do
    send_resp(conn, 200, ...)
end

注意,上面这两个宏如何使用并不存在的conn变量. 这是因为, get宏在生成的代码中绑定了该变量. 可以想象一下, 产生的代码如下:

1
defp do_match("GET", "/resource1", conn) do
    ...
end
defp do_match("POST", "/resource2", conn) do
    ...
end

注意: Plug产生的真实代码是不同的, 这里为了演示对其进行了简化.

这是一个例子, 宏引入了一个变量, 必须不是hygienic. 变量connget宏引入, 必须对调用者可见.

宏参数

揉到一起

使用模块

查看上面的代码, 函数match/2是在客户端模块中实现. 这确实是太不完美了, 因为每个客户端模块都必须提供该函数的正确实现,并知道do_match函数如何调用.

如果Plug.Router能够提供match/2的实现是不是更好. 对此我们可以采用use宏, 它初略地等同于其他语言中的Mixin.

普遍的思想是:

1
# 客户端模块部分
defmodule ClientCode do
  # invokes the mixin
  use GenericCode, option_1: value_1, option_2: value_2, ...
end
# 通用代码部分
defmodule GenericCode do
  # called when the module is used
  defmacro __using__(options) do
    # generates an AST that will be inserted in place of the use
    quote do
      ...
    end
  end
end

use机制允许我们在调用者的上下文种注入代码, 这类似于如下这种替代形式:

1
defmodule ClientCode do
    # 加载通用模块
    require GenericCode
    # 调用通用模块中的公共函数
    GenericCode.__using__(...)
end

其可以通过use宏的Elixir实现源代码得到证明. 同时还证明了另一点 - 增量展开.

use宏生成了调用其他宏的代码. 说的更明白一点, use生成了用于生成代码的代码(use生成了代码,生成的代码又用于生成其他的代码), 早前我们曾说过, 编译器会递归地展开它所发现的所有宏定义, 直到没有可展开的宏为止.

拥有了这方面的知识, 现在可以转移到Plug.Router通用模块的match函数的实现.

1
defmodule Plug.Router do
  defmacro __using__(_options) do
    # 译注:
    # 在调用者use本模块的时候返回一个AST结构,并插入到use被使用的位置
    # 如果你有Web的开发经验, 类似于在DOM树中插入一个子树或子节点,用于改变整个DOM树的结构,
    # 类似的__using__宏返回一个AST片段, 并插入到调用代码的位置, 也就是在客户端模块中`use`宏使用的位置
    quote do
      # 导入自身, 以便在客户端代码中不必以`Plug.Router.get`的形式使用,而是直接使用`get`
      import Plug.Router
      def match(type, route) do
        do_match(type, route, :dummy_connection)
      end
    end
  end
  defmacro get(route, body) do
    ... # 这里的代码保持原样
  end
end

这样, 我们就可以把客户端模块的代码保持的非常苗条:

1
defmodule MyRouter do
  # 使用Plug.Router模块,同时在编译阶段(具体实际上是展开阶段),运行该模块中的`__using__`宏,
  # 并把`__using__`返回的AST片段插入到当前位置
  use Plug.Router
  get "/hello", do: {conn, "Hi!"}
  get "/goodbye", do: {conn, "Bye!"}
end

前面曾提到, 由__using__宏生成的AST片段只是简单地插入到use Plug.Router调用的位置. 特别注意的是, 在__use___宏定义中的import Plug.Router, 严格意义上并不需要这行代码, 它只是使客户端代码变得更加简洁一些, 你可以使用get, 而不必输入其全称Plug.Router.get.

下面这段就不译了, 一堆废话, 看得懂的看, 看不懂的可以忽略.

So what have we gained? The various boilerplate is now confined to the single place (Plug.Router).
Not only does this simplify the client code, it also keeps the abstraction properly closed.
The module Plug.Router ensures that whatever is generated by get macros fits properly with the generic code of match.
As clients, we simply use the module and call into the provided macros to assemble our router.

This concludes today’s session. Many details are not covered,
but hopefully you have a better understanding of how macros integrate with the Elixir compiler.
In the next part I’ll dive deeper and start exploring how we can tear apart the input AST.

译注

对于use的使用我们可以理解为:

  • PHP中的require 'database.php'include 'database.php'
  • Java中的import com.mysql.jdbc.Driver;
  • Python中的import amqp
  • C中的include <stdlib.h>

它是在编译过程中的展开阶段对代码(AST)进行修改和装饰, 并把修改过的代码(AST)插入到使用的位置.

原文

http://www.theerlangelist.com/2014/06/understanding-elixir-macros-part-2.html

(译)理解Elixir宏第1部分

本文中的某些概念, 你最好事先了解过编译原理, 以及抽象语法树的(AST)概念, 否则本文中所描述的东西可能对你来说是看不懂的天书.

编译过程

Elixir源码编译过程图示

通过上图的Elixir编译器的编译过程我们看到:

  1. Elixir源码通过第一步解析过程生成了一个AST 1的中间形式(以Elixir嵌套Term的形式来表示抽象语法树)
  2. AST 1通过(展开expansion)转换为Expanded AST(已展开的抽象语法树)
  3. 展开的AST被转换成字节码

这只是一个近似的过程, 实际上Elixir编译器会生成Erlang AST, 并依赖于底层的Erlang函数把它转换成字节码.

创建AST片段

什么是Elixir AST ? 它是一个Elixir Term, 一个深度嵌套的层次结构, 用于表述一个语法正确的Elixir代码. 为了说得更明白一些, 举个例子. 要生成某段代码的AST, 可以使用quote:

1
iex(1)> quoted = quote do 1 + 2 dn
{:+, [context: Elixir, import: Kernel], [1, 2]}

Quote 以任意复杂的Elixir表达式作为输入,并返回相应的描述该输入代码的AST.

此例中, 生成的AST片段描述一个简单的求和操作(1+2). 通常称为quoted expression. 大多数时候不需要理解quoted结构的具体细节, 来看一个简单的例子. 在这种情况下, AST片段是一个包含如下元素的三元组(triplet)

  • 一个操作符原子
  • 表达式上下文(比如, import和aliases).大多数时候不需要理解该数据
  • 操作参数(operands)

要点: quoted expression是一个描述代码的Elixir term. 编译器会使用它生成最终的字节码.

虽然不常见, 对一个quoted expression求值也是可以的.

1
iex(2)> Code.eval_quoted(quoted)
{3, []}

求值结果为一个元组, 包含表达式的求值结果, 以及构成该表达式的变量.

但是, 在AST被求值前(通常由编译器完成), quoted expression 并没有通过语义上的验证. 例如, 当我们书写如下表达式时:

1
iex(3)> a + b
** (RuntimeError) undefined function: a/0

得到了一个错误, 因为这里还不存在一个名为a的变量或函数.

相比而言, 如果quote一个表达式:

1
iex(4)> quote do a + b end
{:+, [context: Elixir, import: Kernel], [{:a, [], Elixir}, {:b, [], Elixir}]}

而没有发生错误, 我们有了一个表达式 a+b的quoted表示. 其意思是, 生成了一个描述表达式a+b的term, 不管表达式中的变量是否存在. 最终的代码并没有生成, 所以这里不会有错误.

如果把该表述插入到某些a和b是有效标识符的AST中, 刚才发生错误的代码a+b,才是正确的. 下面来试一下, 首先quote一个求和(sum)表达式:

1
iex(5)> sum_expr = quote do a + b end

然后创建一个quoted变量绑定表达式:

1
iex(6) bind_expr = quote do
         a = 1
         b = 2
       end

再说一遍, 记住这仅仅是quoted的表达式, 他们只是描述代码的简单数据, 并没有执行任何求值. 特别是, 变量 ab 在当前的Elixir shell会话中并不存在.

要使这些片段能够一起工作, 必须把它们连接起来:

1
iex(7) final_expr = quote do
         unquote(bind_expr)
         unquote(sum_expr)
       end

这里,我们生成了一个新的quoted表达式final_expr, 由bind_expr表达式和sum_expr表达式构成.

下面可以对最终的AST求值:

1
iex(8)> Code.eval_quoted(final_expr)
{3, [{{:a, Elixir}, 1}, {{:b, Elixir}, 2}]}

求值结果由一个表达式, 一个变量绑定列表构成. 形如:

1
{expression, [{{:variable,Elixir}, value},...]}
===========      ========          ======
  |                 |                 |
表达式            变量名称           变量的值

从这个绑定列表中我们可以看出, 该表达式绑定了两个变量ab, 对应的值分别为12

这就是在Elixir中元编程方法的核心. 当我们进行元编程的时候, 我们基本上是把各种AST片段组合起来生成新的AST. 我们通常对输入AST的内容和结构不感兴趣, 相反, 我们使用quote生成和组合输入片段,并生成经过修饰的代码.

Unquoting

unquote上场了. 注意不管quote块里面包含什么, 它只是把quote ... end块里面的达式转换成AST片段. 这意味着我们不能以常规方式注入存在于quote外部的变量的内容. 看看上面这个例子, 它是不可能工作的:

1
quote do
  bind_expr
  sum_expr
end

在这个例子中, quote仅仅是简单的生成对bind_exprsum_expr的变量引用, 但这不是我想要的结果. 我需要的效果是有一种方式能够直接注入bind_exprsum_expr的内容到生成的AST的对应的位置.

这就是unquote(...)的用途 - 括号内的表达式被立即进行求值, 并就地插入到unquote调用的位置. 这意味着 unquote 的结果也必须是一个有效的AST片段.

理解unquote的另一种方式是, 可以把它看做是字符串插值 (#{}). 对于字符串你可以这样写:

1
"....#{some_expression}...."

类似的, 对于quote可以这样写:

1
quote do
    ...
    unquote(some_expression)
    ...
end

对此两种情况, 求值的表达式必须在当前上下文中是有效的, 并注入该结果到你构建的表达式中.(要么是符串, 或者是一个AST片段)

重要的时理解其含义, unquote并不是quote的反向过程. 如果需要把一个quoted expression转换为字符串, 可以使用Macro.to_string/1

例子: 追踪表达式

让我们把这些理论组合到一个简单的例子中. 我们会编写一个宏来帮助我们调试代码. 下面是这个宏的使用方式:

1
iex(1)> Tracer.trace(1 + 2)
Result of 1 + 2: 3

Tracer.trace以一个给定的表达式并打印其结果到屏幕上. 然后返回表达式结果.

重要的是,意识到这是一个宏, 其输入表达式(1 + 2)会被转换为一个更加复杂的形式 - 一段打印结果并返回该结果的代码.该转换发生在宏展开的时候, 产生的字节码为输入代码经过修饰的版本.

在查看实现之前, 想象一下最终结果. 当我们调用Tracer.trace(1+2)时, 对应产生的字节码类似于这样:

1
mangled_result = 1 + 2
Tracer.print("1+2", mangled_result)
mangled_result

展开AST

在Shell观察其是如何连接起来是很容易的. 启动iex Shell, 复制粘贴上面定义的Tracer模块:

1
iex(1)> defmodule Tracer do
          ...
        end

然后, 必须require Tracer

1
iex(2)> require Tracer

接下来, 对trace的宏调用进行quote操作

1
iex(3)> quoted = quote do Tracer.trace(1+2) end
{{:., [], [{:__aliases__, [alias: false], [:Tracer]}, :trace]}, [],
 [{:+, [context: Elixir, import: Kernel], [1, 2]}]}

现在, 输出看起来有点可怕, 通常你不必需要理解它. 但是如果你仔细看, 在这个结构中你可以看到Tracertrace, 这证明了AST片段是何源代码相对应的, 但还未展开.

参考资料

概念补充

  • Hygiene

    用不冲突的名称替换引入的变量名,这种方法称为健康的(hygiene);产生的宏称为健康的宏(hygienic macros).健康的宏可以安全地在任何地方使用,不必担心与现有的变量名冲突。对于许多元编程任务,这个特性使宏更可预测并容易使用。

    Hygiene 宏的引入是为了解决宏定义上下文宏调用上下文变量名称冲突的问题.

在新版本的npm恢复老版本的下载状态风格

新版本的npm在安装或更新包的时候, 显示的一个spinner一直在旋转, 特别是在网络速度不好的情况下体验感非常差.

老版本的npm安装包的时候显示的过程是这样的:

1
npm http GET https://registry.npmjs.org/repeating/-/repeating-1.1.0.tgz
npm http 304 https://registry.npmjs.org/graceful-fs

新版本显示的却是这样的:

NPM新版本进度指示器

恢复到老版本的进度显示风格可以使用下面的命令完成:

1
npm config set spin=false
npm config set loglevel=http

参考资料

  1. https://github.com/npm/npm/issues/5340

Fleet of time <<匆匆那年> 王菲

<<匆匆那年>> 王菲

匆匆那年我们究竟说了几遍 再见之后再拖延
可惜谁有没有爱过不是一场 七情上面的雄辩

匆匆那年我们一时匆忙撂下难以承受的诺言
只有等别人兑现

不 怪那 吻痕 还 没积累成茧
拥 抱着冬眠 也 没能羽化再成仙

不 怪这一段情 没空反复再排练
是 岁月宽容恩赐 反悔的时间...

[REPEAT 1] 如果再见不能红着眼 是否还能红着脸
[REPEAT 1] 就像那年 匆促刻下 永远一起 那样美丽的谣言
[REPEAT 1] 如果过去还值得眷恋 别太快冰释前嫌
[REPEAT 1] 谁 甘心 就这样 彼此无挂也无牵

我 们要互相 亏欠 要 不然 凭何怀缅

....

匆匆那年我们见过太少世面 只爱看同一张脸
那么莫名其妙 那么讨人欢喜 闹起来又太讨厌

相爱那年活该 匆匆因为我们不懂顽固的诺言
只 是分手的前言

不 怪哪天太冷 泪 滴水成冰
春 风也一样没 吹进凝固的照片

不 怪每一个人 没能完整爱一遍
是 岁月善意落下 残缺的悬念

[REPEAT 2] 如果再见不能红着眼 是否还能红着脸
[REPEAT 2] 就像那年 匆促刻下 永远一起 那样美丽的谣言
[REPEAT 2] 如果过去还值得眷恋 别太快冰释前嫌
[REPEAT 2] 谁 甘心 就这样 彼此无挂也无牵

[REPEAT 3] 如果再见不能红着眼 是否还能红着脸
[REPEAT 3] 就像那年 匆促刻下 永远一起 那样美丽的谣言
[REPEAT 3] 如果过去还值得眷恋 别太快冰释前嫌
[REPEAT 3] 谁 甘心 就这样 彼此无挂也无牵

我们要互相亏欠 我们要藕断丝连

HTTP客户端Httpotion

步骤

本文描述如何使用Elixir的httpotion模块调用豆瓣图书API获取图书信息.

root@7bf32c349b69:~# mix new douban_book_api_v2
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/douban_book_api_v2.ex
* creating test
* creating test/test_helper.exs
* creating test/douban_book_api_v2_test.exs

Your mix project was created successfully.
You can use mix to compile it, test it, and more:

    cd douban_book_api_v2
    mix test

Run `mix help` for more commands.

进入刚刚创建的目录douban_book_api_v2,编辑mix.exs,增加依赖模块:

修改函数deps为:

1
defp deps do
    [
        {:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.0"},
        {:jiffy, github: "davisp/jiffy"},
        {:httpotion, "~> 0.2.0"}
    ]
end

然后执行如下命令,下载依赖模块:

1
# cd douban_book_api_v2
# mix deps.get

注意很多依赖模块托管在Amazon S3上,可能被墙, 可使用如下命令穿墙:

1
export HTTP_PROXY=http://192.168.8.188:8580
export HTTPS_PROXY=https://192.168.8.188:8580

然后编译:

1
# mix compile

接着进入iex(Elixir交互式SHELL):

1
# iex -S mix

获取图书信息

示例代码

https://github.com/developerworks/douban_book_api_v2

参考资料

  1. https://github.com/myfreeweb/httpotion
  2. http://developers.douban.com/wiki/?title=book_v2#get_book
  3. https://api.douban.com/v2/book/1041007
  4. http://learnxinyminutes.com/docs/zh-cn/elixir-cn/

修订记录

Javascript的访问方法和ej的访问方法对比

1
# 把响应体解码为Erlang term
json = :jiffy.decode response.body
# 通过ej模块访问Erlang term
:ej.get({"author"}, josn)
1
iex(21)> :ej.get {"images", "small"}, json
"http://img3.douban.com/spic/s1990480.jpg"
iex(22)> :ej.get {"images", "large"}, json
"http://img3.douban.com/lpic/s1990480.jpg"
iex(23)> :ej.get {"images", "medium"}, json
"http://img3.douban.com/mpic/s1990480.jpg"
iex(24)> :ej.get {"images", "medium"}, json
"http://img3.douban.com/mpic/s1990480.jpg"
iex(25)> :ej.get {"images", "tags", 1}, json
:undefined
iex(26)> :ej.get {"images", "tags"}, json
:undefined
iex(27)> :ej.get {"tags"}, json
[{[{"count", 13368}, {"name", "哈利波特"}, {"title", "哈利波特"}]},
 {[{"count", 10741}, {"name", "J.K.罗琳"}, {"title", "J.K.罗琳"}]},
 {[{"count", 8892}, {"name", "魔幻"}, {"title", "魔幻"}]},
 {[{"count", 7741}, {"name", "小说"}, {"title", "小说"}]},
 {[{"count", 6536}, {"name", "英国"}, {"title", "英国"}]},
 {[{"count", 4591}, {"name", "外国文学"}, {"title", "外国文学"}]},
 {[{"count", 3871}, {"name", "哈利·波特"}, {"title", "哈利·波特"}]},
 {[{"count", 3871}, {"name", "哈利・波特"}, {"title", "哈利・波特"}]}]
iex(28)> :ej.get {"tags", 1}, json
{[{"count", 13368}, {"name", "哈利波特"}, {"title", "哈利波特"}]}
iex(29)> :ej.get {"tags", 1, "count"}, json
13368
iex(30)> :ej.get {"tags", 1, "name"}, json
"哈利波特"
iex(31)> :ej.get {"tags", 1, "title"}, json
"哈利波特"
iex(32)> :ej.get {"tags", 2, "title"}, json
"J.K.罗琳"
iex(33)> :ej.get {"tags", 3, "title"}, json
"魔幻"
iex(34)> :ej.get {"tags", 3, "count"}, json
8892

构建一个IRC记录程序

IRC日志记录程序主要解决时区的问题, 如果你的IRC客户端不是一直连接到IRC服务器, 那么可能错过很多精彩的讨论. 即使你一直开着你的IRC客户端,也可能由于网络的不问题导致IRC客户端断开.下面用Elixir开发的一个IRC日志程序用于记录IRC服务器各个频道的聊天记录, 可以通过日期查看所有订阅频道的聊天信息.

##

IRC日志记录器用户界面

先决条件

  1. 需要安装Erlang OTP/17
  2. 需要安装Elixir 1.0.1以上版本

创建项目

1
2
3
4
5
6
7
8
9
10
11
$ mix new exile
* creating README.md
* creating .gitignore
* creating mix.exs
* creating config
* creating config/config.exs
* creating lib
* creating lib/exile.ex
* creating test
* creating test/test_helper.exs
* creating test/exile_test.exs

编译, (直接执行make test可以自动编译并测试)

1
2
$ cd exile
mix test

添加第三方库

deps函数添加第三方库:socket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule Exile.Mixfile do
use Mix.Project
def project do
[app: :exile,
version: "0.0.1",
elixir: "~> 1.0",
deps: deps]
end
def application do
[applications: [:logger]]
end
defp deps do
[
{:socket, "~> 0.2.8"}
]
end
end

下载第三方依赖库文件

1
$ mix deps.get
Running dependency resolution
Unlocked:   timex, socket
Dependency resolution completed successfully
  socket: v0.2.8
* Getting socket (Hex package)
Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/socket-0.2.8.tar)
Using locally cached package
Unpacked package tarball (/Users/benjamintan/.hex/packages/socket-0.2.8.tar)

构建机器人程序

该机器人程序主要用于连接到IRC服务器,并箭筒IRC服务器的频道聊天信息, 并记录到数据库中.

创建bot.ex

Exile.Bot模块使用了GenServer行为, 用于构建通用服务器程序, 使用GenServer的优点是, 它可以被添加到Supervisor进程树中,对其状态进行监控.

1
defmodule Exile.Bot do
  use GenServer
end

实现需要的回调函数

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
defmodule Exile.Bot do
use GenServer
@doc """
调用start_link初始化,传递一个状态state, state是一个Map, 比如:
%{
host: "irc.freenode.net",
port: 6667,
chan: "#elixir-lang",

nick: "elixir-bot",
sock: socket
}
"""
def start_link(state) do
GenServer.start_link(__MODULE__, state)
end
@doc """

创建一个到IRC服务器的套接字连接
"""
def init(state) do
{:ok, sock} = Socket.TCP.connect(state.host, state.port, packet: :line)
# 第三个参数0标识超时, 0为立即超时,并发生一个:timeout消息
{:ok, %{state | sock: sock}, 0}
end
@doc """

超时处理回调函数
"""
def handle_info(:timeout, state) do
IO.puts "TIMEOUT HANDLED"

{ :noreply, state }
end
end

连接到IRC服务器

1
2
3
4
def handle_info(:timeout, state) do
state |> do_join_channel |> do_listen
{ :noreply, state }
end

进入一个频道

1
2
3
4
5
6
defp do_join_channel(%{sock: sock} = state) do
sock |> Socket.Stream.send!("NICK #{state.nick}\r\n")
sock |> Socket.Stream.send!("USER #{state.nick} #{state.host} #{state.nick} #{state.nick}\r\n")
sock |> Socket.Stream.send!("JOIN #{state.chan}\r\n")
state
end

监听回复信息

进入频道后就可以通过下面的回调获取聊天信息

1
2
3
4
5
6
7
8
9
10
11
12
defp do_listen(%{sock: sock} = state) do
case state.sock |> Socket.Stream.recv! do
data when is_binary(data)->
case parse_message(data, sock) do
message ->
message
end
do_listen(state)
nil ->
:ok
end
end

保持连接

处理其他消息

参考资料

  1. Building an IRC Logger in Elixir
    http://www.neo.com/2014/12/01/building-an-irc-logger-in-elixir

Erlang 100问

Supervisor 的 simple_one_for_one 有什么关键性特点?

  1. Supervisor本身启动时不会启动子进程, 子进程的启动必须调用supervisor:start_child(Sup, Args)来启动.
  2. 只能有一种子进程定义(规范)

Loopback 数据库连接器示例

创建一个Loopback 应用程序

1
slc loopback # 创建项目longfor
cd longfor

项目创建好了之后, 在项目目录longfor下执行 npm install --save loopback-connector-mysql 安装MySQL数据库连接器

1
 $ npm install --save loopback-connector-mysql
npm http GET https://registry.npmjs.org/loopback-connector-mysql
...
...
...
loopback-connector-mysql@1.4.9 node_modules/loopback-connector-mysql
├── sl-blip@1.0.0
├── loopback-connector@1.2.0
├── async@0.9.0
├── debug@2.0.0 (ms@0.6.2)
└── mysql@2.5.3 (require-all@0.0.8, bignumber.js@1.4.1, readable-stream@1.1.13)

添加数据源

1
slc loopback:datasource longfordb

编辑数据源配置文件,添加数据库连接信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"db": {
"name": "db",
"connector": "memory"
},
"longfordb": {
"name": "longfordb",
"connector": "mysql",
"host": "localhost", # Added
"port": 3306, # Added
"database": "longfor", # Added
"username": "root", # Added
"password": "root" # Added
}
}

添加模型account,并创建模型account的三个属性: email, created, modified

1
slc loopback:model account
Enter an empty property name when done.
? Property name: email
   invoke   loopback:property
? Property type: string
? Required? Yes

Let's add another account property.
Enter an empty property name when done.
? Property name: created
   invoke   loopback:property
? Property type: date
? Required? Yes

Let's add another account property.
Enter an empty property name when done.
? Property name: modified
   invoke   loopback:property
? Property type: date
? Required? Yes

创建表并添加测试数据, 下载create-test-data.js脚本, 修改数据源名称为longfordb:

1
# 修改
var dataSource = server.dataSources.accountDB;
# 为
var dataSource = server.dataSources.longfordb;
1
$ cd server
$ node create-test-data.js
Record created: { email: 'foo@bar.com',
  created: Mon Dec 08 2014 00:43:16 GMT+0800 (CST),
  modified: Mon Dec 08 2014 00:43:16 GMT+0800 (CST),
  id: 1 }
Record created: { email: 'bar@bar.com',
  created: Mon Dec 08 2014 00:43:16 GMT+0800 (CST),
  modified: Mon Dec 08 2014 00:43:16 GMT+0800 (CST),
  id: 2 }
done

参考资料

  1. loopback-example-database
    https://github.com/strongloop/loopback-example-database/blob/master/README.md

Opencart 通过vQmod方式用Memcache替换默认的Cache

前提条件

用Memcache替换默认的Cache之前,需要满足如下前提条件:

  • PHP需要Memcache扩展, 可以通过如下方式安装, 并在php.ini配置文件中添加extension=memcache.so
1
pecl install memcache
  • 需要安装配置好Opencart
  • 需要安装vQmod

vQmod模块代码

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?xml version="1.0" encoding="UTF-8"?>
<modification>
<id>Replace opencart default cache with Memcache</id>
<version>1.0.0</version>
<vqmver>2.5.1</vqmver>
<author>hezhiqiang</author>
<file name="system/library/cache.php">
<operation>
<search position="replace" offset="50"><![CDATA[
<?php
]]></search>

<add><![CDATA[<?php
/**
* OpenCart Ukrainian Community
*
* LICENSE
*
* This source file is subject to the GNU General Public License, Version 3
* It is also available through the world-wide-web at this URL:
* http://www.gnu.org/copyleft/gpl.html
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
*
* @category OpenCart
* @package OCU Memcached
* @copyright Copyright (c) 2011 created by UncleAndy, maintained by Eugene Lifescale for
OpenCart Ukrainian Community (http://opencart-ukraine.tumblr.com)
* @license http://www.gnu.org/copyleft/gpl.html
GNU General Public License, Version 3
*/
final class Cache
{
private $expire;
private $memcache;
private $ismemcache = false;
public function __construct($exp = 3600)
{
$this->expire = $exp;
if (CACHE_DRIVER == 'memcached') {
$mc = new Memcache;
if ($mc->pconnect(MEMCACHE_HOSTNAME, MEMCACHE_PORT)) {
$this->memcache = $mc;
$this->ismemcache = true;
};
};
$files = glob(DIR_CACHE . 'cache.*');
if ($files) {
foreach ($files as $file) {
$time = substr(strrchr($file, '.'), 1);
if ($time < time()) {
if (file_exists($file)) {
@unlink($file);
}
}
}
}
}
public function get($key)
{
if ((CACHE_DRIVER == 'memcached') && $this->ismemcache) {
return ($this->memcache->get(MEMCACHE_NAMESPACE . $key, 0));
} else {
$files = glob(DIR_CACHE . 'cache.' . $key . '.*');
if ($files) {
foreach ($files as $file) {
$cache = '';
$handle = fopen($file, 'r');
if ($handle) {
$cache = fread($handle, filesize($file));
fclose($handle);
}
return unserialize($cache);
}
}
}
}
public function set($key, $value)
{
if ((CACHE_DRIVER == 'memcached') && $this->ismemcache) {
$this->memcache->set(MEMCACHE_NAMESPACE . $key, $value, MEMCACHE_COMPRESSED, $this->expire);
} else {
$this->delete($key);
$file = DIR_CACHE . 'cache.' . $key . '.' . (time() + $this->expire);
$handle = fopen($file, 'w');
fwrite($handle, serialize($value));
fclose($handle);
};
}
public function delete($key)
{
if ((CACHE_DRIVER == 'memcached') && $this->ismemcache) {
$this->memcache->delete(MEMCACHE_NAMESPACE . $key, 0);
} else {
$files = glob(DIR_CACHE . 'cache.' . $key . '.*');
if ($files) {
foreach ($files as $file) {
if (file_exists($file)) {
@unlink($file);
clearstatcache();
}
}
}
}
}
}
]]></add>

</operation>
</file>
</modification>

Loopback框架入门

安装strongloop框架

1
npm install -g strongloop

创建项目

运行 slc loopback, 提示输入项目信息, 包括项目名称, 目录名称等.

1
$ slc loopback
[?] Enter a directory name where to create the project: longfor
[?] What's the name of your application? longfor

上述过程创建了一个名为longfor的项目, 目录名称也为longfor.

创建模型

进入 longfor 目录执行 slc loopback:model

1
$ cd longfor
$ slc loopback:model
[?] Enter the model name: user
[?] Select the data-source to attach person to: db (memory)
[?] Expose person via the REST API? Yes
[?] Custom plural form (used to build REST URL): people
Let's add some person properties now.

安装数据库连接器

这里的例子, 我们以MySQL作为例子:

1
npm install --save loopback-connector-mysql

添加数据源

数据库连接器安装好了之后, 我们需要为我们的应用添加数据源, 通过下面的命令给应用程序longfor添加数据源, 数据源的名称为longfordb, MySQL作为连接器:

1
$ slc loopback:datasource longfordb
? Enter the data-source name: longfordb
? Select the connector for longfordb: (Use arrow keys)
❯ In-memory db (supported by StrongLoop)
  Email (supported by StrongLoop)
  MySQL (supported by StrongLoop)
  PostgreSQL (supported by StrongLoop)
  Oracle (supported by StrongLoop)
  Microsoft SQL (supported by StrongLoop)
  MongoDB (supported by StrongLoop)

数据源创建好了, 接下来就是配置数据源, 让代码可以通过我们刚才创建的这个数据源名称去和后端数据库建立连接:

配置数据源

数据源的配置文件在目录$PROJECT/server/datasources.json, 修改内容为:

1
{
  ...
  "longfordb": {
    "name": "longfordb",
    "connector": "mysql",
    "host": "localhost",
    "port": 3306,
    "database": "longfordb",
    "username": "root",
    "password": "root"
  }
}

关于模型

关于模型Loopback有几个内置的模型类

  • PersistedModel
    该模型类继承自Model类,额外支持基本查询功能和CRUD操作.

Node.js语义版本解析库

安装语义版本解析库

1
npm install -g semver

Node Shell示例

1
> var semver = require('semver');
undefined
> var version_info = semver.parse('1.2.3');
undefined
> version_info.major
1
> version_info.minor
2
> version_info.patch
3

验证一个版本号是否是语义化版本, 如果是一个语义化版本返回true, 否则返回false

1
semver.valid('1.2.3')

其他语言版本