因为项目的关系,研究了一下Office的在线编辑功能,写出来共享一下。

Office xp之后的版本支持通过webdav协议(http的扩展)直接编辑服务器上的文件。

IIS(6.0)支持webdav,这在IIS管理器的web服务扩展中可以看到.利用IIS作为webdav的服务器端,可以很容易的实现office(word,excel等)的在线编辑.

可以简单的实验一下:

确保IIS的webdav扩展安装并被启用了,建立一个虚拟目录test,在其中放一个word文档a.doc,然后打开word, 文件->打开->输入word文档的访问url(http://localhost/test/a.doc),
修改一下文档内容,保存一下,发生了什么? 文档被保存到服务器上了.

在IE中,可以通过js创建Word.Application,来打开,修改服务器上的文档.

wApp  =   new  ActiveXObject( " Word.Application.11 " );       
        
wApp.Visible  =   true  ;
        
wApp.Documents.Open( url );

if ( trackRevisions ){ //可以实现痕迹保留呢
     wApp.ActiveDocument.TrackRevisions  =   true  ;
     wApp.ActiveDocument.ShowRevisions  =   false   ;
} else
{
     wApp.ActiveDocument.TrackRevisions  =   false  ;
     wApp.ActiveDocument.ShowRevisions  =   false   ;           
}      
           
wApp.ActiveDocument.Application.UserName =  Global_CurrentUserName;

另外,安装office时,会同时按装一个ActiveX组件:Sharepoint.OpenDocuments,可么用此组件来激活word,编辑服务器上的文档:

var  __OpenDocuments  =   null  ;
    
     function  Document_Edit2( url )
    {
         if ( __OpenDocuments  ==   null  )
        {
             try {
             __OpenDocuments  =   new  ActiveXObject( " SharePoint.OpenDocuments.3 " ); //for office 2007
            } catch (e){} 
           
             if (  __OpenDocuments  ==   null   ||   typeof (__OpenDocuments)  ==   " #ff0000 "  )
            { 
                 try {
                 __OpenDocuments  =   new  ActiveXObject( " SharePoint.OpenDocuments.2 " );  //for office 2003
                } catch (e){}               
             }
              
             if ( __OpenDocuments  ==   null   ||   typeof (__OpenDocuments)  ==   " undefined "  )
             {
              alert(  " 请安装Word(2003或更高版本) "  );
               return  ;
             }
            
        }           
          //  openDocObj.ViewDocument("http://www.abc.com/documents/sample.doc");, "Word.Document"            
          // openDocObj.CreateNewDocument("http://www.abc.com/documents/sampleTemplate.dot", "http://www.abc.com/documents/");              
         
         var  result  =  __OpenDocuments.EditDocument( url ,  " Word.Document "  );
        
         if ( result  ==   false  )
        {
            alert(  " 无法打开文档. "  );
        }    
    }

可以看到,基于IIS的webdav支持,可以非常简单的实现office文档的在线编辑, 但有一个问题:这样,文档是存放在文件系统上,我们很多系统中,

文档是存放在数据库中的,这样一来,如何实现呢???

本篇将讲解如何实现客户端的office直接编辑数据库中的二进制形式保存的office文件。

实现的关键:模拟IIS,自己实现一个webdav的服务器端。

首先,我们简单了解一下webdav:
webdav,中文可以翻译为网络分布式协作协议,它解决了http协议中一个问题:http无法实现版本和单访问控制。
什么是单访问控制呢?假设我们有一个页面编辑某条数据,这个页面可以同时被多个用户使用,那么最终的数据是最后一个用户提交的数据,
而其他用户是不知道的.我们的99%的web程序都存在此问题,当然通过编码可以解决,但http协议本身并没有提供对这种情形的支持。

webdav协议在标准的http协议的基础上,扩展了以下请求动作(verb):
PUT:用于客户端推送二进制文件。(好像http有这个verb)
LOCK:用户锁定一个资源,保证资源的单访问
UNLOCK:解锁一个资源
OPTIONS:获取服务器可以支持的请求类型
DELETE:删除服务器文件
PROPFIND:查询文件属性
其他动作: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
要详细地了解webdav,大家可以google一下,或访问http://en.wikipedia.org/wiki/WebDAV

笔者在实现这个解决方案的时候,是采用fiddler,debug IE的http请求,才搞懂了IIS本身的实现机制,为了形象化,可以看一下webdav请求相应的
数据:
发起一个OPTIONS请求
OPTIONS /PMDemo/Test/待办事务.doc HTTP/1.1
User-Agent: Fiddler
Host: localhost

响应如下:
HTTP/1.1 200 OK
Date: Wed, 27 Dec 2006 11:34:03 GMT
Server: Microsoft-IIS/6.0
MicrosoftOfficeWebServer: 5.0_Pub
X-Powered-By: ASP.NET
MS-Author-Via: DAV
Content-Length: 0
Accept-Ranges: bytes
DASL: <DAV:sql>
DAV: 1, 2
Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH
Allow: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK
Cache-Control: private

搞清楚了这些,下面我们的任务就是如何在asp.net中实现一个wevdav服务器.
显然,这要求我们需要在底层截获http请求,幸运的是asp.net中支持这种技术:HttpHandler.它可以让我们自己的代码来处理http请求.

首先,我们在web.config中做如下配置:

     < httpHandlers >
             < remove  verb ="*"  path ="*" />             
             < add  verb ="GET,PUT,UNLOCK,LOCK,OPTIONS"  path ="*.doc,*.xml"  type ="Webdav.WebdavProtocolHandler, Webdav " />
      </ httpHandlers >

通过这个配置,使我们的 WebdavProtocolHandler可以来处理webdav请求.

WebdavProtocolHandler类是一个标准的httphandler,实现了IHttpHandler接口,它按照客户端的请求类型,返回符合wevdav协议的数据.

WebdavProtocolHandler类需要按照不同的webdav请求动作,做不同的处理,那么怎么来实现这个类呢?
这里就要用到一个设计模式:命令模式.

首先定义一个接口:

public   interface  IVerbHandler
{
       void  Process( HttpContext context );
}

实现对Options请求的处理:

class  OptionsHandler : IVerbHandler
    {
         #region  IVerbHandler 成员

public   void  Process(System.Web.HttpContext context)
        {
            context.Response.AppendHeader( " DASL " ,  " <DAV:sql> " );
            context.Response.AppendHeader( " DAV " ,  " 1, 2 " );

context.Response.AppendHeader( " Public " ,  " OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH " );

context.Response.AppendHeader( " Allow " ,  " OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK " );
        }

#endregion
    }

webdav的请求verb多达15个以上,大多数情况下,我们并不需要一个完整的webdav支持,故我们只要对其中的几个进行实现即可。

实现对LOCK的支持:

  class  LockHandler : IVerbHandler
    {
         #region  IVerbHandler 成员

public   void  Process(System.Web.HttpContext context)
        {
            context.Response.ContentType  =   " text/xml " ;

string  token  =  Guid.NewGuid().ToString()  +   " : "   +  DateTime.Now.Ticks.ToString() ;

context.Response.AppendHeader( " Lock-Token " ,  " <opaquelocktoken: "   +  token  +   " > " );
          
             string  xml  =   @" <?xml version=""1.0""?>
<a:prop xmlns:a=""DAV:""><a:lockdiscovery>
<a:activelock><a:locktype><a:write/></a:locktype>
<a:lockscope><a:exclusive/></a:lockscope><owner xmlns=""DAV:"">Administrator</owner><a:locktoken>
<a:href>opaquelocktoken:{0}</a:href></a:locktoken>
<a:depth>0</a:depth><a:timeout>Second-180</a:timeout></a:activelock></a:lockdiscovery>
</a:prop> " ;

context.Response.Write( String.Format( xml , token ) );
            context.Response.End();
        }

#endregion
    }

注意这篇文章的主题:实现在线编辑。并没有版本控制等其他内容,大家仔细看以上的代码,服务器并没有真正实现"锁定",只是假装告诉客户端,你要的资源已经给你锁定了,你可以放心的编辑了。当然,有兴趣的朋友可以实现真正的锁定,无非可以通过给数据做一个状态字段来实现。但注意,要考虑一些复杂的情况,如自动解锁(用户打开一个文档,然后关机了,文档岂不永远锁定了?)等等。

接着,我们实现UNLOCK,同样是假的:

class  UnLockHandler : IVerbHandler
    {
         #region  IVerbHandler 成员

public   void  Process(System.Web.HttpContext context)
        {            
        }

#endregion
    }

下面,我们将实现两个最重要的请求动作的处理:Get和Put, office请求打开一个服务器上的文件时,采用get请求,office保存一个文件到服务器上时,发送put请求。

首先,我们要考虑一种数据项标识的传递策略,即:客户端发起访问数据库的office文件行,那么如何确认数据行的主键?
有两种策略:
1)通过不同的文件名 , 如,请求http://localhost/weboffice/1.doc  这个请求主键 为1的文件。
2)通过文件路径, 如,请求http://localhost/weboffice/1/文件名.doc  这个请求主键为1的文件。
我们将采用策略2。

