├── .gitignore ├── .lintstagedrc ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .steamer ├── .mobxTemplate │ ├── app.tsx │ ├── containers │ │ ├── shared │ │ │ └── App │ │ │ │ ├── index.tsx │ │ │ │ └── style.scss │ │ └── views │ │ │ ├── Home │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ │ └── NotFound │ │ │ ├── index.tsx │ │ │ └── style.scss │ ├── store │ │ ├── globalStore │ │ │ ├── index.ts │ │ │ └── type.d.ts │ │ └── index.ts │ ├── typings │ │ └── router.d.ts │ └── util │ │ └── constants.ts ├── .svgModified │ ├── src │ │ └── app.scss │ ├── tools │ │ └── rules │ │ │ └── fileRules.js │ └── typings │ │ └── svg.d.ts ├── replace-redux-files-to-mobx.js ├── steamer-plugin-kit.js ├── steamer-react-ts.js └── support-svg-component.js ├── .stylelintignore ├── .stylelintrc.js ├── README.md ├── config ├── project.js └── steamer.config.js ├── docs ├── css.md ├── index.md ├── js.md ├── store.md ├── svg.md ├── test.md └── tree.md ├── jest.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── qshell.prod.json ├── qshell.qa.json ├── src ├── app.scss ├── app.tsx ├── assets │ └── images │ │ └── logo404.png ├── components │ ├── ReducerInjector │ │ └── index.tsx │ └── SagaInjector │ │ └── index.tsx ├── configureStore.ts ├── containers │ ├── shared │ │ └── App │ │ │ ├── actions.ts │ │ │ ├── constants.ts │ │ │ ├── index.tsx │ │ │ ├── reducer.ts │ │ │ ├── selectors.ts │ │ │ └── style.scss │ └── views │ │ ├── Home │ │ ├── actions.ts │ │ ├── constants.ts │ │ ├── index.tsx │ │ ├── reducer.ts │ │ ├── saga.ts │ │ ├── selectors.ts │ │ └── style.scss │ │ └── NotFound │ │ ├── index.tsx │ │ └── style.scss ├── env │ ├── dev.ts │ ├── prod.ts │ └── qa.ts ├── libs │ ├── preact.js │ ├── react-dom.js │ └── react.js ├── main.html ├── reducers.ts ├── services │ ├── api.ts │ └── contants.ts ├── styles │ ├── _base.scss │ ├── _functions.scss │ ├── _mixins.scss │ └── _var.scss └── util │ ├── constants.ts │ ├── http.ts │ ├── reducerInjectors.ts │ └── sagaInjectors.ts ├── test ├── mocks │ └── .gitkeep └── spec │ └── simple.spec.tsx ├── tools ├── feature │ └── feature.js ├── optimization │ └── index.js ├── plugins │ ├── basePlugins.js │ ├── cachePlugins.js │ ├── resourcePlugins.js │ └── spritePlugins.js ├── rules │ ├── fileRules.js │ ├── htmlRules.js │ ├── jsRules.js │ └── styleRules.js ├── script.js ├── server.js ├── template │ ├── dependency.js │ ├── mobxViewTemplate │ │ ├── index.tsx │ │ └── style.scss │ └── viewTemplate │ │ ├── actions.ts │ │ ├── constants.ts │ │ ├── index.tsx │ │ ├── reducer.ts │ │ ├── saga.ts │ │ ├── selectors.ts │ │ └── style.scss └── webpack.base.js ├── tsconfig.json ├── tsconfig.webpack.json ├── tslint.json └── typings ├── antd.shim.d.ts ├── echarts.d.ts ├── http.d.ts ├── index.d.ts ├── redux-store.shim.d.ts ├── redux.shim.d.ts └── typed-css-modules.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | /dev 4 | dist/* 5 | src/**/*.scss.d.ts -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{ts,tsx}": ["tslint -c tslint.json", "prettier --write", "git add"], 3 | "src/**/*.scss": ["stylelint --fix", "prettier --write", "git add"] 4 | } 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npm.taobao.org 2 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /**/libs/** -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "eslintIntegration": true, 3 | "stylelintIntegration": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "semi": false 7 | } 8 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/app.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * 程序的入口 5 | */ 6 | 7 | import * as React from 'react' 8 | import * as ReactDOM from 'react-dom' 9 | import { Provider } from 'mobx-react' 10 | import { configure } from 'mobx' 11 | import createHashHistory from 'history/createHashHistory' 12 | import { syncHistoryWithStore } from 'mobx-react-router' 13 | import { Router } from 'react-router-dom' 14 | 15 | // 如果依赖第三方UI库的样式,建议去掉sanitize.css的依赖 16 | import 'sanitize.css/sanitize.css' 17 | 18 | /** 19 | * 这里开始你的第三方包依赖,包括css 20 | * Example 21 | * 22 | * import "antd/dist/antd.css" 23 | */ 24 | 25 | // app global style 26 | import './app.scss' 27 | 28 | // Import root app 29 | import App from 'containers/shared/App' 30 | 31 | import * as store from './store' 32 | 33 | // 开启mobx严格模式 34 | configure({ enforceActions: true }) 35 | 36 | const hashHistory = createHashHistory() 37 | const history = syncHistoryWithStore(hashHistory, store.routerStore) 38 | 39 | const MOUNT_NODE = document.getElementById('root') 40 | 41 | const render = Component => { 42 | ReactDOM.render( 43 | 44 | 45 | 46 | 47 | , 48 | MOUNT_NODE 49 | ) 50 | } 51 | 52 | render(App) 53 | 54 | if (module.hot) { 55 | // 热更新React Components 56 | // module.hot.accept不支持动态的依赖 57 | // 必须是编译时定义的常量 58 | module.hot.accept(['containers/shared/App'], () => { 59 | ReactDOM.unmountComponentAtNode(MOUNT_NODE) 60 | render(require('containers/shared/App').default) 61 | }) 62 | } 63 | 64 | // TODO: 待验证 65 | // Install ServiceWorker and AppCache in the end since 66 | // it's not most important operation and if main code fails, 67 | // we do not want it installed 68 | if (process.env.NODE_ENV === 'production') { 69 | // tslint:disable-next-line:no-var-requires 70 | require('offline-plugin/runtime').install() // eslint-disable-line global-require 71 | } 72 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/shared/App/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App 4 | * 5 | * 这个组件是每个页面的最外层骨架,所以这里的代码只能存放所有页面公有的, 比如(导航)或者路由 6 | * 切勿在其他组件引用 7 | */ 8 | 9 | import * as React from "react"; 10 | import { HashRouter as Router, Switch, Route } from "react-router-dom"; 11 | 12 | import HomePage from "containers/views/Home"; 13 | import NotFoundPage from "containers/views/NotFound"; 14 | 15 | 16 | import * as styles from "./style.scss"; 17 | 18 | const AppWrapper = props => ( 19 |
{props.children}
20 | ); 21 | 22 | export default function App(props) { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | {/* 30 | */} 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/shared/App/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .appWrapper { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | overflow: auto; 8 | padding: 16px; 9 | margin: 0 auto; 10 | min-height: 100%; 11 | flex-direction: column; 12 | } -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { observer, inject } from 'mobx-react' 3 | import { computed } from 'mobx' 4 | 5 | import * as styles from './style.scss' 6 | 7 | interface IP { 8 | globalStore: IGlobalStore.GlobalStore 9 | routerStore: RouterStore 10 | } 11 | 12 | @inject('globalStore', 'routerStore') 13 | @observer 14 | class Home extends React.Component { 15 | @computed 16 | get hello() { 17 | const { test } = this.props.globalStore 18 | return test ? test.hello : '' 19 | } 20 | 21 | routerTest = () => { 22 | this.props.routerStore.push('/error') 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 |

29 | Hello World! 30 |

31 | {this.hello} 32 |
33 | ) 34 | } 35 | } 36 | 37 | export default Home 38 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/views/Home/style.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_base'; 2 | 3 | .routerTest { 4 | cursor: pointer; 5 | color: purple; 6 | } 7 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/views/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import * as styles from './style.scss' 4 | 5 | import * as Logo from 'assets/images/logo404.png' 6 | 7 | class NotFound extends React.Component { 8 | public render() { 9 | return ( 10 |
11 | 12 |
13 |

404

14 |

抱歉,你访问的页面不存在

