混合开发架构|ReactJs开发全流程|Modern|MobX|Route|axios

Modern手动搭建项目

点击查看项目源码
由于 Modern.js 框架的默认零配置、约定优于配置、开箱即用、避免样板文件、Universal App 等设计,即使不借助任何脚手架、生成器、项目模板等工具,纯手动搭建一个项目,整个过程也是极其简单的。
创建最简单的应用工程,使用Modern.js官网指导
在这里插入图片描述
VSCode终端执行指令pnpm run dev执行成功
在这里插入图片描述

引入三方库依赖

使用npm引入三方库依赖指令,
npm i --save --dev babel-plugin-transform-decorators-legacy@1.3.5 @babel/plugin-proposal-decorators@7.18.10 mobx@6.6.1 mobx-react@7.5.2 axios@0.27.2 antd-mobile@5.21.0 base-64@1.0.0

在执行命令引入依赖时,安装nvm很关键且nvm中已安装的npm版本也很重要。直接影响到你是否能够安装成功上面三方库的依赖。如下展示我个人的
在这里插入图片描述

编码|编译|效果图

经过编码,实现简单效果如图
在这里插入图片描述
ReactJs项目入口App.jsx中修改、调整index.html的body、html属性。以防止UI页面出现白边等丑陋问题。

// App.jsx
import './App.css'
import { Provider } from 'mobx-react'
import RouteVip from './pages/home/index'

/**动态配置body、html样式 */
let bodyStyle = document.body.style;
let htmlStyle = document.getElementsByTagName('html')[0].style;

bodyStyle.margin = 0;
bodyStyle.padding = 0;

htmlStyle.margin = 0;
htmlStyle.padding = 0;

export default function App() {
    return <Provider><RouteVip /></Provider>;
  }

创建全局加载动画,进入页面时刻,请求后台数据并触发加载提示动画,待数据响应并回调完毕关闭动画。定义全局动画组件

import { Mask, SpinLoading } from "antd-mobile";

function LoadingMask (isVisiable = false) {
    const wheight = document.documentElement.clientHeight;

    return <Mask visible={isVisiable} opacity='transparent'>
        <div style={{display:'flex', flex:1, justifyContent:'center', flexDirection:'column',alignItems:'center', height:wheight}}>
            <div style={{display:'flex', flex:1, justifyContent:'center', flexDirection:'column',alignItems:'center', height:'80px', width:'80px', background:'#464646', borderRadius:'6px',borderWidth:'2px'}}>
                <SpinLoading style={{'--size': '32px', '--color': '#efefef'}}></SpinLoading>
            </div>
        </div>
    </Mask>
}

export default LoadingMask;
  • 配置路由react-router-dom功能,抽象出一个层级,home/index.jsx。并为项目第一路由页面配置path='*' ,以防止出现白屏问题,误认为是由于错误Failed to load resource: net::ERR_FILE_NOT_FOUND导致。
  • 引入加载动画组件,并以状态变量loadingShow 控制显、隐全局加载动画组件LoadingMask.js,
  • 通过Mobx统筹当前全局状态state
// home/index.jsx
import { action, observable } from "mobx";
import { inject, observer } from 'mobx-react'
import React,{Component} from "react";
import {BrowserRouter, Route, Routes} from 'react-router-dom'
import VipHome from '../vip-home/index'
import LoadingMask from "../../comps/loading"; // 引入加载动画组件

/**定义被观察者 */
export const homeStore = observable({
    requestQuantity: 0,
    get loadingShow() {
        return this.requestQuantity > 0
    }
})
/**定义Action方法,修改被观察对象 */
homeStore.incrementRequestNum = function () {
    this.requestQuantity++
}

homeStore.decrementRequestNum = function () {
    if (this.requestQuantity > 0) {
        this.requestQuantity--
    }
}

@observer
class RouteVip extends React.Component {
    componentDidMount() {

    }

    render () {
        const {loadingShow} = homeStore;// 解构控制加载动画的变量
        return <div style={{overflow: 'hidden'}}>
            <BrowserRouter>
                <Routes>
                    <Route path='*' element={<VipHome />}></Route>
                </Routes>
            </BrowserRouter> 
            {LoadingMask(loadingShow)/**全局加载动画*/}
        </div>
    }
}