再返回到我们对web.config做的配置:

< add  verb ="GET,PUT,UNLOCK,LOCK,OPTIONS"  path ="*.doc,*.xml"  type ="Webdav.WebdavProtocolHandler, Webdav " />

这个配置允许 WebdavProtocolHandler处理所有对doc和xml的请求处理,为什么要允许xml呢,因为office2003之后,支持xml格式,可以直接在
数据库重以xml的格式存放office文件。

接着,我们要确认我们的数据存储结构,即,office文件在数据库中时如何存放的。

我们有一个附件表:Document

CREATE   TABLE   [ dbo ] . [ Document ]  (
     [ DocumentId ]   [ int ]   IDENTITY  ( 11 )  NOT   NULL  ,
     [ Name ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ Description ]   [ text ]  COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ CreateTime ]   [ datetime ]   NULL  ,
     [ Size ]   [ int ]   NULL  ,
     [ CreatorId ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ CreatorName ]   [ char ]  ( 10 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ CreateYear ]   [ int ]   NULL  ,
     [ ContentType ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ DeptId ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ DeptName ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ Content ]   [ image ]   NULL  ,
     [ ModifyTime ]   [ datetime ]   NULL  ,
     [ OwnerType ]   [ varchar ]  ( 50 ) COLLATE Chinese_PRC_CI_AS  NULL  ,
     [ TemplateAble ]   [ bit ]   NULL  
)  ON   [ PRIMARY ]  TEXTIMAGE_ON  [ PRIMARY ]
GO

