2024-08-06

'# 【uniapp】uniapp的App端web-view加载本地html

一、背景与问题

在uni-app开发中,web-view组件是实现App端嵌入H5页面的核心工具。但其在App端的使用存在特殊性:微信小程序的web-view组件仅支持加载远程网页,而uni-app的web-view在App端却支持加载本地html文件。这种差异源于uni-app对不同平台的差异化封装。

这种能力在实际项目中具有特殊价值:开发者可以将部分复杂页面作为独立的H5项目开发,再通过web-view嵌入App。但这种技术方案也存在明显局限性,需要理解其工作原理和适用场景。

二、基本原理

uni-app的web-view在App端的实现本质上是调用了原生的WebView组件。其核心原理如下:

  1. 原生WebView加载:通过调用App的WebView组件加载本地文件路径(如file:///path/to/index.html
  2. JSBridge通信:通过uni-app提供的$$_bridge对象实现与原生的通信
  3. 路径映射:通过manifest.json配置的pages路径映射关系,将本地html文件路径转化为App可识别的URL

这种机制允许开发者在App端直接访问本地文件系统,但同时也带来了性能和安全方面的挑战。

三、环境准备

在开始开发前,需要准备以下环境:

  1. 开发环境:HBuilderX(最新版本)
  2. 项目结构

    src/
    ├── pages/
    │   └── index/
    │       ├── index.vue
    │       └── local.html
    └── App.vue
  3. 配置文件:在manifest.json中配置pages路径映射

四、核心实现

1. 基础用法

<template>
  <view class="container">
    <web-view :src="localUrl" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      localUrl: 'file:///android_asset/www/local.html' // Android路径
    };
  }
};
</script>

关键代码解析

  • file:///android_asset/www/ 是Android系统中访问assets目录的标准路径
  • iOS系统需要使用 file:///var/mobile/Containers/Data/Application/... 路径(需动态生成)
  • 该路径需要通过uni-app的getAppFilePath方法获取

2. 动态加载本地HTML

<template>
  <view class="container">
    <web-view :src="dynamicUrl" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      dynamicUrl: ''
    };
  },
  mounted() {
    // 获取本地html文件路径
    uni.getFileSystemManager().readFile({
      filePath: `${uni.getStorageSync('projectRoot')}/local.html`,
      success: (res) => {
        this.dynamicUrl = `file:///android_asset/www/${encodeURIComponent(res.data)}`;
      }
    });
  }
};
</script>

关键代码解析

  • 使用getFileSystemManager().readFile读取本地文件
  • encodeURIComponent处理路径中的特殊字符
  • 需要处理不同平台的路径差异(iOS和Android)

3. JSBridge通信

<template>
  <view class="container">
    <web-view :src="localUrl" @message="handleMessage" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      localUrl: 'file:///android_asset/www/local.html'
    };
  },
  methods: {
    handleMessage(e) {
      // 接收来自本地html的JSBridge消息
      console.log('收到本地html消息:', e.detail);
    }
  }
};
</script>

关键代码解析

  • @message事件用于接收来自本地html的JSBridge消息
  • 需要通过$$_bridge对象发送消息
  • 需要处理消息格式:$$_bridge.postMessage(data, callback)

五、完整案例

1. 项目结构

src/
├── pages/
│   └── local/
│       ├── index.vue
│       └── local.html
└── App.vue

2. 前端代码(index.vue)

<template>
  <view class="container">
    <web-view :src="localUrl" @message="handleMessage" />
  </view>
</template>

<script>
export default {
  data() {
    return {
      localUrl: 'file:///android_asset/www/local.html'
    };
  },
  methods: {
    handleMessage(e) {
      console.log('收到本地html消息:', e.detail);
      // 可以在这里处理来自本地html的业务逻辑
    }
  }
};
</script>

3. 本地html文件(local.html)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>本地页面</title>
</head>
<body>
  <h1>这是本地HTML页面</h1>
  <button onclick="sendMessage()">发送消息</button>
  <script>
    // 通过uni-app的JSBridge通信
    window.$$_bridge.postMessage({
      action: 'hello',
      data: '来自本地html的消息'
    }, function(response) {
      console.log('收到App端响应:', response);
    });
  </script>
</body>
</html>

4. 运行说明

  1. 在HBuilderX中打开项目
  2. 点击运行到手机(需确保手机已连接调试)
  3. 点击"发送消息"按钮,观察控制台输出

六、源码解析

在uni-app的源码中,web-view组件的App端实现主要依赖uni-app-compiler中的web-view模块。关键代码如下:

