最近開發一個全新AB測試平臺,思考了下正好可使用react技術開發。css
首先遇到一個概念,redux。這貨還真很差理解,大致的理解:Store包含全部數據,視圖觸發一個Action,Store收到Action後,返回一個新的 State,這樣視圖就發生變化,State計算過程叫作 Reducer,Reducer其實就是一個處理數據的函數,接受 Action和 當前State做爲參數,返回一個新的 State。
明白這個後,就能夠開始實踐了。html
對於我這方面沒搞過的菜鳥,還真是不容易。接下來講下做爲新手如何實踐的。前端
"devDependencies": {
"babel-core": "^6.26.0",
"babel-eslint": "^8.2.2",
"babel-loader": "^7.1.2",
"babel-plugin-import": "^1.6.6",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"css-loader": "^0.28.7",
"eslint": "^4.18.2",
"eslint-config-airbnb": "^16.1.0",
"eslint-loader": "^2.0.0",
"eslint-plugin-import": "^2.9.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.7.0",
"extract-text-webpack-plugin": "^3.0.2",
"html-webpack-plugin": "^3.0.4",
"less": "^2.7.3",
"less-loader": "^4.0.6",
"style-loader": "^0.19.1",
"url-loader": "^1.0.1",
"webpack": "^3.1.0"
},
"dependencies": {
"normalize.css": "^8.0.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-redux": "^5.0.7",
"react-router-dom": "^4.2.2",
"redux": "^3.7.2"
}
複製代碼
dependencies 中引入的依賴包,是react的標配了,不用解釋。
devDependencies 中引入了 webpack,babel,babel插件,eslint語法檢測,eslint配置包airbnb,html模板資源替換插件 html-webpack-plugin,css提取插件 extract-text-webpack-plugin,less編譯相關插件,圖片等靜態資源路徑處理插件 url-loader。
這裏做爲新手,通常都是參考網上的配置,好比我就是github上找了個項目,摸索一下。推薦一本教程書《React全棧》,做者寫的很詳細,對入門絕對有幫助。
至此,基本依賴包已加載完。node
接下來就是webpack的配置了,先上代碼react
const path = require('path');
const webpack = require('webpack');
// html中替換編譯後的js
const HtmlwebpackPlugin = require('html-webpack-plugin');
// css提取
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ROOT_PATH = path.resolve(__dirname);
const APP_PATH = path.resolve(ROOT_PATH, '../src');
const BUILD_PATH = path.resolve(ROOT_PATH, '../build');
module.exports = {
entry: {
entry: path.resolve(APP_PATH, './entry.jsx'),
vendor: ['react', 'react-dom', 'pace']
},
output: {
filename: '[name].js',
path: BUILD_PATH,
chunkFilename: '[name].js',
publicPath: '../'
},
devtool: 'eval-source-map',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
query: {
presets: ['es2015', 'react', 'stage-0'],
plugins: ['syntax-dynamic-import', ['import', { libraryName: 'antd', style: 'css' }]]
}
}
]
},
{
test: /\.(css|less)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
'less-loader'
]
}),
exclude: /node_modules/
},
{
test: /\.(css)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader'
]
}),
include: /node_modules/
},
{
test: /\.(jpg|jpeg|png|svg|gif|bmp)/i,
use: [
'url-loader?limit=5000&name=img/[name].[sha512:hash:base64:8].[ext]'
]
},
{
test: /\.(woff|woff2|ttf|eot)($|\?)/i,
use: [
'url-loader?limit=5000&name=fonts/[name].[sha512:hash:base64:8].[ext]'
]
}
]
},
resolve: {
extensions: ['.js', '.jsx', '.less', '.css', '.png', '.jpg', '.svg', '.gif', '.eot'],
alias: {
pace: path.resolve(ROOT_PATH, '../src/plugins/pace/index.js'),
ImagesPath: path.resolve(ROOT_PATH, '../src/')
}
},
devServer: {
historyApiFallback: true,
hot: true,
inline: true,
progress: true
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['commons', 'vendor'],
minChunks: 2
}),
new ExtractTextPlugin('commons.css', {
allChunks: true
}),
new HtmlwebpackPlugin({
template: path.resolve(ROOT_PATH, '../views/entry.html'),
filename: path.resolve(ROOT_PATH, '../build/entry.html'),
chunks: ['entry', 'vendor'],
hash: false
}),
// 加署名
new webpack.BannerPlugin('Copyright by xxx')
]
};
複製代碼
第一次接觸配置,真的找不到北,太多插件,太多功能。做爲新手,那須要怎麼個思路,我總結:按項目需求來配置。不要認爲其餘人配置的就適合本身項目,要否則給本身帶來各類麻煩。 摸索這個過程還挺長的:
A. 首先需求仍是明確的:less編譯、jsx編譯、公共文件單獨打包、html靜態模板中插入編譯後的文件路徑、css提取。 上面這些對應配置:webpack
const path = require('path');
const webpack = require('webpack');
// html中替換編譯後的js
const HtmlwebpackPlugin = require('html-webpack-plugin');
// css提取
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const ROOT_PATH = path.resolve(__dirname);
const APP_PATH = path.resolve(ROOT_PATH, '../src');
const BUILD_PATH = path.resolve(ROOT_PATH, '../build');
module.exports = {
entry: {
entry: path.resolve(APP_PATH, './entry.jsx'),
vendor: ['react', 'react-dom', 'pace']
},
output: {
filename: '[name].js',
path: BUILD_PATH,
chunkFilename: '[name].js',
publicPath: '../'
},
devtool: 'eval-source-map',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
query: {
presets: ['es2015', 'react', 'stage-0']
}
}
]
},
{
test: /\.(css|less)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader',
'less-loader'
]
}),
exclude: /node_modules/
},
{
test: /\.(jpg|jpeg|png|svg|gif|bmp)/i,
use: [
'url-loader?limit=5000&name=img/[name].[sha512:hash:base64:8].[ext]'
]
},
{
test: /\.(woff|woff2|ttf|eot)($|\?)/i,
use: [
'url-loader?limit=5000&name=fonts/[name].[sha512:hash:base64:8].[ext]'
]
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['commons', 'vendor'],
minChunks: 2
}),
new ExtractTextPlugin('commons.css', {
allChunks: true
}),
new HtmlwebpackPlugin({
template: path.resolve(ROOT_PATH, '../views/entry.html'),
filename: path.resolve(ROOT_PATH, '../build/entry.html'),
chunks: ['entry', 'vendor'],
hash: false
})
]
};
複製代碼
B. 配置到這步後,就能知足基本開發了。試用以後,這時候對本身提出了幾個問題:ios
針對上面4個問題,從新配置:
第2個和3個解決方案一致:即聲明別名nginx
resolve: {
extensions: ['.js', '.jsx', '.less', '.css', '.png', '.jpg', '.svg', '.gif', '.eot'],
alias: {
pace: path.resolve(ROOT_PATH, '../src/plugins/pace/index.js'),
ImagesPath: path.resolve(ROOT_PATH, '../src/')
}
}
複製代碼
當中第3個問題,網上找了好多資料,都沒有結果,後來請教了前端羣的同行,才解決該問題。
解決第1個問題過程當中,我學習到了cssModule的概念,一開始菜鳥還很差理解,實踐了後,還真是個好東西。git
{
test: /\.(css|less)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
'less-loader'
]
}),
exclude: /node_modules/
},
複製代碼
只要css-loader啓動modules就行了。爲了支持 react,引入了 react-css-modules 依賴包。es6
這時候還沒完,又有兩個問題引出來了。
第2個問題,還好解決,查了下 react-css-modules 資料,子組件中經過props獲取
const template = (
<div className={this.props.styles['loadingBox']}> <Loading /> </div>);
複製代碼
第1個問題糾結了很久,後來找了個折中的方案,好心酸。 在entry.jsx中引入的antd組件樣式,改爲
import 'antd/dist/antd.css';
複製代碼
對,直接引入 css文件,跳過less編譯。
而後在webpack中新增配置
{
test: /\.(css|less)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]',
'less-loader'
]
}),
exclude: /node_modules/
},
{
test: /\.(css)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
'css-loader'
]
}),
include: /node_modules/
},
複製代碼
到這一步,你們應該明白個人方案了,就是 node_modules 文件夾中的 css文件不啓動 cssmoduls,其它文件夾中 啓動 cssmoduls。
接下來就是第4個大問題待解決,路由按需加載。
做爲新手,固然首先是搜索一下 react-router 4.x 如何實現按需加載的,果真好多答案。至於如何選擇,固然是哪一個方便哪一個來的原則。 react-loadable 這個插件,固然這個貨得依賴 babel-plugin-syntax-dynamic-import 包。
webpack配置,加入 babel的 syntax-dynamic-import插件
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
query: {
presets: ['es2015', 'react', 'stage-0'],
plugins: ['syntax-dynamic-import']
}
}
]
},
...
複製代碼
react中使用 react-loadable,特別方便
import Loadable from 'react-loadable';
...
const MyLoadingComponent = ({isLoading, error, pastDelay}) => {
// Handle the loading state
if (pastDelay) {
return <div>Loading...</div>;
}
// Handle the error state
else if (error) {
return <div>Sorry, there was a problem loading the page.</div>;
}
else {
return null;
}
}
const AsyncTestManager = Loadable({
loader: () => import('./pages/TestManager/Index'),
loading: MyLoadingComponent
});
ReactDOM.render(
<Provider store={Store}> <BrowserRouter basename="/" forceRefresh={!supportsHistory} keyLength={12}> <div> <Route exact path="/testManager" component={AsyncTestManager}/> </div> </BrowserRouter> </Provider>, document.getElementById('root') ); 複製代碼
這個插件具體使用你們查看相關文檔,很方便強大。記得上線打包的時候,webpack要啓動hash
output: {
filename: '[name][chunkhash].js',
path: BUILD_PATH,
chunkFilename: '[name][chunkhash].js',
publicPath: './'
},
複製代碼
至此,腳手架搭建走過的坑結束了。
順便提下
output: {
...
publicPath: '../'
},
複製代碼
這裏必定要配置爲 ../ ,不要配置爲 ./,由於不當心配錯,致使路由按需加載的時候,js路徑錯誤了。
這裏要介紹下 redux的一箇中間件,redux-thunk。何爲中間件,以及 redux-thunk的做用,你們能夠參考下阮一峯的一篇教程《Redux 入門教程(二):中間件與異步操做》 。 正常狀況下,actions返回的只是一個對象,可是咱們想發送數據前最好能處理下,因此呢,就須要重寫下Store.dispath方法了。中間件就是這樣的做用,改寫 dispatch,在發出 Action 和執行 Reducer 這兩步之間,添加了其餘功能。好比異步操做:發起ajax請求。視圖發起一個action,觸發了一個請求,可是action不能返回函數,這時候redux-thunk就起做用了。
這個過程,就是把 reducer跟Store綁定在一塊兒,同時引入須要的中間件
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducers from '../reducers';
const store = applyMiddleware(
thunkMiddleware
)(createStore)(reducers);
export default store;
複製代碼
applyMiddleware 方法它是 Redux 的原生方法,做用是將全部中間件組成一個數組,依次執行。 createStore 方法建立一個 Store。 至於這個參數寫法,其實就是es6的柯里化語法。用es3,es5實現其實原理很簡單,就是利用了閉包保存了上一次的數據,實現過單列模式的同窗應該很清楚。
function add(number1) {
return function(number2) {
return number1 + number2;
};
}
var addTwo = add(1)(2);
複製代碼
至於Reducer,其實很好實現,它其實就是單純的函數。
例如:
import * as CONSTANTS from '../../constants/TestControl';
const initialState = {};
const testControl = (state = initialState, action) => {
switch (action.type) {
case CONSTANTS.GET_DETAILS_PENDING:
return {
...state,
isFetching: true,
data: action.payload,
success: false
};
case CONSTANTS.GET_DETAILS_SUCCEEDED:
return {
...state,
isFetching: false,
data: action.data.relatedObject,
success: true
};
case CONSTANTS.GET_DETAILS_FAILED:
return {
...state,
isFetching: false,
success: false,
errorCode: action.data.errorCode
};
default:
return state;
}
};
export default testControl;
複製代碼
你們應該注意到,這個實際上是對應action的一個ajax請求,其中,action.type中 ,
_PENDING 結尾的表示 ajax正在發起請求;
_SUCCEEDED 結尾的表示 ajax 請求成功;
_FAILED 結尾的表示 ajax 請求失敗;
這個我是做爲ajax actions的標準命名,你們也能夠用其它方式,原則就是:好理解,統一。 固然其它非ajax的actions(包括ajax的action),個人規則就是,命名要表意,常量要大寫。
因爲個人項目中reduce有n個,因此 reducers/index.js 是這樣的
import { combineReducers } from 'redux';
import testManagerList from './TestManager/list';
import common from './Common';
import system from './System';
import evaluate from './Evaluate';
import ComponentsAddLayer from './Components/addLayer';
import testNew from './TestNew';
import testControl from './TestControl';
export default combineReducers({
testManagerList,
system,
evaluate,
ComponentsAddLayer,
testNew,
common,
testControl
});
複製代碼
引入 redux 的combineReducers 方法,這樣就把多個 reducer集合到一塊兒了,調用state的時候,只要如此:
const mapStateToProps = state => ({
type: state.testManagerList.type
});
複製代碼
你們看明白了吧,testManagerList 是個人一個 reducer。
Actions 我是做爲存放數據的,好比ajax數據請求,視圖默認數據這些。
const testManager = {
testManager_get_list(options) {
return (dispatch) => {
const fetchData = axios.get('/abtest/getList', options);
dispatch({
type: TABLE_GET_LIST_PENDING,
payload: fetchData
});
fetchData.then((response) => {
if (response.data.success) {
dispatch({
type: TABLE_GET_LIST_SUCCEEDED,
...response
});
} else {
dispatch({
type: TABLE_GET_LIST_FAILED,
...response
});
}
}).catch((error) => {
dispatch({
type: TABLE_GET_LIST_FAILED,
...error
});
});
};
},
testManager_change_tabs(activeTabs) {
return {
type: TABS_CHANGE,
active: activeTabs
};
},
testManager_search(value) {
return {
type: SEARCH,
keyWord: value
};
},
testManager_parameters(options) {
return {
type: TEST_MANAGER,
parameters: Object.assign({}, {
page: 1,
pageSize: 10,
sort: '',
type: '',
keyWord: ''
}, options || {})
};
},
testManager_pagination_change(noop) {
return {
type: PAGINATION_CHANGE,
page: noop
};
}
};
複製代碼
這個模塊觸發的actions:獲取表格列表數據,搜索,分頁操做,獲取默認配置,很好理解,這裏就不說了。 具體如何使用,請看下面的 view 實踐
開始的時候,提出幾個問題:
先解決第3個問題,一開始我是想重寫覆蓋第三方的css文件的,後來一看代碼量,果斷放棄了。還好被我發現了 styled-components 這個插件,果真好用。
import styled from 'styled-components';
import Tabs from 'antd/lib/tabs';
const TabsStyle = styled(Tabs)` float: left; .ant-tabs-nav-wrap { margin-bottom: 0; } .ant-tabs-tab { text-align: center; transition: background 0.3s; color: #666666; padding: 6px 12px; font-size: 14px; font-weight: 400; cursor: pointer; user-select: none; background-image: none; margin-left: -10px; } `;
複製代碼
這裏面跟寫less同樣就行了。我是這麼以爲。具體你們能夠查看下對應的文檔。開發過react-native的同窗,都很清楚這個插件的給力。
再結晶第4個問題。react-router 官方提供了 withRouter的api,這個api就是專門爲了解決這個問題。
import CSSModules from 'react-css-modules';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
......
componentDidMount() {
// props中就可拿到路由信息了
const { ACTIONS, match } = this.props;
ACTIONS.TestControl_get_testing_detail({ id: match.params.id });
}
const turnCss = CSSModules(TestManager, styles, { allowMultiple: true });
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(turnCss));
複製代碼
很是方便。
再來講第一個問題,視圖如何跟Store綁定 Store提供了三個方法
store.getState()
store.dispatch()
store.subscribe()
其中,Store 容許使用store.subscribe方法設置監聽函數,一旦 State 發生變化,就自動執行這個函數。因此綁定視圖,調用這個方法就行了。 不過redux做者專門針對react,封裝了一個庫:React-Redux,這裏我就直接引用了,這樣我就不用處理state了。
import { connect } from 'react-redux';
const mapStateToProps = state => ({
isFetching: state.testControl.isFetching,
success: state.testControl.success,
detail: state.testControl.data
});
const mapDispatchToProps = dispath => ({
ACTIONS: bindActionCreators(actions, dispath)
});
const turnCss = CSSModules(TestControl, styles, { allowMultiple: true });
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(turnCss));
複製代碼
這樣 TestControl 視圖就跟 Store綁定到一塊兒了。 具體的API介紹,你們能夠查看下文檔,仍是很好理解的。
解決了第一個問題,再來看第2個問題:ACTIONS如何在視圖中使用
ACTIONS的做用,其實就是消息訂閱/發佈 模式中,發佈那個步驟了。這樣理解,你們應該明白了吧, 好比: 視圖中點擊了一個按鈕後,回調函數中就直接調用對應的ACTIONS方法便可。
還要介紹下redux的bindActionCreators方法:
主要用處:
通常狀況下,咱們能夠經過Provider將store經過React的connext屬性向下傳遞,bindActionCreators的惟一用處就是須要傳遞action creater到子組件,而且該子組件並無接收到父組件上傳遞的store和dispatch。
import { bindActionCreators } from 'redux';
import actions from '../../actions';
class TestControl extends Component {
componentDidMount() {
const { ACTIONS, match } = this.props;
ACTIONS.TestControl_get_testing_detail({ id: match.params.id });
}
// 開始
start() {
const { ACTIONS, match } = this.props;
ACTIONS.TestControl_start({ id: match.params.id });
}
render() {
...
}
}
const mapStateToProps = state => ({
isFetching: state.testControl.isFetching,
success: state.testControl.success,
detail: state.testControl.data
});
const mapDispatchToProps = dispath => ({
ACTIONS: bindActionCreators(actions, dispath)
});
const turnCss = CSSModules(TestControl, styles, { allowMultiple: true });
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(turnCss));
複製代碼
至此,redux實踐結束。
由於是單頁面模式,且使用了 BrowserRouter,故nginx配置以下:
server {
location / {
root E:/program/ark2/abtest-statics/build/;
index index.html index.htm;
expires -1;
try_files $uri $uri/ /entry.html;
}
}
複製代碼
listen 80;
#rewrite_log on;
location /abtest {
rewrite ^/abtest/(.*\.(gif|jpg|jpeg|png|css|js|ico)$) /abtest-statics/build/$1 last;
rewrite ^/abtest.* /abtest-statics/build/entry.html;
}
location /abtest-statics/build {
expires -1;
root E:/program/ark2;
index entry.html;
}
複製代碼
說明: 'E:/program/ark2' 爲項目地址,當訪問 xxx.com/abtest/ 後,rewrite(重定向)到 /abtest-statics/build 下。
開發一個項目,最好須要一個合理的約定,好比代碼風格、模塊定義、方法定義、參數定義等等,這些約定中,還要考慮如何便於寫和維護單元測試這個因素。這些其實仍是挺有挑戰的,只能不斷去完善。
上面方案其實還有不少缺陷待解決,須要慢慢改進了。
@做者:白雲飄飄(534591395@qq.com)
@github: github.com/534591395 歡迎關注個人微信公衆號:
或者微信公衆號搜索新夢想兔
,關注我哦。