阅读视图
证监会启动上市公司治理专项行动
财政部对中兴财光华作出行政处罚决定
上市前SpaceX业绩被曝光,受xAI拖累2025年亏损近50亿美元
亚行上调韩国今年经济增长预期0.2个百分点至1.9%
深入理解浏览器渲染流程
深入理解浏览器渲染流程
0. 事件循环复习
我们之前总结过:事件循环是主线程的工作方式,每执行完一个宏任务,就清空所有微任务,然后可能渲染页面,再取下一个宏任务。
重点来了:渲染到底是怎么发生的? 这就是本篇文章要讲的内容。
1. 为什么需要了解渲染流程?
你每天都在写 HTML、CSS、JS,但浏览器到底是怎么把它们变成屏幕上像素的?
搞懂渲染流程,你就能明白:
- 为什么改
left/top会卡,改transform却很丝滑 - 为什么有些 CSS 属性改了开销大,有些开销小
- 面试官问“重排重绘”时该怎么答
这是前端性能优化的基础,也是面试必考题。
2. 渲染流程五步走
浏览器拿到 HTML 和 CSS 后,会按顺序做这 5 件事:
| 步骤 | 名称 | 做了什么 |
|---|---|---|
| 1 | 构建 DOM 树 | 把 HTML 标签转成树形结构 |
| 2 | 构建 CSSOM 树 | 把 CSS 规则转成树形结构 |
| 3 | 构建渲染树 | 合并 DOM 和 CSSOM,过滤掉不可见元素 |
| 4 | 布局(Layout) | 计算每个元素的位置和大小 |
| 5 | 绘制(Paint) | 把像素画到屏幕上 |
第 4 步也叫 重排(Reflow),第 5 步也叫 重绘(Repaint)。
如下图:
2.1 构建 DOM 树
浏览器从上到下解析 HTML,把标签转成树形结构的 DOM 对象。
例如:
<html>
<body>
<div>hello</div>
</body>
</html>
会变成类似这样的结构(伪代码):
document
└ html
└ body
└ div → text "hello"
注意:<script> 标签会阻塞解析,因为 JS 可能修改 DOM。可以加 defer 或 async 避免阻塞。
2.2 构建 CSSOM 树
浏览器解析 CSS 文件或 <style> 标签内的样式,构建成 CSSOM 树(CSS 对象模型)。
CSSOM 记录了选择器与样式规则的对应关系,以及继承关系(比如 body 的 font-size 会传给子元素)。
CSS 不会阻塞 DOM 树的构建,但会阻塞渲染(因为需要完整的样式才能绘制)。
2.3 构建渲染树(重点)
渲染树 = DOM 树 + CSSOM 树,但会过滤掉不需要显示的东西。
具体操作:
-
只保留能看见的元素
-
display: none的元素不进入渲染树(连占位都没有) -
<head>标签里的元素不进入渲染树 -
visibility: hidden的元素会进入渲染树(它占位置,只是看不见) -
opacity: 0的元素也会进入渲染树(透明也是可见的一种)
-
-
给每个节点附上计算好的样式
从 CSSOM 里找到匹配的规则,经过层叠、继承、优先级计算,得到每个节点的最终样式。
示例:
<div style="display: none;">看不见我</div>
<div>看得见我</div>
渲染树里只有第二个 div,第一个直接被丢掉了。
为什么需要渲染树?
因为 DOM 树里有很多不参与页面绘制的节点(head、script、display: none 的元素),直接拿着 DOM 树去布局会浪费性能。渲染树就是“最终要画到屏幕上的东西”的清单。
2.4 布局(Layout / 重排)
遍历渲染树,计算每个元素在屏幕上的精确位置和尺寸(宽、高、x、y)。
比如一个 div 宽度是父容器的 50%,就要算出实际像素值。
触发布局的情况:
- 首次渲染
- 窗口
resize - 修改元素的几何属性**(宽/高/边距/位置)**
- 添加/删除 DOM
- 读取某些属性(
offsetHeight、getComputedStyle等)
布局是开销最大的步骤。
2.5 绘制(Paint / 重绘)
把每个元素画成像素:背景、边框、文字、阴影、图片等。
浏览器会把页面分成多个图层,分别绘制,最后合成。
触发绘制的情况:
- 改变背景色、文字颜色、边框颜色等(不影响位置)
3. 重排 vs 重绘(核心重点)
这两个概念必须分清。
| 对比项 | 重排(Reflow) | 重绘(Repaint) |
|---|---|---|
| 什么时候发生 | 改宽高、边距、位置、增删 DOM、改字体等 | 改颜色、背景、阴影、可见性等 |
| 开销 | 很大(重新计算位置) | 中等(只重新涂色) |
| 会触发另一个吗 | 会,重排一定导致重绘 | 不会,重绘不一定导致重排 |
| 优化建议 | 尽量避免,或用 transform 替代 |
可接受,但不要频繁 |
3.1 代码示例
// 坏:触发重排
box.style.width = '200px'
box.style.height = '200px'
box.style.margin = '10px'
// 好:合并修改,只触发一次重排
box.style.cssText = 'width:200px; height:200px; margin:10px;'
// 更好:用 transform 做动画,完全不触发重排/重绘
box.style.transform = 'translateX(100px)'
4. 哪些操作会触发重排?
- 改
width/height/margin/padding/border - 改
font-size(文字大小影响盒子大小) - 改
display(比如none→block) - 添加或删除 DOM 元素
- 改变窗口大小
- 读取某些属性:
offsetHeight、offsetTop、scrollTop、getComputedStyle等(浏览器被迫立即重排)
最后一条只是读一下,浏览器也得乖乖重排才能给你准确值。所以不要在循环里读这些属性。
5. 如何减少重排?
| 优化手段 | 说明 |
|---|---|
| 合并样式修改 | 用 cssText 或切换 class,不要一条一条改 |
| 让元素脱离文档流 |
position: absolute 或 fixed,它的重排不影响别人 |
| 批量插入 DOM | 用 documentFragment 先组装好,再一次性插入 |
动画用 transform
|
transform 走合成线程,不触发重排/重绘 |
| 避免读触发布局的属性 | 不要频繁读 offsetHeight 等,如果必须读,先读好存起来 |
5.1 批量插入 DOM 示例
// 坏:每次插入都触发重排
for (let i = 0; i < 100; i++) {
document.body.appendChild(div)
}
// 好:用 fragment 一次性插入
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
fragment.appendChild(div)
}
document.body.appendChild(fragment) // 只触发一次重排
6. transform 为什么快?
transform 不走布局和绘制,它直接进入合成阶段,由 GPU 处理。
简单理解:
-
left/top:改位置 → 触发重排 → 重绘 → 合成(主线程干,慢) -
transform:跳过前两步 → 直接合成(合成线程干,快)
所以做动画时,能用 transform 就别用 left/top。
/* 慢 */
.box {
transition: left 0.3s;
left: 0;
}
.box.active {
left: 100px;
}
/* 快 */
.box {
transition: transform 0.3s;
transform: translateX(0);
}
.box.active {
transform: translateX(100px);
}
7. 常见面试题
7.1 重排和重绘的区别?哪个更耗性能?
重排是重新计算位置和大小,开销大;重绘是重新涂色,开销中等。重排一定触发重绘,反之不一定。
7.2 哪些属性会触发重排?
width、height、margin、padding、border、font-size、display、position 等。还有添加/删除 DOM、改窗口大小。
7.3 如何避免重排?
- 合并样式修改
- 使用
transform做动画 - 批量操作 DOM
- 让元素脱离文档流
7.4 transform 和 left/top 有什么区别?
left/top 触发布局(重排),慢;transform 只触发合成,由 GPU 处理,快。
7.5 为什么有时候读 offsetHeight 会让页面变慢?
因为浏览器需要立即计算最新的布局才能返回准确值,这会强制重排。如果在循环里读,会反复触发重排,性能极差。
8. 总结一句话
浏览器渲染分五步:DOM 树 → CSSOM 树 → 渲染树 → 布局(重排)→ 绘制(重绘)。
重排慢,重绘快,动画用 transform最流畅。
优化核心:减少重排,合并操作,能用合成就合成。
奇瑞发布了 3 款新燃油车,这才是奇瑞销量的支柱和基本盘

