技术学习报告

因为我在团队中负责后端和后台管理系统,所以下面分两部分来进行报告。

后端技术

这次的项目中,后端使用nginx+python flask+mysql&redis作为技术栈。因为时间关系,原本想要在服务器上添加jenkins自动化部署以及Docker容器部署的功能都没有实现。

Nginx

在服务器上的Nginx使用,主要还是利用其做反向代理以及https认证。反向代理是Nginx很常用的功能,这里我就不详细说,主要是部署api.leo-lee.cn作为Restful后端的域名,manage.leo-lee.cn作为后台管理系统的访问域名。

而https认证,我使用了Let’s Encrypt提供的免费证书。使用它们提供的软件certbot来生成ssl证书,如下指令。-d参数后面接的是你的网站的地址,-w参数后面是证书生成的位置。

sudo certbot certonly --webroot -w /usr/share/nginx/html -d myapp.your-domain.com

然后给nginx配置好证书位置,就能使域名拥有https功能,如下面例子。

# HTTP - only for obtaining cert
server {
	listen		80;
	server_name	myapp.your-domain.com;
	location /.well-known {
		root	/usr/share/nginx/html;
		allow	all;
	}
	location / {
		return	301 https://$host$request_uri;
	}
}
# HTTPS - serve nodejs app
server {
	listen		443 ssl;
	server_name	myapp.your-domain.com;
	ssl_certificate		/etc/letsencrypt/live/myapp.your-domain.com/fullchain.pem;
	ssl_certificate_key	/etc/letsencrypt/live/myapp.your-domain.com/privkey.pem;
	location / {
		proxy_set_header	X-Real-IP $remote_addr;
		proxy_set_header	X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header	X-NginX-Proxy true;
		proxy_pass		http://localhost:3000/;
		proxy_ssl_session_reuse	off;
		proxy_set_header	Host $http_host;
		proxy_cache_bypass	$http_upgrade;
		proxy_redirect		off;
	}
}

不过体验下来,其实感觉用nginx来配https不算体验特别好,因为如果你原先的服务是http的,利用nginx来处理https并proxy的话,那么原先服务里可能生成的url是http的,并写入到数据库中。那么返回的数据里就很可能出现http链接了。

最好的话,如果后端框架本身就可以使用https功能,就在程序里设置好,nginx仅作为反向代理服务器。利用nginx作为https加密,也只是为那些老旧的http服务程序,做一个权宜之计。

Flask

服务器框架主要使用python的flask轻量服务器,然后使用flask-restful作为Restful API的快速开发,具体一个资源Resource是按照下面的方式组织。

class CategorySingle(Resource):
    def get(self, restaurant_id, category_id):
        category = Category.find_by_id(category_id)
        return {
            'category': category.to_json()
        }, 200

    @jwt_required
    def put(self, restaurant_id, category_id):
        current_user = User.find_by_username(get_jwt_identity())
        restaurant = Restaurant.find_by_id(restaurant_id)
        category = Category.find_by_id(category_id)
        # TODO: change category priority
        if restaurant.owner_id == current_user.id:
            return {
                'message': 'Change category %s priority success.' % category.name
            }
        else:
            return {
                'message': 'This restaurant is not yours.'
            }, 403

    @jwt_required
    def delete(self, restaurant_id, category_id):
        current_user = User.find_by_username(get_jwt_identity())
        restaurant = Restaurant.find_by_id(restaurant_id)
        category = Category.find_by_id(category_id)
        if restaurant.owner_id == current_user.id:
            try:
                db.session.delete(category)
                db.session.commit()
                return {
                    'message': 'Delete category %s success.' % category.name
                }, 200
            except:
                db.session.rollback()
                return {
                    'message': 'Something went wrong.'
                }, 500
        else:
            return {
                'message': 'This restaurant is not yours.'
            }, 403

然后在路由的blueprint上把该资源定义到指定的路径即可,如下。

# category routes
api.add_resource(category_resources.CategorySellerAll, '/seller/restaurants/<int:restaurant_id>/categories')
api.add_resource(category_resources.CategorySingle, '/api/restaurants/<int:restaurant_id>/categories/<int:category_id>')

而且,在后端服务器上,我使用了jwt作为认证的token,在用户登录或者注册后,服务器将会返回access_token和refresh_token。其中access_token是授权口令,利用这个口令可以访问有权限要求的接口。而refresh_token是刷新口令,因为access_token的有效时间是2小时,当失效了,就要使用refresh_token来重新获取access_token。需要在flask中进行如下设置。

JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or 'jwt-secret-string'
JWT_BLACKLIST_ENABLED = True
JWT_BLACKLIST_CHECKED = ['access', 'refresh']
JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=2)
JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=1)

Redis

在后端服务器中,redis主要是用于用户logout时,对access_token和refresh_token加入黑名单并设置过期时间,还有利用redis来存储验证码的key和value,以及微信服务器的access_token(因为创建小程序码要用)。

后台管理系统

后台管理系统主要以nodejs+webpack+vue作为技术栈,并使用element-ui作为样式。使用axios来进行promise式的网络请求。具体是参考vue-manage-system这个项目作为框架的。

Vue

vue中,主要是使用vue-router作为路由配置工具,配置方法如下。

export default new Router({
    routes: [
        {
            path: '/',
            redirect: '/dashboard'
        },
        {
            path: '/',
            component: resolve => require(['../components/common/Home.vue'], resolve),
            meta: { title: '自述文件' },
            children:[
                {
                    path: '/dashboard',
                    component: resolve => require(['../components/page/Dashboard.vue'], resolve),
                    meta: { title: '系统首页' }
                },
                {
                    path: '/restaurants',
                    component: resolve => require(['../components/page/Restaurants.vue'], resolve),
                    meta: { title: '餐馆列表'}
                }
            ]
        }
    ]
});

为了充分利用vue的组件属性,将一个单页应用(SPA)的框架分成Header,Sidebar,Tags,Home部分,在Home部分留下router-view标签,那么每个不同的页面就能通过router设置在这个标签里面加载对应的组件。具体代码我就不放上来了。

在使用axios的过程中,最主要的问题是部署时的配置问题。一开始以为程序错误,原来是devserver没有配置好,应该要按如下配置。

module.exports = {
    baseUrl: '/',
    productionSourceMap: false,
    devServer: {
        proxy: {
            '/api': {
                target: 'https://api.leo-lee.cn',
                changeOrigin: true,
                pathRewrite: {
                    '/api': ''
                }
            }
        },
        disableHostCheck: true
    }
};

这里注意,如果不开disableHostCheck这个功能,就会使得配置了https的应用访问失败。