Authored by Eirice
项目背景:特殊人群管控监测模型
涉及技术应用:react 11.6,mobx,webpack,mock,mapbox,v-block-lite
项目地址:https://github.com/Eirice01/exhuman
下载地址:https://github.com/Eirice01/exhuman.git
建议使用chrom70+,npm
webpack文件配置
const path = require('path');
const fse = require('fs-extra');
const ROOT = (...args) => path.resolve.apply(null, [__dirname, '../'].concat(args ? args : []));
const SRC = (...args) => path.resolve.apply(null, [__dirname, '../', 'src'].concat(args ? args : []));
const _src = SRC();
const _modules = SRC('modules');
const getDirectorys = base => fse.readdirSync(base).reduce((acc, name) => {
const item = path.join(base, name);
const stat = fse.statSync(item);
return stat.isDirectory() && acc.push({ name, item }), acc;
}, []);
const FRAMESET = getDirectorys(_src);
const MODULEs = getDirectorys(_modules);
const getFiles = (route, filter) => {
if(!fse.pathExistsSync(route)) return null;
return fse.readdirSync(route).filter(filter || (() => true)).map(file => path.join(route, file));
}
module.exports = {
ROOT, SRC, getFiles, FRAMESET, MODULEs
};
webpack基础配置
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { name } = require('../package.json');
const { ROOT } = require('./tools');
const { CONFIG, _src, _mocks } = require('./config-common');
const entry = { app: ROOT('src', 'index.js') };
const devServer = {
host: '0.0.0.0',
open: false,
progress: true,
compress: false,
stats: 'errors-only',
port: 3001,
disableHostCheck: true,
contentBase: [_src, _mocks],
proxy: {
}
};
const plugins = origin => [
new webpack.EnvironmentPlugin({
USE_MOCK_SERVICE: true
}),
...origin,
new HtmlWebpackPlugin({
title: name,
template: ROOT('public', 'index.html'),
inject: 'body',
chunks: ['app']
})
];
const configs = CONFIG({
mode: 'development',
entry,
devtool: "source-map",
target: 'web',
node:{fs:"empty"},
devServer,
plugins
});
module.exports = configs;
webpack config.js
const { ROOT, SRC, FRAMESET } = require('./tools');
const [_root, _src, _index, _dist, _mocks] = [ROOT(), SRC(), SRC('index.js'), ROOT('dist'), ROOT('mocks')];
const alias = FRAMESET.reduce((a, { name, item }) => (a[`@${name}`] = item, a) , {
'@map-data': SRC('assets', 'map-data')
});
console.log(SRC('assets', 'svg-react'))
const rules = {
_items_: {
eslint: {
test: /\.(js|conf)$/,
enforce: 'pre',
use: 'eslint-loader',
exclude: /node_modules/
},
babel: {
test: /\.(js|conf)$/,
use: 'babel-loader',
exclude: /node_modules/
},
css: {
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
less: {
test: /\.less$/,
use: ['style-loader', { loader: 'css-loader', options: { sourceMap: false } }, 'less-loader']
},
png_jpg_etc: {
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
use: [{ loader: 'url-loader', options: { limit: 100 } }],
exclude: [SRC('assets', 'svg-react'),SRC('assets', 'map-img')]
},
map_img_etc: {
test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/,
use: [{ loader: 'url-loader', options: { limit: 8192 } }],
include: SRC('assets', 'map-img')
},
svg: {
test: /\.svg$/,
include: SRC('assets', 'svg-react'),
use: [ "babel-loader",
{
loader: "react-svg-loader",
options: {
svgo: {
plugins: [
{ removeTitle: false }
],
floatPrecision: 2
}
}
}
]
}
},
get: function(name) {
return this._items_[name];
},
set: function(name, rule) {
const raw = this.get(name) || {};
if(!rule)
delete this._items_[name];
else
this._items_[name] = { ...raw, ...rule };
},
toArray() {
return Object.keys(this._items_).map(i => this._items_[i]);
}
};
const externals = ['antd']
// WEBPACK COMMONS CONFIGURATIONS
const commons = {
entry: ['@babel/runtime-corejs2', _index],
output: {
path: _dist,
filename: '[name].js'
},
devtool: 'cheap-module-eval-source-map',
resolve: {
alias,
symlinks: false,
modules: [_src, 'node_modules'],
extensions: ['*', '.js', '.json']
},
module: {
rules: rules.toArray()
},
plugins: [ ],
stats: {
colors: true,
entrypoints: false
},
externals
};
const CONFIG = (options) => options ? Object.keys(options).reduce((commons, name) => {
const common = commons[name];
const custom = options[name];
if(typeof custom === 'function') {
commons[name] = custom(common);
return commons;
}
if(custom) {
commons[name] = custom;
return commons;
}
return commons;
}, commons) : commons
module.exports = {
CONFIG,
_root, _src, _index, _dist, _mocks,
rules
};
mock-service
// For better Performance and Compatibility, write code with ES5, Please!
// /abc/dkas/kdla
/* eslint-disable */
const MOCK_SERVICE_BASE = new URL(location).searchParams.get('mock-service-base');
const version = "great-adventure" + ' T_' + (Number(Date.now()).toString(36));
const prefetch = {
name: version + ' - prefetch',
list: []
};
const cachePatterns = null;
const useMocksServer = true;
const mockRequestPatterns = [];
const fetchOptions = {};
const REG_REFERRER = /^http(s)?:\/\/[^\/]+\//;
const mockFetchOptions = {
mode: 'no-cors',
headers: {
"Accept": "mock-server/data, */*"
}
};
self.addEventListener('install', function(evt) {
if (typeof self.skipWaiting === 'function') {
evt.waitUntil(self.skipWaiting());
}
evt.waitUntil(precache(prefetch.name, prefetch.list));
});
self.addEventListener('activate', function(evt) {
if(self.clients && (typeof self.clients.claim === 'function')) {
evt.waitUntil(self.clients.claim());
}
evt.waitUntil(updateCache([prefetch.name]));
});
self.addEventListener('fetch', function(evt) {
if(useMocksServer && matchMockRequests(evt.request)) {
evt.respondWith(fromMockService(evt.request));
}
});
function precache() {
return caches.open(prefetch.name).then(function(cache) {
return cache.addAll(prefetch.list);
});
}
function updateCache(profileNames) {
return caches.keys().then((cacheNames) => {
return Promise.all(cacheNames.map(cahceName => {
if(profileNames.indexOf(cahceName) === -1) {
return caches.delete(cahceName);
}
}));
});
}
function retrieves(request) {
if(useMocksServer && matchMockRequests(request)) {
return fromMockService(request);
}
return fromCache(request).then(matching => matching).catch(() => fromNetwork(request));
}
function fromCache(request) {
return caches.open(prefetch.name).then(cache => {
return cache.match(request.clone()).then(function (matching) {
return matching || Promise.reject('no-match');
});
});
}
function fromNetwork(request) {
return fetch(request.clone(), fetchOptions).then(response => {
if(response.ok) {
cacheFetch(request.clone(), response.clone());
}
return response || Promise.reject('network-error');
});
}
function cacheFetch(request, response) {
if(matchFile(request.url)) {
caches.open(prefetch.name).then(function(cache) {
return cache.put(request, response);
})
}
}
function matchFile(url) {
return cachePatterns && cachePatterns.some(p => p.test(url));
}
function matchRacers(url) {
return racerPatterns && racerPatterns.some(p => p.test(url));
}
function matchMockRequests(request) {
const url = request.url;
const headers = request.headers;
return headers.get('use-mock-service') === 'true' ? true : false;
}
function fromMockService(request) {
const url = request.url.split(request.referrer);
const method = request.method.toLowerCase();
const type = '.json';
if(url.length !== 2) {
return Promise.reject('mock-service-error');
}
const origin = url.pop();
const routes = origin.split('/');
const max_id = routes.length - 1;
let target = '/';
routes.some((r, i) => i === max_id ? (target += `${method}.${r}`, target) : (target += `${r}/`, false));
let resource = (target + type).replace(/\?/g, '.').replace(/&/g, '.').replace(/=[\w%\d]+/g, '');
console.warn(`\t[USE-MOCK-SERVICE]\t
\t[${method.toUpperCase()}] /${decodeURIComponent(origin)}
\t[${'>'.repeat(method.length)}] ${decodeURIComponent(resource)}`);
resource = MOCK_SERVICE_BASE === 'undefined' ? resource : MOCK_SERVICE_BASE + resource;
return fetch(new URL(resource, request.referrer), mockFetchOptions).then(response => response || Promise.reject('mock-service-error'));
}
mobx 对当前store作为特例说明
import { observable, action, runInAction} from 'mobx'
import { fetchMapStyle } from '@services/people.js'
class PersonStore {
@observable
personInfo={
fuullName:'',
name:'',
tel:'',
percent:''
};
@observable.ref mapStyleJSON = null;
constructor() {
this.fetchMapJSON();
const infoData = sessionStorage.getItem('infoData');
if (infoData) {
this.personInfo = JSON.parse(infoData)
} else {
this.personInfo={
fuullName:'',
name:'',
tel:'',
percent:''
};
}
}
@action getPersonInfo(data){
this.personInfo.name=data.name
this.personInfo.tel=data.tel
this.personInfo.percent=data.percent
this.personInfo.fuullName=data.fuullName
sessionStorage.setItem('infoData',JSON.stringify(data))
}
@action
async fetchMapJSON(){
this.mapStyleJSON = {};
try{
const mapJSON = await fetchMapStyle();
runInAction(
() => {
this.mapStyleJSON = mapJSON;
}
)
} catch (err) {
console.log(err)
}
}
}
const store = new PersonStore();
export default store;
mobx使用可以对照redux或者vux
@observable //监视器
构建监听的实体属性;或者数据对象;类似于前两者的store;
constructor //构造器 对@observable 的深度处理
@action //对对@observable计算处理
这里要特别注意的是@observable 会对当前构造对象的每一个实体都进行监听;但和react及vue一样它在页面刷新时也会丢失数据所以可以结合sessionStorage持久化处理;
@observable监听的对象常常是默认深度监听;举个例子:fetchMapJSON()是我们通过服务要获取的一个地图实体对象data{.....};如何使用传统的定义方式;那么拿到的返回值会被监听后解析成poxy:{{h},{d},{d}},对对象的每一个实体都会监听从而破话我们真正想要的数据格式;故在定义的时候要使用:
> @observable.ref mapStyleJSON = null; 改变其监听的模式;
import { HGroup, VGroup } from 'v-block.lite/layout' 这里是采用fix布局会使用的高效的ui布局插件
HGroup //水平
VGroup //垂直 所有的页面均可由这两个标签结合简单的基础标签完成 摆脱传动布局繁重的css及dom编写
react 路由
import './app.less'
import React from 'react'
import { HashRouter as Router, Switch, Route} from 'react-router-dom'
import { HGroup, VGroup } from 'v-block.lite/layout'
import Statics from './statics'
import Details from './details';
import MapBox from './mapbox';
import HeaderView from '../modules/header'
import Find from '../modules/popFind'
import Control from '../modules/popControl'
import Person from '../modules/person'
import Result from '../modules/result'
const Header = ({ title }) => (
<HGroup className="header" verticalAlign="center" padding="5px 15px">
<HeaderView></HeaderView>
</HGroup>
);
const Main = ({ children }) => (
<HGroup className="main" verticalAlign="stretch" gap={5} flex style={{height:"calc(100% - 90px)",overflow:"hidden"}}>
<Statics/>
<MapBox/>
<Details/>
</HGroup>
);
const App = () => (
<Router>
<VGroup height="100%" horizontalAlign="stretch" gap={5}>
<Header/>
<Switch>
<Route path="/" component={Main} exact/>
<Route path="/find" component={Find}/>
<Route path="/control" component={Control}/>
<Route path="/person" component={Person}/>
<Route path="/result/:type" component={Result}/>
</Switch>
</VGroup>
</Router>
);
export default App
mapbox 高性能地图 这里使用的是模块化的init方式,其实还可以使用reactmapbox因为个人习惯里这种方式故而才用的是init方式
import React, { Component } from "react";
import { HGroup,} from 'v-block.lite/layout'
import mapBoxGl from 'mapbox-gl'
import 'mapbox-gl/dist/mapbox-gl.css';
export default class MapContent extends Component{
constructor(props){
super(props)
this.state={
modalInfo:props.stateData,
linData:[],
}
}
changeMyMapData=(data,line)=>{
let linData=[];
const datas=this.props.MapStyleData
const domId=this.props.domId
this.setState({modalInfo:data},()=>{
})
this.setState({linData:line}, ()=> {
})
this.initMap(domId,datas,data,linData);
}
changeLineData=()=>{
const linData=this.state.linData;
const data=this.props.MapStyleData
const domId=this.props.domId
const stateData=this.state.modalInfo
this.initMap(domId,data,stateData,linData);
}
componentDidMount() {
const linData=this.state.linData;
const data=this.props.MapStyleData
const domId=this.props.domId
const stateData=this.state.modalInfo
this.initMap(domId,data,stateData,linData);
}
initMap(domId,data,stateData,linData){
const map = new mapBoxGl.Map({
container: document.getElementById(domId),
style: data,
zoom: 13.5,
center:stateData.centers
});
mapBoxGl.accessToken = "";
if(linData.length==0){
map.on("load", function() {
// ["accommodation","bus-station","finance","government","hospital","house","life-service","oil-station","railway-station","school","travel"].map(v => {
//
// if(poiStyle2[v]) map.addLayer(poiStyle2[v])
// })
});
}else {
map.on("load", function() {
// ["accommodation","bus-station","finance","government","hospital","house","life-service","oil-station","railway-station","school","travel"].map(v => {
//
// if(poiStyle2[v]) map.addLayer(poiStyle2[v])
// })
var coordinates=linData.features[0].geometry.coordinates;
linData.features[0].geometry.coordinates=[coordinates[0]]
map.addSource('trace-source',{
"type":"geojson",
"data":linData
})
map.addLayer({
"id":"trace",
"type":"line",
"source":"trace-source",
"paint":{
"line-color":"#e5ab3c",
"line-opacity":1,
"line-width":2
}
})
map.jumpTo({'center':coordinates[0],'zoom':13})
map.setPitch(30)
var i=0
var timer=window.setInterval(function () {
if(i<coordinates.length){
linData.features[0].geometry.coordinates.push(coordinates[i]);
map.getSource('trace-source').setData(linData)
map.panTo(coordinates[i]);
i++;
}else {
window.clearInterval(timer)
}
},1000)
});
}
//add markers to map
stateData.features.forEach(function(marker) {
var popup= new mapBoxGl.Popup({
closeButton:false,
closeClick:false,
offset:25
})
// create a DOM element for the marker
var partBox=document.createElement('div');
partBox.style.display='flex';
partBox.style.flexDirection='column';
partBox.style.justifyContent='center';
partBox.style.alignItems="center"
var pEl=document.createElement('p');
var el = document.createElement('div');
el.className = 'marker';
el.style.backgroundImage = `url(${require("../../assets/images/"+marker.properties.icon)})`;
el.style.backgroundSize='100% 100%'
el.style.width = marker.properties.iconSize[0] + 'px';
el.style.height = marker.properties.iconSize[1] + 'px';
pEl.innerHTML=marker.properties.description
pEl.style.marginTop="5px";
pEl.style.padding="2px 5px";
pEl.style.backgroundColor="#015c97"
pEl.style.borderRadius='5px'
pEl.style.textAlign='center'
partBox.appendChild(el)
partBox.appendChild(pEl)
partBox.addEventListener('mouseenter',(e) => {
e.stopPropagation()
var infoBox=document.createElement('div');
var el1=document.createElement('p');
var el2 = document.createElement('p');
var el3=document.createElement('p');
el1.innerHTML='开始时间:'+marker.properties.message.startTime;
el2.innerHTML='结束时间:'+marker.properties.message.endTime
el3.innerHTML='地址:'+marker.properties.message.address
infoBox.style.padding='15px 10px';
infoBox.style.backgroundColor='#033d66';
infoBox.style.opacity='0.5';
infoBox.appendChild(el1);
infoBox.appendChild(el2);
infoBox.appendChild(el3);
popup.setLngLat(marker.geometry.coordinates)
popup.setDOMContent(infoBox)
popup.addTo(map);
})
partBox.addEventListener('mouseleave',(e)=>{
e.stopPropagation()
popup.remove()
})
new mapBoxGl.Marker(partBox)
.setLngLat(marker.geometry.coordinates)
.addTo(map);
});
}
render(){
return(
<div>
<HGroup className="btns"><span style={{display:'block',padding:'2px 5px',color:'#4eddc0',borderRadius:'3px',background:'#034e83'}}onClick={()=>this.changeLineData()}>轨迹回放</span></HGroup>
</div>)
}
}
这里是对地图的渲染,及数据跟新路径操作,mark标记操作,轨迹回放操作
注意:
container: document.getElementById(domId), //实例地图的容器
style: data, //核心这是是加载地图的样式数据可在官网查看具体也可以查看src/services/mapBoxSyle/ 该目录下的jinan.json;使用这个数据也可以渲染
zoom: 13.5, //初始化时的缩放
center:stateData.centers //地图的中心坐标
具体其他参数可以自行度娘 mapbox
该项目为模型类型无后台支撑纯前端构成;数据采用按需模块定制手动加载;后期改后台服务只需改掉service下请求方式
emotionalState(type){
let baseUrl="./msgciyun/"
return require(baseUrl+type+'-msg_state.json')
},