先上效果
一、cesium在react-create-app中的引用
首先
yarn add cesium然后yarn add copy-webpack-plugin -D然后yarn add customize-cra react-app-rewired --dev
设置了customize-cra react-app-rewired就可以改写webpack
新建一个这个文件,在里面改写webpack
const {
override,
addWebpackPlugin,
} = require("customize-cra");
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const webpack = require("webpack");
const cesiumSource = 'node_modules/cesium/Source';
const cesiumWorkers = '../Build/Cesium/Workers';
module.exports = override(
addWebpackPlugin(
new CopyWebpackPlugin({
patterns: [
{ from: path.join(cesiumSource, cesiumWorkers), to: 'cesium/Workers' },
{ from: path.join(cesiumSource, 'Assets'), to: 'cesium/Assets' },
{ from: path.join(cesiumSource, 'Widgets'), to: 'cesium/Widgets' }
],
})
),
addWebpackPlugin(
new webpack.DefinePlugin({
// Define relative base path in cesium for loading assets
CESIUM_BASE_URL: JSON.stringify("/cesium"),
})
)
// addWebpackPlugin(new BundleAnalyzerPlugin())
);
package.json里的打包脚本变成
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
},
这样就会走我们新设定的webpack,将将node_modules/cesium/Source/Assets, node_modules/cesium/Source/Widgets, node_modules/cesium/Build/Cesium/Workers自动文件打包到build文件里,如图
这样yarn start 或者yarn build就可以正常使用cesium了
下面这篇我直接按可发布到掘金的技术文章结构帮你整理好了:有背景、有思路、有代码拆解、有优化建议,基本不需要再大改就能发 👍
二、创建 Viewer 且必须只创建一次!
否则:
- 内存暴涨
- WebGL context 丢失
- 页面卡死
useEffect(() => {
if (!containerRef.current) return;
if (viewerRef.current) return;
const viewer = new Cesium.Viewer(containerRef.current, {
timeline: false,
animation: false,
infoBox: false,
fullscreenButton: false,
});
viewerRef.current = viewer;
return () => {
viewerRef.current?.destroy();
viewerRef.current = null;
};
}, []);
<div ref={containerRef} className="cesium-container" />
这是 Cesium + React 的标准写法。
三、加载倾斜摄影模型(3DTiles)
Cesium.Cesium3DTileset.fromUrl("tileset.json")
.then((tileset) => {
viewer.scene.primitives.add(tileset);
viewer.zoomTo(tileset);
});
一个强烈建议 ⭐⭐⭐⭐⭐
建议监听瓦片失败:
tileset.tileFailed.addEventListener((error) => {
console.error("瓦片加载失败:", error);
});
否则生产环境排查问题会非常痛苦。
四、动态绘制监测点(核心)
很多人喜欢用:
👉 Primitive
👉 PointPrimitive
但在业务系统中,我更推荐:
⭐ Entity
因为:
- 开发简单
- 支持属性绑定
- 支持 pick
- 易维护
function createColorIcon(color: string) {
const canvas = document.createElement("canvas");
const size = 48;
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
if (ctx) {
const cx = size / 2;
const r = 12;
const tipY = size - 6;
// 图钉形状(圆 + 尖)
ctx.beginPath();
ctx.moveTo(cx, tipY);
ctx.quadraticCurveTo(cx + r, r + 12, cx + r, r + 4);
ctx.arc(cx, r + 4, r, 0, Math.PI, true);
ctx.quadraticCurveTo(cx - r, r + 12, cx, tipY);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.fill();
}
return canvas;
}
const getWarningColor = (p: any) => {
const level =
p?.alarmLevel ?? 0;
switch (Number(level)) {
case 4:
return "#D7263D99"; // 红色预警(约 60% 不透明)
case 3:
return "#FF6B0099"; // 橙色预警
case 2:
return "#C7A20099"; // 黄色预警
case 1:
return "#00BEFF99"; // 蓝色预警
default:
return "#d3f26199"; // 约 60% 不透明
}
};
useEffect(() => {
const viewer = viewerRef.current;
if (!viewer) return;
if (dataSource && dataSource?.length > 0) {
pointEntityIdsRef.current.forEach((id) => {
try {
viewer.entities.removeById(id);
} catch (e) {
// ignore
}
});
pointEntityIdsRef.current = [];
console.log('dataSource', dataSource);
(dataSource || []).forEach((p: any) => {
const lng = Number(p.longitude);
const lat = Number(p.latitude);
if (Number.isNaN(lng) || Number.isNaN(lat)) return;
const id = `point-${p.id}`;
viewer.entities.add({
id,
position: Cesium.Cartesian3.fromDegrees(lng, lat, Number(p.height) || 0),
billboard: {
image: createColorIcon(getWarningColor(p)),
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
// 防止被倾斜摄影挡住
disableDepthTestDistance: Number.POSITIVE_INFINITY
},
label: {
text: p.pointName || "",
font: "bold 15px sans-serif",
fillColor: Cesium.Color.fromCssColorString("rgba(68, 229, 255, 0.92)"),
outlineColor: Cesium.Color.fromCssColorString("rgba(124, 121, 121, 0.9)").withAlpha(0.85),
outlineWidth: 2,
style: Cesium.LabelStyle.FILL_AND_OUTLINE,
verticalOrigin: Cesium.VerticalOrigin.BOTTOM,
pixelOffset: new Cesium.Cartesian2(0, -50),
disableDepthTestDistance: Number.POSITIVE_INFINITY,
distanceDisplayCondition: new Cesium.DistanceDisplayCondition(
0,
5000
),
},
properties: {
pickable: true,
pointId: p.id,
pointType: p?.pointType,
pointName: p?.pointName,
}
});
pointEntityIdsRef.current.push(id);
});
const handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((movement: any) => {
const picked = viewer.scene.pick(movement.endPosition);
if (
Cesium.defined(picked) &&
picked.id && // Entity
picked?.id?.properties?.pickable?.getValue()
) {
viewer.canvas.style.cursor = 'pointer';
} else {
viewer.canvas.style.cursor = 'default';
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
// 点击测点
handler.setInputAction((click: any) => {
const picked = viewer.scene.pick(click.position);
if (
Cesium.defined(picked) &&
picked.id &&
picked.id.billboard
) {
const entity = picked.id;
console.log('点到了:', entity.id, picked?.id?.properties?.pointId?.getValue());
// 示例:相机飞过去
viewer.flyTo(entity);
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
}, [dataSource]);
五、点击测点飞行定位
pick 实体
const picked = viewer.scene.pick(click.position);
判断:
if (Cesium.defined(picked) && picked.id?.billboard) {
然后:
viewer.flyTo(entity);
体验直接拉满。
鼠标 Hover 手型
细节决定高级感:
viewer.canvas.style.cursor = 'pointer';
六、相机环绕动画
核心思路:
👉 clock.onTick
const remove = viewer.clock.onTick.addEventListener(() => {
heading += Cesium.Math.toRadians(0.2);
viewer.camera.lookAt(
center,
new Cesium.HeadingPitchRange(
heading,
Cesium.Math.toRadians(-30),
range
)
);
// 转满一圈停止
if (heading >= Cesium.Math.TWO_PI) {
remove();
viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);
}
});
本质:
👉 每帧改变 heading。