您的位置 首页 golang

React+Golang木桶布局实战

活动买了3年的服务器闲置,就想着可以利用做点什么,恰好在了解 Golang ,那么就用Golang写一个后端,Reat做前端,用一个小项目“图片分享”站点穿刺一下。从产品的角度来看,提供批量上传、瀑布流、木桶布局、瀑布流三大功能,上传一些图片供网友鉴赏。

Google木桶布局

所谓木桶布局,就是指许多长宽比例不一致的图片,通过调整他们的大小,使得他们可以整齐地排列在网页中,并随着浏览器窗口的变化而自动适应调整。话不啰嗦,开始我们的实践之旅。

Golang后端API

功能列表: 图片上传(类型检测、缩略图生成、 md5 去重)、单图片原图访问(基于Base62的图片ID)、图片分页;

依赖类库: Gin(Web服务器)、Imaging(图片裁剪处理)、filetype(文件类型检测)、sqlx(数据库访问框架)、go- SQLite 3(驱动)、yaml.v2(读取yml配置文件);

首先定义配置文件,包括服务器的监听地址、端口、数据库访问凭据、图片存储路径:

 # 服务器
server:
    host: "0.0.0.0"
    port: 9000
    mode: 0 
# 数据库
database:
    user: ""
    pass: ""
    path: "./ SQL ite.db"
# 图片存储路径
store:
    base-path: "./data/"  

设计图片存储的表结构(笔者服务器低配,使用SQLite,因此需要自行产生序列):

 -- 图片表
drop table if exists "t_images";
create table "pubimage" (
  "uriid" varchar(128) primary key, -- 短id
  "mime" varchar(64), -- MIME类型
  "file_type" char(32), -- 文件类型
  "local_path" varchar(255), -- 本地存储路径
  "thum_path1" varchar(512), -- 缩略图:规格1
  "store_cls" char(8), -- 存储类型(local\...)
  "meta" varchar(128), -- 文件元数据
  " MD5 sum" varchar(128), -- 文件md5值
  "up_date" date -- 上传时间
);

CREATE INDEX "idx_update"
ON "t_images" (
  "up_date" asc
);
CREATE UNIQUE INDEX "idx_md5"
ON "t_images" (
  "md5sum" asc
);

-- 全局配置表
drop table if exists "t_seq";
create table "t_seq" (
  "key" char(64) primary key, -- 配置key
  "value" varchar(512) -- 配置值
);
-- 图片全局序列
insert into "t_seq" (key, value) values ('seq_pubimage', '1');    

定义与表结构一一对应的实体(这里Golang的Tag非常好用,对应的数据库字段名称、是否将包含在返回Json,通过Tag就能实现):

 type Images struct {
	Uriid     string         `db:" Uri id"`
	Mime      string         `db:"mime"`
	FileType  string         `db:"file_type"`
	LocalPath string         `db:"local_path" json:"-"`
	ThumPath1 string         `db:"thum_path1" json:"-"`
	StoreCls  string         `db:"store_cls"`
	Meta      types.JSONText `db:"meta"`
	MD5Sum    sql.NullString `db:"md5sum" json:"-"`
	UpDate    time.Time      `db:"up_date"`
}

type Seq  Struct  {
	Key   string
	Value string
}  

通过sqlX的QueryRowx、StructScan、Select、MustExec查询和添加数据(处于篇幅原因省略SQL语句,感觉非常好用,有人可能会说复杂SQL不便利,Golang的多行文本提供了不错的可排版性):

 func InsertImage(img Image) {
	db.MustExec(/* SQL */,
		img.Uriid, img.Mime, img.FileType,
		img.LocalPath, img.ThumPath, 
    img.StoreCls, img.Meta, 
    img.MD5Sum.String, time.Now())
}

func GetImgByUriId(uriId string) (*Image, error) {
	var img Image
	row := db.QueryRowx(/* SQL */, uriId)
	err := row.StructScan(&img)
	return &img, err
}

func GetImages(page int) (*[]Image, error) {
	var imgs []Image

	var limit, offset int = Pagesize, 0
	if page > 1 {
		offset = (page - 1) * Pagesize
	}

	log. Printf ("limit: %d, offset: %d \n", limit, offset)
	err := db.Select(&imgs, /* SQL */, limit, offset)

	return &imgs, err
}  

定义对应的yml文件数据struct并加载配置文件(Again:Tag很好用):

 type Config struct {
	Server struct {
		Port string `yaml:"port"`
		Host string `yaml:"host"`
		Mode int    `yaml:"mode"`
	} ` yaml :"server"`
	Database struct {
		Username string `yaml:"user"`
		Password string `yaml:"pass"`
		Path     string `yaml:"path"`
	} `yaml:"database"`
	Store struct {
		BasePath string `yaml:"base-path"`
	} `yaml:"store"`
	 Session  struct {
		CookieName string `yaml:"cookie-name"`
		KeyPairs   string `yaml:"keypairs"`
	} `yaml:"session"`
}

var Yaml Config

func init() {
	f, err := os.Open("conf.yml")
	if err != nil {
		panic(err)
	}
	defer f.Close()
	decoder := yaml.NewDecoder(f)
	err = decoder.Decode(&Yaml)
	if err != nil {
		panic(err)
	}
}  

