從零搭建完整的React項目模板(Webpack + React hooks + Mobx + Antd) 【演戲演全套】

本篇文章講述從零搭建React中後臺項目框架模板,方便快速進行具體項目開發。包括Webpack4.0配置及打包優化、React全家桶使用(React + React-router + Axios + Mobx + Antd)、ESLint等項目開發規範等。javascript

涉及的技術棧均採用當前最新版本的語法:css

  • 使用Webpack4.0構建項目(不使用create-react-appumi等腳手架);
  • 使用Babel7配置轉換ES六、React、Mobx等語法;
  • React版本V16.12.0,所有采用函數化 Hooks特性開發項目組件
  • 採用React-router5 工具 配置項目路由;
  • 採用Mobx5 + Hooks實現項目數據狀態管理;
  • 封裝Axios庫實現與後臺http請求交互;
  • UI庫採用流行的Ant-design3.0組件庫;
  • 完整項目實現及模塊結構拆分;

項目頁面截圖:
html

演示.gif

前言

通常React開發,可使用Facebook提供的 create-react-app 來建立。create-react-app 足夠簡單易用,從學習 React 的角度來看很是合適。但嚴格說來,若是要開發一款大型的應用,須要作到更精細、更靈活的配置,只用 create-react-app 並不合適,有規模點的公司都會考慮搭建本身公司級的腳手架工具和框架模板。而基礎就是基於webpack從零精細化構建。java

