课程章节: 课程总结
主讲老师: Dell
课程内容:
今天学习的内容包括:
如何进行项目的上线;
课程回顾;
课程收获:
10.1 心得:
htdocs是默认访问到根目录下的index.html文件
问题: 项目打包编译后 通过gh-pages部署的项打开之后是空白的
解答:捕获不到数据,json数据应该放在自己的服务器上
问题:登录状态刷新就没有了,数据如何持续化在 redux 中?
解答:利用localStorage做持久存储
问题:create-react-app 项目能打包原生APP应用吗
解答:不可以的,必须还要用cordova这样的东西给一个原生的壳
问题:将静态资源部署到服务器上后,点击浏览器的刷新,报404
解答:上线使用hashHistory,不要使用BrowserHistory
问题:项目上线之后,项目能跳转到详情页,但是刷新详情页,就会提示Not Found
解答:改用hashHistory的路由,否则需要后端改服务器配置
问题:部署到github pages上,public中的api文件怎么处理
解答:直接把接口数据写在代码里
问题:http-proxy-middleware-跨域-axios修改URL不起作用
解答:修改之后需要重启下服务器才生效
问题:如何固定Header在页面顶部
解答:HeaderWrapper添加fixed
10.2 心得:
问题:怎么使用script标签引入的qq定位js
解答:找到定位的js,放到script标签里
问题:react router 如何在回调事件里面控制跳转
解答:使用this.router.redirect
问题:react-router-dom怎么实现前进刷新后退不刷新
解答:可以用redux
10.3 心得:
react基础语法;redux数据层框架;react-redux; react-router4; immutable.js; style-components; reloadable.js
总结:
实例代码:
/package.json
{
"name": "jianshu",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.18.0",
"immutable": "^3.8.2",
"react": "^16.4.0",
"react-dom": "^16.4.0",
"react-loadable": "^5.4.0",
"react-redux": "^5.0.7",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.1.4",
"react-transition-group": "^2.3.1",
"redux": "^4.0.0",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.3.0",
"styled-components": "^3.3.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
/src/App.js
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import Header from './common/header';
import Home from './pages/home';
import Detail from './pages/detail/loadable.js';
import Login from './pages/login';
import Write from './pages/write';
import store from './store';
class App extends Component {
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Header />
<Route path='/' exact component={Home}></Route>
<Route path='/login' exact component={Login}></Route>
<Route path='/write' exact component={Write}></Route>
<Route path='/detail/:id' exact component={Detail}></Route>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './style.js';
import './statics/iconfont/iconfont';
ReactDOM.render(<App />, document.getElementById('root'));
/src/index.js
import { injectGlobal } from 'styled-components';
injectGlobal`
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
`;
/src/common/header/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { CSSTransition } from 'react-transition-group';
import { actionCreators } from './store';
import { actionCreators as loginActionCreators } from '../../pages/login/store'
import {
HeaderWrapper,
Logo,
Nav,
NavItem,
SearchWrapper,
NavSearch,
SearchInfo,
SearchInfoTitle,
SearchInfoSwitch,
SearchInfoList,
SearchInfoItem,
Addition,
Button
} from './style';
class Header extends Component {
getListArea() {
const { focused, list, page, totalPage, mouseIn, handleMouseEnter, handleMouseLeave, handleChangePage } = this.props;
const newList = list.toJS();
const pageList = [];
if (newList.length) {
for (let i = (page - 1) * 10; i < page * 10; i++) {
pageList.push(
<SearchInfoItem key={newList[i]}>{newList[i]}</SearchInfoItem>
)
}
}
if (focused || mouseIn) {
return (
<SearchInfo
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<SearchInfoTitle>
热门搜索
<SearchInfoSwitch
onClick={() => handleChangePage(page, totalPage, this.spinIcon)}
>
<i ref={(icon) => {this.spinIcon = icon}} className="iconfont spin"></i>
换一批
</SearchInfoSwitch>
</SearchInfoTitle>
<SearchInfoList>
{pageList}
</SearchInfoList>
</SearchInfo>
)
}else {
return null;
}
}
render() {
const { focused, handleInputFocus, handleInputBlur, list, login, logout } = this.props;
return (
<HeaderWrapper>
<Link to='/'>
<Logo/>
</Link>
<Nav>
<NavItem className='left active'>首页</NavItem>
<NavItem className='left'>下载App</NavItem>
{
login ?
<NavItem onClick={logout} className='right'>退出</NavItem> :
<Link to='/login'><NavItem className='right'>登陆</NavItem></Link>
}
<NavItem className='right'>
<i className="iconfont"></i>
</NavItem>
<SearchWrapper>
<CSSTransition
in={focused}
timeout={200}
classNames="slide"
>
<NavSearch
className={focused ? 'focused': ''}
onFocus={() => handleInputFocus(list)}
onBlur={handleInputBlur}
></NavSearch>
</CSSTransition>
<i className={focused ? 'focused iconfont zoom': 'iconfont zoom'}>

</i>
{this.getListArea()}
</SearchWrapper>
</Nav>
<Addition>
<Link to='/write'>
<Button className='writting'>
<i className="iconfont"></i>
写文章
</Button>
</Link>
<Button className='reg'>注册</Button>
</Addition>
</HeaderWrapper>
);
}
}
const mapStateToProps = (state) => {
return {
focused: state.getIn(['header', 'focused']),
list: state.getIn(['header', 'list']),
page: state.getIn(['header', 'page']),
totalPage: state.getIn(['header', 'totalPage']),
mouseIn: state.getIn(['header', 'mouseIn']),
login: state.getIn(['login', 'login'])
}
}
const mapDispathToProps = (dispatch) => {
return {
handleInputFocus(list) {
(list.size === 0) && dispatch(actionCreators.getList());
dispatch(actionCreators.searchFocus());
},
handleInputBlur() {
dispatch(actionCreators.searchBlur());
},
handleMouseEnter() {
dispatch(actionCreators.mouseEnter());
},
handleMouseLeave() {
dispatch(actionCreators.mouseLeave());
},
handleChangePage(page, totalPage, spin) {
let originAngle = spin.style.transform.replace(/[^0-9]/ig, '');
if (originAngle) {
originAngle = parseInt(originAngle, 10);
}else {
originAngle = 0;
}
spin.style.transform = 'rotate(' + (originAngle + 360) + 'deg)';
if (page < totalPage) {
dispatch(actionCreators.changePage(page + 1));
}else {
dispatch(actionCreators.changePage(1));
}
},
logout() {
dispatch(loginActionCreators.logout())
}
}
}
export default connect(mapStateToProps, mapDispathToProps)(Header);
/src/common/header/style.js
import styled from 'styled-components';
import logoPic from '../../statics/logo.png';
export const HeaderWrapper = styled.div`
z-index: 1;
position: relative;
height: 56px;
border-bottom: 1px solid #f0f0f0;
`;
export const Logo = styled.div`
position: absolute;
top: 0;
left: 0;
display: block;
width: 100px;
height: 56px;
background: url(${logoPic});
background-size: contain;
`;
export const Nav = styled.div`
width: 960px;
height: 100%;
padding-right: 70px;
box-sizing: border-box;
margin: 0 auto;
`;
export const NavItem = styled.div`
line-height: 56px;
padding: 0 15px;
font-size: 17px;
color: #333;
&.left {
float: left;
}
&.right {
float: right;
color: #969696;
}
&.active {
color: #ea6f5a;
}
`;
export const SearchWrapper = styled.div`
position: relative;
float: left;
.zoom {
position: absolute;
right: 5px;
bottom: 5px;
width: 30px;
line-height: 30px;
border-radius: 15px;
text-align: center;
&.focused {
background: #777;
color: #fff;
}
}
`;
export const NavSearch = styled.input.attrs({
placeholder: '搜索'
})`
width: 160px;
height: 38px;
padding: 0 30px 0 20px;
margin-top: 9px;
margin-left: 20px;
box-sizing: border-box;
border: none;
outline: none;
border-radius: 19px;
background: #eee;
font-size: 14px;
color: #666;
&::placeholder {
color: #999;
}
&.focused {
width: 240px;
}
&.slide-enter {
transition: all .2s ease-out;
}
&.slide-enter-active {
width: 240px;
}
&.slide-exit {
transition: all .2s ease-out;
}
&.slide-exit-active {
width: 160px;
}
`;
export const SearchInfo = styled.div`
position: absolute;
left: 0;
top: 56px;
width: 240px;
padding: 0 20px;
box-shadow: 0 0 8px rgba(0, 0, 0, .2);
background: #fff;
`;
export const SearchInfoTitle = styled.div`
margin-top: 20px;
margin-bottom: 15px;
line-height: 20px;
font-size: 14px;
color: #969696;
`;
export const SearchInfoSwitch = styled.span`
float: right;
font-size: 13px;
cursor: pointer;
.spin {
display: block;
float: left;
font-size: 12px;
margin-right: 2px;
transition: all .2s ease-in;
transform-origin: center center;
}
`;
export const SearchInfoList = styled.div`
overflow: hidden;
`;
export const SearchInfoItem = styled.a`
display: block;
float: left;
line-height: 20px;
padding: 0 5px;
margin-right: 10px;
margin-bottom: 15px;
font-size: 12px;
border: 1px solid #ddd;
color: #787878;
border-radius: 3px;
`;
export const Addition = styled.div`
position: absolute;
right: 0;
top: 0;
height: 56px;
`;
export const Button = styled.div`
float: right;
margin-top: 9px;
margin-right: 20px;
padding: 0 20px;
line-height: 38px;
border-radius: 19px;
border: 1px solid #ec6149;
font-siz: 14px;
&.reg {
color: #ec6149;
}
&.writting {
color: #fff;
background: #ec6149;
}
`
/src/common/header/store/actionCreators.js
import * as constants from './constants';
import { fromJS } from 'immutable';
import axios from 'axios';
const changeList = (data) => ({
type: constants.CHANGE_LIST,
data: fromJS(data),
totalPage: Math.ceil(data.length / 10)
});
export const searchFocus = () => ({
type: constants.SEARCH_FOCUS
});
export const searchBlur = () => ({
type: constants.SEARCH_BLUR
});
export const mouseEnter = () => ({
type: constants.MOUSE_ENTER
});
export const mouseLeave = () => ({
type: constants.MOUSE_LEAVE
});
export const changePage = (page) => ({
type: constants.CHANGE_PAGE,
page
});
export const getList = () => {
return (dispatch) => {
axios.get('/api/headerList.json').then((res) => {
const data = res.data;
dispatch(changeList(data.data));
}).catch(() => {
console.log('error');
})
}
};
/src/common/header/store/constants.js
export const SEARCH_FOCUS = 'header/SEARCH_FOCUS';
export const SEARCH_BLUR = 'header/SEARCH_BLUR';
export const CHANGE_LIST ='header/CHANGE_LIST';
export const MOUSE_ENTER = 'header/MOUSE_ENTER';
export const MOUSE_LEAVE = 'header/MOUSE_LEAVE';
export const CHANGE_PAGE = 'header/CHANGE_PAGE';
/src/common/header/store/index.js
import reducer from './reducer';
import * as actionCreators from './actionCreators';
import * as constants from './constants';
export { reducer, actionCreators, constants };
/src/common/header/store/
import * as constants from './constants';
import { fromJS } from 'immutable';
const defaultState = fromJS({
focused: false,
mouseIn: false,
list: [],
page: 1,
totalPage: 1
});
export default (state = defaultState, action) => {
switch(action.type) {
case constants.SEARCH_FOCUS:
return state.set('focused', true);
case constants.SEARCH_BLUR:
return state.set('focused', false);
case constants.CHANGE_LIST:
return state.merge({
list: action.data,
totalPage: action.totalPage
});
case constants.MOUSE_ENTER:
return state.set('mouseIn', true);
case constants.MOUSE_LEAVE:
return state.set('mouseIn', false);
case constants.CHANGE_PAGE:
return state.set('page', action.page);
default:
return state;
}
}
/src/statics/iconfont/iconfont.js
import { injectGlobal } from 'styled-components';
injectGlobal`
@font-face {
font-family: "iconfont";
src: url('./iconfont.eot?t=1528610804703'); /* IE9*/
src: url('./iconfont.eot?t=1528610804703#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'),
url('./iconfont.ttf?t=1528610804703') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
url('./iconfont.svg?t=1528610804703#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family:"iconfont" !important;
font-size:16px;
font-style:normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`;