普通视图

发现新文章,点击刷新页面。
昨天 — 2025年4月11日掘金 前端

项目基础搭建时的一些基本注意点

2025年4月11日 23:28

一、大型项目搭建要点

  1. 架构设计与技术选型

    • 核心原则‌:强扩展性、可持续维护性、技术生态成熟度。

    • 典型方案‌:

      • 采用‌微前端架构‌(如qiankun),支持多团队并行开发与独立部署‌;
      • 技术栈选择‌React/Vue3+TypeScript‌,搭配‌Webpack/Vite‌构建工具,确保长期生态支持‌;
      • 集成‌SSR(服务端渲染) ‌提升SEO和首屏性能,如Next.js/Nuxt.js‌。
    • 案例‌:某金融平台项目采用微前端+React+TS架构,支持5年内新增10+业务模块,代码复用率达60%‌。

  2. 开发流程与规范

    • 代码质量管控‌:

      • 强制‌ESLint/Prettier‌规范,结合‌Git Hooks‌拦截不规范提交‌;
      • 建立‌组件库‌(如Storybook),统一UI交互逻辑,降低重复开发成本‌。
    • 分支管理‌:

      • 采用‌GitFlow‌模式,定义feature(功能分支)、release(预发布分支)等流程‌。
  3. 长期维护策略

    • 模块化拆分‌:按业务域划分模块(如支付、风控),通过‌Monorepo‌管理依赖‌;
    • 自动化运维‌:搭建‌CI/CD流水线‌(如Jenkins),支持灰度发布和回滚机制‌。

二、中短期项目(7-8个月周期)搭建要点

  1. 敏捷开发与快速交付

    • 技术选型‌:

      • 选择‌轻量框架‌(如Vite+React),减少配置成本‌;
      • 集成‌低代码平台‌(如amis),实现表单/列表页快速生成‌。
    • 案例‌:某政务H5项目采用Vite+预置模板,2周完成核心功能开发,节省30%工时‌。

  2. 需求管理与风险控制

    • 需求冻结机制‌:在开发中期设置‌需求评审截止点‌,避免范围蔓延‌;
    • 动态资源配置‌:使用‌甘特图工具‌(如Jira)可视化进度,及时调整人力分配‌。
  3. 代码复用与灵活扩展

    • 脚手架定制‌:预置‌axios封装、权限模块、错误监控‌等基础能力‌;
    • 动态配置化‌:通过JSON驱动页面布局(如动态表单生成器),支持快速迭代‌。

三、通用核心注意点

  1. 团队协作优化

    • 角色分工‌:明确前端开发、UI协作、测试验收流程(如Figma标注自动生成代码)‌;
    • 知识沉淀‌:建立‌Confluence文档库‌,记录技术决策与踩坑记录‌。
  2. 性能与兼容性

    • 基线要求‌:

      • 兼容Chrome/Firefox/Edge最新3个版本‌;
      • 首屏加载时间≤1.5秒(3G网络)‌。
    • 监控体系‌:集成‌Sentry‌错误追踪+‌Lighthouse‌性能评分‌。

  3. 安全防护

    • XSS/CSRF防御‌:对用户输入内容强制过滤(如DOMPurify)‌;
    • 敏感操作验证‌:关键接口增加二次确认(如短信验证码)‌。

典型项目周期对比表

维度 大型项目(5-6年) 中短期项目(7-8个月)
技术选型 React/Vue3+TS+微前端 Vite+轻量框架+低代码
构建工具 Webpack(定制化配置) Vite(开箱即用)
代码规范 企业级ESLint规则+Monorepo 社区标准规则+快速模板
测试策略 单元测试+集成测试+E2E 核心路径E2E测试
部署频率 按月发布(灰度策略) 按周发布(敏捷迭代)

React+Vite+Typescript项目脚手架模版

作者 runnerdancer
2025年4月11日 23:06

React + TypeScript + Vite

React + TypeScript + Vite + React Router + React Redux项目脚手架模版。GitHub:react-vite-ts-template

  • 很好的集成了Code Lint周边工具,支持对TS,CSS,LESS等文件的自动格式化。
  • 支持PC 和 H5
  • 规范化的目录结构

建议项目的前端相关文档,比如架构设计文档,模块设计文档统一收敛到docs目录下。或者如果是外部文档链接,也可以贴到docs目录里面

一、环境

  • node: >=20.0.0

二、运行

  • npm install
  • npm run dev

三、Code Lint配置及介绍

在开始介绍Code Lint配置前,需要准备以下工作:

需要在vscode安装以下插件:

  • eslint
  • prettier
  • editorconfig
  • Stylelint

下图是Code Lint必备插件:

lint_01.png

code lint相关工具及其作用

  • eslint。负责规范并校验js代码的编写,本身不具备自动格式化功能
  • commit lint。负责校验commit msg是否符合规范
  • stylint。负责规范并校验css/scss/less 代码的编写。
  • prettier。保存代码自动格式化
  • editor config。确保大家编辑器的风格统一,比如空格啥的。确保编辑器安装了editorconfig插件,比如vscode需要安装EditorConfig
  • lint stage
  • husky

3.1 vscode 配置

vscode基础配置,比如保存代码自动格式化等基础配置,均需要在./vscode/settings.json文件修改,同时这份文件需要提交上去,确保团队基础配置统一。

3.2 eslint + prettier

需要确保vscode安装了eslint和prettier插件。

eslint负责规范并校验js代码的编写,本身不具备自动格式化功能。ctrl+s保存代码时,prettier会自动格式化代码,比如下图,eslint提示import语句的顺序不对,同时提示useEffect,useState没有使用到。当ctrl + s保存代码时,prettier会自动格式化这些代码。

eslint_01.gif

如果项目使用的ESLint可能会出现与 Prettier 的格式化规则冲突的情况。为了避免这种情况,可以使用 eslint-config-prettier 来禁用所有与 Prettier 冲突的 ESLint 规则

3.3 stylelint + prettier

需要确保vscode安装了Stylelint和prettier插件。

stylelint负责规范并校验css/scss/less 代码的编写,本身不具备自动格式化的功能呢。ctrl + s保存代码时,prettier会自动格式化代码,比如下图,stylelint提示height的位置不对,因此保存代码时,prettier会自动调整这些规则的顺序。

stylelint_01.gif

3.4 editorconfig

需要确保vscode安装了editorconfig for vscode插件,并在根目录下创建.editorconfig文件。

主要作用:

  • 确保团队成员在不同的编辑器中使用相同的缩进、换行符、字符编码等规则。
  • 避免因代码风格不一致导致的冲突或混乱

比如indent_size指定缩进的大小(2个空格或者4个空格)

比如下面是indent_size指定为2时,按tab键的空格数

indent_01.gif

比如下面是indent_size指定为4时,按tab键的空格数

indent_02.gif

3.5 lintstaged + husky

lint-staged 是一个工具,它允许你在 Git 提交前对暂存区(即通过 git add 的文件)运行预定义的代码检查任务,如 ESLint、Prettier 等。它的核心功能是只对暂存区的文件进行检查,避免对未修改的文件进行不必要的检查

为什么使用 lint-staged:

  • 高效的代码检查:只对暂存区的文件进行检查,避免了全量检查,提高了效率。
  • 提高代码质量:在提交前自动检查代码,减少低质量代码被提交的概率。
  • 集成流畅:与 husky 等工具结合,能够自动化工作流,减少手动操作

执行npm install后,默认会在项目根目录下新建.husky文件目录,需要在.husky下面新增两个文件,才能使得git commit时自动跑lint校验

  • .husky/commit-msg
  • .husky/pre-commit

husky.png

如下图所示,当代码中存在错误时,代码就没法提交上去

lint_stage.gif

因此,在开发的过程中,如果有eslint的告警,要及时修复,避免在提交的时候遇到麻烦。

3.6 commitlint + husky

commitlint 是一个用于校验 Git 提交消息是否符合规范的工具。它可以帮助团队维护一致的提交消息格式,从而提高项目的可维护性和协作效率

主要功能:

  • 格式检查:确保每条提交信息都遵循预定义的格式,例如使用特定的提交类型标签(如 fix、feat、docs 等)。
  • 一致性维护:通过强制执行一致的提交信息格式,减少不同开发者之间的差异。
  • 文档生成:基于提交信息生成项目变更日志,便于团队成员了解项目的历史变更。
  • 自动化测试:可以在持续集成/持续部署(CI/CD)流程中集成,确保每次提交都符合格式要求,从而提高代码质量

commit_msg.png

Flutter插件中引用aar

作者 JarvanMo
2025年4月11日 22:15

背景

最近需要制作一个基于aar的Flutter插件,但遇到了一些问题。 我们假设插件名称叫作my_plugin。以前我的解决方案是把aar文件导入my_plugin/android/libs中,然后在my_plugin/android/build.gradle做如下修改:

rootProject.allprojects {
    repositories {
        google()
        jcenter()
        flatDir {
            dirs project(':my_plugin').file('libs')
        }
    }
}

dependencies {
    implementation fileTree(include: ['*.aar'], dir: 'libs')
}

但我很快发现,这条路现在行不通:

Execution failed for task 'xxxx'.
> Direct local .aar file dependencies are not supported when building an AAR. 
The resulting AAR would be broken because the classes and Android resources from any local .aar 
file dependencies would not be packaged in the resulting AAR. Previous versions of the Android 
Gradle Plugin produce broken AARs in this case too (despite not throwing this error). The 
following direct local .aar file dependencies of the xxxx project caused this error: 
______.aar

很快我又换了一个姿势,同样使用flatDir,但这次我改用了compileOnly

rootProject.allprojects {
    repositories {
        google()
        mavenCentral()
        flatDir {
            dirs project(':my_plugin').file('libs')
        }
    }
}

dependencies {
    compileOnly fileTree(include: ['*.aar'], dir: 'libs')
}

但也引起了一个小小的麻烦,这种方式需要在宿主app中把aar文件再次导入到对应的libs文件下,然后又要在build.gradle中添加 implementation fileTree(include: ['*.aar'], dir: 'libs')。对于使用者来说不友好,而且flatDir这种形式官方其实是不太推荐的(你会在控制台看到一些警告)。所以怎么相对优雅地解决这个问题?最快的方法当然是发布到远程maven之类的仓库了。谁都能想得到,但很实际开发中并不具备发布到远程maven的条件。

既然远程maven不行,那本地的maven是不是就可以了?

姿势一

第一种方式是直接借用maven-publish插件了。在my_plugin/android/build.gradle写点代码:


//define this
String mavenLocalPath = project(":my_plugin").mkdir("m2repository").absolutePath


rootProject.allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url mavenLocalPath
        }
    }
}


//apply plugin
apply plugin: "maven-publish"


publishing {
  publications {
    release(MavenPublication) {
            groupId = 'com.my-company'
            artifactId = 'my-library'
            version = '1.0'
            artifact "path/to/aar"
    }
  }
  repositories {
    maven {
      name = 'myrepo'
      url = mavenLocalPath
    }
  }
}

dependencies {
    implementation "com.my-company:my-library:1.0"
}

然后使用gradle运行publish任务即可:


./gradlew publish

//or 
gradle publish

然后你会发现在my_plugin/m2repository目录下生成了相关文件,大功告成!当有新版本时,我们只需再次publish即可。

姿势二

第一种方是借用了maven-publish插件,当然我们也可以手搓一个类似功能的,假设我们把aar放到了my_plugin/android/libs下:

import java.security.MessageDigest
import java.security.NoSuchAlgorithmException

//define this
String mavenLocalPath = project(":my_plugin").mkdir("m2repository").absolutePath


rootProject.allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
            url mavenLocalPath
        }
    }
}

task useAar {
    File file = project.file("libs")
    if (file.exists() && file.isDirectory()) {
        file.listFiles(new FileFilter() {
            @Override
            boolean accept(File pathname) {
                return pathname.name.endsWith(".aar")
            }
        }).each { item ->
            String aarName = item.name.substring(0, item.name.length() - 4)
            String[] aarInfo = aarName.split("-")
            String sha1 = getFileSha1(item)
            String md5 = getFileMD5(item)
            String fromStr = item.path
            String intoStr = aarPath + "/" + aarInfo[0].replace(".", "/") + "/" + aarInfo[1] + "/" + aarInfo[2]
            String newName = aarInfo[1] + "-" + aarInfo[2] + ".aar"

            project.copy {
                from fromStr
                into intoStr
                rename(item.name, newName)
            }

            project.file(intoStr + "/" + newName + ".md5").write(md5)
            project.file(intoStr + "/" + newName + ".sha1").write(sha1)

            String pomPath = intoStr + "/" + newName.substring(0, newName.length() - 4) + ".pom"
            project.file(pomPath).write(createPomStr(aarInfo[0], aarInfo[1], aarInfo[2]))
            project.file(pomPath + ".md5").write(getFileMD5(project.file(pomPath)))
            project.file(pomPath + ".sha1").write(getFileSha1(project.file(pomPath)))

            String metadataPath = project.file(intoStr).getParentFile().path + "/maven-metadata.xml"
            project.file(metadataPath).write(createMetadataStr(aarInfo[0], aarInfo[1], aarInfo[2]))
            project.file(metadataPath + ".md5").write(getFileMD5(project.file(metadataPath)))
            project.file(metadataPath + ".sha1").write(getFileSha1(project.file(metadataPath)))
            dependencies {
                implementation "${aarInfo[0]}:${aarInfo[1]}:${aarInfo[2]}"
            }
        }
    }
}

public static String createMetadataStr(String groupId, String artifactId, String version) {
    return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
            "<metadata>\n" +
            "  <groupId>$groupId</groupId>\n" +
            "  <artifactId>$artifactId</artifactId>\n" +
            "  <versioning>\n" +
            "    <release>$version</release>\n" +
            "    <versions>\n" +
            "      <version>$version</version>\n" +
            "    </versions>\n" +
            "    <lastUpdated>${new Date().format('yyyyMMdd')}000000</lastUpdated>\n" +
            "  </versioning>\n" +
            "</metadata>\n"
}

public static String createPomStr(String groupId, String artifactId, String version) {
    return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
            "<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\" xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
            "    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
            "  <modelVersion>4.0.0</modelVersion>\n" +
            "  <groupId>$groupId</groupId>\n" +
            "  <artifactId>$artifactId</artifactId>\n" +
            "  <version>$version</version>\n" +
            "  <packaging>aar</packaging>\n" +
            "</project>\n"
}

public static String getFileSha1(File file) {
    FileInputStream input = null;
    try {
        input = new FileInputStream(file);
        MessageDigest digest = MessageDigest.getInstance("SHA-1");
        byte[] buffer = new byte[1024 * 1024 * 10];

        int len = 0;
        while ((len = input.read(buffer)) > 0) {
            digest.update(buffer, 0, len);
        }
        String sha1 = new BigInteger(1, digest.digest()).toString(16);
        int length = 40 - sha1.length();
        if (length > 0) {
            for (int i = 0; i < length; i++) {
                sha1 = "0" + sha1;
            }
        }
        return sha1;
    }
    catch (IOException e) {
        System.out.println(e);
    }
    catch (NoSuchAlgorithmException e) {
        System.out.println(e);
    }
    finally {
        try {
            if (input != null) {
                input.close();
            }
        }
        catch (IOException e) {
            System.out.println(e);
        }
    }
}

