2016年10月我参加了在北京举行的DevDays Asia 2016 - Office 365应用开发”48小时黑客马拉松“,我开发的一个Word Add-In Demo——WordTemplateHelper获得了二等奖。在会场有幸结识了陈希章老师,在与陈老师的交流中受益良多,得知陈老师在准备一个Office解决方案系列后,我想把这个Demo的开发过程简要介绍给大家,以支持陈老师的无私奉献,也希望更多的开发者参与到Office365的开发中来。

Office相关开发主要可以参考这个地址:https://dev.office.com/getting-started

本篇文章主要介绍其中的Office加载项开发,即Office Add-ins:https://msdn.microsoft.com/ZH-CN/library/office/jj220082.aspx

一、什么是Office Add-Ins

什么是Office Add-ins呢?在陈老师的上一篇文章中,对整个Office发展历史都进行了梳理,我个人的理解就是,开发者可以在Office提供的平台上,对Office做出一定的扩展以实现各种功能,比如之前录制的宏,写的VBS的脚本,某种意义上都可以看做是Office的Add-ins。当然这只是个人理解,不一定准确。目前的Office Add-Ins只支持Office2013以后的版本,开发方式也和以前的VBS有了很大的区别。

现在的Office Add-ins结构是这样的:

一个Office Add-in其实是一个Web App,可以将其部署在任意位置,它可以在一个Office应用程序中运行。有一个manifest.xml清单文件用来指定该Web App如何来呈现,包括定义Web App 的URL。当Office加载这个Add-in时,实际上是提供了一个浏览器的环境,来运行指定的Web App。也就是说,现在开发一个Office Add-in,其实跟开发网页程序差不多,这对熟悉html+JavaScript+css的前端开发人员是非常容易上手的。微软提供了丰富的JavaScript API来对Office进行操作,能实现什么就取决于开发者的想象力了。

一个Word Add-In的实例:

二、Word Template Helper需求分析

我在得知有这个活动时,并没有想好要做什么,一直到坐上赴京的高铁,才慢慢有了一个想法,这个想法也是来自平时的工作需要。在工作中经常要撰写大量的文档,如各种软件需求规格说明书、公函、文书、操作手册等,这些文档都有规定的格式,一般情况下我是将一些已经写好的Word文档保存在一个文件夹里当做模板,下次写这种文档的时候复制一份,删删减减的再改。为何不自己写个程序,将这些具有固定模式的文档作为Word模板呢?虽然Word也有自己的模板,但实际上是非常有限的,并不能完全满足我们的需要。如果这个功能做成一个模板商店,大家可以自由上传、分享各自的模板,也许会方便许多。

Word自带的模板是这样的:

这些通用模板对专业性比较强的工作来说是远远不够的。Word Template Helper的效果是这样的:

主意有了,那么就来看一下如何实现。我参加活动时的项目托管在码云上,为了写这篇文章,我重新梳理了这个小demo,在Github上建了一个项目,并尝试使用最新的.NET Core来实现后台API部分。接下来就跟我一起动手吧。

三、项目架构

首先分析一下该项目的结构。文档的模板数据,如模板标题、属性等,需要保存在数据库里,还需要一个Web API项目提供数据,Office Add-in为一个纯前端项目,使用Angular2框架,采用异步调用Web API的数据,实现搜索、加载模板等功能。插件的UI使用微软提供的Fabric UI。整个项目的技术栈如下所示:

至于文档的实体——Word文档,是以Word格式文件存储还是直接保存在数据库中呢?如果是正式项目的话,当然是保存在云存储中是最合适的,但对于一个sample来说,直接保存在数据库中也未尝不可。因为是参加开发马拉松,怎么快怎么来吧。包括ORM框架也是,只是为了快速实现采用的方式,不是最佳实践。

这个sample的开发环境配置如下:

Windows 10 x64,

VS 2017(请确保安装了Office开发工具)

VS Code

Node.js v7.10.0

NPM v4.2.0

ASP.NET Core 1.1

四、Web API开发

VS2017已经正式发布了,我使用最新的.NET Core来实现Web API层。

1.新建项目

新建一个空白解决方案,命名为WordTemplateHelpe,然后在其中添加一个ASP.NET Core项目:

选择Web API:

2.安装EF Core

在nuget管理器中搜索安装一下几个Nuget包:

Microsoft.EntityFrameworkCore.SqlServer:EF Core SQL Server

Microsoft.EntityFrameworkCore.Tools:EF命令行工具

