强缓存与协商缓存
强缓存(STRONG CACHE) 浏览器直接从本地读取缓存资源,不与服务器通信。 控制方式 Expires(HTTP/1.0) Expires: Wed, 30 Oct 2025 12:00:00 GMT * 表示资源在这个时间点之前有效; * 问题:受客户端时间影响。 Cache-Control(HTTP/1.1) Cache-Control: max-age=31536000, public * 表示资源在未来 31536000 秒内(约 1 年)有效; * 常见取值: * public:所有用户都可缓存(默认) * private:仅用户浏览器可缓存 * no-cache:不直接使用强缓存,转协商缓存 * no-store:禁止缓存(敏感信息) 协商缓存(NEGOTIATED CACHE) 当强缓存过期或被禁用时,浏览器会向服务器发送请求: 控制方式 Last-Modified / If-Modified-Since * 服务器返回: Last-Modified: Tue, 28 Oct 2025 12:00:00 GMT * 浏览器下次请求时带上: If-Modified-Since: Tue, 28 Oct 2025 12:00:00 GMT * 服务器比较后: * 没变:返回 304 Not Modified * 变了:返回新的资源和状态码 200 OK ETAG / IF-NONE-MATCH * ETag 是资源的唯一标识(内容哈希): ETag: "abc123" * 浏览器下次请求时带上: If-None-Match: "abc123" * 如果匹配,则返回 304;否则返回新内容。
Jenkins Vue项目自动部署
1.安装JENKINS 安装推荐的插件 2.安装自动部署所需的插件 NodeJS Plugin -> 构建打包Vue项目 Publish Over SSH -> 将我们构建打包的Vue项目上传至服务器 前往Plugins即可搜索安装 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A25%3A24-image.png] 配置SSH服务 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A29%3A29-image.png] 若服务器未启用 SSH 服务 * 打开“设置” → “应用” → “可选功能” * 点击 “添加功能” * 搜索并安装 OpenSSH Server 或PowerShell 管理员模式下运行: Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0、 启动 SSH 服务: Start-Service sshd Set-Service -Name sshd -StartupType 'Automatic' 确保 Windows 防火墙已开放端口 22: New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 3.新建项目 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A26%3A50-image.png] 4.配置项目 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A34%3A31-image.png] 源码管理:私用仓库需填写Credentials [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A35%3A14-image.png] 触发器: 勾选第四个检测当 GitHub 有新的提交(push)时,通过 webhook 主动通知 Jenkins,让它立即去拉取代码并触发构建。 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A35%3A48-image.png] GitHub webhook 配置: [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A44%3A05-image.png] 选择环境 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A36%3A24-image.png] 构建步骤 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A36%3A47-image.png] 构建完成后的操作 [https://chen-1302611521.cos.ap-nanjing.myqcloud.com/blog/article/2025-07-22T09%3A37%3A31-image.png]
格式化XML文本
// 格式化 XML 的函数 function formatXml(xml) { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xml, "application/xml"); const prettyXml = formatNode(xmlDoc.documentElement, 0); return `<?xml version="1.0" encoding="UTF-8"?>\n${prettyXml}`; } function formatNode(node, level) { const indent = " ".repeat(level); let xml = ""; if (node.nodeType === 1) { // 元素节点 xml += `${indent}<${node.nodeName}`; // 遍历属性并加入节点 for (let attr of node.attributes) { xml += ` ${attr.name}="${attr.value}"`; } // 检查是否只有文本子节点 const childNodes = Array.from(node.childNodes); const hasOnlyTextChild = childNodes.length === 1 && childNodes[0].nodeType === 3; if (hasOnlyTextChild) { // 只有文本子节点,保持在一行 xml += `>${childNodes[0].nodeValue.trim()}</${node.nodeName}>\n`; } else if (childNodes.length === 0) { // 没有子节点 xml += ` />\n`; } else { // 有其他子节点,换行处理 xml += ">\n"; for (let child of childNodes) { xml += formatNode(child, level + 1); // 递归格式化子节点 } xml += `${indent}</${node.nodeName}>\n`; } } else if (node.nodeType === 3) { // 文本节点 const text = node.nodeValue.trim(); if (text) { xml += `${indent}${text}\n`; } } return xml; }
自定义element-plus搜索框
组件 SEARCHINPUT: <script setup> import { ref, reactive, watch, onMounted, nextTick } from 'vue'; import { Filter } from '@element-plus/icons-vue'; import { defaultTime, defaultValue, disabledDate } from '@/utlis'; const props = defineProps({ query: Array, shadow: { default: 'always', type: String, }, }); const emit = defineEmits(['setData', 'resetData']); const initialize = ref(false); // 选择的查询类型 const searchType = ref([]); // 存储所有类型键值 const values = reactive({}); // 加载动画控制 const loading = ref(false); // 远程加载数据选项 const options = ref([]); watch(values, v => { if (initialize.value) { const queryInfo = {}; getSearchInfo(searchType.value).forEach((item) => { if (item.type === 'time') { queryInfo['startTime'] = values[item.value]?.[0] || ''; queryInfo['endTime'] = values[item.value]?.[1] || ''; } else { queryInfo[item.value] = values[item.value]; } }); emit('setData', queryInfo); } }); onMounted(async () => { for (const item of props.query) { values[item.value] = ''; } await nextTick(); initialize.value = true; }); /** * 获取查询信息 * @param {Array} types 选择的查询类型 * @returns {*} 返回已选择的 query 数据 */ const getSearchInfo = (types) => { return types.map(type => props.query.find(i => i.value === type)).filter(Boolean); }; /** * 聚焦时发起网络请求,更新 select 选择数据 * @param queryInfo 当前聚焦的 query 数据 */ const handleFocus = async (queryInfo) => { options.value = []; try { loading.value = true; const { data } = await queryInfo.searchFunction(); Object.entries(data).forEach(([key, value]) => { options.value.push({ label: value, value: key }); }); loading.value = false; } catch (e) { loading.value = false; console.log(e); } }; /** * 删除单个 tag * @param tagValue */ const handleRemoveTag = (tagValue) => { values[tagValue] = ''; }; /** * 清空 tag */ const handleClear = () => { Object.keys(values).forEach((item) => { values[item] = ''; }); emit('resetData'); }; </script> <template> <el-card class="mb-4" :shadow="props.shadow"> <div class="flex justify-between items-center"> <div class="flex items-center"> <el-icon :size="32" class="font-bold"> <Filter /> </el-icon> <span class="text-2xl ml-4">Filter</span> </div> <Transition name="list"> <div v-if="searchType.length === 1" class="w-1/3"> <TransitionGroup name="list"> <template v-for="i in getSearchInfo(searchType)" :key="i.value"> <div> <el-input clearable v-model="values[i.value]" :placeholder="`请输入${i.label}`" v-if="i.type==='input'" /> <el-date-picker style="width: auto" v-if="i.type==='time'" v-model="values[i.value]" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" :disabled-date="disabledDate" :default-time="defaultTime" :default-value="defaultValue()" /> <el-radio-group v-if="i.type==='radio'" v-model="values[i.value]"> <el-radio v-for="(label,value) in i.state" :label="label.text" :value="value" :key="value"></el-radio> </el-radio-group> <el-select v-if="i.type==='select'" v-model="values[i.value]" filterable :loading="loading" @focus="handleFocus(i)" :placeholder="`请输入${i.label}`" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> <template #loading> <svg class="circular" viewBox="0 0 50 50"> <circle class="path" cx="25" cy="25" r="20" fill="none" /> </svg> </template> </el-select> </div> </template> </TransitionGroup> </div> </Transition> <div class="min-w-32"> <el-select v-model="searchType" placeholder="查询条件" clearable multiple @remove-tag="handleRemoveTag" @clear="handleClear"> <el-option v-for="i in props.query" :key="i.value" :label="i.label" :value="i.value" /> </el-select> </div> </div> <Transition name="list"> <div class="mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4" v-if="searchType.length>1"> <TransitionGroup name="list"> <template v-for="i in getSearchInfo(searchType)" :key="i.value"> <div> <el-input clearable v-model="values[i.value]" :placeholder="`请输入${i.label}`" v-if="i.type==='input'" /> <el-date-picker style="width: auto" v-if="i.type==='time'" v-model="values[i.value]" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" :disabled-date="disabledDate" :default-time="defaultTime" :default-value="defaultValue()" /> <el-radio-group v-if="i.type==='radio'" v-model="values[i.value]"> <el-radio v-for="(label,value) in i.state" :label="label.text" :value="value" :key="value"></el-radio> </el-radio-group> <el-select v-if="i.type==='select'" v-model="values[i.value]" filterable :loading="loading" @focus="handleFocus(i)" :placeholder="`请输入${i.label}`" > <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" /> <template #loading> <svg class="circular" viewBox="0 0 50 50"> <circle class="path" cx="25" cy="25" r="20" fill="none" /> </svg> </template> </el-select> </div> </template> </TransitionGroup> </div> </Transition> </el-card> </template> <style scoped lang="scss"> .list-move, .list-enter-active, .list-leave-active { transition: all 0.5s ease; } .list-enter-from, .list-leave-to { opacity: 0; transform: translateY(30px); } .list-leave-active { position: absolute; } .circular { display: inline; height: 30px; width: 30px; animation: loading-rotate 2s linear infinite; } .path { animation: loading-dash 1.5s ease-in-out infinite; stroke-dasharray: 90, 150; stroke-dashoffset: 0; stroke-width: 2; stroke: var(--el-color-primary); stroke-linecap: round; } </style> UTILS: 时间范围查询默认配置 // el-date-picker 默认配置 export const defaultTime = [new Date(), new Date()]; export const disabledDate = (time) => { const now = new Date(); return time.getTime() > now.getTime(); }; export const defaultValue = () => { const now = new Date(); const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1); const startOfCurrentMonth = new Date(now.getFullYear(), now.getMonth()); return [lastMonth, startOfCurrentMonth]; }; 使用: <search-input :query="foaQuery" shadow="never" @resetData="updateTableData(page)" @setData="setData" /> // 查询条件 export const foaQuery = [ { label: '用户邮箱', value: 'email', type: 'input', }, { label:'反馈标题', value: 'title', type: 'input', }, { label: '时间范围', value: 'time', type: 'time', }, ]; // 查询方法 /** * 更新表格数据 * @param page * @returns {Promise<void>} */ const updateTableData = async (page) => { const params = { pageModel: page, ...query.value, }; if (replyState.value !== 2) { params.replyState = replyState.value; } try { const res = await getList(params); if (res?.code === 200) { const { data: { records, total } } = res; page.total = total; tableData.value = records; } } catch (e) { ElMessage.error(e); } }; const setData = async (queryInfo) => { query.value = queryInfo; await updateTableData(page); };
HTML文本无效链接替换
首先定义匹配A标签IMG标签的正则表达式 // 匹配 a 标签和 img 标签 const aTagRegex = /<a[^>]*href=["']([^"']+)["'][^>]*>.*?<\/a>/g; const imgTagRegex = /<img[^>]*src=["']([^"']+)["'][^>]*>/g; 链接有效检测方法,请求路径自定义 /** * 检测链接是否有效 * @param {string} url 链接 * @returns {Promise<boolean>} */ export const checkLink = async (url) => { try { const { pathname } = new URL(url); const response = await axios.head(`/download${pathname}`); return response.status >= 200 && response.status < 300; } catch (error) { return false; } }; 自定义异步文本替换方法 /** * 异步文本替换,String.replace 无法执行异步操作 * @param string {String} 需替换的文本 * @param regexp {RegExp} 正则表达式 * @param replacerFunction {Function} 自定义替换方法 * @returns {Promise<*>} */ async function replaceAsync(string, regexp, replacerFunction) { const replacements = await Promise.all( Array.from(string.matchAll(regexp), match => replacerFunction(...match))); let i = 0; return string.replace(regexp, () => replacements[i++]); } 创建替换链接无效的HTML文本 /** * 替换链接无效的 HTML 文本 * @param html * @returns {Promise<*>} */ const replaceInvalidTags = async (html) => { html = await replaceAsync(html, aTagRegex, async (match, href) => { const exists = await checkLink(href); return exists ? match : '<div class="font-bold text-red-700 flex items-center"><svg class="mx-1 w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M17.6567 14.8284L16.2425 13.4142L17.6567 12C19.2188 10.4379 19.2188 7.90524 17.6567 6.34314C16.0946 4.78105 13.5619 4.78105 11.9998 6.34314L10.5856 7.75736L9.17139 6.34314L10.5856 4.92893C12.9287 2.58578 16.7277 2.58578 19.0709 4.92893C21.414 7.27208 21.414 11.0711 19.0709 13.4142L17.6567 14.8284ZM14.8282 17.6569L13.414 19.0711C11.0709 21.4142 7.27189 21.4142 4.92875 19.0711C2.5856 16.7279 2.5856 12.9289 4.92875 10.5858L6.34296 9.17157L7.75717 10.5858L6.34296 12C4.78086 13.5621 4.78086 16.0948 6.34296 17.6569C7.90506 19.2189 10.4377 19.2189 11.9998 17.6569L13.414 16.2426L14.8282 17.6569ZM14.8282 7.75736L16.2425 9.17157L9.17139 16.2426L7.75717 14.8284L14.8282 7.75736Z"></path></svg>链接已失效</div>'; }); html = await replaceAsync(html, imgTagRegex, async (match, href) => { const exists = await checkLink(href); return exists ? match : '<div class="font-bold text-red-700 flex items-center"><svg class="mx-1 w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M2.9918 21C2.44405 21 2 20.5551 2 20.0066V3.9934C2 3.44476 2.45531 3 2.9918 3H21.0082C21.556 3 22 3.44495 22 3.9934V20.0066C22 20.5552 21.5447 21 21.0082 21H2.9918ZM20 15V5H4V19L14 9L20 15ZM20 17.8284L14 11.8284L6.82843 19H20V17.8284ZM8 11C6.89543 11 6 10.1046 6 9C6 7.89543 6.89543 7 8 7C9.10457 7 10 7.89543 10 9C10 10.1046 9.10457 11 8 11Z"></path></svg>图片已失效</div>'; }); return html; };
TS类型使用
TS 子类型可以赋值给父类型,但是父类型不可以赋值给子类型 因为子类型继承了父类型的所有特征,所以可以用在父类型的场合。但是,子类型还可能有一些父类型没有的特征,所以父类型不能用在子类型的场合。
获取xml对象节点属性,内容
export const getNodeAttributes = (node, keys = []) => { let obj = {[node.nodeName]:{}} for (let i of keys) { obj[node.nodeName][i] = node.attributes.getNamedItem(i).value } return obj } export const getNodesAttributes = (nodes, obj = {}) => { if (nodes.length && !obj[nodes[0].nodeName]) { obj[nodes[0].nodeName] = []; } for (let i of nodes) { if (obj[i.nodeName]) { let children = {}; if (i.children.length) { children = getNodesAttributes(i.children, {}); } let res = Object.values(i.attributes).reduce((a, c) => { a.push(c.name); return a; }, []).reduce((acc, cur) => { acc[cur] = i.attributes.getNamedItem(cur).value; return acc; }, {}); obj[i.nodeName].push(Object.assign(children, res)); } else { obj[i.nodeName] = []; } } return obj; }; export const getNodeContent = (node) => { let obj = {} obj[node.nodeName] = node.textContent return obj } export const changeToXml = (str) =>{ const parser = new DOMParser(); return parser.parseFromString(str, "text/xml"); } export const tableScrollToRow = (ref,rowIndex)=>{ const tableRows = ref.value.$el.querySelectorAll( ".el-table__body-wrapper tbody .el-table__row" ); let scrollTop = 0; for (let i = 0; i < tableRows.length; i++) { if (i === rowIndex) { break; } scrollTop += tableRows[i].offsetHeight; } ref.value.scrollTo(0, scrollTop); }
TypeScript类型运算符
TYPESCRIPT类型运算符 1.KEYOF运算符 keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。 type MyObj = { foo: number, bar: string, }; type Keys = keyof MyObj; // 'foo'|'bar' 由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。 // string | number | symbol type KeyT = keyof any; 对于没有自定义键名的类型使用 keyof 运算符,返回never类型,表示不可能有这样类型的键名。 type KeyT = keyof object; // never 上面示例中,由于object类型没有自身的属性,也就没有键名,所以keyof object返回never类型。 2.IN运算符 TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。 type U = 'a'|'b'|'c'; type Foo = { [Prop in U]: number; }; // 等同于 type Foo = { a: number, b: number, c: number }; 3.方括号运算符 方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。 type Person = { age: number; name: string; alive: boolean; }; // Age 的类型是 number type Age = Person['age']; 4.EXTENDS...?: 条件运算符 TypeScript 提供类似 JavaScript 的?:运算符这样的三元运算符,但多出了一个extends关键字。 条件运算符extends...?:可以根据当前类型是否符合某种条件,返回不同的类型。 T extends U ? X : Y 上面式子中的extends用来判断,类型T是否可以赋值给类型U,即T是否为U的子类型,这里的T和U可以是任意类型。 5.INFER关键字 infer关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。 它通常跟条件运算符一起使用,用在extends关键字后面的父类型之中。 type Flatten<Type> = Type extends Array<infer Item> ? Item : Type; 上面示例中,infer Item表示Item这个参数是 TypeScript 自己推断出来的,不用显式传入,而Flatten<Type>则表示Type这个类型参数是外部传入的。Type extends Array<infer Item>则表示,如果参数Type是一个数组,那么就将该数组的成员类型推断为Item,即Item是从Type推断出来的。 6.IS运算符 函数返回布尔值的时候,可以使用is运算符,限定返回值与参数之间的关系。 is运算符用来描述返回值属于true还是false。 function isFish( pet: Fish|Bird ):pet is Fish { return (pet as Fish).swim !== undefined; } 上面示例中,函数isFish()的返回值类型为pet is Fish,表示如果参数pet类型为Fish,则返回true,否则返回false。 is运算符总是用于描述函数的返回值类型,写法采用parameterName is Type的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。 7.模板字符串 TypeScript 允许使用模板字符串,构建类型。 模板字符串的最大特点,就是内部可以引用其他类型。 type World = "world"; // "hello world" type Greeting = `hello ${World}`; 注意,模板字符串可以引用的类型一共6种,分别是 string、number、bigint、boolean、null、undefined。引用这6种以外的类型会报错。 模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。 type T = 'A'|'B'; // "A_id"|"B_id" type U = `${T}_id`; 如果模板字符串引用两个联合类型,它会交叉展开这两个类型。 type T = 'A'|'B'; type U = '1'|'2'; // 'A1'|'A2'|'B1'|'B2' type V = `${T}${U}`; 上面示例中,T和U都是联合类型,各自有两个成员,模板字符串里面引用了这两个类型,最后得到的就是一个4个成员的联合类型。 8.SATISFIES 运算符 satisfies运算符用来检测某个值是否符合指定类型。 type Colors = "red" | "green" | "blue"; type RGB = [number, number, number]; const palette = { red: [255, 0, 0], green: "#00ff00", bleu: [0, 0, 255] // 报错 } satisfies Record<Colors, string|RGB>; const greenComponent = palette.green.substring(1); // 不报错 上面示例中,变量palette的值后面增加了satisfies Record<Colors, string|RGB>,表示该值必须满足Record<Colors, string|RGB>这个条件,所以能够检测出属性名bleu的拼写错误。同时,它不会改变palette的类型推断,所以,TypeScript 知道palette.green是一个字符串,对其调用substring()方法就不会报错。
TypeScript类型-any,unknown,nerver
TYPESCRIPT类型 1.ANY类型 any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值 变量类型一旦设为 any TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。 let x:any = 'hello'; x(1) // 不报错 x.foo = 100; // 不报错 由于这个原因,应该尽量避免使用 any 类型,否则就失去了使用 TypeScript 的意义 any 类型适用于 1. 出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为 any 2. 为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为 any。 总之,TypeScript 认为,只要开发者使用了 any 类型,就表示开发者想要自己来处理这些代码,所以就不对 any 类型进行任何限制,怎么使用都可以。 从集合论的角度看 any 类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。 对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是 any。 TypeScript 提供了一个编译选项 noImplicitAny,打开该选项,只要推断出 any 类型就会报错。 $ tsc --noImplicitAny app.ts 这里有一个特殊情况,即使打开了 noImplicitAny ,使用 let 和 var 命令声明变量,但不赋值也不指定类型,是不会报错的。 由于这个原因,建议使用 let 和 var 声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。 any 类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。 2.UNKNOW类型 它与 any 含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像 any 那样自由,可以视为严格版的 any。 unknow 跟 any 的相似之处,在于所有类型的值都可以分配给 unknow 类型 unknow 类型跟 any 类型的不同之处在于,它不能直接使用。主要有以下几个限制。 1. unknow 类型的变量,不能直接赋值给其他类型的变量(除了 any 类型和 unknow 类型)。 2. 不能直接调用 unkonw 类型变量的方法和属性 3. unknow 类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、===、!=、!==、||、&&、?)、取反运算(运算符 ! )、typeof 运算符和 instanceof 运算符这几种,其他运算都会报错 使用 unknow 类型变量需要缩小 unknow 变量的类型范围(将一个不确定的类型缩小为更明确的类型),确保不会出错 let a:unknown = 1; if (typeof a === 'number') { let r = a + 10; // 正确 } 总之,unknow 可以看作是更安全的 any。一般来说,凡是需要设为 any 类型的地方,通常都应该优先考虑设为 unknow 类型。 在集合论上,unknow 也可以视为所有其他类型(除了any)的全集,所以它和 any 一样,也属于 TypeScript 的顶层类型。 3.NEVER类型 由于不存在任何属于“空类型”的值,所以该类型被称为 never,即不可能有这样的值 never 类型的一个重要特点是,可以赋值给任意其他类型。 为什么 never 类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了 never 类型。因此,never 类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。 总之,TypeScript 有两个“顶层类型”(any和unknow),但是“底层类型”只有 never 唯一一个。
Electron上下文隔离
上下文隔离 上下文隔离是什么? https://www.electronjs.org/zh/docs/latest/tutorial/context-isolation#%E4%B8%8A%E4%B8%8B%E6%96%87%E9%9A%94%E7%A6%BB%E6%98%AF%E4%BB%80%E4%B9%88 上下文隔离功能将确保您的预加载脚本 和 Electron的内部逻辑 运行在所加载的 webContent 网页之外的另一个独立的上下文环境里。 这对安全性很重要,因为它有助于阻止网站访问 Electron 的内部组件 和 您的预加载脚本可访问的高等级权限的API 。 这意味着,实际上,您的预加载脚本访问的 window 对象并不是网站所能访问的对象。 例如,如果您在预加载脚本中设置 window.hello = 'wave' 并且启用了上下文隔离,当网站尝试访问window.hello对象时将返回 undefined。 上下文隔离禁用 在渲染进程中,预加载脚本暴露给已加载的页面 API 是一个常见的使用方式。 当上下文隔离时,您的预加载脚本可能会暴露一个常见的全局 window 对象给渲染进程。 此后,您可以从中添加任意的属性到预加载在脚本。 preload.js // 上下文隔离禁用的情况下使用预加载 window.myAPI = { doAThing: () => {} } doAThing 函数可以在渲染进程中直接使用 renderer.js // 在渲染器进程使用导出的 API window.myAPI.doAThing() 启用上下文隔离 Electron 提供一种专门的模块来无阻地帮助您完成这项工作。contextBridge 模块可以用来安全地从独立运行、上下文隔离的预加载脚本中暴露 API 给正在运行的渲染进程。 API 还可以像以前一样,从 window.myAPI 网站上访问。 preload.js // 在上下文隔离启用的情况下使用预加载 const { contextBridge } = require('electron') contextBridge.exposeInMainWorld('myAPI', { doAThing: () => {} }) renderer.js // 在渲染器进程使用导出的 API window.myAPI.doAThing() 安全事项 https://www.electronjs.org/zh/docs/latest/tutorial/context-isolation#%E5%AE%89%E5%85%A8%E4%BA%8B%E9%A1%B9 单单开启和使用 contextIsolation 不直接意味着您所做的一切都是安全的。 例如,此代码是 不安全的。 preload.js // ❌ 错误使用 contextBridge.exposeInMainWorld('myAPI', { send: ipcRenderer.send }) 它直接暴露了一个没有任何参数过滤的高等级权限 API 。 这将允许任何网站发送任意的 IPC 消息,这不会是你希望发生的。 相反,暴露进程间通信相关 API 的正确方法是为每一种通信消息提供一种实现方法。 preload.js // ✅ 正确使用 contextBridge.exposeInMainWorld('myAPI', { loadPreferences: () => ipcRenderer.invoke('load-prefs') })