详解免费开源的DotNet任务调度组件Quartz.NET(.NET组件介绍之五)

时间:2022-01-03 00:03:38

很多的软件项目中都会使用到定时任务、定时轮询数据库同步,定时邮件通知等功能。.NET Framework具有“内置”定时器功能,通过System.Timers.Timer类。在使用Timer类需要面对的问题:计时器没有持久化机制;计时器具有不灵活的计划(仅能设置开始时间和重复间隔,没有基于日期,时间等);计时器不使用线程池(每个定时器一个线程);计时器没有真正的管理方案 - 你必须编写自己的机制,以便能够记住,组织和检索任务的名称等。

如果需要在.NET实现定时器的功能,可以尝试使用以下这款开源免费的组件Quartz.Net组件。目前Quartz.NET版本为3.0,修改了原来的一些问题:修复由于线程本地存储而不能与AdoJobStore协同工作的调度器信令;线程局部状态完全删除;quartz.serializer.type是必需的,即使非序列化RAMJobStore正在使用;JSON序列化错误地称为序列化回调。

一.Quart.NET概述: 

Quartz是一个作业调度系统,可以与任何其他软件系统集成或一起使用。作业调度程序是一个系统,负责在执行预处理程序时执行(或通知)其他软件组件 - 确定(调度)时间到达。Quartz是非常灵活的,并且包含多个使用范例,可以单独使用或一起使用,以实现您所需的行为,并使您能够以您的项目看起来最“自然”的方式编写代码。组件的使用非常轻便,并且需要非常少的设置/配置 - 如果您的需求相对基础,它实际上可以使用“开箱即用”。Quartz是容错的,并且可以在系统重新启动之间保留(记住)您的预定作业。尽管Quartz对于在给定的时间表上简单地运行某些系统进程非常有用,但当您学习如何使用Quartz来驱动应用程序的业务流程时,Quartz的全部潜能可以实现。

 Quartz是作为一个小的动态链接库(.dll文件)分发的,它包含所有的核心Quartz功能。 此功能的主要接口(API)是调度程序接口。 它提供简单的操作,如调度/非调度作业,启动/停止/暂停调度程序。如果你想安排你自己的软件组件执行,他们必须实现简单的Job接口,它包含方法execute()。 如果希望在计划的触发时间到达时通知组件,则组件应实现TriggerListener或JobListener接口。主要的Quartz'进程'可以在您自己的应用程序或独立应用程序(使用远程接口)中启动和运行。

二.Quartz.NET主体类和方法解析:

1.StdSchedulerFactory类:创建QuartzScheduler实例。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// <summary>
  /// 返回此工厂生成的调度程序的句柄。
  /// </summary>
  /// <remarks>
  ///如果<see cref =“Initialize()”/>方法之一没有先前调用,然后是默认(no-arg)<see cref =“Initialize()”/>方法将被这个方法调用。
  /// </remarks>
  public virtual IScheduler GetScheduler()
  {
    if (cfg == null)
    {
      Initialize();
    }
 
    SchedulerRepository schedRep = SchedulerRepository.Instance;
 
    IScheduler sched = schedRep.Lookup(SchedulerName);
 
    if (sched != null)
    {
      if (sched.IsShutdown)
      {
        schedRep.Remove(SchedulerName);
      }
      else
      {
        return sched;
      }
    }
 
    sched = Instantiate();
 
    return sched;
  }
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface ISchedulerFactory
  {
    /// <summary>
    /// Returns handles to all known Schedulers (made by any SchedulerFactory
    /// within this app domain.).
    /// </summary>
    ICollection<IScheduler> AllSchedulers { get; }
 
    /// <summary>
    /// Returns a client-usable handle to a <see cref="IScheduler" />.
    /// </summary>
    IScheduler GetScheduler();
 
    /// <summary>
    /// Returns a handle to the Scheduler with the given name, if it exists.
    /// </summary>
    IScheduler GetScheduler(string schedName);
  }

2.JobDetailImpl:传递给定作业实例的详细信息属性。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
    /// 获取或设置与<see cref =“IJob”/>相关联的<see cref =“JobDataMap”/>。
    /// </summary>
    public virtual JobDataMap JobDataMap
    {
      get
      {
        if (jobDataMap == null)
        {
          jobDataMap = new JobDataMap();
        }
        return jobDataMap;
      }
 
      set { jobDataMap = value; }
    }

