前端工程化梳理

该篇文章系统地梳理工程化的产生、发展、原理知识,具体工具的使用方面涉及得少。由于工程化的知识点较多,又限于个人水平,工程化中的很多部分没有详细展开,暂只列了一些主要的部分。

1.前端工程化的产生

前端工程化的产生主要源自于前端项目的逐渐复杂化。项目复杂度上去之后,手动管理项目变得非常费力,几十上百的代码文件要合并、压缩、转移,操作起来很麻烦。这不得不借助工具来进行管理,这种需求动力致使了前端工程化的产生。

正式发展的开端则是从09年node的出现。node使用v8引擎运行js代码,c++扩展v8能力,可以在服务端运行,这意味着js代码 借助node的运行环境可以直接调用系统级的能力,不局限于浏览器。再配上10年发布的npm管理前端依赖,前端工程化有了合适的环境,得以快速发展。当然node的产生很大原因也是由于前端工程化的迫切需要。

为什么非要用node呢,其它开发语言java,php等同样能编写工程化工具,速度貌似还更快的样子。其实当时的工程师正是这么干的,用已有的服务端语言自己写工具压缩合并前端代码,但限于两个致命的原因,它们没能发展起来。

其中最主要的原因就是工程化生态的建立 。比如使用java开发1个通用的工程化工具,为了处理前端中的各种打包编译,应对各种项目,必须要支持扩展和插件功能,这些插件如果以java依赖包的方式提供的话,那由谁来写这么多插件是一个问题。既熟悉前端又熟练java语言的开发者毕竟是少数,也不可能让专写java的同事长期维护前端项目。

如果插件支持使用js编写,那java要提供一个浏览器内核 提供运行环境。这看似与node差不多,但有着谁是主控这一本质区别。node环境里,打包工具与其插件都是js,都在同一环境运行,两者通信方便。java的话打包控制与插件在不同环境运行,通信麻烦。频繁通信的情况下,执行速度也不怎么样。这一点可以算做技术原因。

这两点原因决定了工程化发展必须是以js为主,其它语言编写的工具只能做一些专项处理,做为一种辅助使用。

2.工程化的发展

GruntGulp12~15年第一批基于node的工程化使用较多的工具,二者均属于任务运行器,配合一些其它工具插件流式处理工程化任务。使用的时候需开发者‌手动编写配置、自定义自动化任务逻辑,没有内置模块解析、打包这些功能。gulp的简单使用示例如下:

js 复制代码
const { src, dest, series } = require('gulp'); 
const uglify = require('gulp-uglify'); // 压缩工具
const concat = require('gulp-concat'); // 合并工具

function scripts() { 
    return src('src/*.js') // 读取源文件 
    .pipe(concat('app.js')) // 合并为单文件 
    .pipe(uglify()) // 压缩 
    .pipe(dest('dist')); // 输出到dist 
} 
// 监听文件变化 
function watchFiles() { 
    watch('src/*.js', scripts); 
} 

从这种使用方式上可以看得出来,遇到复杂项目的时候,要编写的任务逻辑就会很多。即使用文件匹配、做使用封装降低一些代码量,但项目中一些目录、代码变化的话,这里编写的配置也可能要跟着改动。

冗长的配置,功能不强,使用麻烦,限制了grunt和gulp在继续变得复杂的项目中的运用,目前仅小型项目或特殊合适的场景可以考虑。

旧的工具落幕了,但它们也给后来者带来宝贵的经验。15年webpack 1.0发布,它的定位是模块打包工具 ,涵盖了模块解析、代码编译、合并、压缩这些工程化主要的部分,集成的功能较多,并且流程化。其在使用上多以配置为主,比起grunt和gulp简便得多。

vite本质上与webpack一样都属于集成化很高的模块打包工具,不过它更贴合现代浏览器,充分利用了开发环境与生产环境特性的不同,又改善工具的实现,极大地提高了构建的速度。

上面说的是打包工具的变化,工程化中还有其它几方面功能,像代码规范检查、测试、类型系统、流程自动化,不过它们在复杂度、需求度上都不如打包,所以它们的使用范围、发展速度上也不如打包工具。

代码规范检查是为团队合作开发统一代码规范用,其功能度比较浅,只要装好依赖,写好配置就能一直使用,不需要经常改动,现在的项目中多数都有使用。

