以前看到这个掘金的文章今天这个仇先记下来了,以为挺有趣的,可是他是基于别人已经封装好的框架html2canvas来实现的。本着学习的态度,就用react和canvas原生api从新撸了一遍,在其中遇到了很多坑,也让本身学习到了很多姿式。javascript
react
,ant-design
,styled-components
java
class Face extends Component {
constructor (props) {
super(props)
this.state = {
text: '2018年5月21日 没人给我点赞,这个仇我先记下来了', // 控制文本框和输出图片的文案
canvasToDataURL: '' //把输出图片转换成DataURL,用于新建一张图片覆盖在原来的canvas上面,使得移动端能够长按保存图片
}
this.img = null // 保存输出部分的image dom实例,塞进canvas里
this.canvas = null // 保存canvas dom实例
this.ctx = null // canvas上下文
}
}
复制代码
{/* 输入部分 */}
<div className='input-container'>
<img id="img" src={ require('../../../assets/grudges.png') } alt='记仇'/> <TextArea type='textarea' value={this.state.text} onChange={(e) => { this.onChangeText(e) }} placeholder='请输入你的记仇咯~'> </TextArea> </div>
{/* 输出部分 */}
<div className='display-container'>
{/* 咱们须要操做的canvas */}
<canvas id='canvas' width={200} height={205}></canvas>
{/* 覆盖在canvas上面 */}
<img className='replace-img' src={this.state.canvasToDataURL} alt="from canvas"/>
</div>
复制代码
输入部分由初始图片和输入框组成,输入框添加两个props
,一个是value
,一个是onChange
。value
是组建的状态text
,onChange
的时候会调用onChangeText
这个方法根据新的文案去更新canvas里的文案。react
输出部分由canvas和img组成,canvas用来处理图像,img覆盖在这个canvas上面,主要用于解决移动端不能直接长按canvas保存为图片的问题。用新的img覆盖在canvas上面,移动端就能够长按保存为图片。git
onChangeText (e) {
this.fillText(e.target.value) // 根据新值渲染新的文案
this.setState({
text: e.target.value,
canvasToDataURL: this.canvas.toDataURL('image/png') // 经过渲染文案后的canvas,经过toDataURL方法更新占位img的src
})
}
复制代码
async componentDidMount () {
this.img = document.querySelector('#img')
this.canvas = document.querySelector('#canvas')
this.ctx = this.canvas.getContext('2d')
await this.initCanvas() // 初始化canvas,把img塞进canvas里
this.fillText(this.state.text) // 绘制文案
this.setState({
canvasToDataURL: this.canvas.toDataURL('image/png')
}) // 根据canvas里面的图像内容,生成DataURL,并更新给输出部分的img的src,这样输出部分的img获得了更新}
复制代码
async initCanvas () {
this.ctx.clearRect(0, 0, this.width, this.height) // 清空canvas
this.ctx.fillStyle = 'white'
this.ctx.fillRect(0, 0, this.width, this.height)
const img = await this.loadImage('https://cdn.b1anker.com/grudges.png') // 建立一个新的img
this.drawImage(img)// 把img写进canvas里
return Promise.resolve()
}
复制代码
在这里就会遇到一个坑,就是个人图片等资源是放到本身的cdn上,当使用canvas的getImageData之类的方法的时候会发生跨域的问题。github
解决办法就是给img加个crossOrigin
属性,值为anonymous
,而且相应的资源的响应头必须返回access-control-allow-origin: *
。web
具体能够看跨域解决方案canvas
loadImage (url, crossOrigin = true) {
// 使用canvas以更好地处理回调地狱
return new Promise ((resolve, reject) => {
const img = document.createElement('img')
if (crossOrigin) {
img.crossOrigin = 'anonymous' // 添加crossOrigin以解决canvas跨域问题
}
img.onload = () => {
resolve(img)
}
img.onerror = (err) => {
reject(err)
}
img.src = url
})
}
复制代码
这里又遇到另一个坑,就是文字在canvas中的换行,不是那么好操做。api
首先要经过canvas的measureText方法,测量text每一个字符的长度,而后累加,当长度超过文案最大的宽度时,就换行,而后从新累加。跨域
然而,事情并无那么简单,由于输入的字符多是中文,英文,数字等,因此换行的时候,会有偏差,没有达到理想的换行效果。
能够看到,这个日
字,稍微有点往外边飘了,具体的缘由就是在字符1
以前累加的长度并无超过文案最大的宽度,但这个时候已经很是接近文案最大宽度了,而在加上下一个字符日
后,由于日字是中文字符因此比数字英文字符之类的要长一点,最终究形成了这个日
字有点超出边界。因此在累加的时候,还要计算下一个字符,再判断加上下一个字符是否超过最大宽度减去4的差,若是超过则换行。至于为何要减去4
,这个是考虑到了各类状况啦(当前长度比较接近与最大宽度但为超过最大宽度,可是下一个字符是数字的状况)。
// 绘制文案
fillText (text) {
if (!text) {
this.initCanvas()
return
}
// 清除图片如下的画布空间,以从新绘制文案
this.ctx.clearRect(0, 155, 200, this.height - 155)
this.ctx.fillStyle = 'white'
this.ctx.fillRect(0, 155, 200, this.height - 155)
this.ctx.font = '14px sans-serif'
this.ctx.fillStyle = 'black'
const boundary = this.width - 20 // 最大文案宽度
let initHeight = 175 // 当前文案绘制距离canvas顶部的距离
let lastIndex = 0
let j = 0 // 记录换行深度,若是深度大于canvas能展现的范围,则对canvas进行resize(咱们这里设置了从第三行开始扩展canvas)
for (let i = 0; i < text.length; i++) {
// 当前长度
const currTextWidth = this.ctx.measureText(text.substring(lastIndex, i)).width
// 加上下一次长度
const nextTextWidth = this.ctx.measureText(text.substring(lastIndex, i + 1)).width
if ( currTextWidth > boundary) {
// 绘制文案
this.ctx.fillText(text.substring(lastIndex, i), 8, initHeight)
// 增长绘制文案高度,为换行作准备
initHeight += 20
lastIndex = i
j++
this.resize(j)
}
if (i === text.length - 1) { //绘制剩余部分
this.ctx.fillText(text.substring(lastIndex, i + 1), 8, initHeight)
}
}
this.resize(j)
}
复制代码
由于canvas简单的改变宽高度会使内容发生变形等意向不到的状况,因此对此要作一些处理
// 重置canvas大小
resize (deep) {
if (deep > 1) {
// 存储当前canvas的图片信息
const store = this.ctx.getImageData(0, 0, this.width, this.height)
this.ctx.clearRect(0, 0, this.width, this.height)
// 增长高度
this.canvas.setAttribute('height', 200 + (deep - 1) * 20)
this.ctx.fillStyle = 'white'
this.ctx.fillRect(0, 155, 200, this.height - 155)
this.ctx.font = '14px sans-serif'
this.ctx.fillStyle = 'black'
// 绘制以前存储的图片信息回canvas
this.ctx.putImageData(store, 0, 0)
}
}
复制代码
等到部署在服务器上的时候,本觉得万事大吉了,没想到仍是出问题了。初次访问的时候是ok的,可是刷新以后,就出问题了。有些浏览器没问题,有些浏览器有问题。其中有问题的是iphone的safari,这个时候就用iphone连上mac进行调试。能够发现浏览器报错,居然是说图片跨域了。但是咱们刚刚已经采起了相应的跨域解决方案,为何仍是会跨域呢。经过右键复制为curl到终端访问,明明是能够的,可是safari恰恰不行。估计是不一样浏览器对于service worker的实现有必定的差别。
后来查阅了大量资料后,大概知道了缘由。多是由于service worker不支持动态的请求跨域资源,也就是跨域请求时service worker的request
的属性mode
为cors
可是属性credentials
为same-origin
,就会报错了。因此要对动态请求的跨域资源作一些修正处理:
self.toolbox.router.get("/(.*)", function (request, values, options) {
let newRequest = null
if (request.mode === 'cors') {
/* 当脚本发起的动态跨域请求时,request.mode的值是'cors' * 这个时候须要从新实例化一个Request * 并设置credenti为'include' * 用新的Request去代替原来的 */
const newRequest = new Request(request, {
credentials: 'include'
})
}
return self.toolbox.cacheFirst.apply(this, [newRequest || request, values, options])
}, {
origin: /cdn\.b1anker\.com/,
cache: {
name: staticAssetsCacheName,
maxEntries: maxEntries
}
})
复制代码
- 这里用到了sw-toolbox.js
- service worker中cors的介绍: origin is optional, and it's used to determine whether or not the response that's returned is opaque. If you leave this out, the response will be opaque, and the client will have limited access to the response's body and headers. If the request was made with mode: 'cors', then returning an opaque response will be treated as an error. However, if you specify a string value equal to the origin of the remote client (which can be obtained via event.origin), you're explicitly opting in to provides a CORS-enabled response to the client.
- credentials介绍:credentials 是Request接口的只读属性,用于表示用户代理是否应该在跨域请求的状况下从其余域发送cookies.
此次编码之旅花费了很多时间,可是也让本身学到了不少东西。canvas的相关操做,cdn如何使用,图片跨域,service work跨域怎么解决等等...
第一个版本作的功能有些简单,以后会考虑加入替换本地图片,默认图片库等功能