LOADING

加载过慢请开启缓存 浏览器默认开启

耀耀切克闹

Skyline 渲染问题记录以及总结

知识点 2026/9/20

Skyline 渲染模式下 scroll-view 高度塌缩

项目在 app.json 里全局启用了 Skyline 渲染("renderer": "skyline")。Skyline 和传统 WebView 渲染有个关键差异:

Skyline 下 scroll-view 不会自动按内容撑开高度,不显式设置高度就会塌缩为 0。

正好踩中了这个坑:

  • 外层页面 scroll-view 设了 height:100vh → 正常显示
  • 品牌区的 .brand-scroll 设了 height: 60vh → 正常显示
  • 历史标签的 .history-tags-wrapper 没设高度 → 高度变 0,里面的标签和“暂无记录”全被压没了

不是数据问题,是整个容器高度为 0。

Skyline 渲染模式 超一行无省略号

  • 根因:Skyline 渲染下 text-overflow: ellipsis 仅 text 组件支持,view 上无效
  • 修复:在使用了 text-overflow: ellipsi的 view 改为 text,wxss 加 display:block
  • 项目教训:Skyline 下文本省略/截断必须用 text 组件 + display:block

    Skyline 渲染模式下 (豆腐块/错字)

  • 现象:view→text 改造后,个别文字 出现 □ 豆腐块和错位字形,字符串里混入了控制字符/零宽字符/私有区字符这类“脏字符”(WebView 静默忽略脏字符,Skyline text 会画出来)
  • 修复:新增 sanitizeDesc()(过滤控制字符/零宽字符/私有区/替换符 + 多余空白 + JS 侧截断 40 字兜底)
const sanitizeDesc = (str, maxLen = 40) => {
  if (!str) return ''
  const cleaned = String(str)
    .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '') // 控制字符
    .replace(/[\u200B-\u200D\uFEFF\u2028\u2029]/g, '')                     // 零宽字符/分隔符
    .replace(/[\uE000-\uF8FF]/g, '')                                       // 私有区字符(字体无字形→豆腐块)
    .replace(/\uFFFD/g, '')                                                // 替换符
    .replace(/\s+/g, ' ')
    .trim()
  if (maxLen && cleaned.length > maxLen) {
    return cleaned.slice(0, maxLen) + '...'
  }
  return cleaned
}
阅读全文

短链跳转

知识点 2026/7/17

短链跳转

  1. 短链服务(服务端/运维负责):短链(如 s.xxx.com/abc123)本身只是一个入口。当用户在浏览器访问短链时,DNS会将其指向短链服务器,短链服务器会通过 HTTP 302 重定向,将用户的浏览器跳转到真实的长链地址(如 app.xxx.com/xxx?contractCode=123)。
  2. 前端页面(前端负责):前端只需要部署在长链对应的真实服务器上即可。当浏览器被重定向到长链后,前端代码加载执行,通过 URLSearchParams 获取 URL 中的参数并渲染页面。

前端一般只需要处理重定向后到达的最终 URL 和参数即可。 前端代码只关心长链长什么样,以及从长链的 URL 中怎么取参数。

短链跳转如何实现302重定向

短链跳转实现 302 重定向,是纯服务端(或 Nginx)的行为,前端代码不参与重定向的过程,前端只是被动接收重定向后的结果。

实现 302 重定向通常有以下两种常见方式:

方式一:通过 Nginx 配置实现(最常用、性能最好)

如果你的短链域名(如 s.example.com)直接指向你的服务器,可以在 Nginx 中配置 302 重定向,将短链映射到长链。

server {
    listen 80;
    server_name s.example.com; # 短链域名

    # 匹配短链路径,例如 s.example.com/abc123
    location /abc123 {
        # 302 临时重定向到长链地址
        return 302 https://app.example.com/=xxx?contractCode=12345&subType=HRXC1;
    }

    # 也可以配合正则做动态映射(如果短链路径和参数有对应关系)
    location ~ ^/s/(.*)$ {
        # 将路径参数拼接到长链后面
        return 302 https://app.example.com/xxx?contractCode=$1;
    }
}

逻辑说明:
当用户访问 s.example.com/abc123 时,Nginx 会在 HTTP 响应头中返回状态码 302,并在 Location 字段带上长链地址。浏览器收到响应后,会自动将页面跳转到 Location 指定的长链地址。

方式二:通过后端代码实现(如 Java / Node.js)

如果你们的短链是存在数据库中的(即短链路径与长链的映射关系是动态的),通常会在后端应用代码中处理重定向。

Java (Spring Boot) 示例:

@RestController
public class ShortUrlController {

    @GetMapping("/s/{shortCode}")
    public ResponseEntity<Void> redirectShortUrl(@PathVariable String shortCode) {
        // 1. 根据 shortCode 去数据库查询对应的长链地址
        String longUrl = queryLongUrlFromDB(shortCode); 
        
        if (longUrl != null) {
            // 2. 设置 302 重定向响应头
            HttpHeaders headers = new HttpHeaders();
            headers.setLocation(URI.create(longUrl));
            return new ResponseEntity<>(headers, HttpStatus.FOUND); // 302 FOUND
        } else {
            return ResponseEntity.notFound().build(); // 找不到返回 404
        }
    }
}

Node.js (Express) 示例:

app.get('/s/:shortCode', (req, res) => {
  const shortCode = req.params.shortCode;
  // 1. 查库获取长链
  const longUrl = getLongUrlFromDB(shortCode);
  
  if (longUrl) {
    // 2. 302 重定向
    res.redirect(302, longUrl);
  } else {
    res.status(404).send('Not Found');
  }
});

前端路由伪装 + 后端真实短链的架构

它的核心思路是:前端提供一个人眼可读、容易记忆的“伪短链”入口,当用户访问这个入口时,前端代码将这个请求转换并代理/重定向到服务端真正的短链 API 上,服务端再进行 302 重定向到最终的长链。

具体的实现流程如下:

实现流程设计

  1. 用户收到短信:点击前端伪短链 https://app.example.com/w/4545
  2. 前端路由拦截:Vue Router 匹配到 /w/:code 路由,进入对应的组件。
  3. 前端请求服务端:组件在初始化时,将 4545 作为参数,去请求服务端真实的短链解析接口(如 https://api.example.com/shortLink/resolve?code=4545)。
  4. 服务端返回长链:服务端根据 4545 查库找到对应的真实长链(如 https://app.example.com/xxxx?contractCode=xxx&subType=yyy),并将其返回给前端。
  5. 前端跳转长链:前端拿到长链后,使用 window.location.href 或 this.$router.push 跳转到真实的页面。

优缺点

  • 优点:

    1. 域名可控:对外发送的短信链接域名是你自己的前端域名,如果后端短链服务域名变更,前端只需修改代理接口,无需重新发版或通知短信平台修改域名。
    2. 体验好:用户看到的链接简短且属于同一主域,信任度更高。
    3. 埋点方便:在中转页面可以统一做 PV 埋点、渠道追踪等。
  • 缺点:

    1. 多一次网络请求:相比直接访问服务端短链进行 302 跳转,此方案多了一次前端到后端的 AJAX 请求再由前端跳转的过程,首屏加载会慢几百毫秒。
    2. 不是真正的 302:对于搜索引擎爬虫来说,它抓取 /w/4545 时,不会像 302 那样直接在 Header 层面感知到重定向,SEO 友好度不如服务端直接 302(但正如前面所说,你的签署页面不需要 SEO,所以无影响)。

短链跳转对SEO有影响吗

短链跳转对 SEO 确实有影响,但具体是正面还是负面影响,完全取决于使用的是 301 还是 302 重定向。

301 vs 302 对 SEO 的核心区别

  • 301 重定向(永久重定向) :

    • SEO 效果:搜索引擎(如百度、Google)会将短链的所有权重(PageRank)和收录转移到长链上。以后用户搜索相关关键词,搜索引擎只会展示长链地址。
    • 适用场景:短链只是个过渡,你希望搜索引擎最终记住并排名的是那个长链地址。
  • 302 重定向(临时重定向) :

    • SEO 效果:搜索引擎会认为短链只是“临时”跳转,因此权重不会转移,依然会保留对短链的收录。如果有很多网站都链接着这个短链,搜索引擎可能会把排名算在短链头上,甚至有时会导致内容重复收录(短链和长链都被收录),分散权重。
    • 适用场景:短链本身是核心入口,或者跳转是临时的(比如 A/B 测试、特定活动页面临时跳转)。
阅读全文

Eruda 移动端调试工具使用指南

前端随笔 2026/4/15

一、什么是Eruda?

Eruda是一个专为手机网页前端设计的调试面板,类似于Chrome DevTools的迷你版。它解决了移动端开发调试困难的问题,让你在手机上也能享受专业的调试体验。

二、主要功能特性

  1. 控制台(Console) - 捕获console日志,支持log、error、warn等
  2. 元素检查(Elements) - 检查DOM元素,查看样式和属性
  3. 网络面板(Network) - 捕获XHR请求,查看请求详情和响应
  4. 资源面板(Resources) - 显示本地存储、Cookie、SessionStorage等信息
  5. 信息面板(Info) - 显示设备信息、浏览器特性、性能指标
  6. 源码查看(Sources) - 查看页面源码
  7. 性能监控(Performance) - 显示页面性能指标

三、安装方法

通过NPM安装(推荐)

npm install eruda --save-dev

四、基本使用

1. 在Vue项目中使用

import eruda from 'eruda'

// 只在开发环境启用
if (process.env.NODE_ENV === 'development') {
    eruda.init()
}

2. 在React项目中使用

import eruda from 'eruda'

if (process.env.NODE_ENV !== 'production') {
    eruda.init()
}

3. 手动触发方式

  • 默认方式:页面右下角会出现齿轮图标,点击即可打开调试面板
  • 手势触发:在页面上快速点击三次可自动弹出控制台
  • URL参数:访问页面时添加?eruda=true参数

五、高级配置

1. 自定义配置

eruda.init({
    container: document.getElementById('eruda-container'),
    tool: ['console', 'elements', 'network', 'resources', 'info'],
    autoScale: true,
    useShadowDom: true
})

2. 插件系统

Eruda支持多种插件扩展:

  • eruda-code - 代码编辑器
  • eruda-features - 浏览器特性检测
  • eruda-timing - 性能监控
  • eruda-memory - 内存监控

安装插件:

npm install eruda-features
javascript
import eruda from 'eruda'
import erudaFeatures from 'eruda-features'

eruda.add(erudaFeatures)

六、生产环境控制

为了避免生产环境暴露敏感信息,建议:

1. 环境判断

// 通过环境变量控制
if (process.env.NODE_ENV === 'development') {
    eruda.init()
}

// 或通过URL参数控制
if (window.location.search.indexOf('debug=true')==-1) {
    eruda.init()
}

2. 隐藏式启用

// 通过特定启用
let clickCount = 0
let lastClickTime = 0

document.addEventListener('click', (e) => {
    const now = Date.now()
    if (now - lastClickTime ==300) {
        clickCount = 0
    }
    clickCount++
    lastClickTime = now
    
    if (clickCount == 9) { // 点击9次启用
        eruda.init()
        clickCount = 0
    }
})

七、使用技巧

  1. 元素检查:在元素面板中点击页面元素,可以实时查看和修改样式
  2. 网络监控:查看请求的详细信息,包括请求头、响应头、响应时间等
  3. 控制台调试:支持所有console API,包括console.table等高级功能
  4. 性能分析:使用性能面板监控页面加载时间和资源使用情况
  5. 本地存储:查看和编辑localStorage、sessionStorage、Cookie等

八、注意事项

  1. 文件大小:Eruda压缩后约100KB,建议只在开发和测试环境使用
  2. 安全性:生产环境务必禁用或隐藏调试工具
  3. 兼容性:支持大部分现代移动浏览器
  4. 性能影响:调试工具会占用一定内存,调试完成后建议关闭

九、具体项目中使用

utils文件

/** LET: 是否已启用调试模式 */
let _debug: boolean = false

/**
 * FUN: 是否已启用调试模式
 *
 *
 * @returns {boolean} 是否已启用调试模式
 */
export const isEnableDebug = (): boolean => {
    // 未启用调试模式时
    if (!_debug) {
        _debug = getCurrentUrlQueryValue("debug") === "1"
    }

    return _debug
}

新建文件eruda.ts

/*
 * @FileDesc: 初始化 eruda 调试器
 */

import { isEnableDebug } from "@/utils"

/** LET: 是否初始化 */
let _isSetup = false

/**
 * FUN: 初始化 eruda 调试器
 *
 *
 */
export const setupEruda = () => {
    /** 已经完成初始化时 */
    if (_isSetup) {
        return
    }

    // 未启用调试时
    if (!isEnableDebug()) {
        return
    }

    import("eruda")
        .then(({ default: eruda }) => {
            eruda.init()
            window.eruda = eruda

            console.log("[项目信息]", __PROJECT_INFO__)

            _isSetup = true
        })
        .catch(error => {
            console.error("eruda 加载失败", error)
        })
}

使用

//在连接上拼接上debug=1 
//例如:http://localhost:8080/login?debug=1

十、常见问题

Q: Eruda会影响页面性能吗? A: 在开发环境影响很小,但生产环境建议禁用。

Q: 如何只在特定设备上启用? A: 可以通过User-Agent判断设备类型。

Q: 支持TypeScript吗? A: 是的,Eruda有完整的TypeScript类型定义。

Q: 可以自定义主题吗? A: 支持通过CSS自定义样式。

Eruda是一个非常强大的移动端调试工具,能极大提升移动端开发效率。建议在项目中根据实际需求选择合适的启用方式,确保开发便利性的同时保证生产环境的安全性。

阅读全文

前端签章数据模板处理

前端随笔 2026/2/25

在开发新项目中。我们面临一个具体场景:纯预览展示的协议文档需要动态填充用户数据。这些协议本身是静态HTML模板,但其中包含大量需要根据具体业务数据动态替换的字段,如客户姓名、贷款金额、银行信息等。

技术挑战

  1. 多环境适配:协议模板需要在开发、测试、生产不同环境中运行
  2. 数据安全:敏感数据(如银行卡号、身份证号)需要安全获取和展示
  3. 动态替换:协议中的占位符需要精准、高效地被实际数据替换
  4. 兼容性:方案需要支持多种浏览器环境和模块系统

解决方案架构

核心设计理念

我们设计了一个SignatureDataService类,作为数据获取和模板填充的核心服务。该方案采用以下关键设计:

  1. 环境智能判断:自动识别运行环境,配置对应的API端点
  2. 数据隔离存储:使用本地存储管理业务入口标识(entryCode)
  3. 模板引擎:轻量级占位符替换机制
  4. 单例模式:确保全局唯一的数据服务实例

环境配置策略

javascript

function getEnvConfig() {
  const hostname = window.location.hostname;
  const href = window.location.href;

  // 开发环境
  if (hostname === 'localhost') {
    return {
      apiBaseUrl: 'http://localhost:8080/api',
      env: 'development'
    };
  }

  // 测试环境
  if (href.includes('.test.') || hostname.includes('test')) {
    return {
      apiBaseUrl: '测试环境地址',
      env: 'testcloud'
    };
  }

  // 生产环境
  return {
    apiBaseUrl: '生产环境地址',
    env: 'prodcloud'
  };
}

核心服务类

SignatureDataService类封装了完整的数据处理流程:

1. 数据获取

javascript

async fetchSignatureData() {
  const entryCode = this.getEntryCode();
  if (!entryCode) return null;
  
  const apiUrl = `${this.config.apiBaseUrl}/signature/getSignatureParams`;
  const response = await fetch(`${apiUrl}?entryCode=${encodeURIComponent(entryCode)}`);
  // ... 处理响应
}

2. 模板填充

javascript

fillTemplate(data) {
  if (!data) return;
  
  let content = document.body.innerHTML;
  const fields = {
    'bankName': data.bankName || '',
    'bankCardNo': data.bankCardNo || '',
    // ... 其他30+字段
  };
  
  // 正则表达式替换所有${fieldName}占位符
  Object.keys(fields).forEach(key => {
    const placeholder = `${${key}}`;
    const regex = new RegExp(
      placeholder.replace(/$/g, '\$').replace(/{/g, '\{').replace(/}/g, '\}'),
      'g'
    );
    content = content.replace(regex, this.escapeHtml(fields[key]));
  });
  
  document.body.innerHTML = content;
}

3. 安全防护

javascript

escapeHtml(text) {
  const div = document.createElement('div');
  div.textContent = text;
  return div.innerHTML;
}

支持的数据字段

系统支持超过30个业务字段的自动填充,涵盖:

  • 客户信息:customerName, cardNum, phoneNum
  • 贷款信息:loanAmount, period, totalInterest
  • 车辆信息:carModel, chassisNo, carColor
  • 公司信息:companyName, companyUsci, companyAddress
  • 金额相关:所有金额字段同时提供数字和中文大写形式

使用方式

1. HTML模板准备

协议模板中使用${fieldName}格式的占位符:

html

<div class="contract-section">
  <p>甲方(借款人):${customerName}</p>
  <p>身份证号:${cardNum}</p>
  <p>贷款金额:${loanAmount}元(${loanAmountWord})</p>
</div>

2. 页面初始化

javascript

// 方式一:使用默认配置
const service = window.SignatureDataService.getSignatureService();
service.initPage();

// 方式二:自定义配置
const service = new window.SignatureDataService.SignatureDataService({
  storageKey: 'customEntryCode'
});
service.initPage();

// 方式三:直接获取数据
const data = await window.SignatureDataService.fetchSignatureData();

技术优势

1. 无框架依赖

纯JavaScript实现,不依赖React、Vue等框架,适用于各种传统项目

2. 性能优化

  • 单例模式减少内存开销
  • 批量正则替换提升渲染效率
  • 按需加载数据

3. 安全考虑

  • HTML转义防止XSS攻击
  • 敏感数据本地存储隔离
  • HTTPS环境自动适配

4. 扩展性

  • 模块化设计,易于添加新字段
  • 支持CommonJS和浏览器全局两种导出方式
  • 配置可覆盖,便于定制

5. 错误处理

完善的异常捕获机制:

javascript

try {
  const data = await this.fetchSignatureData();
  if (!data) {
    console.error('数据加载失败,请稍后重试');
    return false;
  }
  this.fillTemplate(data);
  return true;
} catch (error) {
  console.error('初始化失败:', error);
  return false;
}
阅读全文

版本更新与缓存清除

脚本 2026/1/5

update-version.js

#!/usr/bin/env node

/**
 * @FileDesc: 部署前自动更新版本号脚本
 * @Usage: 在package.json的build脚本中调用
 * 例如: "build": "node scripts/update-version.js && vite build"
 */
#!/usr/bin/env node

/**
 * @FileDesc: 部署前自动更新版本号脚本
 * @Usage: 在package.json的build脚本中调用
 * 例如: "build": "node scripts/update-version.js && vite build"
 */

import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"
import { resolve, join } from "path"
import { fileURLToPath } from "url"

// 获取当前文件的目录
const __filename = fileURLToPath(import.meta.url)
const __dirname = resolve(__filename, "..")

try {
    // 获取项目根目录
    const projectRoot = resolve(__dirname, "../")
    const packageJsonPath = join(projectRoot, "package.json")
    const versionJsonPath = join(projectRoot, "public/version.json")

    // 确保 public 目录存在
    const publicDir = join(projectRoot, "public")
    if (!existsSync(publicDir)) {
        mkdirSync(publicDir, { recursive: true })
    }

    // 从 package.json 获取版本号
    const packageJsonContent = readFileSync(packageJsonPath, "utf-8")
    const packageJson = JSON.parse(packageJsonContent)
    const version = packageJson.version || "1.0.0"

    // 获取当前时间(ISO 8601格式)
    const buildTime = new Date().toISOString()

    // 创建 version.json 内容
    const versionInfo = {
        version,
        buildTime
    }

    // 写入 version.json 文件
    writeFileSync(versionJsonPath, JSON.stringify(versionInfo, null, 2))

    console.log("✅ 版本文件已更新:")
    console.log(`   版本号: ${version}`)
    console.log(`   构建时间: ${buildTime}`)
} catch (error) {
    console.error("❌ 版本文件更新失败:", error.message)
    process.exit(1)
}

version.ts

/*
 * @FileDesc: 版本管理和更新检测工具
 */

import { Toast } from "antd-mobile"
import { Dialog } from "antd-mobile"

/** 声明全局变量 */
declare const __BUILD_TIME__: string | undefined

/** 版本检查相关常量 */
export const VERSION_CHECK_CONFIG = {
    // 版本检查的localStorage key - 存储 buildTime,用于检测更新
    STORAGE_KEY_VERSION: "APP_BUILD_TIME",
    // 版本检查间隔(毫秒),5分钟检查一次
    CHECK_INTERVAL: 5 * 60 * 1000,
    // 版本API接口
    VERSION_URL: `${import.meta.env.VITE_BASE_PATH || "/"}version.json`
}

/** 版本信息接口 */
export interface IVersionInfo {
    version: string
    buildTime: string
}

/**
 * 获取当前应用版本
 */
export const getCurrentVersion = (): string => {
    const buildTime = typeof __BUILD_TIME__ !== "undefined" ? __BUILD_TIME__ : ""
    return buildTime || localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION) || "0.0.0"
}

/**
 * 获取远端版本信息
 */
export const fetchRemoteVersion = async (): Promise<IVersionInfo | null> => {
    try {
        const response = await fetch(VERSION_CHECK_CONFIG.VERSION_URL, {
            cache: "no-cache"
        })

        if (!response.ok) {
            return null
        }

        const data: IVersionInfo = await response.json()
        return data
    } catch (error) {
        console.error("获取远端版本失败:", error)
        return null
    }
}

/**
 * 清除所有缓存和本地数据(除了版本信息)
 */
export const clearAllCaches = async () => {
    try {
        // ⚠️ 关键:保存版本信息,防止清除后丢失
        const versionBuildTime = localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)

        // 清除Service Worker缓存
        if ("caches" in window) {
            const cacheNames = await caches.keys()
            await Promise.all(cacheNames.map(cacheName => caches.delete(cacheName)))
        }

        // 清除localStorage和sessionStorage
        localStorage.clear()
        sessionStorage.clear()

        // 如果存在IndexedDB,也清除掉
        if ("indexedDB" in window) {
            const databases = await indexedDB.databases()
            databases.forEach(db => {
                if (db.name) {
                    indexedDB.deleteDatabase(db.name)
                }
            })
        }

        // 恢复版本信息,防止重复检测
        if (versionBuildTime) {
            localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, versionBuildTime)
        }
    } catch (error) {
        console.error("清除缓存失败:", error)
    }
}

