前言
大概16年的時候我們隊react進行了簡單的學習:從DOM操作看Vue&React的前端組件化,順帶補齊React的demo,當時我們只是站在框架角度在學習,隨着近幾年前端的變化,想寫個hello world似乎變得復雜起來,我們今天便一起來看看現代化的前端,應該如何做一個頁面,今天我們學習react首先說一下React的體系圈
無論Vue還是React整個體系圈十分的完備,就一個中級前端想要提高自己,完全就可以學習其中一個體系,便可以收獲很多東西,從而突破自身
從工程化角度來說,前端腳手架,性能優化,構建等等一系列的工作可以使用webpack處理,這里又會涉及到SSR相關工作,稍微深入一點便會踏進node的領域,可以越挖越深
從前端框架角度來說,如何使用React這種框架解決大型項目的目錄設計,小項目拆分,代碼組織,UI組件,項目與項目之間的影響,路由、數據流向等等問題處理完畢便會進步很大一步
從大前端角度來說,使用React處理Native領域的問題,使用React兼容小程序的問題,一套代碼解決多端運行的策略,比如兼容微信小程序,隨便某一點都值得我們研究幾個月
從規范來說,我們可以看看React如何組織代碼的,測試用例怎么寫,怎么維護github,怎么做升級,甚至怎么寫文檔,都是值得學習的
從后期來說,如何在這個體系上做監控、做日志、做預警,如何讓業務與框架更好的融合都是需要思考的
react體系是非常完善的,他不只是一個框架,而是一個龐大的技術體系,優秀的解決方案,基於此,我們十分有必要基於React或者Vue中的一個進行深入學習
也正是因為這個龐大的體系,反而導致我們有時只是想寫一個hello world,都變得似乎很困難,於是我們今天就先來使用標准的知識寫一個demo試試
文章對應代碼地址:https://github.com/yexiaochai/react-demo
演示地址:https://yexiaochai.github.io/react-demo/build/index.html
腳手架
現在的框架已經十分完備了,而且把市場教育的很好,一個框架除了輸出源碼以外,還需要輸出對應腳手架,直接引入框架源文件的做法已經不合適了,如果我們開發react項目,便可以直接使用框架腳手架創建項目,就react來說,暫時這個腳手架create-react-app比較常用,他有以下特點:
① 基本配置為你寫好了,如果按照規范來可做到零配置
② 繼承了React、JSX、ES6、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已經完全幫我們做完了,我們只需要按照他的規則來即可,這個黑匣子里面的東西又非常多,我們后續根據實際的項目再深入了解吧,一時之間也說不完,后續我們繼續研究如何使用這套代碼兼容小程序開發,以及一些工程化問題