过去几年,新能源车抢走了车市里的大部分风头,但在真正贡献销量的基本盘里,燃油车依然有着自己的位置。尤其是在 8 万到 10 万元这根最拥挤的家用 SUV 红线上,消费者关心的无非还是那几个实在问题:车够不够靠谱,空间大不大,配置是不是厚道,以及价格划不划算。风向虽然在变,但需求一直都在。对很多家庭和第一次买车的人来说,挑一台成熟稳定、省心省力的燃油 SUV,依然是最稳妥的决定。
4 月 8 日,奇瑞在芜湖老家办了场发布会,一口气端出了瑞虎 7L、全新瑞虎 7 和瑞虎 5 运动版三款新车。瑞虎 7 系列至今已经在全球卖出了 163.2 万辆,连续四年拿下中国品牌 A 级 SUV 全球销冠;瑞虎 5 系列也积累了超 176.3 万海外及国内用户。奇瑞这次集中推新,其实就是把手里现成的牌重新洗了一遍,把主流燃油 SUV 的分工切得更细:7L 抓年轻人的眼球,全新 7 守住家庭用户的底线,5 运动版则去啃首购市场。

瑞虎 7L:燃油车的里子,智能电车的面子
三款车里,瑞虎 7L 是最明显想要拉开气质差异的一台。奇瑞给它的定位是「新燃油时尚智趣全球车」,目标用户也很直接,瞄准的是更在意设计感、智能交互和日常体验的年轻家庭。和很多同价位燃油 SUV 还停留在「够用就行」的思路不同,瑞虎 7L 试图把设计、座舱氛围和空间表现一起往上抬,让一台家用车多一点情绪价值。
瑞虎 7L 延续了奇瑞近年一直在强调的「自然力量美学」,前脸层次更复杂,型面起伏更明显,整车姿态也更倾向于做出一种带点锋利感的时尚气质。尤其是流光紫这样的配色,很难不让人注意到它在努力摆脱传统家用 SUV 的保守印象。它不是那种一眼就显得特别稳重的车,反而更想传递一种偏年轻、偏精致的存在感。对如今的主流用户来说,车还是交通工具,但早就不只是交通工具了,很多人也希望它至少看起来别太无聊。
真正让瑞虎 7L 变得有记忆点的,还是座舱这部分。15.6 英寸「宠爱滑移屏」几乎是整台车最核心的产品标签,支持在中控和副驾之间滑动,配合 8155 芯片、 AI 语音系统,以及围绕副驾展开的一整套舒适配置,奇瑞显然是想把燃油车上惯常的功能堆叠,做成一种更具体的场景体验。主驾导航,副驾娱乐,停车等人时追剧放松,午休时把车厢变成一个暂时脱离工作节奏的小空间,这种思路其实挺聪明。它不是在讲特别宏大的智能化叙事,而是在试图回答一个更实际的问题:一辆车除了通勤和代步,还能不能让日常相处变得舒服一点。
这种「把体验做满」的思路,也延续到了空间和动力层面。2720 mm 轴距、 639 L – 1417 L 后备厢、 36 处储物空间,这些参数指向的都是很明确的家庭使用场景,婴儿车、露营装备、周末短途出游,全都在它的能力范围里。动力方面,新车提供 1.5T 和 1.6T 两套方案,其中 1.6T 版本最大功率 147kW,最大扭矩 310N·m,在这一价位里已经是相当积极的账面数据。再叠加 80% 高强度钢车身、 7气囊、 540° 全景影像和 L2 级辅助驾驶,瑞虎 7L 的产品逻辑很清楚:它希望自己看起来不只是一台入门家用 SUV,而是一台在预算可控的前提下,把配置和体验尽量拉高的主流选择。官方指导价 8.59 万 – 10.69 万元,限时优惠价 7.89 万 – 9.99 万元,这个区间也说明它卡位的正是最核心的家用 SUV 市场。
如果说瑞虎 7L 代表的是奇瑞想把燃油 SUV 做得更有新鲜感、更有体验感的一面,那么接下来这台全新瑞虎 7,明显就是另一种思路了。它没有那么强调「惊喜」,但在很多家庭用户真正会认真比较的维度上,反而更容易建立说服力。

全新瑞虎 7:回到家用 SUV 最核心
相比瑞虎 7L 的年轻和外放,全新瑞虎 7 的气质稳得多。奇瑞给它的定位是「A 级舒享品质全球车」,目标人群也很清晰,就是那些务实顾家、重视安全和长期使用体验的家庭用户。它不是一台需要靠特别强的视觉冲击力来吸引注意的车,而是一台努力把家用车最核心的几件事做扎实的产品。
全新瑞虎 7 的重点首先落在安全上。瑞虎 7 系列本身已经有很强的全球安全背书,是中国品牌中少数拿到全球四项五星安全认证的车系。到了这次焕新之后,全新瑞虎 7 继续围绕这一点做强化,高强度车身、 Shotgun 结构、多路径传力设计,以及 360° 全景影像、 ESP 等配置,本质上都在服务同一个关键词:安心。对家庭用户来说,这个词的分量其实非常大。很多时候,决定一辆家用 SUV 能不能打动人,不是它讲了多少新概念,而是它能不能让人觉得这台车足够稳妥,足够适合一家人长期使用。
在安全之外,全新瑞虎 7 也试图把「舒享品质」这件事做得具体一点。24.6 英寸曲面沉浸环绕屏、前排座椅通风加热、方向盘加热、电动尾门、四轮独悬、 NVH 优化,这些配置单独看都不算特别夸张,但集中出现在一台 7 万 – 9 万元区间的燃油 SUV 上,还是能看出奇瑞在努力补齐高频体验。它没有把重点放在特别炫目的科技标签上,而是更偏向家庭日常里能真正频繁感知到的舒适性升级。包括 2670 mm 轴距带来的后排空间表现,以及整车加发动机终身质保,这些都属于那种不会第一眼惊艳,但会在实际购车决策里很有分量的内容。
从价格看,全新瑞虎 7 也延续了这种务实取向。官方指导价 7.99 万 – 9.19 万元,限时优惠价 6.99 万 – 8.19 万元,几乎是把竞争直接拉回到最敏感的主流家用车预算区间。对一部分用户来说,瑞虎 7L 提供的是更丰富、更偏个性化的体验;而全新瑞虎 7 提供的,则是一种更容易被家庭接受的均衡感。它不会是这场发布会里最抓眼球的一台车,但很可能会是最容易被大量普通家庭迅速理解的一台车。
如果说瑞虎 7L 和全新瑞虎 7,分别对应的是主流家用 SUV 市场里「更看重体验」和「更看重均衡」的两类需求,那么第三台车的任务就更明确了。它要面对的是预算更敏感、年纪更轻、购车经验也更少的那批人。

