当代前端应该怎么写这个hello world?

前言

大概16年的时候咱们队react进行了简单的学习:从DOM操做看Vue&React的前端组件化,顺带补齐React的demo,当时咱们只是站在框架角度在学习,随着近几年前端的变化,想写个hello world彷佛变得复杂起来,咱们今天便一块儿来看看现代化的前端,应该如何作一个页面,今天咱们学习react首先说一下React的体系圈css

不管Vue仍是React整个体系圈十分的完备,就一个中级前端想要提升本身,彻底就能够学习其中一个体系,即可以收获不少东西,从而突破自身html

从工程化角度来讲,前端脚手架,性能优化,构建等等一系列的工做可使用webpack处理,这里又会涉及到SSR相关工做,稍微深刻一点便会踏进node的领域,能够越挖越深前端

从前端框架角度来讲,如何使用React这种框架解决大型项目的目录设计,小项目拆分,代码组织,UI组件,项目与项目之间的影响,路由、数据流向等等问题处理完毕便会进步很大一步node

从大前端角度来讲,使用React处理Native领域的问题,使用React兼容小程序的问题,一套代码解决多端运行的策略,好比兼容微信小程序,随便某一点都值得咱们研究几个月react

从规范来讲,咱们能够看看React如何组织代码的,测试用例怎么写,怎么维护github,怎么作升级,甚至怎么写文档,都是值得学习的webpack

从后期来讲,如何在这个体系上作监控、作日志、作预警,如何让业务与框架更好的融合都是须要思考的ios

react体系是很是完善的,他不仅是一个框架,而是一个庞大的技术体系,优秀的解决方案,基于此,咱们十分有必要基于React或者Vue中的一个进行深刻学习git

也正是由于这个庞大的体系,反而致使咱们有时只是想写一个hello world,都变得彷佛很困难,因而咱们今天就先来使用标准的知识写一个demo试试es6

文章对应代码地址:https://github.com/yexiaochai/react-demogithub

演示地址:https://yexiaochai.github.io/react-demo/build/index.html

脚手架

如今的框架已经十分完备了,并且把市场教育的很好,一个框架除了输出源码之外,还须要输出对应脚手架,直接引入框架源文件的作法已经不合适了,若是咱们开发react项目,即可以直接使用框架脚手架建立项目,就react来讲,暂时这个脚手架create-react-app比较经常使用,他有如下特色:

① 基本配置为你写好了,若是按照规范来可作到零配置

② 继承了React、JSX、ES六、Flow的支持,这个也是类React框架的标准三件套

③ 由于如今进入了前端编译时代,服务器以及热加载必不可少,一个命令便能运行

首先,咱们一个命令安装依赖:

npm install -g create-react-app

而后就可使用脚手架建立项目了:

create-react-app react-demo
├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ └── serviceWorker.js └── yarn.lock

直接浏览器打开的方法也不适用了,这里开发环境使用一个node服务器,执行代码运行起来:

npm start

系统自动打开一个页面,而且会热更新,看一个项目首先看看其package.json:

{ "name": "demo", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.6.3", "react-dom": "^16.6.3", "react-scripts": "2.1.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ] }

因此当咱们执行npm run start的时候事实上是执行node_modules/react-script目录下对应脚本,能够看到项目目录自己连webpack的配置文件都没有,全部的配置所有在react-scripts中,若是对工程配置有什么定制化需求,执行

npm run eject

就将node_modules中对应配置拷贝出来了,可随意修改:

config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js scripts ├── build.js ├── start.js └── test.js

也能够安装个服务器,能够直接运行build文件中的代码:

npm install -g pushstate-server pushstate-server build

咱们的代码开始比较简单,只写一个hello world就好了,因此把多余的目录文件所有删除之,修改下index.js代码:

├── README.md ├── build │ ├── asset-manifest.json │ ├── index.html │ ├── precache-manifest.ced1e61ba13691d3414ad116326a23a5.js │ ├── service-worker.js │ └── static │ └── js │ ├── 1.794557b9.chunk.js │ ├── 1.794557b9.chunk.js.map │ ├── main.931cdb1a.chunk.js │ ├── main.931cdb1a.chunk.js.map │ ├── runtime~main.229c360f.js │ └── runtime~main.229c360f.js.map ├── config │ ├── env.js │ ├── jest │ │ ├── cssTransform.js │ │ └── fileTransform.js │ ├── paths.js │ ├── webpack.config.js │ └── webpackDevServer.config.js ├── package.json ├── public │ └── index.html ├── scripts │ ├── build.js │ ├── start.js │ └── test.js ├── src │ └── index.js └── yarn.lock
import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<div>hello world</div>, document.getElementById('root'));

这个代码不难,我想关键是,这个代码写完了,忽然就开服务器了,忽然就打包成功了,忽然就能够运行了,这个对于一些同窗有点玄幻,这里就有必要说一下这里的webpack了

webpack

咱们说框架的脚手架,其实说白了就是工程化一块的配置,最初几年的工程化主要集中在压缩和优化、到requireJS时代后工程化变得必不可少,当时主要依赖grunt和gulp这类工具,后续为了把重复的工做杀掉工程化就越走越远了,可是和最初其实变化不大,都是一点一点的将各类优化往上加,加之最近两年typescript一击es6新语法须要编译进行,咱们就进入了编译时代

webpack已经进入了4.X时代,通常一个团队会有一个同事(多是架构师)对webpack特别熟悉,将脚手架进行更改后,就能够很长时间不改一下,这个同事有时候主要就作这么一件事情,因此咱们偶尔会称他为webpack配置工程师,虽然是个笑话,从侧门也能够看出,webpack至少不是个很容易学习的东西,形成这个状况的缘由还不是其自己有多难,主要是最初文档不行,小伙伴想实现一个功能的时候连去哪里找插件,用什么合适的插件只能一个个的试,因此文档是工程化中很重要的一环

这里再简单介绍下webpack,webpack是如今最经常使用的JavaScript程序的静态模块打包器(module bundler),他的特色就是以模块(module)为中心,咱们只要给一个入口文件,他会根据这个入口文件找到全部的依赖文件,最后捆绑到一块儿,这里盗个图:

这里几个核心概念是:

① 入口 - 指示webpack应该以哪一个模块(通常是个js文件),做为内部依赖图的开始

② 输出 - 告诉将打包后的文件输出到哪里,或者文件名是什么

③ loader - 这个很是关键,这个让webpack可以去处理那些非JavaScript文件,或者是自定义文件,转换为可用的文件,好比将jsx转换为js,将less转换为css

test就是正则标志,标识哪些文件会被处理;use表示用哪一个loader 

④ 插件(plugins)

插件被用于转换某些类型的模块,适用于的范围更广,包括打包优化、压缩、从新定义环境中的变量等等,这里举一个小例子进行说明,react中的jsx这种事实上是浏览器直接不能识别的,可是咱们却能够利用webpack将之进行一次编译:

// 原 JSX 语法代码
return <h1>Hello,Webpack</h1>

// 被转换成正常的 JavaScript 代码
return React.createElement('h1', null, 'Hello,Webpack')

这里咱们来作个小demo介绍webpack的低阶使用,咱们先创建一个文件夹webpack-demo,先创建一个文件src/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
  </body>
</html>

而后咱们创建一个js文件src/index.js以及src/data.js以及style.css

import data from './data' console.log(data);
export default { name: '叶小钗' }
* { font-size: 16px;
}
. ├── package.json └── src ├── data.js ├── index.html ├── index.js └── style.css

这个时候轮到咱们的webpack登场,以及会用到的几个加载器(这里不讲安装过程):

npm install webpack webpack-cli webpack-serve html-webpack-plugin html-loader css-loader style-loader file-loader url-loader --save-dev

① webpack-cli是命令行工具,有了他咱们就须要在他的规则下写配置便可,不然咱们要本身在node环境写不少文件操做的代码

② loader结尾的都是文件加载器,读取对应的文件须要对应的加载器,好比你本身定义一个.tpl的文件,若是没有现成的loader,你就只能本身写一个

③ 其中还有个node服务器,方便咱们调试

由于咱们这里的import是es6语法,浏览器不能识别,因此须要安装babel解析语言:

npm install babel-core babel-preset-env babel-loader --save-dev

而后咱们在package.json中加入一行代码:

"babel": { "presets": ["env"] }

