1. 权限管理

Casbin是用于Golang项目的功能强大且高效的开源访问控制库。

官网

https://docs.casbin.cn/zh/docs/get-started

编辑器测试

https://docs.casbin.cn/zh/editor

1.1.1. 特征

Casbin的作用

以经典{subject, object, action}形式或您定义的自定义形式实施策略,同时支持允许和拒绝授权。
处理访问控制模型及其策略的存储。
管理角色用户映射和角色角色映射(RBAC中的角色层次结构)。
支持内置的超级用户,例如root或administrator。超级用户可以在没有显式权限的情况下执行任何操作。
多个内置运算符支持规则匹配。例如,keyMatch可以将资源键映射/foo/bar到模式/foo*。

Casbin不执行的操作

身份验证(又名验证username以及password用户登录时)
管理用户或角色列表。我相信项目本身管理这些实体会更方便。用户通常具有其密码,而Casbin并非设计为密码容器。但是,Casbin存储RBAC方案的用户角色映射。

1.1.2. 怎么运行的

在Casbin中,基于PERM元模型(策略,效果,请求,匹配器)将访问控制模型抽象为CONF文件。因此,切换或升级项目的授权机制就像修改配置一样简单。您可以通过组合可用的模型来定制自己的访问控制模型。例如,您可以在一个模型中同时获得RBAC角色和ABAC属性,并共享一组策略规则。

Casbin中最基本,最简单的模型是ACL。ACL的CONF模型为:

#请求定义 sub:用户 obj:模块 act:请教方式 GET / POST 等等
[request_definition]
r = sub,obj,act #策略定义
[policy_definition]
p = sub,obj,act #政策效果
[policy_effect]
e = some(where (p.eft == allow)) #匹配器
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act ACL模型的示例策略如下: p, alice, data1, read
p, bob, data2, write

1.1.3. 安装

go get github.com/casbin/casbin

1. 示例代码 xormadapter

package main

import (
"fmt"
"log" "github.com/casbin/casbin"
xormadapter "github.com/casbin/xorm-adapter"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
) func main() {
// 要使用自己定义的数据库rbac_db,最后的true很重要.默认为false,使用缺省的数据库名casbin,不存在则创建
a, err := xormadapter.NewAdapter("mysql", "root:root@tcp(127.0.0.1:3306)/goblog?charset=utf8", true)
if err != nil {
log.Printf("连接数据库错误: %v", err)
return
}
e, err := casbin.NewEnforcer("./rbac_models.conf", a)
if err != nil {
log.Printf("初始化casbin错误: %v", err)
return
}
//从DB加载策略
e.LoadPolicy() //获取router路由对象
r := gin.New() r.POST("/api/v1/add", func(c *gin.Context) {
fmt.Println("增加Policy")
if ok, _ := e.AddPolicy("admin", "/api/v1/hello", "GET"); !ok {
fmt.Println("Policy已经存在")
} else {
fmt.Println("增加成功")
}
})
//删除policy
r.DELETE("/api/v1/delete", func(c *gin.Context) {
fmt.Println("删除Policy")
if ok, _ := e.RemovePolicy("admin", "/api/v1/hello", "GET"); !ok {
fmt.Println("Policy不存在")
} else {
fmt.Println("删除成功")
}
})
//获取policy
r.GET("/api/v1/get", func(c *gin.Context) {
fmt.Println("查看policy")
list := e.GetPolicy()
for _, vlist := range list {
for _, v := range vlist {
fmt.Printf("value: %s, ", v)
}
}
})
//使用自定义拦截器中间件
r.Use(Authorize(e))
//创建请求
r.GET("/api/v1/hello", func(c *gin.Context) {
fmt.Println("Hello 接收到GET请求..")
}) r.Run(":9000") //参数为空 默认监听8080端口
} //拦截器
func Authorize(e *casbin.Enforcer) gin.HandlerFunc { return func(c *gin.Context) { //获取请求的URI
obj := c.Request.URL.RequestURI()
//获取请求方法
act := c.Request.Method
//获取用户的角色
sub := "admin" //判断策略中是否存在
if ok, _ := e.Enforce(sub, obj, act); ok {
fmt.Println("恭喜您,权限验证通过")
c.Next()
} else {
fmt.Println("很遗憾,权限验证没有通过")
c.Abort()
}
}
}

