ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 - NanaseRuri - 博客园

mikel阅读(882)

基本登录页面以及授权逻辑的建立

来源: ASP.NET Core 打造一个简单的图书馆管理系统 (修正版)(二)用户数据库初始化、基本登录页面以及授权逻辑的建立 – NanaseRuri – 博客园

前言:

本系列文章主要为我之前所学知识的一次微小的实践,以我学校图书馆管理系统为雏形所作。

本系列文章主要参考资料:

微软文档:https://docs.microsoft.com/zh-cn/aspnet/core/getting-started/?view=aspnetcore-2.1&tabs=windows

《Pro ASP.NET MVC 5》、《锋利的 JQuery

 

当此系列文章写完后会在一周内推出修正版。

 

此系列皆使用 VS2017+C# 作为开发环境。如果有什么问题或者意见欢迎在留言区进行留言。

项目 github 地址:https://github.com/NanaseRuri/LibraryDemo

 

 

本章内容:Identity 框架的配置、对账户进行授权的配置、数据库的初始化方法、自定义 TagHelper

 

 

 一到四为对 Student 即 Identity框架的使用,第五节为对 Admin 用户的配置

 

 

一、自定义账号和密码的限制

在 Startup.cs 的 ConfigureServices 方法中可以对 Identity 的账号和密码进行限制:

复制代码
 1             services.AddIdentity<Student, IdentityRole>(opts =>
 2             {
 3 
 4                 opts.User.RequireUniqueEmail = true;
 5                 opts.User.AllowedUserNameCharacters = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789";
 6                 opts.Password.RequiredLength = 6;
 7                 opts.Password.RequireNonAlphanumeric = false;
 8                 opts.Password.RequireLowercase = false;
 9                 opts.Password.RequireUppercase = false;
10                 opts.Password.RequireDigit = false;
11             }).AddEntityFrameworkStores<StudentIdentityDbContext>()
12                 .AddDefaultTokenProviders();
复制代码

RequireUniqueEmail 限制每个邮箱只能用于一个账号。

此处 AllowedUserNameCharacters 方法限制用户名能够使用的字符,需要单独输入每个字符。

剩下的设置分别为限制密码必须有符号 / 包含小写字母 / 包含大写字母 / 包含数字。

 

 

 

二、对数据库进行初始化

在此创建一个 StudentInitiator 用以对数据库进行初始化:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task Initial(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             if (userManager.Users.Any())
 7             {
 8                 return;
 9             }
10             IEnumerable<Student> initialStudents = new[]
11             {
12                 new Student()
13                 {
14                     UserName = "U201600001",
15                     Name = "Nanase",
16                     Email = "Nanase@cnblog.com",
17                     PhoneNumber = "12345678910",
18                     Degree = Degrees.CollegeStudent,
19                     MaxBooksNumber = 10,
20                 },
21                 new Student()
22                 {
23                     UserName = "U201600002",
24                     Name = "Ruri",
25                     Email = "NanaseRuri@cnblog.com",
26                     PhoneNumber = "12345678911",
27                     Degree = Degrees.DoctorateDegree,
28                     MaxBooksNumber = 15
29                 },
30             };
31 
32             foreach (var student in initialStudents)
33             {
34                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6,6));
35             }
36         } 
37        }
复制代码

 

为确保能够进行初始化,在 Startup.cs 的 Configure 方法中调用该静态方法:

复制代码
1             app.UseMvc(routes =>
2             {
3                 routes.MapRoute(
4                     name: "default",
5                     template: "{controller=Home}/{action=Index}/{id?}");
6             });
7             DatabaseInitiator.Initial(app.ApplicationServices).Wait();
复制代码

 

Initial 方法中 serviceProvider 参数将在传入 ConfigureServices 方法调用后的 ServiceProvider,此时在 Initial 方法中初始化的数据也会使用 ConfigureServices 中对账号和密码的限制。

此处我们使用账号的后六位作为密码。启动网页后查看数据库的数据:

 

 

 

 

三、建立验证所用的控制器以及视图

 

首先创建一个视图模型用于存储账号的信息,为了方便实现多种登录方式,此处创建一个 LoginType 枚举:

[UIHint] 特性构造函数传入一个字符串用来告知对应属性在使用 Html.EditorFor() 时用什么模板来展示数据。

复制代码
 1     public enum LoginType
 2     {
 3         UserName,
 4         Email,
 5         Phone
 6     }
 7 
 8     public class LoginModel
 9     {
10         [Required(ErrorMessage = "请输入您的学号 / 邮箱 / 手机号码")]
11         [Display(Name = "学号 / 邮箱 / 手机号码")]
12         public string Account { get; set; }
13 
14         [Required(ErrorMessage = "请输入您的密码")]
15         [UIHint("password")]
16         [Display(Name = "密码")]
17         public string Password { get; set; }
18 
19         [Required]
20         public LoginType LoginType { get; set; }
21     }
复制代码

 

   使用支架特性创建一个 StudentAccountController

 

StudentAccount 控制器:

第 5 行判断是否授权以避免多余的授权:

复制代码
 1      public class StudentAccountController : Controller
 2      {
 3         public IActionResult Login(string returnUrl)
 4         {
 5             if (HttpContext.User.Identity.IsAuthenticated)
 6             {
 7                 return RedirectToAction("AccountInfo");
 8             }
 9 
10             LoginModel loginInfo = new LoginModel();
11             ViewBag.returnUrl = returnUrl;
12             return View(loginInfo);
13         }
14     }
复制代码

 

在在 Login 视图中添加多种登录方式,并使视图更加清晰,创建了一个 LoginTypeTagHelper ,TagHelper 可制定自定义 HTML 标记并在最终生成视图时转换成标准的 HTML 标记。

复制代码
 1     [HtmlTargetElement("LoginType")]
 2     public class LoginTypeTagHelper:TagHelper
 3     {
 4         public string[] LoginType { get; set; }
 5 
 6         public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
 7         {
 8             foreach (var loginType in LoginType)
 9             {
10                 switch (loginType)
11                 {
12                     case "UserName": output.Content.AppendHtml($"<option selected=\"selected/\" value=\"{loginType}\">学号</option>");
13                         break;
14                     case "Email": output.Content.AppendHtml(GetOption(loginType, "邮箱"));
15                         break;
16                     case "Phone": output.Content.AppendHtml(GetOption(loginType, "手机号码"));
17                         break;
18                     default: break;
19                 }                
20             }            
21             return Task.CompletedTask;
22         }
23 
24         private static string GetOption(string loginType,string innerText)
25         {
26             return $"<option value=\"{loginType}\">{innerText}</option>";
27         }
28     }
复制代码

 

Login 视图:

25 行中使用了刚建立的 LoginTypeTagHelper:

复制代码
 1 @model LoginModel
 2 
 3 @{
 4     ViewData["Title"] = "Login";
 5 }
 6 
 7 <h2>Login</h2>
 8 <br/>
 9 <div class="text-danger" asp-validation-summary="All"></div>
10 <br/>
11 <form asp-action="Login" method="post">
12     <input type="hidden" name="returnUrl" value="@ViewBag.returnUrl"/>
13     <div class="form-group">   
14         <label asp-for="Account"></label>
15         <input asp-for="Account" class="form-control" placeholder="请输入你的学号 / 邮箱 / 手机号"/>
16     </div>
17     <div class="form-group">   
18         <label asp-for="Password"></label>
19         <input asp-for="Password" class="form-control" placeholder="请输入你的密码"/>
20     </div>
21     <div class="form-group">
22         <label>登录方式</label>
23         <select asp-for="LoginType">
24             <option disabled value="">登录方式</option>
25             <LoginType login-type="@Enum.GetNames(typeof(LoginType))"></LoginType>
26         </select>
27     </div>
28     <input type="submit" class="btn btn-primary"/>
29 </form>
复制代码

 

 

然后创建一个用于对信息进行验证的动作方法。

 

为了获取数据库的数据以及对数据进行验证授权,需要通过 DI(依赖注入) 获取对应的 UserManager 和 SignInManager 对象,在此针对 StudentAccountController 的构造函数进行更新。

StudentAccountController 整体:

复制代码
 1     [Authorize]
 2     public class StudentAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5         private SignInManager<Student> _signInManager;
 6 
 7         public StudentAccountController(UserManager<Student> studentManager, SignInManager<Student> signInManager)
 8         {
 9             _userManager = studentManager;
10             _signInManager = signInManager;
11         }
12 
13         [AllowAnonymous]
14         public IActionResult Login(string returnUrl)
15         {
16             if (HttpContext.User.Identity.IsAuthenticated)
17             {
18                 return RedirectToAction("AccountInfo");
19             }
20 
21             LoginModel loginInfo = new LoginModel();
22             ViewBag.returnUrl = returnUrl;
23             return View(loginInfo);
24         }
25 
26         [HttpPost]
27         [ValidateAntiForgeryToken]
28         [AllowAnonymous]
29         public async Task<IActionResult> Login(LoginModel loginInfo, string returnUrl)
30         {
31             if (ModelState.IsValid)
32             {
33                 Student student =await GetStudentByLoginModel(loginInfo);
34 
35                 if (student == null)
36                 {
37                     return View(loginInfo);
38                 }
39                 SignInResult signInResult = await _signInManager.PasswordSignInAsync(student, loginInfo.Password, false, false);
40 
41                 if (signInResult.Succeeded)
42                 {
43                     return Redirect(returnUrl ?? "/StudentAccount/"+nameof(AccountInfo));
44                 }
45 
46                 ModelState.AddModelError("", "账号或密码错误");
47                             
48             }
49 
50             return View(loginInfo);
51         }
52 
53         public IActionResult AccountInfo()
54         {
55             return View(CurrentAccountData());
56         }
57 
58         Dictionary<string, object> CurrentAccountData()
59         {
60             var userName = HttpContext.User.Identity.Name;
61             var user = _userManager.FindByNameAsync(userName).Result;
62 
63             return new Dictionary<string, object>()
64             {
65                 ["学号"]=userName,
66                 ["姓名"]=user.Name,
67                 ["邮箱"]=user.Email,
68                 ["手机号"]=user.PhoneNumber,
69             };
70         }
71     }
复制代码

_userManager 以及  _signInManager 将通过 DI 获得实例;[ValidateAntiForgeryToken] 特性用于防止 XSRF 攻击;returnUrl 参数用于接收或返回之前正在访问的页面,在此处若 returnUrl 为空则返回 AccountInfo 页面;[Authorize] 特性用于确保只有已授权的用户才能访问对应动作方法;CurrentAccountData 方法用于获取当前用户的信息以在 AccountInfo 视图中呈现。

 

