├── assets
└── index.css
├── src
├── assets
│ └── css
│ │ ├── my.scss
│ │ ├── index.scss
│ │ └── _base.scss
├── favicon.ico
├── components
│ ├── VideoItem
│ │ ├── index.js
│ │ ├── VideoItem.scss
│ │ └── VideoItem.js
│ ├── FloatDownloadBtn
│ │ ├── index.js
│ │ ├── FloatDownloadBtn.scss
│ │ └── FloatDownloadBtn.js
│ ├── Model.js
│ └── Loading.js
├── store
│ ├── actions
│ │ ├── thunk.js
│ │ └── home.js
│ ├── constants.js
│ └── reducers
│ │ └── index.js
├── app
│ ├── index.js
│ ├── router
│ │ ├── routes.js
│ │ └── index.js
│ ├── configureStore.js
│ └── createApp.js
├── index.ejs
├── pages
│ ├── user
│ │ ├── index.js
│ │ ├── containers
│ │ │ └── userContainer.js
│ │ └── components
│ │ │ └── userPage.js
│ └── home
│ │ ├── index.js
│ │ ├── containers
│ │ └── homeContainer.js
│ │ ├── components
│ │ └── homePage.js
│ │ └── reducer
│ │ └── index.js
└── index.js
├── .gitignore
├── server
├── app.js
├── ignore.js
├── index.js
└── clientRouter.js
├── package.json
├── README.md
└── config
├── webpack.config.dev.js
└── webpack.config.prod.js
/assets/index.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-size: 30px;
3 | }
--------------------------------------------------------------------------------
/src/assets/css/my.scss:
--------------------------------------------------------------------------------
1 |
2 | div{
3 | color:red;
4 | }
--------------------------------------------------------------------------------
/src/assets/css/index.scss:
--------------------------------------------------------------------------------
1 | body{
2 | font-size: 20px;
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | .idea/
4 | dist/
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wlx200510/react_koa_ssr/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | import Koa from 'koa';
2 |
3 | const app=new Koa();
4 |
5 | export default app;
--------------------------------------------------------------------------------
/src/components/VideoItem/index.js:
--------------------------------------------------------------------------------
1 | import VideoItem from './VideoItem'
2 |
3 | export default VideoItem
4 |
--------------------------------------------------------------------------------
/src/components/VideoItem/VideoItem.scss:
--------------------------------------------------------------------------------
1 | .route--active {
2 | font-weight: bold;
3 | text-decoration: underline;
4 | }
--------------------------------------------------------------------------------
/src/components/FloatDownloadBtn/index.js:
--------------------------------------------------------------------------------
1 | import FloatDownloadBtn from './FloatDownloadBtn'
2 |
3 | export default FloatDownloadBtn
--------------------------------------------------------------------------------
/src/components/Model.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 |
3 | const Model=()=>
4 |
yyy
5 |
6 | export default Model;
--------------------------------------------------------------------------------
/src/store/actions/thunk.js:
--------------------------------------------------------------------------------
1 | import {getHomeInfo} from './home';
2 |
3 | export const homeThunk=store=>store.dispatch(getHomeInfo())
4 |
5 |
--------------------------------------------------------------------------------
/src/store/constants.js:
--------------------------------------------------------------------------------
1 | export const ADD='ADD';
2 | export const GET_HOME_INFO='GET_HOME_INFO';
3 | export const GET_ALBUM_DATA='GET_ALBUM_DATA';
4 |
5 |
--------------------------------------------------------------------------------
/src/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 |
3 | const Loading = props => {
4 | console.log(props);
5 | return Loading...
;
6 | };
7 |
8 | export default Loading;
--------------------------------------------------------------------------------
/src/app/index.js:
--------------------------------------------------------------------------------
1 | import configureStore from './configureStore';
2 | import createApp from './createApp';
3 | import routesConfig from './router/routes';
4 | //暴露给后端渲染用
5 | export default {
6 | configureStore,
7 | createApp,
8 | routesConfig
9 | }
--------------------------------------------------------------------------------
/src/app/router/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import homeRouter from '../../pages/home'
3 | import userRouter from '../../pages/user'
4 |
5 | const routesConfig=[
6 | homeRouter,
7 | userRouter,
8 | ];
9 |
10 | export default routesConfig;
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server/ignore.js:
--------------------------------------------------------------------------------
1 | const ignore=()=> {
2 | var extensions = ['.css', '.scss','.less','.png','.jpg','.gif']; //服务端渲染不加载的文件类型
3 | for (let i = 0, len = extensions.length; i < len; i++) {
4 | require.extensions[extensions[i]] = function () {
5 | return false;
6 | };
7 | }
8 | }
9 | module.exports = ignore;
--------------------------------------------------------------------------------
/src/store/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {combineReducers} from 'redux';
2 | import {HomeReducer} from '../../pages/home/reducer';
3 | import { routerReducer } from 'react-router-redux'
4 | // 这里这个Reducer名称对应到全局state的命名空间(如 state.HomeReducer)
5 | export default combineReducers({
6 | router:routerReducer,
7 | HomeReducer
8 | })
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= htmlWebpackPlugin.options.title %>
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/pages/user/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loadable from 'react-loadable';
3 | import Loading from '../../components/Loading'
4 |
5 | const LoadableUser = Loadable({
6 | loader: () => import(/* webpackChunkName: 'User' */ "./containers/userContainer.js"),
7 | loading: Loading
8 | });
9 |
10 | const userRouter = {
11 | path: "/user",
12 | component: LoadableUser,
13 | thunk: () => {}
14 | };
15 |
16 | export default userRouter
--------------------------------------------------------------------------------
/src/pages/user/containers/userContainer.js:
--------------------------------------------------------------------------------
1 | import userPage from '../components/userPage'
2 | import {connect} from 'react-redux'
3 | import {bindActionCreators} from 'redux'
4 | import * as actions from '../../../store/actions/home';
5 |
6 | const mapStateToProps = state => ({
7 | count: state.HomeReducer.count
8 | });
9 |
10 | const mapDispatchToProps = dispatch =>
11 | bindActionCreators(
12 | {
13 | add: actions.add
14 | },
15 | dispatch
16 | );
17 |
18 | export default connect(mapStateToProps, mapDispatchToProps)(userPage);
--------------------------------------------------------------------------------
/src/app/router/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {Route, Switch } from 'react-router-dom';
3 | import {ConnectedRouter} from 'react-router-redux';
4 | import routesConfig from './routes';
5 |
6 | const Routers=({history})=>(
7 |
8 |
9 | {
10 | routesConfig.map(route=>(
11 |
12 | ))
13 | }
14 |
15 |
16 | )
17 |
18 | export default Routers;
--------------------------------------------------------------------------------
/src/pages/home/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Loadable from 'react-loadable';
3 | import Loading from '../../components/Loading'
4 |
5 | import {homeThunk} from '../../store/actions/thunk';
6 |
7 |
8 | const LoadableHome = Loadable({
9 | loader: () =>import(/* webpackChunkName: 'Home' */'./containers/homeContainer.js'),
10 | loading: Loading,
11 | });
12 |
13 | const HomeRouter = {
14 | path: '/',
15 | exact: true,
16 | component: LoadableHome,
17 | thunk: homeThunk // 服务端渲染会开启并执行这个action,用于获取页面渲染所需数据
18 | }
19 |
20 | export default HomeRouter
--------------------------------------------------------------------------------
/src/pages/home/containers/homeContainer.js:
--------------------------------------------------------------------------------
1 | import homePage from '../components/homePage'
2 | import {connect} from 'react-redux';
3 | import {bindActionCreators} from 'redux';
4 | import * as actions from '../../../store/actions/home';
5 | // 这里的state要看清楚是store的state
6 | const mapStateToProps = state => ({
7 | count: state.HomeReducer.count,
8 | homeInfo: state.HomeReducer.homeInfo
9 | });
10 |
11 | const mapDispatchToProps = dispatch =>
12 | bindActionCreators(
13 | {
14 | add: actions.add,
15 | getHomeInfo: actions.getHomeInfo
16 | },
17 | dispatch
18 | );
19 |
20 | export default connect(mapStateToProps, mapDispatchToProps)(homePage);
--------------------------------------------------------------------------------
/src/store/actions/home.js:
--------------------------------------------------------------------------------
1 | import {ADD,GET_HOME_INFO} from '../constants'
2 | export const add=(count)=>({type: ADD, count,})
3 |
4 | export const getHomeInfo=(sendId=1)=>async(dispatch,getState)=>{
5 | let {name,age,id}=getState().HomeReducer.homeInfo;
6 | if (id === sendId) {
7 | return
8 | }
9 | //上面的return是通过对请求id和已有数据的标识性id进行对比校验,避免重复获取数据。
10 | console.log('footer'.includes('foo'))
11 | await new Promise(resolve=>{
12 | let homeInfo={name:'wd2010',age:'25',id:sendId}
13 | console.log('-----------请求getHomeInfo')
14 | setTimeout(()=>resolve(homeInfo),1000)
15 | }).then(homeInfo=>{
16 | dispatch({type:GET_HOME_INFO,data:{homeInfo}})
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/FloatDownloadBtn/FloatDownloadBtn.scss:
--------------------------------------------------------------------------------
1 | @import '../../assets/css/_base.scss';
2 | @media (min-width: 1080px) {
3 | .float_btn {
4 | display: none;
5 | }
6 | }
7 | @media (max-width: 1080px) {
8 | .float_btn {
9 | border-radius: pr(54);
10 | background: #ff3144;
11 | margin: 0 auto;
12 | text-align: center;
13 | padding: pr(30) 0 pr(30) 0;
14 | width: pr(726);
15 | color: #fff;
16 | font-size: pr(42);
17 | font-weight: bold;
18 | position: fixed;
19 | z-index: 300;
20 | left: 50%;
21 | bottom: 0;
22 | transform: translate(-50%, 100%);
23 | }
24 | .is-shown {
25 | transform: translate(-50%, -2.43rem);
26 | transition: transform .6s 1s;
27 | }
28 | }
--------------------------------------------------------------------------------
/src/app/configureStore.js:
--------------------------------------------------------------------------------
1 | import {createStore, applyMiddleware,compose} from "redux";
2 | import thunkMiddleware from "redux-thunk";
3 | import createHistory from 'history/createMemoryHistory';
4 | import { routerReducer, routerMiddleware } from 'react-router-redux'
5 | import rootReducer from '../store/reducers/index.js';
6 |
7 | const routerReducers=routerMiddleware(createHistory());//路由
8 | const devComposer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
9 | const composeEnhancers = process.env.NODE_ENV=='development' ? devComposer : compose
10 |
11 | const middleware=[thunkMiddleware,routerReducers];
12 |
13 | let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));
14 |
15 | export default configureStore;
16 |
--------------------------------------------------------------------------------
/src/app/createApp.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Provider} from 'react-redux';
3 | import Routers from './router/index';
4 | import Loadable from 'react-loadable';
5 |
6 | const createApp=({store,history,modules})=>{
7 | console.log(process.env.NODE_ENV==='production',process.env.NODE_ENV)
8 | if(process.env.NODE_ENV==='production'){
9 | return (
10 | modules.push(moduleName)}>
11 |
12 |
13 |
14 |
15 | )
16 |
17 | }else{
18 | return (
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | }
26 |
27 | export default createApp;
28 |
--------------------------------------------------------------------------------
/src/pages/user/components/userPage.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 | import {Link } from 'react-router-dom';
3 | import '../../../assets/css/index.scss';
4 |
5 | class User extends Component {
6 | handerClick(e) {
7 | import(/* webpackChunkName: 'Model' */ "../../../components/Model.js").then(
8 | ({ default: Model }) => {
9 | console.log("====按需加载Modal");
10 | }
11 | );
12 | }
13 |
14 | render() {
15 | let { count } = this.props;
16 | return (
17 |
18 |
{count}
19 |
ddsdfd
20 |
21 | {[1, 2, 3, 4, 5, 6].map((item, index) => (
22 | - aabdddb{item}
23 | ))}
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export default User
--------------------------------------------------------------------------------
/src/pages/home/components/homePage.js:
--------------------------------------------------------------------------------
1 | import React,{Component} from 'react';
2 | import {Link} from 'react-router-dom';
3 | import '../../../assets/css/my.scss'
4 | class Home extends Component{
5 | state={
6 | hasError:false,
7 | }
8 |
9 | componentDidMount(){
10 | console.log(this.props)
11 | this.props.getHomeInfo(1)
12 | }
13 |
14 | componentDidCatch(error, info) {
15 | this.setState({ hasError: true });
16 |
17 | // 在这里可以做异常的上报
18 | console.log('发送错误':error,info)
19 | }
20 | render(){
21 | let {add,count,homeInfo:{name,age}}=this.props;
22 | return (
23 |
24 |
{count}
25 |
名字:{name} - 年龄:{age}
26 |
27 |
User
28 |
29 | )
30 | }
31 | }
32 |
33 | export default Home
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require('./ignore.js')();
2 | require('babel-polyfill');
3 | require('babel-register')({
4 | presets: ['env', 'react', 'stage-0'],
5 | plugins: ["react-loadable/babel",'syntax-dynamic-import',"dynamic-import-node"]
6 | });
7 |
8 |
9 | const app=require('./app.js').default,
10 | clientRouter=require('./clientRouter.js').default,
11 | port = process.env.port || 3002,
12 | staticCache = require("koa-static-cache"),
13 | path =require('path'),
14 | cors=require('koa2-cors'),
15 | Loadable=require('react-loadable');
16 |
17 | app.use(cors());
18 | app.use(clientRouter);
19 | app.use(staticCache (path.resolve(__dirname,'../dist'),{
20 | maxAge: 365 * 24 * 60 * 60,
21 | gzip:true
22 | }));
23 |
24 |
25 | console.log(`\n==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.\n`)
26 | Loadable.preloadAll().then(() => {
27 | app.listen(port)
28 | })
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/pages/home/reducer/index.js:
--------------------------------------------------------------------------------
1 | import {ADD,GET_HOME_INFO} from '../../../store/constants';
2 |
3 | const ACTION_HANDLERS = {
4 | [ADD]: (state, action) => Object.assign({}, state, { count: action.count }),
5 | [GET_HOME_INFO]: (state, action) => Object.assign({}, state, action.data)
6 | };
7 |
8 | const initialState = {
9 | count: 33,
10 | homeInfo: {name: '', age: '', id: ''}
11 | }
12 |
13 | export const HomeReducer = function(state = initialState, action) {
14 | const handler = ACTION_HANDLERS[action.type]
15 | return handler ? handler(state, action) : state
16 | }
17 |
18 |
19 | // export const counter=(state={count:33},action)=>{
20 | // switch (action.type){
21 | // case ADD:
22 | // return Object.assign({},state,{count: action.count});
23 | // default:
24 | // return state;
25 | // }
26 | // }
27 |
28 | // export const homeInfo=(state={name:'',age:''},action)=>{
29 | // switch(action.type){
30 | // case GET_HOME_INFO:
31 | // return Object.assign({},state,action.data);
32 | // default:
33 | // return state;
34 | // }
35 | // }
--------------------------------------------------------------------------------
/src/assets/css/_base.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Application Settings Go Here
3 | ------------------------------------
4 | This file acts as a bundler for all variables/mixins/themes, so they
5 | can easily be swapped out without `core.scss` ever having to know.
6 |
7 | For example:
8 |
9 | @import './variables/colors';
10 | @import './variables/components';
11 | @import './themes/default';
12 | */
13 | $orange_red:#fe362d;
14 | $grey_color:#7f7f7f;
15 |
16 | // px转rem
17 | @function pr($px, $base: 32) {
18 | @return ($px / $base) * 1rem / 1.44;
19 | }
20 |
21 | html, body, h1, p, ul, li, h3, h6 {
22 | padding: 0;
23 | margin: 0;
24 | }
25 | h3, h6 {
26 | font-weight: normal;
27 | }
28 | li {
29 | list-style: none;
30 | }
31 | body {
32 | background: #f5f5f5;
33 | font-family: Arial, "Microsoft YaHei";
34 | }
35 |
36 | /* 清理浮动 */
37 | .fn-clear::after {
38 | visibility: hidden;
39 | display: block;
40 | font-size: 0;
41 | content: " ";
42 | clear: both;
43 | height: 0;
44 | }
45 |
46 | .fn-clear {
47 | zoom: 1; /* for IE6 IE7 */
48 | }
49 |
50 | a {
51 | text-decoration: none;
52 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {hydrate,render} from 'react-dom';
3 | import createHistory from 'history/createBrowserHistory'
4 | import Loadable from 'react-loadable';
5 | import app from './app/index.js';
6 | const initialState =window && window.__INITIAL_STATE__;
7 | let history=createHistory()
8 | let {configureStore,createApp}=app;
9 | let store=configureStore(initialState)
10 |
11 | const renderApp=()=>{
12 | let application=createApp({store,history});
13 | hydrate(application,document.getElementById('root'));
14 | }
15 |
16 | window.main = () => {
17 | Loadable.preloadReady().then(() => {
18 | renderApp()
19 | });
20 | };
21 |
22 |
23 | if(process.env.NODE_ENV==='development'){
24 | if(module.hot){
25 | module.hot.accept('./store/reducers/index.js',()=>{
26 | let newReducer=require('./store/reducers/index.js');
27 | store.replaceReducer(newReducer)
28 | /*import('./store/reducers/index.js').then(({default:module})=>{
29 | store.replaceReducer(module)
30 | })*/
31 | })
32 | module.hot.accept('./app/index.js',()=>{
33 | let {createApp}=require('./app/index.js');
34 | let newReducer=require('./store/reducers/index.js');
35 | store.replaceReducer(newReducer)
36 | let application=createApp({store,history});
37 | hydrate(application,document.getElementById('root'));
38 | /*import('./app/index.js').then(({default:module})=>{
39 | let {createApp}=module;
40 | import('./store/reducers/index.js').then(({default:module})=>{
41 | store.replaceReducer(module)
42 | let application=createApp({store,history});
43 | render(application,document.getElementById('root'));
44 | })
45 | })*/
46 | })
47 | }
48 | }
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "webpack-demo1",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "cross-env NODE_ENV=development webpack-dev-server --hotOnly --progress --config ./config/webpack.config.dev.js ",
8 | "build": "cross-env NODE_ENV=production webpack --progress -p --config ./config/webpack.config.prod.js",
9 | "server": "cross-env NODE_ENV=production nodemon ./server/index.js --watch config --watch server",
10 | "dev:server": "npm run build && npm run server"
11 | },
12 | "author": "",
13 | "license": "ISC",
14 | "devDependencies": {
15 | "babel-core": "^6.26.0",
16 | "babel-loader": "^7.1.2",
17 | "babel-plugin-add-module-exports": "^0.2.1",
18 | "babel-plugin-dynamic-import-node": "^1.2.0",
19 | "babel-plugin-dynamic-import-webpack": "^1.0.2",
20 | "babel-plugin-remove-webpack": "^1.1.0",
21 | "babel-plugin-transform-runtime": "^6.23.0",
22 | "babel-polyfill": "^6.26.0",
23 | "babel-preset-env": "^1.6.1",
24 | "babel-preset-react": "^6.24.1",
25 | "babel-preset-stage-0": "^6.24.1",
26 | "babel-register": "^6.26.0",
27 | "clean-webpack-plugin": "^0.1.17",
28 | "copy-webpack-plugin": "^4.3.0",
29 | "cross-env": "^5.1.1",
30 | "css-loader": "^0.28.7",
31 | "extract-text-webpack-plugin": "^3.0.2",
32 | "file-loader": "^1.1.6",
33 | "html-webpack-plugin": "^2.30.1",
34 | "less": "^2.7.3",
35 | "less-loader": "^4.0.5",
36 | "node-sass": "^4.7.2",
37 | "nodemon": "^1.12.5",
38 | "npm-run-all": "^4.1.2",
39 | "postcss-loader": "^2.0.9",
40 | "progress-bar-webpack-plugin": "^1.10.0",
41 | "sass-loader": "^6.0.6",
42 | "style-loader": "^0.19.1",
43 | "url-loader": "^0.6.2",
44 | "webpack": "^3.10.0",
45 | "webpack-dev-server": "^2.9.7",
46 | "webpack-manifest-plugin": "^1.3.2"
47 | },
48 | "dependencies": {
49 | "history": "^4.7.2",
50 | "koa": "^2.4.1",
51 | "koa-static-cache": "^5.1.1",
52 | "koa2-cors": "^2.0.5",
53 | "react": "^16.2.0",
54 | "react-dom": "^16.2.0",
55 | "react-helmet": "^5.2.0",
56 | "react-loadable": "^5.3.1",
57 | "react-redux": "^5.0.6",
58 | "react-router-config": "^1.0.0-beta.4",
59 | "react-router-dom": "^4.2.2",
60 | "react-router-redux": "^5.0.0-alpha.6",
61 | "redux": "^3.7.2",
62 | "redux-thunk": "^2.2.0"
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/FloatDownloadBtn/FloatDownloadBtn.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import './FloatDownloadBtn.scss'
3 |
4 | class FloatDownloadBtn extends Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = {
8 | floatBtnClass: 'float_btn'
9 | }
10 | }
11 |
12 | downloadApp = ()=>{
13 | // const { clickName, albumId } = this.props;
14 | var userAgent = navigator.userAgent;
15 | if(this.state.ckey === 'miWeather'){
16 | window.location.href = "mimarket://details?id=com.xiangkan.android&back=true&ref=xiangkan"
17 | } else if (/iphone/ig.test(userAgent)) { // iphone 跳转到app store
18 | window.location.href = 'https://lkme.cc/6DD/gIJkk8FTI/';
19 | } else if (/android/ig.test(userAgent)) { // 安卓 下载apk文件
20 | if (/MicroMessenger/ig.test(userAgent)) { // 微信
21 | window.location.href = 'https://a.app.qq.com/o/simple.jsp?pkgname=com.xiangkan.android&ckey=CK1359495491899';
22 | } else {
23 | let url = encodeURI('imiui://www.miui.com/xiangkan/home/page');
24 | // if(clickName == 'album_downloadApp') {
25 | // url = encodeURI(`imiui://www.miui.com/xiangkan/feed/album?albumId=${albumId}`);
26 | // }else{
27 | // url = encodeURI('imiui://www.miui.com/xiangkan/home/page');
28 | // }
29 | window.location.href = `imiui://www.miui.com/xiangkan/transition/page?dispatch_uri=${url}`;
30 | var t = Date.now();
31 | setTimeout(function(){
32 | if (Date.now() - t < 1200) {
33 | window.location.href = 'http://api.xk.miui.com/dispatch/index/homepage';
34 | }
35 | }, 1000);
36 | return false;
37 | }
38 | }
39 | localStorage.ckey = this.state.ckey;
40 | }
41 |
42 | changeFloatBtnClass() {
43 | this.setState({
44 | floatBtnClass: 'float_btn is-shown'
45 | })
46 | }
47 |
48 | componentDidMount(){
49 | this.timer = setTimeout(() => this.changeFloatBtnClass(), 500);
50 | }
51 | componentWillUnmount() {
52 | this.timer = null;
53 | }
54 |
55 | render() {
56 | //var currentLocation = this.props.location.pathname;
57 | return (
58 | {this.downloadApp()}}>
59 | 打开想看,查看更多精彩视频
60 |
61 | )
62 | }
63 | }
64 |
65 | export default FloatDownloadBtn
--------------------------------------------------------------------------------
/src/components/VideoItem/VideoItem.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {Link} from 'react-router-dom';
3 | import defineImg from '../../assets/img/define_img.jpg'
4 |
5 | class VideoItem extends Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | formatTime = (duration) => {
11 | var duration = duration / 1000;
12 | let minute = parseInt(duration / 60);
13 | let second= parseInt(duration % 60);
14 | if (minute < 10) {
15 | minute = '0' + minute;
16 | }
17 | if (second < 10) {
18 | second = '0' + second;
19 | }
20 | return minute + ':' + second;
21 | }
22 |
23 | render() {
24 | //var currentLocation = this.props.location.pathname;
25 | const { data } = this.props;
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
{data.title}
33 |
{data.playCount}次观看
34 |
35 |
{this.formatTime(data.duration)}
36 |
37 |
38 |
39 |
{this.downloadApp(data.videoId)}}>
40 |
41 |
42 | {data.authorInfo.headurl ?

:
43 |

}
44 |
45 |
46 |
{data.authorInfo.nickname}
47 |
48 |
49 |
50 |
51 | {data.commentCount}
52 |
53 |
54 |
55 | {data.likeCount}
56 |
57 |
58 |
59 |
60 | )
61 | }
62 | }
63 |
64 | export default VideoItem
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React-koa-ssr
2 | > 本脚手架适合已有的react单页面项目迁移到服务端渲染,又不想改动较大,不适合用NEXT框架重新写的项目.
3 |
4 | webpack+router4+按需加载+webpack-dev-server 实现react服务端渲染(koa)
5 |
6 |
7 | ## 目录介绍
8 |
9 | ```
10 | ├── assets
11 | │ └── index.css //放置一些全局的资源文件 可以是js 图片等
12 | ├── config
13 | │ ├── webpack.config.dev.js 开发环境webpack打包设置
14 | │ └── webpack.config.prod.js 生产环境webpack打包设置
15 | ├── package.json
16 | ├── README.md
17 | ├── server server端渲染文件,如果对不是很了解,建议参考[koa教程](http://wlxadyl.cn/2018/02/11/koa-learn/)
18 | │ ├── app.js
19 | │ ├── clientRouter.js // 在此文件中包含了把服务端路由匹配到react路由的逻辑
20 | │ ├── ignore.js
21 | │ └── index.js
22 | └── src
23 | ├── app 此文件夹下主要用于放置浏览器和服务端通用逻辑
24 | │ ├── configureStore.js //redux-thunk设置
25 | │ ├── createApp.js //根据渲染环境不同来设置不同的router模式
26 | │ ├── index.js
27 | │ └── router
28 | │ ├── index.js
29 | │ └── routes.js //路由配置文件! 重要
30 | ├── assets
31 | │ ├── css 放置一些公共的样式文件
32 | │ │ ├── _base.scss //很多项目都会用到的初始化css
33 | │ │ ├── index.scss
34 | │ │ └── my.scss
35 | │ └── img
36 | ├── components 放置一些公共的组件
37 | │ ├── FloatDownloadBtn 公共组件样例写法
38 | │ │ ├── FloatDownloadBtn.js
39 | │ │ ├── FloatDownloadBtn.scss
40 | │ │ └── index.js
41 | │ ├── Loading.js
42 | │ └── Model.js 函数式组件的写法
43 | │
44 | ├── favicon.ico
45 | ├── index.ejs //渲染的模板 如果项目需要,可以放一些公共文件进去
46 | ├── index.js //包括热更新的逻辑
47 | ├── pages 页面组件文件夹
48 | │ ├── home
49 | │ │ ├── components // 用于放置页面组件,主要逻辑
50 | │ │ │ └── homePage.js
51 | │ │ ├── containers // 使用connect来封装出高阶组件 注入全局state数据
52 | │ │ │ └── homeContainer.js
53 | │ │ ├── index.js // 页面路由配置文件 注意thunk属性
54 | │ │ └── reducer
55 | │ │ └── index.js // 页面的reducer 这里暴露出来给store统一处理 注意写法
56 | │ └── user
57 | │ ├── components
58 | │ │ └── userPage.js
59 | │ ├── containers
60 | │ │ └── userContainer.js
61 | │ └── index.js
62 | └── store
63 | ├── actions // 各action存放地
64 | │ ├── home.js
65 | │ └── thunk.js
66 | ├── constants.js // 各action名称汇集处 防止重名
67 | └── reducers
68 | └── index.js // 引用各页面的所有reducer 在此处统一combine处理
69 | ```
70 |
71 | 项目实践中发现,页面的reducer跟页面的数据需求和数据结构强相关,获取到新数据后的处理应由页面本身处理。
72 | action是请求数据的触发器,通过与具体页面解耦,从而使逻辑更清晰。
73 | 还有一点就是这些action可能正是服务端渲染需要的数据,从而让服务端渲染调用数据获取函数更方便。
74 |
75 | 开发环境使用webpack-dev-server做服务端,实现热加载,生产环境使用koa做后端,实现按需加载,页面渲染前加载数据。
76 |
77 | 1. npm install
78 | 2. npm start 运行开发版环境
79 |
80 |
81 | -------------------------------------------------
82 |
83 | 1. npm install
84 | 2. npm run build 生产环境编译 dist/client+dist/server
85 | 3. npm run server 运行koa
86 | 4. npm run dev:server 本地运行koa检查服务端渲染
87 |
88 |
89 |
90 | -------------------------------------------------
91 | 新增特性
92 | 1. 通过config中webpack配置的全局options来动态改写网页标题和公共路径(HtmlWebpackPlugin)
93 | 2. v4的react路由,但仍可以通过路由数组进行配置
94 | 3. 对页面组件进行分层,将reducer与页面结合,action与页面解耦
95 | 4. 服务端渲染时向页面注入全局变量保存获取的数据,从而react初始化时可以获取到渲染数据
96 | 5. 打包时的js压缩插件更新到最新,可以支持ES6语法,启用平行压缩
--------------------------------------------------------------------------------
/server/clientRouter.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {renderToString} from 'react-dom/server';
3 | import createHistory from 'history/createMemoryHistory'
4 | import { getBundles } from 'react-loadable/webpack';
5 | import stats from '../dist/react-loadable.json';
6 | import Helmet from 'react-helmet';
7 | import {matchPath} from 'react-router-dom';
8 |
9 | import { matchRoutes } from 'react-router-config';
10 | import client from '../src/app/index.js';
11 | import path from 'path';
12 | import fs from 'fs'
13 | let configureStore=client.configureStore;
14 | let createApp=client.createApp;
15 | let routesConfig=client.routesConfig;
16 |
17 | const createStore=(configureStore)=>{
18 | let store=configureStore()
19 | return store;
20 | }
21 |
22 | const createTags=(modules)=>{
23 | let bundles = getBundles(stats, modules);
24 | let scriptfiles = bundles.filter(bundle => bundle.file.endsWith('.js'));
25 | let stylefiles = bundles.filter(bundle => bundle.file.endsWith('.css'));
26 | let scripts=scriptfiles.map(script=>``).join('\n');
27 | let styles=stylefiles.map(style=>``).join('\n');
28 | return {scripts,styles}
29 | }
30 |
31 | const prepHTML=(data,{html,head,rootString,scripts,styles,initState})=>{
32 | data=data.replace('',`${head} \n ${styles}`);
34 | data=data.replace('',`${rootString}
`);
35 | data=data.replace('',` \n `);
36 | data=data.replace('',`${scripts}`);
37 | return data;
38 | }
39 |
40 | const getMatch=(routesArray, url)=>{
41 | return routesArray.some(router=>matchPath(url,{
42 | path: router.path,
43 | exact: router.exact,
44 | }))
45 | }
46 |
47 | const makeup=(ctx,store,createApp,html)=>{
48 | let initState=store.getState();
49 | let history=createHistory({initialEntries:[ctx.req.url]});
50 |
51 | let modules=[];
52 |
53 | let rootString= renderToString(createApp({store,history,modules}));
54 |
55 | let {scripts,styles}=createTags(modules)
56 |
57 | const helmet=Helmet.renderStatic();
58 | let renderedHtml=prepHTML(html,{
59 | html:helmet.htmlAttributes.toString(),
60 | head:helmet.title.toString()+helmet.meta.toString()+helmet.link.toString(),
61 | rootString,
62 | scripts,
63 | styles,
64 | initState
65 | })
66 | return renderedHtml;
67 | }
68 |
69 |
70 | const clientRouter=async(ctx,next)=>{
71 | let html=fs.readFileSync(path.join(path.resolve(__dirname,'../dist'),'index.html'),'utf-8');
72 | let store=createStore(configureStore);
73 | let pureRoutes='';
74 | // 这段逻辑是用于修复路径上有问号和参数时的匹配bug
75 | if (ctx.req.url.indexOf('?')>0) {
76 | pureRoutes=ctx.req.url.split("?")[0]
77 | } else {
78 | pureRoutes=ctx.req.url
79 | }
80 |
81 | let branch=matchRoutes(routesConfig,pureRoutes)
82 | let promises = branch.map(({route,match})=>{
83 | return route.thunk?(route.thunk(store)):Promise.resolve(null)
84 | });
85 | await Promise.all(promises).catch(err=>console.log('err:---',err))
86 |
87 | let isMatch=getMatch(routesConfig,pureRoutes);
88 | if(isMatch){
89 | let renderedHtml=await makeup(ctx,store,createApp,html);
90 | ctx.body=renderedHtml
91 | }
92 | await next()
93 | }
94 |
95 | export default clientRouter;
96 |
97 |
--------------------------------------------------------------------------------
/config/webpack.config.dev.js:
--------------------------------------------------------------------------------
1 | const path=require('path');
2 | const webpack=require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const ProgressBarPlugin = require('progress-bar-webpack-plugin');
5 | const CopyWebpackPlugin = require('copy-webpack-plugin');
6 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
7 | const rootPath=path.join(__dirname,'../')
8 | const devConfig={
9 | context: path.join(rootPath,'./src'),
10 | entry:{
11 | client:'./index.js',
12 | vendors:['react','react-dom','react-loadable','react-redux','redux','react-router-dom','react-router-redux','redux-thunk'],
13 | },
14 | output:{
15 | filename:'[name].[hash:8].js',
16 | path:path.resolve(rootPath,'./dist/client'),
17 | publicPath:'/',
18 | chunkFilename: '[name]-[hash:8].js'
19 | },
20 | resolve:{
21 | extensions:[".js",".jsx","css","less","scss","png","jpg"],
22 | modules:[path.resolve(rootPath, "src"), "node_modules"],
23 | },
24 | devServer:{
25 | contentBase:'assets',
26 | hot:true,
27 | historyApiFallback:true,
28 | },
29 | devtool:'source-map',
30 | module:{
31 | rules:[
32 | {
33 | test:/\.(js|jsx)$/,
34 | exclude: /node_modules/,
35 | include:path.resolve(rootPath, "src"),
36 | use:{
37 | loader:'babel-loader',
38 | options:{
39 | presets: ['env', 'react', 'stage-0'],
40 | plugins: ['transform-runtime', 'add-module-exports'],
41 | cacheDirectory: true,
42 | }
43 | }
44 | },{
45 | test:/\.(css|scss)$/,
46 | exclude:/node_modules/,
47 | include: path.resolve(rootPath, "src"),
48 | use: ExtractTextPlugin.extract({
49 | fallback:'style-loader',//style-loader 将css插入到页面的style标签
50 | use:[{
51 | loader: 'css-loader',//css-loader 是处理css文件中的url(),require()等
52 | options: {
53 | sourceMap:true,
54 | }
55 | },{
56 | loader:'postcss-loader',
57 | options: {
58 | plugins:()=>[require("autoprefixer")({browsers:'last 5 versions'})],
59 | sourceMap:true,
60 | }
61 | },{
62 | loader:'sass-loader',
63 | options:{
64 | sourceMap:true,
65 | }
66 | }]
67 | }),
68 | },{
69 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i,
70 | exclude:/node_modules/,
71 | use: {
72 | loader: 'url-loader',
73 | options: {
74 | limit: 1024,
75 | name: 'img/[sha512:hash:base64:7].[ext]'
76 | }
77 | }
78 | }
79 | ]
80 | },
81 | plugins:[
82 | new webpack.NoEmitOnErrorsPlugin(),
83 | new CopyWebpackPlugin([{from:'favicon.ico'}]),
84 | new webpack.HotModuleReplacementPlugin(),
85 | new ProgressBarPlugin({summary: false}),
86 | new ExtractTextPlugin({filename: 'style.[hash].css',}),
87 | new webpack.DefinePlugin({
88 | 'process.env.NODE_ENV':JSON.stringify(process.env.NODE_ENV||'development')
89 | }),
90 | new webpack.optimize.CommonsChunkPlugin({
91 | name:['vendors','manifest'],
92 | minChunks:2
93 | }),
94 | new HtmlWebpackPlugin({
95 | title:'test1',
96 | publicUrl: './',
97 | filename:'index.html',
98 | template:'./index.ejs',
99 | }),
100 | ],
101 | }
102 |
103 | module.exports=devConfig
--------------------------------------------------------------------------------
/config/webpack.config.prod.js:
--------------------------------------------------------------------------------
1 | const path=require('path');
2 | const webpack=require('webpack');
3 | const CleanWebpackPlugin = require("clean-webpack-plugin");
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 | const ManifestPlugin = require('webpack-manifest-plugin');
6 | const { ReactLoadablePlugin } =require('react-loadable/webpack') ;
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
9 | const isServer=process.env.BUILD_TYPE==='server';
10 | const rootPath=path.join(__dirname,'../');
11 | const UglifyJsPlugin=require('uglifyjs-webpack-plugin');
12 |
13 | const prodConfig={
14 | context: path.join(rootPath,'./src'),
15 | entry: {
16 | client:'./index.js',
17 | vendors:['react','react-dom','react-loadable','react-redux','redux','react-router-dom','react-router-redux','redux-thunk'],
18 | },
19 | output:{
20 | filename:'[name].[hash:8].js',
21 | path:path.resolve(rootPath,'./dist'),
22 | publicPath:'/',
23 | chunkFilename: '[name]-[hash:8].js',
24 | // libraryTarget: isServer?'commonjs2':'umd',
25 | },
26 | resolve:{
27 | extensions:[".js",".jsx",".css",".less",".scss",".png",".jpg"],
28 | modules:[path.resolve(rootPath, "src"), "node_modules"],
29 | },
30 | module:{
31 | rules:[
32 | {
33 | test:/\.jsx?$/,
34 | exclude: /node_modules/,
35 | include:path.resolve(rootPath, "src"),
36 | use:{
37 | loader:'babel-loader',
38 | options:{
39 | presets: ['env', 'react', 'stage-0'],
40 | plugins: ['transform-runtime', 'add-module-exports'] ,
41 | cacheDirectory: true,
42 | }
43 | }
44 | },{
45 | test:/\.(css|scss)$/,
46 | exclude:/node_modules/,
47 | include: path.resolve(rootPath, "src"),
48 | use: ExtractTextPlugin.extract({
49 | fallback:'style-loader',//style-loader将css chunk 插入到html中
50 | use:[{
51 | loader: 'css-loader',//css-loader 是处理css文件中的url(),require()等
52 | options: {
53 | minimize:true,
54 | }
55 | },{
56 | loader:'postcss-loader',
57 | options: {
58 | plugins:()=>[require('autoprefixer')({browsers:'last 5 versions'})],
59 | minimize:true,
60 | }
61 | },{
62 | loader:'sass-loader',
63 | options:{
64 | minimize:true,
65 | }
66 | }]
67 | }),
68 | },{
69 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i,
70 | exclude:/node_modules/,
71 | use: {
72 | loader: 'url-loader',
73 | options: {
74 | limit: 1024,
75 | name: 'img/[sha512:hash:base64:7].[ext]'
76 | }
77 | }
78 | }
79 | ]
80 | },
81 | plugins:[
82 | new ManifestPlugin(),
83 | new webpack.NoEmitOnErrorsPlugin(),
84 | new ExtractTextPlugin({
85 | filename: 'css/style.[hash].css',
86 | allChunks: true,
87 | }),
88 | new CopyWebpackPlugin([{from:'favicon.ico',to:rootPath+'./dist'}]),
89 | new CleanWebpackPlugin(['./dist'],{root: rootPath,}),
90 | new webpack.DefinePlugin({
91 | 'process.env.NODE_ENV':JSON.stringify(process.env.NODE_ENV)
92 | }),
93 | new webpack.optimize.OccurrenceOrderPlugin(),
94 | new HtmlWebpackPlugin({
95 | title:'yyy',
96 | publicUrl: './',
97 | filename:'index.html',
98 | template:'./index.ejs',
99 | }),
100 | new webpack.optimize.CommonsChunkPlugin({
101 | name:['vendors','manifest'],
102 | minChunks:2
103 | }),
104 | new ReactLoadablePlugin({
105 | filename: path.join(rootPath,'./dist/react-loadable.json'),
106 | }),
107 | new UglifyJsPlugin({
108 | parallel: true,
109 | sourceMap: true
110 | })
111 | ]
112 | }
113 |
114 | module.exports=prodConfig
--------------------------------------------------------------------------------