npm run pre 安装依赖resource 目录中config.json配置(如有必要),运行 npm run start,自动完成部署
注意:码云开启Pages需要手持身份证照片实名,审核大概要一个工作日时间个人体验下来 Github 的访问速度比码云更快,当然有被墙的风险,我是绑了自己的域名并加了一层腾讯云免费CDN,应该还是比较稳定的。另外码云不会自动更新网站,要手动进仓库操作,并且还不能自定义域名,居然要收钱。。关键访问速度本就一般。。小了呀。
项目运行起来后大概是这个样子:
网速慢图片多挂着不管就行,提交好了Github会自动部署,部署完毕commit记录旁边会有个绿色的打钩,网站就可以访问了~
点此查看我的在线相册:m.palxp.com (手机访问效果最佳)
接下来我将从零开始,讲解我是如何一步步开发完成这个小项目的,Let's Go~
好了,现在把照片放在resources目录,后续我们只要在电脑上对这个目录中图片进行管理,重新运行即可更新在线相册,这也就不必开发后台管理照片了。
首先创建一个index.js文件使用NodeJs来遍历resources,我们要获得路径名称、图片大小等有用信息(使用 image-size 这个库可以快速获取图片大小),然后输出成JSON文件保存,最后将图片复制到目录view/public/中:
const fs = require('fs')
const path = require('path')
const sizeOf = require('image-size')
const basePath = path.resolve('resources')
const jsonPath = path.resolve('view/src/assets/data/datalist.json')
const picsData = []
fs.readdir(basePath, async function (err, files) {
//遍历读取到的文件列表
for (let i = 0; i < files.length; i++) {
const filename = files[i]
const filedir = path.join(basePath, filename)
//根据文件路径获取文件信息,返回一个fs.Stats对象
const stats = await fs.statSync(filedir)
if (stats.isFile()) {
let dimensions = { url: filename, ...sizeOf(filedir) }
if (dimensions.width) {
cp(filedir, path.resolve(`view/public/${filename}`)) // 复制图片
picsData.push({ ...dimensions })
}
}
}
// 解析完毕,生成json
fs.writeFileSync(jsonPath, JSON.stringify(picsData))
})
// 复制文件
function cp(from, to) {
fs.writeFileSync(to, fs.readFileSync(from))
}
运行node index.js,这样我们就可以开始编写前端项目了,把JSON文件引入即是图片列表数组,为了摆放好这些图片,我们先来写写图片列表的样式布局。
其实用JS实现瀑布流也并不难,我们使用绝对定位布局,由于图片的宽度是平均分布的,只需要计算出图片的高度及left、top定位对应设置到图片元素上即可:
<template>
<div id="list">
<div :style="{ position: 'absolute', width: `${img.w}px`, height: `${img.h}px`, left: `${img.left}px`, top: `${img.top}px` }" v-for="(img, i) in list" :index="i" :key="'img' + i">
<img ............ />
</div>
</div>
</template>
我们用变量columnNums表示有多少列,gap表示图片间隔,容器总宽度可以由当前的DOM往父级查询parentNode.offsetWidth来获取,那么图片在布局中的宽高以及left值就可以计算出来了,而高度则用一个数组columnHeights来储存,有多少列就往里存多少个元素,随着图片列表的循环一直累加取出计算就可以得到每张图片的top定位了,简单几行代码搞定:
let columnNums = 2 // 有多少列
const gap = 8 // 图片之间的间隔
const columnHeights = [] // 列的高度
function waterfall(data) { // data为图片数组
const columnHeights: any = [] // 列的高度
let { offsetWidth: pW } = document.getElementById('list').parentNode.offsetWidth
pW -= gap * (columnNums - 1) // 总体宽度数值等于减去间隔
const newList = JSON.parse(JSON.stringify(data))
for (let i = 0; i < newList.length; i++) {
let index = i % columnNums
const item = newList[i]
item.w = pW / columnNums // 图片宽度
item.h = item.height * (pW / columnNums / item.width) // 图片高度
item.left = index * (pW / columnNums + gap) // 定位
item.top = columnHeights[index] + gap || 0 // 定位
columnHeights[index] = isNaN(columnHeights[index]) ? item.h : item.h + columnHeights[index] + gap // 记录列高度
}
return newList
}
| 两列: | 改成三列: |
|---|---|
![]() |
![]() |
经过1.024秒的思索,我马上发现了问题所在,上面的代码仅仅只是把图片按左右的顺序依次往下排列,而每张图片高度是不一样的,这就导致出现尾部空白的现象,解决的办法也很简单,每次找出最短的那一列来插入图片即可,我们已经将高度都存在了columnHeights这个数组中,通过往Math.min()传入解构数组得到最小值,再用indexOf得到下标,就可以知道下一张图片该插入哪一列了,修改上面方法中的最后一行代码:
function waterfall(data) { // data为图片数组
// ...........
// columnHeights[index] = isNaN(columnHeights[index]) ? item.h : item.h + columnHeights[index] + gap
// TODO: 上面这行代码改为找出最短列计算高度
if (isNaN(columnHeights[index])) {
columnHeights[index] = item.h
} else {
index = columnHeights.indexOf(Math.min(...columnHeights))
item.left = index * (pW / columnNums + gap)
item.top = columnHeights[index] + gap || 0
columnHeights[index] = item.h + columnHeights[index] + gap
}
}
以上就是实现瀑布流排版的全部核心代码,可以根据实际情况进行扩展,比如通过window.onresize监听窗口宽度变化动态改变列数量重新排列等。
虽然看起来似乎是等高不等宽,但实际上每行高度并不都是一样的,因此我们需要一个阈值来决定每行高度可以被允许的上限,与瀑布流一样的是,列表整体宽度是已知的,所以核心是计算每行图片的高度,我们先来看看如何实现这个算法。
抽象问题用简单的数学问题描述往往更容易解决。假设某行存在2张图片,已知的实际宽高分别为w1、h1和w2、h2,而在列表中的相对宽高我们则设为w1'、h1'和w2'、h2',接着设列表总体宽高为W和H,已知的W为列表父级div的宽度,我们的目的就是求这个H的值。
此时由于在同一行中图片等高,于是有:h1'=h2'=H
又因为图片的比例不变,于是有:w1'/h1' = w1/h1,代入上面的式子可得:
w1'/H = w1/h1(同理得到另一个式子:w2'/H = w2/h2)
所以上面推导的两个式子可以得出图片在行内的宽度分别为:
而总体宽度为图片宽度相加:
代入可得到:
到这里我们已经可以轻松推导出计算高度H的方法了:
上面推导过程我是在纸上完成的,回到代码中,我们可以使用递归来操作图片数组,得到一组计算好宽高的新数组,这里我设计了一个工厂函数factory以及计算函数calculate,计算函数核心就是利用上面的公式求图片高度,而工厂函数则是用来输出每一行的图片数组,通过判断计算的高度如果超出阈值,就继续增加这一行的图片(一个隐藏的事实是,该行图片越多高度肯定就会越小),如果高度在我们设置的阈值之内那么就将这些图片"打包"返回,在handleList函数中会拼成一个二维数组,最后拍平就得到我们要的数据:
const gap = 8 // 图片之间的间隔
let limitWidth = document.getElementById('list').parentNode.offsetWidth // 宽度限制,列表父级div宽度
const list = JSON.parse(JSON.stringify(data)) // data为原始图片数组
const newList = await createNewArr(list)
async function createNewArr(list) {
const standardHeight = 180 // 高度阈值
const neatArr = [] // 整理后的数组
function factory(cutArr) {
return new Promise((resolve) => {
const lineup = list.shift()
if (!lineup) {
resolve({ height: calculate(cutArr), list: cutArr })
return
}
cutArr.push(lineup)
const finalHeight = calculate(cutArr)
if (finalHeight > standardHeight) { // 如果计算超出阈值,就继续加入图片
resolve(factory(cutArr))
} else {
resolve({ height: finalHeight, list: cutArr })
}
})
}
function calculate(cutArr) {
let cumulate = 0
for (const iterator of cutArr) {
cumulate += iterator.width / iterator.height
}
return (limitWidth - gap * (cutArr.length - 1)) / cumulate // 实际宽度需要减去图片间隔
}
async function handleList() {
const { list: newList, height } = await factory([list.shift()])
neatArr.push( newList.map((x) => { x.w = (x.width / x.height) * height; x.h = height; return x }))
if (list.length > 0) {
await handleList()
}
}
await handleList()
return neatArr.flat()
}
transform 变换。而实现PC上的点击、移动,H5的手势操作,则离不开DOM事件监听:例如鼠标移动事件对应 mousemove,移动端触摸移动则对应 touchmove,而在本项目中我们不做两套适配,将仅通过指针事件(pointEvent)进行多端统一的事件监听。
这一部分展开来讲篇幅不小,所以我又用原生JS实现了一遍并把完整的过程和思路都写在了这篇文章中:《原生JS手写一个优雅的图片预览功能,带你吃透背后原理》
fs.stat可以获取文件的创建/修改时间,但这并不能代表实际拍摄时间,此时我们就要通过 Exif (Exchangeable image file format) 来获得照片中记录的数据。
照片如果经过一些美图app的处理,元数据会被抹掉,在PS、LR等专业修图软件中导出成片时,也别忘了勾选保留照片元数据,另外微信传图(即使选择原图)也会丢失元数据,这类软件是出于保护隐私考虑。在网页项目中我们可以直接引入
exif.js读取照片元数据,不过在本项目中,图片会先经过一个处理阶段,所以我们直接在Node中解析好数据:
安装一下 exif 这个库:npm install exif
const ExifImage = require('exif').ExifImage
new ExifImage({ image: filedir }, async function (error, exifData) {
if (!error) {
const { ImageWidth: width, ImageHeight: height, ModifyDate } = exifData.image // 获取到图片一些数据
let datetime = exifData.exif.DateTimeOriginal || ModifyDate // 元数据
// TODO: 解析出来的格式不标准,转化成我们可以使用的:
datetime && (result.datetime = datetime.split(' ')[0].replace(/:/g, '-') + ' ' + datetime.split(' ')[1].slice(0, 8))
if (JSON.stringify(exifData.gps) === '{}') {
// 定位属于隐私信息
}
}
})
这一步没太多好讲的,利用插件解析出来整理好需要的数据就可以了,只需要注意不要丢失照片文件的原始数据否则读取不到。
3.2.3这个版本(如果你的网络情况不好导致安装失败,推荐使用 pnpm 安装)
为什么要解码图片呢?如果你经常接触移动端H5开发,应该碰到过图像翻转问题,因为手机照片通常会以原始的拍摄方向展示,可能你在手机上看着没什么问题,但是直接链接到网页上显示就会发现图像翻转了,所以我们需要手动翻转图像到正确位置并重新编码图像。
回到我们的项目中,配合 image-size 可以快速获得照片的方向值,判断如果方向偏移,那么直接使用 images 这个库编码保存,图像会自动翻转到正确方向。
const sizeOf = require('image-size')
const images = require('images')
const filedir = '' // <- 图像地址
const dimensions = sizeOf(filedir)
if ([6, 8, 3].includes(dimensions.orientation)) {
// TODO:通过解码写入来复制图片,判断方向是否正确。
images(filedir).save(.......) // 写入到新的地址
}
通过 images 重新编码的照片,元数据会被抹除,所以我们也可以配合前面 exif 检查照片如果存在 GPS 对象,就不直接复制照片而是重新编码照片以此消除隐私信息,当然相对的处理速度就会变慢。
// filedir: 图像地址,thumbSize: 压缩后图像宽度,quality: 压缩质量
images(filedir).size(thumbSize).save(.....), { quality: 70 })
以压缩到目标宽度500,质量70%为例,可以看到压缩率还是不错的:
if隐藏,应该让它存在DOM当中,否则onLoad回调不会触发:
<template>
<div class="img">
<img v-show="!loading" :src="src" @load="loadDone" />
<div v-if="loading" class="color" :style="{ background: data.color }" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
props: {
src: {},
data: {},
},
setup(props) {
const loading = ref(true)
const loadDone = () => {
loading.value = false
}
watch(() => props.src, () => {
loading.value = true
})
return { loading, loadDone }
},
})
</script>
<style scoped>
.img {
position: relative;
overflow: hidden;
width: 100%;
height: 100%;
}
.img > img {
display: block;
width: 100%;
height: 100%;
}
.color {
width: 100%;
height: 100%;
border-radius: 4px;
animation: breathe 600ms ease-out infinite alternate;
}
/* 呼吸效果 */
@keyframes breathe {
0% { opacity: 0.8 }
100% { opacity: 1 }
}
</style>
如果随机生成个颜色占位,就显得太不专业了,我们可以用 colorthief 这个库来提取图片的主题色:
const ColorThief = require('colorthief')
ColorThief.getColor(image, quality).then((color) => {
rgbToHex(color) // color 为图片主颜色,格式为三原色数组
}).catch((err) => {})
参数解释:得到的颜色为数组(代表RGB三原色),我们可以转换成16进制颜色:image: 在Node中运行时,这个参数为图像的路径。quality: 是一个可选参数,必须是值为1或更大的Integer,默认值为10。这个数字决定了在下一个采样之前跳过多少像素。数值越大,返回值的速度越快。
const rgbToHex = (rgb) => '#' + rgb.map((x) => {
const hex = x.toString(16)
return hex.length === 1 ? '0' + hex : hex
}).join('')
实际使用的过程中发现,在NodeJs中不仅处理速度很慢,还容易因为内存不足等问题发生崩溃,打开 colorthief 的包发现,其依赖的是 get-pixels 这个库解码图像,这是一个纯 JavaScript 实现的库,性能非常低,所以我这里没有直接使用原图来提取主题色,而是使用了前面我们用 images 解码图像生成的缩略图来作为提取颜色的图片,这样就保证了速度与稳定性。
scrollTop与浏览器窗口高度就不难判断图片是否在当前窗口中了,利用这个原理我还可以实现滚动时变换时间的效果:
但是这里图片懒加载我使用另一种方式实现:Intersection Observer,这是一个浏览器原生API,可以用于异步观察目标元素与其祖先元素或顶级文档视窗是否交叉的方法,简单讲就是可以监听一个元素是否出现在视窗当中,就这么简单粗暴,该API其实已经提出很久了,所以不用太担心兼容性问题。
onMounted(async () => {
await nextTick()
observer()
})
function observer() {
const observer = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.isIntersecting) {
// TODO: 换上真实的图片链接
observer.unobserve(item.target) // 停止监听该节点
}
})
}) //不传options参数,默认根元素为浏览器视口
document.querySelectorAll('.img-box').forEach((div) => observer.observe(div)) // 遍历监听所有图片DOM节点
}
实际效果如下所示:
playlist这个接口,就可以看到你的歌单啦,比如我这里创建了一个叫"PhotoGallery"的歌单,找到它的歌单id,等下就把它搬进相册当中。
接下来我们使用 NeteaseCloudMusicApi 这个库,它利用CSRF伪造请求头来调用网易云官方API,通过它我们可以轻松获取到歌单数据以及播放音乐的链接了,虽然它需要部署在服务端,但是没关系,作者提供了 Vercel 部署的配置,仓库里的README.MD文件有详细部署说明,这里就不过多赘述了,总之 Vercel 是国外一个部署前端应用的云平台,我们把node项目部署上去就可以直接使用了。
// api.js
import fetch from '@/utils/axios'
// 文档地址:https://binaryify.github.io/NeteaseCloudMusicApi/#/
// 获取歌曲播放链接
export const getUrl = (params: Type.Object = {}) => fetch(MUSIC_URL + '/song/url', params, 'get')
// 通过 id 获取歌单详情
export const getList = (params: Type.Object = {}) => fetch(MUSIC_URL + '/playlist/detail?id=5183094117', params, 'get')
Vercel 网址在国内被墙不能直接访问,不过我们可以通过配置CNAME解决,如果你有自己的域名可以fork一下玩玩,没有也没关系,本项目已经配置好了我的域名,不过稳定性不敢保证~为了方便与美观,我们直接使用 APlayer 作为播放器界面,由于不是必要插件,所以我通过JS动态引入来使用,就不通过npm安装了。
// deferLoader.js 异步加载脚本方法
export default (type, url) => {
return new Promise((resolve) => {
const link_element = document.createElement(type)
if (type === 'script') {
link_element.setAttribute('src', url)
} else if (type === 'link') {
link_element.setAttribute('rel', 'stylesheet')
link_element.setAttribute('href', url)
}
document.head.appendChild(link_element)
link_element.onload = function () {
resolve()
}
})
}
这个组件很简单,调用接口,通过 id 获取歌单详情,然后通过歌单中的歌曲id获取到歌曲播放地址的url,不过需要注意,由于我们的宗旨是搭建免费网站,缺少服务端,此方式因为没有登录状态,获取到的歌单曲目只会显示 10 首,但是也够用了(总不能把网易云账号密码写进前端项目里吧)
<template>
<div id="aplayer"></div>
</template>
<script>
import { defineComponent, onMounted, nextTick } from 'vue'
import * as api from './api'
import loader from '@/utils/widgets/deferLoader'
export default defineComponent({
setup() {
onMounted(async () => {
const ids = []
const listObj = {}
const { data: resList } = await api.getList()
for (const x of resList.playlist.tracks) {
ids.push(x.id)
listObj[x.id] = { name: x.name, artist: x.ar[0] ? x.ar[0].name : '', cover: x.al.picUrl }
}
let { data: audio } = await api.getUrl({ id: ids + '', realIP: '116.25.146.177' }) // ip是随便填的,不填会无法访问接口
audio = audio.data.map((x: any) => {
return Object.assign({ url: x.url }, listObj[x.id])
})
// 如果接口请求成功,下面开始启动播放器
await load() // 下载插件
await nextTick()
const APlayer = window.APlayer
new APlayer({
container: document.getElementById('aplayer'),
fixed: true, // 播放器会吸附在底部,没有兼容iphone黑边,所以下面补了临时处理
autoplay: true,
audio,
})
})
async function load() {
await loader('script', 'https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js')
await loader('link', 'https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css')
}
},
})
</script>
<style>
.aplayer.aplayer-fixed .aplayer-body {
bottom: calc(constant(safe-area-inset-bottom));
bottom: calc(env(safe-area-inset-bottom));
}
</style>
效果大概就是这样,非常朴实无华:
你在打码的时候又喜欢听些什么歌呢?
以上就是文章的全部内容,感谢看到这里,希望对你有所帮助或启发!创作不易,如果觉得文章写得不错,可以点赞收藏支持一下,也欢迎关注,我会更新更多实用的前端知识与技巧。我是茶无味de一天(公众号: 品味前端),期待与你共同成长~
有这些railscast。http://railscasts.com/episodes/218-making-generators-in-rails-3有了这个,你就会知道如何创建样式表和脚手架生成器。http://railscasts.com/episodes/216-generators-in-rails-3通过这个,您可以了解如何添加一些文件来修改脚手架View。我想把两者结合起来。我想创建一个生成器,它也可以创建脚手架View。有点像RyanBates漂亮的生成器或web_app_themegem(https://github.com/pilu/web-app-theme)。我
作为新的阿里云用户,您可以50免费试用多种优惠,价值高达1,700美元(或8,500美元)。这将让您了解和体验阿里云平台上提供的一系列产品和服务。如果您以个人身份注册免费试用,您将获得价值1,700美元的优惠。但是,如果您是注册公司,您可以选择企业免费试用,提交基本信息通过企业实名注册验证,即可开始价值$8,500的免费试用!本教程介绍了如何设置您的帐户并使用您的免费试用版。关于免费试用在我们开始此试用之前,您还必须遵守以下条款和条件才能访问您的免费试用:只有在一年内创建的账户才有资格获得阿里云免费试用。通过此免费试用优惠,用户可以免费试用免费试用活动页面上列出的每种产品一次。如果您有多个帐
我使用的第一个解析器生成器是Parse::RecDescent,它的指南/教程很棒,但它最有用的功能是它的调试工具,特别是tracing功能(通过将$RD_TRACE设置为1来激活)。我正在寻找可以帮助您调试其规则的解析器生成器。问题是,它必须用python或ruby编写,并且具有详细模式/跟踪模式或非常有用的调试技术。有人知道这样的解析器生成器吗?编辑:当我说调试时,我并不是指调试python或ruby。我指的是调试解析器生成器,查看它在每一步都在做什么,查看它正在读取的每个字符,它试图匹配的规则。希望你明白这一点。赏金编辑:要赢得赏金,请展示一个解析器生成器框架,并说明它的
Ruby有一些不错的文档生成器,例如Yard、rDoc,甚至Glyph。问题是Sphinx可以做网站、PDF、epub、LaTex等。它在重组文本中完成所有这些事情。在Ruby世界中有替代方案吗?也许是程序的组合?如果我也能使用Markdown就更好了。 最佳答案 自1.0版以来,Sphinx有了“域”的概念,它是从Python和/或C以外的语言标记代码实体(如方法调用、对象、函数等)的方法。有一个rubydomain,所以你可以只使用Sphinx本身。您唯一会缺少的(我认为)是Sphinx使用autodoc从源代码自动创建文档
我正在使用遗留数据库并需要创建一些CRUD。我如何使用scaffold生成器并告诉他表的确切名称以避免复数化过程?表格也是西类牙语。 最佳答案 您可以只使用ActiveRecord::Base.table_name=方法手动设置表名。因此,在您的模型中您可以:classOrderDetail 关于ruby-on-rails-如何在Rails脚手架生成器上强制使用单数表名?,我们在StackOverflow上找到一个类似的问题: https://stackove
我是构建Rubygems的新手,正在尝试我的第一个。我正在为我的gem编写一个生成器,它将在我的Rails应用程序中生成一个迁移。我希望将gem简单地包含在Rails应用程序中,运行“railsgmygem:install”以创建迁移,然后运行“rakedb:migrate”以完成所有操作。我已经找到了几种不同的方法来完成类似的任务,但到目前为止没有任何效果。我似乎无法让Rails应用程序找到生成器。我尝试过的最新指南位于此处:http://www.railsdispatch.com/posts/how-rails-3-enables-more-choices-part-1.这是我当前
有没有推荐的测试方式Railsgenerators与RSpec?我找到的唯一解决方案是GeneratorSpecgem,已经两年多没有更新了。 最佳答案 有gemhttps://github.com/petergoldstein/generator_spec,虽然维护得不是很积极,但做得不错 关于ruby-on-rails-如何使用RSpec测试生成器,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/
我有一个自定义表单生成器,使用此自定义生成器的原因之一是对于每个表单,我都需要包含一些额外的参数,我不想在每个表单中使用隐藏字段标签显式放入这些参数写。for_for(@foo,:builder=>MyBuilder)do|f|#stuffIshouldn'thavetoworryabout#thisshouldbeputinallthetimewithoutmehavingtodoithidden_field_tag('extra','myextrainfo')#normalthingsIwouldputinf.text_field(:bar)end我必须在我的自定义表单构建器中做什
软件特点部署后能通过浏览器查看线上日志。支持Linux、Windows服务器。采用随机读取的方式,支持大文件的读取。支持实时打印新增的日志(类终端)。支持日志搜索。使用手册基本页面配置路径配置日志所在的目录,配置后按回车键生效,下拉框选择日志名称。选择日志后点击生效,即可加载日志。windows路径E:\java\project\log-view\logslinux路径/usr/local/XX历史模式历史模式下,不会读取新增的日志。针对历史文件可以分页读取,配置分页大小、跳转。历史模式下,支持根据关键词搜索。目前搜索引擎使用的是jdk自带类库,搜索速度相对较低,优点是比较简单。2G日志全文搜
在我的Rails5.0.0应用程序中,我将以下内容添加到我的Gemfile中:group:development,:testdogem'byebug',platform::mrigem'rspec-rails','~>3.5','>=3.5.2'end我运行了bundleinstall。至此gem安装成功。然后我跑了以下:railsgeneraterspec:install但我收到一条错误消息:RunningviaSpringpreloaderinprocess8893Couldnotfindgenerator'rspec:install'.Maybeyoumeant'css:asse