本文目录导读:

我来介绍PHP项目中内容编辑器的开发方案,从简单到复杂,包含多种选择:
轻量级方案:纯文本编辑器
基础文本编辑器
// config/editor.php
return [
'basic' => [
'type' => 'textarea',
'rows' => 20,
'cols' => 80
]
];
// resources/views/editor/basic.blade.php
<textarea name="content" id="basic-editor" rows="20">
{{ old('content', $post->content ?? '') }}
</textarea>
Markdown编辑器
// 使用 Parsedown 库
composer require erusev/parsedown
// MarkdownEditor.php
class MarkdownEditor {
protected $parsedown;
public function __construct() {
$this->parsedown = new Parsedown();
}
public function render($content) {
return $this->parsedown->text($content);
}
public function toHtml($content) {
return $this->parsedown->text($content);
}
}
// 前端Markdown编辑器
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<textarea id="markdown-editor" name="content"></textarea>
<script>
new SimpleMDE({
element: document.getElementById("markdown-editor"),
spellChecker: false,
toolbar: ["bold", "italic", "heading", "|",
"quote", "code", "|",
"unordered-list", "ordered-list", "|",
"link", "image", "|", "preview"]
});
</script>
中等方案:富文本编辑器
TinyMCE编辑器
// resources/views/editor/tinymce.blade.php
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.tiny.cloud/1/your-api-key/tinymce/6/tinymce.min.js"></script>
</head>
<body>
<textarea id="tinymce-editor" name="content">
{{ $post->content ?? '' }}
</textarea>
<script>
tinymce.init({
selector: '#tinymce-editor',
height: 500,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'preview', 'anchor', 'searchreplace', 'visualblocks', 'code',
'fullscreen', 'insertdatetime', 'media', 'table', 'help',
'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help | image media link',
images_upload_handler: function(blobInfo, progress) {
return new Promise((resolve, reject) => {
let formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.filename());
fetch('/upload/image', {
method: 'POST',
body: formData,
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(response => response.json())
.then(data => {
if (data.location) {
resolve(data.location);
} else {
reject('Upload failed');
}
})
.catch(error => {
reject('Upload failed: ' + error);
});
});
}
});
</script>
</body>
</html>
Quill编辑器(推荐)
// resources/views/editor/quill.blade.php
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
<div id="editor-container">
{!! $post->content ?? '' !!}
</div>
<textarea name="content" id="content-hidden" style="display:none;"></textarea>
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script>
const quill = new Quill('#editor-container', {
theme: 'snow',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ 'color': [] }, { 'background': [] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
['blockquote', 'code-block'],
[{ 'align': [] }],
['link', 'image', 'video'],
['clean']
]
},
placeholder: '请输入内容...'
});
// 提交表单时同步内容
document.querySelector('form').onsubmit = function() {
document.querySelector('#content-hidden').value = quill.root.innerHTML;
};
</script>
完整方案:定制编辑器
编辑器核心框架
// app/Editor/EditorService.php
namespace App\Editor;
class EditorService {
protected $plugins = [];
protected $config = [];
public function registerPlugin($name, $plugin) {
$this->plugins[$name] = $plugin;
}
public function render($content, $options = []) {
$config = array_merge($this->config, $options);
return view('editor::main', [
'content' => $content,
'plugins' => $this->plugins,
'config' => $config
])->render();
}
public function processContent($content) {
foreach ($this->plugins as $plugin) {
$content = $plugin->process($content);
}
return $content;
}
}
// app/Editor/Plugins/ImagePlugin.php
namespace App\Editor\Plugins;
class ImagePlugin {
public function process($content) {
// 处理图片路径、压缩等
return $content;
}
public function upload($file) {
// 实现图片上传逻辑
$path = $file->store('images/content', 'public');
return asset('storage/' . $path);
}
}
后端处理服务
// app/Services/ContentService.php
namespace App\Services;
use App\Models\Content;
use App\Editor\EditorService;
use Illuminate\Support\Facades\Storage;
class ContentService {
protected $editorService;
public function __construct(EditorService $editorService) {
$this->editorService = $editorService;
}
public function save(array $data) {
// 处理内容
$content = $this->processContent($data['content']);
// 提取并处理图片
$images = $this->extractImages($content);
$content = $this->processImages($content, $images);
// 保存内容
$contentModel = new Content();
$contentModel->title = $data['title'];
$contentModel->content = $content;
$contentModel->summary = $data['summary'] ?? '';
$contentModel->status = $data['status'] ?? 'draft';
$contentModel->save();
return $contentModel;
}
protected function processContent($content) {
// 安全过滤
$content = strip_tags($content, '<p><br><strong><em><ul><ol><li><a><img><h1><h2><h3><h4><h5><h6><blockquote><pre><code><table><tr><td><th><figure><figcaption><div><span><hr>');
// XSS过滤
$content = htmlspecialchars_decode($content);
return $content;
}
protected function extractImages($content) {
preg_match_all('/<img[^>]+src="([^"]+)"/', $content, $matches);
return $matches[1] ?? [];
}
protected function processImages($content, $images) {
foreach ($images as $image) {
if (strpos($image, 'base64') !== false) {
// 处理base64图片
$newPath = $this->saveBase64Image($image);
$content = str_replace($image, $newPath, $content);
}
}
return $content;
}
}
控制器实现
// app/Http/Controllers/ContentController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\ContentService;
use App\Editor\EditorService;
class ContentController extends Controller {
protected $contentService;
protected $editorService;
public function __construct(
ContentService $contentService,
EditorService $editorService
) {
$this->contentService = $contentService;
$this->editorService = $editorService;
}
public function create() {
$editor = $this->editorService->render('', [
'editor_id' => 'content-editor',
'height' => '600px'
]);
return view('content.create', ['editor' => $editor]);
}
public function store(Request $request) {
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required',
'summary' => 'nullable|max:500',
'status' => 'in:draft,published'
]);
$content = $this->contentService->save($validated);
return redirect()->route('content.show', $content->id)
->with('success', '内容发布成功');
}
public function edit($id) {
$content = Content::findOrFail($id);
$editor = $this->editorService->render($content->content, [
'editor_id' => 'content-editor',
'height' => '600px'
]);
return view('content.edit', [
'content' => $content,
'editor' => $editor
]);
}
public function upload(Request $request) {
$request->validate([
'file' => 'required|image|max:2048'
]);
$path = $request->file('file')->store('content-images', 'public');
return response()->json([
'location' => asset('storage/' . $path)
]);
}
}
前端完善
// resources/js/editor/custom-editor.js
class CustomEditor {
constructor(options) {
this.element = options.element;
this.content = options.content || '';
this.plugins = options.plugins || [];
this.init();
}
init() {
this.createToolbar();
this.createContentArea();
this.bindEvents();
this.loadContent();
}
createToolbar() {
this.toolbar = document.createElement('div');
this.toolbar.className = 'editor-toolbar';
this.toolbar.innerHTML = `
<button data-command="bold" title="粗体"><b>B</b></button>
<button data-command="italic" title="斜体"><i>I</i></button>
<button data-command="underline" title="下划线"><u>U</u></button>
<span class="separator"></span>
<button data-command="insertUnorderedList" title="无序列表">• List</button>
<button data-command="insertOrderedList" title="有序列表">1. List</button>
<span class="separator"></span>
<button data-command="createLink" title="链接">Link</button>
<button data-command="insertImage" title="图片">Image</button>
<button data-command="formatBlock" data-value="h2" title="标题">H</button>
`;
this.element.prepend(this.toolbar);
}
createContentArea() {
this.contentArea = document.createElement('div');
this.contentArea.className = 'editor-content';
this.contentArea.contentEditable = true;
this.element.appendChild(this.contentArea);
}
bindEvents() {
// 工具栏按钮事件
this.toolbar.addEventListener('click', (e) => {
const button = e.target.closest('button');
if (button) {
this.handleCommand(button.dataset.command, button.dataset.value);
}
});
// 内容变化事件
this.contentArea.addEventListener('input', () => {
if (this.onChange) {
this.onChange(this.getContent());
}
});
}
handleCommand(command, value = null) {
document.execCommand(command, false, value);
this.contentArea.focus();
}
getContent() {
return this.contentArea.innerHTML;
}
setContent(content) {
this.contentArea.innerHTML = content;
}
loadContent() {
if (this.content) {
this.setContent(this.content);
}
}
}
// 使用示例
const editor = new CustomEditor({
element: document.getElementById('editor-wrapper'),
content: '初始内容...',
plugins: ['image', 'link', 'table']
});
安全与优化
XSS防护
// app/Helpers/SecurityHelper.php
class SecurityHelper {
public static function sanitizeContent($content) {
// HTML Purifier
$config = \HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed',
'p,br,strong,em,ul,ol,li,a[href|target],img[src|alt],' .
'h1,h2,h3,h4,h5,h6,blockquote,pre,code,table,tr,td,th,' .
'figure,figcaption,div,span,hr'
);
$config->set('HTML.AllowedAttributes',
'href,target,src,alt,class,id,style,data-*'
);
$purifier = new \HTMLPurifier($config);
return $purifier->purify($content);
}
}
图片优化
// app/Services/ImageOptimizer.php
namespace App\Services;
use Intervention\Image\ImageManager;
class ImageOptimizer {
protected $manager;
public function __construct() {
$this->manager = new ImageManager(['driver' => 'gd']);
}
public function optimize($path, $maxWidth = 1200) {
$image = $this->manager->make($path);
// 调整大小
if ($image->width() > $maxWidth) {
$image->resize($maxWidth, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
}
// 压缩质量
$image->save($path, 80);
return $path;
}
}
推荐方案总结
根据项目需求选择:
- 小型项目/个人博客:使用SimpleMDE(Markdown)或TinyMCE
- 企业级CMS:使用Quill或CKEditor 5
- 需要高度定制:基于Quill开发自定义编辑器
- 需要协作编辑:使用Slate.js或ProseMirror
推荐使用Quill + Laravel组合,文档完善、扩展性强、性能好。