由于未进行授权,在此直接访问 AccountInfo 方法默认会返回 /Account/Login 页面请求验证,可通过在 Startup.cs 的 ConfigureServices 方法进行配置以覆盖这一行为,让页面默认返回 /StudentAccount/Login :

1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.LoginPath = "/StudentAccount/Login";
4             }

 

为了使 [Authorize] 特性能够正常工作,需要在 Configure 方法中使用 Authentication 中间件,如果没有调用 app.UseAuthentication(),则访问带有 [Authorize] 的方法会再度要求进行验证。中间件的顺序很重要:

1             app.UseAuthentication();
2             app.UseHttpsRedirection();
3             app.UseStaticFiles();
4             app.UseCookiePolicy();

 

直接访问 AccountInfo 页面:

 

输入账号密码进行验证:

 

验证之后返回 /StudentAccount/AccountInfo 页面:

 

 

 

四、创建登出网页

简单地调用 SignOutAsync 用以清除当前 Cookie 中的授权信息。

复制代码
 1         public async Task<IActionResult> Logout(string returnUrl)
 2         {
 3             await _signInManager.SignOutAsync();
 4             if (returnUrl == null)
 5             {
 6                 return View("Login");
 7             }
 8 
 9             return Redirect(returnUrl);
10         }
复制代码

 

同时在 AccountInfo 添加登出按钮:

复制代码
 1     @model Dictionary<string, object>
 2     @{
 3         ViewData["Title"] = "AccountInfo";
 4     }
 5     <h2>账户信息</h2>
 6     <ul>
 7         @foreach (var info in Model)
 8         {
 9             <li>@info.Key: @Model[info.Key]</li>
10         }
11     </ul>
12     <br />
13     <a class="btn btn-danger" asp-action="Logout">登出</a>
复制代码

 

 

 

登出后返回 Login 页面,同时 AccountInfo 页面需要重新进行验证。

 

附加使用邮箱以及手机号验证的测试:

 

 

 

 

 

五、基于 Role 的 Identity 授权

修改 StudentInitial 类,添加名为 admin 的学生数组并使用 AddToRoleAsync 为用户添加身份。在添加 Role 之前需要在 RoleManager 对象中使用 Create 方法为 Role 数据库添加特定的 Role 字段:

复制代码
 1     public class StudentInitiator
 2     {
 3         public static async Task InitialStudents(IServiceProvider serviceProvider)
 4         {
 5             UserManager<Student> userManager = serviceProvider.GetRequiredService<UserManager<Student>>();
 6             RoleManager<IdentityRole> roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
 7             if (userManager.Users.Any())
 8             {
 9                 return;
10             }
11 
12             if (await roleManager.FindByNameAsync("Admin")==null)
13             {
14                 await roleManager.CreateAsync(new IdentityRole("Admin"));
15             }
16 
17             if (await roleManager.FindByNameAsync("Student")==null)
18             {
19                 await roleManager.CreateAsync(new IdentityRole("Student"));
20             }
21 
22             IEnumerable<Student> initialStudents = new[]
23             {
24                 new Student()
25                 {
26                     UserName = "U201600001",
27                     Name = "Nanase",
28                     Email = "Nanase@cnblog.com",
29                     PhoneNumber = "12345678910",
30                     Degree = Degrees.CollegeStudent,
31                     MaxBooksNumber = 10,
32                 },
33                 new Student()
34                 {
35                     UserName = "U201600002",
36                     Name = "Ruri",
37                     Email = "NanaseRuri@cnblog.com",
38                     PhoneNumber = "12345678911",
39                     Degree = Degrees.DoctorateDegree,
40                     MaxBooksNumber = 15
41                 }
42             };
43 
44             IEnumerable<Student> initialAdmins = new[]
45             {
46                 new Student()
47                 {
48                     UserName = "A000000000",
49                     Name="Admin0000",
50                     Email = "Admin@cnblog.com",
51                     PhoneNumber = "12345678912",
52                     Degree = Degrees.CollegeStudent,
53                     MaxBooksNumber = 20
54                 },
55                 new Student()
56                 {
57                     UserName = "A000000001",
58                     Name = "Admin0001",
59                     Email = "123456789@qq.com",
60                     PhoneNumber = "12345678910",
61                     Degree = Degrees.CollegeStudent,
62                     MaxBooksNumber = 20
63                 },
64             };
65             foreach (var student in initialStudents)
66             {
67                 await userManager.CreateAsync(student, student.UserName.Substring(student.UserName.Length - 6, 6));
68             }
69             foreach (var admin in initialAdmins)
70             {
71                 await userManager.CreateAsync(admin, "zxcZXC!123");
72                 await userManager.AddToRoleAsync(admin, "Admin");
73             }
74         }
75     }
复制代码

 

对 ConfigureServices 作进一步配置,添加 Cookie 的过期时间和不满足 Authorize 条件时返回的 Url:

复制代码
1             services.ConfigureApplicationCookie(opts =>
2             {
3                 opts.Cookie.HttpOnly = true;
4                 opts.LoginPath = "/StudentAccount/Login";
5                 opts.AccessDeniedPath = "/StudentAccount/Login";
6                 opts.ExpireTimeSpan=TimeSpan.FromMinutes(5);
7             });
复制代码

则当 Role 不为 Admin 时将返回 /StudentAccount/Login 而非默认的 /Account/AccessDeny。

 

 

然后新建一个用以管理学生信息的 AdminAccount 控制器,设置 [Authorize] 特性并指定 Role 属性,使带有特定 Role 的身份才可以访问该控制器。

复制代码
 1     [Authorize(Roles = "Admin")]
 2     public class AdminAccountController : Controller
 3     {
 4         private UserManager<Student> _userManager;
 5 
 6         public AdminAccountController(UserManager<Student> userManager)
 7         {
 8             _userManager = userManager;
 9         }
10 
11         public IActionResult Index()
12         {
13             ICollection<Student> students = _userManager.Users.ToList();
14             return View(students);
15         }
16     }
复制代码

 

Index 视图:

复制代码
  1 @using LibraryDemo.Models.DomainModels
  2 @model IEnumerable<LibraryDemo.Models.DomainModels.Student>
  3 @{
  4     ViewData["Title"] = "AccountInfo";
  5     Student stu = new Student();
  6 }
  7 <link rel="stylesheet" href="~/css/BookInfo.css" />
  8 
  9 <script>
 10     function confirmDelete() {
 11         var userNames = document.getElementsByName("userNames");
 12         var message = "确认删除";
 13         var values = [];
 14         for (i in userNames) {
 15             if (userNames[i].checked) {
 16                 message = message + userNames[i].value+",";
 17                 values.push(userNames[i].value);
 18             }
 19         }
 20         message = message + "?";
 21         if (confirm(message)) {
 22             $.ajax({
 23                 url: "@Url.Action("RemoveStudent")",
 24                 contentType: "application/json",
 25                 method: "POST",
 26                 data: JSON.stringify(values),
 27                 success: function(students) {
 28                     updateTable(students);
 29                 }
 30             });
 31         }
 32     }
 33 
 34     function updateTable(data) {
 35         var body = $("#studentList");
 36         body.empty();
 37         for (var i = 0; i < data.length; i++) {
 38             var person = data[i];
 39             body.append(`<tr><td><input type="checkbox" name="userNames" value="${person.userName}" /></td>
 40             <td>${person.userName}</td><td>${person.name}</td><td>${person.degree}</td>
 41             <td>${person.phoneNumber}</td><td>${person.email}</td><td>${person.maxBooksNumber}</td></tr>`);
 42         }
 43     };
 44 
 45     function addStudent() {
 46         var studentList = $("#studentList");
 47         if (!document.getElementById("studentInfo")) {
 48             studentList.append('<tr id="studentInfo">' +
 49                 '<td></td>' +
 50                 '<td><input type="text" name="UserName" id="UserName" /></td>' +
 51                 '<td><input type="text" name="Name" id="Name" /></td>' +
 52                 '<td><input type="text" name="Degree" id="Degree" /></td>' +
 53                 '<td><input type="text" name="PhoneNumber" id="PhoneNumber" /></td>' +
 54                 '<td><input type="text" name="Email" id="Email" /></td>' +
 55                 '<td><input type="text" name="MaxBooksNumber" id="MaxBooksNumber" /></td>' +
 56                 '<td><button type="submit" onclick="return postAddStudent()">添加</button></td>' +
 57                 '</tr>');
 58         }
 59     }
 60     
 61     function postAddStudent() {
 62         $.ajax({
 63             url: "@Url.Action("AddStudent")",
 64             contentType: "application/json",
 65             method: "POST",
 66             data: JSON.stringify({
 67                 UserName: $("#UserName").val(),
 68                 Name: $("#Name").val(),
 69                 Degree:$("#Degree").val(),
 70                 PhoneNumber: $("#PhoneNumber").val(),
 71                 Email: $("#Email").val(),
 72                 MaxBooksNumber: $("#MaxBooksNumber").val()
 73             }),
 74             success: function (student) {
 75                 addStudentToTable(student);
 76             }
 77         });
 78     }
 79 
 80     function addStudentToTable(student) {
 81         var studentList = document.getElementById("studentList");
 82         var studentInfo = document.getElementById("studentInfo");
 83         studentList.removeChild(studentInfo);
 84 
 85         $("#studentList").append(`<tr>` +
 86             `<td><input type="checkbox" name="userNames" value="${student.userName}" /></td>` +
 87             `<td>${student.userName}</td>` +
 88             `<td>${student.name}</td>`+
 89             `<td>${student.degree}</td>` +
 90             `<td>${student.phoneNumber}</td>` +
 91             `<td>${student.email}</td>` +
 92             `<td>${student.maxBooksNumber}</td >` +
 93             `</tr>`);
 94     }
 95 </script>
 96 
 97 <h2>学生信息</h2>
 98 
 99 <div id="buttonGroup">
100     <button class="btn btn-primary" onclick="return addStudent()">添加学生</button>
101     <button class="btn btn-danger" onclick="return confirmDelete()">删除学生</button>
102 </div>
103 
104 
105 <br />
106 <table>
107     <thead>
108         <tr>
109             <th></th>
110             <th>@Html.LabelFor(m => stu.UserName)</th>
111             <th>@Html.LabelFor(m => stu.Name)</th>
112             <th>@Html.LabelFor(m => stu.Degree)</th>
113             <th>@Html.LabelFor(m => stu.PhoneNumber)</th>
114             <th>@Html.LabelFor(m => stu.Email)</th>
115             <th>@Html.LabelFor(m => stu.MaxBooksNumber)</th>
116         </tr>
117     </thead>
118     <tbody id="studentList">
119 
120         @if (!@Model.Any())
121         {
122             <tr><td colspan="6">未有学生信息</td></tr>
123         }
124         else
125         {
126             foreach (var student in Model)
127             {
128                 <tr>
129                     <td><input type="checkbox" name="userNames" value="@student.UserName" /></td>
130                     <td>@student.UserName</td>
131                     <td>@student.Name</td>
132                     <td>@Html.DisplayFor(m => student.Degree)</td>
133                     <td>@student.PhoneNumber</td>
134                     <td>@student.Email</td>
135                     <td>@student.MaxBooksNumber</td>
136                 </tr>
137             }
138         }
139     </tbody>
140 </table>
复制代码

 

