├── dev-public ├── favicon.ico └── index.html ├── src ├── containers │ ├── Test │ │ ├── test.less │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── Test.service.ts │ │ │ ├── Test.reducer.ts │ │ │ └── Test.action.ts │ │ └── test.tsx │ ├── Home │ │ ├── style.less │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── Home.service.ts │ │ │ ├── Home.reducer.ts │ │ │ └── Home.action.ts │ │ └── index.tsx │ ├── app.less │ └── app.tsx ├── static │ ├── img │ │ └── timg.jpg │ └── fonts │ │ ├── icomoon.eot │ │ ├── icomoon.ttf │ │ ├── icomoon.woff │ │ └── icomoon.svg ├── components │ └── Header │ │ ├── header.less │ │ └── index.tsx ├── service │ └── file.ts ├── saga │ └── index.ts ├── reducer │ └── index.ts ├── routes.ts ├── store.js ├── server │ ├── render.js │ └── index.js ├── client │ └── index.tsx ├── common │ └── icon-font.css └── routes.tsx ├── .gitignore ├── .babelrc ├── .editorconfig ├── tslint.json ├── configs ├── webpack.config.js ├── webpack.config.server.js ├── webpack.config.bak.js └── webpack.config.client.js ├── package.json └── tsconfig.json /dev-public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-music/HEAD/dev-public/favicon.ico -------------------------------------------------------------------------------- /src/containers/Test/test.less: -------------------------------------------------------------------------------- 1 | .a { 2 | width: 200px; 3 | height: 200px; 4 | background: red; 5 | } -------------------------------------------------------------------------------- /src/static/img/timg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-music/HEAD/src/static/img/timg.jpg -------------------------------------------------------------------------------- /src/containers/Home/style.less: -------------------------------------------------------------------------------- 1 | .home { 2 | color: #d24747; 3 | span { 4 | color: green; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Header/header.less: -------------------------------------------------------------------------------- 1 | .header { 2 | color: #fff; 3 | } 4 | .headerInner { 5 | color: aqua; 6 | } 7 | -------------------------------------------------------------------------------- /src/static/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-music/HEAD/src/static/fonts/icomoon.eot -------------------------------------------------------------------------------- /src/static/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-music/HEAD/src/static/fonts/icomoon.ttf -------------------------------------------------------------------------------- /src/static/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neroneroffy/react-music/HEAD/src/static/fonts/icomoon.woff -------------------------------------------------------------------------------- /src/service/file.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios/index'; 2 | export function getFile() { 3 | return axios.get('/data.json') 4 | } 5 | -------------------------------------------------------------------------------- /src/containers/app.less: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0 4 | } 5 | .app { 6 | & > span { 7 | color: #d24747; 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/containers/Test/store/index.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects' 2 | import { readFile } from './Test.service' 3 | import { READ_FILE } from './Test.action'; 4 | 5 | export default function* testSaga() { 6 | yield [ 7 | takeLatest(READ_FILE, readFile), 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/saga/index.ts: -------------------------------------------------------------------------------- 1 | import { fork } from 'redux-saga/effects' 2 | import testSaga from '../containers/Test/store/index' 3 | import homeSaga from '../containers/Home/store/index' 4 | 5 | export default function* rootSaga() { 6 | yield [ 7 | testSaga(), 8 | homeSaga(), 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/reducer/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import fileContent from '../containers/Test/store/Test.reducer' 3 | import home from '../containers/Home/store/Home.reducer' 4 | 5 | const rootReducer = combineReducers({ 6 | fileContent, 7 | home, 8 | }) 9 | export default rootReducer 10 | -------------------------------------------------------------------------------- /src/containers/Home/store/index.ts: -------------------------------------------------------------------------------- 1 | import { takeLatest } from 'redux-saga/effects' 2 | import { fetchHomeData } from './Home.service' 3 | import { GET_HOME_DATA } from './Home.action'; 4 | 5 | export default function* homeSaga() { 6 | yield [ 7 | takeLatest(GET_HOME_DATA, fetchHomeData), 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /dev-public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Title 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Home from './containers/Home/index' 3 | import Test from './containers/Test/test' 4 | 5 | import { Redirect } from 'react-router-dom' 6 | 7 | export default [ 8 | { 9 | path: '/', 10 | component: Home, 11 | exact: true, 12 | }, 13 | { 14 | path: '/test', 15 | component: Test, 16 | exact: true, 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | .idea 3 | /dev-public 4 | 5 | # dependencies 6 | /node_modules 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | /public 14 | 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | package-lock.json 27 | -------------------------------------------------------------------------------- /src/containers/Test/store/Test.service.ts: -------------------------------------------------------------------------------- 1 | import { select, put, call } from 'redux-saga/effects' 2 | import { getFile } from '../../../service/file' 3 | import { readFileFailure, readFileSuccess, readFileComplete } from './Test.action' 4 | 5 | export function* readFile() { 6 | try { 7 | const file = yield call(getFile) 8 | yield put(readFileSuccess(file.data)) 9 | } catch { 10 | yield put(readFileFailure()) 11 | } finally { 12 | yield put(readFileComplete()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "es2015", { 4 | "loose": true 5 | }], 6 | "stage-1", 7 | "react" 8 | ], 9 | "plugins": [ 10 | "transform-decorators-legacy", 11 | "react-hot-loader/babel", 12 | [ 13 | "transform-runtime", 14 | { 15 | "helpers": false, 16 | "polyfill": false, 17 | "regenerator": true, 18 | "moduleName": "babel-runtime" 19 | } 20 | ], 21 | ["import", { "libraryName": "antd-mobile", "style": true }] 22 | ] 23 | } -------------------------------------------------------------------------------- /src/containers/Home/store/Home.service.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios/index'; 2 | import { select, put, call } from 'redux-saga/effects' 3 | import { getHomeSuccess, getHomeFailure } from './Home.action' 4 | const getHomeData = () => axios.get('https://randomuser.me/api/') 5 | 6 | export function* fetchHomeData() { 7 | try { 8 | const data = yield call(getHomeData) 9 | yield put(getHomeSuccess(data.data)) 10 | } catch { 11 | yield put(getHomeFailure()) 12 | } finally { 13 | // 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/containers/Test/store/Test.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as Actions from './Test.action' 2 | const initialState = { 3 | name: '', 4 | } 5 | export default function fileContent(state = initialState, action: any) { 6 | switch (action.type) { 7 | case Actions.READ_FILE: 8 | return { 9 | ...state, 10 | ...action.payload, 11 | } 12 | case Actions.READ_FILE_SUCCESS: 13 | return { 14 | ...state, 15 | ...action.payload, 16 | } 17 | default: 18 | return state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Link } from 'react-router-dom' 3 | import styles from './header.less' 4 | 5 | interface HeaderProps { 6 | menus: any 7 | } 8 | class Header extends React.Component { 9 | componentDidMount() { 10 | // do 11 | } 12 | 13 | render() { 14 | return (
15 |

16 | 首页 17 | 测sdadas 18 |

19 |
) 20 | } 21 | } 22 | 23 | export default Header 24 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset=utf-8 3 | end_of_line=lf 4 | insert_final_newline=true 5 | indent_style=space 6 | indent_size=4 7 | 8 | [{.babelrc,.stylelintrc,jest.config,.eslintrc,.prettierrc,*.json,*.jsb3,*.jsb2,*.bowerrc}] 9 | indent_style=space 10 | indent_size=2 11 | 12 | [*.js] 13 | indent_style=space 14 | indent_size=2 15 | 16 | [{tsconfig.app.json,tsconfig.e2e.json,tsconfig.json,tsconfig.spec.json}] 17 | indent_style=space 18 | indent_size=2 19 | 20 | [*.less] 21 | indent_style=space 22 | indent_size=2 23 | 24 | [{.analysis_options,*.yml,*.yaml}] 25 | indent_style=space 26 | indent_size=2 27 | 28 | -------------------------------------------------------------------------------- /src/containers/Test/store/Test.action.ts: -------------------------------------------------------------------------------- 1 | export const READ_FILE = 'READ_FILE' 2 | export const READ_FILE_SUCCESS = 'READ_FILE_SUCCESS' 3 | export const READ_FILE_FAIL = 'READ_FILE_FAIL' 4 | export const READ_FILE_COMPLETE = 'READ_FILE_COMPLETE' 5 | 6 | export const readFileSuccess = (data: object) => { 7 | return { 8 | type: READ_FILE_SUCCESS, 9 | payload: data, 10 | } 11 | } 12 | export const readFileFailure = () => { 13 | return { 14 | type: READ_FILE_FAIL, 15 | } 16 | } 17 | export const readFileComplete = () => { 18 | return { 19 | type: READ_FILE_COMPLETE, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-empty": true, 9 | "semicolon": false, 10 | "indent":[true, "spaces", 2], 11 | "linebreak-style":[true,"CR/LF"], 12 | "max-classes-per-file":[true,1], 13 | "max-line-length":[true,100], 14 | "quotemark": [true, "single"], 15 | "ordered-imports": false, 16 | "interface-name": false, 17 | "member-access": false, 18 | "object-literal-sort-keys": false 19 | }, 20 | "rulesDirectory": [] 21 | } -------------------------------------------------------------------------------- /src/containers/Home/store/Home.reducer.ts: -------------------------------------------------------------------------------- 1 | import * as Actions from './Home.action' 2 | const initialState = { 3 | homeData: '', 4 | } 5 | export default function home(state = initialState, action: any) { 6 | switch (action.type) { 7 | case Actions.GET_HOME_DATA_SUCCESS: 8 | return { 9 | ...state, 10 | ...action.payload, 11 | } 12 | case Actions.GET_HOME_DATA_FAILURE: 13 | return { 14 | ...state, 15 | homeData: 'failure', 16 | } 17 | default: 18 | return state 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/containers/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderRoutes } from 'react-router-config' 3 | import Header from '../components/Header/index' 4 | import styles from './app.less' 5 | import Routes from '../routes.tsx' 6 | import routes from '../routes' 7 | import { Button } from 'antd-mobile'; 8 | import '../common/icon-font.css' 9 | 10 | class App extends React.Component { 11 | render() { 12 | return
13 |
14 | 15 | {renderRoutes(routes)} 16 |
17 | } 18 | } 19 | 20 | export default App 21 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux' 2 | import createSagaMiddleware from 'redux-saga' 3 | import rootReducers from './reducer/index' 4 | import rootSaga from './saga/index' 5 | const sagaMiddleware = createSagaMiddleware() 6 | const enhancer = typeof window === 'object' ? window.devToolsExtension() : f=>f 7 | const middlewares = compose( 8 | applyMiddleware(sagaMiddleware), 9 | enhancer 10 | ) 11 | const defaultStore = window.context.state 12 | const clientStore = createStore(rootReducers, defaultStore, middlewares) 13 | sagaMiddleware.run(rootSaga) 14 | export const createClientStore = () => clientStore 15 | // export default store 16 | -------------------------------------------------------------------------------- /src/containers/Home/store/Home.action.ts: -------------------------------------------------------------------------------- 1 | export const GET_HOME_DATA = 'GET_HOME_DATA' 2 | export const GET_HOME_DATA_SUCCESS = 'GET_HOME_DATA_SUCCESS' 3 | export const GET_HOME_DATA_FAILURE = 'GET_HOME_DATA_FAILURE' 4 | export const GET_HOME_DATA_COMPLETE = 'GET_HOME_DATA_COMPLETE' 5 | 6 | export const getHomeSuccess = (data: object) => { 7 | return { 8 | type: GET_HOME_DATA_SUCCESS, 9 | payload: data, 10 | } 11 | } 12 | export const getHomeFailure = () => { 13 | return { 14 | type: GET_HOME_DATA_FAILURE, 15 | } 16 | } 17 | 18 | export const getHomeDataAction = (dispatch: any) => { 19 | console.log('服务端渲染调用') 20 | return { 21 | type: GET_HOME_DATA, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { renderToString } from 'react-dom/server' 3 | import { StaticRouter } from 'react-router-dom' 4 | import { Provider } from 'react-redux' 5 | import App from '../containers/app' 6 | import fs from 'fs' 7 | 8 | const serverRender = (req, store) => { 9 | const initialState = `` 14 | const content = renderToString(( 15 | 16 | 17 | 18 | 19 | 20 | )) 21 | const template = fs.readFileSync(process.cwd() + '/public/static/index.html', 'utf8') 22 | return template.replace('', content).replace('', initialState) 23 | } 24 | 25 | export default serverRender 26 | -------------------------------------------------------------------------------- /src/containers/Test/test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { connect } from 'react-redux' 3 | import { Button } from 'antd-mobile' 4 | import style from './test.less' 5 | 6 | interface TestProps { 7 | readFileAsync: any 8 | } 9 | class Test extends React.Component { 10 | componentDidMount() { 11 | // do 12 | } 13 | file = () => { 14 | this.props.readFileAsync() 15 | } 16 | render() { 17 | return
18 | 19 |
20 |
21 | 22 |
23 |
24 |
25 | } 26 | } 27 | 28 | function mapStateToProps(state: { fileContent: {} }) { 29 | const { fileContent } = state || { fileContent: {} } 30 | return { 31 | fileContent, 32 | } 33 | } 34 | function mapDispatchToProps(dispatch: any) { 35 | return { 36 | readFileAsync: () => dispatch({ 37 | type: 'READ_FILE', 38 | }), 39 | } 40 | } 41 | 42 | export default connect(mapStateToProps, mapDispatchToProps)(Test) 43 | -------------------------------------------------------------------------------- /src/client/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | const root = document.getElementById('root') 4 | import {AppContainer} from 'react-hot-loader' 5 | import { Provider } from 'react-redux' 6 | import { BrowserRouter } from 'react-router-dom' 7 | import { createClientStore } from '../store'; 8 | import App from '../containers/app' 9 | 10 | if (module.hot) { 11 | module.hot.accept('../containers/app.tsx', () => { 12 | const NextApp = require('../containers/app.tsx').default 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | , root) 21 | }) 22 | } 23 | 24 | ReactDOM.render( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | , root) 33 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { createStore, applyMiddleware, compose } from 'redux' 3 | import { matchRoutes } from "react-router-config" 4 | import createSagaMiddleware from 'redux-saga' 5 | import rootReducers from '../reducer/index' 6 | import { fetchHomeData } from '../containers/Home/store/Home.service' 7 | import routes from '../routes' 8 | import serverRender from './render' 9 | 10 | // 服务端渲染的store 11 | const sagaMiddleware = createSagaMiddleware() 12 | const middlewares = compose( 13 | applyMiddleware(sagaMiddleware) 14 | ) 15 | const store = createStore(rootReducers, middlewares); 16 | 17 | const app = express() 18 | app.use(express.static('public')) 19 | app.get("*", (req, res) => { 20 | const matchedRoutes = matchRoutes(routes, req.path) 21 | // @Todo 会自动请求robots.txt文件,导致matchedRoutes 为undefined 暂时判断处理,日后补充 22 | if (matchedRoutes[0]) { 23 | sagaMiddleware.run(fetchHomeData).done.then(() => { 24 | res.send(serverRender(req, store)) 25 | }) 26 | 27 | } 28 | }) 29 | 30 | app.listen(3000, () => { 31 | console.log('server listening on port 3000') 32 | }) 33 | -------------------------------------------------------------------------------- /src/common/icon-font.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'icomoon'; 3 | src: url('../static/fonts/icomoon.eot?om4pos'); 4 | src: url('../static/fonts/icomoon.eot?om4pos#iefix') format('embedded-opentype'), 5 | url('../static/fonts/icomoon.ttf?om4pos') format('truetype'), 6 | url('../static/fonts/icomoon.woff?om4pos') format('woff'), 7 | url('../static/fonts/icomoon.svg?om4pos#icomoon') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="icon-"], [class*=" icon-"] { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'icomoon' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .icon-home2:before { 28 | content: "\e901"; 29 | } 30 | .icon-office:before { 31 | content: "\e903"; 32 | } 33 | .icon-newspaper:before { 34 | content: "\e904"; 35 | } 36 | -------------------------------------------------------------------------------- /src/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Home from './containers/Home/index' 3 | import Test from './containers/Test/test' 4 | import App from './containers/app' 5 | import { Route, Redirect } from 'react-router-dom' 6 | 7 | interface RoutesProps { 8 | routes: any 9 | } 10 | class Routes extends React.Component { 11 | state = { 12 | routesList: [ 13 | { 14 | path: '/', 15 | component: () => , 16 | name: 'App', 17 | }, 18 | { 19 | path: '/home', 20 | component: Home, 21 | name: 'Home', 22 | }, 23 | { 24 | path: '/test', 25 | component: Test, 26 | name: 'Test', 27 | }, 28 | ], 29 | } 30 | render() { 31 | return
32 | { 33 | this.state.routesList.map((v: any) => { 34 | return 35 | } 36 | ) 37 | } 38 |
39 | } 40 | } 41 | 42 | export default Routes 43 | -------------------------------------------------------------------------------- /src/containers/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Button } from 'antd-mobile' 3 | import { connect } from 'react-redux' 4 | import { GET_HOME_DATA, getHomeDataAction } from './store/Home.action' 5 | import pic from '../../static/img/timg.jpg' 6 | import styles from './style.less' 7 | interface HomeProps { 8 | getHomeDataAction: any, 9 | } 10 | class Home extends React.Component { 11 | componentDidMount() { 12 | // const { getHomeDataAction } = this.props 13 | // getHomeDataAction() 14 | // do 15 | } 16 | 17 | render() { 18 | const { results } = this.props.home 19 | return
20 | 21 |
22 | { 23 | results && results.map(v => 24 | { v.name.title + v.name.first + v.name.last } 25 | ) 26 | } 27 |
28 | 29 |
30 | } 31 | } 32 | 33 | const mapStateToProps = (state: object) => { 34 | return { 35 | home: state.home, 36 | } 37 | } 38 | 39 | export default connect(mapStateToProps, { 40 | getHomeDataAction, 41 | })(Home) 42 | -------------------------------------------------------------------------------- /src/static/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /configs/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const tsImportPluginFactory = require('ts-import-plugin') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const theme = require('../package.json').theme 6 | 7 | const config = { 8 | resolve: { 9 | extensions: [ '.ts', '.tsx', '.js' ] 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | loader: 'babel-loader', 16 | exclude: [ 17 | path.join(__dirname, '../node_modules'), 18 | ], 19 | 20 | }, 21 | { 22 | test: /\.(tsx|ts)$/, 23 | use: [ 24 | { loader: 'babel-loader' }, 25 | { 26 | loader: 'ts-loader', 27 | options: { 28 | transpileOnly: true, 29 | getCustomTransformers: () => ({ 30 | before: [ tsImportPluginFactory({ 31 | libraryDirectory: 'es', 32 | libraryName: 'antd-mobile', 33 | style: true, 34 | }) ] 35 | }), 36 | compilerOptions: { 37 | module: 'es2015' 38 | } 39 | }, 40 | } 41 | ], 42 | exclude: [ path.join(__dirname, '../node_modules') ] 43 | }, 44 | { 45 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 46 | loader: require.resolve('url-loader'), 47 | options: { 48 | limit: 8000, 49 | name: 'static/img/[name].[hash:8].[ext]', 50 | }, 51 | }, 52 | { 53 | test: /\.(woff2?|eot|ttf|otf|svg)(\?.*)?$/, 54 | loader: 'url-loader', 55 | options: { 56 | limit: 10000, 57 | name: 'static/fonts/[name].[hash:8].[ext]' 58 | } 59 | } 60 | ] 61 | }, 62 | 63 | } 64 | 65 | module.exports = config 66 | -------------------------------------------------------------------------------- /configs/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const merge = require('webpack-merge') 3 | const baseConfig = require('./webpack.config') 4 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 5 | const theme = require('../package.json').theme 6 | const serverConfig = { 7 | entry: path.join(__dirname, '../src/server/index.js'), 8 | target: 'node', 9 | mode: 'production', 10 | output: { 11 | path: path.join(__dirname, '../build'), 12 | filename: 'server.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.less$/, 18 | include: [ path.join(__dirname, '../node_modules/antd-mobile') ], 19 | use: [ 20 | { loader: 'isomorphic-style-loader' }, 21 | { loader: 'css-loader' }, 22 | { 23 | loader: 'less-loader', 24 | options: { 25 | modifyVars: theme 26 | } 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.less$/, 32 | exclude: [ path.join(__dirname, '../node_modules/antd-mobile') ], 33 | use: [ 34 | { loader: 'isomorphic-style-loader' }, 35 | { 36 | loader: 'css-loader', 37 | options: { 38 | importLoaders: 1, 39 | modules: true, 40 | localIdentName: '[name]_[local]_[hash:base64:5]' 41 | } 42 | }, 43 | { 44 | loader: 'less-loader', 45 | options: { 46 | modifyVars: theme 47 | } 48 | } 49 | ] 50 | }, 51 | { 52 | test: /\.css$/, 53 | use: [ 54 | 'isomorphic-style-loader', 55 | 'css-loader', 56 | ], 57 | } 58 | ] 59 | }, 60 | plugins: [ 61 | new CleanWebpackPlugin([path.join(__dirname, '../build')]), 62 | ] 63 | } 64 | module.exports = merge(serverConfig, baseConfig) 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "A react music webapp", 5 | "main": "index.js", 6 | "scripts": { 7 | "dist": "cross-env NODE_ENV=production webpack --config configs/webpack.config.js --mode production", 8 | "build": "npm run dist", 9 | "dev-temp": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.js --mode development", 10 | "dev": "npm-run-all --parallel dev:**", 11 | "start:server": "nodemon --watch build --exec node \"./build/server.js\"", 12 | "build:server": "webpack --config configs/webpack.config.server.js", 13 | "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config configs/webpack.config.client.js --watch", 14 | "build:client": "cross-env NODE_ENV=production webpack --config configs/webpack.config.client.js", 15 | "test": "echo \"Error: no test specified\" && exit 1", 16 | "tslint": "tslint --project . src/**/*.ts src/**/*.tsx" 17 | }, 18 | "author": "", 19 | "license": "ISC", 20 | "devDependencies": { 21 | "@types/react": "^16.4.6", 22 | "@types/react-dom": "^16.0.6", 23 | "@types/react-redux": "^6.0.4", 24 | "@types/react-router": "^4.0.28", 25 | "autoprefixer": "^8.6.5", 26 | "babel-core": "^6.26.3", 27 | "babel-loader": "^7.1.4", 28 | "babel-plugin-import": "^1.8.0", 29 | "babel-plugin-syntax-dynamic-import": "^6.18.0", 30 | "babel-plugin-transform-decorators-legacy": "^1.3.5", 31 | "babel-plugin-transform-runtime": "^6.23.0", 32 | "babel-preset-es2015": "^6.24.1", 33 | "babel-preset-es2015-loose": "^8.0.0", 34 | "babel-preset-react": "^6.24.1", 35 | "babel-preset-stage-1": "^6.24.1", 36 | "clean-webpack-plugin": "^0.1.19", 37 | "cross-env": "^5.2.0", 38 | "css-loader": "^1.0.0", 39 | "file-loader": "^2.0.0", 40 | "html-webpack-plugin": "^3.2.0", 41 | "isomorphic-style-loader": "^4.0.0", 42 | "less": "^3.5.3", 43 | "less-loader": "^4.1.0", 44 | "mini-css-extract-plugin": "^0.4.4", 45 | "nodemon": "^1.18.4", 46 | "postcss-flexbugs-fixes": "^3.3.1", 47 | "postcss-loader": "^2.1.6", 48 | "react-hot-loader": "^4.3.3", 49 | "react-loadable": "^5.5.0", 50 | "rimraf": "^2.6.2", 51 | "style-loader": "^0.21.0", 52 | "ts-import-plugin": "^1.5.4", 53 | "ts-loader": "^4.4.2", 54 | "tslint": "^5.10.0", 55 | "typescript": "^2.9.2", 56 | "url-loader": "^1.1.2", 57 | "webpack": "^4.12.1", 58 | "webpack-cli": "^3.0.8", 59 | "webpack-dev-server": "^3.1.4", 60 | "webpack-hot-middleware": "^2.24.3", 61 | "webpack-merge": "^4.1.4" 62 | }, 63 | "dependencies": { 64 | "@types/react-router-config": "^1.0.9", 65 | "@types/react-router-dom": "^4.3.1", 66 | "antd-mobile": "^2.2.1", 67 | "axios": "^0.18.0", 68 | "express": "^4.16.4", 69 | "react": "^16.4.1", 70 | "react-dom": "^16.4.1", 71 | "react-redux": "^5.0.7", 72 | "react-router": "^4.3.1", 73 | "react-router-config": "^4.4.0-beta.1", 74 | "react-router-dom": "^4.3.1", 75 | "redux": "^4.0.0", 76 | "redux-saga": "^0.16.0" 77 | }, 78 | "theme": { 79 | "brand-primary": "#f68b0e", 80 | "color-text-base": "#fff", 81 | "brand-primary-tap": "#f7a03a" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /configs/webpack.config.bak.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const tsImportPluginFactory = require('ts-import-plugin') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | const theme = require('../package.json').theme 6 | const isDev = process.env.NODE_ENV === 'development' 7 | const config = { 8 | entry: path.join(__dirname, '../src/client/index.tsx'), 9 | output: { 10 | path: path.join(__dirname, '../build'), 11 | filename: '[hash:8]-[name].js', 12 | }, 13 | resolve: { 14 | extensions: [ '.ts', '.tsx', '.js' ] 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | loader: 'babel-loader', 21 | exclude: [ 22 | path.join(__dirname, '../node_modules'), 23 | ], 24 | 25 | }, 26 | { 27 | test: /\.(tsx|ts)$/, 28 | use: [ 29 | { loader: 'babel-loader' }, 30 | { 31 | loader: 'ts-loader', 32 | options: { 33 | transpileOnly: true, 34 | getCustomTransformers: () => ({ 35 | before: [ tsImportPluginFactory({ 36 | libraryDirectory: 'es', 37 | libraryName: 'antd-mobile', 38 | style: true, 39 | }) ] 40 | }), 41 | compilerOptions: { 42 | module: 'es2015' 43 | } 44 | }, 45 | } 46 | ], 47 | exclude: [ path.join(__dirname, '../node_modules') ] 48 | }, 49 | { 50 | test: /\.less$/, 51 | include: [ path.join(__dirname, '../node_modules/antd-mobile') ], 52 | use: [ 53 | { loader: 'style-loader' }, 54 | { loader: 'css-loader' }, 55 | { 56 | loader: 'less-loader', 57 | options: { 58 | modifyVars: theme 59 | } 60 | } 61 | ] 62 | }, 63 | { 64 | test: /\.less$/, 65 | exclude: [ path.join(__dirname, '../node_modules/antd-mobile') ], 66 | use: [ 67 | { loader: 'style-loader' }, 68 | { loader: 'css-loader?modules&localIdentName=[name]-[hash:base64:5]' }, 69 | { 70 | loader: 'less-loader', 71 | options: { 72 | modifyVars: theme 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | test: /\.css$/, 79 | use: [ 80 | 'style-loader', 81 | 'css-loader', 82 | ], 83 | }, 84 | 85 | ] 86 | }, 87 | plugins: [ 88 | new HtmlWebpackPlugin({ 89 | title: 'music', 90 | filename: path.join(__dirname, '../build/index.html'), 91 | template: path.join(__dirname, '../public/index.html'), 92 | inject: true, 93 | }), 94 | ] 95 | } 96 | if (isDev) { 97 | config.devServer = { 98 | host: '0.0.0.0', 99 | port: '8080', 100 | contentBase: path.join(__dirname, '../public'), 101 | overlay: { 102 | error: true, 103 | }, 104 | historyApiFallback: { 105 | index: path.join(__dirname, '../public/index.html'), 106 | }, 107 | hot: true, 108 | } 109 | config.entry = { 110 | app :[ 111 | "react-hot-loader/patch", 112 | path.join(__dirname, '../src/client/index.tsx'), 113 | ] 114 | } 115 | config.plugins.push( 116 | new webpack.HotModuleReplacementPlugin() 117 | ) 118 | } 119 | module.exports = config -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | // "outDir": "./", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | 23 | /* Strict Type-Checking Options */ 24 | "strict": true, /* Enable all strict type-checking options. */ 25 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 26 | // "strictNullChecks": true, /* Enable strict null checks. */ 27 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | 32 | /* Additional Checks */ 33 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 34 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 35 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 36 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 37 | 38 | /* Module Resolution Options */ 39 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 40 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 41 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 42 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 43 | // "typeRoots": [], /* List of folders to include type definitions from. */ 44 | // "types": [], /* Type declaration files to be included in compilation. */ 45 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 46 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 47 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 48 | 49 | /* Source Map Options */ 50 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 51 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 52 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 53 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 54 | 55 | /* Experimental Options */ 56 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 57 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 58 | } 59 | } -------------------------------------------------------------------------------- /configs/webpack.config.client.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const merge = require('webpack-merge') 4 | const baseConfig = require('./webpack.config') 5 | const theme = require('../package.json').theme 6 | const HtmlWebpackPlugin = require('html-webpack-plugin') 7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 8 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 9 | const isDev = process.env.NODE_ENV === 'development' 10 | const isProd = process.env.NODE_ENV === 'production' 11 | const clientConfig = { 12 | entry: path.join(__dirname, '../src/client/index.tsx'), 13 | output: { 14 | path: path.join(__dirname, '../public'), 15 | filename: 'static/js/[name].client.js', 16 | chunkFilename: "static/js/[name].js", 17 | }, 18 | resolve: { 19 | extensions: [ '.ts', '.tsx', '.js', '.json' ] 20 | }, 21 | module: { 22 | rules: [ 23 | ] 24 | }, 25 | devtool: "#eval-source-map", 26 | plugins: [] 27 | } 28 | if (isDev) { 29 | const devRules = [ 30 | { 31 | test: /\.less$/, 32 | include: [ path.join(__dirname, '../node_modules/antd-mobile') ], 33 | use: [ 34 | { loader: 'style-loader' }, 35 | { loader: 'css-loader' }, 36 | { 37 | loader: 'less-loader', 38 | options: { 39 | modifyVars: theme 40 | } 41 | } 42 | ] 43 | }, 44 | { 45 | test: /\.less$/, 46 | exclude: [ path.join(__dirname, '../node_modules/antd-mobile') ], 47 | use: [ 48 | { loader: 'style-loader' }, 49 | { 50 | loader: 'css-loader', 51 | options: { 52 | importLoaders: 1, 53 | modules: true, 54 | localIdentName: '[name]_[local]_[hash:base64:5]' 55 | } 56 | }, 57 | { 58 | loader: 'less-loader', 59 | options: { 60 | modifyVars: theme 61 | } 62 | } 63 | ] 64 | }, 65 | { 66 | test: /\.css$/, 67 | use: [ 68 | {loader: 'style-loader'}, 69 | 'css-loader', 70 | ], 71 | }, 72 | ] 73 | clientConfig.entry = [ 74 | "react-hot-loader/patch", 75 | path.join(__dirname, '../src/client/index.tsx'), 76 | ] 77 | clientConfig.output = { 78 | path: path.join(__dirname, '../dev-public'), 79 | filename: '[name].client.js', 80 | chunkFilename: "[name].js", 81 | }, 82 | 83 | clientConfig.mode = 'development' 84 | clientConfig.module.rules = clientConfig.module.rules.concat(devRules) 85 | clientConfig.devServer = { 86 | host: '0.0.0.0', 87 | port: '8080', 88 | contentBase: path.join(__dirname, '../dev-public'), 89 | inline: true, 90 | overlay: { 91 | error: true, 92 | }, 93 | historyApiFallback: true, 94 | hot: true, 95 | } 96 | clientConfig.plugins = clientConfig.plugins.concat( 97 | new webpack.HotModuleReplacementPlugin(), 98 | new HtmlWebpackPlugin({ 99 | title: 'music', 100 | filename: path.join(__dirname, '../dev-public/index.html'), 101 | template: path.join(__dirname, '../dev-public/index.html'), 102 | inject: true, 103 | }), 104 | ) 105 | } 106 | if (isProd) { 107 | const prodRules = [ 108 | { 109 | test: /\.less$/, 110 | include: /node_modules/, 111 | use: [ 112 | {loader: MiniCssExtractPlugin.loader}, 113 | // { loader: 'style-loader' }, 114 | { loader: 'css-loader' }, 115 | { 116 | loader: 'less-loader', 117 | options: { 118 | modifyVars: theme 119 | } 120 | } 121 | ] 122 | }, 123 | { 124 | test: /\.less$/, 125 | exclude: /node_modules/, 126 | use: [ 127 | {loader: MiniCssExtractPlugin.loader}, 128 | // { loader: 'style-loader' }, 129 | { 130 | loader: 'css-loader', 131 | options: { 132 | importLoaders: 1, 133 | modules: true, 134 | localIdentName: '[name]_[local]_[hash:base64:5]' 135 | } 136 | }, 137 | { 138 | loader: 'less-loader', 139 | options: { 140 | modifyVars: theme 141 | } 142 | } 143 | ] 144 | }, 145 | { 146 | test: /\.css$/, 147 | use: [ 148 | {loader: MiniCssExtractPlugin.loader}, 149 | 'css-loader', 150 | ], 151 | }, 152 | ] 153 | clientConfig.mode = 'production' 154 | clientConfig.module.rules = clientConfig.module.rules.concat(prodRules) 155 | clientConfig.optimization = { 156 | runtimeChunk: { 157 | name: "manifest" 158 | }, 159 | splitChunks: { 160 | cacheGroups: { 161 | vendor: { 162 | test: /[\\/]node_modules[\\/]/, 163 | name: "vendors", 164 | priority: -20, 165 | chunks: "all" 166 | } 167 | } 168 | } 169 | } 170 | clientConfig.plugins.push( 171 | new CleanWebpackPlugin(['../public']), 172 | new MiniCssExtractPlugin({ 173 | filename: "static/css/[name].css", 174 | // chunkFilename: "static/css/style.[id].css", 175 | // allChunks: true 176 | }), 177 | new HtmlWebpackPlugin({ 178 | title: 'music', 179 | filename: path.join(__dirname, '../public/static/index.html'), 180 | template: path.join(__dirname, '../dev-public/index.html'), 181 | inject: true, 182 | }), 183 | 184 | ) 185 | } 186 | module.exports = merge(clientConfig, baseConfig) 187 | --------------------------------------------------------------------------------