距离上篇文章已经好长一段时间了,这两个星期公司派驻到京东方这边出差负责入驻项目团队的前端工做。这段时间从零搭建一下前端项目,此次给的时间比较充裕,思考的也比较多。之前也常有搭过前端项目,可是给的时间都比较紧,所以不少问题都忽略掉了。此次正好对之前的进行一次优化,并总结了一些经验分想给你们。若是你们有更好的想法,欢迎留言交流。css
舒适提示:html
项目地址: https://github.com/ruichengpi...前端
该项目能够用我本身写的脚手架工具asuna-cli完成项目构建,我本身写的脚手架工具地址以下:vue
https://github.com/ruichengpi...node
以上是示例项目的目录结构,下面咱们将逐一进行分析**
这个文件主要放了一些与webpack打包的相关文件。react
这个项目的webpack配置我是在vue-cli的项目上进行修改的,能够用于React的项目构建。目前只要开发环境和生产环境这两个环境,可能一些公司有多个环境,每一个环境下webpack的配置还不一样,此时能够根据不一样环境建一个文件名格式为“webpack.<环境名>.conf.js”的webpack配置使用。webpack.base.conf.js里面有一些基本配置好比rules、input、output的等配置,通常来讲每一个环境下这些大体都是相同,一些不一样之处能够用webpack-merge插件进行合并。通常来讲大多数项目来讲开发环境和生产环境两个webpack配置足够了。webpack
这里存放着不一样环境webpack所须要的配置参数。ios
若是你须要再加一个环境的话,能够建一个文件名为“<环境名>.env.js”并向外暴露环境变量NODE_ENV,而后在index.js中导入,进行相关参数设置。git
这里是用来作接口的mock的,可能不少公司都不太用,我在工做也不多去mock。这里介绍一下本身的接口mock思路,这里我选择mockjs加上json-server的组合。两者具体的使用,你们能够查看其官方文档。github
package.json我配置一个script,以下:
"mock": "json-server mock/index.js --port 3000 --routes mock/routes.json"
控制台执行“npm run mock“便可。
url.js
export default { fetchUserInfo:{ method:'get', url:'/api/user' }, fetchAuthorInfo:{ method:'get', url:'/api/author' }, fetchUserList:{ method:'get', url:'/api/userList' } }
index.js
import _ from 'lodash' import http from '@/utils/http' import API_URL from './url'; function mapUrlObjToFuncObj(urlObj){ const API = {}; _.keys(urlObj).forEach((key)=>{ const item = urlObj[key] API[key]=function(params){ return http[item.method](item.url,params) } }); return API; } function mapUrlObjToStrObj(urlObj){ const Url = {}; _.keys(urlObj).forEach((key)=>{ const item = urlObj[key] Url[key]=item.url }); return Url; } export const API = mapUrlObjToFuncObj(API_URL); export const URL = mapUrlObjToStrObj(API_URL);
这里咱们用来放置api的接口地址,为了后续的接口维护,咱们在使用的过程当中不会直接写死接口地址,而是将接口请求封装成一个个方法。经过对接口的统一维护,咱们就能够作到在执行修改接口地址、修改请求方法、新增接口等等操做时,就不用在整个项目里处处找了,只要维护好url.js向外暴露的对象便可。使用方法以下:
import {API} from '@/api' //params为请求参数 API.fetchUserInfo(params).then(response=>{ //response为返回值 ... })
这里咱们会放项目的所须要图片资源,这些图片资源通常来讲都是作图标的,都比较小。webpack会将其转化成BASE64去使用。若是你不想以这种方式使用,能够在static目录下存放图片资源。
这里存放整个项目所用到的公共组件。定一个组件,这里要求是新建一个文件夹,文件夹名为组件名,另外在这个文件夹下新建index.jsx和style.scss文件。例如作一个HelloWorld组件,则应该是以下结构。
HelloWorld
index.js
import React from 'react'; import './style.scss'; class HelloWorld extends React.PureComponent{ render(){ return ( <h4 className="u-text">Hello World</h4> ) } } export default HelloWorld;
style.scss
.u-text{ color: red; }
这里存放着布局文件。关于这个布局文件我是这么去定义它的,我在开发过程当中有一些页面他们的某一部分都是相同,早以前可能你们可能会在一个React组件加<Switch>和<Route>去实现这个功能,能够这么干,没毛病。可是这个有一个很差点就是你的路由无法作统一的管理,分散在各个组件中,给后续的维护带来不少问题。为了解决这个,我选择利用props.children结合标签嵌套的方式去完成。举个例子:
先定一个layout(本职也是React组件)BasicLayout.jsx
import React from 'react'; class BasicLayout extends React.PureComponent{ render(){ const {children} = this.props; return ( <div> <div>隔壁老王今日行程:</div> <div>{children}</div> </div> ) } } export default BasicLayout;
定义完以后咱们能够这么使用:
import React from 'react'; import BasicLayout from '<BasicLayout的路径>' class Work extends React.PureComponent{ render(){ return ( <BasicLayout> <div>今天隔壁老王比较累,不工做!</div> <BasicLayout> ) } } export default BasicLayout;
最后在的dom结构以下:
<div> <div>隔壁老王今日行程:</div> <div> <div>今天隔壁老王比较累,不工做!</div> </div> </div>
这样咱们能够基于BasicLayout作出不少个像下面的页面。
<div> <div>隔壁老王今日行程:</div> <div> //<不一样的内容> </div> </div>
使用这种方法就能够将咱们得全部路由写在一块儿了,可能有人以为每次都要写引入BasicLayout很麻烦,有没有其余更好用的办法,在讲App.jsx的时候会说到这里就先跳过。
这里的存放的都是页面级组件,跟react-router对应的路由须要一一对应。每一个页面都是一个文件夹,文件名就是页面名称,每一个页面都要包含以下几个文件:
具体代码能够自行git clone 项目查看,这里就不贴出来了。
这里存放共有的scss文件,比较一些经常使用的功能类、@mixin、@function等等。
这里有四个文件:
咱们知道每一个页面都有本身的actions.js、actionTypes.js、reducer.js,可是这里是全局的,另外index.js会向外暴露store,而后再main.js中引入使用。
import {createStore,combineReducers,applyMiddleware} from 'redux'; import thunk from 'redux-thunk'; import API from '@/api'; import user from './reducer'; import author from '@/pages/PageOne/redux/reducer'; const rootReducer = combineReducers({ user, author }); const store=createStore( rootReducer, applyMiddleware(thunk.withExtraArgument({ API })) ) export default store;
这里有一个小细节,redux-thunk是能够携带一些额外的对象或者方法的,这里,我携带API对象。当咱们须要在actions.js里面使用API对象时,就不须要再import导入进来。下面咱们作个对比:
修改前
import * as actionTypes from './actionTypes'; import API from '../api'; export const fecthUserName=(params)=> async (dispatch,getState)=>{ const response =await API.fetchUserInfo(params); const {success,data} = response; if(success){ dispatch({ type:actionTypes.CHANGE_USER_NAME, payload:data }); } }
修改后
import * as actionTypes from './actionTypes'; export const fecthUserName=(params)=> async (dispatch,getState,{API})=>{ const response =await API.fetchUserInfo(params); const {success,data} = response; if(success){ dispatch({ type:actionTypes.CHANGE_USER_NAME, payload:data }); } }
这里会存放一些本身的封装的js工具文件,好比我在项目基于axios封装了一个http.js,简化了axios的操做。
这里以配置化的防止去注册路由,并app.js里面去渲染路由标签。
import Loadable from 'react-loadable'; import createHistory from 'history/createBrowserHistory'; import BasicLayout from '@/layouts/BasicLayout'; import NavTwoLayout from '@/layouts/NavTwoLayout'; import Loading from '@/components/Loading'; import NotFound from '@/pages/Exception/404'; const Home = Loadable({loader: () => import('@/pages/Home'),loading: Loading}); const Teachers = Loadable({loader: () => import('@/pages/Teachers'),loading: Loading}); export const history = createHistory(); export const routes = [ { path:'/', redirect:'/navone/home' }, { path:'/navone', redirect:'/navone/home', children:[{ path:'/home', layout:BasicLayout, component:Home }] }, { path:'/navtwo', redirect:'/navtwo/teachers', children:[{ path:'/teachers', layout:NavTwoLayout, component:Teachers }] }, { path:'*', component:NotFound } ]
这里根据路由配置用来渲染路由标签,先放代码:
import React from 'react'; import {Router} from 'react-router-dom'; import {Switch, Route ,Redirect} from 'react-router'; import {history,routes} from '@/router'; function getRouterByRoutes(routes){ const renderedRoutesList = []; const renderRoutes = (routes,parentPath)=>{ Array.isArray(routes)&&routes.forEach((route)=>{ const {path,redirect,children,layout,component} = route; if(redirect){ renderedRoutesList.push(<Redirect key={`${parentPath}${path}`} exact from={path} to={`${parentPath}${redirect}`}/>) } if(component){ renderedRoutesList.push( layout?<Route key={`${parentPath}${path}`} exact path={`${parentPath}${path}`} render={(props)=>React.createElement(layout,props,React.createElement(component,props))} />: <Route key={`${parentPath}${path}`} exact path={`${parentPath}${path}`} component={component}/>) } if(Array.isArray(children)&&children.length>0){ renderRoutes(children,path) } }); } renderRoutes(routes,'') return renderedRoutesList; } class App extends React.PureComponent{ render(){ return ( <Router history={history}> <Switch> {getRouterByRoutes(routes)} </Switch> </Router> ) } } export default App;
这里咱们须要重点讲的是之间在layouts中咱们跳过的内容,能不能不每次都用layout组件去包裹代码,答案是能够的。这里我选择<Route>中的render属性。
webpack入口文件,主要一些全局js或者scss的导入,并执行react-dom下的render方法,代码以下:
import React from 'react'; import {render} from 'react-dom'; import {Provider} from 'react-redux'; import store from '@/store'; import App from '@/App'; import '@/scss/reset.scss'; import '@/scss/base.scss'; render( <Provider store={store}> <App/> </Provider>, document.getElementById('app') )
这是一个静态资源目录,通常存放一些第三方工具库。这个目录主要两方面考虑:
其实第三方工具库最好的方式是CDN,可是有些公司就是没有,无奈只能如此。你加入的第三工具库均可在当前服务器下”/static/*“路径下获取到。
这里存放着页面和组件级别构建所须要的模板文件,页面级别构建提供了两种模板PageReducer(集成了reducer)和PageSample(不集成reducer),而组件只提供了一种模板ComSample。页面和组件级别的构建是须要配合asuna-cli才能构建,目前项目已经集成了asuna-cli。package.json写了两个script:npm run newPage(页面构建)和npm run newComponent(组件构建)。开发可根据实际须要选择构建,asuna-cli具体使用能够去https://github.com/ruichengpi...查看。
这个只是我的搭建企业级React项目的一些总结。固然存在不足的地方,后面在工做过程当中若是有一些好的想法也会在这上面进行更新。欢迎你们Star关注!若是你也有好的想法欢迎留言交流,但愿这篇拙文能给你们一些启发。