基于create-react-app官方脚手架搭建dva模式的项目(一)

思索良久,决定还是记录下心得体会:一个基于create-react-app官方脚手架,搭建起来的dva开发模式的react项目。

当然现今的前端市场如此强大,你可以在网络上找到你想要的任何脚手架,并且很多可以开箱即用,不可否认它们很优秀,开发它们的人或团队更值得我们竖起大拇指,为他们点赞!比较适合国人还是阿里系的一套react开发脚手架,dva-cliantdUmi等,当然gitHub上也有诸多优秀的脚手架,有兴趣的同学可以自行查阅学习。

这里,仅仅以react官方脚手架开启项目,安装采用dva方式的,构建项目,展示记录过程中遇到的各种点和Keng,若你也遇到类似问题,也许能帮上你。

1 安装create-react-app(鉴于国内网络以下使用cnpm,具体配置网上可找大量资料)

[javascript]  view plain  copy
  1. cnpm i create-react-app -g  

2 创建项目,目录名project

[javascript]  view plain  copy
  1. create-react-app project  

等待命令执行完成,教授叫会为你安装好基础的组件包,目录生成完毕如下:


这就是脚手架标准目录,我们再来看下package.json文件如下:

[javascript]  view plain  copy
  1. {  
  2.   "name""project",  
  3.   "version""0.1.0",  
  4.   "private"true,  
  5.   "dependencies": {  
  6.     "react""^16.4.0",  
  7.     "react-dom""^16.4.0",  
  8.     "react-scripts""1.1.4"  
  9.   },  
  10.   "scripts": {  
  11.     "start""react-scripts start",  
  12.     "build""react-scripts build",  
  13.     "test""react-scripts test --env=jsdom",  
  14.     "eject""react-scripts eject"  
  15.   }  
  16. }  

如上所示,很干净的目录。

此时已可运行npm start运行起来项目了,默认端口3000,效果如图:


注意scripts执行命令中有一个eject,意为弹射暴露出所有配置,其实脚手架还是为我们封装了一些东西的,这里我们就暴露出所有配置吧。

运行npm run eject,一旦选择eject,那么所封装的组件依赖和项目结构会有所变化,如图:


我们打开package.json文件如下所示,是暴露出的依赖配置项,可以看到已经为我们安装好webpack,babel,eslint等:

[javascript]  view plain  copy
  1. {  
  2.   "name""project",  
  3.   "version""0.1.0",  
  4.   "private"true,  
  5.   "dependencies": {  
  6.     "autoprefixer""7.1.6",  
  7.     "babel-core""6.26.0",  
  8.     "babel-eslint""7.2.3",  
  9.     "babel-jest""20.0.3",  
  10.     "babel-loader""7.1.2",  
  11.     "babel-preset-react-app""^3.1.1",  
  12.     "babel-runtime""6.26.0",  
  13.     "case-sensitive-paths-webpack-plugin""2.1.1",  
  14.     "chalk""1.1.3",  
  15.     "css-loader""0.28.7",  
  16.     "dotenv""4.0.0",  
  17.     "dotenv-expand""4.2.0",  
  18.     "eslint""4.10.0",  
  19.     "eslint-config-react-app""^2.1.0",  
  20.     "eslint-loader""1.9.0",  
  21.     "eslint-plugin-flowtype""2.39.1",  
  22.     "eslint-plugin-import""2.8.0",  
  23.     "eslint-plugin-jsx-a11y""5.1.1",  
  24.     "eslint-plugin-react""7.4.0",  
  25.     "extract-text-webpack-plugin""3.0.2",  
  26.     "file-loader""1.1.5",  
  27.     "fs-extra""3.0.1",  
  28.     "html-webpack-plugin""2.29.0",  
  29.     "jest""20.0.4",  
  30.     "object-assign""4.1.1",  
  31.     "postcss-flexbugs-fixes""3.2.0",  
  32.     "postcss-loader""2.0.8",  
  33.     "promise""8.0.1",  
  34.     "raf""3.4.0",  
  35.     "react""^16.4.0",  
  36.     "react-dev-utils""^5.0.1",  
  37.     "react-dom""^16.4.0",  
  38.     "resolve""1.6.0",  
  39.     "style-loader""0.19.0",  
  40.     "sw-precache-webpack-plugin""0.11.4",  
  41.     "url-loader""0.6.2",  
  42.     "webpack""3.8.1",  
  43.     "webpack-dev-server""2.9.4",  
  44.     "webpack-manifest-plugin""1.3.2",  
  45.     "whatwg-fetch""2.0.3"  
  46.   },  
  47.   "scripts": {  
  48.     "start""node scripts/start.js",  
  49.     "build""node scripts/build.js",  
  50.     "test""node scripts/test.js --env=jsdom"  
  51.   },  
  52.   "jest": {  
  53.     "collectCoverageFrom": [  
  54.       "src/**/*.{js,jsx,mjs}"  
  55.     ],  
  56.     "setupFiles": [  
  57.       "<rootDir>/config/polyfills.js"  
  58.     ],  
  59.     "testMatch": [  
  60.       "<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}",  
  61.       "<rootDir>/src/**/?(*.)(spec|test).{js,jsx,mjs}"  
  62.     ],  
  63.     "testEnvironment""node",  
  64.     "testURL""http://localhost",  
  65.     "transform": {  
  66.       "^.+\\.(js|jsx|mjs)$""<rootDir>/node_modules/babel-jest",  
  67.       "^.+\\.css$""<rootDir>/config/jest/cssTransform.js",  
  68.       "^(?!.*\\.(js|jsx|mjs|css|json)$)""<rootDir>/config/jest/fileTransform.js"  
  69.     },  
  70.     "transformIgnorePatterns": [  
  71.       "[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$"  
  72.     ],  
  73.     "moduleNameMapper": {  
  74.       "^react-native$""react-native-web"  
  75.     },  
  76.     "moduleFileExtensions": [  
  77.       "web.js",  
  78.       "js",  
  79.       "json",  
  80.       "web.jsx",  
  81.       "jsx",  
  82.       "node",  
  83.       "mjs"  
  84.     ]  
  85.   },  
  86.   "babel": {  
  87.     "presets": [  
  88.       "react-app"  
  89.     ]  
  90.   },  
  91.   "eslintConfig": {  
  92.     "extends""react-app"  
  93.   }  
  94. }  