设计一个文裆实体:

    [Serializable]
    public   class  Document
   {
        public  Document()
       { }

static   public  Document FromPostFile(System.Web.HttpPostedFile file , User user )
       {
           Document doc  =   new  Document(file);
           doc.CreateTime  =  DateTime.Now;
           doc.CreatorId  =  user.Id;
           doc.CreatorName  =  user.Name; 
           doc.DeptId  =  user.OrgId;
           doc.DeptName  =  user.OrgName;
            return  doc;
       }

public  Document(System.Web.HttpPostedFile file)
        {
             string [] strs  =  file.FileName.Split(  ' \\ '  );
             this .Name  =  strs[strs.Length  -   1 ];
            Size  =  file.ContentLength;
             // 读取文件的数据
             this .Content  =   new   byte [Size];
            Stream fileDataStream  =  file.InputStream;
            fileDataStream.Read(  this .Content ,  0 , Size );
            ContentType  =  file.ContentType;
        }

private   int  _DocumentId;
       ///   <summary>
       ///    任务名
       ///   </summary>
       private   string  _Name;
       ///   <summary>
       ///    任务描述
       ///   </summary>
       private   string  _Description;
       ///   <summary>
       ///    报表创建时间
       ///   </summary>
       private  DateTime _CreateTime  =  DateTime.Now ;
       private   int  _Size  =   0  ;
        private   byte [] _Data;
       ///   <summary>
       ///    创建人Id
       ///   </summary>
       private   string  _CreatorId;
       ///   <summary>
       ///    创建人名
       ///   </summary>
       private   string  _CreatorName;

private   int  _CreateYear;
       private   string  _ContentType;
       ///   <summary>
       ///    部门ID(便于统计)
       ///   </summary>
       private   string  _DeptId;
       ///   <summary>
       ///    部门名
       ///   </summary>
       private   string  _DeptName;   
       //  Property DocumentId
       public   int  DocumentId   
      {
          get
         {
             return  _DocumentId;
         }
          set
         {
             this ._DocumentId  =  value;
         }
      }      
       //  Property Name
       public   string  Name   
      {
          get
         {
             return  _Name;
         }
          set
         {
              this ._Name  =  value;
         }
      }      
       //  Property Description
       public   string  Description   
      {
          get
         {
             return  _Description;
         }
          set
         {
              this ._Description  =  value;
         }
      }      
       //  Property CreateTime
       public  DateTime CreateTime   
      {
          get
         {
             return  _CreateTime;
         }
          set
         {
             this ._CreateTime  =  value;
         }
      }
        private  DateTime _ModifyTime  =  DateTime.Now;
        public  DateTime ModifyTime
       {
            get
           {
                return  _ModifyTime;
           }
            set
           {
                this ._ModifyTime  =  value;
           }
       }      
       //  Property Size
       public   int  Size   
      {
          get
         {
             return  _Size;
         }
          set
         {
             this ._Size  =  value;
         }
      }      
       //  Property Data
       public   byte [] Content   
      {
          get
         {
             return  _Data;
         }
          set
         {
             this ._Data  =  value;
         }
      }      
       //  Property CreatorId
       public   string  CreatorId   
      {
          get
         {
             return  _CreatorId;
         }
          set
         {
             this ._CreatorId  =  value;
         }
      }

//  Property CreatorName
       public   string  CreatorName
      {
           get
          {
               return  _CreatorName;
          }
           set
          {
               this ._CreatorName  =  value;
          }
      }      
       //  Property CreateYear
       public   int  CreateYear   
      {
          get
         {
             return  _CreateYear;
         }
          set
         {
             this ._CreateYear  =  value;
         }
      }      
       //  Property ContentType
       // application/msword
       // text/plain
       public   string  ContentType   
      {
          get
         {
             return  _ContentType;
         }
          set
         {
              this ._ContentType  =  value;
         }
      }

//  Property DeptId
       public   string  DeptId
      {
           get
          {
               return  _DeptId;
          }
           set
          {
               if  ( this ._DeptId  !=  value)
                   this ._DeptId  =  value;
          }
      }
       //  Property DeptName
       public   string  DeptName
      {
           get
          {
               return  _DeptName;
          }
           set
          {
               this ._DeptName  =  value;
          }
      }

private   string  _Type;
        public   string  OwnerType
       {
            get
           {
                return  _Type;
           }
            set
           {
                this ._Type  =  value;
           }
       }
        private   bool  _TemplateAble;
       ///   <summary>
       ///  是否可以作为模版
       ///   </summary>
        public   bool  Templateable
       {
            get
           {
                return  _TemplateAble;
           }
            set
           {
                this ._TemplateAble  =  value;
           }
       }
        public   override   string  ToString()
       {
            return  Encoding.UTF8.GetString( this .Content);
       }

public   static  Document FromString( string  s, User user)
       {
           Document doc  =   new  Document();
           doc.CreateTime  =  DateTime.Now;
           doc.CreatorId  =  user.Id;
           doc.CreatorName  =  user.Name;
           doc.DeptId  =  user.OrgId;
           doc.DeptName  =  user.OrgName;
           doc.Content  =  Encoding.UTF8.GetBytes(s);
           doc.Size  =  doc.Content.Length;
           doc.ContentType  =   " text/plain " ;
            return  doc;
       }
       public   static   string  ByteToString(  byte [] bytes )
      {
           return  Encoding.UTF8.GetString( bytes );
      }
        public   static   byte [] StringToByte( string  s)
       {
            return  Encoding.UTF8.GetBytes(s); 
       }
        public   string  GetExtendName()
       {
            string [] arr  =   this .Name.Split(  ' . '  );

if  (arr.Length  <   1 )  return   "" ;
            else   return  arr[ arr.Length  -   1  ];
       }   
   }