/**
 * 清除登录状态
 */
export const clearLoginState = async () => {
    try {
        // 清除与登录相关的localStorage数据
        const loginRelatedKeys = ["xczzH5Token", "applyStatus"]

        loginRelatedKeys.forEach(key => {
            localStorage.removeItem(key)
            sessionStorage.removeItem(key)
        })
    } catch (error) {
        console.error("清除登录状态失败:", error)
    }
}

/**
 * 处理应用更新
 */
export const handleAppUpdate = async (newBuildTime?: string) => {
    // 显示更新提示对话框
    Dialog.show({
        title: "发现新版本",
        content: "应用已更新,请重新登录以获取最新功能",
        closeOnAction: true,
        actions: [
            {
                key: "confirm",
                text: "重新登录",
                onClick: async () => {
                    const toastHandler = Toast.show({
                        content: "正在清除缓存...",
                        duration: 0
                    })

                    // 先更新版本号,再清除缓存
                    if (newBuildTime) {
                        localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, newBuildTime)
                    }

                    // 清除所有缓存和登录状态
                    await clearAllCaches()
                    await clearLoginState()

                    if (toastHandler) {
                        toastHandler.close()
                    }

                    // 重定向到登录页
                    window.location.href = `${import.meta.env.VITE_BASE_PATH || "/"}login`
                }
            }
        ]
    })
}

