点击查看项目源码
由于 Modern.js 框架的默认零配置、约定优于配置、开箱即用、避免样板文件、Universal App 等设计,即使不借助任何脚手架、生成器、项目模板等工具,纯手动搭建一个项目,整个过程也是极其简单的。
创建最简单的应用工程,使用Modern.js官网指导
VSCode终端执行指令pnpm run dev
执行成功
使用npm引入三方库依赖指令,
npm i --save --dev [email protected] @babel/[email protected] [email protected] [email protected] [email protected] [email protected] [email protected]
在执行命令引入依赖时,安装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;
axios
工具类axiosRequestWithLoading.js,可灵活新增请求配置、灵活请求接口数据、灵活处理请求拦截和响应拦截等。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;
在起服务发布ReactJs静态资源时,需要进行打包处理。并对比两种打包结果。
配置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;
}
}
}
})
在起了服务后,发布的Reactjs内容报错、白屏、无法显示
}>
若显示报错: Failed to load resource: net::ERR_FILE_NOT_FOUND
,指定是路径引用问题导致的了。
如何解决?拿上面nginx服务下启动静态资源为例,根源上分析!
第一种:如果不配置modern.config.js,进行打包。打包出的index.html静态资源引用static目录下的静态资源,是这样的。
第二种:配置modern.config.js,进行打包。打包出的index.html静态资源引用static目录下的相对路径下静态资源,是这样的。
在第一种情况,静态资源在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时能够找到对应静态资源。
mobx官网
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);
}