HTML编辑器 – UEditor

UEditor 是一个功能比较齐全的HTML编辑器,百度出品,官方网站为:http://ueditor.baidu.com/website/。这个编辑器对于文件上传方面提供了强大支持,比如可以直接粘贴(CTRL + V)图片,上传涂鸦文件(客户端画图),多文件上传等。

其中有一个叫UMeditor的产品,简称UM,它是UEditor(简称UE)的功能缩减版本。也可以使用其提供的Ubuilder来构建一个自定义的版本(主要指功能模块)。

一般直接使用UEditor这个全功能版本即可:
ueditor

目录结构:
ueditor-struct
目录dialogs对应各种弹框,目录lang对应语言包(前端展示),目录themes对应皮肤,third-party是第三方插件。文件ueditor.all.js是编辑器实现代码,可见未压缩有1M以上,min压缩后也有接近400K,问津ueditor.config.js是前端的主要配置。

前台主要配置都在ueditor.config.js中,这个文件几乎不用改变,一般需要修改的地方就是serverUrl参数,当有图片或文件需要上传时,就提交到这个指定的地址。其它配置项在这个文件中都有详细的注释(比如皮肤,语言等):

(function () {
    var URL = window.UEDITOR_HOME_URL || getUEBasePath();
    var origin = getOrigin();

    /**
     * 配置项主体。注意,此处所有涉及到路径的配置别遗漏URL变量。
     */
    window.UEDITOR_CONFIG = {

        //为编辑器实例添加一个路径,这个不能被注释
        UEDITOR_HOME_URL: URL

        // 服务器统一请求接口路径
        , serverUrl: origin + "/ueditor/server"
    };
    // ...
    // ...
    function getOrigin() {
        if (typeof location.origin === 'undefined') {
            location.origin = location.protocol + '//' + location.host;
        }
        return location.origin;
    }

    window.UE = {
        getUEBasePath: getUEBasePath,
        getOrigin: getOrigin
    };
})();

其中getOrigin()原始文件是没有的,这个用来解决原来的函数获取基本路径不准确的问题。

以上配置指定了/ueditor/serve,那么文件上传时都会POST到这个地址。所以后端需要处理文件上传的逻辑,原本下载包已经提供了相关的实现,不过为了真正可用,需要做一些改造,比如需要验证:

// 抽象类,Upload, 处理文件上传逻辑,有上个子类:UploadCatch.php 、 UploadFile.php 、 UploadScrawl.php
// 分别实现fire()方法,这个方法实际是读取配置,处理上传逻辑
<?php
namespace UEditor;

abstract class Upload
{
    //文件域名
    protected $fileField;
    //文件上传对象
    protected $file;
    //文件上传对象
    protected $base64;
    //配置信息
    protected $config;
    //原始文件名
    protected $oriName;
    //新文件名
    protected $fileName;
    //完整文件名,即从当前配置目录开始的URL
    protected $fullName;
    //完整文件名,即从当前配置目录开始的URL
    protected $filePath;
    //文件大小
    protected $fileSize;
    //文件类型
    protected $fileType;
    //上传状态信息,
    protected $stateInfo;
    //上传状态映射表,国际化用户需考虑此处数据的国际化
    protected $stateMap = array(
        //上传成功标记,在UEditor中内不可改变,否则flash判断会出错
        "SUCCESS",
        "文件大小超出 upload_max_filesize 限制",
        "文件大小超出 MAX_FILE_SIZE 限制",
        "文件未被完整上传",
        "没有文件被上传",
        "上传文件为空",
        "ERROR_TMP_FILE" => "临时文件错误",
        "ERROR_TMP_FILE_NOT_FOUND" => "找不到临时文件",
        "ERROR_SIZE_EXCEED" => "文件大小超出网站限制",
        "ERROR_TYPE_NOT_ALLOWED" => "文件类型不允许",
        "ERROR_CREATE_DIR" => "目录创建失败",
        "ERROR_DIR_NOT_WRITEABLE" => "目录没有写权限",
        "ERROR_FILE_MOVE" => "文件保存时出错",
        "ERROR_FILE_NOT_FOUND" => "找不到上传文件",
        "ERROR_WRITE_CONTENT" => "写入文件内容错误",
        "ERROR_UNKNOWN" => "未知错误",
        "ERROR_DEAD_LINK" => "链接不可用",
        "ERROR_HTTP_LINK" => "链接不是http链接",
        "ERROR_HTTP_CONTENTTYPE" => "链接contentType不正确",
        "INVALID_URL" => "非法 URL",
        "INVALID_IP" => "非法 IP"
    );

    public function __construct(array $config, $request)
    {
        $this->config = $config;
        $this->request = $request;
        $this->fileField = $this->config['fieldName'];
        if (isset($config['allowFiles'])) {
            $this->allowFiles = $config['allowFiles'];
        } else {
            $this->allowFiles = [];
        }
    }

    abstract function fire();

    public function handle()
    {
        $this->fire();
        return $this->getFileInfo();
    }

    /**
     * 上传错误检查
     * @param $errCode
     * @return string
     */
    protected function getStateInfo($errCode)
    {
        return !$this->stateMap[$errCode] ? $this->stateMap["ERROR_UNKNOWN"] : $this->stateMap[$errCode];
    }