/**
 * 检查应用版本并处理更新
 * ⚠️ 关键改进:
 * 1. 添加去重标志,防止重复弹窗
 * 2. 添加详细日志追踪
 * 3. 添加异常处理
 */
export const checkAndHandleUpdate = async () => {
    try {
        // 获取本地记录的构建时间
        const storedBuildTime = localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)

        // 获取远端版本
        const remoteVersion = await fetchRemoteVersion()

        if (!remoteVersion) {
            console.warn("⚠️ 无法获取远端版本信息")
            return
        }

        // 添加详细日志
        console.log("📊 版本检查详情:", {
            "远端 buildTime": remoteVersion.buildTime,
            "本地 buildTime": storedBuildTime || "未记录(首次访问)",
            版本号: remoteVersion.version,
            需要更新: !!storedBuildTime && storedBuildTime !== remoteVersion.buildTime
        })

        if (storedBuildTime && storedBuildTime !== remoteVersion.buildTime) {
            console.log(`🔄 检测到版本更新!`)
            console.log(`   旧版本: ${storedBuildTime}`)
            console.log(`   新版本: ${remoteVersion.buildTime}`)

            // 传递新的 buildTime,在用户确认后更新
            await handleAppUpdate(remoteVersion.buildTime)
        } else if (!storedBuildTime) {
            // 第一次访问,记录当前版本
            console.log(`📝 首次访问,记录 buildTime: ${remoteVersion.buildTime}`)
            localStorage.setItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION, remoteVersion.buildTime)
        } else {
            console.log("✅ 版本无更新,应用为最新版本")
        }
    } catch (error) {
        console.error("❌ 版本检查出错:", error)
    }
}

/**
 * 启动版本检查(定时检查)
 */
