去哪旅行实战项目
一、 初始化项目
-
使用脚手架创建默认模板文件
npx create-react-app train-tickers
-
使用
eject
命令更改webpack
配置, 将封装在create-react-app
中的配置全部反编译到当前项目,这样用户就能完全取得webpack
文件的控制权npm run ejext
报错:
create-react-app
执行yarn eject
以后yarn start
报错步骤:
其实就是重写安装依赖
- rm -R node_modules/
- rm yarn.lock
- yarn
-
删除多余文件
进入
./src
目录, 执行遍历并删除除了serviceWorker.js
文件以外的所有文件ls | grep -v serviceWorker.js | xargs rm
-
进入
./src
目录,创建初始化文件./src/index/
cd index/ touch index.js touch index.css touch App.jsx touch App.css touch reducers.js touch actions.js touch store.js
-
创建初始文件的原始代码
./src/index/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import {Provider} from 'react-redux' import store from './store' import './index.css'; import App from './App.jsx' ReactDOM.render( <Provider store={store}><App /></Provider>, document.getElementById('root') )
建议再安装并导入
normalize.css/normailze.css
这个css
用于自动适配项目在各个浏览器上的样式,不用自己再手动适配。安装,也是使用
npm
方式切换到根目录 src/index cd .. src/ cd .. ./ npm i normalize.css
报错:
normalize.css/normailze.css
找不到这个包解决方式:
复制
normailze.css
到utils
文件,导入即可。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wlwqfKtm-1584099786657)(C:\Users\80450\AppData\Roaming\Typora\typora-user-images\image-20200312113623963.png)]
import '../utils/normalize.css'
./src/index/App.js
import { connect } from 'react-redux'; import './App.css'; function App(props) { } export default connect( function mapstateToProps(state){ }, function mapDispaTchtoProps(dispatch){ } )(App);
./src/index/reducers.js
export default {};
-
创建
搜索结果query
、坐席选择ticket
、订单填写order
,目录结构和index相同,所以直接将index
目录拷贝壳./src/index/
./src/index/ cd src ./src/ cp -r index query ./src/ cp -r index ticket ./src/ cp -r index order
-
在
public
目录中删除多余文件,创建query.html
,ticket.html
,order.html
./src/ cd ../public ls Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2020/3/1 17:44 3150 favicon.ico -a---- 2020/3/1 17:44 1721 index.html -a---- 2020/3/1 17:44 5347 logo192.png -a---- 2020/3/1 17:44 9664 logo512.png -a---- 2020/3/1 17:44 492 manifest.json -a---- 2020/3/1 17:44 67 robots.txt rm .\logo192.png rm .\logo512.png cp index.html query.html cp index.html ticket.html cp index.html order.html
然后修改复制生成的HTML的title
./public/order.html
<title>React App</title> 改为 <title>订单填写</title>
./public/query.html
<title>React App</title> 改为 <title>搜索结果</title>
./public/titcket.html
<title>React App</title> 改为 <title>坐席选择</title>
-
当前
webpack
的etry
不满足需求,需手动修改etry
./config/webpack.config.js
module.exports = function(webpackEnv) { const isEnvDevelopment = webpackEnv === 'development'; const isEnvProduction = webpackEnv === 'production'; ...... entry: [ // Include an alternative client for WebpackDevServer. A client's job is to // connect to WebpackDevServer by a socket and get notified about changes. // When you save a file, the client will either apply hot updates (in case // of CSS changes), or refresh the page (in case of JS changes). When you // make a syntax error, this client will display a syntax error overlay. // Note: instead of the default WebpackDevServer client, we use a custom one // to bring better experience for Create React App users. You can replace // the line below with these two lines if you prefer the stock client: // require.resolve('webpack-dev-server/client') + '?/', // require.resolve('webpack/hot/dev-server'), isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'), // Finally, this is your app's code: paths.appIndexJs, // We include the app code last so that if there is a runtime error during // initialization, it doesn't blow up the WebpackDevServer client, and // changing JS code would still trigger a refresh. ].filter(Boolean), ......
修改为:
// 数组型entry不满足要求,因此改为对象 entry:{ index:[paths.appIndexJs,isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean), query:[paths.appQueryJs,isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean), ticket:[paths.appTicketJs,isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean), order:[paths.appOrderJs,isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient')].filter(Boolean), }
./config/paths.js
// config after eject: we're in ./config/ module.exports = { dotenv: resolveApp('.env'), appPath: resolveApp('.'), appBuild: resolveApp('build'), appPublic: resolveApp('public'), // 扩充html appHtml: resolveApp('public/index.html'), appQueryHtml: resolveApp('public/query.html'), appTicketHtml: resolveApp('public/ticket.html'), appOrderHtml: resolveApp('public/order.html'), // 修改index的路径 // appIndexJs: resolveModule(resolveApp, 'src/index'), appIndexJs: resolveModule(resolveApp, 'src/index/index'), appQueryJs: resolveModule(resolveApp, 'src/query/index'), appTicketJs: resolveModule(resolveApp, 'src/ticket/index'), appOrderJs: resolveModule(resolveApp, 'src/order/index'), appPackageJson: resolveApp('package.json'), appSrc: resolveApp('src'), appTsConfig: resolveApp('tsconfig.json'), appJsConfig: resolveApp('jsconfig.json'), yarnLockFile: resolveApp('yarn.lock'), testsSetup: resolveModule(resolveApp, 'src/setupTests'), proxySetup: resolveApp('src/setupProxy.js'), appNodeModules: resolveApp('node_modules'), publicUrlOrPath, };
报错:
Cannot read property ‘filter’ of undefined
ManifestPlugin
这个插件的作用是生成一份.json
的文件,通过该文件的映射关系可以让我们知道webpack
是如何追踪所有模块并映射到输出bundle
中的。我们先来看原始的配置,这里fileName
设置了输出文件名asset-manifest.json
,publicPath
设置了输出路径,最终要的是最后一个generate
参数,自定义了输出的内容,里面有一段是取entrypoints.main
,这是针对单一入口的配置,因为单一入口不指定name
的情况默认name
为main
,当改成多入口的方式了之后这里面在entrypoints
中自然是读取不到main
这个值的,因此就报错,这里将generate
这个参数去掉,恢复其默认值即可,或者将entrypoints
这个key
去掉。new ManifestPlugin({ fileName: 'asset-manifest.json', publicPath: paths.publicUrlOrPath, // generate: (seed, files, entrypoints) => { // const manifestFiles = files.reduce((manifest, file) => { // manifest[file.name] = file.path; // return manifest; // }, seed); // const entrypointFiles = entrypoints.main.filter( // fileName => !fileName.endsWith('.map') // ); // return { // files: manifestFiles, // entrypoints: entrypointFiles, // }; // }, }),
或者
new ManifestPlugin({ fileName: 'asset-manifest.json', publicPath: paths.publicUrlOrPath, generate: (seed, files, entrypoints) => { const manifestFiles = files.reduce((manifest, file) => { manifest[file.name] = file.path; return manifest; }, seed); // const entrypointFiles = entrypoints.main.filter( // fileName => !fileName.endsWith('.map') // ); return { files: manifestFiles, // entrypoints: entrypointFiles, }; }, }),
-
最后,编译一下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ktCG93d-1584099786658)(C:\Users\80450\AppData\Roaming\Typora\typora-user-images\image-20200312113759148.png)]
成功运行。
二、搭建Mock server
三、数据结构和模块设计
首先设计项目的整体轮廓。
-
在
index
目录下,创建行程组件Journey
、高铁班次组件HighSpeed
、行程日期组件DepartDate
、提交按钮Submit
./src/index/Journey
import React from 'react' import './DepartDate.css'; /* 行程日期组件 */ export default function DepartDate(props) { return ( <div> </div> ); }
./src/index/HighSpeed
import React from 'react' import './HighSpeed.css'; /* 高铁班次 */ export default function HighSpeed(props) { return ( <div> </div> ); }
./src/index/DepartDate
import React from 'react' import './DepartDate.css'; /* 行程日期组件 */ export default function DepartDate(props) { return ( <div> </div> ); }
./src/index/Submit
import React from 'react' import './Submit.css'; /* 提交按钮 */ export default function Submit(props) { return ( <div> </div> ); }
-
设计store里参数的默认值
./src/index/store.js
import { createStore, combineReducers, applyMiddleware } from 'redux' import reducers from './reducers'; import thunk from 'redux-thunk'; export default createStore( combineReducers(reducers), { from: '北京', // 出发站 to: '上海', // 终止站 isCitySelectorVisible: false, // 复程,默认false currentSelectingLeftCity: false,// 选择复程,数据回填即 from和to更换,默认false cityData: null,//选择行程后城市的数据加载,默认值为空 isLoadingCityData: false, //用于节流操作,判断当前是否在发起城市数据加载,如果是true就是需要发起,false就是不需要。默认值为false isDateSelectorVisible: false,//选择行程后日期选择的开关,默认值为false highSpeed: false//是否选择了高铁,默认值为false }, applyMiddleware(thunk) )
-
设计actionTypes:
./src/index/actions.js
export const ACTION_SET_FROM = 'FROM'; // 出发站 export const ACTION_SET_TO = 'TO'; // 终止站 export const ACTION_SET_IS_CITY_SELECTOR_VISIBLE = 'IS_CITY_SELECTOR_VISIBLE'; //是否选择复程 export const ACTION_SET_CURRENT_SELECTING_LEFT_CITY = 'CURRENT_SELECTING_LEFT_CITY';// 选择复程后城市回填 export const ACTION_SET_CITY_DATA = 'CITY_DATA'; // 选择城市的数据 export const ACTION_SET_IS_LOADING_CITY_DATA = 'IS_LOADING_CITY_DATA'; // 发送城市数据请求 export const ACTION_SET_IS_DATE_SELECTOR_VISIBLE = 'IS_DATE_SELECTOR_VISIBLE'; // 选择复程后日期选择的开关 export const ACTION_SET_HIGH_SPEED = 'HIGH_SPEED'; // 高铁班次
-
设计action:
./src/index/actions.js
// 出发站 export function setFrom(from) { return { type: ACTION_SET_FROM, payload: from, } } // 终止站 export function setTo(to){ return { type: ACTION_SET_TO, payload: to, } } // 发起城市数据请求 export function setIsLoaingCityData(isLoadingCityData) { return { type: ACTION_SET_IS_LOADING_CITY_DATA, payload: isLoadingCityData, } } // 加载城市数据 export function setCityData(cityData){ return{ type: ACTION_SET_CITY_DATA, payload: cityData, } } // 切换高铁 export function toggleHighSpeed(){ // 异步发送 return (dispatch, getState) => { const { highSpeed } = getState(); dispatch({ type: ACTION_SET_HIGH_SPEED, payload: !highSpeed, }) } } // 显示,选择行程 export function showCitySelector(currentSelectingLeftCity){ return (dispatch) => { dispatch({ type: ACTION_SET_IS_CITY_SELECTOR_VISIBLE, payload: true, }); dispatch({ type: ACTION_SET_CURRENT_SELECTING_LEFT_CITY, payload: currentSelectingLeftCity, }); } } // 隐藏,选择行程 export function hideCitySelector(){ return { type: ACTION_SET_IS_CITY_SELECTOR_VISIBLE, payload: false } } // 回填城市, 终止站有则回填 export function setSelectedCity(city) { return (dispatch, getState) => { const { currentSelectingLeftCity } = getState(); if (currentSelectingLeftCity) { dispatch(setFrom(city)); } else { dispatch(setTo(city)); } } } // 显示选择日期 export function showDateSelector() { return { type: ACTION_SET_IS_DATE_SELECTOR_VISIBLE, payload: true, } } // 隐藏选择日期 export function hideDateSelector(){ return { type: ACTION_SET_IS_DATE_SELECTOR_VISIBLE, payload: false, } } // 出发站 和 终止站 互换 export function exchangeFromTo() { return (dispatch,getState) =>{ const { from, to } = getState(); dispatch(setFrom(to)); dispatch(setTo(from)); } }
-
设计reducers更新store状态:
./src/index/reducers.js
import { ACTION_SET_FROM, ACTION_SET_TO, ACTION_SET_IS_CITY_SELECTOR_VISIBLE, ACTION_SET_CURRENT_SELECTING_LEFT_CITY, ACTION_SET_CITY_DATA, ACTION_SET_IS_LOADING_CITY_DATA, ACTION_SET_IS_DATE_SELECTOR_VISIBLE, ACTION_SET_HIGH_SPEED, } from './actions.js' /**写法: *function moudle (state={初始状态}, action){ * switch(action.type){ * case ACTION_TYPE: * return payload; * default: * return state; * } *} */ export default { from(state="北京",action) { const {type,payload} = action; switch(type){ case ACTION_SET_FROM : return payload; default: return state; } }, to(state="上海",action) { const {type,payload} = action; switch(type){ case ACTION_SET_TO: return payload; default: return state; } }, isCitySelectorVisible(state=false,action) { const {type,payload} = action; switch(type){ case ACTION_SET_IS_CITY_SELECTOR_VISIBLE: return payload; default: return state; } }, currentSelectingLeftCity(state=false,action) { const {type,payload} = action; switch(type){ case ACTION_SET_CURRENT_SELECTING_LEFT_CITY: return payload; default: return state; } }, cityData(state=null,action) { const {type,payload} = action; switch(type){ case ACTION_SET_CITY_DATA: return payload; default: return state; } }, isLoadingCityData(state=false,action) { const {type,payload} = action; switch(type){ case ACTION_SET_IS_LOADING_CITY_DATA: return payload; default: return state; } }, isDateSelectorVisible(state=false,action) { const {type,payload} = action; switch(type){ case ACTION_SET_IS_DATE_SELECTOR_VISIBLE: return payload; default: return state; } }, highSpeed(state=false,action) { const {type,payload} = action; switch(type){ case ACTION_SET_HIGH_SPEED: return payload; default: return state; } }, }
四、顶部导航栏
-
在公共组件目录中,创建
Header.jsx
./src/common/
import React from 'react' import PropTypes from 'prop-types'; import './Header.css'; export default function Header(props) { const { onBack, title } = props; return ( <div className="header"> <div className="header-back" onClick={onBack}> <svg width="42" height="42"> <polyline points="25,13 16,21 25,29" stroke="#fff" strokeWidth="2" fill="none" /> </svg> </div> <div> <h1 className="header-title"> { title } </h1> </div> </div> ); } Header.propTypes={ onBack : PropTypes.func.isRequired, title : PropTypes.string.isRequired }
-
./src/index/App.jsx
因为直接挂载组件时,组件没有完全置于顶部,所以增加一个
标签包裹,通过div标签自定义位置import React,{Fragment,useCallback} from 'react'; import { connect } from 'react-redux'; import './App.css'; import Header from '../common/Header.jsx'; import DepartDate from './DepartDate.jsx'; import HighSpeed from './HighSpeed.jsx'; import Journey from './Journey.jsx'; import Sumbit from './Submit.jsx'; function App(props) { const onBack = useCallback(() => { window.history.back(); console.log('我是你爸'); },[]); return ( <Fragment> <div className="header-wrapper"> <Header title="火车票" onBack={onBack}/> </div> <Journey /> <DepartDate /> <HighSpeed /> <Sumbit /> </Fragment> ) }; export default connect( function mapStateToProps(state){ return {}; }, function mapDispatchToProps(dispatch){ return {}; } )(App);