使用 Role 不是 Admin 的账户登录:

 

 

使用 Role 为 Admin 的账户登录:

朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh) - lovecindywang - 博客园

mikel阅读(795)

十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别。最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器

来源: 朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh) – lovecindywang – 博客园

十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别。最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器编排的地位确定之后大家又开始实践起以K8S为核心的云原生思想和微服务的结合如何去落地,2018年又多出一个ServiceMesh服务网格的概念,大家又在思考如何引入落地ServiceMesh,ServiceMesh和K8S以及Spring Cloud的关系如何等等。

确实有点乱了,这一波又一波的热潮,几乎每两年都会来一波有关微服务架构理念和平台,许多公司还没完成微服务的改造就又提出了服务+容器道路,又有一些公司打算从微服务直接升级成ServiceMesh。本文尝试总结一下我见过的或实践过的一些微服务落地方式,并且提出一些自己的观点,希望抛砖引玉,大家可以畅谈一下自己公司的微服务落地方式。

1、微服务v0.1——古典玩法


(图中灰色部分代表元数据存储区域,也就是Service和Endpoint关系所保存的地方,之后所有的图都是这样)

其实在2006年在使用.NET Remoting做服务拆分的时候(其实当时我们没有意识到这叫服务拆分,这是打算把一些逻辑使用独立的进程来承载,以Windows服务形式安装在不同服务器上分散压力),我们使用了F5来做服务的负载均衡。没有所谓的服务发现,针对每一个服务,我们直接在程序配置文件中写死F5的IP地址和端口,使用Excel来记录所有服务在F5的端口地址以及服务实际部署的IP:端口,然后在F5进行配置。F5在这里做了负载均衡、简单的路由策略(相同的客户端总是优先路由到相同的后端)以及简单的白名单策略等等。

2、微服务v0.2——改进版古典玩法


之后尝试过这种改进版的古典玩法。相比v0.1的区别是,不再使用硬件F5了,而是使用几组软件反向代理服务器,比如Nginx来做服务负载均衡(如果是TCP的负载均衡的话可以选择HaProxy),Nginx的配置会比F5更方便而且还不花钱。由于生产环境Nginx可能是多组,客户端不在配置文件中写死Nginx地址而是把地址放到了配置中心去,而Nginx的配置由源码仓库统一管理,运维通过文件同步方式或其它方式从源码仓库拉取配置文件下发到不同的Nginx集群做后端服务的配置(Nginx的配置也不一定需要是一个大文件放所有的配置,可以每一组服务做一个配置文件更清晰)。

虽然我的标题说这是古典玩法,但是可以说很多公司如果没有上RPC,没有上Spring Cloud,也没有上K8S的话很可能就是这样的玩法。无论是v0.2还是v0.1,本质上服务是固定在虚拟机或实体机部署的,如果需要扩容,需要迁移,那么肯定需要修改反向代理或负载均衡器的配置。少数情况下,如果调整了反向代理或负载均衡器的IP地址,那么还可能会需要修改客户端的配置。

3、微服务v0.5——SOA ESB玩法


SOA的一个特点是使用了服务总线,服务总线承担了服务的发现、路由、协议转换、安全控制、限流等等。2012年我参与了一个大型MMO游戏《激战2》项目的技术整合工作,这个游戏整个服务端就是这种架构。它有一个叫做Portal的服务总线,所有游戏的十几个子服务都会把自己注册到服务总线,不管是什么服务需要调用什么接口,都是在调用服务总线,由服务总线来进行服务的寻址路由和协议转换,服务总线也会做服务的精细化限流,每一个用户都有自己的服务请求队列。这种架构的好处是简单,服务总线承担了所有工作,但是服务总线的压力很大,承担了所有的服务转发工作。同时需要考虑服务总线本身如何进行扩容,如果服务总线是有状态的,显然要进行扩容不是这么简单。对于游戏服务器来说,扩容可能不是一个强需求,因为游戏服务天然会按照大区进行分流,一个大区的最大人数上限是固定的。

貌似互联网公司这样玩的不多,传统企业或是游戏服务端是比较适合服务总线这种架构的,如果服务和服务之间的协议不统一的话,要在客户端做协议转换的工作比较痛苦,如果可以由统一的中间层接入所有协议统一进行转换的话,客户端会比较轻量,但是这种架构的很大问题在于服务总线的扩容和可靠性。

4、微服务v1.0——传统服务框架玩法


上图是大多数RPC框架的架构图。大多数早期的微服务实践都是RPC的方式,最近几年Spring Cloud盛行后其实Spring Cloud的玩法也差不多,只是Spring Cloud推崇的是JSON over HTTP的RESTful接口,而大多数RPC框架是二进制序列化over TCP的玩法(也有JSON over HTTP的RPC)。

其实RPC框架我个人喜欢JSON over HTTP,虽然我们知道HTTP和JSON序列化性能肯定不如一些精简的二进制序列化+TCP,但是优点是良好的可读性、测试方便、客户端开发方便,而且我不认为15000的QPS和20000的QPS对于一般应用有什么区别。

总的来说,我们会有一个集群化的分布式配置中心来充当服务注册的存储,比如ZK、Consul、Eureka或etcd。我们的服务框架会有客户端和服务端部分,客户端部分会提供服务的发现、软负载、路由、安全、策略控制等功能(可能也会通过插件形式包含Metrics、Logging、Tracing、Resilience等功能),服务端部分对于RPC框架会做服务的调用也会辅助做一些安全、策略控制,对于RESTful的话就服务端一般除了监控没有额外的功能。

比如使用Spring Cloud来玩,那么:

  • Service Discovery:Eureka、Open Feign
  • Load Balancing:Ribbon、Spring Cloud LoadBalancer
  • Metrics:Micrometer、Spring Boot Actuator
  • Resilience:Hystrix、Resilience4j
  • Tracing:Sleuth、Zipkin

在之前《朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子)》一文中,我有一个完整的例子介绍过Spring Cloud的这套玩法,可以说的确Spring Cloud给了我们构建一套微服务体系最基本的东西,我们只需要进行一些简单的扩展和补充,比如灰度功能,比如更好的配置服务,就完全可以用于生产。
这种模式和之前0.x的很大区别是,服务的注册有一个独立的组件,注册中心完成,通过配合客户端类库的服务发现,至少服务的扩容很轻松,扩容后也不需要手动维护负载均衡器的配置,相当于服务端从死到活的一个重大转变。而且在1.0的时代,我们更多看到了服务治理的部分,开始意识到成百上千的服务,如果没有Metrics、Logging、Tracing、Resilience等功能来辅助的话,微服务就是一个灾难。

Spring Cloud已经出了G版了,表示Netflix那套已经进入了维护模式,许多程序员表示表示扶我起来还能学。我认为Spring Cloud这个方向其实是挺对的,先有开源的东西来填补空白,慢慢再用自己的东西来替换,但是开发比较苦,特别是一些公司基于Spring Cloud辛苦二次开发的框架围绕了Netflix那套东西来做的会比较痛苦。总的来说,虽然Spring Cloud给人的感觉很乱,变化很大,大到E到G版的升级不亚于在换框架,而且组件质量层次不齐,但是它确实是一无所有的创业公司能够起步微服务的不多的选择之一。如果没有现成的框架(不是说RPC框架,RPC框架虽是微服务功能的80%重点,但却是代码量20%的部分,工作量最大的是治理和整合那套),基于Spring Cloud起步微服务,至少你可以当天起步,1个月完成适合自己公司的二次开发改造。

5、微服务v2.0——容器+K8S容器调度玩法


K8S或者说容器调度平台的引入是比较革命性的,容器使得我们的微服务对环境的依赖可以打包整合进行随意分发,这是微服务节点可以任意调度的基础,调度平台通过服务的分类和抽象,使得微服务本身的部署和维护实现自动化,以及实现更上一层楼的自动伸缩。在1.x时代,服务可以进行扩缩容,但是一切都需要人工介入,在2.x时代,服务本身在哪里存在甚至有多少实例存在并不重要,重要的只是我们有多少资源,希望服务的SLA是怎么样的,其余留给调度平台来调度。

如果说1.0时代大家纠结过Dubbo还是Spring Cloud,2.0时代我相信也有一些公司上过Mesos的“贼船”,我们不是先知很难预测什么框架什么技术会在最后存活下来,但是这却是也给技术带来了不少痛苦,相信还是有不少公司在干Mesos转K8S的事情。

如果引入了K8S,那么服务发现可以由K8S来做,不一定需要Eureka。我们可以为Pod创建Service,通过Cluster虚拟IP的方式(如上图所示,通过IP tables)路由到Pod IP来做服务的路由(除了Cluster IP方式也有的人对于内部连接会采用Ingress方式去做,路由方面会更强大,不过这是不是又类似v0.2了呢?)。当然,我们还可以更进一步引入内部DNS,使用内部域名解析成Cluster IP,客户端在调用服务的时候直接使用域名(域名可以通过配置服务来配置,也可以直接读取环境变量)即可。如果这么干的话其实就没有Eureka啥事了,有的公司没有选择这种纯K8S服务路由的方式还是使用了注册中心,如果这样的话其实服务注册到注册中心的就是Pod IP,还是由微服务客户端做服务发现的工作。我更喜欢这种方式,我觉得K8S的服务发现还是弱了一点,而且IP tables的方式让人没有安全感(IPVS应该是更好的选择),与其说是服务发现,我更愿意让K8S只做容器调度的工作以及Pod发现的工作。

虽然K8S可以做一部分服务发现的工作,我们还是需要在客户端中去实现更多的一些弹力方面的功能,因此我认为2.0时代只是说是微服务框架结合容器、容器调度,而不能是脱离微服务框架本身完全依靠K8S实现微服务。2.0和1.0的本质区别或者说增强还是很明显,那就是我们可以全局来统筹解决我们的微服务部署和可靠性问题,在没有容器和容器调度这层抽象之前,有的公司通过实现自动化虚拟机分配拉起,加上自动化初始脚本来实现自动的微服务调度扩容,有类似的意思,但是非常花时间而且速度慢。K8S真正让OPS成为了DEV而不是执行者,让OPS站在总体架构的层面通过DEV(咱不能说开发DSL文件不算开发吧)资源和资源之间的关系来统筹整个集群。在只有十几个微服务若干台服务器的小公司可能无法发挥2.0容器云的威力,但是服务器和服务一多,纯手工的命令式配置容易出错且难以管理,K8S真的释放了几十个运维人力。