15 |
16 |
17 | ) 18 | } 19 | } 20 | 21 | export default NotFound -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/containers/views/NotFound/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .pageContainer { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | align-items: center; 8 | justify-content: center; 9 | background: #f0f2f5; 10 | } 11 | 12 | .leftWrapper { 13 | margin-right: 10%; 14 | } 15 | 16 | .pageTitle { 17 | color: #434e59; 18 | font-size: 70px; 19 | } 20 | 21 | .pageDescription { 22 | color: rgba(#000, 0.45); 23 | font-size: 20px; 24 | } 25 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/store/globalStore/index.ts: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx' 2 | 3 | export class GlobalStore { 4 | @observable test: IGlobalStore.Test = { 5 | hello: 'hello mobx!' 6 | } 7 | 8 | @action 9 | changeTest = (test: IGlobalStore.Test) => { 10 | this.test = test 11 | } 12 | } 13 | 14 | export default new GlobalStore() 15 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/store/globalStore/type.d.ts: -------------------------------------------------------------------------------- 1 | import { GlobalStore as GlobalStoreModel } from './index' 2 | 3 | export as namespace IGlobalStore 4 | 5 | export interface GlobalStore extends GlobalStoreModel {} 6 | 7 | export interface Test { 8 | hello: string 9 | } 10 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/store/index.ts: -------------------------------------------------------------------------------- 1 | import { RouterStore } from 'mobx-react-router' 2 | 3 | export const routerStore = new RouterStore() 4 | 5 | export { default as globalStore } from './globalStore' 6 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/typings/router.d.ts: -------------------------------------------------------------------------------- 1 | import { RouterStore as _RouterStore } from 'mobx-react-router' 2 | 3 | declare global { 4 | interface RouterStore extends _RouterStore {} 5 | } 6 | -------------------------------------------------------------------------------- /.steamer/.mobxTemplate/util/constants.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/steamer-react-ts/cb18e32841f2000e218a74476e29c3faebff1306/.steamer/.mobxTemplate/util/constants.ts -------------------------------------------------------------------------------- /.steamer/.svgModified/src/app.scss: -------------------------------------------------------------------------------- 1 | @import 'styles/_base.scss'; 2 | 3 | :global { 4 | html, 5 | body { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #root { 11 | width: 100%; 12 | height: 100%; 13 | background: $body-background; 14 | } 15 | 16 | svg, 17 | svg path { 18 | fill: currentcolor; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.steamer/.svgModified/tools/rules/fileRules.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | let configWebpack = config.webpack 3 | let isProduction = config.env === 'production' 4 | 5 | let rules = [ 6 | { 7 | test: /\.ico$/, 8 | loader: 'url-loader', 9 | options: { 10 | name: '[name].[ext]' 11 | } 12 | }, 13 | { 14 | test: /\.(jpe?g|png|gif)$/i, 15 | use: [ 16 | { 17 | loader: 'url-loader', 18 | options: { 19 | publicPath: isProduction 20 | ? configWebpack.imgCdn 21 | : configWebpack.webserver, 22 | limit: 10, 23 | name: `img/[path]${configWebpack.hashName}.[ext]` 24 | } 25 | } 26 | ] 27 | }, 28 | { 29 | test: /\.(woff|woff2|eot|ttf)\??.*$/, 30 | use: [ 31 | { 32 | loader: 'url-loader', 33 | options: { 34 | limit: 10, 35 | name: `font/[path]${configWebpack.hashName}.[ext]` 36 | } 37 | } 38 | ] 39 | }, 40 | { 41 | test: /\.svg$/, 42 | loader: '@svgr/webpack' 43 | } 44 | ] 45 | 46 | return rules 47 | } 48 | -------------------------------------------------------------------------------- /.steamer/.svgModified/typings/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare interface SvgrComponent extends React.StatelessComponent> {} 2 | 3 | declare module '*.svg' { 4 | const content: SvgrComponent 5 | export default content 6 | } 7 | -------------------------------------------------------------------------------- /.steamer/replace-redux-files-to-mobx.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const mobxTemplatePath = path.join(__dirname, '.mobxTemplate') 4 | 5 | module.exports = function(folderPath) { 6 | // 删除/修改入口文件中redux相关的模块 7 | this.fs.removeSync(path.join(folderPath, 'src/configureStore.ts')) 8 | this.fs.removeSync(path.join(folderPath, 'src/reducers.ts')) 9 | this.fs.copySync( 10 | path.join(mobxTemplatePath, 'app.tsx'), 11 | path.join(folderPath, 'src/app.tsx') 12 | ) 13 | 14 | // 添加store 15 | this.fs.copySync( 16 | path.join(mobxTemplatePath, 'store'), 17 | path.join(folderPath, 'src/store') 18 | ) 19 | 20 | // 删除/修改util中redux相关的模块 21 | const utilPath = path.join(folderPath, 'src/util') 22 | this.fs.removeSync(path.join(utilPath, 'reducerInjectors.ts')) 23 | this.fs.removeSync(path.join(utilPath, 'sagaInjectors.ts')) 24 | this.fs.removeSync(path.join(utilPath, 'constants.ts')) 25 | 26 | // 删除typings中redux相关的模块 27 | const typingsPath = path.join(folderPath, 'typings') 28 | this.fs.removeSync(path.join(typingsPath, 'redux-store.shim.d.ts')) 29 | this.fs.removeSync(path.join(typingsPath, 'redux.shim.d.ts')) 30 | // 添加router.d.ts到typings 31 | this.fs.copySync( 32 | path.join(mobxTemplatePath, 'typings/router.d.ts'), 33 | path.join(typingsPath, 'router.d.ts') 34 | ) 35 | 36 | // 删除/修改components中redux相关的组件 37 | const componentsPath = path.join(folderPath, 'src/components') 38 | this.fs.removeSync(path.join(componentsPath, 'ReducerInjector/')) 39 | this.fs.removeSync(path.join(componentsPath, 'SagaInjector')) 40 | 41 | // 删除/修改containers中redux相关的组件 42 | const containersPath = path.join(folderPath, 'src/containers') 43 | this.fs.emptyDirSync(containersPath) 44 | this.fs.copySync(path.join(mobxTemplatePath, 'containers'), containersPath) 45 | } 46 | -------------------------------------------------------------------------------- /.steamer/steamer-plugin-kit.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugin": "steamer-plugin-kit", 3 | "config": { 4 | "kit": "steamer-react-redux-ts", 5 | "version": "1.0.0", 6 | "template": { 7 | "src": "./tools/template", 8 | "dist": "./src/containers/views", 9 | "npm": "yarn" 10 | } 11 | } 12 | }; -------------------------------------------------------------------------------- /.steamer/steamer-react-ts.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const replaceReduxFilesToMobx = require('./replace-redux-files-to-mobx') 4 | const supportSvgComponent = require('./support-svg-component') 5 | 6 | const onInstall = function(folderPath, config) { 7 | const pkg = this.getPkgJson(folderPath) 8 | let pkgHasChanged = false 9 | if (!!config.mobx) { 10 | pkgHasChanged = true 11 | pkg.dependencies = Object.assign({}, pkg.dependencies, { 12 | mobx: '^5.0.0', 13 | 'mobx-react': '^5.2.2', 14 | 'mobx-react-router': '^4.0.4' 15 | }) 16 | const rmMods = { 17 | dependencies: [ 18 | 'react-redux', 19 | 'react-router-redux', 20 | 'redux', 21 | 'redux-immutable', 22 | 'redux-saga', 23 | 'reselect' 24 | ], 25 | devDependencies: ['@types/react-redux', '@types/react-router-redux'] 26 | } 27 | for (const dep of Object.keys(rmMods)) { 28 | for (const mod of rmMods[dep]) { 29 | delete pkg[dep][mod] 30 | } 31 | } 32 | replaceReduxFilesToMobx.bind(this)(folderPath) 33 | } 34 | if (!!config.svg) { 35 | pkgHasChanged = true 36 | pkg.devDependencies = Object.assign({}, pkg.devDependencies, { 37 | '@svgr/webpack': '^2.1.1' 38 | }) 39 | supportSvgComponent.bind(this)(folderPath) 40 | } 41 | if (!!config.jest) { 42 | pkgHasChanged = true 43 | pkg.dependencies = Object.assign({}, pkg.dependencies, { 44 | enzyme: '^3.3.0', 45 | jest: '^22.4.4' 46 | }) 47 | pkg.devDependencies = Object.assign({}, pkg.devDependencies, { 48 | '@types/enzyme': '^3.1.10', 49 | '@types/jest': '^22.2.3', 50 | 'enzyme-adapter-react-16': '^1.1.1' 51 | }) 52 | pkg.scripts = Object.assign({}, pkg.scripts, { 53 | test: 'jest', 54 | coverage: 'jest --coverage' 55 | }) 56 | } 57 | if (pkgHasChanged) { 58 | fs.writeFileSync( 59 | path.join(folderPath, 'package.json'), 60 | JSON.stringify(pkg, null, 4), 61 | 'utf-8' 62 | ) 63 | } 64 | this.walkAndReplace( 65 | folderPath, 66 | ['.js', '.jsx', '.ts', '.tsx', '.html', '.json'], 67 | { 68 | ProjectName: config.projectName.replace(/^[a-z]/, l => 69 | l.toUpperCase() 70 | ), 71 | projectName: config.projectName.replace(/^[A-Z]/, L => 72 | L.toLowerCase() 73 | ) 74 | } 75 | ) 76 | } 77 | 78 | module.exports = { 79 | files: [ 80 | 'config', 81 | 'src', 82 | 'tools', 83 | 'typings', 84 | '.gitignore', 85 | '.lintstagedrc', 86 | '.npmrc', 87 | '.prettierrc', 88 | '.stylelintrc.js', 89 | 'package.json', 90 | 'postcss.config.js', 91 | 'qshell.qa.json', 92 | 'qshell.prod.json', 93 | 'README.md', 94 | 'tsconfig.json', 95 | 'tsconfig.webpack.json', 96 | 'tslint.json' 97 | ], 98 | beforeInstallCopy: function(answers, folderPath, files) { 99 | console.log('------------- beforeInstallCopy -------------') 100 | if (!!answers.jest) { 101 | files.push('test', 'jest.config.js') 102 | } 103 | }, 104 | beforeInstallDep: function(answers, folderPath) { 105 | console.log('------------- beforeInstallDep -------------') 106 | onInstall.bind(this)(folderPath, answers) 107 | }, 108 | beforeUpdateDep: function() { 109 | console.log('------------- beforeUpdateDep -------------') 110 | const folderPath = process.cwd() 111 | const answersConfig = this.readKitConfig( 112 | path.join(folderPath, 'config/steamer.config.js') 113 | ) 114 | onInstall.bind(this)(folderPath, answersConfig) 115 | }, 116 | options: [ 117 | { 118 | type: 'input', 119 | name: 'webserver', 120 | message: 'dev server domain(//localhost)', 121 | default: '//localhost' 122 | }, 123 | { 124 | type: 'input', 125 | name: 'cdn', 126 | message: 'common cdn domain(//ss.yidejia.com)', 127 | default: '//ss.yidejia.com' 128 | }, 129 | { 130 | type: 'input', 131 | name: 'port', 132 | message: 'dev server port(9000)', 133 | default: '9000' 134 | }, 135 | { 136 | type: 'confirm', 137 | name: 'jest', 138 | message: 'wanna use jest to unit test?(N)', 139 | default: false 140 | }, 141 | { 142 | type: 'confirm', 143 | name: 'mobx', 144 | message: 'wanna use mobx instead of redux?(N)', 145 | default: false 146 | }, 147 | { 148 | type: 'confirm', 149 | name: 'svg', 150 | message: 'support svg component?(N)', 151 | default: false 152 | } 153 | ] 154 | } 155 | -------------------------------------------------------------------------------- /.steamer/support-svg-component.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const svgModifiedPath = path.join(__dirname, '.svgModified') 4 | 5 | module.exports = function(folderPath) { 6 | // 修改fileRules(添加svg loader) 7 | const fileRulesPath = path.join(folderPath, 'tools/rules/fileRules.js') 8 | this.fs.copySync( 9 | path.join(svgModifiedPath, 'tools/rules/fileRules.js'), 10 | fileRulesPath 11 | ) 12 | 13 | // 修改入口scss 14 | const scssPath = path.join(folderPath, 'src/app.scss') 15 | this.fs.copySync(path.join(svgModifiedPath, 'src/app.scss'), scssPath) 16 | 17 | // 添加svg typing 18 | const svgTypingPath = path.join(folderPath, 'typings/svg.d.ts') 19 | this.fs.copySync( 20 | path.join(svgModifiedPath, 'typings/svg.d.ts'), 21 | svgTypingPath 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/css/sprites/** 2 | **/dist/**/*.css 3 | **/*.{ts, tsx} -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['stylelint-config-ydj/scss', 'stylelint-config-ydj/prettier', 'stylelint-config-css-modules'], 3 | ignoreFiles: [ 4 | '**/*.md', 5 | '**/*.ts', 6 | '**/*.tsx', 7 | '**/*.js' 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # steamer-react-ts 2 | 3 | 伊的家前端出品: react + typescript 项目脚手架 4 | 5 | ## 说明 6 | 7 | 本脚手架基于AlloyTeam出品的steamer体系制作,详情[请点击前往](https://steamerjs.github.io/) 8 | 9 | ## 使用文档 10 | 11 | [点击前往](docs/index.md) 12 | 13 | -------------------------------------------------------------------------------- /config/project.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | let utils = require('steamer-webpack-utils'); 3 | let steamerConfig = require('./steamer.config'); 4 | let __basename = path.dirname(__dirname); 5 | let __env = process.env.NODE_ENV || 'development'; 6 | let __app_env = process.env.APP_ENV || 'dev'; 7 | 8 | let isProduction = __env === 'production'; 9 | 10 | let srcPath = path.resolve(__basename, 'src'); 11 | let devPath = path.resolve(__basename, 'dev'); 12 | let distPath = path.resolve(__basename, 'dist'); 13 | let spritePath = path.resolve(__basename, 'src/img/sprites'); 14 | 15 | let hash = '[hash:6]'; 16 | let chunkhash = '[chunkhash:6]'; 17 | let contenthash = '[contenthash:6]'; 18 | 19 | // ========================= webpack快捷配置 ========================= 20 | // 基本情况下,你只需要关注这里的配置 21 | let config = { 22 | // ========================= webpack环境配置 ========================= 23 | env: __env, 24 | 25 | appEnv: __app_env, 26 | 27 | // 默认使用的npm命令行 28 | npm: 'npm', 29 | 30 | webpack: { 31 | 32 | useCdn: true, // 是否使用webserver, cdn 分离 html 与其它静态资源 33 | 34 | // ========================= webpack路径与url ========================= 35 | // 项目路径 36 | path: { 37 | src: srcPath, 38 | dev: devPath, 39 | dist: distPath, 40 | sprite: spritePath, 41 | distCdn: 'cdn', // 生成cdn的目录,dist/cdn 42 | distWebserver: 'webserver' // 生成webserver的目录, dist/webserver 43 | }, 44 | 45 | // ========================= webpack服务器及路由配置 ========================= 46 | // 开发服务器配置 47 | webserver: `${steamerConfig.webserver}:${steamerConfig.port}/`, 48 | cdn: `${steamerConfig.cdn}/<% projectName %>/dist/${__app_env}/`, 49 | cssCdn: `${steamerConfig.cssCdn || steamerConfig.cdn}/<% projectName %>/dist/${__app_env}/`, 50 | imgCdn: `${steamerConfig.imgCdn || steamerConfig.cdn}/<% projectName %>/dist/${__app_env}/`, 51 | port: steamerConfig.port, // port for local server 52 | route: [], // proxy route, 例如: /news/ 53 | 54 | 'api-port': steamerConfig['api-port'] || 7000, // 后台转发端口,默认配合 steamer-plugin-mock 使用 55 | 'api-route': steamerConfig['api-route'] || [], // 后台转发路径 56 | 57 | // ========================= webpack自定义配置 ========================= 58 | // 是否显示开发环境下的生成文件 59 | showSource: true, 60 | 61 | // 是否在生产环境下生成manifest文件 62 | manifest: false, 63 | 64 | // 是否清理生成文件夹 65 | clean: true, 66 | 67 | // sourcemap, 请写具体的sourcemap名称,而不是写true 68 | // 参考文章: https://segmentfault.com/a/1190000004280859 69 | sourceMap: { 70 | development: 'cheap-module-eval-source-map', 71 | production: false, 72 | }, 73 | 74 | cssSourceMap: false, 75 | 76 | // 预编译器,默认支持css 和 less. sass, scss 和 stylus,会自动安装 77 | style: [ 78 | 'css', 'scss' 79 | ], 80 | 81 | // 生产环境是否提取css 82 | extractCss: true, 83 | 84 | // 是否启用css模块化 85 | cssModule: true, 86 | 87 | // 合图,normal (仅1倍图) , retinaonly (仅2倍图), retina (包括1倍及2倍图), none (不使用合图) 88 | spriteMode: 'retinaonly', 89 | // 默认支持less. sass, scss 和 stylus,会自动安装 90 | spriteStyle: 'scss', 91 | 92 | // html 模板. 默认支持html 和 ejs, handlebars 和 pug(原jade),art(art-template) 会自动安装 93 | template: [ 94 | 'html' 95 | ], 96 | 97 | // 生产环境下资源(js, css, html)是否压缩 98 | compress: true, 99 | 100 | // 不经webpack打包的资源 101 | static: [ 102 | { 103 | src: 'libs/', 104 | hash: true 105 | } 106 | ], 107 | 108 | // 利用DefinePlugin给应用注入变量 109 | injectVar: { 110 | 'process.env': { 111 | NODE_ENV: JSON.stringify(__env), 112 | APP_ENV: JSON.stringify(__app_env) 113 | } 114 | }, 115 | 116 | // webpack resolve.alias 包别名 117 | alias: { 118 | 'env': path.join(srcPath, 'env', `${__app_env}.ts`) 119 | }, 120 | 121 | // 文件名与哈希, hash, chunkhash, contenthash 与webpack的哈希配置对应 122 | hash: hash, 123 | chunkhash: chunkhash, 124 | contenthash: contenthash, 125 | hashName: isProduction ? ('[name]-' + hash) : '[name]', 126 | chunkhashName: isProduction ? ('[name]-' + chunkhash) : '[name]', 127 | contenthashName: isProduction ? ('[name]-' + contenthash) : '[name]', 128 | 129 | // ========================= webpack entry配置 ========================= 130 | // 多页模式 131 | // 根据约定,自动扫描js entry,约定是src/page/xxx/main.js 或 src/page/xxx/main.jsx 132 | /** 133 | 获取结果示例 134 | { 135 | 'index': [path.join(configWebpack.path.src, "/page/index/main.js")], 136 | 'spa': [path.join(configWebpack.path.src, "/page/spa/main.js")], 137 | 'pindex': [path.join(configWebpack.path.src, "/page/pindex/main.jsx")], 138 | } 139 | */ 140 | // entry: utils.filterJsFileByCmd(utils.getJsEntry({ 141 | // srcPath: path.join(srcPath, 'page'), 142 | // fileName: 'main', 143 | // extensions: ['js', 'jsx', 'ts', 'tsx'], 144 | // level: 1 145 | // })), 146 | 147 | // 单页模式 148 | entry: { 149 | app: [path.join(srcPath, 'app.tsx')] 150 | }, 151 | 152 | // 多页模式 153 | // 自动扫描html,配合html-res-webpack-plugin 154 | /** 155 | 获取结果示例 156 | [ 157 | { 158 | key: 'index', 159 | path: 'path/src/page/index/index.html' 160 | }, 161 | { 162 | key: 'spa', 163 | path: 'path/src/page/spa/index.html' 164 | }, 165 | { 166 | key: 'pindex', 167 | path: 'path/src/page/pindex/index.html' 168 | } 169 | ] 170 | */ 171 | // html: utils.filterHtmlFileByCmd(utils.getHtmlEntry({ 172 | // srcPath: path.join(srcPath, 'page'), 173 | // level: 1 174 | // })), 175 | 176 | // 单页模式 177 | html: [{ 178 | key: 'index', 179 | path: path.join(srcPath, 'main.html') 180 | }], 181 | 182 | // 自动扫描合图,配合webpack-spritesmith插件 183 | /** 184 | 获取结果示例 185 | [ 186 | { 187 | key: 'btn', 188 | path: 'path/src/img/sprites/btn' 189 | }, 190 | { 191 | key: 'list', 192 | path: 'path/src/img/sprites/list' 193 | } 194 | ] 195 | */ 196 | sprites: utils.getSpriteEntry({ 197 | srcPath: spritePath 198 | }) 199 | 200 | } 201 | }; 202 | 203 | // ========================= webpack深度配置 ========================= 204 | // 使用了webpack-merge与webpack.base.js进行配置合并 205 | // 如果上面的配置仍未能满足你,你可以在此处对webpack直接进行配置,这里的配置与webpack的配置项目一一对应 206 | config.custom = { 207 | // webpack output 208 | getOutput: function() { 209 | return {}; 210 | }, 211 | 212 | // webpack module 213 | getModule: function() { 214 | let module = { 215 | rules: [] 216 | }; 217 | 218 | return module; 219 | }, 220 | 221 | // webpack resolve 222 | getResolve: function() { 223 | return { 224 | alias: config.webpack.alias 225 | }; 226 | }, 227 | 228 | // webpack plugins 229 | getPlugins: function() { 230 | let plugins = []; 231 | 232 | return plugins; 233 | }, 234 | 235 | // webpack externals 236 | getExternals: function() { 237 | if (isProduction) { 238 | return { 239 | 'react': 'React', 240 | 'react-dom': 'ReactDOM', 241 | 'preact': 'preact' 242 | }; 243 | } 244 | 245 | return {}; 246 | }, 247 | 248 | // 其它 webpack 配置 249 | getOtherOptions: function() { 250 | return { 251 | optimization: {} 252 | }; 253 | } 254 | }; 255 | 256 | // ========================= webpack merge 的策略 ========================= 257 | config.webpackMerge = { 258 | // webpack-merge smartStrategy 配置 259 | smartStrategyOption: { 260 | 'module.rules': 'append', 261 | 'plugins': 'append' 262 | }, 263 | 264 | // 在smartStrategy merge 之前,用户可以先行对 webpack.base.js 的配置进行处理 265 | mergeProcess: function(webpackBaseConfig) { 266 | return webpackBaseConfig; 267 | } 268 | }; 269 | 270 | module.exports = config; 271 | -------------------------------------------------------------------------------- /config/steamer.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | "webserver": "//localhost", 4 | "cdn": "//ss.yidejia.com", // 注意这里结尾不要加`/` 5 | "port": "9000", 6 | "jest": false, 7 | "mobx": false 8 | } -------------------------------------------------------------------------------- /docs/css.md: -------------------------------------------------------------------------------- 1 | # CSS 2 | 3 | 以下所有支持方案皆带postcss能力,具体配置可根据需要修改项目根目录下的`postcss.config.js` 4 | 5 | ## scss + css-module 6 | 7 | 脚手架默认使用的样式方案。 8 | 9 | 采用scss作为预编译语言,搭配css-module的模块化方案。 10 | 11 | `src/styles`目录下的所有scss文件皆是公用的scss能力,不推荐这里编写实际的样式,避免产生大量重复的css样式输出。 12 | 13 | 全局基础样式可在`src/app.scss`下面编写。 14 | 15 | 在css-module方案下,所有组件的样式的class命名都可以用简单易懂的单词进行描述。关于cssModule的介绍,可[点击前往](https://github.com/css-modules/css-modules) 16 | 17 | 如果需要复写第三方className, 可在嵌套的指定位置声明 18 |  19 | ```scss 20 | //... 21 | :global { 22 | .exitsClassName { 23 | // ... 24 | } 25 | } 26 | 27 | //... 28 | ``` 29 | 30 | 31 | ## styled-component 32 | 33 | TODO: 待完善 34 | 35 | 36 | ## less 37 | 38 | TODO: 待完善 39 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## 环境准备 2 | - 本脚手架依赖`steamerjs`和`steamer-plugin-kit`两个包 3 | 4 | ```shell 5 | $ npm i -g steamerjs@latest steamer-plugin-kit@latest 6 | ``` 7 | 8 | 注意: 这个脚手架依赖的steamerjs>=3.0.0 9 | 10 | ## 使用 11 | 12 | - 确保环境依赖正确 13 | 14 | ```shell 15 | $ str --verion 16 | # 检查版本是否大于3.0.0 17 | 18 | $ str kit --version 19 | # 检查版本是否大于3.0.0 20 | ``` 21 | 22 | 如果没有正确打印版本号,请[前往安装依赖](#环境准备) 23 | 24 | - 安装脚手架 25 | 26 | ```shell 27 | $ str kit --add https://github.com/YDJ-FE/steamer-react-ts 28 | ``` 29 | 30 | - 创建项目 31 | 32 | ```shell 33 | $ str kit 34 | # 选择脚手架, 版本,按引导一步一步输入项目信息 35 | ``` 36 | 37 | - 更新脚手架 38 | 39 | ```shell 40 | $ str kit -u --global 41 | ``` 42 | 43 | - 更新脚手架到项目 44 | ```shell 45 | # cd 到你项目根目录 46 | $ str kit -u 47 | ``` 48 | 49 | - 添加模板代码到项目 50 | 51 | ```shell 52 | $ str template 53 | # 按提示操作 54 | ``` 55 | 56 | ## 文件目录 57 | 58 | [点击跳转](./tree.md) 59 | 60 | ## css 61 | 62 | 默认使用*css-module* 和 _scss_ 63 | 64 | 你也可以使用你喜欢的预处理语言和模块化方式, e.g. `less`, `stylus`, 又或者`styled-components`, `scss-bem` 65 | 66 | [详情点击](./css.md) 67 | 68 | ## js 69 | 70 | 目前仅支持 typescript,如果需要 js,推荐[steamer-react](https://github.com/steamerjs/steamer-react)官方脚手架 71 | 72 | [详情点击](./js.md) 73 | 74 | ## 状态管理 75 | 76 | 默认使用 redux 77 | 78 | 支持选择 mobx 79 | 80 | [详情点击](./store.md) 81 | 82 | ## 可选的 svg 支持 83 | 84 | 使用[@svgr/webpack](https://github.com/smooth-code/svgr/tree/master/packages/webpack) 85 | 86 | [使用示例](./svg.md) 87 | -------------------------------------------------------------------------------- /docs/js.md: -------------------------------------------------------------------------------- 1 | # js 2 | 3 | Only Typescript! 4 | 5 | TODO: 待完善 -------------------------------------------------------------------------------- /docs/store.md: -------------------------------------------------------------------------------- 1 | # state manager 2 | 3 | ## Redux 4 | 5 | 脚手架默认使用的状态管理工具 6 | 7 | 默认搭配: redux + typesafe-action + reselect + redux-saga 8 | 9 | 10 | ## Mobx 11 | TODO: 待完善 -------------------------------------------------------------------------------- /docs/svg.md: -------------------------------------------------------------------------------- 1 | ## 例 2 | 3 | ```ts 4 | import IconReact from 'assets/svg/react.svg' 5 | ``` 6 | 7 | ```jsx 8 | ... 9 | render() { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | ... 17 | ``` 18 | -------------------------------------------------------------------------------- /docs/test.md: -------------------------------------------------------------------------------- 1 | # 测试 2 | 3 | ``` shell 4 | $ steamer kit -d [aliasName] 5 | $ cd 6 | $ steamer kit 7 | ``` 8 | 9 | 通过`steamer kit -d [aliasName]`会生成一个软链接在`~/.steamer/starterkits/`, 并已当前的版本号命令产生一个分支 10 | 11 | 然后可以一边开发, 一边在新的folder里面init项目 -------------------------------------------------------------------------------- /docs/tree.md: -------------------------------------------------------------------------------- 1 | # File Tree 2 | ``` javascript 3 | . 4 | ├── .steamer 5 | │   └── steamer-plugin-kit.js // 脚本配置 6 | ├── .stylelintignore // stylelint配置 7 | ├── .stylelintrc.js // stylelint配置 8 | ├── README.md // 项目说明 9 | ├── config // 项目配置 10 | │   ├── project.js // 构建相关配置 11 | │   └── steamer.config.js // steamer相关配置 12 | ├── package.json 13 | ├── postcss.config.js // postcss配置 14 | ├── qshell.qa.json // 测试环境qshell配置,创建项目后需要修改 15 | ├── qshell.prod.json // 生产环境配置, 创建项目后需要修改 16 | ├── src 17 | │   ├── app.scss // 公共样式 18 | │   ├── app.tsx // 入口 19 | │   ├── assets // 多媒体资源 20 | │   │   ├── images/ 21 | │   │   └── svgs/ 22 | │   ├── components/ // 展示组件 23 | │   ├── configureStore.ts // redux store 配置 24 | │   ├── containers // 容器组件 25 | │   │   ├── shared // 可共享容器组件 26 | │   │   └── views // 页面 27 | │   ├── env // 环境配置 28 | │   │   ├── dev.ts 29 | │   │   ├── prod.ts 30 | │   │   └── qa.ts 31 | │   ├── libs // 不编译的第三方库 32 | │   │   ├── preact.js 33 | │   │   ├── react-dom.js 34 | │   │   └── react.js 35 | │   ├── main.html // 页面模板 36 | │   ├── reducers.ts // reducer入口 37 | │   ├── services // api服务 38 | │   │   ├── api.ts 39 | │   │   └── contants.ts 40 | │   ├── styles // scss能力库 41 | │   │   ├── _base.scss 42 | │   │   ├── _functions.scss 43 | │   │   ├── _mixins.scss 44 | │   │   └── _var.scss 45 | │   └── util // 工具 46 | │   ├── constants.ts 47 | │   ├── http.ts 48 | │   ├── reducerInjectors.ts 49 | │   └── sagaInjectors.ts 50 | ├── tools // 构建相关工具和配置 51 | │   ├── feature // TODO 52 | │   ├── optimization // 优化相关 53 | │   ├── plugins // 插件相关 54 | │   │   ├── basePlugins.js 55 | │   │   ├── cachePlugins.js 56 | │   │   ├── resourcePlugins.js 57 | │   │   └── spritePlugins.js 58 | │   ├── rules // loader相关 59 | │   │   ├── fileRules.js 60 | │   │   ├── htmlRules.js 61 | │   │   ├── jsRules.js 62 | │   │   └── styleRules.js 63 | │   ├── script.js // 构建启动脚本 64 | │   ├── server.js // 开发服务器 65 | │   ├── template // 脚手架代码模板 66 | │   │   ├── dependency.js 67 | │   │   └── viewTemplate 68 | │   └── webpack.base.js 69 | ├── tsconfig.json // tsconfig 70 | ├── tslint.json // tslint配置 71 | └── typings // ts类型声明 72 | ``` 73 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ["js", "ts", "tsx"], 3 | moduleNameMapper: { 4 | "\\.(css|scss)$": "identity-obj-proxy" 5 | }, 6 | transform: { 7 | "^.+\\.tsx?$": "ts-jest" 8 | }, 9 | testMatch: [ 10 | "/test/spec/**/?(*.)(spec|test).ts?(x)" 11 | ], 12 | verbose: true 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steamer-react-ts", 3 | "version": "1.4.9", 4 | "description": "伊的家 react+ts脚手架", 5 | "license": "MIT", 6 | "scripts": { 7 | "lint": "lint-staged", 8 | "dev": "cross-env NODE_ENV=development node ./tools/script.js", 9 | "build:qa": "cross-env NODE_ENV=production APP_ENV=qa node ./tools/script.js", 10 | "deploy:qa": "npm run build:qa && qshell qupload ./qshell.qa.json", 11 | "build:prod": "cross-env NODE_ENV=production APP_ENV=prod node ./tools/script.js", 12 | "deploy:prod": "npm run build:prod && qshell qupload ./qshell.prod.json" 13 | }, 14 | "main": "./.steamer/steamer-react-redux-ts.js", 15 | "keywords": [ 16 | "steamer starterkit" 17 | ], 18 | "pre-commit": [ 19 | "lint" 20 | ], 21 | "dependencies": { 22 | "antd": "^3.5.0", 23 | "axios": "^0.18.0", 24 | "blueimp-md5": "^2.10.0", 25 | "bourbon": "^5.0.0", 26 | "lodash": "^4.17.10", 27 | "moment": "^2.22.1", 28 | "offline-plugin": "^5.0.3", 29 | "prop-types": "^15.6.1", 30 | "qs": "^6.5.2", 31 | "react": "16.2.0", 32 | "react-dom": "16.2.0", 33 | "react-loadable": "^5.3.1", 34 | "react-redux": "^5.0.7", 35 | "react-router": "^4.2.0", 36 | "react-router-dom": "^4.2.2", 37 | "react-router-redux": "^5.0.0-alpha.9", 38 | "redux": "^3.7.2", 39 | "redux-immutable": "^4.0.0", 40 | "redux-saga": "^0.16.0", 41 | "reselect": "^3.0.1", 42 | "styled-components": "^3.2.6" 43 | }, 44 | "devDependencies": { 45 | "@types/lodash": "^4.14.109", 46 | "@types/node": "^10.0.4", 47 | "@types/react": "^16.3.13", 48 | "@types/react-dom": "^16.0.5", 49 | "@types/react-hot-loader": "^4.1.0", 50 | "@types/react-redux": "^6.0.0", 51 | "@types/react-router-dom": "^4.3.0", 52 | "@types/react-router-redux": "^5.0.13", 53 | "@types/webpack-env": "^1.13.6", 54 | "autoprefixer": "^8.3.0", 55 | "awesome-typescript-loader": "^5.0.0", 56 | "browserslist": "^3.2.4", 57 | "cache-loader": "^1.2.2", 58 | "caniuse-db": "^1.0.30000830", 59 | "clean-webpack-plugin": "^0.1.19", 60 | "copy-webpack-plugin-hash": "^6.0.0", 61 | "cross-env": "^5.1.4", 62 | "css-loader": "^0.28.11", 63 | "exports-loader": "^0.7.0", 64 | "expose-loader": "^0.7.5", 65 | "express": "^4.16.3", 66 | "file-loader": "^1.1.11", 67 | "file-webpack-plugin": "^2.0.0", 68 | "fs-extra": "^6.0.1", 69 | "hoist-non-react-statics": "^2.5.5", 70 | "html-loader": "^0.5.5", 71 | "html-res-webpack-plugin": "^4.0.0", 72 | "http-proxy-middleware": "^0.18.0", 73 | "identity-obj-proxy": "^3.0.0", 74 | "imports-loader": "^0.8.0", 75 | "invariant": "^2.2.4", 76 | "lint-staged": "^7.2.0", 77 | "mini-css-extract-plugin": "^0.4.0", 78 | "node-sass": "^4.9.0", 79 | "optimize-css-assets-webpack-plugin": "^4.0.0", 80 | "postcss-assets": "^5.0.0", 81 | "postcss-import": "^11.1.0", 82 | "postcss-loader": "^2.1.4", 83 | "pre-commit": "^1.2.2", 84 | "react-hot-loader": "^4.1.3", 85 | "sanitize.css": "^5.0.0", 86 | "sass-loader": "^7.0.1", 87 | "spritesheet-templates-steamer": "^1.0.2", 88 | "steamer-webpack-utils": "^1.1.0", 89 | "style-loader": "^0.21.0", 90 | "stylelint": "^9.2.0", 91 | "stylelint-config-css-modules": "^1.2.0", 92 | "stylelint-config-ydj": "^0.1.1", 93 | "ts-jest": "^22.4.6", 94 | "tslint": "^5.10.0", 95 | "tslint-config-prettier": "^1.12.0", 96 | "typescript": "^2.8.3", 97 | "typings-for-css-modules-loader": "^1.7.0", 98 | "uglifyjs-webpack-plugin": "^1.2.5", 99 | "url-loader": "^1.0.1", 100 | "webpack": "^4.6.0", 101 | "webpack-dev-middleware": "^3.1.2", 102 | "webpack-hot-middleware": "^2.22.1", 103 | "webpack-manifest-plugin": "^2.0.1", 104 | "webpack-merge": "^4.1.2", 105 | "webpack-spritesmith": "^0.4.1", 106 | "write-file-webpack-plugin": "^4.2.0" 107 | }, 108 | "peerDependencies": {}, 109 | "engines": { 110 | "node": ">=8.9.4" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | let config = require('./config/project'); 2 | let PostcssImport = require('postcss-import'); 3 | let Autoprefixer = require('autoprefixer'); 4 | let PostcssAsset = require('postcss-assets'); 5 | 6 | module.exports = { 7 | plugins: [ 8 | PostcssImport({ 9 | path: [config.webpack.path.src] 10 | }), 11 | Autoprefixer({ 12 | browsers: ['iOS 7', '> 0.1%', 'android 2.1'] 13 | }), 14 | PostcssAsset({ 15 | loadPaths: [config.webpack.path.src] 16 | }) 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /qshell.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_dir": "./dist/cdn", 3 | "bucket": "ydj-statics", 4 | "key_prefix": "<% projectName %>/dist/prod/", 5 | "ignore_dir": false, 6 | "overwrite": false, 7 | "check_exists": false, 8 | "check_hash": false, 9 | "check_size": false, 10 | "rescan_local": true, 11 | "log_stdout": true 12 | } 13 | -------------------------------------------------------------------------------- /qshell.qa.json: -------------------------------------------------------------------------------- 1 | { 2 | "src_dir": "./dist/cdn", 3 | "bucket": "ydj-statics", 4 | "key_prefix": "<% projectName %>/dist/qa/", 5 | "ignore_dir": false, 6 | "overwrite": false, 7 | "check_exists": false, 8 | "check_hash": false, 9 | "check_size": false, 10 | "rescan_local": true, 11 | "log_stdout": true 12 | } 13 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | 2 | @import "styles/_base.scss"; 3 | 4 | :global { 5 | html, body { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | #root { 11 | width: 100%; 12 | height: 100%; 13 | background: $body-background; 14 | } 15 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * 程序的入口 5 | */ 6 | 7 | import * as React from "react"; 8 | import * as ReactDOM from "react-dom"; 9 | import { Provider } from "react-redux"; 10 | import { ConnectedRouter } from "react-router-redux"; 11 | import configureStore from "./configureStore"; 12 | 13 | 14 | // 如果依赖第三方UI库的样式,建议去掉sanitize.css的依赖 15 | import "sanitize.css/sanitize.css"; 16 | 17 | /** 18 | * 这里开始你的第三方包依赖,包括css 19 | * Example 20 | * 21 | * import "antd/dist/antd.css" 22 | */ 23 | 24 | 25 | // app global style 26 | import './app.scss' 27 | 28 | // Import root app 29 | import App from "containers/shared/App"; 30 | 31 | // 如果用brwoserHistory方式的话服务器需要配合设置 32 | // import createHistory from 'history/createBrowserHistory' 33 | import createHistory from "history/createHashHistory"; 34 | 35 | 36 | 37 | 38 | const initialState = {}; 39 | const history = createHistory(); 40 | const store = configureStore(initialState, history); 41 | const MOUNT_NODE = document.getElementById("root"); 42 | 43 | const render = (Component) => { 44 | ReactDOM.render( 45 | 46 | 47 | 48 | 49 | , 50 | MOUNT_NODE 51 | ); 52 | }; 53 | 54 | render(App) 55 | 56 | if (module.hot) { 57 | // 热更新React Components 58 | // module.hot.accept不支持动态的依赖 59 | // 必须是编译时定义的常量 60 | module.hot.accept(["containers/shared/App"], () => { 61 | ReactDOM.unmountComponentAtNode(MOUNT_NODE); 62 | render(require('containers/shared/App').default); 63 | }); 64 | } 65 | 66 | // TODO: 待验证 67 | // Install ServiceWorker and AppCache in the end since 68 | // it's not most important operation and if main code fails, 69 | // we do not want it installed 70 | if (process.env.NODE_ENV === "production") { 71 | // tslint:disable-next-line:no-var-requires 72 | require("offline-plugin/runtime").install(); // eslint-disable-line global-require 73 | } 74 | -------------------------------------------------------------------------------- /src/assets/images/logo404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/steamer-react-ts/cb18e32841f2000e218a74476e29c3faebff1306/src/assets/images/logo404.png -------------------------------------------------------------------------------- /src/components/ReducerInjector/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 高阶组件, 注入reducer 3 | */ 4 | 5 | import * as React from "react"; 6 | import { ReactPropTypes } from "react"; 7 | // import hoisNonReactStatics from "hoist-non-react-statics"; 8 | import getInjectors, { RESET_SUB_STATE } from "util/reducerInjectors"; 9 | import { Reducer } from "redux"; 10 | import * as PropTypes from "prop-types"; 11 | import hoisNonReactStatics from 'hoist-non-react-statics' 12 | 13 | interface IP { 14 | store: IStore; 15 | [key: string]: any; 16 | } 17 | 18 | interface IInjectReducerParams { 19 | /** 20 | * reducer key 21 | * 22 | * @type {string} 23 | * @memberof IInjectReducerParams 24 | */ 25 | key: string; 26 | /** 27 | * reducer instance 28 | * 29 | * @type {Reducer} 30 | * @memberof IInjectReducerParams 31 | */ 32 | reducer: Reducer; 33 | /** 34 | * 如果为true, 则页面ummount之后仍保持state, 否则重置初始值 35 | * 默认false 36 | * 37 | * @type {boolean} 38 | * @memberof IInjectReducerParams 39 | */ 40 | keepStateAlive?: boolean; 41 | } 42 | 43 | export default ({ key, reducer, keepStateAlive }: IInjectReducerParams) => ( 44 | WrapperComponent: React.ComponentType 45 | ) => { 46 | class ReducerInjector extends React.Component { 47 | public static WrapperComponent = WrapperComponent; 48 | 49 | public static displayName = `withReducer(${WrapperComponent.displayName || 50 | WrapperComponent.name || 51 | "Component"})`; 52 | 53 | static contextTypes = { 54 | store: PropTypes.object.isRequired 55 | }; 56 | 57 | injectors = getInjectors(this.context.store); 58 | 59 | componentWillMount() { 60 | const { injectReducer } = this.injectors; 61 | injectReducer(key, reducer); 62 | } 63 | 64 | componentWillUnmount() { 65 | if (!keepStateAlive) { 66 | this.context.store.dispatch({ type: RESET_SUB_STATE, key }); 67 | } 68 | } 69 | 70 | render() { 71 | return ; 72 | } 73 | } 74 | 75 | return hoisNonReactStatics(ReducerInjector, WrapperComponent); 76 | }; 77 | -------------------------------------------------------------------------------- /src/components/SagaInjector/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 高阶组件 注入saga 3 | * 4 | */ 5 | import * as React from 'react' 6 | import * as PropTypes from 'prop-types' 7 | import hoisNonReactStatics from 'hoist-non-react-statics' 8 | import getInjectors from 'util/sagaInjectors' 9 | import { SagaIterator } from 'redux-saga' 10 | 11 | interface IP { 12 | store: IStore 13 | [key: string]: any 14 | } 15 | 16 | interface IInjectSagaParams { 17 | /** 18 | * saga的名字(key) 19 | * 20 | * @type {string} 21 | * @memberof IInjectSagaParams 22 | */ 23 | key: string 24 | /** 25 | * saga对象 26 | * 27 | * @type {SagaIterator} 28 | * @memberof IInjectSagaParams 29 | */ 30 | saga: SagaIterator 31 | mode?: string 32 | } 33 | 34 | /** 35 | * Dynamically injects a saga, passes component's props as saga arguments 36 | * 37 | * @param {string} key A key of the saga 38 | * @param {Saga} saga A root saga that will be injected 39 | * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and 40 | * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options: 41 | * - constants.DAEMON—starts the saga on component mount and never cancels it or starts again, 42 | * - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again. 43 | * 44 | */ 45 | export default ({ key, saga, mode }: IInjectSagaParams) => ( 46 | WrappedComponent: React.ComponentType 47 | ) => { 48 | class InjectSaga extends React.Component { 49 | static WrappedComponent = WrappedComponent 50 | static displayName = `withSaga(${WrappedComponent.displayName || 51 | WrappedComponent.name || 52 | 'Component'})` 53 | 54 | static contextTypes = { 55 | store: PropTypes.object.isRequired 56 | } 57 | 58 | componentWillMount() { 59 | const { injectSaga } = this.injectors 60 | 61 | injectSaga(key, { saga, mode }, this.props) 62 | } 63 | 64 | componentWillUnmount() { 65 | const { ejectSaga } = this.injectors 66 | 67 | ejectSaga(key) 68 | } 69 | 70 | // tslint:disable-next-line:member-ordering 71 | injectors = getInjectors(this.context.store) 72 | 73 | render() { 74 | return 75 | } 76 | } 77 | return hoisNonReactStatics(InjectSaga, WrappedComponent) 78 | } 79 | -------------------------------------------------------------------------------- /src/configureStore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create the store with dynamic reducers 3 | */ 4 | 5 | import Redux, { createStore, applyMiddleware, compose } from "redux"; 6 | import { fromJS } from "immutable"; 7 | import { routerMiddleware } from "react-router-redux"; 8 | import createSagaMiddleware from "redux-saga"; 9 | import createReducer from "./reducers"; 10 | 11 | import { Task, SagaIterator } from "redux-saga"; 12 | 13 | const sagaMiddleware = createSagaMiddleware(); 14 | 15 | export default function configureStore(initialState = {}, history) { 16 | // Create the store with two middlewares 17 | // 1. sagaMiddleware: Makes redux-sagas work 18 | // 2. routerMiddleware: Syncs the location/URL path to the state 19 | const middlewares = [sagaMiddleware, routerMiddleware(history)]; 20 | 21 | const enhancers = [applyMiddleware(...middlewares)]; 22 | 23 | // If Redux DevTools Extension is installed use it, otherwise use Redux compose 24 | /* eslint-disable no-underscore-dangle */ 25 | const composeEnhancers = 26 | process.env.NODE_ENV !== "production" && 27 | typeof window === "object" && 28 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ 29 | ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ 30 | // TODO Try to remove when `react-router-redux` is out of beta, LOCATION_CHANGE should not be fired more than once after hot reloading 31 | // Prevent recomputing reducers for `replaceReducer` 32 | shouldHotReload: false 33 | }) 34 | : compose; 35 | /* eslint-enable */ 36 | 37 | const store: IStore = createStore( 38 | createReducer(), 39 | fromJS(initialState), 40 | composeEnhancers(...enhancers) 41 | ); 42 | 43 | // Extensions 44 | store.runSaga = sagaMiddleware.run; 45 | store.injectedReducers = {}; // Reducer registry 46 | store.injectedSagas = {}; // Saga registry 47 | 48 | // Make reducers hot reloadable, see http://mxs.is/googmo 49 | /* istanbul ignore next */ 50 | if (module.hot) { 51 | module.hot.accept("./reducers", () => { 52 | store.replaceReducer(createReducer(store.injectedReducers)); 53 | }); 54 | } 55 | 56 | return store; 57 | } 58 | -------------------------------------------------------------------------------- /src/containers/shared/App/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * App Actions 3 | * 4 | * * To add a new Action: 5 | * 1) Import your constant 6 | * 2) Add a function like this: 7 | * export function yourAction(var) { 8 | * return { type: YOUR_ACTION_CONSTANT, var: var } 9 | * } 10 | */ 11 | 12 | -------------------------------------------------------------------------------- /src/containers/shared/App/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * AppConstants 3 | * 每个action都有相应的type, 好让reducer知道如何处理 4 | * 避免在reducer和action之间使用了错别字, 我们都从这里获取相同的constants 5 | * 我们推荐加前缀比如 ‘yourproject/yourComponent’避免reducer搞错action,类似命名空间 6 | * 7 | * Follow this format: 8 | * export const YOUR_ACTION_CONSTANT = 'pageName/containerName/YOUR_ACTION_CONSTANT'; 9 | */ 10 | 11 | const PAGENAME = 'finance/App' 12 | 13 | export const DEMO_TYPE = `${PAGENAME}/DEMO_TYPE` 14 | -------------------------------------------------------------------------------- /src/containers/shared/App/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App 4 | * 5 | * 这个组件是每个页面的最外层骨架,所以这里的代码只能存放所有页面公有的, 比如(导航)或者路由 6 | * 切勿在其他组件引用 7 | */ 8 | 9 | import * as React from "react"; 10 | import { Switch, Route } from "react-router-dom"; 11 | 12 | import HomePage from "containers/views/Home"; 13 | import NotFoundPage from "containers/views/NotFound"; 14 | 15 | 16 | import * as styles from "./style.scss"; 17 | 18 | const AppWrapper = props => ( 19 |
{props.children}
20 | ); 21 | 22 | export default function App(props) { 23 | return ( 24 | 25 | 26 | 27 | 28 | {/* 29 | */} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/shared/App/reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AppReducer 3 | * 4 | * 全局的reducer 5 | * 6 | * Example 7 | * case YOUR_ACTION_CONSTANT: 8 | * return state.set('yourStateVarivle', yourActionPayload) 9 | */ 10 | 11 | import { fromJS } from "immutable"; 12 | import { AnyAction } from "redux"; 13 | 14 | import * as types from './constants' 15 | 16 | // The initial state of the App 17 | 18 | const initialState = fromJS({ 19 | loading: false, 20 | }); 21 | 22 | function appReducer(state = initialState, action: AnyAction) { 23 | switch (action.type) { 24 | case types.DEMO_TYPE: 25 | return state 26 | .set("loading", true) 27 | 28 | default: 29 | return state; 30 | } 31 | } 32 | 33 | export default appReducer; 34 | -------------------------------------------------------------------------------- /src/containers/shared/App/selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The global state selectors 3 | * 4 | * 添加一个selector(functoin),提取state的数据并计算 5 | * 6 | * Example 7 | * const yourSelector = state => state.get('rootStateField') 8 | * 9 | * or 10 | * const yourSelect = () => createSelector(parentSelector, selectorState => selectorState.get('stateField') ) 11 | * 12 | */ 13 | 14 | import { createSelector } from "reselect"; 15 | 16 | export const selectGlobal = state => state.get("global"); 17 | 18 | export const selectRoute = state => state.get("route"); 19 | 20 | 21 | export const makeSelectLoading = () => 22 | createSelector(selectGlobal, globalState => globalState.get("loading")); 23 | -------------------------------------------------------------------------------- /src/containers/shared/App/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .appWrapper { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | overflow: auto; 8 | padding: 16px; 9 | margin: 0 auto; 10 | min-height: 100%; 11 | flex-direction: column; 12 | } -------------------------------------------------------------------------------- /src/containers/views/Home/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Home Actions 3 | * 4 | * * To add a new Action: 5 | * 1) Import your constant 6 | * 2) Add a function like this: 7 | * export function yourAction(var) { 8 | * return { type: YOUR_ACTION_CONSTANT, var: var } 9 | * } 10 | */ 11 | 12 | import { 13 | DEMO_TYPE 14 | } from "./constants"; 15 | 16 | export function demoAction() { 17 | return { 18 | type: DEMO_TYPE 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/containers/views/Home/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Home Constants 3 | * 4 | * 每个action都有相应的type, 好让reducer知道如何处理 5 | * 避免在reducer和action之间使用了错别字, 我们都从这里获取相同的constants 6 | * 我们推荐加前缀比如 ‘yourproject/yourComponent’避免reducer搞错action,类似命名空间 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'pageName/containerName/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | const PAGENAME = 'finanace/Home' 13 | 14 | export const DEMO_TYPE = `${PAGENAME}/DEMO_TYPE` 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/containers/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { compose } from "redux"; 3 | import { connect } from "react-redux"; 4 | import injectReducer from "components/ReducerInjector"; 5 | import injectSaga from "components/SagaInjector"; 6 | import { createStructuredSelector } from "reselect"; 7 | 8 | import reducer from "./reducer"; 9 | import saga from "./saga"; 10 | import * as selectors from "./selectors"; 11 | import * as actions from "./actions"; 12 | 13 | import * as styles from "./style.scss"; 14 | 15 | interface IP { 16 | demoProps: any 17 | } 18 | 19 | class Home extends React.PureComponent { 20 | render() { 21 | return
22 |

Hello World!

23 |
; 24 | } 25 | } 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | }); 29 | 30 | const mapStateToProps = createStructuredSelector({ 31 | // TODO: add Selector 32 | }); 33 | 34 | const withReducer = injectReducer({ key: "home", reducer }); 35 | const withSaga = injectSaga({ key: "home", saga }); 36 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 37 | 38 | export default compose(withReducer, withSaga, withConnect)(Home); 39 | -------------------------------------------------------------------------------- /src/containers/views/Home/reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Home Reducer 3 | * 4 | * The reducer takes care of our data. Using actions, we can change our 5 | * application state. 6 | * To add a new action, add it to the switch statement in the reducer function 7 | * 8 | * Example 9 | * case YOUR_ACTION_CONSTANT: 10 | * return state.set('yourStateVarivle', yourActionPayload) 11 | */ 12 | 13 | import { fromJS } from "immutable"; 14 | import { AnyAction } from "redux"; 15 | import { DEMO_TYPE } from "./constants"; 16 | 17 | const initialState = fromJS({ 18 | }); 19 | 20 | export default (state = initialState, action: AnyAction) => { 21 | switch (action.type) { 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/containers/views/Home/saga.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * home Saga 3 | * 4 | */ 5 | 6 | 7 | import { takeEvery, takeLatest, put, call, select } from "redux-saga/effects"; 8 | 9 | import * as api from "services/api"; 10 | // import { makePageIndexSelector, makePageSizeSelector } from "./selectors"; 11 | 12 | // 入口 13 | export default function* mainSaga() { 14 | // yield takeLatest(QUERY_LIST, queryListSaga); 15 | // yield takeLatest(CHANGE_PAGE_QUERY, changePageQuery); 16 | } 17 | -------------------------------------------------------------------------------- /src/containers/views/Home/selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Selector 3 | */ 4 | 5 | import { createSelector } from "reselect"; 6 | 7 | export const homeSelector = state => 8 | state.get("home"); 9 | -------------------------------------------------------------------------------- /src/containers/views/Home/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .pageContainer { 4 | // .. 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /src/containers/views/NotFound/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { compose } from "redux"; 3 | import { connect } from "react-redux"; 4 | 5 | import * as styles from './style.scss' 6 | 7 | import * as Logo from 'assets/images/logo404.png' 8 | 9 | class NotFound extends React.Component { 10 | public render() { 11 | return ( 12 |
13 | 14 |
15 |

404

16 |

抱歉,你访问的页面不存在

17 |
18 |
19 | ) 20 | } 21 | } 22 | 23 | export function mapDispatchToProps(dispatch) { 24 | return {}; 25 | } 26 | 27 | export function mapStateToProps(state) { 28 | return {} 29 | } 30 | 31 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 32 | 33 | export default compose(withConnect)(NotFound); 34 | -------------------------------------------------------------------------------- /src/containers/views/NotFound/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .pageContainer { 4 | display: flex; 5 | width: 100%; 6 | height: 100%; 7 | align-items: center; 8 | justify-content: center; 9 | background: #f0f2f5; 10 | } 11 | 12 | .leftWrapper { 13 | margin-right: 10%; 14 | } 15 | 16 | .pageTitle { 17 | color: #434e59; 18 | font-size: 70px; 19 | } 20 | 21 | .pageDescription { 22 | color: rgba(#000, 0.45); 23 | font-size: 20px; 24 | } 25 | -------------------------------------------------------------------------------- /src/env/dev.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | env: 'dev', 3 | apiUrlPrefix: '/' // api server baseURL 4 | }; -------------------------------------------------------------------------------- /src/env/prod.ts: -------------------------------------------------------------------------------- 1 | 2 | import env from './dev' 3 | 4 | env.env = 'prod' 5 | 6 | export default env -------------------------------------------------------------------------------- /src/env/qa.ts: -------------------------------------------------------------------------------- 1 | 2 | import env from './dev'; 3 | import { eventNames } from 'cluster'; 4 | 5 | env.env = 'qa'; 6 | 7 | export default env; -------------------------------------------------------------------------------- /src/libs/preact.js: -------------------------------------------------------------------------------- 1 | !function(){"use strict";function e(){}function t(t,n){var o,r,i,l,a=E;for(l=arguments.length;l-- >2;)W.push(arguments[l]);n&&null!=n.children&&(W.length||W.push(n.children),delete n.children);while(W.length)if((r=W.pop())&&void 0!==r.pop)for(l=r.length;l--;)W.push(r[l]);else"boolean"==typeof r&&(r=null),(i="function"!=typeof t)&&(null==r?r="":"number"==typeof r?r+="":"string"!=typeof r&&(i=!1)),i&&o?a[a.length-1]+=r:a===E?a=[r]:a.push(r),o=i;var u=new e;return u.nodeName=t,u.children=a,u.attributes=null==n?void 0:n,u.key=null==n?void 0:n.key,void 0!==S.vnode&&S.vnode(u),u}function n(e,t){for(var n in t)e[n]=t[n];return e}function o(e,o){return t(e.nodeName,n(n({},e.attributes),o),arguments.length>2?[].slice.call(arguments,2):e.children)}function r(e){!e.__d&&(e.__d=!0)&&1==A.push(e)&&(S.debounceRendering||P)(i)}function i(){var e,t=A;A=[];while(e=t.pop())e.__d&&k(e)}function l(e,t,n){return"string"==typeof t||"number"==typeof t?void 0!==e.splitText:"string"==typeof t.nodeName?!e._componentConstructor&&a(e,t.nodeName):n||e._componentConstructor===t.nodeName}function a(e,t){return e.__n===t||e.nodeName.toLowerCase()===t.toLowerCase()}function u(e){var t=n({},e.attributes);t.children=e.children;var o=e.nodeName.defaultProps;if(void 0!==o)for(var r in o)void 0===t[r]&&(t[r]=o[r]);return t}function _(e,t){var n=t?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n.__n=e,n}function p(e){var t=e.parentNode;t&&t.removeChild(e)}function c(e,t,n,o,r){if("className"===t&&(t="class"),"key"===t);else if("ref"===t)n&&n(null),o&&o(e);else if("class"!==t||r)if("style"===t){if(o&&"string"!=typeof o&&"string"!=typeof n||(e.style.cssText=o||""),o&&"object"==typeof o){if("string"!=typeof n)for(var i in n)i in o||(e.style[i]="");for(var i in o)e.style[i]="number"==typeof o[i]&&!1===V.test(i)?o[i]+"px":o[i]}}else if("dangerouslySetInnerHTML"===t)o&&(e.innerHTML=o.__html||"");else if("o"==t[0]&&"n"==t[1]){var l=t!==(t=t.replace(/Capture$/,""));t=t.toLowerCase().substring(2),o?n||e.addEventListener(t,f,l):e.removeEventListener(t,f,l),(e.__l||(e.__l={}))[t]=o}else if("list"!==t&&"type"!==t&&!r&&t in e)s(e,t,null==o?"":o),null!=o&&!1!==o||e.removeAttribute(t);else{var a=r&&t!==(t=t.replace(/^xlink\:?/,""));null==o||!1===o?a?e.removeAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase()):e.removeAttribute(t):"function"!=typeof o&&(a?e.setAttributeNS("http://www.w3.org/1999/xlink",t.toLowerCase(),o):e.setAttribute(t,o))}else e.className=o||""}function s(e,t,n){try{e[t]=n}catch(e){}}function f(e){return this.__l[e.type](S.event&&S.event(e)||e)}function d(){var e;while(e=D.pop())S.afterMount&&S.afterMount(e),e.componentDidMount&&e.componentDidMount()}function h(e,t,n,o,r,i){H++||(R=null!=r&&void 0!==r.ownerSVGElement,j=null!=e&&!("__preactattr_"in e));var l=m(e,t,n,o,i);return r&&l.parentNode!==r&&r.appendChild(l),--H||(j=!1,i||d()),l}function m(e,t,n,o,r){var i=e,l=R;if(null!=t&&"boolean"!=typeof t||(t=""),"string"==typeof t||"number"==typeof t)return e&&void 0!==e.splitText&&e.parentNode&&(!e._component||r)?e.nodeValue!=t&&(e.nodeValue=t):(i=document.createTextNode(t),e&&(e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0))),i.__preactattr_=!0,i;var u=t.nodeName;if("function"==typeof u)return U(e,t,n,o);if(R="svg"===u||"foreignObject"!==u&&R,u+="",(!e||!a(e,u))&&(i=_(u,R),e)){while(e.firstChild)i.appendChild(e.firstChild);e.parentNode&&e.parentNode.replaceChild(i,e),b(e,!0)}var p=i.firstChild,c=i.__preactattr_,s=t.children;if(null==c){c=i.__preactattr_={};for(var f=i.attributes,d=f.length;d--;)c[f[d].name]=f[d].value}return!j&&s&&1===s.length&&"string"==typeof s[0]&&null!=p&&void 0!==p.splitText&&null==p.nextSibling?p.nodeValue!=s[0]&&(p.nodeValue=s[0]):(s&&s.length||null!=p)&&v(i,s,n,o,j||null!=c.dangerouslySetInnerHTML),g(i,t.attributes,c),R=l,i}function v(e,t,n,o,r){var i,a,u,_,c,s=e.childNodes,f=[],d={},h=0,v=0,y=s.length,g=0,w=t?t.length:0;if(0!==y)for(var C=0;Cu.length&&u.push(a)}function t(a,b,c,d){var f=typeof a;if("undefined"===f||"boolean"===f)a=null;var l=!1;if(null===a)l=!0;else switch(f){case "string":case "number":l=!0;break;case "object":switch(a.$$typeof){case r:case P:case Q:case R:l=!0}}if(l)return c(d,a,""===b?"."+D(a,0):b),1;l=0;b=""===b?".":b+":";if(Array.isArray(a))for(var e= 13 | 0;ea;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(d){return!1}}()?Object.assign: 16 | function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var d,f=1;f 2 | 3 | 4 | 5 | 6 | 7 | <% projectName %> 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/reducers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | */ 4 | 5 | import { fromJS } from "immutable"; 6 | import { combineReducers } from "redux-immutable"; 7 | import { routerReducer } from "react-router-redux"; 8 | import * as Redux from "redux"; 9 | 10 | import globalReducer from "containers/shared/App/reducer"; 11 | 12 | // import testReducer from 'containers/views/CertificateList/reducer' 13 | 14 | export const RESET_SUB_STATE = "ROOT/RESET_SUB_STATE"; 15 | 16 | /** 17 | * Creates the main reducer with the dynamically injected ones 18 | */ 19 | export default function createReducer(injectedReducers?) { 20 | const appReducer = combineReducers({ 21 | // route: routeReducer, // TODO: check route state in redux 22 | router: routerReducer, 23 | global: globalReducer, 24 | ...injectedReducers 25 | }); 26 | const rootReducers = (state, action) => { 27 | let s; 28 | switch (action.type) { 29 | case RESET_SUB_STATE: 30 | s = state.delete(action.key); 31 | break; 32 | default: 33 | s = state; 34 | } 35 | return appReducer(s, action); 36 | }; 37 | return rootReducers; 38 | } 39 | -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "qs"; 2 | import http from "util/http"; 3 | import { message } from "antd"; 4 | 5 | import { DEFAULT_HTTP_OPTION } from "./contants"; 6 | 7 | export async function demoRequest(id: string) { 8 | return http.get('yourPath', { 9 | id 10 | }, DEFAULT_HTTP_OPTION) 11 | } 12 | -------------------------------------------------------------------------------- /src/services/contants.ts: -------------------------------------------------------------------------------- 1 | import { message } from 'antd' 2 | 3 | /** 4 | * 默认的http请求配置, 默认显示错误信息提示 5 | */ 6 | export const DEFAULT_HTTP_OPTION: HttpRequestOptions = { 7 | onError: (err: IHttpError) => { 8 | message.error(err.msg || '未知错误') 9 | } 10 | } -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | // 所有sass文件依赖基础入口 2 | // 注意: 因为所有scss文件都依赖, 3 | // 此文件包括此文件依赖的所有模块都不能有实际的css代码输出, 4 | // 避免太多重复的css代码输出 5 | 6 | @import "_var"; 7 | 8 | // -------- bourbon -------- 9 | @import "~bourbon/core/_bourbon.scss"; 10 | // -------- bourbon:galobal-setting -------- 11 | $bourbon: ( 12 | "contrast-switch-dark-color" : #000, 13 | "contrast-switch-light-color": #fff, 14 | "global-font-file-formats" : ("ttf", "woff2", "woff"), 15 | "modular-scale-base" : 1em, 16 | "modular-scale-ratio" : $major-third, 17 | "rails-asset-pipeline" : false, 18 | ); 19 | 20 | // -------- sass-bem -------- 21 | // 如果使用bem, 可以通过 yarn add -D sass-bem 方式安装 22 | // @import "~sass-bem/_bem"; 23 | // 如果使用sass-bem, 安装成功后取消下面注释可修改默认配置 24 | // $bem: ( 25 | // debug: false, 26 | // separator: (element: "__", modifier: "--", "state": "-"), 27 | // namespace: 28 | // ( 29 | // component: "ykc", 30 | // hack: "_", 31 | // object: "yk", 32 | // scope: "s", 33 | // test: "qa", 34 | // theme: "t", 35 | // utility: "yku" 36 | // ), 37 | // shortcodes: (ab: ("before", "after")) 38 | // ) !default; 39 | 40 | @import "_functions"; 41 | @import "_mixins"; -------------------------------------------------------------------------------- /src/styles/_functions.scss: -------------------------------------------------------------------------------- 1 | // @function rem($px1, $px2: n, $px3: n, $px4: n) { 2 | // $all: $px1 / $rem + rem; 3 | 4 | // @each $value in $px2, $px3, $px4 { 5 | // @if $value != n { 6 | // $all: $all + " " + $value / $rem + rem; 7 | // } 8 | // } 9 | 10 | // @return $all; 11 | // } 12 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/steamer-react-ts/cb18e32841f2000e218a74476e29c3faebff1306/src/styles/_mixins.scss -------------------------------------------------------------------------------- /src/styles/_var.scss: -------------------------------------------------------------------------------- 1 | // global variables 2 | // -------- Colors ----------- 3 | $primary-color : #1890ff; 4 | $info-color : #1890ff; 5 | $success-color : #52c41a; 6 | $processing-color : $primary-color; 7 | $error-color : #f5222d; 8 | $highlight-color : #f5222d; 9 | $warning-color : #faad14; 10 | $normal-color : #d9d9d9; 11 | $black-color : #000; 12 | $body-color : rgba(#000, 0.65); 13 | $heading-color : rgba(#000, 0.85); 14 | $text-color : rgba(#000, 0.65); 15 | $text-color-secondary : rgba(#000, 0.45); 16 | $heading-color-dark : rgba(#fff, 1); 17 | $text-color-dark : rgba(#fff, 0.85); 18 | $text-color-secondary-dark: rgba(#fff, 0.65); 19 | // Background color for `` 20 | $body-background : #F0F2F5; 21 | // Base background color for most components 22 | $component-background : #fff; 23 | 24 | 25 | 26 | // -------- Font -------- 27 | $font-family-no-number: "Chinese Quote", -apple-system, BlinkMacSystemFont, 28 | "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", 29 | "Helvetica Neue", Helvetica, Arial, sans-serif; 30 | $font-family : "Monospaced Number", $font-family-no-number; 31 | $font-weight-light : 300 !default; 32 | $font-weight-normal : 400 !default; 33 | $font-weight-bold : 700 !default; 34 | $font-weight-base : $font-weight-normal !default; 35 | $font-size-base : 14px; 36 | $font-size-lg : $font-size-base + 2px; 37 | $font-size-sm : 12px; 38 | $code-family : Consolas, Menlo, Courier, monospace; 39 | 40 | 41 | // -------- box & border -------- 42 | $line-height-base : 1.5; 43 | $border-radius-base: 4px; 44 | $border-radius-sm : 2px; 45 | 46 | // -------- Function -------- 47 | $rem: 40; 48 | $flexs: ( 49 | 1, 50 | 2, 51 | 3, 52 | 4, 53 | 5, 54 | 6, 55 | 7, 56 | 8, 57 | 9, 58 | 10, 59 | 11, 60 | 12 61 | ); 62 | 63 | 64 | 65 | 66 | // -------- Media queries breakpoints -------- 67 | // Extra small screen / phone 68 | $screen-xs : 480px; 69 | $screen-xs-min : $screen-xs; 70 | 71 | // Small screen / tablet 72 | $screen-sm : 576px; 73 | $screen-sm-min : $screen-sm; 74 | 75 | // Medium screen / desktop 76 | $screen-md : 768px; 77 | $screen-md-min : $screen-md; 78 | 79 | // Large screen / wide desktop 80 | $screen-lg : 992px; 81 | $screen-lg-min : $screen-lg; 82 | 83 | // Extra large screen / full hd 84 | $screen-xl : 1200px; 85 | $screen-xl-min : $screen-xl; 86 | 87 | // Extra extra large screen / large descktop 88 | $screen-xxl : 1600px; 89 | $screen-xxl-min: $screen-xxl; 90 | 91 | // provide a maximum 92 | $screen-xs-max : ($screen-sm-min - 1px); 93 | $screen-sm-max : ($screen-md-min - 1px); 94 | $screen-md-max : ($screen-lg-min - 1px); 95 | $screen-lg-max : ($screen-xl-min - 1px); 96 | $screen-xl-max : ($screen-xxl-min - 1px); 97 | 98 | // Border color 99 | $border-color-base : hsv(0, 0, 85%); // base border outline a component 100 | $border-color-split: hsv(0, 0, 91%); // split border inside a component 101 | $border-width-base : 1px; // width of the border for a component 102 | $border-style-base : solid; // style of a components border 103 | -------------------------------------------------------------------------------- /src/util/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * react-saga 模式 4 | */ 5 | export enum ENUM_ALLOW_MODES { 6 | RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount', 7 | DAEMON = '@@saga-injector/daemon', 8 | ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount', 9 | } -------------------------------------------------------------------------------- /src/util/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * axios封装的http请求 3 | */ 4 | 5 | import axios, { AxiosResponse } from 'axios' 6 | import * as md5 from 'blueimp-md5' 7 | import * as qs from 'qs' 8 | import ENVCONFIG from 'env' 9 | 10 | enum HTTPERROR { 11 | LOGICERROR, 12 | TIMEOUTERROR, 13 | NETWORKERROR 14 | } 15 | 16 | 17 | const isSuccess = res => 18 | res.code !== undefined && res.code !== null && Number(res.code) === 1 19 | 20 | const resFormat = res => res.response || res.data || {} 21 | 22 | const http: HttpResquest = {} 23 | 24 | const methods = ['get', 'post', 'put', 'delete'] 25 | 26 | methods.forEach(method => { 27 | http[method] = ( 28 | url: string, 29 | data: PlainObject, 30 | options?: HttpRequestOptions 31 | ): Promise => { 32 | const opts: HttpRequestOptions = Object.assign( 33 | { 34 | baseUrl: ENVCONFIG.apiUrlPrefix, // 默认apiUrl前缀使用环境配置,特殊接口可以通过httpRequestOptions覆盖 35 | formatResponse: true, 36 | }, 37 | options || {} 38 | ) 39 | 40 | const axiosConfig: AxiosRequestConfig = { 41 | method, 42 | url, 43 | baseURL: opts.baseUrl, 44 | withCredentials: true, // 如果不需要带cookie请求, 请注释 45 | } 46 | 47 | const requestInstance = axios.create() 48 | 49 | // 创建axios实例,配置全局追加请求参数 50 | requestInstance.interceptors.request.use( 51 | (cfg: AxiosRequestConfig) => { 52 | const ts = Date.now() / 1000 53 | const queryData = { 54 | ts 55 | } 56 | cfg.params = Object.assign({}, cfg.params || {}, queryData) 57 | return cfg 58 | }, 59 | error => Promise.reject(error) 60 | ) 61 | 62 | // 全局请求错误拦截 63 | requestInstance.interceptors.response.use( 64 | response => response, 65 | error => { 66 | const errorDetail: IHttpError = { 67 | msg: error.message || '网络故障', 68 | type: /^timeout of/.test(error.message) 69 | ? HTTPERROR[HTTPERROR.TIMEOUTERROR] 70 | : HTTPERROR[HTTPERROR.NETWORKERROR], 71 | config: error.config 72 | } 73 | // cbNetworkError && cbNetworkError.call(null, _err); 74 | return Promise.reject(errorDetail) 75 | } 76 | ) 77 | 78 | // 参数传递方式 79 | if (method === 'get') { 80 | axiosConfig.params = data 81 | } else if (data instanceof FormData) { 82 | axiosConfig.data = data 83 | } else { 84 | axiosConfig.data = qs.stringify(data) 85 | } 86 | 87 | return requestInstance 88 | .request(axiosConfig) 89 | .then(response => { 90 | let rdata: IHttpResponse 91 | if ( 92 | typeof response.data === 'object' && 93 | Array.isArray(response.data) 94 | ) { 95 | rdata = response.data[0] 96 | } else { 97 | rdata = response.data 98 | } 99 | if (!opts.formatResponse) { 100 | return rdata 101 | } 102 | if (!isSuccess(rdata)) { 103 | const errorDetail: IHttpError = { 104 | msg: rdata.msg, 105 | code: rdata.code, 106 | type: HTTPERROR[HTTPERROR.LOGICERROR], 107 | config: response.config 108 | } 109 | return Promise.reject(errorDetail) 110 | } 111 | return resFormat(rdata) 112 | }) 113 | .catch((err: IHttpError) => { 114 | if(opts.onError) { 115 | options.onError(err) 116 | } 117 | return Promise.reject(err) 118 | }) 119 | } 120 | }) 121 | 122 | export default http 123 | -------------------------------------------------------------------------------- /src/util/reducerInjectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Reducer注入器 3 | */ 4 | 5 | import * as invariant from "invariant"; 6 | import { isEmpty, isFunction, isString } from "lodash"; 7 | import { Reducer } from "redux"; 8 | 9 | import createReducer, { RESET_SUB_STATE } from "../reducers"; 10 | 11 | export { RESET_SUB_STATE }; 12 | 13 | export function injectReducerFactory(store: IStore) { 14 | return function injectReducer(key: string, reducer: Reducer) { 15 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different 16 | if ( 17 | Reflect.has(store.injectedReducers, key) && 18 | store.injectedReducers[key] === reducer 19 | ) { 20 | return; 21 | } 22 | 23 | store.injectedReducers[key] = reducer; 24 | store.replaceReducer(createReducer(store.injectedReducers)); 25 | }; 26 | } 27 | 28 | export default function getInjectors(store: IStore) { 29 | return { 30 | injectReducer: injectReducerFactory(store) 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/util/sagaInjectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Saga注入器 3 | */ 4 | 5 | import isEmpty from "lodash/isEmpty"; 6 | import isFunction from "lodash/isFunction"; 7 | import isString from "lodash/isString"; 8 | import invariant from "invariant"; 9 | import conformsTo from "lodash/conformsTo"; 10 | 11 | import { SagaIterator } from 'redux-saga' 12 | 13 | 14 | import { ENUM_ALLOW_MODES } from "./constants"; 15 | 16 | interface ISagaDescriptor { 17 | saga: SagaIterator, 18 | mode?: string 19 | } 20 | 21 | type InjectSagaFunc = (key: string, sagaDescriptor:ISagaDescriptor, args) => void 22 | type EjectSagaFunc = (key: string) => void 23 | 24 | /** 25 | * 工厂-生成注入Saga函数 26 | * 27 | * @export 28 | * @param {IStore} store 29 | * @returns 30 | */ 31 | export function injectSagaFactory(store: IStore): InjectSagaFunc { 32 | return function injectSaga(key: string, descriptor: ISagaDescriptor, args: PlainObject) { 33 | 34 | const newDescriptor = { 35 | ...descriptor, 36 | mode: descriptor.mode || ENUM_ALLOW_MODES.RESTART_ON_REMOUNT 37 | }; 38 | const { saga, mode } = newDescriptor; 39 | 40 | 41 | let hasSaga = Reflect.has(store.injectedSagas, key); 42 | 43 | // webpack配置注入‘process.env.NODE_ENV’变量 44 | if (process.env.NODE_ENV !== "production") { 45 | const oldDescriptor = store.injectedSagas[key]; 46 | // enable hot reloading of daemon and once-till-unmount sagas 47 | if (hasSaga && oldDescriptor.saga !== saga) { 48 | oldDescriptor.task.cancel(); 49 | hasSaga = false; 50 | } 51 | } 52 | 53 | if ( 54 | !hasSaga || 55 | (hasSaga && mode !== ENUM_ALLOW_MODES.DAEMON && mode !== ENUM_ALLOW_MODES.ONCE_TILL_UNMOUNT) 56 | ) { 57 | store.injectedSagas[key] = { 58 | ...newDescriptor, 59 | task: store.runSaga(saga, args) 60 | }; 61 | } 62 | }; 63 | } 64 | 65 | /** 66 | * 工厂-生成ejectSaga函数 67 | * 68 | * @export 69 | * @param {IStore} store 70 | * @returns 71 | */ 72 | export function ejectSagaFactory(store: IStore): EjectSagaFunc { 73 | return function ejectSaga(key) { 74 | 75 | if (Reflect.has(store.injectedSagas, key)) { 76 | const descriptor = store.injectedSagas[key]; 77 | if (descriptor.mode !== ENUM_ALLOW_MODES.DAEMON) { 78 | descriptor.task.cancel(); 79 | // Clean up in production; in development we need `descriptor.saga` for hot reloading 80 | if (process.env.NODE_ENV === "production") { 81 | // Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga` 82 | store.injectedSagas[key] = null; 83 | } 84 | } 85 | } 86 | }; 87 | } 88 | 89 | export default function getInjectors(store: IStore) { 90 | return { 91 | injectSaga: injectSagaFactory(store), 92 | ejectSaga: ejectSagaFactory(store) 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /test/mocks/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YDJ-FE/steamer-react-ts/cb18e32841f2000e218a74476e29c3faebff1306/test/mocks/.gitkeep -------------------------------------------------------------------------------- /test/spec/simple.spec.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, configure } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | import React from 'react' 4 | 5 | configure({ adapter: new Adapter() }) 6 | 7 | test('simple test', () => { 8 | expect(shallow(
).type()).toBe('div') 9 | }) 10 | -------------------------------------------------------------------------------- /tools/feature/feature.js: -------------------------------------------------------------------------------- 1 | // // used for install dependencies and files to support certain kinds of features 2 | 3 | // let path = require('path'); 4 | // let project = require('../../config/project'); 5 | // let pkgJson = require('../../package.json'); 6 | // let merge = require('lodash').merge; 7 | // let spawnSync = require('child_process').spawnSync; 8 | // let utils = require('steamer-webpack-utils'); 9 | 10 | // let dependency = { 11 | // template: { 12 | // html: { 13 | // 'html-loader': '^0.5.5' 14 | // }, 15 | // handlebars: { 16 | // 'handlebars-loader': '^1.7.0', 17 | // 'handlebars': '^4.0.11' 18 | // }, 19 | // pug: { 20 | // 'pug-loader': '^2.4.0', 21 | // 'pug': '^2.0.3' 22 | // }, 23 | // ejs: { 24 | // 'ejs-loader': '^1.1.0', 25 | // 'ejs': '^2.5.9' 26 | // }, 27 | // art: { 28 | // 'art-template': '^4.12.2', 29 | // 'art-template-loader': '^1.4.3' 30 | // } 31 | // }, 32 | // style: { 33 | // css: { 34 | // 'style-loader': '^0.21.0', 35 | // 'css-loader': '^0.28.11' 36 | // }, 37 | // less: { 38 | // 'style-loader': '^0.21.0', 39 | // 'css-loader': '^0.28.11', 40 | // 'less': '^3.0.1', 41 | // 'less-loader': '^4.1.0' 42 | // }, 43 | // sass: { 44 | // 'style-loader': '^0.21.0', 45 | // 'css-loader': '^0.28.11', 46 | // 'node-sass': '^4.8.3', 47 | // 'sass-loader': '^7.0.1' 48 | // }, 49 | // scss: { 50 | // 'style-loader': '^0.21.0', 51 | // 'css-loader': '^0.28.11', 52 | // 'node-sass': '^4.8.3', 53 | // 'sass-loader': '^7.0.1' 54 | // }, 55 | // stylus: { 56 | // 'style-loader': '^0.21.0', 57 | // 'css-loader': '^0.28.11', 58 | // 'stylus': '^0.54.5', 59 | // 'stylus-loader': '^3.0.2' 60 | // } 61 | // }, 62 | // js: { 63 | // ts: { 64 | // 'awesome-typescript-loader': '^5.0.0', 65 | // 'typescript': '^2.8.3', 66 | // '@types/react': '^15.6.15', 67 | // '@types/react-dom': '^15.5.7' 68 | // } 69 | // } 70 | // }; 71 | 72 | // let files = { 73 | // template: {}, 74 | // style: {}, 75 | // js: { 76 | // ts: [ 77 | // { 78 | // src: path.join(__dirname, './tsconfig.json'), 79 | // dist: path.resolve('tsconfig.json') 80 | // } 81 | // ] 82 | // } 83 | // }; 84 | 85 | // module.exports = { 86 | // installDependency: function() { 87 | // console.log(); 88 | 89 | // let dependencies = merge({}, pkgJson.dependencies, pkgJson.devDependencies); 90 | 91 | // let installDep = {}; 92 | // let installFile = { 93 | // template: {}, 94 | // style: {}, 95 | // js: {} 96 | // }; 97 | // let cmd = ''; 98 | 99 | // project.webpack.template.forEach((item1) => { 100 | // let dep = dependency['template'][item1] || {}; 101 | 102 | // Object.keys(dep).forEach((item2) => { 103 | // if (!dependencies[item2]) { 104 | // installDep[item2] = dependency['template'][item1][item2]; 105 | // installFile.template[item1] = true; 106 | // } 107 | // }); 108 | // }); 109 | 110 | // project.webpack.style.forEach((item1) => { 111 | // let dep = dependency['style'][item1] || {}; 112 | 113 | // Object.keys(dep).forEach((item2) => { 114 | // if (!dependencies[item2]) { 115 | // installDep[item2] = dependency['style'][item1][item2]; 116 | // installFile.style[item1] = true; 117 | // } 118 | // }); 119 | // }); 120 | 121 | // project.webpack.js.forEach((item1) => { 122 | // let dep = dependency['js'][item1] || {}; 123 | 124 | // Object.keys(dep).forEach((item2) => { 125 | // if (!dependencies[item2]) { 126 | // installDep[item2] = dependency['js'][item1][item2]; 127 | // installFile.js[item1] = true; 128 | // } 129 | // }); 130 | // }); 131 | 132 | // Object.keys(installDep).forEach((item) => { 133 | // cmd += (item + '@' + installDep[item] + ' '); 134 | // }); 135 | 136 | // if (cmd) { 137 | // utils.info('Start installing missing dependencies. Please wait......'); 138 | // this.copyFile(installFile); 139 | // spawnSync(project.npm, ['install', '--save-dev', cmd], { stdio: 'inherit', shell: true }); 140 | // utils.info('Dependencies installation complete. Please run your command again.'); 141 | // return true; 142 | // } 143 | // else { 144 | // return false; 145 | // } 146 | // }, 147 | // copyFile: function(installFile) { 148 | // Object.keys(installFile.template).forEach((item1) => { 149 | // let fileArr = files.template[item1] || []; 150 | // fileArr.forEach((item2) => { 151 | // utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 152 | // utils.fs.copySync(item2.src, item2.dist); 153 | // }); 154 | // }); 155 | 156 | // Object.keys(installFile.style).forEach((item1) => { 157 | // let fileArr = files.style[item1] || []; 158 | // fileArr.forEach((item2) => { 159 | // utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 160 | // utils.fs.copySync(item2.src, item2.dist); 161 | // }); 162 | // }); 163 | // Object.keys(installFile.js).forEach((item1) => { 164 | // let fileArr = files.js[item1] || []; 165 | // fileArr.forEach((item2) => { 166 | // utils.info('file ' + item2.src + ' is copyied to ' + item2.dist); 167 | // utils.fs.copySync(item2.src, item2.dist); 168 | // }); 169 | // }); 170 | // } 171 | // }; 172 | -------------------------------------------------------------------------------- /tools/optimization/index.js: -------------------------------------------------------------------------------- 1 | let UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 2 | let OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 3 | 4 | module.exports = function(config, webpack) { 5 | let configWebpack = config.webpack; 6 | let optimization = { 7 | minimizer: [] 8 | }; 9 | 10 | if (configWebpack.compress) { 11 | optimization.minimizer.push(new UglifyJsPlugin({ 12 | cache: true, 13 | parallel: true, 14 | sourceMap: true // set to true if you want JS source maps 15 | })); 16 | optimization.minimizer.push( 17 | new OptimizeCSSAssetsPlugin({ 18 | cssProcessor: require('cssnano'), 19 | cssProcessorOptions: { 20 | reduceIdents: false, 21 | autoprefixer: false, 22 | }, 23 | }) 24 | ); 25 | } 26 | 27 | return optimization; 28 | }; 29 | -------------------------------------------------------------------------------- /tools/plugins/basePlugins.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | let ManifestPlugin = require('webpack-manifest-plugin'); 4 | 5 | module.exports = function(config, webpack) { 6 | 7 | let configWebpack = config.webpack; 8 | let isProduction = config.env === 'production'; 9 | 10 | let plugins = [ 11 | new webpack.DefinePlugin(configWebpack.injectVar) 12 | ]; 13 | 14 | if (isProduction) { 15 | if (configWebpack.manifest) { 16 | plugins.push(new ManifestPlugin()); 17 | } 18 | } 19 | else { 20 | plugins.push(new webpack.NoEmitOnErrorsPlugin()); 21 | plugins.push(new webpack.HotModuleReplacementPlugin()); 22 | } 23 | 24 | return plugins; 25 | }; 26 | -------------------------------------------------------------------------------- /tools/plugins/cachePlugins.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config, webpack) { 2 | 3 | let isProduction = config.env === 'production'; 4 | let plugins = []; 5 | 6 | if (isProduction) { 7 | plugins = [ 8 | new webpack.HashedModuleIdsPlugin({ 9 | hashFunction: 'sha256', 10 | hashDigest: 'hex', 11 | hashDigestLength: 10 12 | }) 13 | ]; 14 | } 15 | 16 | return plugins; 17 | }; 18 | -------------------------------------------------------------------------------- /tools/plugins/resourcePlugins.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let Clean = require('clean-webpack-plugin'); 4 | let CopyWebpackPlugin = require('copy-webpack-plugin-hash'); 5 | let WriteFilePlugin = require('write-file-webpack-plugin'); 6 | let FileWebpackPlugin = require('file-webpack-plugin'); 7 | let HtmlResWebpackPlugin = require('html-res-webpack-plugin'); 8 | let MiniCssExtractPlugin = require('mini-css-extract-plugin'); 9 | 10 | module.exports = function(config, webpack) { 11 | 12 | let configWebpack = config.webpack; 13 | let isProduction = config.env === 'production'; 14 | 15 | let plugins = [ 16 | new MiniCssExtractPlugin({ 17 | filename: `css/${config.webpack.contenthashName}.css`, 18 | chunkFilename: 'css/[name]-[id]-[hash].css' 19 | }), 20 | 21 | ]; 22 | 23 | if (isProduction) { 24 | let useCdn = configWebpack.useCdn || true; 25 | 26 | if (useCdn) { 27 | plugins.push(new FileWebpackPlugin({ 28 | 'after-emit': [ 29 | { 30 | from: path.join(configWebpack.path.dist, '**/*'), 31 | to: path.join(configWebpack.path.dist, configWebpack.path.distCdn), 32 | action: 'move', 33 | options: { 34 | cwd: configWebpack.path.dist, 35 | absolute: true, 36 | ignore: [ 37 | '*.html', 38 | '**/*.ico', 39 | '**/*.html' 40 | ] 41 | } 42 | }, 43 | { 44 | from: path.join(configWebpack.path.dist, '*.ico'), 45 | to: path.join(configWebpack.path.dist, configWebpack.path.distWebserver), 46 | action: 'move', 47 | options: { 48 | cwd: configWebpack.path.dist, 49 | absolute: true, 50 | } 51 | }, 52 | { 53 | from: path.join(configWebpack.path.dist, '*.html'), 54 | to: path.join(configWebpack.path.dist, configWebpack.path.distWebserver), 55 | action: 'move', 56 | options: { 57 | cwd: configWebpack.path.dist, 58 | absolute: true, 59 | } 60 | } 61 | ] 62 | })); 63 | } 64 | } 65 | else { 66 | if (configWebpack.showSource) { 67 | plugins.push(new WriteFilePlugin()); 68 | } 69 | } 70 | 71 | if (configWebpack.clean) { 72 | plugins.push(new Clean([isProduction ? configWebpack.path.dist : configWebpack.path.dev], { root: path.resolve() })); 73 | } 74 | 75 | if (config.webpack.promise) { 76 | plugins.push(new webpack.ProvidePlugin({ 77 | Promise: 'imports-loader?this=>global!exports-loader?global.Promise!es6-promise' 78 | })); 79 | } 80 | 81 | configWebpack.static.forEach((item) => { 82 | plugins.push(new CopyWebpackPlugin([{ 83 | from: item.src, 84 | to: (item.dist || item.src) + (item.hash ? '[path]' + configWebpack.hashName : '[path][name]') + '.[ext]', 85 | }])); 86 | }); 87 | 88 | config.webpack.html.forEach(function(page, key) { 89 | plugins.push(new HtmlResWebpackPlugin({ 90 | // removeUnMatchedAssets: true, 91 | env: isProduction ? 'production' : 'development', 92 | mode: 'html', 93 | filename: page.key + '.html', 94 | template: page.path, 95 | // favicon: 'src/assets/favicon.ico', 96 | htmlMinify: null, 97 | entryLog: !key, 98 | cssPublicPath: isProduction ? config.webpack.cssCdn : config.webpack.webserver, 99 | templateContent: function(tpl) { 100 | return tpl; 101 | } 102 | })); 103 | }); 104 | 105 | return plugins; 106 | }; 107 | -------------------------------------------------------------------------------- /tools/plugins/spritePlugins.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | let SpritesmithPlugin = require('webpack-spritesmith'); 4 | 5 | module.exports = function(config, webpack) { 6 | 7 | let configWebpack = config.webpack; 8 | 9 | let plugins = []; 10 | 11 | configWebpack.sprites = (configWebpack.spriteMode === 'none') ? [] : configWebpack.sprites; 12 | 13 | configWebpack.sprites.forEach(function(sprites) { 14 | let style = configWebpack.spriteStyle, 15 | extMap = { 16 | css: 'css', 17 | stylus: 'styl', 18 | less: 'less', 19 | sass: 'sass', 20 | scss: 'scss' 21 | }, 22 | spriteMode = (~sprites.key.indexOf('_retina')) ? 'retinaonly' : configWebpack.spriteMode, 23 | retinaTplMap = { 24 | retinaonly: '_retinaonly', 25 | 'normal': '', 26 | 'retina': '_retina' 27 | }, 28 | retinaTpl = retinaTplMap[spriteMode] || ''; 29 | 30 | let spritesConfig = { 31 | src: { 32 | cwd: sprites.path, 33 | glob: '*.png' 34 | }, 35 | target: { 36 | image: path.join(configWebpack.path.src, 'css/sprites/' + sprites.key + '.png'), 37 | css: [ 38 | [ 39 | path.join(configWebpack.path.src, 'css/sprites/' + sprites.key + '.' + extMap[style]), 40 | { 41 | format: `${sprites.key}${retinaTpl}` 42 | } 43 | ], 44 | [ 45 | path.join(configWebpack.path.src, 'css/sprites/' + sprites.key + '.full.css'), 46 | { 47 | format: `${sprites.key}${retinaTpl}-full` 48 | } 49 | ], 50 | ] 51 | }, 52 | spritesmithOptions: { 53 | padding: 2 54 | }, 55 | apiOptions: { 56 | cssImageRef: '~' + sprites.key + '.png', 57 | handlebarsHelpers: { 58 | 'half': function(val) { 59 | return (val.replace('px', '') / 2) + 'px'; 60 | } 61 | } 62 | } 63 | }; 64 | 65 | let templatePath = require.resolve('spritesheet-templates-steamer/lib/templates/' + style + retinaTpl + '.template.handlebars'); 66 | // 引入所有的合图样式模板 67 | let templateFullPath = require.resolve(`spritesheet-templates-steamer/lib/templates/full${retinaTpl}.template.handlebars`); 68 | 69 | spritesConfig.customTemplates = { 70 | [`${sprites.key}${retinaTpl}`]: templatePath, 71 | [`${sprites.key}${retinaTpl}-full`]: templateFullPath 72 | }; 73 | 74 | if (spriteMode === 'retina') { 75 | spritesConfig.retina = '@2x'; 76 | spritesConfig.target.css[0].push({ 77 | format: `${sprites.key}` 78 | }); 79 | } 80 | else { 81 | spritesConfig.target.css[0].push({ 82 | format: `${sprites.key}${retinaTpl}` 83 | }); 84 | } 85 | 86 | plugins.push(new SpritesmithPlugin(spritesConfig)); 87 | }); 88 | 89 | return plugins; 90 | }; 91 | -------------------------------------------------------------------------------- /tools/rules/fileRules.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 3 | let configWebpack = config.webpack; 4 | let isProduction = config.env === 'production'; 5 | 6 | let rules = [ 7 | { 8 | test: /\.ico$/, 9 | loader: 'url-loader', 10 | options: { 11 | name: '[name].[ext]' 12 | } 13 | }, 14 | { 15 | test: /\.(jpe?g|png|gif|svg)$/i, 16 | use: [ 17 | { 18 | loader: 'url-loader', 19 | options: { 20 | publicPath: isProduction ? configWebpack.imgCdn : configWebpack.webserver, 21 | limit: 10, 22 | name: `img/[path]${configWebpack.hashName}.[ext]` 23 | } 24 | } 25 | ] 26 | }, 27 | { 28 | test: /\.(woff|woff2|eot|ttf)\??.*$/, 29 | use: [ 30 | { 31 | loader: 'url-loader', 32 | options: { 33 | limit: 10, 34 | name: `font/[path]${configWebpack.hashName}.[ext]` 35 | } 36 | } 37 | ] 38 | } 39 | ]; 40 | 41 | return rules; 42 | }; 43 | -------------------------------------------------------------------------------- /tools/rules/htmlRules.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | 3 | let configWebpack = config.webpack; 4 | 5 | // 模板loader 6 | const templateRules = { 7 | html: { 8 | test: /\.html$/, 9 | loader: 'html-loader' 10 | }, 11 | pug: { 12 | test: /\.pug$/, 13 | loader: 'pug-loader' 14 | }, 15 | handlebars: { 16 | test: /\.handlebars$/, 17 | loader: 'handlebars-loader' 18 | }, 19 | ejs: { 20 | test: /\.ejs$/, 21 | loader: 'ejs-loader', 22 | query: { 23 | 'htmlmin': true, // or enable here 24 | 'htmlminOptions': { 25 | removeComments: true 26 | } 27 | } 28 | }, 29 | art: { 30 | test: /\.art$/, 31 | loader: 'art-template-loader', 32 | } 33 | }; 34 | 35 | let rules = []; 36 | 37 | configWebpack.template.forEach((tpl) => { 38 | let rule = templateRules[tpl] || ''; 39 | rule && rules.push(rule); 40 | }); 41 | 42 | return rules; 43 | }; 44 | -------------------------------------------------------------------------------- /tools/rules/jsRules.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = function(config) { 4 | 5 | let configWebpack = config.webpack; 6 | 7 | let rules = [ 8 | { 9 | test: /\.(tsx?|js)$/, 10 | loader: 'awesome-typescript-loader', 11 | exclude: /node_modules/, 12 | options: { 13 | configFileName: path.join(__dirname, '../../tsconfig.webpack.json') 14 | } 15 | } 16 | ]; 17 | 18 | return rules; 19 | }; 20 | -------------------------------------------------------------------------------- /tools/rules/styleRules.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const merge = require('lodash').merge; 3 | 4 | let MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | 6 | module.exports = function(config) { 7 | let configWebpack = config.webpack; 8 | let isProduction = config.env === "production"; 9 | 10 | let includePaths = [ 11 | path.resolve("node_modules"), 12 | config.webpack.path.src, 13 | path.join(configWebpack.path.src, "css/sprites") 14 | ]; 15 | 16 | // 样式loader 17 | let commonLoaders = [ 18 | { 19 | loader: "cache-loader", 20 | options: { 21 | // provide a cache directory where cache items should be stored 22 | cacheDirectory: path.resolve(".cache") 23 | } 24 | }, 25 | { 26 | loader: "css-loader", 27 | }, 28 | { 29 | loader: "postcss-loader" 30 | } 31 | ]; 32 | 33 | if (isProduction) { 34 | commonLoaders.splice(0, 0, { loader: MiniCssExtractPlugin.loader }); 35 | } else { 36 | commonLoaders.splice(0, 0, { loader: "style-loader" }); 37 | } 38 | 39 | const withTypingCssModule = loaders => { 40 | let newLoaders = [...loaders]; 41 | if (config.webpack.cssModule) { 42 | // replace css-loader, remove cache-loader 43 | newLoaders.splice(1, 2, { 44 | loader: "typings-for-css-modules-loader", 45 | options: { 46 | localIdentName: "[name]-[local]-[hash:base64:5]", 47 | module: true, 48 | namedExport: true, 49 | camelCase: true, 50 | sourceMap: configWebpack.cssSourceMap, 51 | importLoaders: 2, 52 | sass: true 53 | } 54 | }); 55 | } 56 | return newLoaders; 57 | }; 58 | 59 | const styleRules = { 60 | css: { 61 | test: /\.css$/, 62 | use: commonLoaders, 63 | include: includePaths 64 | }, 65 | less: { 66 | test: /\.less$/, 67 | use: merge([], withTypingCssModule(commonLoaders)).concat([ 68 | { 69 | loader: "less-loader", 70 | options: { 71 | sourceMap: configWebpack.cssSourceMap 72 | // paths: includePaths 73 | } 74 | } 75 | ]), 76 | include: includePaths 77 | }, 78 | stylus: { 79 | test: /\.styl$/, 80 | use: merge([], withTypingCssModule(commonLoaders)).concat([ 81 | { 82 | loader: "stylus-loader" 83 | } 84 | ]), 85 | include: includePaths 86 | }, 87 | sass: { 88 | test: /\.s(a|c)ss$/, 89 | use: merge([], withTypingCssModule(commonLoaders)).concat([ 90 | { 91 | loader: "sass-loader", 92 | options: { 93 | sourceMap: configWebpack.cssSourceMap 94 | } 95 | } 96 | ]), 97 | include: includePaths 98 | } 99 | }; 100 | 101 | let rules = []; 102 | 103 | configWebpack.style.forEach(styleParam => { 104 | let style = styleParam === "scss" ? "sass" : styleParam; 105 | let rule = styleRules[style] || ""; 106 | rule && rules.push(rule); 107 | }); 108 | 109 | // console.log(JSON.stringify(rules, null, 4)) 110 | return rules; 111 | }; 112 | -------------------------------------------------------------------------------- /tools/script.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | let isProduction = process.env.NODE_ENV === 'production'; 4 | 5 | // const feature = require('./feature/feature'); 6 | 7 | // if (feature.installDependency()) { 8 | // return; 9 | // } 10 | 11 | if (!isProduction) { 12 | require('./server'); 13 | } 14 | else if (isProduction) { 15 | 16 | compilerRun(require('./webpack.base')); 17 | } 18 | 19 | function compilerRun(config) { 20 | let compiler = webpack(config); 21 | 22 | compiler.run(function(err, stats) { 23 | if (!err) { 24 | console.log(stats.toString({ 25 | assets: true, 26 | cached: false, 27 | colors: true, 28 | children: false, 29 | errors: true, 30 | warnings: true, 31 | version: true, 32 | modules: false, 33 | publicPath: true, 34 | })); 35 | } 36 | else { 37 | console.log(err); 38 | } 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /tools/server.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | const express = require('express'); 3 | const app = express(); 4 | const webpack = require('webpack'); 5 | const webpackDevMiddleware = require('webpack-dev-middleware'); 6 | const webpackHotMiddleware = require('webpack-hot-middleware'); 7 | const proxy = require('http-proxy-middleware'); 8 | 9 | let webpackConfig = require('./webpack.base.js'); 10 | let config = require('../config/project'); 11 | let configWebpack = config.webpack; 12 | let port = configWebpack.port; 13 | let route = Array.isArray(configWebpack.route) ? configWebpack.route : [configWebpack.route]; 14 | let apiPort = configWebpack['api-port']; 15 | let apiRoute = configWebpack['api-route']; 16 | 17 | function addProtocal(urlString) { 18 | if (!!~urlString.indexOf('http:') || !!~urlString.indexOf('https:')) { 19 | return urlString; 20 | } 21 | 22 | return 'http:' + urlString; 23 | } 24 | 25 | let urlObject = url.parse(addProtocal(configWebpack.webserver)); 26 | 27 | for (let key in webpackConfig.entry) { 28 | if (webpackConfig.entry.hasOwnProperty(key)) { 29 | webpackConfig.entry[key].unshift(`webpack-hot-middleware/client?reload=true&dynamicPublicPath=true&path=__webpack_hmr`); 30 | webpackConfig.entry[key].unshift('react-hot-loader/patch'); 31 | } 32 | } 33 | 34 | let compiler = webpack(webpackConfig); 35 | app.use(webpackDevMiddleware(compiler, { 36 | noInfo: true, 37 | stats: { 38 | colors: true 39 | }, 40 | publicPath: configWebpack.webserver 41 | })); 42 | 43 | app.use(webpackHotMiddleware(compiler, { 44 | // 这里和上面的client配合,可以修正 webpack_hmr 的路径为项目路径的子路径,而不是直接成为 host 子路径(从publicPath开始,而不是根开始) 45 | // https://github.com/glenjamin/webpack-hot-middleware/issues/24 46 | path: `${urlObject.path}__webpack_hmr` 47 | })); 48 | 49 | // 静态资源转发 50 | route.forEach((rt) => { 51 | app.use(rt, proxy({ target: `http://127.0.0.1:${port}`, pathRewrite: { [`^${rt}`]: '/' }})); 52 | }); 53 | 54 | // 后台转发 55 | apiRoute.forEach((rt) => { 56 | app.use(rt, proxy({ target: `http://127.0.0.1:${apiPort}` })); 57 | }); 58 | 59 | app.listen(port, function(err) { 60 | if (err) { 61 | console.error(err); 62 | } 63 | else { 64 | console.info('Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port); 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /tools/template/dependency.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 'preact-list': { 3 | // 'preact': '^8.2.7', 4 | // 'preact-redux': '^2.0.3', 5 | // 'react-list-scroll': '^1.0.6', 6 | // 'react-touch-component': '^1.1.4', 7 | // 'react-spin-component': '0.0.1', 8 | // }, 9 | // 'react-simple-mobx': { 10 | // 'mobx': '^3.2.2', 11 | // 'mobx-react': '^4.2.2', 12 | // 'mobx-react-devtools': '^4.2.15' 13 | // }, 14 | // 'react-simple-redux': { 15 | // 'redux': '^4.0.0', 16 | // 'redux-devtools': '^3.4.1', 17 | // 'redux-devtools-dock-monitor': '^1.1.3', 18 | // 'redux-devtools-log-monitor': '^1.4.0', 19 | // 'redux-thunk': '^2.2.0' 20 | // }, 21 | // 'react-spa-mobx': { 22 | // 'react-list-scroll': '^1.0.6', 23 | // 'react-loadable': '^5.3.1', 24 | // 'react-touch-component': '^1.1.4', 25 | // 'react-spin-component': '0.0.1', 26 | // 'react-router': '^4.2.0', 27 | // 'react-router-dom': '^4.2.2', 28 | // 'mobx-react-router': '^4.0.2', 29 | // 'history': '^4.7.2', 30 | // 'mobx-react-devtools': '^4.2.15' 31 | // }, 32 | // 'react-spa-redux': { 33 | // 'react-list-scroll': '^1.0.6', 34 | // 'react-loadable': '^5.3.1', 35 | // 'react-touch-component': '^1.1.4', 36 | // 'react-spin-component': '0.0.1', 37 | // 'react-router': '^4.2.0', 38 | // 'react-router-dom': '^4.2.2', 39 | // 'react-router-redux': '^5.0.0-alpha.9', 40 | // 'history': '^4.7.2', 41 | // 'redux-devtools': '^3.4.1', 42 | // 'redux-devtools-dock-monitor': '^1.1.3', 43 | // 'redux-devtools-log-monitor': '^1.4.0', 44 | // 'redux-thunk': '^2.2.0' 45 | // }, 46 | // 'react-typescript': { 47 | // '@types/react': '^15.0.35', 48 | // '@types/react-dom': '^15.5.1' 49 | // } 50 | // 'list': { 51 | // 'react-list-scroll': '^1.0.6', 52 | // 'react-touch-component': '^1.1.4', 53 | // 'react-spin-component': '0.0.1', 54 | // }, 55 | // 'list-immutable': { 56 | // 'react-list-scroll': '^1.0.6', 57 | // 'react-touch-component': '^1.1.4', 58 | // 'immutable': '^3.8.2', 59 | // 'pure-render-immutable-decorator': '^1.0.3', 60 | // 'redux-immutable': '^4.0.0', 61 | // }, 62 | }; 63 | -------------------------------------------------------------------------------- /tools/template/mobxViewTemplate/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { observer } from 'mobx-react' 3 | 4 | import * as styles from "./style.scss"; 5 | 6 | interface IP { 7 | demoProps: any 8 | } 9 | 10 | @observer 11 | class <% Title %> extends React.Component { 12 | render() { 13 | return
14 |

