ProseMirror

  • prosemirror-model
  • prosemirror-state
  • prosemirror-view
  • prosemirror-transform

Schema

Schema 描述的是:这篇文档允许出现哪些结构——能有哪些节点(Node)、哪些标记(Mark),以及它们之间如何嵌套、谁里能装谁

创建 Schema 大致是

1
2
3
4
new Schema({
nodes: { doc, paragraph, text, /* ... */ },
marks: { strong, link, /* ... */ },
})
  • nodes:名字 → NodeSpec 的映射。其中必须有一个 根节点(通常叫 doc),且 Schema 会把它当作 schema.topNode(整篇文档的根类型)。

    • content:
    • group:inline、block、tile分组(其概念与 html 节点的 inline 节点与 block 节点一致)
    • marks:
  • marks:名字 → MarkSpec 的映射(加粗、链接等)。
    编译后会得到 schema.nodes / schema.marks:从名字查到 NodeType / MarkType(带上了已解析的 content 规则、默认属性等)。

prosemirror 不允许有空文本节点,我们看到在 p 标签中, 文本内容为空时,会填入一个 br 标签。

阅读全文 »

实现一个极简富文本编辑器

一个最基础的富文本编辑器应该具备以下能力:

  • 文本格式化:加粗、斜体、下划线、颜色、背景色等多种文本样式。​
  • 多媒体插入:图片、视频、文件、链接等。​
  • 撤销重做:用户在编辑过程中可以随时撤销和重做操作。​
  • 跨浏览器兼容性:在不同浏览器和平台上行为一致。​
  • 协作功能:多用户实时协作,显示协同光标和实时文档同步。

contenteditable

contenteditable属性

  • 可以使任何元素变为可编辑状态。通过设置 contenteditable=”true”,用户可以直接在元素内输入或删除文本。
  • 常用于构建富文本编辑器的编辑区域,简单高效。

示例

1
2
3
<div class="editor-content" contenteditable="true">​
<p>这是一个简单的富文本编辑器。</p>​
</div>​

浏览器 execCommand

document.execCommand 是一种浏览器 API,用于在 HTML 文档中执行与文档内容相关的命令。该方法最初是为实现富文本编辑功能而设计的,可以让开发者轻松实现一些常见的编辑操作,如加粗文本、插入链接、剪切、复制、粘贴等。尽管这种方法在过去非常流行,但随着 Web 技术的发展,它的使用逐渐减少,并在一些现代浏览器中被标记为过时或即将废弃。

基本语法
1
document.execCommand(command, showUI, value);​
  • command (字符串): 要执行的命令名称,如 ‘bold’、’italic’、’copy’ 等。​
  • showUI (布尔值): 指示是否显示默认的用户界面,通常传入 false,因为许多命令都不支持或忽略这个参数。​
  • value (字符串): 与某些命令一起使用的值,如在插入链接时的 URL。对于不需要值的命令,该参数可以省略或传入 null。
常见命令

Selection + Range + Compiler

编辑器个性需求

@ 提及
/ 推荐补全
知识图谱

协同编辑核心实现

协同化的前提

协同底层传输协议

  • web socket
  • socket io

协同数据类别

  • 头像、光标
  • 业务核心数据

数据冲突

  • OT
  • CRDT
    • 面向过程
    • 面向状态、操作

常见方案

Docker相关见:Docker学习
Nginx相关见:Nginx学习

极简部署

手写一个简单的静态资源服务器

基于 docker/docker-compose 对极简项目的部署

基于Nginx镜像的部署


单页应用部署

单页应用的静态资源

所有的前端单页应用对于部署,最重要的就是构建静态资源,也就是npm run build

一个最简单的Dockerfile:

1
2
3
4
5
6
7
8
9
FROM node:14-alpine

WORKDIR /code

ADD . /code
RUN yarn && npm run build

CMD npx serve -s build
EXPOSE 3000

这样其实就已经完成构建了,然而还可以针对以下两点进行优化:

  • 构建镜像时间过长,优化构建时间
  • 构建镜像文件过大,优化镜像体积
阅读全文 »

移动端适配

vw/vh + rem 结合动态计算根字体大小

1. rem 配合动态根字体大小

让 1rem 的大小,永远跟着屏幕宽度自动变化

1
2
3
4
5
6
7
8
html {
--screen-width: var(--fallback-screen-width, 100vw);
/* 根字体大小 = 视口宽度 / 3.75 */
font-size: calc(var(--safe-width) / 3.75);
}
body {
font-size: 0.16rem; /* 业务层使用 rem 作为单位 */
}

2. 现代视口单位兼容(dvw/dvh)

  • dvw/dvh(动态视口单位)规避移动端 “刘海屏 / 导航栏” 占用视口的问题;
  • 通过 @supports 做特性检测,仅在支持 dvh 的浏览器中替换为 dvw/dvh,向下兼容不支持的设备(仍用 vw/auto)。
1
2
3
4
5
6
@supports (height: 100dvh) {
html {
--screen-width: 100dvw;
--screen-height: 100dvh;
}
}
阅读全文 »

JSBridge是什么

JSBridge本质就是:Web端和客户端Native之间的通信桥梁,让混合开发模式中的Web端和Native端能够互相通信,实现双向调用。

Web端调用Native端

在Webview中注入JS API

通过WebView提供的接口,App将Native的相关接口注入到JS的Context(window)的对象中
Web端就可以直接在全局 window 下使用这个暴露的全局JS对象,进而调用原生端的方法

Android注入方法:

  • addJavascriptInterface配合@JavascriptInterface:Android 4.2+ 配合 @JavascriptInterface 才安全可用

IOS注入方法:

  • WKWebView + WKScriptMessageHandler 是 iOS 官方方案,推荐新项目优先使用,链路更清晰。
  • WebViewJavascriptBridge 是常见历史封装方案,接入快、兼容旧项目,但初始化握手和调试成本更高。
阅读全文 »

Nginx是什么

Nginx 是一个高性能的 Web 服务器和反向代理服务器
最常见的作用有这几个:

  • 静态资源服务器:直接返回 html/js/css/img
  • 反向代理:把请求转发给后端服务(Node/Java/Python)
  • 负载均衡:把流量分发到多台后端
  • 网关能力:HTTPS 终止、重定向、缓存、压缩(gzip/brotli)、跨域头等

Nginx的配置文件

在 nginx 中,其中比较重要的有以下几个文件,它们都是有层层关联的:

/etc/nginx/nginx.conf
/etc/nginx/conf.d/default.conf

/etc/nginx/nginx.conf

nginx 主配置文件,引用了 /etc/nginx/conf.d/ 目录下的所有配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
user  nginx;
worker_processes auto;

error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;


events {
worker_connections 1024;
}


http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
#tcp_nopush on;

keepalive_timeout 65;

#gzip on;

include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/conf.d/default.conf

1
2
3
4
5
6
7
8
9
10
11
12
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

/usr/share/nginx/html

默认的静态资源目录,其目录下的 index.html 就是 nginx 的欢迎页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html {
color-scheme: light dark;
}
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
</p>

<p>
For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br />
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.
</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

利用Docker启动Nginx镜像

1
docker run -it --rm -p 9000:80 nginx:alpine

root和index


location

location 用以匹配路由,配置语法如下

location [ = | ~ | * | ^ ] uri { … }

其中 uri 前可提供以下修饰符

  • = 精确匹配,优先级最高。
  • ^~ 前缀匹配,优先级其次。如果同样是前缀匹配,走最长路径。
  • ~ 正则匹配,优先级再次 (~* 只是不区分大小写,不单列)。如果同样是正则匹配,走第一个路径。
  • / 通用匹配,优先级再次。

location修饰符

location优先级


proxy_pass

proxy_pass 反向代理,也就是用来解决跨域的配置

当使用 proxy_pass 代理路径时,有两种情况

1. 转发时 保留 /api 路径

1
2
3
location /api/ {
proxy_pass http://localhost:3000;
}

结尾没有 / 斜杠
前端请求:/api/user –> 转发到后端:http://localhost:3000/api/user

2. 转发时 去掉 /api 路径

1
2
3
location /api/ {
proxy_pass http://localhost:3000/;
}

末尾加 / 斜杠
前端请求:/api/user –> 转发到后端:http://localhost:3000/user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
server {
listen 80;
server_name localhost;

root /usr/share/nginx/html;
index index.html index.htm;

# 建议使用此种 proxy_pass 不加 URI 的写法,原样路径即可
# http://localhost:8300/api1/hello -> proxy:3000/api1/hello
location /api1 {
# 可通过查看响应头来判断是否成功返回
add_header X-Config A;
proxy_pass http://api:3000;
}

# http://localhost:8300/api2/hello -> proxy:3000/hello
location /api2/ {
add_header X-Config B;
proxy_pass http://api:3000/;
}

# http://localhost:8300/api3/hello -> proxy:3000/hello/hello
location /api3 {
add_header X-Config C;
proxy_pass http://api:3000/hello;
}

# http://localhost:8300/api4/hello -> proxy:3000//hello
location /api4 {
add_header X-Config D;
proxy_pass http://api:3000/;
}
}

add_header

控制响应头。

很多特性都是通过响应头控制,因此基于此指令可做很多事情,比如:

Cache
CORS
HSTS
CSP

Cache

1
2
3
location /static {
add_header Cache-Control max-age=31536000;
}

CORS

1
2
3
location /api {
add_header Access-Control-Allow-Origin *;
}

HSTS

HTTPS,SSL相关

1
2
3
4
5
location / {
listen 443 ssl;

add_header Strict-Transport-Security max-age=7200;
}

CSP

1
2
3
location / {
add_header Content-Security-Policy "default-src 'self';";
}

最佳实践

  • /index.html:304
  • /assets/*.[hash].(js|css|…):一年强缓存 + immutable
  • gzip/brotli 开启
  • SPA try_files 回退
  • redirect、rewrite配置
  • HTTPS + HTTP2 + 基础安全头
  • 发布前 nginx -t,发布后灰度验证

缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# HTML 走协商缓存(每次都向服务端校验)
location ~* \.html?$ {
expires -1; # 不走 Expires 强缓存
add_header Cache-Control "no-cache";
etag on; # 默认一般就是 on,显式写更清晰
if_modified_since exact; # 默认即可,显式写
try_files $uri =404;
}

# JS,CSS走强缓存
location /static/ {
expires 30d;
add_header Cache-Control "public, max-age=2592000, immutable";
try_files $uri =404;
}

接口转发解决跨域

Gzip压缩

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 开启 gzip
gzip on;
gzip_vary on; # 响应头加 Vary: Accept-Encoding,避免代理缓存错配
gzip_proxied any; # 被代理请求也允许压缩
gzip_comp_level 6; # 压缩等级 1-9,6 是常用平衡点
gzip_min_length 1024; # 小于 1KB 不压缩
gzip_buffers 16 8k;
gzip_http_version 1.1;

# 压缩的 MIME 类型(注意 text/html 通常默认已启用)
gzip_types
text/plain
text/css
text/javascript
application/javascript
application/json
application/xml
application/rss+xml
image/svg+xml
font/ttf
font/otf
application/vnd.ms-fontobject
application/x-font-ttf
application/font-woff
application/font-woff2;

History路由

1
2
3
location / {
try_files $uri /index.html;
}

Redirect重定向

Redirect(重定向):告诉浏览器“去另一个 URL”,浏览器地址栏会变,状态码通常是 301/302/307/308

使用场景:

  • 域名跳转(a.com -> b.com)
  • 协议跳转(http -> https)
  • 旧链接永久迁移(SEO 友好,用 301/308)
1
2
3
location = /old-path {
return 301 /new-path;
}

Rewrite重写

Rewrite(重写):Nginx 在服务器内部改 URI 去匹配资源,通常不告诉浏览器,地址栏不变

使用场景:

  • SPA 回退(try_files $uri /index.html 本质类似内部重写思路)
  • 历史路径内部兼容映射
  • 隐藏后端真实路径结构
1
2
3
location /old/ {
rewrite ^/old/(.*)$ /new/$1 last;
}

限制访问source-map文件

1
2
3
4
5
6
7
# 禁止访问 sourcemap
location ~* \.map$ {
access_log off;
log_not_found off;
add_header Cache-Control "no-store" always;
return 404;
}

实现业务逻辑

比如实现移动端跳转xx链接,pc端不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
listen 80;
server_name example.com;

location / {
# 默认不跳转
set $is_mobile 0;

# 简单 UA 判断
if ($http_user_agent ~* "(Android|iPhone|iPad|iPod|Mobile)") {
set $is_mobile 1;
}

# 移动端跳到 H5/落地页
if ($is_mobile = 1) {
return 302 https://m.example.com$request_uri;
}

# PC 保持原页面
proxy_pass http://pc_upstream;
}
}

黑白名单配置

Docker是什么

Docker 是一套把应用连同运行环境一起打包、并在任何机器上几乎一致运行的容器技术。

docker

主要解决三类问题:

  1. 环境一致:利用容器技术,减少本地和真实机器运行环境不一致的问题
  2. 交付标准化:发布物从「一堆文件 + 安装步骤」变成「一个镜像 + 一条启动命令」
  3. 隔离与密度:多个docker容器能跑在同一台物理机上,彼此进程级隔离,相比虚拟机更轻量级
阅读全文 »

币圈参与者和生态

  • 长期资金:机构、投行、矿企
  • 风险投资:VC、加速器
  • 交易所与做市商:庄家、抽水、点差(插针,收割散户:以前存在,现在大所基本较少)
  • 量化基金:职业牌手、模型驱动
  • 开发者、项目方:发币者
  • 散户:韭菜

币圈基本构成

现货市场(Spot Market)
  • 一级市场:NFT铸造、土狗、盲盒、空投认购
    参与链上的初始发行环节(容易被套、被骗、资金盘)
  • 二级市场:交易所、DEX买卖
    中心化交易所买币或去中心化交易所链上买卖
衍生品市场(Derivatives Market):合约、期权
  • 合约
  • 期权
阅读全文 »

不要被学术界的思维限制了头脑,不要被程序员的思维限制了想象力。

比特币中的密码学

比特币中的哈希特性

Collision resistance (抗哈希碰撞)

没有高效的方法来人为的制造哈希碰撞

  • Collision resistance的定义:给定X,没有高效的方法找到Y,使得H(X) = H(Y)
  • Collision resistance的特性:无法用数学证明
  • MD5哈希函数:以前认为是Collision resistance,后来被鉴定为不安全的哈希函数,可通过人为的方式制造哈希碰撞
  • 比特币中使用的哈希函数:SHA-256(Secure Hash Algorithm)
Hiding

哈希的过程单向不可逆

  • Hiding的定义:输入值的空间够大,且分布均匀,取值可能性相同
    实际场景如何实现Hiding(保证分布均匀):输入值 + 随机数(输入X || nonce随机数)后经过Hash
Puzzle friendly

事先无法知道什么样的输入能得到一个什么样的哈希值,只能一个个尝试

比如挖矿,H(nonce + block header) <= target,没有捷径,只能去尝试多个nonce来找到解 => proof of work

比特币中的账户管理

非对称加密(asymmetric encryption algorithm):加密解密不用同一个密钥,加密用公钥,解密用私钥
去中心化,每个用户本地自己生成一组公钥和私钥,公钥相当于银行账号,私钥相当于账号密码
比特币交易过程中,为了能知道交易是由谁发起,需要用私钥将交易签名,公钥验证

两个人生成的公钥私钥相同怎么办(256位的值,产生两组相同公钥私钥的概率微乎其微)

message取hash->hash取签名

阅读全文 »
0%