2. 示例代码 gormadapter

package main

import (
"fmt"
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"log"
) var roleKey = "admin" func main() {
DB, err := gormadapter.NewAdapter("mysql", "root:123456@tcp(127.0.0.1:3306)/casbin?charset=utf8mb4&parseTime=True&loc=Local", true) // Your driver and data source.
if err != nil {
log.Printf("连接数据库错误: %v", err)
return
}
// 加载RBAC.conf
e, _ := casbin.NewEnforcer("./rbac_models.conf", DB) //从DB加载策略
e.LoadPolicy() //获取router路由对象
r := gin.New() //获取policy
// curl -X GET 127.0.0.1:9000/api/v1/get
r.GET("/api/v1/get", func(c *gin.Context) {
fmt.Println("查看policy")
// 查所中所有的策略
list := e.GetPolicy()
for _, vlist := range list {
fmt.Printf("================ \n ") fmt.Printf("value: %s \n ", vlist) fmt.Printf("================循环打印 \n ")
//for _, v := range vlist {
// fmt.Printf("value: %s \n ", v)
//
//}
}
c.JSON(200, list)
}) // curl -X POST 127.0.0.1:9000/api/v1/add
r.POST("/api/v1/add", func(c *gin.Context) {
fmt.Println("增加Policy") //单个添加策略
//_, err:= e.AddPolicy(roleKey, "/api/v1/hello", "GET")
//_, err = cb.AddNamedPolicy("p", roleKey, "/api/v1/hello", "GET") policies := [][]string{
{roleKey, "/api/v1/hello", "GET"},
{roleKey, "/api/v1/add", "POST"},
{roleKey, "/api/v1/delete", "DELETE"},
{roleKey, "/api/v1/delete", "DELETE"},
}
polices := GetUniqPolices(policies)
if len(polices) == 0 {
fmt.Println("polices is empty")
return
} // 删除 p0是admin角色的所有权限
_, err = e.RemoveFilteredPolicy(0, roleKey)
if err != nil {
fmt.Println("err:", err)
return
}
//批量删除 方式一
ok, err := e.AddPolicies(polices)
//批量删除 方式二
//ok, err := e.AddNamedPolicies("p", policies) if err != nil {
fmt.Println("err:", err)
return
}
if !ok {
fmt.Println("err:", err)
fmt.Println("Policy已经存在")
c.JSON(200, "Policy已经存在")
} else {
fmt.Println("增加成功")
c.JSON(200, "增加成功")
}
}) //删除policy
//curl -X DELETE 127.0.0.1:9000/api/v1/detele
r.DELETE("/api/v1/delete", func(c *gin.Context) {
fmt.Println("删除Policy") // 单条删除
//ok, err := e.RemovePolicy(roleKey, "/api/v1/hello", "GET") // 批量删除 p0是admin角色的所有权限
ok, err := e.RemoveFilteredPolicy(0, "admin")
if err != nil {
fmt.Println("err:", err)
return
}
if !ok {
fmt.Println("Policy不存在")
c.JSON(200, "Policy不存在")
} else {
fmt.Println("删除成功")
c.JSON(200, "删除成功")
}
}) //使用自定义拦截器中间件
r.Use(Authorize(e)) //创建请求
//curl -X GET 127.0.0.1:9000/api/v1/hello
r.GET("/api/v1/hello", func(c *gin.Context) {
fmt.Println("Hello 接收到GET请求..")
c.JSON(200, "Hello 接收到GET请求..")
}) r.Run(":9000") //参数为空 默认监听8080端口
} //拦截器
func Authorize(e *casbin.Enforcer) gin.HandlerFunc { return func(c *gin.Context) { //获取请求的URI
obj := c.Request.URL.RequestURI()
//获取请求方法
act := c.Request.Method
//获取用户的角色
sub := roleKey
fmt.Println("====>", sub, obj, act)
//判断策略中是否存在
if ok, _ := e.Enforce(sub, obj, act); ok {
fmt.Println("恭喜您,权限验证通过")
c.JSON(200, "恭喜您,权限验证通过")
c.Next()
} else {
fmt.Println("很遗憾,权限验证没有通过")
c.JSON(200, "很遗憾,权限验证没有通过") c.Abort()
}
}
} //添加权限去重复
func GetUniqPolices(polices [][]string) (uniqPolices [][]string) {
mp := make(map[string]interface{}, 0)
//polices := make([][]string, 0)
for _, api := range polices {
rKey := api[0]
path := api[1]
action := api[2]
mapKey := rKey + "-" + path + "-" + action
if mp[mapKey] != "" {
mp[mapKey] = ""
uniqPolices = append(uniqPolices, []string{rKey, path, action})
}
}
return
}