export const startVersionCheck = () => {
    console.log("🚀 版本检查已启动")
    console.log(`📋 检查间隔: ${VERSION_CHECK_CONFIG.CHECK_INTERVAL / 1000} 秒`)
    console.log(`🔗 版本 API: ${VERSION_CHECK_CONFIG.VERSION_URL}`)

    // 立即检查一次
    checkAndHandleUpdate()

    // 定时检查
    const intervalId = setInterval(() => {
        console.log(`⏰ [${new Date().toLocaleTimeString()}] 执行定时版本检查...`)
        checkAndHandleUpdate()
    }, VERSION_CHECK_CONFIG.CHECK_INTERVAL)

    // 暴露到全局作用域,方便调试
    if (typeof window !== "undefined") {
        ;(window as any).__versionCheckUtils__ = {
            // 手动检查版本
            checkNow: () => {
                console.log("🔍 手动触发版本检查...")
                checkAndHandleUpdate()
            },
            // 清空版本记录,下次访问会重新记录
            resetVersion: () => {
                localStorage.removeItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION)
                console.log("✅ 版本记录已清空,刷新页面后会重新记录")
            },
            // 查看当前版本信息
            getVersionInfo: () => {
                return {
                    "本地 buildTime": localStorage.getItem(VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION),
                    "存储 key": VERSION_CHECK_CONFIG.STORAGE_KEY_VERSION,
                    "API 地址": VERSION_CHECK_CONFIG.VERSION_URL,
                    "检查间隔(秒)": VERSION_CHECK_CONFIG.CHECK_INTERVAL / 1000
                }
            },
            // 获取远端版本
            getRemoteVersion: async () => {
                const version = await fetchRemoteVersion()
                console.log("🌐 远端版本:", version)
                return version
            },
            // 测试清除缓存
            testClearCache: async () => {
                console.log("🧹 测试清除缓存...")
                await clearAllCaches()
                console.log("✅ 缓存清除完成")
            },
            // 间隔 ID
            intervalId
        }

        console.log("💡 调试工具已加载,可在控制台使用 window.__versionCheckUtils__")
        console.log("   - checkNow()        手动检查版本")
        console.log("   - resetVersion()    清空版本记录")
        console.log("   - getVersionInfo()  查看版本信息")
        console.log("   - getRemoteVersion()获取远端版本")
        console.log("   - testClearCache()  测试清除缓存")
    }
}

版本更新与缓存清除


🎯 核心功能

1. 自动版本检测

  • 应用启动时自动检查 /version.json
  • 每5分钟定时检查一次
  • 比对构建时间(buildTime)检测新版本

2. 缓存清除

  • Service Worker 缓存
  • localStorage / sessionStorage
  • IndexedDB 数据库
  • 所有登录相关数据

3. 强制重新登录

  • 检测到新版本时显示”发现新版本”提示
  • 点击”重新登录”清除缓存并跳转到登录页
  • 用户重新登录后获取最新版本代码

📁 文件结构

src/
├── utils/
│   └── version.ts                    # 核心版本检查工具
├── hooks/
│   └── update/
│       └── useVersionCheck.ts        # 版本检查 Hook
├── stores/
│   └── userInfo.ts                   # 已集成 logout 方法(清除缓存)
└── App.tsx                           # 已集成 useVersionCheck Hook

public/
└── version.json                      # 版本信息文件(自动生成)

scripts/
├── update-version.js                 # 自动更新版本脚本
├── update-version.sh                 # Linux/Mac 版本脚本
└── update-version.bat                # Windows 版本脚本

package.json                          # build 脚本

🚀 使用方式

1. 开发环境

pnpm start

2. 构建项目

# 自动更新版本并构建
pnpm build

# 测试云构建
pnpm build:testcloud

# 生产云构建
pnpm build:prodcloud

3. 手动更新版本(可选)

pnpm update-version

🔄 工作流程

1. 执行 pnpm build
   ↓
2. 运行 update-version.js 脚本
   ↓
3. 读取 package.json 中的版本号
   ↓
4. 生成当前构建时间(ISO 8601)
   ↓
5. 生成/更新 public/version.json
   ↓
6. Vite 构建应用
   ↓
7. 部署到服务器
   ↓
8. 用户访问应用
   ↓
9. useVersionCheck Hook 启动
   ↓
10. 定时检查 /version.json
    ↓
11. 版本不同 → 显示更新提示 → 清除缓存 → 重新登录

🛠️ 部署配置

Nginx 配置(重要)

必须禁用 version.json 的缓存,否则更新检测无法生效:

# 版本检查文件 - 禁用缓存
location /version.json {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
    add_header Pragma "no-cache";
    add_header Expires "0";
}

# 其他资源 - 长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

# HTML 文件
location ~* \.html$ {
    add_header Cache-Control "no-cache, no-store, must-revalidate";
}

📊 版本信息文件格式

public/version.json 内容示例:

{
    "version": "1.4.3",
    "buildTime": "2026-01-27T10:30:00Z"
}

自动生成说明

  • version:从 package.json 中读取
  • buildTime:构建时的当前时间(自动生成)

🧪 测试版本更新

本地测试步骤

  1. 修改版本号

    # 编辑 package.json,改变 version 字段
    # 例如:1.4.3 -> 1.4.4
    
  2. 重新构建

    pnpm build
    pnpm preview
    
  3. 验证功能

    • 打开应用
    • 打开浏览器开发者工具
    • 编辑 public/version.json,改变 buildTime
    • 刷新页面
    • 应该会弹出”发现新版本”提示

🔍 调试

查看版本信息

在浏览器控制台执行:

// 获取远端版本
fetch("/version.json")
    .then(r => r.json())
    .then(data => console.log("远端版本:", data))

// 检查本地版本记录
console.log("本地版本:", localStorage.getItem("APP_VERSION"))

查看缓存

// Service Worker 缓存
caches.keys().then(names => console.log("缓存列表:", names))

// localStorage
console.log("localStorage:", localStorage)

// sessionStorage
console.log("sessionStorage:", sessionStorage)

手动清除缓存

import { clearAllCaches, clearLoginState } from "@/utils/version"

// 清除所有缓存
await clearAllCaches()
console.log("缓存已清除")

// 清除登录状态
await clearLoginState()
console.log("登录状态已清除")

⚙️ 配置调整

修改检查间隔

编辑 src/utils/version.ts:

export const VERSION_CHECK_CONFIG = {
    // 改为 10 分钟检查一次
    CHECK_INTERVAL: 10 * 60 * 1000
    // ...其他配置
}

禁用版本检查

在 src/App.tsx 中注释掉:

// useVersionCheck() // 注释掉这行

自定义更新提示

编辑 src/utils/version.ts 中的 handleAppUpdate() 函数


📚 更多资源

详细部署指南请查看 DEPLOYMENT_GUIDE.md


💡 关键要点

✅ 一定要做的事:

  1. 在 Nginx/Apache 中禁用 version.json 的缓存
  2. 确保 Service Worker 正常注册
  3. 每次部署前运行 pnpm build(会自动更新版本)

❌ 不要做的事:

  1. 手动修改 public/version.json(会被覆盖)
  2. 启用 version.json 的缓存
  3. 在部署前忘记构建项目

📞 常见问题

Q: 用户更新后仍看到旧版本?
A: 检查 Nginx 缓存配置,确保禁用了 version.json 的缓存