export default RouteVip;
  • 绘制截图UI页面,并完成响应的业务交互逻辑的编码。
  • 引入已封装的axios工具类axiosRequestWithLoading.js,可灵活新增请求配置、灵活请求接口数据、灵活处理请求拦截和响应拦截等。详尽使用可参考代码。
  • 引入静态JSON资源, 这里虽然已创建封装axios但未使用。
  • 封装图片静态资源入口引用——ImageRequire.js
  • 自定义无状态内容组件,可点击按钮BarItemContent
  • Mobx定义当前页面State状态信息为被观察者,及修改状态信息的Action方法。并实现对该页面State信息的统筹管理。
// vip-home/index.jsx
import { action, observable } from "mobx";
import { inject, observer } from 'mobx-react'
import React,{Component} from "react";
import {Button, Dialog, SideBar, Space, NavBar} from 'antd-mobile'
import JSON from './vip-home-json.json'
import ImageRequire from '../../images/imge_require'
import { axiosRequestWithLoading } from "../../api/http/axios-request";
import { valueString } from "./value-str";

export const _height = window.screen.height; // 获取手机屏幕的高
export const _width = window.screen.width;
export const primary_color = '#eb1313' // 默认主题色

/**定义Vip首页被观察者 */
export const vipHomeStore = observable({
    tabList: [],
    slidBarArrayKey: '0001',
    ActiveKeyIndex: 0,
    get activeKeyIndex() { // 定义计算属性
        this.tabList.map((item, index)=> {
            if (item.tabId == this.slidBarArrayKey) {
                this.ActiveKeyIndex = index;
                return index;
            }
        })
    }
})
// 定义action方法
vipHomeStore.updateDataUI = function (tabs) {
    this.tabList = tabs;
}
vipHomeStore.updataActiveKey = function (activeKey) {
    this.slidBarArrayKey = activeKey;
}


@observer
class VipHome extends React.Component {
    componentDidMount() {
        this.updateDataUI(JSON.result.tabList)
    }

    /**解析处理手机传送来的域名 */
    handleDomain () {
        let domain = ''
        const _domainUrl = window.location.href;
        if (_domainUrl && _domainUrl.indexOf('./StaticVIPAcctServ')) {
            const urlIndex = _domainUrl.indexOf('./StaticVIPAcctServ')
            domain = _domainUrl.substring(0, urlIndex);
        }

        return domain;
    }

    /** 执行网络请求 */
    aiosRequestMethod () {
        axiosRequestWithLoading({
            baseURL:this.handleDomain(),
            url: '/VipAcctServ/discovery/remotevip'
        }).then(res => {
            console.log('axiosRequestWithLoading', res);
        })

    }

    backCallNativeMethod() {

    }

    /** 弹窗提示 */
    showErr(content = '', back = true, title = '温馨提示') {
        const showBack = back ? '好的, 点击返回' : '好的'
        Dialog.alert({
            title: title,
            content: content,
            closeOnMaskClick: false,
            closeOnAction: true,
            afterClose: () => back&&this.backCallNativeMethod(),
            confirmText:<div style={{color: primary_color, fontSize:'16px', background:'white'}}>{showBack}</div>
        })

    }

    /**更新UI数据 */
    updateDataUI (tabs) {
        console.log('updateDataUI', tabs)
        if (tabs && tabs instanceof Array && tabs.length > 0) {
            console.log('updateDataUI--更新UI数据', tabs)
            vipHomeStore.updateDataUI(tabs);
        } else {
            this.showErr(valueString.errData)
        }
    }
    updataActiveKey = (key) =>{
        console.log(key)
        vipHomeStore.updataActiveKey(key)
    }

    /** 分享 */
    shareClickCallNativeMethod () {
    }