3. 示例代码 gormadapter + gorm

package main

import (
"github.com/casbin/casbin/v2"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"os"
"time" gormadapter "github.com/casbin/gorm-adapter/v3"
//_ "github.com/go-sql-driver/mysql" "github.com/gin-gonic/gin" "fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
) var roleKey = "admin" func main() {
// 方式一 使用casbin 包的方式连接 mysql
// 此方式使用 以下包
//gormadapter "github.com/casbin/gorm-adapter/v3"
//_ "github.com/go-sql-driver/mysql" 包
// 创建表casbin_rule表
//DB, err := gormadapter.NewAdapter("mysql", "root:123456@tcp(127.0.0.1:3306)/casbin?charset=utf8mb4&parseTime=True&loc=Local", true) // Your driver and data source.
//DB, err := gormadapter.NewAdapter("mysql", "root:123456@tcp(127.0.0.1:3306)/casbin?charset=utf8mb4&parseTime=True&loc=Local", "sys_casbin_rule", true) // 指定表名sys_casbin_rule,不存在时创建表 // 方式二 使用gorm包的方式连接mysql
// 此方式使用 以下包
//gormadapter "github.com/casbin/gorm-adapter/v3"
//"gorm.io/driver/mysql"
//"gorm.io/gorm"
//"gorm.io/gorm/logger"
//"gorm.io/gorm/schema"
InitMysql() //init db
// 创建表casbin_rule表
DB, err := gormadapter.NewAdapterByDBUseTableName(Orm, "sys", "casbin_rule") // 指定表名sys_casbin_rule if err != nil {
log.Printf("连接数据库错误: %v", err)
return
} // 加载RBAC.conf
e, _ := casbin.NewEnforcer("./rbac_models.conf", DB) //从DB加载策略
e.LoadPolicy() //获取router路由对象
r := gin.New() //获取policy
// curl -X GET 127.0.0.1:9000/api/v1/get
r.GET("/api/v1/get", func(c *gin.Context) {
fmt.Println("查看policy")
// 查所中所有的策略
list := e.GetPolicy()
for _, vlist := range list {
fmt.Printf("================ \n ") fmt.Printf("value: %s \n ", vlist) fmt.Printf("================循环打印 \n ")
//for _, v := range vlist {
// fmt.Printf("value: %s \n ", v)
//
//}
}
c.JSON(200, list)
}) // curl -X POST 127.0.0.1:9000/api/v1/add
r.POST("/api/v1/add", func(c *gin.Context) {
fmt.Println("增加Policy") //单个添加策略
//_, err:= e.AddPolicy(roleKey, "/api/v1/hello", "GET")
//_, err = cb.AddNamedPolicy("p", roleKey, "/api/v1/hello", "GET") policies := [][]string{
{roleKey, "/api/v1/hello", "GET"},
{roleKey, "/api/v1/add", "POST"},
{roleKey, "/api/v1/delete", "DELETE"},
{roleKey, "/api/v1/delete", "DELETE"},
}
polices := GetUniqPolices(policies)
if len(polices) == 0 {
fmt.Println("polices is empty")
return
} // 删除 p0是admin角色的所有权限
_, err = e.RemoveFilteredPolicy(0, roleKey)
if err != nil {
fmt.Println("err:", err)
return
}
//批量删除 方式一
ok, err := e.AddPolicies(polices)
//批量删除 方式二
//ok, err := e.AddNamedPolicies("p", policies) if err != nil {
fmt.Println("err:", err)
return
}
if !ok {
fmt.Println("err:", err)
fmt.Println("Policy已经存在")
c.JSON(200, "Policy已经存在")
} else {
fmt.Println("增加成功")
c.JSON(200, "增加成功")
}
}) //删除policy
//curl -X DELETE 127.0.0.1:9000/api/v1/detele
r.DELETE("/api/v1/delete", func(c *gin.Context) {
fmt.Println("删除Policy") // 单条删除
//ok, err := e.RemovePolicy(roleKey, "/api/v1/hello", "GET") // 批量删除 p0是admin角色的所有权限
ok, err := e.RemoveFilteredPolicy(0, "admin")
if err != nil {
fmt.Println("err:", err)
return
}
if !ok {
fmt.Println("Policy不存在")
c.JSON(200, "Policy不存在")
} else {
fmt.Println("删除成功")
c.JSON(200, "删除成功")
}
}) //使用自定义拦截器中间件
r.Use(Authorize(e)) //创建请求
//curl -X GET 127.0.0.1:9000/api/v1/hello
r.GET("/api/v1/hello", func(c *gin.Context) {
fmt.Println("Hello 接收到GET请求..")
c.JSON(200, "Hello 接收到GET请求..")
}) r.Run(":9000") //参数为空 默认监听8080端口
} //拦截器
func Authorize(e *casbin.Enforcer) gin.HandlerFunc { return func(c *gin.Context) { //获取请求的URI
obj := c.Request.URL.RequestURI()
//获取请求方法
act := c.Request.Method
//获取用户的角色
sub := roleKey
fmt.Println("====>", sub, obj, act)
//判断策略中是否存在
if ok, _ := e.Enforce(sub, obj, act); ok {
fmt.Println("恭喜您,权限验证通过")
c.JSON(200, "恭喜您,权限验证通过")
c.Next()
} else {
fmt.Println("很遗憾,权限验证没有通过")
c.JSON(200, "很遗憾,权限验证没有通过") c.Abort()
}
}
} //添加权限去重复
func GetUniqPolices(polices [][]string) (uniqPolices [][]string) {
mp := make(map[string]interface{}, 0)
//polices := make([][]string, 0)
for _, api := range polices {
rKey := api[0]
path := api[1]
action := api[2]
mapKey := rKey + "-" + path + "-" + action
if mp[mapKey] != "" {
mp[mapKey] = ""
uniqPolices = append(uniqPolices, []string{rKey, path, action})
}
}
return
} var Orm *gorm.DB const (
username = "root"
password = "123456"
host = "127.0.0.1"
port = "3306"
dbName = "casbin"
maxIdleConns = 10 //设置空闲连接池中连接的最大数量
maxOpenConns = 100 //设置打开数据库连接的最大数量。 appMode = "debug" //debug 开发模式,test 测试模式 release 生产模式 ) // 初始化数据库并产生数据库全局变量
func InitMysql() {
// 默认的级别,会打印find找不到模型时的sql语句。
logMode := logger.Info
aMode := "debug"
if appMode == aMode {
logMode = logger.Silent
}
// Silent 就不会。
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logMode, // gorm日志模式:silent 可选 Silent,Error,Warn,Info
Colorful: true, // Disable color true/false
},
)
//"root:123456@tcp(127.0.0.1:3306)/gorm_class?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
dns := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
username,
password,
host,
port,
dbName,
)
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: dns,
DefaultStringSize: 256, //string 类型字段的默认长度 // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, //根据版本自动配置 // 根据当前 MySQL 版本自动配置
}), &gorm.Config{
PrepareStmt: true, //Logger: logger.Default.LogMode(logMode), // gorm日志模式:Silent / Info(控制台显示日志) / Error / Warn
Logger: newLogger, DisableForeignKeyConstraintWhenMigrating: true, // 外键约束 //SkipDefaultTransaction: true, // 禁用默认事务(提高运行速度)
NamingStrategy: schema.NamingStrategy{
// 使用单数表名,启用该选项,此时,`User` 的表名应该是 `user`
SingularTable: true,
},
})
if err != nil {
//fmt.Println(utils.Red("gorm.Open err:"+ err.Error()))
//os.Exit(1)
panic(fmt.Sprintf("gorm.Open err:%v \n", err))
return
}
//Db = db
Orm = db
sqlDB, _ := db.DB()
// Enable Logger, show detailed log
sqlDB.SetMaxIdleConns(maxIdleConns) //设置空闲连接池中连接的最大数量
sqlDB.SetMaxOpenConns(100) //设置打开数据库连接的最大数量。
sqlDB.SetConnMaxLifetime(maxOpenConns * time.Second) //设置了连接可复用的最大时间。 //autoMigrate() //迁移文件 go func() {
time.Sleep(2 * time.Second)
fmt.Println("TIDB Database connection succeeded")
}()
}