// src/uni-app-compiler/bridge/web-view.js
export function renderWebview(context) {
  const platform = getPlatform();
  if (platform === 'app') {
    const url = getLocalHtmlPath();
    return `<web-view :src="${url}"></web-view>`;
  }
  // 其他平台的处理逻辑
}

关键点解析

  • getLocalHtmlPath方法会根据项目配置生成正确的文件路径
  • 对于Android平台,会使用file:///android_asset/www/前缀
  • 对于iOS平台,会使用file:///var/mobile/Containers/Data/Application/...路径
  • 需要处理不同平台的路径差异

七、进阶使用

1. 路径动态生成

function getLocalHtmlPath() {
  const platform = getPlatform();
  let base = '';
  if (platform === 'android') {
    base = 'file:///android_asset/www/';
  } else if (platform === 'ios') {
    base = 'file:///var/mobile/Containers/Data/Application/';
  }
  return `${base}${encodeURIComponent(getLocalHtmlFile())}`;
}

2. JSBridge高级用法

// App端监听消息
uni.$on('bridge_message', (data) => {
  console.log('收到bridge消息:', data);
});

// 本地html发送消息
window.$$_bridge.postMessage({
  action: 'test',
  data: 'test data'
});

3. 动态内容加载

<template>
  <web-view :src="dynamicUrl" @message="handleMessage" />
</template>

<script>
export default {
  data() {
    return {
      dynamicUrl: ''
    };
  },
  mounted() {
    uni.getFileSystemManager().readFile({
      filePath: `${uni.getStorageSync('projectRoot')}/local.html`,
      success: (res) => {
        this.dynamicUrl = `file:///android_asset/www/${encodeURIComponent(res.data)}`;
      }
    });
  }
};
</script>

八、性能与工程实践

1. 性能优化

  • 缓存策略:对常用页面进行缓存,减少重复加载
  • 懒加载:只在需要时加载特定页面
  • 资源压缩:使用Gzip压缩HTML文件
  • 避免频繁重绘:减少不必要的DOM操作

2. 异常处理

uni.getFileSystemManager().readFile({
  filePath: filePath,
  success: (res) => {
    // 处理成功
  },
  fail: (err) => {
    console.error('读取文件失败:', err);
    // 显示错误提示
  }
});

3. 安全风险

  • XSS攻击:本地html可能包含恶意脚本
  • 数据泄露:通过JSBridge可能泄露敏感数据
  • 路径遍历:未正确处理路径可能导致文件读取漏洞

解决方案

  • 对用户输入进行严格校验
  • 使用Content Security Policy(CSP)限制脚本执行
  • 对关键数据进行加密传输

九、常见问题与踩坑

1. 路径问题

错误示例

const url = 'file:///local.html';

问题:未指定完整路径,导致无法找到文件

解决方法:使用getLocalHtmlPath生成完整路径

2. JSBridge通信失败

错误示例

window.$$_bridge.postMessage('test');

问题:未正确传递参数和回调函数

解决方法

window.$$_bridge.postMessage({
  action: 'test',
  data: 'test data'
}, function(response) {
  console.log('收到响应:', response);
});

3. 安全漏洞

错误示例

<script src="http://malicious.com/script.js"></script>

问题:加载外部脚本可能引入恶意代码

解决方法:禁用外部脚本加载,使用CSP头限制

十、最佳实践

1. 使用场景

  • 需要复用现有H5项目时
  • 需要与原生功能深度交互时
  • 需要展示复杂UI但不想开发原生模块时

2. 应用场景

  • 配置页面(如设置、帮助中心)
  • 个性化页面(如用户自定义的仪表盘)
  • 需要展示动态内容的页面(如数据看板)

3. 避免使用场景

  • 需要频繁交互的页面
  • 需要高性能计算的页面
  • 需要访问本地文件系统的场景
  • 需要实时更新的页面

十一、总结

uni-app的App端web-view组件提供了将本地HTML嵌入App的能力,这在特定场景下非常有用。但开发者需要理解其工作原理,合理使用JSBridge通信,注意安全风险和性能优化。在选择使用该技术时,需要权衡其优缺点,合理规划应用场景。通过正确使用和优化,可以充分发挥这一技术的优势,构建更丰富的App功能。

2024-08-05

'# 使用el-input文本框实现文本HTML格式的保存,并回显编辑,显示详情等

一、背景与问题

在实际开发中,我们经常需要在前端页面中实现富文本编辑功能。传统的el-input组件仅支持纯文本输入,无法直接处理HTML格式内容。但某些业务场景需要同时支持:

  1. 保存用户输入的富文本内容(如加粗、换行、列表等格式)
  2. 前端展示时保持原有格式
  3. 在编辑状态下支持格式化编辑