这个时候就能够建立webpack文件了:

 1 const { resolve } = require('path')  2 const HtmlWebpackPlugin = require('html-webpack-plugin')  3 
 4 // 使用 WEBPACK_SERVE 环境变量检测当前是不是在 webpack-server 启动的开发环境中
 5 const dev = Boolean(process.env.WEBPACK_SERVE)  6 
 7 module.exports = {  8   /*
 9  webpack 执行模式  10  development:开发环境,它会在配置文件中插入调试相关的选项,好比 moduleId 使用文件路径方便调试  11  production:生产环境,webpack 会将代码作压缩等优化  12   */
 13   mode: dev ? 'development' : 'production',  14 
 15   /*
 16  配置 source map  17  开发模式下使用 cheap-module-eval-source-map, 生成的 source map 能和源码每行对应,方便打断点调试  18  生产模式下使用 hidden-source-map, 生成独立的 source map 文件,而且不在 js 文件中插入 source map 路径,用于在 error report 工具中查看 (好比 Sentry)  19   */
 20   devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map',  21 
 22   // 配置页面入口 js 文件
 23   entry: './src/index.js',  24 
 25   // 配置打包输出相关
 26  output: {  27     // 打包输出目录
 28     path: resolve(__dirname, 'dist'),  29 
 30     // 入口 js 的打包输出文件名
 31     filename: 'index.js'
 32  },  33 
 34  module: {  35     /*
 36  配置各类类型文件的加载器,称之为 loader  37  webpack 当遇到 import ... 时,会调用这里配置的 loader 对引用的文件进行编译  38     */
 39  rules: [  40  {  41         /*
 42  使用 babel 编译 ES6 / ES7 / ES8 为 ES5 代码  43  使用正则表达式匹配后缀名为 .js 的文件  44         */
 45         test: /\.js$/,  46 
 47         // 排除 node_modules 目录下的文件,npm 安装的包不须要编译
 48         exclude: /node_modules/,  49 
 50         /*
 51  use 指定该文件的 loader, 值能够是字符串或者数组。  52  这里先使用 eslint-loader 处理,返回的结果交给 babel-loader 处理。loader 的处理顺序是从最后一个到第一个。  53  eslint-loader 用来检查代码,若是有错误,编译的时候会报错。  54  babel-loader 用来编译 js 文件。  55         */
 56         use: ['babel-loader', 'eslint-loader']  57  },  58 
 59  {  60         // 匹配 html 文件
 61         test: /\.html$/,  62         /*
 63  使用 html-loader, 将 html 内容存为 js 字符串,好比当遇到  64  import htmlString from './template.html';  65  template.html 的文件内容会被转成一个 js 字符串,合并到 js 文件里。  66         */
 67         use: 'html-loader'
 68  },  69 
 70  {  71         // 匹配 css 文件
 72         test: /\.css$/,  73 
 74         /*
 75  先使用 css-loader 处理,返回的结果交给 style-loader 处理。  76  css-loader 将 css 内容存为 js 字符串,而且会把 background, @font-face 等引用的图片,  77  字体文件交给指定的 loader 打包,相似上面的 html-loader, 用什么 loader 一样在 loaders 对象中定义,等会下面就会看到。  78         */
 79         use: ['style-loader', 'css-loader']  80  }  81 
 82  ]  83  },  84 
 85   /*
 86  配置 webpack 插件  87  plugin 和 loader 的区别是,loader 是在 import 时根据不一样的文件名,匹配不一样的 loader 对这个文件作处理,  88  而 plugin, 关注的不是文件的格式,而是在编译的各个阶段,会触发不一样的事件,让你能够干预每一个编译阶段。  89   */
 90  plugins: [  91     /*
 92  html-webpack-plugin 用来打包入口 html 文件  93  entry 配置的入口是 js 文件,webpack 以 js 文件为入口,遇到 import, 用配置的 loader 加载引入文件  94  但做为浏览器打开的入口 html, 是引用入口 js 的文件,它在整个编译过程的外面,  95  因此,咱们须要 html-webpack-plugin 来打包做为入口的 html 文件  96     */
 97     new HtmlWebpackPlugin({  98       /*
 99  template 参数指定入口 html 文件路径,插件会把这个文件交给 webpack 去编译, 100  webpack 按照正常流程,找到 loaders 中 test 条件匹配的 loader 来编译,那么这里 html-loader 就是匹配的 loader 101  html-loader 编译后产生的字符串,会由 html-webpack-plugin 储存为 html 文件到输出目录,默认文件名为 index.html 102  能够经过 filename 参数指定输出的文件名 103  html-webpack-plugin 也能够不指定 template 参数,它会使用默认的 html 模板。 104       */
105       template: './src/index.html', 106 
107       /*
108  由于和 webpack 4 的兼容性问题,chunksSortMode 参数须要设置为 none 109  https://github.com/jantimon/html-webpack-plugin/issues/870 110       */
111       chunksSortMode: 'none'
112  }) 113  ] 114 }
webpack.config.js

而后执行webpack命令便构建好了咱们的文件:

. ├── dist │ ├── index.html │ ├── index.js │ └── index.js.map ├── package-lock.json ├── package.json ├── src │ ├── data.js │ ├── index.html │ ├── index.js │ └── style.css └── webpack.config.js

能够看到,只要找到咱们的入口文件index.js,便能轻易的将全部的模块打包成一个文件,包括样式文件,咱们关于webpack的介绍到此为止,更详细的介绍请看这里:https://juejin.im/entry/5b63eb8bf265da0f98317441

咱们脚手架中的webpack配置实现相对比较复杂,咱们先学会基本使用,后面点再来怎么深刻这块,由于现有的配置确定不能知足咱们项目的需求

页面实现

这里为了更多的解决你们工做中会遇到到问题,咱们这里实现两个页面:

① 首页,包括城市列表选择页面

② 列表页面,而且会实现滚动刷新等效果

页面大概长这个样子(由于这个页面以前我就实现过,因此样式部分我便直接拿过来使用便可,你们关注逻辑实现便可):

咱们这里先捡硬骨头坑,直接就来实现这里的列表页面,这里是以前的页面,你们能够点击对比看看

组件拆分

react两个核心第一是摆脱dom操做,第二是组件化开发,这两点在小型项目中意义都不是十分大,只有经历过多人维护的大项目,其优势才会体现出来,咱们这里第一步固然也是拆分页面

这里每个模块都是一个组件,从通用性来讲咱们能够将之分为:

① UI组件,与业务无关的组件,只须要填充数据,好比这里的header组件和日历组件以及其中的列表模块也能够分离出一个组件,但看业务耦合大不大

② 页面组件,页面中的元素

工欲善其事必先利其器,因此咱们这里先来实现几个组件模块,这里首先是对于新人比较难啃的日历模块,咱们代码过程当中也会给你们说目录该如何划分

日历组件

日了组件是相对比较复杂的组件了,单单这个组件又能够分为:

① 月组件,处理月部分

② 日部分,处理日期部分

可以将这个组件作好,基本对组件系统会有个初步了解了,咱们这里首先来实现日历-日部分,这里咱们为项目创建一个src/ui/calendar目录,而后建立咱们的文件:

. ├── index.js └── ui └── calendar └── calendar.js
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; ReactDOM.render(<Calendar/>, document.getElementById('root'));
import React from 'react'; export default class Calendar extends React.Component { render() { return ( <div>日历</div> ) } }

这个时候再执行如下命令便会编译运行:

npm run start

虽然不知为何,可是咱们的代码运行了,大概就是这么一个状况:),接下来咱们开始来完善咱们的代码,日历组件,咱们外层至少得告诉日历年和月,日历才好作展现,那么这里出现了第一个问题,咱们怎么将属性数据传给组件呢?这里咱们来简单描述下react中的state与props

state是react中的状态属性,定义一个正确的状态是写组件的第一步,state须要表明组件UI的完整状态集,任何UI的改变都应该从state体现出来,判断组件中一个变量是否是该做为state有如下依据:

① 这个变量是不是从父组件获取,若是是,那么他应该是一个属性

② 这个变量是否在组件的整个生命周期不会变化,若是是,那么他也是个属性

③ 这个变量是不是经过其余状态或者属性计算出来的,若是是,那么他也不是一个状态

④ 状态须要在组件render时候被用到

这里的主要区别是state是可变的,而props是只读的,若是想要改变props,只能经过父组件修改,就本章内容,咱们将年月等设置为属性,这里先忽略样式的处理,简单几个代码,轮廓就出来了,这里有如下变化:

① 新增common文件夹,放了工具类函数

② 新增static目录存放css,这里的css咱们后续会作特殊处理,这里先不深刻

因而,咱们目录变成了这样:

. ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── index.html │ └── static │ └── css │ ├── global.css │ └── index.css ├── src │ ├── common │ │ └── utils.js │ ├── index.js │ └── ui │ └── calendar │ ├── calendar.js │ ├── day.js │ └── month.js

咱们将calendar代码贴出来看看:

import React from 'react'; import dateUtils from '../../common/utils' export default class Calendar extends React.Component { render() { let year = this.props.year; let month = this.props.month; let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; //获取当前日期数据
        let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); return ( <ul className="cm-calendar ">
                <ul className="cm-calendar-hd"> { weekDayArr.map((data, i) => { return <li className="cm-item--disabled">{data}</li>
 }) } </ul>
            </ul>
 ) } }

样式基本出来了:

这个时候咱们须要将月组件实现了,这里贴出来第一阶段的完整代码:

import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; ReactDOM.render( <Calendar year="2018" month="12"/>, 
    document.getElementById('root') );
 1 let isDate = function (date) {  2     return date && date.getMonth;  3 };  4 
 5 //兼容小程序日期
 6 let getDate = function(year, month, day) {  7     if(!day) day = 1;  8     return new Date(year, month, day);  9 } 10 
11 let isLeapYear = function (year) { 12     //传入为时间格式须要处理
13     if (isDate(year)) year = year.getFullYear() 14     if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 15     return false; 16 }; 17 
18 let getDaysOfMonth = function (date) { 19     var month = date.getMonth() + 1; //注意此处月份要加1
20     var year = date.getFullYear(); 21     return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; 22 } 23 
24 let getBeginDayOfMouth = function (date) { 25     var month = date.getMonth(); 26     var year = date.getFullYear(); 27     var d = getDate(year, month, 1); 28     return d.getDay(); 29 } 30 
31 let getDisplayInfo = function(date) { 32     if (!isDate(date)) { 33       date = getDate(date) 34  } 35     var year = date.getFullYear(); 36 
37     var month = date.getMonth(); 38     var d = getDate(year, month); 39 
40     //这个月一共多少天
41     var days = getDaysOfMonth(d); 42 
43     //这个月是星期几开始的
44     var beginWeek = getBeginDayOfMouth(d); 45 
46     return { 47  year: year, 48  month: month, 49  days: days, 50  beginWeek: beginWeek 51  } 52  } 53 
54   let isOverdue = function isOverdue(year, month, day) { 55     let date = new Date(year, month, day); 56     let now = new Date(); 57     now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 58     return date.getTime() < now.getTime(); 59  } 60   
61   let isToday = function isToday(year, month, day, selectedDate) { 62     let date = new Date(year, month, day); 63     return date.getTime() == selectedDate; 64  } 65 
66 let dateUtils = { 67  isLeapYear, 68  getDaysOfMonth, 69  getBeginDayOfMouth, 70  getDisplayInfo, 71  isOverdue, 72  isToday 73 }; 74 
75 export default dateUtils;
utils.js
 1 import React from 'react';  2 import dateUtils from '../../common/utils'
 3 import CalendarMonth from './month'
 4 
 5 
 6 export default class Calendar extends React.Component {  7  render() {  8         let year = this.props.year;  9         let month = this.props.month; 10         let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; 11         //获取当前日期数据
12         let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); 13         return ( 14             <ul className="cm-calendar ">
15                 <ul className="cm-calendar-hd">
16  { 17                         weekDayArr.map((data, index) => { 18                             return <li key={index} className="cm-item--disabled">{data}</li>
19  }) 20  } 21                 </ul>
22                 <CalendarMonth year={year} month={month}/>
23             </ul>
24  ) 25  } 26 }
calendar.js
 1 import React from 'react';  2 import dateUtils from '../../common/utils'
 3 import CalendarDay from './day'
 4 
 5 export default class CalendarMonth extends React.Component {  6 
 7     //获取首次空格
 8  _renderBeginDayOfMouth(beforeDays) {  9         let html = []; 10         for (let i = 0; i < beforeDays; i++) { 11             html.push(<li key={i} className="cm-item--disabled"></li>);
12  } 13         return html; 14  } 15 
16     //和_renderBeginDayOfMouth相似能够重构掉
17  _renderDays(year, month, days) { 18         let html = []; 19         for(let i = 0; i < days; i++) { 20  html.push( 21                 <CalendarDay key={i} year={year} month={month} day={i} />
22  ) 23  } 24         return html; 25  } 26 
27  render() { 28         let year = this.props.year; 29         let month = this.props.month; 30         let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); 31 console.log(displayInfo) 32         return ( 33             <ul className="cm-calendar-bd ">
34                 <h3 className="cm-month calendar-cm-month js_month">{year + '-' + month}</h3>
35                 
36                 <ul className="cm-day-list">
37                     { this._renderBeginDayOfMouth( displayInfo.beginWeek) } 38                     { this._renderDays(year, month, displayInfo.days) } 39                 </ul>
40             </ul>
41  ) 42  } 43 }
month.js
 1 import React from 'react';  2 import dateUtils from '../../common/utils'
 3 
 4 export default class CalendarDay extends React.Component {  5 
 6 
 7  render() {  8         let year = this.props.year;  9         let month = this.props.month; 10         let day = this.props.day; 11 
12         let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; 13 
14         return ( 15             <li year={year} month={month} day={day}  >
16                 <div className="cm-field-wrapper ">
17                     <div className="cm-field-title">{day + 1}</div>
18                 </div>
19             </li>
20  ) 21  } 22 }
day.js

这段代码的效果是:

基础框架结构出来后,咱们就须要一点一点向上面加肉了,首先咱们加一个选中日期,须要一点特效,这里稍微改下代码,具体各位去GitHub上面看代码了,这段代码就不贴出来了,由于咱们这里是写demo,这个日历组件功能完成60%便可,不会所有完成,这里咱们作另外一个操做,就是在页面上添加一个上一个月下一个月按钮,而且点击日历时候在控制台将当前日期打印出来便可,这里是效果图:

这个时候咱们首先为左右两个按钮添加事件,这里更改下代码变成了这个样子,这里贴出阶段代码,完整代码请你们在git上查看

 1 import React from 'react';  2 import ReactDOM from 'react-dom';  3 import Calendar from './ui/calendar/calendar';  4 
 5 class CalendarMain extends React.Component {  6  constructor(props) {  7  super(props);  8         let today = new Date().getTime();  9         this.state = { 10             month: 12, 11  selectdate: today 12  }; 13  } 14  preMonth() { 15         this.setState({ 16             month: this.state.month - 1
17  }); 18  } 19  nextMonth() { 20         this.setState({ 21             month: this.state.month + 1
22  }); 23  } 24  ondayclick(year, month, day) { 25 
26         this.setState({ 27             selectdate: new Date(year, parseInt(month) - 1, day).getTime() 28  }) 29 
30  } 31  render() { 32         // today = new Date(today.getFullYear(), today.getMonth(), 1);
33         let selectdate = this.state.selectdate;; 34         let month = this.state.month; 35         return ( 36             <div className="calendar-wrapper-box">
37                 <div className="box-hd">
38                     <span className="fl icon-back js_back " onClick={this.preMonth.bind(this)}  ></span>
39                     <span className="fr icon-next js_next" onClick={this.nextMonth.bind(this)} ></span>
40                 </div>
41                 <Calendar ondayclick={this.ondayclick.bind(this)} year="2018" month={month} selectdate={selectdate} />
42             </div>
43  ) 44  } 45 } 46 
47 ReactDOM.render( 48     <CalendarMain />
49 
50  , 51     document.getElementById('root') 52 );
index.js
 1 let isDate = function (date) {  2     return date && date.getMonth;  3 };  4 
 5 //兼容小程序日期
 6 let getDate = function(year, month, day) {  7     if(!day) day = 1;  8     return new Date(year, month, day);  9 } 10 
