1. 背景介绍
自己编写了一个博客系统,想要支持用户写作博客,考查了几种开源编辑器方案,例如 Quill, ckEditor,最后还是选择了 TinyMCE 编辑器。
原因主要在与它开箱即用,插件丰富,而且很多插件都是比其他编辑器做得优秀。
2. 创建 Angular 工程
2.1. 创建工程
首先创建一个 angular 工程. 工程的名字就叫 angular-richtext-editor.
1 2 3
| ng new angular-richtext-editor
|
2.2. 添加依赖
这里需要添加 tinymce/tinymce-angular 依赖包, 以下是 tinymce-angular 与 Angular 之间的兼容关系.
tinymce-angular, angular 的版本兼容性描述可以在tinymce-angular官网找到。
由于我目前使用的 angular 版本 17.3.2, 我选择的 tinymce-angular 为 8.x 版本。
1 2 3 4
| cd angular-richtext-editor npm install --save @tinymce/tinymce-angular@^8
|
2.3. 安装 tinymce
@tinymce/tinymce-angular 这个依赖包只是用来整合 angular 与 tinymce,但是真正需要的 tinymce 仍然没有包含在工程之中。
如果要安装 tinymce 有三种方法。
- 通过 CDN 安装
- 通过 npm manager 安装
- 通过.zip 文件安装
2.3.1. 通过 CDN 安装 tinyMCE
使用 CDN 安装 tinyMCE 比较简单方便,但是需要到 tiny.cloud 上注册账号,并获取 apikey.
获取到 apikey 以后,将 apikey 配置到编辑器即可。
1
| <editor apiKey="your-api-key" [init]="init" />
|
2.3.2. 通过 npm manager 安装 tinyMCE
通过 npm manager 安装 tinyMCE 分为以下几个步骤:
- 安装 tinyMCE 依赖包
1 2 3
| npm install --save tinymce
|
- 配置 angular.json, 将 tinymce 单独编译为文件。
1 2 3
| "assets": [ { "glob": "**/*", "input": "node_modules/tinymce", "output": "/tinymce/" } ]
|
- 加载 TinyMCE
要在编辑器初始化时加载 TinyMCE(也称为延迟加载),请使用 TinyMCE_SCRIPT_SRC 令牌向组件添加依赖项提供程序。
1 2 3 4 5 6 7 8 9 10
| import { EditorComponent, TINYMCE_SCRIPT_SRC } from '@tinymce/tinymce-angular';
@Component({ standalone: true, imports: [EditorComponent], providers: [ { provide: TINYMCE_SCRIPT_SRC, useValue: 'tinymce/tinymce.min.js' } ] })
|
或者:
要在加载页面或应用程序时加载 TinyMCE,请执行以下操作:
打开 angular.json 并将 TinyMCE 添加到全局脚本标记中。
1 2 3
| "scripts": [ "node_modules/tinymce/tinymce.min.js" ]
|
更新编辑器配置以包括 base_url 和后缀选项。
1 2 3 4 5 6 7 8 9 10
| export class AppComponent { init: EditorComponent['init'] = { base_url: '/tinymce', suffix: '.min' }; }
|
2.3.3. 通过.zip 文件安装 tinyMCE
通过 zip 包安装 tinyMCE,可以参考这篇文章
2.4. 配置 tinyMCE
首先在 component 定义配置
1 2 3 4 5 6 7 8 9 10 11
| export class AppComponent { /* ... */ init: EditorComponent['init'] = { /* ... */ base_url: '/tinymce', // Root for resources suffix: '.min' // Suffix to use when loading resources /* ... */ }; }
|
一份完整的配置列表如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| tinyConfig: EditorComponent['init'] = { plugins: 'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help charmap quickbars emoticons accordion fullscreen', editimage_cors_hosts: ['picsum.photos'], menubar: false, language: 'zh_CN', toolbar: 'undo redo | code preview | blocks fontfamily fontsize | bold italic underline strikethrough removeformat | align numlist bullist | link image media table | lineheight outdent indent| forecolor backcolor | charmap emoticons | save print | pagebreak anchor codesample | ltr rtl | fullscreen', autosave_ask_before_unload: true, autosave_interval: '30s', autosave_prefix: '{path}{query}-{id}-', autosave_restore_when_empty: false, autosave_retention: '2m', image_advtab: true, quickbars_insert_toolbar: false, link_list: [ { title: 'My page 1', value: 'https://www.tiny.cloud' }, { title: 'My page 2', value: 'http://www.moxiecode.com' }, ], image_list: [ { title: 'My page 1', value: 'https://www.tiny.cloud' }, { title: 'My page 2', value: 'http://www.moxiecode.com' }, ], image_class_list: [ { title: 'None', value: '' }, { title: 'Some class', value: 'class-name' }, ], importcss_append: true, file_picker_callback: this.filePickHandler.bind(this), height: 600, image_caption: true, quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', noneditable_class: 'mceNonEditable', contextmenu: 'link image table', skin: 'oxide', content_css: 'default', content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:16px }', };
|
3. 使用tinyMCE
app.component.html
1 2 3 4 5
| <h1>TinyMCE 7 Angular Demo</h1> <editor [init]="tinyConfig" ></editor>
|
4. 设置语言
修改语言设置,默认为英文
1 2 3 4 5 6 7 8
| tinyConfig: EditorComponent['init'] = { ...... language_url: '/assets/js/langs/zh_CN.js', language: 'zh_CN', ....... }
|
从tiny cloud languagepacks 下载中文语言包,将其放置在例如:src/assets/js/langs/
将 language_url 指向 zh_CN.js. Language 设置为 zh_CN。
修改 angular.json, 将 assets 包含在 assets 内。
1 2 3 4
| "assets": [ ...... "src/assets", ......
|
5. 如何上传图片到self hosted服务器
首先将tinyMCE配置的file_picker_callback指向自定义函数filePickHandler
file_picker_callback: this.filePickHandler.bind(this),
以下是一段客户端代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| filePickHandler(callback: any, value: any, meta: any) { const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', 'image/*'); let that = this; input.addEventListener('change', (e: Event) => { const target = e.target as HTMLInputElement; if (target.files && target.files.length > 0) { const file: File = target.files[0];
const formData = new FormData(); formData.append('file', file, file.name);
that.http .post( 'url_to_file_upload_server', formData ) .subscribe({ next: (response: any) => { const href = response.url; callback(href, { title: file.name }); }, error: (err: any)=> { console.log(err) } }); } });
input.click(); }
|
url_to_file_upload_server 修改为服务端url.
服务端代码golang 版本如下:
由于涉及到安全性问题,这里只公开部分代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
func (ctrl *blogController) UploadCover(c *gin.Context) { _, fileHeader, err := c.Request.FormFile("file") if err != nil { control.ReturnError(c, control.ErrFileReceive, err) return }
oss := upload.NewOSS() filePath, _, err := oss.UploadFile(fileHeader) if err != nil { control.ReturnError(c, control.ErrFileUpload, err) return }
control.ReturnSuccessWithImgUrl(c, filePath) }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
func (ls *Local) UploadFile(file *multipart.FileHeader) (filePath, fileName string, err error) { ext := path.Ext(file.Filename) name := strings.TrimSuffix(file.Filename, ext) name = utils.MD5(name) filename := name + "_" + time.Now().Format("20060102150405") + ext
storeDir := 获取服务器端存储位置文件夹子 displayPath := 文件上传后,获取文件的的url mkdirErr := os.MkdirAll(storeDir, os.ModePerm) if mkdirErr != nil { zap.S().Error("function os.MkdirAll() Filed", mkdirErr) return "", "", errors.New("function os.MkdirAll() Filed, err:" + mkdirErr.Error()) }
storePath := storeDir + "/" + filename filepath := displayPath + "/" + filename
f, openError := file.Open() if openError != nil { zap.S().Error("function file.Open() Failed", openError) return "", "", errors.New("function file.Open() Failed, err:" + openError.Error()) } defer f.Close()
out, createErr := os.Create(storePath) if createErr != nil { zap.S().Error("function os.Create() Failed", createErr) return "", "", errors.New("function os.Create() Failed, err:" + createErr.Error()) } defer out.Close()
_, copyErr := io.Copy(out, f) if copyErr != nil { zap.S().Error("function io.Copy() Failed", copyErr) return "", "", errors.New("function io.Copy() Failed, err:" + copyErr.Error()) } return filepath, filename, nil }
|
6. 参考文档
Using the TinyMCE package with the Angular framework