测试、流程自动化类型系统 这三种的使用范围则比较小。这里的使用范围我指的是全国,而且不能单纯按项目中占比计算,叠加公司、开发者的维度来看它们的使用率都不足三成。国内多数都是中小企业,项目上讲究快速开发试错,功能频繁改动,流程也不标准严格。测试、ts类型系统、流程自动化用于这些情况下的项目所带来的效益抵不过花费的成本。

有些项目即使用了,使用的程度也比较有限,可能只写了基本的单元测试,ts也只用于开发工具库时生成类型。所以这几部分的发展比较缓慢,估计要等到经济环境变好了,IT公司规模普遍扩大了,或者这3者使用成本明显降低了才有大范围使用的可能。

3.依赖管理

如果一个开发了半年多的项目,你删了lock文件,重新安装依赖,那么多半在运行,甚至安装依赖时就会报错。这与包管理工具(npm/yarn/pnpm)的安装行为、包的不规范使用等可能都是有关系的。基本上可以分为下面几种情况:

node版本不兼容 :项目中有些依赖包仅在业务代码中使用 ,这一类包不在node环境中运行,所以对node版本基本不做要求。另一类依赖是仅在编译时使用 ,它们用于工程化构建,代码运行在node环境中。如果你切换了node版本,或者团队中其它人用了不同的node版本(主版本号不同)就可能出现api不兼容或功能差异而报错的问题。尤其是用到以下几个node版本差异比较大的时候:

  • Node 8.x:Async/Await异步编程加入。
  • Node 16.x:ES模块成为默认,.mjs与.cjs明显区分模块规范。支持顶层await,简化异步逻辑
  • Node 18.x:Web API标准化,内置更多浏览器端的标准api。
  • Node 24.x:安全与工程化升级。优化Monorepo依赖管理。支持Temporal时间API,替代有缺陷的Date对象等。

可以在package.json中配置engines指定node版本、打包工具的版本,再.npmrc配置engine-strict=true强制要求使用配置的版本。

依赖安装失败 :依赖下载慢,则通过更换镜像源、管理员运行、多次下载基本都能解决。也有可能出现提示node版本不兼容,更换依赖包的版本即可。较为少见的是依赖包版本冲突的问题 ,若某个依赖包配置的 peerDependcs(希望应用项目安装的依赖)中的依赖版本与项目中安装版本不合,或者两个子依赖包指定了同一个依赖的不同主版本,就会出现依赖冲突的提示。

安装后,某些依赖相关功能运行时报错 :这基本上是因为安装依赖后,各依赖包的版本已经不是之前的版本,新版本的包功能、结构变化或本身的bug导致错误。对于版本号,包管理工具是按照:主版本号.次版本号.修订号这种Semantic Versioning的格式理解的。

  • 修订号增加 :只修复 bug,API 完全兼容
  • 次版本号增加 :新增了功能,但向后兼容
  • 主版本号增加 :做了不兼容的 API 修改

package.json中的依赖版本一般是一个范围性的写法,如:"@babel/core": "^7.21.0"表示可安装>=7.21.0 <8.0.0的版本,在没有lock文件,或者lock文件中没有这个包的记录时,包管理工具就会查找上面范围内最新的包下载。

按理说主版本号不变,照上面版本更新的行为也不会造成运行报错的问题,但实际情况是许多包并未遵循上面的版本号发布规则,某个包可能在次版本号 修改的时候就做一些api结构、功能上的改变 。也有可能是你的项目里、哪个依赖包里使用了另一个依赖包未正式开放的api或属性,这部分很容易小版本更新时也发生变化。

依赖缺失 :项目中直接引用了在node_modules中的包,但这个包并未在package.json中配置,更新依赖后这个包很可能就会出现消失在node_modules顶层这种幽灵依赖 的问题。包管理工具除了安装时确定包的版本,还要减少重复的包 。两个依赖中如果安装了一个同样的包且可兼容的版本(主版本号一致),npm就会将其安装到node_modules顶层,按照node逐层向上查找的机制,这些包依然可以被它们的主包找到。

但如果更换了某个依赖版本导致上面这种子依赖的主版本号变化,npm认为其是不兼容的版本,就会将它们安装到各自子依赖下的node_modules中。