11 let isLeapYear = function (year) { 12     //传入为时间格式须要处理
13     if (isDate(year)) year = year.getFullYear() 14     if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 15     return false; 16 }; 17 
18 let getDaysOfMonth = function (date) { 19     var month = date.getMonth() + 1; //注意此处月份要加1
20     var year = date.getFullYear(); 21     return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; 22 } 23 
24 let getBeginDayOfMouth = function (date) { 25     var month = date.getMonth(); 26     var year = date.getFullYear(); 27     var d = getDate(year, month, 1); 28     return d.getDay(); 29 } 30 
31 let getDisplayInfo = function(date) { 32     if (!isDate(date)) { 33       date = getDate(date) 34  } 35     var year = date.getFullYear(); 36 
37     var month = date.getMonth(); 38     var d = getDate(year, month); 39 
40     //这个月一共多少天
41     var days = getDaysOfMonth(d); 42 
43     //这个月是星期几开始的
44     var beginWeek = getBeginDayOfMouth(d); 45 
46     return { 47  year: year, 48  month: month, 49  days: days, 50  beginWeek: beginWeek 51  } 52  } 53 
54   let isOverdue = function isOverdue(year, month, day) { 55     let date = new Date(year, month, day); 56     let now = new Date(); 57     now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 58     return date.getTime() < now.getTime(); 59  } 60   
61   let isToday = function isToday(year, month, day, selectedDate) { 62     let date = new Date(year, month, day); 63     let d = new Date(selectedDate); 64     d = new Date(d.getFullYear(), d.getMonth(), d.getDate()); 65     selectedDate = d.getTime(); 66     return date.getTime() == selectedDate; 67  } 68 
69 let dateUtils = { 70  isLeapYear, 71  getDaysOfMonth, 72  getBeginDayOfMouth, 73  getDisplayInfo, 74  isOverdue, 75  isToday 76 }; 77 
78 export default dateUtils;
utils.js
import React from 'react'; import dateUtils from '../../common/utils' import CalendarMonth from './month' export default class Calendar extends React.Component { render() { let year = this.props.year; let month = this.props.month; let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; //获取当前日期数据
        let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); return ( <ul className="cm-calendar ">
                <ul className="cm-calendar-hd"> { weekDayArr.map((data, index) => { return <li key={index} className="cm-item--disabled">{data}</li>
 }) } </ul>
                <CalendarMonth ondayclick={this.props.ondayclick} selectdate={this.props.selectdate} year={year} month={month}/>
            </ul>
 ) } }
calendar.js
 1 import React from 'react';  2 import dateUtils from '../../common/utils'
 3 import CalendarDay from './day'
 4 
 5 export default class CalendarMonth extends React.Component {  6 
 7     //获取首次空格
 8  _renderBeginDayOfMouth(beforeDays) {  9         let html = []; 10         for (let i = 0; i < beforeDays; i++) { 11             html.push(<li key={i} className="cm-item--disabled"></li>);
12  } 13         return html; 14  } 15 
16     //和_renderBeginDayOfMouth相似能够重构掉
17  _renderDays(year, month, days) { 18         let html = []; 19         for(let i = 1; i <= days; i++) { 20  html.push( 21                 <CalendarDay ondayclick={this.props.ondayclick}  selectdate={this.props.selectdate}  key={i} year={year} month={month} day={i} />
22  ) 23  } 24         return html; 25  } 26 
27  render() { 28         let year = this.props.year; 29         let month = this.props.month; 30         
31         let name = new Date(year, parseInt(month) - 1, 1); 32         name = name.getFullYear() + '-' + (name.getMonth() + 1); 33 
34         let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); 35 console.log(displayInfo) 36         return ( 37             <ul className="cm-calendar-bd ">
38                 <h3 className="cm-month calendar-cm-month js_month">{name}</h3>
39                 
40                 <ul className="cm-day-list">
41                     { this._renderBeginDayOfMouth( displayInfo.beginWeek) } 42                     { this._renderDays(year, month, displayInfo.days) } 43                 </ul>
44             </ul>
45  ) 46  } 47 }
month.js
 1 import React from 'react';  2 import dateUtils from '../../common/utils'
 3 
 4 export default class CalendarDay extends React.Component {  5  onClick(e) {  6         let year = this.props.year;  7         let month = this.props.month;  8         let day = this.props.day;  9 
10         this.props.ondayclick(year, month, day) 11  } 12 
13  render() { 14         let year = this.props.year; 15         let month = this.props.month; 16         let day = this.props.day; 17         let selectdate = this.props.selectdate; 18 
19         let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; 20         
21         if(dateUtils.isToday(year, parseInt(month) - 1, day, selectdate)) 22             klass += ' active '
23 
24         return ( 25             <li onClick={this.onClick.bind(this)} className={klass} year={year} month={month} day={day}  >
26                 <div className="cm-field-wrapper ">
27                     <div className="cm-field-title">{day }</div>
28                 </div>
29             </li>
30  ) 31  } 32 }
day.js

至此,咱们日历一块的基本代码完成,完成度应该有60%,咱们继续接下来的组件编写

header组件

日历组件结束后,咱们来实现另外一个UI类组件-header组件,咱们这里实现的header算是比较中规中矩的头部组件,复杂的状况要考虑hybrid状况,那就会很复杂了,话很少说,咱们先在ui目录下创建一个header目录,写下最简单的代码后,咱们的index:

ReactDOM.render( <Header title="我是标题" />
 , document.getElementById('root') );

而后是咱们的header组件:

 1 import React from 'react';  2 export default class Header extends React.Component {  3  render() {  4         return (  5             <div class="cm-header">
 6                 <span class=" cm-header-icon fl  js_back">
 7                     <i class="icon-back"></i>
 8                 </span>
 9                 <h1 class="cm-page-title js_title">
10                     {this.props.title} 11                 </h1>
12             </div>
13  ) 14  } 15 }

因而header部分的框架就出来了,这个时候咱们来将之增强,这里也不弄太强,就将后退的事件加上,以及左边按钮加上对应的按钮和事件,这里改造下index和header代码:

import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; import Header from './ui/header/header'; class CalendarMain extends React.Component { constructor(props) { super(props); let today = new Date().getTime(); this.state = { month: 12, selectdate: today }; } preMonth() { this.setState({ month: this.state.month - 1 }); } nextMonth() { this.setState({ month: this.state.month + 1 }); } ondayclick(year, month, day) { this.setState({ selectdate: new Date(year, parseInt(month) - 1, day).getTime() }) } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        let selectdate = this.state.selectdate;; let month = this.state.month; return ( <div className="calendar-wrapper-box">
                <div className="box-hd">
                    <span className="fl icon-back js_back " onClick={this.preMonth.bind(this)}  ></span>
                    <span className="fr icon-next js_next" onClick={this.nextMonth.bind(this)} ></span>
                </div>
                <Calendar ondayclick={this.ondayclick.bind(this)} year="2018" month={month} selectdate={selectdate} />
            </div>
 ) } } class HeaderMain extends React.Component { constructor(props) { super(props); this.state = { title: '我是标题' }; //这里定义,右边按钮规则
        this.state.right = [ { tagname: 'search', callback: function() { console.log('搜索') } }, { tagname: 'tips', value: '说明', callback: function() { alert('我是按钮') } } ] } onBackaction() { console.log('返回') } render() { return ( <Header right={this.state.right} title={this.state.title} backaction={this.onBackaction.bind(this)} />
 ) } } class PageMain extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        let selectdate = this.state.selectdate;; let month = this.state.month; return ( <HeaderMain />
 ) } } ReactDOM.render( <PageMain />, document.getElementById('root')
);
index.js
import React from 'react'; export default class Header extends React.Component { _renderRight() { let html = []; let arr = this.props.right; if(!arr) return; for(let i = 0, len = arr.length; i < len; i++) { let item = arr[i]; html.push( <span onClick={item.callback} key={i} className={item.value ? 'cm-header-btn fr' : 'cm-header-icon fr'} > {item.value ? item.value : <i className={'icon-' + item.tagname}></i>}
                </span>
 ) } return html; } onClick() { if(this.props.backaction) { this.props.backaction(); } } render() { return ( <div className="cm-header"> {this._renderRight()} <span className=" cm-header-icon fl  js_back" onClick={this.onClick.bind(this)} >
                    <i className="icon-back"></i>
                </span>
                <h1 className="cm-page-title js_title"> {this.props.title} </h1>
            </div>
 ) } }

就这样按钮和点击时候的事件回调都作好了,这里图标有点丑这个事情你们就别关注了,注意这里是一种规则,设定了规则后按照规则写代码后续会极大提升工做效率,到此咱们header部分的代码就完成了,非常轻松加愉快啊!!!

列表组件

列表组件这里涉及到部分业务代码了,由于存在请求后端数据了,因而咱们就比较尴尬了,由于我一点点都不想去为了写一个demo而去写创建数据库或者写代码,因而咱们这里使用mock搞定数据部分,工欲善其事必先利其器,咱们这里首先将数据部分解决掉(PS:由于原来项目的接口不能访问,因此直接胡乱mock数据,这样也许会形成以前作的日历没有多大的意义,事实上数据应该是用日期参数请求的)

如今想作假数据已经有不少成熟的平台了,好比这个:https://www.easy-mock.com,使用起来很是简单,你们去看看他的教程就行,咱们这里直接使用之:

如今访问这个url就能看到咱们的列表数据:https://www.easy-mock.com/mock/5c29d45956db174d47ce162a/example_copy/train/list#!method=get

在react中咱们使用fetch获取数据,这里直接上代码了:

fetch( 'https://www.easy-mock.com/mock/5c29d45956db174d47ce162a/example_copy/train/list' ) .then(res => res.json()) .then(data => { console.log(data) })

这样就会将咱们的数据打印到控制台,可是实际项目中咱们不会这样请求数据,而会对他进行两层封装,第一层封装隐藏fetch,让咱们不管是ajax或者fetch均可以,第二层是要给他加上缓存功能好比咱们的localstorage,包括一些公告参数处理撒的,因此咱们会在咱们的目录中新增data目录专门用来处理数据请求部分,甚至咱们会为没一个数据请求创建一个“实体”,让各个页面重复调用,我这里偷懒就直接将以前微信小程序的请求模块和换成模块拿过来使用便可:

import listModel from './data/demo'; listModel.setParam({ a: 1, b: 'aa' }); listModel.execute(function (data) { console.log(data) })
export default class Model { constructor() { this.url = ''; this.param = {}; this.validates = []; this.type = 'GET'; } pushValidates(handler) { if (typeof handler === 'function') { this.validates.push(handler); } } setParam(key, val) { if (typeof key === 'object') { Object.assign(this.param, key); } else { this.param[key] = val; } } //@override
 buildurl() { return this.url; } onDataSuccess() { } //执行数据请求逻辑
 execute(onComplete, onError) { let scope = this; let _success = function (data) { let _data = data; if (typeof data == 'string') _data = JSON.parse(data); // @description 开发者能够传入一组验证方法进行验证
      for (let i = 0, len = scope.validates.length; i < len; i++) { if (!scope.validates[i](data)) { // @description 若是一个验证不经过就返回
          if (typeof onError === 'function') { return onError.call(scope || this, _data, data); } else { return false; } } } // @description 对获取的数据作字段映射
      let datamodel = typeof scope.dataformat === 'function' ? scope.dataformat(_data) : _data; if (scope.onDataSuccess) scope.onDataSuccess.call(scope, datamodel, data); if (typeof onComplete === 'function') { onComplete.call(scope, datamodel, data); } }; this._sendRequest(_success); } _getParamStr(s) { let str = '', f = false; for (let k in this.param) { f = true; str = str + k + '=' + (typeof this.param[k] === 'object' ? JSON.stringify(this.param[k]) : this.param[k])  + s; } if(f) str = str.substr(0, str.length - 1); return str; } //删除过时缓存
 _sendRequest(callback) { let url = this.buildurl(); let param = { method: this.type, headers: { 'Content-Type': 'application/json;charset=UTF-8' }, mode: 'cors', cache: 'no-cache' }; if (this.type === 'POST') { param.body = JSON.stringify(this.param); } else { if (url.search(/\?/) === -1) { url += '?' + this._getParamStr('&') } else { url += '&' + this._getParamStr('&') } } fetch(url, param) .then(res => res.json()) .then((data) => { callback && callback(data); }) //小程序模块
    // wx.request({
    // url: this.buildurl(),
    // data: this.param,
    // success: function success(data) {
    // callback && callback(data);
    // }
    // });
 } }
view code
//处理微信小程序兼容
let wx = { getStorageSync: function (key) { return localStorage.getItem(key) }, setStorage: function (o) { let k = o.key; let v = JSON.stringify(o.data) let callback = o.callback; localStorage.setItem(k, v); callback && callback(); }, getStorage: function (key, callback) { let data = localStorage.getItem(key); callback(data); } } export default class Store { constructor(opts) { if (typeof opts === 'string') this.key = opts; else Object.assign(this, opts); //若是没有传过时时间,则默认30分钟
    if (!this.lifeTime) this.lifeTime = 1; //本地缓存用以存放全部localstorage键值与过时日期的映射
    this._keyCache = 'SYSTEM_KEY_TIMEOUT_MAP'; } //获取过时时间,单位为毫秒
 _getDeadline() { return this.lifeTime * 60 * 1000; } //获取一个数据缓存对象,存能够异步,获取我同步便可
 get(sign) { let key = this.key; let now = new Date().getTime(); var data = wx.getStorageSync(key); if (!data) return null; data = JSON.parse(data); //数据过时
    if (data.deadLine < now) { this.removeOverdueCache(); return null; } if (data.sign) { if (sign === data.sign) return data.data; else return null; } return null; } /*产出页面组件须要的参数 sign 为格式化后的请求参数,用于同一请求不一样参数时候返回新数据,好比列表为北京的城市,后切换为上海,会判断tag不一样而更新缓存数据,tag至关于签名 每一键值只会缓存一条信息 */ set(data, sign) { let timeout = new Date(); let time = timeout.setTime(timeout.getTime() + this._getDeadline()); this._saveData(data, time, sign); } _saveData(data, time, sign) { let key = this.key; let entity = { deadLine: time, data: data, sign: sign }; let scope = this; wx.setStorage({ key: key, data: JSON.stringify(entity), success: function () { //每次真实存入前,须要往系统中存储一个清单
 scope._saveSysList(key, entity.deadLine); } }); } _saveSysList(key, timeout) { if (!key || !timeout || timeout < new Date().getTime()) return; let keyCache = this._keyCache; wx.getStorage({ key: keyCache, complete: function (data) { let oldData = {}; if (data.data) oldData = JSON.parse(data.data); oldData[key] = timeout; wx.setStorage({ key: keyCache, data: JSON.stringify(oldData) }); } }); } //删除过时缓存
 removeOverdueCache() { let now = new Date().getTime(); let keyCache = this._keyCache; wx.getStorage({ key: keyCache, success: function (data) { if (data && data.data) data = JSON.parse(data.data); for (let k in data) { if (data[k] < now) { delete data[k]; wx.removeStorage({ key: k, success: function () { } }); } } wx.setStorage({ key: keyCache, data: JSON.stringify(data) }); } }); } }
View Code
 1 import Model from './abstractmodel';  2 import Store from './abstractstore';  3 
 4 class DemoModel extends Model {  5  constructor() {  6  super();  7     let scope = this;  8     this.domain = 'https://www.easy-mock.com/mock/5c29d45956db174d47ce162a/example_copy';  9     this.param = {  10  head: {  11         version: '1.0.1',  12         ct: 'ios'
 13  }  14  };  15 
 16     //若是须要缓存,能够在此设置缓存对象
 17     this.cacheData = null;  18 
 19     this.pushValidates(function (data) {  20       return scope._baseDataValidate(data);  21  });  22  }  23 
 24   //首轮处理返回数据,检查错误码作统一验证处理
 25  _baseDataValidate(data) {  26     if (typeof data === 'string') data = JSON.parse(data);  27     if (data.errno === 0) {  28       if (data.data) data = data.data;  29       return true;  30  }  31     return false;  32  }  33 
 34  dataformat(data) {  35     if (typeof data === 'string') data = JSON.parse(data);  36     if (data.data) data = data.data;  37     if (data.data) data = data.data;  38     return data;  39  }  40 
 41  buildurl() {  42     return this.domain + this.url;  43  }  44 
 45  getSign() {  46     return JSON.stringify(this.param);  47  }  48  onDataSuccess(fdata, data) {  49     if (this.cacheData && this.cacheData.set)  50       this.cacheData.set(fdata, this.getSign());  51  }  52 
 53   //若是有缓存直接读取缓存,没有才请求
 54  execute(onComplete, ajaxOnly) {  55     let data = null;  56     if (!ajaxOnly && this.cacheData && this.cacheData.get) {  57       data = this.cacheData.get(this.getSign());  58       if (data) {  59  onComplete(data);  60         return;  61  }  62  }  63  super.execute(onComplete);  64  }  65 
 66 }  67 
 68 class ListStore extends Store {  69  constructor() {  70  super();  71     this.key = 'DEMO_LIST';  72     //30分钟过时时间
 73     this.lifeTime = 30;  74  }  75 }  76 
 77 class ListModel extends DemoModel {  78  constructor() {  79  super();  80     this.url = '/train/list';  81     this.type = 'GET';  82     // this.type = 'POST';
 83 
 84     this.cacheData = new ListStore;  85  }  86   //每次数据访问成功,错误码为0时皆会执行这个回调
 87  onDataSuccess(fdata, data) {  88  super.onDataSuccess(fdata, data);  89     //开始执行自我逻辑
 90     let o = {  91       _indate: new Date().getTime()  92  };  93     // for (let k in fdata) {
 94     // o[k] = typeof fdata[k];
 95     // }
 96     //执行数据上报逻辑
 97     console.log('执行数据上报逻辑', o);  98  }  99 } 100 