6、微服务v3.0——ServiceMesh服务网格玩法


在之前提到过几个问题:

  • SOA的模式虽然简单,但是集中的Proxy在高并发下性能和扩容会是问题
  • 传统的RPC方式,客户端很重,做了很多工作,甚至协议转换都在客户端做,而且如果涉及到跨语言,那么RPC框架需要好几套客户端和服务端
  • K8S虽然是一个重要的变革,但是在服务调度方面还是太弱了,它的专项在于资源调度

于是ServiceMesh服务网格的概念腾空而出,巧妙解决了这几个问题:

  • 采用边车模式的Proxy随服务本身部署,一服务一边车与服务共生死(当然,有的公司会使用类似ServiceBus的Global Proxy作为Sidecar Proxy的后备,防止服务活着Sidecar死了的情况)可以解决性能问题
  • Sidecar里面做了路由、弹力等工作,客户端里可以啥都不干,如上图所示,上图是Istio的架构,Istio的理念是把ServiceMesh分成了数据面和控制面,数据面主要是负责数据传输,由智能代理负责(典型的组件是Envoy),控制面由三大组件构成,Pilot负责流量管理和配置(路由策略、授权策略)下发,Mixer负责策略和数据上报(遥测),Citadel用于密钥和证书管理
  • 由于我们双边都走Sidecar Proxy,我们对于流量的进出都可以做很细粒度的控制,这个控制力度是之前任何一种模式都无法比拟的,这种架构的方式就像把服务放到了网格之中,服务连接外部的通讯都由网格进行,服务本身轻量且只需要关注业务逻辑,网格功能强大而灵活
  • 对于Proxy的流量劫持可以使用IP table进行拦截,对于服务本身无感知,而且Sidecar可以自动注入Pod,和K8S进行自动整合,无需特殊配置,做到透明部署透明使用
  • Pilot是平台无关的,采用适配器形式可以和多个平台做整合,如果和K8S整合的话,它会和API Server进行通讯,订阅服务、端点的信息,然后把信息转变成Istio自己的格式作为路由的元数据
  • Mixer期望的是抽象底层的基础设施,不管是Logging还是Metrics、Tracing,在之前RPC时代的做法是客户端和服务端都会直接上报信息到InfluxDb、Tracing Server等,这让客户端变得很臃肿,Istio的理念是这部分对接后端的工作应该由统一的组件进行,不但使得Proxy可以更轻而且可以通过Plugin机制对接各种后端基础设施

说了这么多ServiceMesh的优势,我们来看一下这种模式的性能问题。想一下各种模式下客户端要请求服务端整个HTTP请求(跳)次数:

  • 古典模式:2跳,代理转发一次
  • SOA模式:2跳,总线转发一次
  • 传统模式:1跳,客户端直连服务端
  • K8S Service模式:1跳(路由表会有一定损耗)
  • ServiceMesh模式:3跳(其中2跳是localhost回环)

总的来说,3跳并不是ServiceMesh的瓶颈所在,而更多的可能性是Istio的倔强的架构理念。Istio认为策略和遥测不应该耦合在Sidecar Proxy应该放到Mixer,那么相当于在调用服务的时候还需要额外增加Mixer的同步请求(来获得策略方面的放行)。Istio也在一直优化这方面,比如为Mixer的策略在Proxy做本地缓存,为遥测数据做批量上报等等。虽然经过层层优化,但是Istio目前的TPS不足2000,还是和一般的RPC能达到的20000+有着十倍的差距,说不定将来Istio会有架构上的妥协,把Mixer变为非直接依赖,策略方面还是采用类似Pilot统一管理配置下发的方式,遥测方面还是由Sidecar直接上报数据到Mixer。

我个人认为,ServiceMesh是一个非常正确的道路,而且ServiceMesh和K8S结合会更好,理由在于:

  • K8S让资源调度变得自由,但微服务调度不是其所长也不应该由它深入实现
  • 以Istio为代表的ServiceMesh做了K8S少的,但是微服务又必须的那块工作
  • Istio的设计方面和K8S极其相似,低耦合,抽象的很好,两者结合的也很好,我非常喜欢和赞同Agent+统一的资源管理配置下发的方式(K8S的Agent就是KubeProxy和Kubelet,Istio的Agent就是Sidecar Proxy),这是松耦合和高性能的平衡
  • 在复杂的异构环境下,多协议的内部通讯,跨平台跨语言的内部通讯很常见,如果采用传统方式,框架太胖太重,把这部分工作从内部剥离出来好处多多

但是,可以看到目前ServiceMesh还不算非常成熟,Istio在不断优化中,Linkerd 2.x也想再和Istio拼一下,到底谁会胜出还难以知晓,经过之前Dubbo vs Spring Cloud的折腾,Mesos vs K8S的折腾,VM vs Docker的折腾,是否还能经得起折腾Istio vs Linkerd 2呢?我建议还是再看一看,再等一等。

7、畅想Everything Mesh模式?


之前看到过ShardingSphere受到ServiceMesh的理念影响提出了DB Mesh的架构。其实DB Proxy的中间件已经存在很多年了(集中化的Proxy类似服务总线的方式),DB Mesh把Proxy也变为轻量的Sidecar方式,DB的访问也都走本地代理。那么这里我也在想,是不是有可能所有东西都有本地的代理呢?

作为应用服务本身而言,只需要和本地代理做通讯调用外部服务、缓存、数据库、消息队列,不需要关心服务和资源所在何地,以及背后的实际服务的组件形态。当然,这只是一个畅想了,对于有状态的资源,Mesh的难度很大,对于类似DB这样的资源因为调用层次并不复杂,也不太会存在异构场景,Mesh的意义不大,综合起来看Everything Mesh的投入产出比相比Service Mesh还是小很多。

8、Spring Cloud、K8S和ServiceMesh的关系

如果搞Java微服务的话,Spring Boot是离不开的,但是是否要用Spring Cloud呢?我的观点是,在目前阶段如果没有什么更好的选择,还是应该先用。Spring Cloud和K8S首先并不是矛盾的东西,K8S是偏运维的,主要做资源整合和管理,如果彻底没有服务治理框架纯靠K8S的话会很累,而且功能不完整。开发和架构可以在Spring Cloud方面深耕,运维可以在容器和K8S方面发力,两套体系可以协作形成目前来说比较好的微服务基石。至于K8S的推行,这一定是一个正确的方向,而且和软件架构方面的改进工作一点不矛盾,毕竟K8S是脱离于具体语言和平台的。

至于Service Mesh,它做的事情和Spring Cloud是有很多重复的,在将来Istio如果发展的更好的情况下,应该可以替代Spring Cloud,开发人员只需要用Spring Boot开发微服务即可,客户端方面也可以很瘦,不需要过多关心服务如何通讯和路由,服务的安全、通讯、治理、控制都由Service Mesh进行(但是,是否有了Sidecar,客户端真的完全不需要SDK了呢?我认为可能还是需要的,对于Tracing,如果没有客户端部分显然是不完整的,虽然Sidecar是localhost但是还是跨进程了)。

Spring Cloud目前虽然针对K8S和Istio做了一些整合,但是并没看到一套针对ServiceMesh的最佳实践出来,是否将来Spring Cloud会在微服务这方面做退化给ServiceMesh让步还不得而知。总的来说,长期我看好Spring Boot + K8S + Istio的组合,短期我认为还是Spring Boot + K8S + Spring Cloud这么用着。

9、总结

本文总结了各种微服务落地的形态,由于技术多样,各种理念层出不穷,造成了微服务的落地方式真的很难找到两家相同的公司,本文中我们介绍了:

  • 客户端写死地址+F5代理的方式
  • 客户端把地址配置在配置服务+Nginx代理的方式
  • SOA+集中式ESB的方式
  • 传统的具有注册中心的服务框架SDK形式
  • 服务框架+K8S方式
  • K8S Service Iptables路由方式
  • ServiceMesh代理3跳转发方式

当然,可能还会有更多的方式:

  • 内部DNS方式(直接DNS轮询)
  • K8S内部服务走Ingress方式(内部服务也走Ingress,类似所有服务Nginx代理的方式)
  • ServiceMesh代理2跳转发方式(可以根据需要跳过远端的Sidecar来提高性能等等)
  • 瘦服务框架SDK+ServiceMesh方式(也就是还是有一个小的SDK来对接ServiceMesh的Sidecar,而不是让应用程序自己发挥Http Client,这个方式的好处在于更灵活,这个SDK可以在这一层再做一次路由,甚至在Sidecar出问题的时候直接把流量切换出去,切换为直连远端或统一的Global Proxy)

也可能很多公司在混用各种方式,具有N套服务注册中心,正在做容器化迁移,想想就头痛,微服务的理念层出不穷伴随着巨头之间的技术战役,苦的还是架构和开发,当然,运维可能也苦,2019新年快乐,Enjoy微服务!

怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢 - qsa - 博客园

mikel阅读(876)

来源: 怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢 – qsa – 博客园

怎么用SQL查询昨天、今天、明天和本周的记录?又怎么用SQL查询一天,三天,一周,一个月,更长一些——一个季度的记录呢?本文中给出了一些方法。

SQL查询今天的记录:

  1. datediff(day,[Datetime],getdate())=0 把Datetime换为你的相应字段;

SQL查询昨天的记录:

  1. datediff(day,[Datetime],getdate())=1 把Datetime换为你的相应字段,getdate()-Datetime即为时间差。

本月记录:

  1. SELECT * FROM 表 WHERE datediff(month,[dateadd],getdate())=0

本周记录:

  1. SELECT * FROM 表 WHERE datediff(week,[dateadd],getdate())=0

本日记录:

  1. SELECT * FROM 表 WHERE datediff(day,[dateadd],getdate())=0

一天

  1. select * from T_news where datediff(day,addtime,getdate())=0

三天

  1. select * from T_news where datediff(day,addtime,getdate())<= 2 and datediff(day,addtime,getdate())>= 0

一周

  1. select * from T_news WHERE (DATEPART(wk, addtime) = DATEPART(wk, GETDATE())) AND (DATEPART(yy, addtime) = DATEPART(yy, GETDATE()))

注意:此时不能用 datediff 差值为7,因为,datediff只表示间隔数

一月

  1. select * from T_news WHERE (DATEPART(yy, addtime) = DATEPART(yy, GETDATE())) AND (DATEPART(mm, addtime) = DATEPART(mm, GETDATE()))