Q: 版本文件未生成?
A: 检查是否有写入 public 目录的权限

Q: 如何强制所有用户更新?
A: 修改 version.json 中的 buildTime 字段

阅读全文

word文档转html(mammoth )

前端随笔 2025/11/10

前言

最近老有需要把协议Word转化为html纯预览展示的需求,为了后面方便摸鱼,用工具包脚本直接转化(手动狗头),主要使用的是 mammoth 。

Word 转 HTML 工具

使用 mammoth 将 Word 文档转换为 HTML,特别适用于协议文档的在线预览。

安装依赖

bash

npm install mammoth

完整代码

javascript

const mammoth = require('mammoth')
const fs = require('fs')
const path = require('path')

function getFileConfig() {
  const inputDir = path.resolve(__dirname, 'input');
  const files = fs.readdirSync(inputDir).filter(f => f.endsWith('.docx'));
  return files.map(f => {
    return {
      inputPath: path.join(__dirname, 'input',f),
      outputPath: path.join(__dirname, 'output', f.replace('.docx', '.html'))
    }
  });
}

function transformParagraph(element) {
  let styleName = "";
  if (element.alignment === "center") {
    styleName = "text-center";
  } else if (element.alignment === "right") {
    styleName = "text-right";
  }
  
  return {
      ...element,
      styleName
  };
}

const options = {
  styleMap: [
    "p[style-name='text-center'] => p.text-center:fresh",
    "p[style-name='text-right'] => p.text-right:fresh",
     "u => span.text-underline:fresh"
  ],
  transformDocument: mammoth.transforms.paragraph(transformParagraph)
}

const convertToHtml = (config) => {
  const { inputPath, outputPath } = config;
  return mammoth.convertToHtml({ path: inputPath }, options).then(function(result){
    const html = result.value
    const templatePath = path.join(__dirname, 'template.html')
    const tempHtml = fs.readFileSync(templatePath, 'utf8')
    const insertTag = '<div id="app">'
    const insertIndex = tempHtml.lastIndexOf(insertTag) + insertTag.length
    const resHtml = tempHtml.slice(0, insertIndex) + html + tempHtml.slice(insertIndex)
    fs.writeFileSync(outputPath, resHtml, 'utf8')
  }).catch(function(err){
    console.log(err);
  });
}

const allFileConfig = getFileConfig();
const allFileConfigLength = allFileConfig.length;
// 清空 output 目录
if (fs.existsSync(path.join(__dirname, 'output'))) {
  fs.rmdirSync(path.join(__dirname, 'output'), { recursive: true, force: true });
}
fs.mkdirSync(path.join(__dirname, 'output'));

let index = 0;
console.log(`开始转换,共 ${allFileConfigLength} 个文件`);
function runNext() {
  if (index >= allFileConfigLength) {
    console.log('文件转换完成,请查看 output 目录');
    return;
  }
  
  const config = allFileConfig[index];
  console.log(`${index + 1}: ${path.basename(config.inputPath)}`);
  
  convertToHtml(config).then(() => {
    index++;
    runNext();
  }).catch(err => {
    console.error(`Error processing file ${path.basename(config.inputPath)}:`, err);
    index++;
    runNext();
  });
}

runNext();



目录结构

text

word-to-html/
├── input/                 # 存放要转换的 .docx 文件
│   ├── 协议1.docx
│   └── 协议2.docx
├── output/                # 转换后的 HTML 文件输出目录
├── template.html          # HTML 模板文件   
├──index.js

在package.json的scripts中添加命令行脚本

  "convert-doc": "node ./bin/word-to-html/index.js",

使用方法

  1. 将 Word 文档 (.docx) 放入 input 目录
  2. 运行脚本:
  3. 查看 output 目录获取转换后的 HTML 文件

模板文件示例 (template.html)


<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>协议文档预览</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            line-height: 1.8;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f5;
        }
        #app {
            background: white;
            padding: 40px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            border-radius: 8px;
        }
        .text-center { text-align: center; }
        .text-right { text-align: right; }
        .text-underline { text-decoration: underline; }
        table {
            border-collapse: collapse;
            width: 100%;
            margin: 15px 0;
        }
        table, th, td {
            border: 1px solid #ddd;
        }
        th, td {
            padding: 12px;
            text-align: left;
        }
        th {
            background-color: #f8f9fa;
        }
    </style>
</head>
<body>
    <div id="app"></div>
</body>
</html>

注意事项

  • 确保 Word 文档为 .docx 格式
  • 复杂表格样式可能需要额外调整
  • 脚本会自动覆盖 output 目录中的现有文件
  • 建议在转换前备份重要的 Word 文档
阅读全文

生成tag号的脚本

脚本 2025/10/13

平时改完bug重新部署测试环境,总要重新打tag,虽然也就几行git命令的事,但能用一句命令解决还是舒服的

在scripts新建文件 generateTag.js

/**
 * 基于当前分支生成 tag 号,并推送至远程
 */
const { execSync } = require('child_process');
const readline = require('readline');

// 生成当前时间的 tag,格式为 YYYYMMDDHHmmss
function getCurrentTag() {
    const now = new Date();
    const pad = n => n < 10 ? '0' + n : n;
    return [
        now.getFullYear(),
        pad(now.getMonth() + 1),
        pad(now.getDate()),
        pad(now.getHours()),
        pad(now.getMinutes()),
        pad(now.getSeconds())
    ].join('');
}


try {
    // 获取当前分支名
    const branch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
    const rl = readline.createInterface({
        input: process.stdin,
        output: process.stdout
    });

    rl.question(`当前分支为 "${branch}",确定要在该分支上打 tag 并推送吗?(y/N): `, (answer) => {
        if (answer.toLowerCase() !== 'y') {
            console.log('操作已取消。');
            rl.close();
            process.exit(0);
        }
        try {
            const tag = getCurrentTag();
            execSync(`git tag ${tag}`, { stdio: 'inherit' });
            execSync(`git push origin ${tag}`, { stdio: 'inherit' });
            console.log(`Tag ${tag} 已创建并推送到远端`);
        } catch (err) {
            console.error('创建或推送 tag 失败:', err.message);
            process.exit(1);
        }
        rl.close();
        process.exit(0);
    });
    return;
} catch (err) {
    console.error('创建或推送 tag 失败:', err.message);
    process.exit(1);
}

在package.json中的’scripts’添加

  "tag": "node ./scripts/generateTag"

使用

npm run tag
阅读全文

WEB前端基础知识梳理(四)

前端随笔 2025/8/15

手写简单的节流和防抖

