Skip to content

Commit

Permalink
Page splitting Close #207 (#264)
Browse files Browse the repository at this point in the history
トップにいくつかの情報を載せたり、ほかのページを追加するための布石として、
動画プレイヤーをトップに配置するのではなくほかのURIのべつのページとして切り出すようにする。

`react-router` v4.0.0を使い、かつdynamic importを使っているため、
該当ページがロードされるまでページ本体のスクリプトファイルは読み込まれない。
そのためページの数が増えても
スクリプトファイル自体のファイルサイズが大きくなりすぎることはないのではない。
……と期待している。
  • Loading branch information
ykzts committed Mar 4, 2017
1 parent dd08872 commit 651dbbe
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 118 deletions.
21 changes: 6 additions & 15 deletions __tests__/components/player.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,15 @@ import React from 'react';
import Player from '../../src/components/player';

const context = {
styleManager: {
render: () => ({
player: 'player',
}),
},
setVideo() {},
};

test('mount', () => {
const player = shallow(<Player />, { context });
expect(player.find('.player').length).toBe(1);
const player = shallow(<Player location={{ search: '' }} />, { context });
expect(player.find('div').length).toBe(1);
});

test('aria-hidden=true', () => {
const player = shallow(<Player />, { context });
expect(player.find('.player[aria-hidden]').length).toBe(1);
});

test('aria-hidden=false', () => {
const player = shallow(<Player src="https://example.com/index.m3u8" />, { context });
expect(player.find('.player[aria-hidden=false]').length).toBe(1);
test('have search', () => {
const player = shallow(<Player location={{ search: 'https://example.com/index.m3u8' }} />, { context });
expect(player.find('div').length).toBe(1);
});
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"babelrc": false,
"plugins": [
"transform-class-properties",
"transform-object-rest-spread",
"transform-react-jsx"
],
"presets": [
Expand Down Expand Up @@ -38,6 +39,7 @@
"material-ui": "^1.0.0-alpha.6",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-router-dom": "^4.0.0-beta.7",
"url-search-params": "^0.6.1"
},
"description": ":tv: Television!",
Expand All @@ -48,6 +50,7 @@
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-class-properties": "^6.18.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.18.0",
"babel-plugin-transform-object-rest-spread": "^6.23.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"babel-plugin-transform-react-jsx-source": "^6.9.0",
"babel-preset-env": "^1.1.4",
Expand Down Expand Up @@ -89,6 +92,7 @@
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx}",
"!src/components/loader.jsx",
"!src/client.jsx"
],
"coverageDirectory": "<rootDir>/coverage",
Expand Down
7 changes: 6 additions & 1 deletion src/client.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'babel-polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';