瑞虎 5 运动版:也许是年轻人真正的第一辆运动版 SUV
这次发布会里,瑞虎 5 运动版是价格门槛最低的一台车,也是定位最鲜明的一台车。奇瑞给它的定义是「全球智酷精品 SUV」,核心任务很直接,就是守住年轻首购市场。和前两台车相比,它没有太强的家庭属性包袱,更强调的是年轻人第一次买车时最在意的几件事:外观得顺眼,智能得好用,安全感不能太弱,价格还得尽量压低。
从设计开始,瑞虎 5 运动版就在努力把自己和传统家用 SUV 区分开。运动溜背、刀锋腰线、隐藏式格栅、红色卡钳这些细节,都是很典型的年轻化表达。它不一定是那种特别夸张的运动风格,但足够让整台车看起来更轻快、更利落,也更接近年轻用户对第一台车的想象。对于预算在 7 万级左右的首购人群来说,实用当然重要,但能不能开出去不显得太普通,同样是一个真实存在的购买理由。
智能部分,瑞虎 5 运动版给得也很直接。8155 芯片、 15.6 英寸 2K 中控屏、 AI 语音助手、手机互联、索尼 8 扬声器、整车 OTA,这些配置放在 7 万级燃油 SUV 里,已经足够形成明显记忆点。它并不是要证明自己有多前沿,而是在尽量缩小燃油车和电车之间最容易被年轻用户感知到的那层「数码体验差距」。说得更直白一点,很多年轻人第一次坐进一辆车,判断它值不值得买,已经很像在判断一台电子产品是不是顺手。瑞虎 5 运动版显然明白这个变化,所以它把「好用」放在了很前面的位置。

