說明
React作為Facebook 內部開發 Instagram 的專案中,是一個用來構建使用者介面的優秀 JS 庫,於 2013 年 5 月開源。作為前端的三大框架之一,React的應用可以說是非常的廣泛。這裡講一個react服務端渲染的框架-next.js踩坑過程。
技術棧
react、next.js、ant design、axios
複製程式碼
大綱
按照以下思路來寫:
react基本語法
react基本語法參照react文件,這裡發放一個連結doc.react-china.org/。
以下是React重要的部分
- JSX – 允許我們編寫類似HTML的語法,轉換為JavaScript物件;
- 虛擬DOM – 實際DOM的JavaScript表示;
- React.Component – 建立新元件的方式;
- render(方法) – 渲染元件;
- ReactDOM.render – 用於將模板轉為 HTML 語言,並插入指定的 DOM 節點;
- state – 元件的內部資料儲存(物件),型別理解vue的data;
- constructor(this.state) – 建立元件初始 state(狀態) 的方式;
- setState – 一種輔助方法,用於更新元件的 state(狀態) 並重新渲染,類似微信小程式;
- props – 從父元件傳遞給子元件的資料;
- propTypes – 允許您控制傳遞給子元件的某些 props(屬性) 的存在或型別;
- defaultProps – 允許您為元件設定預設 props(屬性) ;
- className - 節點的class,css標記
- style jsx樣式控制
jsx語法
直接寫在 JavaScript 語言之中,不加任何引號,它允許 HTML 與 JavaScript 的混寫。
//demo-1.js
import React from 'react';
class Demo1 extends React.Component{
render(){
const lists = ['我是誰','我來自哪裡','我要到哪兒去'];
return(
<div className="list">
<ul>
{
lists.map((list,index)=>{
return(
<li key={index}>{list}</li>
)
})
}
</ul>
</div>
)
}
}
export default Demo1;
複製程式碼
上面程式碼體現了 JSX 的基本語法規則:遇到 HTML 標籤(以 < 開頭),就用 HTML 規則解析;遇到程式碼塊(以 { 開頭),就用 JavaScript 規則解析,其中注意一點react沒有vue中v-for遍歷,需要自己寫遍歷。上面程式碼的執行結果如下:
元件及元件傳值
//demo-2.js
import React from 'react';
class MyComponent extends React.Component{
render(){
return (
<h1>{this.props.name}</h1>
)
}
}
class Demo2 extends React.Component{
render(){
const lists = ['我是誰','我來自哪裡','我要到哪兒去'];
return(
<div className="list">
<ul>
{
lists.map((list,index)=>{
return(
<li key={index}>
<MyComponent name={list}></MyComponent>
</li>
)
})
}
</ul>
</div>
)
}
}
export default Demo2;
複製程式碼
React 允許將程式碼封裝成元件(component),然後像插入普通 HTML 標籤一樣,在網頁中插入這個元件。以上程式碼中MyComponent即為元件,需要注意的一點元件類的第一個字母必須大寫,否則會報錯,元件類只能包含一個頂層標籤,否則也會報錯。vue通過子元件中的props來傳遞資料,而React則是用this.props.name來傳遞。下圖為渲染頁面效果:
React之ref(獲取真實的DOM節點)
- 同vue的ref作用一樣,元件並不是真實的DOM節點,而是存在於記憶體之中的一種資料結構,叫做虛擬 DOM。
- 只有當它插入文件以後,才會變成真實的 DOM 。根據 React的設計,所有的DOM 變動,都先在虛擬 DOM上發生,然後再將實際發生變動的部分,反映在真實 DOM上,這種演算法叫做 DOM diff ,它可以極大提高網頁的效能表現。
- 有時需要從元件獲取真實 DOM 的節點,這時就要用到 ref 屬性。
//dem0-3.
import React from 'react';
class Demo3 extends React.Component{
handleClick(){
this.refs.myTextInput.focus();
}
render(){
return(
<div>
<input type="text" ref="myTextInput" />
<br/>
<input type="button" value="Focus the text input" onClick={this.handleClick.bind(this)} />
</div>
)
}
}
export default Demo3;
複製程式碼
在這裡需要注意的是:
- React的事件中如果不用剪頭函式,那就要用bind來繫結this,否則需要在constructor建構函式裡呼叫this.handleClick = this.handleClick.bind(this),這裡建議用onClick={this.handleClick.bind(this)}。
- 由於 this.refs.[refName] 屬性獲取的是真實 DOM ,所以必須等到虛擬 DOM 插入文件以後,才能使用這個屬性,否則會報錯。上面程式碼中,通過為元件指定 Click 事件的回撥函式,確保了只有等到真實 DOM 發生 Click 事件之後,才會讀取 this.refs.[refName] 屬性。
React之this.state以及點選事件
React中的state就相當於vue裡的data資料儲存,而小程式的this.setData就和React的this.setState類似。
//demo-4.js
import React from 'react';
class Demo4 extends React.Component{
// constructor(props){
// super(props)
// this.state = {
// liked:false
// }
// }
state = {
liked: false
}
handleClick(){
this.setState({liked: !this.state.liked});
}
render(){
let text = this.state.liked ? '喜歡' : '不喜歡';
return(
<div>
<p onClick={this.handleClick.bind(this)}>
你 {text} 這個點選.
</p>
</div>
)
}
}
export default Demo4;
複製程式碼
上面程式碼是一個 LikeButton 元件,它的 constructor 方法(或者直接state宣告)用於定義初始狀態,也就是一個物件,這個物件可以通過 this.state 屬性讀取。當使用者點選元件,導致狀態變化,this.setState 方法就修改狀態值,每次修改以後,自動呼叫 this.render 方法,再次渲染元件。
由於 this.props 和 this.state 都用於描述元件的特性,可能會產生混淆。一個簡單的區分方法是,this.props 表示那些一旦定義,就不再改變的特性,而 this.state 是會隨著使用者互動而產生變化的特性。
利用input的onChange事件實現簡單的雙向繫結
vue裡面v-model一鍵實現的事情React沒有,我們可以利用input元件的onChange事件來簡單實現它,直接上程式碼。
//Demo5.js
import React from 'react';
class Demo5 extends React.Component{
// constructor(props){
// super(props)
// this.state = {
// text:''
// }
// }
state = {
text: ''
}
handleChange(e){
this.setState(
{text: e.target.value}
);
}
render(){
return(
<div>
<p>
你輸入了{this.state.text}
</p>
<input type="text" placeholder="請輸入..." onChange={this.handleChange.bind(this)}/>
</div>
)
}
}
export default Demo5;
複製程式碼
元件的生命週期
一個React元件的生命週期分為三個部分:掛載期(Mounting)、存在更新期(Updating)和銷燬時(Unmounting)。
-
Mounting:已插入真實 DOM
當一個元件例項被建立並且插入到DOM中,以下鉤子將被呼叫 constructor() 繼承react的props,和設定state的初始化。
constructor(props) {
super(props); //不能缺少
this.state = {
color: props.initialColor
};
}
複製程式碼
- Updating:當元件的props和state發生改變時,重新渲染頁面是呼叫的。
- Unmounting:元件從DOM中移動時呼叫的。
React 為每個狀態都提供了兩種處理函式,will 函式在進入狀態之前呼叫,
did 函式在進入狀態之後呼叫,三種狀態共計五種處理函式。
複製程式碼
- componentWillMount()
- componentDidMount()
- componentWillUpdate(object nextProps, object nextState)
- componentDidUpdate(object prevProps, object prevState)
- componentWillUnmount()
事件
- onClick-點選事件
- onSubmit-表單提交事件,可以配合antdesign的表單校驗
- onChange-input框資料變化,可實現資料的雙向繫結(react裡沒有vue的v-modal)
style jsx
我們可以通過普通樣式(className)和行內樣式(LineStyle)控制React元件的樣式:
- 使用className設定樣式(與CSS的選擇器相同)
import React from 'react';
// import styles from '../../static/css/demo.css'
class Demo6 extends React.Component{
// constructor(props){
// super(props)
// this.state = {
// text:''
// }
// }
state = {
text: ''
}
handleChange(e){
this.setState(
{text: e.target.value}
);
}
render(){
return(
<div>
<p className="style">
你輸入了{this.state.text}
</p>
<input className="input" type="text" placeholder="請輸入..." onChange={this.handleChange.bind(this)}/>
<style jsx>
{
` .style {
background: #dcdcdc;
font-size: 20px;
height:40px;
line-height:40px;
padding:0 2%;
}
.input{
width:96%;
height:40px;
padding:0 2%;
}
`
}
</style>
</div>
)
}
}
export default Demo6;
複製程式碼
為什麼要用next
Next.js是一個基於React的一個服務端渲染簡約框架。它使用React語法,可以很好的實現程式碼的模組化,有利於程式碼的開發和維護。
-
預設服務端渲染模式,以檔案系統為基礎的客戶端路由,類似nuxt.js;
-
與nuxt.js相同,pages檔案目錄即路由;
-
以webpack的熱替換為基礎的開發環境(npm run dev);
next建立的專案目錄結構
next路由
- 預設從 pages 目錄下取頁面進行渲染返回給前端展示,路由可以使用Link(無重新整理頁面)或者a標籤(重新整理頁面)
//pages/router.js
import Link from 'next/link';
<Link prefetch href='/about?id=11'>
<a >Link</a>
</Link>
<a href="/about?id=11">Link</a>
複製程式碼
- 接收引數:getInitialProps方法context引數或者{props.query.id},同時在getInitialProps 裡面獲取資料,服務端渲染會提前執行這個方法獲取資料渲染到模板,這裡用到axios庫,前後端都可以使用
//pages/test.js http://localhost:3000/test
import React from 'react';
import MyHeader from '../components/MyHeader';
import axios from '../lib/axios';
import { bmobConfigDevdev, bmobConfig } from '../lib/config';
import urls from '../lib/urls';
import { Table, Form, Icon, Input, Button } from 'antd';
const FormItem = Form.Item;
const config1 = {
headers: {
'X-Bmob-Application-Id': bmobConfig.applicationId,
'X-Bmob-REST-API-Key': bmobConfig.restApiKey,
'Content-Type': 'application/json'
}
};
const config2 = {
headers: {
'X-Bmob-Application-Id': bmobConfigDevdev.applicationId,
'X-Bmob-REST-API-Key': bmobConfigDevdev.restApiKey,
'Content-Type': 'application/json'
}
};
const myStyle = {
body: {
width: '1200px',
margin: '10px auto'
}
}
const columns = [{
title: 'id',
dataIndex: 'objectId',
key: 'objectId',
}, {
title: '姓名',
dataIndex: 'uname',
key: 'uname',
}, {
title: '電話',
dataIndex: 'uphone',
key: 'uphone',
}, {
title: '地址',
dataIndex: 'address',
key: 'address',
}];
const dataSource = [{
key: '1',
name: '胡彥斌',
age: 32,
address: '西湖區湖底公園1號'
}, {
key: '2',
name: '胡彥祖',
age: 42,
address: '西湖區湖底公園1號'
}];
//封裝form元件
const MyForm = (props) => (
<Form layout="inline" onSubmit={this.handleSubmit}>
<FormItem>
<Button
type="primary"
htmlType="submit"
>
增加資料
</Button>
</FormItem>
</Form>
)
//宣告類
class addDeleteSelectUpdate extends React.Component {
render() {
return (
<MyHeader title="使用者列表">
<div style={myStyle.body}>
<p className="about">傳遞的引數為:{this.props.query}</p>
<p className="about">增加資料</p>
<MyForm ></MyForm>
<p className="about">獲取資料{this.props.dataToString}</p>
<Table dataSource={this.props.data} columns={columns} size="small" />
<div className="">
<p className="about">遍歷</p>
<ul>
{
this.props.data.map(els => {
return (
<li key={els.objectId}>
{els.address}
</li>
)
})
}
</ul>
</div>
<style jsx>
{`
.about {color:#666;padding:10px}
`}
</style>
</div>
</MyHeader>
)
}
}
/**
* @description 獲取初始資料
*/
const isServer = typeof window === 'undefined'
addDeleteSelectUpdate.getInitialProps = async function (context) {
console.log(context.query);
const res = await axios.get(urls.userList, config1)
console.log(res.data);
var query = context.query;
return {
data: res.data.results,
dataToString: JSON.stringify(res.data.results),
query: JSON.stringify(context.query)
}
}
export default addDeleteSelectUpdate;
複製程式碼
資料請求-axios配置
Axios 是一個基於 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。
複製程式碼
axios具有以下幾個特點:
- 既可以在服務端使用又可以在瀏覽器端使用;
- 支援 Promise API(async await,解決地獄回撥);
- 攔截請求和響應(401跳轉登入等操作);
- 轉換請求資料和響應資料;
- 自動轉換 JSON 資料。
以下為axios配置程式碼:
//lib/axios.js
import axios from 'axios';
import NProgress from 'nprogress';
axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded';
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.withCredentials = true;
axios.defaults.timeout = 5000;
//請求之前
axios.interceptors.request.use((config)=>{
if(process.broswer) NProgress.start()//加一個loading
return config;
});
//
axios.interceptors.response.use(
response => {
// console.log('-------axiosResponse---------')
if(process.broswer) NProgress.done()
return response;
},
error => {
// console.log('-------axiosError---------:');
// console.log(error.response.status==401)
if(process.broswer) NProgress.done()
if(error.response&&error.response.status==401){
window.location.href="/login";//登入失效跳轉
}
return Promise.reject(error);
}
);
export default axios;
複製程式碼
使用時在頁面引入axios配置檔案axios.js,具體的axios配置可以參考link-axios文件。
//pages/login.js
import axios from '../lib/axios';
...
axios.post(urls.login,qs.stringify(obj))
.then(res=>{
console.log(res);
Router.push('/contract/list');
})
.catch(error=>{
console.log(error);
})
...
複製程式碼
資料請求-開發環境代理
前後端分離以後,前端最常見的一個問題就是跨域,之前的om專案基於nuxt可以配置proxy,在next.js裡需要用到express和http-proxy-middleware在服務端(node)端做一層轉發,程式碼配置如下:
//server.js
var express =require('express');
var next = require('next');
const devProxy = {
'/api': {
target: 'http://39.107.58.75:9092/',// 目標伺服器 host
pathRewrite: {'^/api': '/'}, // 重寫請求,比如我們源訪問的是api/login,那麼請求會被解析為/www/login
changeOrigin: true,// 預設false,是否需要改變原始主機頭為目標URL
},
'/jt':{
target: 'https://api.dededemo.com/plus/weapp.php?action=index&domain=http://23jt.net&skin=weapp&ver=3.0&page=1',// 目標伺服器 host
// pathRewrite: {'^/api': '/'}, // 重寫請求,比如我們源訪問的是api/login,那麼請求會被解析為/www/login
changeOrigin: true,// 預設false,是否需要改變原始主機頭為目標URL
}
}
const port = parseInt(process.env.PORT, 10) || 3000
const env = process.env.NODE_ENV
const dev = env !== 'production'
const app = next({
dir: '.', // base directory where everything is, could move to src later
dev
})
const handle = app.getRequestHandler()
let server
app
.prepare()
.then(() => {
server = express()
// Set up the proxy.
if (dev && devProxy) {
const proxyMiddleware = require('http-proxy-middleware')
Object.keys(devProxy).forEach(function (context) {
server.use(proxyMiddleware(context, devProxy[context]))
})
}
// Default catch-all handler to allow Next.js to handle all other routes
server.all('*', (req, res) => {
req.headers['Content-Type'] = 'application/x-www-form-urlencoded';
req.headers['X-Requested-With'] = 'XMLHttpRequest';
handle(req, res);
// console.log(req.body);
}
)
server.listen(port, err => {
if (err) {
throw err
}
console.log(`> Ready on port ${port} [${env}]`)
})
})
.catch(err => {
console.log('An error occurred, unable to start the server')
console.log(err)
})
複製程式碼
配置完server.js後還需要在packge.json中配置啟動指令碼:
"devdev": "cross-env NODE_ENV=development PORT=9527 node server.js",
複製程式碼
注意上線後需要在ngnix裡配置反向代理,就不存在跨域的問題。
在next.js裡使用ant design
- babel配置
//.babelrc
{
"presets": [
"next/babel"
],
"plugins": [
[
"module-resolver", {
"root": ["."],
"alias": {
"styles": "./styles"
},
"cwd": "babelrc"
}],
["import", { "libraryName": "antd" }]
],
"ignore": []
}
複製程式碼
- 引用ant design元件,需要用到ant design什麼元件按照自己需要引用即可,各個元件使用方法參考ant design的link-官方文件
import {Form, Icon,Input, Button, Checkbox, message} from 'antd';
複製程式碼
部署一個next.js專案
Next.js 專案的部署,需要一個 Node.js的伺服器。 在開發環境伺服器的入口檔案就使用上文中提到的 server.js,在 server.js 裡新增了針對部署環境的選擇,程式碼如下
const dev = process.env.NODE_ENV !== 'production'
複製程式碼
為了區分部署環境,我們需要在 package.json 中修改 script 屬性如下:
"scripts": {
"dev": "next",
"devdev": "cross-env NODE_ENV=development PORT=9527 node server.js",
"build": "next build",
"start": "next start",
"generate": "next build && next export",
"export-h":"next export -h"
},
複製程式碼
其中,build 命令是用於打包專案,start 命令是用於生產環境部署,devdev 命令是用於本地開發,PORT為啟動埠。
注意的幾個點
- 增加第三方npm包必須加上--save關鍵字,會pakage.json檔案里加入依賴包;
//如增加js-md5庫
$ npm install js-md5 --save
複製程式碼
- 元件名稱必須以大寫字母開頭,如MyHeader;
- React的事件中如果不用剪頭函式,那就要用bind來繫結this,否則需要在constructor建構函式裡呼叫this.handleClick = this.handleClick.bind(this),這裡建議用onClick={this.handleClick.bind(this)}。
總結
只要熟悉React開發,上手一個Next專案很容易,Next 讓前端專案開發效率更高。
參考資料
- ant design官方文件:ant.design/docs/react/…
- ant design腳手架市場:scaffold.ant.design/#/?tags=ant…
- ant design使用小結:www.cnblogs.com/wx1993/p/65…
- 用Next.js快速上手React伺服器渲染 segmentfault.com/p/121000001…
- React 服務端渲染框架 Next.js 基於 Gank api 實戰 orangexc.xyz/2017/10/19/…
- 基於 React 的通用框架 Next.js:服務端 React(axios) www.zcfy.cc/article/rea…
- 使用next.js完成從開發到部署 juejin.im/post/5b0807…
- react學習線路圖 juejin.im/entry/5b49a…
- React使用Next.js作伺服器端渲染 blog.csdn.net/xcg132566/a…
- css modules 用法 www.ruanyifeng.com/blog/2016/0…
更多
更多前端技術請訪問我的部落格:hurely.github.io