Hello World!

15 |
; 16 | } 17 | } 18 | 19 | export default <% Title %>; 20 | -------------------------------------------------------------------------------- /tools/template/mobxViewTemplate/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .pageContainer { 4 | // .. 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/actions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * <% Title %> Actions 3 | * 4 | * * To add a new Action: 5 | * 1) Import your constant 6 | * 2) Add a function like this: 7 | * export function yourAction(var) { 8 | * return { type: YOUR_ACTION_CONSTANT, var: var } 9 | * } 10 | */ 11 | 12 | import { 13 | DEMO_TYPE 14 | } from "./constants"; 15 | 16 | export function demoAction() { 17 | return { 18 | type: DEMO_TYPE 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * <% Title %> Constants 3 | * 4 | * 每个action都有相应的type, 好让reducer知道如何处理 5 | * 避免在reducer和action之间使用了错别字, 我们都从这里获取相同的constants 6 | * 我们推荐加前缀比如 ‘yourproject/yourComponent’避免reducer搞错action,类似命名空间 7 | * 8 | * Follow this format: 9 | * export const YOUR_ACTION_CONSTANT = 'pageName/containerName/YOUR_ACTION_CONSTANT'; 10 | */ 11 | 12 | const PAGENAME = 'finanace/<% Title %>' 13 | 14 | export const DEMO_TYPE = `${PAGENAME}/DEMO_TYPE` 15 | 16 | 17 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { compose } from "redux"; 3 | import { connect } from "react-redux"; 4 | import injectReducer from "components/ReducerInjector"; 5 | import injectSaga from "components/SagaInjector"; 6 | import { createStructuredSelector } from "reselect"; 7 | 8 | import reducer from "./reducer"; 9 | import saga from "./saga"; 10 | import * as selectors from "./selectors"; 11 | import * as actions from "./actions"; 12 | 13 | import * as styles from "./style.scss"; 14 | 15 | interface IP { 16 | demoProps: any 17 | } 18 | 19 | class <% Title %> extends React.PureComponent { 20 | render() { 21 | return
22 |

Hello World!

23 |
; 24 | } 25 | } 26 | 27 | const mapDispatchToProps = dispatch => ({ 28 | }); 29 | 30 | const mapStateToProps = createStructuredSelector({ 31 | // TODO: add Selector 32 | }); 33 | 34 | const withReducer = injectReducer({ key: "<% title %>", reducer }); 35 | const withSaga = injectSaga({ key: "<% title %>", saga }); 36 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 37 | 38 | export default compose(withReducer, withSaga, withConnect)(<% Title %>); 39 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * <% Title %> Reducer 3 | * 4 | * The reducer takes care of our data. Using actions, we can change our 5 | * application state. 6 | * To add a new action, add it to the switch statement in the reducer function 7 | * 8 | * Example 9 | * case YOUR_ACTION_CONSTANT: 10 | * return state.set('yourStateVarivle', yourActionPayload) 11 | */ 12 | 13 | import { fromJS } from "immutable"; 14 | import { AnyAction } from "redux"; 15 | import { DEMO_TYPE } from "./constants"; 16 | 17 | const initialState = fromJS({ 18 | }); 19 | 20 | export default (state = initialState, action: AnyAction) => { 21 | switch (action.type) { 22 | default: 23 | return state; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/saga.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * <% title %> Saga 3 | * 4 | */ 5 | 6 | 7 | import { takeEvery, takeLatest, put, call, select } from "redux-saga/effects"; 8 | 9 | import * as api from "services/api"; 10 | // import { makePageIndexSelector, makePageSizeSelector } from "./selectors"; 11 | 12 | // 入口 13 | export default function* mainSaga() { 14 | // yield takeLatest(QUERY_LIST, queryListSaga); 15 | // yield takeLatest(CHANGE_PAGE_QUERY, changePageQuery); 16 | } 17 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Selector 3 | */ 4 | 5 | import { createSelector } from "reselect"; 6 | 7 | export const <% title %>Selector = state => 8 | state.get("<% title %>"); 9 | -------------------------------------------------------------------------------- /tools/template/viewTemplate/style.scss: -------------------------------------------------------------------------------- 1 | @import "styles/_base"; 2 | 3 | .pageContainer { 4 | // .. 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /tools/webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const webpack = require('webpack'); 4 | const webpackMerge = require('webpack-merge'); 5 | 6 | let config = require('../config/project'); 7 | let configWebpack = config.webpack; 8 | let configWebpackMerge = config.webpackMerge; 9 | let configCustom = config.custom; 10 | let isProduction = process.env.NODE_ENV === 'production'; 11 | 12 | 13 | let baseConfig = { 14 | mode: isProduction ? 'production' : 'development', 15 | context: configWebpack.path.src, 16 | entry: configWebpack.entry, 17 | output: { 18 | publicPath: isProduction ? configWebpack.cdn : configWebpack.webserver, 19 | path: isProduction ? configWebpack.path.dist : configWebpack.path.dev, 20 | filename: `js/${configWebpack.chunkhashName}.js`, 21 | chunkFilename: 'chunk/' + configWebpack.chunkhashName + '.js' 22 | }, 23 | module: { 24 | rules: [] 25 | }, 26 | resolve: { 27 | modules: [ 28 | configWebpack.path.src, 29 | 'node_modules', 30 | path.join(configWebpack.path.src, 'css/sprites') 31 | ], 32 | extensions: [ 33 | '.ts', '.tsx', '.js', '.jsx', '.css', '.scss', 'sass', '.less', '.styl', 34 | '.png', '.jpg', '.jpeg', '.ico', '.ejs', '.pug', '.art', '.handlebars', 'swf', 'svg', 35 | ], 36 | // alias: {} 37 | }, 38 | plugins: [], 39 | optimization: {}, 40 | watch: !isProduction, 41 | devtool: isProduction ? configWebpack.sourceMap.production : configWebpack.sourceMap.development, 42 | performance: { 43 | hints: isProduction ? 'warning' : false, 44 | assetFilter: function(assetFilename) { 45 | return assetFilename.endsWith('.js') || assetFilename.endsWith('.css'); 46 | } 47 | } 48 | }; 49 | 50 | /** *********** 处理脚手架基础rules & plugins *************/ 51 | let rules = fs.readdirSync(path.join(__dirname, 'rules')); 52 | let plugins = fs.readdirSync(path.join(__dirname, 'plugins')); 53 | 54 | let baseConfigRules = []; 55 | let baseConfigPlugins = []; 56 | 57 | rules.forEach((rule) => { 58 | baseConfigRules = baseConfigRules.concat(require(`./rules/${rule}`)(config)); 59 | }); 60 | 61 | plugins.forEach((plugin) => { 62 | baseConfigPlugins = baseConfigPlugins.concat(require(`./plugins/${plugin}`)(config, webpack)); 63 | }); 64 | 65 | baseConfig.module.rules = baseConfigRules; 66 | baseConfig.plugins = baseConfigPlugins; 67 | baseConfig.optimization = require('./optimization')(config, webpack); 68 | 69 | // console.log(rules, plugins); 70 | 71 | /** *********** base 与 user config 合并 *************/ 72 | let userConfig = { 73 | output: configCustom.getOutput(), 74 | module: configCustom.getModule(), 75 | resolve: configCustom.getResolve(), 76 | externals: configCustom.getExternals(), 77 | plugins: configCustom.getPlugins(), 78 | }; 79 | 80 | let otherConfig = configCustom.getOtherOptions(); 81 | 82 | for (let key in otherConfig) { 83 | if (otherConfig.hasOwnProperty(key)) { 84 | userConfig[key] = otherConfig[key]; 85 | } 86 | } 87 | 88 | baseConfig = configWebpackMerge.mergeProcess(baseConfig); 89 | 90 | let webpackConfig = webpackMerge.smartStrategy( 91 | configWebpackMerge.smartStrategyOption 92 | )(baseConfig, userConfig); 93 | 94 | // console.log(JSON.stringify(webpackConfig, null, 4)); 95 | 96 | module.exports = webpackConfig; 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "sourceMap": true, 8 | "baseUrl": "src", 9 | "alwaysStrict": true, 10 | "allowJs": true, 11 | "importHelpers": true, 12 | "experimentalDecorators": true, 13 | "jsx": "react", 14 | "skipLibCheck": true, 15 | "typeRoots": ["node_modules/@types", "./typings"], 16 | "lib": [ 17 | "es5", 18 | "es2015.core", 19 | "es2015.reflect", 20 | "es2015.promise", 21 | "es2015.generator", 22 | "es2016.array.include", 23 | "dom" 24 | ] 25 | }, 26 | 27 | "include": [ 28 | "src/**/*", 29 | "typings" 30 | ], 31 | 32 | "awesomeTypescriptLoaderOptions": { 33 | "reportFiles": ["src/**/*.{ts,tsx}"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.webpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": [ 4 | "src/**/*.d.ts", 5 | "typings" 6 | ], 7 | "files": [ 8 | "src/app.tsx" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "ordered-imports": false, 5 | "object-literal-sort-keys": false, 6 | "member-access": false, 7 | "member-ordering": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /typings/antd.shim.d.ts: -------------------------------------------------------------------------------- 1 | import { Cascader } from "antd"; 2 | 3 | declare module 'antd/lib/Cascader' { 4 | export interface CascaderProps { 5 | autoFocus?: boolean, 6 | onKeyDown?: (evt) => void 7 | } 8 | } 9 | 10 | -------------------------------------------------------------------------------- /typings/echarts.d.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | // export as namespace echarts 4 | 5 | // export interface EChartOption { 6 | // title?: echarts.EChartOption | echarts.EChartOption[] 7 | // } 8 | 9 | /// 10 | type Diff = 11 | ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T]; 12 | type Overwrite = Pick> & U; 13 | 14 | declare namespace echarts { 15 | 16 | interface TitleExtension { 17 | title?: echarts.EChartTitleOption | echarts.EChartTitleOption[] 18 | } 19 | 20 | interface EChartOptionNew extends Overwrite { 21 | axisPointer?: object 22 | } 23 | 24 | interface ECharts { 25 | setOption(option: EChartOption | EChartOptionNew, notMerge?: boolean, notRefreshImmediately?: boolean): void 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /typings/http.d.ts: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios' 2 | 3 | declare global { 4 | interface AxiosRequestConfig extends axios.AxiosRequestConfig { 5 | startTime?: Date 6 | } 7 | 8 | interface IHttpError { 9 | msg: string 10 | type: string 11 | config: AxiosRequestConfig 12 | code?: number 13 | } 14 | 15 | interface IHttpResponse { 16 | code: number 17 | response: PlainObject | string | number 18 | msg: string 19 | } 20 | 21 | interface HttpResquest { 22 | /** 23 | * get请求 24 | * 25 | * @param {string} url 接口地址(不带域名) 26 | * @param {PlainObject} data 接口参数 27 | * @param {HttpRequestOptions} [options] http模块的请求选项 28 | * @returns {Promise} 29 | * @memberof HttpResquest 30 | */ 31 | get?(url: string, data: PlainObject, options?: HttpRequestOptions): Promise 32 | 33 | /** 34 | * post请求 35 | * 36 | * @param {string} url 接口地址(不带域名) 37 | * @param {PlainObject} data 接口参数 38 | * @param {HttpRequestOptions} [options] http模块的请求选项 39 | * @returns {Promise} 40 | * @memberof HttpResquest 41 | */ 42 | post?(url: string, data: PlainObject, options?: HttpRequestOptions): Promise 43 | /** 44 | * delete请求 45 | * 46 | * @param {string} url 接口地址(不带域名) 47 | * @param {PlainObject} data 接口参数 48 | * @param {HttpRequestOptions} [options] http模块的请求选项 49 | * @returns {Promise} 50 | * @memberof HttpResquest 51 | */ 52 | delete?(url: string, data: PlainObject, options?: HttpRequestOptions): Promise 53 | /** 54 | * put请求 55 | * 56 | * @param {string} url 接口地址(不带域名) 57 | * @param {PlainObject} data 接口参数 58 | * @param {HttpRequestOptions} [options] http模块的请求选项 59 | * @returns {Promise} 60 | * @memberof HttpResquest 61 | */ 62 | put?(url: string, data: PlainObject, options?: HttpRequestOptions): Promise 63 | } 64 | 65 | interface HttpRequestOptions { 66 | /** 67 | * 请求接口的path前缀,默认使用环境设置的apiUrlPrefix 68 | * 69 | * @type {string} 70 | * @memberof HttpRequestOptions 71 | */ 72 | baseUrl?: string 73 | /** 74 | * 是否格式化话输出,默认为true, 如果格式化输入,则成功时候只返回response字段 75 | * 76 | * @type {boolean} 77 | * @memberof HttpRequestOptions 78 | */ 79 | formatResponse?: boolean 80 | /** 81 | * 错误回调, 默认不做任何处理 82 | * 83 | * @memberof HttpRequestOptions 84 | */ 85 | onError?: (error: IHttpError) => any 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /typings/index.d.ts: -------------------------------------------------------------------------------- 1 | interface PlainObject { 2 | [key: string]: any; 3 | } 4 | 5 | interface Console { 6 | dev?: (param: any) => any; 7 | } 8 | 9 | declare module "*.png"; 10 | 11 | /** 12 | * 环境设置模块 13 | */ 14 | declare module 'env' { 15 | import env from 'env/dev' 16 | export default env 17 | } 18 | -------------------------------------------------------------------------------- /typings/redux-store.shim.d.ts: -------------------------------------------------------------------------------- 1 | import Redux from "redux"; 2 | import { Task, SagaIterator } from "redux-saga"; 3 | 4 | interface InjectedReducers { 5 | [key: string]: Redux.Reducer; 6 | } 7 | 8 | interface InjectedSagas { 9 | [key: string]: { 10 | mode: string; 11 | saga: SagaIterator; 12 | task: Task 13 | }; 14 | } 15 | 16 | declare global { 17 | // 扩展Store接口 18 | interface IStore extends Redux.Store { 19 | /** 20 | * 运行一个saga 21 | * 22 | * @memberof IStore 23 | */ 24 | runSaga?: any; // TODO: cleanup 25 | asyncReducers?: Redux.ReducersMapObject; 26 | 27 | /** 28 | * 注入的Reducers 29 | * 30 | * @type {InjectedReducers} 31 | * @memberof IStore 32 | */ 33 | injectedReducers?: InjectedReducers; 34 | 35 | /** 36 | * 注入的sagas 37 | * 38 | * @type {InjectedSagas} 39 | * @memberof IStore 40 | */ 41 | injectedSagas?: InjectedSagas; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /typings/redux.shim.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: any; 3 | } 4 | -------------------------------------------------------------------------------- /typings/typed-css-modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: any; 3 | export = content; 4 | } --------------------------------------------------------------------------------