rbac_models.conf里面的内容如下:

# 请求定义 sub:用户 obj:模块 act:请教方式 GET / POST 等等
[request_definition]
r = sub, obj, act # 策略定义
[policy_definition]
p = sub, obj, act
#p = sub, obj, act, eft # 多写一个eft可以使用下面不同的 policy effects # 角色域 rbac模型才有[role_definition]
[role_definition]
g = _, _ # 表示以角色为基础
#g = _, _,_ # 表示以域(商户+角色)为基础,多商户模式 # 政策效果
[policy_effect]
e = some(where (p.eft == allow)) # 有一个allow的(通过) 返回true
#e = !some(where (p.eft == deny)) # 没有一个deny(阻止)的 返回true
#e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) #有一条通过,并且没有阻止的 # 匹配器
[matchers]
# m = r.sub == p.sub && r.obj == p.obj && r.act == p.act # ACL模型
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act # RBAL模型 # Policy
# p, alice, data1, read
# p, bob, data2, write
# p, alice, data1, write, deny
# p, alice, data1, write, allow # Request
# alice, data1,write

配置链接数据库不需要手动创建数据库,系统自动创建casbin_rule表

支持的 policy effects 如下:

Policy effect定义 意义 示例 意义
some(where (p.eft == allow)) allow-override ACL, RBAC, etc. 有一个allow的(通过) 返回true
!some(where (p.eft == deny)) deny-override 拒绝改写 没有一个deny(阻止)的 返回true
some(where (p.eft == allow)) && !some(where (p.eft == deny)) allow-and-deny 同意与拒绝 有一条通过,并且没有阻止的
priority(p.eft) || deny priority 优先级
subjectPriority(p.eft) 基于角色的优先级 主题优先级