定义主函数,通过go run main.go运行应用,大功告成:

 func main() {
	r := gin.Default()
	r.GET("/api/pictures/:page", apis.ListPic) 
	r.POST("/api/pictures/upload", apis.PostFile)   // 上传
	r.GET("/i/:query", apis.GetPicture)  // 原图
	r.GET("/thum/:query", apis.GetThum) // 缩略图
	r.Run(":" + config.Yaml.Server.Port)
}  

笔者省略了非关键的代码,通过一般CRUD项目所需要的Entity定义、SQL查询、配置文件读取、API定义代码,你应该可以大致感受到Golang编写项目的优缺点,有一个大概的判断。相信读者朋友可以通过文中使用的类库信息找到所需要的信息。

React 与木桶布局

使用create-react-app创建一个前端工程,为了方便本地开发避免跨域的问题,扩展webpack的配置,添加devServer代理Golang后端的API(webpack.config.js):

 const config = {
    entry: {
      index: './src/index.js'
    },
    devServer: {
      overlay: true,
      proxy: [{
        context: ['/i/*','/thum/*','/api/**'], 
        target : '#39;
      },]
    }
  };
module.exports = config;  

配置好后我们开始编写React组件。木桶布局实现的基本原理是: body中是图片流,每一行图片称之为row,每一行中有多个图片。我们需要遍历要显示的图片,获取图片的宽高信息,然后进行宽度累加,当宽度大于body的宽度时,移除最后一张图片, 并重新计算当前row中的图片高度 。重复这一过程,直到图片全部计算完毕。

如果你通过上一章节构建了后端接口,则返回的数据类型如下,可以看到已经返回了图片的宽高信息(没有实现Golang接口可以利用生成占位符图片):

 {
data: [
  {
    UriId: "4p-E",
    Mime: "image/jpeg",
    FileType: "jpg",
    StoreCls: "LDISK",
    Meta: {
      thum: {
        width: 384,
        height: 240
      }
    },
    UpDate: "2020-04-06T10:18:54"
  }
}  

如果没有返回宽高信息,你需要在img标签的onLoad事件中获取图片的宽高信息,并存储起来以供后续使用:

 let images = []
let image = new Image(); 
image.src = "http:/....."
image.onload = function() {
  let ratio = this.Width / this.height;
  let imageMeta = {
     height: presetHeight,
     width: ratio * presetHeight, 
     ratio: ratio,
   }
   images.push(imageMeta)
}  

计算图片的宽高:关键点在于需要预先设定好row的高度,高度按照你的喜好初始化,宽度就是显示容器body的宽度,每张图片按照预设高度等比调整( 遍历图片,当宽度超出row的设定宽度后进行高度调整 ),当row中的图片宽度之和大于row的宽度,也就是body的宽度时,调整当前row的高度,将row中的图片铺满row的宽度。

 // 加载图片
processImage() {
  let sourceImages = this.sourceImages
  let imgPool = [], imgRow = []
  let wholeWidth = 0 // 该行图片宽度和 

  for(let i = 0; i < sourceImages.length; i++) {
    let img = sourceImages[i]
    imgRow.push(img)
		// 比例
    let ratio = img.Meta.thum.width / img.Meta.thum.height;
    let ratioWidth = ratio * this.rowHeight
    wholeWidth += ratioWidth

    if( wholeWidth > this.rowWidth ) {
      imgRow.pop()
      wholeWidth -= ratioWidth
      // 面积相等原则:计算新的高度
      let nheight = this.rowWidth * this.rowHeight / wholeWidth
      // 将Row中的图片加入渲染数组
      imgPool = imgPool.concat([this.processRowImage(nheight, imgRow)])

      // 重置计算池、宽度、最后一张图片进入下轮
      imgRow = []    
      wholeWidth = ratioWidth
      imgRow.push(img)
    } 
  }

  // 未能填满row的图片
  imgPool = imgPool.concat([this.processRowImage(this.rowHeight, imgRow)])
  this.setState({ 
    showImgs: imgPool
  })
}

// row中图片未占满row的宽度重新计算每张图片都宽度
processRowImage(nHeight, rowImages) {
  let newRowImgs = []
  for (const idx in rowImages) {

    let ratio = rowImages[idx].Meta.thum.height / rowImages[idx].Meta.thum.width

    let divWidth = (nHeight / rowImages[idx].Meta.thum.height) * rowImages[idx].Meta.thum.width
    let imgWidth = divWidth - 6 // 处理图片间的间隙宽度
    let imgHeight = imgWidth * ratio

    newRowImgs.push({
      src: '/thum/' + rowImages[idx].Uriid,
      divH: nHeight,
      divW: divWidth,
      imgH: imgHeight,
      imgW: imgWidth,
    })
  }
  return newRowImgs
}  

调整图片高度的基于简单的面积相等原则 ,重新计算出row的高度,当image高度被设定后其宽度等比调整。经过上面的步骤会得到一个二维数组Array[m][n],m是row,n是row中的image信息。image里面包含src、height等属性,接着使用 JSX 渲染图片集合。

GIF动画效果

以上动画就是最终的实现效果,当然要达到生产级的要求还有很多优化的空间,比如按照图片比例进行排序,使一行图片尽量保持各比例图片的均匀等等。你还知道什么实现方法,欢迎留言讨论。

文章来源:智云一二三科技

文章标题:React+Golang木桶布局实战

文章地址:https://www.zhihuclub.com/87100.shtml

关于作者: 智云科技

热门文章

网站地图