一套代码小程序&Web&Native运行的探索01
前言
前面我们对微信小程序进行了研究:【微信小程序项目实践总结】30分钟从陌生到熟悉
并且用小程序翻写了之前一个demo:【组件化开发】前端进阶篇之如何编写可维护可升级的代码
之前一直在跟业务方打交道后面研究了下后端,期间还做了一些运营、管理相关工作,哈哈,最近一年工作经历十分丰富啊,生命在于不断的尝试嘛。
当然,不可避免的在前端技术一块也稍微有点落后,对React&Vue没有进行过深入一点的研究,这里得空我们便来一起研究一番(回想起来写代码的日子才是最快乐的??),因为我们现在也慢慢在切React、想尝试下React Native,但是我这边对于到底React还是Vue还是比较疑惑,所以我这里研究下,顺便看看能不能解决小程序一套代码的问题
我们现在就来尝试,是否可以用React或者Vue让一套代码能在三端同时运行
我们这里依旧使用这个我觉得还算复杂的例子,做一个首页一个列表页,后面尝试将这套代码翻译成React Native以及微信小程序,于是便开始我们这次的学习吧
PS:我这里对React&Vue熟悉度一般,文中就是demo研究,有不妥的地方请各位指正
React的开发方式
工欲善其事必先利其器,我们这里依旧先做UI组件,首先我们做一个弹出层提示组件alert,这里我们尽量尝试与小程序开发模式保持一致点
我们这里先来创建一个组件:
1 class UIAlert extends React.Component { 2 propType() { 3 //name必须有,并且必须是字符串 4 name: React.PropTypes.string.isRequired 5 } 6 render() { 7 return ( 8 <view>我是{this.props.name}</view> 9 ); 10 } 11 }; 12 13 React.render( 14 <UIAlert name="alert"/>, 15 document.getElementById(‘main‘) 16 );
//输出 我是alert
生成的HTML结构为:
1 <view data-reactid=".0"> 2 <span data-reactid=".0.0">我是</span><span data-reactid=".0.1">alert</span> 3 </view>
这里view显然不会被识别,我们简单做下处理(这里顺便升级下我们React的版本):
1 class View extends React.Component { 2 render() { 3 return ( 4 <div >{this.props.children}</div> 5 ); 6 } 7 } 8 class UIAlert extends React.Component { 9 render() { 10 return ( 11 <View>我是{this.props.name}</View> 12 ); 13 } 14 }; 15 ReactDOM.render( 16 <UIAlert name="alert" />, 17 document.getElementById(‘root‘) 18 );
于是我们生成了这个样子的代码,没有额外添加span也没有添加id标识了:
<div id="root"><div>我是alert</div></div>
我们这里依旧以一个实际的例子来说明React的各种细节,这里我们索性来做一个提示框组件吧,这里我们先实现一个遮盖层:
1 class UIMask extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = { 5 }; 6 this.onClick = this.onClick.bind(this); 7 } 8 onClick(e) { 9 if(e.target.dataset.flag !== ‘mask‘) return; 10 this.props.onMaskClick(); 11 } 12 render() { 13 return ( 14 <div onClick={this.onClick} data-flag="mask" className="cm-overlay" style={{zIndex: this.props.uiIndex, display: this.props.isShow}} > 15 {this.props.children} 16 </div> 17 ); 18 } 19 }
这里简单说下React中状态以及属性的区别(我理解下的区别):
React中的属性是只读的,原则上部允许修改自己的属性,他一般作为属性由父组件传入
state作为组件状态机而存在,表示组件处于不同的状态,所以是可变的,state是组件数据基础
这句话说的好像比较抽象,这里具体表达一下是:
① 属性可以从父组件获取,并且父组件赋值是组件的主要使用方式
② 一个组件内部不会有调用setProps类似的方法期望引起属性的变化
③ 总之属性便是组件的固有属性,我们只能像函数一样使用而不是想去改变
④ 如果你想改变一个属性的值,那么说明他该被定义为状态
⑤ 反之如果一个变量可以从父组件中获取,那么他一定不是一个状态
这里以我们这里的遮盖层组件为例说明:
遮盖层的z-index以及是否显示,对于遮盖层来说就是最小原子单元了,而且他们也是父组件通过属性的方式传入的,我们看看这里提示框的代码:
1 class UIAlert extends React.Component { 2 constructor(props) { 3 super(props); 4 this.state = { 5 isShow: ‘‘, 6 uiIndex: 3000, 7 title: ‘‘, 8 message: ‘message‘, 9 btns: [{ 10 type: ‘ok‘, 11 name: ‘确定‘ 12 }, { 13 type: ‘cancel‘, 14 name: ‘取消‘ 15 }] 16 }; 17 this.onMaskClick = this.onMaskClick.bind(this); 18 19 } 20 21 onMaskClick() { 22 this.setState({ 23 isShow: ‘none‘ 24 }); 25 } 26 27 render() { 28 29 return ( 30 <UIMask onMaskClick={this.onMaskClick} uiIndex={this.state.uiIndex} isShow={this.state.isShow}> 31 <div className="cm-modal cm-modal--alert" style={{zIndex: this.state.uiIndex + 1, display: this.state.isShow}}> 32 <div className="cm-modal-bd"> 33 <div className="cm-alert-title">{this.state.title}</div> 34 {this.state.message.length > 10 ? <div className="cm-mutil-lines">{this.state.message}</div> : <div>{this.state.message}</div>} 35 </div> 36 <div className={this.state.btns.length > 2 ? ‘cm-actions cm-actions--full‘ : ‘cm-actions ‘}> 37 { 38 this.state.btns.map(function(item) { 39 return <span data-type={item.type} className={item.type == ‘ok‘ ? ‘cm-btns-ok cm-actions-btn ‘ : ‘cm-actions-btn cm-btns-cancel‘}>{item.name}</span> 40 }) 41 } 42 </div> 43 </div> 44 </UIMask> 45 ); 46 } 47 };
为了方便调试,我们这里给提示框组件定义了很多“状态”,而这里面的状态可能有很多是“不合适”的,可以看到我们遮盖层UIMask使用的几个属性全部是这里传入的,然后我们这里想象下真实使用场景,肯定是全局有一个变量,或者按钮控制显示隐藏,所以我们这个组件进行一轮改造:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 7 <script src="./libs/react.development.js"></script> 8 <script src="./libs/react-dom.development.js"></script> 9 <script src="./libs/JSXTransformer.js"></script> 10 </head> 11 <body> 12 13 <div id="root">ddd</div> 14 15 16 <script type="text/jsx"> 17 18 19 20 class UIMask extends React.Component { 21 constructor(props) { 22 super(props); 23 this.state = { 24 }; 25 this.onClick = this.onClick.bind(this); 26 } 27 onClick(e) { 28 if(e.target.dataset.flag !== ‘mask‘) return; 29 this.props.onMaskClick(); 30 } 31 render() { 32 return ( 33 <div onClick={this.onClick} data-flag="mask" className="cm-overlay" style={{zIndex: this.props.uiIndex, display: this.props.isShow}} > 34 {this.props.children} 35 </div> 36 ); 37 } 38 } 39 40 class UIAlert extends React.Component { 41 constructor(props) { 42 super(props); 43 this.state = { 44 uiIndex: 3000 45 }; 46 this.onMaskClick = this.onMaskClick.bind(this); 47 this.onBtnClick = this.onBtnClick.bind(this); 48 49 } 50 onMaskClick() { 51 this.props.hideMessage(); 52 } 53 onBtnClick(e) { 54 let index = e.target.dataset.index; 55 this.props.btns[index].callback(); 56 } 57 render() { 58 let scope = this; 59 return ( 60 <UIMask onMaskClick={this.onMaskClick} uiIndex={this.state.uiIndex} isShow={this.props.isShow}> 61 <div className="cm-modal cm-modal--alert" style={{zIndex: this.state.uiIndex + 1, display: this.props.isShow}}> 62 <div className="cm-modal-bd"> 63 <div className="cm-alert-title">{this.props.title}</div> 64 {this.props.message.length > 10 ? <div className="cm-mutil-lines">{this.props.message}</div> : <div>{this.props.message}</div>} 65 </div> 66 <div className={this.props.btns.length > 2 ? ‘cm-actions cm-actions--full‘ : ‘cm-actions ‘}> 67 { 68 this.props.btns.map(function(item, index) { 69 return <span onClick={scope.onBtnClick} data-index={index} data-type={item.type} className={item.type == ‘ok‘ ? ‘cm-btns-ok cm-actions-btn ‘ : ‘cm-actions-btn cm-btns-cancel‘}>{item.name}</span> 70 }) 71 } 72 </div> 73 </div> 74 </UIMask> 75 ); 76 } 77 }; 78 79 80 81 82 //这里是真正的调用者,页面级别控制器 83 84 class MainPage extends React.Component { 85 constructor(props) { 86 super(props); 87 this.showMessage = this.showMessage.bind(this); 88 this.hideMessage = this.hideMessage.bind(this); 89 let scope = this; 90 91 this.state = { 92 alertShow: ‘none‘, 93 btns: [{ 94 type: ‘ok‘, 95 name: ‘确定‘, 96 callback: function() { 97 scope.hideMessage(); 98 console.log(‘成功‘); 99 } 100 }, { 101 type: ‘cancel‘, 102 name: ‘取消‘, 103 callback: function() { 104 scope.hideMessage(); 105 console.log(‘取消‘); 106 } 107 }] 108 }; 109 110 } 111 showMessage() { 112 this.setState({ 113 alertShow: ‘‘ 114 }) 115 } 116 hideMessage() { 117 this.setState({ 118 alertShow: ‘none‘ 119 }) 120 } 121 render() { 122 return ( 123 <div> 124 <input type="button" value="我是一个一般的按钮" onClick={this.showMessage} /> 125 <UIAlert 126 title="title" 127 message="点点滴滴" 128 btns={this.state.btns} 129 showMessage={this.showMessage} 130 hideMessage={this.hideMessage} 131 isShow={this.state.alertShow} 132 name="alert" 133 uiIndex="333" 134 /> 135 </div> 136 ); 137 } 138 } 139 140 141 ReactDOM.render( 142 <MainPage />, 143 document.getElementById(‘root‘) 144 ); 145 146 </script> 147 148 </body> 149 </html>
如此一来,我们这个组件便基本结束了,可以看到,事实上我们页面组件所有的状态全部汇集到了顶层组件也就是页面层级来了,在页面层级依旧需要分块处理,否则代码依旧可能会很乱
阶段总结
我们这里回顾小程序中的弹出层组件是这样写的:
1 <view class="cm-modal cm-modal--alert" style="z-index: {{uiIndex}}; display: {{isShow}}; "> 2 <view class="cm-modal-bd"> 3 <block wx:if="{{title}}"> 4 <view class="cm-alert-title">{{title}}</view> 5 </block> 6 <block wx:if="{{message.length > 20}}"> 7 <view class="cm-mutil-lines">{{message}}</view> 8 </block> 9 <block wx:else> 10 <view>{{message}}</view> 11 </block> 12 </view> 13 <view class="cm-actions {{ btns.length > 2 ? ‘cm-actions--full‘ : ‘‘ }}"> 14 <block wx:for="{{btns}}" wx:key="{{k}}"> 15 <text bindtap="onBtnEvent" data-type="{{item.type}}" class="{{item.type == ‘ok‘ ? ‘cm-btns-ok‘ : ‘cm-btns-cancel‘}} cm-actions-btn">{{item.name}}</text> 16 </block> 17 18 </view> 19 </view> 20 <view class="cm-overlay" bindtap="onMaskEvent" style="z-index: {{maskzIndex}}; display: {{isShow}}"> 21 </view>
简单研究到这里,感觉要使用React完成一套代码三端运行有点困难,小程序中的模板全部不能直接调用就是,除非是有wxs,而React支持的就是模板中写很多js,除非给React做很多的规则限制,或者由React编译为小程序识别的代码,否则暂时是不大可能的,所以我们现在研究下Vue是不是合适
Vue的开发方式
这里先不考虑mpvue框架,否则一点神秘感都没有了,我们依旧使用弹出层组件为例,用纯纯的Vue代码尝试是否做得到,首先Vue会将代码与模板做一个绑定,大概可以这样:
1 <div id="app"> 2 <p>{{ foo }}</p> 3 </div> 4 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 5 <script type="text/javascript"> 6 var obj = { 7 foo: ‘bar‘ 8 } 9 new Vue({ 10 el: ‘#app‘, 11 data: obj 12 }) 13 </script>
这块与小程序还比较类似,于是我们来完成我们的提示框组件:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 7 </head> 8 <body> 9 10 <div id="app"> 11 <input type="button" value="我是一个一般的按钮" v-on:click="showMessage" /> 12 <ui-mask v-bind:style="{zIndex: uiIndex, display: isShow}"> 13 <ui-alert :title="title" 14 :message="message" 15 v-bind:style="{zIndex: uiIndex + 1, display: isShow}" 16 :btns="btns" ></ui-alert> 17 </ui-mask> 18 </div> 19 20 21 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 22 <script type="text/javascript"> 23 24 Vue.component(‘ui-mask‘, { 25 methods:{ 26 onClick: function (e) { 27 if(e.target.dataset.flag !== ‘mask‘) return; 28 this.$parent.hideMessage(); 29 } 30 }, 31 template: ` 32 <div v-on:click="onClick" data-flag="mask" class="cm-overlay" > 33 <slot></slot> 34 </div> 35 ` 36 }); 37 38 Vue.component(‘ui-alert‘, { 39 props: { 40 title: { 41 type: String 42 }, 43 message: { 44 type: String 45 }, 46 uiIndex: { 47 type: String 48 }, 49 isShow: { 50 type: String 51 }, 52 btns: Array 53 }, 54 methods: { 55 onBtnEvent: function (e) { 56 let index = e.target.dataset.index; 57 this.btns[index].callback.call(this.$parent.$parent); 58 } 59 }, 60 template: ` 61 <div class="cm-modal cm-modal--alert"> 62 <div class="cm-modal-bd"> 63 <template v-if="title"> 64 <div class="cm-alert-title">{{title}}</div> 65 </template> 66 <template v-if="message.length > 20"> 67 <div class="cm-mutil-lines">{{message}}</div> 68 </template> 69 <template v-else> 70 <div>{{message}}</div> 71 </template> 72 </div> 73 <div class="cm-actions" v-bind:class="[btns.length > 2 ? ‘cm-actions--full‘ : ‘‘]"> 74 <template v-for="(item, index) in btns"> 75 <span v-on:click="onBtnEvent" :data-index="index" class="cm-actions-btn" v-bind:class="[item.type == ‘ok‘ ? ‘cm-btns-ok‘ : ‘cm-btns-cancel‘]">{{item.name}}</span> 76 </template> 77 </div> 78 </div> 79 ` 80 }) 81 new Vue({ 82 methods: { 83 showMessage: function() { 84 this.isShow = ‘‘; 85 }, 86 hideMessage: function() { 87 this.isShow = ‘none‘; 88 } 89 }, 90 data: function() { 91 return { 92 title: ‘title1‘, 93 message: ‘message1‘, 94 uiIndex: 3000, 95 isShow: ‘none‘, 96 btns: [{ 97 type: ‘ok‘, 98 name: ‘确定‘, 99 callback: function() { 100 this.hideMessage(); 101 console.log(‘成功‘); 102 } 103 }, { 104 type: ‘cancel‘, 105 name: ‘取消‘, 106 callback: function() { 107 this.hideMessage(); 108 console.log(‘取消‘); 109 } 110 }] 111 }; 112 } , 113 el: ‘#app‘ 114 }) 115 116 </script> 117 </body> 118 </html>
从相似度上来说比React高得多,但是仍然有很多区别:
① class&style一块的处理
② 属性的传递
③ setData....
这些等我们后续代码写完看看如何处理之,能不能达到一套代码三端运行,这里做首页的简单实现。
首页逻辑的实现
这里简单的组织了下目录结构,做了最简单的实现,这里大家注意我这里对Vue不是很熟悉,不会遵循Vue的最佳实践,我这里是在探索能不能借助Vue让一套代码三端运行,所以这里写法会尽量靠近小程序写法:
1 <html lang="en"> 2 <head> 3 <meta charset="UTF-8"> 4 <title>Title</title> 5 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 6 <link href="./static/css/index.css" rel="stylesheet" type="text/css"/> 7 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 8 </head> 9 <body> 10 <div id="app"></div> 11 <script type="module"> 12 import index from ‘./pages/index/index.js‘ 13 Vue.component(index.name, index.data); 14 new Vue({ 15 data: function() { 16 return { 17 }; 18 } , 19 template: `<page-index></page-index>`, 20 el: ‘#app‘ 21 }) 22 </script> 23 </body> 24 </html>
1 import html from ‘./index.html.js‘ 2 3 export default { 4 name: ‘page-index‘, 5 data: { 6 template: html, 7 methods: { 8 showCitylist: function(e) { 9 console.log(‘showCitylist‘) 10 }, 11 showCalendar: function(e) { 12 console.log(‘showCalendar‘) 13 }, 14 goList: function(e) { 15 console.log(‘goList‘) 16 } 17 }, 18 data: function() { 19 return { 20 cityStartName: ‘请选择出发地‘, 21 cityArriveName: ‘请选择到达地‘, 22 calendarSelectedDateStr: ‘请选择出发日期‘} 23 } 24 } 25 }
1 export default 2 `<div class="container"> 3 <div class="c-row search-line" data-flag="start" @click="showCitylist"> 4 <div class="c-span3"> 5 出发</div> 6 <div class="c-span9 js-start search-line-txt"> 7 {{cityStartName}}</div> 8 </div> 9 <div class="c-row search-line" data-flag="arrive" @click="showCitylist"> 10 <div class="c-span3"> 11 到达</div> 12 <div class="c-span9 js-arrive search-line-txt"> 13 {{cityArriveName}}</div> 14 </div> 15 <div class="c-row search-line" data-flag="arrive" @click="showCalendar"> 16 <div class="c-span3"> 17 出发日期</div> 18 <div class="c-span9 js-arrive search-line-txt"> 19 {{calendarSelectedDateStr}}</div> 20 </div> 21 <div class="c-row " data-flag="arrive"> 22 <span class="btn-primary full-width js_search_list" @click="goList" >查询</span> 23 </div> 24 </div> 25 `
如此我们首页页面框架就出来了,后续只需要完成对应的几个组件,如日历组件以及城市列表,这里为了更完整的体验,我们先完成日历组件
日历组件
之前我们做日历组件的时候做的比较复杂,这里可以看到小程序中里面的模板代码:
1 <wxs module="dateUtil"> 2 var isDate = function(date) { 3 return date && date.getMonth; 4 }; 5 6 var isLeapYear = function(year) { 7 //传入为时间格式需要处理 8 if (isDate(year)) year = year.getFullYear() 9 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 10 return false; 11 }; 12 13 var getDaysOfMonth = function(date) { 14 var month = date.getMonth(); //注意此处月份要加1,所以我们要减一 15 var year = date.getFullYear(); 16 return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]; 17 } 18 19 var getBeginDayOfMouth = function(date) { 20 var month = date.getMonth(); 21 var year = date.getFullYear(); 22 var d = getDate(year, month, 1); 23 return d.getDay(); 24 } 25 26 var getDisplayInfo = function(date) { 27 28 if (!isDate(date)) { 29 date = getDate(date) 30 } 31 var year = date.getFullYear(); 32 33 var month = date.getMonth(); 34 var d = getDate(year, month); 35 36 37 //这个月一共多少天 38 var days = getDaysOfMonth(d); 39 40 //这个月是星期几开始的 41 var beginWeek = getBeginDayOfMouth(d); 42 43 return { 44 year: year, 45 month: month, 46 days: days, 47 beginWeek: beginWeek 48 } 49 }; 50 51 //分割月之间的展示 52 var monthClapFn = function(year, month) { 53 month = month + 1; 54 return year + ‘年‘ + (month) + ‘月‘; 55 }; 56 57 var getChangedDate = function(date, m) { 58 59 if (!isDate(date)) { 60 date = getDate(date) 61 } 62 var year = date.getFullYear(); 63 64 var month = date.getMonth(); 65 var changedMonth = month + m; 66 var yyy = parseInt((month + m) / 12); 67 if (changedMonth > 11) { 68 changedMonth = changedMonth - 12 * yyy; 69 } 70 changedYear = year + yyy; 71 72 return { 73 str_month: monthClapFn(changedYear, changedMonth) 74 date: getDate(changedYear, changedMonth), 75 year: changedYear, 76 month: changedMonth 77 }; 78 }; 79 80 var isSelected = function(date, year, month, day) { 81 if (!isDate(date)) { 82 date = getDate(date); 83 } 84 85 if (date.getFullYear() == year && date.getMonth() == month && date.getDate() == day) return ‘active‘; 86 return ‘‘; 87 88 }; 89 90 var formatNum = function(n) { 91 if (n < 10) return ‘0‘ + n; 92 return n; 93 }; 94 95 var getDayName = function(dayMap, month, day) { 96 97 if (!dayMap) { 98 dayMap = { 99 ‘0101‘: ‘元旦节‘, 100 ‘0214‘: ‘情人节‘, 101 ‘0501‘: ‘劳动节‘, 102 ‘0601‘: ‘儿童节‘, 103 ‘0910‘: ‘教师节‘, 104 ‘1001‘: ‘国庆节‘, 105 ‘1225‘: ‘圣诞节‘ 106 }; 107 } 108 109 var name = formatNum(parseInt(month) + 1) + formatNum(day); 110 111 return dayMap[name] || day; 112 }; 113 114 module.exports = { 115 test: function (zzz) { 116 console.log(‘test‘, zzz) 117 }, 118 getDipalyInfo: getDisplayInfo, 119 getChangedDate: getChangedDate, 120 isSelected: isSelected, 121 getDayName: getDayName 122 } 123 </wxs> 124 <view class="cm-calendar" style="display: {{isShow}};"> 125 <view class="cm-calendar-hd "> 126 <block wx:for="{{weekDayArr}}" wx:key="weekDayKey"> 127 <view class="item">{{item}}</view> 128 </block>i 129 </view> 130 131 <block wx:for="{{displayMonthNum}}" wx:for-index="i" wx:key="t"> 132 133 <view class="cm-calendar-bd "> 134 <view class="cm-month ex-class"> 135 {{dateUtil.getChangedDate(displayTime, i).str_month }} 136 </view> 137 <view class="cm-day-list"> 138 139 <block wx:key="tt" wx:for="{{dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).days + dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).beginWeek}}" wx:for-index="index"> 140 141 <view wx:if="{{index < dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).beginWeek }}" class="item "></view> 142 <view bindtap="onDayTap" wx:else data-year="{{dateUtil.getChangedDate(displayTime, i).year}}" data-month="{{dateUtil.getChangedDate(displayTime, i).month}}" data-day="{{index + 1 - dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).beginWeek}}" class="item {{dateUtil.isSelected(selectedDate, dateUtil.getChangedDate(displayTime, i).year, dateUtil.getChangedDate(displayTime, i).month, index + 1 - dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).beginWeek)}}"> 143 <view class="cm-field-title"> 144 {{dateUtil.getDayName(dayMap, dateUtil.getChangedDate(displayTime, i).month, index + 1 - dateUtil.getDipalyInfo(dateUtil.getChangedDate(displayTime, i).date).beginWeek) }} {{}} 145 </view> 146 </view> 147 148 </block> 149 150 <view class=" cm-item--disabled " data-cndate="" data-date=""> 151 </view> 152 153 </view> 154 </view> 155 156 </block> 157 158 </view>
我思考了下,应该还是尽量少在模板里面调用js方法,所以我们这里将粒度再拆细一点,实现一个单日的表格组件,所以我们这里的日历由两部分组成:
① 日历-月组件
② 日历-天组件,被月所包裹
所以我们这里先来实现一个天的组件,对于日历来说,他会拥有以下特性:
① 是否过期,这个会影响显示效果
② 是否特殊节日,如中秋等,也会影响显示
③ 点击事件响应......
④ 是不是今天
好像也没多少东西...
这里先简单些个demo将日历单元格展示出来:
<ui-calendar year="2018" month="8" day="8" ></ui-calendar>
1 import data from ‘./ui-calendar-day.js‘ 2 3 Vue.component(data.name, data.data); 4 5 export default { 6 name: ‘ui-calendar‘, 7 data: { 8 methods: { 9 }, 10 data: function() { 11 return { 12 } 13 }, 14 template: 15 ` 16 <ul> 17 <template v-for="num in 30" > 18 <ui-calendar-day year="2018" month="8" v-bind:day="num" ></ui-calendar-day> 19 </template> 20 </ul> 21 ` 22 } 23 }
1 //公共方法抽离,输入年月日判断在今天前还是今天后 2 function isOverdue(year, month, day) { 3 let date = new Date(year, month, day); 4 let now = new Date(); 5 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 6 return date.getTime() < now.getTime(); 7 } 8 9 function isToday(year, month, day) { 10 let date = new Date(year, month, day); 11 let now = new Date(); 12 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 13 return date.getTime() === now.getTime(); 14 } 15 16 export default { 17 name: ‘ui-calendar-day‘, 18 data: { 19 props: { 20 year: { 21 type: String 22 }, 23 month: { 24 type: String 25 }, 26 day: { 27 type: Number 28 } 29 }, 30 methods: { 31 }, 32 data: function() { 33 //是否过期了 34 let klass = isOverdue(this.year, this.month, this.day) ? ‘cm-item--disabled‘ : ‘‘; 35 if(isToday(this.year, this.month, this.day)) klass += ‘active‘ 36 37 return { 38 klass: klass 39 } 40 }, 41 template: 42 ` 43 <li v-bind:class="klass" v-bind:data-year="year" v-bind:data-month="month" v-bind:data-day="day"> 44 {{day}} 45 </li> 46 ` 47 } 48 }
形成的html结构如下:
1 <ul year="2018" month="8" day="8"><li data-year="2018" data-month="8" data-day="1" class="cm-item--disabled"> 2 1 3 </li><li data-year="2018" data-month="8" data-day="2" class="cm-item--disabled"> 4 2 5 </li><li data-year="2018" data-month="8" data-day="3" class="cm-item--disabled"> 6 3 7 </li><li data-year="2018" data-month="8" data-day="4" class="cm-item--disabled"> 8 4 9 </li><li data-year="2018" data-month="8" data-day="5" class="cm-item--disabled"> 10 5 11 </li><li data-year="2018" data-month="8" data-day="6" class="cm-item--disabled"> 12 6 13 </li><li data-year="2018" data-month="8" data-day="7" class="cm-item--disabled"> 14 7 15 </li><li data-year="2018" data-month="8" data-day="8" class="active"> 16 8 17 </li><li data-year="2018" data-month="8" data-day="9" class=""> 18 9 19 </li><li data-year="2018" data-month="8" data-day="10" class=""> 20 10 21 </li><li data-year="2018" data-month="8" data-day="11" class=""> 22 11 23 </li><li data-year="2018" data-month="8" data-day="12" class=""> 24 12 25 </li><li data-year="2018" data-month="8" data-day="13" class=""> 26 13 27 </li><li data-year="2018" data-month="8" data-day="14" class=""> 28 14 29 </li><li data-year="2018" data-month="8" data-day="15" class=""> 30 15 31 </li><li data-year="2018" data-month="8" data-day="16" class=""> 32 16 33 </li><li data-year="2018" data-month="8" data-day="17" class=""> 34 17 35 </li><li data-year="2018" data-month="8" data-day="18" class=""> 36 18 37 </li><li data-year="2018" data-month="8" data-day="19" class=""> 38 19 39 </li><li data-year="2018" data-month="8" data-day="20" class=""> 40 20 41 </li><li data-year="2018" data-month="8" data-day="21" class=""> 42 21 43 </li><li data-year="2018" data-month="8" data-day="22" class=""> 44 22 45 </li><li data-year="2018" data-month="8" data-day="23" class=""> 46 23 47 </li><li data-year="2018" data-month="8" data-day="24" class=""> 48 24 49 </li><li data-year="2018" data-month="8" data-day="25" class=""> 50 25 51 </li><li data-year="2018" data-month="8" data-day="26" class=""> 52 26 53 </li><li data-year="2018" data-month="8" data-day="27" class=""> 54 27 55 </li><li data-year="2018" data-month="8" data-day="28" class=""> 56 28 57 </li><li data-year="2018" data-month="8" data-day="29" class=""> 58 29 59 </li><li data-year="2018" data-month="8" data-day="30" class=""> 60 30 61 </li></ul>
简单来说,关于天的处理似乎处理完成,我们现在来开始月的处理,于是日历雏形就已经出来了:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 7 <link href="./static/css/index.css" rel="stylesheet" type="text/css"/> 8 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 9 </head> 10 <body> 11 <div id="app"></div> 12 <script type="module"> 13 import index from ‘./pages/index/index.js‘ 14 import data from ‘./components/ui-calendar.js‘ 15 Vue.component(index.name, index.data); 16 Vue.component(data.name, data.data); 17 new Vue({ 18 data: function() { 19 return { 20 }; 21 } , 22 // template: `<page-index></page-index>`, 23 template: `<ui-calendar year="2018" month="8" day="8" ></ui-calendar>`, 24 el: ‘#app‘ 25 }) 26 </script> 27 </body> 28 </html>
1 import data from ‘./ui-calendar-month.js‘ 2 3 Vue.component(data.name, data.data); 4 5 export default { 6 name: ‘ui-calendar‘, 7 data: { 8 methods: { 9 }, 10 data: function() { 11 return { 12 weekDayArr: [‘日‘, ‘一‘, ‘二‘, ‘三‘, ‘四‘, ‘五‘, ‘六‘] 13 } 14 }, 15 template: 16 ` 17 <ul class="cm-calendar "> 18 <ul class="cm-calendar-hd"> 19 <template v-for="i in 7" > 20 <li class="cm-item--disabled">{{weekDayArr[i-1]}}</li> 21 </template> 22 </ul> 23 <ui-calendar-month year="2018" month="8" ></ui-calendar-month> 24 </ul> 25 ` 26 } 27 }
1 import data from ‘./ui-calendar-day.js‘ 2 3 Vue.component(data.name, data.data); 4 5 //公共方法抽离,输入年月日判断在今天前还是今天后 6 function isLeapYear(year) { 7 if ((typeof year == ‘object‘) && (year instanceof Date)) year = year.getFullYear() 8 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 9 return false; 10 } 11 12 // @description 获取一个月份1号是星期几,注意此时保留开发习惯,月份传入时需要自主减一 13 // @param year {num} 可能是年份或者为一个date时间 14 // @param year {num} 月份 15 // @return {num} 当月一号为星期几0-6 16 function getBeginDayOfMouth(year, month) { 17 //自动减一以便操作 18 month--; 19 if ((typeof year == ‘object‘) && (year instanceof Date)) { 20 month = year.getMonth(); 21 year = year.getFullYear(); 22 } 23 var d = new Date(year, month, 1); 24 return d.getDay(); 25 } 26 27 export default { 28 name: ‘ui-calendar-month‘, 29 data: { 30 props: { 31 year: { 32 type: String 33 }, 34 month: { 35 type: String 36 } 37 }, 38 methods: { 39 }, 40 data: function() { 41 let days = [31, isLeapYear(this.year) ? 29 : 28, 42 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 43 //本月从哪天开始 44 let beforeDays = getBeginDayOfMouth(this.year, parseInt(this.month) + 1); 45 46 return { 47 days: days[this.month], 48 beforeDays: beforeDays 49 } 50 }, 51 template: 52 ` 53 <ul class="cm-calendar-bd "> 54 <h3 class="cm-month js_month">{{year + ‘-‘ + month}}</h3> 55 <ul class="cm-day-list"> 56 <template v-for="n in beforeDays" > 57 <li class="cm-item--disabled"></li> 58 </template> 59 <template v-for="num in days" > 60 <ui-calendar-day :year="year" :month="month" v-bind:day="num" ></ui-calendar-day> 61 </template> 62 </ul> 63 </ul> 64 ` 65 } 66 }
1 //公共方法抽离,输入年月日判断在今天前还是今天后 2 function isOverdue(year, month, day) { 3 let date = new Date(year, month, day); 4 let now = new Date(); 5 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 6 return date.getTime() < now.getTime(); 7 } 8 9 function isToday(year, month, day) { 10 let date = new Date(year, month, day); 11 let now = new Date(); 12 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 13 return date.getTime() === now.getTime(); 14 } 15 16 export default { 17 name: ‘ui-calendar-day‘, 18 data: { 19 props: { 20 year: { 21 type: String 22 }, 23 month: { 24 type: String 25 }, 26 day: { 27 type: Number 28 } 29 }, 30 methods: { 31 }, 32 data: function() { 33 //是否过期了 34 let klass = isOverdue(this.year, this.month, this.day) ? ‘cm-item--disabled‘ : ‘‘; 35 if(isToday(this.year, this.month, this.day)) klass += ‘active‘ 36 return { 37 klass: klass 38 } 39 }, 40 template: 41 ` 42 <li v-bind:class="klass" v-bind:data-year="year" v-bind:data-month="month" v-bind:data-day="day"> 43 <div class="cm-field-wrapper "><div class="cm-field-title">{{day}}</div></div> 44 </li> 45 ` 46 } 47 }
这样分解下来,似乎代码变得更加简单了,接下来我们花点功夫完善这个组件,最后形成了这样的代码:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>Title</title> 6 <link href="./static/css/global.css" rel="stylesheet" type="text/css"/> 7 <link href="./static/css/index.css" rel="stylesheet" type="text/css"/> 8 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 9 </head> 10 <body> 11 <div id="app"></div> 12 <script type="module"> 13 // console.error = function () { 14 // } 15 import index from ‘./pages/index/index.js‘ 16 import data from ‘./components/ui-calendar.js‘ 17 Vue.component(index.name, index.data); 18 Vue.component(data.name, data.data); 19 new Vue({ 20 methods: { 21 onDayClick: function (data) { 22 this.selectedDate = new Date(data.year, data.month, data.day).getTime() + ‘‘; 23 // debugger; 24 } 25 }, 26 data: function() { 27 28 29 return { 30 displayTime: new Date().getTime(), 31 selectedDate: new Date(2018, 8, 13).getTime(), 32 displayMonthNum: 2 33 } 34 }, 35 // template: `<page-index></page-index>`, 36 template: ` 37 <ui-calendar v-on:dayclick="onDayClick" :selectedDate="selectedDate" :displayMonthNum="displayMonthNum" :displayTime="displayTime" ></ui-calendar> 38 `, 39 el: ‘#app‘ 40 }) 41 </script> 42 </body> 43 </html>
1 import data from ‘./ui-calendar-month.js‘ 2 3 Vue.component(data.name, data.data); 4 5 export default { 6 name: ‘ui-calendar‘, 7 data: { 8 props: { 9 displayMonthNum: { 10 type: Number 11 }, 12 displayTime: { 13 type: Number 14 }, 15 selectedDate: { 16 type: Number 17 } 18 }, 19 methods: { 20 }, 21 data: function() { 22 //要求传入的当前显示时间必须是时间戳 23 let date = new Date(this.displayTime); 24 25 return { 26 year: date.getFullYear(), 27 month: date.getMonth(), 28 weekDayArr: [‘日‘, ‘一‘, ‘二‘, ‘三‘, ‘四‘, ‘五‘, ‘六‘] 29 } 30 }, 31 template: 32 ` 33 <ul class="cm-calendar "> 34 <ul class="cm-calendar-hd"> 35 <template v-for="i in 7" > 36 <li class="cm-item--disabled">{{weekDayArr[i-1]}}</li> 37 </template> 38 </ul> 39 <template v-for="m in displayMonthNum" > 40 <ui-calendar-month :selectedDate="selectedDate" :year="year" :month="month+m-1" ></ui-calendar-month> 41 </template> 42 </ul> 43 ` 44 } 45 }
1 import data from ‘./ui-calendar-day.js‘ 2 3 Vue.component(data.name, data.data); 4 5 //公共方法抽离,输入年月日判断在今天前还是今天后 6 function isLeapYear(year) { 7 if ((typeof year == ‘object‘) && (year instanceof Date)) year = year.getFullYear() 8 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 9 return false; 10 } 11 12 // @description 获取一个月份1号是星期几,注意此时保留开发习惯,月份传入时需要自主减一 13 // @param year {num} 可能是年份或者为一个date时间 14 // @param year {num} 月份 15 // @return {num} 当月一号为星期几0-6 16 function getBeginDayOfMouth(year, month) { 17 //自动减一以便操作 18 month--; 19 if ((typeof year == ‘object‘) && (year instanceof Date)) { 20 month = year.getMonth(); 21 year = year.getFullYear(); 22 } 23 var d = new Date(year, month, 1); 24 return d.getDay(); 25 } 26 27 export default { 28 name: ‘ui-calendar-month‘, 29 data: { 30 props: { 31 year: { 32 type: Number 33 }, 34 month: { 35 type: Number 36 }, 37 selectedDate: { 38 type: Number 39 } 40 }, 41 methods: { 42 43 }, 44 data: function() { 45 let days = [31, isLeapYear(this.year) ? 29 : 28, 46 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 47 //本月从哪天开始 48 let beforeDays = getBeginDayOfMouth(this.year, parseInt(this.month) + 1); 49 50 return { 51 days: days[this.month], 52 beforeDays: beforeDays 53 } 54 }, 55 template: 56 ` 57 <ul class="cm-calendar-bd "> 58 <h3 class="cm-month js_month">{{year + ‘-‘ + month}}</h3> 59 <ul class="cm-day-list"> 60 <template v-for="n in beforeDays" > 61 <li class="cm-item--disabled"></li> 62 </template> 63 <template v-for="num in days" > 64 <ui-calendar-day :selectedDate="selectedDate" :year="year" :month="month" v-bind:day="num" ></ui-calendar-day> 65 </template> 66 </ul> 67 </ul> 68 ` 69 } 70 }
1 //公共方法抽离,输入年月日判断在今天前还是今天后 2 function isOverdue(year, month, day) { 3 let date = new Date(year, month, day); 4 let now = new Date(); 5 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 6 return date.getTime() < now.getTime(); 7 } 8 9 function isToday(year, month, day, selectedDate) { 10 let date = new Date(year, month, day); 11 return date.getTime() == selectedDate; 12 } 13 14 export default { 15 name: ‘ui-calendar-day‘, 16 data: { 17 props: { 18 year: { 19 type: Number 20 }, 21 month: { 22 type: Number 23 }, 24 day: { 25 type: Number 26 }, 27 selectedDate: { 28 type: Number 29 } 30 }, 31 methods: { 32 onDayClick: function (e) { 33 let data = e.currentTarget.dataset; 34 this.$parent.$parent.$emit(‘dayclick‘, data); 35 } 36 }, 37 //引入计算属性概念 38 computed: { 39 // 计算属性的 getter 40 klass: function () { 41 //是否过期了 42 let klass = isOverdue(this.year, this.month, this.day) ? ‘cm-item--disabled‘ : ‘‘; 43 44 if(isToday(this.year, this.month, this.day, this.selectedDate)) klass += ‘active‘ 45 return klass; 46 } 47 }, 48 data: function() { 49 return {} 50 }, 51 template: 52 ` 53 <li :selectedDate="selectedDate" @click="onDayClick" :class="klass" v-bind:data-year="year" v-bind:data-month="month" v-bind:data-day="day"> 54 <div class="cm-field-wrapper "><div class="cm-field-title">{{day}}</div></div> 55 </li> 56 ` 57 } 58 }
至此虽然,我们这块代码简陋,却完成了一个简单日历组件,至此我们第一阶段的探索结束,明天继续研究