此处我本地更改下3000端口为9999,避免与其他本地项目冲突,安装cross-env:

[javascript]  view plain  copy
  1. cnpm i cross-env  

修改start命令为:

[javascript]  view plain  copy
  1. "start""node scripts/start.js",  
  2. "start""cross-env PORT=9999 node scripts/start.js",  

然后启动:npm start 效果如图:


3 安装dva库(dva也有自己的脚手架dva-cli,也可快速构建项目,目前已升至2.x版本,采用[email protected]路由版本)

[javascript]  view plain  copy
  1. cnpm i dva --save  

安装history组件,等会后面会用到,主要做BrowserHistory浏览器历史功能,有兴趣的同学可网上自行学习。

4 改造项目为dva模式(不了解dva的同学可网上自行学习),在src目录下新建目录:models,services,routes,utils(utils将来存放配置文件和工具方法),如图:


5 路由设计,简单点暂设计为三个demo地址,分别如下:

(1)http://localhost:9999/aaa

(2)http://localhost:9999/aaa/bbb

(3)http://localhost:9999/ccc

6 组件设计,在routes目录下新建三个文件:AAA.js BBB.js CCC.js 如图:


代码分别为:

AAA.js

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2.   
  3. class AAA extends Component {  
  4.   render() {  
  5.     return (  
  6.       <div>  
  7.         <p>  
  8.           AAA页  
  9.         </p>  
  10.       </div>  
  11.     );  
  12.   }  
  13. }  
  14.   
  15. export default AAA;  

BBB.js

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2.   
  3. class BBB extends Component {  
  4.   render() {  
  5.     return (  
  6.       <div>  
  7.         <p>  
  8.           BBB页  
  9.         </p>  
  10.       </div>  
  11.     );  
  12.   }  
  13. }  
  14.   
  15. export default BBB;  

CCC.js

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2.   
  3. class CCC extends Component {  
  4.   render() {  
  5.     return (  
  6.       <div>  
  7.         <p>  
  8.           CCC页  
  9.         </p>  
  10.       </div>  
  11.     );  
  12.   }  
  13. }  
  14.   
  15. export default CCC;  

7 model设计,在model下新建四个文件:aaa.js bbb.js ccc.js 和app.js(app作为全局model使用,将来存放全局变量,如国际化参数,登录用户信息等)

代码分别为:

aaa.js

[javascript]  view plain  copy
  1. export default {  
  2.   
  3.     namespace: 'aaa',  
  4.     
  5.     state: {  
  6.       name:'这是aaa的model'  
  7.     },  
  8.     
  9.     subscriptions: {  
  10.         
  11.     },  
  12.     
  13.     effects: {  
  14.         
  15.     },  
  16.     
  17.     reducers: {  
  18.         
  19.     },  
  20.     
  21.   };  
  22.     