1.1.4. 请求接口

postman

使用postman请求http://localhost:9000/api/v1/hello

运行解决结果显示为很遗憾,权限验证没有通过

下面我在数据表中添加数据在演示的时候可以直接手动按照图片的格式直接添加数据表,或者使用postman POST方式请求http://localhost:9000/api/v1/add

然后继续请求http://localhost:9000/api/v1/hello

.http

使用.http文件请求

resq-api.http

GET http://localhost:9000/api/v1/hello
Accept: application/json ### GET http://localhost:9000/api/v1/get
Accept: application/json ### POST http://localhost:9000/api/v1/add
Accept: application/json ### DELETE http://localhost:9000/api/v1/delete
Accept: application/json

原文地址:

http://www.topgoer.com/gin框架/其他/权限管理.html

Golang 之 casbin(权限管理)的更多相关文章

  1. Casbin权限模型

    权限框架casbin1.概述Casbin是一个强大的.高效的开源访问控制框架,其权限管理机制支持多种访问控制模型. Casbin支持以下编程语言: Casbin可以做到:支持自定义请求的格式,默认的请 ...

  2. Android权限管理之RxPermission解决Android 6.0 适配问题

    前言: 上篇重点学习了Android 6.0的运行时权限,今天还是围绕着Android 6.0权限适配来总结学习,这里主要介绍一下我们公司解决Android 6.0权限适配的方案:RxJava+RxP ...

  3. Android权限管理之Android 6.0运行时权限及解决办法

    前言: 今天还是围绕着最近面试的一个热门话题Android 6.0权限适配来总结学习,其实Android 6.0权限适配我们公司是在今年5月份才开始做,算是比较晚的吧,不过现在Android 6.0以 ...

  4. Android权限管理之Permission权限机制及使用

    前言: 最近突然喜欢上一句诗:"宠辱不惊,看庭前花开花落:去留无意,望天空云卷云舒." 哈哈~,这个和今天的主题无关,最近只要不学习总觉得生活中少了点什么,所以想着围绕着最近面试过 ...

  5. SpringMVC+Shiro权限管理【转】

    1.权限的简单描述 2.实例表结构及内容及POJO 3.Shiro-pom.xml 4.Shiro-web.xml 5.Shiro-MyShiro-权限认证,登录认证层 6.Shiro-applica ...

  6. Android6.0运行时权限管理

    自从Android6.0发布以来,在权限上做出了很大的变动,不再是之前的只要在manifest设置就可以任意获取权限,而是更加的注重用户的隐私和体验,不会再强迫用户因拒绝不该拥有的权限而导致的无法安装 ...

  7. Oracle 表空间和用户权限管理

    一. 表空间 Oracle数据库包含逻辑结构和物理结构. 数据库的物理结构指的是构成数据库的一组操作系统文件. 数据库的逻辑结构是指描述数据组织方式的一组逻辑概念以及它们之间的关系. 表空间是数据库逻 ...

  8. [Django]用户权限学习系列之权限管理界面实现

    本系列前三章: http://www.cnblogs.com/CQ-LQJ/p/5604331.htmlPermission权限基本操作指令 http://www.cnblogs.com/CQ-LQJ ...

  9. [Django]用户权限学习系列之设计自有权限管理系统设计思路

    若在阅读本片文章遇到权限操作问题,请查看本系列的前两章! http://www.cnblogs.com/CQ-LQJ/p/5609690.html和http://www.cnblogs.com/CQ- ...

  10. 我的MYSQL学习心得(十三) 权限管理

    我的MYSQL学习心得(十三) 权限管理 我的MYSQL学习心得(一) 简单语法 我的MYSQL学习心得(二) 数据类型宽度 我的MYSQL学习心得(三) 查看字段长度 我的MYSQL学习心得(四) ...

