vscode錄音及語音實時轉(zhuǎn)寫插件開發(fā)并在工作區(qū)生成本地mp3文件附踩坑日記!
前言
最近接到一個需求,實現(xiàn)錄音功能并生成mp3文件到本地工作區(qū),一開始考慮到的是在vscode主體代碼里面開發(fā),但這可不是一個小的工作量。時間緊,任務(wù)重!市面上實現(xiàn)錄音功能的案例其實很多,一些功能代碼是可以復(fù)用過來的,最后決定寫一個插件去實現(xiàn)這個需求!但是插件頁面是瀏覽器環(huán)境,想要生成mp3文件是不可能的!需要把語音數(shù)據(jù)傳到node環(huán)境。
以目前的vscode
版本來說,作者并沒有開放訪問本地媒體權(quán)限,所以插件市場里面的所有語音相關(guān)插件也并沒有直接獲取vscode
的媒體權(quán)限。畢竟vscode是開源項目,有廣大的插件市場,如果開放了所有權(quán)限,遇到了圖謀不軌的人 ,想通過插件獲取你的個人信息很容易,比如打開你的麥克風(fēng) 打開你的攝像頭 獲取地理定位,在你不經(jīng)意間可能就獲取了你的個人信息,所以作者對權(quán)限做了限制。 這樣如果想單純通過寫插件調(diào)用本地媒體設(shè)備的同學(xué),可以放棄你的想法了。
對于一些二次開發(fā)的同學(xué)則很容易,在主體代碼里面放開對應(yīng)權(quán)限。
這里我們主要講下 插件實現(xiàn):
對應(yīng)目錄結(jié)構(gòu),主要文件
本地錄音及生成mp3文件
對應(yīng)demo頁面
wavesurfer.js
->實現(xiàn) 聲紋圖、聲譜圖、播放錄音lame.min.js
-> mp3編碼器record.js
的 -> 實現(xiàn)錄音
這幾個文件github有很多例子,大同小異,核心api都是一樣的
初始化
我們再了解幾個js關(guān)于語音的apinavigator.getUserMedia
: 該對象可提供對相機和麥克風(fēng)等媒體輸入設(shè)備的連接訪問,也包括屏幕共享。AudioContext
: 接口表示由鏈接在一起的音頻模塊構(gòu)建的音頻處理圖,每個模塊由一個AudioNode表示。音頻上下文控制它包含的節(jié)點的創(chuàng)建和音頻處理或解碼的執(zhí)行。在做任何其他操作之前,您需要創(chuàng)建一個AudioContext對象,因為所有事情都是在上下文中發(fā)生的。建議創(chuàng)建一個AudioContext對象并復(fù)用它,而不是每次初始化一個新的AudioContext對象,并且可以對多個不同的音頻源和管道同時使用一個AudioContext對象。createMediaStreamSource
: 方法用于創(chuàng)建一個新的 MediaStreamAudioSourceNode 對象,需要傳入一個媒體流對象 (MediaStream 對象)(可以從 navigator.getUserMedia 獲得 MediaStream 對象實例), 然后來自 MediaStream 的音頻就可以被播放和操作。createScriptProcessor
: 處理音頻。onaudioprocess
: 監(jiān)聽音頻錄制過程,實時獲取語音流
頁面
mounted() { this.wavesurfer = WaveSurfer.create({ container: '#waveform', waveColor: 'black', interact: false, cursorWidth: 1, barWidth: 1, plugins: [ WaveSurfer.microphone.create() ] }); this.wavesurfer.microphone.on('deviceReady', function (stream) { console.log('Device ready!', stream); }); this.wavesurfer.microphone.on('deviceError', function (code) { console.warn('Device error: ' + code); }); this.recorder = new Recorder({ sampleRate: 44100, //采樣頻率,默認為44100Hz(標準MP3采樣率) bitRate: 128, //比特率,默認為128kbps(標準MP3質(zhì)量) success: function() { //成功回調(diào)函數(shù) console.log('success-->') // start.disabled = false; }, error: function(msg) { //失敗回調(diào)函數(shù) alert('msg-->', msg); }, fix: function(msg) { //不支持H5錄音回調(diào)函數(shù) alert('msg--->', msg); } }); }
點擊開始和結(jié)束錄音
start() { // start the microphone this.wavesurfer.microphone.start(); // 開始錄音 this.recorder.start(); }, end() { // same as stopDevice() but also clears the wavesurfer canvas this.wavesurfer.microphone.stop(); // 結(jié)束錄音 this.recorder.stop(); let that = this; this.recorder.getBlob(function(blob) { that.audioPath = URL.createObjectURL(blob); that.$refs.myAudio.load(); }); }
recorder.js
//初始化 init: function () { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; window.AudioContext = window.AudioContext || window.webkitAudioContext; },
// 訪問媒體設(shè)備 navigator.getUserMedia({ audio: true //配置對象 }, function (stream) { //成功回調(diào) var context = new AudioContext(), microphone = context.createMediaStreamSource(stream), //媒體流音頻源 processor = context.createScriptProcessor(0, 1, 1), //js音頻處理器 } })
// 開始錄音 _this.start = function () { if (processor && microphone) { microphone.connect(processor); processor.connect(context.destination); Util.log('開始錄音'); } };
//結(jié)束錄音 _this.stop = function () { if (processor && microphone) { microphone.disconnect(); processor.disconnect(); Util.log('錄音結(jié)束'); } };
// new worker 開啟后臺線程,為數(shù)據(jù)編碼,這里我部署到線上 是為了避免訪問限制 fetch( "https://lawdawn-download.oss-cn-beijing.aliyuncs.com/js/recorderWorker.js" ) .then((response) => response.blob()) .then((blob) => { const url = URL.createObjectURL(blob); realTimeWorker = new Worker(url); realTimeWorker.onmessage = async function (e) { ... } })
recorderWorker.js
// 后臺線程接受到語音流數(shù)據(jù)之后做編碼 (function(){ 'use strict'; importScripts('https://lawdawn-download.oss-cn-beijing.aliyuncs.com/js/lame.min.js'); var mp3Encoder, maxSamples = 1152, samplesMono, lame, config, dataBuffer; var clearBuffer = function(){ dataBuffer = []; }; var appendToBuffer = function(mp3Buf){ dataBuffer.push(new Int8Array(mp3Buf)); }; var init = function(prefConfig){ config = prefConfig || {}; lame = new lamejs(); mp3Encoder = new lame.Mp3Encoder(1, config.sampleRate || 44100, config.bitRate || 128); clearBuffer(); self.postMessage({ cmd: 'init' }); }; var floatTo16BitPCM = function(input, output){ for(var i = 0; i < input.length; i++){ var s = Math.max(-1, Math.min(1, input[i])); output[i] = (s < 0 ? s * 0x8000 : s * 0x7FFF); } }; var convertBuffer = function(arrayBuffer){ var data = new Float32Array(arrayBuffer); var out = new Int16Array(arrayBuffer.length); floatTo16BitPCM(data, out); return out; }; var encode = function(arrayBuffer){ samplesMono = convertBuffer(arrayBuffer); var remaining = samplesMono.length; for(var i = 0; remaining >= 0; i += maxSamples){ var left = samplesMono.subarray(i, i + maxSamples); var mp3buf = mp3Encoder.encodeBuffer(left); appendToBuffer(mp3buf); remaining -= maxSamples; } }; var finish = function(){ appendToBuffer(mp3Encoder.flush()); self.postMessage({ cmd: 'end', buf: dataBuffer }); clearBuffer(); }; self.onmessage = function(e){ switch(e.data.cmd){ case 'init': init(e.data.config); break; case 'encode': encode(e.data.buf); break; case 'finish': finish(); break; } }; })();
整個recorder.js
(function (exports) { //公共方法 var Util = { //初始化 init: function () { navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; window.AudioContext = window.AudioContext || window.webkitAudioContext; }, //日志 log: function () { console.log.apply(console, arguments); } }; let realTimeWorker; var Recorder = function (config) { var _this = this; config = config || {}; //初始化配置對象 config.sampleRate = config.sampleRate || 44100; //采樣頻率,默認為44100Hz(標準MP3采樣率) config.bitRate = config.bitRate || 128; //比特率,默認為128kbps(標準MP3質(zhì)量) Util.init(); if (navigator.getUserMedia) { navigator.getUserMedia({ audio: true //配置對象 }, function (stream) { //成功回調(diào) var context = new AudioContext(), microphone = context.createMediaStreamSource(stream), //媒體流音頻源 processor = context.createScriptProcessor(0, 1, 1), //js音頻處理器 successCallback, errorCallback; config.sampleRate = context.sampleRate; processor.onaudioprocess = function (event) { //監(jiān)聽音頻錄制過程 var array = event.inputBuffer.getChannelData(0); realTimeWorker.postMessage({ cmd: 'encode', buf: array }); }; fetch( "https://lawdawn-download.oss-cn-beijing.aliyuncs.com/js/recorderWorker.js" ) .then((response) => response.blob()) .then((blob) => { const url = URL.createObjectURL(blob); realTimeWorker = new Worker(url); realTimeWorker.onmessage = async function (e) { //主線程監(jiān)聽后臺線程,實時通信 switch (e.data.cmd) { case 'init': Util.log('初始化成功'); if (config.success) { config.success(); } break; case 'end': if (successCallback) { var blob = new Blob(e.data.buf, { type: 'audio/mp3' }); let formData = new FormData(); formData.append('file', blob, 'main.mp3'); fetch("http://127.0.0.1:8840/microm", { method: 'POST', body: formData }) successCallback(blob); Util.log('MP3大?。? + blob.size + '%cB', 'color:#0000EE'); } break; case 'error': Util.log('錯誤信息:' + e.data.error); if (errorCallback) { errorCallback(e.data.error); } break; default: Util.log('未知信息:' + e.data); } }; _this.start = function () { if (processor && microphone) { microphone.connect(processor); processor.connect(context.destination); Util.log('開始錄音'); } }; //結(jié)束錄音 _this.stop = function () { if (processor && microphone) { microphone.disconnect(); processor.disconnect(); Util.log('錄音結(jié)束'); } }; //獲取blob格式錄音文件 _this.getBlob = function (onSuccess, onError) { successCallback = onSuccess; errorCallback = onError; realTimeWorker.postMessage({ cmd: 'finish' }); }; realTimeWorker.postMessage({ cmd: 'init', config: { sampleRate: config.sampleRate, bitRate: config.bitRate } }); }); // var realTimeWorker = new Worker('js/recorderWorker.js'); //開啟后臺線程 //接口列表 //開始錄音 }, function (error) { //失敗回調(diào) var msg; switch (error.code || error.name) { case 'PermissionDeniedError': case 'PERMISSION_DENIED': case 'NotAllowedError': msg = '用戶拒絕訪問麥克風(fēng)'; break; case 'NOT_SUPPORTED_ERROR': case 'NotSupportedError': msg = '瀏覽器不支持麥克風(fēng)'; break; case 'MANDATORY_UNSATISFIED_ERROR': case 'MandatoryUnsatisfiedError': msg = '找不到麥克風(fēng)設(shè)備'; break; default: msg = '無法打開麥克風(fēng),異常信息:' + (error.code || error.name); break; } Util.log(msg); if (config.error) { config.error(msg); } }); } else { Util.log('當(dāng)前瀏覽器不支持錄音功能'); if (config.fix) { config.fix('當(dāng)前瀏覽器不支持錄音功能'); } } }; //模塊接口 exports.Recorder = Recorder; })(window);
踩坑!
前面錄音都進行的很順利,錄音在渲染進程里都可以正常播放,接下來就是生成本地mp3文件的處理了。
第一次嘗試:瀏覽器端獲取的Blob數(shù)據(jù),通過vscode.postMessage
傳送過去之后獲取的竟是{}
空對象。
肯能原因 數(shù)據(jù)傳輸過程做了序列化,導(dǎo)致丟失,或者Blob
只是瀏覽器API,node無法獲取
第二次嘗試:將實時獲取的語音數(shù)據(jù)流傳遞過去
數(shù)據(jù) 是Float32Array格式數(shù)據(jù),但是通過序列化傳遞過去之后,發(fā)現(xiàn)數(shù)據(jù)無法恢復(fù)原來的樣子,還是失??!
經(jīng)過苦想后接下來,用了一個方法
第三次嘗試:
在extension.ts
里面開啟了一個本地服務(wù)
export async function activate(context: vscode.ExtensionContext) { (global as any).audioWebview = null; context.subscriptions.push(VsAudioEditorProvider.register(context)); const server = http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.setHeader('Access-Control-Allow-Credentials', 'true'); if (req.method === 'POST' && req.url === '/microm') { const stream = fs.createWriteStream(path.join(path.dirname((global as any).documentUri!.fsPath), 'main.mp3')); req.on("data", (chunck) => { stream.write(chunck); }); req.on("end", () => { vscode.window.showInformationMessage('語音文件生成成功!'); res.writeHead(200); res.end('success!'); }); } }).listen(8840); }
在瀏覽器端錄音結(jié)束后,調(diào)用了接口把數(shù)據(jù)傳送了過來
經(jīng)過在本地和生產(chǎn)環(huán)境測試都沒有問題~ 大工告成!
語音實時轉(zhuǎn)寫功能
語音實時轉(zhuǎn)寫調(diào)用的科大訊飛的接口,科大訊飛給出的demo頁面
js
/** * Created by lcw on 2023/5/20. * * 實時語音轉(zhuǎn)寫 WebAPI 接口調(diào)用示例 接口文檔(必看):https://www.xfyun.cn/doc/asr/rtasr/API.html * 錯誤碼鏈接: * https://www.xfyun.cn/doc/asr/rtasr/API.html * https://www.xfyun.cn/document/error-code (code返回錯誤碼時必看) * */ if (typeof (Worker) == undefined) { // 不支持 Web Workers alert('不支持 Web Workers'); } else { // 支持 Web Workers console.log('支持 Web Workers'); } let recorderWorker fetch( "https://lawdawn-download.oss-cn-beijing.aliyuncs.com/js/transformpcm.worker.js" ) .then((response) => response.blob()) .then((blob) => { const url = URL.createObjectURL(blob); recorderWorker = new Worker(url); recorderWorker.onmessage = function (e) { buffer.push(...e.data.buffer) } }); // 音頻轉(zhuǎn)碼worker // let recorderWorker = new Worker('transformpcm.worker.js'); // 記錄處理的緩存音頻 let buffer = [] let AudioContext = window.AudioContext || window.webkitAudioContext navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia class IatRecorder { constructor(config) { this.config = config this.state = 'ing' //以下信息在控制臺-我的應(yīng)用-實時語音轉(zhuǎn)寫 頁面獲取 this.appId = 'xxxxx' this.apiKey = 'xxxxx' } start() { this.stop() if (navigator.getUserMedia && AudioContext) { this.state = 'ing' if (!this.recorder) { var context = new AudioContext() this.context = context this.recorder = context.createScriptProcessor(0, 1, 1) var getMediaSuccess = (stream) => { var mediaStream = this.context.createMediaStreamSource(stream) this.mediaStream = mediaStream this.recorder.onaudioprocess = (e) => { this.sendData(e.inputBuffer.getChannelData(0)) } this.connectWebsocket() } var getMediaFail = (e) => { this.recorder = null this.mediaStream = null this.context = null console.log('請求麥克風(fēng)失敗') } if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then((stream) => { getMediaSuccess(stream) }).catch((e) => { getMediaFail(e) }) } else { navigator.getUserMedia({ audio: true, video: false }, (stream) => { getMediaSuccess(stream) }, function (e) { getMediaFail(e) }) } } else { this.connectWebsocket() } } } stop() { this.state = 'end' try { this.mediaStream.disconnect(this.recorder) this.recorder.disconnect() } catch (e) { } } sendData(buffer) { recorderWorker.postMessage({ command: 'transform', buffer: buffer }) } // 生成握手參數(shù) getHandShakeParams() { var appId = this.appId var secretKey = this.apiKey var ts = Math.floor(new Date().getTime() / 1000);//new Date().getTime()/1000+''; var signa = hex_md5(appId + ts)//hex_md5(encodeURIComponent(appId + ts));//EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(appId + ts), secretKey); var signatureSha = CryptoJSNew.HmacSHA1(signa, secretKey) var signature = CryptoJS.enc.Base64.stringify(signatureSha) signature = encodeURIComponent(signature) return "?appid=" + appId + "&ts=" + ts + "&signa=" + signature; } connectWebsocket() { // var url = 'wss://rtasr.xfyun.cn/v1/ws' var url = 'ws://xxxx.xxx.xx/xf-rtasr'; var urlParam = this.getHandShakeParams() // url = `${url}${urlParam}` if ('WebSocket' in window) { this.ws = new WebSocket(url) } else if ('MozWebSocket' in window) { this.ws = new MozWebSocket(url) } else { alert(notSupportTip) return null } this.ws.onopen = (e) => { if (e.isTrusted) { this.mediaStream.connect(this.recorder) this.recorder.connect(this.context.destination) setTimeout(() => { this.wsOpened(e) }, 500) this.config.onStart && this.config.onStart(e) } else { alert('出現(xiàn)錯誤'); } } this.ws.onmessage = (e) => { // this.config.onMessage && this.config.onMessage(e) this.wsOnMessage(e) } this.ws.onerror = (e) => { console.log('err-->', e) this.stop() console.log("關(guān)閉連接ws.onerror"); this.config.onError && this.config.onError(e) } this.ws.onclose = (e) => { this.stop() console.log("關(guān)閉連接ws.onclose"); $('.start-button').attr('disabled', false); this.config.onClose && this.config.onClose(e) } } wsOpened() { if (this.ws.readyState !== 1) { return } var audioData = buffer.splice(0, 1280) this.ws.send(new Int8Array(audioData)) this.handlerInterval = setInterval(() => { // websocket未連接 if (this.ws.readyState !== 1) { clearInterval(this.handlerInterval) return } if (buffer.length === 0) { if (this.state === 'end') { this.ws.send("{\"end\": true}") console.log("發(fā)送結(jié)束標識"); clearInterval(this.handlerInterval) } return false } var audioData = buffer.splice(0, 1280) if (audioData.length > 0) { this.ws.send(new Int8Array(audioData)) } }, 40) } wsOnMessage(e) { let jsonData = JSON.parse(e.data) // if (jsonData.action == "started") { // // 握手成功 // console.log("握手成功"); // } else if (jsonData.action == "result") { // 轉(zhuǎn)寫結(jié)果 if (this.config.onMessage && typeof this.config.onMessage == 'function') { this.config.onMessage(jsonData) } // } else if (jsonData.action == "error") { // // 連接發(fā)生錯誤 // console.log("出錯了:", jsonData); // } } ArrayBufferToBase64(buffer) { var binary = '' var bytes = new Uint8Array(buffer) var len = bytes.byteLength for (var i = 0; i < len; i++) { binary += String.fromCharCode(bytes[i]) } return window.btoa(binary) } } class IatTaste { constructor() { var iatRecorder = new IatRecorder({ onClose: () => { this.stop() this.reset() }, onError: (data) => { this.stop() this.reset() alert('WebSocket連接失敗') }, onMessage: (message) => { // this.setResult(JSON.parse(message)) this.setResult(message) }, onStart: () => { $('hr').addClass('hr') var dialect = $('.dialect-select').find('option:selected').text() $('.taste-content').css('display', 'none') $('.start-taste').addClass('flex-display-1') $('.dialect-select').css('display', 'none') $('.start-button').text('結(jié)束轉(zhuǎn)寫') $('.time-box').addClass('flex-display-1') $('.dialect').text(dialect).css('display', 'inline-block') this.counterDown($('.used-time')) } }) this.iatRecorder = iatRecorder this.counterDownDOM = $('.used-time') this.counterDownTime = 0 this.text = { start: '開始轉(zhuǎn)寫', stop: '結(jié)束轉(zhuǎn)寫' } this.resultText = '' } start() { this.iatRecorder.start() } stop() { $('hr').removeClass('hr') this.iatRecorder.stop() } reset() { this.counterDownTime = 0 clearTimeout(this.counterDownTimeout) buffer = [] $('.time-box').removeClass('flex-display-1').css('display', 'none') $('.start-button').text(this.text.start) $('.dialect').css('display', 'none') $('.dialect-select').css('display', 'inline-block') $('.taste-button').css('background', '#0b99ff') } init() { let self = this //開始 $('#taste_button').click(function () { if (navigator.getUserMedia && AudioContext && recorderWorker) { self.start() } else { alert(notSupportTip) } }) //結(jié)束 $('.start-button').click(function () { if ($(this).text() === self.text.start && !$(this).prop('disabled')) { $('#result_output').text('') self.resultText = '' self.start() //console.log("按鈕非禁用狀態(tài),正常啟動" + $(this).prop('disabled')) } else { //$('.taste-content').css('display', 'none') $('.start-button').attr('disabled', true); self.stop() //reset this.counterDownTime = 0 clearTimeout(this.counterDownTimeout) buffer = [] $('.time-box').removeClass('flex-display-1').css('display', 'none') $('.start-button').text('轉(zhuǎn)寫停止中...') $('.dialect').css('display', 'none') $('.taste-button').css('background', '#8E8E8E') $('.dialect-select').css('display', 'inline-block') //console.log("按鈕非禁用狀態(tài),正常停止" + $(this).prop('disabled')) } }) } setResult(data) { let rtasrResult = [] var currentText = $('#result_output').html() rtasrResult[data.seg_id] = data rtasrResult.forEach(i => { let str = "實時轉(zhuǎn)寫" str += (i.cn.st.type == 0) ? "【最終】識別結(jié)果:" : "【中間】識別結(jié)果:" i.cn.st.rt.forEach(j => { j.ws.forEach(k => { k.cw.forEach(l => { str += l.w }) }) }) if (currentText.length == 0) { $('#result_output').html(str) } else { $('#result_output').html(currentText + "<br>" + str) } var ele = document.getElementById('result_output'); ele.scrollTop = ele.scrollHeight; }) } counterDown() { /*//計時5分鐘 if (this.counterDownTime === 300) { this.counterDownDOM.text('05: 00') this.stop() } else if (this.counterDownTime > 300) { this.reset() return false } else */ if (this.counterDownTime >= 0 && this.counterDownTime < 10) { this.counterDownDOM.text('00: 0' + this.counterDownTime) } else if (this.counterDownTime >= 10 && this.counterDownTime < 60) { this.counterDownDOM.text('00: ' + this.counterDownTime) } else if (this.counterDownTime % 60 >= 0 && this.counterDownTime % 60 < 10) { this.counterDownDOM.text('0' + parseInt(this.counterDownTime / 60) + ': 0' + this.counterDownTime % 60) } else { this.counterDownDOM.text('0' + parseInt(this.counterDownTime / 60) + ': ' + this.counterDownTime % 60) } this.counterDownTime++ this.counterDownTimeout = setTimeout(() => { this.counterDown() }, 1000) } } var iatTaste = new IatTaste() iatTaste.init()
到此這篇關(guān)于vscode錄音及語音實時轉(zhuǎn)寫插件開發(fā)并在工作區(qū)生成本地mp3文件 踩坑日記!的文章就介紹到這了,更多相關(guān)vscode錄音內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Bootstrap基本樣式學(xué)習(xí)筆記之表格(2)
Bootstrap提供了一個清晰的創(chuàng)建表格的布局,這篇文章主要介紹了Bootstrap學(xué)習(xí)筆記之表格基本樣式的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12原生JS封裝ajax 傳json,str,excel文件上傳提交表單(推薦)
這篇文章主要介紹了原生JS封裝ajax 傳json,str,excel文件上傳提交表單(推薦)的相關(guān)資料,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-06-06JS實現(xiàn)的檢驗身份證格式并輸出出生日期,年齡,性別,出生地示例
這篇文章主要介紹了JS實現(xiàn)的檢驗身份證格式并輸出出生日期,年齡,性別,出生地,涉及javascript字符串遍歷、運算、轉(zhuǎn)換等相關(guān)操作技巧,需要的朋友可以參考下2019-05-05