    render () {
        const tabs = vipHomeStore.tabList;
        const activeKey = vipHomeStore.slidBarArrayKey;
        const defaultKeyIndex = vipHomeStore.activeKeyIndex;
        const tab = tabs[vipHomeStore.ActiveKeyIndex || 0] || {};
        const goodsList = (tab && tab.goodsList) || [];
        return (<div style={{background: '#efefef', width: _width}}>
            <img src={ImageRequire.fillJpegName('ic_logo_v')} style={{display: 'flex', width: _width, height: _height*0.34, tintColor:'white'}}></img>
            <div style={{height:_height - _height*0.34 -60, display: 'flex', justifyContent:'flex-start', alignItems:'stretch', marginTop: '2px'}}>
                <div style={{background:'white', flex:'block-inline',height:_height - _height*0.34 -60, width:'80', paddingTop: '24px', paddingLeft:'10px', borderTopLeftRadius:'12px'}}>
                <SideBar activeKey={activeKey} onChange={key=>this.updataActiveKey(key)}
                    style={{"--background-color":"transparent", "--adm-color-primary":primary_color, '--width':'80px', paddingBottom: '30px', color:'rgba(102,102,102,0.8)'}}>
                        {tabs.map((item, index)=> {return <SideBar.Item key={item.tabId} title={item.tabName}></SideBar.Item>})}
                    </SideBar>
                </div>
                <div style={{display:'flex', justifyContent:'flex-start', alignItems:'flex-start', height:_height-_height*0.34-60,width:_width-90, background:'#ffffff', paddingTop:'20px',paddingBottom: '10px', borderTopRightRadius: '12px'}}>
                    <div><BarItemContent barItems={goodsList} title={tab.tabName||defaultKeyIndex}/></div>
                </div>
            </div>
            <NavBar 
            right={<Space onClick={()=>{}}><img src={ImageRequire.fillPngName('ic_share')} style={{display:'block-inline',width:'26px',height:'26px',marginRight:'22px'}}></img></Space>}
            style={{background:'transparent', display:'flex', position:'absolute',top:2, zIndex:500, width:_width,aliginSelf:'center',fontWeight:'bold',color:'rgba(30,30,30,0.7)'}}>
                {'唯品会'}
            </NavBar>
        </div>);
    }
}


const tabItemClick2MenuMethod = (item) => {

}
/**封装内容组件 */
const BarItemContent = props => {
    const {barItems=[], title=''} = props;
    return (<div style={{background:'#fff'}}>
        <div style={{fontSize:'14px', fontWeight:'inherit', color:'rgba(102,102,102,0.6)', fontWeight:'bold',paddingLeft:'15px'}}>{title}</div>
        <div style={{background:'rgba(299,299,299,0.5)', height:'1px',width:_width-120, marginTop:'8px', marginBottom:'8px'}}></div>
        <Space wrap block justify='around' style={{'--gap-horizontal': '15px'}}>
            {barItems.map(item=><ItemViewClick  key={item.goodId} 
            touchItem={item} onItemClick={()=>tabItemClick2MenuMethod(item)}/>)}
        </Space>
    </div>)
}