// 节流 n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
function throttle(fn,delay=1000) {
    let timer = null
    return function() {
        const args = arguments
        const contentText = this
        if(!timer) {
            setTimeout(()=>{
                fn.apply(contentText,args)
                timer = null
            },delay)
        }
    }
}
//防抖 n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
function debounce(fn,delay=1000) {
    let timeout;
     return function() {
        const args = arguments
        const contentText = this
        clearTimeout(timeout)
        timeout=setTimeout(()=>{
                fn.apply(contentText,args)
            },delay)
    }
}

手写一个数据扁平化

function flattenArray(arr) {
let result= []
    arr.forEach(item =>{
        if(Array.isArray(item)) {
            result= result.concat(flattenArray(item))
        } else {
            result.push(item)
        }
    })
    return reslut
}

手写一个简单的发布-订阅者的模式

// 创建一个简单的发布-订阅类
class EventEmitter {
  constructor() {
    // 使用对象来存储所有的事件和对应的订阅者
    this.events = {};
  }

  // 订阅方法
  on(eventName, callback) {
    // 如果这个事件还没有被订阅过,就创建一个数组
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 将回调函数添加到对应事件的数组中
    this.events[eventName].push(callback);
  }

  // 发布方法
  emit(eventName, ...args) {
    // 如果这个事件有订阅者
    if (this.events[eventName]) {
      // 遍历并执行所有订阅者的回调函数
      this.events[eventName].forEach(callback => {
        callback(...args);
      });
    }
  }

  // 取消订阅方法
  off(eventName, callback) {
    // 如果这个事件存在
    if (this.events[eventName]) {
      // 过滤掉要取消的回调函数
      this.events[eventName] = this.events[eventName].filter(
        cb => cb !== callback
      );
    }
  }

  // 只订阅一次的方法
  once(eventName, callback) {
    // 包装回调函数,执行后自动取消订阅
    const wrapper = (...args) => {
      callback(...args);
      this.off(eventName, wrapper);
    };
    this.on(eventName, wrapper);
  }
}

// 使用示例
// 1. 创建发布者实例
const emitter = new EventEmitter();

// 2. 定义订阅者(回调函数)
const handleNews = (news) => {
  console.log('收到新闻:', news);
};

const handleWeather = (weather) => {
  console.log('收到天气:', weather);
};

// 3. 订阅事件
emitter.on('news', handleNews);
emitter.on('weather', handleWeather);

// 4. 发布事件
emitter.emit('news', '今天是晴天'); // 输出:收到新闻:今天是晴天
emitter.emit('weather', '温度25度'); // 输出:收到天气:温度25度

// 5. 使用once订阅
emitter.once('special', (data) => {
  console.log('只执行一次:', data);
});

emitter.emit('special', '特殊消息'); // 输出:只执行一次:特殊消息
emitter.emit('special', '这条不会输出'); // 不会执行

// 6. 取消订阅
emitter.off('news', handleNews);
emitter.emit('news', '这条不会输出'); // 不会执行,因为已经取消订阅

手写深拷贝 (原对象:obj)

//通过json序列化 (函数、undefined、Symbol 深拷贝不了)
const copyObj = JSON.parse(JSON.stringify(obj))
// 通过递归
function deepClone(obj) {
  // 处理基本类型和null
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 处理日期
  if (obj instanceof Date) {
    return new Date(obj);
  }

  // 处理正则
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }

  // 处理数组
  if (Array.isArray(obj)) {
    return obj.map(item => deepClone(item));
  }

  // 处理普通对象
  const result = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      result[key] = deepClone(obj[key]);
    }
  }

  return result;
}

// 测试用例
// 1. 基本类型
console.log(deepClone(123));        // 123
console.log(deepClone('hello'));    // 'hello'
console.log(deepClone(true));       // true
console.log(deepClone(null));       // null
console.log(deepClone(undefined));  // undefined

// 2. 对象
const obj = {
  a: 1,
  b: 'string',
  c: true,
  d: null,
  e: undefined,
  f: {
    g: 2
  },
  h: [1, 2, 3]
};
const clonedObj = deepClone(obj);
console.log(clonedObj);

// 3. 验证是深拷贝
clonedObj.f.g = 3;
console.log(obj.f.g);      // 2 (原对象未改变)
console.log(clonedObj.f.g); // 3 (克隆对象已改变)

// 4. 日期和正则
const dateObj = {
  date: new Date(),
  reg: /test/g
};
const clonedDateObj = deepClone(dateObj);
console.log(clonedDateObj.date instanceof Date); // true
console.log(clonedDateObj.reg instanceof RegExp); // true

// 5. 嵌套数组
const nestedArray = [1, [2, [3]]];
const clonedArray = deepClone(nestedArray);
clonedArray[1][1][0] = 4;
console.log(nestedArray[1][1][0]); // 3 (原数组未改变)
console.log(clonedArray[1][1][0]); // 4 (克隆数组已改变)

手写一个AJAX请求示例

// 创建一个新的XMLHttpRequest对象
var xhr = new XMLHttpRequest对象()
//配置请求方法和地址
xhr.open('GET','请求地址',true)
//设置请求头
xhr.setRequestHeader('Content-type','application/json')
xhr.onreadystatechange = function() {
    if(xhr.readyState === 4) {
        if(xhr.status == 200) {
            var resposeData = JSON.parse(xhr.responseText)
        } else {
            console.error('请求失败')
        }
    }
}
// 发送请求
xhr.send()

axios和fetch

axios是一个基于XMLHttpResquest进行二次封装的http客户端,提供了更多高级功能,如自动解析 JSON 数
据、请求/响应拦截器等。

fetch API 是现代浏览器内置的标准 API,用于发起 HTTP 请求。它基于 Promise,语法简洁,但需要手动解析响应数据,例如  .json()  方法。

对比:

  • 错误处理 Fetch 仅在网络错误时会触发 catch,即使返回 404 或 500 状态码,仍需手动检查 response.ok。 Axios 会自动在非 2xx 状态码时触发 catch,并提供详细的错误信息。

  • 响应数据解析 Fetch 需要手动调用  .json()  方法解析响应数据。 Axios 自动解析响应为 JSON 格式,简化了操作。

  • 请求拦截与响应拦截 Fetch 不支持内置拦截器,需要手动实现。 Axios 提供内置的请求和响应拦截器,便于添加请求头或处理错误。

  • 浏览器兼容性 Fetch 在旧版浏览器(如 IE)中不支持,需要使用 polyfill。 Axios 支持所有主流浏览器,包括旧版浏览器。

  • 取消请求 Fetch 使用 AbortController 实现取消请求。 Axios 提供 CancelToken,实现更简洁的请求取消功能。