function insertCss(...styles) {
// eslint-disable-next-line no-underscore-dangle
Expand All @@ -24,7 +25,11 @@ async function main() {
const container = document.getElementById('root');
const { default: App } = await import('./app');
try {
await render(<App context={{ insertCss }} />, container);
await render((
<Router>
<App context={{ insertCss }} />
</Router>
), container);
} catch (error) {
console.error(error); // eslint-disable-line no-console
}
Expand Down
28 changes: 23 additions & 5 deletions src/components/header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,23 @@ import ToolBar from 'material-ui/Toolbar';
import MenuIcon from 'material-ui/svg-icons/menu';
import customPropTypes from 'material-ui/utils/customPropTypes';
import React, { Component, PropTypes } from 'react';
import { Link } from 'react-router-dom';

const styleSheet = createStyleSheet('Header', () => ({
title: {
flex: 1,
},
titleLink: {
color: 'inherit',
textDecoration: 'none',
},
}));

export default class Header extends Component {
static contextTypes = {
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}).isRequired,
setVideo: PropTypes.func.isRequired,
styleManager: customPropTypes.muiRequired,
};
Expand Down Expand Up @@ -58,7 +66,11 @@ export default class Header extends Component {

componentWillUpdate(nextProps) {
if (nextProps.videoUri && this.props.videoUri !== nextProps.videoUri) {
this.setState({ open: false });
const newState = { open: false };
if (this.state.videoUri !== nextProps.videoUri) {
newState.videoUri = nextProps.videoUri;
}
this.setState(newState);
}
}

Expand All @@ -85,10 +97,14 @@ export default class Header extends Component {
}

handleSubmit = (event) => {
const { videoUri } = this.state;
event.preventDefault();
this.context.setVideo({
uri: this.state.videoUri,
});
if (videoUri) {
this.setState({ open: false }, () => {
this.context.setVideo({ uri: videoUri })
.then(() => this.context.history.push(`/player/?uri=${videoUri}`));
});
}
return false;
}

Expand All @@ -101,7 +117,9 @@ export default class Header extends Component {
<IconButton contrast>
<MenuIcon onClick={this.handleClick} />
</IconButton>
<Text className={classes.title} colorInherit type="title">TV</Text>
<Text className={classes.title} colorInherit type="title">
<Link className={classes.titleLink} to="/">TV</Link>
</Text>
<Button contrast onClick={this.handleClick} primary>Open</Button>
<Dialog onRequestClose={this.handleRequestClose} open={this.state.open}>
<form action="/" onSubmit={this.handleSubmit}>
Expand Down
9 changes: 9 additions & 0 deletions src/components/home.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export default function Home() {
return (
<div>
{/* home */ }
</div>
);
}
33 changes: 33 additions & 0 deletions src/components/loader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { Component, PropTypes } from 'react';

export default class Loader extends Component {
static displayName = 'Loader';

static propTypes = {
name: PropTypes.string.isRequired,
};

state = {
component: null,
};

componentDidMount() {
const { name, ...otherProps } = this.props;
import(`./${name}`)
.then(({ default: C }) => {
this.setState({
component: (
<C {...otherProps} />
),
});
});
}

shouldComponentUpdate(nextProps, nextState) {
return this.state.component !== nextState.component;
}

render() {
return this.state.component;
}
}
73 changes: 8 additions & 65 deletions src/components/main.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import React, { Component, PropTypes } from 'react';
import URLSearchParams from 'url-search-params';
import { Route } from 'react-router-dom';
import Loader from './loader';
import Header from './header';
import Player from './player';

function getQueryString() {
if (typeof location === 'undefined') {
return '';
}
return (location.search || '?').slice(1);
}

export default class Main extends Component {
static childContextTypes = {
Expand All @@ -28,67 +21,17 @@ export default class Main extends Component {
};
}

componentWillMount() {
const queryString = getQueryString();
const searchParams = new URLSearchParams(queryString);
const videoUri = searchParams.get('uri');
if (videoUri) {
this.setState({ videoUri });
} else {
this.setState({
open: true,
});
}
}

componentDidMount() {
window.addEventListener('popstate', this.handlePopState);
}

shouldComponentUpdate(nextProps, nextState) {
return (
this.state.open !== nextState.open ||
this.state.videoUri !== nextState.videoUri
);
}

componentWillUpdate(nextProps, nextState) {
const { open, videoUri } = nextState;
if (this.state.videoUri !== videoUri) {
if (videoUri && open) {
this.setState({
open: false,
});
}
const currentUri = location.pathname + location.search;
const uri = `/${videoUri ? `?uri=${encodeURIComponent(videoUri)}` : ''}`;
if (currentUri !== uri) {
history.pushState({ videoUri }, document.title, uri);
}
}
}

componentWillUnmount() {
window.removeEventListener('popstate', this.handlePopState);
}

setVideo = ({ uri }) => {
this.setState({
videoUri: uri,
});
}

handlePopState = ({ state }) => {
const { videoUri = '' } = state || {};
this.setState({ videoUri });
}
setVideo = async ({ uri }) => new Promise((resolve) => {
this.setState({ videoUri: uri }, resolve);
})

render() {
return (
<div>
<Header videoUri={this.state.videoUri} />
<Header open={this.state.open} videoUri={this.state.videoUri} />
<main>
<Player src={this.state.videoUri} />
<Route exact path="/" render={props => <Loader name="home" {...props} />} />
<Route path="/player/" render={props => <Loader name="player" {...props} />} />
</main>
</div>
);
Expand Down
59 changes: 38 additions & 21 deletions src/components/player.jsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,57 @@
import { createStyleSheet } from 'jss-theme-reactor/styleSheet';
import customPropTypes from 'material-ui/utils/customPropTypes';
import React, { Component, PropTypes } from 'react';
import URLSearchParams from 'url-search-params';
import Video from './video';

const styleSheet = createStyleSheet('Player', () => ({
player: {
'&[aria-hidden]:not([aria-hidden="false"])': {
display: 'none',
},
},
}));

export default class Player extends Component {
static contextTypes = {
styleManager: customPropTypes.muiRequired,
};

static defaultProps = {
src: null,
};
setVideo: PropTypes.func.isRequired,
}

static displayName = 'Player';

static propTypes = {
src: PropTypes.string,
location: PropTypes.shape({
search: PropTypes.string.isRequired,
}).isRequired,
};

constructor(props, ...args) {
super(props, ...args);
const { search } = props.location;
const searchParams = new URLSearchParams(search);
Object.assign(this.state, {
src: searchParams.get('uri') || null,
});
}

state = {
src: null,
};

componentWillMount() {
const { src: videoUri } = this.state;
if (videoUri) {
this.context.setVideo({ uri: videoUri });
}
}

shouldComponentUpdate(nextProps) {
return this.props.src !== nextProps.src;
return this.props.location.search !== nextProps.location.search;
}

componentWillUpdate(nextProps) {
const { search } = nextProps.location;
const searchParams = new URLSearchParams(search);
const videoUri = searchParams.get('uri');
if (videoUri && this.state.src !== videoUri) {
this.setState({ src: videoUri });
}
}

render() {
const classes = this.context.styleManager.render(styleSheet);
return (
<div aria-hidden={!this.props.src} className={classes.player}>
<Video src={this.props.src} />
<div data-video-uri={this.state.src || false}>
<Video src={this.state.src} />
</div>
);
}
Expand Down
Loading

0 comments on commit 651dbbe

Please sign in to comment.