public static String getFileMD5(File file) {
    FileInputStream input = null;
    try {
        input = new FileInputStream(file);
        MessageDigest digest = MessageDigest.getInstance("MD5");
        byte[] buffer = new byte[1024 * 1024 * 10];

        int len = 0;
        while ((len = input.read(buffer)) > 0) {
            digest.update(buffer, 0, len);
        }
        String md5 = new BigInteger(1, digest.digest()).toString(16);
        int length = 32 - md5.length();
        if (length > 0) {
            for (int i = 0; i < length; i++) {
                md5 = "0" + md5;
            }
        }
        return md5;
    }
    catch (IOException e) {
        System.out.println(e);
    }
    catch (NoSuchAlgorithmException e) {
        System.out.println(e);
    }
    finally {
        try {
            if (input != null) {
                input.close();
            }
        }
        catch (IOException e) {
            System.out.println(e);
        }
    }
}

结束语

当然方式可能有很多,最后我也只用了第一种方式,如果大家有更好的招,欢迎指出~

vite作者如何评价turbopack

作者 CharlesYu01
2025年4月11日 21:34

Turbopack 是 Vercel 推出的一款高性能前端打包工具,旨在显著提升前端开发过程中服务器启动和模块热更新(HMR)的速度。

核心特性

  • 基于 Rust 构建:利用 Rust 语言出色的性能和内存管理能力,充分发挥多核 CPU 优势,能高效处理大量文件和复杂模块,为大型项目提供快速构建和热更新能力。同理 Vite 借助了一些用 Rust 编写的工具和库来提升性能
  • 智能增量更新:具备智能缓存机制,可精准识别项目中发生变化的模块,仅对修改部分重新打包,避免重复工作,极大提高构建效率。
  • 多框架兼容:与众多主流前端框架良好集成,如 Next.js、React、Vue 等,为不同技术栈项目提供统一高效的打包解决方案。

优势体现

  • 开发效率提升:显著缩短开发服务器启动时间和模块热更新时间,让开发者能更快看到代码修改效果,专注于编码工作。
  • 适配 Next.js:作为 Next.js 官方推荐的打包工具,与 Next.js 深度集成,能更好支持 Next.js 的服务器端渲染(SSR)、静态站点生成(SSG)等特性。

局限性

  • 社区资源不足:作为较新的工具,相比 Webpack、Vite 等成熟工具,社区资源和文档不够丰富,开发者遇到问题时解决难度较大。
  • 特定场景表现一般:在某些特定场景下,其性能优势不明显甚至不如其他工具。

应用场景

  • 大型项目:对于文件和模块众多的大型前端项目,Turbopack 的高性能可充分发挥,有效提升开发效率。
  • Next.js 项目:天然适配 Next.js 生态,能为使用 Next.js 进行开发的项目提供高效打包支持。

总体而言,Turbopack 凭借其高性能和多框架兼容性,为前端开发带来了新的选择,尤其在大型项目和 Next.js 项目中具有显著优势。不过,由于其社区尚在发展中,在使用时可能会面临一些挑战。


尤雨溪作为 Vite 的核心开发者,对 Turbopack 的评价主要围绕性能基准测试技术实现细节展开,既有对其技术创新的认可,也指出了宣传与实际场景的差异。以下是具体分析:

一、对 Turbopack 性能的客观验证

尤雨溪曾通过实际测试直接对比 Vite 与 Turbopack 的 HMR(模块热更新)性能。他创建了一个名为 vite-vs-next-turbo-hmr 的测试项目,模拟包含 1000 个组件的复杂应用,并在 M1 MacBook Pro 上进行测试。结果显示:

  • 客户端模式:Turbopack 的 HMR 速度确实优于 Vite(根组件更新平均 334.6ms vs. Vite 的 606ms,子组件 84.4ms vs. 171.8ms)。
  • 服务器组件(RSC)模式:Vite 的表现反而更优(根组件 836.4ms vs. Turbopack 的 782ms)。

这表明 Turbopack 在特定场景下(如客户端渲染的大型项目)确实有显著优势,但并非普遍适用。

二、对 Vercel 宣传的质疑与争议

尤雨溪对 Turbopack 最初宣称的“比 Vite 快 10 倍”提出了方法论层面的批评

  1. 测试条件差异:Vercel 的基准测试中,Vite 使用默认的 Babel 进行 React 转换,而 Turbopack 依赖更快的 SWC(Rust 实现)。若 Vite 切换为 SWC,性能差距会大幅缩小。
  2. 四舍五入问题:Vercel 将 Turbopack 的 15ms 四舍五入为 0.01s,而 Vite 的 87ms 显示为 0.09s,人为放大了差距。
  3. 场景局限性:Turbopack 的“10 倍优势”仅在模块数量超过 30k 时成立,这对绝大多数项目来说并不现实。

尤雨溪强调,用户更关心端到端的 HMR 体验(从保存文件到浏览器更新),而非单纯的“模块评估时间”。他认为 Vercel 的营销存在误导性,呼吁更透明的基准测试。

三、技术层面的交流与认可

尽管存在争议,尤雨溪与 Vercel 团队保持了技术对话

  • Bug 反馈:Turbopack 官方博客提到,尤雨溪帮助他们发现并纠正了基准测试中的错误(如四舍五入问题),并感谢其贡献。
  • 架构差异:尤雨溪指出,Turbopack 的增量计算引擎(基于 Rust)在处理超大型项目时确实有优势,但其设计更偏向“黑箱优化”,而 Vite 强调灵活性生态兼容性(如支持 Vue、React、Svelte 等多框架)。

四、对前端工具生态的态度

尤雨溪多次强调,不同工具适用于不同场景:

  • Turbopack 的定位:作为 Next.js 的官方打包工具,其性能优化与 Next.js 的架构(如 RSC)深度绑定,更适合 React 生态的大型项目。
  • Vite 的优势:Vite 以原生 ESM 为核心,注重开发体验的“开箱即用”和跨框架支持,尤其适合中小型项目和需要高度定制化的场景。

他认为,前端工具的竞争推动了技术进步,但开发者应根据项目需求选择工具,而非盲目追求“最快”。

总结

尤雨溪对 Turbopack 的评价呈现出理性平衡的态度:

  • 认可其技术创新:Turbopack 在 Rust 性能、增量计算等方面的突破值得肯定,尤其在 Next.js 生态中表现突出。
  • 批评过度营销:Vercel 的“10 倍快”宣传存在测试条件不透明、场景局限性等问题,可能误导用户。
  • 强调生态多样性:不同工具服务于不同需求,开发者应根据项目特点选择,而非陷入“工具之争”。

这一立场体现了尤雨溪作为开源项目维护者对技术中立性的坚持,以及对用户实际需求的关注。

前端视角 Java Web 入门手册 5.1:真实世界 Web 开发——初识 Spring Boot

作者 谦行
2025年4月11日 20:18

Spring Framework 是一个开源的、功能强大的 Java框架,旨在简化企业级 Java开发。它通过提供全面的基础设施支持,使开发者能够专注于业务逻辑的实现。Spring 框架由多个模块组成,如 Spring Core、Spring MVC、Spring Data、Spring Security等。开发者可以根据项目需求,选择性地引入和使用这些模块,构建灵活且可扩展的应用。

虽然 Spring 功能强大,但其配置和设置过程可能繁琐。Spring Boot 建立在 Spring Framework 之上,简化了 Spring 应用的配置和部署。它并不是另一个框架,而是一个增强Spring Framework的配置和启动工具。

Spring 和 SpringBoot 的关系有点类似于 Node.js 中 Koa 与 Egg.js 的关系,Spring 和 Koa 都是基础性的框架,提供了核心功能,但需要更多定制;Spring Boot 和 Egg.js 在各自的基础框架之上进行了增强,提供了更多的预配置和自动化工具,帮助开发者快速上手并提高效率。

使用 Spring Initializr 创建 SpringBoot 项目

Spring Initializr 是一个在线工具,用于生成Spring Boot项目的基础结构

点击 Generate 按钮,下载生成的项目压缩包并解压,即可得到如下的项目结构

hellospring
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com.example.hellospring
│   │   │       ├── HellospringApplication.java
│   │   │       └── controller
│   │   │           └── HelloController.java
│   │   └── resources
│   │       ├── application.properties
│   │       └── static
│   │           └── ...
│   └── test
│       └── java
│           └── com.example.hellospring
│               └── HellospringApplicationTests.java
├── mvnw
├── mvnw.cmd
├── pom.xml
└── README.md
  • src/main/java:Java 源代码目录
  • src/main/resources:资源文件目录,如配置文件、静态资源等
  • pom.xml:Maven 项目配置文件
  • mvnw & mvnw.cmd:Maven Wrapper 脚本,确保团队使用相同版本的 Maven

Controller

既然是 Web 应用请求自然先到 Controller,Controller 负责处理 HTTP 请求,它们接收请求,调用相应的服务,并返回响应

// src/main/java/com/example/hellospring/controller/HelloController.java
package com.example.hellospring.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello, Spring Boot!";
    }
}

@RestController 用于将类标记为 Spring MVC 控制器,并自动将控制器中的方法返回的对象序列化为 JSON 或 XML 格式的响应体,它是 @Controller 和 @ResponseBody 注解的组合

  • @Controller: 标记一个类作为控制器,处理 HTTP 请求
  • @ResponseBody: 指示控制器方法的返回值应该直接写入 HTTP 响应体,而不是返回一个视图

@GetMapping@RequestMapping(method = RequestMethod.GET) 的简写,用来处理 HTTP GET 请求。sayHello 方法使用 @GetMapping("/hello") 注解,这意味着该方法会处理 /hello 的 GET 请求

通过这些注解,Spring 提供了直观和简洁的方法来定义控制器的请求映射和响应处理,大大简化了开发 RESTful 服务的工作

构建一个简单的 REST API

为了更好地理解 Spring Boot 的工作机制,通过一个简单的示例项目,构建一个 RESTful API,实现用户的 CRUD 操作

创建文件

├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── example
    │   │           └── hellospring
    │   │               ├── HellospringApplication.java
    │   │               ├── controller
    │   │               │   └── UserController.java
    │   │               ├── model
    │   │               │   └── User.java
    │   │               └── repository
    │   │                   └── UserRepository.java
    │   └── resources
    │       ├── application.properties
    │       ├── static
    │       └── templates
    └── test

添加必要依赖

pom.xml添加依赖

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Data JPA -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- H2 Database -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
  • spring-boot-starter-web:用于快速启动和构建基于 Spring MVC 的 Web 应用程序。这个 Starter 包含了构建 Web 应用程序所需的常用依赖和配置,简化了开发过程
    • Spring MVC:提供构建 Web 应用的基础架构,支持 MVC 架构设计模式,帮助开发者创建 Web 控制器、服务和 View 层
    • Embedded Servers:自带嵌入式服务器(如 Tomcat、Jetty、Undertow),使应用可以独立运行,不需要额外安装外部服务器
    • RESTful Services:支持创建 RESTful 风格的 Web 服务和 API,非常适合构建轻量级服务
    • JSON 处理:默认集成 Jackson 库,用于处理 JSON 数据的序列化和反序列化
    • 数据验证:支持使用 Java Bean Validation API 进行数据验证
    • 内容协商:提供内容协商机制,可以根据客户端请求的 Accept 头返回不同格式(如 JSON、XML)的响应
  • spring-boot-starter-data-jpa:用于简化基于 Java Persistence API (JPA) 的数据访问层开发,它集成了 Spring Data JPA 和 Hibernate ORM 等常用组件,使得与数据库的交互更加简单高效
  • com.h2database:是 Maven 依赖的一个组 ID,对应的是 H2 数据库,H2 是一种轻量级的嵌入式关系型数据库。H2 数据库因为其小巧、易于使用以及内存模式的支持,常常被用作开发、测试阶段的内存数据库

定义 Entity

在 Java 的持久化领域中,Entity 是一个非常重要的概念。实体类用来表示一个数据库表。每个实体类的实例,即一个对象,通常对应数据库表中的一行记录

  • @Entity:表明该类是一个实体,需要持久化
  • @Table:用于指定实体类对应的数据库表名,如果没有这个注解,默认表名是实体类的名字
  • @Id:指定实体的主键字段
  • @Column:用于指定实体类的某个字段对应于数据库表的特定列

实体类需要一个无参数构造函数,以便 JPA 提供的工具来构造此对象的实例

// src/main/java/com/example/hellospring/model/User.java
package com.example.hellospring.model;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键的生成策略
    private Long id;

    private String name;
    private String email;

    // 构造器
    public User() {}

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    // Getters 和 Setters

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    // toString 方法(可选)
    @Override
    public String toString() {
        return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
    }
}

创建 Repository

Repository 接口抽象化数据访问层,负责与数据库进行交互,执行 CRUD 操作,Spring Data JPA 自动生成实现类,简化数据访问逻辑

  • CrudRepository:定义了一些通用的 CRUD 操作方法。如 save()、findById()、findAll()、deleteById() 等
  • JpaRepository:继承自 CrudRepository,提供了更多 JPA 相关的功能,例如批量操作
  • PagingAndSortingRepository:扩展自 CrudRepository,添加了分页和排序功能的方法
// src/main/java/com/example/hellospring/repository/UserRepository.java
package com.example.hellospring.repository;

import com.example.hellospring.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // 可以在这里定义自定义查询方法
}

编写 Controller

在传统的 Java 编程中,一个类依赖于另一个类时,通常需要明确地创建依赖对象,而在 Spring 中,框架自动决定类中依赖的对象,并创建和注入这些对象,这个过程被称为自动装配。

@Autowired 用于实现自动装配,告诉 Spring 要在上下文中找到被标注的依赖类型的 Bean,并将其注入到注解所在的位置

// src/main/java/com/example/hellospring/controller/UserController.java
package com.example.hellospring.controller;

import com.example.hellospring.model.User;
import com.example.hellospring.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/users")
public class UserController {
    
    private final UserRepository userRepository;
    
    @Autowired
    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    // 创建用户
    @PostMapping
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);
    }
    
    // 获取所有用户
    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
    
    // 根据ID获取用户
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }
    
    // 更新用户
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User updatedUser) {
        return userRepository.findById(id)
                .map(user -> {
                    user.setName(updatedUser.getName());
                    user.setEmail(updatedUser.getEmail());
                    return userRepository.save(user);
                })
                .orElseThrow(() -> new RuntimeException("User not found"));
    }
    
    // 删除用户
    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        userRepository.deleteById(id);
    }
}

配置数据库访问

application.properties 或 application.yml 用于配置应用的各种参数,如数据库连接、服务器端口等

# src/main/resources/application.properties

spring.application.name=hellospring

# Server Configuration
server.port=8080

# H2 Database Configuration (内存型数据库,方便测试)
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update

# H2 Console Configuration
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

项目启动后可以通过 http://localhost:8080/h2-console 可视化访问数据库

测试

创建用户

curl -X POST http://localhost:8080/users \
         -H "Content-Type: application/json" \
         -d '{"name":"Alice","email":"alice@example.com"}'
         
curl -X POST http://localhost:8080/users \
     -H "Content-Type: application/json" \
     -d '{"name":"Charlie","email":"charlie@example.com"}'

获取所有用户

curl -X GET http://localhost:8080/users

获取单个用户

curl -X GET http://localhost:8080/users/2

更新用户

curl -X PUT http://localhost:8080/users/1 \
         -H "Content-Type: application/json" \
         -d '{"name":"Alice Smith","email":"alice.smith@example.com"}'

删除用户

curl -X DELETE http://localhost:8080/users/1

自动化构建与热部署

Spring Boot DevTools 提供了自动重启和热部署功能,类似于前端的热模块替换(HMR),可以大幅提升开发效率