yarn v1.x 与npm 安装行为一样也是扁平安装,都有这种幽灵依赖的问题。不过 yarn v2/v3/v4版本中,包管理方式做了重大变更,采用即插即用(Plug'n'Play, PnP) 策略,下载的包在全局缓存,不放置到node_modules目录,所有包转为 zip压缩包 放入项目.yarn/cache中。生成 .pnp.cjs 文件,映射所有依赖包对应压缩包内的文件位置。压缩包通常被提交到仓库,其他成员直接拉取后可直接使用无需再安装。而读取包时,yarn通过拦截 requireimport的行为,直接从 zip 包读取文件。

没有node_modules目录,所以完全解决了幽灵依赖问题。有些包可能内部硬编码./node_modules/packa这种指定路径读取,需要使用yarn的nodeLinker: node-modules项配置兼容这些包。

pnpm也解决了幽灵依赖问题 :其将所有的包安装到一个全局仓库,然后项目的node_modules/.pnpm 目录下,通过硬链接 (指向全局 store 的文件)或拷贝(硬链接不能跨分区和磁盘),为项目创建所有依赖包的虚拟副本。node_modules 下只会放置直接依赖软链接 ,指向 .pnpm 中的真实位置。 每个包的依赖也通过软链接指向它们在 .pnpm 下的对应位置。类似下面的结构:

text 复制代码
node_modules/
├── .pnpm/                        # 真实的包(硬链接/拷贝)
│   ├── react@18.2.0/
│   │   └── node_modules/
│   │       ├── react/            # 真实的 react 文件
│   │       └── object-assign -> ../../object-assign@4.1.1/node_modules/object-assign
│   ├── object-assign@4.1.1/
│   │   └── node_modules/
│   │       └── object-assign/    # 真实的 object-assign 文件
│   └── lodash@4.17.21/
│       └── node_modules/
│           └── lodash/           # 真实的 lodash 文件
├── lodash -> .pnpm/lodash@4.17.21/node_modules/lodash   # 软链接
├── react -> .pnpm/react@18.2.0/node_modules/react       # 软链接
└── ... (项目直接声明的依赖)

利用软链接、硬链接,node在查找包的时候,会被链接到具体的包位置,没有找到依然可以向上查找,完美兼容node的查找机制(打包过程中的依赖查找也是模拟这种查找机制)。新项目可以都尝试换成pnpm管理。

4.模块管理

为避免污染全局变量,方便功能的组合使用,模块成为1个比较合适的管理单位,有自己的作用域、能导入其它模块、能导出方法 。不仅是开发时使用,运行时也按模块的方式组织代码。要支持上述模块的3个特性,需要运行时的模块管理。但早期浏览器没有ESM支持,模块使用AMD和CMD规范。AMD模块使用示例如下:

js 复制代码
//  'myModule' 模块,依赖于 'jquery' 和 'underscore'
define('myModule', ['jquery', 'underscore'], function($, _) {
  const privateVar = '模块私有变量';
  // 模块公开接口
  return {
    publicMethod: function() {
      console.log('Public method called. Using jQuery version:', $.fn.jquery);
      console.log('Using Underscore version:', _.VERSION);
    }
  };
});
// =====在另一个文件中,使用该模块======
require(['myModule'], function(myModule) {
  myModule.publicMethod();
});

其作用域用函数隔离,运行时,浏览器通过全局引入RequireJS包管理器进行模块的管理。先require.config({baseUrl, paths, shim}) 映射模块 ID 到真实 URL。加载模块文件时,JS 动态创建 <script> 元素并设置 async=true,插入 DOM 触发网络请求;利用 onload/onerror 事件监听加载完成状态。define时,待所有依赖模块请求完后,将模块注册到一个模块列表中。

CMD大致原理与AMD类似,使用SeaJs管理,不过使用上有些改进。

node使用的是自己的CJS模块规范 ,浏览器端不支持。CJS的用法要简化一些,为了支持使用,工程化中开发时写CJS模块,打包时需要将其转换为浏览器可运行的方式(早期用Browserify转换)

js 复制代码
// 导出
module.exports = {name:'cs'}
exports = { b: 2 }
exports.a = 1; 
// 导入
const md = require('test.js');
console.info(md.a);

UMD规范 :严格上说UMD并不是一种模块规范,他没有一套自己的模块管理方法,只是对AMD,CJS,浏览器全局环境的3种兼容性写法。

js 复制代码
// 一个通用的 UMD 模板示例
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD 环境 (如 RequireJS)
    define(['jquery', 'underscore'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS 环境 (如 Node.js / Browserify)
    module.exports = factory(require('jquery'), require('underscore'));
  } else {
    // 浏览器全局变量环境,挂载到一个全局变量下
    root.myLibrary = factory(root.jQuery, root._);
  }
}(this, function($, _) {
  // 模块逻辑 (工厂函数)
  return {
    pubfn: function() {
      console.log('UMD module works in any environment!');
      console.log('Using jQuery version:', $.fn.jquery);
      console.log('Using Underscore version:', _.VERSION);
    }
  };
}));

一些依赖包在开发时会支持node使用,浏览器环境加载使用,用这种规范包装出来的包两种环境都可以使用。

以上几种规范都存在些许问题,它们的导入导出可以用变量拼接这种动态的写法,也可以写到代码块、函数体内,这种动态写法深层嵌套 的情况不适合打包编译时做分析优化

ESM规范 则强制要求静态写法,并且只能写在顶层,这种明确要求的特性,在编译分析和优化时更容易(动态import()可嵌套在代码块中,但只是少数情况)。

UMD与ESM兼容使用 :对于有些旧的UMD包,你可能为了方便使用而在ESM模块中导入它,希望它仍能象全局环境那样挂在window下 。这种方式是可行的,不过上面的UMD格式需要做些修改,esm模块中顶层this会指向undefined,esm中执行时会报错。可以将更换第一个参数this更换为self/globalThis/window,或者立即执行函数中判断root是否为undefined进行更换。

如果开发的依赖包只想支持浏览器全局导入,不兼容CJS规范的话,可以使用纯粹的 iife(立即执行表达式):

js 复制代码
(function (global) {
    ...逻辑代码
    global.CooName = 模块导出对象;
})(window);

这样即使在工程化中的esm模块引入也能正常使用(没了exports, require字样,不会触发打包工具查找模块)。

UMD格式还可以适当调整,传入的this改为传入当前环境的一个变量 ,将挂载到全局换成挂载到某个指定变量上 。一些库用这种方式实现功能的按需添加

js 复制代码
// 使用时指定 CodeMirror 名
import CodeMirror from 'codemirror';
// codemirror额外js支持
import 'codemirror/mode/javascript/javascript.js';

//==========javascript.js===============
(function(mod) {
  if (typeof exports == "object" && typeof module == "object") // CommonJS
    mod(require("../../lib/codemirror"));
  else if (typeof define == "function" && define.amd) // AMD
    define(["../../lib/codemirror"], mod);
  else 
    // ========传入当前环境中的 Codemirror======
    mod(CodeMirror);
})(function(CodeMirror) {
    "use strict";
    CodeMirror.defineMode("javascript", function(config, parserConfig) {
      var indentUnit = config.indentUnit;
      var statementIndent = parserConfig.statementIndent;
      ...
    });
 });

这种模块的组合使用方式实际上同时兼容了ESM,CJS,全局环境3种。

不过上面这种使用方式在vite打包中CodeMirror可能会因为变量名替换的步骤而被改写,需要额外配置。另外,需要设计为按需引入的话,现在开发的库更倾向于使用monoRepo的风格,子功能分散在各子包中按需引入。

esm与cjs混用 :node和打包工具中均支持一定程度的esm与cjs模块混用,一个模块既有ESM 导入又有CJS‌这种情况并不少。但import导入cjs包,require导入esm包这种则支持有限,应该避免这两者的混用。开发的依赖若希望两种规范都支持 的话可以module字段配置esm的入口,main字段配置cjs的入口,使用时,node和打包工具会按不同规范解析入口。

打包后的模块:应用打包中,vite默认的打包格式都是ESM,CJS的包都被转换成ESM,一些模块被合并,最后都是ESM格式,直接浏览器解析。

Webpack因为出现的较早,当时ESM才正式成为规范,所以打包后使用的是自己的一套运行时模块管理方式 ,这种方式在性质上与AMD, CMD一样。不过webpack的更为强大,其对esm,cjs两种使用方式做了一些模拟,保证两者使用效果不变。大致像下面的方式管理:

js 复制代码
(() => {
  // 模块列表
  var __webpack_modules__ = {
    "./src/a.js": (module, __webpack_exports__, __webpack_require__) => {
      //【esm模块】 import foo from './bar' 转为:
      var _bar__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./bar.js").default;
      var a = 11;
      __webpack_require__.r(__webpack_exports__);    // 标记为 ES module
      __webpack_require__.d(__webpack_exports__, {   // 绑定到exports的get方法,模拟esm的实时取值
      	say: () => console.log(name),
        "default": () => a 
      });
      __webpack_exports__["default"] = (42); // 直接赋值给default属性
    },
    "./src/b.js": (module, __webpack_exports__, __webpack_require__) => {
      //【cjs】模块的正常使用,不做特别处理
      var name = "Bob";
      module.exports = name;
    },
    ...
  };
  // 模块缓存
  var __webpack_module_cache__ = {
     'a.js': {id,loaded:true,exports:{ },}
  };
  // 核心加载函数
  function __webpack_require__(moduleId) {
    // 先缓存检查,没有则从__webpack_modules__加载
    ...
  }

  // ES Module 标记、属性定义等
  __webpack_require__.r = (exports) => { /* 标记 exports 为 __esModule */ };
  __webpack_require__.d = (exports, definition) => { 
      for(var key in definition) {
        if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
            Object.defineProperty(exports, key, { 
                  enumerable: true, 
                  get: definition[key],
                  writable: false  // 不可写
            });
        }
      }
  };
  __webpack_require__.o = (obj, prop) => { /* hasOwnProperty */ };
  // 动态加载函数
  __webpack_require__.e = (chunkId) => {
    // 返回 Promise,通过 JSONP 或 import() 加载 chunk
  };
  // 入口
  var __webpack_exports__ = __webpack_require__("./src/index.js");
})();

5.打包过程

将开发时代码打包为浏览器环可运行的代码,这是工程化的核心功能。打包中有两个主要任务,一是将开发时代码转换为浏览器可运行,二是对代码进行合适的组织、优化。各打包工具在打包过程的具体处理不同,但大致能总结出一些相同的阶段:

路径解析 :从入口文件路径开始,根据resolv配置中的alias别名,exteneral扩展名,得到具体路径。如果是裸路径则到node_modules中查找,根据包的package.json中配置的main, module得到路径。

生成AST:根据解析得路径导入其文件代码,解析生成AST语法树,所有的函数、变量都被其标记出来,类似下面的结构:

js 复制代码
// 解析的文件
import uu from './jgh.js';
var lc = null;
function add(a, b) { 
  let tmd = import('./cvi/ui.js');
  console.info(tmd);
  return a + b; 
}
export {lc,add}
// ========生成的AST语法树=========
Node {
  type: 'Program',
  start: 0,
  end: 155,
  body: [
    Node {
      type: 'ImportDeclaration',
      start: 1,
      end: 27,
      specifiers: [Array],
      source: [Node]
    },
    Node {
      type: 'VariableDeclaration',
      start: 28,
      end: 42,
      declarations: [Array],
      kind: 'var'
    },
    Node {
      type: 'FunctionDeclaration',
      start: 43,
      end: 139,
      id: [Node],
      expression: false,
      generator: false,
      async: false,
      params: [Array],
      body: [Node]
    },
    Node {
      type: 'ExportNamedDeclaration',
      start: 140,
      end: 155,
      declaration: null,
      specifiers: [Array],
      source: null
    }
  ],
  sourceType: 'module'
}

递归构建依赖图 :找出当前语法树中的import/ require/import() 引用。路径依然交给resolver解析,再导入对应模块文件代码,再次解析生成AST,如此重复递归进行构建出完整的模块依赖图(import()的被标记为异步模块产生新的模块图子图)。这个过程基本上都被设计为异步并行,然后缓存已解析的模块避免重复解析。

代码转换 :如果导入的文件资源不是js类型(.vue/jsx/ts等),或者要经过语法降级(es6转es5)则需要编译转换为浏览器可运行的js,这个操作交由对应的插件完成(webpack中几乎都由loader处理),处理完成后可以返回AST,返回代码则需要再由打包工具生成AST。

  • .scss/.less的处理:这类文件中的@import可能使用了配置的别名(alias)所以打包工具会先将其解析为具体的路径。然后调用对应的sass/less编译器进行编译,编译器内部对@import的依赖文件进行导入处理、合并生成css。
  • css后处理url :因为url()中引入的图片、svg等资源要打包到指定输出目录,而.scss/.less中的url又可能使用变量拼接,所以要待编译得到css后再使用postcss这类插件处理url中的资源得到最终路径。
  • 图片字体文件等处理:一般有内置的插件处理后按配置的资源路径输出到对应目录,返回最终的文件路径(文件小的被内联)。

