نواصل مناقشة المواضيع التي نوقشت في مؤتمر You Gonna Love Frontend . هذه المقالة مستوحاة من محاضرة ميكايلا لير . ستكون مقاطع الفيديو الخاصة بالمؤتمر متاحة هذا الأسبوع طالما أن هناك شرائح . ( الفيديو متاح بالفعل )

قامت Michaela Lehr بتوصيل الهزاز بالمتصفح باستخدام Web APIs ، وهي Web Bluetooth API. حركة المرور Prosniferif بين التطبيق والهزاز ، وجدت أن الأوامر المرسلة بسيطة للغاية ، على سبيل المثال: vibrate: 5
. ثم بعد أن علمته الاهتزاز على صوت الآهات من مقطع فيديو يمكنها العثور عليه على الإنترنت ، حققت أهدافها :)
ليس لدي مثل هذه الألعاب ، والتصميم غير مخصص للاستخدام ، ولكن هناك جهاز مراقبة معدل ضربات القلب Polar H10 يستخدم البلوتوث لنقل البيانات. في الواقع ، قررت "كسرها".
لا قرصنة
بادئ ذي بدء ، يجدر فهم كيفية توصيل الجهاز بالمتصفح؟ Google أو Yandex ، اعتمادًا على ميولك: Web Bluetooth API
، وعلى الرابط الأول نرى مقالة حول هذا الموضوع.
لسوء الحظ ، كل شيء أبسط بكثير ولا يوجد شيء يمكن التعرف عليه إذا كنت لا تريد إرسال شيء إلى جهاز لا يريده. في نفس المقالة ، يوجد حتى عرض توضيحي لجهاز مراقبة معدل ضربات القلب متصل.

لقد أحبطني ذلك بشدة ، حتى أن شفرة المصدر هي. في أي وقت مضى؟
نقوم بتوصيل الجهاز
لنقم بإنشاء index.html
بالترميز النموذجي:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> </html>
نظرًا لأن جهاز مراقبة معدل ضربات القلب المعتمد الخاص بي تم تزويره في ورش العمل الصينية ، ولكن وفقًا للمعايير ، فإن اتصاله واستخدامه لا يجب أن يسبب أي صعوبات. هناك شيء من هذا القبيل - السمات العامة (GATT) . لم أخوض في الكثير من التفاصيل ، ولكن إذا كانت بسيطة ، فهي نوع من المواصفات التي تتبعها أجهزة البلوتوث. تصف اتفاقية الجات خصائصها وتفاعلاتها. بالنسبة لمشروعنا ، هذا كل ما نحتاج إلى معرفته. من الروابط المفيدة لنا أيضًا قائمة الخدمات (الأجهزة في الواقع). ثم وجدت خدمة معدل ضربات القلب (org.bluetooth.service.heart_rate) التي تبدو كما نحتاج.
لتوصيل الجهاز بالمتصفح ، يجب على المستخدم التفاعل بوعي مع واجهة المستخدم. لذا ، بالطبع ، الأمان ، مع الأخذ في الاعتبار أنه عند الدخول إلى صالة الألعاب الرياضية ، يتصل جهاز مراقبة معدل ضربات القلب بصمت بكل ما أريده (في وقت واحد فوجئت بهذا). شكرا بالطبع لمطوري المتصفح ، ولكن لماذا ؟! حسنًا ، إنها ليست صعبة وليست مقرفة جدًا.
دعنا نضيف زرًا ومعالجًا إلى الصفحة في نص <body>
:
<button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') button.addEventListener('pointerup', function(event) { </script>
كما ترون ، حتى الآن لا يوجد Vue الذي وعدت بالحكم عليه حسب العنوان. لكني لا أعرف كل شيء بنفسي وأكتب مقالًا على طول الطريق. لذا ما نقوم به بهذه الطريقة :)
من أجل توصيل الجهاز ، يجب علينا استخدام navigator.bluetooth.requestDevice
. يمكن لهذه الطريقة قبول مجموعة من المرشحات. نظرًا لأن تطبيقنا سيعمل في الغالب فقط مع أجهزة مراقبة معدل ضربات القلب ، فسوف نقوم بالفلترة وفقًا لها:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
افتح ملف html في متصفح أو استخدم browser-sync
:
browser-sync start --server --files ./
أرتدي جهازًا لمراقبة معدل ضربات القلب وبعد بضع ثوان اكتشفه Chrome:

بعد العثور على الجهاز الذي نحتاجه ، من الضروري قراءة البيانات منه. للقيام بذلك ، قم بتوصيله بخادم الجات:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); })
البيانات التي نريد قراءتها موجودة في خصائص الخدمة. تحتوي أجهزة مراقبة معدل ضربات القلب على 3 خصائص فقط ، ونحن مهتمون بالمؤسسة. bluetooth.characteristic.heart_rate_measurement
من أجل النظر في هذه الخاصية ، نحتاج إلى الحصول على الخدمة الرئيسية. بصراحة لا أعرف لماذا. ربما تحتوي بعض الأجهزة على العديد من الخدمات الفرعية. ثم احصل على شهادة واشترك في الإخطارات.
.then(server => { return server.getPrimaryService('heart_rate'); }) .then(service => { return service.getCharacteristic('heart_rate_measurement'); }) .then(characteristic => characteristic.startNotifications()) .then(characteristic => { characteristic.addEventListener( 'characteristicvaluechanged', handleCharacteristicValueChanged ); }) .catch(error => { console.log(error); }); function handleCharacteristicValueChanged(event) { var value = event.target.value; console.log(parseValue(value)); }
parseValue
وظيفة تُستخدم لتحليل البيانات ، ويمكن العثور على مواصفات البيانات هنا - org.bluetooth.characteristic.heart_rate_measurement . لن نتحدث عن هذه الوظيفة بالتفصيل ، فكل شيء مبتذل هناك.
تحليل القيمة parseValue = (value) => { // Chrome 50+ DataView. value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); // let rate16Bits = flags & 0x1; let result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR let rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { let rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; }
أخذت من هنا: heartRateSensor.js
وهكذا ، في وحدة التحكم نرى البيانات التي نحتاجها. بالإضافة إلى معدل ضربات القلب ، يُظهر جهاز مراقبة معدل ضربات القلب أيضًا فترات RR. لم أتوصل أبداً إلى كيفية استخدامها ، هذا هو واجبك المنزلي :)
كود الصفحة الكاملة <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <button id="pair">Pair device</button> <script> window.onload = () => { const button = document.getElementById('pair') parseValue = (value) => { // Chrome 50+ DataView. value = value.buffer ? value : new DataView(value); let flags = value.getUint8(0); // let rate16Bits = flags & 0x1; let result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR let rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { let rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; } button.addEventListener('pointerup', function(event) { navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] }) .then((device) => { return device.gatt.connect(); }) .then(server => { return server.getPrimaryService('heart_rate'); }) .then(service => { return service.getCharacteristic('heart_rate_measurement'); }) .then(characteristic => characteristic.startNotifications()) .then(characteristic => { characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); }) .catch(error => { console.log(error); }); function handleCharacteristicValueChanged(event) { var value = event.target.value; console.log(parseValue(value)); // See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js } }); } </script> </body> </html>
التصميم
الخطوة التالية هي النظر في تصميم التطبيق. أوه ، بالطبع ، مقال بسيط للوهلة الأولى يتحول إلى مهمة غير تافهة. أود استخدام جميع أنواع الشفقة ، وفي رأسي هناك قائمة انتظار من المقالات التي أحتاج إلى قراءتها على CSS Grids Flexbox و معالجة الرسوم المتحركة في CSS باستخدام JS (الرسوم المتحركة النبضية ليست ثابتة).
رسم
أنا أحب التصميم الجميل ، لكن المصمم لطيف معي.
ليس لدي Photoshop ، سنبتعد بطريقة ما عن الطريق.
أولاً ، لنقم بإنشاء مشروع Vue.js جديد باستخدام Vue-cli
vue create heart-rate
اخترت الإعدادات اليدوية وتبدو صفحة الإعدادات الأولى كما يلي:

بعد ذلك ، اختر بنفسك ، ولكن لدي تكوين Airbnb و Jest و Sass.
لقد نظرت في نصف دروس Wes Bos CSS Grids التعليمية ، أوصي أنها مجانية.
حان الوقت للقيام بالتخطيط الأولي. لن نستخدم أي أطر عمل CSS ، كلها خاصة بنا. بالطبع ، لا نفكر في الدعم أيضًا.
البومة رسم السحر
لذا ، أولاً وقبل كل شيء ، دعنا نحدد layout
. في الواقع ، سيتألف التطبيق من جزأين. سوف نطلق عليهم ذلك - first
second
. في الجزء الأول سيكون لدينا تمثيل رقمي (نبضة في الدقيقة) ، في الرسم البياني الثاني.
قررت سرقة مخطط الألوان من هنا .

نطلق تطبيق Vue إذا لم تكن قد قمت بذلك بالفعل:
npm run serve
ستفتح الأداة نفسها المتصفح (أو لا) ، وهناك إعادة تحميل سريعة ورابط للاختبار الخارجي. قمت على الفور بوضع هاتف محمول بجانبي ، لأننا نفكر في التصميم الأول للجوال. لسوء الحظ ، أضفت PWA إلى القالب ، وعلى الهاتف المحمول ، يتم تنظيف ذاكرة التخزين المؤقت عند إغلاق المتصفح ، ولكن يحدث ، ويتم تحديث موافق للحفظ. بشكل عام ، لحظة غير مفهومة لم أفهمها.
أولاً ، قم بإضافة utils.js
، من خلال utils.js
الخاصة بتحليل القيم ، وإعادة هيكلتها قليلاً تحت eslint في المشروع.
يستخدم. js export const parseHeartRateValues = (data) => { // Chrome 50+ DataView. const value = data.buffer ? data : new DataView(data); const flags = value.getUint8(0); // const rate16Bits = flags & 0x1; const result = {}; let index = 1; // if (rate16Bits) { result.heartRate = value.getUint16(index, true); index += 2; } else { result.heartRate = value.getUint8(index); index += 1; } // RR const rrIntervalPresent = flags & 0x10; if (rrIntervalPresent) { const rrIntervals = []; for (; index + 1 < value.byteLength; index += 2) { rrIntervals.push(value.getUint16(index, true)); } result.rrIntervals = rrIntervals; } return result; }; export default { parseHeartRateValues, };
ثم نقوم بإزالة كل ما هو غير ضروري من HelloWolrd.vue
وإعادة تسميته إلى HeartRate.vue
، سيكون هذا المكون مسؤولًا عن عرض HeartRate.vue
في الدقيقة.
<template> <div> <span>{{value}}</span> </div> </template> <script> export default { name: 'HeartRate', props: { </script> // SCSS <style scoped lang="scss"> @import '../styles/mixins'; div { @include heart-rate-gradient; font-size: var(--heart-font-size); // } </style>
إنشاء HeartRateChart.vue
للمخطط:
// HeartRateChart.vue <template> <div> chart </div> </template> <script> export default { name: 'HeartRateChart', props: { values: { type: Array, default: () => [], . . }, }, }; </script>
تحديث App.vue
:
App.vue <template> <div class=app> <div class=heart-rate-wrapper> <HeartRate v-if=heartRate :value=heartRate /> <i v-else class="fas fa-heartbeat"></i> <div> <button v-if=!heartRate class=blue>Click to start</button> </div> </div> <div class=heart-rate-chart-wrapper> <HeartRateChart :values=heartRateData /> </div> </div> </template> <script> import HeartRate from './components/HeartRate.vue'; import HeartRateChart from './components/HeartRateChart.vue'; import { parseHeartRateValues } from './utils'; export default { name: 'app', components: { HeartRate, HeartRateChart, }, data: () => ({ heartRate: 0, heartRateData: [], }), methods: { handleCharacteristicValueChanged(e) { this.heartRate = parseHeartRateValues(e.target.value).heartRate; }, }, }; </script> <style lang="scss"> @import './styles/mixins'; html, body { margin: 0px; } :root { // COLORS
وفي الواقع mixins.scss
، بينما يوجد mixins.scss
واحد فقط هو المسؤول عن لون الرمز والنص الذي يعرض نبضات في الدقيقة.
@mixin heart-rate-gradient { background: -webkit-linear-gradient(
اتضح مثل هذا:
من بين النقاط المثيرة للاهتمام - يتم استخدام متغيرات CSS الأصلية ، ولكن mixins
من SCSS.
الصفحة كاملة هي CSS Grid
:
display: grid; grid-gap: 1rem; height: 100vh; grid-template-rows: 1fr 1fr; grid-template-areas: "first" "second";
مثل flexbox
، يجب أن تحتوي الحاوية الرئيسية على نوع من display
. في هذه الحالة ، هو grid
.
grid-gap
- نوع من المسافات بين columns
rows
.
height: 100vh
- ارتفاع viewport
بأكمله ، من الضروري أن تشغل مساحة بارتفاع كامل (جزءان من تطبيقنا).
grid-template-rows
- حدد قالبنا ، fr
هي وحدة سكر تأخذ في الاعتبار grid-gap
وغيرها من الخصائص التي تؤثر على الحجم.
grid-template-areas
- في مثالنا ، دلالي فقط.
لم يقدم Chrome حتى الآن أدوات عادية لفحص شبكات CSS:

في نفس الوقت في إفشل:

نحتاج الآن إلى إضافة معالج النقر بزر ، على غرار الطريقة التي قمنا بها من قبل.
أضف معالج:
// App.vue <button v-if=!heartRate @click=onClick class=blue>Click to start</button>
// Methods: {} onClick() { navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }], }) .then(device => device.gatt.connect()) .then(server => server.getPrimaryService('heart_rate')) .then(service => service.getCharacteristic('heart_rate_measurement')) .then(characteristic => characteristic.startNotifications()) .then(characteristic => characteristic.addEventListener('characteristicvaluechanged', this.handleCharacteristicValueChanged.bind(this))) .catch(error => console.log(error)); },
لا تنس أن هذا يعمل فقط في الكروم وفقط في الكروم على android :)
بعد ذلك ، أضف مخططًا ، وسوف نستخدم Chart.js
لـ Vue.js
npm install vue-chartjs chart.js
يوجد في Polar 5 مناطق تدريب . لذلك ، نحتاج إلى التمييز بين هذه المناطق و / أو تخزينها بطريقة أو بأخرى. لدينا بالفعل heartRateData
. بالنسبة للجماليات ، نجعل القيمة الافتراضية للنموذج:
heartRateData: [[], [], [], [], [], []],
سنبعثر القيم وفقًا لـ 5 مناطق:
pushData(index, value) { this.heartRateData[index].push({ x: Date.now(), y: value }); this.heartRateData = [...this.heartRateData]; }, handleCharacteristicValueChanged(e) { const value = parseHeartRateValues(e.target.value).heartRate; this.heartRate = value; switch (value) { case value > 104 && value < 114: this.pushData(1, value); break; case value > 114 && value < 133: this.pushData(2, value); break; case value > 133 && value < 152: this.pushData(3, value); break; case value > 152 && value < 172: this.pushData(4, value); break; case value > 172: this.pushData(5, value); break; default: this.pushData(0, value); } },
يتم استخدام Vue.js ChartJS على النحو التالي:
// Example.js import { Bar } from 'vue-chartjs' export default { extends: Bar, mounted () { this.renderChart({ labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], datasets: [ { label: 'GitHub Commits', backgroundColor: '#f87979', data: [40, 20, 12, 39, 10, 40, 39, 80, 40, 20, 12, 11] } ] }) } }
يمكنك استيراد نمط الرسم البياني الضروري ، وتوسيع المكون الخاص بك ، واستخدام this.renderChart
عرض المخطط.
في حالتنا ، من الضروري تحديث الجدول عند وصول بيانات جديدة ، لذلك نقوم بإخفاء الشاشة في طريقة updateChart
منفصلة updateChart
mounted
ونستخدم الساعة لمراقبة values
:
HeartRateChart.vue <script> import { Scatter } from 'vue-chartjs'; export default { extends: Scatter, name: 'HeartRateChart', props: { values: { type: Array, default: () => [[], [], [], [], [], []], }, }, watch: { values() { this.updateChart(); }, }, mounted() { this.updateChart(); }, methods: { updateChart() { this.renderChart({ datasets: [ { label: 'Chilling', data: this.values[0], backgroundColor: '#4f775c', borderColor: '#4f775c', showLine: true, fill: false, }, { label: 'Very light', data: this.values[1], backgroundColor: '#465f9b', borderColor: '#465f9b', showLine: true, fill: false, }, { label: 'Light', data: this.values[2], backgroundColor: '#4e4491', borderColor: '#4e4491', showLine: true, fill: false, }, { label: 'Moderate', data: this.values[3], backgroundColor: '#6f2499', borderColor: '#6f2499', showLine: true, fill: false, }, { label: 'Hard', data: this.values[4], backgroundColor: '#823e62', borderColor: '#823e62', showLine: true, fill: false, }, { label: 'Maximum', data: this.values[5], backgroundColor: '#8a426f', borderColor: '#8a426f', showLine: true, fill: false, }, ], }, { animation: false, responsive: true, maintainAspectRatio: false, elements: { point: { radius: 0, }, }, scales: { xAxes: [{ display: false, }], yAxes: [{ ticks: { beginAtZero: true, fontColor: '#394365', }, gridLines: { color: '#2a334e', }, }], }, }); }, }, }; </script>
طلبنا جاهز. ولكن ، حتى لا نقفز أمام الشاشة وننقل أنفسنا إلى المستوى الخامس ، دعنا نضيف زرًا سيولد بيانات عشوائية لجميع المستويات الخمسة لنا:
// App.vue <div> <button v-if=!heartRate @click=onClickTest class=blue>Test dataset</button> </div> ... import data from './__mock__/data'; ... onClickTest() { this.heartRateData = [ data(300, 60, 100), data(300, 104, 114), data(300, 133, 152), data(300, 152, 172), data(300, 172, 190), ]; this.heartRate = 73; },
// __mock__/date.js const getRandomIntInclusive = (min, max) => Math.floor(Math.random() * ((Math.floor(max) - Math.ceil(min)) + 1)) + Math.ceil(min); export default (count, from, to) => { const array = []; for (let i = 0; i < count; i += 1) { array.push({ y: getRandomIntInclusive(from, to), x: i }); } return array; };
النتيجة:

الاستنتاجات
يعد استخدام Web Bluetooth API أمرًا بسيطًا للغاية. هناك لحظات تحتاج إلى قراءة البيانات باستخدام عوامل تشغيل أحادي البت ، ولكن هذه منطقة محددة. من السلبيات بالطبع هو الدعم. في الوقت الحالي هو الكروم فقط ، والهواتف المحمولة كروم وفقط على الروبوت.

مصادر جيثب
تجريبي