<!-- pom.xml -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>

为了避免在服务环境启用,可以在 pom.xmlbuild 部分添加以下配置

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludeDevtools>true</excludeDevtools>
            </configuration>
        </plugin>
    </plugins>
</build>

如果使用 IntelliJ IDEA 还需要做几个设置

TypeScript技术系列13:深入理解配置文件tsconfig.json

作者 黑土豆
2025年4月11日 19:20

前言

TypeScript是现代前端和Node.js项目中非常受欢迎的一种类型化JavaScript超集语言。而 tsconfig.json文件是TypeScript项目的配置核心,它定义了项目编译的行为和规则。了解并掌握 tsconfig.json的配置选项对于提高开发效率、构建可靠的类型系统以及适应多种运行环境有着极大的帮助。

本文将对tsconfig.json文件进行全面解析,涵盖从基础到高级的各类配置项,并结合实际项目的应用场景,帮助读者构建属于自己的TypeScript编译策略。

一、什么是tsconfig.json?

tsconfig.jsonTypeScript项目的配置文件,用于告知编译器如何编译项目中的代码。它一般位于项目根目录中,TypeScript编译器tsc会默认在当前目录中查找该文件。如果存在,则会根据其定义的选项执行编译流程。

一个最简单的tsconfig.json文件可能如下:

{
  "compilerOptions": {
    "target": "ES6"
  }
}

这段配置表示将TypeScript编译为ES6JavaScript代码。

二、编译选项compilerOptions

compilerOptionstsconfig.json的核心部分,几乎所有与编译行为相关的配置项都集中在此对象中。将其按照功能划分为几类进行逐项解析。

2.1 编译目标与模块系统配置

2.1.1 target

定义编译输出的JavaScript版本。可选值包括:ES3ES5ES6/ES2015ES2016ES2017、...、ESNext

{
  "compilerOptions": {
    "target": "ES2020"
  }
}

示例说明:使用可选链和空值合并操作符的代码可在ES2020及以上版本中保留原样。

const config = settings?.database?.host ?? 'localhost';

如果将target设置为ES5,该语法将被降级处理。

2.2.2 module

指定模块系统的种类。可选值包括:CommonJSES6UMDAMDSystemNone

{
  "compilerOptions": {
    "module": "CommonJS"
  }
}

适用于Node.js环境。

import fs from 'fs';

2.2.3 moduleResolution

模块解析策略,决定import路径如何被定位。

  • classic:传统TypeScript解析方式
  • node:Node.js风格的模块解析方式(推荐)
{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

示例说明:

import { loadConfig } from './config';

编译器将尝试解析以下路径:

./config.ts
./config.tsx
./config.d.ts
./config/index.ts

2.2 源文件与输出配置

2.2.1 outDir

编译后的JavaScript文件输出目录。

{
  "compilerOptions": {
    "outDir": "./dist"
  }
}

2.2.2 rootDir

源文件根目录。

{
  "compilerOptions": {
    "rootDir": "./src"
  }
}

这样可以保持源码结构的清晰性:

src/
  app.ts
  lib/
    utils.ts
↓----------- 编译输出--------------↓
dist/
  app.js
  lib/
    utils.js

2.2.3 declaration

是否生成类型声明文件.d.ts

{
  "compilerOptions": {
    "declaration": true
  }
}

示例:

export function greet(name: string): string {
  return `Hello, ${name}`;
}

将生成:

// greet.d.ts
export declare function greet(name: string): string;

2.3 严格模式与类型检查

2.3.1 strict

开启TypeScript严格模式,一次性启用以下检查:

  • strictNullChecks
  • noImplicitAny
  • strictFunctionTypes
  • strictBindCallApply
  • strictPropertyInitialization
  • alwaysStrict
{
  "compilerOptions": {
    "strict": true
  }
}

建议总是开启此项以获得最佳的类型安全保障。

2.3.2 noImplicitAny

禁止隐式any类型。

function calculate(x, y) {
  return x + y;
}

开启后会提示:参数xy类型不明确。

修改为:

function calculate(x: number, y: number): number {
  return x + y;
}

2.3.3 strictNullChecks

开启后,nullundefined必须显式处理。

function getLength(str?: string) {
  return str.length; // Error!
}

修改方式:

function getLength(str?: string) {
  return str?.length ?? 0;
}

2.4 增量构建与缓存配置

2.4.1 incremental

启用增量编译。

{
  "compilerOptions": {
    "incremental": true
  }
}

首次编译后生成.tsbuildinfo缓存文件,加快下次编译速度。

2.5 Source Map 与调试配置

2.5.1 sourceMap

是否生成.map文件用于调试。

{
  "compilerOptions": {
    "sourceMap": true
  }
}

2.5.2 inlineSources

将源代码嵌入到Source Map中。

{
  "compilerOptions": {
    "inlineSources": true
  }
}

此配置有助于线上调试时查看源代码内容。

2.6. 模块路径映射与解析

2.6.1 baseUrl

配置模块导入时的基础目录。

{
  "compilerOptions": {
    "baseUrl": "./src"
  }
}
import { log } from 'utils/logger';

2.6.2 paths

用于设置路径别名。

{
  "compilerOptions": {
    "paths": {
      "@components/*": ["components/*"],
      "@models/*": ["models/*"]
    }
  }
}

实际代码中:

import Header from '@components/Header';

三、项目结构相关选项

3.1 include

指定需要编译的文件路径或模式。

{
  "include": ["src/**/*"]
}

3.2 exclude

指定需要排除的文件路径或模式。

{
  "exclude": ["node_modules", "dist"]
}

3.3 files

精确指定要编译的文件。

{
  "files": ["src/index.ts"]
}

不推荐大项目使用该方式。

四、类型声明相关选项

4.1 typeRoots & types

{
  "compilerOptions": {
    "typeRoots": ["./types"],
    "types": ["node", "jest"]
  }
}

这将只包含./types目录下的.d.ts声明文件,以及@types/node@types/jest类型。

五、实验性功能支持

5.1 experimentalDecorators

启用装饰器支持:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}
function Log(target: any, key: string) {
  console.log(`Accessed property: ${key}`);
}

class User {
  @Log
  name: string;
}

六、高级编译优化

6.1 skipLibCheck

跳过node_modules中类型检查,提升编译速度。

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

6.2 forceConsistentCasingInFileNames

强制一致的文件名大小写。

{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true
  }
}

七、配置继承 extends

支持继承其他配置文件。

{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./build"
  }
}

这样可以拆分通用配置与项目差异,提高可维护性。

八、常见项目模板

8.1 React项目配置模板

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext",
    "jsx": "react-jsx",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": "./src",
    "paths": {
      "@/*": ["*"],
      "@components/*": ["components/*"]
    }
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}

总结

tsconfig.jsonTypeScript项目的基础核心配置文件,熟悉其配置项可以帮助开发者更灵活地管理项目结构、提升开发体验、保障代码质量。通过合理组合使用各类配置项,我们可以根据项目需求定制出高效可靠的编译方案。希望本文能为你深入掌握TypeScript项目配置打下坚实基础。

后语

小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走吧^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。

魔改chromium源码——新增自定义变量到windows属性

2025年4月11日 18:34

在进行以下操作之前,请确保已完成之前文章中提到的 源码拉取及编译 部分。

如果已顺利完成相关配置,即可继续执行后续操作。


目标

在 Chromium 中添加一个全局变量 myCode,值为 "你好!我是来自C++的字符串",并通过 JavaScript 的 console.log(window.myCodeApi.myCode()) 直接访问。

实现步骤

步骤 1:定义全局变量

在 src/base 模块中定义全局变量 myCode,以便在整个 Chromium 项目中复用

在src/base目录下,创建文件:my_globals.h

文件内容:

#ifndef BASE_MY_GLOBALS_H_
#define BASE_MY_GLOBALS_H_

#include <string>

namespace base {
extern const char* const kMyCode;
}

#endif

base目录下创建文件:my_globals.cc

文件内容:

#include "base/my_globals.h"

namespace base {
const char* const kMyCode = "你好!我是来自C++的字符串";
}

在这里插入图片描述

修改 base/BUILD.gn

文件路径: src/base/BUILD.gn 操作: 在 component("base") 的 sources 列表中添加新文件的文件名

在这里插入图片描述

步骤 2:创建 JavaScript 绑定

在 src/content/renderer 目录中,创建文件:my_code_binding.h

文件内容:

#ifndef CONTENT_RENDERER_MY_CODE_BINDING_H_
#define CONTENT_RENDERER_MY_CODE_BINDING_H_

#include "v8/include/v8.h"

namespace content {

class MyCodeBinding {
 public:
  // 安装绑定到指定的 V8 上下文中
  static void Install(v8::Local<v8::Context> context);

 private:
  // 获取 my_code 值的 JavaScript 函数
  static void GetMyCode(const v8::FunctionCallbackInfo<v8::Value>& args);
};
}

#endif

在 src/content/renderer 目录中,创建文件:my_code_binding.cc

#include "content/renderer/my_code_binding.h"

#include "base/my_globals.h"
#include "third_party/blink/public/web/blink.h"
#include "v8/include/v8.h"

namespace content {

void MyCodeBinding::Install(v8::Local<v8::Context> context) {
  // 从 context 获取 Isolate
  v8::Isolate* isolate = context->GetIsolate();
  v8::HandleScope handle_scope(isolate);
  v8::Local<v8::Object> global = context->Global();
  v8::Local<v8::Object> my_code_api = v8::Object::New(isolate);
  my_code_api->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked(),
      v8::Function::New(context, &MyCodeBinding::GetMyCode).ToLocalChecked())
      .Check();

  global->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCodeApi").ToLocalChecked(),
      my_code_api)
      .Check();
}

void MyCodeBinding::GetMyCode(const v8::FunctionCallbackInfo<v8::Value>& args) {
  v8::Isolate* isolate = args.GetIsolate();
  v8::HandleScope handle_scope(isolate);
  args.GetReturnValue().Set(
      v8::String::NewFromUtf8(isolate, base::kMyCode)
          .ToLocalChecked());
}

}

修改 content/renderer/BUILD.gn

文件路径: src/content/renderer/BUILD.gn 操作: 在 target(link_target_type, "renderer") 的 sources 列表中添加新文件

在这里插入图片描述

步骤 3:绑定到 RenderFrameImpl

在 RenderFrameImpl 中调用绑定逻辑,将 myCode 属性安装到脚本上下文中

修改文件路径:src/content/renderer/render_frame_impl.cc

在文件顶部添加 my_code_binding.h 头文件,可以按文件头字母顺序添加

#include "content/renderer/my_code_binding.h"

在这里插入图片描述

在RenderFrameImpl::DidCreateScriptContext中添加如下代码

void RenderFrameImpl::DidCreateScriptContext(v8::Local<v8::Context> context,
                                             int world_id) {
   // 新增代码
  if (world_id == 0) {
    MyCodeBinding::Install(context);
  }
  // 新增代码

  TRACE_EVENT_WITH_FLOW0("navigation",
                         "RenderFrameImpl::DidCreateScriptContext",
                         TRACE_ID_LOCAL(this),
                         TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT);
  v8::MicrotasksScope microtasks(GetAgentGroupScheduler().Isolate(),
                                 context->GetMicrotaskQueue(),
                                 v8::MicrotasksScope::kDoNotRunMicrotasks);
  if (((enabled_bindings_.Has(BindingsPolicyValue::kMojoWebUi)) ||
       enable_mojo_js_bindings_) &&
      IsMainFrame() && world_id == ISOLATED_WORLD_ID_GLOBAL) {
    // We only allow these bindings to be installed when creating the main
    // world context of the main frame.
    blink::WebV8Features::EnableMojoJS(context, true);

    if (mojo_js_features_) {
      if (mojo_js_features_->file_system_access)
        blink::WebV8Features::EnableMojoJSFileSystemAccessHelper(context, true);
    }
  }

  if (world_id == ISOLATED_WORLD_ID_GLOBAL &&
      mojo_js_interface_broker_.is_valid()) {
    // MojoJS interface broker can be enabled on subframes, and will limit the
    // interfaces JavaScript can request to those provided in the broker.
    blink::WebV8Features::EnableMojoJSAndUseBroker(
        context, std::move(mojo_js_interface_broker_));
  }

  for (auto& observer : observers_)
    observer.DidCreateScriptContext(context, world_id);
}

在这里插入图片描述

最后,在src目录下,执行 gn gen out/Default ,重新生成构建文件

构建成功之后运行一下命令进行编译

autoninja -C out/Default chrome

在这里插入图片描述

如果你想实现console.log(window.myCode);这样的效果

将 my_code_binding.h 修改为

#ifndef CONTENT_RENDERER_MY_CODE_BINDING_H_
#define CONTENT_RENDERER_MY_CODE_BINDING_H_

#include "v8/include/v8.h"

namespace content {

class MyCodeBinding {
 public:
  static void Install(v8::Local<v8::Context> context);
};

}
#endif

将前面的 my_code_binding.cc 修改为

#include "content/renderer/my_code_binding.h"

#include "base/my_globals.h"
#include "v8/include/v8.h"

namespace content {

void MyCodeBinding::Install(v8::Local<v8::Context> context) {
  v8::Isolate* isolate = context->GetIsolate();
  v8::HandleScope handle_scope(isolate);

  v8::Local<v8::Object> global = context->Global();

  global->Set(
      context,
      v8::String::NewFromUtf8(isolate, "myCode").ToLocalChecked(),
      v8::String::NewFromUtf8(isolate, base::kMyCode).ToLocalChecked())
      .Check();
}

}

在这里插入图片描述

实现可配置的滚动效果:JavaScript与CSS双方案

2025年4月11日 18:27

实现可配置的滚动效果:JavaScript与CSS双方案

滚动效果是现代Web界面中常见的交互方式,本文将详细介绍如何通过JavaScript实现一个可配置的公共滚动函数,以及纯CSS实现滚动效果的方法。

JavaScript实现可配置滚动函数

下面是一个通用的滚动函数,可以通过参数控制滚动方向、速度和行为:

/**
 * 通用滚动函数
 * @param {Object} options 配置对象
 * @param {HTMLElement} options.element 需要滚动的元素
 * @param {string} options.direction 滚动方向 ('horizontal'或'vertical')
 * @param {number} options.speed 滚动速度 (像素/帧)
 * @param {boolean} options.loop 是否循环滚动
 * @param {number} options.delay 开始前的延迟(毫秒)
 * @param {boolean} options.pauseOnHover 悬停时暂停
 */
function startAutoScroll(options) {
  const {
    element,
    direction = 'vertical',
    speed = 1,
    loop = true,
    delay = 0,
    pauseOnHover = true
  } = options;

  // 设置容器样式
  element.style.overflow = 'hidden';
  element.style.position = 'relative';
  
  // 创建内容包裹器
  const wrapper = document.createElement('div');
  wrapper.style.display = 'inline-block';
  wrapper.style.whiteSpace = 'nowrap';
  wrapper.style.transition = 'none';
  
  // 移动内容到包裹器
  while (element.firstChild) {
    wrapper.appendChild(element.firstChild);
  }
  element.appendChild(wrapper);

  // 克隆内容以实现无缝循环
  if (loop) {
    const clone = wrapper.cloneNode(true);
    element.appendChild(clone);
  }

  let isPaused = false;
  let animationId;
  let position = 0;

  // 悬停暂停功能
  if (pauseOnHover) {
    element.addEventListener('mouseenter', () => isPaused = true);
    element.addEventListener('mouseleave', () => isPaused = false);
  }

  // 开始滚动
  setTimeout(() => {
    function scroll() {
      if (isPaused) {
        animationId = requestAnimationFrame(scroll);
        return;
      }

      if (direction === 'horizontal') {
        position -= speed;
        wrapper.style.transform = `translateX(${position}px)`;
        
        // 循环逻辑
        if (loop && -position >= wrapper.offsetWidth) {
          position = 0;
        }
      } else {
        position -= speed;
        wrapper.style.transform = `translateY(${position}px)`;
        
        // 循环逻辑
        if (loop && -position >= wrapper.offsetHeight) {
          position = 0;
        }
      }

      animationId = requestAnimationFrame(scroll);
    }

    scroll();
  }, delay);

  // 返回停止函数
  return function stop() {
    cancelAnimationFrame(animationId);
  };
}

