WSGI
在正式了解Pecan之前,需要介绍WSGI的基本原理:
WSGI
:全称是Web Server Gateway Interface,WSGI不是服务器,python模块,框架,API或者任何软件,只是一种规范,描述web server如何与web application通信的规范。server和application的规范在PEP 3333中有具体描述。要实现WSGI协议,必须同时实现web server和web application,当前运行在WSGI协议之上的web框架有Torando,Flask,Django等
Web应用的本质:
- 浏览器发送一个HTTP请求
- 服务器收到请求,生成一个HTML文档
- 服务器把HTML文档作为HTTP响应的body发给浏览器
- 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示
上面的web应用过程,如果由我们自己来实现是复杂的,涉及到接收HTTP请求,解析HTTP请求,响应HTTP请求等。通常这些操作都由WSGI服务器来完成,WSGI(Web Server Gateway Interface)定义了WSGI服务器执行的接口规范,我们只需要编写符合WSGI规范的接口,然后由WSGI服务器来执行,就可以了。
WSGI接口编写示例:
def application(environ,start_response):
start_response('200 OK',[('Content-Type','text/html')])
return '<h1>Hello,web!</h1>'
上面的application()函数就是符合WSGI标准的一个HTTP处理函数,其中的参数:
environ
:包含HTTP请求信息的dict对象
start_response
:发送HTTP响应的函数
函数说明:start_response(‘200 OK’, [(‘Content-Type’, ‘text/html’)]):发送HTTP响应的Header,Header只能发送一次,意思是start_response函数只能执行一次。'200 OK’是HTTP响应码参数,[(‘Content-Type’, ‘text/html’)]表示HTTP Header,函数的返回值作为HTTP响应body发送给服务器。
为什么要使用Pecan?Pecan是一个路由对象分发的轻量级Python web框架。本质上可以将url通过分割为每一部分,然后对每一部分查找对应处理该URL部分的处理类,处理后,继续交给后面部分的URL处理,直到所有URL部分都被处理后,调用最后分割的URL对应的处理函数处理。
Pecan
以下内容翻译自Pecan官方教程:https://pecan.readthedocs.io/en/latest/
欢迎使用Pecan,这是一个受CherryPy,TurboGears和Pylons启发的轻量级Python Web框架。 Pecan最初由ShootQ的开发人员在Pictage工作时创建。
Pecan的创建填补了Python网络框架世界中的空白——因为它是一个非常轻巧的框架,它提供了对象分派(object-dispatch)样式路由。 Pecan并非旨在成为“全栈”框架,因此不提供对会话或数据库之类的开箱即用的支持(尽管一些教程包含了这些内容,使用者可以自己将其整合到几行代码中)。Pecan相反地专注于HTTP本身。
虽然它很轻巧,但是Pecan提供了广泛的功能集,可以将其用于构建基于HTTP的应用程序,包括:
- 对路由基于对象分派策略
- 全面支持REST风格的控制器
- 可扩展的安全框架
- 可扩展的模板语言支持
- 可扩展的JSON支持
- 简单的基于Python的配置
框架原理分析
1. 原理
Pecan是一个路由对象分发的python web框架。本质上可以将url通过分割为每一部分,然后对每一部分查找对应处理该URL部分的处理类,处理后,继续交给后面部分的URL处理,直到所有URL部分都被处理后,调用最后分割的URL对应的处理函数处理。
2. 处理流程
代码位于Pecan的core.py中。
1. 当一个请求从wsgiserver转发过来
当一个请求从wsgiserver转发过来,首先处理的是Pecan中的__call__方法。主要调用了find_controller和invoke_controller方法。find_controller根据对象分发机制找到url的处理方法,如果没找到,则抛出异常,由后面的except代码块处理,找到了就调用invoke_controller执行该处理方法,将处理结果保存到state中。
def __call__(self, environ, start_response):
'''
Implements the WSGI specification for Pecan applications, utilizing
``WebOb``.
'''
# create the request and response object
req = self.request_cls(environ)
resp = self.response_cls()
state = RoutingState(req, resp, self)
environ['pecan.locals'] = {
'request': req,
'response': resp
}
controller = None
# track internal redirects
internal_redirect = False
# handle the request
try:
# add context and environment to the request
req.context = environ.get('pecan.recursive.context', {})
req.pecan = dict(content_type=None)
controller, args, kwargs = self.find_controller(state)
self.invoke_controller(controller, args, kwargs, state)
except Exception as e:
# if this is an HTTP Exception, set it as the response
if isinstance(e, exc.HTTPException):
# if the client asked for JSON, do our best to provide it
accept_header = acceptparse.create_accept_header(
getattr(req.accept, 'header_value', '*/*') or '*/*')
offers = accept_header.acceptable_offers(
('text/plain', 'text/html', 'application/json'))
best_match = offers[0][0] if offers else None
state.response = e
if best_match == 'application/json':
json_body = dumps({
'code': e.status_int,
'title': e.title,
'description': e.detail
})
if isinstance(json_body, six.text_type):
e.text = json_body
else:
e.body = json_body
state.response.content_type = best_match
environ['pecan.original_exception'] = e
# note if this is an internal redirect
internal_redirect = isinstance(e, ForwardRequestException)
# if this is not an internal redirect, run error hooks
on_error_result = None
if not internal_redirect:
on_error_result = self.handle_hooks(
self.determine_hooks(state.controller),
'on_error',
state,
e
)
# if the on_error handler returned a Response, use it.
if isinstance(on_error_result, WebObResponse):
state.response = on_error_result
else:
if not isinstance(e, exc.HTTPException):
raise
# if this is an HTTP 405, attempt to specify an Allow header
if isinstance(e, exc.HTTPMethodNotAllowed) and controller:
allowed_methods = _cfg(controller).get('allowed_methods', [])
if allowed_methods:
state.response.allow = sorted(allowed_methods)
finally:
# if this is not an internal redirect, run "after" hooks
if not internal_redirect:
self.handle_hooks(
self.determine_hooks(state.controller),
'after',
state
)
self._handle_empty_response_body(state)
# get the response
return state.response(environ, start_response)
2. find_controller方法中主要调用了route方法
def find_controller(self, state):
'''
The main request handler for Pecan applications.
'''
# get a sorted list of hooks, by priority (no controller hooks yet)
req = state.request
pecan_state = req.pecan
# store the routing path for the current application to allow hooks to
# modify it
pecan_state['routing_path'] = path = req.path_info
# handle "on_route" hooks
self.handle_hooks(self.hooks, 'on_route', state)
# lookup the controller, respecting content-type as requested
# by the file extension on the URI
pecan_state['extension'] = None
# attempt to guess the content type based on the file extension
if self.guess_content_type_from_ext \
and not pecan_state['content_type'] \
and '.' in path:
_, extension = splitext(path.rstrip('/'))
# preface with a letter to ensure compat for 2.5
potential_type = guess_type('x' + extension)[0]
if extension and potential_type is not None:
path = ''.join(path.rsplit(extension, 1))
pecan_state['extension'] = extension
pecan_state['content_type'] = potential_type
controller, remainder = self.route(req, self.root, path)
cfg = _cfg(controller)
if cfg.get('generic_handler'):
raise exc.HTTPNotFound
# handle generic controllers
im_self = None
if cfg.get('generic'):
im_self = six.get_method_self(controller)
handlers = cfg['generic_handlers']
controller = handlers.get(req.method, handlers['DEFAULT'])
handle_security(controller, im_self)
cfg = _cfg(controller)
# add the controller to the state so that hooks can use it
state.controller = controller
# if unsure ask the controller for the default content type
content_types = cfg.get('content_types', {})
if not pecan_state['content_type']:
# attempt to find a best match based on accept headers (if they
# exist)
accept = getattr(req.accept, 'header_value', '*/*') or '*/*'
if accept == '*/*' or (
accept.startswith('text/html,') and
list(content_types.keys()) in self.SIMPLEST_CONTENT_TYPES):
pecan_state['content_type'] = cfg.get(
'content_type',
'text/html'
)
else:
best_default = None
accept_header = acceptparse.create_accept_header(accept)
offers = accept_header.acceptable_offers(
list(content_types.keys())
)
if offers:
# If content type matches exactly use matched type
best_default = offers[0][0]
else:
# If content type doesn't match exactly see if something
# matches when not using parameters
for k in content_types.keys():
if accept.startswith(k):
best_default = k
break
if best_default is None:
msg = "Controller '%s' defined does not support " + \
"content_type '%s'. Supported type(s): %s"
logger.error(
msg % (
controller.__name__,
pecan_state['content_type'],
content_types.keys()
)
)
raise exc.HTTPNotAcceptable()
pecan_state['content_type'] = best_default
elif cfg.get('content_type') is not None and \
pecan_state['content_type'] not in content_types:
msg = "Controller '%s' defined does not support content_type " + \
"'%s'. Supported type(s): %s"
logger.error(
msg % (
controller.__name__,
pecan_state['content_type'],
content_types.keys()
)
)
raise exc.HTTPNotFound
# fetch any parameters
if req.method == 'GET':
params = req.GET
elif req.content_type in ('application/json',
'application/javascript'):
try:
if not isinstance(req.json, dict):
raise TypeError('%s is not a dict' % req.json)
params = NestedMultiDict(req.GET, req.json)
except (TypeError, ValueError):
params = req.params
else:
params = req.params
# fetch the arguments for the controller
args, varargs, kwargs = self.get_args(
state,
params.mixed(),
remainder,
cfg['argspec'],
im_self
)
state.arguments = Arguments(args, varargs, kwargs)
# handle "before" hooks
self.handle_hooks(self.determine_hooks(controller), 'before', state)
return controller, args + varargs, kwargs
self.route(req, self.root, path)
- req:WebOb的Request对象,存储了请求的信息
- self.root:是第一个处理对象(config.py中定义的root对象)
- path:路径信息,如:/v1/books
3. route方法中调用了lookup_controller方法对截取后的路径进行继续处理
lookup_controller针对每一个controller对象,在其中查找对应的处理方法,如果没找到,则会继续找_default,如果没定义_default,
则找_lookup,然后继续循环调用lookup_controller,直到找到对应的方法,或notfound_handlers 为空抛出异常。
def route(self, req, node, path):
'''
Looks up a controller from a node based upon the specified path.
:param node: The node, such as a root controller object.
:param path: The path to look up on this node.
'''
path = path.split('/')[1:]
try:
node, remainder = lookup_controller(node, path, req)
return node, remainder
except NonCanonicalPath as e:
if self.force_canonical and \
not _cfg(e.controller).get('accept_noncanonical', False):
if req.method == 'POST':
raise RuntimeError(
"You have POSTed to a URL '%s' which "
"requires a slash. Most browsers will not maintain "
"POST data when redirected. Please update your code "
"to POST to '%s/' or set force_canonical to False" %
(req.pecan['routing_path'],
req.pecan['routing_path'])
)
redirect(code=302, add_slash=True, request=req)
return e.controller, e.remainder
4. lookup_controller方法中调用find_object方法
def lookup_controller(obj, remainder, request=None):
'''
Traverses the requested url path and returns the appropriate controller
object, including default routes.
Handles common errors gracefully.
'''
if request is None:
warnings.warn(
(
"The function signature for %s.lookup_controller is changing "
"in the next version of pecan.\nPlease update to: "
"`lookup_controller(self, obj, remainder, request)`." % (
__name__,
)
),
DeprecationWarning
)
notfound_handlers = []
while True:
try:
obj, remainder = find_object(obj, remainder, notfound_handlers,
request)
handle_security(obj)
return obj, remainder
except (exc.HTTPNotFound, exc.HTTPMethodNotAllowed,
PecanNotFound) as e:
if isinstance(e, PecanNotFound):
e = exc.HTTPNotFound()
while notfound_handlers:
name, obj, remainder = notfound_handlers.pop()
if name == '_default':
# Notfound handler is, in fact, a controller, so stop
# traversal
return obj, remainder
else:
# Notfound handler is an internal redirect, so continue
# traversal
result = handle_lookup_traversal(obj, remainder)
if result:
# If no arguments are passed to the _lookup, yet the
# argspec requires at least one, raise a 404
if (
remainder == [''] and
len(obj._pecan['argspec'].args) > 1
):
raise e
obj_, remainder_ = result
return lookup_controller(obj_, remainder_, request)
else:
raise e
obj, remainder = find_object(obj, remainder, notfound_handlers, request)具体查找:
obj:当前的controller对象
remainder:路由信息,如[‘v1’, ‘books’]
notfound_handlers:该controller中没找到时,存储_default或者_lookup
request:请求信息
5. find_object方法
find_object 首先会处理自定义的路由信息,然后存储_default和_lookup,最后处理默认路由。
def find_object(obj, remainder, notfound_handlers, request):
'''
'Walks' the url path in search of an action for which a controller is
implemented and returns that controller object along with what's left
of the remainder.
'''
prev_obj = None
while True:
if obj is None:
raise PecanNotFound
if iscontroller(obj):
if getattr(obj, 'custom_route', None) is None:
return obj, remainder
_detect_custom_path_segments(obj)
if remainder:
custom_route = __custom_routes__.get((obj.__class__, remainder[0]))
if custom_route:
return getattr(obj, custom_route), remainder[1:]
# are we traversing to another controller
cross_boundary(prev_obj, obj)
try:
next_obj, rest = remainder[0], remainder[1:]
if next_obj == '':
index = getattr(obj, 'index', None)
if iscontroller(index):
return index, rest
except IndexError:
# the URL has hit an index method without a trailing slash
index = getattr(obj, 'index', None)
if iscontroller(index):
raise NonCanonicalPath(index, [])
default = getattr(obj, '_default', None)
if iscontroller(default):
notfound_handlers.append(('_default', default, remainder))
lookup = getattr(obj, '_lookup', None)
if iscontroller(lookup):
notfound_handlers.append(('_lookup', lookup, remainder))
route = getattr(obj, '_route', None)
if iscontroller(route):
if len(getargspec(route).args) == 2:
warnings.warn(
(
"The function signature for %s.%s._route is changing "
"in the next version of pecan.\nPlease update to: "
"`def _route(self, args, request)`." % (
obj.__class__.__module__,
obj.__class__.__name__
)
),
DeprecationWarning
)
next_obj, next_remainder = route(remainder)
else:
next_obj, next_remainder = route(remainder, request)
cross_boundary(route, next_obj)
return next_obj, next_remainder
if not remainder:
raise PecanNotFound
prev_remainder = remainder
prev_obj = obj
remainder = rest
try:
obj = getattr(obj, next_obj, None)
except UnicodeEncodeError:
obj = None
# Last-ditch effort: if there's not a matching subcontroller, no
# `_default`, no `_lookup`, and no `_route`, look to see if there's
# an `index` that has a generic method defined for the current request
# method.
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder
routing.py中的lookup_controller 和 find_object是核心路由方式的实现,从代码中可以看出,最终找到处理方法的方式是根据路径(/v1/books)中每一个segment来查找对应的对象,然后根据当前对象再查找下一个对象,所以pecan的路由机制叫做对象分发。
3. expose()装饰器
标识了这个被装饰的方法可以被路由找到。在routing.py中find_object方法会返回找到的subcontroller,它是有@expose装饰的一个方法
4. 小结
首先,Pecan库根据配置文件中配置的主入口Controller类获取到请求,然后对url分割,查找对应的Controller对象,经过处理后,根据default或者lookup方法,把除了当前路径外剩余的路径给当前Controller下一个处理的Controller对象处理,直到所有URL都被处理。
使用Pecan
1. 安装Pecan
创建虚拟环境并激活:
$ virtualenv pecan-env
$ cd pecan-env
$ source bin/activate
使用pip安装pecan:
$ pip install pecan
2.创建第一个Pecan应用
- 对于一个新的Pecan应用,Pecan框架包含一个基本的模板,这里创建一个名为test_project的Pecan项目
$ pecan create pecandemo
$ cd pecandemo
如果想要在“开发模式”下部署它,这样就可以“sys.path”就可以用了:
$ python setup.py develop
新的项目包含的文件如下:
$ ls
├── MANIFEST.in
├── config.py
├── public # 静态文件,包括CSS 、JS、images
│ ├── css
│ │ └── style.css
│ └── images
├── setup.cfg
├── setup.py
└── test_project # 基于MVC模型生成的结构
├── __init__.py
├── app.py # 决定应用是如何创造的,必须包含set_app()并返回WSGI应用程序,一般不变
├── controllers # 控制层实现
│ ├── __init__.py
│ └── root.py
├── model # 模型实现
│ └── __init__.py # 在这里可以加入与database交互,定义表和ORM等
├── templates # 模板实现
│ ├── error.html
│ ├── index.html
│ └── layout.html
└── tests # 单元测试
├── __init__.py
├── config.py
├── test_functional.py
└── test_units.py
可能由于不同版本文件有所不同,但是基本结构不变。
2. 文件描述:
- public:存放所有静态文件,例如CSS、Javascript和images。
Pecan应用程序结构通常遵循MVC模式。项目下的目录包含模型、控制器和模板。
- test_project/controllers:控制器文件的容器目录
- test_project/templates:模板文件
- test_project/model:控制器文件的容器目录
- test_project/tests:测试脚本目录
pecandemo/app.py文件控制如何创建Pecan应用程序。此文件必须包含返回WSGI应用程序对象的setup_app()函数。通常不需要修改。
为了避免不必要的依赖关系并尽可能保持灵活性,Pecan不强制任何数据库或ORM(Object-Relational Mapper)。如果您的项目将与数据库交互,则可以将代码添加到model/_init_.py以从配置文件加载数据库绑定,并定义表和ORM定义。
- 运行Pecan应用
运行Pecan应用程序需要配置文件config.py. 此文件包括运行服务器的主机和端口、控制器和模板在磁盘上的存储位置,以及包含任何静态文件的目录的名称。
运行Pecan的命令如下:
$ pecan serve config.py
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
3. Python-Based配置
Pecan的的配置文件是一个纯Python语言脚本,默认的config.py的文件内容如下:
# Server Specific Configurations
server = {
'port': '8080',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': 'pecandemo.controllers.root.RootController', # RootController所在的路径
'modules': ['pecandemo'], # 其中包含的python包,是app.py所在的包,即setup_app方法所在的包
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/pecandemo/templates',
'debug': True, # 是否开启调式
'errors': {
404: '/error/404',
'__force_dict__': True
}
}
logging = {
'root': {'level': 'INFO', 'handlers': ['console']},
'loggers': {
'pecandemo': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False},
'py.warnings': {'handlers': ['console']},
'__force_dict__': True
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'color'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
},
'color': {
'()': 'pecan.log.ColorFormatter',
'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]'
'[%(threadName)s] %(message)s'),
'__force_dict__': True
}
}
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf
4. Pecan的根控制器
Root Controller是Pecan应用的单一入口,可以认为它类似于应用程序的根URL路径。在上面的例子中为http://localhost:8080/
该文件的主要内容示例如下:
from pecan import expose
from webob.exc import status_map
class RootController(object):
@expose(generic=True, template='index.html')
def index(self):
return dict()
@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)
@expose('error.html')
def error(self, status):
try:
status = int(status)
except ValueError:
status = 0
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)
如果需要,可以指定其他类和方法,但现在,让我们逐个解释示例中的每一项:
@expose(generic=True, template='index.html')
def index(self):
return dict()
这里的index方法名是不能改的,routing的时候会查找有没有这个方法,而且必须使用装饰器 @pecan.expose()
注意:有些时候会把@pecan.expose**()**写成@pecan.expose,没有带括号。一旦运行起来,界面上就报:“A server error occurred. Please contact the administrator.”,控制台会打出“AttributeError: ‘instancemethod’ object has no attribute ‘_pecan’”。这就是没用到装饰器,一些属性没有加上。装饰器使用时带不带括号是有区别的。
5. 对象分发式路由
示例如下:
class v1Controller(rest.RestController):
@pecan.expose()
def get(self):
return 'This is v1Controller GET.'
class RootController(rest.RestController):
v1 = v1Controller()
在RootController类中,生成了一个v1Controller对象,这个对象就是用来做分发式路由的,因此可以使用如下命令去调用它:
curl -X GET http://127.0.0.1:8080/v1
使用WSME来规范API的响应值
Pecan可以比较好的处理HTTP请求中的参数以及控制HTTP返回值。那么为什么我们还需要WSME呢?因为Pecan在做下面这个事情的时候比较麻烦:请求参数和响应内容的类型检查(英文简称就是typing)。当然,做是可以做的,不过你需要自己访问pecan.request和pecan.response,然后检查指定的值的类型。WSME就是为解决这个问题而生的,而且适用场景就是RESTful API。
1. WSME简介
WSME的全称是Web Service Made Easy,是专门用于实现REST服务的typing库,让你不需要直接操作请求和响应,而且刚好和Pecan结合得非常好,所以OpenStack的很多项目都使用了Pecan + WSME的组合来实现API。
WSME的理念是:在大部分情况下,Web服务的输入和输出对数据类型的要求都是严格的。所以它就专门解决了这个事情,然后把其他事情都交给其他框架去实现。因此,一般WSME都是和其他框架配合使用的,支持Pecan、Flask等。
WSME的文档地址是http://wsme.readthedocs.org/en/latest/index.html。
2. WSME的使用
用了WSME后的好处是什么呢?WSME会自动帮你检查HTTP请求和响应中的数据是否符合预先设定好的要求。
WSME的主要方式是通过装饰器来控制controller方法的输入和输出。
WSME中主要使用两个控制器:
- @signature: 这个装饰器用来描述一个函数的输入和输出。
- @wsexpose: 这个装饰器包含@signature的功能,同时会把函数的路由信息暴露给Web框架,效果就像Pecan的expose装饰器。
这里我们结合Pecan来讲解WSME的使用。先来看一个原始类型的例子:
from wsmeext.pecan import wsexpose
class RootController(rest.RestController):
_custom_actions = {
'test': ['GET'],
}
@wsexpose(int, int)
def test(self, number):
return number
如果不提供参数,访问会失败:
$ curl http://localhost:8080/test
{"debuginfo": null, "faultcode": "Client", "faultstring": "Missing argument: \"number\""}%
如果提供的参数不是整型,访问也会失败:
$ curl http://localhost:8080/test\?number\=a
{"debuginfo": null, "faultcode": "Client", "faultstring": "Invalid input for field/attribute number. Value: 'a'. unable to convert to int"}%
上面这些错误信息都是由WSME框架直接返回的,还没有执行到你写的方法。
如果请求正确,那么会是这样的:
$ curl -v http://localhost:8080/test\?number\=1
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test?number=1 HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Wed, 16 Sep 2015 15:06:35 GMT
< Server: WSGIServer/0.1 Python/2.7.9
< Content-Length: 1
< Content-Type: application/json; charset=UTF-8
<
* Closing connection 0
1%
请注意返回的content-type,这里返回JSON是因为我们使用的wsexpose设置的返回类型是XML和JSON,并且JSON是默认值。上面这个例子就是WSME最简单的应用了。
那么现在有下面这些问题需要思考一下:
- 如果想用POST的方式来传递参数,要怎么做呢?提示:要阅读WSME中@signature装饰器的文档。
- 如果我希望使用/test/1这种方式来传递参数要怎么做呢?提示:要阅读Pecan文档中关于路由的部分。
- WSME中支持对哪些类型的检查呢?WSME支持整型、浮点型、字符串、布尔型、日期时间等,甚至还支持用户自定义类型。提示:要阅读WSME文档中关于类型的部分。
- WSME支持数组类型么?支持。
参考资料
- https://blog.youkuaiyun.com/qq527631128/article/details/90245555
- https://blog.youkuaiyun.com/meisanggou/article/details/88559446
- https://blog.youkuaiyun.com/warrior_0319/article/details/86611883