我是一个前端小白,开始了解前端不过几个月的时间,最近在参加360的前端星计划。此文主要整理了我做任务的过程中走过的路、思考的问题,如果你和我一样刚接触前端,发现网上大多只是干枯枯的代码的话,那这篇文章对你可能有帮助。
用时32小时35分钟,我并未实现我最初的美好设想:「可以不只实现题目要求,还可以做一下Google日历那样具有拖拽效果的日程管理功能」,只完成了最简单的功能,不过我踩过的坑,恰好是初学者可以参考的注意点。但我有的看法和思路不是很符合规范,仅仅作为参考即可。也希望有大神不吝赐教、批评指正,我虚心改正。
因为想借此巩固基础知识,所以专注点放在了细节和基础上,我所有的代码都是手写(打)的,用原生JavaScript,有的地方可能略微化简为繁,但有些细节深究起来是有意义的,文末我会整理一下所有的参考资料,我写的源代码见JS_Calendar。强烈求 Star ♪(∇*)
下面以时间线来写写,我的解决思路、解决过程、学习收获。
1. 需求分析
尽管题目(用js实现万年历)初看起来并不复杂,但还是先进行需求分析,也就是明确一下,我的日历要实现的功能点,以便后期有序的行动。
我的思考结果如下:
- 选择年、月,显示此年此月日历(列表选择、上下按钮)
- 选择日期,显示当日(选择日)信息:
- year-month-day
- 星期
- 农历年月日
- 星座
- 历史上的今天
- 返回到今天
- 显示当前时间(实时变化)
- 日历中显示主要假期
- 可选择的假期列表,并在日历定位假期区间
得到这些基础功能点之后,开始看网上的日历,寻找是否有没注意到但很重要的功能点,同时也研究UI为下一步作储备,后期遇到问题时都可以研究一下他们是怎么处理的(如写HTML页面时会疑惑要用table还是list实现……)
下面列举了可供参考的日历:
- 历史上的今天
- 360万年历
- Bing引用的日历-有趣网
- 农历网日历
-
便民查询网日历
还有一些需要注册才能使用的日历 - 365日历
- 滴答清单高级账户可用日历
- Google日历
补充完善之后,在此基础上进行下一步。
2.原型设计
存在这样的结构:HTML → CSS → JavaScript
分别是内容、样式、行为三层的结构,所以我第一步先确定内容。其实就是根据上一步得到的需求,提炼出HTML页面里需要的内容,同时考虑到用div分块,于是提取内容如下:
- Navigate
- 北京时间
- 年月选项
- 回到今天
- 今年假期
- Calendar
- 日期排列
- 农历日期节气
- 国际节假日标识
- Today
- 日期:YYYY-MM-DD
- 星期
- 日期:DAY
- 农历日期:生肖-月-初几
- 天干地支日期:年-月-日
- 星座
- 宜&忌
- 历史上的今天
这样一来,大概有了HTML的雏形(nav、calendar、today三个部分),就开始考虑如何布局了。我画了一个自己想的,当然因为前期看过了不少的例子,所以看起来都是大同小异。但重点在于,自己画的过程,也能帮助自己想清楚每个部分是怎么回事。
然后就开工了。我列了一下要做的事,有的是后来逐步添加进去的,作为一个整体概览,提醒自己下一步要做什么。
3.实现细节
代码历史版本可以到Github去看,我基本是每天commit一次,和我初期的思路一样,总体按照如下流程来完成:
- .html初版,涵盖前面所述内容
- CSS布局,实现想要的效果
- JavaScript,这个阶段会很多次返回去修改HTML和CSS
下面就以我遇到的问题为引线来说说写日历的编程细节。
如何让散乱的div去到该去的位置?
这事应该交由CSS来管,虽然我知道一些基础的语法,但用起来还是不能随心所欲,于是Google CSS+布局的第一个链接就拯救了我。学习CSS布局是一个关于CSS基础的网站,利于初学者理解学习的地方在于,网站把讲解和演示融为了一体,可以在阅读的过程中,随时通过放大缩小浏览器显示比例来感受不同的效果。
看完一遍之后,就开始对我的页面进行调整尝试,最后得到了这样的布局样式:
尝试的过程就不细说了,就是很朴素的方法:反复试。
HTML里面DOM树如下:
主要(不是全部)起作用的代码如下:
#calendar_s {
width:80%;
height:100%;
color:#e25c38;
margin:auto;
}
nav {
position:relative;
margin-left:0;
top:0;
height:20%;
background-color:#86a9c0;
}
#calendar {
float:left;
width:70%;
height:80%;
top:0;
bottom:0;
left:0;
background-color:#E9967A;
}
#todayInfo {
float:right;
width:30%;
height:80%;
top:0;
background-color:#FAEBD7;
}
思路则是使nav区域吸附到顶部(top:0)并保持高度(height:20%),余下的calendar和todayInfo两块分别左右浮动,占满剩下的区域。从3D视图看过去大概是这个模样:
日历中日期部分用什么?table还是list?
这是我在写日历主体部分时候的疑惑,两种方式各有各的特点,现有的日历既有table的也有list的,到现在我仍然觉得两种都可以实现想要的效果,只是相应的CSS侧重点会不一样。受到上个问题中CSS布局网站的影响,我希望不采用绝对定位和宽度,在不同大小的浏览器中可以很好的显示,而table可以保证行高相同,同时用两层for循环生成table容易理解,所以我采用了table。
在HTML里写出主体的table结构以及表头thead:
<div id="calendar">
<table id="dateZone">
<thead>
<tr>
<td>日</td>
<td>一</td>
<td>二</td>
<td>三</td>
<td>四</td>
<td>五</td>
<td>六</td>
</tr>
</thead>
<tbody id="dateTable">
</tbody>
</table>
</div>
完成HTML之后,就需要JavaScript上场了。但我并不是很熟悉DOM可以如何操作,于是找了课程Coursera《HTML、CSS 和 JavaScript》中DOM部分来学习,然后再自己写一遍像下面这样的代码,就可以生成想要的HTML了。
//来源:Coursera课程
//实验:DOM操作Node
function insert_new_text(){
var newText = document.createTextNode("this is an added text");// 新建文本元素
var textPart = document.getElementById("jfksdj"); //获取节点
textPart.appendChild(newText); //使newText成为textPart的子节点
}
function insert_new_node(){
var newItem = document.createElement("td"); //新建元素节点
var destParent = document.getElementsByTagName("body")[0]; //通过tag获取标签
destParent.insertBefore(newItem, destParent.firstChild); //在destParent的第一个子节点之前插入newItem
}
下面就是我用JavaScript生成tbody部分的代码:
function generateTable() {
for (var i = 0; i < 6; i++){
var newRow = document.createElement("tr");
for(var j = 0; j < 7; j++){
var newDate = document.createElement("td");
var date;//获取日期信息
newDate.innerText = date;
newRow.appendChild(newDate);
}
dateTable.appendChild(newRow);
}
}
表格可以生成了,但这只能生成一个外壳,真正的日历内容还没有解决,那么现在的问题就变成了:
如何知道每月日历的所有日期?
首先去查了一下原生JavaScript所提供给我们的关于时间的对象Date,参考MDN,可以发现JavaScript能读取浏览器本地的当前时间数据,也就是我们可以获得“今天”的年、月、日、星期,那么根据这些信息能否推算出6*7表格中每个位置的日期值呢?
Google了很多,当然不是要去抄代码,而且与其去看没有解释的复杂代码,还不如自己思考。最后我在一个博客里有了灵感,博主写了一篇叫做How to build simple calendar with JavaScript的文章。他在文章开头说:
While there are lots of JavaScript-based calendar widgets out there, there's not much in the way of explaining how they work for the JS acolyte. I recently had the opportunity of building one from memory (and best of all, for no particular reason), using none of the popular JS libraries. This is the tutorial I wish I had found five years ago.
不得不说因为这个博客才有了你正在看的这篇文章,其中主要是这段话激励了我。
回到日历的生成上来,我的灵感是怎么来的呢?上面这篇文章中详细讲了生成当前这个月的日历的实现过程,文末博主说他这周会继续教大家做更高级的日历,但有趣的是,他在4个月之后才更新了博客(文章链接),说自己“好久没更新了”……然后附上网友对他上一篇文章的建议,就是这些建议,让我知道了我该怎么做。
Michiel van der Blonk suggested a way to determine the number of days in a month:
var d = new Date(nYear, nMonth + 1, 0); // nMonth is 0 thru 11return(d.getDate());
The mechanism here is the Date constructor will attempt to convert year, month and day values to the nearest valid date. So in the above code, nMonth is incremented to find the following month, and then a Date object is created with zero days. Since this is an invalid date, the Date object defaults to the last valid day of the previous month — which is the month we're interested in to begin with. Then getDate() returns the day (1-31) of that date — voilá, we have the number of days.
Bonus: it also seems to automatically correct for leap year. It feels like a hack but it seems to work consistently.
这里提到了一件事,Date这个对象会返回离参数最近的有效日期,利用这点就可以处理闰年、跨月份的日期问题。回去再看MDN文档,的确如此:
Note: Date需要调用多个参数的构造函数,当数值大于合理范围时(如月份为13或者分钟数为70),会被调整为相邻值。比如 new Date(2013, 13, 1)会等于 new Date(2014, 1, 1),还会新建一个2014-02-01的日期(注意月份是从0开始的)。
所以我想到的方法是这样:获取此年此月1号是周几(getDay()
),然后用1减去这个值,赋给Date后就能得到所需的日历开始日期,但如果1号是周日的话,那就不需要减了,画了一下流程图:
但在后来的过程中我发现,如果第一行周日是1号的话,那么周日对应的getDay()值是0,这时候就不需要判断了。最后写出来的代码是这个样子:
var month = currentDate.getMonth();
var year = currentDate.getFullYear();
var thisMonthDay = new Date(year, month, 1);
var thisMonthFirstDay = thisMonthDay.getDay();
var thisMonthFirstDate = new Date(year, month, - thisMonthFirstDay);
generateTable(thisMonthFirstDate); //生成日历主体的日期区域
generateTable(firstDate)中for循环内部增加的部分:
//获取日期信息
firstDate.setDate(++date);
date = firstDate.getDate();
这里要提的一点是,我把上面改成了new Date(year, month, - thisMonthFirstDay)
,是因为我调试的过程中,当我写成firstDate.setDate(date++);
的时候,第一个值会出现问题,同时为了代码更简洁(其实是为了我好理解),就改成了- thisMonthFirstDay与++date的搭配,这样一来,可以免去++未执行的可能。(写到这里才发现我可能对这里的理解不够,当时试着改了一下就成功了,没有细想,竟然忘记了具体的问题是什么了)
至此,已经可以生成一个6*7table的日历,你可以想到我们很容易通过改参数来生成不同月份的日历,也就是说“上一月”这样的功能,可以在按钮上添加事件监听,然后只需要设定调用函数时使用的参数就可以了。
为了避免这篇文章太长(我没想到写了这么多还没写完……),后面的部分我用另一篇文章来说(如果有必要的话)。