快速構建Web應用,從零學習React後臺專案模版
想要快速構建實際應用,離不開一個好的應用模版,React作為大廠出品工具,有著穩定性和可維護性的保障,同時可以使用相關的全套全家桶(React + React-router + Axios + Mobx + Antd)進行連貫敏捷開發,本文將從如何在雲開發平臺建立專案應用模版,基於應用模版建立《後臺管理》專案,以及上傳並且通過雲平臺將專案上線部署應用,為專案開發提供更加便捷的操作環境。
一 、通過雲開發平臺快速建立初始化應用
1.建立相關應用模版請參考連結:https://developer.aliyun.com/article/878171?spm=a2c6h.12873581.0.dArticle878171.c61253e8nVBtAv
2.完成建立後就可以在github中檢視到新增的react倉庫
二 、本地編寫《後臺管理》專案
1.將應用模版克隆到本地
• 首先假定你已經安裝了Git、node,沒有安裝請移步node官網進行安裝。克隆專案:
git clone + 專案地址
• 進入專案檔案
cd create-react-app
• 切換到feature/1.0.0 分支上
git checkout feature/1.0.0
• 使用一下命令全域性安裝 React :
npm install -g create-react-app
• 安裝依賴包
npm install
• 啟動服務
npm start
這裡開啟瀏覽器3000埠,並出現預設頁面。
2.架構與效果預覽
• 《後臺管理》專案架構
• 效果預覽
3.初始化專案
• 初始化package.json
npm init
• 安裝webpack
npm add -D webpack webpack-cli webpack-merge
專案中使用的Webpack版本是^5.10.0,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根據開發環境/生產環境進行拆分配置:
Webpack4.0釋出已經很長時間了,相信基本上專案都已遷移至4.0,在這裡就不多贅述了。
• 配置Html模版
安裝:
npm add -D html-webpack-plugin
配置:
const srcDir = path.join(__dirname, "../src");
plugins: [
new HtmlWebpackPlugin({
template: `${srcDir}/index.html`
})
]
• 配置本地服務及熱更新
安裝:
npm 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
安裝:
npm 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樣式和圖片等資源
安裝:
npm 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多執行緒打包
安裝:
npm 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 配合,規範化團隊專案程式碼開發,統一程式碼風格。
npm 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目錄 下 https://github.com/now1then/react-web-pro/tree/master/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"
},
...
}
命令列執行:
// 命令列執行
// 執行開發環境;
npm start
// 生產環境打包壓縮;
npm build
// 圖形化分析打包檔案大小;
npm build:report
// 方便排查生產環境打包後文件的錯誤資訊(檔案source map);
npm 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());
}
• 專案程式碼架構
npm add react react-dom react-router-dom mobx mobx-react mobx-react-router
axios antd moment
4.函式化Hooks
當前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);
5.Router路由配置
專案是單頁應用,路由配置一般分為約定式動態路由和集中配置式路由。
在 React 的世界裡,直接採用成熟的react-router工具管理頁面路由。我們現在說到react-router,基本上都是在說 react-router 的第4版之後的版本,當前的最新版本已經更新到5.1.x了。
當前react-router支援動態路由,完全用React元件來實現路由,在渲染過程中動態設定路由規則,匹配命中規則載入對應頁面元件。
本專案採用集中配置式路由(方便路由鑑權、從服務端介面獲取選單路由配置等),同時兼顧方便地設定側邊選單欄。 當然為簡單起見,專案中讀取本地靜態選單配置,也暫未引入路由鑑權。
6.靜態路由配置 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;
上面是靜態路由的一部分配置,
注意:中會用包裹,會匹配命中的第一個。"/login"等沒有選單導航欄等基本佈局的頁面, 要放在基本佈局BasicLayout之前。
利用和React.lazy()實現頁面元件懶載入。
7.路由組建渲染 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;
8.路由 hooks 語法
react-router-dom 也已經支援 hooks語法,獲取路由資訊或路由跳轉,可以使用新的hooks 函式:
• useHistory:獲取歷史路由,回退、跳轉等操作;
• useLocation:檢視當前路由資訊;
• useParams:讀取路由附帶的params引數資訊;
• 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>
);
}
9.結合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);
以上為部分演示程式碼,具體業務實現可以檢視專案程式碼。
10.Axios Http 請求封裝
Axios請求封裝,具體程式碼見 /src/services/newRequest.js
11.UI元件及頁面佈局
UI元件使用優秀的Ant Design 元件庫,注意使用 babel-plugin-import 配置實現元件的按需載入。
本專案的內部頁面佈局採用 Antd 上經典的佈局方式:
頁面佈局需要合理拆分模組,左側選單導航欄根據靜態選單渲染。實際完整程式碼詳見專案,以下為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配置之前)。
三 、雲端一鍵部署上線應用
1.上傳程式碼
git add .
git commit -m '新增你的註釋'
git push
2.在日常環境部署
一鍵進行應用部署。在應用詳情頁面點選日常環境的「部署」按鈕進行一鍵部署,部署狀態變成綠色已部署以後可以點選訪問部署網站檢視效果。
3.配置自定義域名在線上環境上線
• 配置線上環境自定義域名。在功能開發驗證完成後要在線上環境進行部署,在線上環境的「部署配置」-「自定義域名」中填寫自己的域名。例如我們新增一個二級域名 company.workbench.fun 來繫結我們部署的前端應用。然後複製自定義域名下方的API閘道器地址對新增的二級域名進行CNAME配置。
• 配置CNAME地址。複製好 API閘道器域名地址後,來到你自己的域名管理平臺(此示例中的域名管理是阿里雲的域名管理控制檯,請去自己的域名控制檯操作)。新增記錄的「記錄型別」選擇「CNAME」,在「主機記錄」中輸入你要建立的二級域名,這裡我們輸入「company」,在「記錄值」中貼上我們之前複製的 API閘道器域名地址,「TTL」保留預設值或者設定一個你認為合適的值即可。
• 在線上環境部署上線。回到雲開發平臺的應用詳情頁面,按照部署的操作,點選線上環境的「部署按鈕」,部署完成以後就在你自定義的域名進行了上線。CNAME 生效之後,我們輸入 company.workbench.fun(示例網址) 可以開啟部署的頁面。至此,如何部署一個應用到線上環境,如何繫結自己的域名來訪問一個線上的應用就完成了,趕緊部署自己的應用到線上環境,用自己的域名玩起來吧 ;)
一鍵建立React應用模版連結 :https://workbench.aliyun.com/application/front/create?fromConfig=12&fromRepo=sol_github_12