传统的解决方案是引入第三方富文本编辑器(如Quill、TinyMCE等),但这种方式存在以下问题:

  • 需要引入额外的库
  • 需要处理复杂的配置
  • 与Element UI组件库的集成需要额外封装

本文将探索一种基于el-input的替代方案,通过结合contenteditable特性实现HTML格式的处理,同时保证安全性与性能。

二、基本原理

1. 原生HTML内容处理机制

HTML内容的处理需要解决两个核心问题:

  1. 输入内容的转义:防止XSS攻击(如<script>标签注入)
  2. 内容的存储与回显:确保HTML格式在保存和展示时保持一致

2. contenteditable的特殊处理

虽然el-input本身不支持HTML内容,但可以通过以下方式模拟:

  • 使用<div contenteditable="true">作为编辑容器
  • 通过el-input作为显示容器(需配合v-model绑定)

3. 安全处理机制

需要引入HTML转义/反转义机制,推荐使用DOMPurify库处理用户输入内容:

npm install dompurify

三、环境准备

1. 项目结构示例

src/
├── components/
│   └── RichTextEditor.vue
├── utils/
│   └── htmlUtils.js
├── App.vue

2. 安装依赖

npm install dompurify

四、核心实现

1. HTML转义/反转义工具类

// utils/htmlUtils.js
import { sanitizeHtml } from 'dompurify';

export const htmlEscape = (html) => {
  return sanitizeHtml(html, {
    allowedTags: ['b', 'i', 'u', 'br', 'p', 'ul', 'li', 'a'],
    allowedAttributes: {
      'a': ['href', 'target']
    }
  });
};

export const htmlUnescape = (html) => {
  return html;
};

2. 编辑器组件实现

<!-- components/RichTextEditor.vue -->
<template>
  <div>
    <el-input 
      v-model="displayContent" 
      type="textarea" 
      rows="10" 
      placeholder="请输入内容(支持HTML格式)"
      @focus="toggleEditor"
    ></el-input>
    <div 
      v-if="isEditing" 
      contenteditable="true" 
      class="editor" 
      @input="onInput"
      v-html="htmlEscape(content)"
    ></div>
  </div>
</template>

<script>
import { htmlEscape, htmlUnescape } from '../utils/htmlUtils';

export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      isEditing: false,
      content: ''
    };
  },
  computed: {
    displayContent() {
      return this.isEditing ? this.content : this.htmlUnescape(this.value);
    }
  },
  watch: {
    value(newVal) {
      this.content = newVal;
    }
  },
  methods: {
    toggleEditor() {
      this.isEditing = !this.isEditing;
      if (this.isEditing) {
        this.content = this.htmlEscape(this.value);
      }
    },
    onInput(event) {
      const html = event.target.innerHTML;
      this.content = html;
      this.$emit('input', html);
    }
  }
};
</script>

<style scoped>
.editor {
  border: 1px solid #ccc;
  padding: 10px;
  min-height: 100px;
  outline: none;
}
</style>

3. 安全处理关键代码解析

// 在onInput方法中
const html = event.target.innerHTML;
this.content = htmlEscape(html);
  • event.target.innerHTML获取当前编辑器内容
  • htmlEscape对内容进行转义处理
  • 配置的allowedTags限制了允许的HTML标签类型
  • allowedAttributes限制了允许的属性

五、完整案例

1. 基础案例:富文本编辑器

<!-- pages/EditorPage.vue -->
<template>
  <div>
    <RichTextEditor v-model="textContent" />
    <el-button @click="saveContent">保存内容</el-button>
    <el-button @click="showPreview">预览内容</el-button>
    <div v-if="previewVisible" class="preview">
      <p>预览内容:</p>
      <div v-html="htmlUnescape(textContent)"></div>
    </div>
  </div>
</template>

<script>
import RichTextEditor from '../components/RichTextEditor.vue';
import { htmlEscape, htmlUnescape } from '../utils/htmlUtils';

export default {
  components: { RichTextEditor },
  data() {
    return {
      textContent: '',
      previewVisible: false
    };
  },
  methods: {
    saveContent() {
      // 保存逻辑:将textContent存储到后端
      console.log('保存的内容:', this.textContent);
    },
    showPreview() {
      this.previewVisible = true;
    }
  }
};
</script>

<style>
.preview {
  margin-top: 20px;
  border: 1px solid #ddd;
  padding: 10px;
}
</style>

2. 使用场景示例

// 在保存到后端前进行最后处理
const sanitizedContent = htmlEscape(this.textContent);
axios.post('/api/save', { content: sanitizedContent });