/**封装内容组件 */
const ItemViewClick = props => {
    const {touchItem={}, onItemClick=()=>{}} = props;
    return (<Button onClick={onItemClick}
    style={{display:'flex',flexDirection:'row',alignItems:'center',justifyContent:'flex-start','--border-color':'transparent',width: (_width-90)*0.4}}>
        <img src={ImageRequire.fillJpegName(touchItem.goodImage)} style={{display: 'flex', width: (_width-90)*0.4, height: (_width-90)*0.35, backgroundRepeat:'no-repeat'}}></img>
        <div>
            <div style={{display:'inline-block', textAlign:'left',fontSize:'12px',color:'#666',maxLines:1,width:'100px',height:'14px',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{touchItem.goodName}</div>
            <div>
            <span style={{display:'inline',fontSize:'12px',color:'#666'}}>$</span>
            <span style={{fontSize:'14px',color:primary_color}}>31.9</span>
            <span style={{fontSize:'12px',color:'rgba(102,102,102,0.5)',maxLines:1}}>已拼10+</span>
            </div>
        </div>
    </Button>)
}

export default VipHome;

起Nginx服务|打包|发布

在起服务发布ReactJs静态资源时,需要进行打包处理。并对比两种打包结果。

未配置modern.config.js

  • 终端指令打包,pmpm run build,打包结果,其中如下
  • <script defer="defer" src="/static/js/main.77b302e8.js"></script>
    在这里插入图片描述

已配置modern.config.js

配置modern信息,新增js文件modern.config.js配置

import {defineConfig} from '@modern-js/app-tools'
export default defineConfig ({
    output: { // 配置静态资源打包路径
        assetPrefix: '../../'
    },
    tools: { // 清除console日志
        terser: opt => {
            if (typeof opt.terserOptions.compress === 'object') {
                opt.terserOptions.compress.drop_console = true;
              }
        }
    }
})
  • 终端指令打包,pmpm run build,此时打包结果,其中如下
  • <script defer="defer" src="../../static/js/main.dbdf0d63.js">
    在这里插入图片描述

存放资源,起Nginx服务

  • 指令start nginx启动服务,此时Nginx服务目录结构
    在这里插入图片描述
  • 将打包后的ReactJs静态资源拷贝到Nginx服务指定目录,在此状态启动发布即可。此时Nginx服务目录结构
    在这里插入图片描述

巧遇问题|解决方案

在起了服务后,发布的Reactjs内容报错、白屏、无法显示

  • 路由问题,需要有一个兜底策略。进入该工程必显示的首页<Route path='*' element={<VipHome />}></Route>
  • 服务中发布的静态资源,路径问题(非根目录下,须使用相对路径

若显示报错: Failed to load resource: net::ERR_FILE_NOT_FOUND,指定是路径引用问题导致的了。
如何解决?拿上面nginx服务下启动静态资源为例,根源上分析!
第一种:如果不配置modern.config.js,进行打包。打包出的index.html静态资源引用static目录下的静态资源,是这样的<script defer="defer" src="/static/js/main.77b302e8.js"></script>
第二种:配置modern.config.js,进行打包。打包出的index.html静态资源引用static目录下的相对路径下静态资源,是这样的<script defer="defer" src="../../static/js/main.77b302e8.js"></script>

  • 在第一种情况,静态资源在nginx服务发布时,通过http://domain:port/dist/html/main/index.html查看项目显示、白屏、报错。index.html中引用js静态资源路径看,其实是这样的http://domain:port/static/js/main.77b302e8.js",根据实际目录看,肯定要报错、找不到资源的。

  • 在第二种情况,静态资源在nginx服务发布时,通过http://domain:port/dist/html/main/index.html查看项目显示、正常。index.html中引用js静态资源相对路径看,其实是这样的http://domain:port/dist/static/js/main.77b302e8.js",根据实际目录看,是必定能找到资源并显示的。

总之一句话,配置静态资源static目录的路径,使其在加载index.html时能够找到对应静态资源。

完美封装axios

mobx官网

  • 封装axios,请求拦截器、响应拦截,并配合mobx全局牵动加载动画提示;
  • 封装axios,灵活请求头对象封装,默认post,5秒超时。
import axios from "axios";
import { homeStore } from "../../pages/home";
// 定义基础配置
let baseConfig = {
    baseURL: '',
    timeout: 5*1000,
    withCredentials: true,
    responseType: 'json',
    method: 'post',
    headers: { 'Content-Type': 'application/json;charset=UTF-8'}
}

/** 请求接口方法+显示加载动画 */
export function axiosRequestWithLoading(options = {}) {
    baseConfig = { ...baseConfig, ...options };
    // 配置公共信息并创建axios实例
    const instance = axios.create(baseConfig)
    // axios 配置请求拦截器
    instance.interceptors.request.use(config => {
        homeStore.incrementRequestNum();
        return config;
    }, err => {
        homeStore.decrementRequestNum();
        return Promise.reject(err)
    })

    // axios 配置响应拦截器
    instance.interceptors.response.use(response => {
        // 正常请求响应
        homeStore.decrementRequestNum();
        if (response.status == 200) {
            return response.data;
        } else {
            return "";
        }
    }, err => {
        // 异常
        homeStore.decrementRequestNum();
        return Promise.reject(err);
    })

    return instance(options);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值