3.JobKey:键由名称和组组成,名称必须是唯一的,在组内。 如果只指定一个组,则默认组将使用名称。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Serializable]
public sealed class JobKey : Key<JobKey>
{
  public JobKey(string name) : base(name, null)
  {
  }
 
  public JobKey(string name, string group) : base(name, group)
  {
  }
 
  public static JobKey Create(string name)
  {
    return new JobKey(name, null);
  }
 
  public static JobKey Create(string name, string group)
  {
    return new JobKey(name, group);
  }
}

4.StdSchedulerFactory.Initialize():

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/// <summary>
    /// 使用初始化<see cref =“ISchedulerFactory”/>
     ///给定键值集合对象的内容。
    /// </summary>
    public virtual void Initialize(NameValueCollection props)
    {
      cfg = new PropertiesParser(props);
      ValidateConfiguration();
    }
 
    protected virtual void ValidateConfiguration()
    {
      if (!cfg.GetBooleanProperty(PropertyCheckConfiguration, true))
      {
        // should not validate
        return;
      }
 
      // determine currently supported configuration keys via reflection
      List<string> supportedKeys = new List<string>();
      List<FieldInfo> fields = new List<FieldInfo>(GetType().GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy));
      // choose constant string fields
      fields = fields.FindAll(field => field.FieldType == typeof (string));
 
      // read value from each field
      foreach (FieldInfo field in fields)
      {
        string value = (string) field.GetValue(null);
        if (value != null && value.StartsWith(ConfigurationKeyPrefix) && value != ConfigurationKeyPrefix)
        {
          supportedKeys.Add(value);
        }
      }
 
      // now check against allowed
      foreach (string configurationKey in cfg.UnderlyingProperties.AllKeys)
      {
        if (!configurationKey.StartsWith(ConfigurationKeyPrefix) || configurationKey.StartsWith(ConfigurationKeyPrefixServer))
        {
          // don't bother if truly unknown property
          continue;
        }
 
        bool isMatch = false;
        foreach (string supportedKey in supportedKeys)
        {
          if (configurationKey.StartsWith(supportedKey, StringComparison.InvariantCulture))
          {
            isMatch = true;
            break;
          }
        }
        if (!isMatch)
        {
          throw new SchedulerConfigException("Unknown configuration property '" + configurationKey + "'");
        }
      }
 
    }

三.Quartz.NET的基本应用:

 下面提供一些较为通用的任务处理代码:

1.任务处理帮助类:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/// <summary>
 /// 任务处理帮助类
 /// </summary>
 public class QuartzHelper
 {
   public QuartzHelper() { }
 
   public QuartzHelper(string quartzServer, string quartzPort)
   {
     Server = quartzServer;
     Port = quartzPort;
   }
 
   /// <summary>
   /// 锁对象
   /// </summary>
   private static readonly object Obj = new object();
 
   /// <summary>
   /// 方案
   /// </summary>
   private const string Scheme = "tcp";
 
   /// <summary>
   /// 服务器的地址
   /// </summary>
   public static string Server { get; set; }
 
   /// <summary>
   /// 服务器的端口
   /// </summary>
   public static string Port { get; set; }
 
   /// <summary>
   /// 缓存任务所在程序集信息
   /// </summary>
   private static readonly Dictionary<string, Assembly> AssemblyDict = new Dictionary<string, Assembly>();
 
   /// <summary>
   /// 程序调度
   /// </summary>
   private static IScheduler _scheduler;
 
   /// <summary>
   /// 初始化任务调度对象
   /// </summary>
   public static void InitScheduler()
   {
     try
     {
       lock (Obj)
       {
         if (_scheduler != null) return;
         //配置文件的方式,配置quartz实例
         ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
         _scheduler = schedulerFactory.GetScheduler();
       }
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 启用任务调度
   /// 启动调度时会把任务表中状态为“执行中”的任务加入到任务调度队列中
   /// </summary>
   public static void StartScheduler()
   {
     try
     {
       if (_scheduler.IsStarted) return;
       //添加全局监听
       _scheduler.ListenerManager.AddTriggerListener(new CustomTriggerListener(), GroupMatcher<TriggerKey>.AnyGroup());
       _scheduler.Start();
 
       //获取所有执行中的任务
       List<TaskModel> listTask = TaskHelper.GetAllTaskList().ToList();
 
       if (listTask.Count > 0)
       {
         foreach (TaskModel taskUtil in listTask)
         {
           try
           {
             ScheduleJob(taskUtil);
           }
           catch (Exception e)
           {
            throw new Exception(taskUtil.TaskName,e);
           }
         }
       }      
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 启用任务
   /// <param name="task">任务信息</param>
   /// <param name="isDeleteOldTask">是否删除原有任务</param>
   /// <returns>返回任务trigger</returns>
   /// </summary>
   public static void ScheduleJob(TaskModel task, bool isDeleteOldTask = false)
   {
     if (isDeleteOldTask)
     {
       //先删除现有已存在任务
       DeleteJob(task.TaskID.ToString());
     }
     //验证是否正确的Cron表达式
     if (ValidExpression(task.CronExpressionString))
     {
       IJobDetail job = new JobDetailImpl(task.TaskID.ToString(), GetClassInfo(task.AssemblyName, task.ClassName));
       //添加任务执行参数
       job.JobDataMap.Add("TaskParam", task.TaskParam);
 
       CronTriggerImpl trigger = new CronTriggerImpl
       {
         CronExpressionString = task.CronExpressionString,
         Name = task.TaskID.ToString(),
         Description = task.TaskName
       };
       _scheduler.ScheduleJob(job, trigger);
       if (task.Status == TaskStatus.STOP)
       {
         JobKey jk = new JobKey(task.TaskID.ToString());
         _scheduler.PauseJob(jk);
       }
       else
       {
         List<DateTime> list = GetNextFireTime(task.CronExpressionString, 5);
         foreach (var time in list)
         {
           LogHelper.WriteLog(time.ToString(CultureInfo.InvariantCulture));
         }
       }
     }
     else
     {
       throw new Exception(task.CronExpressionString + "不是正确的Cron表达式,无法启动该任务!");
     }
   }
 
 
   /// <summary>
   /// 初始化 远程Quartz服务器中的,各个Scheduler实例。
   /// 提供给远程管理端的后台,用户获取Scheduler实例的信息。
   /// </summary>
   public static void InitRemoteScheduler()
   {
     try
     {
       NameValueCollection properties = new NameValueCollection
       {
         ["quartz.scheduler.instanceName"] = "ExampleQuartzScheduler",
         ["quartz.scheduler.proxy"] = "true",
         ["quartz.scheduler.proxy.address"] =string.Format("{0}://{1}:{2}/QuartzScheduler", Scheme, Server, Port)
       };
 
       ISchedulerFactory sf = new StdSchedulerFactory(properties);
 
       _scheduler = sf.GetScheduler();
     }
     catch (Exception ex)
     {
       throw new Exception(ex.StackTrace);
     }
   }
 
   /// <summary>
   /// 删除现有任务
   /// </summary>
   /// <param name="jobKey"></param>
   public static void DeleteJob(string jobKey)
   {
     try
     {
       JobKey jk = new JobKey(jobKey);
       if (_scheduler.CheckExists(jk))
       {
         //任务已经存在则删除
         _scheduler.DeleteJob(jk);
         
       }
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
  
 
   /// <summary>
   /// 暂停任务
   /// </summary>
   /// <param name="jobKey"></param>
   public static void PauseJob(string jobKey)
   {
     try
     {
       JobKey jk = new JobKey(jobKey);
       if (_scheduler.CheckExists(jk))
       {
         //任务已经存在则暂停任务
         _scheduler.PauseJob(jk);
       }
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 恢复运行暂停的任务
   /// </summary>
   /// <param name="jobKey">任务key</param>
   public static void ResumeJob(string jobKey)
   {
     try
     {
       JobKey jk = new JobKey(jobKey);
       if (_scheduler.CheckExists(jk))
       {
         //任务已经存在则暂停任务
         _scheduler.ResumeJob(jk);
       }
     }
     catch (Exception ex)
     {
      throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 获取类的属性、方法
   /// </summary>
   /// <param name="assemblyName">程序集</param>
   /// <param name="className">类名</param>
   private static Type GetClassInfo(string assemblyName, string className)
   {
     try
     {
       assemblyName = FileHelper.GetAbsolutePath(assemblyName + ".dll");
       Assembly assembly = null;
       if (!AssemblyDict.TryGetValue(assemblyName, out assembly))
       {
         assembly = Assembly.LoadFrom(assemblyName);
         AssemblyDict[assemblyName] = assembly;
       }
       Type type = assembly.GetType(className, true, true);
       return type;
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 停止任务调度
   /// </summary>
   public static void StopSchedule()
   {
     try
     {
       //判断调度是否已经关闭
       if (!_scheduler.IsShutdown)
       {
         //等待任务运行完成
         _scheduler.Shutdown(true);
       }
     }
     catch (Exception ex)
     {
       throw new Exception(ex.Message);
     }
   }
 
   /// <summary>
   /// 校验字符串是否为正确的Cron表达式
   /// </summary>
   /// <param name="cronExpression">带校验表达式</param>
   /// <returns></returns>
   public static bool ValidExpression(string cronExpression)
   {
     return CronExpression.IsValidExpression(cronExpression);
   }
 
   /// <summary>
   /// 获取任务在未来周期内哪些时间会运行
   /// </summary>
   /// <param name="CronExpressionString">Cron表达式</param>
   /// <param name="numTimes">运行次数</param>
   /// <returns>运行时间段</returns>
   public static List<DateTime> GetNextFireTime(string CronExpressionString, int numTimes)
   {
     if (numTimes < 0)
     {
       throw new Exception("参数numTimes值大于等于0");
     }
     //时间表达式
     ITrigger trigger = TriggerBuilder.Create().WithCronSchedule(CronExpressionString).Build();
     IList<DateTimeOffset> dates = TriggerUtils.ComputeFireTimes(trigger as IOperableTrigger, null, numTimes);
     List<DateTime> list = new List<DateTime>();
     foreach (DateTimeOffset dtf in dates)
     {
       list.Add(TimeZoneInfo.ConvertTimeFromUtc(dtf.DateTime, TimeZoneInfo.Local));
     }
     return list;
   }
 
 
   public static object CurrentTaskList()
   {
     throw new NotImplementedException();
   }
 
   /// <summary>
   /// 获取当前执行的Task 对象
   /// </summary>
   /// <param name="context"></param>
   /// <returns></returns>
   public static TaskModel GetTaskDetail(IJobExecutionContext context)
   {
     TaskModel task = new TaskModel();
 
     if (context != null)
     {
 
       task.TaskID = Guid.Parse(context.Trigger.Key.Name);
       task.TaskName = context.Trigger.Description;
       task.RecentRunTime = DateTime.Now;
       task.TaskParam = context.JobDetail.JobDataMap.Get("TaskParam") != null ? context.JobDetail.JobDataMap.Get("TaskParam").ToString() : "";
     }
     return task;
   }
 }

2.设置执行中的任务:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class TaskBll
  {
    private readonly TaskDAL _dal = new TaskDAL();
 
    /// <summary>
    /// 获取任务列表
    /// </summary>
    /// <param name="pageIndex"></param>
    /// <param name="pageSize"></param>
    /// <returns></returns>
    public PageOf<TaskModel> GetTaskList(int pageIndex, int pageSize)
    {
      return _dal.GetTaskList(pageIndex, pageSize);
    }
 
    /// <summary>
    /// 读取数据库中全部的任务
    /// </summary>
    /// <returns></returns>
    public List<TaskModel> GetAllTaskList()
    {
      return _dal.GetAllTaskList();
    }
 
    /// <summary>
    ///
    /// </summary>
    /// <param name="taskId"></param>
    /// <returns></returns>
    public TaskModel GetById(string taskId)
    {
      throw new NotImplementedException();
    }
 
    /// <summary>
    /// 删除任务
    /// </summary>
    /// <param name="taskId"></param>
    /// <returns></returns>
    public bool DeleteById(string taskId)
    {
      return _dal.UpdateTaskStatus(taskId, -1);
    }
 
    /// <summary>
    /// 修改任务
    /// </summary>
    /// <param name="taskId"></param>
    /// <param name="status"></param>
    /// <returns></returns>
    public bool UpdateTaskStatus(string taskId, int status)
    {
      return _dal.UpdateTaskStatus(taskId, status);
    }
 
    /// <summary>
    /// 修改任务的下次启动时间
    /// </summary>
    /// <param name="taskId"></param>
    /// <param name="nextFireTime"></param>
    /// <returns></returns>
    public bool UpdateNextFireTime(string taskId, DateTime nextFireTime)
    {
      return _dal.UpdateNextFireTime(taskId, nextFireTime);
    }
 
    /// <summary>
    /// 根据任务Id 修改 上次运行时间
    /// </summary>
    /// <param name="taskId"></param>
    /// <param name="recentRunTime"></param>
    /// <returns></returns>
    public bool UpdateRecentRunTime(string taskId, DateTime recentRunTime)
    {
      return _dal.UpdateRecentRunTime(taskId, recentRunTime);
    }
 
    /// <summary>
    /// 根据任务Id 获取任务
    /// </summary>
    /// <param name="taskId"></param>
    /// <returns></returns>
    public TaskModel GetTaskById(string taskId)
    {
      return _dal.GetTaskById(taskId);
    }
 
    /// <summary>
    /// 添加任务
    /// </summary>
    /// <param name="task"></param>
    /// <returns></returns>
    public bool Add(TaskModel task)
    {
      return _dal.Add(task);
    }
 
    /// <summary>
    /// 修改任务
    /// </summary>
    /// <param name="task"></param>
    /// <returns></returns>
    public bool Edit(TaskModel task)
    {
      return _dal.Edit(task);
    }
  }

3.任务实体:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/// <summary>
  /// 任务实体
  /// </summary>
  public class TaskModel
  {
    /// <summary>
    /// 任务ID
    /// </summary>
    public Guid TaskID { get; set; }
 
    /// <summary>
    /// 任务名称
    /// </summary>
    public string TaskName { get; set; }
 
    /// <summary>
    /// 任务执行参数
    /// </summary>
    public string TaskParam { get; set; }
 
    /// <summary>
    /// 运行频率设置
    /// </summary>
    public string CronExpressionString { get; set; }
 
    /// <summary>
    /// 任务运频率中文说明
    /// </summary>
    public string CronRemark { get; set; }
 
    /// <summary>
    /// 任务所在DLL对应的程序集名称
    /// </summary>
    public string AssemblyName { get; set; }
 
    /// <summary>
    /// 任务所在类
    /// </summary>
    public string ClassName { get; set; }
 
    public TaskStatus Status { get; set; }
 
    /// <summary>
    /// 任务创建时间
    /// </summary>
    public DateTime? CreatedTime { get; set; }
 
    /// <summary>
    /// 任务修改时间
    /// </summary>
    public DateTime? ModifyTime { get; set; }
 
    /// <summary>
    /// 任务最近运行时间
    /// </summary>
    public DateTime? RecentRunTime { get; set; }
 
    /// <summary>
    /// 任务下次运行时间
    /// </summary>
    public DateTime? NextFireTime { get; set; }
 
    /// <summary>
    /// 任务备注
    /// </summary>
    public string Remark { get; set; }
 
    /// <summary>
    /// 是否删除
    /// </summary>
    public int IsDelete { get; set; }
  }

 4.配置文件:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# You can configure your scheduler in either <quartz> configuration section
# or in quartz properties file
# Configuration section has precedence
 
quartz.scheduler.instanceName = ExampleQuartzScheduler
 
# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal
 
# job initialization plugin handles our xml reading, without it defaults are used
# quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz
# quartz.plugin.xml.fileNames = ~/quartz_jobs.xml
 
# export this server to remoting context
quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
quartz.scheduler.exporter.port = 555
quartz.scheduler.exporter.bindName = QuartzScheduler
quartz.scheduler.exporter.channelType = tcp
quartz.scheduler.exporter.channelName = httpQuartz

四.总结:

 在项目中比较多的使用到定时任务的功能,今天的介绍的组件可以很好的完成一些定时任务的要求。这篇文章主要是作为引子,简单的介绍了组件的背景和组件的使用方式,如果项目中需要使用,可以进行更加深入的了解。

原文链接:http://www.cnblogs.com/pengze0902/p/6128558.html