Flask框架(二)

目录

  • 一、请求扩展
    • 1.1befor_request
    • 1.2after_request
    • 1.3before_first_request
    • 1.4teardown_request
    • 1.5errhandler(错误码)
    • 1.6template_global()
    • 1.7template_filter()
  • 二、flask中间件
  • 三、flask蓝图
    • 3.1蓝图的介绍与使用
    • 3.2蓝图的高级使用(重点内容)
    • 3.3使用蓝图之中小型系统
    • 3.4使用蓝图之使用大型系统
  • 四、flask之协程和线程
    • 4.1threading.local
    • 4.2通过字典自定义threading.local
    • 4.3通过setattr和getattr实现自定义threthreading.local
    • 4.4每个对象有自己的存储空间(字典)
    • 4.5如何设计flask的请求并发?
  • 五、偏函数

一、请求扩展

1.1befor_request

1.app.befor_request装饰过得函数在真正的响应函数之前执行,可以有多个,当有多个的时候,执行顺序是谁在前面谁先执行。

2.只要一个befor_request有返回值,那后面所有的befor_request都不会执行,真正的响应函数也不会执行。

3.被befor_request装饰的函数可以没有return。

from  flask import Flask,render_template

app = Flask(__name__)

@app.before_request
def befor_process():
    print("befor_process0")
    return "tttt"

@app.before_request
def befor_process():
    print("befor_process1")


if __name__ == '__main__':
    #app()--->app.__call__
    app.__call__
    app.run()

1.2after_request

1.after_request是在真正响应函数之后执行的。

2.它可以有多个,当为多个的时候执行顺序为,谁在前面谁后执行。

3.befor_request有没有返回值都不会影响after_resquest的执行。

4.after_resquest装饰的函数必须接收参数,还要把参数返回否则报错

@app.after_request
def after_process(response):

    print("我是请求之后1")
    return  response

@app.after_request
def after_process1(response):
    print("我是请求之后2")
    return  response

1.3before_first_request

before_first_request在整个项目启动后第一次接收到请求时会执行,以后不会执行

#这个是整个项目启动后第一次接收到请求,就会执行这个方法。以后不会执行
@app.before_first_request
def first():
    print(123)

1.4teardown_request

不管有没有错误,都会执行teardown_request,如果没有错误,错误值为None;如果有错误,直接建报错信息传递给改被装饰的函数。

@app.teardown_request
def tre(e):
    print("e",e)

1.5errhandler(错误码)

如果没有该错误码的错误出现,则被装饰的函数不会执行,如果有错误码的错误出现 就会执行被装饰的函数,不会让错误暴露给用户,相当于Django的报错页面。

@app.errorhandler(500)
def error_handler(*args,**kwargs):
    print(*args,**kwargs)
    return "500错误"

@app.errorhandler(404)
def errrrrr(e):
    return render_template("index404.html")#此处可以返回一个页面

1.6template_global()

全局模板标签,被装饰的函数可以在模板中作为全局的标签使用,在模板中可以直接调用。

@app.template_global()
def sb(a1,a2,a3):
    return a1+a2+a3

调用方式:

{{sb(1,2,3)}}#与Django类似

1.7template_filter()

全局模板过滤器,可以在模板中作为全局过滤器使用,在模板中可以直接调用。

@app.template_filter()
def sb1(a1,a2,a3):
    return a1+a2+a3

调用方式(注意同template_global的区别) :

{{1|add_filter(2,3,4)}}

全局模板标签和全局模板过滤器简化了需要手动传一个函数给模板调用。

二、flask中间件

flask的中间件的性质,就是可以理解为在整个请求的过程的前后定制一些个性化的功能。

flask的中间件的实现案例:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    print('视图函数中')
    return 'hello world'

class my_middle:

    def __init__(self,wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, *args, **kwargs):
        print('中间件的代码上')
        obj = self.wsgi_app( *args, **kwargs)
        print('中间件的代码下')

        return obj