企業級React開發也能夠採用螞蟻金服的Umi(一個可插拔的企業級 react 應用框架),可使用相關的全套全家桶連貫快捷開發,優勢還在於大廠出品,經歷衆多大項目的考驗,穩定性和可維護性獲得極大的保障。可是增長了很多學習成本,項目的粒度可控性不高,也比較受限。
在構建公司級全套項目架構上能夠從Umi、[Ant Design Pro](https://pro.ant.design/)等上獲取很多極有價值的參考。node

本項目從零搭建React應用模板,一來方便本身快速構建實際應用;二來重點在於梳理各技術棧最新知識點。但願也對看到的人有所幫助。react

項目說明

本項目爲React中後臺項目框架模板,方便快速進行具體項目開發。包括Webpack4.0配置及打包優化、React全家桶使用(React + React-router + Axios + Mobx + Antd)、ESLint等項目開發規範等。webpack

項目Git地址:github.com/now1then/re…
文章連接-語雀:www.yuque.com/nowthen/lon…
在線演示地址:ios

目錄結構:git

├── build                   // webpack配置
│   ├── webpack.common.js   // webpack通用配置
│   ├── webpack.dev.js      // webpack開發環境配置
│   └── webpack.prod.js     // webpack生產環境配置
├── dist                    // 打包輸出目錄
├── public                  // 項目公開目錄
├── src                     // src開發目錄
│   ├── assets              // 靜態資源
│   ├── components          // 公共組件
│   ├── layouts             // 頁面佈局組件
│   ├── modules             // 公共業務模塊
│   ├── pages               // 具體業務頁面
│   ├── routers             // 項目路由配置
│   ├── services            // axios服務等相關
│   ├── stores              // 全局公共 mobx store
│   ├── styles              // 存放公共樣式
│   ├── utils               // 工具庫/通用函數
│   ├── index.html          // 入口html頁面
│   └── main.js            // 項目入口文件
├── .babelrc                // babel配置
├── .editorconfig           // 項目格式配置
├── .eslintrc.js            // ESLint配置
├── .gitignore              // git 忽略配置
├── .postcssrc.js           // postcss配置
├── package.json            // 依賴包配置
└── README.md               // 項目說明
複製代碼

項目構建

文章中使用 Yarn 管理安裝包,若未安裝Yarn,替換成 Npm 對應命令便可。github

初始化項目

初始化package.json

yarn init
複製代碼

安裝webpack

yarn add -D webpack webpack-cli webpack-merge
複製代碼

項目中使用的Webpack版本是^4.41.2,Webpack4.0 打包構建作了不少默認的優化配置,很多配置項無需配置或更改。
好比:針對開發模式的加快打包速度,合併chunk; 針對生產模式的代碼壓縮,減小打包體積等。

// 一部分默認配置 
optimization: {
    removeAvailableModules: true, // 刪除已解決的chunk (默認 true)
    removeEmptyChunks: true, // 刪除空的chunks (默認 true)
    mergeDuplicateChunks: true // 合併重複的chunk (默認 true)
  }
  
 // 針對生產環境默認配置
  optimization: {
    sideEffects:true, //配合tree shaking
    splitChunks: {...}, //拆包
    namedModules: false, // namedChunks:false 不啓用chunk命名,默認自增id
    minimize: true, // 代碼壓縮
  }
複製代碼

根據開發環境/生產環境 區分webpack配置很是有必要,能夠加快開發環境的打包速度,有時候遇到開發環境打包過慢,能夠排查下是否配置有誤(好比開發環境開啓了代碼壓縮等)。
項目中配合webpack-merge根據開發環境/生產環境進行拆分配置:

build.png

Webpack4.0發佈已經很長時間了,相信基本上項目都已遷移至4.0,在這裏就很少贅述了。

配置Html模板

安裝:

yarn add -D html-webpack-plugin
複製代碼

配置:

const srcDir = path.join(__dirname, "../src");
plugins: [
  new HtmlWebpackPlugin({
    template: `${srcDir}/index.html`
	})
]
複製代碼

配置本地服務及熱更新

安裝:

yarn add -D webpack-dev-server clean-webpack-plugin
複製代碼

開發環境利用webpack-dev-server搭建本地 web server,並啓用模塊熱更新(HMR)
爲方便開發調試,轉發代理請求(本例中配合axios封裝 轉發接口到easy-mock在線平臺)

配置:

mode: "development", // 開發模式
devServer: { // 本地服務配置
  port: 9000,
  hot: true,
  open: false,
  historyApiFallback: true,
  compress: true,
  proxy: { // 代理
    "/testapi": {
      target:
      "https://www.easy-mock.com/mock/5dff0acd5b188e66c6e07329/react-template",
       changeOrigin: true,
       secure: false,
       pathRewrite: { "^/testapi": "" }
    }
  }
},
plugins: [
  new webpack.NamedModulesPlugin(),
  new webpack.HotModuleReplacementPlugin()
],
複製代碼

配置Babel

安裝:

yarn add -D babel-loader @babel/core @babel/plugin-transform-runtime 
	@babel/preset-env @babel/preset-react  babel-plugin-import
	@babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators
複製代碼

Webpack中Babel配置,是比較重要的一環。關係着ES6語法、React jsx、Mobx等語法通過打包後可否正常運行。
其中:

  • @babel/preset-react轉換React jsx語法;
  • @babel/plugin-proposal-class-properties 轉換 Class語法;
  • @babel/plugin-proposal-decorators 轉換 Mobx 等更高級的語法;
  • babel-plugin-import 配合實現React組件的按需加載;

這裏須要注意Babel7.0 相較於Babel6.0的區別。

配置:

module: {
  rules: [
    {
      test: /\.(js|jsx)$/,
      include: [srcDir],
      use: ["babel-loader?cacheDirectory=true"]
    },
  ]
}
複製代碼

.babelrc 文件配置
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/transform-runtime",
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    [
      "import",
      {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": "css" // `style: true` 會加載 less 文件
      }
    ]
  ]
}
複製代碼

處理Less樣式和圖片等資源

安裝:

yarn add -D less less-loader style-loader css-loader url-loader 
	mini-css-extract-plugin postcss-loader autoprefixer
複製代碼

其中:

  • less-loader、style-loader、css-loader處理加載less、css文件;
  • postcss-loader、autoprefixer處理css樣式瀏覽器前綴兼容;
  • url-loader處理圖片、字體文件等資源;
  • mini-css-extract-plugin 分離css成單獨的文件;