// 使用示例
const stopVerticalScroll = startAutoScroll({
  element: document.getElementById('vertical-scroller'),
  direction: 'vertical',
  speed: 0.5
});

const stopHorizontalScroll = startAutoScroll({
  element: document.getElementById('horizontal-scroller'),
  direction: 'horizontal',
  speed: 1,
  loop: true,
  pauseOnHover: true
});

// 需要停止时调用返回的函数
// stopVerticalScroll();
// stopHorizontalScroll();

功能特点

  1. 方向可控:通过direction参数指定垂直或水平滚动
  2. 速度可调speed参数控制滚动速度
  3. 循环模式loop参数决定是否无缝循环
  4. 交互控制:支持悬停暂停功能
  5. 可停止:返回停止函数以便控制

CSS实现滚动效果

1. 垂直滚动

<div class="vertical-scroll-container">
  <div class="vertical-scroll-content">
    <!-- 你的内容 -->
  </div>
</div>
.vertical-scroll-container {
  height: 300px;
  overflow: hidden;
  position: relative;
}

.vertical-scroll-content {
  position: absolute;
  top: 0;
  left: 0;
  animation: verticalScroll 10s linear infinite;
}

@keyframes verticalScroll {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-100%);
  }
}

/* 悬停暂停 */
.vertical-scroll-container:hover .vertical-scroll-content {
  animation-play-state: paused;
}

2. 水平滚动

<div class="horizontal-scroll-container">
  <div class="horizontal-scroll-content">
    <!-- 你的内容 -->
  </div>
</div>
.horizontal-scroll-container {
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
}

.horizontal-scroll-content {
  display: inline-block;
  animation: horizontalScroll 15s linear infinite;
}

@keyframes horizontalScroll {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100%);
  }
}

/* 悬停暂停 */
.horizontal-scroll-container:hover .horizontal-scroll-content {
  animation-play-state: paused;
}

3. 无限循环滚动(CSS方案)

要实现无缝循环,需要复制一份内容:

<div class="infinite-scroll-container">
  <div class="infinite-scroll-content">
    <!-- 你的内容 -->
  </div>
  <div class="infinite-scroll-content" aria-hidden="true">
    <!-- 重复内容 -->
  </div>
</div>
.infinite-scroll-container {
  overflow: hidden;
  white-space: nowrap;
}

.infinite-scroll-content {
  display: inline-block;
  animation: infiniteScroll 20s linear infinite;
}

@keyframes infiniteScroll {
  0% {
    transform: translateX(0);
  }
  100% {
    transform: translateX(-100%);
  }
}

方案对比

特性 JavaScript方案 CSS方案
控制精度 高,可逐像素控制 中等,依赖关键帧
性能 中等(需要requestAnimationFrame) 高(浏览器优化)
复杂度 高,需要处理逻辑 低,纯声明式
交互性 强,可随时调整参数 有限,只能暂停/继续
兼容性 好(可polyfill) 好(现代浏览器)

最佳实践建议

  1. 优先考虑CSS方案:对于简单滚动效果,CSS性能更好且实现简单

  2. 复杂交互用JavaScript:当需要动态调整速度、方向或复杂逻辑时

  3. 注意性能优化

    • 减少滚动区域的回流和重绘
    • 对滚动内容使用will-change: transform
    • 避免在滚动容器中有太多子元素
  4. 无障碍考虑

    • 为可滚动区域添加适当的ARIA属性
    • 确保滚动不会影响键盘导航
    • 提供暂停/继续的控件

通过上述方案,你可以灵活地为项目添加各种滚动效果,无论是简单的文字滚动还是复杂的交互式滚动区域都能轻松应对。

关于web应用开发赛道的备考

作者 看晴天了
2025年4月11日 18:09

一定记得return

1、flex布局

flex-wrap: wrap; 
align-content: space-between; 
justify-content: space-between;

2、布局切换:去除类名给点击的对象添加类名

记得使用检查查看对象名

document.querySelector('.active').classList.remove('active');
option.classList.add('active')

image.png

3、promise函数使用

const pipeline = async (initialValue, sequence) => 
{ 
    let res = initialValue; 
    for (const fn of sequence) { 
    res = await fn(res) 
 } 
 return res 
 };

4、element-plus 表单验证

认真读题,一般题中会给出使用方法,正则使用.test(/ /)来判断

5、在处理函数传多个参数时,合理使用三点...运算符

image.png

6、函数递归

function generateTree(dirPath) { // 读取目录下的所有文件和文件夹 const files = fs.readdirSync(dirPath); 
const tree = []; 
files.forEach(file => 
{ const filePath = path.join(dirPath, file); 
const isDirectory = fs.statSync(filePath).isDirectory();
if (isDirectory) { // 如果是目录,则递归生成子目录的文件树 
const subtree = generateTree(filePath);
tree.push({ name: file, children: subtree }); } 
else { // 如果是文件,则直接添加到文件树 
tree.push({ name: file }); 
} 
});
return tree;
}

7、.fliter和.slice

.fliter(item => item.name === name.value)和.slice(data.vale - 1, dataend.value)

8、.trim().charAt(0).toUpperCase()取首字母大写,.toLowerCase()大写转化为小写

.trim()是一个字符串方法,用于移除字符串两端的空白字符(包括空格、制表符 \t、换行符 \n 等)。它不会修改原字符串,而是返回一个新的字符串。

取首字母大写
let num = ' hell o  world';
let firstChar = num.trim().charAt(0).toUpperCase();

9、.find和.findIndex

image.png

10、Object.keys(obj)把对象中的键值封装成一个数组和.includes()判断字符串是否有

function appendParamsToURL(url, params) { 
  const paramString = Object.keys(params)
  .map((key) => `${encodeURIComponent(key)}
  =${encodeURIComponent(params[key])}`).join('&')
  const sep = url.includes('?') ? '&' : '?'

  const newurl = `${url}${sep}${paramString}`
  return newurl

}

11、.split("、")对字符串进行分割,.sort()对数组进行排序(也可以对对象进行排序)

const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 20 }
];
const numbers = [10, 2, 5, 1];
numbers.sort((a, b) => a - b);升序
numbers.sort((a, b) => b - a);降序
// 按 age 升序排序
users.sort((a, b) => a.age - b.age);
console.log(users);
// 输出:
// [
//   { name: 'Charlie', age: 20 },
//   { name: 'Alice', age: 25 },
//   { name: 'Bob', age: 30 }
// ]

12、typeof检查数据类型,返回数据类型

typeof "hello"      // "string"
typeof 42            // "number"
typeof true          // "boolean"
typeof undefined     // "undefined"
typeof null          // "object" (这是历史遗留问题)
typeof {}            // "object"
typeof []            // "object"
typeof function(){}  // "function"
typeof Symbol()      // "symbol"
typeof 10n           // "bigint"

13、for...in和for..of的用法

// 示例
const obj = {a: 1, b: 2, c: 3};
for (const key in obj) {
  console.log(key, obj[key]); // 输出 "a 1", "b 2", "c 3"
}

// 示例
const arr = ['a', 'b', 'c'];
for (const value of arr) {
  console.log(value); // 输出 "a", "b", "c"
}

// 适用于字符串
for (const char of 'hello') {
  console.log(char); // 输出 "h", "e", "l", "l", "o"
}

14、document的常用API

// 通过 CSS 选择器获取单个元素
const submitBtn = document.querySelector('.submit-btn'); 

// 通过 CSS 选择器获取多个元素
const menuItems = document.querySelectorAll('.menu li');

// 示例:添加点击事件
submitBtn.addEventListener('click', () => {
  console.log('按钮被点击了');
});

// 创建新元素
`&`const newDiv = document.createElement('div');
    newDiv.className = 'alert';
    newDiv.textContent = '这是一条新消息';
// 给submitBtn元素添加子标签createTextNode添加文本内容
`&`submitBtn
.appendChild(newDiv.createTextNode('一些文本内容')) 

`&`//修改样式
const box = document.querySelector('.box');
    // 直接修改样式
    box.style.color = 'red';
    box.style.backgroundColor = '#f0f0f0';
`&`// 添加/移除/切换类名
    box.classList.add('active');
    box.classList.remove('inactive');
    box.classList.toggle('hidden'); // 有则移除,无则添加 

`&`//给对象添加属性和值
let str = "color: red "
box["style"] = str
box["click"] = function(){}
box.setAttribute(name, value); 
    name :要设置的属性名称(字符串)
    value :要为属性设置的值(字符串)

15、.replace()和如何拿到方法和对象

//替换操作
.replace(/[A-Z]/,c => `-${c.toLowerCase()}`)

//给dom元素添加方法
dom[key] = prop

image.png

16、Map()和Set(),常用于去重

// 创建 Map
const map = new Map();

// 添加元素
map.set('name', 'Alice');
map.set(1, 'number one');
map.set({ id: 1 }, 'object key');

// 获取元素
console.log(map.get('name')); // 'Alice'
console.log(map.get(1));      // 'number one'

// 检查键是否存在
console.log(map.has('name')); // true

// 删除元素
map.delete('name');

// 清空 Map
map.clear();

// 获取大小
console.log(map.size); // 0



`// 创建 Set`
const set = new Set();

// 添加元素
set.add(1);
set.add(2);
set.add(2); // 重复值不会被添加

// 检查值是否存在
console.log(set.has(1)); // true

// 删除元素
set.delete(1);

// 清空 Set
set.clear();

// 获取大小
console.log(set.size); // 0

17、grid布局

.seat-area {
  margin-top: 50px;          /* 区域顶部有50px的外边距 */
  display: grid;             /* 使用CSS Grid布局 */
  grid-template-columns: repeat(8, auto);  /* 创建8列,每列宽度自动 */
  grid-template-rows: 100px 200px 150px;
 /* 三行,高度分别为100px、200px、150px */
  gap: 10px;                 /* 网格项之间的间隙为10px */
}
//:nth-of-type(8n + 2)伪选择器表示没8个的第二个
.seat:nth-of-type(8n + 2) {
  margin-right: 20px;        /* 每行第2个座位右侧增加20px外边距 */
}

.seat:nth-of-type(8n + 6) {
  margin-right: 20px;        /* 每行第6个座位右侧增加20px外边距 */
}

18、生成随机数

let randomInt = Math.floor(Math.random() * 10) + 1; 
console.log(randomInt); // 输出一个介于 1 到 10 之间的随机整数
/**
 * 封装函数,函数名 getRandomNum(min,max,countNum)
 * 生成 min~max 范围的 countNum 个不重复的随机数,存入数组并返回
 */
//生成指定数目和范围的随机数
const getRandomNum = function(min,max,countNum){
    var arr = [];
    // 在此处补全代码
    for(let i =0; i < countNum; i++){
        let random = Math.floor(Math.random() * max) + min

        if(!arr.includes(random)){
            arr.push(random)
        }
    }
    return arr;
}
module.exports = getRandomNum; //请勿删除

image.png

19、文本溢出,......代替

let wenben = document.querySelector('.more2_info_name')
    wenben.style.overflow = "hidden"
    wenben.style['-webkit-line-clamp'] = 2

20、@media媒体查询加弹性布局(看父元素)

@media(max-width: 768px){
    #container{
        flex-direction: column;
        gap: 45px;
    }
    #banner{
        width: 100%;
    }
    #news{
        width: 100%;
    }
}

21、关于获取Attribute的css相关用法

image.png

22、axios请求

image.png

23、css的var()设置属性

image.png

24、本地存储

image.png

最后心态放平不要看的太重

[Flutter小试牛刀] 写一个低配版的signals

作者 孤鸿玉
2025年4月11日 18:02

signals是继privider,riverpod状态管理器之后的又一个热门; 它运用方便,理解简单,特别是已经对前端signals框架比较熟悉的同学。

文本旨在研究signals的原理,复刻一个“够用就行”的低配版状态管理框架。

signals 的核心用法就是 signal 和 computed 方法。

final a = signal('a');
final b = signal('b');
final c = computed(() => a.value + b.value);
final dispose = effect((){
    print("c is ${c.value}");
});
expect(c.value, 'ab');
a.value = 'aa';
expect(c.value, 'aab');
dispose();

从上面的代码可以看出,我们在改变a的值后,c的值也随之改变。当c值改变时会触发effect的回调。 在flutter中我们可以将effect方法看作是 build方法,返回Wdiget树,那么当我们改变某个signal值时就会触发build重绘widget。

有意思的是,effect方法实际上监听的只是c的值,之所以改变a会触发c的改变是因为competed方法里c的值用到了a.value。

因此singals的核心思想就是这种链式监听。

初学者会感觉这像是一种魔法,因为c.value并没有传入任何的变量,那c和a,b等是如何实现关联监听的呢? 从singals源码中可以一窥究竟:

@override
T get value {
    final node = addDependency(this);

    if (node != null) {
        node.version = this.version;
    }
    return this.internalValue;
}

Node? addDependency(ReadonlySignal signal) {

    if (evalContext == null) {
        return null;
    }
    var node = signal.node;
    if (node == null || node.target != evalContext) {
        node = Node()
        ..version = 0
        ..source = signal
        ..prevSource = evalContext!.sources
        ..nextSource = null
        ..target = evalContext!
        ..prevTarget = null
        ..nextTarget = null
        ..rollbackNode = node;
        if (evalContext!.sources != null) {
            evalContext!.sources!.nextSource = node;
        }
        evalContext!.sources = node;
        signal.node = node;

        if ((evalContext!.flags & TRACKING) != 0) {
            signal.subscribeToNode(node);
        }
        return node;

    } else if (node.version == -1) {
        node.version = 0;
        if (node.nextSource != null) {
            node.nextSource!.prevSource = node.prevSource;
        if (node.prevSource != null) {
            node.prevSource!.nextSource = node.nextSource;
        }

        node.prevSource = evalContext!.sources;
        node.nextSource = null;
        evalContext!.sources!.nextSource = node;
        evalContext!.sources = node;
    }
        return node;
    }
    return null;
}

以上是Signal类中截取的获取value的方法,可以看出它调用了addDependency返回了一个node。而在addDependency则对每个Signal初始化了一个Node对象用来管理Signal之间的关系,从代码实现来看Node对象之间是通过双向链表实现关联的。且从代码中还可以发evalContext这个引用频繁出现,且是一个全局变量。

我们再看看Computed的实现:

@override
T get value {
    if ((flags & RUNNING) != 0) {
        throw Exception('Cycle detected');
    }
    final node = addDependency(this);
    internalRefresh();
    if (node != null) {
        node.version = version;
    }
    if ((flags & HAS_ERROR) != 0) {
        throw error!;
    }
    return _internalValue;
}

