1. 程式人生 > >當代前端應該怎麼寫這個hello world? 從DOM操作看Vue&React的前端元件化,順帶補齊React的demo 【前端優化之拆分CSS】前端三劍客的分分合合

當代前端應該怎麼寫這個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來說,暫時這個腳手架

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.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: