前言
大概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框架的標準三件套
③ 因為現在進入了前端編譯時代,伺服器以及熱載入必不可少,一個命令便能執行
首先,我們一個命令安裝依賴:
1 |
npm install -g create-react-app |
然後就可以使用腳手架建立專案了:
1 |
create-react-app react-demo |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
├── 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伺服器,執行程式碼執行起來:
1 |
npm start |
系統自動開啟一個頁面,並且會熱更新,看一個專案首先看看其package.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
{ "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中,如果對工程配置有什麼定製化需求,執行
1 |
npm run eject |
就將node_modules中對應配置拷貝出來了,可隨意修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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檔案中的程式碼:
1 2 |
npm install -g pushstate-server pushstate-server build |
我們的程式碼開始比較簡單,只寫一個hello world就行了,所以把多餘的目錄檔案全部刪除之,修改下index.js程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
├── 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 |
1 2 3 4 |
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將之進行一次編譯:
1 2 3 4 5 |
// 原 JSX 語法程式碼 return <h1>Hello,Webpack</h1> // 被轉換成正常的 JavaScript 程式碼 return React.createElement('h1', null, 'Hello,Webpack') |
Hello,Webpack
1 2 |
// 被轉換成正常的 JavaScript 程式碼 return React.createElement('h1', null, 'Hello,Webpack') |
這裡我們來做個小demo介紹webpack的低階使用,我們先建立一個資料夾webpack-demo,先建立一個檔案src/index.html
1 2 3 4 5 6 7 8 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> </body> </html> |
然後我們建立一個js檔案src/index.js以及src/data.js以及style.css
1 2 |
import data from './data' console.log(data); |
1 2 3 |
export default { name: '葉小釵' } |
1 2 3 |
* { font-size: 16px; } |
1 2 3 4 5 6 7 |
. ├── package.json └── src ├── data.js ├── index.html ├── index.js └── style.css |
這個時候輪到我們的webpack登場,以及會用到的幾個載入器(這裡不講安裝過程):
1 |
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解析語言:
1 |
npm install babel-core babel-preset-env babel-loader --save-dev |
然後我們在package.json中加入一行程式碼:
1 2 3 |
"babel": { "presets": ["env"] } |
這個時候就可以建立webpack檔案了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
const { resolve } = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') // 使用 WEBPACK_SERVE 環境變數檢測當前是否是在 webpack-server 啟動的開發環境中 const dev = Boolean(process.env.WEBPACK_SERVE) module.exports = { /* webpack 執行模式 development:開發環境,它會在配置檔案中插入除錯相關的選項,比如 moduleId 使用檔案路徑方便除錯 production:生產環境,webpack 會將程式碼做壓縮等優化 */ mode: dev ? 'development' : 'production', /* 配置 source map 開發模式下使用 cheap-module-eval-source-map, 生成的 source map 能和原始碼每行對應,方便打斷點除錯 生產模式下使用 hidden-source-map, 生成獨立的 source map 檔案,並且不在 js 檔案中插入 source map 路徑,用於在 error report 工具中檢視 (比如 Sentry) */ devtool: dev ? 'cheap-module-eval-source-map' : 'hidden-source-map', // 配置頁面入口 js 檔案 entry: './src/index.js', // 配置打包輸出相關 output: { // 打包輸出目錄 path: resolve(__dirname, 'dist'), // 入口 js 的打包輸出檔名 filename: 'index.js' }, module: { /* 配置各種型別檔案的載入器,稱之為 loader webpack 當遇到 import ... 時,會呼叫這裡配置的 loader 對引用的檔案進行編譯 */ rules: [ { /* 使用 babel 編譯 ES6 / ES7 / ES8 為 ES5 程式碼 使用正規表示式匹配字尾名為 .js 的檔案 */ test: /\.js$/, // 排除 node_modules 目錄下的檔案,npm 安裝的包不需要編譯 exclude: /node_modules/, /* use 指定該檔案的 loader, 值可以是字串或者陣列。 這裡先使用 eslint-loader 處理,返回的結果交給 babel-loader 處理。loader 的處理順序是從最後一個到第一個。 eslint-loader 用來檢查程式碼,如果有錯誤,編譯的時候會報錯。 babel-loader 用來編譯 js 檔案。 */ use: ['babel-loader', 'eslint-loader'] }, { // 匹配 html 檔案 test: /\.html$/, /* 使用 html-loader, 將 html 內容存為 js 字串,比如當遇到 import htmlString from './template.html'; template.html 的檔案內容會被轉成一個 js 字串,合併到 js 檔案裡。 */ use: 'html-loader' }, { // 匹配 css 檔案 test: /\.css$/, /* 先使用 css-loader 處理,返回的結果交給 style-loader 處理。 css-loader 將 css 內容存為 js 字串,並且會把 background, @font-face 等引用的圖片, 字型檔案交給指定的 loader 打包,類似上面的 html-loader, 用什麼 loader 同樣在 loaders 物件中定義,等會下面就會看到。 */ use: ['style-loader', 'css-loader'] } ] }, /* 配置 webpack 外掛 plugin 和 loader 的區別是,loader 是在 import 時根據不同的檔名,匹配不同的 loader 對這個檔案做處理, 而 plugin, 關注的不是檔案的格式,而是在編譯的各個階段,會觸發不同的事件,讓你可以干預每個編譯階段。 */ plugins: [ /* html-webpack-plugin 用來打包入口 html 檔案 entry 配置的入口是 js 檔案,webpack 以 js 檔案為入口,遇到 import, 用配置的 loader 載入引入檔案 但作為瀏覽器開啟的入口 html, 是引用入口 js 的檔案,它在整個編譯過程的外面, 所以,我們需要 html-webpack-plugin 來打包作為入口的 html 檔案 */ new HtmlWebpackPlugin({ /* template 引數指定入口 html 檔案路徑,外掛會把這個檔案交給 webpack 去編譯, webpack 按照正常流程,找到 loaders 中 test 條件匹配的 loader 來編譯,那麼這裡 html-loader 就是匹配的 loader html-loader 編譯後產生的字串,會由 html-webpack-plugin 儲存為 html 檔案到輸出目錄,預設檔名為 index.html 可以通過 filename 引數指定輸出的檔名 html-webpack-plugin 也可以不指定 template 引數,它會使用預設的 html 模板。 */ template: './src/index.html', /* 因為和 webpack 4 的相容性問題,chunksSortMode 引數需要設定為 none https://github.com/jantimon/html-webpack-plugin/issues/870 */ chunksSortMode: 'none' }) ] } // webpack.config.js |
然後執行webpack命令便構建好了我們的檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
. ├── 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目錄,然後建立我們的檔案:
1 2 3 4 5 |
. ├── index.js └── ui └── calendar └── calendar.js |
1 2 3 4 5 |
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; ReactDOM.render(<Calendar/>, document.getElementById('root')); |
1 2 3 4 5 6 7 8 9 10 |
import React from 'react'; export default class Calendar extends React.Component { render() { return ( <div>日曆</div> ) } } |
這個時候再執行以下命令便會編譯執行:
1 |
npm run start |
雖然不知為什麼,但是我們的程式碼執行了,大概就是這麼一個情況:),接下來我們開始來完善我們的程式碼,日曆元件,我們外層至少得告訴日曆年和月,日曆才好做展示,那麼這裡出現了第一個問題,我們怎麼將屬性資料傳給元件呢?這裡我們來簡單描述下react中的state與props
state是react中的狀態屬性,定義一個正確的狀態是寫元件的第一步,state需要代表元件UI的完整狀態集,任何UI的改變都應該從state體現出來,判斷元件中一個變數是不是該作為state有以下依據:
① 這個變數是否是從父元件獲取,如果是,那麼他應該是一個屬性
② 這個變數是否在元件的整個生命週期不會變化,如果是,那麼他也是個屬性
③ 這個變數是否是通過其他狀態或者屬性計算出來的,如果是,那麼他也不是一個狀態
④ 狀態需要在元件render時候被用到
這裡的主要區別是state是可變的,而props是隻讀的,如果想要改變props,只能通過父元件修改,就本章內容,我們將年月等設定為屬性,這裡先忽略樣式的處理,簡單幾個程式碼,輪廓就出來了,這裡有以下變化:
① 新增common資料夾,放了工具類函式
② 新增static目錄存放css,這裡的css我們後續會做特殊處理,這裡先不深入
於是,我們目錄變成了這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
. ├── 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程式碼貼出來看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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> ) } } |
樣式基本出來了:
這個時候我們需要將月元件實現了,這裡貼出來第一階段的完整程式碼:
1 2 3 4 5 6 7 8 |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
let isDate = function (date) { return date && date.getMonth; }; //相容小程式日期 let getDate = function(year, month, day) { if(!day) day = 1; return new Date(year, month, day); } let isLeapYear = function (year) { //傳入為時間格式需要處理 if (isDate(year)) year = year.getFullYear() if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; return false; }; let getDaysOfMonth = function (date) { var month = date.getMonth() + 1; //注意此處月份要加1 var year = date.getFullYear(); return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; } let getBeginDayOfMouth = function (date) { var month = date.getMonth(); var year = date.getFullYear(); var d = getDate(year, month, 1); return d.getDay(); } let getDisplayInfo = function(date) { if (!isDate(date)) { date = getDate(date) } var year = date.getFullYear(); var month = date.getMonth(); var d = getDate(year, month); //這個月一共多少天 var days = getDaysOfMonth(d); //這個月是星期幾開始的 var beginWeek = getBeginDayOfMouth(d); return { year: year, month: month, days: days, beginWeek: beginWeek } } let isOverdue = function isOverdue(year, month, day) { let date = new Date(year, month, day); let now = new Date(); now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); return date.getTime() < now.getTime(); } let isToday = function isToday(year, month, day, selectedDate) { let date = new Date(year, month, day); return date.getTime() == selectedDate; } let dateUtils = { isLeapYear, getDaysOfMonth, getBeginDayOfMouth, getDisplayInfo, isOverdue, isToday }; export default dateUtils; utils.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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 year={year} month={month}/> </ul> ) } } calendar.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import React from 'react'; import dateUtils from '../../common/utils' import CalendarDay from './day' export default class CalendarMonth extends React.Component { //獲取首次空格 _renderBeginDayOfMouth(beforeDays) { let html = []; for (let i = 0; i < beforeDays; i++) { html.push(<li key={i} className="cm-item--disabled"></li>); } return html; } //和_renderBeginDayOfMouth類似可以重構掉 _renderDays(year, month, days) { let html = []; for(let i = 0; i < days; i++) { html.push( <CalendarDay key={i} year={year} month={month} day={i} /> ) } return html; } render() { let year = this.props.year; let month = this.props.month; let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); console.log(displayInfo) return ( <ul className="cm-calendar-bd "> <h3 className="cm-month calendar-cm-month js_month">{year + '-' + month}</h3> <ul className="cm-day-list"> { this._renderBeginDayOfMouth( displayInfo.beginWeek) } { this._renderDays(year, month, displayInfo.days) } </ul> </ul> ) } } month.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import React from 'react'; import dateUtils from '../../common/utils' export default class CalendarDay extends React.Component { render() { let year = this.props.year; let month = this.props.month; let day = this.props.day; let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; return ( <li year={year} month={month} day={day} > <div className="cm-field-wrapper "> <div className="cm-field-title">{day + 1}</div> </div> </li> ) } } day.js |
這段程式碼的效果是:
基礎框架結構出來後,我們就需要一點一點向上面加肉了,首先我們加一個選中日期,需要一點特效,這裡稍微改下程式碼,具體各位去GitHub上面看程式碼了,這段程式碼就不貼出來了,因為我們這裡是寫demo,這個日曆元件功能完成60%即可,不會全部完成,這裡我們做另一個操作,就是在頁面上新增一個上一個月下一個月按鈕,並且點選日曆時候在控制檯將當前日期列印出來即可,這裡是效果圖:
這個時候我們首先為左右兩個按鈕新增事件,這裡更改下程式碼變成了這個樣子,這裡貼出階段程式碼,完整程式碼請大家在git上檢視
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; 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> ) } } ReactDOM.render( <CalendarMain /> , document.getElementById('root') ); index.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
let isDate = function (date) { return date && date.getMonth; }; //相容小程式日期 let getDate = function(year, month, day) { if(!day) day = 1; return new Date(year, month, day); } let isLeapYear = function (year) { //傳入為時間格式需要處理 if (isDate(year)) year = year.getFullYear() if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; return false; }; let getDaysOfMonth = function (date) { var month = date.getMonth() + 1; //注意此處月份要加1 var year = date.getFullYear(); return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; } let getBeginDayOfMouth = function (date) { var month = date.getMonth(); var year = date.getFullYear(); var d = getDate(year, month, 1); return d.getDay(); } let getDisplayInfo = function(date) { if (!isDate(date)) { date = getDate(date) } var year = date.getFullYear(); var month = date.getMonth(); var d = getDate(year, month); //這個月一共多少天 var days = getDaysOfMonth(d); //這個月是星期幾開始的 var beginWeek = getBeginDayOfMouth(d); return { year: year, month: month, days: days, beginWeek: beginWeek } } let isOverdue = function isOverdue(year, month, day) { let date = new Date(year, month, day); let now = new Date(); now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); return date.getTime() < now.getTime(); } let isToday = function isToday(year, month, day, selectedDate) { let date = new Date(year, month, day); let d = new Date(selectedDate); d = new Date(d.getFullYear(), d.getMonth(), d.getDate()); selectedDate = d.getTime(); return date.getTime() == selectedDate; } let dateUtils = { isLeapYear, getDaysOfMonth, getBeginDayOfMouth, getDisplayInfo, isOverdue, isToday }; export default dateUtils; utils.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React from 'react'; import dateUtils from '../../common/utils' import CalendarMonth from './month' export default class Calendar extends React.Component { render() { let year = this.props.year; let month = this.props.month; let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; //獲取當前日期資料 let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); return ( <ul className="cm-calendar "> <ul className="cm-calendar-hd"> { weekDayArr.map((data, index) => { return <li key={index} className="cm-item--disabled">{data}</li> }) } </ul> <CalendarMonth ondayclick={this.props.ondayclick} selectdate={this.props.selectdate} year={year} month={month}/> </ul> ) } } calendar.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import React from 'react'; import dateUtils from '../../common/utils' import CalendarDay from './day' export default class CalendarMonth extends React.Component { //獲取首次空格 _renderBeginDayOfMouth(beforeDays) { let html = []; for (let i = 0; i < beforeDays; i++) { html.push(<li key={i} className="cm-item--disabled"></li>); } return html; } //和_renderBeginDayOfMouth類似可以重構掉 _renderDays(year, month, days) { let html = []; for(let i = 1; i <= days; i++) { html.push( <CalendarDay ondayclick={this.props.ondayclick} selectdate={this.props.selectdate} key={i} year={year} month={month} day={i} /> ) } return html; } render() { let year = this.props.year; let month = this.props.month; let name = new Date(year, parseInt(month) - 1, 1); name = name.getFullYear() + '-' + (name.getMonth() + 1); let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); console.log(displayInfo) return ( <ul className="cm-calendar-bd "> <h3 className="cm-month calendar-cm-month js_month">{name}</h3> <ul className="cm-day-list"> { this._renderBeginDayOfMouth( displayInfo.beginWeek) } { this._renderDays(year, month, displayInfo.days) } </ul> </ul> ) } } month.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import React from 'react'; import dateUtils from '../../common/utils' export default class CalendarDay extends React.Component { onClick(e) { let year = this.props.year; let month = this.props.month; let day = this.props.day; this.props.ondayclick(year, month, day) } render() { let year = this.props.year; let month = this.props.month; let day = this.props.day; let selectdate = this.props.selectdate; let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; if(dateUtils.isToday(year, parseInt(month) - 1, day, selectdate)) klass += ' active ' return ( <li onClick={this.onClick.bind(this)} className={klass} year={year} month={month} day={day} > <div className="cm-field-wrapper "> <div className="cm-field-title">{day }</div> </div> </li> ) } } day.js |
至此,我們日曆一塊的基本程式碼完成,完成度應該有60%,我們繼續接下來的元件編寫
header元件
日曆元件結束後,我們來實現另一個UI類元件-header元件,我們這裡實現的header算是比較中規中矩的頭部元件,複雜的情況要考慮hybrid情況,那就會很複雜了,話不多說,我們先在ui目錄下建立一個header目錄,寫下最簡單的程式碼後,我們的index:
1 2 3 4 5 |
ReactDOM.render( <Header title="我是標題" /> , document.getElementById('root') ); |
然後是我們的header元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import React from 'react'; export default class Header extends React.Component { render() { return ( <div class="cm-header"> <span class=" cm-header-icon fl js_back"> <i class="icon-back"></i> </span> <h1 class="cm-page-title js_title"> {this.props.title} </h1> </div> ) } } |
於是header部分的框架就出來了,這個時候我們來將之加強,這裡也不弄太強,就將後退的事件加上,以及左邊按鈕加上對應的按鈕和事件,這裡改造下index和header程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; import Header from './ui/header/header'; class CalendarMain extends React.Component { constructor(props) { super(props); let today = new Date().getTime(); this.state = { month: 12, selectdate: today }; } preMonth() { this.setState({ month: this.state.month - 1 }); } nextMonth() { this.setState({ month: this.state.month + 1 }); } ondayclick(year, month, day) { this.setState({ selectdate: new Date(year, parseInt(month) - 1, day).getTime() }) } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1); let selectdate = this.state.selectdate;; let month = this.state.month; return ( <div className="calendar-wrapper-box"> <div className="box-hd"> <span className="fl icon-back js_back " onClick={this.preMonth.bind(this)} ></span> <span className="fr icon-next js_next" onClick={this.nextMonth.bind(this)} ></span> </div> <Calendar ondayclick={this.ondayclick.bind(this)} year="2018" month={month} selectdate={selectdate} /> </div> ) } } class HeaderMain extends React.Component { constructor(props) { super(props); this.state = { title: '我是標題' }; //這裡定義,右邊按鈕規則 this.state.right = [ { tagname: 'search', callback: function() { console.log('搜尋') } }, { tagname: 'tips', value: '說明', callback: function() { alert('我是按鈕') } } ] } onBackaction() { console.log('返回') } render() { return ( <Header right={this.state.right} title={this.state.title} backaction={this.onBackaction.bind(this)} /> ) } } class PageMain extends React.Component { constructor(props) { super(props); this.state = {}; } render() { // today = new Date(today.getFullYear(), today.getMonth(), 1); let selectdate = this.state.selectdate;; let month = this.state.month; return ( <HeaderMain /> ) } } ReactDOM.render( <PageMain />, document.getElementById('root') ); index.js |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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獲取資料,這裡直接上程式碼了:
1 2 3 4 5 6 7 |
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目錄專門用來處理資料請求部分,甚至我們會為沒一個資料請求建立一個“實體”,讓各個頁面重複呼叫,我這裡偷懶就直接將之前微信小程式的請求模組和換成模組拿過來使用即可:
1 2 3 4 5 6 7 8 9 |
import listModel from './data/demo'; listModel.setParam({ a: 1, b: 'aa' }); listModel.execute(function (data) { console.log(data) }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
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; } } //<a href='http://www.jobbole.com/members/wx610506454'>@override</a> 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); // } // }); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
//處理微信小程式相容 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
import Model from './abstractmodel'; import Store from './abstractstore'; class DemoModel extends Model { constructor() { super(); let scope = this; this.domain = 'https://www.easy-mock.com/mock/5c29d45956db174d47ce162a/example_copy'; this.param = { head: { version: '1.0.1', ct: 'ios' } }; //如果需要快取,可以在此設定快取物件 this.cacheData = null; this.pushValidates(function (data) { return scope._baseDataValidate(data); }); } //首輪處理返回資料,檢查錯誤碼做統一驗證處理 _baseDataValidate(data) { if (typeof data === 'string') data = JSON.parse(data); if (data.errno === 0) { if (data.data) data = data.data; return true; } return false; } dataformat(data) { if (typeof data === 'string') data = JSON.parse(data); if (data.data) data = data.data; if (data.data) data = data.data; return data; } buildurl() { return this.domain + this.url; } getSign() { return JSON.stringify(this.param); } onDataSuccess(fdata, data) { if (this.cacheData && this.cacheData.set) this.cacheData.set(fdata, this.getSign()); } //如果有快取直接讀取快取,沒有才請求 execute(onComplete, ajaxOnly) { let data = null; if (!ajaxOnly && this.cacheData && this.cacheData.get) { data = this.cacheData.get(this.getSign()); if (data) { onComplete(data); return; } } super.execute(onComplete); } } class ListStore extends Store { constructor() { super(); this.key = 'DEMO_LIST'; //30分鐘過期時間 this.lifeTime = 30; } } class ListModel extends DemoModel { constructor() { super(); this.url = '/train/list'; this.type = 'GET'; // this.type = 'POST'; this.cacheData = new ListStore; } //每次資料訪問成功,錯誤碼為0時皆會執行這個回撥 onDataSuccess(fdata, data) { super.onDataSuccess(fdata, data); //開始執行自我邏輯 let o = { _indate: new Date().getTime() }; // for (let k in fdata) { // o[k] = typeof fdata[k]; // } //執行資料上報邏輯 console.log('執行資料上報邏輯', o); } } let listModel = new ListModel() export default listModel |
這裡data目錄是,然後可以看到資料請求成功,並且localstrage中有資料了:
1 2 3 4 |
data ├── abstractmodel.js ├── abstractstore.js └── demo.js |
有了資料後,我們來完善我們的列表,因為資料原因,我們這裡便不做滾動分頁功能了,一般來說列表類元件特點還是比較突出的:需要提供一個資料請求模組以及一個資料處理器,最後加一個模板就可以完成所有功能了,這裡還是先來實現列表部分程式碼,這個列表元件因為涉及的業務比較多而且每個頁面的列表變化也比較大,我們暫且將之放到ui目錄,後續看看這塊怎麼處理一下,我們依然先在這裡建立list目錄:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import React from 'react'; export default class List extends React.Component { render() { return ( <ul class="bus-list js_bus_list "> <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> <li data-index="1" data-dstation="上海南" class="bus-list-item "> <div class="bus-seat"> <span class=" fl">K1511 | 其它</span><span class=" fr">1小時49分 </span> </div> <div class="detail"> <div class="sub-list set-out"> <span class="bus-go-off">04:56</span> <span class="start"><span class="icon-circle s-icon1"> </span>上海南</span> <span class="fr price">¥24.5起</span> </div> <div class="sub-list"> <span class="bus-arrival-time">06:45</span> <span class="end"><span class="icon-circle s-icon2"> </span>杭州東</span> <span class="fr ">34張</span> </div> </div> <div class="bus-seats-info"> <span>硬座(8) </span> <span>硬臥(24) </span> <span>軟臥(2) </span> <span>無座(0) </span> </div> </li> </ul> ) } } list檔案 |
這樣一來,我們輕易的就將頁面做出來了:
接下來我們使用元件完成其功能,這裡我們將程式碼做一層分離,將列表元件分成兩部分,第一部分是不變放在UI中的部分,另一部分是我們要求傳入的模板元件,因為每個頁面的列表展示都是不一樣的,於是我們先實現外層列表,這裡就相當於要傳遞一個元件給另一個元件使用,我們簡單的嘗試了下可行性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
//業務列表專案,因為每個頁面列表展示皆不一樣,所以將這段程式碼外放 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帶來的框架理解有時候優於機械原始碼閱讀,所以在非核心專案上,我們非常建議你騷操作)
1 |
一個高階元件只是一個包裝了另一個React元件的React元件 |
上面的說法有點不好理解,這裡換個方式說,所謂高階元件,就是我們有一個元件,這個時候我們會給他傳遞各種引數,其中一個引數是另一個React元件,並且我們需要在父元件中使用他:
1 |
const EnhancedComponent = higherOrderComponent(WrappedComponent); |
這個例子依舊不夠清晰,我們再舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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') ); |
這裡會輸出(這裡說爸爸可能不太合適,這裡應該是個組合關係):
1 |
<h1>我是元件A的爸爸<div>我是元件A</div></h1> |
這裡核心概念還是這裡使用了一個繼承解決這個問題:
1 2 3 4 5 6 7 8 9 |
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結構一定是非常一致的,這裡是個規則約定,規則先與程式碼,先於框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
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中分離出來,我們先找到對應的樣式程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 |
.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就無所謂了,這裡分離出來後稍作改造即可:
1 2 3 |
//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已經完全幫我們做完了,我們只需要按照他的規則來即可,這個黑匣子裡面的東西又非常多,我們後續根據實際的專案再深入瞭解吧,一時之間也說不完,後續我們繼續研究如何使用這套程式碼相容小程式開發,以及一些工程化問題