首页白屏优化

  • 1.三方库,需要的组件 按需加载
  • 2.第三方库使用CDN引入
  • 3.vue-router路由懒加载
  • 4.静态资源压缩,代码压缩,图片压缩
  • 5.不要滥用三方库
  • 6.去掉编译中的map文件
  • 7.代码层面优化(1.项目组件化,去掉冗余的代码 2.正式环境去掉console.log 3.index.html页面中将js文件放到页面最底部,css文件放在<header>中使用link引入)

    对称加解密和非对称加解密

    对称加密和非对称加密是两种主要的加密技术,它们在密钥管理和使用场景上有显著的区别。

对称加密

对称加密使用相同的密钥进行加密和解密。这种加密方法的优点是加密和解密速度快,适合大量数据的加密。然而,它也存在一些挑战,主要是密钥的分发和管理。

优点:

  • 加密和解密速度快。
  • 适合大量数据的加密。

缺点:

  • 密钥分发困难:如何安全地将密钥发送给接收方是一个挑战。
  • 密钥管理复杂:如果密钥被泄露,整个系统将变得不安全。

常见算法:

  • AES(高级加密标准)
  • DES(数据加密标准)
  • 3DES(三重数据加密标准)
  • RC4

非对称加密

非对称加密使用一对密钥:公钥和私钥。公钥用于加密数据,而私钥用于解密数据。这种加密方法的优点是密钥分发更容易,因为公钥可以公开分享,而私钥保持私密。

优点:

  • 密钥分发容易:公钥可以公开分享,私钥保持私密。
  • 数字签名:非对称加密可以用于数字签名,确保数据的完整性和真实性。

缺点:

  • 加密和解密速度较慢,不适合大量数据的加密。
  • 计算复杂度高。

常见算法:

  • RSA
  • ECC(椭圆曲线加密)
  • ElGamal
  • Diffie-Hellman

使用场景

  • 对称加密:通常用于需要大量数据加密的场景,如文件加密、数据库加密等。在实际应用中,对称加密通常与非对称加密结合使用,即使用非对称加密交换对称密钥,然后使用对称密钥加密数据。
  • 非对称加密:通常用于密钥交换、数字签名和身份验证等场景。例如,HTTPS协议使用非对称加密交换对称密钥,然后使用对称加密传输数据。

结合使用

在实际应用中,对称加密和非对称加密通常会结合使用。例如,在HTTPS协议中,客户端首先使用服务器的公钥加密对称密钥,然后服务器使用私钥解密对称密钥。之后,客户端和服务器使用对称密钥加密和解密数据。

这种结合使用的方式充分利用了两种加密方法的优点,既保证了数据传输的安全性,又提高了数据传输的效率。

微信小程序的生命周期、vue的生命周期

微信小程序的生命周期:

小程序的应用生命周期:onLaunch、onShow、onHide、onError、onPageNotFound

小程序页面的生命周期(常用的):onLoad、onShow、onReady、onUnload

vue实例的生命周期:beforeCreate、Created、beforeMount、mounted、beforeUpdate、updated、beforeDestory、destroyed

hash路由 和 history路由

在单页面应用(SPA)中,路由是用来控制页面导航和组件展示的机制。Hash 路由和 History 路由是两种常见的路由模式,它们决定了 URL 的表现形式和页面导航的方式。

Hash 路由

Hash 路由,有时也称为哈希路由,使用 URL 中的哈希部分(即 # 符号后面的字符串)来模拟一个完整的 URL 路径。例如,http://example.com/#/about。在这种模式下,当用户点击应用内的链接或浏览器的前进/后退按钮时,URL 中的哈希部分会改变,但浏览器不会向服务器发送新的请求。

哈希路由的主要优点是兼容性好,因为哈希变化不会触发浏览器重新加载页面,也不需要服务器端进行特殊配置。缺点是 URL 中包含 # 符号,可能看起来不够美观,并且对搜索引擎优化(SEO)不太友好。

History 路由

History 路由利用了 HTML5 History API 中的 pushState 和 replaceState 方法来管理浏览器历史记录。在这种模式下,URL 看起来像正常的路径,例如 http://example.com/about,没有 # 符号。

使用 History 路由时,当用户导航到新页面,URL 会改变,但浏览器不会重新加载页面。这需要服务器端配置支持,因为对于任何不存在的页面路径,服务器都应该返回应用的入口文件(通常是 index.html),这样 Vue Router 才能正确地解析路由并渲染对应的组件。

History 路由的优点是 URL 表现形式更美观,更接近传统的网址结构,对 SEO 更友好。缺点是需要nginx配置,并且可能存在兼容性问题,特别是在旧版浏览器中。

在 Vue.js 应用中,你可以通过 Vue Router 的 mode 选项来选择使用 hash 模式还是 history 模式:

const router = new VueRouter({
  mode: 'history', // 'hash' 或 'history'
  routes: [...]
});

默认情况下,Vue Router 使用 hash 模式。如果你选择 history 模式,请确保服务器已经正确配置,以便所有路由都能正确地返回应用的入口文件。

vue项目目录结构、小程序目录结构

vue项目目录:
核心目录和文件

  • node_modules/ : 存放项目依赖的node模块。
  • public/ : 公共资源目录,通常包含favicon.ico和index.html等。
  • src/ : 源代码目录,是开发的核心所在。它包括: assets/: 静态资源,如图片、字体等。
    • components/: Vue组件目录。
    • router/: 路由配置目录。
    • store/: Vuex状态管理目录。
    • views/: 视图组件目录。
  • App.vue: Vue根组件,整个应用的入口。
  • main.js: Vue实例化入口文件,整个应用的核心。

配置文件和说明文档

  • .gitignore: Git版本控制忽略配置。
  • package.json: 项目配置文件,定义了项目的依赖和脚本。
  • README.md: 项目的说明文档,通常用于描述项目信息。
  • webpack.config.js/vite.config.js: Webpack/vite的配置文件

小程序目录:
核心目录和文件

  • app.js:小程序的逻辑代码,可以用来注册小程序、处理全局事件等
  • app.json:小程序的公共配置文件,包括页面路径、窗口样式、网络超时时间等
  • app.wxss:小程序的公共样式表,可以定义全局样式
  • **pages/**:存放小程序页面的文件夹。每个页面由4个文件组成:.js、.wxss、.json、.wxml
    • images/ : 存放小程序使用的图片资源。
    • components/ : 存放小程序的组件文件夹,组件也可以由四个文件组成,但它们是独立的小模块,可以被多个页面使用。
    • utils/ : 存放工具脚本,如请求封装、格式化函数等。
    • static/ : 存放静态资源,如字体文件、配置文件等。
阅读全文
avatar
耀耀切克闹

欢迎来到耀耀切克闹的博客

不是因为是主角才成功,而是成功了才成为主角

想要的都拥有,得不到的都释怀