bbb.js

[javascript]  view plain  copy
  1. export default {  
  2.   
  3.     namespace: 'bbb',  
  4.     
  5.     state: {  
  6.       name:'这是bbb的model'  
  7.     },  
  8.     
  9.     subscriptions: {  
  10.         
  11.     },  
  12.     
  13.     effects: {  
  14.         
  15.     },  
  16.     
  17.     reducers: {  
  18.         
  19.     },  
  20.     
  21.   };  
  22.     

ccc.js

[javascript]  view plain  copy
  1. export default {  
  2.   
  3.     namespace: 'ccc',  
  4.     
  5.     state: {  
  6.       name:'这是ccc的model'  
  7.     },  
  8.     
  9.     subscriptions: {  
  10.         
  11.     },  
  12.     
  13.     effects: {  
  14.         
  15.     },  
  16.     
  17.     reducers: {  
  18.         
  19.     },  
  20.     
  21.   };  
  22.     

app.js

[javascript]  view plain  copy
  1. export default {  
  2.   
  3.     namespace: 'app',  
  4.     
  5.     state: {  
  6.       name:'这是app的model'  
  7.     },  
  8.     
  9.     subscriptions: {  
  10.         
  11.     },  
  12.     
  13.     effects: {  
  14.         
  15.     },  
  16.     
  17.     reducers: {  
  18.         
  19.     },  
  20.     
  21.   };  
  22.     

8 src目录下创建router.js路由控制文件,其中menuGlobal后期会提出到配置文件中,此处暂先这样放便于查看,大家发现其中也定义了id,pid,name,icon等字段,这些并不是dva路由必须字段,没错这些事自定义的字段,将来会用到,菜单关系,菜单是否显示,和icon图标等

[javascript]  view plain  copy
  1. import React from 'react';  
  2. import { Router, Route, Switch } from 'dva/router';  
  3. import dynamic from 'dva/dynamic'  
  4.   
  5. const menuGlobal=[  
  6.   {  
  7.       id:'aaa',  
  8.       pid:'0',  
  9.       name:'aaa页',  
  10.       icon:'user',  
  11.       path: '/aaa',  
  12.       models: () => [import('./models/aaa')], //models可多个  
  13.       component: () => import('./routes/AAA'),  
  14.   },   
  15.   {  
  16.       id:'bbb',  
  17.       pid:'0',  
  18.       name:'bbb页',  
  19.       icon:'user',  
  20.       path: '/aaa/bbb',  
  21.       models: () => [import('./models/bbb')], //models可多个  
  22.       component: () => import('./routes/BBB'),  
  23.   },   
  24.   {  
  25.       id:'ccc',  
  26.       pid:'0',  
  27.       name:'ccc页',  
  28.       icon:'user',  
  29.       path: '/ccc',  
  30.       models: () => [import('./models/ccc')], //models可多个  
  31.       component: () => import('./routes/CCC'),  
  32.   },   
  33. ];  
  34.   
  35. function RouterConfig({ history, app }) {  
  36.   
  37.   return (  
  38.     <Router history={history}>  
  39.       <Switch>  
  40.         {  
  41.           menuGlobal.map(({path,...dynamics},index)=>(  
  42.             <Route  
  43.               key={index}   
  44.               path={path}   
  45.               exact   
  46.               component={dynamic({  
  47.                 app,  
  48.                 ...dynamics  
  49.               })}   
  50.             />  
  51.           ))  
  52.         }  
  53.       </Switch>  
  54.     </Router>  
  55.   );  
  56. }  
  57.   
  58. export default RouterConfig;  

9 修改src目录下index.js入口文件:

[javascript]  view plain  copy
  1. import dva from 'dva';  
  2. import './index.css';  
  3. import createHistory from 'history/createBrowserHistory'  
  4.   
  5. // 1. Initialize  
  6. const app = dva({  
  7.     history:createHistory()  
  8. });  
  9.   
  10. // 2. Plugins  
  11. // app.use({});  
  12.   
  13. // 3. Model  
  14. app.model(require('./models/app').default);  
  15.   
  16. // 4. Router  
  17. app.router(require('./router').default);  
  18.   
  19. // 5. Start  
  20. app.start('#root');  


至此,dva的改造基本完毕,后续还有其他的细节补充,运行起来吧:npm start

效果如图:




试试看,基本的路由已经OK了,接下来,我们将进行对以上主体构造的细节坐下补充。