一季度

  1. select * from T_news where DATEPART(qq, addtime) = DATEPART(qq, GETDATE()) and DATEPART(yy, addtime) = DATEPART(yy, GETDATE())

Sql中CHARINDEX用法 - 齐_大圣 - 博客园

mikel阅读(768)

来源: Sql中CHARINDEX用法 – 齐_大圣 – 博客园

CHARINDEX作用

SQL语句我们经常需要判断一个字符串中是否包含另一个字符串,但是SQL SERVER中并没有像C#提供了Contains函数,不过SQL SERVER中提供了一个叫CHAEINDX的函数,顾名思义就是找到字符(char)的位置(index),既然能够知道所在的位置,当然就可以判断是否包含在其中了。

通过CHARINDEX如果能够找到对应的字符串,则返回该字符串位置,否则返回0。

基本语法如下:

CHARINDEX ( expressionToFind , expressionToSearch [ , start_location ] )

expressionToFind :目标字符串,就是想要找到的字符串,最大长度为8000 。

expressionToSearch :用于被查找的字符串。

start_location:开始查找的位置,为空时默认从第一位开始查找。

 

CHAEINDEX示例

1.简单用法

select charindex(‘test’,’this Test is Test’)

查询结果:  

2.增加开始位置

select charindex(‘test’,’this Test is Test’,7)

查询结果:

3.大小写敏感

select charindex(‘test’,’this Test is Test’COLLATE Latin1_General_CS_AS)

查询结果:

返回结果为0???,不要怀疑你的眼睛,因为大小写敏感,找不到test所以返回的就是0,默认情况下, SQL SERVER是大小不敏感的,所以我们简单示例中返回结果不为0,但是有些时候我们需要特意去区分大小写,因此专门SQL SERVE提供了特殊的关键字用于查询时区分大小写,其中CS为Case-Sensitve的缩写。

4.大小写不敏感

select charindex(‘Test’,’this Test is Test’COLLATE Latin1_General_CI_AS)

查询结果:

我们也可以这样说明是大小写不敏感,其中CI是Case-InSensitve的缩写,即大小写不敏感,当然我们没必要多此一举。

 

PATINDEX

和CHARINDEX类似,PATINDEX也可以用来判断一个字符串中是否包含另一个字符串,两种的差异在于,前者是全匹配,后者支持模糊匹配。

1.简单示例

select PATINDEX(‘%ter%’,’interesting data’)

查询结果:

2.简单示例2

select PATINDEX(‘%t_ng%’,’interesting data’)

查询结果:

 

PATINDEX也允许支持大小写敏感,做法和CHARINDEX一样,此处不再累述。

参考:

https://docs.microsoft.com/en-us/sql/t-sql/functions/charindex-transact-sql

https://docs.microsoft.com/en-us/sql/t-sql/functions/patindex-transact-sql

ECharts.js学习(三)交互组件 - leoxuan - 博客园

mikel阅读(913)

来源: ECharts.js学习(三)交互组件 – leoxuan – 博客园

ECharts.js有很多的交互组件,一般经常用到的组件有这些:

title:标题组件,包含主标题和副标题。

legend:图例组件,展现了不同系列的标记(symbol),颜色和名字。可以通过点击图例控制哪些系列不显示。

xAxis:直角坐标系 grid 中的 x 轴,一般情况下单个 grid 组件最多只能放左右两个 x 轴,多于两个 x 轴需要通过配置 offset 属性防止同个位置多个 x 轴的重叠。

yAxis:直角坐标系 grid 中的 y 轴,一般情况下单个 grid 组件最多只能放左右两个 y 轴,多于两个 y 轴需要通过配置 offset 属性防止同个位置多个 Y 轴的重叠。

tooltip:提示框组件,就是当你的鼠标悬浮在图表上的提示内容。

toolbox:工具栏组件。内置有导出图片、数据视图、动态类型切换、数据区域缩放、重置五个工具。

series:系列列表。我理解为数据列表。这里可以定义每组数据内容,以及数据的展现形式。

timeline:提供了在多个ECharts option 之间进行切换、播放等操作的功能。

dataZoom:用于区域缩放,从而能自由关注细节的数据信息,或者概览数据整体,或者去除离群点的影响。

….

官方给出的案例是dataZoom组件。它是用于区域缩放,从而能自由关注细节的数据信息,或者概览数据整体,或者去除离群点的影响。主要是对 数轴(axis) 进行操作。

效果展示

 

toolbox组件

其中很多组件其实我们都会用到,不过使用的都是一些基本配置。比如title组件,往往只写一个text 值。legend,会一些每个系列数据的name等等。

因为后面项目需要将图表保存为图片,以及一种数据多种展现形势,所以就研究一下toolbox组件的使用。

toolbox参数:

show:工具栏默认是隐藏的。所以一定要设置show为true显示出来。

orient:工具栏的的布局方向,可选值有horizontal(横向)和vertical(竖向)。默认值是horizontal

itemSize:工具栏的大小。默认值是15。

itemGap:工具栏每个工具之间的距离,默认值是10。

showTitle:鼠标悬浮的是否显示每个工具的说明,默认是true。

feature:这个是设置工具栏里要显示哪些工具,以及这些工具的样式等。

默认的插件工具:

savaAsImage:保存图片

restore:还原配置

dataView:数据视图工具,可以展现图表所用的数据,并且可以编辑数据,再将编辑后的数据展示出来。同时也可以设置为数据为只读。

optionToContent:并且可以通过对显示出来的数据进行排版编辑,以HTML展现。

optionToOption:在使用 optionToContent 的情况下,如果支持数据编辑后的刷新,需要自行通过该函数实现组装 option 的逻辑。

dataZoom:数据区域缩放。目前只支持直角坐标系的缩放。

xAxisIndex、yAxisIndex:分别控制xAxis和yAxis轴的缩放。

除了使用默认的工具意外,我们还可以根据需求自定义工具。需要注意的是,每个自定义的工具,名称必须以“my”打头。在onclick函数中编写需要进行的操作。

 

复制代码
toolbox:{
    show:true,
    orient:'vertical',                
    feature:{
        magicType:{type:['line','bar']},
        restore:{},
        saveAsImage:{},
        dataZoom:{
            show:true,
            xAxisIndex:[0,3]
        },
        myTool1:{
            show:true,
            title:'自定义工具一',
            icon: 'path://M432.45,595.444c0,2.177-4.661,6.82-11.305,6.82c-6.475,0-11.306-4.567-11.306-6.82s4.852-6.812,11.306-6.812C427.841,588.632,432.452,593.191,432.45,595.444L432.45,595.444z M421.155,589.876c-3.009,0-5.448,2.495-5.448,5.572s2.439,5.572,5.448,5.572c3.01,0,5.449-2.495,5.449-5.572C426.604,592.371,424.165,589.876,421.155,589.876L421.155,589.876z M421.146,591.891c-1.916,0-3.47,1.589-3.47,3.549c0,1.959,1.554,3.548,3.47,3.548s3.469-1.589,3.469-3.548C424.614,593.479,423.062,591.891,421.146,591.891L421.146,591.891zM421.146,591.891',
            
            onclick:function(){
                alert("this is myTool1");
            }
        },
        myTool2:{
            show:true,
            title:'自定义工具二',
            icon: 'image://http://echarts.baidu.com/images/favicon.png',                        
            onclick:function(){
                alert("this is myTool2");
            }
        }
    }
}
复制代码

 

magicType:设置可切换的图表类型。目前支持的只有4种,line折线图、bar柱状图、stack堆叠模式、tiled平铺模式。

brush:选框组件的控制按钮。

iconStyle:公用的icon样式设置

zlevel:所有图形的zlevel值。zlevel用于Canvas分层。

z:所有图形的z值。z不会创建Canvas层。比zlevel等级低。

left、top、right、bottom、width、height:工具栏的样式,边距设置。

 

 

复制代码
<script type="text/javascript">        
    //初始化echarts实例
    var myChart = echarts.init(document.getElementById('chartmain'));    

    var option = {
        title:{
            text:"马云和马化腾期末成绩图",
            subtext:'本图表纯属虚构',                
        },
        anmation:false,
        legend:{
            data:["马云成绩","马化腾成绩"],
            left:'50%',
            top:5
        },
        tooltip:{
            trigger:"axis"
        },
        xAxis:{
            type:'category',
            boundaryGap:false,
            data:['语文','数学','英语','历史','体育','生物','化学']
        },
        yAxis:{
            type:'value',
            axisLabel:{
                formatter:'{value}分'
            },
            min:20
        },
        toolbox:{
            show:true,
            orient:'vertical',
            itemSize:20,
            itemGap:20,

            feature:{
                dataView:{
                    readOnly:true,
                    backgroundColor:'#f5f5f5',
                    optionToContent:function(opt){
                        var axisData = opt.xAxis[0].data;
                        var series = opt.series;
                        var table ='<table style="width:100%;text-align:center;border:1px solid red;"><tbody><tr>'
                                    +'<td>学生</td>'
                                    +'<td>'+series[0].name+'</td>'
                                    +'<td>'+series[1].name+'</td>'
                                    +'</tr>';
                        for (var i = 0; i < axisData.length; i++) {
                            table +='<tr>'
                                    +'<td>'+axisData[i]+'</td>'
                                    +'<td>'+series[0].data[i]+'</td>'
                                    +'<td>'+series[1].data[i]+'</td>'
                                    +'</tr>'
                        }
                        table +='</tbody></table>';
                        return table;
                    }                        
                },
                dataZoom:{
                    show:true,
                    xAxisIndex:[0,3]
                },
                magicType:{type:['line','bar','stack','tiled']},
                restore:{},
                saveAsImage:{},                    
                myTool1:{
                    show:true,
                    title:'自定义工具一',
                    icon: 'path://M432.45,595.444c0,2.177-4.661,6.82-11.305,6.82c-6.475,0-11.306-4.567-11.306-6.82s4.852-6.812,11.306-6.812C427.841,588.632,432.452,593.191,432.45,595.444L432.45,595.444z M421.155,589.876c-3.009,0-5.448,2.495-5.448,5.572s2.439,5.572,5.448,5.572c3.01,0,5.449-2.495,5.449-5.572C426.604,592.371,424.165,589.876,421.155,589.876L421.155,589.876z M421.146,591.891c-1.916,0-3.47,1.589-3.47,3.549c0,1.959,1.554,3.548,3.47,3.548s3.469-1.589,3.469-3.548C424.614,593.479,423.062,591.891,421.146,591.891L421.146,591.891zM421.146,591.891',
                    
                    onclick:function(){
                        alert("this is myTool1");
                    }
                },
                myTool2:{
                    show:true,
                    title:'自定义工具二',
                    icon: 'image://http://echarts.baidu.com/images/favicon.png',                        
                    onclick:function(){
                        alert("this is myTool2");
                    }
                }
                
            },            

        },
        series:[
            {
                name:'马云成绩',
                type:'line',
                data:[90,88,75,82,95,89,97],
                markLine:{
                    data:[{type:'average',name:'平均值'}]
                },
                markPoint:{
                    data:[
                        {type:'max',name:'最高分'},
                        {type:'min',name:'最低分'}
                    ]
                }                    
            },
            {
                name:'马化腾成绩',
                type:'line',
                data:[55,45,99,60,35,45,74],
                markLine:{
                    data:[{type:'average',name:'平均值'}]
                },
                markPoint:{
                    data:[
                        {type:'max',name:'最高分'},
                        {type:'min',name:'最低分'}
                    ]
                }
            }
        ]
    }

    myChart.setOption(option);