配置:

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
...
module: {
  rules: [
    {
      test: /\.less$/,
      use: [
        devMode ? "style-loader" : MiniCssExtractPlugin.loader,
        "css-loader",
        "postcss-loader",
        "less-loader"
      ]
    },
    {
      test: /\.css$/,
      use: [
        devMode ? "style-loader" : MiniCssExtractPlugin.loader,
        "css-loader",
        "postcss-loader"
      ]
    },
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      use: ["url-loader"],
      include: [srcDir]
    },
    {
      test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
      use: ["url-loader"],
      include: [srcDir]
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      use: ["url-loader"],
      include: [srcDir]
    }
  ]
},
plugins: [
  new MiniCssExtractPlugin({
    filename: "[name].[contenthash:8].css",
    chunkFilename: "chunk/[id].[contenthash:8].css"
  }),
  ],
複製代碼

配置postcss .postcssrc.js文件

// .postcssrc.js
module.exports = {
  plugins: {
    autoprefixer: {}
  }
};

// package.json中配置兼容瀏覽器
"browserslist": [
  "> 1%",
  "last 2 versions",
  "not ie <= 10"
]
複製代碼

利用happypack 多線程打包

安裝:

yarn add -D happypack
複製代碼

配置:

const os = require("os");
const HappyPack = require("happypack");
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module: {
  rules: [
    {
      test: /\.(js|jsx)$/,
      include: [srcDir],
      exclude: /(node_modules|bower_components)/,
      use: ["happypack/loader?id=happybabel"]
    },
  ]
},
plugins: [
  //開啓 happypack 的線程池
  new HappyPack({
    id: "happybabel",
    loaders: ["babel-loader?cacheDirectory=true"],
    threadPool: happyThreadPool,
    cache: true,
    verbose: true
  }),
]
複製代碼

生產環境 拆分模塊

根據實際項目狀況拆分模塊,配合異步加載,防止單個文件過大。

optimization: {
    runtimeChunk: {
      name: "manifest"
    },
    splitChunks: {
      chunks: "all", //默認只做用於異步模塊,爲`all`時對全部模塊生效,`initial`對同步模塊有效
      cacheGroups: {
        dll: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-dom-router|babel-polyfill|mobx|mobx-react|mobx-react-dom|antd|@ant-design)/,
          minChunks: 1,
          priority: 2,
          name: "dll"
        },
        codeMirror: {
          test: /[\\/]node_modules[\\/](react-codemirror|codemirror)/,
          minChunks: 1,
          priority: 2,
          name: "codemirror"
        },
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          minChunks: 1,
          priority: 1,
          name: "vendors"
        }
      }
    }
  }
複製代碼

其餘配置

引入 ESLint 與 Prettier 配合,規範化團隊項目代碼開發,統一代碼風格。

yarn add -D prettier babel-eslint eslint eslint-loader eslint-config-airbnb 
eslint-config-prettier eslint-plugin-babel eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
複製代碼

(細節待補充)

具體配置詳見**/build目錄**下 項目代碼

npm scripts

package.json 文件

{
  ...
  "scripts": {
    "start": "webpack-dev-server --color --inline --progress --config build/webpack.dev.js", //
    "build": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js",
    "build:report": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js",
    "build:watch": "NODE_ENV=production webpack --progress --config ./build/webpack.prod.js"
  },
  ...
}
複製代碼

命令行運行:

// 命令行執行
// 運行開發環境;
yarn start 

// 生產環境打包壓縮;
yarn build

// 圖形化分析打包文件大小;
yarn build:report

// 方便排查生產環境打包後文件的錯誤信息(文件source map);
yarn build:watch
複製代碼

其中build:report、build:watch 可以實現功能,是在build/webpack.prod.js中有以下代碼:

