51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

楼主: 八戒你干嘛
打印 上一主题 下一主题

[python] 【转】Python教程

[复制链接]

该用户从未签到

81#
 楼主| 发表于 2017-7-7 09:49:23 | 只看该作者
Day 8 - 构建前端[转]
虽然我们跑通了一个最简单的MVC,但是页面效果肯定不会让人满意。
对于复杂的HTML前端页面来说,我们需要一套基础的CSS框架来完成页面布局和基本样式。另外,jQuery作为操作DOM的JavaScript库也必不可少。
从零开始写CSS不如直接从一个已有的功能完善的CSS框架开始。有很多CSS框架可供选择。我们这次选择uikit这个强大的CSS框架。它具备完善的响应式布局,漂亮的UI,以及丰富的HTML组件,让我们能轻松设计出美观而简洁的页面。
可以从uikit首页下载打包的资源文件。
所有的静态资源文件我们统一放到www/static目录下,并按照类别归类:
static/+- css/|  +- addons/|  |  +- uikit.addons.min.css|  |  +- uikit.almost-flat.addons.min.css|  |  +- uikit.gradient.addons.min.css|  +- awesome.css|  +- uikit.almost-flat.addons.min.css|  +- uikit.gradient.addons.min.css|  +- uikit.min.css+- fonts/|  +- fontawesome-webfont.eot|  +- fontawesome-webfont.ttf|  +- fontawesome-webfont.woff|  +- FontAwesome.otf+- js/   +- awesome.js   +- html5.js   +- jquery.min.js   +- uikit.min.js
由于前端页面肯定不止首页一个页面,每个页面都有相同的页眉和页脚。如果每个页面都是独立的HTML模板,那么我们在修改页眉和页脚的时候,就需要把每个模板都改一遍,这显然是没有效率的。
常见的模板引擎已经考虑到了页面上重复的HTML部分的复用问题。有的模板通过include把页面拆成三部分:
<html>    <% include file="inc_header.html" %>    <% include file="index_body.html" %>    <% include file="inc_footer.html" %></html>
这样,相同的部分inc_header.html和inc_footer.html就可以共享。
但是include方法不利于页面整体结构的维护。jinjia2的模板还有另一种“继承”方式,实现模板的复用更简单。
“继承”模板的方式是通过编写一个“父模板”,在父模板中定义一些可替换的block(块)。然后,编写多个“子模板”,每个子模板都可以只替换父模板定义的block。比如,定义一个最简单的父模板:
<!-- base.html --><html>    <head>        <title>{% block title%} 这里定义了一个名为title的block {% endblock %}</title>    </head>    <body>        {% block content %} 这里定义了一个名为content的block {% endblock %}    </body></html>
对于子模板a.html,只需要把父模板的title和content替换掉:
{% extends 'base.html' %}{% block title %} A {% endblock %}{% block content %}    <h1>Chapter A</h1>    <p>blablabla...</p>{% endblock %}
对于子模板b.html,如法炮制:
{% extends 'base.html' %}{% block title %} B {% endblock %}{% block content %}    <h1>Chapter B</h1>    <ul>       <li>list 1</li>       <li>list 2</li>    </ul>{% endblock %}
这样,一旦定义好父模板的整体布局和CSS样式,编写子模板就会非常容易。
让我们通过uikit这个CSS框架来完成父模板__base__.html的编写:
<!DOCTYPE html><html><head>    <meta charset="utf-8" />    {% block meta %}<!-- block meta  -->{% endblock %}    <title>{% block title %} ? {% endblock %} - Awesome Python Webapp</title>    <link rel="stylesheet" href="/static/css/uikit.min.css">    <link rel="stylesheet" href="/static/css/uikit.gradient.min.css">    <link rel="stylesheet" href="/static/css/awesome.css" />    <script src="/static/js/jquery.min.js"></script>    <script src="/static/js/md5.js"></script>    <script src="/static/js/uikit.min.js"></script>    <script src="/static/js/awesome.js"></script>    {% block beforehead %}<!-- before head  -->{% endblock %}</head><body>    <nav class="uk-navbar uk-navbar-attached uk-margin-bottom">        <div class="uk-container uk-container-center">            <a href="/" class="uk-navbar-brand">Awesome</a>            <ul class="uk-navbar-nav">                <li data-url="blogs"><a href="/"><i class="uk-icon-home"></i> 日志</a></li>                <li><a target="_blank" href="#"><i class="uk-icon-book"></i> 教程</a></li>                <li><a target="_blank" href="#"><i class="uk-icon-code"></i> 源码</a></li>            </ul>            <div class="uk-navbar-flip">                <ul class="uk-navbar-nav">                {% if user %}                    <li class="uk-parent" data-uk-dropdown>                        <a href="#0"><i class="uk-icon-user"></i> {{ user.name }}</a>                        <div class="uk-dropdown uk-dropdown-navbar">                            <ul class="uk-nav uk-nav-navbar">                                <li><a href="/signout"><i class="uk-icon-sign-out"></i> 登出</a></li>                            </ul>                        </div>                    </li>                {% else %}                    <li><a href="/signin"><i class="uk-icon-sign-in"></i> 登陆</a></li>                    <li><a href="/register"><i class="uk-icon-edit"></i> 注册</a></li>                {% endif %}                </ul>            </div>        </div>    </nav>    <div class="uk-container uk-container-center">        <div class="uk-grid">            <!-- content -->            {% block content %}            {% endblock %}            <!-- // content -->        </div>    </div>    <div class="uk-margin-large-top" style="background-color:#eee; border-top:1px solid #ccc;">        <div class="uk-container uk-container-center uk-text-center">            <div class="uk-panel uk-margin-top uk-margin-bottom">                <p>                    <a target="_blank" href="#" class="uk-icon-button uk-icon-weibo"></a>                    <a target="_blank" href="#" class="uk-icon-button uk-icon-github"></a>                    <a target="_blank" href="#" class="uk-icon-button uk-icon-linkedin-square"></a>                    <a target="_blank" href="#" class="uk-icon-button uk-icon-twitter"></a>                </p>                <p>Powered by <a href="#">Awesome Python Webapp</a>. Copyright &copy; 2014. [<a href="/manage/" target="_blank">Manage</a>]</p>                <p><a href="http://www.liaoxuefeng.com/" target="_blank">www.liaoxuefeng.com</a>. All rights reserved.</p>                <a target="_blank" href="#"><i class="uk-icon-html5" style="font-size:64px; color: #444;"></i></a>            </div>        </div>    </div></body></html>