</script>
复制代码

 

 

效果展示

ECharts.js学习(二)动态数据绑定 - leoxuan - 博客园

mikel阅读(975)

Echarts 数据绑定 简单的统计表已经可以生成,不过之前图标数据都是直接写在参数里面的,而实际使用中,我们的数据一般都是异步读取的。EChart.js对于数据异步读取这块提供了异步加载的

来源: ECharts.js学习(二)动态数据绑定 – leoxuan – 博客园

charts 数据绑定

简单的统计表已经可以生成,不过之前图标数据都是直接写在参数里面的,而实际使用中,我们的数据一般都是异步读取的。EChart.js对于数据异步读取这块提供了异步加载的方法。

绑定多组数据

很多时候需要展示的数据不单单是一组数据,很多时候会进行一个数据对比。这个时候只需要在series中增加一组数据,legend中添加一下这个数据组的name

复制代码
<!DOCTYPE html>
<html>
<head>
    <title>ECharts.js 数据绑定</title>
    <meta charset="utf-8">
    <script type="text/javascript" src="js/echarts.js"></script>
</head>
<body>
    <div id="chartmain" style="width:600px; height: 400px;"></div>
    <script type="text/javascript">
        //指定图标的配置和数据
        var option = {
            title:{
                text:'ECharts 数据统计'
            },
            legend:{
                data:['访问量','用户量']
            },
            xAxis:{
                data:["Android","IOS","PC","Other"]
            },
            yAxis:{},
            series:[
            {
                name:'访问量',
                type:'bar',
                data:[180,420,333,83]
            },
            {
                name:'用户量',
                type:'bar',
                data:[125,330,230,60]
            }
            ]
        };
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));

        //使用制定的配置项和数据显示图表
        myChart.setOption(option);
    </script>
</body>
</html>
复制代码

效果展示

数据异步加载

EChart中实现异步数据的更新非常简单,在图表初始化后不管任何时候只要通过 JQuery 等工具异步获取数据后通过 setOption 填入数据和配置项就行。

绑定数据的方式有两种,一种是写写好一些图表参数,然后数据留空,然后在异步读取数据的时候,绑定数据。还有一种就是直接异步读取数据的时候同时设置图表参数和数据绑定。

首先我们准备一份需要加载的数据文件data.json,数据内容:

{"name":["Android","IOS","PC","Other"],"data":[420,200,360,100]}

第一种异步加载的时候设置图表参数和绑定数据

复制代码
    <script type="text/javascript">        
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));        
        //异步加载的配置项和数据显示图表
        $.get('data.json').done(function (data) {
            data = eval('('+data+')');            
            myChart.setOption({
                title:{
                    text:'ECharts 异步加载数据'
                },
                tooltip:{},
                legend:{
                    data:['访问量']
                },
                xAxis:{
                    data:data.name
                },
                yAxis:{},
                series:[
                    {
                        name:'访问量',
                        type:'bar',
                        data:data.data
                    }
                ]
            })
        })


    </script>
复制代码

第二种先设置图表参数,后绑定数据

复制代码
<script type="text/javascript">        
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));        
        //设置图标配置项
        myChart.setOption({
            title:{
                text:'ECharts 异步加载数据'
            },
            tooltip:{},
            legend:{
                data:['访问量']
            },
            xAxis:{
                data:[]
            },
            yAxis:{},
            series:[
                {
                    name:'访问量',
                    type:'bar',
                    data:[]
                }
            ]
        })
        //异步加载数据
        $.get('data.json').done(function (data) {
            data = eval('('+data+')');            
            myChart.setOption({                
                xAxis:{
                    data:data.name
                },                
                series:[
                    {    
                        //根据名字对应到相应的系列
                        name:"访问量",
                        data:data.data
                    }
                ]
            })
        })
    </script>
复制代码

效果展示

因为是异步加载,所以有时候数据加载会慢,或者延迟。在数据没有加载前,图表这样的。面对这样的图表,肯定会觉得这是没有数据吗,还是图表有问题.对于这块ECharts增加了一个加载动画。

Loading动画加载

复制代码
        //打开loading动画
        myChart.showLoading();
        //加载数据函数
        function bindData(){
            //为了效果明显,我们做了延迟读取数据
            setTimeout(function(){                
                //异步加载数据
                $.get('data.json').done(function (data) {                    
                    //获取数据后,隐藏loading动画
                    myChart.hideLoading();
                    data = eval('('+data+')');            
                    myChart.setOption({
                        xAxis:{
                            data:data.name
                        },
                        series:[
                            {
                                //根据名字对应到相应的系列
                                name:"访问量",
                                data:data.data
                            }
                        ]
                    })
                })
            },2000)
        }
        
        bindData();
复制代码

效果展示

数据动态实时更新

复制代码
<script type="text/javascript">        
    //初始化echarts实例
    var myChart = echarts.init(document.getElementById('chartmain'));
    var base = + new Date(2017,3,8);
    var oneDay = 24*3600*1000;
    var date = [];
    var data = [Math.random()*150];
    var now = new Date(base);
    var day = 30;
    function addData(shift){
        now = [now.getFullYear(),now.getMonth()+1,now.getDate()].join('/');        
        date.push(now);        
        data.push((Math.random()-0.5)*10+data[data.length-1]);
        if (shift) {
            console.log(data);
            date.shift();
            data.shift();
        }
        now = new Date(+new Date(now)+oneDay);        
    }

    for (var i = 0; i < day; i++) {
        addData();
    }
    //设置图标配置项
    myChart.setOption({
        title:{
            text:'ECharts 30天内数据实时更新'
        },
        xAxis:{
            type:"category",
            boundaryGap:false,
            data:date
        },
        yAxis:{
            boundaryGap:[0,'100%'],
            type:'value'
        },
        series:[{
            name:'成交',
            type:'line',
            smooth:true, //数据光滑过度
            symbol:'none', //下一个数据点
            stack:'a',
            areaStyle:{
                normal:{
                    color:'red'
                }
            },
            data:data
        }]
    })
    setInterval(function(){
        addData(true);
        myChart.setOption({
            xAxis:{
                data:date
            },
            series:[{
                name:'成交',
                data:data
            }]
        });
    },1000)
</script>
复制代码

效果展示

 

ECharts.js学习(一) 简单入门 - leoxuan - 博客园

mikel阅读(754)

来源: ECharts.js学习(一) 简单入门 – leoxuan – 博客园

EChart.js 简单入门

最近有一个统计的项目要做,在前端的数据需要用图表的形式展示。网上搜索了一下,发现有几种统计图库。

MSChart  

这个是Visual Studio里的自带控件,使用比较简单,不过数据这块需要在后台绑定。

ichartjs

是一款基于HTML5的图形库。使用纯JavaScript语言, 利用HTML5的canvas标签绘制各式图形。 支持饼图、环形图、折线图、面积图、柱形图、条形图等。

Chart.js

也是一款基于HTML5的图形库和ichartjs整体类似。不过Chart.js的教程文档没有ichartjs的详细。不过感觉在对于移动的适配上感觉比ichartjs要好一点。

ECharts.js

这是我准备在这个项目中使用的图形库,这也是一款基于HTML5的图形库。图形的创建也比较简单,直接引用JavaScript即可。使用这个库的原因主要有三点,一个是因为这个库是百度的项目,而且一直有更新,目前最新的是EChart 3;第二个是这个库的项目文档比较详细,每个点都说明的比较清楚,而且是中文的,理解比较容易;第三点是这个库支持的图形很丰富,并且可以直接切换图形,使用起来很方便。

官网:ECharts.js

下面来简单说明一下EChart.js的使用。

第一步,引用Js文件

<script type="text/javascript" src="js/echarts.js"></script>

js文件有几个版本,可以根据实际需要引用需要的版本。下载链接

第二步,准备一个放图表的容器

<div id="chartmain" style="width:600px; height: 400px;"></div>

第三步,设置参数,初始化图表

复制代码
<script type="text/javascript">
        //指定图标的配置和数据
        var option = {
            title:{
                text:'ECharts 数据统计'
            },
            tooltip:{},
            legend:{
                data:['用户来源']
            },
            xAxis:{
                data:["Android","IOS","PC","Ohter"]
            },
            yAxis:{

            },
            series:[{
                name:'访问量',
                type:'line',
                data:[500,200,360,100]
            }]
        };
        //初始化echarts实例
        var myChart = echarts.init(document.getElementById('chartmain'));

        //使用制定的配置项和数据显示图表
        myChart.setOption(option);
    </script>
复制代码

这样简单的一个统计图表就出来了,官网使用的柱状图,我这边改用了折线图。

 

柱状图其实也很简单,只要修改一个参数就可以了。把series里的type 值修改为”bar”

饼图和折线图、柱状图有一点区别。主要是在参数和数据绑定上。饼图没有X轴和Y轴的坐标,数据绑定上也是采用value 和name对应的形式。

复制代码
        var option = {
            title:{
                text:'ECharts 数据统计'
            },            
            series:[{
                name:'访问量',
                type:'pie',    
                radius:'60%', 
                data:[
                    {value:500,name:'Android'},
                    {value:200,name:'IOS'},
                    {value:360,name:'PC'},
                    {value:100,name:'Ohter'}
                ]
            }]
        };
复制代码

 

前端数据可视化echarts.js使用指南 - 谢灿勇 - 博客园

mikel阅读(1109)

来源: 前端数据可视化echarts.js使用指南 – 谢灿勇 – 博客园

一、开篇