// 方便排查生產環境打包後文件的錯誤信息(文件source map)
if (process.env.npm_lifecycle_event == "build:watch") {
  config = merge(config, {
    devtool: "cheap-source-map"
  });
}
// 圖形化分析打包文件大小
if (process.env.npm_lifecycle_event === "build:report") {
  const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
    .BundleAnalyzerPlugin;
  config.plugins.push(new BundleAnalyzerPlugin());
}
複製代碼

項目代碼架構

實際開發用到的依賴包安裝:

yarn add react react-dom react-router-dom mobx mobx-react mobx-react-router 
	axios antd moment
複製代碼

咱們在寫具體代碼以前要作的第一個決定就是,目錄結構怎麼構建?要把這些組件放在哪裏?

目錄結構

根據我的習慣及經驗,項目目錄構建以下圖所示:

├── build                   // webpack配置
│   ├── webpack.common.js   // webpack通用配置
│   ├── webpack.dev.js      // webpack開發環境配置
│   └── webpack.prod.js     // webpack生產環境配置
├── dist                    // 打包輸出目錄
├── public                  // 項目公開目錄
├── src                     // src開發目錄
│   ├── assets              // 靜態資源
│   ├── components          // 公共組件
│   ├── layouts             // 頁面佈局組件
│   ├── modules             // 公共業務模塊
│   ├── pages               // 具體業務頁面
│   ├── routers             // 項目路由配置
│   ├── services            // axios服務等相關
│   ├── stores              // 全局公共 mobx store
│   ├── styles              // 存放公共樣式
│   ├── utils               // 工具庫/通用函數
│   ├── index.html          // 入口html頁面
│   └── main.js            // 項目入口文件
├── .babelrc                // babel配置
├── .editorconfig           // 項目格式配置
├── .eslintrc.js            // ESLint配置
├── .gitignore              // git 忽略配置
├── .postcssrc.js           // postcss配置
├── package.json            // 依賴包配置
└── README.md               // 項目說明
複製代碼

頁面模塊目錄結構,好比FormDemo頁面結構:

├── FormDemo                   // 表單演示 頁面
│   ├── index.js               // 頁面入口文件
│   ├── newModal.js            // 彈窗組件
│   ├── searchForm.js          // 搜索表單 模塊組件
│   ├── store.js               // 本頁面使用的 mobx store 數據
│   └── style.less             // 頁面樣式
複製代碼

函數化Hooks

Hook 是 React 16.8 的新增特性。它可讓你在不編寫 class 的狀況下使用 state 以及其餘的 React 特性。

當前React版本已更新到16.12,Hooks 徹底應該成爲 React 使用的主流。本項目中將徹底擁抱Hook,通常再也不用 class 來實現組件。
**
如下爲部分實現代碼(可暫忽略mobx的使用):

import React, { useState, useEffect, useContext } from 'react';
import { observer } from 'mobx-react';
import { Button } from 'antd';
import Store from './store';

import './style.less';

const HomePage = () => {
  // useContext 訂閱mobx數據
  const pageStore = useContext(Store);
  // useState state狀態
  const [num, setNum] = useState(0);
  // useEffect反作用
  useEffect(() => {
    pageStore.qryTableDate();
  }, []);

  return (
    <div className="page-home page-content"> <h2>{pageStore.pageTitle}</h2> <div> <span>num值:{num}</span> <Button type="primary" size="small" style={{ marginLeft: 10 }} onClick={() => setNum(num + 1)} >+1</Button> </div> </div>
  );
};

export default observer(HomePage); 
複製代碼

Router路由配置

項目是單頁應用,路由配置通常分爲約定式動態路由和集中配置式路由。
在 React 的世界裏,直接採用成熟的react-router工具管理頁面路由。咱們如今說到react-router,基本上都是在說 react-router 的第4版以後的版本,當前的最新版本已經更新到5.1.x了。
當前react-router支持動態路由,徹底用React組件來實現路由,在渲染過程當中動態設置路由規則,匹配命中規則加載對應頁面組件。

