前言
大概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命令便構建好了咱們的文件:
. ├── 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;
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 }
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 }
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 }
這段代碼的效果是:
基礎框架結構出來後,咱們就須要一點一點向上面加肉了,首先咱們加一個選中日期,須要一點特效,這裏稍微改下代碼,具體各位去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 );
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;
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> ) } }
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 }
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 }
至此,咱們日曆一塊的基本代碼完成,完成度應該有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') );
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); // } // }); } }
//處理微信小程序兼容 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) }); } }); } }
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
這裏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 }
這樣一來,咱們輕易的就將頁面作出來了:
接下來咱們使用組件完成其功能,這裏咱們將代碼作一層分離,將列表組件分紅兩部分,第一部分是不變放在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') );
證實是可行的,其實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;
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') );
由此,基本框架就出來了:
咱們這裏繼續完善這個組件便可,這裏具體代碼各位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; }
新建一個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已經徹底幫咱們作完了,咱們只須要按照他的規則來便可,這個黑匣子裏面的東西又很是多,咱們後續根據實際的項目再深刻了解吧,一時之間也說不完,後續咱們繼續研究如何使用這套代碼兼容小程序開發,以及一些工程化問題