首先这里要感谢一下我的公司,因为公司需求上面的新颖(奇葩)的需求,让我有幸可以学习到一些好玩有趣的前端技术,前端技术中好玩而且比较实用的我想应该要数前端的数据可视化这一方面,目前市面上的数据可视化的框架琳琅满目,例如:D3.js、hightcharts.js、echarts.js…………。由于公司对这个项目的需求是1、开发时间短,所以也就限制了D3.js的使用。2、要尽量的减少开发的成本,所以也就不能使用hightcharts.js(hightcharts是一款个人免费,商业付费的框架)。所以在再三的比对之下最终选择了echarts.js

 

二、echarts.js的优势与总体情况

echarts.js作为国内的IT三巨头之一的百度的推出一款相对较为成功的开源项目,总体上来说有这样的一些优点

1、echarts.js容易使用

echarts.js的官方文档比较详细,而且官网中提供大量的使用示例供大家使用

2、echarts.js支持按需求打包

echarts.js官网提供了在线构建的工具,可以在线构建项目时,选择项目所需要使用到的模块,从而达到减小JS文件的体积

3、echarts.js开源

4、支持中国地图功能

这个在其他的一些框架中是没有的,所以为这个功能点个赞

 

但是echarts.js也存在着一些不好的地方,比如说:

1、echarts.js的体积较大

一个基础的echarts.js都要400K左右,相对于D3.js和hightcharts.js来说都是比较大的

2、echarts.js的可定制性差

说到echarts.js的定制性差,其实不止是包括echarts.js,hightcharts.js也是如此,因为这一类型的数据可视化框架主要是高度的进行分装,所以你在使用的时候只需要设置一下配置就可以了,但是如果是出现了要绘制配置中不支持的图表怎么办,那么你就只能放弃,尝试着使用其他的框架了

总的来说:从大的方向上面来看,echarts.js还是值得去了解学习使用的,因为echarts.js得到了百度团队的重视,在git上面的更新也是比较的频繁,所以不会出现一些比较严重的bug之类的,最后这款框架一点就是框架的配置文件相当的详细,但是交互API文档虽然有说明,但是还是没有示例来举证,这个可能就是我认为的一个不足之处吧

 

三、echarts的应用

首先要说明一点是,echarts这个框架的配置内容很是多,所以不要尝试着把这个框架中的方法都给记住,这是不太可能的事。但是由于这个框架的配置文件参数比较多,所以我们就需要来学习一下echarts是怎样来对其进行分类的

1、首先echarts的图形化呈现主要是通过配置方法来实现的(setOption),然后是对图形标签进行初始化,最后把配置方法(setOption)赋值到初始化图形中,详细的配置文件请戳这里,这里我就来介绍一下关于配置文件的学习的经验之谈,比较常见的配置大致如下图:

上面用红色方框标出来的就是echarts的基础配置,也是我认为的学习echarts一定要掌握的配置,其他的一些配置比如什么时间轴.visualMap组件之类,我认为这些异曲同工,所以这一部分也就是只有在当你的业务需要使用的时候才加入,也就是说,这一部分的知识我认为到时候现炒现卖就可以了(更正:图标悬停的提示内容应该更正为鼠标悬停的提示内容),下面我就来讲解一下echarts.js的使用,首先我在官网中下载默认的精简版,下载地址如下:http://echarts.baidu.com/builder.html,直接下载即可(建议在开发期间使用源码版,方便调试)

3.1 echarts.js入门基础小项目1

HTML和JavaScript代码:

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>echarts.js案例一</title>
    <script type="text/javascript" src='echarts.js'></script>
</head>
<body>
    <div id="chart" style="width:400px;height:400px;"></div>
</body>
<script type="text/javascript">
    // 初始化图表标签
    var myChart = echarts.init(document.getElementById('chart'));
    var options={
        //定义一个标题
        title:{
            text:'测试成绩'
        },
        legend:{
            data:['销量']
        },
        //X轴设置
        xAxis:{
            data:['60分','70分','80分','90分','100分']
        },
        yAxis:{
        },
        //name=legend.data的时候才能显示图例
        series:[{
            name:'销量',
            type:'bar',
            data:['12','32','45','21','1']
        }]

    };
    myChart.setOption(options);
</script>
</html>
复制代码

运行效果如下图,如果需要在线观看的请戳这里

注意事项:这里案例是最基础,但是里面还是有一个知识点来的,就是在使用echarts.js的时候一定要配置xAxis,yAxis,series这三个参数,如果是不想设置的话也要初始化可以将其设置为空JSON就可以了,要不然会出项报错,同时要保证在echarts.init之前的对象是有宽高的,要不然也会出现错误

3.2 echarts.js多系列综合使用DEMO

在讲解这个案例之前,首先我们来假设一个命题,假设要统计一个商店一周的购买金额和一周的销售金额,其中的购买金额用柱状图表示,销售金额用折线图表示,然后还要标出一周中最大值和最小值,同时还要求出销售和购买的平均数,购买金额分别是[200,312,431,241,175,275,369],销售金额[321,432,543,376,286,298,400]

这个问题其实也不是很难,想一想,其实也就是一个把多个 系列图表应用到一个画布上面的过程,为了简短文章的篇幅,所以把不贴出全部的代码,仅贴出主要的其中关键的代码,代码如下:

 

复制代码
            series:[{
                name:'购买金额',
                type:'bar',
                data:[200,312,431,241,175,275,369],
                markPoint: {
                    data: [
                        {type: 'max', name: '最大值'},
                        {type: 'min', name: '最小值'}
                    ]
                },
                markLine:{
                    data:[
                        {type:'average',name:'平均值',itemStyle:{
                            normal:{
                                color:'green'
                            }
                        }}
                    ]
                }
            },{
                name:'销售金额',
                type:'line',
                data:[321,432,543,376,286,298,400],
                markPoint: {
                    data: [
                        {type: 'max', name: '最大值'},
                        {type: 'min', name: '最小值'}
                    ]
                },
                markLine:{
                    data:[
                        {type:'average',name:'平均值',itemStyle:{
                            normal:{
                                color:'blue'
                            }
                        }}
                    ]
                }
            }]
复制代码

实现效果:

如需看完整的代码请戳这里,自行fork下来

3.3 echarts.js响应式实现

echarts响应式在echarts官网上面的介绍比较详细,这里原理跟CSS3的媒体查询有点类似,但是echarts.js的响应除了支持媒体查询的在不同情况下面的相应还支持,根据长宽比来相应的方法,但是在官方文档中还是有一点缺陷的,比如:一个是案例中的响应式没有涉及到处理series之外的响应,另外一个是按照DEMO中去做,会发现每次都要刷新页面才能出现响应的结果,所以下面我将写一个简单的案例来解决这些问题,数据样式与上面的例子一样

这里就把所有的JS代码贴出来:

 

复制代码
var echart=echarts.init(document.getElementById('main1'));
        var option={
            baseOption:{
                    title:{
                    text:'模拟商店一周销售情况',
                    subtext:'虚拟数据'
                },
                legend:{
                    data:['购买金额','销售金额']
                },
                xAxis:{
                    data:['周一','周二','周三','周四','周五','周六','周日']
                },
                yAxis:{

                },
                tooltip:{
                    show:true,
                    formatter:'系列名:{a}<br />类目:{b}<br />数值:{c}'
                },
                series:[{
                    name:'购买金额',
                    type:'bar',
                    data:[200,312,431,241,175,275,369],
                    markPoint: {
                        data: [
                            {type: 'max', name: '最大值'},
                            {type: 'min', name: '最小值'}
                        ]
                    },
                    markLine:{
                        data:[
                            {type:'average',name:'平均值',itemStyle:{
                                normal:{
                                    color:'green'
                                }
                            }}
                        ]
                    }
                },{
                    name:'销售金额',
                    type:'line',
                    data:[321,432,543,376,286,298,400],
                    markPoint: {
                        data: [
                            {type: 'max', name: '最大值'},
                            {type: 'min', name: '最小值'}
                        ]
                    },
                    markLine:{
                        data:[
                            {type:'average',name:'平均值',itemStyle:{
                                normal:{
                                    color:'blue'
                                }
                            }}
                        ]
                    }
                }]
            },
            media:[
                {
                    //小与1000像素时候响应
                    query:{
                        maxWidth:1000
                    },
                    option:{
                        title:{
                            show:true,
                            text:'测试一下'
                        }
                    }
                }
            ]
        };
        //每次窗口大小改变的时候都会触发onresize事件,这个时候我们将echarts对象的尺寸赋值给窗口的大小这个属性,从而实现图表对象与窗口对象的尺寸一致的情况
        window.onresize = echart.resize;
        echart.setOption(option);
复制代码

效果展示:原本是想要展示GIF的,但是录制的时候卡顿太严重所以也就只能发一下在无刷新的时候对比图片

3.4 echarts的API交互

首先我们来理清官方文档中的API的分类,大致的API可以分成这样的四类

这里我们就来解释一下echarts对象里面主要是包括一些销毁对象(dispose),注册地图(registerMap),初始化对象(echarts.init),关联对象(connect),属于全局属性的设置,echartsInstance这个是包含echarts图中的某些属性的获取或者设置,获取宽高(getWidth、getHeight),获取配置(getOption),设置配置(setOption)等操作action和events这两个操作在上图中已经很明白了,所以就不多做解释,具体的使用方法要与业务进行挂钩才有意义,所以在这里就不提供DEMO了,我相信大家看一下文档都能够看懂个究竟。

 

四、echarts常见问题解决

1、当X轴上面要渲染的数据太多的时候就会出现只渲染出来一部分,但是图表中的数据显示(比如说柱状图中的每个柱子)又会自动的进行宽度的缩放,所以会导致X轴与图中的信息不相匹配的问题,解决的方法是在X轴设置axisLabel :{ interval:0 }这个属性问题就可以解决了,Y轴的使用方法相同

2、为了使echart图表随着浏览器的大小发生响应式变化,所以需要在设置配置之前添加 window.onresize = echart.resize;  具体参照3.3 示例

内容持续更新中,敬请期待

 

五、总结

虽然echarts在同类型的数据可视化框架中还算是比较的简单易用的,但是在入手的时候可能也会有一些比较麻烦的地方困扰着你,比如文档这么多我怎么看,实例已经比较齐全了我还需要看API文档吗,这些问题我想在这里统一的说一下,其实echarts的学习无外乎就是先了解一下框架大致上的分类,然后是在将全部的API阅读以便,使对框架有一个全局的认识,然后在通过实践去深入学习,示例只不过是这一步的一个辅助而已,只有扎扎实实的通过学习API一步一个脚印去了解学习,才能做到在使用的时候心中有底。以上的文章是博主在学习和项目中使用echarts的一些体会,希望对大家有帮助

反射学习系列2-特性(Attribute) - 你听海是不是在笑 - 博客园

mikel阅读(884)

来源: 反射学习系列2-特性(Attribute) – 你听海是不是在笑 – 博客园