AST分析 :所有文件被递归解析后会得到一个完整的模块依赖图,有了全量的信息就可以开始进行分析和优化操作。

  • 作用域分析:记录各模块的导入、变量、函数,它们所在的作用域,为后续的作用域提升与Tree Shaking准备。
  • 转换标记:记录 import 的变量名与依赖模块导出的映射,后续用于模块合并。
  • 统计每个模块的大小、模块类型、被引用的数量等等。
  • 未用到的代码、常量分支进行标记,如对应AST节点上添加_included = false后续进行剔除。
  • 词法语法等分析,明显的词法、语法错误直接打印错误。

代码合并与优化:这步同样在AST上操作,按之前的作用域分析和模块引用标记,模块会尽量被合并。

  • 合并时,import被转成常量赋值为export导出的对应的部分,export则被移除。
  • 用到的变量、函数等也会尽量提升到顶层作用域,以更安全的合并代码。
  • 循环依赖情况:一般引入一个中间变量,顶层初始化,对接模块导入后再赋值给该变量。
  • Tree Shaking:import导入的模块如果未被使用,其模块又不存在副作用则整个模块被剔除。导出但未使用的也被添加剔除标记。
  • RuntimeChunk 分离:应用打包中,webpack因为使用自己的模块管理器,会有一个模块管理的运行时代码,配置RuntimeChunk分割后这部分被单独分离出来。
  • 合并过程也会按照配置的分割策略,合并出一至多个chunk。

代码生成:因为生成代码时要遍历整棵AST树,所以一些细粒度的优化也会放到这里。

  • 变量与函数名缩短、表达式简化、console,debug 清除等。
  • 全局变量替换:process.env / import.meta.env等替换为字面量。
  • 遍历每个chunk的AST,按照之前的分析标记,跳过有剔除标记的部分,根据配置的format格式(模块规范),使用对应的包装代码,组合、压缩输出bundle(物理文件,基本1个chunk对应1个bundle)。
  • 生成对应chunk的sourceMap文件,其路径按格式插入文件最末尾。

插入html :按照依赖的顺序,将同步的chunk生成的bundle(js文件)用<script>标签插入html中(异步chunk不进行插入)。esm的<script type="module">是异步加载的,所以vite打包结果只有1个<script type="module">,多chunk引入在其内决定顺序。

以上打包过程在开发环境生产环境 的行为会有区别,不同打包工具也有区别。webpack5在开发和生产环境都进行全量打包,解析完路径导入源码均交给匹配的loader处理(导入源码前还有pitch阶段,从左到右执行loader的pitch方法)递归构建完整依赖图,生产环境开启本地服务,监听代码变化更新依赖图,更新对应的文件(只放到内存中)不进行优化、压缩等操作。

vite的开发环境仅使用esbuild进行依赖预构建,node_modules的cjs依赖包都被转换为esm,然后按浏览器请求的文件才进行单个编译(代码转换仍交给插件处理),css使用<style>标签插入,图片、字体等资源直接返回路径。不进行代码合并、优化等操作(必要的全局变量替换、语法检查等仍保留)。生产环境则主要使用rollup进行完整打包流程(使用缓存了的预构建好的依赖)

静态分析的局限性 :因为许多代码是动态的,一些变量也只有运行后才知道具体值,象import.meta.env[name]遇到这种动态情况,打包中基本就放弃了继续分析。使用明确的静态编写可便于优化。

相关推荐
小林ixn1 小时前
全栈项目实战:前端独立开发,不再傻等后端接口
前端·javascript·react.js
李剑一1 小时前
Anthropic将在AI生成文本中嵌入水印!难道是用我之前写的这个技术?
前端·aigc·ai编程
Asize1 小时前
前端接口工程:axios + mock,前端不再傻等后端
前端·javascript
结网的兔子1 小时前
【前端开发】UniApp 项目地图选型与Web-APP跨端迁移方案
前端·uni-app
Canace1 小时前
给 Claude 一个链接,它真的读了原文吗
前端·人工智能·ai编程
爱丶不疚1 小时前
Electron net 模块你可以没用过,但不能不知道
前端·electron
cidy_981 小时前
React + Ant Design 通用企业数据统计模块实战
前端
渣波1 小时前
React 性能优化与状态管理双雄:useMemo 与 useReducer 深度解析
前端·javascript
Maxkim1 小时前
把智能体塞进浏览器侧边栏:我在 MV3 里踩的 5 个坑
前端·后端