Canvas简历编辑器-Monorepo+Rspack工程实践
Canvas简历编辑器-Monorepo+Rspack工程实践
在之前我们围绕Canvas
聊了很多代码设计层面的东西,在这里我们聊一下工程实践。在之前的文中我也提到过,因为是本着学习的态度以及对技术的好奇心来做的,所以除了一些工具类的库例如 ArcoDesign
、ResizeObserve
、Jest
等包之外,关于 数据结构packages/delta
、插件化packages/plugin
、核心引擎packages/core
等都是手动实现的,所以在这里除了学习了Canvas
之外,实际上还做了一些项目工程化的实践。
- 在线编辑: https://windrunnermax.github.io/CanvasEditor
- 开源地址: https://github.com/WindrunnerMax/CanvasEditor
关于Canvas
简历编辑器项目的相关文章:
- 社区老给我推Canvas,我也学习Canvas做了个简历编辑器
- Canvas图形编辑器-数据结构与History(undo/redo)
- Canvas图形编辑器-我的剪贴板里究竟有什么数据
- Canvas简历编辑器-图形绘制与状态管理(轻量级DOM)
- Canvas简历编辑器-Monorepo+Rspack工程实践
- Canvas简历编辑器-层级渲染与事件管理能力设计
- Canvas简历编辑器-选中绘制与拖拽多选交互方案
Pnpm+Monorepo
我们先来聊聊为什么要用monorepo
,先举一个我之前踩过的坑作为例子,在之前我的富文本编辑器项目 DocEditor 就是完全写在了独立的单个src
目录中,在项目本身的运行过程中是没什么问题的,但是当时我想将编辑器独立出来作为NPM
包用,打包的过程是借助了Rollup
也没什么问题,问题就出在了引用方上。当时我在简历编辑器中引入文档编辑器的NPM
包时,发现有一个模块被错误的TreeShaking
了,现在都还能在编辑器中看到这部分兼容。
module: {
rules: [
{
// 对`doc-editor-light`的`TreeShaking`有点问题
test: /doc-editor-light\/dist\/tslib.*\.js/,
sideEffects: true,
},
]
}
这个问题导致了我在dev
模式下没有什么问题,但是在build
之后这部分代码被错误地移除掉了,导致编辑器的wrapper
节点出现了问题,列表等元素不能正确添加。当然实际上这不能说明独立包项目不好,只能说整个管理的时候可能并不是那么简单,尤其是打包为NPM
包的时候需要注意各个入口问题。那么现在引用我的富文本编辑器包已经变成了4
个独立的包分别引用,各司其职,就没再出现过这个问题。
说起来打包的问题,我还踩过一个坑,不知道大家是不是见到过React
的Invalid hook call
这个经典报错。之前我将其独立拆包的时候之后,发现会报这个错,但是我在package.json
中是标注的peerDependencies "react": ">=16"
,按理说这里会直接应用安装该包的React
,不可能出现版本不一致的问题,至于Rules of Hooks
肯定也不可能,因为我之前是好好的,拆完包才出的问题。最后发现是我在rollup
中没把peerDependencies
这部分解析,导致jsx-runtime
被打进了包里,虽然React
的版本都是17.0.2
但是实际上是运行了两个独立词法作用域的React Hooks
,这才导致了这个问题。
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app See for tips about how to debug and fix this problem.
接着回到项目本身,当前项目已经抽离出来独立的RspackMonoTemplate,平时开发也会基于这个模版创建仓库。当前简历编辑器项目的结构tree -L 2 -I node_modules --dirsfirst
如下:
CanvasEditor
│── packages
│ ├── core
│ ├── delta
│ ├── plugin
│ ├── react
│ └── utils
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.json
packages/core
: 编辑器核心引擎模块,对于 剪贴板操作、事件管理、状态管理、History
模块、Canvas
操作、选区操作 等等都封装在这里,相当于实现了基本的Canvas
引擎能力。packages/delta
: 数据结构模块,设计了基准数据结构,实现了DeltaSet
数据结构以及原子化的Op
操作,主要用于描述整个编辑器的数据结构以及操作,实现了invert
等能力,对于实现History
模块有很大的意义。packages/plugin
: 插件模块,在packages/delta
的基础上设计了插件化的能力,主要为了实现编辑器的功能模块化,例如Text
、Image
、Rect
等插件都是在这里实现的。packages/react
:React
模块,主要是为了通过实现编辑器的视图层,在这里有比较重要的一点,我们的核心模块是视图框架无关的,如果有必要的话同样可以使用Vue
、Angular
等框架来实现视图层。packages/utils
: 工具模块,主要是一些工具函数的封装,例如FixedNumber
、Palette
等等,这些工具函数在整个编辑器中都有使用,是作为基础包在整个workspace
中引用的。package.json
: 整个workspace
的package.json
,在这里配置了一些项目的信息,EsLint
、StyleLint
相关的配置也都在这里实现。pnpm-lock.yaml
:pnpm
的锁文件,用于锁定整个workspace
的依赖版本。pnpm-workspace.yaml
:pnpm
的workspace
配置文件,用于配置monorepo
的能力。tsconfig.json
: 整个workspace
的tsconfig
配置文件,用于配置整个workspace
的TypeScript
编译配置,在这里是作为基准配置以提供给项目中的模块引用。
pnpm
自身是非常优秀的包管理器,通过硬链接和符号链接来节省磁盘空间,每个版本的包只需要存储一次,最重要的是pnpm
创建了一个非扁平化的node_modules
结构,从而确保依赖与声明严格匹配,严格控制了依赖提升,能够避免依赖升级的意外问题,这提高了项目的一致性和可预测性。
而说回到monorepo
,pnpm
不光是非常优秀的包管理器,其还提供了一个开箱即用的monorepo
能力。在pnpm
中存在一个pnpm-workspace.yaml
文件,这个文件是用来配置workspack
的,而pnpm
的workspace
就可以作为monorepo
的能力,而我们的配置也非常简单,我们认为在packages
目录下的所有目录都作为子项目。
packages:
- 'packages/*'
通过monorepo
我们可以很方便的管理所有子项目,特别是对于需要发Npm
包的项目,将子模块拆分是个不错的选择,特别如果能够做到视图层框架无关的话就显得更加有意义。此外,monorepo
对于整个项目的管理也有很多益处,例如在打包整个应用的时候,我们不需要对每个子项目发新的包之后才能打包,而是可以直接将编译过程放在workspace
层面,这样就可以保证整个项目的一致性,简化了构建过程和持续集成流程,让所有项目可以共享构建脚本和工具配置。此外所有项目和模块共用同一个版本控制系统,便于进行统一的版本管理和变更跟踪,而且还有助于同步更新这些项目间的依赖关系。
TS+Rspack最佳实践
说了这么多使用pnpm + monorepo
管理项目带来的好处,我们再来聊聊我对TS
与Rspack
应用于Monorepo
的最佳实践,不知道大家是不是遇到过这样的两个问题:
- 子项目的
TS
声明更改后不能实时生效,必须要编译一次子项目才可以,而子项目编译的过程中如果将dist
等产物包删除,那么在vsc
或者其他编辑器中就会报TS
找不到引用声明的错误,这个时候就必须要用命令重新Reload TypeScript Project
来去掉报错。而如果不将产物包删除的话,就会出现一些隐性的问题,例如原来某个文件命名为a.tsx
,此时因为一些原因需要将其移动到同名的a
目录并且重新命名为index.tsx
,那么执行了这一顿操作之后,发现如果更改此时的index.tsx
代码不会更新,必须要重启应用的webpack
等编译器才行,因为其还是引用了原来的文件,产生类似的问题虽然不复杂但是排查起来还是需要时间的。 - 更改子项目的
TS
代码必须要重新编译子项目,因为项目是monorepo
管理的,在package.json
中会有workspace
引用,而workspace
实际上是在node_modules
被引用的,所以虽然是子项目但是仍然需要遵循node_modules
的规则才可以,那么其通常需要被编译为js
才可以被执行,所以每次修改代码都必须要全量执行一遍很是麻烦,当然通常我们可以通过-w
命令来观察变动,但是毕竟多了一道步骤,且如果是存在alias
的项目可能仅仅使用tsc
来编译还不够。此外在monorepo
中我们通常会有很多子项目,如果每个子项目都需要这样的话,特别在这种编译时全量编译而不是增量编译的情况下,那么整个项目的编译时间就会变得非常长。
那么在这里我们先来看第一个问题,子项目的TS
声明更改后不能实时生效,因为我们也提到了monorepo
子项目实际上是通过node_modules
来管理和引用的,所以其在默认情况下依然需要遵循node_modules
的规则,即packages.json
的types
字段指向的TS
声明文件,那么我们有没有什么办法可以修改这个行为呢,当然是有的,我们在整个项目的根tsconfig.json
配置path
就可以完美解决这个问题。当我们配置好如下的内容之后,通过按住Ctrl
加鼠标左键点击的时候,就可以跳转到子项目的根目录声明了。此外这里有个要关注的点是,在项目中不建议配置"baseUrl": "."
,在这里会有一些奇奇怪怪的路径引用问题,所以在简历编辑器项目中除了要打包Npm
的tsconfig.build.json
之外,都是直接使用相对路径配置的。
{
"compilerOptions": {
"...": "...",
"paths": {
"sketching-core": ["./packages/core/src"],
"sketching-delta": ["./packages/delta/src"],
"sketching-plugin": ["./packages/plugin/src"],
"sketching-utils": ["./packages/utils/src"],
},
},
"include": [
"packages/*/src"
]
}
那么解决了项目的TS
声明问题之后,我们再来看编译的问题,这里的问题看起来会复杂一些,因为TS
声明就单纯只是类型声明而已,不会影响到项目本身代码的编译,编译类型检查除外。那么在Rspack
中应该配置才能让我们的代码直接指向子项目,而不是必须要走node_modules
这套规则,实际上这里也很简单,只需要配置resolve.alias
就可以了,这样当我们直接修改TS
代码时,也能让编辑器立即响应增量编译。
{
// ....
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"sketching-core": path.resolve(__dirname, "../core/src"),
"sketching-delta": path.resolve(__dirname, "../delta/src"),
"sketching-plugin": path.resolve(__dirname, "../plugin/src"),
"sketching-utils": path.resolve(__dirname, "../utils/src"),
},
},
// ....
}
实际上对于Rspack
而言其帮我们做了很多事,比如即使是node_modules
的TS
文件也会编译,而对于一些通过CRA
创建的webpack
项目来说,这个配置就麻烦一些,当然我们同样也可以借助customize-cra
来完成这件事,此外我们还要关闭一些类似于ModuleScopePlugin
的插件才可以,下面是富文本编辑器项目 DocEditor 的配置。
const src = path.resolve(__dirname, "src");
const index = path.resolve(__dirname, "src/index.tsx");
const core = path.resolve(__dirname, "../core/src");
const delta = path.resolve(__dirname, "../delta/src");
const plugin = path.resolve(__dirname, "../plugin/src");
const utils = path.resolve(__dirname, "../utils/src");
module.exports = {
paths: function (paths) {
paths.appSrc = src;
paths.appIndexJs = index;
return paths;
},
webpack: override(
...[
// ...
addWebpackResolve({
alias: {
"doc-editor-core": core,
"doc-editor-delta": delta,
"doc-editor-plugin": plugin,
"doc-editor-utils": utils,
},
}),
babelInclude([src, core, delta, plugin, utils]),
// ...
configWebpackPlugins(),
].filter(Boolean)
),
};
此外,简历编辑器是纯前端的项目,这样的项目有个很大的优势是可以直接使用静态资源就可以运行,而如果我们借助GitHub Action
就可以通过Git Pages
在仓库中直接部署,并且可以直接通过GitHub Pages
访问,这样在仓库中就能呈现一个完整的DEMO
。
// .github/workflows/deploy.yml
name: deploy gh-pages
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: install node-v16
uses: actions/setup-node@v3
with:
node-version: '16.16.0'
- name: install dependencies
run: |
node -v
npm install -g pnpm
pnpm config set registry https://registry.npmjs.org/
pnpm install --registry=https://registry.npmjs.org/
- name: build project
run: |
npm run build:react
- name: deploy project
uses: JamesIves/github-pages-deploy-action@releases/v3
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
FOLDER: packages/react/build
最后
在这里我们聊了为什么要用Monorepo
以及简单聊了一下pnpm workspace
的优势,然后解决了在子项目开发中会遇到的TS
编译、项目编译的两个实际问题,分别在Monorepo
、Rspack
、Webpack
项目中相关的部分实践了一下,最后还简单聊了一下利用GitHub Action
直接在Git Pages
部署在线DEMO
。那么再往后边的文章中,我们就需要聊一聊如何实现 层级渲染与事件管理 的能力设计。
Canvas简历编辑器-Monorepo+Rspack工程实践的更多相关文章
- 基于menu小插件探索工程实践
目录 一.准备工作 1.C/C++环境搭建 2.VSCode的配置 (1) 安装插件: (2) 设置配置文件: 二.工程化编程实战 1.模块化设计 2.可重用设计:进一步抽象 menu的进一步优化 可 ...
- panguan(判官):一个自研的任务执行引擎的工程实践
来某厂接近半年了,几乎没写过C++代码,说实话还真的有点手生.最近刚好有一个需求,然而我感觉我也没有办法用C++以外的语言去实现它.于是还是花了几天时间用C++完成编码,这是一个简单的任务执行引擎,它 ...
- 公司简介 - CCDI悉地国际-工程实践专业服务的引领者
公司简介 - CCDI悉地国际-工程实践专业服务的引领者 关于悉地国际 CCDI悉地国际(以下简称"CCDI")创立于1994年,是在城市建设和开发领域从事综合专业 ...
- Linux开源模块迁移概述暨交叉编译跨平台移植总结--从《嵌入式Linux驱动模板简洁和工程实践》
本文摘录<嵌入式Linux驱动模板简洁和工程实践>一本书"开发和调试技术". Linux强大的是,有那么多的开源项目可以使用.通常非常需要可以通过寻找相关的源模块被定义 ...
- LDA工程实践之算法篇之(一)算法实现正确性验证(转)
研究生二年级实习(2010年5月)开始,一直跟着王益(yiwang)和靳志辉(rickjin)学习LDA,包括对算法的理解.并行化和应用等等.毕业后进入了腾讯公司,也一直在从事相关工作,后边还在yiw ...
- 工程实践:给函数取一个"好"的名字
工程实践:给函数取一个"好"的名字 早在2013年,国外有个程序员做了一个有意思的投票统计(原始链接请见:<程序员:你认为最难做的事情是什么?>),该投票是让程序员从以 ...
- 50代码HTML5 Canvas 3D 编辑器优雅搞定
1024程序员节刚过,手痒想实现一个html的3d编辑器,看了three.js 同时还看了网上流传已久的<<基于 HTML5 Canvas 的简易 2D 3D 编辑器>>,都觉 ...
- webpack 从入门到工程实践
from:https://www.jianshu.com/p/9349c30a6b3e?utm_campaign=maleskine&utm_content=note&utm_medi ...
- C++工程实践 一个开始
打算把C++工程过程中的一些总结写下来.从打算写这个内容到今天(2017/1/12)已经很多年了,但是一直没有想好如何写,反正就这样,有什么写什么吧. C++工程实践之所以难产,主要原因有几个 内容比 ...
- Go 在游戏行业中的工程实践
在今年 1 月由七牛云主办的 ECUG Con 十周年盛会上,真有趣技术总监陈明达带来了题为< Go 在游戏行业中的工程实践>的精彩分享,深入讲解了 Go 的工程经验,错误和异常处理,in ...
随机推荐
- oeasy教您玩转linux010206 蒸汽机车 sl
我们来回顾一下 上一部分我们都讲了什么? 两种字符画 从figlet开始️ 到toilet 字符画选项 变彩色 字体效果 figlet oeasy toilet oeasy 这里面还有什么好玩的游戏可 ...
- [oeasy]python0041_teletype历史_博多码_shift_capslock_字符数字切换_gear
teletypewriter 历史 回忆上次内容 上次见到了一个真的机械打字机 感受到了蒸汽朋克的时代背景 上上次区分了一些概念 terminal终端,电脑连线最终的端点 TeleTYpewriter ...
- Web 开发技术栈
Web 开发技术栈 Web 是什么? 简单地说,网络是一个遍布全球的网络,它连接大量设备并允许它们相互通信 Internet 上的网站托管在称为服务器的设备上,当与 Internet 上的网页交互时, ...
- MySQL之DDL
1. 数据库 * 查看所有数据库:SHOW DATABASES * 切换(选择要操作的)数据库:USE 数据库名 * 创建数据库:CREATE DATABASE [IF NOT EXISTS] myd ...
- 是否可以在线创建ios证书
生成苹果证书,假如使用官方的教程去生成,非常麻烦,因为它需要使用苹果mac电脑去生成,而且生成的流程还要对苹果电脑的证书导入和导出比较熟. 因此,生成苹果ios证书,不建议使用官方的方法去生成,少走弯 ...
- RHCA cl210 013 制作镜像 轮转key rabbitmq追踪 写时复制 keystone多域登录图形界面
undercloud 部署 overcloud overcloud控制节点上的组建rabbitmq 排错需要rabbitmq,开启追踪则会更详细,会消耗性能 环境问题 登录一下classroom os ...
- 【Centos6】手动配置网卡
在安装时忘记手动勾选链接网络 导致初始状态没有网卡的IP地址 这里参考这篇文章的解决办法: https://blog.51cto.com/u_13570193/2091655 首先检查是否有E1000 ...
- 【C3】07 盒子模型
在 CSS 中,所有的元素都被一个个的"盒子(box)"包围着, 理解这些"盒子"的基本原理,是我们使用CSS实现准确布局.处理元素排列的关键. 本文围绕 &q ...
- 【SpringBoot】10 Web开发 Part1 静态资源
使用SpringBoot创建工程的方式: 1.在IDEA集成的Boot官网选项中点选可能需要的框架环境即可 2.SpringBoot已经设置好了这些场景,只需要配置文件中指定少量配置就可以运行起来 3 ...
- 【DataBase】MySQL 08 SQL函数 单行函数其二 数值函数
数值函数 # 数值函数 -- ROUND() 四舍五入 SELECT ROUND(3.14),ROUND(-3.14); -- 重载,保留指定参数的小数位数 SELECT ROUND(-3.14,3) ...