101 let listModel = new ListModel() 102 
103 export default listModel
View Code

这里data目录是,而后能够看到数据请求成功,而且localstrage中有数据了:

data ├── abstractmodel.js ├── abstractstore.js └── demo.js

有了数据后,咱们来完善咱们的列表,由于数据缘由,咱们这里便不作滚动分页功能了,通常来讲列表类组件特色仍是比较突出的:须要提供一个数据请求模块以及一个数据处理器,最后加一个模板就能够完成全部功能了,这里仍是先来实现列表部分代码,这个列表组件由于涉及的业务比较多并且每一个页面的列表变化也比较大,咱们暂且将之放到ui目录,后续看看这块怎么处理一下,咱们依然先在这里创建list目录:

class PageMain extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        return ( <div class="page-list cm-page">
                <HeaderMain />
                <div className="calendar-bar-wrapper js_calendar_wrapper">
                </div>
                <List />
            </div>
 ) } } ReactDOM.render( <PageMain />, document.getElementById('root')
);
 1 import React from 'react';  2 export default class List extends React.Component {  3 
 4  render() {  5         return (  6             <ul class="bus-list js_bus_list ">
 7                 <li data-index="0" data-dstation="上海南" class="bus-list-item ">
 8                     <div class="bus-seat">
 9                         <span class=" fl">K1805 | 其它</span><span class=" fr">2小时7分 </span>
10                     </div>
11                     <div class="detail">
12                         <div class="sub-list set-out">
13                             <span class="bus-go-off">04:22</span> <span class="start"><span class="icon-circle s-icon1">
14                             </span>上海南</span> <span class="fr price">¥28.5起</span>
15                         </div>
16                         <div class="sub-list">
17                             <span class="bus-arrival-time">06:29</span> <span class="end"><span class="icon-circle s-icon2">
18                             </span>杭州</span> <span class="fr ">2598张</span>
19                         </div>
20                     </div>
21                     <div class="bus-seats-info">
22                         <span>硬座(555) </span>
23                         <span>硬卧(1653) </span>
24                         <span>软卧(56) </span>
25                         <span>无座(334) </span>
26                     </div>
27                 </li>
28                 <li data-index="1" data-dstation="上海南" class="bus-list-item ">
29                     <div class="bus-seat">
30                         <span class=" fl">K1511 | 其它</span><span class=" fr">1小时49分 </span>
31                     </div>
32                     <div class="detail">
33                         <div class="sub-list set-out">
34                             <span class="bus-go-off">04:56</span> <span class="start"><span class="icon-circle s-icon1">
35                             </span>上海南</span> <span class="fr price">¥24.5起</span>
36                         </div>
37                         <div class="sub-list">
38                             <span class="bus-arrival-time">06:45</span> <span class="end"><span class="icon-circle s-icon2">
39                             </span>杭州东</span> <span class="fr ">34张</span>
40                         </div>
41                     </div>
42                     <div class="bus-seats-info">
43                         <span>硬座(8) </span>
44                         <span>硬卧(24) </span>
45                         <span>软卧(2) </span>
46                         <span>无座(0) </span>
47                     </div>
48                 </li>
49             </ul>
50  ) 51  } 52 }
list文件

这样一来,咱们轻易的就将页面作出来了:

接下来咱们使用组件完成其功能,这里咱们将代码作一层分离,将列表组件分红两部分,第一部分是不变放在UI中的部分,另外一部分是咱们要求传入的模板组件,由于每一个页面的列表展现都是不同的,因而咱们先实现外层列表,这里就至关于要传递一个组件给另外一个组件使用,咱们简单的尝试了下可行性:

//业务列表项目,由于每一个页面列表展现皆不同,因此将这段代码外放
class ListItem extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        return ( <li data-index="0" data-dstation="上海南" class="bus-list-item ">
                <div class="bus-seat">
                    <span class=" fl">K1805 | 其它</span><span class=" fr">2小时7分 </span>
                </div>
                <div class="detail">
                    <div class="sub-list set-out">
                        <span class="bus-go-off">04:22</span> <span class="start"><span class="icon-circle s-icon1">
                        </span>上海南</span> <span class="fr price">¥28.5起</span>
                    </div>
                    <div class="sub-list">
                        <span class="bus-arrival-time">06:29</span> <span class="end"><span class="icon-circle s-icon2">
                        </span>杭州</span> <span class="fr ">2598张</span>
                    </div>
                </div>
                <div class="bus-seats-info">
                    <span>硬座(555) </span>
                    <span>硬卧(1653) </span>
                    <span>软卧(56) </span>
                    <span>无座(334) </span>
                </div>
            </li>
 ) } } class PageMain extends React.Component { constructor(props) { super(props); this.state = {}; } render() { let _ListItem = this.props.list; let list = new _ListItem(); debugger; // today = new Date(today.getFullYear(), today.getMonth(), 1);
        return ( <div class="page-list cm-page">
                <HeaderMain />
 {list.render()} <div className="calendar-bar-wrapper js_calendar_wrapper">
                </div>
                <List />
            </div>
 ) } } ReactDOM.render( <PageMain list={ListItem} />, document.getElementById('root')
);
View Code

证实是可行的,其实React早就知道咱们有这种骚操做,因此衍生了高阶组件的几率,这里咱们简单介绍下

PS:你们能够看到,咱们文中的例子都不是生拉硬套的要应用某个知识点是确实有这种需求

高阶组件-继承的应用

参考:https://github.com/sunyongjian/blog/issues/25

高阶组件只是名字比较高阶罢了,其实跟咱们上面代码的例子差很少,每一个React组件事实上都是一个js对象,咱们能够实例化一下他,完成任何骚操做,可是出于规范化和代码可控(在不很是熟悉底层代码的时候,随意使用骚操做,可能会出莫名其妙的BUG,可是也是由于莫名其妙的BUG会致使你更熟悉框架,BUG带来的框架理解有时候优于机械源码阅读,因此在非核心项目上,咱们很是建议你骚操做

一个高阶组件只是一个包装了另外一个React组件的React组件

上面的说法有点很差理解,这里换个方式说,所谓高阶组件,就是咱们有一个组件,这个时候咱们会给他传递各类参数,其中一个参数是另外一个React组件,而且咱们须要在父组件中使用他:

const EnhancedComponent = higherOrderComponent(WrappedComponent);

这个例子依旧不够清晰,咱们再举个例子:

class A extends React.Component { render() { return ( <div>我是组件A</div>
 ) } } const AContainer = WrappedComponent => { console.log('simpleHoc'); return class extends React.Component { render() { return ( <h1> 我是组件A的爸爸 <WrappedComponent  {...this.props} />
                </h1>
 ) } } } let Parent = AContainer(A); ReactDOM.render( <Parent />, document.getElementById('root')
);

这里会输出(这里说爸爸可能不太合适,这里应该是个组合关系):

<h1>我是组件A的爸爸<div>我是组件A</div></h1>

这里核心概念仍是这里使用了一个继承解决这个问题:

return class extends React.Component { render() { return ( <ul class="bus-list js_bus_list ">
                <WrappedComponent  {...this.props} />
            </ul>
 ) } }

因此,高阶组件其实并不神秘,就是实现了一个用于继承的组件,而后在子组件里面作业务性操做,在以前属于很是常规的操做,这里推荐看一点老一点的东西,脱离框架的东西,类比帮助你们了解高阶组件https://www.cnblogs.com/yexiaochai/p/3888373.html,因而这里咱们稍微改造下咱们的list组件的框架结构:

PS:这里必定要注意,一个项目或者几个项目中,列表的大致HTML结构必定是很是一致的,这里是个规则约定,规则先与代码,先于框架

import React from 'react'; let ListContainer = WrappedComponent => { return class extends React.Component { render() { return ( <ul class="bus-list js_bus_list ">
                    <WrappedComponent  {...this.props} />
                </ul>
 ) } } } export default ListContainer;
View Code
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; import Header from './ui/header/header'; import ListContainer from './ui/list/list'; import listModel from './data/demo'; listModel.setParam({ a: 1, b: 'aa' }); listModel.execute(function (data) { console.log(data) }) class CalendarMain extends React.Component { constructor(props) { super(props); let today = new Date().getTime(); this.state = { month: 12, selectdate: today }; } preMonth() { this.setState({ month: this.state.month - 1 }); } nextMonth() { this.setState({ month: this.state.month + 1 }); } ondayclick(year, month, day) { this.setState({ selectdate: new Date(year, parseInt(month) - 1, day).getTime() }) } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        let selectdate = this.state.selectdate;; let month = this.state.month; return ( <div className="calendar-wrapper-box">
                <div className="box-hd">
                    <span className="fl icon-back js_back " onClick={this.preMonth.bind(this)}  ></span>
                    <span className="fr icon-next js_next" onClick={this.nextMonth.bind(this)} ></span>
                </div>
                <Calendar ondayclick={this.ondayclick.bind(this)} year="2018" month={month} selectdate={selectdate} />
            </div>
 ) } } class HeaderMain extends React.Component { constructor(props) { super(props); this.state = { title: '我是标题' }; //这里定义,右边按钮规则
        this.state.right = [ { //但愿代码执行时候的做用域
                view: this, tagname: 'search', callback: function () { console.log(this) console.log('搜索') } }, { view: this, tagname: 'tips', value: '说明', callback: function () { alert('我是按钮') } } ] } onBackaction() { console.log('返回') } render() { return ( <Header right={this.state.right} title={this.state.title} backaction={this.onBackaction.bind(this)} />
 ) } } //业务列表项目,由于每一个页面列表展现皆不同,因此将这段代码外放