考虑到数据操作逻辑的可变性,不同的项目里面附件表设计的不同,这里引入一个数据操作接口:

public   interface  IWebdavDocumentHandler
{
        Document GetDocument( int  id);//获取文档数据
         void  ModifyDocContent( int  docId,  byte [] data);//修改文档内容
}

具体的实现这里就不写了。

好了,我们的数据访问逻辑已经有了,那么首先看get动作处理的实现:

     class  GetHandler : IVerbHandler
    {
         #region  IVerbHandler 成员
         public   void  Process(System.Web.HttpContext context)
        {
             int  id  =  WebdavProtocolHandler.GetDocumentId( context ); //获取到主键

IWebdavDocumentHandler  docSvr  =  new  DefaultWebdavDocumentHandler() ; //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式
            Document doc  =  docSvr.GetDocument(id);

if  (doc  ==   null )
            {
                context.Response.Write( " 文档不存在! " );
                 return ;
            }

context.Response.Clear();
            context.Response.ContentType  =  doc.ContentType;
             // 下载文件名限制32字符 16 汉字
             int  maxlength  =   15 ;
             string  fileName  =  doc.Name;  // att.FileName ;
             if  (fileName.Length  >  maxlength)
            {
                fileName  =   " - "   +  fileName.Substring(fileName.Length  -  maxlength, maxlength);
            }

fileName  =  HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8);  // 必须编码,不然文件名会出现乱码
            context.Response.AppendHeader( " Content-Disposition " ,  " attachment;filename= "   +  fileName  +   "" );

if  (doc.Content  !=   null   &&  doc.Content.Length  >   0 )
                context.Response.BinaryWrite(doc.Content);

context.Response.End();
        }
         #endregion
    }

