前言
最近公司的后台项目中,遇到个需求:如果客户上传的是 A3 样式的图片,前端需将该图片从中间切割,分为左右对等的两半,即 2 张 A4 样式的图片后,再传给服务器。一开始我去网上找了一些截图的库,比如 cropperjs 来实现该功能,后来学习了截图背后的原理,发现针对公司的具体需求,根本用不着安装第三方库,自己写一个纯函数即可实现,本文就来说说具体过程。
实现图片的一分为二
根据图片尺寸生成 canvas 画布
函数接收的参数为 File 类型的图片文件 imgFile
,它可以通过 <input type="file">
或是第三方 UI 库的上传组件获取。拿到了 imgFile
后,就可以传给 URL.createObjectURL()
获取图片的 URL 的字符串 imgObjUrl
,然后生成图片 img
:
typescript
// 获取图片文件名、类型备用
const imgName = imgFile.name
const imgType = imgFile.type
imgObjUrl = URL.createObjectURL(imgFile)
img = new Image()
img.src = imgObjUrl
注意,通过 URL.createObjectURL()
创建的 URL 的生命周期会与其创建时所在窗口的 document
绑定在一起,所以为了更好的性能和内存使用状况,需要在合适的地方调用 URL.revokeObjectURL()
释放 URL。
等图片加载完成 ,就可以创建 canvas 元素,让它的宽高属性值等于图片的固有的宽高。注意,设置 canvas 宽高的时候不能仅通过 canvas.style.width = img.naturalWidth + 'px'
或 canvas.style.height = img.naturalHeight + 'px'
,这样设置的是样式尺寸,在不同的 devicePixelRatio
下会表现不一致:
typescript
img.onload = () => {
canvas = document.createElement('canvas')
ctx = canvas.getContext('2d', { willReadFrequently: true })
canvas.width = width = img.naturalWidth
canvas.height = height = img.naturalHeight
}
因为之后会多次使用到 ctx.getImageData()
方法,所以给 canvas.getContext()
传入第二个参数 { willReadFrequently: true }
, 迫使浏览器使用 2D canvas 并节省内存(而不是硬件加速)。否则可能会遇到如下警告:

绘制完整图片到 canvas 画布
有了图片,有了 canvas 画布,就可以将图片绘制到画布上去了。绘制前调用 ctx.clearRect()
清空画布,其中 width
和 height
等于图片的固有宽高,也等于画布的宽高:
typescript
ctx.clearRect(0, 0, width, height)
ctx.drawImage(img, 0, 0, width, height)
获取 canvas 画布半边的像素数据
接着就可以通过 ctx.getImageData()
方法获取 canvas 画布的左右两边的隐含像素数据了,传入的参数分别代表要提取数据的矩形区域的左上角 x,y 坐标和宽高。比如我们要提取左半边的数据:
typescript
const imgData = ctx.getImageData(0, 0, width / 2, height)
如果上传一张纯红色的图片,打印 imgData
获取到的结果如下,data
属性中就是各个像素点的颜色信息 ------ 255, 0, 0
即为纯红色 :
如果你想对图片进行置灰、反相之类的颜色处理,可以通过改变 data
中的数据实现。
根据像素数据生成新的 canvas
创建一个新的 canvas 画布,该画布的宽度为完整图片的一半,高度保持一致,然后把得到的半边图片的像素数据通过 ctx2.putImageData()
从新画布的 (0, 0)
点开始绘制到新画布上:
typescript
const screenshotCanvas = document.createElement('canvas')
const screenshotCtx = screenshotCanvas.getContext('2d')
screenshotCanvas.width = width / 2
screenshotCanvas.height = height
screenshotCtx.putImageData(imgData, 0, 0)
canvas 生成 File
canvas 画布生成 File
对象的过程是先由 canvas 的 toBlob()
方法让画布上的图片生成 Blob
类型的对象 blob
,再把 blob
传给 new File()
生成 File
对象,第 2、3 个参数分别指定文件的名称和类型:
typescript
screenshotCanvas.toBlob((blob) => {
if (!blob) return
const file = new File([blob], `${position}-${imgName}`, { type: imgType })
files.push(file)
}, imgType)
打印查看 file
对象,可以看到它的原型对象 File
的原型对象,其实就是 Blob
:

现在,当用户上传 A3 样式的图片时,实际传给服务器的文件数据,就是切割图片后生成的文件对象数组 files
了。
打包发布到 npm
之前在 《npm 细节与原理探究,顺便发布个自己的包》里介绍过如何将一个用 js 编写的项目打包发布到 npm 的仓库,本项目中的代码是使用 ts 编写的,应该如何打包呢?其实就是多了个将 ts 文件编译为 js 文件的过程,其中涉及到以下 2 个文件的修改或创建:
package.json
在 package.json 中,项目的主入口文件 "main"
的值需要是个 js 文件,所以需要安装 typescript 以便使用 tsc 编译 ts 文件;
"types"
:值为 ts 类型声明文件,tsc 编译时生成;
"scripts"
:添加打包命令 build
,执行时就会编译 tsconfig.json 中 include
指定的 ts 文件,prepublishOnly
则是在发布到 npm 仓库前执行的命令:
json
{
"name": "bisect-image-to-files",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "npx tsc",
"prepublishOnly": "pnpm build"
}
}
tsconfig.json
执行 tsc --init
可以生成 tsconfig.json:
json
{
"compilerOptions": {
"target": "es2015",
"module": "ES2015",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
compilerOptions
对象中:
target
:指定编译生成的 js 文件的 ECMAScript 版本;module
:指定模块化方案;rootDir
:指定源文件的目录,也就是我们编写的 ts 文件的目录;declaration
:为true
则会在编译时生成 .d.ts 文件;sourceMap
:为true
则是在编译后生成 .map 文件,方便问题追踪;outDir
:指定编译后生成的文件的存放目录。
include
:指定哪些目录下的 ts 文件需要编译;
exclude
:指定哪些文件不用被编译。
其它步骤则和发布 js 编写的项目一样:登录 npm 仓库,然后通过 npm publish
发布即可。发布成功后就能在 npm 仓库中搜到本项目了:

效果演示
在使用了 element-plus 的框架的 vue3 后台项目中,安装我们发布到 npm 的包:
powershell
pnpm add bisect-image-to-files
在需要上传图片的组件内就可以直接引入使用了:
vue
<script lang="tsx" setup>
import bisectImageToFiles from 'bisect-image-to-files'
const [左半边图片文件, 右半边图片文件] = await bisectImageToFiles(图片文件)
</script>
我用 express 写了个服务器接收上传的图片,效果如下,在前端点击上传图片后,就能在服务器项目中的静态目录 uploads 中查看到左右两张图片了:
