美文网首页前端都会去了解的Vueweb_front-end
Vue 2.0 起步(4) 轻量级后端Flask用户认证 - 微

Vue 2.0 起步(4) 轻量级后端Flask用户认证 - 微

作者: 非梦nj | 来源:发表于2016-12-23 12:57 被阅读8872次

    参考:

    本篇实现

    1. Flask框架搭建
    2. 后端用户注册、认证 (注意:改用Flask-Security了:http://www.jianshu.com/p/f37871e31231)
    3. 跨域(Access-Control-Allow-Origin)本地调试
    4. 表单验证validation

    Demo(http://vue2.heroku.com)

    本章完成效果
    使用RESTful思想,互联网应用的后端仅提供API接口来提供鉴权服务和资源,前端Vue使用Ajax访问获取数据并显示。
    MVVM模型 - 这里Flask担任Model(数据模型)、View(路由)角色,Vue担任VM(ViewModel视图模型)角色。

    工具选择

    • 后端:使用Flask这一广受好评的Python微(Micro)框架,非常适合快速开发。当然,使用Node.js、PHP、Java等等其它语言,思路也是大同小异的。Flask被称为“micro framework”,是因为它使用简单的核心,用extension增加其他功能。Flask没有默认使用的数据库、窗体等等验证工具。然而,Flask保留了扩增的弹性,可以用Flask-extension加入这些功能:ORM、窗体验证工具、文件上传、各种开放式身份验证技术。是不是跟vue很像啊?
    flask logo

    Flask最基本的hello-world,用几行代码、一个文件就能实现。但为了适应大型项目扩展,跟vue-cli创建的脚手架类似,Flask也有推荐的项目典型目录,结构如下:

    vue-tutorial/
        |--app/
            |--api_1_0/    # api目录,对于REST访问返回数据
                |--users.py
            |--main/
                |--views.py  # 路由文件,SPA里,只需要返回"/"根路由
            |--static/      # js, css
            |--templates/    # SPA里,只需要index.html  
            |--__init__.py  # flask app初始化
            |--models.py    # model数据库定义
        |--config.py  # Flask配置
        |--manage.py  # Flask启动文件,包含命令行
    

    参考:Flask官网
    我的Flask快速入门

    • 用户鉴权:使用JWT(JSON Web Token)。本实例是SPA(单页面应用),前端vue-router插件已经实现路由功能,后端Flask只需要提供api接口就行。所以不需要使用Flask_login来管理session。JWT是一个非常轻巧的规范,允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

    参考:我的Flask-JWT入门

    jwt.png

    (注意:改用Flask-Security了:http://www.jianshu.com/p/f37871e31231)

    1. Flask框架搭建

    请先下载项目源码,对照源码阅读和实践会效率更高

    首先,设计一个结构清晰的关系型数据库(Model):

    • User用户表,肯定是要有的,记录用户名、密码Hash和他关注的公众号
    • Mp公众号表,记录哪些人关注了这个公众号,以及这个公众号有哪些文章
    • Article文章表,相对简单,记录公众号文章

    关系

    • Mp和Article是一对多的关系
    • User和Mp,看起来像一对多,但其实是多对多的关系:一个用户关注多个公众号,同一个公众号也可能被多个用户共同关注,所以需要另加一张“关联表” - Subscription
    • User和Subscription:一对多
    • Mp和Subscription:一对多
    数据库EER

    下面就来创建model,在Flask models.py实现。

    /app/models.py

    • Subscription:关联到User和Mp两个表
    # encoding: utf-8
    from datetime import datetime
    import hashlib
    from werkzeug.security import generate_password_hash, check_password_hash
    from flask import current_app, request, url_for, jsonify
    from flask_login import UserMixin, AnonymousUserMixin
    from . import db
    
    # 订阅公众号和User是多对多关系
    class Subscription(db.Model):
        __tablename__ = 'subscriptions'
        # follower_id
        subscriber_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                                primary_key=True)
        # followed_id
        mp_id = db.Column(db.Integer, db.ForeignKey('mps.id'),
                                primary_key=True)
        subscribe_timestamp = db.Column(db.DateTime, default=datetime.utcnow)
    
    • User:功能最为复杂。
      1. 关联到Subscription表
      2. password.setter:用户注册时,密码转化为hash存储。永远不要存储密码明文!
      3. subscribed_mps方法:用过滤器和联结查询,返回该用户订阅的所有公众号
    class User(UserMixin, db.Model):
        __tablename__ = 'users'
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(64), unique=True, index=True)
        password_hash = db.Column(db.String(128))
        member_since = db.Column(db.DateTime(), default=datetime.utcnow)
        last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
        mps = db.relationship('Subscription',
                                   foreign_keys=[Subscription.subscriber_id],
                                   backref=db.backref('subscriber', lazy='joined'),
                                   lazy='dynamic',
                                   cascade='all, delete-orphan')
    
        @property
        def password(self):
            raise AttributeError('password is not a readable attribute')
    
        @password.setter
        def password(self, password):
            self.password_hash = generate_password_hash(password)
    
        def verify_password(self, password):
            return check_password_hash(self.password_hash, password)
    
        @property
        def subscribed_mps(self):
            # SQLAlchemy 过滤器和联结
            return Mp.query.join(Subscription, Subscription.mp_id == Mp.id)\
                .filter(Subscription.subscriber_id == self.id)
    
        def __repr__(self):
            return '<User %r>' % self.username
    
    • Mp:
      1. 关联到Subscription表
      2. to_json方法:在REST访问时,用来返回json格式的公众号数据
    # 公众号
    class Mp(db.Model):
        __tablename__ = 'mps'
        id = db.Column(db.Integer, primary_key=True)
        weixinhao = db.Column(db.Text)
        image = db.Column(db.Text)
        summary = db.Column(db.Text)
        sync_time = db.Column(db.DateTime, index=True, default=datetime.utcnow)
        articles = db.relationship('Article', backref='mp', lazy='dynamic')
        subscribers = db.relationship('Subscription',
                                   foreign_keys=[Subscription.mp_id],
                                   backref=db.backref('mp', lazy='joined'),
                                   lazy='dynamic',
                                   cascade='all, delete-orphan')
        def to_json(self):
            json_mp = {
                'weixinhao': self.weixinhao,
                'image': self.image,
                'summary': self.summary,
                'articles_count': self.articles.count()
            }
            return json_mp
    
    • Article:最为简单
      1. 关联到Mp表
      2. to_json方法:在REST访问时,用来返回json格式的文章数据
    # 公众号的文章
    class Article(db.Model):
        __tablename__ = 'articles'
        id = db.Column(db.Integer, primary_key=True)
        title = db.Column(db.Text)
        image = db.Column(db.Text)
        summary = db.Column(db.Text)
        url = db.Column(db.Text)
        timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
        mp_id = db.Column(db.Integer, db.ForeignKey('mps.id'))
    
        def to_json(self):
            json_article = {
                'url': url_for('api.get_comment', id=self.id, _external=True),
                'body': self.body,
                'timestamp': self.timestamp
            }
            return json_article
    

    第二步,在app初始化时,引用models.py,并创建JWT

    /app/_init_.py

    # encoding: utf-8
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from flask_jwt import JWT
    from config import config
    
    db = SQLAlchemy()
    
    # models引用必须在 db之后,不然会循环引用
    from .models import User
    
    # JWT鉴权:默认参数为username/password,在数据库里查找并比较password_hash
    def authenticate(username, password):
        print 'JWT auth argvs:', username, password
        user = User.query.filter_by(username=username).first()
        if user is not None and user.verify_password(password):
            return user
    # JWT检查user_id是否存在
    def identity(payload):
        print 'JWT payload:', payload
        user_id = payload['identity']
        user = User.query.filter_by(id=user_id).first()
        return user_id if user is not None else None
    # 创建jwt实例
    jwt = JWT(authentication_handler=authenticate, identity_handler=identity)
    
    def create_app(config_name):
        app = Flask(__name__)
    # 引入Flask用户配置
        app.config.from_object(config[config_name])
        config[config_name].init_app(app)
    # 初始化数据库和JWT
        db.init_app(app)
        jwt.init_app(app)
    # 注册main/api蓝本,这样用户访问路径“/xxx”指向main,“/api/v1.0”指向api
        from .main import main as main_blueprint
        app.register_blueprint(main_blueprint)
        from .api_1_0 import api as api_1_0_blueprint
        app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
    
        return app
    

    第三步,在启动文件中,创建命令行(数据库、部署、测试等等),启动app

    /manage.py

    #!/usr/bin/env python
    import os
    from app import create_app, db, jwt
    from app.models import User, Subscription, Mp, Article
    from flask_script import Manager, Shell
    from flask_migrate import Migrate, MigrateCommand
    
    app = create_app(os.getenv('FLASK_CONFIG') or 'default')
    manager = Manager(app)
    migrate = Migrate(app, db)
    
    def make_shell_context():
        return dict(app=app, db=db, User=User, Subscription=Subscription, Mp=Mp,
                    Article=Article)
    manager.add_command("shell", Shell(make_context=make_shell_context))
    manager.add_command('db', MigrateCommand)
    
    @manager.command
    def deploy():
        """Run deployment tasks."""
        from flask_migrate import upgrade
        from app.models import User
    
        # migrate database to latest revision
        upgrade()
    
    if __name__ == '__main__':
        manager.run()
    

    好了,总算初始框架都完成了。看上去很复杂,但跟vue-cli脚手架工具一样,你下载项目源码,框架都是现成的(而且是成熟可靠的),稍微修改一下就能跑起来了。主要是数据库定义models.py,完全要自己来定!

    Python的依赖模块安装:
    跟<code>npm install</code> node_modules类似,直接用<code>pip install -r requirements.txt</code>就一步完成了。

    现在数据库还没有,我们来创建吧,很简单,三行命令:
    打开CMD(Windows),或Shell(Linux)

    c:\git\vue-tutorial>python manage.py db init
    Creating directory c:\git\vue-tutorial\migrations ... done
    Creating directory c:\git\vue-tutorial\migrations\versions ... done
    Generating c:\git\vue-tutorial\migrations\alembic.ini ... done
    Generating c:\git\vue-tutorial\migrations\env.py ... done
    Generating c:\git\vue-tutorial\migrations\env.pyc ... done
    Generating c:\git\vue-tutorial\migrations\README ... done
    Generating c:\git\vue-tutorial\migrations\script.py.mako ... done
    Please edit configuration/connection/logging settings in 'c:\\git\\vue-tutorial\\migrations\\alembic.ini' before proceed
    ing.
    
    c:\git\vue-tutorial>python manage.py db migrate -m "init"
    INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
    INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
    INFO  [alembic.autogenerate.compare] Detected added table 'mps'
    INFO  [alembic.autogenerate.compare] Detected added index 'ix_mps_sync_time' on '['sync_time']'
    INFO  [alembic.autogenerate.compare] Detected added table 'users'
    INFO  [alembic.autogenerate.compare] Detected added index 'ix_users_username' on '['username']'
    INFO  [alembic.autogenerate.compare] Detected added table 'articles'
    INFO  [alembic.autogenerate.compare] Detected added index 'ix_articles_timestamp' on '['timestamp']'
    INFO  [alembic.autogenerate.compare] Detected added table 'subscriptions'
    Generating c:\git\vue-tutorial\migrations\versions\599e99548c86_init.py ... done
    
    c:\git\vue-tutorial>python manage.py db upgrade
    INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
    INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
    INFO  [alembic.runtime.migration] Running upgrade  -> 599e99548c86, init
    

    现在,数据库文件已经产生了,默认是config.py文件里定义的<code>/data-dev.sqlite</code>,打开来看看:
    models.py里定义的表,都创建好了,是不是很清楚啊?比如:Subscription表,有两个Foreign_Key,指向Mp和User。


    sqlite数据库.png

    2. 后端用户注册、认证

    回到Vue.js,我们准备把注册功能,放在右侧Siderbar.vue

    • template第一部分:如果已经登录is_login,则显示用户头像和退出按钮
    • template第二部分:如果没有登录,则显示一个表单,可以输入username/password,注册和登录两个按钮
    # /src/components/Sidebar.vue (部分)
    <template>
        <div class="card">
            <div v-if="is_login" class="card-header" align="center">
                <img src="http://avatar.csdn.net/1/E/E/1_kevin_qq.jpg"
                     class="avatar img-circle img-responsive" />
                <p><strong v-text="username"></strong>
                    <a href="javascript:" @click="logout()" title="退出">
                        <i class="fa fa-sign-out float-xs-right"></i></a>
                </p>
            </div>
            <div v-else class="card-header" align="center">
                <form class="form" @submit.prevent>
                    <div class="form-group">
                        <input class="form-control" name="username" type="text" placeholder="用户名" v-model="username"
                               required pattern="\w{3,12}" />
                               <p class="text-muted"><small>3~12位字母、数字、下划线</small></p>
                    </div>
                    <div class="form-group">
                        <input class="form-control" name= "password" type="password" placeholder="密码" v-model="password"
                               required pattern="\w{4,}"/>
                               <p class="text-muted"><small>至少4位,字母数字下划线</small></p>
                    </div>
                    <div class="form-group clearfix">
                        <input type="submit" @click="register()" class="btn btn-outline-danger float-xs-left"                       value="注册" />
                        <input type="submit" @click="login()" class="btn btn-outline-success float-xs-right"                        value="登录" />
                    </div>
                </form>
            </div>
    。。。
        </div>
    </template>
    
    • Script部分:加入新的data和methods
    • methods.register(),用vue-resource post功能,提交username/password到Flask,由<code>/api/users.py</code>处理
    # /src/components/Sidebar.vue (部分)
    <script>
        export default {
            name : 'Sidebar',
            data() {
                return {
                    is_login: false,
                    username: '',
                    password: '',
                    token: ''
                }
            },
    。。。
            methods : {
                register() {
                    this.$http.post('http://127.0.0.1:5000/api/v1.0/register',
    //body
                            {   username: this.username,
                                password: this.password
                            },
                            //options
                            {
                                headers: {'Content-Type':'application/json; charset=UTF-8'}
                            }                 ).then((response) => {
                        // 响应成功回调
                        var data = response.body;
                    if (data.status=='success') {
                        alert('Success! ' + data.msg)
                    }
                    else {
                        alert(data.msg)
                    }
                    this.password = ''
                }, (response) => {
                        // 响应错误回调
                        alert('注册出错了! '+ JSON.stringify(response))
                    });
                }
    </script>
    
    • 后端处理register请求:
      对于ajax post请求,上面是用json提交的,所以用<code>request.get_json</code>来得到数据。然后检查数据是否有效,用户名是否已经注册。一切无误的话就会添加用户到数据库。
    # /app/api_1_0/users.py
    @api.route('/register', methods=['GET', 'POST'])
    def register():
        username = request.get_json()['username']
        password = request.get_json()['password']
        print 'register Header: %s\nusername: %s, password:%s'% (request.headers, username, password)
        if username <> '' and password <> '':
            if User.query.filter_by(username=username).first():
                 return jsonify({
                'status': 'failure',
                'msg': u'用户名已被占用,换一个吧'
                })           
    
            user = User(username=username, password=password)
            db.session.add(user)
            db.session.commit()
            return jsonify({
            'status': 'success',
            'msg': 'register OK, please login!'
            })
        return jsonify({
        'status': 'failure',
        'msg': 'register fail, check username and password.'
        })
    
    

    Flask跑起来,用的是5000端口:

    c:\git\vue-tutorial>python manage.py runserver
     * Restarting with stat
     * Debugger is active!
     * Debugger pin code: 302-156-201
     * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
    

    前端点击注册按钮,应该成功了!咦,怎么返回Bad Request (400)?

    127.0.0.1 - - [15:25:09] "OPTIONS /api/v1.0/register HTTP/1.1" 400 -
    

    我明明发的是POST,为什么服务器端说收到是的OPTIONS呢?哈哈,这就是著名的CORS跨域请求了!请看下一节的灵巧解决方案!

    3. 跨域(Access-Control-Allow-Origin)本地调试

    前端跨域Post请求,由于CORS(cross origin resource share)规范的存在,浏览器会首先发送一次Options嗅探,同时header带上origin,判断是否有跨域请求权限,服务器响应access control allow origin的值,供浏览器与origin匹配,如果匹配,浏览器则正式发送post请求。
    如果有服务器程序权限,设置headeraccess control allow origin等于*,就可以允许前端跨域访问了。
    我以前也是这么解决的:Flask: Ajax 设置Access-Control-Allow-Origin实现跨域访问
    CORS深入

    目前jsonp是最简单跨域方案,不过只能GET,不支持POST。如果要POST,则服务器端设置ACAO很麻烦,或用其它的绕路方法。

    但是:
    我们上一篇,不是有这个方案吗:Vue+Flask轻量级前端、后端框架,如何完美同步开发
    这样就不存在跨域烦恼了,我们本身就在一个服务器(localhost:5000,包含端口号)下呀!

    好了,马上来试试。上一篇的步骤是适用于最简的Flask项目,在这里有个小小改动,因为我们用了新的Flask目录框架,需要把修改后的index.html放入

    /app/templates/index.html
    

    同样,static文件放入新的目录:

    /app/static/font-awesome/
    

    再修改Siderbar.vue,POST不需要跨域了,直接是同一服务器+端口号上的路径<code>/api/v1.0/register</code>:

    # /src/components/Sidebar.vue (部分)
    <script>
    。。。
            methods : {
                register() {
                    this.$http.post('/api/v1.0/register',
    。。。
                }
    </script>
    

    Flask跑起来,再点击“注册”,成功啦!

    注册成功.png

    对应的Flask log:

    register Header: Referer: http://localhost:5000/
    Origin: http://localhost:5000
    Content-Length: 41
    User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTM
    537.36
    Connection: keep-alive
    X-Requested-With: XMLHttpRequest
    Host: localhost:5000
    Accept: application/json, text/plain, */*
    Accept-Language: en-US,en;q=0.8,zh-CN;q=0.6,zh;q=0.4
    Content-Type: application/json; charset=UTF-8
    Accept-Encoding: gzip, deflate
    
    
    username: hellovue, password:1111
    127.0.0.1 - - [23/Dec/2016 12:16:03] "POST /api/v1.0/register HTTP/1.1" 200 -
    

    想不到,我们一个小小改进,不仅前后端完美同步开发,而且还解决了CORS跨域问题!

    OK,继续完成用户登录、登出功能,很简单,在Siderbar.vue里添加methods就行

    • login():/auth是Flask-JWT默认的鉴权路由,鉴权方法已经在/app/_init_.py里写好了。如果登录成功,本地LocalStorage把得到的token保存下来,以后REST请求会用到这个。
    • logout():is_login设成false,然后本地LocalStorage删除user(目的是删除保存的token)
    # /src/components/Sidebar.vue (部分)
           methods : {
                login() {
                    this.$http.post('/auth',
                        //body
                            {   username: this.username,
                                password: this.password
                            },
                            //options
                            {
                                headers: {'Content-Type':'application/json; charset=UTF-8'}
                            }                 ).then((response) => {
                        // 响应成功回调
                        var data = response.body;
                    this.token = data.access_token;
                    this.is_login = true;
       //             alert(data.access_token);
                    var userData = {'username': this.username, 'token': this.token};
                    window.localStorage.setItem("user", JSON.stringify(userData))
    
                }, (response) => {
                        // 响应错误回调
                        alert('登录出错了! '+ response.status+ response.statusText)
                    });
                },
                logout() {
                    this.is_login = false;
                    this.password = '';
                    this.token = '';
                    window.localStorage.removeItem("user")
                },
    

    4. 表单验证validation

    表单验证是个很普遍的需求,如果用户不停地收到输入错误的告警,会很抓狂滴!所以,最好在提交前做好验证。
    vue-validator是Vue全家桶里用到的表单验证插件,但适用于Vue2.0的版本迟迟没推出。
    那就自己写一个简单的呗,几行代码而已。
    对于HTML5,本身就有基本的表单验证功能,提交时浏览器会自动检查,但仅限于部分浏览器

    无效输入,按钮不可点击 输入有效,按钮可点击
    • template input里,<code>required pattern="\w{3,12}"</code>是HTML5的功能
    • 按钮上,绑定一个计算属性validation: <code>:disabled="!validation"</code>
    • 提交事件: <form class="form" @submit.prevent>,阻止了默认submit事件,由vue方法接手
    # /src/components/Sidebar.vue (部分)
              <form class="form" @submit.prevent>
                    <div class="form-group">
                        <input class="form-control" name="username" type="text" placeholder="用户名" v-model="username"
                               required pattern="\w{3,12}" />
                               <p class="text-muted"><small>3~12位字母、数字、下划线</small></p>
                    </div>
                    <div class="form-group">
                        <input class="form-control" name= "password" type="password" placeholder="密码" v-model="password"
                               required pattern="\w{4,}"/>
                               <p class="text-muted"><small>至少4位,字母数字下划线</small></p>
                    </div>
                    <div class="form-group clearfix">
                        <input type="submit" @click="register()" class="btn btn-outline-danger float-xs-left" 
                            value="注册" :disabled="!validation" />
                        <input type="submit" @click="login()" class="btn btn-outline-success float-xs-right" 
                            value="登录" :disabled="!validation" />
                    </div>
    
    • 计算属性validation,实时计算用户输入内容是否有效。比如:/(\w{3,12})/是判断是否为:3到12位的数字、字母、下划线。
    • login/register方法:在post提交前,如果计算属性validation为false(输入有误),就不提交
    # /src/components/Sidebar.vue (部分)
           computed : {
                validation() {
                    var patt1 = /(\w{3,12})/;
                    var patt2 = /(\w{4,})/;
    //              alert(this.username + patt1.test(this.username));
                    return patt1.test(this.username) && patt2.test(this.password)
                }
            },
            methods : {
                login() {
                    if (!this.validation) return;
                    this.$http.post('/auth',
    。。。
    

    舒了一口气,这篇写得时间较长,因为相当于把后端Flask启蒙了一遍。。。
    后续的ajax保存、请求订阅列表,相对比较简单明了,大家有什么其它需求,请评论留言哦!
    项目源码

    TODO:

    • 后端保存用户订阅的公众号,搜狗的链接都是临时的
    • 公众号文章的更新,这个Python爬虫最拿手了

    敬请关注Vue 2.0 起步(5) 订阅列表上传和下载 - 微信公众号RSS

    http://www.jianshu.com/p/ab778fde3b99

    相关文章

      网友评论

      • 我是小栗子:ImportError: cannot import name config,什么意思?
        我是小栗子:@非梦nj 建了,我意识到到后面光跟文章做不行了。。省略了好多
        非梦nj:config.py你没创建?
        建议直接 git clone我的代码,里面是全的
      • interrupt_3941:请问一下,我已经把font-awesome还有其他的img放进了statics里为什么还是404?
        非梦nj:@interrupt_3941 浏览器F12,看一下Network -- 哪个文件没找到
      • 木子tar:看到这一篇,瞬间一脸懵逼😖😖
      • SuKong:src/main.js 倒数第二行的 ...App 是什么语法?
        SuKong:@非梦nj
        可是你的代码里 App 是一个对象,不是一个数组啊,虽然用 webpack 可以成功编译,但我还是不太理解。
        非梦nj:ES6 spread运算符, 用于数组的构造,析构,以及在函数调用时使用数组填充参数列表。e.g.:

        let cold = ['autumn', 'winter'];
        let warm = ['spring', 'summer'];
        // 构造数组
        [...cold, ...warm] // => ['autumn', 'winter', 'spring', 'summer']
        // 析构数组
        let otherSeasons, autumn;
        [autumn, ...otherSeasons] = cold; otherSeasons // => ['winter']
        // 将数组元素用于函数参数
        cold.push(...warm);
        cold // => ['autumn', 'winter', 'spring', 'summer']
      • zcdll:我把遇到的错误以及解决办法,写到这篇文章中了,谢谢大神的分享以及提供的思路,Thanks!
        http://zcdll.me/2017/02/21/flask-vue-restful/
        非梦nj:@zcdll 谢谢你的总结,赞一个!
      • 非梦nj:第5篇 上传下载订阅列表:http://www.jianshu.com/p/3a1a6986ca94
      • 逍遥生:数据库ER图你是用什么画的啊?
        逍遥生: @非梦nj 嗯 好的,谢谢了
        非梦nj: @逍遥生 手工画的。自动也可以:先manage.py db init创建数据库,然后MySQL Workbench: Database -> Reverse Engineer -> 选择 connection -> database -> 一路 Next
      • 16f10d6e5e11:默默电个赞:+1:

      本文标题:Vue 2.0 起步(4) 轻量级后端Flask用户认证 - 微

      本文链接:https://www.haomeiwen.com/subject/zegxvttx.html