很简单吧,跟我们普通实现文档下载的代码一样。

put动作的实现:

  class  PutHandler : IVerbHandler
    {
         #region  IVerbHandler 成员

public   void  Process(System.Web.HttpContext context)
        {
             int  docId  =  WebdavProtocolHandler.GetDocumentId(context);

Document doc  =  GetDocFromInput(context.Request);

doc.DocumentId  =  docId;

IWebdavDocumentHandler  docSvr  =  new  DefaultWebdavDocumentHandler() ; //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式

docSvr.ModifyDocContent( doc.DocumentId , doc.Content );
        }

private  Document GetDocFromInput(System.Web.HttpRequest request )
        {
            Document doc  =   new  Document();
             // 读取文件的数据
            doc.Content  =   new   byte [ request.ContentLength ];
            doc.Size  =  request.ContentLength;
            Stream fileDataStream  =  request.InputStream;
            fileDataStream.Read( doc.Content ,  0 , doc.Size );
            doc.ContentType  =  request.ContentType;
             return  doc;
        }
         #endregion
    }

OK,主要的动作都实现了,下面,我们需要WebdavProtocolHandler将各命令处理对象整合到一起:

     public   class  WebdavProtocolHandler : IHttpHandler
    {
         public   static   int  GetDocumentId( HttpContext context )//按照前面确定的主键策略返回主键
        {
             string  url  =  context.Request.Url.ToString();
             string [] arr  =  url.Split(  ' / '  );
             string  id  =  arr[arr.Length  -   2 ];
             return  Convert.ToInt32( id );
        }
         public   void  ProcessRequest(HttpContext context)
        {
            HttpRequest Request  =  context.Request;
            context.Response.AppendHeader( " OpenWebDavServer " ,  " 1.0 " );
             string  verb  =  Request.HttpMethod;
             // Log.Write(verb);
            IVerbHandler vh  =  GetVerbHandler( verb );

if ( vh  ==   null  )
                 return  ;

vh.Process(context);      
        }

private  IVerbHandler GetVerbHandler( string  verb)
        {
             switch  (verb)
            {
                 case   " LOCK "  :
                     return   new  LockHandler();
                 case   " UNLOCK " :
                     return   new  UnLockHandler();
                 case   " GET " :
                     return   new  GetHandler();
                 case   " PUT " :
                     return   new  PutHandler();               
                 case   " OPTIONS " :
                     return   new  OptionsHandler();
                 default  :
                     return   null ;
            }
        }     
         public   bool  IsReusable
        {
             get  {  return   false ; }
        }
    }

到这里呢,已经基本上算game over了,基于以上代码设计,可以完全实现office文档的在线编辑。若要通过链接直接打开编辑,可以
采用 Office文档在线编辑的实现之一 的 Document_Edit2函数触发office编辑。

哦,IIS还需要做一点小配置:
1)将.doc , .xml 加入到站点虚拟目录的isapi映射, 不要选中 "确认文件是否存在",动作要选全部动作,
2)禁用IIS本身的Webdav扩展, 
3)删除虚拟目录HTTP头中的自定义HTTP头: MicrosoftOfficeWebServer,如果有的话。

原文:http://www.cnblogs.com/jianyi0115/archive/2007/07/15/818566.html

