PHP项目内容发布如何开发编辑器

wen PHP项目 23

本文目录导读:

PHP项目内容发布如何开发编辑器

  1. 轻量级方案:纯文本编辑器
  2. 中等方案:富文本编辑器
  3. 完整方案:定制编辑器
  4. 安全与优化
  5. 推荐方案总结

我来介绍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;
    }
}

推荐方案总结

根据项目需求选择:

  1. 小型项目/个人博客:使用SimpleMDE(Markdown)或TinyMCE
  2. 企业级CMS:使用Quill或CKEditor 5
  3. 需要高度定制:基于Quill开发自定义编辑器
  4. 需要协作编辑:使用Slate.js或ProseMirror

推荐使用Quill + Laravel组合,文档完善、扩展性强、性能好。

抱歉,评论功能暂时关闭!