bool internalRefresh() {
    this.flags &= ~NOTIFIED;
    if ((this.flags & RUNNING) != 0) {
        return false;
    }

    if ((this.flags & (OUTDATED | TRACKING)) == TRACKING) {
        return true;
    }

    this.flags &= ~OUTDATED;
    if (this.internalGlobalVersion == globalVersion) {
        return true;
    }

    this.internalGlobalVersion = globalVersion;
    this.flags |= RUNNING;
    if (version > 0 && !needsToRecompute(this)) {
        this.flags &= ~RUNNING;
        return true;
    }
    final prevContext = evalContext;
    try {
        prepareSources(this);
        evalContext = this;
        final val = this.fn();
        if (!_isInitialized ||
            (flags & HAS_ERROR) != 0 ||
            _internalValue != val ||
            version == 0) {
            internalValue = val;
            flags &= ~HAS_ERROR;
            version++;
        }
    } catch (err) {
        error = err;
        flags |= HAS_ERROR;
        version++;
    }
    evalContext = prevContext;
    cleanupSources(this);
    flags &= ~RUNNING;
    return true;
}

代码很长,我们可以看到Computed和Signal的value对象一样调用了addDependency,之后调用了internalRefresh方法,在internalRefresh方法里我们又看到evalContext的身影,我们发现在调用 final val = this.fn();前它:evalContext = this;调用结束后又重制回来了:evalContext = prevContext;

从上面代码我们不难看出,Signals的魔法就在这里通过全局变量evalContext临时存储上下文信息,执行结束后又重置回来。 魔法就此揭开了。也许有人会问,这样设置全局变量不会有并发问题吗?答案是不会,因为dart的运行时isolate是单线程模型,它运行的实际上是按照顺序执行代码块,这里不详细展开讲解,可以自行查isolate的执行原理。


OK,那么我们如何在flutter中实现一个简单的像signals里signal方法和computed方法呢?首先我们也需要写一个Node用来管理调用链之间的关系,这里为了简单只设计两层的调用链,也就是computed只允许signal,不允许computed嵌套使用。他们的关系就先用树结构进行管理:

mixin class Node {
    //关联的子节点用Set是为了避免节点重复加入
    final Set<Node> children = {};
    //添加子节点
    void addChild(Node node) {
        children.add(node);
    }
    //删除子节点
    void removeChild(Node node) {
        children.remove(node);
    }
    //通知子节点改变
    void notify() {
        for (var child in children) {
            child.notify();
        }
    }
    //销毁
    void dispose() {
        children.clear();
    }
}

有了节点,我们就先写Singal,这里为了和Signal区分就用UseValue代替,为了方便就直接用Flutter的ValueNotifier:

typedef DisposeFn = void Function();

typedef ComputeFn<T> = T Function();

Node? childNode;

class UseValue<T> extends ValueNotifier<T> with Node {
    final DisposeFn? disposeFn;
    UseValue(super.value, {this.disposeFn});
    
    @override
    T get value {
        if (childNode != null) {
            addChild(childNode!);
        }
        var value = super.value;
        return value;
    }

    @override
    void dispose() {
        disposeFn?.call();
        super.dispose();
    }

    @override
    void notifyListeners() {
        super.notifyListeners();
        notify();
    }
}

在以上代码中我们定义了一个childNode全局对象,这个跟signals里的evelContext是一个作用,用于关联节点之间的关系。在T get value方法里,会去判断childNode是否为空,如果不为空则会将它加入到自己的children里,如果自己的之值发生改变则会去通知children也去改变。

有了Signal再写Computed,为了区分,我这里用UseComputed代替:

class UseComputed<T> extends ChangeNotifier with Node {
    final ComputeFn<T> computeFn;
    UseComputed(this.computeFn);
    T get value {
        var oldChildNode = childNode;
        try {
            childNode = this;
            var computeValue = computeFn();
            return computeValue;
        } finally {
            childNode = oldChildNode;
        }
    }

    @override
    void notify() {
        notifyListeners();
    }
}

从上面代码中 T get value 的实现可以看出,我们会把全局变量 childNode暂时缓存并替换成自己,执行完回调函数后又重置回去。

我们再写一个State的实现:

mixin MixinUseState<W extends StatefulWidget> on State<W> {
    final Set<DisposeFn> _disposeFnSet = {};
    @override
    void dispose() {
        for (var disposeFn in _disposeFnSet) {
            disposeFn.call();
        }
        super.dispose();
    }
    
    void _notifyChanged() {
        setState(() {});
    }
    
    //类似 signals里的 signal 方法
    UseValue<T> use<T>(T value,{bool addListener}) {
        var useValue = UseValue<T>(value);
        if(addListener){
            useValue.addListener(_notifyChanged);
        }
        _disposeFnSet.add(useValue.dispose);
        return useValue;
    }
    //类似 signals里的 computed 方法
    UseComputed<T> computed<T>(ComputeFn<T> computeFn) {
        var useComputed = UseComputed(computeFn);
        _disposeFnSet.add(useComputed.dispose);
        useComputed.addListener(_notifyChanged);
        return useComputed;
    }

}

在上面代码中,还添加了自动dispose的功能,调用use和compted时会将dispose方法放入到 _disposeFnSet中。

再写个Demo:

    void main() {
        runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
        const MyApp({super.key});
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                title: 'Flutter Demo',
                theme: ThemeData(
                colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
                useMaterial3: true,
                ),
                home: const MyHomePage(title: 'Flutter Demo Home Page'),
            );
        }
    }

    class MyHomePage extends StatefulWidget {
        const MyHomePage({super.key, required this.title});
        final String title;
        @override
        State<MyHomePage> createState() => _MyHomePageState();
    }


    class _MyHomePageState extends State<MyHomePage> with MixinUseState {
        late final c1 = use(0);
        late final c2 = use(2);
        late final counter = computed(() {
            return c1.value + c2.value;
        });

        @override
        Widget build(BuildContext context) {
            return Scaffold(
                appBar: AppBar(
                    backgroundColor: Theme.of(context).colorScheme.inversePrimary,
                    title: Text(widget.title),
                ),
                body: Center(
                    child: Column(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                            const Text(
                            'You have pushed the button this many times:',
                            ),
                            Text(
                                '${counter.value}',
                                style: Theme.of(context).textTheme.headlineMedium,
                            ),
                        ],
                    ),),
               floatingActionButton: FloatingActionButton(
                    onPressed: () {
                        c1.value++;
                    },
                    tooltip: 'Increment',
                    child: const Icon(Icons.add),
                    ), // This trailing comma makes auto-formatting nicer for build methods.
                );
            }
    }

在测试代码中,定义了 c1,c2,c3,首先c1和c2类似于signal,而c3则是computed,我们在改变c1.value时会触发c3的改变,进行重新计算,并刷新UI。

自此这个简单的demo就实现完成了,但signals的功能远不止这些,例如batch可以同时改变多个值减少刷新次数,containner,类似riverpod里的family等。

微信小程序custom-tab-bar

作者 张天宇
2025年4月11日 18:00

1. 安装tdesign-miniprogram

创建package.json 创建终端执行命令 npm i tdesign-miniprogram -S --production

配置project.config.json

"setting": {
 "packNpmRelationList": [
   {
     "packageJsonPath": "package.json",
     "miniprogramNpmDistDir": "./"
   }
 ],
 "packNpmManually": true,
 },

选择工具-> 构建npm

1744364754767.jpg

配置app.json

"usingComponents": {
    "t-button": "tdesign-miniprogram/button/button"
  },

自定义主题 配置app.wxss

page {
  --td-primary-color-7: rgb(12, 200, 164); /* 基础颜色 */
  --td-primary-color-8: rgb(12, 200, 164); /* 按下颜色 */
}

2. 自定义底部导航栏

文件夹custom-tab-bar

image.png 是一个组件 index.json apply-shared为了自定义css使用

{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {
  "t-tab-bar": "tdesign-miniprogram/tab-bar/tab-bar",
  "t-tab-bar-item": "tdesign-miniprogram/tab-bar-item/tab-bar-item"
}
}

index.wxml 自定义主题需要加一层view

<view class="wrapper">
<t-tab-bar t-class="t-tab-bar" value="{{value}}" bindchange="onChange" theme="tag" split="{{false}}">
<t-tab-bar-item wx:for="{{list}}" wx:key="index" value="{{item.value}}"> {{item.label}} </t-tab-bar-item>
</t-tab-bar>
</view>

index.wxss

.wrapper {
--td-tab-bar-active-color: rgb(12, 200, 164);
}

index.js

methods: {
  onChange(e) {
    wx.switchTab({
      url: `/pages/${e.detail.value}/index`,
    })
    this.setData({
      value: e.detail.value,
    });
  },
},

配置app.json pages: 里面每一个都是组件 tabBar: 配置custom和list

 "pages": [
  "pages/policy/index",
  "pages/data/index",
  "pages/me/index",
  "pages/login/login"
],
"tabBar": {
  "custom": true,
  "list": [
    {
      "pagePath": "pages/policy/index",
      "text": "策略"
    },
    {
      "pagePath": "pages/data/index",
      "text": "数据"
    },
    {
      "pagePath": "pages/me/index",
      "text": "我的"
    }
  ]
},

pages/policy/index 代码 index.json

{
"usingComponents": {}
}

index.js 是一个Component, 填写pageLifetimes 回调

Component({
    pageLifetimes: {
      show() {
        if (typeof this.getTabBar === 'function' &&
          this.getTabBar()) {
          this.getTabBar().setData({
            value: 'policy'
          })
        }
      }
    },
})

CSS Grid布局(Grid Layout) 4

作者 WEI_Gaot
2025年4月11日 17:57

一、 Grid 容器属性 (Properties for the Grid Container)

应用于设置了 display: grid 或 display: inline-grid 的元素。

  1. display

    • 作用:定义元素为 Grid 容器。

    • 值:

      • grid: 块级 Grid 容器。
      • inline-grid: 行内级 Grid 容器。
  2. grid-template-columns

    • 作用:定义网格列的数量和尺寸。

    • 值:

      • : 定义每列的尺寸。可以是:

        • : 如 100px, 2em。
        • : 相对于容器宽度的百分比,如 25%。
        • auto: 由内容或 minmax() 函数决定。
        • (即 fr 单位): 弹性系数,代表可用空间的一份。如 1fr, 2fr。
        • minmax(min, max): 定义一个尺寸范围,如 minmax(100px, 1fr) (最小100px,最大占1份可用空间)。
        • fit-content(): 根据内容尺寸调整,但不超过指定值。
      • repeat(, ): 重复定义轨道。如 repeat(3, 1fr) (3列,每列1fr), repeat(auto-fill, minmax(100px, 1fr)) (自动填充尽可能多的列,每列最小100px)。

      • none: (默认值,但通常会显式定义) 没有显式定义的列。

  3. grid-template-rows

    • 作用:定义网格行的数量和尺寸。
    • 值:同 grid-template-columns。
  4. grid-template-areas

    • 作用:通过命名网格区域来定义网格布局。

    • 值:

      • 字符串 (Strings): 每个字符串代表一行,字符串中的名字代表单元格所属区域名。用空格分隔名字。

      • . (点): 代表一个空单元格。

      • none: (默认值) 不使用命名区域。

      • 示例:

              grid-template-areas:
          "header header header"
          "nav main aside"
          "footer footer footer";
            
        
  5. grid-template

    • 作用:grid-template-rows, grid-template-columns, 和 grid-template-areas 的简写。

    • 值:组合上述三个属性的值,语法较复杂,通常分开写更清晰。

      • none: (默认值)
      • /
      • [ ? ? ? ]+ [ / ]? (定义 area 和 row/col)
  6. gap (或 grid-gap)

    • 作用:设置网格线之间的间距(沟槽)。

    • 值:

      • : 设置一个值,同时用于行间距和列间距,如 10px。
      • : 分别设置行间距和列间距,如 15px 10px。
  7. row-gap (或 grid-row-gap)

    • 作用:单独设置行间距。
    • 值:,如 20px。
  8. column-gap (或 grid-column-gap)

    • 作用:单独设置列间距。
    • 值:,如 10px。
  9. justify-items

    • 作用:定义网格项目在其分配的单元格内沿 行轴(水平方向) 的对齐方式。

    • 值:

      • start: 对齐到单元格的行起始边缘。
      • end: 对齐到单元格的行结束边缘。
      • center: 在单元格内水平居中。
      • stretch: (默认值) 项目填充单元格的整个宽度(如果项目未设置宽度)。
  10. align-items

    • 作用:定义网格项目在其分配的单元格内沿 列轴(垂直方向) 的对齐方式。

    • 值:

      • start: 对齐到单元格的列起始边缘。
      • end: 对齐到单元格的列结束边缘。
      • center: 在单元格内垂直居中。
      • stretch: (默认值) 项目填充单元格的整个高度(如果项目未设置高度)。
      • baseline: 基于项目内容的基线对齐。
  11. place-items

    • 作用:align-items 和 justify-items 的简写。

    • 值:

      • : 如果只提供一个值,justify-items 也使用相同的值。
      • : 分别设置。
  12. justify-content

    • 作用:定义 整个网格 在容器内沿 行轴(水平方向) 的对齐方式(当网格总宽度小于容器宽度时)。

    • 值:

      • start: 网格向容器的行起始边缘对齐。
      • end: 网格向容器的行结束边缘对齐。
      • center: 网格在容器内水平居中。
      • stretch: 拉伸网格轨道以填满容器宽度(如果轨道尺寸是 auto)。
      • space-around: 网格轨道均匀分布,轨道两侧空间相等。
      • space-between: 网格轨道均匀分布,首尾轨道贴边。
      • space-evenly: 网格轨道和它们与容器边缘之间的空间都均匀分布。
  13. align-content

    • 作用:定义 整个网格 在容器内沿 列轴(垂直方向) 的对齐方式(当网格总高度小于容器高度时)。
    • 值:同 justify-content(但作用于垂直方向)。
  14. place-content

    • 作用:align-content 和 justify-content 的简写。

    • 值:

      • : 如果只提供一个值,justify-content 也使用相同的值。
      • : 分别设置。
  15. grid-auto-columns

    • 作用:指定 隐式 创建的网格列的尺寸(当项目放置在显式定义的列之外时)。
    • 值: (同 grid-template-columns 的值)。
  16. grid-auto-rows

    • 作用:指定 隐式 创建的网格行的尺寸。
    • 值: (同 grid-template-rows 的值)。
  17. grid-auto-flow

    • 作用:指定自动布局算法如何放置没有明确放置在网格中的项目。

    • 值:

      • row: (默认值) 优先逐行填充。
      • column: 优先逐列填充。
      • dense: 尝试填补网格中较早出现的空隙,可能会打乱项目顺序。
  18. grid

    • 作用:一个复杂的简写属性,可以同时设置 grid-template-rows, grid-template-columns, grid-template-areas, grid-auto-rows, grid-auto-columns, grid-auto-flow。因其复杂性,通常不推荐使用,分开写更清晰。

二、 Grid 项目属性 (Properties for the Grid Items)