if __name__ == '__main__':
   
    app.wsgi_app = my_middle(app.wsgi_app)
     # app.wsgi_app(environ, start_response)
    app.run()
    # 梳理一下 根据werkzeug我们可以知道 每次请求必然经历了app()
    # 所以我们要查看Flask的源码找到__call__方法
    # 找到了__call__方法后发现执行了return self.wsgi_app(environ, start_response)
    # 然后flask里面所有的内容调度都是基于这个self.wsgi_app(environ, start_response),这就是就是flask的入口
    # 如何实现中间件呢? 原理上就是重写app.wsgi_app,然后在里面添加上一些自己想要实现的功能。
    # 首先分析  app.wsgi_app需要加括号执行  所以我们把app.wsgi_app做成一个对象,并且这个对象需要加括号运行
    # 也就是会触发这个对象的类的__call__()方法
    # 1 那么就是app.wsgi_app=对象=自己重写的类(app.wsgi_app) ,我们需要在自己重写的类里面实现flask源码中的app.wsgi_app,在实例化的过程把原来的app.wsgi_app变成对象的属性
    # 2         app.wsgi_app() =对象() = 自己重写的类.call()方法
    # 3         那么上面的代码就可以理解了,在自己重写的类中实现了原有的__call__方法

梳理

  • 根据werkzeug我们可以知道 每次请求必然经历了app()
  • 所以我们要查看Flask的源码找到__call__方法
  • 找到了Flask的__call__方法后发现执行了return self.wsgi_app(environ, start_response)
  • flask里面所有的内容调度都是基于这个self.wsgi_app(environ, start_response),这就是就是flask的入口,也就是self是app,也就是app.wsgi_app(environ, start_response)为程序的入口。
  • 如何实现中间件呢? 原理上就是重写app.wsgi_app,然后在里面添加上一些自己想要实现的功能。
  • 首先分析 app.wsgi_app需要加括号执行 所以我们把app.wsgi_app做成一个对象,并且这个对象需要加括号运行。
  • 也就是会触发这个对象的类的__call__()方法。

实操理解

  1. app.wsgi_app=对象=自己重写的类(app.wsgi_app) 提示:我们需要在自己重写的类里面实现flask源码中的app.wsgi_app,在实例化的过程把原来的 app.wsgi_app变成对象的属性
  2. app.wsgi_app() =对象() = 自己重写的类.call()方法 app.wsgi_app(实参) =对象(实参) = 自己重写的类.call(实参)方法
  3. 那么上面的代码就可以理解了,在自己重写的类中实现了原有的call方法,并且重新调用了原有的app.wsgi_app

三、flask蓝图

3.1蓝图的介绍与使用

蓝图用来对程序的目录进行划分,比如下面的代码很乱就需要蓝图进行管理:

from flask import Flask

app = Flask(__name__)

@app.route('/login/')
def login():
    return "login"

@app.route('/logout/')
def logout():
    return "logout"

@app.route('/add_order/')
def add_order():
    return "add_order"

@app.route('modify_order')
def modify_order():
    return "modify_order"


if __name__ == '__main__':
    app.run()

项目目录:

-templates
-static
-views
    -user.py
    -order.py
-app.py

views/user.py

from flask import Blueprint

# 1 创建蓝图
user_bp = Blueprint('user',__name__)

# 2 利用蓝图创建路由关系
@user_bp.route('/login/')
def login():
    return "login"

@user_bp.route('/logout/')
def logout():
    return "logout"

views/order.py

from flask import Blueprint

order_bp = Blueprint('order',__name__)

@order_bp.route('/add_order/')
def add_order():
    return "add_order"

@order_bp.route('/modify_order/')
def modify_order():
    return "modify_order"

app.py

from flask import Flask
from views.user import user_bp
from views.order import order_bp

app = Flask(__name__)
# 3 注册蓝图
app.register_blueprint(user_bp)
app.register_blueprint(order_bp)


if __name__ == '__main__':
    app.run()

访问:

观察views/user.py

  • 我们可以把所有的视图函数抽出来多个文件。
  • 在这里我们通过user_bp = Blueprint('user',__name__)创建一个蓝图对象 参数讲解:
    • user_bp :是用于指向创建出的蓝图对象,可以自由命名。
    • Blueprint的第一个参数自定义命名的‘user’用于url_for翻转url时使用。
    • __name__用于寻找蓝图自定义的模板和静态文件使用。
  • 蓝图对象的用法和之前实例化出来的app对象用法很像,可以进行注册路由。

观察app.py

  • 这里我们需要手动的去注册一下蓝图,才会建立上url和视图函数的映射关系。

关键词:

  1. 创建蓝图 user_bp = Blueprint('user',__name__)
  2. 利用蓝图创建路由关系 @bp.route('/login/') def login(): return "login"
  3. 注册蓝图 app.register_blueprint(bp)

3.2蓝图的高级使用(重点内容)

3.2.1蓝图中实现path部分的url前缀

创建蓝图的时候填写url_prefix可以为增加url的path部分的前缀,可以更方便的去管理访问视图函数。

from flask import Blueprint

# 1 创建蓝图
user_bp = Blueprint('user',__name__,url_prefix='/user')
# 注意斜杠跟视图函数的url连起来时候不要重复了。

注意:

  1. 斜杠跟视图函数的url连起来时候不要重复了。 图解:

2.url加前缀的时候也可以再注册蓝图的时候加上,更推荐这么做,因为代码的可读性更强。

app.register_blueprint(user_bp,url_prefix='/order')

3.2.2蓝图中自定义模板路径

创建蓝图的时候填写template_folder可以指定自定义模板路径

# 1 创建蓝图                                           #所对应的参数路径是相对于蓝图文件的
user_bp = Blueprint('user',__name__,url_prefix='/user',template_folder='views_templates')

注意

  1. 蓝图虽然指定了自定义的模板查找路径,但是查找顺序还是会先找主app规定的模板路径(templates),找不到再找蓝图自定义的模板路径。
  2. Blueprinttemplate_folder参数指定的自定义模板路径是相对于蓝图文件的路径。 图解: (01)

(02)

3.2.3蓝图中自定义静态文件路径

创建蓝图的时候填写static_folder可以指定自定义静态文件的路径

user_bp = Blueprint('user',__name__,url_prefix='/user',template_folder='views_templates',
                    static_folder='views_static')

注意:

  1. 在模板中使用自定义的静态文件路径需要依赖url_for()
  2. 下节讲解如何在模板中应用蓝图自定义的静态文件。

3.2.4url_for()翻转蓝图

视图中翻转url:
url_for('创建蓝图时第一个参数.蓝图下的函数名')
# 如:
url_for('user.login')
模板中翻转url:
{{ url_for('创建蓝图时第一个参数.蓝图下的函数名') }}
# 如:
{{ url_for('user.login') }}
模板中应用蓝图自定义路径的静态文件:
{{ url_for('创建蓝图时第一个参数.static',filename='蓝图自定义静态文件路径下的文件') }}
# 如:
{{ url_for('user.static',filename='login.css') }}

3.2.5蓝图子域名的实现

创建蓝图的时候填写subdomain可以指定子域名,可以参考之前注册路由中实现子域名。

(1) 配置C:\Windows\System32\drivers\etc\hosts

127.0.0.1 bookmanage.com
127.0.0.1 admin.bookmanage.com

(2)给app增加配置

app.config['SERVER_NAME'] = 'bookmanage.com:5000'

(3)创建蓝图的时候添加子域名 subdomain='admin'

# 1 创建蓝图                                           
user_bp = Blueprint('user',__name__,url_prefix='/user',template_folder='views_templates',
                    static_folder='views_static',subdomain='admin')


# 2 利用蓝图创建路由关系
@user_bp.route('/login/')
def login():
    return render_template('login_master.html')

(4) 访问admin.bookmanage.com:5000/user/login/

3.2.6蓝图中使用自己请求扩展

在蓝图中我们可以利用创建好的蓝图对象,设置访问蓝图的视图函数的时候触发蓝图独有的请求扩展。

