企業級React項目的我的構建總結

前言

距離上篇文章已經好長一段時間了,這兩個星期公司派駐到京東方這邊出差負責入駐項目團隊的前端工做。這段時間從零搭建一下前端項目,此次給的時間比較充裕,思考的也比較多。之前也常有搭過前端項目,可是給的時間都比較緊,所以不少問題都忽略掉了。此次正好對之前的進行一次優化,並總結了一些經驗分想給你們。若是你們有更好的想法,歡迎留言交流。css

舒適提示:html

  • 這個項目是以PC端前端項目爲視角,移動端前端項目並不徹底適用。這點各位小夥們還須要注意一下。前端

  • 該項目已分不一樣方向去維護,每一個分支與之對應的方向可在CONTRIBUTING.md裏查看vue

項目說明

項目地址: https://github.com/ruichengping/react-webpack-pcnode

該項目能夠用我本身寫的腳手架工具asuna-cli完成項目構建,我本身寫的腳手架工具地址以下:react

https://github.com/ruichengping/asuna-cliwebpack


項目結構

以上是示例項目的目錄結構,下面咱們將逐一進行分析**ios

build

這個文件主要放了一些與webpack打包的相關文件。git

  • build.js ---- webpack打包腳本,用於構建生產環境的包
  • check-versions.js ---- 主要檢測當前打包環境的node以及npm的版本是否符合要求
  • utils.js ---- webpack打包所須要的一些工具庫
  • webpack.base.conf.js ---- webpack的一些基礎配置,不一樣環境的webpack配置都是基於此
  • webpack.dev.conf.js ---- 開發環境的webpack配置
  • webpack.prod.conf.js ---- 生產環境的webpack配置

這個項目的webpack配置我是在vue-cli的項目上進行修改的,能夠用於React的項目構建。目前只要開發環境和生產環境這兩個環境,可能一些公司有多個環境,每一個環境下webpack的配置還不一樣,此時能夠根據不一樣環境建一個文件名格式爲「webpack.<環境名>.conf.js」的webpack配置使用。webpack.base.conf.js裏面有一些基本配置好比rulesinputoutput的等配置,通常來講每一個環境下這些大體都是相同,一些不一樣之處能夠用webpack-merge插件進行合併。通常來講大多數項目來講開發環境和生產環境兩個webpack配置足夠了。github

config

這裏存放着不一樣環境webpack所須要的配置參數。

  • dev.env.js ---- 向外暴露開發環境下的環境變量NODE_ENV
  • index.js ---- 存放不一樣環境的配置參數
  • prod.env.js ---- 向外暴露生產環境下的環境變量NODE_ENV

若是你須要再加一個環境的話,能夠建一個文件名爲「<環境名>.env.js」並向外暴露環境變量NODE_ENV,而後在index.js中導入,進行相關參數設置。

mock

這裏是用來作接口的mock的,可能不少公司都不太用,我在工做也不多去mock。這裏介紹一下本身的接口mock思路,這裏我選擇mockjs加上json-server的組合。兩者具體的使用,你們能夠查看其官方文檔。

  • api ---- 存放不一樣api所對應的數據
  • index.js ---- json-server的主文件
  • routes.json ---- 路由的映射

package.json我配置一個script,以下:

"mock": "json-server mock/index.js --port 3000 --routes mock/routes.json"
複製代碼

控制檯執行「npm run mock「便可。

src

api

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爲返回值
    ...
})
複製代碼

assets

這裏咱們會放項目的所須要圖片資源,這些圖片資源通常來講都是作圖標的,都比較小。webpack會將其轉化成BASE64去使用。若是你不想以這種方式使用,能夠在static目錄下存放圖片資源。

components

這裏存放整個項目所用到的公共組件。定一個組件,這裏要求是新建一個文件夾,文件夾名爲組件名,另外在這個文件夾下新建index.jsx和style.scss文件。例如作一個HelloWorld組件,則應該是以下結構。