    /**
     * 文件大小检测
     * @return bool
     */
    protected function checkSize()
    {
        return $this->fileSize <= ($this->config["maxSize"]);
    }

    /**
     * 获取文件扩展名
     * @return string
     */
    protected function getFileExt()
    {
        return '.' . $this->file->guessExtension();
    }

    /**
     * 重命名文件
     * @return string
     */
    protected function getFullName()
    {
        //替换日期事件
        $t = time();
        $d = explode('-', date("Y-y-m-d-H-i-s"));
        $format = $this->config["pathFormat"];
        $format = str_replace("{yyyy}", $d[0], $format);
        $format = str_replace("{yy}", $d[1], $format);
        $format = str_replace("{mm}", $d[2], $format);
        $format = str_replace("{dd}", $d[3], $format);
        $format = str_replace("{hh}", $d[4], $format);
        $format = str_replace("{ii}", $d[5], $format);
        $format = str_replace("{ss}", $d[6], $format);
        $format = str_replace("{time}", $t, $format);

        //过滤文件名的非法自负,并替换文件名
        $oriName = substr($this->oriName, 0, strrpos($this->oriName, '.'));
        $oriName = preg_replace("/[\|\?\"\<\>\/\*\\\\]+/", '', $oriName);
        $format = str_replace("{filename}", $oriName, $format);

        //替换随机字符串
        $randNum = rand(1, 10000000000) . rand(1, 10000000000);
        if (preg_match("/\{rand\:([\d]*)\}/i", $format, $matches)) {
            $format = preg_replace("/\{rand\:[\d]*\}/i", substr($randNum, 0, $matches[1]), $format);
        }

        $ext = $this->getFileExt();
        return $format . $ext;
    }

    /**
     * 获取文件完整路径
     * @return string
     */
    protected function getFilePath()
    {
        $fullName = $this->fullName;
        $rootPath = public_path();
        $fullName = ltrim($fullName, '/');

        return $rootPath . '/' . $fullName;
    }

    /**
     * 文件类型检测
     * @return bool
     */
    protected function checkType()
    {
        return in_array($this->getFileExt(), $this->config["allowFiles"]);
    }

    /**
     * 获取当前上传成功文件的各项信息
     * @return array
     */
    public function getFileInfo()
    {
        return array(
            "state" => $this->stateInfo,
            "url" => $this->fullName,
            "title" => $this->fileName,
            "original" => $this->oriName,
            "type" => $this->fileType,
            "size" => $this->fileSize
        );
    }
}


// 文件上传
class UploadFile extends Upload
{
    public function fire()
    {
        $file = $this->request->file($this->fileField);
        if (empty($file)) {
            $this->stateInfo = $this->getStateInfo("ERROR_FILE_NOT_FOUND");
            return false;
        }
        if (!$file->isValid()) {
            $this->stateInfo = $this->getStateInfo($file->getError());
            return false;
        }

        $this->file = $file;
        $this->oriName = $this->file->getClientOriginalName();
        $this->fileSize = $this->file->getSize();
        $this->fileType = $this->getFileExt();
        $this->fullName = $this->getFullName();
        $this->filePath = $this->getFilePath();
        $this->fileName = basename($this->filePath);

        //检查文件大小是否超出限制
        if (!$this->checkSize()) {
            $this->stateInfo = $this->getStateInfo("ERROR_SIZE_EXCEED");
            return false;
        }
        //检查是否不允许的文件格式
        if (!$this->checkType()) {
            $this->stateInfo = $this->getStateInfo("ERROR_TYPE_NOT_ALLOWED");
            return false;
        }

        if (config('ueditor.drivers.default') == 'local') {
            try {
                $this->file->move(dirname($this->filePath), $this->fileName);
                $this->stateInfo = $this->stateMap[0];
            } catch (\Symfony\Component\HttpFoundation\File\Exception\FileException $exception) {
                $this->stateInfo = $this->getStateInfo("ERROR_WRITE_CONTENT");
                return false;
            }
        } else {
            $this->stateInfo = $this->getStateInfo("ERROR_UNKNOWN");
            return false;
        }
        return true;
    }
}

要真正可用,还需要结合具体使用的框架做配置,比如在Laravel框架中,经过一些改造,就可以继承到项目中,如下是实际使用部署:
ueditor.zip

至于使用,则非常简单,需要编辑器的地方引入:

<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.config.js"></script>
<script type="text/javascript" charset="utf-8" src="/ueditor/ueditor.all.min.js"> </script>
<script type="text/javascript" charset="utf-8" src="/ueditor/lang/zh-cn/zh-cn.js"></script>

然后插入类似如下模板:

<div>
    <script id="editor" type="text/plain" style="width:1024px;height:500px;"></script>
</div>

<script type="text/javascript">

    //实例化编辑器
    //建议使用工厂方法getEditor创建和引用编辑器实例,如果在某个闭包下引用该编辑器,直接调用UE.getEditor('editor')就能拿到相关的实例
    var ue = UE.getEditor('editor');
</script>

具体的配置和使用,可以参考:http://fex.baidu.com/ueditor/