CKEditor5 集成 Firebase Storage 实现图片上传
写在前面的话
在 CKEditor5 中默认提供的上传图片方式是 Base64 upload adapter ,该方式将图片转码成 Base64 格式后会导致文本内容过大,也不便于检索图片资源。所以实际集成过程中,都会重新定义图片上传方式。
选择合适的图片上传方式
CKEditor5 原生提供以下几种上传方式:
- CKBox / Easy Image / CKFinder 收费,不考虑
- Base64 adapter 图片被编码后直接存入数据库,导致文本内容过大
- Simple adapter 提供上传请求自行上传,适合大部分自定义场景
具体描述可参考 Official upload adapters 。
如果使用的是 Google Cloud 原生的对象存储上传服务,使用 Simple adapter 基本就够用了。但本文中使用的是 Google Firebase Storage 来实现上传,该服务的特点在于可以通过 Google 提供的集成插件快速实现上传、控制权限。不过上传请求是插件内置的,外部无法直接更改。
最终实际应该使用的上传方式是 Custom image upload adapter ,也就是完全重写 CKEditor5 的图片上传逻辑。
具体实现过程
Vue 如何集成 CKEditor5 本文不做赘述,重点讲解如何在集成好的 CKEditor5 中引入自定义的图片上传插件,组件的基础目录结构如下图:
集成的步骤大致分两步,首先编写自定义的图片上传插件,之后在具体的上传逻辑处引入 Firebase 即可。
编写自定义的图片上传插件
编写 ImageUploadAdapter
创建 imageUploadAdapter.ts
文件,实现代码如下:
export class ImageUploadAdapter {
loader: any
// 指定图片存储的二级目录
prefix: string
constructor(loader: any, prefix: string) {
this.loader = loader
this.prefix = prefix
}
upload() {
return this.loader.file.then((file: File) => {
// 上传的文件,后续在此处编写实际上传操作
console.log(file)
})
}
abort() {
this.loader.abort()
}
}
在实际组件中引入插件
组件的基础实现使用的是 CKEditor5 官方提供的 vue 集成版本 @ckeditor/ckeditor5-vue ,该组件对外提供一个 @ready
事件,通过监听该事件可获取到组件的可用实例。
随后动态插入自行编写好的 ImageUploadAdapter
插件即可。
该实现方式比官方提供的代码略微简单,因为官方参考代码中使用的是原始的 ClassicEditor 对象,而本文使用的是 Vue 的快速集成版本。
<template>
<ckeditor :editor="editor" v-model="value" @ready="initExtraPlugins"/>
</template>
<script lang="ts">
import CKEditor from '@ckeditor/ckeditor5-vue'
import ClassicEditor from '@/components/editor/ckeditor'
import { defineComponent } from 'vue'
import { ImageUploadAdapter } from '@/components/editor/plugin/imageUploadAdapter'
export default defineComponent({
components: {
ckeditor: CKEditor.component
},
props: {
modelValue: String,
// 由外部传入,用于指定图片存储的二级目录
imagePrefix: {
type: String,
default: 'common'
}
},
emits: ['update:modelValue'],
data() {
return {
editor: ClassicEditor,
value: ''
}
},
mounted() {
this.value = this.modelValue || ''
},
watch: {
// 此处省略了自定义实现双向监听的代码
},
methods: {
initExtraPlugins(editor: any) {
editor.plugins.get('FileRepository').createUploadAdapter = (loader: any) => {
return new ImageUploadAdapter(loader, this.imagePrefix)
}
}
}
})
</script>
使用 Firebase 完成上传操作
关于 Firebase Storage 详细对接文档可参考 Cloud Storage 使用入门 (Web) 。
开始前的一些提示
Firebase Storage 和 Google Cloud Storage 并不是同一个产品,在使用 Firebase Storage 之前需要先前往 Firebase Dashboard 创建对应的项目并添加 Storage 实例。
准备好上述这些后,在项目根目录执行 yarn add firebase
即可为项目添加 Firebase 插件。
初始化配置
在 imageUploadAdapter.ts
中添加以下代码:
import { initializeApp } from 'firebase/app'
import { getStorage } from 'firebase/storage'
import { FIREBASE_STORAGE_BUCKET } from '@/assets/ts/constant'
const firebaseConfig = {
storageBucket: FIREBASE_STORAGE_BUCKET
}
const app = initializeApp(firebaseConfig)
const storage = getStorage(app)
export class ImageUploadAdapter {
// 已省略重复代码
}
storageBucket
的链接来自下图中红框位置:
编写上传操作
具体代码解释请阅读注释。
import { initializeApp } from 'firebase/app'
import { getDownloadURL, getStorage, ref, uploadBytesResumable } from 'firebase/storage'
import dayjs from 'dayjs'
import { FIREBASE_STORAGE_BUCKET } from '@/assets/ts/constant'
const firebaseConfig = {
storageBucket: FIREBASE_STORAGE_BUCKET
}
const app = initializeApp(firebaseConfig)
const storage = getStorage(app)
export class ImageUploadAdapter {
loader: any
prefix: string
constructor(loader: any, prefix: string) {
this.loader = loader
this.prefix = prefix
}
upload() {
return this.loader.file.then((file: File) => new Promise((resolve, reject) => {
// 获取文件名称
const filename = generateFilename(this.prefix, file)
const storageRef = ref(storage, filename)
// 使用能够监控上传进度的接口来进行上传操作
const uploadTask = uploadBytesResumable(storageRef, file)
// 监控上传进度
uploadTask.on('state_changed', snapshot => {
// 将获取到的上传进度回传给 CKEditor5
this.loader.uploadedPercent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
},
err => reject(err),
// 上传成功后,再通过指定接口来获取完整的图片地址,并返回给 CKEditor5
() => getDownloadURL(storageRef).then(url => resolve({ default: url })))
}))
}
abort() {
this.loader.abort()
}
}
const generateFilename = (prefix: string, file: File) => {
const originalName = file.name
// 获取文件扩展名
const extName = originalName.substring(originalName.lastIndexOf('.'))
// 通过外部传入的二级目录,再加上 dayjs 生成的毫秒值,以及文件扩展名,生成完整的文件名称
return `${prefix}/${dayjs().valueOf()}${extName}`
}
最终上传效果
因为上传时有传入二级目录,所以在 Firebase 的控制台可以看到相应的目录结构。
可进行的扩展项
带项目上线之前,还应该配置一下 App Check ,用于避免任何网站拿到桶名称后都可以进行上传。具体可参考 将 App Check 与 reCAPTCHA v3 搭配使用 。
如果没有完成这部分操作,在 Firebase 控制台会看到如下提示:
转载自:https://juejin.cn/post/7204010033204805691