例如:

order_bp = Blueprint('order',__name__)


@order_bp.route('/add_order/')
def add_order():
    return "add_order"

@order_bp.before_request
def order_bp_before_request():
    return '请登录'

注意:

  • 只有访问该蓝图下的视图函数时候才会触发该蓝图的请求扩展。
  • 可以这么理解:相当app的请求扩展是全局的,而蓝图的请求扩展是局部的只对本蓝图下的视图函数有效。

3.3使用蓝图之中小型系统

目录结构:

-flask_small_pro
    -app01
        -__init__.py
        -static
        -templates
        -views
            -order.py
            -user.py
     -manage.py 
        
__init__.py
from flask import Flask
from app01.views.user import user_bp
from app01.views.order import order_bp


app = Flask(__name__)

app.register_blueprint(user_bp,url_prefix='/user')
app.register_blueprint(order_bp)

user.py

from flask import Blueprint

user_bp = Blueprint('user',__name__)


@user_bp.route('/login/')
def login():
    return 'login'

@user_bp.route('/logout/')
def logout():
    return 'logout'

order.py

from flask import Blueprint

order_bp = Blueprint('order',__name__)

@order_bp.route('/add_order/')
def add_order():
    return 'buy_order'


@order_bp.route('/modify_order/')
def modify_order():
    return 'modify_order'

manage.py

from app01 import app


if __name__ == '__main__':
    app.run()

3.4使用蓝图之使用大型系统

这里所谓的大型系统并不是绝对的大型系统,而是相对规整的大型系统,相当于提供了一个参考,在真实的生成环境中会根据公司的项目以及需求,规划自己的目录结构。

文件路径:

│  run.py  
│
│
└─pro_flask  # 文件夹
    │  __init__.py 
    │
    ├─admin  # 文件夹
    │  │  views.py
    │  │  __init__.py
    │  │
    │  ├─static # 文件夹
    │  └─templates  # 文件夹
    │
    └─web   # 文件夹
       │  views.py
       │  __init__.py
       │
       ├─static  # 文件夹
       └─templates # 文件夹

run.py 启动app

from pro_flask import app

if __name__ == '__main__':
    app.run()

__init__.py 实例化核心类,导入蓝图对象,注册蓝图。

from flask import Flask
from .admin import admin
from .web import web

app = Flask(__name__)
app.debug = True

app.register_blueprint(admin, url_prefix='/admin')
app.register_blueprint(web)

admin.views.py 完成注册路由以及视图函数

from . import admin


@admin.route('/index')
def index():
    return 'Admin.Index'

admin.__init__.py 生成蓝图对象导入views,使得views的代码运行完成注册路由

from flask import Blueprint

admin = Blueprint(
    'admin',
    __name__,
    template_folder='templates',
    static_folder='static'
)
from . import views

web文件夹下和admin文件夹下目录构成完全一致,这里就不举例子了。

四、flask之协程和线程

假设每一个请求进来的时候都开一个进程肯定不合理,那么如果每一个请求进来都是串行的根本实现不了并发,所以我们假定每一个请求进来使用的是线程。但是线程中数据互相不隔离,存在修改数据的时候数据不安全的问题。

假定我们的需求是,每个线程都要设置值,并且该线程打印该线程修改的值。

from threading import Thread,current_thread
import time

class Foo(object):
    def __init__(self):
        self.name = 0

locals_values = Foo()

def func(num):
    locals_values.name = num
    time.sleep(2)             # 取出该线程的名字
    print(locals_values.name, current_thread().name)

for i in range(10):
                                    # 设置该线程的名字
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

很明显阻塞了2秒的时间所有的线程都完成了修改值,而2秒后所有的线程打印出来的时候都是9了,这就产生了数据不安全的问题。

