活动买了3年的服务器闲置,就想着可以利用做点什么,恰好在了解 Golang ,那么就用Golang写一个后端,Reat做前端,用一个小项目“图片分享”站点穿刺一下。从产品的角度来看,提供批量上传、瀑布流、木桶布局、瀑布流三大功能,上传一些图片供网友鉴赏。
所谓木桶布局,就是指许多长宽比例不一致的图片,通过调整他们的大小,使得他们可以整齐地排列在网页中,并随着浏览器窗口的变化而自动适应调整。话不啰嗦,开始我们的实践之旅。
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 渲染图片集合。
以上动画就是最终的实现效果,当然要达到生产级的要求还有很多优化的空间,比如按照图片比例进行排序,使一行图片尽量保持各比例图片的均匀等等。你还知道什么实现方法,欢迎留言讨论。