當代前端應該怎麼寫這個hello world? 從DOM操作看Vue&React的前端元件化,順帶補齊React的demo 【前端優化之拆分CSS】前端三劍客的分分合合
前言
大概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來說,暫時這個腳手架
① 基本配置為你寫好了,如果按照規範來可做到零配置
② 繼承了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.config.js
然後執行webpack命令便構建好了我們的檔案:
. ├── dist │ ├── index.html │ ├── index.js │ └── index.js.map ├── package-lock.json ├── package.json ├── src │ ├── data.js │ ├── index.html │ ├── index.js │ └── style.css └── webpack.config.js
可以看到,只要找到我們的入口檔案index.js,便能輕易的將所有的模組打包成一個檔案,包括樣式檔案,我們關於webpack的介紹到此為止,更詳細的介紹請看這裡:https://juejin.im/entry/5b63eb8bf265da0f98317441
我們腳手架中的webpack配置實現相對比較複雜,我們先學會基本使用,後面點再來怎麼深入這塊,因為現有的配置肯定不能滿足我們專案的需求
頁面實現
這裡為了更多的解決大家工作中會遇到到問題,我們這裡實現兩個頁面:
① 首頁,包括城市列表選擇頁面
② 列表頁面,並且會實現滾動重新整理等效果
頁面大概長這個樣子(因為這個頁面之前我就實現過,所以樣式部分我便直接拿過來使用即可,大家關注邏輯實現即可):
我們這裡先撿硬骨頭坑,直接就來實現這裡的列表頁面,這裡是之前的頁面,大家可以點選對比看看
元件拆分
react兩個核心第一是擺脫dom操作,第二是元件化開發,這兩點在小型專案中意義都不是十分大,只有經歷過多人維護的大專案,其優點才會體現出來,我們這裡第一步當然也是拆分頁面
這裡每一個模組都是一個元件,從通用性來說我們可以將之分為:
① UI元件,與業務無關的元件,只需要填充資料,比如這裡的header元件和日曆元件以及其中的列表模組也可以分離出一個元件,但看業務耦合大不大
② 頁面元件,頁面中的元素
工欲善其事必先利其器,所以我們這裡先來實現幾個元件模組,這裡首先是對於新人比較難啃的日曆模組,我們程式碼過程中也會給大家說目錄該如何劃分
日曆元件
日了元件是相對比較複雜的元件了,單單這個元件又可以分為:
① 月元件,處理月部分
② 日部分,處理日期部分
能夠將這個元件做好,基本對元件系統會有個初步瞭解了,我們這裡首先來實現日曆-日部分,這裡我們為專案建立一個src/ui/calendar目錄,然後建立我們的檔案:
.
├── index.js
└── ui
└── calendar
└── calendar.js
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; ReactDOM.render(<Calendar/>, document.getElementById('root'));
import React from 'react'; export default class Calendar extends React.Component { render() { return ( <div>日曆</div> ) } }
這個時候再執行以下命令便會編譯執行:
npm run start
雖然不知為什麼,但是我們的程式碼運行了,大概就是這麼一個情況:),接下來我們開始來完善我們的程式碼,日曆元件,我們外層至少得告訴日曆年和月,日曆才好做展示,那麼這裡出現了第一個問題,我們怎麼將屬性資料傳給元件呢?這裡我們來簡單描述下react中的state與props
state是react中的狀態屬性,定義一個正確的狀態是寫元件的第一步,state需要代表元件UI的完整狀態集,任何UI的改變都應該從state體現出來,判斷元件中一個變數是不是該作為state有以下依據:
① 這個變數是否是從父元件獲取,如果是,那麼他應該是一個屬性
② 這個變數是否在元件的整個生命週期不會變化,如果是,那麼他也是個屬性
③ 這個變數是否是通過其他狀態或者屬性計算出來的,如果是,那麼他也不是一個狀態
④ 狀態需要在元件render時候被用到
這裡的主要區別是state是可變的,而props是隻讀的,如果想要改變props,只能通過父元件修改,就本章內容,我們將年月等設定為屬性,這裡先忽略樣式的處理,簡單幾個程式碼,輪廓就出來了,這裡有以下變化:
① 新增common資料夾,放了工具類函式
② 新增static目錄存放css,這裡的css我們後續會做特殊處理,這裡先不深入
於是,我們目錄變成了這樣:
. ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── index.html │ └── static │ └── css │ ├── global.css │ └── index.css ├── src │ ├── common │ │ └── utils.js │ ├── index.js │ └── ui │ └── calendar │ ├── calendar.js │ ├── day.js │ └── month.js
我們將calendar程式碼貼出來看看:
import React from 'react'; import dateUtils from '../../common/utils' export default class Calendar extends React.Component { render() { let year = this.props.year; let month = this.props.month; let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; //獲取當前日期資料 let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); return ( <ul className="cm-calendar "> <ul className="cm-calendar-hd"> { weekDayArr.map((data, i) => { return <li className="cm-item--disabled">{data}</li> }) } </ul> </ul> ) } }
樣式基本出來了:
這個時候我們需要將月元件實現了,這裡貼出來第一階段的完整程式碼:
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; ReactDOM.render( <Calendar year="2018" month="12"/>, document.getElementById('root') );
1 let isDate = function (date) { 2 return date && date.getMonth; 3 }; 4 5 //相容小程式日期 6 let getDate = function(year, month, day) { 7 if(!day) day = 1; 8 return new Date(year, month, day); 9 } 10 11 let isLeapYear = function (year) { 12 //傳入為時間格式需要處理 13 if (isDate(year)) year = year.getFullYear() 14 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 15 return false; 16 }; 17 18 let getDaysOfMonth = function (date) { 19 var month = date.getMonth() + 1; //注意此處月份要加1 20 var year = date.getFullYear(); 21 return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; 22 } 23 24 let getBeginDayOfMouth = function (date) { 25 var month = date.getMonth(); 26 var year = date.getFullYear(); 27 var d = getDate(year, month, 1); 28 return d.getDay(); 29 } 30 31 let getDisplayInfo = function(date) { 32 if (!isDate(date)) { 33 date = getDate(date) 34 } 35 var year = date.getFullYear(); 36 37 var month = date.getMonth(); 38 var d = getDate(year, month); 39 40 //這個月一共多少天 41 var days = getDaysOfMonth(d); 42 43 //這個月是星期幾開始的 44 var beginWeek = getBeginDayOfMouth(d); 45 46 return { 47 year: year, 48 month: month, 49 days: days, 50 beginWeek: beginWeek 51 } 52 } 53 54 let isOverdue = function isOverdue(year, month, day) { 55 let date = new Date(year, month, day); 56 let now = new Date(); 57 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 58 return date.getTime() < now.getTime(); 59 } 60 61 let isToday = function isToday(year, month, day, selectedDate) { 62 let date = new Date(year, month, day); 63 return date.getTime() == selectedDate; 64 } 65 66 let dateUtils = { 67 isLeapYear, 68 getDaysOfMonth, 69 getBeginDayOfMouth, 70 getDisplayInfo, 71 isOverdue, 72 isToday 73 }; 74 75 export default dateUtils;utils.js
1 import React from 'react'; 2 import dateUtils from '../../common/utils' 3 import CalendarMonth from './month' 4 5 6 export default class Calendar extends React.Component { 7 render() { 8 let year = this.props.year; 9 let month = this.props.month; 10 let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; 11 //獲取當前日期資料 12 let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); 13 return ( 14 <ul className="cm-calendar "> 15 <ul className="cm-calendar-hd"> 16 { 17 weekDayArr.map((data, index) => { 18 return <li key={index} className="cm-item--disabled">{data}</li> 19 }) 20 } 21 </ul> 22 <CalendarMonth year={year} month={month}/> 23 </ul> 24 ) 25 } 26 }calendar.js
1 import React from 'react'; 2 import dateUtils from '../../common/utils' 3 import CalendarDay from './day' 4 5 export default class CalendarMonth extends React.Component { 6 7 //獲取首次空格 8 _renderBeginDayOfMouth(beforeDays) { 9 let html = []; 10 for (let i = 0; i < beforeDays; i++) { 11 html.push(<li key={i} className="cm-item--disabled"></li>); 12 } 13 return html; 14 } 15 16 //和_renderBeginDayOfMouth類似可以重構掉 17 _renderDays(year, month, days) { 18 let html = []; 19 for(let i = 0; i < days; i++) { 20 html.push( 21 <CalendarDay key={i} year={year} month={month} day={i} /> 22 ) 23 } 24 return html; 25 } 26 27 render() { 28 let year = this.props.year; 29 let month = this.props.month; 30 let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); 31 console.log(displayInfo) 32 return ( 33 <ul className="cm-calendar-bd "> 34 <h3 className="cm-month calendar-cm-month js_month">{year + '-' + month}</h3> 35 36 <ul className="cm-day-list"> 37 { this._renderBeginDayOfMouth( displayInfo.beginWeek) } 38 { this._renderDays(year, month, displayInfo.days) } 39 </ul> 40 </ul> 41 ) 42 } 43 }month.js
1 import React from 'react'; 2 import dateUtils from '../../common/utils' 3 4 export default class CalendarDay extends React.Component { 5 6 7 render() { 8 let year = this.props.year; 9 let month = this.props.month; 10 let day = this.props.day; 11 12 let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; 13 14 return ( 15 <li year={year} month={month} day={day} > 16 <div className="cm-field-wrapper "> 17 <div className="cm-field-title">{day + 1}</div> 18 </div> 19 </li> 20 ) 21 } 22 }day.js
這段程式碼的效果是:
基礎框架結構出來後,我們就需要一點一點向上面加肉了,首先我們加一個選中日期,需要一點特效,這裡稍微改下程式碼,具體各位去GitHub上面看程式碼了,這段程式碼就不貼出來了,因為我們這裡是寫demo,這個日曆元件功能完成60%即可,不會全部完成,這裡我們做另一個操作,就是在頁面上新增一個上一個月下一個月按鈕,並且點選日曆時候在控制檯將當前日期打印出來即可,這裡是效果圖:
這個時候我們首先為左右兩個按鈕新增事件,這裡更改下程式碼變成了這個樣子,這裡貼出階段程式碼,完整程式碼請大家在git上檢視
1 import React from 'react'; 2 import ReactDOM from 'react-dom'; 3 import Calendar from './ui/calendar/calendar'; 4 5 class CalendarMain extends React.Component { 6 constructor(props) { 7 super(props); 8 let today = new Date().getTime(); 9 this.state = { 10 month: 12, 11 selectdate: today 12 }; 13 } 14 preMonth() { 15 this.setState({ 16 month: this.state.month - 1 17 }); 18 } 19 nextMonth() { 20 this.setState({ 21 month: this.state.month + 1 22 }); 23 } 24 ondayclick(year, month, day) { 25 26 this.setState({ 27 selectdate: new Date(year, parseInt(month) - 1, day).getTime() 28 }) 29 30 } 31 render() { 32 // today = new Date(today.getFullYear(), today.getMonth(), 1); 33 let selectdate = this.state.selectdate;; 34 let month = this.state.month; 35 return ( 36 <div className="calendar-wrapper-box"> 37 <div className="box-hd"> 38 <span className="fl icon-back js_back " onClick={this.preMonth.bind(this)} ></span> 39 <span className="fr icon-next js_next" onClick={this.nextMonth.bind(this)} ></span> 40 </div> 41 <Calendar ondayclick={this.ondayclick.bind(this)} year="2018" month={month} selectdate={selectdate} /> 42 </div> 43 ) 44 } 45 } 46 47 ReactDOM.render( 48 <CalendarMain /> 49 50 , 51 document.getElementById('root') 52 );index.js
1 let isDate = function (date) { 2 return date && date.getMonth; 3 }; 4 5 //相容小程式日期 6 let getDate = function(year, month, day) { 7 if(!day) day = 1; 8 return new Date(year, month, day); 9 } 10 11 let isLeapYear = function (year) { 12 //傳入為時間格式需要處理 13 if (isDate(year)) year = year.getFullYear() 14 if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) return true; 15 return false; 16 }; 17 18 let getDaysOfMonth = function (date) { 19 var month = date.getMonth() + 1; //注意此處月份要加1 20 var year = date.getFullYear(); 21 return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][parseInt(month) - 1]; 22 } 23 24 let getBeginDayOfMouth = function (date) { 25 var month = date.getMonth(); 26 var year = date.getFullYear(); 27 var d = getDate(year, month, 1); 28 return d.getDay(); 29 } 30 31 let getDisplayInfo = function(date) { 32 if (!isDate(date)) { 33 date = getDate(date) 34 } 35 var year = date.getFullYear(); 36 37 var month = date.getMonth(); 38 var d = getDate(year, month); 39 40 //這個月一共多少天 41 var days = getDaysOfMonth(d); 42 43 //這個月是星期幾開始的 44 var beginWeek = getBeginDayOfMouth(d); 45 46 return { 47 year: year, 48 month: month, 49 days: days, 50 beginWeek: beginWeek 51 } 52 } 53 54 let isOverdue = function isOverdue(year, month, day) { 55 let date = new Date(year, month, day); 56 let now = new Date(); 57 now = new Date(now.getFullYear(), now.getMonth(), now.getDate()); 58 return date.getTime() < now.getTime(); 59 } 60 61 let isToday = function isToday(year, month, day, selectedDate) { 62 let date = new Date(year, month, day); 63 let d = new Date(selectedDate); 64 d = new Date(d.getFullYear(), d.getMonth(), d.getDate()); 65 selectedDate = d.getTime(); 66 return date.getTime() == selectedDate; 67 } 68 69 let dateUtils = { 70 isLeapYear, 71 getDaysOfMonth, 72 getBeginDayOfMouth, 73 getDisplayInfo, 74 isOverdue, 75 isToday 76 }; 77 78 export default dateUtils;utils.js
import React from 'react'; import dateUtils from '../../common/utils' import CalendarMonth from './month' export default class Calendar extends React.Component { render() { let year = this.props.year; let month = this.props.month; let weekDayArr = ['日', '一', '二', '三', '四', '五', '六']; //獲取當前日期資料 let displayInfo = dateUtils.getDisplayInfo(new Date(year, month, 0)); return ( <ul className="cm-calendar "> <ul className="cm-calendar-hd"> { weekDayArr.map((data, index) => { return <li key={index} className="cm-item--disabled">{data}</li> }) } </ul> <CalendarMonth ondayclick={this.props.ondayclick} selectdate={this.props.selectdate} year={year} month={month}/> </ul> ) } }calendar.js
1 import React from 'react'; 2 import dateUtils from '../../common/utils' 3 import CalendarDay from './day' 4 5 export default class CalendarMonth extends React.Component { 6 7 //獲取首次空格 8 _renderBeginDayOfMouth(beforeDays) { 9 let html = []; 10 for (let i = 0; i < beforeDays; i++) { 11 html.push(<li key={i} className="cm-item--disabled"></li>); 12 } 13 return html; 14 } 15 16 //和_renderBeginDayOfMouth類似可以重構掉 17 _renderDays(year, month, days) { 18 let html = []; 19 for(let i = 1; i <= days; i++) { 20 html.push( 21 <CalendarDay ondayclick={this.props.ondayclick} selectdate={this.props.selectdate} key={i} year={year} month={month} day={i} /> 22 ) 23 } 24 return html; 25 } 26 27 render() { 28 let year = this.props.year; 29 let month = this.props.month; 30 31 let name = new Date(year, parseInt(month) - 1, 1); 32 name = name.getFullYear() + '-' + (name.getMonth() + 1); 33 34 let displayInfo = dateUtils.getDisplayInfo(new Date(year, parseInt(month) - 1), 1); 35 console.log(displayInfo) 36 return ( 37 <ul className="cm-calendar-bd "> 38 <h3 className="cm-month calendar-cm-month js_month">{name}</h3> 39 40 <ul className="cm-day-list"> 41 { this._renderBeginDayOfMouth( displayInfo.beginWeek) } 42 { this._renderDays(year, month, displayInfo.days) } 43 </ul> 44 </ul> 45 ) 46 } 47 }month.js
1 import React from 'react'; 2 import dateUtils from '../../common/utils' 3 4 export default class CalendarDay extends React.Component { 5 onClick(e) { 6 let year = this.props.year; 7 let month = this.props.month; 8 let day = this.props.day; 9 10 this.props.ondayclick(year, month, day) 11 } 12 13 render() { 14 let year = this.props.year; 15 let month = this.props.month; 16 let day = this.props.day; 17 let selectdate = this.props.selectdate; 18 19 let klass = dateUtils.isOverdue(year, parseInt(month) - 1, day) ? 'cm-item--disabled' : ''; 20 21 if(dateUtils.isToday(year, parseInt(month) - 1, day, selectdate)) 22 klass += ' active ' 23 24 return ( 25 <li onClick={this.onClick.bind(this)} className={klass} year={year} month={month} day={day} > 26 <div className="cm-field-wrapper "> 27 <div className="cm-field-title">{day }</div> 28 </div> 29 </li> 30 ) 31 } 32 }day.js
至此,我們日曆一塊的基本程式碼完成,完成度應該有60%,我們繼續接下來的元件編寫
header元件
日曆元件結束後,我們來實現另一個UI類元件-header元件,我們這裡實現的header算是比較中規中矩的頭部元件,複雜的情況要考慮hybrid情況,那就會很複雜了,話不多說,我們先在ui目錄下建立一個header目錄,寫下最簡單的程式碼後,我們的index:
ReactDOM.render( <Header title="我是標題" /> , document.getElementById('root') );
然後是我們的header元件:
1 import React from 'react'; 2 export default class Header extends React.Component { 3 render() { 4 return ( 5 <div class="cm-header"> 6 <span class=" cm-header-icon fl js_back"> 7 <i class="icon-back"></i> 8 </span> 9 <h1 class="cm-page-title js_title"> 10 {this.props.title} 11 </h1> 12 </div> 13 ) 14 } 15 }
於是header部分的框架就出來了,這個時候我們來將之加強,這裡也不弄太強,就將後退的事件加上,以及左邊按鈕加上對應的按鈕和事件,這裡改造下index和header程式碼:
import React from 'react'; import ReactDOM from 'react-dom'; import Calendar from './ui/calendar/calendar'; import Header from './ui/header/header'; class CalendarMain extends React.Component { constructor(props) { super(props); let today = new Date().getTime(); this.state = { month: