# 从 Web 迁移到 Lynx

将现有的 Web 项目迁移到 Lynx 的完整指南。

## 迁移概览

### 文件结构变化

**Web 项目**:

```
src/
├── components/
│   ├── Button.js
│   └── Card.js
├── pages/
│   ├── Home.js
│   └── About.js
├── styles/
│   ├── global.css
│   └── variables.css
└── index.html
```

**Lynx 项目**:

```
src/
├── components/
│   ├── Button.jsx
│   └── Card.jsx
├── pages/
│   ├── Home.jsx
│   └── About.jsx
├── styles/
│   ├── global.scss
│   └── variables.scss
└── App.jsx
```

### 主要变化点

1. **文件扩展名**: `.js` → `.jsx`
2. **HTML 标签** → **Lynx 组件**: `div` → `view`, `span/p` → `text`, `img` → `image`
3. **CSS 文件**: `.css` → `.scss` (推荐)
4. **事件处理**: `onClick` → `bindtap`
5. **路由**: 使用 Lynx 导航 API

## Web 根容器映射

### `html` / `body` 与 Lynx `<page>` 的对应关系

在 Web 中，`html` 和 `body` 构成了页面的根容器，默认具备以下职责：
- 作为文档根节点
- 作为默认的页面滚动容器（body scroll）
- 作为 `position: fixed` 的视口参考系
- 作为 `html, body { height: 100% }` 所定义的视口大小画布

在 Lynx 中，这些职责由 `<page>` 承担：
- `<page>` 是页面的 DOM/布局根节点（可由框架隐式生成）
- `<page>` 本身**不是**滚动容器，默认 `overflow: hidden`
- 如果需要实现 Web 中“body scroll”的等效长页面滚动，通常需要在 `<page>` 内部显式包裹 `<scroll-view>`

### 滚动转换的注意事项

将 Web 的普通 body 滚动页面迁移到 Lynx 时，通常的做法是：

```html
<!-- Web: 普通 body 滚动 -->
<body>
  <header>Header</header>
  <main><!-- 长内容 --></main>
</body>
```

```jsx
<!-- Lynx: 等效实现 -->
<page>
  <scroll-view scroll-orientation="vertical" style={{ height: '100%' }}>
    <view className="header">Header</view>
    <view className="main"><!-- 长内容 --></view>
  </scroll-view>
</page>
```

**关键差异**：引入 `<scroll-view>` 后，`position: fixed` 的参考系可能从视口变为 `<scroll-view>` 的滚动上下文，而 `z-index` 的层叠也可能受 `<scroll-view>` 内部的 stacking context 影响。迁移时需要特别检查固定定位元素和覆盖层级的行为。

## 步骤 1: HTML 标签转换

### 基础标签映射

| Web                  | Lynx                   |
| -------------------- | ---------------------- |
| `div`                | `view`                 |
| `span`, `p`, `h1-h6` | `text`                 |
| `img`                | `image`                |
| `input`              | `input`                |
| `button`             | `view` + 事件          |
| `a`                  | `text` + 事件          |
| `ul/ol/li`           | `list` + `list-item`   |
| `form`               | 手动处理               |
| `table`              | 使用 Grid 或 Flex 布局 |

### 转换示例

**Web 代码**:

```jsx
function Card({ title, description, image }) {
  return (
    <div className="card">
      <img src={image} alt={title} />
      <div className="content">
        <h3>{title}</h3>
        <p>{description}</p>
      </div>
    </div>
  );
}
```

**Lynx 代码**:

```jsx
function Card({ title, description, image }) {
  return (
    <view className="card">
      <image src={image} />
      <view className="content">
        <text className="title">{title}</text>
        <text className="description">{description}</text>
      </view>
    </view>
  );
}
```

### Table 布局迁移

Lynx **完全不支持** `<table>`、`<tr>`、`<td>` 元素以及 `display: table*`、`border-spacing`、`border-collapse` 等表格相关属性。必须将 table 结构重写为 Grid 或 Flex 布局。

**Web 代码（简单表格）**:

```html
<table>
  <tr>
    <td>Cell 1</td>
    <td>Cell 2</td>
    <td>Cell 3</td>
  </tr>
  <tr>
    <td>Cell 4</td>
    <td>Cell 5</td>
    <td>Cell 6</td>
  </tr>
</table>
```

```css
table { border-spacing: 2px; }
td { border: 1px solid; padding: 8px; }
```

**Lynx 代码（Grid 替代）**:

```jsx
<view className="grid-table">
  <view className="cell"><text>Cell 1</text></view>
  <view className="cell"><text>Cell 2</text></view>
  <view className="cell"><text>Cell 3</text></view>
  <view className="cell"><text>Cell 4</text></view>
  <view className="cell"><text>Cell 5</text></view>
  <view className="cell"><text>Cell 6</text></view>
</view>
```

```css
.grid-table {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;  /* 替代 border-spacing */
}
.cell {
  border: 1px solid;
  padding: 8px;
}
```

**Lynx 代码（Flex 替代 - 单行多列）**:

```jsx
<view className="flex-table">
  <view className="cell"><text>Cell 1</text></view>
  <view className="cell"><text>Cell 2</text></view>
  <view className="cell"><text>Cell 3</text></view>
</view>
```

```css
.flex-table {
  display: flex;
  flex-direction: row;
  gap: 2px;
}
.cell {
  width: 10px;
  height: 10px;
  border: 1px solid;
}
```

### 文本处理

**Web**: 可以直接在容器中放文本

```jsx
<div>Hello World</div>
```

**Lynx**: 必须使用 text 组件

```jsx
<view>
  <text>Hello World</text>
</view>
```

## 步骤 2: CSS 转换

### 1. Display 属性与默认布局

**关键差异**：Lynx 默认使用 linear layout，不是 Web 的 block flow layout。

```css
/* Web: 默认 block flow，子元素自动填满父元素宽度 */
div { /* 默认 display: block，宽度 100% */ }

/* Lynx: 默认 linear layout，子元素由内容决定尺寸 */
view { /* 默认 display: linear; linear-direction: column */ }
```

**移除**:

```css
/* 移除这些 */
display: inline;
display: inline-block;
```

**保留或改用**:

```css
/* display: block 在 Lynx 2.0+ 中保留（会回退到 Flex/Linear） */
display: block;

/* 使用这些 */
display: flex;
display: grid;
display: linear; /* Lynx 特有 */
display: relative; /* Lynx 特有 */
```

**嵌套元素尺寸处理**：

从 Web 迁移时，如果原始 CSS 使用 `div { width: X; height: Y; }` 来设置所有嵌套元素的尺寸，在 Lynx 中需要确保选择器能匹配到嵌套的 `view` 元素。

```css
/* Web: 所有嵌套 div 自动填满 */
div { width: 5em; height: 1em; }

/* Lynx: 需要显式设置，或使用 stretch */
view { width: 5em; height: 1em; }
/* 或者 */
.nested-view {
  width: 100%;
  height: 100%;
}
/* 或者 */
.parent {
  display: linear;
  linear-cross-gravity: stretch;
}
```

### 2. Float 移除

**Web**:

```css
.left {
  float: left;
}
.right {
  float: right;
}
.clear {
  clear: both;
}
```

**Lynx**:

```css
.container {
  display: flex;
  flex-direction: row;
}

.left {
  /* 正常文档流 */
}
.right {
  margin-left: auto;
}
```

### 3. 单位转换

**注意区分**:

```css
/* max-content / fit-content 在 Lynx 中支持，可直接保留 */
width: max-content;
width: fit-content;

/* min-content 部分支持（如在 flex-basis 中会被视为 0px），迁移时需谨慎测试 */
width: min-content;
```

**改用**（针对原本不支持的 Web 值）:

```css
width: auto;
width: 100%;
width: 200px;
/* 或使用 rem（推荐） */
page {
  font-size: calc(100vw / 23.4375);
}
width: 23.4rem;
```

### 4. Position 调整

**Web**:

```css
.element {
  /* 默认 position: static */
  z-index: 10;
}
```

**Lynx**:

```css
.element {
  position: relative; /* 必须显式设置 */
  z-index: 10;
}
```

### 5. 文本换行

**Web**: 默认换行

```css
/* 改用 */
width: auto;
width: 100%;
width: 200px;
/* 或使用 rem（推荐） */
page {
  font-size: calc(100vw / 23.4375); /* 1rem = 16px @ 375px */
}
width: 23.4rem; /* 约 375px @ 375px */
```

**Lynx**: 默认不换行

```css
text {
  white-space: normal; /* 需要显式设置 */
}
```

### 6. Margin Collapsing

**Web**: 自动合并

```css
.top {
  margin-bottom: 20px;
}
.bottom {
  margin-top: 20px;
}
/* 实际间距: 20px */
```

**Lynx**: 不合并

```css
.top {
  margin-bottom: 10px;
}
.bottom {
  margin-top: 10px;
}
/* 实际间距: 20px，需要调整 */
```

**嵌套容器元素的额外补偿**：

嵌套结构中的 margin 在 Web 和 Lynx 中的行为差异更明显。例如外层容器设置 `margin: 8px`，内层容器（第一个子容器）设置 `margin-top: 16px`：

```css
/* Web: margin collapse 后最终间距为 16px */
.body {
  margin: 8px;
}
.content-wrapper {
  margin-top: 16px;  /* 与外层 8px collapse，最终 16px */
}

/* Lynx: margin 相加后最终间距为 24px */
.body {
  margin: 8px;
}
.content-wrapper {
  margin-top: 16px;  /* 8px + 16px = 24px */
}
```

**迁移修复方案**：

```html
<!-- 迁移示例：外层容器 + 第一个子容器 -->
<view class="body">
  <view class="content-wrapper"></view>
</view>
```

```css
/* 方案 1: 调整内层 margin，使相加后等于预期值 */
.content-wrapper {
  margin-top: 8px;   /* 如果预期总间距 16px: 8px + 8px = 16px */
}

/* 方案 2: 使用 padding 替代 margin */
.body {
  margin: 0;
  padding: 8px;
}
.content-wrapper {
  margin-top: 0;
}

/* 方案 3: 父元素使用 flex + gap */
.body {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 8px;
}
```

**常见场景**：
- 外层容器有 margin，内层容器也有 margin 时，总间距会比预期大
- 组件封装时内部容器与外部容器之间的间距叠加
- 多层嵌套结构中 margin 的累积效应

## 步骤 3: 事件处理转换

### 点击事件

**Web**:

```jsx
<button onClick={handleClick}>Click me</button>
```

**Lynx**:

```jsx
<view bindtap={handleClick}>
  <text>Click me</text>
</view>
```

### 事件名称映射

| Web            | Lynx             |
| -------------- | ---------------- |
| `onClick`      | `bindtap`        |
| `onTouchStart` | `bindtouchstart` |
| `onTouchMove`  | `bindtouchmove`  |
| `onTouchEnd`   | `bindtouchend`   |
| `onChange`     | `bindchange`     |
| `onInput`      | `bindinput`      |
| `onFocus`      | `bindfocus`      |
| `onBlur`       | `bindblur`       |
| `onScroll`     | `bindscroll`     |

## 步骤 4: 图片处理

**Web**:

```jsx
<img src="image.png" alt="Description" />
```

**Lynx**:

```jsx
<image src="image.png" />
```

**注意事项**:

- Lynx 的 `image` 必须设置尺寸或父容器有尺寸
- 不支持 `alt` 属性
- 支持懒加载: `lazy-load={true}`

## 步骤 5: 列表处理

**Web**:

```jsx
<ul>
  {items.map((item) => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>
```

**Lynx**:

```jsx
<list className="item-list">
  {items.map((item) => (
    <list-item key={item.id} item-key={item.id}>
      <view className="item">
        <text>{item.name}</text>
      </view>
    </list-item>
  ))}
</list>
```

## 步骤 6: 表单处理

**Web**:

```jsx
<form onSubmit={handleSubmit}>
  <input type="text" value={value} onChange={handleChange} />
  <button type="submit">Submit</button>
</form>
```

**Lynx**:

```jsx
<view className="form">
  <input type="text" value={value} bindinput={handleChange} />
  <view bindtap={handleSubmit} className="button">
    <text>Submit</text>
  </view>
</view>
```

## 步骤 7: 样式组织

### CSS 变量（完全支持）

Lynx **完全支持** CSS 变量（`var()`），可以通过 `:root` 或内联方式定义。

**定义变量**:

```css
:root {
  --primary-color: #ff351a;
  --secondary-color: #666;
  --spacing-unit: 8px;
  --border-radius: 4px;
}
```

**使用变量**:

```css
.button {
  background-color: var(--primary-color);
  padding: var(--spacing-unit);
  border-radius: var(--border-radius);
}

.card {
  margin: calc(var(--spacing-unit) * 2);
  color: var(--secondary-color);
}
```

**注意事项**:

- ✅ 变量可以在所有 CSS 属性中使用（包括 `calc()`）
- ✅ 支持默认值：`var(--undefined, fallback-value)`
- ✅ 支持级联和作用域覆盖
- ✅ 与 Web 标准完全一致

### 使用 CSS 变量（SCSS 示例）

**Web**:

```css
:root {
  --primary-color: #ff351a;
}
```

**Lynx**:

```css
/* 相同语法 */
:root {
  --primary-color: #ff351a;
}
```

### 使用 SCSS

推荐在 Lynx 中使用 SCSS 以便更好地组织样式。

```scss
// variables.scss
$primary-color: #ff351a;
$spacing-unit: 8px;

// component.scss
@import './variables.scss';

.component {
  color: $primary-color;
  padding: $spacing-unit * 2;
}
```

## 迁移检查清单

- [ ] 替换 HTML 标签为 Lynx 组件
- [ ] 所有文本使用 `<text>` 组件包裹
- [ ] 移除 `float` 和 `clear`
- [ ] 移除 `display: inline/inline-block`；`display: block` 在 Lynx 2.0+ 中保留（回退到 Flex/Linear）
- [ ] 检查 `position: static`，改用 `relative`
- [ ] `max-content` / `fit-content` 可直接保留；`min-content` 部分支持（flex-basis 中视为 0px），迁移时测试确认
- [ ] 为所有 `z-index` 添加 `position`
- [ ] Check z-index in scrollable containers causes scroll not to follow issue
- [ ] 检查 CSS 变量（`var()`）兼容性 - Lynx 完全支持
- [ ] 移除所有 `::before` / `::after` 使用（完全不支持）
- [ ] 检查 margin collapsing 问题
- [ ] 设置 `white-space: normal` 使文本换行
- [ ] 转换事件处理器名称
- [ ] 处理图片组件
- [ ] 转换列表组件
- [ ] 处理表单组件
- [ ] 测试所有交互功能
- [ ] 测试响应式布局
- [ ] 性能优化（列表虚拟化等）
