前端錯(cuò)誤日志上報(bào)的超詳細(xì)解決方案
前言
項(xiàng)目上線之后,用戶如果出現(xiàn)錯(cuò)誤(代碼報(bào)錯(cuò)、資源加載失敗以及其他情況),基本上沒有辦法復(fù)現(xiàn),如果用戶出了問題但是不反饋或直接不用了,對(duì)開發(fā)者或公司來說都是損失。
由于我這個(gè)項(xiàng)目比較小,只是一個(gè)迷你商城,所以不需要收集很復(fù)雜的數(shù)據(jù),只需要知道有沒有資源加載失敗、哪行代碼報(bào)錯(cuò)就可以了,市面上有很多現(xiàn)成的監(jiān)控平臺(tái)比如sentry,在這里我選擇通過nodejs自己搭一個(gè)服務(wù)。
概述
我的項(xiàng)目是使用Vue2寫的,所以本文主要是講Vue相關(guān)的部署過程
1、部署后臺(tái)服務(wù)(使用express)
2、收集前端錯(cuò)誤(主要是Vue)
3、提交信息到后臺(tái)分析源碼位置及記錄日志
js異常處理
function test1 () { console.log('test1 Start'); console.log(a); console.log('test1 End'); } function test2 () { console.log('test2 Start'); console.log('test2 End'); } test1(); test2();
這里可以看到,當(dāng)js運(yùn)行報(bào)錯(cuò)后,代碼就不往下執(zhí)行了,這是因?yàn)閖s是單線程,具體可以看看事件循環(huán),這里不做解釋
接下來看看使用異步的方式執(zhí)行,可以看到?jīng)]有影響代碼的繼續(xù)運(yùn)行
function test1 () { console.log('test1 Start'); console.log(a); console.log('test1 End') } function test2 () { console.log('test2 Start'); console.log('test2 End') } setTimeout(() => { test1(); }, 0) setTimeout(() => { test2(); }, 0)
那報(bào)錯(cuò)之后我們?nèi)绾问占e(cuò)誤呢?
try catch
function test1 () { console.log('test1 Start'); console.log(a); console.log('test1 End') } try { test1(); } catch (e) { console.log(e); }
使用try catch
將代碼包裹起來之后,當(dāng)運(yùn)行報(bào)錯(cuò)時(shí),會(huì)將收集到的錯(cuò)誤傳到catch
的形參中,打印之后我們可以拿到錯(cuò)誤信息和錯(cuò)誤的堆棧信息,但是try catch
無法捕獲到異步的錯(cuò)誤
function test1 () { console.log('test1 Start'); console.log(a); console.log('test1 End') } try { setTimeout(function() { test1(); }, 100); } catch (e) { console.log(e); }
可以看到try catch
是無法捕獲到異步錯(cuò)誤的,這時(shí)候就要用到window的error事件
監(jiān)聽error事件
window.addEventListener('error', args => { console.log(args); return true; }, true) function test1 () { console.log('test1 Start'); console.log(a); console.log('test1 End') } setTimeout(function() { test1(); }, 100);
除了window.addEventListener
可以監(jiān)聽error
之后,window.onerror
也可以監(jiān)聽error
,但是window.onerror
和window.addEventListener
相比,無法監(jiān)聽網(wǎng)絡(luò)異常
window.addEventListener
<img src="https://www.baidu.com/abcdefg.gif"> <script> window.addEventListener('error', args => { console.log(args); return true; }, true) // 捕獲 </script>
window.onerror
<img src="https://www.baidu.com/abcdefg.gif"> <script> window.onerror = function(...args) { console.log(args); } </script>
由于無法監(jiān)聽到,這里就不放圖了
unhandledrejection
到目前為止,Promise
已經(jīng)成為了開發(fā)者的標(biāo)配,加上新特性引入了async await
,解決了回調(diào)地獄的問題,但window.onerror
和window.addEventListener
,對(duì)Promise
報(bào)錯(cuò)都是無法捕獲
window.addEventListener('error', error => { console.log('window', error); }) new Promise((resolve, reject) => { console.log(a); }).catch(error => { console.log('catch', error); })
可以看到,監(jiān)聽window上的error事件是沒有用的,可以每一個(gè)Promise寫一個(gè)catch,如果覺得麻煩,那么就要使用一個(gè)新的事件,unhandledrejection
window.addEventListener('unhandledrejection', error => { console.log('window', error); }) new Promise((resolve, reject) => { console.log(a); })
其中,reason
中存放著錯(cuò)誤相關(guān)信息,reason.message
是錯(cuò)誤信息,reason.stack
是錯(cuò)誤堆棧信息
Promise錯(cuò)誤也可以使用 try catch捕獲到,這里就不做演示了
至此,js中同步、異步、資源加載、Promise、async/await都有相對(duì)應(yīng)的捕獲方式
window.addEventListener('unhandledrejection', error => { console.log('window', error); throw error.reason; }) window.addEventListener('error', error => { console.log(error); return true; }, true)
vue異常處理
由于我的項(xiàng)目使用Vue2搭建的,所以還需要處理一下vue的報(bào)錯(cuò)
export default { name: 'App', mounted() { console.log(aaa); } }
現(xiàn)在的項(xiàng)目基本上都是工程化的,通過工程化工具打包出來的代碼長(zhǎng)這樣,上面的代碼打包后運(yùn)行
通過報(bào)錯(cuò)提示的js文件,查看后都是壓縮混淆之后的js代碼,這時(shí)候就需要打包時(shí)生成的source map
文件了,這個(gè)文件中保存著打包后代碼和源碼對(duì)應(yīng)的位置,我們只需要拿到報(bào)錯(cuò)的堆棧信息,通過轉(zhuǎn)換,就能通過source map
找到對(duì)應(yīng)我們?cè)创a的文件及出錯(cuò)的代碼行列信息
那我們?cè)趺床拍鼙O(jiān)聽error事件呢?
使用Vue的全局錯(cuò)誤處理函數(shù)Vue.config.errorHandler
在src/main.js
中寫入以下代碼
Vue.config.errorHandler = (err, vm, info) => { console.log('Error: ', err); console.log('vm', vm); console.log('info: ', info); }
現(xiàn)在打包vue項(xiàng)目
打包vue之后然后通過端口訪問index.html
,不建議你雙擊打開,如果你沒改過打包相關(guān)的東西,雙擊打開是不行的,可以通過vs code裝插件live server
,然后將打包文件夾通過vs code打開
上報(bào)錯(cuò)誤數(shù)據(jù)
經(jīng)過上述的異常處理后,我們需要將收集到的錯(cuò)誤進(jìn)行整理,將需要的信息發(fā)送到后臺(tái),我這里選擇使用ajax發(fā)請(qǐng)求到后端,當(dāng)然你也可以使用創(chuàng)建一個(gè)圖片標(biāo)簽,將需要發(fā)送的數(shù)據(jù)拼接到src上
這里我選擇使用tracekit
庫來解析錯(cuò)誤的堆棧信息,axios
發(fā)請(qǐng)求,dayjs
格式化時(shí)間
npm i tracekit npm i axios npm i dayjs
安裝完成后在src/main.js
中引入tracekit、axios、dayjs
上報(bào)Vue錯(cuò)誤
import TraceKit from 'tracekit'; import axios from 'axios'; import dayjs from 'dayjs'; const protcol = window.location.protocol; let errorMonitorUrl = `${protcol}//127.0.0.1:9999`; const errorMonitorVueInterFace = 'reportVueError'; // vue錯(cuò)誤上報(bào)接口 TraceKit.report.subscribe((error) => { const { message, stack } = error || {}; const obj = { message, stack: { column: stack[0].column, line: stack[0].line, func: stack[0].func, url: stack[0].url } }; axios({ method: 'POST', url: `${errorMonitorUrl}/${errorMonitorVueInterFace}`, data: { error: obj, data: { errTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), // 是否移動(dòng)端 isWechat: /MicroMessenger/i.test(navigator.userAgent), // 是否微信瀏覽器 isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, // 兩個(gè)都是false就是未知設(shè)備 isAndroid: /Android/.test(navigator.userAgent) && !/Windows Phone/.test(navigator.userAgent) }, browserInfo: { userAgent: navigator.userAgent, protcol: protcol } } }).then(() => { console.log('錯(cuò)誤上報(bào)成功'); }).catch(() => { console.log('錯(cuò)誤上報(bào)失敗'); }); }); Vue.config.errorHandler = (err, vm, info) => { TraceKit.report(err); }
如果你還需要其他的數(shù)據(jù)就自己加
打包vue之后然后通過端口訪問index.html
,不建議你雙擊打開,如果你沒改過打包相關(guān)的東西,雙擊打開是不行的,可以通過vs code裝插件live server
,然后將打包文件夾通過vs code打開
現(xiàn)在去項(xiàng)目中看看發(fā)出去的請(qǐng)求參數(shù)是什么
可以看到我們需要的數(shù)據(jù)都已經(jīng)收集到了,上報(bào)失敗是肯定的,因?yàn)槲覀冞€沒有寫好接口
上報(bào)window錯(cuò)誤
接下來在監(jiān)聽window的error事件,也向后臺(tái)發(fā)送一個(gè)錯(cuò)誤上報(bào)請(qǐng)求
const errorMonitorWindowInterFace = 'reportWindowError'; // window錯(cuò)誤上報(bào)接口 window.addEventListener('error', args => { const err = args.target.src || args.target.href; const obj = { message: '加載異常' + err }; if (!err) { return true; } axios({ method: 'POST', url: `${errorMonitorUrl}/${errorMonitorWindowInterFace}`, data: { error: obj, data: { errTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent), // 是否移動(dòng)端 isWechat: /MicroMessenger/i.test(navigator.userAgent), // 是否微信瀏覽器 isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, // 兩個(gè)都是false就是未知設(shè)備 isAndroid: /Android/.test(navigator.userAgent) && !/Windows Phone/.test(navigator.userAgent) }, browserInfo: { userAgent: navigator.userAgent, protcol: protcol } } }).then(() => { console.log('錯(cuò)誤上報(bào)成功'); }).catch(() => { console.log('錯(cuò)誤上報(bào)失敗'); }); return true; }, true);
搭建監(jiān)控后臺(tái)
創(chuàng)建一個(gè)文件夾,名字隨便,然后在終端中打開文件夾,初始化npm
npm init -y
初始化完成后創(chuàng)建一個(gè)server.js,這里我使用express進(jìn)行搭建后端,source-map用于解析js.map文件,這些庫后面會(huì)用到
npm i express npm i nodemon npm i source-map
下好包之后在server.js
中輸入以下代碼,然后在終端輸入nodemon server.js
const express = require('express'); const path = require('path'); const fs = require('fs'); const PORT = 9999; const app = express(); app.use(express.urlencoded({ extended: true })); app.use(express.json()); app.get('/', (req, res) => { res.send('Hello World!').status(200); }) app.listen(PORT, () => { console.log(`服務(wù)啟動(dòng)成功,端口號(hào)為:${PORT}`) })
服務(wù)啟動(dòng)之后,訪問本地的9999端口,查看是否生效,當(dāng)看到屏幕上顯示Hello World!表示我們的后端服務(wù)成功跑起來了,接下來就是寫錯(cuò)誤的上傳接口
在這里我將為Vue和Window監(jiān)控分別寫一個(gè)接口(因?yàn)槲覒械靡粋€(gè)接口做判斷區(qū)分,如果你覺得兩個(gè)接口太麻煩,那你也可以自己優(yōu)化成一個(gè)接口)
編寫Vue錯(cuò)誤上報(bào)接口
在server.js
中繼續(xù)添加
const SourceMap = require('source-map'); app.post('/reportVueError',async (req, res) => { const urlParams = req.body; console.log(`收到Vue錯(cuò)誤報(bào)告`); console.log('urlParams', urlParams); const stack = urlParams.error.stack; // 獲取文件名 const fileName = path.basename(stack.url); // 查找map文件 const filePath = path.join(__dirname, 'uploads', fileName + '.map'); const readFile = function (filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, { encoding: 'utf-8'}, (err, data) => { if (err) { console.log('readFileErr', err) return reject(err); } resolve(JSON.parse(data)); }) }) } async function searchSource({ filePath, line, column }) { const rawSourceMap = await readFile(filePath); const consumer = await new SourceMap.SourceMapConsumer(rawSourceMap); const res = consumer.originalPositionFor({ line, column }) consumer.destroy(); return res; } let sourceMapParseResult = ''; try { // 解析sourceMap結(jié)果 sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: stack.column }); } catch (err) { sourceMapParseResult = err; } console.log('解析結(jié)果', sourceMapParseResult) res.send({ data: '錯(cuò)誤上報(bào)成功', status: 200, }).status(200); })
然后nodemon
會(huì)自動(dòng)重啟服務(wù),如果你不是用nodemon啟動(dòng)的,那自己手動(dòng)重啟一下
打包vue之后然后通過端口訪問index.html,不建議你雙擊打開,如果你沒改過打包相關(guān)的東西,雙擊打開是不行的,可以通過vs code裝插件live server,然后將打包文件夾通過vs code打開,通過live server
運(yùn)行,此時(shí)應(yīng)該會(huì)報(bào)跨域問題
設(shè)置允許跨域
可以自己手動(dòng)設(shè)置響應(yīng)頭實(shí)現(xiàn)跨域,我這里選擇使用cors庫
npm i cors
const cors = require('cors'); app.use(cors()); // 這條需要放在 const app = express(); 后
此時(shí)重新運(yùn)行后臺(tái),再觀察
此時(shí)發(fā)現(xiàn),解析map
文件報(bào)錯(cuò)了,那是因?yàn)槲覀冞€沒有上傳map
文件
在server.js
同級(jí)目錄下創(chuàng)建一個(gè)uploads
文件夾
回到打包vue打包文件目錄dist,將js文件夾中所有js.map
結(jié)尾的文件剪切到創(chuàng)建的文件夾中,如果你打包文件中沒有js.map
,那是因?yàn)槟銢]有打開生成js.map
的開關(guān),打開vue.config.js
,在defineConfig
中設(shè)置屬性productionSourceMap
為true
,然后重新打包就可以了
module.exports = defineConfig({ productionSourceMap: true, // 設(shè)置為true,然后重新打包 transpileDependencies: true, lintOnSave: false, configureWebpack: { devServer: { client: false } } })
為什么是剪切?如果真正的項(xiàng)目上線時(shí),你把js.map文件上傳了,別人拿到之后是可以知道你的源碼的,所以必須剪切,或者復(fù)制之后回到dist目錄刪掉所有js.map
這時(shí)候我們?cè)偎⑿戮W(wǎng)頁,然后看后臺(tái)的輸出,顯示src/App.vue
的第10行有錯(cuò)
編寫window錯(cuò)誤上傳接口
// 處理Window報(bào)錯(cuò) app.post('/reportWindowError',async (req, res) => { const urlParams = req.body; console.log(`收到Window錯(cuò)誤報(bào)告`); console.log('urlParams', urlParams); res.send({ data: '錯(cuò)誤上報(bào)成功', status: 200, }).status(200); })
此時(shí)我們?nèi)ue項(xiàng)目中添加一個(gè)img標(biāo)簽,獲取一張不存在的圖片即可出發(fā)錯(cuò)誤,由于不用解析,所以這里就不再上傳js.map
了
寫入日志
錯(cuò)誤上報(bào)之后我們需要記錄下來,接下來我們改造一下接口,收到報(bào)錯(cuò)之后寫一下日志
我需要知道哪一天的日志報(bào)錯(cuò)了,所有我在node項(xiàng)目中也下載dayjs用來格式化時(shí)間.
npm i dayjs
此處的日志記錄內(nèi)容只是我自己需要的格式,如果你需要其他格式請(qǐng)自己另外添加
vue錯(cuò)誤寫入日志
// let sourceMapParseResult = ''; // try { // // 解析sourceMap結(jié)果 // sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: //stack.column }); //} catch (err) { // sourceMapParseResult = err; //} //console.log('解析結(jié)果', sourceMapParseResult) // 直接將下面的內(nèi)容粘貼在上面的log下面 const today = dayjs().format('YYYY-MM-DD') // 今天 const logDirPath = path.join(__dirname, 'log'); const logFilePath = path.resolve(__dirname, 'log/' + `log-${today}.txt`) if (!fs.existsSync(logDirPath)) { console.log(`創(chuàng)建log文件夾`) fs.mkdirSync(logDirPath, { recursive: true }); } if (!fs.existsSync(logFilePath)) { console.log(`創(chuàng)建${today}日志文件`) fs.writeFileSync(logFilePath, '', 'utf8'); } const writeStream = fs.createWriteStream(logFilePath, { flags: 'a' }); writeStream.on('open', () => { // writeStream.write('UUID:' + urlParams.data.uuid + '\n'); writeStream.write('錯(cuò)誤類型:Window' + '\n'); writeStream.write('錯(cuò)誤發(fā)生時(shí)間:' + urlParams.data.errTime + '\n'); writeStream.write('IP:' + req.ip + '\n'); writeStream.write(`安卓: ${urlParams.data.isAndroid} IOS: ${urlParams.data.isIOS} 移動(dòng)端: ${urlParams.data.isMobile} 微信: ${urlParams.data.isWechat} (安卓和ios同時(shí)為false表示未知設(shè)備)` + '\n'); writeStream.write('用戶代理:' + urlParams.browserInfo.userAgent + '\n'); writeStream.write('錯(cuò)誤信息:' + urlParams.error.message + '\n'); writeStream.write('---------------------------------- \n'); writeStream.end(() => { console.log('vue錯(cuò)誤日志寫入成功'); console.log('---------------------'); res.send({ data: '錯(cuò)誤上報(bào)成功', status: 200, }).status(200); }); }) writeStream.on('error', err => { res.send({ data: '錯(cuò)誤上報(bào)失敗', status: 404, }).status(404); console.error('發(fā)生錯(cuò)誤:', err); })
window錯(cuò)誤寫入日志
和vue寫入的方式差不多,存在優(yōu)化空間
const today = dayjs().format('YYYY-MM-DD') // 今天 const logDirPath = path.join(__dirname, 'log'); const logFilePath = path.join(__dirname, 'log' + `/log-${today}.txt`) if (!fs.existsSync(logDirPath)) { console.log(`創(chuàng)建log文件夾`) fs.mkdirSync(logDirPath, { recursive: true }); } if (!fs.existsSync(logFilePath)) { console.log(`創(chuàng)建${today}日志文件`) fs.writeFileSync(logFilePath, '', 'utf8'); } const writeStream = fs.createWriteStream(logFilePath, { flags: 'a' }); writeStream.on('open', () => { writeStream.write('錯(cuò)誤類型:Window' + '\n'); writeStream.write('錯(cuò)誤發(fā)生時(shí)間:' + urlParams.data.errTime + '\n'); writeStream.write('IP:' + req.ip + '\n'); writeStream.write(`安卓: ${urlParams.data.isAndroid} IOS: ${urlParams.data.isIOS} 移動(dòng)端: ${urlParams.data.isMobile} 微信: ${urlParams.data.isWechat} (安卓和ios同時(shí)為false表示未知設(shè)備)` + '\n'); writeStream.write('用戶代理:' + urlParams.browserInfo.userAgent + '\n'); writeStream.write('錯(cuò)誤信息:' + urlParams.error.message + '\n'); writeStream.write('---------------------------------- \n'); writeStream.end(() => { console.log('window錯(cuò)誤日志寫入成功'); console.log('---------------------'); res.send({ data: '錯(cuò)誤上報(bào)成功', status: 200, }).status(200); }); }) writeStream.on('error', err => { res.send({ data: '錯(cuò)誤上報(bào)失敗', status: 404, }).status(404); console.error('發(fā)生錯(cuò)誤:', err); })
至此,收集錯(cuò)誤,上報(bào)錯(cuò)誤,寫入日志已經(jīng)全部完成。
其他
錯(cuò)誤監(jiān)控持久化運(yùn)行在服務(wù)器
這個(gè)可以使用pm2
,在服務(wù)器上使用node全局安裝pm2
庫
pm2 ls #顯示所有pm2啟動(dòng)的應(yīng)用 pm2 start /xxx/xxx # 啟動(dòng)/xxx/xxx應(yīng)用 pm2 save # 保存當(dāng)前應(yīng)用列表 pm2 stop id # id 通過pm2 ls查看 pm2 logs id # 查看日志
自動(dòng)上傳js.map文件
如果每次打包后都手動(dòng)復(fù)制js.map
文件的到uploads
文件夾下,似乎有些麻煩
雖然麻煩,但是我自己還是沒有自動(dòng)上傳,原因是如果打包就自動(dòng)上傳,那么如果項(xiàng)目還未發(fā)布,但是文件已經(jīng)替換掉之前的文件了,新版本未發(fā)布之前,vue的錯(cuò)誤就無法解析了,當(dāng)然,如果你每次上傳都不刪除以前的文件也是可以的
修改vue項(xiàng)目
在vue項(xiàng)目src
下創(chuàng)建一個(gè)plugin
目錄,新建一個(gè)UploadSourceMap.js
,將下面的代碼粘貼進(jìn)去
const glob = require('glob') const path = require('path') const http = require('http') const fs = require('fs') class UploadSourceMap { constructor (options) { this.options = options } apply (compiler) { console.log('UploadSourceMap') // 在打包完成后運(yùn)行 compiler.hooks.done.tap('UploadSourceMap', async stats => { const list = glob.sync(path.join(stats.compilation.outputOptions.path, '**/*.js.map')) for (const item of list) { const fileName = path.basename(item); console.log(`開始上傳${fileName}`) await this.upload(this.options.url, item) console.log(`上傳${fileName}完成`) } }) } upload (url, file) { return new Promise((resolve, reject) => { const req = http.request( `${url}/upload?name=${path.basename(file)}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream', Connection: 'keep-alive', 'Transfer-Encoding': 'chunked' } } ) fs.createReadStream(file) .on('data', chunk => { req.write(chunk) }) .on('end', () => { req.end() // 刪除文件 fs.unlink(file, (err) => { if (err) { console.error(err) } }) resolve() }) }) } } module.exports = UploadSourceMap
修改vue.config.js
主要是引入UploadSourceMap
,并且在configureWebpack => plugins
下使用
const { defineConfig } = require('@vue/cli-service') const UploadSourceMap = require('./src/plugin/UploadSourceMap') module.exports = defineConfig({ productionSourceMap: true, transpileDependencies: true, lintOnSave: false, configureWebpack: { plugins: [ new UploadSourceMap({ url: 'http://127.0.0.1:9999' // 后面換成自己的服務(wù)器地址 }) ] } })
修改后臺(tái)
修改server.js
,新增一個(gè)上傳文件的接口
app.post('/upload', (req, res) => { const fileName = req.query.name const filePath = path.join(__dirname, 'uploads', fileName) if (!fs.existsSync(path.dirname(filePath))) { fs.mkdirSync(path.dirname(filePath), { recursive: true }) } const writeStream = fs.createWriteStream(filePath) req.on('data', (chunk) => { writeStream.write(chunk) }) req.on('end', () => { writeStream.end(() => { res.status(200).send(`File ${fileName} has been saved.`) }) }) writeStream.on('error', (err) => { fs.unlink(filePath, () => { console.error(`Error writing file ${fileName}: ${err}`) // res.status(500).send(`Error writing file ${fileName}.`) }) }) })
然后現(xiàn)在重新打包,觀察打包輸出
最后
盡量是不要開啟跨域,否則誰都能給發(fā)請(qǐng)求到后臺(tái),如果要開跨域,那需要做好判斷,主域名不符合的直接返回404終止這次請(qǐng)求。
市面上的監(jiān)控有很多,有些甚至能實(shí)現(xiàn)錄制用戶操作生成gif,本文只是實(shí)現(xiàn)一個(gè)基本的錯(cuò)誤監(jiān)控,如有錯(cuò)誤請(qǐng)指出。
源碼參考:https://github.com/ytanck/demos/tree/master/error-monitor-demo
相關(guān)文章
js實(shí)現(xiàn)的簡(jiǎn)單radio背景顏色選擇器代碼
這篇文章主要介紹了js實(shí)現(xiàn)的簡(jiǎn)單radio背景顏色選擇器代碼,利用鼠標(biāo)事件及頁面元素動(dòng)態(tài)操作實(shí)現(xiàn)頁面背景顏色的改變功能,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-08-08在VSCode中進(jìn)行JavaScript調(diào)試的詳細(xì)流程
在JavaScript開發(fā)中,調(diào)試是一個(gè)關(guān)鍵的過程,它幫助我們理解和修復(fù)代碼中的問題,Visual Studio Code(VSCode)以其豐富的擴(kuò)展和內(nèi)置調(diào)試工具,為JavaScript開發(fā)者提供了強(qiáng)大的支持,本文將詳細(xì)介紹如何在VSCode中進(jìn)行JavaScript調(diào),需要的朋友可以參考下2024-07-07JavaScript 總結(jié)幾個(gè)提高性能知識(shí)點(diǎn)(推薦)
下面小編就為大家?guī)硪黄狫avaScript 總結(jié)幾個(gè)提高性能知識(shí)點(diǎn)(推薦)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02javaScript函數(shù)中執(zhí)行C#代碼中的函數(shù)方法總結(jié)
這篇文章介紹了javaScript函數(shù)中執(zhí)行C#代碼中的函數(shù)方法總結(jié),有需要的朋友可以參考一下2013-08-08vscode使用Prettier Code插件的詳細(xì)教程
這篇文章主要介紹了vscode使用Prettier Code插件的詳細(xì)教程,本文通過圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09關(guān)于JS中的apply,call,bind的深入解析
下面小編就為大家?guī)硪黄P(guān)于JS中的apply,call,bind的深入解析。小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-04-04