此处路由只是通过浏览器地址栏直接访问的形式,我们还需要增加路由跳转<Link>

(1)AAA.js中引入import { Link } from 'dva/router'; 并增加跳转链接如下:

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2. import { Link } from 'dva/router';  
  3.   
  4. class AAA extends Component {  
  5.   render() {  
  6.     return (  
  7.       <div>  
  8.         <p>  
  9.           AAA页  
  10.         </p>  
  11.         <Link to={'/aaa/bbb'}>去BBB页面</Link>  
  12.         <br />  
  13.         <Link to={'/ccc'}>去CCC页面</Link>  
  14.       </div>  
  15.     );  
  16.   }  
  17. }  
  18.   
  19. export default AAA;  

(1)BBB.js中引入import { Link } from 'dva/router'; 并增加跳转链接如下:

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2. import { Link } from 'dva/router';  
  3.   
  4. class BBB extends Component {  
  5.   render() {  
  6.     return (  
  7.       <div>  
  8.         <p>  
  9.           BBB页  
  10.         </p>  
  11.         <Link to={'/aaa'}>去AAA页面</Link>  
  12.         <br />  
  13.         <Link to={'/ccc'}>去CCC页面</Link>  
  14.       </div>  
  15.     );  
  16.   }  
  17. }  
  18.   
  19. export default BBB;  

(1)CCC.js中引入import { Link } from 'dva/router'; 并增加跳转链接如下:

[javascript]  view plain  copy
  1. import React, { Component } from 'react';  
  2. import { Link } from 'dva/router';  
  3.   
  4. class CCC extends Component {  
  5.   render() {  
  6.     return (  
  7.       <div>  
  8.         <p>  
  9.           CCC页  
  10.         </p>  
  11.         <Link to={'/aaa'}>去AAA页面</Link>  
  12.         <br />  
  13.         <Link to={'/aaa/bbb'}>去BBB页面</Link>  
  14.       </div>  
  15.     );  
  16.   }  
  17. }  
  18.   
  19. export default CCC;  

此时页面中已有链接,三个页面可相互跳转了。

现在我们将router.js中的menuGlobal提出,放于公用文件中,首先在utils下新建文件config.js和index.js 代码分别如下:

config.js文件:

[javascript]  view plain  copy
  1. const menuGlobal=[  
  2.     {  
  3.         id:'aaa',  
  4.         pid:'0',  
  5.         name:'aaa页',  
  6.         icon:'user',  
  7.         path: '/aaa',  
  8.         models: () => [import('../models/aaa')], //models可多个  
  9.         component: () => import('../routes/AAA'),  
  10.     },   
  11.     {  
  12.         id:'bbb',  
  13.         pid:'0',  
  14.         name:'bbb页',  
  15.         icon:'user',  
  16.         path: '/aaa/bbb',  
  17.         models: () => [import('../models/bbb')], //models可多个  
  18.         component: () => import('../routes/BBB'),  
  19.     },   
  20.     {  
  21.         id:'ccc',  
  22.         pid:'0',  
  23.         name:'ccc页',  
  24.         icon:'user',  
  25.         path: '/ccc',  
  26.         models: () => [import('../models/ccc')], //models可多个  
  27.         component: () => import('../routes/CCC'),  
  28.     },   
  29.   ];  
  30.     
  31. export default {  
  32.     menuGlobal  
  33. }  

index.js文件(之后utils中的配置文件,均可在index.js暴露出去):

[javascript]  view plain  copy
  1. import config from './config';  
  2.   
  3. export {  
  4.     config  
  5. }  

修改src下的router.js文件,如下:

[javascript]  view plain  copy
  1. import React from 'react';  
  2. import { Router, Route, Switch } from 'dva/router';  
  3. import dynamic from 'dva/dynamic'  
  4.   
  5. import {config} from './utils'  
  6. const { menuGlobal } = config  
  7.   
  8. function RouterConfig({ history, app }) {  
  9.   
  10.   return (  
  11.     <Router history={history}>  
  12.       <Switch>  
  13.         {  
  14.           menuGlobal.map(({path,...dynamics},index)=>(  
  15.             <Route  
  16.               key={index}   
  17.               path={path}   
  18.               exact   
  19.               component={dynamic({  
  20.                 app,  
  21.                 ...dynamics  
  22.               })}   
  23.             />  
  24.           ))  
  25.         }  
  26.       </Switch>  
  27.     </Router>  
  28.   );  
  29. }  
  30.   
  31. export default RouterConfig;  

刷新页面即可,之所以把menu提出配置文件中,便于管理和后期拓展。