阅读视图
🌍geotools入门
🔗axios源码解析
React:如何做到“可中断”的渲染?
从 node_modules 逆向生成 package-lock.json 的救火实战
背景
在风和日丽的下午,小C 正在愉快的敲着代码,突然接到一个任务,说需要紧急开发一个介绍文件的下载页面,并部署到线上。
小C 询问了项目仓库地址,娴熟地拉下了代码,准备重新安装依赖时,发现项目中没有 package-lock.json 文件(心态崩了,因为这个项目 4 年前维护的,package-lock.json 文件早就被删除了,无法追溯😭)。
因开发时间有限,小C 需要尽快解决这个安装依赖问题,否则会影响到项目进度。
问题分析🤔
为什么需要 package-lock.json 文件?
在了解 package-lock.json 文件之前,先了解一下 package.json 文件,如下图所示:
在 package.json 中,依赖的版本号存在大版本、小版本、补丁版本
- 大版本:表示主版本号,表示重大更新,可能不兼容旧版本
- 次版本: 表示次版本号,表示次要更新,可能兼容旧版本
- 补丁版本:表示修订号,表示 bug 修复,可能兼容旧版本
而 ~
表示 npm install 会安装符合补丁版本的最新版本, 例如,~1.2.3会允许安装1.2.4,但不允许1.3.0
^
会更加灵活一些,表示 npm install 会安装符合次版本号的最新版本, 例如,^1.2.3可以安装1.3.0,但不包括2.0.0
同归问题,当小 A 和小 C 在不同的时间安装依赖时,可能会出现不同的依赖版本,从而导致项目无法正常运行。
因此,为了保证在不同的时间安装依赖时,依赖版本的一致性,需要使用 package-lock.json 文件。
回归业务场景
目前这个 4 年前项目中没有 package-lock.json 文件,如果随意安装最新的版本,可能导致版本不一致,无法确保项目正常稳定地运行。
在焦头烂额之际,小C 突然发现生产环境的容器里面存在 node_modules 文件夹,并确认了构建时的 Node 版本。
小C 灵机一动,如果从生产环境的容器里面拷贝 node_modules 文件夹,基于 node_modules 生成 package-lock.json 文件,是否可以解决这个问题呢?
小C 说干就干,拷贝了 node_modules 文件夹,并安装了依赖,项目成功运行了。
通过 node_modules 文件生成 package-lock.json
生成初始 lock 文件
npm install --package-lock-only --offline
#// --package-lock-only:不修改 node_modules
#// --offline:离线安装
最后将 package-lock.json 文件放入版本控制。
生成 lock 文件的原理
在执行 npm install 的过程如下
-
初始条件判断
- 存在 package-lock.json
- 不存在 package-lock.json
-
依赖解析阶段 npm 会递归分析每个依赖包的 dependencies、devDependencies 和 peerDependencies,形成完整的依赖树。
-
缓存检查与下载
- 检查缓存中是否存在该依赖
- 如果存在,则直接使用缓存中的依赖
- 如果不存在,则从 npm 仓库下载依赖
- 下载的依赖会存储在缓存中,方便下次使用
-
node_modules 构建
- 根据依赖树,构建 node_modules 目录结构
-
lock 文件生成/更新
归回业务场景,小C 拷贝了生产环境的 node_modules 文件夹,在执行 npm install 时,因存在缓存包,复用了 node_modules 文件夹,进而生成了和生产环境依赖版本一致的 package-lock.json 文件,确保了项目正常运行。
总结
在没有 package-lock.json 文件 并确保 Node 版本一致的情况下,可以通过执行 npm install --package-lock-only --offline
命令,基于 node_modules 文件夹,生成 package-lock.json 文件,确保项目正常运行。
通过打包后的源码解析 Webpack 懒加载原理 🤓🤓🤓
面试导航 是一个专注于前、后端技术学习和面试准备的 免费 学习平台,提供系统化的技术栈学习,深入讲解每个知识点的核心原理,帮助开发者构建全面的技术体系。平台还收录了大量真实的校招与社招面经,帮助你快速掌握面试技巧,提升求职竞争力。如果你想加入我们的交流群,欢迎通过微信联系:
yunmz777
。
Webpack 的懒加载通过动态 import()
语法实现按需加载模块。它将代码分割成多个独立的 chunk,只有在需要时才加载相应的模块,从而减少初始加载的代码量。通过 SplitChunksPlugin
,Webpack 还可以提取共享依赖,避免重复加载。开发者还可以使用 webpackChunkName
注释自定义 chunk 名称,进一步优化加载过程。
demo 演示
首先我们先来看一个简单的 demo 演示吧,如下目录结构所示:
接着我们有这样的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="button">点击按钮懒加载</button>
</body>
<script src="../dist/main.js"></script>
</html>
// a.js
export default () => {
console.log("Moment");
};
// main.js
const buttonEle = document.getElementById("button");
buttonEle.onclick = function () {
//懒加载 a 模块
import("./a").then((module) => {
const callback = module.default;
callback();
});
};
除了这些基础的配置之外,我们还有一些 webpack 配置和相关的依赖:
module.exports = {
mode: "development",
entry: "./src/main.js",
};
还有相关的 package.json 信息:
{
"name": "lazy",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --config ./webpack.config.js --env production"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"webpack": "^5.99.5"
},
"devDependencies": {
"webpack-cli": "^6.0.1"
}
}
我们执行 pnpm build
之后会得到如下文件:
其中,src_a_js.js
就是通过 import("./a")
懒加载的模块:
我们在浏览器中打开 index.html,打开控制台:
首次加载只能看到只加载了 main.js 这个文件,而且浏览器上是没有任何的输出。
当我们点击按钮之后,才会执行后,才会执行 src_a_js.js
模块,控制台打印 Moment
原理解析
首先我们先来讲解一下大概的执行流程:
-
点击按钮后,加载模块:通过
jsonp
异步加载a.js
模块对应的文件。 -
执行并合并模块:加载回来的
a.js
文件会在浏览器中执行,并将模块定义合并到main.js
的__webpack_modules__
中。 -
加载并缓存模块:模块加载后会被缓存,以便后续使用时直接返回缓存的内容。
-
导出并使用模块内容:获取并使用该模块导出的功能或对象。
1. __webpack_require__.e
__webpack_require__.e(chunkId)
是 Webpack 用于异步加载模块的关键方法。它返回一个 Promise
,并触发指定 chunkId
对应的模块(chunk)加载。当多个异步模块需要加载时,Webpack 会利用 Promise.all
确保在所有异步模块加载完毕之后,才执行后续的操作。调用 __webpack_require__.e("src_a_js")
时,Webpack 会根据 chunkId
加载对应的代码文件,确保所有必要的模块都已加载完成。
chunkId 一般为打包后的文件名:
如下代码所示:
__webpack_require__.e("src_a_js").then(() => {
// 所有异步模块加载完毕后执行的代码
});
2. __webpack_require__.f.j
__webpack_require__.f.j
是 Webpack 用来处理异步模块加载的核心函数。它会将异步模块的 Promise
添加到一个队列(promises
)中,并通过 installedChunks
跟踪每个模块的加载状态,以确保同一模块不会被重复加载。Webpack 维护一个 installedChunks
的 Map,记录每个 chunkId
的加载进度(是否已加载)。
每次加载异步模块时,__webpack_require__.f.j
会检查模块的加载状态,如果模块仍在加载中,它会将新的 Promise
加入 promises
队列,直到模块加载完成。这样,Webpack 能高效地管理模块加载,避免不必要的重复加载。
如下代码所示:
__webpack_require__.f.j = (chunkId, promises) => {
var installedChunkData = installedChunks[chunkId];
if (installedChunkData !== 0) {
if (installedChunkData) {
promises.push(installedChunkData[2]); // 推入正在加载的 Promise
} else {
// 开始加载模块并更新 installedChunks
var promise = new Promise((resolve, reject) => {
installedChunks[chunkId] = [resolve, reject];
});
promises.push((installedChunkData[2] = promise));
// 触发实际的异步加载过程
var url = __webpack_require__.p + __webpack_require__.u(chunkId);
__webpack_require__.l(url, loadingEnded, chunkId);
}
}
};
3. __webpack_require__.l
__webpack_require__.l
是 Webpack 用来异步加载模块的核心函数。它会动态创建一个 <script>
标签,向浏览器加载对应的异步模块文件(如 chunkId.js
)。
通过插入 <script>
标签,Webpack 向服务器请求所需的模块文件。当模块加载完成后,WebPack 会执行一个 IIFE(立即调用函数表达式),该函数将加载的模块内容添加到 Webpack 的模块缓存中,从而完成模块的加载和执行。
这种机制依赖于 JSONP 技术,通过动态加载 JavaScript 文件实现模块的按需加载。
如下代码所示:
__webpack_require__.l(url, done, chunkId);
4. IIFE 执行和 webpackChunk
的回调
当异步模块文件(如 src_a_js.js
)加载完成后,浏览器会执行返回的 IIFE 函数。这个 IIFE 会触发 webpackChunkwebpack.push
,通过 Webpack 维护的 webpackJsonpCallback
函数,将异步模块的内容合并到主模块的 __webpack_modules__
中。
具体来说,加载完成的异步模块文件会将 chunkId
、模块内容及运行时代码推送到 webpackChunkwebpack.push
中。随后,webpackJsonpCallback
函数会将这些模块内容添加到 Webpack 的 __webpack_modules__
缓存中,使得后续代码能够通过 __webpack_require__
正常访问和执行该模块。
这样,Webpack 确保了按需加载的模块能够正确集成进应用的模块系统中,支持异步加载和模块共享。
如下代码所示:
(self["webpackChunkwebpack"] = self["webpackChunkwebpack"] || []).push([
["src_a_js"], // chunkId
{
"./src/a.js": function (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const __WEBPACK_DEFAULT_EXPORT__ = () => {
console.log("Moment");
};
},
},
]);
5. webpackJsonpCallback
函数
webpackJsonpCallback
函数充当 Webpack 主模块与异步模块之间的桥梁。它将异步加载的模块内容合并到主模块的 __webpack_modules__
中,从而实现模块的同步访问。
具体来说,webpackJsonpCallback
会根据 chunkId
在 installedChunks
中找到对应的 Promise
,并调用 resolve
方法。一旦 resolve
被触发,异步模块的代码就会执行,之后 __webpack_require__
可以同步地访问该模块的内容。
这一过程确保了异步模块能够在加载完成后与主模块的模块系统正确集成,支持高效的按需加载。
如下代码所示:
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
for (var moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) runtime(__webpack_require__);
for (let i = 0; i < chunkIds.length; i++) {
installedChunks[chunkIds[i]] = 0;
}
parentChunkLoadingFunction && parentChunkLoadingFunction(data);
};
6. 完成加载:执行 __webpack_require__(chunkId)
当所有 Promise
完成时,__webpack_require__.e
返回的 Promise.all
会进入 then
方法,此时会调用 __webpack_require__(chunkId)
来执行并返回异步模块的导出内容(module.exports
)。
Promise.all
会等待所有异步模块加载完成后,才会执行后续的代码。此时,__webpack_require__(chunkId)
会像同步模块一样执行异步模块的代码,并返回其 module.exports
,确保模块在加载完成后能够正常访问。
如下代码所示:
__webpack_require__(chunkId).then((module) => {
// 执行异步模块的代码,返回 module.exports
});
总结
Webpack 的懒加载通过按需加载模块来优化性能。当某个模块被异步加载时,Webpack 会将其拆分为单独的 chunk,只有在需要时才会加载这些模块。使用 __webpack_require__.e
方法触发模块加载,并通过 Promise
确保模块加载完成后再执行后续操作。模块加载完成后,Webpack 会将其缓存,以便后续访问时直接使用缓存的内容,从而减少不必要的加载和提高页面加载速度。
它的加载原理如下步骤所示:
-
__webpack_require__.e
返回一个Promise
,确保异步模块加载完成。 -
__webpack_require__.f.j
将异步模块的Promise
添加到加载队列。 -
__webpack_require__.l
创建<script>
标签,加载异步模块。 -
异步模块加载完成,执行 IIFE,调用
webpackJsonpCallback
。 -
webpackJsonpCallback
合并异步模块到主模块的__webpack_modules__
中。 -
异步模块的
Promise
被解析,执行__webpack_require__(chunkId)
,返回模块内容。
通过这种方式,Webpack 实现了异步模块加载、按需加载和模块缓存,使得浏览器能够高效地加载和执行模块代码。
highlight.js支持动态代码
背景
最近在做一个根据json自动生成java vo, ts interface, go struct的小工具,前端使用vue3 + typescript来实现。为了更好的用户体验,决定使用highlight.js来做生成代码的语法高亮。在使用过程中,发现只有第一次生成的代码可以语法高亮,后续再生成就不支持语法高亮了。通过搜索发现highlight.js不支持动态代码的高亮。
目标
通过改造,可以支持动态代码的高亮
解决方法
首先我们看下基础的使用
首先是html骨架
<div class="output-section">
<pre><code id="code" :class="targetLanguage">{{ outputClass }}</code></pre>
</div>
然后是语法高亮渲染
const convertJson = () => {
try {
const jsonObj = JSON.parse(inputJson.value);
outputClass.value = generateClassDefinition(jsonObj, targetLanguage.value);
console.log(outputClass.value);
nextTick(() => {
const codeElement = document.getElementById('code');
hljs.highlightElement(codeElement!);
});
} catch (error) {
outputClass.value = '❌ 无效的JSON格式';
}
};
这里通过nextTick实现对于code元素渲染后的高亮操作
我们可以看下渲染后的dom
<code data-v-f51febca="" id="code" class="java hljs language-java" data-highlighted="yes"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">AutoGenerated</span> {
<span class="hljs-keyword">private</span> <span class="hljs-type">int</span> age;
<span class="hljs-keyword">private</span> String name;
}
</code>
我们可以发现,如果不使用highlight.js,理论上生成的code,内容应该就是一个标准字符串,使用后highlight.js增加了一些span元素进去
那么,我们可以不可以每次需要重新渲染时,就将code的内容改变,重新调用一次hightlight.js呢?我们先试一下
改造后的代码如下
const convertJson = () => {
try {
const jsonObj = JSON.parse(inputJson.value);
outputClass.value = generateClassDefinition(jsonObj, targetLanguage.value);
console.log(outputClass.value);
nextTick(() => {
const codeElement = document.getElementById('code');
if (codeElement) {
// 先移除所有highlight相关的class
codeElement.className = targetLanguage.value;
// 强制重新渲染
codeElement.innerHTML = outputClass.value;
}
// 重新应用highlight
hljs.highlightElement(codeElement!);
});
} catch (error) {
outputClass.value = '❌ 无效的JSON格式';
}
};
相比之前的变化是,多了一次判断,就判断codeElement已经存在的话,通过innerHTML先修改code元素的值,然后再通过highlight.js来做语法高亮。修改完成之后,发现还是只有第一次可以语法高亮,而且后面的dom中也没有span之类的标签了
i继续观察dom,发现相比预期,code元素多了一个属性data-highlighted="yes",有理由怀疑highlight.js是根据这个属性来判断要不要做高亮操作。尝试着移除这个属性试试。改造后的代码如下
const convertJson = () => {
try {
const jsonObj = JSON.parse(inputJson.value);
outputClass.value = generateClassDefinition(jsonObj, targetLanguage.value);
console.log(outputClass.value);
nextTick(() => {
const codeElement = document.getElementById('code');
if (codeElement) {
// 先移除所有highlight相关的class
codeElement.className = targetLanguage.value;
// 强制重新渲染
codeElement.innerHTML = outputClass.value;
// 移除属性k
codeElement.removeAttribute('data-highlighted');
}
// 重新应用highlight
hljs.highlightElement(codeElement!);
});
} catch (error) {
outputClass.value = '❌ 无效的JSON格式';
}
};
这里多了一行,即通过removeAttribute方法来移除已有的属性,再次尝试,果然可以了。
广告
工具地址
kennethfan.github.io/web-tools/j…
这是一个个人开发的工具集,代码已经分享到github,欢迎各位共建
参考
前端如何监控页面异常
如何在浏览器中使用 View Transitions API 实现无缝页面动画
使用flutter开发的Windows桌面壁纸软件
在swiftui项目中使用WKWebView加载自定义脚本文件
1.确保你的 custom.js
文件已添加到项目目标中(在文件检查器中勾选目标成员资格)
2.对于复杂的 JavaScript 交互,可能需要使用 WKUserContentController
和 WKScriptMessageHandler
3.考虑 Web 内容加载时间,脚本注入应在页面加载完成后进行(如示例中的 didFinish navigation
回调)
4。如果需要与 Swift 代码通信,可以使用 evaluateJavaScript(_:completionHandler:)
方法
先添加js文件到项目中:
按照提示添加后,项目中就会显示你添加的文件:
然后再使用自定义一个加载文件逻辑:
extension WebView {
static func loadJSFile(named filename: String) -> String? {
guard let path = Bundle.main.path(forResource: filename, ofType: "js") else {
print("Could not find \(filename).js in bundle")
return nil
}
do {
let jsString = try String(contentsOfFile: path, encoding: .utf8)
return jsString
} catch {
print("Error loading \(filename).js: \(error)")
return nil
}
}
}
最后添加:
// 2. 加载并注入自定义脚本
if let customScript = WebView.loadJSFile(named: "custom") {
let userScript = WKUserScript(
source: customScript,
injectionTime: .atDocumentStart,
forMainFrameOnly: false
)
webView.configuration.userContentController.addUserScript(userScript)
}
// 3.load url
webView.load(URLRequest(url: url))
CSS选择器(选择器的权重):细化网页样式的秘密武器
Electron 跨平台开发避坑指南:兼容性问题解析与解决方案实践
作用域和闭包
一、作用域
作用域是一套规则,用于确定在何处、如何查找变量。
在理解作用域前,我们要先理解js对变量进行编译处理的过程。
1、变量声明的编译过程
var a = 2;
在这行代码中,js会做以下2件事。
-
声明变量a:查找当前作用域中是否已有同名变量,如有则忽略该声明。同一个作用域内多次用var声明同一个变量名不会报错(let/const声明会)。
-
赋值:如果当前作用域找不到,会顺着作用域链往上层找,找到a后赋值。如果都没找到,就会在最上层的作用域声明变量a后赋值。
function fn() {
// var a = 3 // 声明fn内的局部变量a
a = 3 // 声明全局变量a
}
2、LHS查询和RHS查询
在作用域中查找变量的方式,可分为两种类型:LHS查询和RHS查询。
LHS查询:查找以赋值
RHS查询:查找以使用
function foo(a) {
var b = a
return a + b
}
var c = foo(2)
如以上代码,实际发生了:
-
定义foo函数,还没有执行,不发生查询
-
到第5行,开始执行代码,声明全局变量c,查找c以赋值,1次LHS
-
查找foo函数以执行,1次RHS
-
进入foo内部的函数作用域,对形参a赋值(隐式),1次LHS(容易被忽略)
-
声明局部变量b,查找b以赋值,1次LHS;查找a以使用,1次RHS
-
执行+,查找a和b以使用,2次RHS
3处LHS:c、a(隐式赋值)、b
4处RHS:foo、a、a、b
3、为什么要区分LHS查询和RHS查询?
在变量未声明(所有作用域都无法找到变量)的情况下,两种查询方式进行的行为不同。
var a = 2
console.log(a + b); // 对b的RHS查询,会报错
b = a // 对b的LHS查询,不会报错
如以上代码,对b进行了一次RHS一次LHS,进行RHS查询时,找不到变量,会抛出ReferenceError异常;进行LHS查询时,找不到变量,会创建变量后返回。(非严格模式)
(严格模式下禁止隐式创建变量,因此在LHS查询失败时也会抛出ReferenceError异常)
一般来说,ReferenceError异常表示寻找变量过程中的相关异常,TypeError异常表示找到变量了、但对变量的操作错误(如对非函数类型进行函数调用)
二、词法作用域
1、词法作用域的定义
作用域有2种主要的工作模型,词法作用域(也称为静态作用域)和动态作用域。大部分语言都采用的词法(js也是),也有一些语言使用动态。
词法作用域指定义在词法阶段的作用域。
function foo() {
console.log(a);
}
var a = 2
function foo2() {
var a = 3
foo()
}
foo2() // 2
如以上代码。foo函数在定义时作用域就已经确定了,无论函数在何处、被如何调用,作用域都不会发生改变。因此它执行时作用域链=全局→foo,foo内找不到就会去全局找。除非foo在foo2内定义,否则它不会找到foo2内的a变量。
相对地,动态作用域指的就是运行时才确定的作用域,类似js中的this机制。
2、修改词法作用域
(1)eval()函数
接收一个字符串作为参数,会将其中的内容视为代码执行(不支持es6语法),可用于动态插入代码
function foo(str, a) {
eval(str);
console.log(a, b); // 1. 3
}
foo("var b = 3", 1);
如以上代码,var b = 3会直接在eval的位置执行。没有eval时,b会向上查找全局变量,找不到后返回undefined。但增加了eval代码,foo就会有一个函数作用域内的局部变量b。
在严格模式下,eval会有自己单独的作用域,b会是eval作用域内的变量,eval就不会对作用域产生影响。
(2)with
with是用于快速重复引用同一个对象属性的,它可以将传入的对象处理为隔离的词法作用域。
var obj = {
a: 1,
b: 2,
c: 3,
};
// 会创建一个obj内的词法作用域
with (obj) {
a = 3;
b = 4;
c = 5;
d = 6; // 当对象不存在时,变量会被泄漏到with所处的上级作用域中
}
如以上代码,使用with,可以快速更改obj.a、obj.b、obj.c的值。但由于with创建了一个作用域,在其中修改d时,由于没有obj.d属性,它就会往上层作用域找,导致创建了一个全局变量d。
在严格模式下,是不允许使用with的。
(3)修改作用域的坏处
以上方式,虽然可以更改已确定了的作用域,但会造成性能损耗。
因为在编译阶段,js引擎会对代码做一个简单分析,提前确定所有变量和函数的定义位置,以便在执行时快速查找。修改作用域,这个分析也就不起作用了,会拖慢代码的运行效率。
三、函数作用域和块作用域
1、函数作用域
函数会创建自身的作用域,而作用域是一层一层向上访问的。因此,可以说函数作用域达到了隐藏代码的效果(它能访问外部,外部访问不到它)。这个特点也符合软件设计中的最小暴露原则(最小限度地暴露必要内容)。
除了函数声明以外,也可以使用函数表达式(IIFE)的方式:
// 两种方式都可以
(function foo(){})()
(function foo(){}())
函数表达式的好处在于foo在外部作用域也是访问不到的,同时foo这个名称也是可以省略的。函数声明需要具名,函数表达式可以不具名。
2、块级作用域
块级作用域指用{}包裹起来的代码内的作用域,常见的有if else和for循环内的代码块作用域。使用块级作用域的好处有:
(1)利于垃圾回收机制
function process(data) {}
// let data = { a: 1 };
// process(data);
{
let data = { a: 1 };
process(data);
}
var btn = document.getElementById("btn");
btn.addEventListener("click", function (evt) {
console.log("clicked");
});
如上,执行到事件绑定时,如果不显式地声明块级作用域,js引擎会认为上面声明的变量可能仍会在绑定事件中使用,不会回收。而显式使用块级作用域,往下执行时不能再访问到作用域的内容,引擎就会知道这段代码不需要保留、可以进行回收。
(2)循环时重新绑定
在循环中,当使用let声明变量时,每次循环的i值都是独立的
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 01234
}, 0);
}
这是因为它的内部执行如下,在每次循环的块级作用域内,都会重新声明一个变量绑定赋值,重新声明的 j 仅在当前次循环的块级作用域内生效,不会对之前或之后的循环产生影响。
{
let i
for (i = 0; i < 5; i++) {
let j = i
setTimeout(() => {
console.log(j); // 01234
}, 0);
}
}
四、声明提升
变量和函数的声明会被提升到当前作用域的最前方最先执行,函数声明又优先于变量声明。let/const和函数表达式不会被提升。
foo(); // 1
var foo; // 重复声明被忽略,用let会报错
function foo() { // 会被提升到最前最先声明
console.log(1);
}
foo = function () { // 赋值操作,foo()执行完才会执行
console.log(2);
};
foo(); // 3
function foo() {
console.log(1);
}
var foo = function () {
console.log(2);
};
function foo() {
console.log(3);
}
// 两个函数声明都会被提升,后面的覆盖前面的,然后才是变量重复声明、赋值
五、作用域闭包
1、什么是闭包
在函数a内声明函数b,将函数b作为返回值,使得b在a以外的作用域被执行时,仍然能够访问a作用域内的定义。闭包的特点是函数的定义和执行不在同一个作用域。
function f1() {
let n = 999;
function f2() {
console.log(n);
}
return f2;
}
const f3 = f1()
f3()
如上代码,可以看到,f2虽然是f1作用域内定义的,却在全局作用域执行了,并访问到了f1作用域内的变量n。
闭包的特征
-
函数的返回值至少包含一个对内部函数的引用;
-
函数被调用(当f1未被调用时,不会创建f1作用域,也就不存在闭包)
闭包的缺点
一般来说,f1执行后,js的垃圾回收机制会认为f1作用域用不到了并进行回收。但由于闭包的影响,虽然f1已经执行了,但f2还有可能被执行,所以f1作用域不会被回收。因此,闭包过多时会占用太多内存,造成内存泄漏。
闭包的应用
闭包在实际开发中应用很多,比如常见的模块,在模块中创建私有函数,模块外创建实例以调用私有函数,就是典型的闭包。还有如对象的私有方法、定时器传参函数,都是闭包的应用。
2、函数柯里化
函数柯里化也是闭包的一个应用,它指只传递给函数一部分参数以调用,函数内部返回另一个函数去处理剩下的参数。
如以下代码,add是一个累加函数,累加数值会以单次传参的形式传入。
function add(a) {
let ret = a;
return function add2(b) {
if (b) {
ret = ret + b;
return add(ret);
} else {
return ret;
}
};
add(1)(2)() // 3
add(1)(2)(3)(4)() // 10
add(1)(1)(1)(1)(1)(1)(1)(1)(1)(1)() // 10
你不知道的Git忽略文件新方式
正常来说
我们忽略不提交的文件都是配置.gitignore
或者就是在IDE里将不提交的文件放入其他的变更列表
但是
两种方式在某些时候都并不想使用
因为我仅仅只是想让这个文件的更改只运用在我本地环境而已
例如配置文件
(打个比方)
配置忽略吧,以后可能还会加新的东西
不配置忽略吧,不小心提上去又会影响到同事的本地环境
放入变更列表吧,每次看着都难受,变更分支还得git stash
然后我就去了解了一下....
在 Git 中实现仅本地忽略文件修改,且不修改 .gitignore
文件,可通过以下两种方法实现:
方法一:标记文件为“假设未修改”
-
执行命令
在终端输入:git update-index --assume-unchanged <文件路径>
此命令会告知 Git 将该文件视为未修改状态,后续变更不会显示在
git status
中。 -
取消忽略
若需恢复跟踪,执行:git update-index --no-assume-unchanged <文件路径>
适用场景
需临时忽略本地修改(如配置文件调试),但需手动维护忽略状态,且换仓库后需重新设置。
方法二:标记文件为“跳过工作树”
-
执行命令
git update-index --skip-worktree <文件路径>
此命令会完全跳过对文件工作树状态的检查,即使文件被修改,Git 也不会检测到变更。
-
取消忽略
git update-index --no-skip-worktree <文件路径>
适用场景
需长期忽略本地修改(如 IDE 生成文件),且希望彻底屏蔽变更检测。
总结
-
两种方法对比
方法 特点 推荐场景 --assume-unchanged
仅标记文件为未修改,Git 仍可能偶尔检测到变更 临时调试或短期忽略 --skip-worktree
完全跳过文件状态检查,忽略更彻底 长期忽略或频繁变更的文件 -
注意事项
- 两种方法均为本地配置,不会影响
.gitignore
或远程仓库。 - 以上指的 <文件路径> 是绝对路径,如:
E:\shop\src\main\resources\application.yml
- 两种方法均为本地配置,不会影响
最后
不管你会与否,看看有个记忆。
感谢前来了解,大家共同进步,相互分享知识。
🔥为啥一行代码就可以让当前页代码显示到网页中?
看过白泽开源团队出品的baize-quick-study
的小伙伴们,可能或多或少都会有这样的一个问题,为啥一行<CodeDemo />
就可以让当前页的代码
显示到网页中?这到底是什么神奇
的写法
呀?
链接:baize-quick-study.pages.dev/main
莫慌莫慌,待我们来一步步揭秘。
拉好扶手,焊死车门,一个也不准下车!!!
代码渲染
咱们先找到CodeDemo
的代码,看看它里面做了些什么操作?
哦,easy,咱们可以看到里面划分了父子组件(CodeDemo 和 CodeDemoItem)。
然后其中的CodeDemoItem,不就是用highlight.js
对代码进行渲染吗?
秒了,完结撒花!!!
嘿嘿,看到这,老夫只想说一句:
年轻人是不是高兴的太早了?
年轻人是不是高兴的太早了?
年轻人是不是高兴的太早了?
哎呀,重要的事情就是容易不小心多说了两遍。反正撤不回了,那就继续往下讲吧。
是的,通过highlight.js
来渲染代码是没有问题的,但是数据
怎么来的?
咱们继续看父组件
CodeDemo,可以看到是从props中获取数据,然后传递给CodeDemoItem进行渲染的。
好像也没啥问题呀!
小伙伴们有没有发现遗漏了一个问题?
咱们外部使用组件时,也没有传入
codeData、codePath、fileListCode这些props啊,为啥它能拿到的?
糟糕,脑子好痒,好像要长脑子了!!!
emmm,确实常规写法
是要从外层组件中传入props的
但是,有没有一种可能,这不是常规写法呢?
众所周知,像react、vue这样的代码,直接放在浏览器中是无法直接运行的,需要通过一层转译
才可以执行的。又或者es6怎么运行在低版本的浏览器中?
一般来说,这层转译都是通过babel
来处理的,那你说有没有可能CodeDemo的数据也是这样获取的?
是的,你猜对了。
答案就是:vite 自定义插件
+ babel
+ ast
插件详解
插件入口
咱们可以在vite.config.ts
中找到插件的入口
下面用react版本的vite插件进行讲解
拦截 tsx
首先,我们需要在插件的 load
钩子中拦截所有后缀为 .tsx
的文件。通过自定义插件,我们可以让 Vite 在构建时对这些文件进行处理。
function viteRenderCode(): PluginOption {
return {
name: "vite-render-code",
enforce: "pre",
load(id) {
// 拦截所有的tsx文件
if (id.endsWith(".tsx")) {
}
}
}
注入props
通过解析文件的 AST
,我们可以检查每个 React 组件的定义。我们需要判断组件名称是否为 CodeDemo
。找到 CodeDemo
组件后,我们将提取其相关参数(例如 props
中的 codeData
和 codePath
)并对其进行修改或注入新的值。
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
const currentPath = id;
traverse.default(ast, {
JSXOpeningElement(path) {
if (path.node.name.name === "CodeDemo") {
// 将 code 作为 props 注入到 CodeDemo 组件中
// 将fileList构建成对应的ast节点
}
},
});
显示其他文件
需要注意的是fileList
,这个主要是让我们可以将一些想显示的文件也一并显示出来。这里主要是通过ast语法树
来构建ast节点
。
const astFileListCode = t.jsxAttribute(
t.jsxIdentifier("fileListCode"),
t.jsxExpressionContainer(
t.arrayExpression(
fileListCode.map((item) =>
t.objectExpression([
t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
]),
),
),
),
);
path.node.attributes.push(astFileListCode);
完整代码
// viteRenderCode.ts
import { join } from "path";
import { PluginOption } from "vite";
import { readFileSync } from "fs";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import generate from "@babel/generator";
import t from "@babel/types";
const htmlEntities: { [key: string]: string } = {
"&": "&",
"<": "<",
">": ">",
"'": "'",
'"': """,
"`": "`",
"^": "ˆ",
"~": "˜",
"—": "—",
"•": "•",
"–": "–",
"?": "?",
":": ":",
$: "$",
};
const escapeHtml = (str: string) => {
return str?.replace(/[&<>'"`^~—•–?:$]/g, (tag) => htmlEntities[tag] || tag);
};
const getReactComponentProps = ({ data, name }) => {
return {
type: "JSXAttribute",
name: {
type: "JSXIdentifier",
name: name,
},
value: {
type: "StringLiteral",
value: data,
},
};
};
const addReactCompoentProps = ({ path, data, name }) => {
const params = getReactComponentProps({
data,
name,
});
path.node.attributes.push(params);
};
function viteRenderCode(): PluginOption {
let _originalConfig;
let _resolvedConfig;
let _basePath = join(process.cwd(), "..");
return {
name: "vite-render-code",
enforce: "pre",
configResolved(resolvedConfig) {
_resolvedConfig = resolvedConfig;
},
config(config) {
_originalConfig = config;
},
load(id) {
if (id.endsWith(".tsx")) {
const code = readFileSync(id, "utf-8");
if (code.indexOf("<CodeDemo") !== -1 && id.indexOf("CodeDemo") === -1) {
const ast = parse(code, {
sourceType: "module",
plugins: ["typescript", "jsx"],
});
const currentPath = id; // id.replace(_basePath, "");
traverse.default(ast, {
JSXOpeningElement(path) {
if (path.node.name.name === "CodeDemo") {
// 将 code 作为 props 注入到 CodeDemo 组件中
const codeProp = path.node.attributes.find((attr) => attr.name.name === "codeData");
if (!codeProp) {
addReactCompoentProps({
path,
data: escapeHtml(code),
name: "codeData",
});
addReactCompoentProps({
path,
data: currentPath,
name: "codePath",
});
}
const fileListProp = path.node.attributes
.find((attr) => attr.name.name === "fileList")
?.value.expression.elements.map((item) => item.value);
if (fileListProp) {
const fileListCode = [];
for (let item of fileListProp) {
const curAlias = item.split("/")[0];
const filePath = item.replace(curAlias, _originalConfig.resolve.alias[curAlias]);
const fileCode = readFileSync(filePath, "utf-8");
fileListCode.push({
fileCode: escapeHtml(fileCode),
filePath: filePath, // filePath.replace(_basePath, ""),
});
}
const astFileListCode = t.jsxAttribute(
t.jsxIdentifier("fileListCode"),
t.jsxExpressionContainer(
t.arrayExpression(
fileListCode.map((item) =>
t.objectExpression([
t.objectProperty(t.stringLiteral("fileCode"), t.stringLiteral(item.fileCode)),
t.objectProperty(t.stringLiteral("filePath"), t.stringLiteral(item.filePath)),
]),
),
),
),
);
path.node.attributes.push(astFileListCode);
}
}
},
});
const { code: transformedCode } = generate.default(ast);
return transformedCode;
}
return code;
}
return null;
},
};
}
export default viteRenderCode;
小结
本文主要介绍了如何通过编写一个自定义的 Vite 插件
,结合AST
,将我们项目中的真实代码
动态展示到网页中。通过本文,希望小伙伴们可以学习到在构建过程中如何处理文件,解析其中的组件,并根据需要注入特定的属性或代码,进而掌握通过ast修改源码
以及开发vite插件
的能力。
Chat2DB创始人姬朋飞:AI在 text2sql应用领域的实践
2025年5月17日,第76期DataFunSummit:AI Agent技术与应用峰会将在DataFun线上社区举办。Manus的爆火并非偶然,随着基础模型效果不断的提升,Agent作为大模型的超级应用备受全世界的关注。为了推动其技术和应用,本次峰会计划邀请40+位深耕AI Agent领域的专家学者同台分享交流,共同探讨技术进展和应用形式。
在5月17日上午的DataAgent论坛上,爱獭科技CEO姬朋飞将出席此次会议,并带来主题分享:《AI在 text2sql应用领域的实践》
专家介绍:
Chat2DB 创始人,Easyexcel 作者
演讲提纲
一、业务背景与挑战
传统SQL开发痛点分析
业务分析场景特点
现有解决方案的局限性
项目目标与技术挑战
二、技术方案设计
整体架构
系统架构设计
核心技术模块
数据流转流程
关键技术突破
领域知识增强的Prompt设计
多阶段生成策略详解
RAG检索增强方案
人类反馈闭环机制
三、落地实践与优化
实践难点攻克
复杂查询处理
跨库查询优化
性能提升方案
效果优化策略
准确率提升方法
业务知识积累
错误处理机制
四、应用成效与经验总结
应用效果
准确率指标
效率提升数据
业务价值体现
经验与展望
落地经验总结
最佳实践分享
未来演进方向
五、互动问答 (10分钟)
Key Takeaways:
大规模落地经验
技术创新点
实践中的坑与解决方案
产品化建议
听众收益:
技术实践价值
深入了解Text2SQL在大规模商业场景的完整解决方案
掌握LLM结合业务知识的关键技术路径和实现方法
获取准确率优化、复杂查询处理等技术难点的解决思路
学习RAG、多阶段生成等前沿技术的实战应用
工程落地经验
获得大规模AI系统从0到1的完整建设经验
了解项目推进过程中的典型坑点和规避方法
掌握效果优化、成本控制的实用策略
学习性能调优和系统可用性保障的实践经验
落地挑战和方案重点
1.数据准确性
2.权限
报名方式
大会议程
为何 css 写了不生效?
前言
前端开发有时候我明明写了 css 属性但是样式却没能在浏览器中生效,这其实很大概率原因就是你可能还不了解 css 属性值的计算过程,本期文章就带大家过一遍这部分知识,或许有你不清楚的内容🤷♂️
css 属性值的计算过程
CSS属性值的计算过程指的是浏览器渲染引擎将 CSS 属性从声明到最终渲染的处理流程,直白点就是 css 属性从没有值 到 有值的 过程。
css 属性并不是不写 css 就没有属性了,浏览器会有个 用户代理样式表 user agent stylesheet
,你可以理解为浏览器默认的样式
比如 我写一个 h1
元素,不给他写任何属性,默认就会有如下样式
这里你会发现 h1 默认样式中有个
display: block;
这其实就是为啥 h1 是一个块级元素,就是因为浏览器赋予了它这个属性,仅此而已,在 html 语义化出来之前你可以这样叫,什么块级元素都是html5
之前的叫法,h5 出来之后官方摒弃了这个说法,如今你要知道的是这都是 css 属性默认值的效果
当我们展开 computed,你就会看到更多的属性,computed 就是这个属性最终计算出来的样式,实际上 computed 中有这个 元素 的所有样式属性
css 属性值的计算过程总览:
- 确定声明值
- 层叠
- 继承
- 使用默认值
第一步:确定声明值
这个会从两个表中获取,一个是作者样式表,一个就是上面提到的 浏览器默认样式表,其英文名为 user agent stylesheet
,这里直译起来是用户代理样式表不方便理解。两个样式表的样式都是声明值
早期其实还有个
user stylesheet
用户样式表,这个样式表在早期的 ie 浏览器存在,在 IE 中,可以转到Tool
>Internet Options
>General Tab
>Accessibility button
>Accessibility Window
>User style sheet section
>“使用我的样式表格式化文档”复选框。其实就是用户在浏览器夹杂一些自己写的样式:参考stackoverflow css - 默认、用户和作者样式表有什么区别?_Stack Overflow中文网
作者样式表 就是我们写的 css 属性,若我们引入了 ui 库,这些 ui 组件的 样式也是 作者样式表
确定声明值 也有步骤:
- 找到 没有冲突 的样式,直接作为计算后的样式
- 将相对单位转成绝对单位
比如我给 h1 加一个 color 属性,color 在浏览器默认样式表中不存在,因此没有冲突,color 就会作为计算后的样式
浏览器默认样式表中的 h1 有一个 font-size: 2em;
em
就是相对单位,它相对的是父容器的 字体大小,我这儿没有给父容器,因此相对的是浏览器的默认字体大小 16px
,因此这里 h1 最终 computed 就是 32px 的 font-size。所以说假设我们给 h1 一个 父容器并且设置 font-size 15px,那么最终 h1 的font-size 就是 30px
相对单位不仅仅说的是 em,%,rem 这种,还包括了 font-weight: bold,color: red 等这种关键字,bold 就是对应 字重 700, red 就是对应 rgb(255, 0, 0)
第二步:层叠
层叠的目的就是一件事:解决冲突,这个过程才是最重要的
层叠有三个步骤:
- 比较重要性
- 比较特异性
- 比较源次序
先看重要性,重要性从高到低:
- 带有 important 的作者样式表
- 带有 important 的默认样式表
- 作者样式表
- 默认样式表
由于早期的 user stylesheet 已经不存在了,这里不做探讨
作者样式表 会覆盖 默认样式表 这很好说明
这里我在代码中写了 h1 标签的样式,默认样式表中的 font-size 就被覆盖了
这里我想要验证 带有 important 的默认样式表 会大于 作者样式表,但是始终没能成功,input 有个 属性 writing-mode,默认样式就有个 important 值,但是我写了不带 important 的 input 的 writing-mode 也能覆盖,这里有大佬清楚可以评论区补充下🚀。
第二步:比较特异性
特异性英文单词就是 specificity
,一般我们会说成权重
内联样式 | ID选择器 | 类选择器、属性选择器、伪类 | 元素选择器、伪元素 | 通配符、关系选择器 |
---|---|---|---|---|
权重:1000 | 权重:100 | 权重:10 | 权重:1 | 权重:0 |
style="color: red" |
#header |
.container [type="text"] :hover
|
div ::before
|
* > +
|
vscode 上 hover 样式选择器时会呈现最终的权重,不过由于不是写的内联样式,他只会给你呈现 后面三个值。
另外,mdn 上关于 css 权重有个 很形象的 图,这里可以看到 important 的权重是 10000
因此直观上感受,选择器内容越多,权重就会比较高
特殊伪类的权重计算
特殊伪类这里说的是 :is() 、:not() 、:where()
:is() 和 :not() 二者均是采用 参数中 最高特异性选择器的权重
如图这个例子,选取 item 中 值最高的 #id 作为最终权重,not 同理
:where() 权重始终为 0,无论参数中选择器的权重如何
where 的功能与 is 几乎相同,允许指定多个选择器,但是 where 权重始终为 0,这就是为了方便后续覆盖
为何要少用 !important
!important
权重是 10000,我们用它一时爽,后面维护起来就要骂娘了,主要是因为用 !important 会带来一些问题:
-
破坏 css 级联原则
css 全称 就是 Cascading Style Sheets 层叠样式表,层叠一词就能直接体现 选择器 优先级的重要性,当我们对某个样式使用了 !important 时,它就会绕过这个机制,这样样式覆盖我们就无法预测
-
难以调试
当多个 !important 冲突时,这个时候最终生效的样式只能是最后加载的样式,而非选择器权重,这样调试起来效率就很低
第三步:比较源次序
若前面的重要性一样,特殊性一样,那么最终就是源次序靠后的胜出
比如我在同一个选择器中对同一个 css 属性写了多次,那么最终只取最后一个值
div h1.className {
font-size: 10px;
font-size: 20px;
}
这里最终就是 20px 的 font-size
这个例子过于简单了,这里我再举一个隐形一点的🌰
比如这里我写一个样式
.parent {
width: 400px;
height: 400px;
padding: 20px;
border: 5px solid;
background-clip: content-box;
background: red;
}
background-clip
属性设置元素的背景是否延伸到边框、内边距盒子,内容盒子下面,content-box
就不会让背景色延伸到 padding
,实际上这里最终效果全部覆盖了
这其实就是因为 background-clip
被覆盖了,background
属性展开可以看到里面具有 background-clip
属性,且用的是默认值 initial
,在源次序中 initial
取胜
因此这里我们可以将 background-clip
属性放最后写,或者也可以直接写 background: red content-box
第三步:继承
第二步骤结束后,很多属性是没有值的,若这些属性 默认可以继承 ,则使用继承,
比如 font-size 容易继承,但是background-color 默认无法继承,默认无法继承可以用 inherit 作为 value 去手动继承
.child {
background: inherit;
}
第四步:使用默认值
若继承过后还有属性没有值,那么就会使用 css 属性的默认值,这就是 user agent stylesheet
,比如 position
的默认值 为static
最后
了解这部分内容应该会对你 开发 css 调试上更容易些,但大概也不会让你写出更漂亮的界面,最终该写成啥样的还是啥样🥲
文章中若出现错误内容还请各位大佬见谅并指正。如果有任何问题或建议,欢迎指出,另外,有不懂之处欢迎在评论区留言。如果觉得文章对你的学习有所帮助,还请 关注、点赞、收藏 一键三连,感谢支持!欢迎关注我的公众号:
Dolphin_Fung