所以我们要解决这种线程不安全的问题,有如下两种解决方案。

  • 方案一:是加锁
  • 方案二:使用threading.local对象把要修改的数据复制一份,使得每个数据互不影响。 我们要实现的并发是多个请求实现并发,而不是纯粹的只是修改一个数据,所以第二种思路更适合做我们每个请求的并发,把每个请求对象的内容都复制一份让其互相不影响。 详解:为什么不用加锁的思路?加锁的思路是多个线程要真正实现共用一个数据,并且该线程修改了数据之后会影响到其他线程,更适合类似于12306抢票的应用场景,而我们是要做请求对象的并发,想要实现的是该线程对于请求对象这部分内容有任何修改并不影响其他线程。所以使用方案二

4.1threading.local

多个线程修改同一个数据,复制多份数据给每个线程用,为每个线程开辟一块空间进行数据存储

实例:

from threading import Thread,current_thread,local
import time

locals_values = local()
# 可以简单理解为,识别到新的线程的时候,都会开辟一片新的内存空间,相当于每个线程对该值进行了拷贝。

def func(num):
    locals_values.name = num
    time.sleep(2)
    print(locals_values.name, current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

如上通过threading.local实例化的对象,实现了多线程修改同一个数据,每个线程都复制了一份数据,并且修改的也都是自己的数据。达到了我们想要的效果。

4.2通过字典自定义threading.local

实例:

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    storage = {}# 初始化一个字典
    get_ident = get_ident # 拿到get_ident的地址
    def set(self,k,v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def get(self,k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    locals_values.set('KEY',num)
    time.sleep(2)
    print(locals_values.get('KEY'),current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

利用get_ident()获取每个线程的唯一标记作为键,然后组织一个字典storage。

:{线程1的唯一标记:{k:v},线程2的唯一标记:{k:v}.......}

 {
    15088: {'KEY': 0}, 
    8856: {'KEY': 1},
    17052: {'KEY': 2}, 
    8836: {'KEY': 3}, 
    13832: {'KEY': 4}, 
    15504: {'KEY': 5}, 
    16588: {'KEY': 6}, 
    5164: {'KEY': 7}, 
    560: {'KEY': 8}, 
    1812: {'KEY': 9}
                    }

运行效果

4.3通过setattr和getattr实现自定义threthreading.local

实例

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    storage = {}# 初始化一个字典
    get_ident = get_ident # 拿到get_ident的地址

    def __setattr__(self, k, v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

提示:

4.4每个对象有自己的存储空间(字典)

我们可以自定义实现了threading.local的功能,但是现在存在一个问题,如果我们想生成多个Local对象,但是会导致多个Local对象所管理的线程设置的内容都放到了类属性storage = {}里面,所以我们如果想实现每一个Local对象所对应的线程设置的内容都放到自己的storage里面,就需要重新设计代码。

实例:

from threading import get_ident,Thread,current_thread
# get_ident()可以获取每个线程的唯一标记,
import time

class Local(object):
    def __init__(self):
        # 千万不要按照注释里这么写,否则会造成递归死循环,死循环在__getattr__中,不理解的话可以全程使用debug测试。
        # self.storage = {}
        # self.get_ident =get_ident
        object.__setattr__(self,"storage",{})
        object.__setattr__(self,"get_ident",get_ident) #借用父类设置对象的属性,避免递归死循环。

    def __setattr__(self, k, v):
        ident =self.get_ident()# 获取当前线程的唯一标记
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 获取当前线程的唯一标记
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
locals_values2 = Local()
def func(num):
    # get_ident() 获取当前线程的唯一标记
    # locals_values.set('KEY',num)
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)
    # print('locals_values2.storage:',locals_values2.storage) #查看locals_values2.storage的私有的storage

for i in range(10):
    t = Thread(target=func,args=(i,),name='线程%s'%i)
    t.start()

显示效果我们就不做演示了,和前几个案例演示效果一样。

4.5如何设计flask的请求并发?

  • 情况一:单进程单线程,基于全局变量就可以做
  • 情况二:单进程多线程,基于threading.local对象做
  • 情况三:单进程多线程多协程,如何做? 提示:协程属于应用级别的,协程会替代操作系统自动切换遇到 IO的任务或者运行级别低的任务,而应用级别的切换速度远高于操作系统的切换 当然如果是自己来设计框架,为了提升程序的并发性能,一定是上诉的情况三,不光考虑多线程并且要多协程,那么该如何设计呢? 在我们的flask中为了这种并发需求,依赖于底层的werkzeug外部包,werkzeug实现了保证多线程和多携程的安全,werkzeug基本的设计理念和上一个案例一致,唯一的区别就是在导入的时候做了一步处理,且看werkzeug源码。 werkzeug.local.py部分源码 ... try: from greenlet import getcurrent as get_ident # 拿到携程的唯一标识 except ImportError: try: from thread import get_ident #线程的唯一标识 except ImportError: from _thread import get_ident class Local(object): ... def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) ... def __getattr__(self, name): try: return self.__storage__[self.__ident_func__()][name] except KeyError: raise AttributeError(name) def __setattr__(self, name, value): ident = self.__ident_func__() storage = self.__storage__ try: storage[ident][name] = value except KeyError: storage[ident] = {name: value} 讲解: 原理就是在最开始导入线程和协程的唯一标识的时候统一命名为get_ident,并且先导入协程模块的时候如果报错说明不支持协程,就会去导入线程的get_ident,这样无论是只有线程运行还是协程运行都可以获取唯一标识,并且把这个标识的线程或协程需要设置的内容都分类存放于__storage__字典中。

五、偏函数

当函数的参数个数太多,需要简化时,使用functools.partial可以创建一个新的函数,这个新函数可以固定住原函数的部分参数,从而在调用时更简单。

from functools import partial
def func(a1,a2,a3):
    print(a1,a2,a3)


new_func1 = partial(func,a1=1,a2=2)
new_func1(a3=3)

new_func2 = partial(func,1,2)
new_func2(3)

new_func3 = partial(func,a1=1)
new_func3(a2=2,a3=3)

注意:partial括号内第一个参数是原函数,其余参数是需要固定的参数

效果图

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Celery的使用

    Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度,可用于处理实时数据以及任务调度。

    GH
  • Flask框架(一)

    Flask是一个python编写的web微框架,使用flask我们可以使用python快速实现一个网站或web服务。

    GH
  • css(2)

    font-family可以将多个字体名保存起来,如果浏览器不支持第一个字体会依次尝试后面的字体。

    GH
  • Android 组件化开源app -开眼短视频(OpenEyes)

    该开源项目采用组件化的方式开发,使用MVVM + AndroidX + jetpack 组件为基本架构进行开发。

    darryrzhong
  • 爆:谷歌为移动应用(App)发布顶级域名,这一次别再错过了

    今天,我们(谷歌)发布 . app,Google 注册表中最新的顶级域名(TLD)。

    非著名程序员
  • app中的webview通识篇(上)

    如果你还是第一次与app合作开发webview的页面,那么对于如何调试,可能有哪些问题可能是不够了解的。本文尝试性的根据自己的经验给大家一个入门级别的了解,如果...

    RobinsonZhang
  • sanic(1):创建app

    sanic是一个非常NB的高性能python框架。最近正好公司有一个小项目。所以用sanic来试试手是很不错的了。 由于sanic的中文资料和项目还很少很少,...

    超级大猪
  • 一个 2 年 Android 开发者的 18 条忠告

    我仍记得2014年我决定做安卓开发的那天,这是我一生中做出的最好决定之一。到现在已经有2年半了, 最初的时候,并没有人告诉我如何做才是正确的。我犯了很多错误,浪...

    我就是马云飞
  • Go 每日一库之 go-app

    go-app是一个使用 Go + WebAssembly 技术编写渐进式 Web 应用的库。WebAssembly 是一种可以运行在现代浏览器中的新式代码。近两...

    用户7731323
  • 人工智能会带来一个没有app的世界吗?

    ? 现在很少有人智能手机中装满了app——并且这样的人也变得越来越少。App正在逐渐衰落。根据Comscore的分析师,大部分人(65%)根本不下载app,只...

    新智元

扫码关注云+社区

领取腾讯云代金券