普通视图
江阴市国联锡洲人工智能产业基金注册成立,出资额5亿元
中国信通院启动智能助理智能体(Claw)系列标准编制工作
ecahrts图形多的页面,怎么解决数据量大的渲染问题?
在一个页面上使用多个 ECharts 图表并处理大量数据时,性能问题确实是个不小的挑战。这主要是因为浏览器既要处理庞大的数据量,又要同时渲染和更新多个图表,很容易导致页面卡顿甚至崩溃。
解决这个问题需要一个系统性的策略,从数据处理、渲染配置、计算优化和资源管理这四个核心层面入手。下面是一个清晰的解决框架:
![]()
🧮 数据层面:从源头"减负"
这是最有效的一步,直接从数据量上下手,减轻前端的处理压力。
-
数据降采样:当数据点超过屏幕像素时,很多点是“画不出来的”。可以通过算法保留关键特征点(如峰值、趋势),同时大幅减少数据量。推荐使用 LTTB (最大三角形三桶) 算法,它能在保留数据轮廓的同时将10万条数据压缩至千条以内,渲染效率提升90%以上。ECharts 也内置了
sampling: 'lttb'配置。 - 数据聚合:在后端或前端对数据进行预处理,比如将每秒的数据聚合成每分钟的平均值、总和等。例如,展示全年订单趋势时,将每日数据聚合为周或月数据,数据量可减少80%以上。
-
数据分片与懒加载:不要一次性请求所有数据。可以先加载概览数据,当用户通过
dataZoom组件缩放或滚动时,再按需加载更精细的数据。ECharts 的appendData方法可以实现增量渲染。
⚙️ 配置层面:让渲染更"轻松"
通过调整ECharts的配置项,可以显著降低图表的渲染计算量。
-
开启“大型数据”模式:在
series中设置large: true,ECharts会启用内部的优化算法进行批量绘制,这对于超过1-2千的数据点非常有效。同时可以设置largeThreshold来手动触发阈值。 -
关闭非必要的视觉效果:
-
动画:设置
animation: false,这在大量数据下可节省约30%的渲染时间。 -
数据点标记:设置
symbol: 'none',隐藏折线上的小圆圈。 -
平滑曲线:设置
smooth: false,因为计算贝塞尔曲线非常消耗CPU。 -
阴影和渐变:简化
itemStyle中的复杂样式。
-
动画:设置
-
启用渐进式渲染:通过
progressive(渐进渲染步长)和progressiveThreshold(启用渐进渲染的阈值)配置,让图表分批次渲染数据点,避免一次性绘制大量图形造成的卡顿。 -
优化交互:将
tooltip的触发方式从默认的'mousemove'改为'click',或者使用tooltip.delay来减少高频计算。
🧠 计算层面:把"重活"放后台
JavaScript是单线程的,复杂的计算会阻塞UI更新。使用Web Worker可以将耗时任务移到后台线程。
-
使用 Web Worker 处理数据:将获取数据、解析数据、执行LTTB降采样算法等CPU密集型任务放在Worker线程中执行。这样,即使后台在处理5万以上的数据点,页面主线程依然可以流畅响应用户的滚动和点击。
// 主线程代码 const worker = new Worker('/path/to/lttb-worker.js'); worker.postMessage({ data: rawData, threshold: 2000 }); worker.onmessage = (event) => { const sampledData = event.data; myChart.setOption({ series: [{ data: sampledData }] }); }; -
事件节流与防抖:对
window.resize、mousemove等高频率事件进行节流(throttle)或防抖(debounce)处理,避免频繁调用chart.resize()或触发重绘。
🧹 工程与资源层面:做好"大管家"
良好的编码习惯和资源管理是页面长期稳定运行的保障。
-
按需引入ECharts模块:不要全量引入ECharts。只引入项目实际用到的图表类型和组件(如
LineChart,BarChart,Grid等),可以有效减少打包后的体积,加快页面加载速度。 -
及时销毁图表实例:在组件卸载或页面隐藏时,务必调用
chart.dispose()方法来释放图表占用的内存和监听器,防止内存泄漏。 -
使用高效的数据格式:对于纯数值型数据,考虑使用
Typed Array(类型数组,如Float32Array)来代替普通的JavaScript数组。它的解析速度更快,内存占用更低,ECharts可以直接消费这类数据。 - 选择正确的渲染器:对于大多数大数据量场景(万级以上),Canvas渲染器是首选,因为它基于像素绘制,无需维护庞大的DOM树。实测数据显示,10万数据点下Canvas渲染时间(约900ms)远优于SVG(约1800ms)。
今年前两月广州港外贸吞吐量超3000万吨,增长显著
MOVA全面披露芯片战略,首次系统发布“家庭AI系统”
断网也能装包? 我在物理隔离内网搭了一套完整的私有npm仓库
![]()
引言
你有没有想过这样一种场景,你所有的开发电脑全部都是内网的完全的物理隔离,而且这台电脑上没有安装前端开的的环境,但是你的项目又是不同的技术栈,比如说vue2、vue3、react等,同时你使用包管理器可能有npm、yarn、pnpm、bun等,而且你还需要在内网环境中开发桌面端类似于electron、tauri等,同时你的构建还需要二进制的依赖等,并且你还使用vue3开发了一些业务组件库之类的,解决这类问题,那这种你可能又两种选择:
- 方案一:全部改成npm,把node_modules跟着项目走;
- 方案二:使用verdaccio搭建完整的私服npm源,支持不同的技术栈和包管理器
那就引发出来一类场景:我们怎么在一台纯物理隔离的纯净电脑上搭建出支持不同技术栈的前端私有npm源!
下面我们将开启verdaccio使用,希望你读完这篇文章之后能对verdaccio有一定的了解和认识。
准备
由于我们是全新的环境,以windows环境为例,我们首选需要安装的是node, 但是我们的项目又涉及不同的vue版本等,所有的开发环境需要多个node版本,鉴于遮掩这样的情况我们需要node版本的管理工具,常见的用nvm、fnm、volta等,这里以使用nvm为例进行安装(可以自己自行选择),下面以nvm进行操作。
换句话说下面的操作可以让我们在一个纯净的内网机器下把前端开发前置环境搭建起来!
NVM 安装部署
下面示例进行windows和linux的nvm的安装。
Windows环境安装
下载nvm-windows
在github上搜索
nvm-windows, 进入项目,点击右侧的Releases的,下载nvm-setup.exe或nvm-noinstall.zip。
Github地址:github.com/coreybutler…
下载node js
访问 nodejs.org/dist/ 下载需要的版本
Windows版本列表:
https://nodejs.org/dist/v12.22.12/node-v12.22.12-win-x64.zip
https://nodejs.org/dist/v14.21.3/node-v14.21.3-win-x64.zip
https://nodejs.org/dist/v16.20.2/node-v16.20.2-win-x64.zip
https://nodejs.org/dist/v18.19.0/node-v18.19.0-win-x64.zip
https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip
安装nvm-windows
- 点击 nvm-setup.exe 进行安装
- 解压 nvm-noinstall.zip 到指定目录
安装目录以:C:\nvm为例, 后续操作都是再这个目录下进行,如需更换做相应的替换即可
安装时记住两个路径:
- nvm安装路径:
C:\nvm - node js符号链接路径:
C:\Program Files\nodejs
手动添加node js版本
将下载的 node js 手动压缩包解压到 nvm 安装目录下:
操作步骤:
# 1. 解压 node-v12.22.12-win-x64.zip
# 2. 将解压后的文件夹重命名为 v12.22.12
# 3. 将 v12.22.12 文件夹移动到 C:\nvm\ 目录下
# 4. 重复以上步骤处理其他版本
# 目录结构应该是这样
C:\nvm\
├─ v12.22.12\
│ ├─ node.exe
│ ├─ npm
│ └─ ...
├─ v14.21.3\
├─ v16.20.2\
├─ v18.19.0\
└─ v20.11.0\
修改settings.txt
编辑 C:\nvm\settings.txt,进行下面的修改,修改对应的root和path:
root: C:\nvm
path: C:\Program Files\nodejs
arch: 64
proxy: none
配置环境变量
确保以下环境变量已设置:
NVM_HOME = C:\nvm
NVM_SYMLINK = C:\Program Files\nodejs
Path 中添加:
%NVM_HOME%
%NVM_SYMLINK%
Linux环境安装
下载nvm
在github上搜索
nvm, 进入项目,点击右侧的Releases的,下载对应版本的,解压即可。
Github地址: github.com/nvm-sh/nvm
下载node js
访问 nodejs.org/dist/ 下载需要的版本
Linux版本列表:
https://nodejs.org/dist/v12.22.12/node-v12.22.12-linux-x64.tar.gz
https://nodejs.org/dist/v14.21.3/node-v14.21.3-linux-x64.tar.gz
...
下载linux上需要的node安装包时需要注意架构版本arm和x64是有区别的,留意一下即可
安装nvm
步骤:
# 以某个版本为例
wget https://github.com/nvm-sh/nvm/archive/refs/tags/v0.39.0.tar.gz
tar -xzf v0.39.0.tar.gz
mv nvm-0.39.0 .nvm
配置环境变量
编辑 ~/.bashrc 或 ~/.zshrc:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
生效配置:
source ~/.bashrc # 或 source ~/.zshrc
手动添加node js版本
# 创建版本目录
mkdir -p ~/.nvm/versions/node
# 解压并移动 Node.js
tar -xzf node-v12.22.12-linux-x64.tar.gz
mv node-v12.22.12-linux-x64 ~/.nvm/versions/node/v12.22.12
# 重复以上步骤处理其他版本
目录结构:
~/.nvm/versions/node/
├─ v12.22.12/
├─ v14.21.3/
├─ v16.20.2/
├─ v18.19.0/
└─ v20.11.0/
NVM验证和使用
验证安装
# 重新加载环境
source ~/.bashrc
# 验证NVM
nvm --version
# 查看已安装的Node.js版本
nvm list
# 使用特定版本
nvm use 18.17.1
# 验证Node.js和npm
node --version
npm --version
# 测试npm基本功能
npm config list
使用操作
# 切换Node.js版本
nvm use 16.20.2 # 切换到16版本
nvm use 18.17.1 # 切换到18版本
nvm use 20.5.1 # 切换到20版本
# 设置默认版本
nvm alias default 18.17.1
# 查看当前使用的版本
nvm current
# 查看所有可用版本
nvm list
# 在特定版本下运行命令
nvm exec 16.20.2 node --version
# 显示可用命令
nvm --help
# 卸载指定版本(删除文件夹)
nvm uninstall 12.22.12
因为我们时离线安装,所有nvm install version其实不可用的,上述步骤就是手动的实现这个命令。
NVM常见问题
Linux找不到nvm命令
# 手动加载NVM
source ~/.nvm/nvm.sh
# 或者重新加载bashrc
source ~/.bashrc
切换版本后 node 命令无效
# Windows: 重新打开命令行窗口
# Linux/Mac: 检查 PATH 环境变量
echo $PATH
# 手动设置PATH
export PATH="$NVM_DIR/versions/node/v18.17.1/bin:$PATH"
npm 全局包丢失
这个是个比较常见的问题:
- 每个node版本有独立的全局包目录
- 切换版本后需要重新安装全局包
node版本无法切换
# 检查版本目录是否正确
ls -la ~/.nvm/versions/node/
# 手动设置PATH
export PATH="$NVM_DIR/versions/node/v18.17.1/bin:$PATH"
权限问题
# 修复NVM目录权限
chmod -R 755 ~/.nvm
chmod +x ~/.nvm/nvm.sh
NVM卸载
windows卸载
把安装目录和环境变量删除即可
linux卸载
如果需要完全卸载:
# 删除NVM目录
rm -rf ~/.nvm
# 从.bashrc中移除NVM配置
sed -i '/# NVM配置/,/# 加载nvm bash补全/d' ~/.bashrc
# 重新加载shell配置
source ~/.bashrc
上面的操作我们相当于再内网机器上安装了node和node的版本管理器,这是第一步也是前置准备工作,下面将进入到verdaccio搭建的正式环节。
Verdaccio 安装部署
首先想一个问题,我们的verdaccio是通过npm全局安装的,verdaccio是发布到npm js的,但是我的内网环境是访问不了npm js的?我需要怎么做呢?
下面以windows系统为例,详细说明我们要做的事情:
- 第一步:可以上网的机器安装verdaccio
- 第二部:利用verdaccio缓存需要的依赖,包括项目依赖和全局依赖(pnpm等)
- 第三步:把verdaccio本身以及缓存的依赖全部平移到内部机器
- 第四步:根据需求动态的更新verdaccio的storage中的依赖和config.yaml
- 第五步:把更新的storage依赖和config.yaml传输到内部机器下做合并
重复上述步骤便能实现大多数场景的依赖私有化部署(但是某些特殊情况下可能不适用,后续单独说明)。
Windows环境安装部署
Verdaccio安装
我们上面已经安装了node,那我们直接使用npm再上网机上面直接全局安装
# 如果担心有问题,可以使用管理员权限安装
npm install -g verdaccio
# 其他包管理器的安装方式
yarn global add verdaccio
pnpm install -g verdaccio
等待安装完成即可。
安装完成之后执行:
verdaccio --version # 验证是否安装成功
之后进行启动,启动之后的启动信息要注意查看,这是有用的:
C:\Users\LMX>verdaccio
info --- config file - C:\Users\LMX\AppData\Roaming\verdaccio\config.yaml
info --- plugin @verdaccio/local-storage successfully loaded (storage)
info --- using htpasswd file: C:\Users\LMX\AppData\Roaming\verdaccio\htpasswd
info --- plugin verdaccio-htpasswd successfully loaded (authentication)
info --- plugin verdaccio-audit successfully loaded (middleware)
info --- plugin @verdaccio/ui-theme successfully loaded (theme)
warn --- http address - http://localhost:4873/ - verdaccio/6.2.1
如果不侧重私有包的管理的话,我们要关注两个地方:
- C:\Users\LMX\AppData\Roaming\verdaccio\ (配置文件和storage所在路径)
- http://localhost:4873/ (可视化地址,我们后续会让所有的依赖再这个地址简单显示)
注册安装源
verdaccio安装完成之后,后续一个比较重要的动作就是注册各个包管理器的registry,以便verdaccio可以正常的缓存依赖。
为所有的项目设置全局的注册中心:
npm set config registry http://localhost:4873 # 注册地址就是上面的verdaccio的访问地址
yarn config set registry http://localhost:4873
也可以为某个依赖单独注册:
npm install lodash --registry http://localhost:4873
其他常用的命令:
# 终端窗口打开项目的根目录
npm set registry http://localhost:4873/ --location project
# 单次依赖安装指定注册中心,以lodash为例
npm install lodash --registry http://localhost:4873
上面的命令其实就相当于再你的项目.npmrc下增加了注册中心, 你也可以直接修改.npmrc:
registry=http://localhost:4873/
但是现在好多vue3的项目都是
monorepo多包管理的,那我们肯定离不开pnpm,其实这里有扩展出来一点知识点,
纯内网机制下的pnpm全局安装,这是其实用两种实现思路。
- 思路一: 借助verdaccio缓存pnpm依赖
- 思路二: 借助npm的pack命令打包pnpm
思路二后续章节展开说明,先按照思路一进行实践。
按照上述步骤启动verdaccio,并指定pnpm的注册源:
verdaccio
npm install pnpm --registry http://localhost:4873
这时verdaccio的窗口会显示pnpm的被缓存安装的信息:
C:\Users\LMX>verdaccio
warn --- This is a deprecated method, please use runServer instead
info --- config file - C:\Users\LMX\AppData\Roaming\verdaccio\config.yaml
info --- plugin @verdaccio/local-storage successfully loaded (storage)
info --- using htpasswd file: C:\Users\LMX\AppData\Roaming\verdaccio\htpasswd
info --- plugin verdaccio-htpasswd successfully loaded (authentication)
info --- plugin verdaccio-audit successfully loaded (middleware)
info --- plugin @verdaccio/ui-theme successfully loaded (theme)
warn --- http address - http://localhost:4873/ - verdaccio/6.2.1
info <-- 127.0.0.1 requested 'GET /pnpm'
http <-- 200, user: null(127.0.0.1), req: 'GET /pnpm', bytes: 0/0
info --- making request: 'GET https://registry.npmjs.org/pnpm'
http --- 200, req: 'GET https://registry.npmjs.org/pnpm' (streaming)
http --- 200, req: 'GET https://registry.npmjs.org/pnpm', bytes: 0/5275788
http <-- 200, user: null(127.0.0.1), req: 'GET /pnpm', bytes: 0/779825
info <-- 127.0.0.1 requested 'POST /-/npm/v1/security/advisories/bulk'
http <-- 200, user: null(127.0.0.1), req: 'POST /-/npm/v1/security/advisories/bulk', bytes: 40/0
info <-- 127.0.0.1 requested 'GET /pnpm/-/pnpm-10.32.1.tgz'
http <-- 200, user: null(127.0.0.1), req: 'GET /pnpm/-/pnpm-10.32.1.tgz', bytes: 0/0
info --- making request: 'GET https://registry.npmjs.org/pnpm/-/pnpm-10.32.1.tgz'
http <-- 200, user: null(127.0.0.1), req: 'POST /-/npm/v1/security/advisories/bulk', bytes: 40/2
http <-- 200, user: null(127.0.0.1), req: 'GET /pnpm/-/pnpm-10.32.1.tgz', bytes: 0/4524746
之后去verdaccio的storage目录查看是否被正常的缓存了。
正常pnpm文件夹下有两个文件:
package.json
pnpm-10.32.1.tgz
正常的话,tgz的包是有大小的,当是ok的时候要注意了,当然也不可能只有pnpm这个一个依赖,它是依赖树,这里只是举例说明。
注意点:
我们使用verdaccio做依赖缓存时可能会遇到,依赖没有缓存到的情况,确实存在这种情况,一般的处理思路如下:
- 清空缓存再次进行安装
- 重启verdaccio再次进行安装
- 条件允许的话删除整个verdaccio缓存再次安装
相关命令如下:
npm cache clean --force
pnpm store prune
配置文件
上面配置完注册源了,我们需要来了解一下俩个文件config.yaml和.verdaccio-db.json, 这两个文件对我们后续操作有帮助,需要了解它。
config.yaml
这个文件是和storage再同级目录的,文件的配置内容如下,我们需要了解其中的一些配置项:
# 缓存依赖的存储位置
storage: ./storage
# 监听所有网卡(让其他机器能访问)
listen: 0.0.0.0:4873
web:
title: Verdaccio
auth:
htpasswd:
file: ./htpasswd
# 内网机器的一定要关闭uplinks
uplinks:
npmjs:
url: https://registry.npmjs.org/
cache: true
packages:
"@*/*":
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs
"**":
access: $all
publish: $authenticated
unpublish: $authenticated
proxy: npmjs # 内网机绝对不能有这行
server:
keepAliveTimeout: 60
middlewares:
audit:
enabled: true
log:
type: stdout
format: pretty
level: http
i18n:
web: en-US
cache属性官方的表述: 设置cache为false可以帮助节省你的硬盘空间。 这将避免存储tarballs,但是它将保留元数据在文件夹里。
其实官方的解释有点晦涩,其实可以这样理解:
- cache: true(默认值); 从上游拉取的tarball会缓存到
storage目录下,下次请求直接从本地返回 - cache: false; tarball不会写入磁盘,但是包的元数据(package.json、版本列表等)依然会被缓存,每次请求tarball都会请求上游。
在解释一下tarball是啥? 答:是依赖包的tgz文件。
这个配置设置不当也是导致缓存不能进行的一个重要原因。
更近一步说明:
⚠️ 关键点:内网机:物理隔离环境下,一定要去掉 proxy: npmjs,否则每次装包都会卡在尝试连接外网。
⚠️ 关键点:外网机:可以上网的机器,不要去掉proxy但是cache不设置或者设置为true
.verdaccio-db.json
我们怎么理解.verdaccio-db.json?
官方给的说法是:
微型数据库用于存储用户发布的私有包。 该数据库基于JSON文件,其中包含已发布的私有包列表以及用于令牌签名的秘密令牌。 首次启动应用程序时会自动创建。
文件的内容如下:
{ "list": [], "secret": "KzZPQifqvCrV7HBMyVPb1C9+FdWteKqe" }
我这里放出了secret(令牌密钥), 正常来说这个是不能暴漏的,只做演示用。
启动verdaccio,访问: http://localhost:4873, 我们会发现什么也没有,那是因为我们的list: []是空的。
只用我们发部私服npm包时(发布私服包verdaccio才主动写入数据库文件),list才会更新。
但是我们想要verdaccio承担私用npm的方式,所有我们想要我们已经缓存的包放追加到list中,因此我们需要编写一个js文件sync-verdaccio-db.js来更新它:
主要实现的思路是访问:http://localhost:4873/-/all,获取所有的依赖数据。
const fs = require("fs");
const path = require("path");
const http = require("http");
const VERDACCIO_URL = "http://localhost:4873";
// 根据实际路径调整
const DB_PATH = path.join(__dirname, "storage", ".verdaccio-db.json");
/**
* 获取所有包名(从/-/all)
* @returns
*/
function fetchPackages() {
return new Promise((resolve, reject) => {
http
.get(`${VERDACCIO_URL}/-/all`, (res) => {
let data = "";
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
const json = JSON.parse(data);
resolve(Object.keys(json)); // 返回包名数组
} catch (e) {
reject(e);
}
});
})
.on("error", reject);
});
}
/**
* 更新数据库文件
* @param {*} packages
*/
function updateDb(packages) {
let db;
try {
db = JSON.parse(fs.readFileSync(DB_PATH, "utf8"));
} catch (e) {
// 文件不存在或损坏,创建一个新的
db = {
list: [],
secret: require("crypto").randomBytes(16).toString("hex"),
};
}
// 去重并排序(可选)
db.list = [...new Set(packages)].sort();
fs.writeFileSync(DB_PATH, JSON.stringify(db, null, 2));
console.log(
`[${new Date().toISOString()}] Database updated with ${db.list.length} packages.`,
);
}
/**
* 主函数
*/
async function main() {
try {
const packages = await fetchPackages();
updateDb(packages);
} catch (err) {
console.error("Sync failed:", err.message);
}
}
main();
注意: 我们上面的做法只是想让安装了哪些依赖都显示出来,依赖是有依赖树的,这个默认都理解,不做过多解释!
先启动verdaccio, 再storage的同级目录使用node执行sync-verdaccio-db.js文件即可,如下:
node .sync-verdaccio-db.js
⚠️ 完成之后重启verdaccio,再浏览器中打开http://localhost:4873 , 可以查看缓存的依赖了。
创建用户
verdaccio的用户注册和登录之类的也加单说明一下,可以后续会用的上,但不是当前文章的侧重点。
使用npm 8或更低版本时, adduser或login都可以同时创建用户并登录。
npm adduser --registry http://localhost:4873
在 npm@9 之后的版本,这两个命令分开工作:
npm login --registry http://localhost:4873
npm adduser --registry http://localhost:4873
默认情况下,两个命令都依赖于web登录,添加 --auth-type=legacy 可以使用之前的登录方式。
内网部署
上述操作完成之后,下一步需要做的就是怎么把verdaccio整体部署到内网机器了,只要把verdaccio移入到内网机器,启动起来,后续只需要 更新storage和.verdaccio-db.json就可以实现依赖的更新了。
其实也有两种思路:
- 思路一: 使用npm的pack打包verdaccio, 之后再内网机器使用npm全局安装
- 思路二: 把上网机器上的安装的verdaccio直接复制到内网机(个人推荐最可靠方式)
思路一感觉上规范,但是可能遇到意想不到的问题,思路二在windows环境成功率更高,先介绍思路二,在说明思路一。
上面已经全局安装了,我们需要查看全局安装路径
npm config get prefix
输出结果如下:
C:\Users\LMX>npm config get prefix
C:\nvm4w\nodejs # 这个路径是你自己的,每个人的安装路径都不一样
进入到安装目录复制相关文件到内网机器下npm的相同目录
需要复制的文件包括(verdaccio、verdaccio.cmd、verdaccio.ps1和node_modules/verdaccio文件夹):
![]()
![]()
最后内网机器验证verdaccio是否成功
verdaccio --version
verdaccio安装成功之后,启动verdaccio即可,在将上述保存的storage和config.yaml复制到内网机器下verdaccio缓存目录
思路一的方式虽然有点野路子,但是是在windows下尝试成功概率比较高的方式。
稍微解释一下为啥思路一的简单粗暴:
我们安装依赖的时候不是单个的依赖,却是复杂的依赖树,我们直接拷贝node_modules/verdaccio 相当于跳过了复杂依赖树的查找和安装过程,这种操作相当于是一个环境依赖平移的过程。
下面在来说明一下看似正规的思路一:
思路一要想成功需要很重要的一点,不光要npm pack verdaccio,而且要npm pack每一个verdaccio需要的子依赖,极其繁琐,容易出错。
那我们是否可以直接打包node_modules/verdaccio,类似于方案二的思想呢?那其实有回到思路二,这里先不做过多的说明, 其实本质上就是思路二脚本化和标准化的一个过程。
但是我们可以通过思路一和思路二想到一个更优的实践方式:
用 verdaccio 来引导 verdaccio
先用思路二在内网机上部署完verdaccio,在外网启动verdaccio,让它缓存verdaccio自身及其所有依赖,然后把存储数据一起复制到内网机verdaccio的缓存下。 其他内网机直接通过已安装的verdaccio的内网机安装verdaccio即可。
上面所有的安装都是以windows进行举例说明的,那当我们的开发环境是linux的时候,我们需要怎么做呢?
通常来说前端大部分的开发环境是都是windows的,所有windows部分才是我们最关心的!
正好借此说明一下不通过verdaccio缓存pnpm, 通过打包的方式平移pnpm
Linux环境安装部署
linux环境简短说明,大家了解即可,不是本篇文章的侧重点。
Verdaccio安装
# 安装 Verdaccio 和 pnpm
npm install -g verdaccio pnpm
# 验证安装
verdaccio --version
pnpm --version
确认安装结构
PREFIX=$(npm config get prefix)
# 查看可执行文件类型(确认是否为软链接)
ls -la $PREFIX/bin/ | grep -E "verdaccio|pnpm"
# 示例输出:
# lrwxrwxrwx ... verdaccio -> ../lib/node_modules/verdaccio/bin/verdaccio
# lrwxrwxrwx ... pnpm -> ../lib/node_modules/pnpm/bin/pnpm.cjs
# 查看模块目录
ls $PREFIX/lib/node_modules/ | grep -E "verdaccio|pnpm"
启动verdaccio并下载项目依赖
# 后台启动 Verdaccio
verdaccio &
# 配置 pnpm 使用 Verdaccio
pnpm config set registry http://localhost:4873/
# 安装所有项目依赖(会自动缓存到 Verdaccio)
cd /path/to/project1
pnpm install
cd /path/to/project2
pnpm install
# 重复所有项目...
# 完成后停止 Verdaccio
pkill -f verdaccio
打包所有文件
PREFIX=$(npm config get prefix)
mkdir -p ~/verdaccio-offline-package
cd ~/verdaccio-offline-package
# 打包完整模块目录(含所有子依赖,这是关键)
tar -czf verdaccio-full.tar.gz -C $PREFIX/lib/node_modules verdaccio
tar -czf pnpm-full.tar.gz -C $PREFIX/lib/node_modules pnpm
# 记录软链接目标路径(供内网还原使用)
VERDACCIO_LINK=$(readlink $PREFIX/bin/verdaccio)
PNPM_LINK=$(readlink $PREFIX/bin/pnpm)
PNPX_LINK=$(readlink $PREFIX/bin/pnpx 2>/dev/null || echo "")
cat > link-targets.txt << EOF
VERDACCIO_LINK=$VERDACCIO_LINK
PNPM_LINK=$PNPM_LINK
PNPX_LINK=$PNPX_LINK
EOF
echo "软链接信息已记录:"
cat link-targets.txt
# 打包 verdaccio 配置和依赖缓存
tar -czf verdaccio-data.tar.gz -C ~ .config/verdaccio
# 查看打包结果
ls -lh
# 应该看到:
# verdaccio-full.tar.gz # Verdaccio 完整模块
# pnpm-full.tar.gz # pnpm 完整模块
# link-targets.txt # 软链接记录
# verdaccio-data.tar.gz # 配置和依赖缓存
du -sh .
内网解压部署
cd /tmp
tar -xzf verdaccio-offline-linux.tar.gz
cd verdaccio-offline-package
# 查看文件
ls -lh
配置npm用户全局安装目录
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
# 确认配置
npm config get prefix
还原模块文件
cd /tmp/verdaccio-offline-package
PREFIX=$(npm config get prefix)
# 确保目标目录存在
mkdir -p $PREFIX/lib/node_modules
mkdir -p $PREFIX/bin
# 还原完整模块(含所有子依赖)
tar -xzf verdaccio-full.tar.gz -C $PREFIX/lib/node_modules/
tar -xzf pnpm-full.tar.gz -C $PREFIX/lib/node_modules/
# 验证模块已还原
ls $PREFIX/lib/node_modules/ | grep -E "verdaccio|pnpm"
还原软链接
PREFIX=$(npm config get prefix)
# 读取外网记录的软链接目标
source /tmp/verdaccio-offline-package/link-targets.txt
# 建立软链接
ln -sf $PREFIX/lib/node_modules/$VERDACCIO_LINK $PREFIX/bin/verdaccio
ln -sf $PREFIX/lib/node_modules/$PNPM_LINK $PREFIX/bin/pnpm
# pnpx 可选
if [ -n "$PNPX_LINK" ]; then
ln -sf $PREFIX/lib/node_modules/$PNPX_LINK $PREFIX/bin/pnpx
fi
# 验证软链接
ls -la $PREFIX/bin/ | grep -E "verdaccio|pnpm"
还原Verdaccio数据
tar -xzf /tmp/verdaccio-offline-package/verdaccio-data.tar.gz -C ~
# 验证
ls -la ~/.config/verdaccio/
ls ~/.config/verdaccio/storage/
设置systemd服务(可选)
PREFIX=$(npm config get prefix)
sudo tee /etc/systemd/system/verdaccio.service > /dev/null << EOF
[Unit]
Description=Verdaccio Private NPM Registry
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=$HOME
Environment=PATH=$PREFIX/bin:$HOME/nodejs/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=$PREFIX/bin/verdaccio
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl start verdaccio
sudo systemctl enable verdaccio
# 查看状态
sudo systemctl status verdaccio
# 查看日志
sudo journalctl -u verdaccio -f
缺失内容
上述我们只提到了最常见的场景,但是还有几类场景是不能忽视的,由于篇幅原因,这里先列出来,不做展开了,后续在补充。
- electron/tauri等桌面端构建(涉及native binary 依赖的处理)
- 二进制依赖, 如何处理 node-gyp、prebuild等
- 私有业务组件库发布流程为涉及说明
扩展和思考
其实理论上有更加成熟的方案,Nexus Repository Manager或者淘宝 cnpm方案,但是cnpmcore对纯物理隔离部署有一定的要求,上述方案也是仅针对前端开发者去部署的,有更好的方案可以一起讨论。
南向资金净买入额超50亿港元
香港:已关注到OpenClaw的潜在风险,建议相关单位采取充足安全措施
基于 wujie.js 进行微前端融合
项目背景
目前项目中需要将A ,B 两个项目进行融合,A ,B 项目有各自的用户token与自己的交互方式,技术栈一致。之前是用跳链的方式,新开一个页面进行操作,给用户的感官是两个项目较为割裂,目前期望让两个项目看起来像一个整体,减少割裂感。
解决方案
- iframe内嵌 + 菜单融合
- 微前端融合
iframe内嵌 + 菜单融合
iframe内嵌有两种方式:
第一种是将整个B 项目作为一个页面融入A 项目,这样做的好处是切换页面不会重复请求,session,local等存储便于管理,不会出现样式覆盖。缺点是依旧有一些割裂感,且B 项目是整体作为一个页面,感官上很奇怪。
第二种是将B 项目的多个页面融入A 项目的菜单,整体感官上会是一个整体,但是切换页面的时候容易造成重复请求,白屏等情况,用户体验较差
微前端融合——qiankun
qiankun 是一个由阿里巴巴(蚂蚁金服)团队开源的微前端解决方案。它基于Single-SPA进行二次开发和增强,旨在帮助开发者将单体前端应用改造为由多个独立“微应用”聚合而成的架构。
如果你是使用
umi框架进行开发,我建议使用qiankun,因为umi提供开箱即用的微前端支持,通过简单的配置即可实现微前端融合
qiankun 使用 Proxy沙箱拦截全局变量,通过重写CSS选择器,添加前缀实现样式隔离,依赖 import-html-entry解析HTML。通过 props 传递和 globalState 进行全局状态管理。基于数据流传递内容
微前端融合——wujie
Wujie 是由京东物流技术团队自主研发的微前端框架,用于解决大型前端应用在模块解耦、独立部署、技术栈兼容等方面的挑战。
如果你是使用 vue 进行页面开发,同时期望用尽可能少的改动实现简单的微前端融合,我推荐使用 wujie。
无界使用iframe 作为JS运行沙箱,通过 WebComponent 将渲染内容映射到主应用上,既享受了iframe对JS的绝对隔离,又避免了iframe的UI通信和样式局限性。CSS通过样式作用域和 Shadow-DOM机制,隔离性优于qiankun的Proxy模式。原生支持子应用保活,预加载等功能,开发改动量小。
Wujie 接入
详细代码见:wujie/examples at master · Tencent/wujie · GitHub
主应用改造
main.js 改造
// main.js
import WujieVue from "wujie-vue2";
const isProduction = process.env.NODE_ENV === "production";
const { setupApp, preloadApp, bus } = WujieVue;
bus.$on("click", (msg) => window.alert(msg));
// 在 xxx-sub 路由下子应用将激活路由同步给主应用,主应用跳转对应路由高亮菜单栏
bus.$on("sub-route-change", (name, path) => {
const mainName = `${name}-sub`;
const mainPath = `/${name}-sub${path}`;
const currentName = router.currentRoute.name;
const currentPath = router.currentRoute.path;
if (mainName === currentName && mainPath !== currentPath) {
router.push({ path: mainPath });
}
});
const degrade = window.localStorage.getItem("degrade") === "true" || !window.Proxy || !window.CustomElementRegistry;
const props = {
jump: (name) => {
router.push({ name });
},
};
/**
* 大部分业务无需设置 attrs
* 此处修正 iframe 的 src,是防止github pages csp报错
* 因为默认是只有 host+port,没有携带路径
*/
const attrs = isProduction ? { src: hostMap("//localhost:8000/") } : {};
/**
* 配置应用,主要是设置默认配置
* preloadApp、startApp的配置会基于这个配置做覆盖
*/
setupApp({
name: "react16",
url: hostMap("//localhost:7600/"),
attrs,
exec: true,
props,
fetch: credentialsFetch,
plugins,
prefix: { "prefix-dialog": "/dialog", "prefix-location": "/location" },
degrade,
...lifecycles,
});
子应用渲染组件实现
//Vue2Sub
<template>
<!--单例模式,name相同则复用一个无界实例,改变url则子应用重新渲染实例到对应路由 -->
<WujieVue width="100%" height="100%" name="vue2" :url="vue2Url"></WujieVue>
</template>
<script>
import hostMap from "../hostMap";
export default {
computed: {
vue2Url() {
return hostMap("//localhost:7200/") + `#/${this.$route.params.path}`;
},
},
methods: {
jump(name) {
this.$router.push({ name });
},
},
};
</script>
<style lang="scss" scoped></style>
编写路由!!!
//router.js
{
path: "/vue2-sub/:path",
name: "vue2-sub",
component: Vue2Sub,
},
总结
wujie 的优势很明显,代码改动量小,无需配置子应用前缀,适合快速接入开发的项目,数据传递主要依赖于事件系统,例如我本来想传递一个全局用户信息给子应用,结果发现数据变化时,不会触发任何内容,并且子应用的 props 也没有更新,所以我自己基于事件传递实现了一套自动化获取token然后登陆的逻辑,实现相关功能。此外wujie的文档较为简单,有些功能函数的具体作用也不是很清晰,只能根据自己去猜(也可能是笔者能力较弱,叠个甲,哈哈哈)
速腾聚创千线级激光雷达组合获百度萝卜快跑独家定点
深入解析 Vue 包:`vue` 究竟导出了什么?
本文系统梳理 Vue 3 各子包(
vue、@vue/reactivity、@vue/runtime-core、@vue/runtime-dom、@vue/compiler-core、@vue/compiler-dom、@vue/shared)的完整导出结构,重点覆盖官方文档未详细说明、日常开发中被忽视但架构价值极高的 API。
一、Vue 包体系全景:Monorepo 架构
很多开发者误认为 vue 是一个完整的实现包,实则它只是一个 Facade(门面模块) :
vue(聚合导出层)
│
├── @vue/runtime-dom ← 浏览器平台渲染层
│ └── @vue/runtime-core ← 平台无关运行时
│ └── @vue/reactivity ← 纯响应式系统
│
├── @vue/compiler-dom ← 浏览器模板编译器
│ └── @vue/compiler-core ← 平台无关编译器
│
└── @vue/shared ← 内部共享工具函数
每个子包都可以独立安装使用,这正是 Vue 3 架构解耦的核心体现。
二、@vue/reactivity:独立响应式引擎
这是 Vue 3 最具突破性的设计——响应式系统与框架完全解耦,可以在任何 JS 环境中独立使用。
npm install @vue/reactivity
2.1 核心响应式原语
ref / shallowRef / customRef
import { ref, shallowRef, customRef } from '@vue/reactivity'
// 标准 ref:深度响应
const state = ref({ a: { b: 1 } })
// shallowRef:只有 .value 本身是响应式,内部属性不追踪
const shallow = shallowRef({ count: 0 })
shallow.value.count++ // 不触发更新
shallow.value = { count: 1 } // 触发更新
// customRef:完全自定义追踪时机(防抖、节流场景极为有用)
function useDebouncedRef(value, delay = 200) {
let timer
return customRef((track, trigger) => ({
get() {
track() // 手动声明依赖追踪
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
trigger() // 手动触发更新
}, delay)
}
}))
}
customRef 是实现防抖输入、异步数据源等场景的底层利器,但鲜少被人使用。
reactive / shallowReactive / readonly / shallowReadonly
import { reactive, shallowReactive, readonly, shallowReadonly } from '@vue/reactivity'
// shallowReactive:只追踪顶层属性,不深度转换
const state = shallowReactive({
user: { name: 'Tom' } // user.name 的变化不会触发更新
})
// readonly:深度只读代理,任何写入都会在 dev 环境报警告
const config = readonly({ api: 'https://example.com' })
// shallowReadonly:只有顶层只读,嵌套对象仍可修改
const mixed = shallowReadonly({ nested: { value: 1 } })
mixed.nested.value = 2 // 允许(不报警告)
mixed.nested = {} // 阻止(报警告)
2.2 副作用系统底层 API(极少被直接使用)
effect / ReactiveEffect
effect 是 Vue 响应式系统最底层的副作用原语,computed、watch 都是基于它构建的。
import { effect, reactive } from '@vue/reactivity'
const state = reactive({ count: 0 })
// effect 会立即执行,并自动追踪其中访问的响应式数据
const runner = effect(() => {
console.log('count is:', state.count)
})
state.count++ // 自动重新执行 effect
// 手动停止追踪
runner.effect.stop()
effectScope / getCurrentScope / onScopeDispose
这组 API 是 Vue 3.2 引入的副作用作用域管理机制,专为编写可复用组合函数设计,但日常业务代码中极少被直接使用。
import { effectScope, getCurrentScope, onScopeDispose } from '@vue/reactivity'
// 创建一个作用域,批量管理所有 effect/watch/computed
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => count.value * 2)
watchEffect(() => console.log(doubled.value))
})
// 一次性停止作用域内所有副作用——无需逐一手动 stop()
scope.stop()
架构价值:在大型组合函数库(如 VueUse)中,effectScope 是正确管理副作用生命周期的标准方式,避免内存泄漏。
// 在自定义组合函数中使用
function useFeature() {
const scope = effectScope()
scope.run(() => {
// 所有 effect 在这里注册
onScopeDispose(() => {
// scope 被 stop 时的清理逻辑
scope.stop()
})
})
return scope
}
pauseTracking / resumeTracking / resetTracking
这组 API 允许手动控制依赖追踪的暂停与恢复,是构建高级响应式工具的基础。
import { pauseTracking, resetTracking, effect } from '@vue/reactivity'
effect(() => {
state.a // 正常追踪
pauseTracking()
state.b // 访问但不追踪,state.b 变化不会触发 re-run
resetTracking()
state.c // 恢复正常追踪
})
实际应用场景:在实现 readonly 包装、不希望某些读操作建立依赖时使用。
trackOpBit / triggerRef
import { ref, triggerRef } from '@vue/reactivity'
// triggerRef:强制触发 shallowRef 的更新(直接修改内部结构后手动通知)
const state = shallowRef({ list: [1, 2, 3] })
state.value.list.push(4) // 直接改内部,不触发更新
triggerRef(state) // 手动强制触发
2.3 工具函数
toRaw / markRaw
import { reactive, toRaw, markRaw } from '@vue/reactivity'
const state = reactive({ data: {} })
// toRaw:获取响应式对象的原始对象(绕过 Proxy),用于性能敏感操作
const raw = toRaw(state) // 直接操作,不触发任何追踪
// markRaw:标记对象永不被转为响应式(第三方库实例、大型数据集等)
const chart = markRaw(new ECharts())
const state2 = reactive({ chart }) // chart 不会被代理
架构意义:在将第三方库对象(如 Canvas 实例、WebSocket 对象)放入响应式状态时,必须用 markRaw 避免性能问题。
proxyRefs
import { proxyRefs, ref } from '@vue/reactivity'
// proxyRefs:自动解包对象中的所有 ref(这正是 setup() 返回值的内部处理机制)
const state = { count: ref(0), name: ref('Tom') }
const proxied = proxyRefs(state)
proxied.count // 直接访问,无需 .value(内部自动解包)
这是 Vue 模板自动解包 ref 的底层实现机制。
isRef / isReactive / isReadonly / isProxy / isShallow
import { ref, reactive, readonly, isRef, isReactive, isReadonly, isProxy, isShallow, shallowRef } from '@vue/reactivity'
isRef(ref(0)) // true
isReactive(reactive({})) // true
isReadonly(readonly({})) // true
isProxy(reactive({})) // true(reactive 和 readonly 都是 Proxy)
isShallow(shallowRef(0)) // true(Vue 3.2+ 新增)
unref
import { unref, ref } from '@vue/reactivity'
// unref:安全地获取 ref 的值,非 ref 原样返回
function useValue(val) {
return unref(val) // 不需要判断 isRef
}
useValue(ref(42)) // 42
useValue(42) // 42
这是编写接受 MaybeRef<T> 参数的组合函数的标准做法。
toRef / toRefs / toValue(Vue 3.3+)
import { reactive, toRef, toRefs, toValue } from '@vue/reactivity'
const state = reactive({ x: 1, y: 2 })
// toRef:从 reactive 对象中创建单个属性的 ref(保持响应式连接)
const xRef = toRef(state, 'x')
// toRefs:将整个 reactive 对象解构为 ref 集合(解构时保持响应性的核心工具)
const { x, y } = toRefs(state)
// toValue(3.3+):同 unref,但还能接受 getter 函数
const val = toValue(() => state.x + 1) // 执行 getter 并返回结果
2.4 计算属性底层
computed 的完整签名
import { computed } from '@vue/reactivity'
// 可写计算属性(文档有提及但很少被用到)
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})
plusOne.value = 10
console.log(count.value) // 9
三、@vue/runtime-core:平台无关运行时
这是 Vue 组件模型的核心,包含大量未被官方文档重点介绍的底层 API。
3.1 组件内部访问
getCurrentInstance
import { getCurrentInstance } from '@vue/runtime-core'
// 只能在 setup() 或生命周期钩子中调用
const instance = getCurrentInstance()
// 实例上有大量内部属性(仅用于框架级开发,生产代码慎用)
instance.uid // 组件唯一 ID
instance.type // 组件定义对象
instance.parent // 父组件实例
instance.root // 根组件实例
instance.appContext // 应用上下文(含全局注册的组件、指令、插件数据)
instance.subTree // 当前 vnode 子树
instance.isMounted // 是否已挂载
instance.isUnmounted // 是否已卸载
场景:编写需要感知组件树的底层库(如 Pinia 内部通过它访问 appContext)。
useAttrs / useSlots
import { useAttrs, useSlots } from '@vue/runtime-core'
// 在 setup 中访问透传的 attrs 和 slots(等效于选项式 API 的 $attrs/$slots)
const attrs = useAttrs() // 包含 class, style, 事件监听器等非 prop 属性
const slots = useSlots() // 包含所有插槽的渲染函数
useCssModule(与 <style module> 配合)
import { useCssModule } from '@vue/runtime-core'
// 访问 CSS Modules 的类名映射(通常与 SFC <style module> 配合)
const css = useCssModule()
// css.myClass → 'myClass_hash123_1'(编译后的哈希类名)
3.2 依赖注入高级用法
inject 的完整签名与 InjectionKey
import { provide, inject, InjectionKey } from '@vue/runtime-core'
// 使用 Symbol 作为类型安全的注入键(TypeScript 推荐做法)
const UserKey: InjectionKey<{ name: string }> = Symbol('user')
// 父组件
provide(UserKey, { name: 'Tom' })
// 子组件 - 有完整类型推断
const user = inject(UserKey) // User | undefined
const user2 = inject(UserKey, { name: 'Guest' }) // 带默认值,类型为 User
const user3 = inject(UserKey, () => ({ name: 'Guest' }), true) // 工厂函数(第三个参数 true 表示是工厂)
3.3 VNode 操作 API
h 渲染函数的完整能力
import { h, resolveComponent, resolveDirective, withDirectives } from '@vue/runtime-core'
export default {
render() {
// 渲染组件
const MyComp = resolveComponent('MyComp') // 按名称解析已注册的全局组件
// 渲染带自定义指令的 vnode
const dir = resolveDirective('my-directive')
return withDirectives(
h('div', 'hello'),
[[dir, value, argument, modifiers]]
)
}
}
cloneVNode
import { h, cloneVNode } from '@vue/runtime-core'
// 克隆 VNode 并可覆盖部分 props(用于高阶组件 HOC 模式)
const original = h('div', { class: 'foo' }, 'hello')
const cloned = cloneVNode(original, { class: 'bar', style: 'color: red' })
// 等效于 h('div', { class: ['foo', 'bar'], style: 'color: red' }, 'hello')
mergeProps
import { mergeProps } from '@vue/runtime-core'
// 智能合并多个 props 对象(class/style 合并,事件监听器链式调用)
const merged = mergeProps(
{ class: 'a', onClick: handler1 },
{ class: 'b', onClick: handler2 }
)
// { class: ['a', 'b'], onClick: [handler1, handler2] }
这是实现透传属性合并、封装 UI 组件库的核心工具。
createVNode / openBlock / createBlock / createElementVNode
这些是编译器输出的运行时助手函数,通常不应在业务代码中直接使用,但理解它们有助于分析编译产物性能。
// 模板编译后生成的代码示意:
import { createElementVNode, openBlock, createElementBlock } from '@vue/runtime-core'
function render() {
return (openBlock(), createElementBlock("div", null, [
createElementVNode("span", null, "hello")
]))
}
openBlock + createBlock 组合是 Vue 3 **Fragment 优化与靶向更新(patch flag)**的基础。
createTextVNode / createCommentVNode / createStaticVNode
import { createTextVNode, createCommentVNode, createStaticVNode } from '@vue/runtime-core'
createTextVNode('hello') // 创建文本节点
createCommentVNode('debug info') // 创建注释节点(v-if 的占位符就是它)
// createStaticVNode:将静态 HTML 字符串作为 vnode(SSR hydration 场景)
createStaticVNode('<div>static content</div>', 1)
3.4 异步组件
defineAsyncComponent 完整配置
import { defineAsyncComponent } from '@vue/runtime-core'
const AsyncComp = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner, // 加载中显示的组件
delay: 200, // 显示 loading 前的延迟(避免闪烁)
errorComponent: ErrorDisplay, // 加载失败显示的组件
timeout: 3000, // 超时时间(ms),超时视为失败
// 高级:自定义加载函数(控制重试逻辑)
onError(error, retry, fail, attempts) {
if (attempts <= 3) retry() // 最多重试 3 次
else fail()
}
})
3.5 内置组件的底层实现
Suspense 深度用法
import { Suspense, defineAsyncComponent } from 'vue'
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
Suspense 还支持与 async setup() 配合:
export default {
async setup() {
// setup 可以是异步的,父级 Suspense 会等待它 resolve
const data = await fetch('/api/data').then(r => r.json())
return { data }
}
}
3.6 调度器 API
nextTick 与内部调度机制
import { nextTick } from '@vue/runtime-core'
// nextTick 返回 Promise,可在 DOM 更新后执行操作
await nextTick()
// 也接受回调(兼容旧写法)
nextTick(() => {
// DOM 已更新
})
queuePostFlushCb(内部 API,谨慎使用)
import { queuePostFlushCb } from '@vue/runtime-core'
// 在当前刷新周期结束后异步执行回调(比 nextTick 更底层)
queuePostFlushCb(() => {
// 所有 DOM 更新和 watchPostEffect 之后执行
})
3.7 生命周期扩展钩子
除了常见的八个生命周期,还有以下较少使用的:
import {
onRenderTracked, // 每次响应式依赖被追踪时触发(仅 dev)
onRenderTriggered, // 每次重渲染被触发时携带具体原因(仅 dev)
onActivated, // KeepAlive 激活时
onDeactivated, // KeepAlive 停用时
onServerPrefetch, // SSR:组件 prefetch 数据钩子
onErrorCaptured, // 捕获子孙组件的错误
} from '@vue/runtime-core'
// onRenderTracked / onRenderTriggered 是性能调试利器
onRenderTriggered((event) => {
console.log('触发重渲染的原因:', {
effect: event.effect,
target: event.target, // 被修改的对象
type: event.type, // 'set' / 'add' / 'delete'
key: event.key, // 被修改的属性名
newValue: event.newValue,
oldValue: event.oldValue,
})
})
onRenderTracked 和 onRenderTriggered 是排查不必要重渲染的终极工具,但极少被开发者使用。
3.8 defineComponent 的类型推断能力
import { defineComponent, PropType } from '@vue/runtime-core'
// defineComponent 的核心价值是 TypeScript 类型推断,而非运行时行为
const MyComp = defineComponent({
props: {
user: {
type: Object as PropType<{ name: string; age: number }>,
required: true
},
// 函数类型 prop
onClick: Function as PropType<(id: number) => void>
},
emits: {
// 带类型验证的 emits 定义
change: (value: string) => typeof value === 'string',
submit: null // 无验证
},
setup(props, { emit, expose, attrs, slots }) {
// props 此处有完整类型推断
expose({ publicMethod() {} }) // 限制对外暴露的 API
}
})
3.9 渲染器相关(高级)
createRenderer / createHydrationRenderer
这是 Vue 跨平台渲染的核心 API,允许创建自定义渲染器。
import { createRenderer } from '@vue/runtime-core'
// 创建一个 Canvas 渲染器(伪代码示意)
const { render, createApp } = createRenderer({
createElement(type) { /* 创建 Canvas 元素 */ },
patchProp(el, key, prevVal, nextVal) { /* 更新属性 */ },
insert(el, parent, anchor) { /* 插入节点 */ },
remove(el) { /* 删除节点 */ },
createText(text) { /* 创建文本节点 */ },
setText(node, text) { /* 更新文本 */ },
// ...其他 DOM 操作
})
这是 Pixi.js、Three.js、Native 渲染器等 Vue 跨平台项目的基础 API。
四、@vue/runtime-dom:浏览器平台层
在 runtime-core 之上,runtime-dom 提供了浏览器专属能力。
4.1 内置指令实现
这些通常由编译器自动生成,但在 Render Function 场景下需要手动导入:
import {
vModelText, // <input v-model>
vModelCheckbox, // <input type="checkbox" v-model>
vModelRadio, // <input type="radio" v-model>
vModelSelect, // <select v-model>
vModelDynamic, // 动态类型的 v-model(根据 input type 自动选择实现)
vShow, // v-show
vOn, // v-on 事件监听
vBind, // v-bind
withDirectives, // 应用自定义指令
} from '@vue/runtime-dom'
// 在 render function 中手动使用 v-model
import { h, withDirectives, vModelText } from 'vue'
export default {
setup() {
const text = ref('')
return () => withDirectives(
h('input', { 'onUpdate:modelValue': val => (text.value = val) }),
[[vModelText, text.value]]
)
}
}
4.2 CSS 过渡钩子(内部实现)
import { Transition, TransitionGroup } from '@vue/runtime-dom'
// TransitionGroup 配置详解(文档常被忽视的属性)
h(TransitionGroup, {
name: 'list',
tag: 'ul', // 渲染的包装元素(默认 span)
moveClass: 'move', // FLIP 动画时的 CSS 类名
appear: true, // 初次渲染也触发过渡
css: false, // 禁用 CSS 过渡,纯 JS 控制
onBeforeEnter(el) {},
onEnter(el, done) { /* 必须调用 done() */ },
onLeave(el, done) { /* 必须调用 done() */ },
})
4.3 SSR 相关
import { createSSRApp } from '@vue/runtime-dom'
// SSR hydration:将服务端渲染的 HTML 与 Vue 状态绑定
const app = createSSRApp(App)
app.mount('#app') // 自动检测已有 HTML,进行 hydration 而非全量渲染
五、@vue/compiler-core:编译器核心
这是 Vue 模板编译管道的核心,纯函数式架构,完全平台无关。
5.1 完整编译管道
模板字符串
↓
parse() → AST(抽象语法树)
↓
transform() → 转换 AST(应用各种 transform 插件)
↓
generate() → 渲染函数代码字符串
import { parse, transform, generate, baseParse } from '@vue/compiler-core'
// 解析模板为 AST
const ast = parse('<div>{{ msg }}</div>')
// 对 AST 进行转换(插件化)
transform(ast, {
nodeTransforms: [
// 自定义 transform 插件
(node, context) => {
if (node.type === 1 /* ELEMENT */ && node.tag === 'div') {
// 可以修改 AST,添加、删除、替换节点
}
}
]
})
// 生成代码
const { code } = generate(ast)
console.log(code) // 输出渲染函数字符串
5.2 AST 节点类型(NodeTypes 枚举)
import { NodeTypes } from '@vue/compiler-core'
// 完整节点类型列表(调试 AST 时极为有用)
NodeTypes.ROOT // 0 - 根节点
NodeTypes.ELEMENT // 1 - 元素节点
NodeTypes.TEXT // 2 - 文本节点
NodeTypes.COMMENT // 3 - 注释节点
NodeTypes.SIMPLE_EXPRESSION // 4 - 简单表达式(如 msg、count + 1)
NodeTypes.INTERPOLATION // 5 - 插值({{ }})
NodeTypes.ATTRIBUTE // 6 - 普通属性
NodeTypes.DIRECTIVE // 7 - 指令(v-if、v-for 等)
NodeTypes.COMPOUND_EXPRESSION // 8 - 复合表达式
NodeTypes.IF // 9 - v-if 结构
NodeTypes.IF_BRANCH // 10 - v-if 分支
NodeTypes.FOR // 11 - v-for 结构
NodeTypes.TEXT_CALL // 12
NodeTypes.VNODE_CALL // 13 - createVNode 调用
NodeTypes.JS_CALL_EXPRESSION // 14
NodeTypes.JS_OBJECT_EXPRESSION // 15
NodeTypes.JS_PROPERTY // 16
NodeTypes.JS_ARRAY_EXPRESSION // 17
NodeTypes.JS_FUNCTION_EXPRESSION // 18
NodeTypes.JS_CONDITIONAL_EXPRESSION // 19
NodeTypes.JS_CACHE_EXPRESSION // 20 - 带缓存的表达式(v-once / 静态提升)
5.3 编译优化标记(PatchFlags)
这是 Vue 3 性能优化的核心机制,编译器通过这些标记让运行时的 diff 算法跳过静态内容:
import { PatchFlags } from '@vue/compiler-core'
PatchFlags.TEXT // 1 - 动态文本内容
PatchFlags.CLASS // 2 - 动态 class
PatchFlags.STYLE // 4 - 动态 style
PatchFlags.PROPS // 8 - 动态 props(非 class/style)
PatchFlags.FULL_PROPS // 16 - 有动态键名的 props(如 v-bind="obj")
PatchFlags.HYDRATE_EVENTS // 32 - 含事件监听的节点(SSR hydration 用)
PatchFlags.STABLE_FRAGMENT // 64 - 子节点顺序稳定的 Fragment
PatchFlags.KEYED_FRAGMENT // 128 - 带 key 的 Fragment
PatchFlags.UNKEYED_FRAGMENT // 256 - 无 key 的 Fragment(v-for 无 key 时)
PatchFlags.NEED_PATCH // 512 - 需要 patch 但不在上述分类中
PatchFlags.DYNAMIC_SLOTS // 1024 - 动态插槽
PatchFlags.DEV_ROOT_FRAGMENT // 2048 - dev 模式根节点 Fragment
PatchFlags.HOISTED // -1 - 静态提升节点,永不更新
PatchFlags.BAIL // -2 - 退出优化,进行完整 diff
理解 PatchFlags 是分析 Vue 模板编译产物、进行极致性能优化的必备知识。
5.4 ShapeFlags(组件形态标记)
import { ShapeFlags } from '@vue/shared' // 实际在 shared 包中
ShapeFlags.ELEMENT // 1 - 普通 DOM 元素
ShapeFlags.FUNCTIONAL_COMPONENT // 2 - 函数式组件
ShapeFlags.STATEFUL_COMPONENT // 4 - 有状态组件
ShapeFlags.TEXT_CHILDREN // 8 - children 是文本
ShapeFlags.ARRAY_CHILDREN // 16 - children 是数组
ShapeFlags.SLOTS_CHILDREN // 32 - children 是插槽对象
ShapeFlags.TELEPORT // 64 - Teleport 组件
ShapeFlags.SUSPENSE // 128 - Suspense 组件
ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE // 256
ShapeFlags.COMPONENT_KEPT_ALIVE // 512
ShapeFlags.COMPONENT // 6 - FUNCTIONAL | STATEFUL 的组合
5.5 自定义编译器插件
import { parse, transform, generate, createTransformContext } from '@vue/compiler-core'
// 实现一个自定义 transform:自动给所有 div 添加 data-testid
const myTransform = (node, context) => {
if (node.type === 1 && node.tag === 'div') {
node.props.push({
type: 6, // NodeTypes.ATTRIBUTE
name: 'data-testid',
value: { type: 2, content: 'auto-id' }
})
}
}
const ast = parse('<div><span>hello</span></div>')
transform(ast, { nodeTransforms: [myTransform] })
const { code } = generate(ast, { mode: 'module' })
六、@vue/compiler-dom:浏览器编译器
在 compiler-core 基础上扩展了浏览器专属的处理逻辑。
6.1 @vue/compiler-dom vs @vue/compiler-core
| 能力 | compiler-core | compiler-dom |
|---|---|---|
| 模板解析 | ✅ | ✅(扩展 HTML 实体) |
| v-if / v-for | ✅ | ✅ |
| v-model | 基础实现 | ✅(区分 input/select/checkbox) |
| v-on | 基础实现 | ✅(支持 .stop .prevent 等修饰符) |
| 静态提升 | ✅ | ✅ |
| 内联事件缓存 | ✅ | ✅ |
| SSR 优化 | ❌ | ✅ |
6.2 compile 函数
import { compile } from '@vue/compiler-dom'
// 直接将模板字符串编译为渲染函数代码
const { code } = compile('<div>{{ msg }}</div>', {
mode: 'module', // 'module' | 'function'
prefixIdentifiers: true, // 是否给变量名加前缀
hoistStatic: true, // 静态节点提升
cacheHandlers: true, // 内联事件处理器缓存
ssr: false, // 是否生成 SSR 渲染代码
ssrCssVars: '', // SSR CSS 变量
isNativeTag: (tag) => true, // 判断是否是原生 HTML 标签
isCustomElement: (tag) => tag.includes('-'), // 自定义元素判断
// 编译时优化选项
scopeId: 'data-v-xxxxxx', // Scoped CSS 的 scope ID(SFC 场景)
})
console.log(code)
// import { toDisplayString as _toDisplayString, ... } from "vue"
// export function render(_ctx, _cache) {
// return (_openBlock(), _createElementBlock("div", null, _toDisplayString(_ctx.msg), ...))
// }
七、@vue/shared:内部工具函数
这个包虽然标注为"内部使用",但其中有不少工具函数在框架级开发中很有价值。
import {
EMPTY_OBJ, // Object.freeze({}) — 空对象常量
EMPTY_ARR, // Object.freeze([]) — 空数组常量
NOOP, // () => {} — 空操作函数
NO, // () => false
isArray, // Array.isArray
isMap, // 判断是否为 Map
isSet, // 判断是否为 Set
isDate, // 判断是否为 Date
isRegExp, // 判断是否为 RegExp
isFunction, // typeof val === 'function'
isString, // typeof val === 'string'
isSymbol, // typeof val === 'symbol'
isObject, // val !== null && typeof val === 'object'
isPromise, // isObject(val) && isFunction(val.then)
isPlainObject, // Object.prototype.toString.call(val) === '[object Object]'
extend, // Object.assign
hasOwn, // Object.prototype.hasOwnProperty.call
camelize, // kebab-case → camelCase('on-click' → 'onClick')
capitalize, // 首字母大写
hyphenate, // camelCase → kebab-case(与 camelize 互逆)
toHandlerKey, // 'click' → 'onClick'
looseEqual, // 宽松相等比较(数组、对象深比较,用于 v-model 多选场景)
looseIndexOf, // 使用 looseEqual 的 indexOf
invokeArrayFns, // 批量调用函数数组(生命周期钩子的内部调用方式)
def, // Object.defineProperty 封装
toRawType, // Object.prototype.toString 取类型名('Map', 'Set', 'Array' 等)
makeMap, // 从字符串创建成员检测函数(HTML 标签白名单等场景)
} from '@vue/shared'
// makeMap 示例:
const isHTMLTag = makeMap('div,span,p,a,img,ul,li,...')
isHTMLTag('div') // true
isHTMLTag('my-comp') // false
八、完整包导出结构总览
Vue 3 包生态
│
├── @vue/reactivity(独立响应式)
│ ├── 核心:ref, reactive, computed, watch
│ ├── 浅层:shallowRef, shallowReactive, shallowReadonly
│ ├── 副作用:effect, effectScope, getCurrentScope, onScopeDispose
│ ├── 追踪控制:pauseTracking, resumeTracking, resetTracking
│ ├── 工具:toRaw, markRaw, proxyRefs, triggerRef, customRef
│ ├── 类型判断:isRef, isReactive, isReadonly, isProxy, isShallow
│ └── 转换:toRef, toRefs, toValue, unref
│
├── @vue/runtime-core(核心运行时)
│ ├── 应用:createApp(通过 runtime-dom 暴露)
│ ├── 组件:defineComponent, defineAsyncComponent
│ ├── VNode:h, createVNode, cloneVNode, mergeProps, createTextVNode
│ ├── 编译助手:openBlock, createBlock, createElementBlock(内部)
│ ├── 实例:getCurrentInstance, useAttrs, useSlots, useCssModule
│ ├── 注入:provide, inject, InjectionKey
│ ├── 生命周期:onMounted...onUnmounted, onRenderTracked, onRenderTriggered
│ ├── 调度:nextTick, queuePostFlushCb
│ ├── 渲染器:createRenderer, createHydrationRenderer
│ └── 内置组件:Suspense, KeepAlive, Teleport(定义在此)
│
├── @vue/runtime-dom(浏览器运行时)
│ ├── 应用:createApp, createSSRApp, render, hydrate
│ ├── 指令实现:vModelText, vModelCheckbox, vShow 等
│ ├── 过渡:Transition, TransitionGroup
│ └── 重新导出:全部 runtime-core 导出
│
├── @vue/compiler-core(编译器核心)
│ ├── 编译管道:parse, transform, generate
│ ├── 枚举:NodeTypes, PatchFlags, ElementTypes
│ ├── 工具:createTransformContext, traverseNode
│ └── 助手:createSimpleExpression, createCallExpression 等
│
├── @vue/compiler-dom(浏览器编译器)
│ ├── 顶层:compile(模板字符串 → 渲染函数代码)
│ └── 重新导出:全部 compiler-core 导出
│
└── @vue/shared(内部工具)
├── 类型判断:isArray, isFunction, isObject, isPromise...
├── 字符串:camelize, hyphenate, capitalize, toHandlerKey
├── 标志:ShapeFlags, PatchFlags(部分)
└── 工具:makeMap, extend, hasOwn, looseEqual, invokeArrayFns
九、架构层次与使用建议
| 包 | 适用场景 | 是否可独立使用 |
|---|---|---|
@vue/reactivity |
非 UI 的响应式状态管理 | ✅ 完全独立 |
@vue/runtime-core |
自定义渲染器 | ✅ 需搭配自定义渲染器 |
@vue/runtime-dom |
浏览器 Vue 应用 | ✅ 等价于 vue(无编译器) |
@vue/compiler-core |
构建工具插件、代码转换 | ✅ 独立使用 |
@vue/compiler-dom |
运行时模板编译(少用) | ✅ 独立使用 |
@vue/shared |
框架级工具函数 | ⚠️ 内部包,API 随版本变化 |
vue |
标准应用开发 | ✅ 聚合所有能力 |
十、总结
Vue 3 的包体系围绕关注点分离构建:
-
响应式 与 渲染 完全解耦,
@vue/reactivity可独立运行 -
平台无关运行时 与 浏览器平台实现 分离,
createRenderer支持跨平台 -
编译器 同样分层,
compiler-core可扩展用于 Vite 插件、Babel 转换等 -
PatchFlags和effectScope等低层 API 是框架性能优化的真正秘密
理解这套架构,不仅能写出更高质量的组合函数和 UI 组件库,也能在需要时深入框架层进行扩展——这是从"会用 Vue"到"精通 Vue 架构"的关键跨越。
深成指、创业板指均跌超1%
菜鸟将在海外部署大规模机器人仓储网络
布伦特原油突破100美元/桶
国铁集团:1至2月中欧班列开行3501列
清华学霸团队打造“AI工程师”,帮2000万工程师打造AI助手|水下项目
2026年,全球AI产业迎来关键转折点,大模型狂热逐渐退潮,核心叙事从技术军备竞赛转向商业化兑现。
在这样的周期里,AI+CAD平台公司「品览科技」选择了一条相对另类的发展路径:没有追逐大模型风口,没有做C端爆款,也不想讲AI替代人力的故事,而是选择钻进图纸堆,切入工业设计这个不太“性感”的细分领域。
品览科技创立于2018年,专注于AI+CAD核⼼平台的研发与产业化应用,为AEC⼯程设计和MFG⼯业设计⾏业提供AI设计整体解决⽅案。创始人李一帆毕业于清华⼤学电⼦⼯程系,后在卡耐基梅隆大学攻读硕士学位。在硅谷创业期间,曾参与多个创业项目,担任MailTime创始团队成员与Caicloud⾏业AI负责⼈,领导开发多个AI相关产品与解决⽅案。
近年来,AutoDesk等头部CAD厂商也开始布局AI能力,但李一凡并不想做“中国版AutoDesk”,而是要构建一个可以接受自然语言任务、理解设计意图和物理规律,并且能够自主规划与持续自我进化的“AI工程师”。
1.从图纸识别到“AI工程师”的产品演进
2018年李一帆从硅谷回国创办品览科技后,曾用一年时间在不同行业寻找适合AI技术落地的应用场景。直到2019年,品览科技在微软一场论坛上的演讲引起万科团队的注意。
经过几个月沟通探讨,李一帆了解到,建筑设计需要大量绘图工作,单张图纸平均修改10次,重复劳动占比超80%,仅管线排布的施工图就需要半个月反复核对。与此同时,大量的数字化图纸、规范与经验,恰恰具备AI技术落地的核心数据基础。这让李一帆看到技术落地的可能性,品览科技与万科达成合作,研发出一套AI审图系统。
不同于简单的图片识别,图纸的复杂性远超自然图像,包含图层信息、专业符号、尺寸标注、多页关联等高度结构化的专业表达,几百张图纸之间还有复杂的逻辑关系。“那时候的‘AI四小龙’虽然能做图片识别,但设计图纸的识别,除了我们,到今天也没有企业能做。”李一帆说道。
品览科技的算法团队从几何原理、结构力学等基础知识入手拆解绘图逻辑,搭建底层的模型算法,设计部门提供行业know-how,两个团队互相配合反哺,从避雷针的极小模块,到整栋住宅的电气系统,再到各类非建筑行业的设计需求,逐渐构建起适配工程场景、能够让设计师用起来的设计模型,从而不断打磨和优化公司AI CAD核心产品筑绘通的能力。
这套技术架构的核心逻辑是,用通用的底层算法作为“内功”,应对不同行业场景需求的“出招”。
“客户不用重复发明轮子,只需在平台上训练自己的专属模型。”李一帆解释,从而将绘图效率提升10倍,返工率降低90%。
目前,筑绘通已经历七次关键迭代,基于大语言模型、深度学习识图引擎、生成式设计引擎与云端建模引擎的技术底层,具备云CAD、AI识图、设计大模型与AI设计agent能力,现已应用于建筑、电力工程、船舶等工程项目的排布设计与图纸绘制。
![]()
品览技术产品架构
![]()
筑绘通工作界面
“国外传统厂商已经注意到我们的产品,但是更有可能对我们造成威胁的其实是基模巨头,不过目前还没有这个迹象,基模还无法处理设计图纸这种形态。”李一帆说道。
2.从垂直应用转向平台化战略,以“ODM”模式出海日本
2023年,中国房地产行业经历波动,商品房年开发量骤降,也导致品览科技原本聚焦于住宅AI设计的战略受到直接影响。
“市场环境促使我们认清一个现实,我们的优势不是行业专家。而是AI技术专家,所以我们应该专注于做好技术平台,行业的事交给合作伙伴和客户,他们比我们更懂。”李一帆说,“就像华为做基站,却不会去卖SIM卡。”
目前筑绘通的产品定位为“AI+CAD核心平台提供商”,采用“软件授权+实施服务”(License+ Service)的服务模式,也可以与客户共创开发定制版。截至2026年3月,筑绘通B端客户数量突破300家,已实现全年经营性盈利。
![]()
商业化布局
例如,海尔基于筑绘通开发的空调设计应用“海筑绘”,较以往实际节省70%的设计时间。某连锁酒店运用品览科技AI平台排布酒店房间,快速⽣成符合品牌标准的建筑平⾯图和⼯程量清单,将原本4小时的设计时长缩减⾄1⼩时,提效约75%。
![]()
“海筑绘”工作界面
原本品览科技对出海并没有明确规划,有日本客户在寻找供应商的过程中,看到36氪对品览科技的报道后,伸出合作的橄榄枝。
从接到咨询到正式启动合作,品览科技用半年时间研判,最终选择联合共创的合作模式:品览科技出技术,本地伙伴出本地行业专家与销售渠道,共同开发面向日本市场的行业版AI设计工具。
这种类似ODM式的出海路径与近年来流行的SaaS出海不同,不是将产品进行语言翻译后直接进行销售,而是需要从技术适配到产品定义都“重做一遍”。
李一帆解释,“我们过往的经验是,不要着急在面上铺开,和C端用户‘尬聊’,先选择一个有行业know-how的合作伙伴深度共创,从一个专业方向纵深切进去。试点成功了,向客户证明了我们的能力,再往外延展就只是时间问题。”
“在日本、韩国这样高度发达的本土化市场,一定得是当地人做当地事。”合作启动后,李一帆确实很快体会到日本市场的“不一样”。日本独特的建筑形态以独栋的“一户建”为主,集中式公寓较少,并且设计规范与标准体系也完全不同,因此模型算法要做全面改动和适配。
其次,日本市场对产品的可靠性要求很高,对试错更加保守,希望问世的AI产品必须足够更成熟。“所以我们的产品打磨也必须得慢工出细活。”李一帆表示,这种“细”体现在方方面面,比如更高的抗震设计要求,模型能够与本地常用的设计软件对接,符合日本工程设计师的工作流程和使用习惯。
为解决这些问题,品览科技采用两地协同的模式:一部分团队常驻日本,一部分留在国内,通过线上协作推进开发。“出海这件事是急不来的,我们毕竟不是巨头企业,先在日本把第一步走好,未来无论去韩国还是欧美,都会像这样由点及面地慢慢做大。”李一帆表示。
这种“慢”或许正是工程技术应用赛道的本质,需要时间积累行业理解,以耐心建立信任关系。当被问及最终愿景时李一帆表示:“让AI能够做工程设计,创造一个更好的世界。”这个愿景很大,但实现路径很简单,从一个避雷针的AI设计开始,到一座核电站的电缆系统,当别人在做“性感”的事,品览科技选择慢慢积累内功,然后等时间给出答案。
南向资金净买入额达40亿港元
低代码工具很多,为什么 RollCode 更像一套「页面生产平台」
过去几年,低代码工具几乎成了企业数字化里的“标配”。从表单搭建到活动页面,从运营后台到数据看板,各类拖拽工具层出不穷。但很多前端开发者用过几次之后都会产生一种微妙的感觉:这些工具很适合“搭页面”,却很难真正进入团队的工程体系。
原因其实很简单。大多数低代码工具只解决了一件事——让页面更快被拖出来。而真正的业务场景里,一个页面的生命周期远不止“拖拽完成”这么简单。
页面需要:和代码仓库共存、能复用模板、持开发者自定义逻辑、可以静态发布、可以被运营同学快速修改。
当这些能力被拆开在不同工具里时,团队的效率并不会真正提升。这也是我最近重新看了一遍 RollCode 官网之后的一个直观感受:
它想做的事情,已经不只是低代码。 它更像是一套完整的 页面生产平台(Page Production Platform) 。【传送门】
一、传统低代码工具的问题在哪里?
很多低代码产品的定位,其实非常清晰:
让不会写代码的人,也能快速搭出页面。
这个目标没有问题,但在真实团队协作中会遇到一个非常典型的断层。
通常的流程会是这样:
- 运营同学在低代码平台拖拽页面
- 页面上线
- 业务复杂度增加
- 前端开发重新写一套页面
于是就形成了一个循环: “低代码做原型 → 开发重写正式版本” 这种模式的效率其实并不高。因为低代码平台做出来的页面,往往存在几个工程问题:
- 代码结构不可控
- 自定义能力有限
- 组件体系不统一
- 很难接入现有前端工程
所以很多前端团队对低代码的态度一直很微妙:能用,但很难真正进入工程体系。
二、RollCode 的思路:把“搭建”和“开发”放进同一套系统
当你仔细看 RollCode 的能力结构时,会发现一个很明显的设计思路:
它并没有把“拖拽”和“代码”做成两个世界。
而是尝试把它们融合到同一个生产流程里。
从架构角度看,大致可以理解为下面这层结构。
![]()
在这套结构里,页面并不是一个“编辑器里的成品”。它更像是一份 可持续迭代的页面配置。这带来一个很重要的变化:
页面既可以拖拽搭建,也可以被开发者扩展。这种结构对于前端团队来说就非常关键了。
三、它和传统低代码最大的差别:工程能力
如果用一个比较直观的方式理解,可以看下面这个能力对比。
暂时无法在飞书文档外展示此内容
![]()
从这个角度看,RollCode 的定位其实更接近:Page Builder + Frontend Framework 的结合体。 它解决的并不是“如何拖拽页面”。而是:如何把页面生产流程工程化。
四、从“页面搭建”升级为“页面生产链路”
如果站在团队效率的角度看,一个营销页面从需求到上线,大致会经历这些环节:
- 需求设计
- 页面搭建
- 开发扩展
- 发布上线
- 模板复用
很多公司会用 3~4个工具来完成这件事。
而 RollCode 的思路是把这些能力放进同一个平台。
![]()
这样带来的直接变化是:页面从一次性产物变成可复用资产。
例如:
- 活动页模板
- 落地页模板
- 产品介绍页模板
这些都可以沉淀在系统里。当业务需要新页面时,只需要在模板上做轻量修改。页面生产效率会明显提升。
五、开发者为什么会喜欢这种结构
对于开发者来说,一个平台好不好用,其实只看两件事:
1、有没有工程能力 2、有没有扩展能力
RollCode 在这两个点上的设计,其实比较接近开发者的习惯。
第一点是 组件体系。组件并不是编辑器里的黑盒,而是可以被扩展和复用的能力模块。
第二点是 代码融合能力。很多低代码平台只允许写少量脚本。
而在 RollCode 的设计里:页面既可以通过可视化搭建,也可以通过代码扩展。
这样一来,团队协作就会变得非常顺滑。运营可以快速搭建页面结构。开发者可以补充复杂逻辑。
两者并不会互相冲突。
结尾
如果说传统低代码工具解决的是 “不会写代码的人如何做页面” 。那么 RollCode 更像是在解决另一个问题:
如何让页面搭建、开发、复用和发布成为同一条生产链路。 当这条链路被打通之后,页面就不再是一次性的交付物。
它会逐渐变成团队可复用的资产。这也是为什么在看完 RollCode 的设计之后,我更愿意把它理解为:
一套面向团队协作的页面生产平台。
如果你也在做营销落地页、活动页面或者企业官网系统,这种“可视化 + 工程能力”的组合,其实值得认真研究一下。
以上就是本次分享。我是安东尼(TUARAN),持续关注大模型应用、AI工程化与自动化系统。欢迎一起交流 OpenClaw、Agent、数字员工 等实践,也欢迎共创 《前端周刊》 、加入 博主联盟。加我或进群,一起做点有意思的 AI 项目。