回复 支持 反对

使用道具 举报

该用户从未签到

82#
 楼主| 发表于 2017-7-7 09:50:03 | 只看该作者
__base__.html定义的几个block作用如下:
用于子页面定义一些meta,例如rss feed:
{% block meta %} ... {% endblock %}
覆盖页面的标题:
{% block title %} ... {% endblock %}
子页面可以在<head>标签关闭前插入JavaScript代码:
{% block beforehead %} ... {% endblock %}
子页面的content布局和内容:
{% block content %}    ...{% endblock %}
我们把首页改造一下,从__base__.html继承一个blogs.html:
{% extends '__base__.html' %}{% block title %}日志{% endblock %}{% block content %}    <div class="uk-width-medium-3-4">        {% for blog in blogs %}            <article class="uk-article">                <h2><a href="/blog/{{ blog.id }}">{{ blog.name }}</a></h2>                <p class="uk-article-meta">发表于{{ blog.created_at}}</p>                <p>{{ blog.summary }}</p>                <p><a href="/blog/{{ blog.id }}">继续阅读 <i class="uk-icon-angle-double-right"></i></a></p>            </article>            <hr class="uk-article-divider">        {% endfor %}    </div>    <div class="uk-width-medium-1-4">        <div class="uk-panel uk-panel-header">            <h3 class="uk-panel-title">友情链接</h3>            <ul class="uk-list uk-list-line">                <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">编程</a></li>                <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">读书</a></li>                <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">Python教程</a></li>                <li><i class="uk-icon-thumbs-o-up"></i> <a target="_blank" href="#">Git教程</a></li>            </ul>        </div>    </div>{% endblock %}
相应地,首页URL的处理函数更新如下:
@get('/')def index(request):    summary = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'    blogs = [        Blog(id='1', name='Test Blog', summary=summary, created_at=time.time()-120),        Blog(id='2', name='Something New', summary=summary, created_at=time.time()-3600),        Blog(id='3', name='Learn Swift', summary=summary, created_at=time.time()-7200)    ]    return {        '__template__': 'blogs.html',        'blogs': blogs    }
Blog的创建日期显示的是一个浮点数,因为它是由这段模板渲染出来的:
<p class="uk-article-meta">发表于{{ blog.created_at }}</p>
解决方法是通过jinja2的filter(过滤器),把一个浮点数转换成日期字符串。我们来编写一个datetime的filter,在模板里用法如下:
<p class="uk-article-meta">发表于{{ blog.created_at|datetime }}</p>
filter需要在初始化jinja2时设置。相关代码如下:
def datetime_filter(t):    delta = int(time.time() - t)    if delta < 60:        return '1分钟前'    if delta < 3600:        return '%s分钟前' % (delta // 60)    if delta < 86400:        return '%s小时前' % (delta // 3600)    if delta < 604800:        return '%s天前' % (delta // 86400)    dt = datetime.fromtimestamp(t)    return '%s年%s月%s日' % (dt.year, dt.month, dt.day)...init_jinja2(app, filters=dict(datetime=datetime_filter))...
现在,完善的首页显示如下:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

83#
 楼主| 发表于 2017-7-7 09:52:08 | 只看该作者
Day 9 - 编写API[转]
自从Roy Fielding博士在2000年他的博士论文中提出REST(Representational State Transfer)风格的软件架构模式后,REST就基本上迅速取代了复杂而笨重的SOAP,成为Web API的标准了。
什么是Web API呢?
如果我们想要获取一篇Blog,输入http://localhost:9000/blog/123,就可以看到id为123的Blog页面,但这个结果是HTML页面,它同时混合包含了Blog的数据和Blog的展示两个部分。对于用户来说,阅读起来没有问题,但是,如果机器读取,就很难从HTML中解析出Blog的数据。
如果一个URL返回的不是HTML,而是机器能直接解析的数据,这个URL就可以看成是一个Web API。比如,读取http://localhost:9000/api/blogs/123,如果能直接返回Blog的数据,那么机器就可以直接读取。
REST就是一种设计API的模式。最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。
编写API有什么好处呢?由于API就是把Web App的功能全部封装了,所以,通过API操作数据,可以极大地把前端和后端的代码隔离,使得后端代码易于测试,前端代码编写更简单。
一个API也是一个URL的处理函数,我们希望能直接通过一个@api来把函数变成JSON格式的REST API,这样,获取注册用户可以用一个API实现如下:
@get('/api/users')def api_get_users(*, page='1'):    page_index = get_page_index(page)    num = yield from User.findNumber('count(id)')    p = Page(num, page_index)    if num == 0:        return dict(page=p, users=())    users = yield from User.findAll(orderBy='created_at desc', limit=(p.offset, p.limit))    for u in users:        u.passwd = '******'    return dict(page=p, users=users)
只要返回一个dict,后续的response这个middleware就可以把结果序列化为JSON并返回。
我们需要对Error进行处理,因此定义一个APIError,这种Error是指API调用时发生了逻辑错误(比如用户不存在),其他的Error视为Bug,返回的错误代码为internalerror。
客户端调用API时,必须通过错误代码来区分API调用是否成功。错误代码是用来告诉调用者出错的原因。很多API用一个整数表示错误码,这种方式很难维护错误码,客户端拿到错误码还需要查表得知错误信息。更好的方式是用字符串表示错误代码,不需要看文档也能猜到错误原因。
可以在浏览器直接测试API,例如,输入http://localhost:9000/api/users,就可以看到返回的JSON:


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

84#
 楼主| 发表于 2017-7-7 09:57:19 | 只看该作者
Day 10 - 用户注册和登录[转]
用户管理是绝大部分Web网站都需要解决的问题。用户管理涉及到用户注册和登录。
用户注册相对简单,我们可以先通过API把用户注册这个功能实现了:
_RE_EMAIL = re.compile(r'^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$')_RE_SHA1 = re.compile(r'^[0-9a-f]{40}$')@post('/api/users')def api_register_user(*, email, name, passwd):    if not name or not name.strip():        raise APIValueError('name')    if not email or not _RE_EMAIL.match(email):        raise APIValueError('email')    if not passwd or not _RE_SHA1.match(passwd):        raise APIValueError('passwd')    users = yield from User.findAll('email=?', [email])    if len(users) > 0:        raise APIError('register:failed', 'email', 'Email is already in use.')    uid = next_id()    sha1_passwd = '%s:%s' % (uid, passwd)    user = User(id=uid, name=name.strip(), email=email, passwd=hashlib.sha1(sha1_passwd.encode('utf-8')).hexdigest(), image='http://www.gravatar.com/avatar/%s?d=mm&s=120' % hashlib.md5(email.encode('utf-8')).hexdigest())    yield from user.save()    # make session cookie:    r = web.Response()    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)    user.passwd = '******'    r.content_type = 'application/json'    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')    return r
注意用户口令是客户端传递的经过SHA1计算后的40位Hash字符串,所以服务器端并不知道用户的原始口令。
接下来可以创建一个注册页面,让用户填写注册表单,然后,提交数据到注册用户的API:
{% extends '__base__.html' %}{% block title %}注册{% endblock %}{% block beforehead %}<script>function validateEmail(email) {    var re = /^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$/;    return re.test(email.toLowerCase());}$(function () {    var vm = new Vue({        el: '#vm',        data: {            name: '',            email: '',            password1: '',            password2: ''        },        methods: {            submit: function (event) {                event.preventDefault();                var $form = $('#vm');                if (! this.name.trim()) {                    return $form.showFormError('请输入名字');                }                if (! validateEmail(this.email.trim().toLowerCase())) {                    return $form.showFormError('请输入正确的Email地址');                }                if (this.password1.length < 6) {                    return $form.showFormError('口令长度至少为6个字符');                }                if (this.password1 !== this.password2) {                    return $form.showFormError('两次输入的口令不一致');                }                var email = this.email.trim().toLowerCase();                $form.postJSON('/api/users', {                    name: this.name.trim(),                    email: email,                    passwd: CryptoJS.SHA1(email + ':' + this.password1).toString()                }, function (err, r) {                    if (err) {                        return $form.showFormError(err);                    }                    return location.assign('/');                });            }        }    });    $('#vm').show();});</script>{% endblock %}{% block content %}    <div class="uk-width-2-3">        <h1>欢迎注册!</h1>        <form id="vm" v-on="submit: submit" class="uk-form uk-form-stacked">            <div class="uk-alert uk-alert-danger uk-hidden"></div>            <div class="uk-form-row">                <label class="uk-form-label">名字:</label>                <div class="uk-form-controls">                    <input v-model="name" type="text" maxlength="50" placeholder="名字" class="uk-width-1-1">                </div>            </div>            <div class="uk-form-row">                <label class="uk-form-label">电子邮件:</label>                <div class="uk-form-controls">                    <input v-model="email" type="text" maxlength="50" placeholder="your-name@example.com" class="uk-width-1-1">                </div>            </div>            <div class="uk-form-row">                <label class="uk-form-label">输入口令:</label>                <div class="uk-form-controls">                    <input v-model="password1" type="password" maxlength="50" placeholder="输入口令" class="uk-width-1-1">                </div>            </div>            <div class="uk-form-row">                <label class="uk-form-label">重复口令:</label>                <div class="uk-form-controls">                    <input v-model="password2" type="password" maxlength="50" placeholder="重复口令" class="uk-width-1-1">                </div>            </div>            <div class="uk-form-row">                <button type="submit" class="uk-button uk-button-primary"><i class="uk-icon-user"></i> 注册</button>            </div>        </form>    </div>{% endblock %}
这样我们就把用户注册的功能完成了:



本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

85#
 楼主| 发表于 2017-7-7 09:57:49 | 只看该作者
用户登录比用户注册复杂。由于HTTP协议是一种无状态协议,而服务器要跟踪用户状态,就只能通过cookie实现。大多数Web框架提供了Session功能来封装保存用户状态的cookie。
Session的优点是简单易用,可以直接从Session中取出用户登录信息。
Session的缺点是服务器需要在内存中维护一个映射表来存储用户登录信息,如果有两台以上服务器,就需要对Session做集群,因此,使用Session的Web App很难扩展。
我们采用直接读取cookie的方式来验证用户登录,每次用户访问任意URL,都会对cookie进行验证,这种方式的好处是保证服务器处理任意的URL都是无状态的,可以扩展到多台服务器。
由于登录成功后是由服务器生成一个cookie发送给浏览器,所以,要保证这个cookie不会被客户端伪造出来。
实现防伪造cookie的关键是通过一个单向算法(例如SHA1),举例如下:
当用户输入了正确的口令登录成功后,服务器可以从数据库取到用户的id,并按照如下方式计算出一个字符串:
"用户id" + "过期时间" + SHA1("用户id" + "用户口令" + "过期时间" + "SecretKey")
当浏览器发送cookie到服务器端后,服务器可以拿到的信息包括:
  • 用户id
  • 过期时间
  • SHA1值

如果未到过期时间,服务器就根据用户id查找用户口令,并计算:
SHA1("用户id" + "用户口令" + "过期时间" + "SecretKey")
并与浏览器cookie中的MD5进行比较,如果相等,则说明用户已登录,否则,cookie就是伪造的。
这个算法的关键在于SHA1是一种单向算法,即可以通过原始字符串计算出SHA1结果,但无法通过SHA1结果反推出原始字符串。
所以登录API可以实现如下:
@post('/api/authenticate')def authenticate(*, email, passwd):    if not email:        raise APIValueError('email', 'Invalid email.')    if not passwd:        raise APIValueError('passwd', 'Invalid password.')    users = yield from User.findAll('email=?', [email])    if len(users) == 0:        raise APIValueError('email', 'Email not exist.')    user = users[0]    # check passwd:    sha1 = hashlib.sha1()    sha1.update(user.id.encode('utf-8'))    sha1.update(b':')    sha1.update(passwd.encode('utf-8'))    if user.passwd != sha1.hexdigest():        raise APIValueError('passwd', 'Invalid password.')    # authenticate ok, set cookie:    r = web.Response()    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)    user.passwd = '******'    r.content_type = 'application/json'    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')    return r# 计算加密cookie:def user2cookie(user, max_age):    # build cookie string by: id-expires-sha1    expires = str(int(time.time() + max_age))    s = '%s-%s-%s-%s' % (user.id, user.passwd, expires, _COOKIE_KEY)    L = [user.id, expires, hashlib.sha1(s.encode('utf-8')).hexdigest()]    return '-'.join(L)
对于每个URL处理函数,如果我们都去写解析cookie的代码,那会导致代码重复很多次。
利用middle在处理URL之前,把cookie解析出来,并将登录用户绑定到request对象上,这样,后续的URL处理函数就可以直接拿到登录用户:
@asyncio.coroutinedef auth_factory(app, handler):    @asyncio.coroutine    def auth(request):        logging.info('check user: %s %s' % (request.method, request.path))        request.__user__ = None        cookie_str = request.cookies.get(COOKIE_NAME)        if cookie_str:            user = yield from cookie2user(cookie_str)            if user:                logging.info('set current user: %s' % user.email)                request.__user__ = user        return (yield from handler(request))    return auth# 解密cookie:@asyncio.coroutinedef cookie2user(cookie_str):    '''    Parse cookie and load user if cookie is valid.    '''    if not cookie_str:        return None    try:        L = cookie_str.split('-')        if len(L) != 3:            return None        uid, expires, sha1 = L        if int(expires) < time.time():            return None        user = yield from User.find(uid)        if user is None:            return None        s = '%s-%s-%s-%s' % (uid, user.passwd, expires, _COOKIE_KEY)        if sha1 != hashlib.sha1(s.encode('utf-8')).hexdigest():            logging.info('invalid sha1')            return None        user.passwd = '******'        return user    except Exception as e:        logging.exception(e)        return None
这样,我们就完成了用户注册和登录的功能。

回复 支持 反对

使用道具 举报

该用户从未签到

86#
 楼主| 发表于 2017-7-7 10:01:58 | 只看该作者
Day 11 - 编写日志创建页[转]
在Web开发中,后端代码写起来其实是相当容易的。
例如,我们编写一个REST API,用于创建一个Blog:
@post('/api/blogs')def api_create_blog(request, *, name, summary, content):    check_admin(request)    if not name or not name.strip():        raise APIValueError('name', 'name cannot be empty.')    if not summary or not summary.strip():        raise APIValueError('summary', 'summary cannot be empty.')    if not content or not content.strip():        raise APIValueError('content', 'content cannot be empty.')    blog = Blog(user_id=request.__user__.id, user_name=request.__user__.name, user_image=request.__user__.image, name=name.strip(), summary=summary.strip(), content=content.strip())    yield from blog.save()    return blog
编写后端Python代码不但很简单,而且非常容易测试,上面的API:api_create_blog()本身只是一个普通函数。
Web开发真正困难的地方在于编写前端页面。前端页面需要混合HTML、CSS和JavaScript,如果对这三者没有深入地掌握,编写的前端页面将很快难以维护。
更大的问题在于,前端页面通常是动态页面,也就是说,前端页面往往是由后端代码生成的。
生成前端页面最早的方式是拼接字符串:
s = '<html><head><title>'    + title    + '</title></head><body>'    + body    + '</body></html>'
显然这种方式完全不具备可维护性。所以有第二种模板方式:
<html><head>    <title>{{ title }}</title></head><body>    {{ body }}</body></html>
ASP、JSP、PHP等都是用这种模板方式生成前端页面。
如果在页面上大量使用JavaScript(事实上大部分页面都会),模板方式仍然会导致JavaScript代码与后端代码绑得非常紧密,以至于难以维护。其根本原因在于负责显示的HTML DOM模型与负责数据和交互的JavaScript代码没有分割清楚。
要编写可维护的前端代码绝非易事。和后端结合的MVC模式已经无法满足复杂页面逻辑的需要了,所以,新的MVVM:Model View ViewModel模式应运而生。
MVVM最早由微软提出来,它借鉴了桌面应用程序的MVC思想,在前端页面中,把Model用纯JavaScript对象表示:
<script>    var blog = {        name: 'hello',        summary: 'this is summary',        content: 'this is content...'    };</script>
View是纯HTML:
<form action="/api/blogs" method="post">    <input name="name">    <input name="summary">    <textarea name="content"></textarea>    <button type="submit">OK</button></form>
由于Model表示数据,View负责显示,两者做到了最大限度的分离。
把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。
ViewModel如何编写?需要用JavaScript编写一个通用的ViewModel,这样,就可以复用整个MVVM模型了。
好消息是已有许多成熟的MVVM框架,例如AngularJS,KnockoutJS等。我们选择Vue这个简单易用的MVVM框架来实现创建Blog的页面templates/manage_blog_edit.html:
{% extends '__base__.html' %}{% block title %}编辑日志{% endblock %}{% block beforehead %}<script>var    ID = '{{ id }}',    action = '{{ action }}';function initVM(blog) {    var vm = new Vue({        el: '#vm',        data: blog,        methods: {            submit: function (event) {                event.preventDefault();                var $form = $('#vm').find('form');                $form.postJSON(action, this.$data, function (err, r) {                    if (err) {                        $form.showFormError(err);                    }                    else {                        return location.assign('/api/blogs/' + r.id);                    }                });            }        }    });    $('#vm').show();}$(function () {    if (ID) {        getJSON('/api/blogs/' + ID, function (err, blog) {            if (err) {                return fatal(err);            }            $('#loading').hide();            initVM(blog);        });    }    else {        $('#loading').hide();        initVM({            name: '',            summary: '',            content: ''        });    }});</script>{% endblock %}{% block content %}    <div class="uk-width-1-1 uk-margin-bottom">        <div class="uk-panel uk-panel-box">            <ul class="uk-breadcrumb">                <li><a href="/manage/comments">评论</a></li>                <li><a href="/manage/blogs">日志</a></li>                <li><a href="/manage/users">用户</a></li>            </ul>        </div>    </div>    <div id="error" class="uk-width-1-1">    </div>    <div id="loading" class="uk-width-1-1 uk-text-center">        <span><i class="uk-icon-spinner uk-icon-medium uk-icon-spin"></i> 正在加载...</span>    </div>    <div id="vm" class="uk-width-2-3">        <form v-on="submit: submit" class="uk-form uk-form-stacked">            <div class="uk-alert uk-alert-danger uk-hidden"></div>            <div class="uk-form-row">                <label class="uk-form-label">标题:</label>                <div class="uk-form-controls">                    <input v-model="name" name="name" type="text" placeholder="标题" class="uk-width-1-1">                </div>            </div>            <div class="uk-form-row">                <label class="uk-form-label">摘要:</label>                <div class="uk-form-controls">                    <textarea v-model="summary" rows="4" name="summary" placeholder="摘要" class="uk-width-1-1" style="resize:none;"></textarea>                </div>            </div>            <div class="uk-form-row">                <label class="uk-form-label">内容:</label>                <div class="uk-form-controls">                    <textarea v-model="content" rows="16" name="content" placeholder="内容" class="uk-width-1-1" style="resize:none;"></textarea>                </div>            </div>            <div class="uk-form-row">                <button type="submit" class="uk-button uk-button-primary"><i class="uk-icon-save"></i> 保存</button>                <a href="/manage/blogs" class="uk-button"><i class="uk-icon-times"></i> 取消</a>            </div>        </form>    </div>{% endblock %}
初始化Vue时,我们指定3个参数:
el:根据选择器查找绑定的View,这里是#vm,就是id为vm的DOM,对应的是一个<div>标签;
data:JavaScript对象表示的Model,我们初始化为{ name: '', summary: '', content: ''};
methods:View可以触发的JavaScript函数,submit就是提交表单时触发的函数。
接下来,我们在<form>标签中,用几个简单的v-model,就可以让Vue把Model和View关联起来:
<!-- input的value和Model的name关联起来了 --><input v-model="name" class="uk-width-1-1">
Form表单通过<form v-on="submit: submit">把提交表单的事件关联到submit方法。


回复 支持 反对

使用道具 举报

该用户从未签到

87#
 楼主| 发表于 2017-7-7 10:02:58 | 只看该作者
需要特别注意的是,在MVVM中,Model和View是双向绑定的。如果我们在Form中修改了文本框的值,可以在Model中立刻拿到新的值。试试在表单中输入文本,然后在Chrome浏览器中打开JavaScript控制台,可以通过vm.name访问单个属性,或者通过vm.$data访问整个Model:
如果我们在JavaScript逻辑中修改了Model,这个修改会立刻反映到View上。试试在JavaScript控制台输入vm.name = 'MVVM简介',可以看到文本框的内容自动被同步了:
双向绑定是MVVM框架最大的作用。借助于MVVM,我们把复杂的显示逻辑交给框架完成。由于后端编写了独立的REST API,所以,前端用AJAX提交表单非常容易,前后端分离得非常彻底。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

88#
 楼主| 发表于 2017-7-7 10:05:15 | 只看该作者
Day 12 - 编写日志列表页[转]
MVVM模式不但可用于Form表单,在复杂的管理页面中也能大显身手。例如,分页显示Blog的功能,我们先把后端代码写出来:
在apis.py中定义一个Page类用于存储分页信息:
class Page(object):    def __init__(self, item_count, page_index=1, page_size=10):        self.item_count = item_count        self.page_size = page_size        self.page_count = item_count // page_size + (1 if item_count % page_size > 0 else 0)        if (item_count == 0) or (page_index > self.page_count):            self.offset = 0            self.limit = 0            self.page_index = 1        else:            self.page_index = page_index            self.offset = self.page_size * (page_index - 1)            self.limit = self.page_size        self.has_next = self.page_index < self.page_count        self.has_previous = self.page_index > 1    def __str__(self):        return 'item_count: %s, page_count: %s, page_index: %s, page_size: %s, offset: %s, limit: %s' % (self.item_count, self.page_count, self.page_index, self.page_size, self.offset, self.limit)    __repr__ = __str__
在handlers.py中实现API:
@get('/api/blogs')def api_blogs(*, page='1'):    page_index = get_page_index(page)    num = yield from Blog.findNumber('count(id)')    p = Page(num, page_index)    if num == 0:        return dict(page=p, blogs=())    blogs = yield from Blog.findAll(orderBy='created_at desc', limit=(p.offset, p.limit))    return dict(page=p, blogs=blogs)
管理页面:
@get('/manage/blogs')def manage_blogs(*, page='1'):    return {        '__template__': 'manage_blogs.html',        'page_index': get_page_index(page)    }
模板页面首先通过API:GET /api/blogs?page=?拿到Model:
{    "page": {        "has_next": true,        "page_index": 1,        "page_count": 2,        "has_previous": false,        "item_count": 12    },    "blogs": [...]}
然后,通过Vue初始化MVVM:
<script>function initVM(data) {    var vm = new Vue({        el: '#vm',        data: {            blogs: data.blogs,            page: data.page        },        methods: {            edit_blog: function (blog) {                location.assign('/manage/blogs/edit?id=' + blog.id);            },            delete_blog: function (blog) {                if (confirm('确认要删除“' + blog.name + '”?删除后不可恢复!')) {                    postJSON('/api/blogs/' + blog.id + '/delete', function (err, r) {                        if (err) {                            return alert(err.message || err.error || err);                        }                        refresh();                    });                }            }        }    });    $('#vm').show();}$(function() {    getJSON('/api/blogs', {        page: {{ page_index }}    }, function (err, results) {        if (err) {            return fatal(err);        }        $('#loading').hide();        initVM(results);    });});</script>
View的容器是#vm,包含一个table,我们用v-repeat可以把Model的数组blogs直接变成多行的<tr>:
<div id="vm" class="uk-width-1-1">    <a href="/manage/blogs/create" class="uk-button uk-button-primary"><i class="uk-icon-plus"></i> 新日志</a>    <table class="uk-table uk-table-hover">        <thead>            <tr>                <th class="uk-width-5-10">标题 / 摘要</th>                <th class="uk-width-2-10">作者</th>                <th class="uk-width-2-10">创建时间</th>                <th class="uk-width-1-10">操作</th>            </tr>        </thead>        <tbody>            <tr v-repeat="blog: blogs" >                <td>                    <a target="_blank" v-attr="href: '/blog/'+blog.id" v-text="blog.name"></a>                </td>                <td>                    <a target="_blank" v-attr="href: '/user/'+blog.user_id" v-text="blog.user_name"></a>                </td>                <td>                    <span v-text="blog.created_at.toDateTime()"></span>                </td>                <td>                    <a href="#0" v-on="click: edit_blog(blog)"><i class="uk-icon-edit"></i>                    <a href="#0" v-on="click: delete_blog(blog)"><i class="uk-icon-trash-o"></i>                </td>            </tr>        </tbody>    </table>    <div v-component="pagination" v-with="page"></div></div>
往Model的blogs数组中增加一个Blog元素,table就神奇地增加了一行;把blogs数组的某个元素删除,table就神奇地减少了一行。所有复杂的Model-View的映射逻辑全部由MVVM框架完成,我们只需要在HTML中写上v-repeat指令,就什么都不用管了。
可以把v-repeat="blog: blogs"看成循环代码,所以,可以在一个<tr>内部引用循环变量blog。v-text和v-attr指令分别用于生成文本和DOM节点属性。
完整的Blog列表页如下:


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

89#
 楼主| 发表于 2017-7-7 10:10:53 | 只看该作者
Day 13 - 提升开发效率[转]
现在,我们已经把一个Web App的框架完全搭建好了,从后端的API到前端的MVVM,流程已经跑通了。
在继续工作前,注意到每次修改Python代码,都必须在命令行先Ctrl-C停止服务器,再重启,改动才能生效。
在开发阶段,每天都要修改、保存几十次代码,每次保存都手动来这么一下非常麻烦,严重地降低了我们的开发效率。有没有办法让服务器检测到代码修改后自动重新加载呢?
Django的开发环境在Debug模式下就可以做到自动重新加载,如果我们编写的服务器也能实现这个功能,就能大大提升开发效率。
可惜的是,Django没把这个功能独立出来,不用Django就享受不到,怎么办?
其实Python本身提供了重新载入模块的功能,但不是所有模块都能被重新载入。另一种思路是检测www目录下的代码改动,一旦有改动,就自动重启服务器。
按照这个思路,我们可以编写一个辅助程序pymonitor.py,让它启动wsgiapp.py,并时刻监控www目录下的代码改动,有改动时,先把当前wsgiapp.py进程杀掉,再重启,就完成了服务器进程的自动重启。
要监控目录文件的变化,我们也无需自己手动定时扫描,Python的第三方库watchdog可以利用操作系统的API来监控目录文件的变化,并发送通知。我们先用pip安装:
$ pip3 install watchdog
利用watchdog接收文件变化的通知,如果是.py文件,就自动重启wsgiapp.py进程。
利用Python自带的subprocess实现进程的启动和终止,并把输入输出重定向到当前进程的输入输出中:
#!/usr/bin/env python3# -*- coding: utf-8 -*-__author__ = 'Michael Liao'import os, sys, time, subprocessfrom watchdog.observers import Observerfrom watchdog.events import FileSystemEventHandlerdef log(s):    print('[Monitor] %s' % s)class MyFileSystemEventHander(FileSystemEventHandler):    def __init__(self, fn):        super(MyFileSystemEventHander, self).__init__()        self.restart = fn    def on_any_event(self, event):        if event.src_path.endswith('.py'):            log('Python source file changed: %s' % event.src_path)            self.restart()command = ['echo', 'ok']process = Nonedef kill_process():    global process    if process:        log('Kill process [%s]...' % process.pid)        process.kill()        process.wait()        log('Process ended with code %s.' % process.returncode)        process = Nonedef start_process():    global process, command    log('Start process %s...' % ' '.join(command))    process = subprocess.Popen(command, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)def restart_process():    kill_process()    start_process()def start_watch(path, callback):    observer = Observer()    observer.schedule(MyFileSystemEventHander(restart_process), path, recursive=True)    observer.start()    log('Watching directory %s...' % path)    start_process()    try:        while True:            time.sleep(0.5)    except KeyboardInterrupt:        observer.stop()    observer.join()if __name__ == '__main__':    argv = sys.argv[1:]    if not argv:        print('Usage: ./pymonitor your-script.py')        exit(0)    if argv[0] != 'python3':        argv.insert(0, 'python3')    command = argv    path = os.path.abspath('.')    start_watch(path, None)
一共70行左右的代码,就实现了Debug模式的自动重新加载。用下面的命令启动服务器:
$ python3 pymonitor.py wsgiapp.py
或者给pymonitor.py加上可执行权限,启动服务器:
$ ./pymonitor.py app.py
在编辑器中打开一个.py文件,修改后保存,看看命令行输出,是不是自动重启了服务器:
$ ./pymonitor.py app.py [Monitor] Watching directory /Users/michael/Github/awesome-python3-webapp/www...[Monitor] Start process python app.py......INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...[Monitor] Python source file changed: /Users/michael/Github/awesome-python-webapp/www/handlers.py[Monitor] Kill process [2747]...[Monitor] Process ended with code -9.[Monitor] Start process python app.py......INFO:root:application (/Users/michael/Github/awesome-python3-webapp/www) will start at 0.0.0.0:9000...
现在,只要一保存代码,就可以刷新浏览器看到效果,大大提升了开发效率。


回复 支持 反对

使用道具 举报

该用户从未签到

90#
 楼主| 发表于 2017-7-7 10:14:22 | 只看该作者
Day 14 - 完成Web App[转]
在Web App框架和基本流程跑通后,剩下的工作全部是体力活了:在Debug开发模式下完成后端所有API、前端所有页面。我们需要做的事情包括:
把当前用户绑定到request上,并对URL/manage/进行拦截,检查当前用户是否是管理员身份:
@asyncio.coroutinedef auth_factory(app, handler):    @asyncio.coroutine    def auth(request):        logging.info('check user: %s %s' % (request.method, request.path))        request.__user__ = None        cookie_str = request.cookies.get(COOKIE_NAME)        if cookie_str:            user = yield from cookie2user(cookie_str)            if user:                logging.info('set current user: %s' % user.email)                request.__user__ = user        if request.path.startswith('/manage/') and (request.__user__ is None or not request.__user__.admin):            return web.HTTPFound('/signin')        return (yield from handler(request))    return auth
后端API包括:
  • 获取日志:GET /api/blogs
  • 创建日志:POST /api/blogs
  • 修改日志:POST /api/blogs/:blog_id
  • 删除日志:POST /api/blogs/:blog_id/delete
  • 获取评论:GET /api/comments
  • 创建评论:POST /api/blogs/:blog_id/comments
  • 删除评论:POST /api/comments/:comment_id/delete
  • 创建新用户:POST /api/users
  • 获取用户:GET /api/users

管理页面包括:
  • 评论列表页:GET /manage/comments
  • 日志列表页:GET /manage/blogs
  • 创建日志页:GET /manage/blogs/create
  • 修改日志页:GET /manage/blogs/
  • 用户列表页:GET /manage/users

用户浏览页面包括:
  • 注册页:GET /register
  • 登录页:GET /signin
  • 注销页:GET /signout
  • 首页:GET /
  • 日志详情页:GET /blog/:blog_id

把所有的功能实现,我们第一个Web App就宣告完成!


回复 支持 反对

使用道具 举报

该用户从未签到

91#
 楼主| 发表于 2017-7-7 10:20:25 | 只看该作者
Day 15 - 部署Web App[转]
作为一个合格的开发者,在本地环境下完成开发还远远不够,我们需要把Web App部署到远程服务器上,这样,广大用户才能访问到网站。
很多做开发的同学把部署这件事情看成是运维同学的工作,这种看法是完全错误的。首先,最近流行DevOps理念,就是说,开发和运维要变成一个整体。其次,运维的难度,其实跟开发质量有很大的关系。代码写得垃圾,运维再好也架不住天天挂掉。最后,DevOps理念需要把运维、监控等功能融入到开发中。你想服务器升级时不中断用户服务?那就得在开发时考虑到这一点。
下面,我们就来把awesome-python3-webapp部署到Linux服务器。
搭建Linux服务器
要部署到Linux,首先得有一台Linux服务器。要在公网上体验的同学,可以在Amazon的AWS申请一台EC2虚拟机(免费使用1年),或者使用国内的一些云服务器,一般都提供Ubuntu Server的镜像。想在本地部署的同学,请安装虚拟机,推荐使用VirtualBox。
我们选择的Linux服务器版本是Ubuntu Server 14.04 LTS,原因是apt太简单了。如果你准备使用其他Linux版本,也没有问题。
Linux安装完成后,请确保ssh服务正在运行,否则,需要通过apt安装:
$ sudo apt-get install openssh-server
有了ssh服务,就可以从本地连接到服务器上。建议把公钥复制到服务器端用户的.ssh/authorized_keys中,这样,就可以通过证书实现无密码连接。
部署方式
利用Python自带的asyncio,我们已经编写了一个异步高性能服务器。但是,我们还需要一个高性能的Web服务器,这里选择Nginx,它可以处理静态资源,同时作为反向代理把动态请求交给Python代码处理。这个模型如下:
Nginx负责分发请求:
在服务器端,我们需要定义好部署的目录结构:
/+- srv/   +- awesome/       <-- Web App根目录      +- www/        <-- 存放Python源码      |  +- static/  <-- 存放静态资源文件      +- log/        <-- 存放log
在服务器上部署,要考虑到新版本如果运行不正常,需要回退到旧版本时怎么办。每次用新的代码覆盖掉旧的文件是不行的,需要一个类似版本控制的机制。由于Linux系统提供了软链接功能,所以,我们把www作为一个软链接,它指向哪个目录,哪个目录就是当前运行的版本:
而Nginx和python代码的配置文件只需要指向www目录即可。
Nginx可以作为服务进程直接启动,但app.py还不行,所以,Supervisor登场!Supervisor是一个管理进程的工具,可以随系统启动而启动服务,它还时刻监控服务进程,如果服务进程意外退出,Supervisor可以自动重启服务。
总结一下我们需要用到的服务有:
  • Nginx:高性能Web服务器+负责反向代理;
  • Supervisor:监控服务进程的工具;
  • MySQL:数据库服务。

在Linux服务器上用apt可以直接安装上述服务:
$ sudo apt-get install nginx supervisor python3 mysql-server
然后,再把我们自己的Web App用到的Python库安装了:
$ sudo pip3 install jinja2 aiomysql aiohttp
在服务器上创建目录/srv/awesome/以及相应的子目录。
在服务器上初始化MySQL数据库,把数据库初始化脚本schema.sql复制到服务器上执行:
$ mysql -u root -p < schema.sql
服务器端准备就绪。
部署
用FTP还是SCP还是rsync复制文件?如果你需要手动复制,用一次两次还行,一天如果部署50次不但慢、效率低,而且容易出错。
正确的部署方式是使用工具配合脚本完成自动化部署。Fabric就是一个自动化部署工具。由于Fabric是用Python 2.x开发的,所以,部署脚本要用Python 2.7来编写,本机还必须安装Python 2.7版本。
要用Fabric部署,需要在本机(是开发机器,不是Linux服务器)安装Fabric:
$ easy_install fabric
Linux服务器上不需要安装Fabric,Fabric使用SSH直接登录服务器并执行部署命令。
下一步是编写部署脚本。Fabric的部署脚本叫fabfile.py,我们把它放到awesome-python-webapp的目录下,与www目录平级:
awesome-python-webapp/+- fabfile.py+- www/+- ...
Fabric的脚本编写很简单,首先导入Fabric的API,设置部署时的变量:
# fabfile.pyimport os, refrom datetime import datetime# 导入Fabric API:from fabric.api import *# 服务器登录用户名:env.user = 'michael'# sudo用户为root:env.sudo_user = 'root'# 服务器地址,可以有多个,依次部署:env.hosts = ['192.168.0.3']# 服务器MySQL用户名和口令:db_user = 'www-data'db_password = 'www-data'
然后,每个Python函数都是一个任务。我们先编写一个打包的任务:
_TAR_FILE = 'dist-awesome.tar.gz'def build():    includes = ['static', 'templates', 'transwarp', 'favicon.ico', '*.py']    excludes = ['test', '.*', '*.pyc', '*.pyo']    local('rm -f dist/%s' % _TAR_FILE)    with lcd(os.path.join(os.path.abspath('.'), 'www')):        cmd = ['tar', '--dereference', '-czvf', '../dist/%s' % _TAR_FILE]        cmd.extend(['--exclude=\'%s\'' % ex for ex in excludes])        cmd.extend(includes)        local(' '.join(cmd))
Fabric提供local('...')来运行本地命令,with lcd(path)可以把当前命令的目录设定为lcd()指定的目录,注意Fabric只能运行命令行命令,Windows下可能需要Cgywin环境。
在awesome-python-webapp目录下运行:
$ fab build
看看是否在dist目录下创建了dist-awesome.tar.gz的文件。
打包后,我们就可以继续编写deploy任务,把打包文件上传至服务器,解压,重置www软链接,重启相关服务:
_REMOTE_TMP_TAR = '/tmp/%s' % _TAR_FILE_REMOTE_BASE_DIR = '/srv/awesome'def deploy():    newdir = 'www-%s' % datetime.now().strftime('%y-%m-%d_%H.%M.%S')    # 删除已有的tar文件:    run('rm -f %s' % _REMOTE_TMP_TAR)    # 上传新的tar文件:    put('dist/%s' % _TAR_FILE, _REMOTE_TMP_TAR)    # 创建新目录:    with cd(_REMOTE_BASE_DIR):        sudo('mkdir %s' % newdir)    # 解压到新目录:    with cd('%s/%s' % (_REMOTE_BASE_DIR, newdir)):        sudo('tar -xzvf %s' % _REMOTE_TMP_TAR)    # 重置软链接:    with cd(_REMOTE_BASE_DIR):        sudo('rm -f www')        sudo('ln -s %s www' % newdir)        sudo('chown www-data:www-data www')        sudo('chown -R www-data:www-data %s' % newdir)    # 重启Python服务和nginx服务器:    with settings(warn_only=True):        sudo('supervisorctl stop awesome')        sudo('supervisorctl start awesome')        sudo('/etc/init.d/nginx reload')
注意run()函数执行的命令是在服务器上运行,with cd(path)和with lcd(path)类似,把当前目录在服务器端设置为cd()指定的目录。如果一个命令需要sudo权限,就不能用run(),而是用sudo()来执行。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

92#
 楼主| 发表于 2017-7-7 10:21:26 | 只看该作者
配置Supervisor
上面让Supervisor重启awesome的命令会失败,因为我们还没有配置Supervisor呢。
编写一个Supervisor的配置文件awesome.conf,存放到/etc/supervisor/conf.d/目录下:
[program:awesome]command     = /srv/awesome/www/app.pydirectory   = /srv/awesome/wwwuser        = www-datastartsecs   = 3redirect_stderr         = truestdout_logfile_maxbytes = 50MBstdout_logfile_backups  = 10stdout_logfile          = /srv/awesome/log/app.log
配置文件通过[program:awesome]指定服务名为awesome,command指定启动app.py。
然后重启Supervisor后,就可以随时启动和停止Supervisor管理的服务了:
$ sudo supervisorctl reload$ sudo supervisorctl start awesome$ sudo supervisorctl statusawesome                RUNNING    pid 1401, uptime 5:01:34配置Nginx
Supervisor只负责运行app.py,我们还需要配置Nginx。把配置文件awesome放到/etc/nginx/sites-available/目录下:
server {    listen      80; # 监听80端口    root       /srv/awesome/www;    access_log /srv/awesome/log/access_log;    error_log  /srv/awesome/log/error_log;    # server_name awesome.liaoxuefeng.com; # 配置域名    # 处理静态文件/favicon.ico:    location /favicon.ico {        root /srv/awesome/www;    }    # 处理静态资源:    location ~ ^\/static\/.*$ {        root /srv/awesome/www;    }    # 动态请求转发到9000端口:    location / {        proxy_pass       http://127.0.0.1:9000;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    }}
然后在/etc/nginx/sites-enabled/目录下创建软链接:
$ pwd/etc/nginx/sites-enabled$ sudo ln -s /etc/nginx/sites-available/awesome .
让Nginx重新加载配置文件,不出意外,我们的awesome-python3-webapp应该正常运行:
$ sudo /etc/init.d/nginx reload
如果有任何错误,都可以在/srv/awesome/log下查找Nginx和App本身的log。如果Supervisor启动时报错,可以在/var/log/supervisor下查看Supervisor的log。
如果一切顺利,你可以在浏览器中访问Linux服务器上的awesome-python3-webapp了:
如果在开发环境更新了代码,只需要在命令行执行:
$ fab build$ fab deploy
自动部署完成!刷新浏览器就可以看到服务器代码更新后的效果。
友情链接
嫌国外网速慢的童鞋请移步网易和搜狐的镜像站点。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

93#
 楼主| 发表于 2017-7-7 10:25:13 | 只看该作者
Day 16 - 编写移动App[转]
网站部署上线后,还缺点啥呢?
在移动互联网浪潮席卷而来的今天,一个网站没有上线移动App,出门根本不好意思跟人打招呼。
所以,awesome-python3-webapp必须得有一个移动App版本!
开发iPhone版本
我们首先来看看如何开发iPhone App。前置条件:一台Mac电脑,安装XCode和最新的iOS SDK。
在使用MVVM编写前端页面时,我们就能感受到,用REST API封装网站后台的功能,不但能清晰地分离前端页面和后台逻辑,现在这个好处更加明显,移动App也可以通过REST API从后端拿到数据。
我们来设计一个简化版的iPhone App,包含两个屏幕:列出最新日志和阅读日志的详细内容:
只需要调用API:/api/blogs。
在XCode中完成App编写:
由于我们的教程是Python,关于如何开发iOS,请移步Develop Apps for iOS。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

该用户从未签到

94#
 楼主| 发表于 2017-7-7 10:39:08 | 只看该作者
FAQ【转】
常见问题
本节列出常见的一些问题。
如何获取当前路径
当前路径可以用'.'表示,再用os.path.abspath()将其转换为绝对路径:
# -*- coding:utf-8 -*-# test.pyimport osprint(os.path.abspath('.'))
运行结果:
$ python3 test.py /Users/michael/workspace/testing如何获取当前模块的文件名
可以通过特殊变量__file__获取:
# -*- coding:utf-8 -*-# test.pyprint(__file__)
输出:
$ python3 test.pytest.py如何获取命令行参数
可以通过sys模块的argv获取:
# -*- coding:utf-8 -*-# test.pyimport sysprint(sys.argv)
输出:
$ python3 test.py -a -s "Hello world"['test.py', '-a', '-s', 'Hello world']
argv的第一个元素永远是命令行执行的.py文件名。
如何获取当前Python命令的可执行文件路径
sys模块的executable变量就是Python命令可执行文件的路径:
# -*- coding:utf-8 -*-# test.pyimport sysprint(sys.executable)
在Mac下的结果:
$ python3 test.py /usr/local/opt/python3/bin/python3.4


回复 支持 反对

使用道具 举报

该用户从未签到

95#
 楼主| 发表于 2017-7-7 10:41:56 | 只看该作者
期末总结!
经过一段时间的学习,相信都能对Python已经有了初步掌握。一开始,可能觉得Python上手很容易,可是越往后学,会越困难,有的时候,发现理解不了代码,这时,不妨停下来思考一下,先把概念搞清楚,代码自然就明白了。
Python非常适合初学者用来进入计算机编程领域。Python属于非常高级的语言,掌握了这门高级语言,就对计算机编程的核心思想——抽象有了初步理解。如果希望继续深入学习计算机编程,可以学习Java、C、JavaScript、Lisp等不同类型的语言,只有多掌握不同领域的语言,有比较才更有收获。


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
回复 支持 反对

使用道具 举报

本版积分规则

关闭

站长推荐上一条 /1 下一条

小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

GMT+8, 2024-5-20 04:04 , Processed in 0.079699 second(s), 21 queries .

Powered by Discuz! X3.2

© 2001-2024 Comsenz Inc.

快速回复 返回顶部 返回列表