六、源码解析

1. 编辑器组件关键流程

  1. 用户点击输入框触发@focus事件
  2. 进入编辑模式,显示contenteditable容器
  3. 用户输入内容时触发@input事件
  4. 获取innerHTML内容并进行转义处理
  5. 通过v-model同步到父组件

2. 转义处理机制

sanitizeHtml(html, {
  allowedTags: ['b', 'i', 'u', 'br', 'p', 'ul', 'li', 'a'],
  allowedAttributes: {
    'a': ['href', 'target']
  }
})
  • 允许的标签限制了可编辑的格式类型
  • 允许的属性限制了超链接的使用范围
  • 这种限制可以有效防止XSS攻击

七、进阶使用

1. 增加格式化工具

// utils/formatUtils.js
export const addBold = (html) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(html, 'text/html');
  const selection = window.getSelection();
  const range = selection.getRangeAt(0);
  
  const span = doc.createElement('span');
  span.style.fontWeight = 'bold';
  
  const selectedText = range.toString();
  span.textContent = selectedText;
  
  range.deleteContents();
  range.insertNode(span);
  
  return doc.body.innerHTML;
};

2. 与后端的协作

// 后端接口示例(Node.js/Express)
app.post('/api/save', (req, res) => {
  const { content } = req.body;
  // 假设内容为纯文本,需要转义处理
  const sanitizedContent = sanitizeHtml(content, {
    allowedTags: ['b', 'i', 'u', 'br', 'p', 'ul', 'li', 'a'],
    allowedAttributes: {
      'a': ['href', 'target']
    }
  });
  
  // 存储到数据库
  db.saveContent(sanitizedContent);
  
  res.json({ success: true });
});

八、性能与工程实践

1. 性能优化策略

  1. 内容长度限制:限制用户输入内容长度(如限制在5000字以内)
  2. 防抖处理:对频繁输入进行防抖处理
  3. 懒加载:仅在需要时才进行内容转义处理

2. 异常处理机制

try {
  const sanitizedContent = htmlEscape(this.textContent);
  axios.post('/api/save', { content: sanitizedContent });
} catch (error) {
  console.error('保存内容失败:', error);
  this.$notify.error({
    title: '错误',
    message: '内容保存失败'
  });
}

3. 安全风险分析

风险类型描述解决方案
XSS攻击用户输入恶意脚本使用DOMPurify进行内容净化
内容污染未转义导致格式混乱全局使用htmlEscape处理
资源消耗大量内容处理影响性能限制内容长度,使用防抖

九、常见问题与踩坑

1. 常见错误示例

// 错误:未进行转义直接保存
axios.post('/api/save', { content: this.textContent });

问题分析:直接保存原始HTML内容可能导致XSS攻击

改进方案

// 正确做法:先进行转义处理
axios.post('/api/save', { content: htmlEscape(this.textContent) });

2. 其他典型问题

问题解决方案
内容显示异常确保在回显时使用v-html而非{{ }}
编辑器未响应检查contenteditable属性是否生效
格式丢失确保htmlEscape的allowedTags包含所需标签

十、最佳实践

  1. 安全优先:始终使用DOMPurify进行内容净化
  2. 格式限制:根据业务需求限定允许的HTML标签
  3. 内容长度控制:设置最大输入长度限制
  4. 输入校验:在前端和后端都进行输入校验
  5. 性能优化:对大量内容进行懒加载处理
  6. 用户提示:在输入时提示允许的格式类型

十一、总结

通过结合contenteditable特性与DOMPurify库,我们可以在不引入第三方富文本编辑器的情况下,实现基于el-input的富文本处理功能。这种方案具有以下特点:

  • 灵活性:可自定义允许的HTML标签和属性
  • 安全性:通过DOMPurify有效防止XSS攻击
  • 可维护性:与Element UI组件库无缝集成
  • 性能可控:可针对具体业务场景进行优化

建议在以下场景使用本方案:

  • 需要保存简单格式化内容(如加粗、换行等)
  • 不需要复杂的编辑功能(如图片上传、表格处理等)
  • 对安全性要求较高的场景

但需注意以下限制:

  • 不支持复杂的富文本功能(如表格、嵌入多媒体)
  • 需要手动处理大部分格式化逻辑
  • 与第三方富文本编辑器相比功能有限

通过合理的设计和实现,这种方案可以在保持代码简洁性的同时,满足大部分基础的富文本处理需求。

2024-08-04

'# 无涯教程-HTML - <iframe>属性

一、背景与问题

在现代Web开发中,