HelloWorld

  • index.jsx
  • style.scss //存放組件的樣式

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;
}
複製代碼

layouts

這裏存放着佈局文件。關於這個佈局文件我是這麼去定義它的,我在開發過程當中有一些頁面他們的某一部分都是相同,早以前可能你們可能會在一個React組件加 和去實現這個功能,能夠這麼幹,沒毛病。可是這個有一個很差點就是你的路由無法作統一的管理,分散在各個組件中,給後續的維護帶來不少問題。爲了解決這個,我選擇利用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的時候會說到這裏就先跳過。

pages

這裏的存放的都是頁面級組件,跟react-router對應的路由須要一一對應。每一個頁面都是一個文件夾,文件名就是頁面名稱,每一個頁面都要包含以下幾個文件:

  • components ---- 存放當前頁獨有的一些組件
  • redux ---- 存放三個文件actions.jsactionTypes.jsreducer.js,這幾個文件應該只與這個頁面相關
  • index.jsx ---- 頁面的入口文件
  • style.scss ---- 頁面所須要的樣式 具體代碼能夠自行git clone 項目查看,這裏就不貼出來了。

scss

這裏存放共有的scss文件,比較一些經常使用的功能類、@mixin、@function等等。

store

這裏有四個文件:

  • actions.js
  • actionTypes.js
  • reducer.js
  • index.js

咱們知道每一個頁面都有本身的actions.jsactionTypes.jsreducer.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
    });
  }
}
複製代碼

utils

這裏會存放一些本身的封裝的js工具文件,好比我在項目基於axios封裝了一個http.js,簡化了axios的操做。

router/index.js

這裏以配置化的防止去註冊路由,並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
  }
]
複製代碼

App.js

這裏根據路由配置用來渲染路由標籤,先放代碼:

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組件去包裹代碼,答案是能夠的。這裏我選擇中的render屬性。

main.js

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')
)

複製代碼

static

這是一個靜態資源目錄,通常存放一些第三方工具庫。這個目錄主要兩方面考慮:

  • 有些第三方工具庫沒有npm包,咱們沒法用npm install 或者 yarn add方式添加
  • 一些比較大的第三方工具庫會影響咱們的打包速度,能夠把它拿出來經過script的方式引入

其實第三方工具庫最好的方式是CDN,可是有些公司就是沒有,無奈只能如此。你加入的第三工具庫均可在當前服務器下」/static/*「路徑下獲取到。

templates

這裏存放着頁面和組件級別構建所須要的模板文件,頁面級別構建提供了兩種模板PageReducer(集成了reducer)和PageSample(不集成reducer),而組件只提供了一種模板ComSample。頁面和組件級別的構建是須要配合asuna-cli才能構建,目前項目已經集成了asuna-cli。package.json寫了兩個script:npm run newPage(頁面構建)和npm run newComponent(組件構建)。開發可根據實際須要選擇構建,asuna-cli具體使用能夠去https://github.com/ruichengping/asuna-cli查看。

其餘文件

  • .babelrc ---- babel轉換的配置文件
  • .gitignore ---- git操做所須要忽略的文件
  • .postcssrc.js ---- postcss的配置文件
  • index.html ---- 模板index.html,webpack會根據今生成新的index.html,配合html-webpack-plugin使用
  • package.json ---- 家喻戶曉的東西
  • README.md ---- 項目說明
  • theme.js ---- ant-design的主題色配置文件,具體使用能夠參考ant-design
  • asuna.config.js ---- asuna-cli的配置文件
  • yarn.lock ---- 鎖定包的版本

結語

這個只是我的搭建企業級React項目的一些總結。固然存在不足的地方,後面在工做過程當中若是有一些好的想法也會在這上面進行更新。歡迎你們Star關注!若是你也有好的想法歡迎留言交流,但願這篇拙文能給你們一些啓發。

相關文章
相關標籤/搜索