本項目採用集中配置式路由(方便路由鑑權、從服務端接口獲取菜單路由配置等),同時兼顧方便地設置側邊菜單欄。 固然爲簡單起見,項目中讀取本地靜態菜單配置,也暫未引入路由鑑權。

靜態路由配置 src/routes/config.js

import React, { lazy } from "react";
import BasicLayout from "@/layouts/BasicLayout";
import BlankLayout from "@/layouts/BlankLayout";

const config = [
  {
    path: "/",
    component: BlankLayout, // 空白頁佈局
    childRoutes: [ // 子菜單路由
      { 
        path: "/login", // 路由路徑
        name: "登陸頁", // 菜單名稱 (不設置,則不展現在菜單欄中)
        icon: "setting", // 菜單圖標
        component: lazy(() => import("@/pages/Login")) // 懶加載 路由組件
      },
      // login等沒有菜單導航欄等基本佈局的頁面, 要放在基本佈局BasicLayout以前。
      {
        path: "/",
        component: BasicLayout, // 基本佈局框架
        childRoutes: [
          {
            path: "/welcome",
            name: "歡迎頁",
            icon: "smile",
            component: lazy(() => import("@/pages/Welcome"))
          },
          {... /* 其餘 */}, 
          { path: "/", exact: true, redirect: "/welcome" },
          { path: "*", exact: true, redirect: "/exception/404" }
        ]
      }
    ]
  }
];

export default config;
複製代碼

上面是靜態路由的一部分配置,
注意:<Router>中會用<Switch>包裹,會匹配命中的第一個。"/login"等沒有菜單導航欄等基本佈局的頁面, 要放在基本佈局BasicLayout以前。

利用<Suspense>React.lazy()實現頁面組件懶加載。

路由組件渲染 src/routes/AppRouter.js:

import React, { lazy, Suspense } from "react";
import LoadingPage from "@/components/LoadingPage";
import {
  HashRouter as Router,
  Route,
  Switch,
  Redirect
} from "react-router-dom";
import config from "./config";

const renderRoutes = routes => {
  if (!Array.isArray(routes)) {
    return null;
  }

  return (
    <Switch>
      {routes.map((route, index) => {
        if (route.redirect) {
          return (
            <Redirect
              key={route.path || index}
              exact={route.exact}
              strict={route.strict}
              from={route.path}
              to={route.redirect}
            />
          );
        }

        return (
          <Route
            key={route.path || index}
            path={route.path}
            exact={route.exact}
            strict={route.strict}
            render={() => {
              const renderChildRoutes = renderRoutes(route.childRoutes);
              if (route.component) {
                return (
                  <Suspense fallback={<LoadingPage />}>
                    <route.component route={route}>
                      {renderChildRoutes}
                    </route.component>
                  </Suspense>
                );
              }
              return renderChildRoutes;
            }}
          />
        );
      })}
    </Switch>
  );
};

const AppRouter = () => {
  return <Router>{renderRoutes(config)}</Router>;
};

export default AppRouter;
複製代碼

路由 hooks語法