class ListItem extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1);
        return ( <li data-index="0" data-dstation="上海南" class="bus-list-item ">
                <div class="bus-seat">
                    <span class=" fl">K1805 | 其它</span><span class=" fr">2小时7分 </span>
                </div>
                <div class="detail">
                    <div class="sub-list set-out">
                        <span class="bus-go-off">04:22</span> <span class="start"><span class="icon-circle s-icon1">
                        </span>上海南</span> <span class="fr price">¥28.5起</span>
                    </div>
                    <div class="sub-list">
                        <span class="bus-arrival-time">06:29</span> <span class="end"><span class="icon-circle s-icon2">
                        </span>杭州</span> <span class="fr ">2598张</span>
                    </div>
                </div>
                <div class="bus-seats-info">
                    <span>硬座(555) </span>
                    <span>硬卧(1653) </span>
                    <span>软卧(56) </span>
                    <span>无座(334) </span>
                </div>
            </li>
 ) } } class PageMain extends React.Component { constructor(props) { super(props); this.state = {}; } render() { let List = ListContainer(ListItem); // today = new Date(today.getFullYear(), today.getMonth(), 1);
        return ( <div class="page-list cm-page">
                <HeaderMain />
                <div className="calendar-bar-wrapper js_calendar_wrapper">
                </div>
                <List />
            </div>
 ) } } ReactDOM.render( <PageMain list={ListItem} />, document.getElementById('root')
);
View Code

由此,基本框架就出来了:

咱们这里继续完善这个组件便可,这里具体代码各位github上看吧:https://github.com/yexiaochai/react-demo

PS:事实上,咱们index.js里面代码已经不少了,应该分离开,可是咱们代码已经接近尾声就懒得分离了,你们实际工做中必定要分离

咱们代码稍做改造后就变成了这个样子(因为只是demo,对于一些须要计算展现好比筛选硬座票数等未作实现):

至此,咱们的demo就结束了,若是有必要能够添加各类筛选条件,好比这里的排序:

好比这里的筛选:

可是咱们这里因为是简单的demo加之本篇博客篇幅已经很长了,咱们这里就不作实现了,反正也是操做数据,就此,咱们业务部分代码结束了,接下来咱们来作一点工程化的操做

组件样式问题

能够看到,以前咱们的组件样式,所有被咱们柔和到了global.css或者index.css中了,对于有些工厂做业作的很好的公司,会具体分出重构工程师(写css的)和程序工程师(写js的)两个岗位,通常是重构同事将css直接交给js同事,这样作起来效率会很高,因此多数状况下,咱们全局会有一个样式文件,业务页面会有一个样式文件,这其实没什么大问题,可能出现的问题请你们阅读下这篇文章:【前端优化之拆分CSS】前端三剑客的分分合合,这里其实已经涉及到了一个工做习惯他要求咱们作页面的时候就分红模块,作模块的时候要考虑模块的css,这样作也会有一个弊端就是全局性的东西就比较难过了,因此一个大项目的样式相关工做最好由一个资深一点的同事设计规则和公共的点,其次否则很容易各自为战,咱们这里完成一个简单的工做,将列表部分的代码从global中分离出来,咱们先找到对应的样式代码:

.page-list { padding-bottom: 45px; } .page-list .icon-setout { margin: 0 5px; border-color: #00B358;
} .page-list .icon-arrival { margin: 0 5px; border-color: #f06463;
} .page-list .icon-sec { position: relative; top: -4px; display: inline-block; width: 8px; height: 8px; vertical-align: middle; border-left: 1px solid; border-bottom: 1px solid; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); -webkit-box-sizing: border-box; box-sizing: border-box; margin-left: 5px;
} .page-list .active .icon-sec { top: 1px; -webkit-transform: rotate(135deg); transform: rotate(135deg);
} .page-list .active .icon-setout, .page-list .active .icon-arrival { border-color: #fff;
} .page-list .bus-tabs.list-filter { position: fixed; left: 0; bottom: 0; height: 36px; line-height: 36px; background-color: #fcfcfc;
} .page-list .bus-tabs.list-filter .tabs-item { border-right: 1px solid #d2d2d2; border-top: 1px solid #d2d2d2;
} .page-list .bus-tabs.list-filter .tabs-item.active { color: #fff; background-color: #00b358;
} .page-list .bus-tabs.list-filter .tabs-item .line{ height: 22px; line-height: 22px; text-align: center; font-size: 12px;
} .page-list .bus-tabs.list-filter .tabs-item .line:last-child{ color: #00b358 } .page-list .bus-tabs.list-filter .tabs-item.active .line:last-child{ color: #fff } .page-list .bus-tabs.list-filter .tabs-item .line .icon-time{ top: 2px; margin-right: 4px;
} .page-list .bus-list .bus-list-item { position: relative; height: 110px; background-color: #fff; margin: 8px 0; border: 1px solid #e5e5e5; border-width: 1px 0;
} .page-list .bus-list .bus-list-item.disabled, .page-list .bus-list .bus-list-item.disabled .price { color: #c5c5c5;
} .page-list .bus-list .bus-seat { height: 32px; line-height: 32px; padding: 0 15px;
    
} .page-list .bus-list .bus-list-item .bus-time { position: absolute; left: 0; width: 67px; height: 50px; line-height: 50px; margin: 15px 0; color: #00b358; text-align: center; font-size: 20px; font-family: Arial;
} .page-list .bus-list .bus-list-item .detail { margin: 0 15px 0 15px;
} .page-list .bus-list .bus-seats-info { margin: 0 15px 0 15px;
} .page-list .bus-list .bus-list-item .detail .sub-list{ height: 26px;
} .page-list .sub-list.set-out { font-size: 16px; font-weight: 600;
} .page-list .bus-list .bus-go-off,.page-list .bus-list .bus-arrival-time{ display: inline-block; width: 50px; 
    
} .page-list .bus-list .bus-list-item .price { font-family: Arial; color: #fd8f01; font-size: 16px; font-weight: 600;
} .page-list .bus-list .bus-list-item.disabled .ticket { display: none;
} .page-list .bus-list .bus-list-item .ticket { color: #fd8f01; font-size: 12px; border: 1px solid #fd8f01; padding: 1px 4px; border-radius: 5px; font-family: Arial;
} .page-list .bus-list .bus-list-item.disabled .ticket { color: #c5c5c5;
} .page-list .bus-list .bus-list-item .s-icon1 { margin: 0 5px; border-color: #00B358;
} .page-list .bus-list .bus-list-item .s-icon2 { margin: 0 5px; border-color: #f06463;
} .page-list .calendar-bar-wrapper { height: 52px;
} .page-list .calendar-bar { height: 36px; line-height: 36px; background-color: #08c563; color: #fff; top: 50px; left: 0; position: fixed;
} .page-list .calendar-bar .tabs-item { font-size: 13px; border-right: 1px solid #02ad56;
} .page-list .calendar-bar .tabs-item.disabled { color: #01994c;
} .baidubox .page-list .calendar-bar{ top: 0;
 } .baidubox .page-list .sort-bar{ top: 36px;
 } .page-list .sort-bar-wrapper { height: 50px;
 } .page-list .sort-bar { height: 36px; line-height: 36px; background-color: #fff; top: 50px; left: 0; position: fixed; border-bottom: 1px solid #EAEAEA;
} .page-list .icon-sort { position: relative; margin: 0 0 0 8px; border-top: 4px solid #c3c3c3; border-right: 4px solid #c3c3c3; border-bottom: 4px solid #c3c3c3; border-left: 4px solid #c3c3c3; bottom: 1px; display: inline-block; -webkit-transform: rotate(-225deg); transform: rotate(-225deg);
    
    
} .page-list .icon-sort.up { display: inline-block; -webkit-transform: rotate(-225deg); transform: rotate(-225deg); border-bottom: 4px solid #02ad56; border-left: 4px solid #02ad56;
} .page-list .icon-sort.down { display: inline-block; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); border-bottom: 4px solid #02ad56; border-left: 4px solid #02ad56;
} .page-list .icon-sort::before { content: ''; position: absolute; top: 0px; left: -8px; width: 18px; height: 2px; background-color: #fff; -webkit-transform: rotate(-135deg); transform: rotate(-135deg);
} .page-list.page-list--search .bus-list .bus-list-item .tobooking{ display: none;
 } .page-list.page-list--search .bus-list .bus-list-item .detail { margin-right: 10px;
 } .page-list .ad-wrapper { display: none; 
 } .page-list.page-list--search .ad-wrapper { display: block; position: fixed; bottom: 45px; left: 0; width: 100%; z-index: 500;
 } .page-list.page-list--search .ad-wrapper img { width: 100%;
 } .page-list .b-tags { position: absolute; bottom: 15px; right: 70px;
 } .page-list .bus-tips { background: #fff; padding: 10px 15px; height: 33px; overflow: hidden; border-bottom: 1px solid #e5e5e5;
} .page-list .bus-tip-text { margin-right: 150px; word-break: break-all; font-size: 13px; line-height: 17px; color: #8c8c8c; margin: 0;
} .page-list .bus-tip-icon { border: 1px solid #00b358; padding: 2px 12px; color: #00b358; border-radius: 22px;
} .page-list .cm-modal { background-color: #efefef;
} .page-list .more-filter-line { overflow: hidden; box-sizing: border-box; -webkit-box-sizing: border-box; border-bottom: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5; background-color: #fff; margin: 8px 0;
} .page-list .more-filter-line ul{ display: none;
} .page-list .more-filter-line.active ul{ display: block;
} .page-list .more-filter-line:first-child { margin-top: 0; border-top: none;
} .page-list .more-filter-line:last-child { margin-bottom: 0; border-bottom: none;
} .page-list .more-filter-line .filter-time-title{ position: relative; font-size: 16px; padding-right: 30px; margin: 0 10px ; height: 46px; line-height: 46px;
} .page-list .more-filter-line.active .filter-time-title{ border-bottom: 1px solid #e5e5e5;
} .page-list .more-filter-line .filter-time-title::after { position: absolute; content: ''; right: 15px; top: 17px; width: 8px; height: 8px; border-left: 1px solid; border-bottom: 1px solid; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); -webkit-box-sizing: border-box; box-sizing: border-box; border-color: #00b358;
} .page-list .more-filter-line.active .filter-time-title::after { top: 21px; -webkit-transform: rotate(135deg); transform: rotate(135deg);
} .page-list .more-filter-line .filter-time-title .fr{ font-size: 14px; display: inline-block;
} .page-list .more-filter-line.active .filter-time-title .fr{ display: none ;
} .page-list .more-filter-line ul { padding: 5px 15px ;
} .page-list .more-filter-line ul li{ position: relative; height: 32px; line-height: 32px;
} .page-list .more-filter-line ul li.active{ color: #00b358;
} .page-list .more-filter-line ul li.active::after { content: ""; width: 14px; height: 6px; border-bottom: 2px solid #00b358; border-left: 2px solid #00b358; position: absolute; top: 50%; right: 8px; margin-top: -4px; -webkit-transform: rotate(-45deg) translateY(-50%); transform: rotate(-45deg) translateY(-50%);
} .page-list .more-filter-line1 { overflow: hidden; box-sizing: border-box; -webkit-box-sizing: border-box; border-bottom: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5; background-color: #fff; margin: 8px 0; padding: 0 10px; height: 48px; line-height: 48px;
} .page-list .more-filter-wrapper .btn-wrapper { text-align: center; margin: 15px 0; padding-bottom: 15px;
 
} .page-list .more-filter-wrapper .btn-primary { border-radius: 50px; width: 80%; border: 1px solid #00b358; color: #00b358; background-color: #fff;
} .page-list .lazy-load .bus-seat { display: none;
} .page-list .lazy-load .detail { display: none;
} .page-list .lazy-load .bus-seats-info { display: none;
} .page-list .bus-list .lazy-info { display: none;
} .page-list .bus-list .lazy-load .lazy-info { padding: 10px 0; text-align: center; display: block;
}


/** * station group */ .page-list .bs-price { font-family: Arial; color: #fd8f01; font-size: 16px; font-weight: 600;
} .page-list .bs-ellipsis { white-space: nowrap; overflow-x: hidden; text-overflow: ellipsis;
} .page-list .bs-icon-bus, .page-list .bs-icon-carpool, .page-list .bs-icon-train, .page-list .bs-icon-icline { width: 31px; height: 31px; background-size: 31px 31px; background-repeat: no-repeat; background-position: 0 0; display: inline-block;
} .page-list .bs-icon-arrow { width: 15px; height: 4px; background: url(/webapp/bus/static/images/icon-arrow.png) 0 0 no-repeat; background-size: 15px 4px; display: inline-block;
} .page-list .bs-icon-bus { background-image: url(/webapp/bus/static/images/icon-bus.png);
} .page-list .bs-icon-carpool { background-image: url(/webapp/bus/static/images/icon-carpool.png);
} .page-list .bs-icon-train { background-image: url(/webapp/bus/static/images/icon-train.png);
} .page-list .bs-icon-icline { background-image: url(/webapp/bus/static/images/icon-icline.png);
} .page-list .bs-st-wrapper { position: relative; background: url(/webapp/bus/static/images/icon-dot.png) 5px 19px no-repeat; background-size: 2px 10px;
} .page-list .bs-st-end { margin-top: 6px;
} .page-list .bs-st-start:before, .page-list .bs-st-end:before { content: ''; display: inline-block; width: 8px; height: 8px; margin-right:5px; vertical-align: -2px; border-radius: 50% 50%;
} .page-list .bs-st-start:before { border: 2px solid #13bd65;
} .page-list .bs-st-end:before { border: 2px solid #f06463;
} .page-list .sch-prem { margin: 8px; padding: 8px; border: 1px solid #e8e8e8; background: #fff; position: relative;
} .page-list .sch-prem .icon-wrapper { width: 49px; float: left; margin-top: 8px;
} .page-list .sch-prem .info-wrapper { margin: 0 70px 0 49px;
} .page-list .sch-prem .st-name { font-size: 16px;
} .page-list .sch-prem .st-name .bs-icon-arrow { margin:0 10px; vertical-align: 4px;
} .page-list .sch-prem .price-wrapper { position: absolute; right: 15px; width: 70px; text-align: right; bottom: 8px;
} .page-list .sch-prem-icline .icon-wrapper, .page-list .sch-prem-bus .icon-wrapper{ margin-top: 19px;
} .page-list .sch-prem-icline .price-wrapper, .page-list .sch-prem-bus .price-wrapper{ bottom: 19px;
}
View Code

新建一个style.css暂且放到ui/list目录中,其实这个list的样式跟业务代码更有关系,放里面不合适,可是咱们这里作demo就无所谓了,这里分离出来后稍做改造便可:

//list.js
import React from 'react'; import './style.css';//这段css样式会被style标签插入到header中

这里未作高阶使用,关于高阶的用法,咱们后续有机会再介绍,接下来就是部署以及工程化相关工做了,考虑篇幅,咱们后续再继续

结语

本文代码地址:https://github.com/yexiaochai/react-demo

演示地址:https://yexiaochai.github.io/react-demo/build/index.html

能够看到,从组件化一块的设计,React是作的十分好的,咱们没花什么时间就把一个简单的页面搭建了出来,实际项目过程当中正确的使用React会有很高的效率;另外一方面,webpack一块的配置,create-react-app已经彻底帮咱们作完了,咱们只须要按照他的规则来便可,这个黑匣子里面的东西又很是多,咱们后续根据实际的项目再深刻了解吧,一时之间也说不完,后续咱们继续研究如何使用这套代码兼容小程序开发,以及一些工程化问题

相关文章
相关标签/搜索