Office文档在线编辑的实现相关推荐

  1. [转载]Office文档在线编辑的实现之二

    上篇文章 http://www.cnblogs.com/jianyi0115/archive/2007/03/16/677712.html 讲述了如何通过iis的webdav支持实现客户端的offic ...

  2. 用pageOffice控件实现 office 文档在线编辑Word 打开文档后在页面里触发事件

    OA办公中,业务需要编辑打开word文档后 执行一些js操作 怎么实现编辑打开word文档后 执行一些js操作呢? 2 实现方法 通过pageOffice实现简单的在线打开编辑word时, 通过设置 ...

  3. Office文档在线编辑

    Office文档特别是Word文档在线编辑基本上有如下几个方案: 1.使用dsoframer,毕竟是MS微软的东东,功能很强悍,而且国内的很多产品都是基于dsoframer开发的 2.使用WebOff ...

  4. 通达OA-今日学习:OFFICE文档在线编辑控件与工作流表单手写签章控件的区别是什么?

    NTKOOFFICE文档在线编辑控件,控件提供商为重庆软航科技,使用范围:OA各个模块上传附件(比如文件柜.工作流.公告通知等模块),如果是Office文档,就可以在线编辑或阅读,在线编辑时可以加盖电 ...

  5. 【Office文档在线编辑和预览服务搭建】

    友情提醒!文章篇幅巨长!!! 原博客文章https://blog.csdn.net/m0_66640832/article/details/124482483转移到此更新 搭建流程:域控服务==> ...

  6. Office文档在线编辑和预览服务搭建

    友情提醒!文章篇幅巨长!!! 搭建流程:域控服务==>OWA服务==>WOPI服务 1. 搭建域控服务器 准备一台服务器,干净环境,系统版本:window server 2016 服务搭建 ...

  7. JavaScript Office文档在线编辑备忘

    来源: http://www.cnblogs.com/jianyi0115/articles/677712.html http://www.cnblogs.com/fxwdl/archive/2009 ...

  8. 用pageOffice文档控件实现 office文档在线编辑

    第三方文档控件,pageOffice 系统开发中经常要处理办公文档,如果word,excel,ppt,编辑整理,保存,归档. 开发市场上也有很多第三文文档控件,多年的总结,还是认为pageOffice ...

  9. 文档在线编辑开发心得

    一.背景 在本次公司的开发任务中,偶然接触到了畅写office的文档在线编辑集成开发,在开发中遇到点问题,觉得这个东西挺有趣的,写此文章保存开发心得. 二.前期准备 文档在线编辑功能只有一个api.文 ...

最新文章

  1. QIIME 2教程. 30补充资源SupplementaryResources(2021.2)
  2. 自律到极致-人生才精致:第11期 - 领奖通知
  3. glide默认的缓存图片路径地址_手写一个静态资源中间件,加深了解服务器对文件请求的缓存策略...
  4. Spring MVC和Thymeleaf:如何从模板访问数据
  5. [vue] v-on可以绑定多个方法吗?
  6. 6-MyBatis基础
  7. 博客目录 Blog directory
  8. 与target_el 相关的 makeNode
  9. paip.提升效率----几款任务栏软件
  10. 168.Excel表列名称
  11. Java 防止 SQL 注入工具类
  12. 一个高难度的 Java 3D 智力游戏,立方四子棋
  13. JavaWeb实用项目之----化妆品销售网
  14. 微信小程序学习之路——API媒体
  15. 准备要注销的公司,但是公司名下商标怎么处理?
  16. NoSQL之 Redis配置与优化
  17. Cipher Code
  18. 算法竞赛中计算机1000ms一般能运行的范围
  19. MySQL数据库-删除表中的数据详解
  20. laravel-admin 省市区三级联动的爬坑问题

热门文章

  1. 大疆招聘java工程师_Java工程师(上海)
  2. 自选天气,F1 Delta Time 大奖赛重燃战火
  3. 【Linux】Linux文件目录
  4. STM32学习笔记(六 定时器及应用 4 光敏传感器实验 )
  5. 安卓解析xml格式字符串
  6. 相片打印机原理_喷墨打印机工作原理 喷墨打印机优缺点介绍【详解】
  7. iOS后台运行任务的应用
  8. Go语言从控制台读取数据
  9. vue下的@change事件
  10. 虚拟机Ubuntu18.04开机后一直卡在[OK] Started GNOME Display Manager处,进不去系统。