随机推荐

  1. KingbaseES V8R6 集群运维案例--备库timeline not contain minimum recovery point故障

    ​ 案例现象: KingbaseES V8R6集群备库启动后,加入集群失败,sys_log日志信息提示,如下图所示: 适用版本: kingbaseES V8R6 一.问题分析 在timeline对应的 ...

  2. Scala 模式匹配拓展

    1 package chapter08 2 3 object Test03_MatchTupleExtend { 4 def main(args: Array[String]): Unit = { 5 ...

  3. #欧拉回路#AT4518 [AGC032C] Three Circuits

    题目 给定一个 \(n\) 个点,\(m\) 条边的简单无向连通图, 问是否能将边分成三部分,使每部分都能成为环 分析 每个点的度数都得为偶数,这不由得想到了欧拉回路. 如果整张图是一个简单环那么一定 ...

  4. USACO 4.1

    目录 洛谷 2737 麦香牛块 分析 代码 洛谷 2738 篱笆回路 分析 代码 麦香牛块洛谷传送门,麦香牛块USACO传送门,篱笆回路洛谷传送门,篱笆回路USACO传送门 洛谷 2737 麦香牛块 ...

  5. Android Compose 入门,深入底层源码分析

    Android Compose 入门,深入底层源码分析 我是跟着AS官网学习的,但是官方的教程写的不是很详细.官网链接 首先创建一个Compose项目,目录结构是这样: ui -> theme ...

  6. 双端队列的基本实现【数据结构与算法—TypeScript 实现】

    笔记整理自 coderwhy 『TypeScript 高阶数据结构与算法』课程 特性 本质:允许队列在两端进行 入队 和 出队 操作 设计 实现方式:基于 数组 实现 属性: data:存放队列元素 ...

  7. mongodb基础整理篇————聚合操作[三]

    前言 简单整理一下聚合操作. 正文 什么是聚合框架: 作用于一个或多个集合上 对集合的数据进行的一系列运算 将这些数据转换为期望的形式 从效果而言, 聚合框架相当于SQL 查询中的: Group By ...

  8. 重新整理数据结构与算法(c#)—— 顺序存储二叉树[十九]

    前言 二叉树顺序存bai储是二叉树的一种存储方式.将二du叉树存储在一zhi个数组中,通过存储元素的下dao标反映元素之间的父子关系. 正文 这个概念比较简单,比如一个节点的在数组的index是x,那 ...

  9. 房屋设计H51图纸

  10. 一些奇奇怪怪的js知识

    0.关于前端为什么typeof null 得到的结果是 object 对于 null 来说,很多人会认为他是个对象类型,其实这是错误的. 虽然 `typeof null` 会输出 `object`,但 ...