安全和动力部分,则是它用来稳住基本盘的内容。1.5 T 发动机搭配 DCT 湿式双离合,属于成熟、清晰、没有太高理解门槛的动力组合。80% 高强度钢车身、 6 安全气囊、 L2 级辅助驾驶、 540° 高清全景影像,也基本覆盖了年轻用户第一次买车时最容易紧张的那些场景:怕停车难,怕路况复杂,怕自己经验不够。它不是一台靠极端性能吸引人的车,而是一台尽可能把第一次拥有一辆 SUV 的门槛降下来,同时又不让用户觉得太凑合的产品。官方指导价 7.99 万 – 8.99 万元,限时优惠价 7.09 万 – 8.09 万元,再叠加置换、金融、流量和终身质保等政策,核心目标其实就一句话:让年轻人用更低的成本,买到一台看起来、用起来都更像样的第一台车。
把三台车放在一起看,这场发布会的意思就不只是「一次性上了三款新车」这么简单了。更准确地说,奇瑞是在用三种不同的产品表达,重新划分主流燃油 SUV 市场里的用户需求。瑞虎 7L 负责把设计感、智趣体验和越级配置往上抬,全新瑞虎 7 负责把家庭用户最看重的安全、舒适和长期使用价值做得更稳,而瑞虎 5 运动版则继续下探年轻首购市场,把外观、智能和价格门槛做成更容易接受的样子。
燃油车今天确实不在舞台中央了,但这不代表它已经失去市场意义。对很多三四线城市家庭、预算敏感型用户、首购人群和务实消费者来说,成熟的动力系统、明确的用车逻辑、稳定的补能方式,依旧是很有说服力的购买理由。放到奇瑞的销量结构里,瑞虎这样一类燃油 SUV,也仍然是相当扎实的基本盘。它们可能没有新能源产品那样密集的话题和流量,却依旧承担着巨大的市场需求和份额,也是奇瑞销量体系里非常稳定的中坚力量。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
证监会:推出更多创业板相关ETF和期权,适时推出创业板股指期货
证监会:意见出台更好支持科技创新和新质生产力发展的迫切需要
证监会:深化创业板改革,更好支持企业利用资本市场融资发展、做优做强
OpenLayers 地图绘制与交互实战:从零构建一个完整的绘制系统
前言
在 WebGIS 开发中,地图绘制功能是一个常见且重要的需求。本文将基于 OpenLayers 框架,手把手教你构建一个完整的地图绘制系统,包含点、线、面、圆的绘制,要素编辑、选择和删除等功能。通过本文,你将深入理解 OpenLayers 的交互机制和图层管理。
最终效果预览
我们将实现一个具有以下功能的地图应用:
- 🎯 支持绘制点、线、面、圆
- ✏️ 支持编辑已绘制的图形
- 🖱️ 支持点击选择要素
- 🗑️ 支持删除选中要素
- 🧲 支持顶点吸附功能
- 📍 支持 GeoJSON 数据展示
项目结构
ol-app/
├── main.js # 入口文件,初始化地图
├── src/
│ ├── drawLayer.js # 绘制图层核心类
│ └── draw.js # GeoJSON 图层初始化
├── geojsonObject.js # GeoJSON 数据
└── style.css # 样式文件
一、核心类设计:DrawLayer
首先,我们创建一个 DrawLayer 类来封装所有的绘制和交互逻辑。这种封装方式让代码更加模块化,便于维护和复用。
1.1 类结构初始化
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { Draw, Modify, Snap, Select } from 'ol/interaction';
import { Style, Fill, Stroke, Circle as CircleStyle } from 'ol/style';
import { click } from 'ol/events/condition';
export class DrawLayer {
constructor(map) {
this.map = map;
// 创建矢量数据源
this.source = new VectorSource();
// 创建矢量图层
this.layer = new VectorLayer({
source: this.source,
style: this.getDefaultStyle()
});
// 添加到地图
this.map.addLayer(this.layer);
// 交互对象
this.drawInteraction = null;
this.modifyInteraction = null;
this.snapInteraction = null;
this.selectInteraction = null;
this.selectedFeature = null;
}
}
关键点解析:
-
VectorSource:存储所有绘制的要素数据 -
VectorLayer:负责将要素渲染到地图上 - 各种
Interaction:OpenLayers 的交互对象,分别处理绘制、编辑、吸附、选择等功能
1.2 样式定义
/**
* 获取默认样式
*/
getDefaultStyle() {
return new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)'
}),
stroke: new Stroke({
color: '#ffcc33',
width: 2
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: '#ffcc33'
})
})
});
}
/**
* 获取选中样式
*/
getSelectStyle() {
return new Style({
stroke: new Stroke({ color: 'red', width: 3 }),
fill: new Fill({ color: 'rgba(255, 0, 0, 0.2)' }),
image: new CircleStyle({
radius: 7,
fill: new Fill({ color: 'red' })
})
});
}
二、绘制功能实现
2.1 开始绘制
/**
* 开始绘制
* @param {string} type - 绘制类型: 'Point', 'LineString', 'Polygon', 'Circle'
* @param {Function} callback - 绘制完成回调
*/
startDraw(type, callback) {
// 清除之前的绘制交互
this.stopDraw();
// 禁用选择模式,避免冲突
this.disableSelect();
this.drawInteraction = new Draw({
source: this.source,
type: type
});
this.drawInteraction.on('drawend', (event) => {
const feature = event.feature;
if (callback) {
callback(feature);
}
});
this.map.addInteraction(this.drawInteraction);
// 添加吸附功能
this.snapInteraction = new Snap({
source: this.source
});
this.map.addInteraction(this.snapInteraction);
}
技术要点:
-
Draw交互会自动将绘制的要素添加到指定的 source 中 -
Snap交互让新绘制的点可以吸附到已有要素的顶点上,提高精度 - 绘制前需要停止其他交互,避免冲突
2.2 停止绘制
/**
* 停止绘制
*/
stopDraw() {
if (this.drawInteraction) {
this.map.removeInteraction(this.drawInteraction);
this.drawInteraction = null;
}
if (this.snapInteraction) {
this.map.removeInteraction(this.snapInteraction);
this.snapInteraction = null;
}
}
三、编辑功能实现
3.1 启用编辑
/**
* 启用编辑模式
*/
enableModify() {
this.disableModify();
this.disableSelect();
this.modifyInteraction = new Modify({
source: this.source
});
this.map.addInteraction(this.modifyInteraction);
}
/**
* 禁用编辑模式
*/
disableModify() {
if (this.modifyInteraction) {
this.map.removeInteraction(this.modifyInteraction);
this.modifyInteraction = null;
}
}
Modify 交互允许用户拖拽要素的顶点来编辑图形形状。
四、选择功能实现
4.1 启用选择模式
/**
* 启用选择模式
*/
enableSelect() {
this.disableSelect();
this.stopDraw();
this.disableModify();
this.selectInteraction = new Select({
layers: [this.layer],
style: this.getSelectStyle(),
multi: true,
toggleCondition: click
});
// 监听选择事件
this.selectInteraction.on('select', (e) => {
const selected = e.selected;
if (selected.length > 0) {
this.selectedFeature = selected[0];
console.log('选中要素:', this.selectedFeature);
} else {
this.selectedFeature = null;
}
});
this.map.addInteraction(this.selectInteraction);
}
重点解析:
-
layers: [this.layer]:指定可选中的图层,使用像素检测实现精确选择 -
multi: true:允许多选 -
toggleCondition: click:点击切换选中状态,无需按住 Shift 键
4.2 禁用选择
/**
* 禁用选择模式
*/
disableSelect() {
if (this.selectInteraction) {
this.map.removeInteraction(this.selectInteraction);
this.selectInteraction = null;
this.selectedFeature = null;
}
}
五、要素管理
/**
* 移除选中的要素
*/
removeSelectedFeature() {
if (this.selectedFeature) {
this.source.removeFeature(this.selectedFeature);
this.selectedFeature = null;
return true;
}
return false;
}
/**
* 清除所有绘制
*/
clear() {
this.source.clear();
this.selectedFeature = null;
}
/**
* 获取所有绘制的要素
*/
getFeatures() {
return this.source.getFeatures();
}
/**
* 移除指定要素
*/
removeFeature(feature) {
this.source.removeFeature(feature);
if (this.selectedFeature === feature) {
this.selectedFeature = null;
}
}
六、工具栏创建
export function createDrawToolbar(container, drawLayer) {
const toolbar = document.createElement('div');
toolbar.className = 'draw-toolbar';
toolbar.innerHTML = `
<button data-type="Point" class="draw-point">点</button>
<button data-type="LineString" class="draw-line">线</button>
<button data-type="Polygon" class="draw-polygon">面</button>
<button data-type="Circle" class="draw-circle">圆</button>
<button id="modify-btn" class="draw-modify">编辑</button>
<button id="clear-btn" class="draw-clear">清除</button>
<button id="remove-btn" class="draw-remove">移除</button>
<button id="select-btn" class="draw-select">选择</button>
`;
let isModifying = false;
let isSelecting = false;
toolbar.addEventListener('click', (e) => {
// 绘制按钮
if (e.target.classList.contains("draw-point")) {
drawLayer.startDraw("Point");
}
if (e.target.classList.contains("draw-line")) {
drawLayer.startDraw("LineString");
}
if (e.target.classList.contains("draw-polygon")) {
drawLayer.startDraw("Polygon");
}
if (e.target.classList.contains("draw-circle")) {
drawLayer.startDraw("Circle");
}
// 编辑按钮
if (e.target.classList.contains("draw-modify")) {
isModifying = !isModifying;
if (isModifying) {
drawLayer.enableModify();
e.target.textContent = "完成";
} else {
drawLayer.disableModify();
e.target.textContent = "编辑";
}
}
// 清除按钮
if (e.target.classList.contains("draw-clear")) {
drawLayer.clear();
}
// 移除按钮
if (e.target.classList.contains("draw-remove")) {
if (drawLayer.removeSelectedFeature()) {
console.log("移除成功");
} else {
console.log("没有选中的要素");
}
}
// 选择按钮
if (e.target.classList.contains("draw-select")) {
isSelecting = !isSelecting;
if (isSelecting) {
drawLayer.enableSelect();
e.target.textContent = "退出选择";
} else {
drawLayer.disableSelect();
e.target.textContent = "选择";
}
}
});
container.appendChild(toolbar);
return toolbar;
}
七、主入口文件
import './style.css';
import {Map, View} from 'ol';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';
import Overlay from 'ol/Overlay';
import {fromLonLat} from 'ol/proj';
import {geojsonObject1} from '/geojsonObject.js';
import { initDrawLayer } from './src/draw.js';
import { DrawLayer, createDrawToolbar } from './src/drawLayer.js';
// 1. 创建基础绘制图层(显示 GeoJSON 数据)
const vectorLayer = initDrawLayer(geojsonObject1, {
point: {
radius: 8,
fillColor: 'red',
strokeColor: 'white',
strokeWidth: 2
},
line: {
color: 'blue',
width: 4,
lineDash: [10, 10]
},
polygon: {
fillColor: 'rgba(0, 255, 0, 0.3)',
strokeColor: 'green',
strokeWidth: 2
}
});
// 2. 创建地图
const map = new Map({
target: 'map',
layers: [
new TileLayer({
source: new XYZ({
url: 'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
})
}),
vectorLayer
],
controls: [],
view: new View({
center: fromLonLat([116.4074, 39.9042]),
zoom: 15
})
});
// 3. 创建用户绘制图层
const drawLayer = new DrawLayer(map);
// 4. 创建绘制工具栏
const toolbarContainer = document.createElement('div');
toolbarContainer.id = 'draw-toolbar-container';
document.body.appendChild(toolbarContainer);
createDrawToolbar(toolbarContainer, drawLayer);
// 5. 创建 popup 容器
const popupContainer = document.createElement('div');
popupContainer.className = 'popup';
const popupOverlay = new Overlay({
element: popupContainer,
positioning: 'bottom-center',
offset: [0, -15]
});
map.addOverlay(popupOverlay);
// 6. 点击事件 - 显示要素信息
map.on('click', (evt) => {
const feature = map.forEachFeatureAtPixel(evt.pixel, (feat) => feat);
if (feature) {
const props = feature.getProperties();
popupContainer.innerHTML = `<b>${props.name}</b><br>类型: ${props.type}`;
popupOverlay.setPosition(evt.coordinate);
} else {
popupOverlay.setPosition(undefined);
}
});
八、关键技术点总结
8.1 Source vs Layer
| 特性 | Source | Layer |
|---|---|---|
| 作用 | 数据存储 | 可视化渲染 |
| 关系 | 被 Layer 引用 | 引用 Source |
| 类比 | 数据库 | 表格组件 |
8.2 交互优先级
OpenLayers 的交互是按照添加顺序执行的,后添加的优先级更高。因此需要合理管理交互的启用和禁用:
// 启用新交互前,先禁用冲突的交互
startDraw() {
this.stopDraw(); // 停止之前的绘制
this.disableSelect(); // 禁用选择,避免冲突
// ... 创建新交互
}
8.3 多选实现
多选的关键在于 toggleCondition 配置:
const select = new Select({
multi: true,
toggleCondition: click // 点击切换选中状态
});
如果不设置 toggleCondition,默认需要按住 Shift 键才能多选。
九、扩展思路
-
导出 GeoJSON:使用
GeoJSONformat 将绘制的要素导出 - 撤销重做:维护操作历史栈,实现撤销重做功能
- 样式编辑器:提供 UI 让用户自定义绘制样式
- 测量工具:计算绘制图形的面积和长度
十、完整代码
本文的完整代码已开源,你可以在 GitHub 上找到: github.com/yourname/ol…
结语
通过本文,我们实现了一个功能完整的 OpenLayers 绘制系统。核心思想是将功能封装成独立的类,通过交互对象管理用户操作,使用 Source-Layer 模式管理数据。希望本文对你有所帮助,如果有任何问题,欢迎在评论区讨论!
参考链接:
证监会:进一步深化投资端改革,促进投融资协调发展
证监会:完善融资并购制度,提升股债融资灵活性便利性
前端表单构建神器 - formkit初体验
传统表单开发 vs 低代码方案
传统的表单开发,无论是基于dom还是数据驱动的,都离不开手写html模板。尤其对于复杂的表单:关联字段联动、校验、表单字段的排版等等都有相当大的工作量。为此近些年涌现出不少的低代码方案,旨在通过页面拖拽配置的形式来高效的维护表单功能,来代替繁重的代码开发维护。基于JSON Schema的表单构建方案就是在这个背景下诞生,而具有代表性的就是本文要介绍的FormKit
FormKit项目初始化
准备源码路径:D:\2026学习\study\code\formkit示例\001_formkit项目初始化\parent
右键parent,用idea打开
自动完成依赖安装更新
看下默认安装的依赖:
"dependencies": {
"@formkit/core": "^2.0.0",
"@formkit/icons": "^2.0.0",
"@formkit/themes": "^2.0.0",
"@formkit/vue": "^2.0.0",
"@tailwindcss/vite": "^4.2.2",
"tailwindcss": "^4.2.2",
"vue": "^3.5.32"
}
formkit不光是UI框架,更是开箱即用的json schema渲染表单的解决方案。对于UI,formkit直接用Tailwind来构建和维护组件样式。
修复类型引入问题
创建pnpm启动项
运行dev,访问:http://localhost:5173/,将看到页面:
组件的渲染方式
有两种方式:html中编写组件标签和基于schema的集中维护定义。
前者属于传统的组件使用方式,大部分场景下我们的表单开发都是直接用开源组件库如element plus,来编写和维护表单,FormKit也支持这个方式,它提供了内置的常用表单组件,同时提供了非常好的机制让我们扩展自定义组件,包括集成现有的UI组件。
组件定义方式
直接写组件标签,类似于使用Element Plus中的组件来手动构建表单:
<FormKit
type="form"
#default="{ value }"
@submit="submit"
>
<FormKit
type="text"
name="name"
label="Name"
help="..."
/>
<FormKit
type="checkbox"
name="flavors"
label="..."
:options="{ ... }"
validation="required|min:2"
/>
<FormKit
type="checkbox"
name="agree"
label="..."
/>
...
</FormKit>
基于Schema的定义方式
这种方式方便集中维护表单字段定义,FormKit可以基于表单定义的Schema动态的渲染表单,是低代码表单设计器的构建产物。有了它,我们只要关注于字段配置的扩展以及如何设计和实现表单设计器来在线生成表单定义数据。
<script setup lang="ts">
import {ref} from "vue"
const formSchema = {
$formkit: 'form',
children: [{
$formkit: 'text',
name: 'name',
label: 'Name',
help: '...',
},
{
$formkit: 'checkbox',
name: 'flavors',
label: 'Favorite ice cream flavors',
options: { ... },
validation: 'required|min:2',
},
{
$formkit: 'checkbox',
name: 'agree',
label: '...',
},
]}
const data = ref({})
async function submit() {
await new Promise(r => setTimeout(r, 1000))
alert('Submitted! 🎉')
}
</script>
<template>
<div class="...">
<img ...>
<FormKitSchema :schema="formSchema" v-model="data" @submit="submit" />
<pre class="...">{{ data }}</pre>
</div>
</template>
校验
FormKit提供了非常强大的内置校验和自定义扩展的方式,具体可参考校验官方文档
示例中对一个字段启用非空和长度校验非常简单,比如这里的多选框字段,只需简单配置为:validation: 'required|min:2',页面效果:
FormKit支持国际化,只需要在formkit.config.ts中进行如下配置:
...
import { zh } from '@formkit/i18n'
const config: DefaultConfigOptions = {
...
locales: { zh },
locale: 'zh',
}
export default config
会看到系统的校验信息变成了中文
官方文档
以上我们的介绍只是FormKit功能特性的九牛一毛,具体的API用法配置请参考FormKit官方文档。个人觉得看了那么多技术文档,FormKit无论是可读性和用户体验都是非常好的,唯一的遗憾是没有中文版。后续的例子也都会从官方文档来扩展。
好了,本次的学习分享就到这里。希望本篇能给前端低代码研发的小伙伴一些启示,我是小卷,一个爱学习分享的搬砖老码农,我们下期再见!
证监会:进一步压实深交所审核主体责任 严把质量关
证监会:增设创业板第四套上市标准
证监会发布《关于深化创业板改革 更好服务新质生产力发展的意见》
恒指收涨0.45%,恒生科技指数涨0.73%
ESTree 规范 (acorn@8.15.0示例)
ESTree 是一套用于描述 ECMAScript(JavaScript)代码抽象语法树(AST)的标准化规范。ESTree 规范并非一成不变,而是跟随 ECMAScript 官方版本迭代,分为多个阶段的规范:
- ES5 规范:最早的 ESTree 规范,仅支持 ES5 语法(如 var、普通函数、
if/for等)。 - ES6+ 规范:新增 ES6 及后续版本的语法节点(如
ArrowFunctionExpression箭头函数、ClassDeclaration类、ImportDeclaration模块导入等)。 - ESNext 规范:支持尚未正式纳入 ECMAScript 标准的实验性语法(如装饰器、管道运算符等),供工具提前适配。
语法节点类型
根节点唯一 (Program)
{
"type": "Program", // 节点类型,`Program` 表示整个程序。
"start": 0, // 在源码中的开始索引
"end": 9, // 在源码中的结束索引,这里原代码长度为 9,即共 9 个字符
"body": [ ... ], // 程序体,是一个语句数组
"sourceType": "script" // "script" 表示源码是普通脚本(非模块),如果是 `"module"`,则支持 `import`/`export`
}
声明节点
- VariableDeclaration 变量声明(统一包裹const/let/var)
- FunctionDeclaration 函数声明(具名函数,提升)
- ClassDeclaration 类声明(具名类,提升)
- ImportDeclaration 模块导入声明(仅模块环境)
- ExportDeclaration 模块导出声明(仅模块环境,含命名 / 默认)
- ExportNamedDeclaration命名导出
- ExportDefaultDeclaration默认导出
- ExportAllDeclaration全部导出
语句节点
- BlockStatement 块语句({}包裹的代码块)
- ExpressionStatement 表达式语句(包裹单个表达式作为语句执行)
- IfStatement 条件判断语句
- ForStatement for 循环语句
- WhileStatement while 循环语句
- ReturnStatement 返回语句(函数内)
- TryStatement 异常捕获语句
- BreakStatement 中断循环语句
- ContinueStatement 继续循环语句
表达式节点
- Identifier标识符(变量名、函数名、属性名等
- Literal字面量(直接写死的值)
- BinaryExpression 二元表达式(双操作数运算)
- UnaryExpression 一元表达式(单操作数运算)
- AssignmentExpression 赋值表达式
- CallExpression 函数调用表达式
- MemberExpression 成员访问表达式
- ArrowFunctionExpression 箭头函数表达式
- ObjectExpression 对象字面量表达式
- ArrayExpression 数组字面量表达式
其他节点
- TryStatementtry...catch 语句
- TemplateLiteral模板字符串
- TaggedTemplateExpression带标签的模板字符串
- SpreadElement扩展运算符
- RestElement剩余参数
Acorn
Acorn 是一个轻量、快速的 JavaScript 解析器,能将代码转换为 ESTree 标准的抽象语法树(AST)。
它主要提供三大核心 API
-
parse(input, options):解析一段完整的 JavaScript 程序。成功返回 ESTree AST,失败抛出包含位置信息的SyntaxError对象 -
parseExpressionAt(input, pos, options):解析一个独立的 JavaScript 表达式。适用于解析模板字符串内的内嵌表达式等混合内容 -
tokenizer(input, options):返回一个迭代器,逐个生成代码的 Token。可用于自定义的语法高亮或极简解析器。
parseExpressionAt
const code = 'const x = 10; const y = 20; x + y * 2;'
const result = acorn.parseExpressionAt(code, code.indexOf('x + y'),{
ecmaVersion: 2020,
sourceType: 'module',
});
console.log(result);
tokenizer
示例
const result = acorn.tokenizer('let a = "hello";',{
ecmaVersion: 2020,
sourceType: 'module',
});
console.log(result);
关键字(可用于代码高亮)
^(?:break|case|catch|continue|debugger|default|do|else|finally|for|function|if|return|switch|throw|try|var|while|with|null|true|false|instanceof|typeof|void|delete|new|in|this|const|class|extends|export|import|super)$
示例
const result = acorn.tokenizer('let a = "hello";',{
ecmaVersion: 2020,
sourceType: 'module',
});
console.log(result);
for(let token of result){
console.log('token',token);
}
每个 Token 对象都会包含一个 type 属性,指向这样的类型描述对象。
{
"label": "string", // Token 类型的人类可读名称
"beforeExpr": false, // 该 Token 类型是否可以在表达式之前出现
"startsExpr": true, // 该 Token 类型是否作为表达式的开始
"isLoop": false, // 是否为循环关键字(如 for, while, do)
"isAssign": false, // 是否为赋值操作符(如 =, +=, -=)
"prefix": false, // 是否为前缀操作符(如 ++, --, !, ~)
"postfix": false, // 是否为后缀操作符(如 ++, --)
"binop": null,// 如果是二元操作符,这里会有一个优先级数值;否则为 null
"updateContext": null // 可选函数,用于在解析时更新上下文(通常为 null)
}
声明变量
例1 声明一个变量(基本类型)
const ast = acorn.parse(`let a = 1`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 9,
"body": [
{
"type": "VariableDeclaration", // 变量声明符
"start": 0,
"end": 9,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
// 标识符节点,即变量名。
"id": {
"type": "Identifier", // 变量名标识符
"start": 4,
"end": 5,
"name": "a" // 变量名
},
// 初始化表达式节点,即等号右边的值
"init": {
"type": "Literal", // 字面量
"start": 8,
"end": 9,
"value": 1, // 运行时的值,这里是数字 1
"raw": "1" // 源码中的原始字符串表示 "1"
}
}
],
"kind": "let" // 表示使用 let 关键字声明
}
],
"sourceType": "script"
}
例2 声明一个变量(数组)
const ast = acorn.parse(`const arr = [1,2]`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 17,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 17,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 17,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "arr"
},
"init": {
"type": "ArrayExpression",
"start": 12,
"end": 17,
"elements": [
{
"type": "Literal",
"start": 13,
"end": 14,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 15,
"end": 16,
"value": 2,
"raw": "2"
}
]
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
例3 声明一个变量(对象)
const ast = acorn.parse(`const arr = {a: 1, b: 2}`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 24,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 24,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 24,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "arr"
},
"init": {
"type": "ObjectExpression",
"start": 12,
"end": 24,
"properties": [
{
"type": "Property",
"start": 13,
"end": 17,
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
"value": {
"type": "Literal",
"start": 16,
"end": 17,
"value": 1,
"raw": "1"
},
"kind": "init"
},
{
"type": "Property",
"start": 19,
"end": 23,
"method": false,
"shorthand": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 19,
"end": 20,
"name": "b"
},
"value": {
"type": "Literal",
"start": 22,
"end": 23,
"value": 2,
"raw": "2"
},
"kind": "init"
}
]
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
例4 三元表达式
const ast = acorn.parse(`const flag = a > b ? true : false`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 33,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 33,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 33,
"id": {
"type": "Identifier",
"start": 6,
"end": 10,
"name": "flag"
},
"init": {
"type": "ConditionalExpression",
"start": 13,
"end": 33,
"test": {
"type": "BinaryExpression",
"start": 13,
"end": 18,
"left": {
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
"operator": ">",
"right": {
"type": "Identifier",
"start": 17,
"end": 18,
"name": "b"
}
},
"consequent": {
"type": "Literal",
"start": 21,
"end": 25,
"value": true,
"raw": "true"
},
"alternate": {
"type": "Literal",
"start": 28,
"end": 33,
"value": false,
"raw": "false"
}
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
例5 声明变量(逻辑运算符)
const code = 'let name = jon || "hello";'
const result = acorn.parse(code, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(result, null, 2));
{
"type": "Program",
"start": 0,
"end": 26,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 26,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 25,
// 声明标识
"id": {
"type": "Identifier",
"start": 4,
"end": 8,
"name": "name"
},
// 声明初始化内容
"init": {
"type": "LogicalExpression",// 逻辑表达式
"start": 11,
"end": 25,
"left": {
"type": "Identifier",
"start": 11,
"end": 14,
"name": "jon"
},
"operator": "||",// 操作符
"right": {
"type": "Literal",
"start": 18,
"end": 25,
"value": "hello",
"raw": "\"hello\""
}
}
}
],
"kind": "let"
}
],
"sourceType": "script"
}
函数
例1 箭头函数
const ast = acorn.parse(`const getFlag = (a, b) => a + b`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 31,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 31,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 31,
"id": {
"type": "Identifier",
"start": 6,
"end": 13,
"name": "getFlag"
},
"init": {
"type": "ArrowFunctionExpression",
"start": 16,
"end": 31,
"id": null,
"expression": true,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 17,
"end": 18,
"name": "a"
},
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "b"
}
],
"body": {
"type": "BinaryExpression",
"start": 26,
"end": 31,
"left": {
"type": "Identifier",
"start": 26,
"end": 27,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 30,
"end": 31,
"name": "b"
}
}
}
}
],
"kind": "const"
}
],
"sourceType": "script"
}
例2 普通函数 含有返回值
const ast = acorn.parse(`function getFlag(a, b) { return a + b } `, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 41,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 39,
"id": {
"type": "Identifier",
"start": 9,
"end": 16,
"name": "getFlag"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 17,
"end": 18,
"name": "a"
},
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 23,
"end": 39,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 37,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 37,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 36,
"end": 37,
"name": "b"
}
}
}
]
}
}
],
"sourceType": "script"
}
例3 函数调用
const ast = acorn.parse(`function getFlag(a, b) { return a + b } getFlag(1, 2)`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 53,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 39,
"id": {
"type": "Identifier",
"start": 9,
"end": 16,
"name": "getFlag"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 17,
"end": 18,
"name": "a"
},
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 23,
"end": 39,
"body": [
{
"type": "ReturnStatement",
"start": 25,
"end": 37,
"argument": {
"type": "BinaryExpression",
"start": 32,
"end": 37,
"left": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 36,
"end": 37,
"name": "b"
}
}
}
]
}
},
{
"type": "ExpressionStatement",
"start": 40,
"end": 53,
"expression": {
"type": "CallExpression",
"start": 40,
"end": 53,
"callee": {
"type": "Identifier",
"start": 40,
"end": 47,
"name": "getFlag"
},
"arguments": [
{
"type": "Literal",
"start": 48,
"end": 49,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 51,
"end": 52,
"value": 2,
"raw": "2"
}
],
"optional": false
}
}
],
"sourceType": "script"
}
例4 条件语句
const ast = acorn.parse(`function getFlag(a, b) { if(a > b) { return true } } getFlag(1, 2)`, {
ecmaVersion: 2020,
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 66,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 52,
"id": {
"type": "Identifier",
"start": 9,
"end": 16,
"name": "getFlag"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 17,
"end": 18,
"name": "a"
},
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 23,
"end": 52,
"body": [
{
"type": "IfStatement",
"start": 25,
"end": 50,
"test": {
"type": "BinaryExpression",
"start": 28,
"end": 33,
"left": {
"type": "Identifier",
"start": 28,
"end": 29,
"name": "a"
},
"operator": ">",
"right": {
"type": "Identifier",
"start": 32,
"end": 33,
"name": "b"
}
},
"consequent": {
"type": "BlockStatement",
"start": 35,
"end": 50,
"body": [
{
"type": "ReturnStatement",
"start": 37,
"end": 48,
"argument": {
"type": "Literal",
"start": 44,
"end": 48,
"value": true,
"raw": "true"
}
}
]
},
"alternate": null
}
]
}
},
{
"type": "ExpressionStatement",
"start": 53,
"end": 66,
"expression": {
"type": "CallExpression",
"start": 53,
"end": 66,
"callee": {
"type": "Identifier",
"start": 53,
"end": 60,
"name": "getFlag"
},
"arguments": [
{
"type": "Literal",
"start": 61,
"end": 62,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 64,
"end": 65,
"value": 2,
"raw": "2"
}
],
"optional": false
}
}
],
"sourceType": "script"
}
类
声明一个空类
{
"type": "Program",
"start": 0,
"end": 11,
"body": [
{
"type": "ClassDeclaration", // 类声明
"start": 0,
"end": 11,
// 类名,是一个 Identifier 节点
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "Cat"
},
// 父类 ,如果有 extends 关键字,这里会是表达式节点
"superClass": null,
// 包含类的所有成员(方法、属性等)
"body": {
"type": "ClassBody",
"start": 9,
"end": 11,
"body": []
}
}
],
"sourceType": "module"
}
带构造函数的类
{
"type": "Program",
"start": 0,
"end": 50,
"body": [
{
"type": "ClassDeclaration",
"start": 0,
"end": 50,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "Cat"
},
"superClass": null,
"body": {
"type": "ClassBody",
"start": 9,
"end": 50,
"body": [
{
"type": "MethodDefinition",
"start": 11,
"end": 49,
"static": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 11,
"end": 22,
"name": "constructor"
},
"kind": "constructor",
"value": {
"type": "FunctionExpression",
"start": 22,
"end": 49,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 23,
"end": 27,
"name": "name"
}
],
"body": {
"type": "BlockStatement",
"start": 28,
"end": 49,
"body": [
{
"type": "ExpressionStatement",
"start": 30,
"end": 47,
"expression": {
"type": "AssignmentExpression",
"start": 30,
"end": 46,
"operator": "=",
"left": {
"type": "MemberExpression",
"start": 30,
"end": 39,
"object": {
"type": "ThisExpression",
"start": 30,
"end": 34
},
"property": {
"type": "Identifier",
"start": 35,
"end": 39,
"name": "name"
},
"computed": false,
"optional": false
},
"right": {
"type": "Identifier",
"start": 42,
"end": 46,
"name": "name"
}
}
}
]
}
}
}
]
}
}
],
"sourceType": "module"
}
截取片段this.name = name
{
"body": [
{
"type": "ExpressionStatement", // 表达式语句
"start": 30,
"end": 47,
// 真正的表达式
"expression": {
"type": "AssignmentExpression", // 赋值表达式
"start": 30,
"end": 46,
"operator": "=",
"left": {
"type": "MemberExpression", // 属性访问表达式
"start": 30,
"end": 39,
// 被访问的对象
"object": {
"type": "ThisExpression", // this
"start": 30,
"end": 34
},
// 属性
"property": {
"type": "Identifier",
"start": 35,
"end": 39,
"name": "name"
},
// 表示使用点号 . 访问属性(而非 [计算属性名])
"computed": false,
// 可选链操作符 ?.
"optional": false
},
"right": {
"type": "Identifier",
"start": 42,
"end": 46,
"name": "name"
}
}
}
]
}
继承
const code = 'class Cat extends Animal { constructor(name){ super(name); }}'
const result = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
});
console.log(JSON.stringify(result, null, 2));
{
"type": "Program",
"start": 0,
"end": 61,
"body": [
{
"type": "ClassDeclaration",
"start": 0,
"end": 61,
"id": {
"type": "Identifier",
"start": 6,
"end": 9,
"name": "Cat"
},
"superClass": {
"type": "Identifier",
"start": 18,
"end": 24,
"name": "Animal"
},
"body": {
"type": "ClassBody",
"start": 25,
"end": 61,
"body": [
{
"type": "MethodDefinition",
"start": 27,
"end": 60,
"static": false,
"computed": false,
"key": {
"type": "Identifier",
"start": 27,
"end": 38,
"name": "constructor"
},
"kind": "constructor",
"value": {
"type": "FunctionExpression",
"start": 38,
"end": 60,
"id": null,
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 39,
"end": 43,
"name": "name"
}
],
"body": {
"type": "BlockStatement",
"start": 44,
"end": 60,
"body": [
{
"type": "ExpressionStatement",
"start": 46,
"end": 58,
"expression": {
"type": "CallExpression",
"start": 46,
"end": 57,
"callee": {
"type": "Super",
"start": 46,
"end": 51
},
"arguments": [
{
"type": "Identifier",
"start": 52,
"end": 56,
"name": "name"
}
],
"optional": false
}
}
]
}
}
}
]
}
}
],
"sourceType": "module"
}
截取片段分析 super(name)
{
"type": "ExpressionStatement",
"start": 46,
"end": 58,
"expression": {
"type": "CallExpression",//调用表达式
"start": 46,
"end": 57,
// 被调用的函数或方法
"callee": {
"type": "Super", // super关键字
"start": 46,
"end": 51
},
// 参数列表
"arguments": [
{
"type": "Identifier",
"start": 52,
"end": 56,
"name": "name"
}
],
"optional": false
}
}
模块
命名导入
const ast = acorn.parse(`import { add } from './utills.js'`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 33,
"body": [
{
"type": "ImportDeclaration", // 导入声明
"start": 0,
"end": 33,
"specifiers": [
{
"type": "ImportSpecifier", // 导入语句
"start": 9,
"end": 12,
// 模块导入的名称
"imported": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
// 本地使用的名称
"local": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
}
}
],
// 源
"source": {
"type": "Literal",
"start": 20,
"end": 33,
"value": "./utills.js", // 运行中
"raw": "'./utills.js'" // 代码中保留了引号
}
}
],
"sourceType": "module"
}
命名导入
const ast = acorn.parse(`import { add } from './utills.js';const result = add(1, 2);`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 59,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 34,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 12,
"imported": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"local": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
}
}
],
"source": {
"type": "Literal",
"start": 20,
"end": 33,
"value": "./utills.js",
"raw": "'./utills.js'"
}
},
{
"type": "VariableDeclaration",
"start": 34,
"end": 59,
"declarations": [
{
"type": "VariableDeclarator",
"start": 40,
"end": 58,
"id": {
"type": "Identifier",
"start": 40,
"end": 46,
"name": "result"
},
"init": {
"type": "CallExpression",
"start": 49,
"end": 58,
"callee": {
"type": "Identifier",
"start": 49,
"end": 52,
"name": "add"
},
"arguments": [
{
"type": "Literal",
"start": 53,
"end": 54,
"value": 1,
"raw": "1"
},
{
"type": "Literal",
"start": 56,
"end": 57,
"value": 2,
"raw": "2"
}
],
"optional": false
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
别名导入
const ast = acorn.parse(`import { add as addFun} from './utills.js'`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 42,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 42,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 22,
"imported": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"local": {
"type": "Identifier",
"start": 16,
"end": 22,
"name": "addFun"
}
}
],
"source": {
"type": "Literal",
"start": 29,
"end": 42,
"value": "./utills.js",
"raw": "'./utills.js'"
}
}
],
"sourceType": "module"
}
命名导出一个 变量声明
const ast = acorn.parse(`export const Max_Size = 100;`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 28,
"body": [
{
"type": "ExportNamedDeclaration", // 表示一个命名导出
"start": 0,
"end": 28,
// 被导出的声明节点
"declaration": {
"type": "VariableDeclaration",
"start": 7,
"end": 28,
"declarations": [
{
"type": "VariableDeclarator",
"start": 13,
"end": 27,
"id": {
"type": "Identifier",
"start": 13,
"end": 21,
"name": "Max_Size"
},
"init": {
"type": "Literal",
"start": 24,
"end": 27,
"value": 100,
"raw": "100"
}
}
],
"kind": "const"
},
"specifiers": [],
"source": null // 从其他模块重导出
}
],
"sourceType": "module"
}
命名导出一个 函数声明
const ast = acorn.parse(`export function add(a, b) {return a + b;}`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 41,
"body": [
{
"type": "ExportNamedDeclaration",
"start": 0,
"end": 41,
"declaration": {
"type": "FunctionDeclaration",
"start": 7,
"end": 41,
"id": {
"type": "Identifier",
"start": 16,
"end": 19,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 20,
"end": 21,
"name": "a"
},
{
"type": "Identifier",
"start": 23,
"end": 24,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 26,
"end": 41,
"body": [
{
"type": "ReturnStatement",
"start": 27,
"end": 40,
"argument": {
"type": "BinaryExpression",
"start": 34,
"end": 39,
"left": {
"type": "Identifier",
"start": 34,
"end": 35,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 38,
"end": 39,
"name": "b"
}
}
}
]
}
},
"specifiers": [],
"source": null
}
],
"sourceType": "module"
}
命名导出一个变量
const ast = acorn.parse(`const Max_Size = 100;export { Max_Size };`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
export { Max_Size };这是一个命名导出语句,但它不包含声明(declaration: null),而是通过 specifiers 列表来指定要导出的已有变量。
{
"type": "Program",
"start": 0,
"end": 41,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 21,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 20,
"id": {
"type": "Identifier",
"start": 6,
"end": 14,
"name": "Max_Size"
},
"init": {
"type": "Literal",
"start": 17,
"end": 20,
"value": 100,
"raw": "100"
}
}
],
"kind": "const"
},
{
"type": "ExportNamedDeclaration",
"start": 21,
"end": 41,
"declaration": null, // 没有内联声明
// 导出说明符列表
"specifiers": [
{
"type": "ExportSpecifier",
"start": 30,
"end": 38,
// 当前模块本地名称
"local": {
"type": "Identifier",
"start": 30,
"end": 38,
"name": "Max_Size"
},
// 导出后名称
"exported": {
"type": "Identifier",
"start": 30,
"end": 38,
"name": "Max_Size"
}
}
],
"source": null // 不是从其他模块中导出
}
],
"sourceType": "module"
}
命名导出一个函数
const ast = acorn.parse(`function add(a, b) {return a + b;} export { add };`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 50,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 34,
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 34,
"body": [
{
"type": "ReturnStatement",
"start": 20,
"end": 33,
"argument": {
"type": "BinaryExpression",
"start": 27,
"end": 32,
"left": {
"type": "Identifier",
"start": 27,
"end": 28,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 31,
"end": 32,
"name": "b"
}
}
}
]
}
},
{
"type": "ExportNamedDeclaration",
"start": 35,
"end": 50,
"declaration": null,
"specifiers": [
{
"type": "ExportSpecifier",
"start": 44,
"end": 47,
"local": {
"type": "Identifier",
"start": 44,
"end": 47,
"name": "add"
},
"exported": {
"type": "Identifier",
"start": 44,
"end": 47,
"name": "add"
}
}
],
"source": null
}
],
"sourceType": "module"
}
默认导出字面量
const ast = acorn.parse(`export default 12;`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 18,
"body": [
{
"type": "ExportDefaultDeclaration",
"start": 0,
"end": 18,
"declaration": {
"type": "Literal",
"start": 15,
"end": 17,
"value": 12,
"raw": "12"
}
}
],
"sourceType": "module"
}
默认导出 变量(基本类型)
const ast = acorn.parse(`var Max_Size = 100;export default Max_Size ;`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 45,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 19,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 18,
"id": {
"type": "Identifier",
"start": 4,
"end": 12,
"name": "Max_Size"
},
"init": {
"type": "Literal",
"start": 15,
"end": 18,
"value": 100,
"raw": "100"
}
}
],
"kind": "var"
},
{
"type": "ExportDefaultDeclaration",
"start": 19,
"end": 45,
"declaration": {
"type": "Identifier",
"start": 35,
"end": 43,
"name": "Max_Size"
}
}
],
"sourceType": "module"
}
默认导出变量 (函数)
const ast = acorn.parse(`function a(){} export default a;`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 32,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 14,
"id": {
"type": "Identifier",
"start": 9,
"end": 10,
"name": "a"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 12,
"end": 14,
"body": []
}
},
{
"type": "ExportDefaultDeclaration",
"start": 15,
"end": 32,
// 被导出的声明或表达式
"declaration": {
"type": "Identifier",
"start": 30,
"end": 31,
"name": "a"
}
}
],
"sourceType": "module"
}
默认导出 对象表达式
const ast = acorn.parse(`function add(a, b) {return a + b;} export default { add };`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 58,
"body": [
{
"type": "FunctionDeclaration",
"start": 0,
"end": 34,
"id": {
"type": "Identifier",
"start": 9,
"end": 12,
"name": "add"
},
"expression": false,
"generator": false,
"async": false,
"params": [
{
"type": "Identifier",
"start": 13,
"end": 14,
"name": "a"
},
{
"type": "Identifier",
"start": 16,
"end": 17,
"name": "b"
}
],
"body": {
"type": "BlockStatement",
"start": 19,
"end": 34,
"body": [
{
"type": "ReturnStatement",
"start": 20,
"end": 33,
"argument": {
"type": "BinaryExpression",
"start": 27,
"end": 32,
"left": {
"type": "Identifier",
"start": 27,
"end": 28,
"name": "a"
},
"operator": "+",
"right": {
"type": "Identifier",
"start": 31,
"end": 32,
"name": "b"
}
}
}
]
}
},
{
"type": "ExportDefaultDeclaration", // 默认导出声明
"start": 35,
"end": 58,
"declaration": {
"type": "ObjectExpression",
"start": 50,
"end": 57,
// 对象属性
"properties": [
{
"type": "Property", // 对象属性节点
"start": 52,
"end": 55,
"method": false,
"shorthand": true,
"computed": false,
"key": {
"type": "Identifier",
"start": 52,
"end": 55,
"name": "add"
},
"value": {
"type": "Identifier",
"start": 52,
"end": 55,
"name": "add"
},
// 表示普通数据属性,非getter、setter
"kind": "init"
}
]
}
}
],
"sourceType": "module"
}
默认导出 函数声明
const ast = acorn.parse(`export default function fn() {}`, {
ecmaVersion: 2020,
sourceType: "module",
});
console.log(JSON.stringify(ast, null, 2));
{
"type": "Program",
"start": 0,
"end": 31,
"body": [
{
"type": "ExportDefaultDeclaration",
"start": 0,
"end": 31,
"declaration": {
"type": "FunctionDeclaration",
"start": 15,
"end": 31,
"id": {
"type": "Identifier",
"start": 24,
"end": 26,
"name": "fn"
},
"expression": false,
"generator": false,
"async": false,
"params": [],
"body": {
"type": "BlockStatement",
"start": 29,
"end": 31,
"body": []
}
}
}
],
"sourceType": "module"
}