应用于 Grid 容器的直接子元素。

  1. grid-column-start

    • 作用:定义项目在列方向上开始的网格线。

    • 值:

      • : 线号 (如 1, 2, -1) 或线名。
      • span : 跨越指定数量的列。
      • auto: 自动放置。
  2. grid-column-end

    • 作用:定义项目在列方向上结束的网格线。
    • 值:同 grid-column-start。
  3. grid-row-start

    • 作用:定义项目在行方向上开始的网格线。
    • 值:同 grid-column-start。
  4. grid-row-end

    • 作用:定义项目在行方向上结束的网格线。
    • 值:同 grid-column-start。
  5. grid-column

    • 作用:grid-column-start 和 grid-column-end 的简写。

    • 值:

      • / : 如 1 / 3。
      • / span : 如 2 / span 3。
      • span / : 如 span 2 / 5。
      • : 如果只提供一个值,表示开始线,结束线自动推断(通常是下一条线)。
      • span : 如 span 2 (从自动放置位置开始跨越2列)。
  6. grid-row

    • 作用:grid-row-start 和 grid-row-end 的简写。
    • 值:同 grid-column。
  7. grid-area

    • 作用:给项目指定一个名称,使其放置在由 grid-template-areas 定义的同名区域中;或者是 grid-row-start / grid-column-start / grid-row-end / grid-column-end 的简写。

    • 值:

      • : 引用 grid-template-areas 中定义的区域名。
      • / / / : 如 2 / 1 / 4 / 3。
      • auto: 自动放置。
  8. justify-self

    • 作用:覆盖容器的 justify-items,定义 当前项目 在其单元格内沿 行轴(水平方向) 的对齐方式。
    • 值:start, end, center, stretch。
  9. align-self

    • 作用:覆盖容器的 align-items,定义 当前项目 在其单元格内沿 列轴(垂直方向) 的对齐方式。
    • 值:start, end, center, stretch, baseline。
  10. place-self

    • 作用:align-self 和 justify-self 的简写。

    • 值:

      • : 如果只提供一个值,justify-self 也使用相同的值。
      • : 分别设置。
  11. order (与 Flexbox 共享)

    • 作用:虽然 Grid 主要通过放置属性控制顺序,但 order 仍然可以影响未明确放置的项目的自动布局顺序,或在视觉顺序与 DOM 顺序需要分离时使用。数值越小越靠前。
    • 值: (默认为 0)。

案例 1

    <!-- 11.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Grid 布局示例</title>
    <link rel="stylesheet" href="11.css">
</head>
<body>

<h1>Grid 布局示例</h1>

<!-- 示例 1: 基础的 3x2 网格 -->
<h2>示例 1: 基础的 3x2 网格</h2>
<div class="grid-container example1">
    <div class="grid-item">1</div>
    <div class="grid-item">2</div>
    <div class="grid-item">3</div>
    <div class="grid-item">4</div>
    <div class="grid-item">5</div>
    <div class="grid-item">6</div>
</div>

<!-- 示例 2: 使用 fr 单位的弹性列 -->
<h2>示例 2: 使用 fr 单位的弹性列</h2>
<div class="grid-container example2">
    <div class="grid-item">1 (1fr)</div>
    <div class="grid-item">2 (2fr)</div>
    <div class="grid-item">3 (1fr)</div>
</div>

<!-- 示例 3: 经典的圣杯布局 (Holy Grail Layout) 变体 -->
<h2>示例 3: 经典的圣杯布局变体 (使用命名区域)</h2>
<div class="grid-container example3">
    <header class="grid-item header">页眉 (Header)</header>
    <nav class="grid-item nav">导航 (Nav)</nav>
    <main class="grid-item main">主内容区域 (Main)</main>
    <aside class="grid-item aside">侧边栏 (Aside)</aside>
    <footer class="grid-item footer">页脚 (Footer)</footer>
</div>

<!-- 示例 4: 响应式卡片布局 -->
<h2>示例 4: 响应式卡片布局</h2>
<div class="grid-container example4">
    <div class="grid-item card">卡片 1</div>
    <div class="grid-item card">卡片 2</div>
    <div class="grid-item card">卡片 3</div>
    <div class="grid-item card">卡片 4</div>
    <div class="grid-item card">卡片 5</div>
    <div class="grid-item card">卡片 6</div>
    <div class="grid-item card">卡片 7</div>
    <div class="grid-item card">卡片 8</div>
</div>

<!-- 示例 5: 项目跨越与显式放置 -->
<h2>示例 5: 项目跨越与显式放置</h2>
<div class="grid-container example5">
    <div class="grid-item item-a">A (跨2列)</div>
    <div class="grid-item item-b">B</div>
    <div class="grid-item item-c">C (跨2行)</div>
    <div class="grid-item item-d">D</div>
    <div class="grid-item item-e">E</div>
</div>

<!-- 示例 6: 项目对齐 -->
<h2>示例 6: 项目对齐</h2>
<div class="grid-container example6">
    <div class="grid-item align-start">Start</div>
    <div class="grid-item justify-center">Center (Horiz)</div>
    <div class="grid-item place-end">End (Both)</div>
    <div class="grid-item baseline-align">Base<br>line</div>
</div>

</body>
</html>
/* 11.css */
body {
  font-family: "Microsoft YaHei", sans-serif;
  margin: 20px;
}

h1, h2 {
  text-align: center;
  margin-bottom: 20px;
}

.grid-container {
  border: 3px solid steelblue;
  background-color: lightgray;
  padding: 10px;
  margin-bottom: 40px; /* 区分示例 */
  display: grid; /* !! 核心:启用 Grid 布局 !! */
}

.grid-item {
  border: 1px dashed #666;
  background-color: lightcoral;
  padding: 20px;
  text-align: center;
  font-size: 1.1em;
  color: white;
}

/* --- 各示例特定样式 --- */

/* 示例 1: 基础的 3x2 网格 */
.example1 {
  /* 定义 3 列,每列 100px */
  grid-template-columns: 100px 100px 100px;
  /* 定义 2 行,每行 50px */
  grid-template-rows: 50px 50px;
  /* 设置行和列的间距 */
  gap: 10px;
  /* 如果容器宽度大于 3*(100px + 10px) - 10px,网格会靠左对齐 */
  justify-content: start; /* 默认就是 start */
}

/* 示例 2: 使用 fr 单位的弹性列 */
.example2 {
  /* 定义 3 列,按 1:2:1 的比例分配可用宽度 */
  grid-template-columns: 1fr 2fr 1fr;
  /* 行高自动 */
  grid-template-rows: auto;
  gap: 15px;
}

/* 示例 3: 经典的圣杯布局变体 (使用命名区域) */
.example3 {
  /* 定义列:第一列固定宽度,第二、三列弹性 */
  grid-template-columns: 150px 1fr 150px;
  /* 定义行:页眉页脚高度自动,中间区域弹性 */
  grid-template-rows: auto 1fr auto;
  /* 定义区域名称 */
  grid-template-areas:
      "header header header"  /* 第一行:页眉跨越所有列 */
      "nav    main   aside"   /* 第二行:导航、主内容、侧边栏 */
      "footer footer footer"; /* 第三行:页脚跨越所有列 */
  gap: 10px;
  min-height: 400px; /* 给容器一个最小高度看效果 */
}
/* 将 HTML 元素分配到对应的区域 */
.example3 .header { grid-area: header; background-color: #16a085; }
.example3 .nav    { grid-area: nav;    background-color: #27ae60; }
.example3 .main   { grid-area: main;   background-color: #2ecc71; }
.example3 .aside  { grid-area: aside;  background-color: #f1c40f; }
.example3 .footer { grid-area: footer; background-color: #e67e22; }

/* 示例 4: 响应式卡片布局 */
.example4 {
  /* 关键:创建尽可能多的列,每列最小宽度180px,最大等分剩余空间 */
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 20px;
}
.example4 .card {
  background-color: #3498db;
  min-height: 100px; /* 卡片最小高度 */
}

/* 示例 5: 项目跨越与显式放置 */
.example5 {
  /* 定义一个 3 列 2 行的基础网格 */
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: repeat(2, 100px);
  gap: 10px;
}
.example5 .item-a {
  background-color: #8e44ad;
  /* 从第 1 条列线开始,跨越 2 列 (到第 3 条列线) */
  grid-column: 1 / span 2;
  /* 或者 grid-column: 1 / 3; */
  grid-row: 1; /* 放在第 1 行 (默认跨1行) */
}
.example5 .item-c {
  background-color: #c0392b;
  grid-column: 1; /* 放在第 1 列 */
   /* 从第 2 条行线开始,跨越 2 行 (到第 4 条行线) */
  grid-row: 2 / span 2;
  /* 或者 grid-row: 2 / 4; (如果只有2行,会自动适配) */
}
/* B, D, E 会自动填充剩余单元格 */

/* 示例 6: 项目对齐 */
.example6 {
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: 150px; /* 固定行高以观察垂直对齐 */
  gap: 10px;
  /* 可以设置容器默认对齐方式 */
  /* justify-items: center; */
  /* align-items: center; */
}
.example6 .align-start {
  align-self: start; /* 当前项目在单元格内顶部对齐 */
  background-color: #2980b9;
}
.example6 .justify-center {
  justify-self: center; /* 当前项目在单元格内水平居中 */
  background-color: #d35400;
  width: 60%; /* 设置宽度,否则 stretch 会覆盖 justify-self */
}
.example6 .place-end {
  place-self: end end; /* align-self: end; justify-self: end; 右下角对齐 */
  background-color: #2c3e50;
  width: 50%;
  height: 50%; /* 设置宽高看效果 */
}
.example6 .baseline-align {
  align-self: baseline; /* 基于基线对齐 */
  font-size: 1.5em;
  background-color: #7f8c8d;
}

案例 2

<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Grid 博客布局</title>
  <style>
    * {
      box-sizing: border-box;
    }

    body {
      margin: 0;
      font-family: sans-serif;
    }

    .grid-container {
      display: grid;
      grid-template-areas:
        "header header header"
        "sidebar main aside"
        "footer footer footer";
      grid-template-columns: 200px 1fr 200px;
      grid-template-rows: auto 1fr auto;
      min-height: 100vh;
      gap: 10px;
      padding: 10px;
    }

    header {
      grid-area: header;
      background: #4CAF50;
      color: white;
      padding: 20px;
      font-size: 24px;
    }

    nav {
      grid-area: sidebar;
      background: #f4f4f4;
      padding: 20px;
    }

    main {
      grid-area: main;
      background: #fff;
      padding: 20px;
    }

    aside {
      grid-area: aside;
      background: #f9f9f9;
      padding: 20px;
    }

    footer {
      grid-area: footer;
      background: #222;
      color: white;
      text-align: center;
      padding: 10px;
    }

    /* 响应式:小屏幕折叠侧栏 */
    @media (max-width: 768px) {
      .grid-container {
        grid-template-areas:
          "header"
          "main"
          "aside"
          "sidebar"
          "footer";
        grid-template-columns: 1fr;
      }

      nav, aside {
        order: 1;
      }
    }
  </style>
</head>
<body>

  <div class="grid-container">
    <header>我的博客</header>
    <nav>导航<br>首页<br>归档<br>关于我</nav>
    <main>
      <h2>文章标题</h2>
      <p>这是文章的内容段落……</p>
    </main>
    <aside>热门推荐<br>文章1<br>文章2</aside>
    <footer>© 2025 我的博客. 保留所有权利。</footer>
  </div>

</body>
</html>

探索 Vue 3 中 vue-router 的 router.resolve () API

2025年4月11日 17:47

一、router.resolve() 是什么

router.resolve() 就好比是一个精准的 “导航参谋”。当我们在 Vue 3 应用里需要明确某个路由地址对应的详细信息时,它就能派上用场。我们给它传入路由信息,像路径、参数等,它会解析出对应的路由对象,这个对象包含了最终的 URL、要渲染的组件等关键信息。

二、它的用途

1. 动态创建链接

想象一下,我们的应用有一个商品详情页面,路由路径是 /product/:id。要是我们想在代码里动态生成指向某个商品详情页的链接,使用 router.resolve() 就很合适。通过传入商品的 id 参数,它能为我们生成准确无误的 URL,避免手动拼接路径可能产生的错误。

2. 获取路由详情

有时候,我们可能需要提前知晓某个路由对应的组件是什么,或者它是否有嵌套路由等信息。router.resolve() 能帮助我们获取这些信息,方便在业务逻辑中做出恰当的决策。比如在进行权限管理时,我们可以依据路由信息判断用户是否有权限访问该页面。

三、Vue 3 中的使用示例

假设我们的路由表是这样的

//router.js
import { createRouter, createWebHistory } from 'vue-router';
import Home from './views/Home.vue';
import Product from './views/Product.vue';

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home
  },
  {
    path: '/product/:id',
    name: 'product',
    component: Product
  }
];

const router = createRouter({
  history: createWebHistory(),
  routes
});

export default router;

然后,在某个组件里,我们想生成一个指向商品详情页的链接,代码可以这样写:

<template>
  <div>
    <button @click="generateProductLink">生成商品链接</button>
  </div>
</template>

<script setup>
import { useRouter } from 'vue-router';

const router = useRouter();

const generateProductLink = () => {
  const productId = 456; // 假设这是我们要查看详情的商品 id
  const resolved = router.resolve({
    name: 'product',
    params: { id: productId }
  });
  const link = resolved.href;
  console.log(link); // 输出类似于 /product/456 的 URL
};
</script>

我们主要还可以使用到resolve返回值,例如我再解析一个以下这样的路由:

    //指定要解析的路由以及其参数
    let routes = router.resolve({
        path: "/home",
        query: {
            Id: 802,
            Type: 'Create'
        }
    });
    
   console.log("routes", routes);

那么解析出来的结果如下:

{
    "fullPath": "/home?Id=802&Type=Create",
    "hash": "",
    "query": {
        "EvalId": "802",
        "Type": "Create"
    },
    "name": "home",
    "path": "/home",
    "params": {},
    "matched": [
        {
            "path": "/home",
            "name": "home",
            "meta": {
                "sideMenu": false,
                "requireAuth": false
            },
            "props": {
                "default": false
            },
            "children": [],
            "instances": {},
            "leaveGuards": {},
            "updateGuards": {},
            "enterCallbacks": {},
            "components": {}
        }
    ],
    "meta": {
        "sideMenu": false,
        "requireAuth": false
    },
    "href": "#/home?Id=802&Type=Create"
}

我们主要看下href:

//返回的对象有href属性--一个完整的url,这个时候我们就完全可以使用router.push()去跳转了。
const path= #/home?Id=802&Type=Create

router.push(path)//路由跳转

朋友,我是喝西瓜汁的兔叽,感谢您的阅读,衷心祝福您和家人身体健康,事事顺心。

CSS 实战笔记:记录我的成长与收获

作者 yanessa_yu
2025年4月11日 17:46

滑动定屏

fullpage组件:alvarotrigo.com/fullPage/

scroll-snap-type

background-attachment: fixed;

CSS filter属性

将模糊或颜色偏移等图形效果应用于元素。滤镜通常用于调整图像、背景和边框的渲染。

outline和border

字体官方下载地址

github.com/googlefonts… 将这个链接中的blob改为raw就可以直接下载Unbounded-Regular字体,即 github.com/googlefonts…

github.com/adobe-fonts… ——> github.com/adobe-fonts…

Swiper 实现轮播图

官网地址: swiperjs.com/

注意: swiper需要在data大于0时呈现,即vue需要判断v-if=“data && data.length > 0”,否则会出现切换到最后一屏,就无法切换到第一屏的问题。

vue-awesome-swiper 3 实现案例

 "vue-awesome-swiper": "^3.1.3",
 "swiper": "^4.4.2",
<template>
  <div>
    <swiper :options="swiperOption" class="feat-swiper" ref="swiper" key="feat-swiper" v-if="data && data.length > 0">
        <swiper-slide class="feat-item" v-for="item in data" :key="item.id">
          <img :src="item.img" class="img" />
        </swiper-slide>

        <!-- 自定义导航按钮 -->
        <div class="swiper-pagination" slot="pagination"></div>
        <div class="swiper-button-prev" slot="button-prev"></div>
        <div class="swiper-button-next" slot="button-next"></div>
      </swiper>
  </div>
</template>

<script>

export default {
  data() {
    return {
      data: [],
      swiperOption: {
        loop: true,
        observer: true,
        observeParents: true,
        speed: 600,
        autoplay: {
          delay: 3000,
          stopOnLastSlide: false, // 当自动播放(autoplay)到达最后一个幻灯片时,自动播放将停止**。注意:**在循环模式(loop mode)下,此参数无效**。
          disableOnInteraction: false, // 将其设置为 `false` 时,**在用户交互(例如滑动)后,自动播放(autoplay)将不会被禁用**,而是会在每次交互后重新启动。
        },

        pagination: { // 自定义分页功能
          el: '.feat-swiper .swiper-pagination',
          clickable: true,
        },
        navigation: { // 自定义导航功能
          nextEl: '.feat-swiper .swiper-button-next',
          prevEl: '.feat-swiper .swiper-button-prev',
        }
      },
    };
  },

 
  
};
</script>

<style lang="scss" scoped>

    .feat-swiper {
      width: 1188px;
      padding: 50px 0;
      position: relative;
      .feat-item {
        position: relative;
        cursor: pointer;
        width: 988px;
        height: 588px;
        display: flex;
        justify-content: center;
        align-items: center;

        img.img {
          width: 945px;
          height: 535px;
        }

      }
      .swiper-button-next {
        width: 50px;
        height: 85px;
        transform: rotate(180deg);
      }

      .swiper-button-prev {
        width: 50px;
        height: 85px;
        transform: translateY(50%);
      }
      .swiper-pagination {
        ::v-deep .swiper-pagination-bullet {
          width: 14px;
          height: 14px;
          border: 1px solid #7db6d4;
          background-color: #fff;
          opacity: 1;
          position: relative;
          
       }
       ::v-deep .swiper-pagination-bullet-active::after {
          content: "";
          width: 8px;
          height: 8px;
          border-radius: 50%;
          background-color: #7db6d4;
          position: absolute;
          top: 50%; 
          left: 50%; 
          transform: translate(-50%, -50%);
       }
      }

}

</style>

深入理解 useContext:从原理到实现

作者 少卿
2025年4月11日 17:35

在 React 中,useContext 是一个非常强大的 Hook,用于访问上下文(Context)中的值。它允许组件直接从上下文中读取数据,而无需通过逐层传递 props。本文将通过模拟实现 useContext,深入探讨其工作原理,并逐步改进代码,使其更接近 React 的实际行为。

一、useContext 的基本原理

在 React 中,useContext 的核心功能是读取上下文中的值。上下文(Context)是一种全局可访问的机制,用于在组件树中共享数据。以下是 useContext 的基本使用方式:

JavaScript

复制

import React, { useContext } from 'react';

const MyContext = React.createContext('default');

function ChildComponent() {
  const contextValue = useContext(MyContext);
  return <div>{contextValue}</div>;
}

function ParentComponent() {
  return (
    <MyContext.Provider value="Hello, useContext!">
      <ChildComponent />
    </MyContext.Provider>
  );
}

在上述代码中:

  • React.createContext 创建一个上下文对象。

  • Provider 用于提供上下文值。

  • useContext 用于消费上下文值。

二、模拟实现 useContext

为了更好地理解 useContext 的工作原理,我们可以通过简化的方式模拟实现以下功能:

1. createContext 的实现

const contextMap = new Map(); // 存储上下文值
const subscribers = new Map(); // 存储上下文的订阅者

function createContext(defaultValue) {
  // 创建一个唯一的上下文键
  const contextKey = Symbol();
  // 初始化上下文值
  contextMap.set(contextKey, defaultValue);
  // 初始化订阅者集合
  subscribers.set(contextKey, new Set());

  return { Provider, useContext };
}

2. Provider 的实现

function Provider({ value, children }) {
  // 使用 useState 管理上下文值
  const [state, setState] = useState(value);

  // 使用 useEffect 在值更新时通知订阅者
  useEffect(() => {
    // 更新上下文值
    contextMap.set(contextKey, value);
    // 获取当前上下文的订阅者
    const subs = subscribers.get(contextKey);
    // 通知所有订阅者
    subs.forEach((sub) => sub());
  }, [value]);

  // 返回子组件
  return children;
}

3. useContext 的实现

function useContext() {
  // 使用 useState 存储当前的上下文值
  const [value, setValue] = useState(() => contextMap.get(contextKey));

  // 使用 useEffect 订阅上下文的变化
  useEffect(() => {
    // 获取当前上下文的订阅者集合
    const subs = subscribers.get(contextKey);
    // 定义更新值的回调
    const updateValue = () => {
      setValue(contextMap.get(contextKey));
    };
    // 添加订阅
    subs.add(updateValue);
    // 组件卸载时移除订阅
    return () => {
      subs.delete(updateValue);
    };
  }, [contextKey]);

  // 返回当前的上下文值
  return value;
}

4. 模拟 useState 和 useEffect

// 模拟 useState
function useState(initialValue) {
  const state = { value: initialValue };
  const setState = (newValue) => {
    state.value = newValue;
  };
  return [state.value, setState];
}

// 模拟 useEffect
function useEffect(callback, dependencies) {
  callback();
}

5. 测试代码

// 创建上下文
const { Provider, useContext } = createContext("default");

// 子组件
function ChildComponent() {
  const contextValue = useContext();
  console.log("ChildComponent rendered with value:", contextValue);
  return <div>{contextValue}</div>;
}

// 父组件
function ParentComponent() {
  return (
    <Provider value="Hello, useContext!">
      <ChildComponent />
    </Provider>
  );
}

// 模拟渲染
function render(component) {
  return component();
}

// 初始渲染
const rootElement = render(ParentComponent);
console.log(rootElement.innerHTML); // 输出: <div>Hello, useContext!</div>

代码解析

  • createContext:创建一个唯一的上下文键(contextKey),并初始化上下文值和订阅者。

  • Provider:使用 useStateuseEffect 来管理上下文值的更新,并通知所有订阅者。

  • useContext:使用 useState 来存储当前的上下文值,并通过 useEffect 订阅上下文的变化。

  • useStateuseEffect:简化实现,用于模拟 React 的状态管理和副作用处理。

四、总结

通过模拟实现 useContext,我们深入了解了其工作原理:

  1. 上下文的创建和提供:通过 createContext 创建上下文,并通过 Provider 提供上下文值。

  2. 上下文的消费:通过 useContext 从上下文中读取值。

  3. 上下文的更新和重新渲染:通过订阅机制确保上下文值更新时,相关组件能够重新渲染。

useContext 是一个非常强大的工具,它简化了组件间的数据传递,特别是在需要跨多层组件共享数据时。希望本文的模拟实现能帮助你更好地理解 useContext 的原理和实际应用。

Vue2数据绑定

2025年4月11日 17:29

Object.defineProperty 详解

Object.defineProperty() 是 JavaScript 中的一个重要方法,它允许你精确地添加或修改对象的属性。这个方法在 Vue 2.x 的响应式系统中扮演了核心角色。

基本语法

Object.defineProperty(obj, prop, descriptor)
  • obj:要在其上定义属性的对象
  • prop:要定义或修改的属性的名称
  • descriptor:将被定义或修改的属性描述符

属性描述符

描述符有两种主要形式:数据描述符存取描述符

数据描述符

Object.defineProperty(obj, 'propertyName', {
  value: 'some value',    // 属性值
  writable: true,         // 是否可写
  enumerable: true,      // 是否可枚举(for...in 或 Object.keys())
  configurable: true      // 是否可删除或修改特性
})

存取描述符(getter/setter)

let internalValue = 'initial'

Object.defineProperty(obj, 'propertyName', {
  get() {
    console.log('Getting value')
    return internalValue
  },
  set(newValue) {
    console.log('Setting new value:', newValue)
    internalValue = newValue
  },
  enumerable: true,
  configurable: true
})

关键特性

  1. 默认值差异

    • 使用 Object.defineProperty() 时,描述符的默认值与直接赋值不同:
      // 直接赋值
      obj.a = 1 
      // 等同于:
      Object.defineProperty(obj, 'a', {
        value: 1,
        writable: true,
        enumerable: true,
        configurable: true
      })
      
      // 使用 defineProperty 不加配置
      Object.defineProperty(obj, 'b', { value: 2 })
      // b 的属性描述符为:
      // value: 2, writable: false, enumerable: false, configurable: false
      
  2. 不可重复配置

    • 不能同时指定 value/writableget/set
    • 不能混合使用数据描述符和存取描述符

Vue 2.x 中的响应式原理

Vue 2.x 使用 Object.defineProperty 实现数据响应式:

function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`获取 ${key}: ${val}`)
      return val
    },
    set(newVal) {
      console.log(`设置 ${key}: ${newVal}`)
      if (newVal !== val) {
        val = newVal
        // 触发更新...
      }
    }
  })
}