Microsoft.EntityFrameworkCore.Tools.DotNet:EF Core命令行工具

3.建立Models

目前最新的EF都推荐使用Code First模式,即直接写Model,EF框架会自动创建所需的数据库。如果习惯DB First的话,也有一个很好的工具推荐:EntityFramework-Reverse-POCO-Code-First-Generator:https://visualstudiogallery.msdn.microsoft.com/ee4fcff9-0c4c-4179-afd9-7a2fb90f5838

可以直接在VS的扩展与更新里下载。这个工具可以很方便的根据数据库生成所需的实体类。

首先添加一个模板类型的枚举:

    /// <summary>/// 类型    /// </summary>public enum TemplateType{        /// <summary>/// Private         /// </summary>[Description("Private")]Private = 0,        /// <summary>/// Public         /// </summary>[Description("Public")]Public = 1,        /// <summary>/// Organization         /// </summary>[Description("Organization")]Organization = 2,}

添加一个模板类:

    public class PrivateTemplateInfo{         ///<summary>/// Id         ///</summary>public string Id { get; set; }        ///<summary>/// User Id         ///</summary>public string UserId { get; set; }        ///<summary>/// Template Id         ///</summary>public string TemplateId { get; set; }        ///<summary>/// Create Time         ///</summary>public DateTime CreateTime { get; set; }}

因为还需要组织机构模板、用户收藏等几个表,这里就不写了,可参考Github上的示例。

4.创建数据库上下文

有了Model后,需要指定哪些实体包含在数据模型中。添加一个Data文件夹,在其中创建一个名为WordTemplateContext.cs的文件:

    public class WordTemplateContext:DbContext{     

    public WordTemplateContext(DbContextOptions<WordTemplateContext> options) : base(options){}        public DbSet<WordTemplateInfo> WordTemplateInfoes { get; set; }     

   public DbSet<UserFavoriteInfo> UserFavoriteInfoes { get; set; }       

 public DbSet<PrivateTemplateInfo> PrivateTemplateInfoes { get; set; }       

 public DbSet<OrganizationTemplateInfo> OrganizationTemplateInfoes { get; set; }}

这样就为每个实体创建了一个DbSet,对应数据库中的表,实体对应表中的行。

5.使用依赖注入注册上下文

ASP.NET Core默认实现了依赖注入。要把刚才建立的WordTemplateContext注册成服务,需要在Startup.cs中添加以下代码:

  public void ConfigureServices(IServiceCollection services){            // Add framework services.services.AddDbContext<WordTemplateContext>(options =>options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));services.AddMvc();}

注意要添加using Microsoft.EntityFrameworkCore;不然会找不到UseSqlServer方法。

数据库连接字符串在appsettings.json中配置:

{  "ConnectionStrings": {    "DefaultConnection": "Server=.;User ID=sa;Password=12QWasZX;Initial Catalog=WordTemplate;"},  "Logging": {    "IncludeScopes": false,    "LogLevel": {      "Default": "Warning"}}
}

这里使用了LocalDb,用于测试。当需要正式部署时,这里需要更改为正式数据库服务器的地址及用户名密码。

6.初始化数据库

下面使用命令行初始化数据库。在Data目录下新建一个DbInitializer类,输入以下方法:

    public static class DbInitializer{        

public static void Initialize(WordTemplateContext context){context.Database.EnsureCreated();            //TODO            context.SaveChanges();}}

确保数据被创建。然后修改Startup.cs文件中的Configure方法:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, WordTemplateContext context){loggerFactory.AddConsole(Configuration.GetSection("Logging"));loggerFactory.AddDebug();app.UseMvc();DbInitializer.Initialize(context);}
7.创建API接口

现在写个Controller看看。在Controller文件夹中添加一个控制器:

这里可以使用依赖注入,将数据库上下文注入进来:

    [Produces("application/json")][Route("api/WordTemplate/[action]")]      public class WordTemplateController : Controller{      

      private readonly WordTemplateContext _context;            public WordTemplateController(WordTemplateContext context){_context = context;}

我们以一个搜索模板的api为例:

        [HttpGet]              public async Task<ResponseResultInfo<List<WordTemplateInfo>>> SearchWordTemplateList(string keyword){ResponseResultInfo<List<WordTemplateInfo>> respResult = new ResponseResultInfo<List<WordTemplateInfo>>();            try{List<WordTemplateInfo> list = await _context.WordTemplateInfoes.Where(x => x.Type == TemplateType.Public && x.Name.Contains(keyword)).OrderByDescending(x => x.CreateTime).ToListAsync();respResult.IsSuccess = true;respResult.Result = list;                return respResult;}                 catch (Exception ex){                //LogHelper.ErrorWriteLine("Something wrong. The exception message::{0}", ex);respResult.IsSuccess = false;respResult.Message = string.Format("Something wrong. The exception message::{0}", ex.Message);                return respResult;}}

命令行转到项目目录,运行以下命令

dotnet run

可以使用前端调试利器Postman来测试:

API项目运行的具体地址需要记一下,后面做Add-In的时候要用到。具体代码请参考Github。

8.允许跨域访问

为了支持Add-in能够跨域访问我们的接口,还需要安装以下的库:

然后在Startup.cs的ConfigureServices方法中添加以下代码:

#region 跨域services.AddCors(options =>options.AddPolicy("AllowCrossDomain",builder => builder.WithOrigins().AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin().AllowCredentials()));            #endregion

在需要跨域的WordTemplateController上添加一行:

[EnableCors("AllowCrossDomain ")]

这样api就可以支持跨域访问了。

五、Word Add-In开发

有了API,就可以开发Add-In部分了。开篇说到,Add-In实际上是一个Web App,通过JavaScript操作Office文档对象,具体到这个项目来说,就是使用异步的js去查询、上传、搜索存在服务器上的模板文件,并动态的对当前Word文档进行操作。

微软在Github上开源了这个JavaScript API:https://github.com/Microsoft/Office-js-docs_zh-cn,相关文档:https://msdn.microsoft.com/zh-cn/library/office/fp142185.aspx

开发步骤可参考:https://github.com/Microsoft/Office-js-docs_zh-cn/blob/master/docs/get-started/create-and-debug-office-add-ins-in-visual-studio.md

下面来开发Add-In部分。

1.新建Add-In项目

在解决方案上点击右键,添加一个Word Web外接程序:

添加完成后,多了两个项目:

其中一个是清单文件,带Web后缀的就是Web App了。

2.设置manifest.xml

清单文件是非常重要的一个文件,描述加载项的所有设置。这个文件是自动生成的,但需要我们手动修改一些地方。好在文件中都有注释,所以修改还比较容易:

最重要的是修改SourceLocation这个节点,这个地址设置的是Web App托管的位置。在Web端开发并部署后,要将这个节点改为正确的位置才能发布。下面这个节点也要改掉。

3.Web App分析

可以先运行一下这个模板试试,直接F5:

点击此处就可以调出这个插件:

会自动打开Word并加载这个插件,文档中的文本就是插件插入的。那么是哪里的代码起作用的呢?

打开Home.js文件,找到如下代码:

    function loadSampleData() {         // Run a batch operation against the Word object model.Word.run(function (context) {             // Create a proxy object for the document body.var body = context.document.body;            // Queue a commmand to clear the contents of the body.            body.clear();               // Queue a command to insert text into the end of the Word document body.            body.insertText(                "This is a sample text inserted in the document",Word.InsertLocation.end);            // Synchronize the document state by executing the queued commands, and return a promise to indicate task completion.return context.sync();}).catch(errorHandler);}

这里的Word就是JavaScript API提供的对象,可以方便的对当前文档内容进行操作。这样思路就有了,可以通过JavaScript动态去调用Web API获取查询结果,将查询到的文档内容插入到当前文档中,就实现了最初的目的。同时还可以将当前文档的内容保存为模板上传到服务器上进行分享,一个完整功能的sample已经呼之欲出了。

4.使用Angular

我们使用最新的Angular4来开发前端页面。当然如果使用JQuery的话也可以,但现在已经有点out了不是吗?使用Angular可以快速开发一个MVVM架构的单页面WebApp,非常适合这个需求。

这个demo的部分代码参考微软开源的一个项目:https://github.com/OfficeDev/Office-Add-in-UX-Design-Patterns-Code

Angular上手曲线还是有点陡的,官方给出了Angular CLI工具,可以快速搭建一个Angular应用。首先安装TypeScript:

npm install -g typescript

然后安装Angular CLI:https://github.com/angular/angular-cli

npm install -g @angular/cli

运行以下命令创建一个Angular项目:

ng new WordTemplateHelperSource

然后使用cd WordTemplateHelperSource 命令转到项目目录,运行以下命令:

npm install

这个命令会安装ng项目所需的依赖,如果安装不成功,建议切换成淘宝npm镜像进行安装。

使用以下命令运行ng项目:

ng serve

可以在Chorme浏览器中浏览http://localhost:4200来查看效果:

注意如果在IE中浏览是不正常的,这个问题我们到最后一节再给出解决办法。

为什么不直接在WordTemplateHelperWeb建呢?因为Angular应用还要进行打包,会在项目目录下生成dist目录,这才是正式要运行的部分。所以等开发完成后,将生成的dist目录内的文件拷到WordTemplateHelperWeb就可以了。

在开发Angular的过程中,推荐使用VS Code,对TypeScript和Angular的支持都非常好。

因为本篇文章不是Angular的开发教程,所以Angular的具体知识这里就不展开详述了,感兴趣的话可以自行下载Github代码运行即可。

5.添加操作Word文件的service

为了操作Word文件,我们需要将其封装成服务。使用以下命令添加一个service:

ng g service services\word-document\WordDocument

这样会在app目录中的相应路径中生成一个名为WordDocumentService的服务。与此类似,生成其他的几个service。其中主要的几个方法如下:

查询搜索的方法:

/*** search* * @param {string} keyword* @returns {Promise<ResponseResultInfo<Array<WordTemplateInfo>>>}* * @memberOf WordTemplateApiService     */searchWordTemplateList(keyword: string): Promise<ResponseResultInfo<Array<WordTemplateInfo>>> {let url = `${AppGlobal.getInstance().server}/SearchWordTemplateList?keyword=${keyword}`;let promise = this.httpService.get4Json<ResponseResultInfo<Array<WordTemplateInfo>>>(url);       return promise;}

这样可以得到服务器上存储的文档模板,实际是以Ooxml格式保存的string。

对于这个sample来说,使用Office JavaScript API并没有太难的东西,主要用到了两个方法:getOoxml()和insertOoxml(),前者可以读取当前word文档的Ooxml格式,后者可以设置当前word文档的Ooxml格式。Ooxml就是Office2007之后版本使用的格式,如docx这种。

原API提供的都是callback函数,为了使用方便我将其封装成Promise:

/*** get the ooxml of the doc* * * @memberOf WordDocumentService     */getOoxml() {        // Run a batch operation against the Word object model.return Word.run(function (context) {            // Create a proxy object for the document body.var body = context.document.body;            // Queue a commmand to get the HTML contents of the body.var bodyOOXML = body.getOoxml();            // Synchronize the document state by executing the queued commands, // and return a promise to indicate task completion.// return context.sync().then(function () {//     console.log("Body HTML contents: " + bodyHTML.value);//     return bodyHTML.value;// });return context.sync().then(() => { return bodyOOXML.value });}).catch(function (error) {console.log("Error: " + JSON.stringify(error));                if (error instanceof OfficeExtension.Error) {console.log("Debug info: " + JSON.stringify(error.debugInfo));}                return "";});}    /*** set the ooxml of the doc* * @param {string} ooxml * * @memberOf WordDocumentService     */setOoxml(ooxml: string) {        // Run a batch operation against the Word object model.Word.run(function (context) {            // Create a proxy object for the document body.var body = context.document.body;            // Queue a commmand to insert OOXML in to the beginning of the body.            body.insertOoxml(ooxml, Word.InsertLocation.replace);            // Synchronize the document state by executing the queued commands, // and return a promise to indicate task completion.return context.sync().then(function () {console.log('OOXML added to the beginning of the document body.');});}).catch(function (error) {console.log('Error: ' + JSON.stringify(error));                if (error instanceof OfficeExtension.Error) {console.log('Debug info: ' + JSON.stringify(error.debugInfo));}});}

当搜索到合适的模板后,可以单击按钮,调用setOoxml()方法,将其插入到当前word文档中:

applyTemplate(template: WordTemplateInfo) {    this.wordDocument.setOoxml(template.TemplateContent);}

这样就完成了应用模板的功能。

如果要实现将当前文档的内容保存为模板上传到服务器上,就可以调用getOoxml()方法得到当前文档的Ooxml格式文本,上传到服务器保存即可。至于其他的加为收藏、添加为机构模板、设置为个人模板等都是设置模板属性更新了,具体代码不再赘述。

还有一点需要注意的是,开发的时候,这里的服务器地址要写刚才我们开发的ASP.NET Core的地址。

6.使用Fabric UI

对于一个Office Add-in来说,具有简洁美观、与Office统一的UI是必须的。微软推荐使用Fabric UI来实现统一的界面样式,详见:https://dev.office.com/fabric

这里提供了样式、图标、设计规范等很多资源,甚至还提供了React版的组件,如果使用React开发的话直接拿来用就可以了。这个demo是直接引用的style文件,配置在.angular-cli.json文件中:

应用后就变成这样子:

7.打包Add-in

刚才只是在一个新项目里开发了一个静态Web App,还要将其打包,复制到WordTemplateHelperWeb项目中。使用ng build –prod来打包Angular应用。打包后的文件会输出到dist目录下:

注意还有一个需要注意的地方,如果仅这样打包的话,是不支持IE浏览器的,但Office Add-In实际上内置的浏览器就是IE内核,所以我们需要做如下修改,找到src目录中的polyfills.ts文件,将下面部分的注释取消:

还要根据提示,运行npm install命令安装几个必须的依赖。这样才能在IE系列浏览器中正常运行。再次运行ng build –prod进行打包。--prod参数的意义是以生产模式进行build,这样生成的代码体积更小,运行速度更快。

将WordTemplateHelperWeb项目中的原文件除了Web.config外,全部删除。把dist目录中的文件复制过来。

虽然本机开发时可以直接调试运行,但为了模拟真实的使用情况,我们把这个Web App也正式发布一下。如果我们有Azure或其他主机的话就直接部署到服务器上,现在只用本机IIS来承载这个Web App:

这样该Add-In的地址就是:http://localhost/WordTemplateHelperWeb,

下面把api运行起来,进入WordTemplateHelperApi目录,运行dotnet run命令:

这样API项目的地址是:http://localhost:5000/api/

这两个地址不要混淆。刚才在打包WebApp的时候也要注意,在common\app-global.ts文件中的api地址也要改成和实际api地址一样的才可以:

    /*** api url* * @type {string}* @memberOf AppGlobal     */public server: string = "http://localhost:5000/api/WordTemplate";

现在打开WordTemplateHelperManifest清单文件,修改如下位置:

这里填的是Add-In的地址,一定不要搞错了。

6.运行测试

现在可以重新运行Add-In项目了,将启动项目设置为WordTemplateHelper,运行:

我们可以粘贴一个模板,并上传到服务器上:

点击Upload按钮即可将当前文档作为模板上传到服务器上分享。

搜索到相应的模板后,点击apply按钮即可将模板内容插入到当前文档。

我们可以搜索模板,添加自己的模板,并将模板内容应用到当前文档中。针对组织和个人还可以分别进行管理,我的设想是,这个小插件能够做成一个模板商店之类的平台,用户可以自由的交换彼此的文档模板,并可以收藏、添加到本人组织的模板库中等等。稍加扩展就可以做成一个正式产品了。

7.载入加载动画

在页面加载时可以加一个载入提示,使用户体验更加友好。具体代码可参考index.html中的css样式。

六、小结

这篇文章拖了很久,去年的比赛,今年才把过程整理出来,实在很想对陈老师说一声抱歉^_^。Office Add-In是一个比较新的开发领域,跟以前的开发方式有所不同,但熟悉前端的同学可以迅速进入这个领域,实际上就是写网页。这个实例从后端接口到前台实现,是一个比较完整的项目,希望对Office开发有兴趣的同学下载代码研究一下,开发出更加实用的Add-In。因为这个项目并没有实际部署,所以没有上传到商店中。下载代码的用户请勿用于商业用途。特此说明。

Github地址:https://github.com/yanxiaodi/WordTemplateHelper

原文地址:http://www.cnblogs.com/yanxiaodi/p/7192280.html


.NET社区新闻,深度好文,微信中搜索dotNET跨平台或扫描二维码关注

Office365开发系列——开发一个全功能的Word Add-In相关推荐

  1. 开发一个全功能的 Word Add-in

    (点击放大图像) \\ \\ 2016 年 10 月我参加了在北京举行的 DevDays Asia 2016 - Office 365 应用开发 "48 小时黑客马拉松 ",我开发 ...

  2. 用终端访问路由器设置端口开发_Serial for Mac(全功能串行终端管理软件)

    Serial for mac是一个现代化的终端设计,使与服务器,网络设备的工作,并为工程师和系统管理员嵌入式硬件更容易.内置从头开始为OS X,串行不再需要搜索并安装驱动程序,以便与大多数的USB转串 ...

  3. 【安卓开发系列 -- 开发环境】Unbuntu 下 Android 持续集成打包环境搭建 -- Jenkins 构建工具安装(gradle + git + android 工具)

    [安卓开发系列 -- 开发环境]Unbuntu 下 Android 持续集成打包环境搭建 -- Jenkins 构建工具安装(gradle + git + android 工具) [1]Unbuntu ...

  4. 一个全功能的个人财务管理中的应用,功能包括:网上银行,在线支付账单,投资管理,预算跟踪,预定交易,检查印刷,详细的图表,报告等等。

    Mac财务软件哪款好?Moneydance 2021是一个全功能的个人财务管理中的应用,功能包括:网上银行,在线支付账单,投资管理,预算跟踪,预定交易,检查印刷,详细的图表,报告等等. Moneyda ...

  5. Septentrio:mosaic系列内置全功能Ntrip

    mosaic的系列产品内置了全功能Ntrip(Full-feature Ntrip),包括Ntrip Server.Ntrip Client以及Ntrip Caster.在这期,将围绕以下三个话题展开 ...

  6. 智能手表能测新冠?你的Fitbit可能是一个全功能脉搏血氧计

    全文共2776字,预计学习时长9分钟 图源:unsplash 新冠病毒最让人担心的一点就是,它能将一名病情稳定的感染者迅速带入病危的境地.Covid-19官方指南告诉我们,要呆在家里,就像患了感冒或流 ...

  7. [ Office 365 开发系列 ] 开发模式分析

    前言 本文完全原创,转载请说明出处,希望对大家有用. 在正式开发Office 365应用前,我们先了解一下Office 365的开发模式,根据不同的应用场景,我们选择最适合的开发模式. 阅读目录 Of ...

  8. 搭建一个全功能 GPS 追踪系统

    0X00    前言 Traccar 是一个开源的 GPS 跟踪系统.此存储库包含基于 Java 的后端服务.它支持 170 多种 GPS 协议和 1500 多种型号的 GPS 跟踪设备.Tracca ...

  9. pdf.net sod oracle,SOD: 原PDF.NET框架将成为一个全功能的企业开发框架,而 SOD框架将是PDF.NET开发框架下面的 “数据开发框架...

    Beginner:How to use Entity Framework? SOS! Senior men:Try using the SOD Framework! 菜鸟:怎么使用EF框架啊?遇到麻烦 ...

最新文章

  1. 为什么 Linux 和 macOS 不需要碎片整理
  2. xmake新增对WDK驱动编译环境支持
  3. 面试题:冒泡排序的优化
  4. Codeforces 985C (贪心)
  5. android:碎片的概念
  6. GeneralUpdate20220323里程碑版本发布
  7. python变量名要求_Python中用中文变量名、函数名,会影响性能吗?
  8. 黑鲨4S系列正式发布全系2699元起并推出自由高达联名套装
  9. Linux基础——怎么样用 TeamViewer 和 VNC 从远程控制电脑
  10. [转载] python中pprint模块详解——print()和pprint()两者的区别
  11. 拓端tecdat|R语言Gabor滤波进行目标图像纹理特征的提取
  12. 了解有关计算机病毒的知识,从基础知识开始 全面认识计算机病毒
  13. 二叉树层序遍历算法实现
  14. 新商用密码产品认证梳理——检测和认证机构篇
  15. 学习可爱彩色线条PS极简马克笔简笔画:饮品篇
  16. 爬虫笔记-Bugku秋名山老司机(入门)
  17. 使用计算机需要准备硬件和什么,2017年计算机硬件知识参考试题
  18. 计算机网络笔记Part3 数据链路层(Data Link Layer)
  19. 艾司博讯:拼多多机器人客服在哪里
  20. 案例:谷歌人工智能算法Dropout申请专利

热门文章

  1. 理解流量监管和整形的关键算法—令牌桶
  2. 4项技巧使你不再为PHP中文编码苦恼
  3. 什么是事件冒泡?如何用jquery/js阻止事件冒泡?阻止冒泡有什么作用?小生来抛个砖。...
  4. 测试Live Writer 发表博客
  5. 用sniffer技术盗取电话银行密码
  6. 如何使用ASP.NET Core Web API实现短链接服务
  7. 前端快闪三:多环境灵活配置react
  8. 不会自动化UI测试?不会编程?没问题,会造句就行!
  9. WPF 动态更换图片路径
  10. asp.net core中使用cookie身份验证