├── 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 | 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 --------------------------------------------------------------------------------