注意事项

  1. 数组限制

    • Object.defineProperty 不能检测数组长度的变化
    • Vue 2.x 通过重写数组方法(push、pop 等)解决这个问题
  2. 新属性问题

    • 必须为每个属性单独调用 Object.defineProperty
    • Vue 2.x 中需要使用 Vue.setthis.$set 添加新响应式属性
  3. 性能考虑

    • 大量使用会影响性能
    • Vue 3 改用 Proxy 实现响应式系统,部分原因是为了解决这个问题

实际应用示例

  1. 创建只读属性

    const obj = {}
    Object.defineProperty(obj, 'readOnlyProp', {
      value: 'cannot change',
      writable: false
    })
    
  2. 实现简单的数据绑定

    const data = { message: 'Hello' }
    const input = document.querySelector('input')
    
    Object.defineProperty(data, 'message', {
      get() { return input.value },
      set(value) { input.value = value }
    })
    

Object.defineProperty 提供了对对象属性更精细的控制能力,是 JavaScript 元编程的重要工具之一。

Vue 2.x 中 Object.defineProperty 的数组和新增属性处理

数组变化的检测问题

问题本质

Object.defineProperty 无法直接检测以下数组变化:

  1. 通过索引直接设置项:arr[index] = newValue
  2. 修改数组长度:arr.length = newLength

Vue 2.x 的解决方案

Vue 通过重写数组的变异方法(mutation methods)来实现响应式:

// 保存数组原型
const arrayProto = Array.prototype
// 创建新对象继承数组原型
const arrayMethods = Object.create(arrayProto)

// 需要重写的数组方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function(method) {
  // 缓存原始方法
  const original = arrayProto[method]
  
  // 定义新方法
  Object.defineProperty(arrayMethods, method, {
    value: function mutator(...args) {
      // 先执行原始方法
      const result = original.apply(this, args)
      
      // 获取Observer实例
      const ob = this.__ob__
      
      // 对于push/unshift/splice可能新增元素的情况
      let inserted
      switch (method) {
        case 'push':
        case 'unshift':
          inserted = args
          break
        case 'splice':
          inserted = args.slice(2)
          break
      }
      
      // 如果有新增元素,将它们转为响应式
      if (inserted) ob.observeArray(inserted)
      
      // 通知依赖更新
      ob.dep.notify()
      
      return result
    },
    enumerable: false,
    writable: true,
    configurable: true
  })
})

实际效果

这样处理后,以下操作可以触发视图更新:

// 可以触发更新
this.items.push(newItem)
this.items.splice(0, 1, replacement)

// 不能触发更新(需要特殊处理)
this.items[0] = newValue  // 需要使用 Vue.set 或 splice
this.items.length = 0     // 需要使用 splice

新增属性的处理问题

问题本质

Object.defineProperty 必须在初始化时就定义好所有响应式属性,后添加的属性不会自动变为响应式。

Vue.set / this.$set 的实现

Vue 提供了全局方法 Vue.set 和实例方法 this.$set 来解决这个问题:

// 简化版实现
function set(target, key, val) {
  // 处理数组情况
  if (Array.isArray(target)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val) // 使用splice确保响应式
    return val
  }
  
  // 对象已有该属性,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  
  // 获取Observer实例
  const ob = target.__ob__
  
  // 非响应式对象,直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  
  // 将新属性转为响应式
  defineReactive(ob.value, key, val)
  
  // 通知依赖更新
  ob.dep.notify()
  
  return val
}

使用示例

// 对于对象
this.$set(this.someObject, 'newProperty', 'value')

// 对于数组
this.$set(this.someArray, index, newValue)

// 全局使用
Vue.set(vm.someObject, 'newProperty', 'value')

为什么需要这样处理

  1. 对象新增属性

    // 不会触发视图更新
    this.someObject.newProp = 'value'
    
    // 会触发视图更新
    this.$set(this.someObject, 'newProp', 'value')
    
  2. 数组索引设置

    // 不会触发视图更新
    this.someArray[index] = newValue
    
    // 会触发视图更新
    this.$set(this.someArray, index, newValue)
    // 或
    this.someArray.splice(index, 1, newValue)
    

总结对比

情况 问题 Vue 2.x 解决方案
数组索引修改 arr[index] = value 不触发更新 使用 Vue.setsplice
数组长度修改 arr.length = newLength 不触发更新 使用 splice
对象新增属性 obj.newProp = value 不触发更新 使用 Vue.set
嵌套对象属性 深层次属性可能不是响应式 初始化时应完整定义嵌套结构

这些限制是 Vue 3 改用 Proxy 实现响应式系统的主要原因之一,Proxy 可以更全面地拦截对象操作。

Node版本管理工具NVM使用文档

作者 walking957
2025年4月11日 17:23

版本管理工具NVM使用文档

一、NVM简介

NVM(Node Version Manager) 是一个跨平台的Node.js版本管理工具,允许开发者在同一设备上安装、切换和管理多个Node.js版本。其核心价值体现在:

  • 多版本共存:支持同时安装多个Node.js版本,解决不同项目对运行环境的差异化需求
  • 快速切换:通过命令行实现版本间的秒级切换,避免手动修改环境变量
  • 镜像配置:支持国内镜像源加速下载(如淘宝镜像),提升安装效率
  • 权限管理:无需sudo权限即可管理Node.js版本,避免系统级污染

适用场景:需要维护多个Node.js项目的开发环境、测试框架版本兼容性、参与开源项目贡献等。


二、安装与配置

1. 系统环境准备

  • 卸载旧版Node.js(Windows/Mac/Linux均需执行)
    # 检查现有版本
    node -v
    # 完全卸载(具体步骤因系统而异)
    

2. 安装NVM