react-router-dom 也已經支持 hooks語法,獲取路由信息或路由跳轉,可使用新的hooks 函數:

  • [useHistory](https://reacttraining.com/react-router/core/api/Hooks/usehistory):獲取歷史路由,回退、跳轉等操做;
  • useLocation:查看當前路由信息;
  • [useParams](https://reacttraining.com/react-router/core/api/Hooks/useparams):讀取路由附帶的params參數信息;
  • [useRouteMatch](https://reacttraining.com/react-router/core/api/Hooks/useroutematch):匹配當前路由;

只要包裹在中的子組件均可以經過這幾個鉤子函數獲取路由信息。

代碼演示:

import { useHistory } from "react-router-dom";

function HomeButton() {
  const history = useHistory();

  function onClick() {
    history.push("/home");
  }

  return (
    <button type="button" onClick={onClick}> 跳轉Home頁 </button>
  );
}
複製代碼

結合mobx管理數據狀態

項目中是否使用狀態管理工具或使用何種管理工具,依據實際項目狀況而定。
本項目使用本身比較熟悉的Mobx,Mobx是一個功能強大,上手很是容易的狀態管理工具。

爲了使用簡潔及管理方便,在組織上,分爲全局公共數據狀態頁面數據狀態。
公用數據狀態存放在/src/stores目錄下;頁面幾數據存放於對應頁面目錄下。

在實現上,利用mobx + useContext Hook特性 實現函數式組件的狀態管理。
具體在於利用React的createdContext構建包含Mobx 的context上下文;函數式組件中使用useContext Hook 訂閱Mobx數據變化。

頁面級store.js代碼:

import { createContext } from "react";
import { observable, action, computed } from "mobx";
import request from "@/services/newRequest";

class HomeStore {
  @observable tableData = [];
  @observable pageTitle = "Home主頁";
  @observable loading = false;

  @action.bound setData(data = {}) {
    Object.entries(data).forEach(item => {
      this[item[0]] = item[1];
    });
  }

  // 列表數據
  @action.bound
  async qryTableDate(page = 1, size = 10) {
    this.loading = true;
    const res = await request({
      url: "/list",
      method: "post",
      data: { page, size }
    });

    if (res.success) {
      const resData = res.data || {};
      console.log(resData);
    }
    this.loading = false;
  }
}

export default createContext(new HomeStore());
複製代碼

頁面組件 代碼:

import React, { useContext } from "react";
import { observer } from "mobx-react";
import Store from "./store";

import "./style.less";

const HomePage = () => {
  const pageStore = useContext(Store);

  return (
    <div className="page-home page-content"> home頁面 <h2>{pageStore.pageTitle}</h2> </div>
  );
};

export default observer(HomePage);
複製代碼

以上爲部分演示代碼,具體業務實現能夠查看項目代碼。

Axios Http請求封裝

Axios請求封裝,具體代碼見/src/services/newRequest.js
思路詳見本人以前的另外一篇文章(忽略外部組件便可):「漫漫長路-Axios封裝

UI組件及頁面佈局

UI組件使用優秀的Ant Design 組件庫,注意使用 babel-plugin-import 配置實現組件的按需加載。

本項目的內部頁面佈局採用Antd上經典的佈局方式:

image.png

頁面佈局須要合理拆分模塊,左側菜單導航欄根據靜態菜單渲染。實際完整代碼詳見項目,如下爲BasicLayout組件:

import React from "react";
import { Layout } from "antd";
import SiderMenu from "../SiderMenu";
import MainHeader from "../MainHeader";
import MainFooter from "../MainFooter";

import "./style.less";

const BasicLayout = ({ route, children }) => {
  return (
    <Layout className="main-layout">
      {/* 左側菜單導航 */}
      <SiderMenu routes={route.childRoutes} /> 
      <Layout className="main-layout-right">
        {/* 頂部展現佈局 */}
        <MainHeader></MainHeader>
        <Layout.Content className="main-layout-content">
          {/* 實際頁面佈局 */}
          {children}
          {/* <MainFooter></MainFooter> */}
        </Layout.Content>
      </Layout>
    </Layout>
  );
};

export default BasicLayout;
複製代碼

對於登陸頁等頁面無需套在上面的基本佈局之類,須要單獨處理(菜單配置在BasicLayout配置以前)。

image.png

待完善項:

  • 完善 ESLint+ prettier規範化團隊代碼風格;
  • 引入TypeScript及配置;
  • 根據實際場景提取公共模塊、組件;

最後

項目Git地址:github.com/now1then/re…
文章連接-語雀:www.yuque.com/nowthen/lon…
在線演示地址:

書寫不易,以爲還不錯或者有幫助的童鞋,歡迎關注、多多star;(-.-)

相關文章
相關標籤/搜索