反射学习系列目录

反射学习系列1-反射入门

反射学习系列2-特性(Attribute)

反射学习系列3-反射实例应用

作者

 

 

先看一个简单的例子
[Table(Name=”dbo.[User]”)]
public partial class User
{
C#编译器发现这个属性有一个特性Table时,首先会把字符串Attribute添加到这个名称的后面,形成一个组合名称TableAttribute,然后在其搜索路径的所有命名空间中搜索有相同类名的类。但要注意,如果该特性名结尾是Attribute,编译器就不会把该字符串加到组合名称中。所有的特性都是从System.Attribute类型上面派生的。
接着我们来看一下Table特性的定制格式
[AttributeUsageAttribute(AttributeTargets.Class, Inherited=true,AllowMultiple=false)]
public class TalbeAttribute:Attribute
{


在定义类型时使用System.AttributeUsage特性来表明这个自定义特性的使用范围,这里使用了Class样式,表示TableAttribute特性只能用在其它的Class类型前面,若放置在Interface或Struct类型前面,或者放在对象成员的前面则会出现编译错误。这里还是用语句 AllowMultiple=false 语句来表明对于一个类型,该特性只能用一次,若一个Class类型前面出现多个TableAttribute,则会出现编译错误。若设置AllowMultiple=true,则该特性可以多次定义,也就是一个Class类型前面可以出现多个相同类型的特性。不过这里我们假设一个对象只能映射到一个数据表上,没有多重映射,因此就指明对同一个类型该特性不能多次使用。Inherited参数设定为true,就表示应用到类或接口上的特性也可以自动应用到所派生的类或接口上。
我们再看一下定制TalbeAttribute特性的完整例子:

 

复制代码

[AttributeUsageAttribute(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public class TableAttribute : Attribute
{
//保存表名的字段
private string _tableName;

public TableAttribute()
{
}

public TableAttribute(string tableName)
{
this._tableName = tableName;
}

/// <summary>
/// 映射的表名(表的全名:模式名.表名)
/// </summary>
public string TableName
{
set
{
this._tableName = value;
}
get
{
return this._tableName;
}
}
}

复制代码

 

 特性也是一个Class类型,可以有多个构造函数,就像C#的new语句一样,我们向类型附加特性时可以使用不同的初始化参数来指明使用特性的那个构造函数。我们附加特性时还可以使用“属性名=属性值”的方法来直接指明特性的属性值。该特性中定义了一个TableName属性,该属性就是被修饰的对象所映射的数据库表的名称。

下面我们举一个使用特性来进行O/RMapping的例子,也就是将对象转化成SQL语句

用户类:

[Table(“User”)]
public class User
{
[Colum(“userID”, DbType = DbType.Int32)]
public int UserID { get; set; }
[Colum(“UserName”, DbType = DbType.String)]
public string UserName { get; set; }
}

 

表特性

 

表特性

 

列特性:

 


[AttributeUsageAttribute(AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public class ColumAttribute : Attribute
{
private string _columName;

private DbType _dbType ;

public ColumAttribute()
{
}

public ColumAttribute(string columName)
: this()
{
this._columName = columName;
}

public ColumAttribute(string columName, DbType dbType)
: this(columName)
{
this._dbType = dbType;
}

//列名
public virtual string ColumName
{
set
{
this._columName = value;
}
get
{
return this._columName;
}
}

//描述一些特殊的数据库类型
public DbType DbType
{
get { return _dbType; }
set { _dbType = value; }
}

}

 

 


public class ORMHelp
{
public void Insert(object table)
{
Type type = table.GetType();
//定义一个字典来存放表中字段和值的对应序列
Dictionary<string, string> columValue = new Dictionary<string, string>();
StringBuilder SQLStr=new StringBuilder();
SQLStr.Append(“insert into “);
//得到表名子
TableAttribute temp = (TalbeAttribute)type.GetCustomAttributes(typeof(TalbeAttribute), false).First();
SqlStr.Append(temp.TableName);
SqlStr.Append(“(“);
PropertyInfo[] Propertys=type.GetProperties();
foreach (var item in Propertys)
{
object[] attributes = item.GetCustomAttributes(false);
foreach (var item1 in attributes)
{
//获得相应属性的值
string value= table.GetType().InvokeMember(item.Name, System.Reflection.BindingFlags.GetProperty, null, table, null).ToString();
ColumAttribute colum = item1 as ColumAttribute;
if (colum != null)
{
columValue.Add(colum.ColumName,value);
}
}
}
//拼插入操作字符串
foreach (var item in columValue)
{
SqlStr.Append(item.Key);
SqlStr.Append(“,”);

}
SqlStr.Remove(SqlStr.Length-1, 1);
SqlStr.Append(“) values(‘”);
foreach (var item in columValue)
{
SqlStr.Append(item.Value);
SqlStr.Append(“‘,'”);

}
SqlStr.Remove(SqlStr.Length – 2, 2);
SqlStr.Append(“)”);
Console.WriteLine(SqlStr.ToString());

}
}

SqlStr中的内容为insert into User(userID,UserName) values(‘1′,’lfm’)

前端使用代码:

 


static void Main(string[] args)
{
ORMHelp o = new ORMHelp();
User u = new User() { UserID=1,UserName=”lfm”};
o.Insert(u);
}

作者

源码下载

asp.net(c#)实现从sql server存取二进制图片 - JeffWong - 博客园

mikel阅读(775)

有一个员工表Employee,需要保存员工照片(Photo)到数据库(SQL server)上。员工照片对应的字段是varbinary(max),也就是要存成二进制文件类型(这和以前讨巧地存图片文件路

来源: asp.net(c#)实现从sql server存取二进制图片 – JeffWong – 博客园

有一个员工表Employee,需要保存员工照片(Photo)到数据库(SQL server)上。员工照片对应的字段是varbinary(max),也就是要存成二进制文件类型(这和以前讨巧地存图片文件路径就不相同了),默认可以为空。下面说说主要实现思路:
1、存取图片
(1)、将图片文件转换为二进制并直接存进sql server


//UploadHelper.cs
/// <summary>
/// 将图片转化为长二进制
/// </summary>
/// <param name=”photopath”></param>
/// <returns></returns>
public static Byte[] SetImgToByte(string imgPath)
{
FileStream file = new FileStream(imgPath, FileMode.Open, FileAccess.Read);
Byte[] byteData = new Byte[file.Length];
file.Read(byteData, 0, byteData.Length);
file.Close();
return byteData;
}

/// <summary>
/// 将转换成二进制码的图片保存到数据库中
/// </summary>
public static bool SaveEmployeeImg2Db(Employee model, string path)
{
try
{
Byte[] imgBytes = SetImgToByte(path);
model.Photo = imgBytes;
bool flag=EmployeeService.SaveEmployeePhoto(model); //EmployeeService是公司内部的库调用,插入或者更新照片,这里不透露细节
return flag;

}
catch (Exception ex)
{
throw ex;
}
}

(2)、在网页中上传图片


/// <summary>
/// 上传图片
/// </summary>
/// <param name=”sender”></param>
/// <param name=”e”></param>
protected void btnUpload_Click(object sender, EventArgs e)
{
string serverPath = Server.MapPath(“~/images/”);
if (this.fuPhoto.HasFile) //fuPhoto是fileupload控件
{
string fileName = this.fuPhoto.PostedFile.FileName;
FileInfo fi = new FileInfo(fileName);
string mimeType = this.fuPhoto.PostedFile.ContentType.ToLower();
if (mimeType.IndexOf(“image”) < 0)
{
//(“上传的照片格式不对”);
}
else if(fi.Length > 2* 1024 * 1024)
{
//图片大于2M,重新处理
}
else
{
string saveFilePath = serverPath + DateTime.Now.ToString(“yyyyMMddHHmmss”) + fileName;
try
{
//先存图片到服务器
this.fuPhoto.PostedFile.SaveAs(saveFilePath);

//转成二进制
Employee model = new Employee(int.Parse(id)); //id是EmployeeId,这里是模拟字段
bool flag = UploadHelper.SaveEmployeeImg2Db(model, saveFilePath);
}
catch
{
//(“照片上传失败”);
}
finally
{
//最后删掉该图片
if (System.IO.File.Exists(saveFilePath))
{
System.IO.File.Delete(saveFilePath);
}
}
}
}
else
{
//(“全选择要上传的照片”);
}
}

(3)、从数据库取出照片(返回格式Image)


//UploadHelper.cs
/// <summary>
/// 将二进制转化为图片Image
/// </summary>
/// <param name=”photopath”></param>
/// <returns></returns>
public static System.Drawing.Image GetImgFromByte(Employee model)
{
System.Drawing.Image img = null;
try
{
Stream stream = new MemoryStream(model.Photo);
img = System.Drawing.Image.FromStream(stream,false);
}
catch
{
img = null;
}
return img;
}

上面的这个方法取出来之后,如果在winform下,直接给一个PictureBox的Image属性赋值就可以了。可是web下没有这么强大的控件,所以,就有了下面的步骤。
2、直接在网页中以流的形式显示图片
(1)、生成图片流页面(ImgHelper .aspx)
这个页面的设计页面什么也没有,类文件如下:


using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.IO;

/// <summary>
/// 图片辅助类
/// </summary>
public partial class ImgHelper : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!string.IsNullOrEmpty(Request[“employeeId”])) //需要显示照片的页面传递的员工id
{
int employeeId = int.Parse(Request[“employeeId”]);
Employee model = //EmployeeService.GetEmployeeByCondition(new Employee(employeeId))[0] as Employee; //内部函数 查找一个员工 不透漏细节
try
{
Byte[] byteImg = model.Photo;
Stream stream = new MemoryStream(byteImg);
System.Drawing.Bitmap img =(System.Drawing.Bitmap) System.Drawing.Bitmap.FromStream(stream, false); //转换成Bitmap
Response.Buffer = false;
Response.ContentType = “image/jpg”;
Response.AddHeader(“Content-Disposition”, “attachment;filename=photo.jpg”);//照片名称叫photo.jpg
Response.BinaryWrite(byteImg);//写入二进制流
Response.End();
}
catch
{
Response.End();
}
}
}
}

(2)、显示照片的页面调用ImgHelper .aspx
在页面加载的时候,给图片控件赋值如下:

   this.imgPhoto.ImageUrl = “/ImgHelper.aspx?employeeId=”+tmpEmployee.Id.ToString(); //imgPhoto是图片控件

总体来说,一存一取,对于winform是很方便的,但是对于webform,我们需要稍微有一个转化的思路。如果有牛人写出像winform下那种直接绑定Image对象的控件更好了。上面代码测试通过,希望对你有帮助。