Windows系统
  1. 下载安装包:
    GitHub Releases 选择nvm-setup.exe
  2. 双击安装,注意:
    • 安装路径避免中文/空格(推荐D:\dev\nvm
    • Node.js存储路径独立设置(如D:\dev\nvm\nodejs
macOS/Linux系统
# 使用安装脚本
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.5/install.sh | bash
# 或通过Homebrew
brew install nvm
验证安装
nvm --version  # 输出版本号即成功

3. 配置镜像源

修改settings.txt(位于NVM安装目录):

node_mirror: https://npmmirror.com/mirrors/node/
npm_mirror: https://npmmirror.com/mirrors/npm/

或通过命令配置:

nvm npm_mirror https://npmmirror.com/mirrors/npm/
nvm node_mirror https://npmmirror.com/mirrors/node/

三、核心命令手册

命令 功能 示例
nvm install <version> 安装指定版本 nvm install 20.16.0
nvm use <version> 切换版本 nvm use 18.13.0
nvm ls 列出已安装版本 nvm ls
nvm uninstall <version> 卸载版本 nvm uninstall 16.15.0
nvm current 显示当前版本 nvm current
nvm alias default <version> 设置默认版本 nvm alias default 20.16.0
nvm list available 查看可安装版本 nvm list available
nvm run <version> <script> 指定版本运行脚本 nvm run 14.21.3 app.js

特殊版本标识:

  • nvm install latest:安装最新稳定版
  • nvm install lts:安装LTS长期支持版
  • nvm install --lts=hydrogen:安装特定LTS版本

四、项目级版本管理

1. 版本锁定机制

  • .nvmrc文件(项目根目录)

    v18.13.0
    

    执行nvm use自动识别并切换版本

  • package.json配置(版本约束)

    "engines": {
      "node": ">=16.0.0 <21.0.0"
    }
    

2. 依赖迁移工具

nvm reinstall-packages <old_version>  # 将旧版本全局包迁移到新版本

五、最佳实践

  1. 版本维护策略

    • 生产环境使用LTS版本(如20.x)
    • 本地开发环境保持3-4个主流版本(推荐16/18/20)
    • 每季度检查版本更新
  2. 目录结构优化

    ├── nvm
    │   ├── nodejs      # 版本存储目录
    │   └── settings.txt
    ├── npm
    │   ├── node_global # 全局包目录
    │   └── node_cache  # 缓存目录
    
  3. 环境变量配置

    npm config set prefix "D:\dev\nvm\npm\node_global"
    npm config set cache "D:\dev\nvm\npm\node_cache"
    

六、常见问题排查

1. 切换版本后npm失效

nvm reinstall-packages <version>  # 重新安装全局包

2. 镜像配置无效

  • 检查settings.txt文件编码是否为UTF-8
  • 确认镜像地址结尾包含/

3. 权限问题(Linux/macOS)

# 修复目录权限
sudo chown -R $(whoami) ~/.nvm

4. 命令无法识别

  • Windows:检查环境变量NVM_HOMENVM_SYMLINK
  • Unix:在~/.bashrc~/.zshrc添加:
    export NVM_DIR="$HOME/.nvm"
    [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
    

七、进阶功能

  1. 版本别名

    nvm alias my-project 18.13.0  # 创建别名
    nvm use my-project           # 使用别名切换
    
  2. 多架构支持

    nvm install --arch=x64 18.13.0  # 指定64位架构
    
  3. 版本自动切换(通过zsh插件):

    # 安装zsh-nvm插件
    git clone https://github.com/lukechilds/zsh-nvm ~/.oh-my-zsh/custom/plugins/zsh-nvm
    

八、注意事项

  1. Windows用户

    • 安装后需配置系统环境变量NVM_HOMENVM_SYMLINK
    • 避免使用PowerShell,推荐CMD或Git Bash
    • 首次使用前执行nvm on启用管理
  2. macOS/Linux用户

    • 出现command not found时检查Shell配置文件加载顺序
    • 使用sudo可能导致权限混乱,推荐用户级安装
  3. 通用建议

    • 项目开发优先使用LTS版本(偶数版本号)
    • 定期执行nvm cache clear清理无效缓存
    • 通过nvm version-remote --lts检查最新长期支持版本

九、常见问题解决

问题现象 解决方案
切换版本后npm包丢失 配置统一npm全局目录:npm config set prefix "D:\nvm\npm_global"
安装时提示权限不足 检查目录所有权:sudo chown -R $(whoami) ~/.nvm(Linux/macOS)
版本号显示N/A 删除损坏版本:rm -rf ~/.nvm/versions/node/v<version>
Windows提示exit status 1 关闭杀毒软件/防火墙,管理员权限运行安装程序

参考链接

【常用功能】下载文件和复制到剪切板

2025年4月11日 17:15

前言

前端人员在开发时经常会遇到:

  • 后端给一个地址,需要去下载的需求。
  • 将页面的内容复制到剪切板

下载文件

我们先说下载文件,通常情况下我们会自己写上一个非常简单的工具函数。 思路如下:

  • 创建一个a元素
  • 设置a元素跳转的链接,以及下载的文件名
  • a元素添加到页面
  • 模拟点击a元素
  • 移除a元素
/**
 * 通过url下载文件
 * @param {*} url      下载的地址
 * @param {*} fileName  下载的文件名,需自行指定后缀
 */
export const downloadByUrl = (url, fileName) => {
    const link = document.createElement('a');
    link.href = url;
    link.download = fileName || '下载文件';
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

以上将后端返回的链接地址传进来就可以了,支持blob返回流。

复制到剪切板

通常剪切板的复制用于这种情况,像某个网站,禁止了用户使用鼠标或者ctrl + c来进行复制时候,在页面给一个复制按钮,让用户手动点击复制。

思路:

  • 我们通常使用window的navigator.clipboard对象来进行。clipboard下有一个API--writeText(),可以将内容复制到clipboard剪切板中。
  • 但是有的浏览器比较老旧,不支持clipboard对象,我们就得换一种兼容的方法,使用textarea来模拟,具体的看代码吧~
/**
 * 复制到剪切板
 * @param {*} content  要复制的文本
 */
export const copyToClipboard = (content) => {
    /* 复制剪切板的更推荐项目内置的clipboard.js插件,这种方法对浏览器版本要求比较高 */
    if (!content) {
        return ElMessage({
            message: '没有要复制的内容!',
            grouping: true,
            type: 'error',
        })
    }
    // 浏览器兼容性判断
    if (navigator.clipboard) {
        navigator.clipboard.writeText(content).then(() => {
            ElMessage({
                message: '复制成功!',
                grouping: true,
                type: 'success',
            })
        })
    } else {
        // 旧版浏览器兼容方案
        const textArea = document.createElement('textarea')
        textArea.value = content
        textArea.style.position = 'fixed'
        // 使临时创建的元素不可见
        textArea.style.position = 'fixed';
        textArea.style.top = '0';
        textArea.style.left = '0';
        textArea.style.width = '2em';
        textArea.style.height = '2em';
        textArea.style.padding = '0';
        textArea.style.border = 'none';
        textArea.style.outline = 'none';
        textArea.style.boxShadow = 'none';
        textArea.style.background = 'transparent';
        // 复制
        document.body.appendChild(textArea);
        textArea.focus();
        textArea.select();
        // 判断是否复制成功
        try {
            const successful = document.execCommand('copy');
            if (successful) {
                ElMessage({
                    message: '复制成功!',
                    grouping: true,
                    type: 'success',
                })
            }
        } catch (err) {
            ElMessage({
                message: '复制失败!',
                grouping: true,
                type: 'error',
            })
        }
    }
}

好了,今天的分享就结束啦~


朋友,我是喝西瓜汁的兔叽,感谢您的阅读,衷心祝福您和家人身体健康,事事顺心。

这一次,彻底搞懂跨域之CORS解决方案

作者 Lemonjing
2025年4月11日 17:12

在大概很多年以前,那时的我还是个初出茅庐的大学生,两大框架,前端基础了解的都还可以。平时前端方面的问题也可以解决个七七八八。但是在前后端请求通信这一块,只有一些粗浅的八股文知识。前后两三次遇到跨域问题,问了当时的领导,都是告诉我交给后端解决。于是当时就给我一种固有观念,跨域嘛,后端问题。

直到有一次,遇到一个跨域请求(没错,其他请求都没跨域,只有这一个请求跨域了)。按照惯性思维,交给后端解决。快下班了,后端告诉我,你这个请求为啥是OPTIONS请求?

看着这个也是刚入职不久的年轻人,我缓缓的问出了一个问题:你毕业多久了。

他说:快一年了,怎么了?

丸辣!!!

于是在两个人查了许多资料没解决问题之后决定,要不上线试试?由于当时项目部署是前后端不分离的,所以上线之后竟然意外的没有问题。两个人内心只有一个想法:下班,反正代码还能跑。

几年后,我的同事又重新遇到这个问题,刚好最近在研究nodejs,看到了express这部分。于是我的内心,就有一种冲动:

这一次,彻底搞懂如何使用CORS解决这个问题

1. 为什么会跨域?

既然要解决跨域,我们要知道为什么会跨域:

浏览器出于安全策略,限制了一个源的文档或者脚本与另一个源的交互行为,只有同源的交互才是不被限制的。
同源的判定: 如果两个 URL 的协议端口(如果有指定的话)和主机都相同的话,则这两个 URL 是同源

有了这个概念,我们就知道,我们就知道因为我们浏览器访问的页面在一个域名下,而要访问的接口在另一个域名下资源(ajax请求),所以浏览器觉得你们可能不认识,所以禁止了这个行为。

2. 跨域问题的解决

这个时候,如果有人能出示一下凭证,证明一下关系可以信任,浏览器还是没有那么死板的。而这个时候,最让浏览器可信的,当然是服务端的态度:就像你要去别人家拜访,请求方再怎么花言巧语,开不开门还是主人家说的算。

这个交互就叫做CORS,全称是Cross-Origin Resource Sharing,中文翻译为跨域资源共享。即当出现跨域问题的时候,只要服务端允许(服务端通过一定的方式通知客户端表明当前请求可以获取资源),那么浏览器就可以访问跨域资源。

在发送请求的时候,一个请求可能会携带很多信息,所以对服务端的影响也不一样。
针对不同的请求,CORS制定了三种不同的交互模式:

  1. 简单请求
  2. 需要预检请求
  3. 需要附带身份凭证的请求

简单请求

简单请求的判定:

  1. 请求方法必须是:GET、POST、HEAD。
  2. 请求头中仅包含安全字段。安全字段包括:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  1. 如果请求头包含了Content-Type,那么他的值只能是application/x-www-form-urlencoded,multipart/form-data,text/plain

同时满足了上述条件的请求,就会被判定为简单请求。

当产生一个简单请求的时候,浏览器发送请求时,请求标头会自动带上Origin字段,该字段向服务端表明,是哪个源域的请求。

服务端接收到该请求后,会在响应头标明一个access-control-allow-origin字段,该字段标明哪些域是允许跨域访问的。

  1. 如果access-control-allow-origin的值是*,则表示允许所有域的请求。
  2. 如果access-control-allow-origin的值与浏览器请求中Origin字段的值一致,表明当前请求也是可访问的。

但是对于浏览器来说,响应头中access-control-allow-origin的值不论是*还是与浏览器请求中Origin字段的值一致,没有什么差别,浏览器只关心当前

所以对于简单请求,可以通过express的中间件方式处理,加上对应的响应头即可:

const express = require('express');
const app = express();
const allowOriginList = ['http://localhost:8080'];

app.use((req, res, next) => {
  if("Origin" in req.headers) {
    let origin = req.headers.Origin;
    if(allowOriginList.includes(origin)) {
      res.header('Access-Control-Allow-Origin', Origin);
    }
  }
  next();
})

需要预检请求

如果一个请求超出了简单请求的判定,如:

  1. 请求方法不是GET、POST、HEAD,
  2. 包含了自定义的请求头,比如AuthorizationX-Requested-With等,
  3. 请求头中包含Content-Type,且它的值不是application/x-www-form-urlencoded,multipart/form-data,text/plain

那么这个请求就会被判定成为复杂请求,在发送之前,浏览器会发送一个预检请求,复杂请求交互的流程:

  1. 浏览器首先会发送一个预检请求,询问服务器是否允许: 比如有以下请求:
fetch('http://localhost:3000/api/login', {
  method: "POST",
  headers: {
    a: 1,
    b: 2,
    Content-Type: 'application/json'
  },
  body: JSON.stringify({name: 'zhangsan'})
})

那么经过浏览器处理之后,请求报文中会产生如下格式:

OPTIONS /api/login HTTP/1.1
Host: localhost:3000
...
Origin: http://localhost:8080   // 请求源域
Access-Control-Request-Method: POST // 请求方法
Access-Control-Request-Headers: a, b, Content-Type

预检请求的目的是询问服务器,是否允许后续的请求,他不包含请求体,只包含了之后请求要做的事。 预检请求的特征:

  • 请求方法为OPTIONS
  • 没有请求体
  • 请求头中包含Origin字段,该字段的值就是当前请求的源域
  • 请求头中包含Access-Control-Request-Method字段,该字段的值就是后续请求的请求方法
  • 请求头中包含Access-Control-Request-Headers字段,该字段的值就是后续请求的请求头中包含的自定义字段
  1. 服务器判断是否允许该请求,如果允许,那么服务端需要对每一个特殊加上的请求头作出回应,任意一个没有作出回应,或者回应对不上,那么就表示不允许,返回的响应头如下:
HTTP/1.1 200 OK
...
// 每一个都需要对应
Access-Control-Allow-Origin: http://localhost:8080 
Access-Control-Allow-Method: POST
Access-Control-Allow-Headers: a, b, Content-Type
Access-Control-Max-Age: 86400 // 接下来86400秒内,同样的请求(三个消息都一样),都可以不用发送预检请求

预检请求的响应没有消息体,只有一个类似上面的响应头

  1. 浏览器发送后续真实请求,真实请求和简单请求流程一样,只携带一个origin字段

  2. 服务器响应真实的消息体。

服务端处理复杂请求的方式:

const express = require('express');
const app = express();

app.use((req, res, next) => {
  if(req.method === 'OPTIONS') {
    // 这是一个预检请求,要检测三个
    let methods = req.headers['access-control-request-method'];
    let headers = req.headers['access-control-request-headers'];
    if(methods && headers) {
      // res.header('Access-Control-Allow-Origin', req.headers.Origin);  // 允许的源域
      res.header('Access-Control-Allow-Method', methods);
      res.header('Access-Control-Allow-Headers', headers);
    }
  }
  if("Origin" in req.headers) {
    let origin = req.headers.Origin;
    if(allowOriginList.includes(origin)) {
      res.header('Access-Control-Allow-Origin', Origin);
    }
  }
  next();
})

需要附带身份凭证的请求

默认情况下,跨域请求不会携带Cookie,当一些请求需要鉴权时,必须携带Cookie。但是携带Cookie可能会对服务器造成更大的影响,所以如果请求中需要携带Cookie,需要对请求进行配置:

在请求时

fetch('http://localhost:3000/api/login', {
  credentials: 'include', // omit代表不携带Cookie, include代表携带Cookie, same-origin代表同源的请求才携带Cookie
})

Cookie通常是一个用户的身份凭证,所以携带了Cookie的跨域请求,需要更严格的配置,服务端需要明确告诉客户端,允许携带Cookie。

允许的方式就是在相应的时候添加一个响应头:Access-Control-Allow-Credentials: true。若没有明确告知客户端,则该请求也被视为不被允许的跨域请求。

如果一个跨域请求,规定了需要携带身份凭证,那么这个请求的响应头中,Access-Control-Allow-Origin的值不能是*,必须是当前请求的源域。

跨域的中间件

以上函数可以不用自己实现,实现的目的是了解CORS的原理,express中提供了cors中间件,可以简化上述的实现:

npm install cors

如果全部允许跨域那么只需要设置:

const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());

通过上面的讲解,再查看cors文档应该就可以很快的理解这个中间件的用法。

总结

当年的自己因为一心只求最终的解决方案,所以导致查阅了很多文章都没能搞懂这个问题。
现在的自己再遇到问题,已经慢慢学会去追查其本质,从本质上去解决这个问题。通过这一系列了解,相信下一次遇到类似的问题,不管是自己解决,还是帮同事解决,都可以手到擒来。

❌
❌