import.then和require.ensure原理
require.ensure: 把一些js模块给独立出一个个js文件,然后需要用到的时候,在创建一个script对象,加入到document.head对象中即可,浏览器会自动帮我们发起请求,去请求这个js文件,写个回调,去定义得到这个js文件后,需要做什么业务逻辑操作;
import.then: 创建一个 script 标签用于请求脚本,方法执行完返回一个 promise,script 标签被添加到 document.head 后,触发浏览器网络请求。请求成功后,动态的脚本会自动执行,将动态的模块添加到
__webpack_require__.m
属性中,同时调用 promise 的 resolve 方法改变状态,模块加载完成;模块的概念
实际上把文件的内容(方法、变量等)保存到一个对象上面,由于对象的边界限制,所以可以在不同的moduleId中声明相同的变量,例如:
javascriptconst export = { moduleId1: { 变量1: xxx, fun1: () {} }, moduleId2: { 变量1: xxx, fun1: () {} } }
低代码:
组件库设计:单独发布
表单引擎:通过模块联邦的方式依赖组件库,组件库更新,
初始化表单,递归解析表单字段配置,并过滤出可以设置默认值的组件 创建dag图实例 遍历过滤过的字段,提取defaultValue中的(公式、api)依赖项 生成图形数据结构(节点、边)
表单设计器:通过模块联邦的方式依赖组件库,组件库更新,动态获取导出的组件以及配置项
条件联动 :[{condition: ‘条件’, effects: [{componentid: ‘组件id’, effect:’api/assignment/clearValue/defaultValue/show/attribute’ }]}]
widgetItem组件中间层拦截数据变动;set(value) {},触发change事件;
生成默认值:遍历所有的节点,采用深度遍历优先的方式,形成拓扑排序,从最开始的变量依次生成默认值
当值变动时,拓扑排序(采用深度遍历优先),依次生成依赖于这个变量的其他变量值
javascript// 深度遍历优先 先添加b的依赖,再添加b topologicalSort(): DagNode[] { const visited = new Set<DagNode>(); const result: DagNode[] = []; // a -> b -> c const visit = (node: DagNode) => { if (visited.has(node)) return; visited.add(node); for (const dependent of node.getDependents()) { visit(dependent); } result.push(node); }; for (const [, startNode] of this.dagNodes) { visit(startNode); } return result.reverse(); // 返回逆序,即拓扑排序的顺序 }
判断是否有环:遍历所有的node节点,将依赖添加到组件节点的数组中,每遍历一个节点前判断是否祖先节点中已经存在当前node,如果存在就是有环
javascripthasCycleInDAG(): boolean { const visited = new Set<DagNode>(); const stack: { node: DagNode; ancestors: Set<DagNode> }[] = []; for (const [, startNode] of this.dagNodes) { stack.push({ node: startNode, ancestors: new Set() }); while (stack.length > 0) { const { node, ancestors } = stack.pop() as { node: DagNode; ancestors: Set<DagNode> }; if (ancestors.has(node)) { // 如果当前节点已经在祖先节点中,说明存在循环依赖 return true; } if (visited.has(node)) { // 如果当前节点已经被访问过,无需再次处理 continue; } visited.add(node); ancestors.add(node); for (const dependent of node.getDependents()) { stack.push({ node: dependent, ancestors: new Set(ancestors) }); } } } return false; }
javascript// 使用拓扑排序的方式验证是否有环 function hasCycleInDAG(): boolean { const inDegree = new Map<DagNode, number>(); const queue: DagNode[] = []; // 初始化节点的入度 for (const [, node] of this.dagNodes) { inDegree.set(node, 0); } for (const [, node] of this.dagNodes) { for (const dependent of node.getDependents()) { inDegree.set(dependent, (inDegree.get(dependent) || 0) + 1); } } // 将所有入度为 0 的节点加入队列 for (const [node, degree] of inDegree) { if (degree === 0) { queue.push(node); } } let visitedCount = 0; while (queue.length > 0) { const node = queue.shift()!; visitedCount++; for (const dependent of node.getDependents()) { inDegree.set(dependent, inDegree.get(dependent)! - 1); if (inDegree.get(dependent) === 0) { queue.push(dependent); } } } // 如果遍历的节点数少于图中的节点数,说明存在循环 return visitedCount !== this.dagNodes.size; }
为什么离职?
个人发展空间不大,产品竞争力不足;
产品分歧:默认值设置可以联动,但是后期修改值还会绑定联动关系,根据图形数据的拓扑关系,条件和默认值联动执行,实际感觉默认值的联动是首次的值,后续再去修改某个值就不需要进行默认值规则的联动,而是根据联动配置的去进行联动操作
配置条件联动时,再根据值的依赖配置生成新的联动dag图形数据,校验联动配置的关系图中是否存在循环引用;
为什么使用change事件监听属性变化,因为所有的起源都是来源于用户操作,使用watch
为什么不用definePrototype监听数组;
- 数组的长度不固定,改变数组的方式一个是直接赋值,vue已通过definePrototype实现,还有通过下标实现,this.data[0] = 100,还有数组的7个方法,如果不是改写的话,需要push的时候,还要去遍历数组,数组长度不固定,每次遍历都有性能浪费。
npm @scope
遇到的困难:
权限设置:前端想根据后端返回的数据形成目录结构;但是由于后端的数据格式为目录和资源,每个实体页面都是资源,资源中保存目录的信息,目录组件全部写死的,调整为动态渲染,
为什么eval会干扰作用域链?
watch 为什么要使用函数
tsconfig的paths和webpack的alias
闭包常用场景:
- 防抖节流函数
- 常用hooks封装
layout组件包裹router-view组件,router加载不同页面时通过在加载的组件中调用usePageHook动态加载面包屑、底部操作栏等,替代router中通过meta配置控制layout中布局组件的显隐(router使用多个布局组件时,多次加载usePageHook问题,以及加载顺序)
css区别:auto-fit、auto-fill、auto
console.log打印一个盒子
javascriptfunction box(s) { const lines = s.trim().split('\n') const width = lines.reduce((a, b) => Math.max(a, b.length), 0) const surround = (x) => '║ \x1b[0m' + x.padEnd(width) + '\x1b[31m ║' const bar = '═'.repeat(width) const top = '\x1b[31m╔═══' + bar + '═══╗' const pad = surround('') const bottom = '╚═══' + bar + '═══╝\x1b[0m' return [top, pad, ...lines.map(surround), pad, bottom].join('\n') }
node_modules中.bin文件中的软链生成过程,根据依赖package.json中的bin属性自动生成,是否会递归解析依赖中的依赖项,并提取到最上层的.bin文件
实现一个函数,类似于dom异步更新,多次出发后只执行最后的结果
文件下载请求头格式:
markdown当服务器需要向客户端发送一个文件时,可以使用 `application/octet-stream` MIME 类型, 以确保浏览器将其作为文件下载而不是尝试在浏览器中打开。
有趣bug;表格设置row-key属性为表格某一列的xxxkey,表格可以点击某个单元格时变为编辑状态;编辑状态input框失焦时,变为详情状态;当改变xxxkey这列某行数据;只要输入就会触发输入框自动失焦;因为表格的行key为这个数据,key改变,触发行重新渲染,所以输入会触发自动失焦;
watch什么时候使用函数,什么时候使用值
offsetLeft、offsetTop会触发回流吗,offsetWidth呢;
兄弟div,后面的div设置背景颜色后,为什么会遮盖前一个的box-shadow;
带有base的域名配置解决刷新后nginx404问题
el-dialog内部嵌套el-input时,使用nextTick自动聚焦问题
html<template> <el-dialog v-model="dialogShow" title="意见" align-center width="40%" destroy-on-close> <el-form ref="formRef" :model="form" label-width="120px" :rules="optionRules"> <el-form-item label="意见" prop="opinion"> <el-input ref="inputRef" type="textarea" :rows="4" maxlength="2000" clearable v-model.trim="form.opinion" placeholder="请输入" style="width: 80%" /> </el-form-item> </el-form> <template #footer> <span class="dialog-footer"> <el-button @click="cancelOption">取消</el-button> <el-button :loading="loading" type="primary" @click="handleConfirm">确定</el-button> </span> </template> </el-dialog> </template>
javascript使用nextTick自动聚焦问题 watch( () => props.show, (v) => { if (!v) { form.value.opinion = null; } else { // 方式1: setTimeout(() => { inputRef.value?.focus() }); // 方式2: nextTick(() => { nextTick(() => { inputRef.value?.focus() }) }) } }, );
axios 获取application/octet-stream数据
- responseType字段以及blob,大文件下载使用stream时如何分片
javascriptservice({ url: url, method: 'get', params, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, responseType: 'blob', }) function resolveBlob(res, fileName) { const aLink = document.createElement('a'); const blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); // 从response的headers中获取filename, 后端response.setHeader("Content-disposition", "attachment; filename=xxxx.docx") 设置的文件名; // eslint-disable-next-line prefer-regex-literals if (!fileName) { const patt = new RegExp('filename=([^;]+\\.[^\\.;]+);*'); const contentDisposition = decodeURI(res.headers['content-disposition']); const result = patt.exec(contentDisposition); fileName = result[1]; } // fileName = fileName.replace(/\"/g, '') aLink.href = URL.createObjectURL(blob); aLink.setAttribute('download', fileName); // 设置下载文件名称 document.body.appendChild(aLink); aLink.click(); document.body.appendChild(aLink); }
文件大小计算并添加单位
javascriptfunction formatSizeUnits(kb) { let units = ['KB', 'MB', 'GB', 'TB', 'PB']; let unitIndex = 0; while (kb >= 1024 && unitIndex < units.length - 1) { kb /= 1024; unitIndex++; } return `${kb.toFixed(2)} ${units[unitIndex]}`; }
低代码步骤回溯(undo/redo):
- json patch,仅记录json结构改变的规则 使用参考地址;
- 游标记录currentIndex,游标位置移动实现undo, redo;
- 优化:区分可合并不可合并事件;
- 新增组件、配置规则等不可合并
- 组件名称配置、规则输入框等change事件可使用最小时间间隔方式,类似防抖事件,连续多次输入并且时间间隔不超过300ms的输入合并为一个记录;
- 步骤序列化;
- 低代码全部可记录操作
- 新增、删除控件
- 编辑控件属性
- 控件位置拖动
- 条件联动配置
- 绑定模型、关联字段配置
- 序列化分组;
- 低代码全部可记录操作
- 序列化回显;
- 保存时机;
- 存储方式;
- 使用jsonpatch记录操作历史,存储方式使用indexedDB数据库;
- 对比全部json保存历史记录,由于字段不固定,使用localStorage保存;
- 兼容性;
markdown/** * undo * redo * * json数据保存; * 链表结构 * currentIndex; * reserve * * 区分历史记录回溯和属性编辑操作; * 属性及组件编辑操作时,currentIndex置为lastIndex; * 只能后退, * 历史记录后退时,移动游标,可前进后退; * * currentJson; * * currentIndex: number * currentJson: json * patchs: []; * * 优化: * 局部更新:操作json(不需要生成json后全部重新渲染); * 可合并操作; * 区分是否可合并操作: * 不可合并:组件列表变动(新增、删除、移动) * 可合并(属性变动、条件联动、事件配置) * * * 条件联动配置 * 模型绑定、关联字段配置 * * 触发历史记录保存; * 提取历史记录为单独的sdk; * 触发方式webWorker postMessage或者调用抛出api方法,传入序列化参数(json, 前进,后退) * * * 物料区 vue-sfc-loader * 画板;draggable * - 表单引擎 * 配置区域 * 联动设置 * 模型绑定设置(关联字段配置) * 历史记录(sdk); * combo值设置(设置属性值,根据json解析值)(component) * - 类型: * - 基础值 * - api获取 (jsonPath提取, api参数选择提取) * - 组件属性值; * dag图数据结构(默认值设置后,校验是否有循环引用问题,以及生成默认值拓扑排序api)(sdk) * 协作:待完成 */
低代码动态加载remote组件;(参考:vue3-sfc-loader)
multer上传文件时传输其他参数;
期望在req.body中获取fileId参数;但是一直是
javascriptconst chunkUpload = multer({ storage: multer.diskStorage({ destination: function (req, file, cb) { console.log('req.body', req.body); // 从请求中获取fileId,优先使用URL参数,因为在multer处理过程中req.body可能还未被完全解析 const { fileId } = req.body; if (!fileId) { return cb(new Error('Missing fileId parameter')); } const chunkDir = path.resolve(__dirname, '../../uploads/chunks', fileId); // 确保分片目录存在 try { require('fs').mkdirSync(chunkDir, { recursive: true }); cb(null, chunkDir); } catch (err) { cb(new Error(`Failed to create chunk directory: ${err.message}`)); } }, filename: function (req, file, cb) { // 同样优先使用URL参数获取chunkIndex const { chunkIndex } = req.body; if (!chunkIndex && chunkIndex !== 0) { return cb(new Error('Missing chunkIndex parameter')); } cb(null, `chunk-${chunkIndex}`); }, }), limits: { fileSize: 50 * 1024 * 1024, // 分片大小限制增加到50MB }, });
原因:客户端传输字段顺序限制;
在multer生成的中间件中,如果先解析到req.body中的对应文件字段的话,会停止挂载后面的数据
调整顺序,解决问题
javascriptformData.append('chunkIndex', chunk.index); formData.append('fileId', chunk.fileId); // 文件字段移到最后 formData.append('chunk', chunk.chunk);
项目启动问题;
- 移除@vicons-fluent依赖;执行下载,npm -w xxx(包名) install @xxx(依赖)单独安装其他版本的依赖;如果屡次不行,可以重启一下编辑器(vscode)
- 前端爆栈问题
- window:
"buile": set NODE_OPTIONS=--max-old-space-size=8192 && vite build
- linux|macos
"buile": export NODE_OPTIONS=--max-old-space-size=8192 && vite build
(流水线中配置:npm install --unsafe-perm=true --allow-root --legacy-peer-deps && cd packages/app &&
export NODE_OPTIONS=--max-old-space-size=8192 &&
npm run build:dev
) - 跨平台解决方案:
- 第一步:
npm install --save-dev cross-env
- 第二部:
"build:dev": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build
- 注意:****
"cross-env NODE_OPTIONS=--max-old-space-size=8192
和vite build
中间不需要 && 连接符
- 第一步:
- window:
vite打包发布后资源问题;
可以正常访问css等静态资源,访问js静态资源时重定向到index;(调整nginx配置静态资源访问配置)