предисловие
Автор - фронтовик.После изучения апплета в течение некоторого времени, я решил сделать программное обеспечение для мобильного телефона, чтобы попрактиковаться своими руками.Я обычно люблю музыку, и я обнаружил, что апплет каждой музыкальной платформы относительно Простой.Поэтому я выбрал это направление для имитационного обучения.Я также столкнулся со многими проблемами в процессе.Решая эти проблемы,я также добился некоторых успехов.Сегодня я поделюсь с вами самой сложной частью этой небольшой программы.воспроизводить музыкуРазличные проблемы и решения в этой части.
Прежде всего, спасибо поставщику API этого проекта.binaryify
Я выбрал этот проект, потому что back-end API предоставляется большим парнем. Когда вам нужны данные, вам нужно только инициировать некоторые запросы интерфейса. Он больше подходит для новичков, таких как я, чтобы начать работу, и вам нужно только написать некоторые простая логика интерфейса.
Поскольку на странице воспроизведения нужно заниматься многими вещами (например, обработкой и отображением текстов песен, быстрой перемоткой вперед и назад индикатора выполнения и т. д.), и имеется много ям, для того, чтобы описать как можно яснее, это статья в основном посвящена введению ивоспроизводить музыкуРазличные операции, связанные с этим проектом, будут подробно представлены на других страницах этого проекта, которые будут подробно описаны в последующих статьях Спасибо, читатели, за понимание.
Предварительный просмотр интерфейса проекта:
git-адрес
Облачная деревня и видеомодули еще не разработаны, и будут написаны, когда будет время в будущем.Этот проект будет время от времени обновляться, и я напишу документ об использовании проекта, когда у меня будет время в будущем.
Официально начать
Есть несколько запросов интерфейса для воспроизведения музыки, почти все из которых нужно выполнятьидентификатор песни, на всех страницах этого проекта страница воспроизведения существует как независимая страница. Когда другие страницы переходят на страницу воспроизведения, они будут нестиидентификатор песни
Интерфейсная инкапсуляция
В этом проекте используется очень много интерфейсных запросов, для удобства я инкапсулирую его вutils
в папкеapi.js
файл, а затем сослаться на файл управления интерфейсом на странице.
// method(HTTP 请求方法),网易云API提供get和post两种请求方式
const GET = 'GET';
const POST = 'POST';
// 定义全局常量baseUrl用来存储前缀
const baseURL = 'http://neteasecloudmusicapi.zhaoboy.com';
function request(method, url, data) {
return new Promise(function (resolve, reject) {
let header = { //定义请求头
'content-type': 'application/json',
};
wx.request({
url: baseURL + url,
method: method,
data: method === POST ? JSON.stringify(data) : data,
header: header,
success(res) {
//请求成功
//判断状态码---errCode状态根据后端定义来判断
if (res.data.code == 200) { //请求成功
resolve(res);
} else {
//其他异常
reject('运行时错误,请稍后再试');
}
},
fail(err) {
//请求失败
reject(err)
}
})
})
}
const API = {
getSongDetail: (data) => request(GET, `/song/detail`, data), //获取歌曲详情
getSongUrl:(data) => request(GET, `/song/url`, data), //获取歌曲路径
};
module.exports = {
API: API
}
Здесь показаны только два API-интерфейса запросов, используемые на этой странице, и их можно использовать, когда они представлены на странице, для которой требуются интерфейсные запросы.
const $api = require('../../utils/api.js').API;
обработка музыки
источник данных страницы
используется на этой страницеdata
источник данных
data: {
musicId: -1,//音乐id
hidden: false, //加载动画是否隐藏
isPlay: true, //歌曲是否播放
song: [], //歌曲信息
hiddenLyric: true, //是否隐藏歌词
backgroundAudioManager: {}, //背景音频对象
duration: '', //总音乐时间(00:00格式)
currentTime: '00:00', //当前音乐时间(00:00格式)
totalProcessNum: 0, //总音乐时间 (秒)
currentProcessNum: 0, //当前音乐时间(秒)
storyContent: [], //歌词文稿数组,转化完成用来在页面中使用
marginTop: 0, //文稿滚动距离
currentIndex: 0, //当前正在第几行
noLyric: false, //是否有歌词
slide: false //进度条是否在滑动
},
**Пример перехода на другие страницы:** Переход на страницу воспроизведения с других страниц с параметром musicId
//播放音乐
playMusic: function (e) {
let musicId = e.currentTarget.dataset.in.id // 获取音乐id
// 跳转到播放页面
wx.navigateTo({
url: `../play/play?musicId=${musicId}`
})
},
Жизненный цикл onLoad
существуетplay.js
изonLoad
В функции жизненного цикла черезoptions
Получить его с других страницmusicId
этот параметр и вызовplay()
функция
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
const musicId = options.musicId //获取到其他页面传来的musicId
this.play(musicId) //调用play方法
},
функция воспроизведения
play()
Функция требует один параметр:musicId
, Этот формальный параметр очень важен, и последующие запросы к интерфейсу должны его использовать.
//播放音乐
play(musicId) {
const that = this;//将this对象复制给that
that.setData({
hidden: false,
musicId
})
app.globalData.musicId = musicId // 将当前音乐id传到全局
// 通过musicId发起接口请求,请求歌曲详细信息
//获取到歌曲音频,则显示出歌曲的名字,歌手的信息,即获取歌曲详情;如果失败,则播放出错。
$api.getSongDetail({ ids: musicId }).then(res => {
// console.log('api获取成功,歌曲详情:', res);
if (res.data.songs.length === 0) {
that.tips('服务器正忙~~', '确定', false)
} else { //获取成功
app.globalData.songName = res.data.songs[0].name
that.setData({
song: res.data.songs[0], //获取到歌曲的详细内容,传给song
})
wx.request({ // 获取歌词
url: 'http://47.98.159.95/m-api/lyric',
data: {
id: musicId
},
success: res => {
if (res.data.nolyric || res.data.uncollected) { //该歌无歌词,或者歌词未收集
// console.log("无歌词")
that.setData({
noLyric: true
})
}
else { //如果有歌词,先调用sliceNull()去除空行,再调用parseLyric()格式化歌词
that.setData({
storyContent: that.sliceNull(that.parseLyric(res.data.lrc.lyric))
})
}
}
})
// 通过音乐id获取音乐的地址,请求歌曲音频的地址,失败则播放出错,成功则传值给createBackgroundAudioManager(后台播放管理器,让其后台播放)
$api.getSongUrl({ id: musicId }).then(res => {
//请求成功
if (res.data.data[0].url === null) { //获取出现错误出错
that.tips('音乐播放出了点状况~~', '确定', false)
} else {
// 调用createBackgroundAudioManager方法将歌曲url传入backgroundAudioManager
that.createBackgroundAudioManager(res.data.data[0]);
}
})
.catch(err => {
//请求失败
that.tips('服务器正忙~~', '确定', false)
})
}
})
.catch(err => {
//请求失败
that.tips('服务器正忙~~', '确定', false)
})
},
Общая идея такова:
- Сначала запросите подробную информацию о песне через musicId (песня, исполнитель, изображение песни и т. д.).
- После успешного приобретения информация о тексте песни будет получена (существует проблема с исходным адресом запроса текста, что приводит к изменению интерфейса здесь, поэтому он не упакован и не используется напрямую).
wx.request
сделал запрос), если в результате запроса есть текст, он будет запрошен обратноЛирические данныеустановить на источник данныхstoryContent
, лирика в это время не обработана, а значит лирику нужно обработать, сначала звонитеparseLyric()
отформатировать текст, затем позвонитеsliceNull()
удалить пустые строки.
Если в песне нет слов (например, чистая музыка, такая как фортепианная музыка, не имеет слов, или некоторые очень нишевые личные песни не имеют загруженных слов), установитеnoLyric
дляtrue
, После настройки на странице будет отображаться: чистая музыка, без текстов.
Нажмите, чтобы переключить текст и обложку
showLyric() {
this.setData({
hiddenLyric: !this.data.hiddenLyric
})
},
отформатировать текст
После запроса текста обратно текст должен быть разветвлен.
//格式化歌词
parseLyric: function (text) {
let result = [];
let lines = text.split('\n'), //切割每一行
pattern = /\[\d{![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4c2ec80ed514746bdd642986f54913f~tplv-k3u1fbpfcp-zoom-1.image)2}:\d{2}.\d+\]/g;//用于匹配时间的正则表达式,匹配的结果类似[xx:xx.xx]
// console.log(lines);
//去掉不含时间的行
while (!pattern.test(lines[0])) {
lines = lines.slice(1);
};
//上面用'\n'生成数组时,结果中最后一个为空元素,这里将去掉
lines[lines.length - 1].length === 0 && lines.pop();
lines.forEach(function (v /*数组元素值*/, i /*元素索引*/, a /*数组本身*/) {
//提取出时间[xx:xx.xx]
var time = v.match(pattern),
//提取歌词
value = v.replace(pattern, '');
// 因为一行里面可能有多个时间,所以time有可能是[xx:xx.xx][xx:xx.xx][xx:xx.xx]的形式,需要进一步分隔
time.forEach(function (v1, i1, a1) {
//去掉时间里的中括号得到xx:xx.xx
var t = v1.slice(1, -1).split(':');
//将结果压入最终数组
result.push([parseInt(t[0], 10) * 60 + parseFloat(t[1]), value]);
});
});
// 最后将结果数组中的元素按时间大小排序,以便保存之后正常显示歌词
result.sort(function (a, b) {
return a[0] - b[0];
});
return result;
},
Текст песни удалить пустые строки
sliceNull: function (lrc) {
var result = []
for (var i = 0; i < lrc.length; i++) {
if (lrc[i][1] !== "") {
result.push(lrc[i]);
}
}
return result
},
- Затем используйте идентификатор, чтобы получить путь воспроизведения песни, и после получения источника данных аудио вызовите
createBackgroundAudioManager()
Функция, передайте только что полученный источник аудиоданных. (подробно ниже)
- Если есть проблема с какой-либо из ссылок, появится всплывающее сообщение, будет вызвана функция tip(), и будет возвращена домашняя страница.
Дружеские советы
- Есть много запросов на интерфейс страницы воспроизведения, и звонки часты.Помимо некоторых колебаний сети, в интерфейсных вызовах неизбежно будут возникать некоторые сбои.Чтобы предоставить пользователям более качественную обратную связь и подсказки, официальный дисплей WeChat используется модальное диалоговое окно.
wx.showModal
, написанный какtips()
функция, когда вы хотите вызвать диалоговое окно, вызовите его напрямуюtips()
Функция в порядке. После возникновения ошибки пользователь нажимает кнопку «ОК», чтобы активировать функцию обратного вызова.res.confirm
Суждение, затем вернитесь на домашнюю страницу, потому что навигация облачного мобильного приложения NetEase находится в голове, поэтому я использую пользовательский компонент для навигации, а не использую его.tabBar
, используется для перехода по страницамwx.navigateTo()
, если вы используетеtabBar
, то скачок следует заменить наwx.switchTab()
tips(content, confirmText, isShowCancel) {
wx.showModal({
content: content,
confirmText: confirmText,
cancelColor: '#DE655C',
confirmColor: '#DE655C',
showCancel: isShowCancel,
cancelText: '取消',
success(res) {
if (res.confirm) {
// console.log('用户点击确定')
wx.navigateTo({
url: '/pages/find/find'
})
} else if (res.cancel) {
// console.log('用户点击取消')
}
}
})
},
- Запрос интерфейса занимает некоторое время.Есть период ожидания при нарезке песен, запросе различных данных и загрузке страницы.Для повышения удобства использования лучше всего добавить несколько анимаций ожидания при загрузке.Я буду использовать это напрямую здесь. Более простой способ добавить
loading
ярлык, через источник данныхhidden
, контролироватьloading
Отображается ли анимация, изначально установлено значениеfalse
, а затем после завершения запроса данных измените его наtrue
.
В wxml:
<loading hidden="{{hidden}}">
拼命加载中...
</loading>
Музыкальный проигрыватель
Как было сказано выше, после того, как интерфейс запросит обратно аудиотракт, будет вызвана эта функция, и данные запроса будут переданы в качестве параметра, поэтому давайте сейчас проанализируем эту функцию.
// 背景音频播放方法
createBackgroundAudioManager(res) {
const that = this;//将this对象复制给that
const backgroundAudioManager = wx.getBackgroundAudioManager(); //调用官方API获取全局唯一的背景音频管理器。
console.log(backgroundAudioManager.src);
if (res.url != null) {
if (backgroundAudioManager.src != res.url) { //首次放歌或者切歌
that.setData({ //重设一下进度,避免切歌部分数据更新过慢
currentTime: '00:00', //当前音乐时间(00:00格式)
currentProcessNum: 0, //当前音乐时间(秒)
marginTop: 0, //文稿滚动距离
currentIndex: 0, //当前正在第几行
})
backgroundAudioManager.title = that.data.song.name; //把title音频标题给实例
backgroundAudioManager.singer = that.data.song.ar[0].name; //音频歌手给实例
backgroundAudioManager.coverImgUrl = that.data.song.al.picUrl; //音频图片 给实例
backgroundAudioManager.src = res.url; // 设置backgroundAudioManager的src属性,音频会立即播放
let musicId = that.data.musicId
app.globalData.history_songId = that.unique(app.globalData.history_songId, musicId) //去除重复历史
}
that.setData({
isPlay: true, //是否播放设置为true
hidden: true, //隐藏加载动画
backgroundAudioManager
})
}
app.globalData.backgroundAudioManager = backgroundAudioManager
//监听背景音乐进度更新事件
backgroundAudioManager.onTimeUpdate(() => {
that.setData({
totalProcessNum: backgroundAudioManager.duration,
currentTime: that.formatSecond(backgroundAudioManager.currentTime),
duration: that.formatSecond(backgroundAudioManager.duration)
})
if (!that.data.slide) { //如果进度条在滑动,就暂停更新进度条进度,否则会出现进度条进度来回闪动
that.setData({
currentProcessNum: backgroundAudioManager.currentTime,
})
}
if (!that.data.noLyric) { //如果没有歌词,就不需要调整歌词位置
that.lyricsRolling(backgroundAudioManager)
}
})
backgroundAudioManager.onEnded(() => { //监听背景音乐自然结束事件,结束后自动播放下一首。自然结束,调用go_lastSong()函数,即歌曲结束自动播放下一首歌
that.nextSong();
})
},
Логика в функции воспроизведения звука относительно сложна, общая идея такова:
- Сначала создайте
BackgroundAudioManager
например, черезwx.getBackgroundAudioManager
Получать.
Затем здесь необходимо принять решение, потому что при вызове этого метода есть несколько ситуаций, одна из которых - воспроизвести песню в первый раз или переключить песни, а другая - не переключать песни при входе, поэтому необходимо определить получает ли текущий идентификатор музыки URL-адрес или нетbackgroundAudioManager.src
, если не равно, это первый случай, вам нужно преобразовать песнюmusicId
передачаunique()
Метод дедупликации, хранящийся в глобальномhistory_songId[]
,этоисторический плейлистВ основном используется для переключения предыдущей песни для пользователя, что будет подробно описано позже.
Затем установите экземплярtitle
,singer
,coverImgURL
,src
, при установке новогоsrc
Когда музыка начинает воспроизводиться автоматически, настройка этих свойств в основном используется для отображения и совместного использования собственного аудиоплеера (обратите внимание, что название должно быть установлено).После настройки используйте апплет для воспроизведения музыки на мобильном телефоне, а собственный появится аудиоплеер устройства, как показано на рисунке:
Это чувствует себя довольно хорошо, но, к сожалению, кажется, что пока что этот родной аудиоплеер не может устанавливать тексты, а может устанавливать только основные свойства.Это тоже небольшое сожаление.Я надеюсь, что команда WeChat сможет улучшить его в будущем.
Дедупликация исторических плейлистов
Функция: Каждый раз, когда пользователь воспроизводит песню, она сохраняется в списке истории. Перед сохранением оценивается, существует ли уже песня. Если она не существует, она сохраняется непосредственно в конце массива списка исторических песен. Если он уже существует, сначала удалите старую запись и сохраните новую.
// 历史歌单去重
unique(arr, musicId) {
let index = arr.indexOf(musicId) //使用indexOf方法,判断当前musicId是否已经存在,如果存在,得到其下标
if (index != -1) { //如果已经存在在历史播放中,则删除老记录,存入新记录
arr.splice(index, 1)
arr.push(musicId)
} else {
arr.push(musicId) //如果不存在,则直接存入历史歌单
}
return arr //返回新的数组
},
- Вторым шагом является обновление некоторых данных источника данных.Операция и функции относительно просты, поэтому я не буду вдаваться в подробности.
- Третий шаг очень важен, используйте
backgroundAudioManager.onTimeUpdate()
Следите за прогрессом обновления фоновой музыки, и второе обновление индикатора выполнения страницы связано с этим!
wxml:
<view class="page-slider">
<view>
{{currentTime}}
</view>
<slider class="slider_middle" bindchange="end" bindtouchstart="start" max="{{totalProcessNum}}" min="0" backgroundColor="rgba(255,255,255,.3)"
activeColor="rgba(255,255,255,.8)" value="{{currentProcessNum}}" block-size="12"></slider>
<view>
{{duration}}
</view>
</view>
backgroundAudioManager.currentTime
а такжеbackgroundAudioManager.currentTime
Он вернет позицию воспроизведения звука и длину звука соответственно в секундах, а текущее время слева от индикатора выполнения и общую продолжительность песни справа необходимо отобразить в формате 00:00, поэтому используйтеformatSecond()
форматировать секунды
время форматирования
// 格式化时间
formatSecond(second) {
var secondType = typeof second;
if (secondType === "number" || secondType === "string") {
second = parseInt(second);
var minute = Math.floor(second / 60);
second = second - minute * 60;
return ("0" + minute).slice(-2) + ":" + ("0" + second).slice(-2);
} else {
return "00:00";
}
},
Прокрутите тексты песен
wxml:
<!-- 歌词 -->
<!-- 需要设置高度,否则scroll-top可能失效 -->
<scroll-view
hidden="{{hiddenLyric}}"
scroll-y="true"
scroll-with-animation='true'
scroll-top='{{marginTop}}'
class="body-scroll"
>
<view class='contentText'>
<view class="contentText-noLyric" wx:if="{{noLyric==true}}">纯音乐,无歌词 </view>
<block wx:for='{{storyContent}}' wx:key="index">
<view class="lyric">
<view class="lyric-text {{currentIndex == index ? 'currentTime' : ''}}">{{item[1]}}</view>
</view>
</block>
</view>
</scroll-view>
- Прокрутка текста на экране определяет, сколько строк содержит текущий текст, исходя из времени текста и текущей позиции аудио.Автоматическая прокрутка использует количество строк для расчета высоты.Установив источник данных
marginTop
, это значение действует наscroll-view
изscroll-top
, чтобы добиться автоматической прокрутки, следует отметить, чтоscroll-view
Необходимо установить высоту, иначеscroll-top
может потерпеть неудачу - по решению
currentIndex
Является ли оно равным значению индекса в цикле for на странице, чтобы добавить имя класса к текущей песне, чтобы выделить ее.
// 歌词滚动方法
lyricsRolling(backgroundAudioManager) {
const that = this
// 歌词滚动
that.setData({
marginTop: (that.data.currentIndex - 3) * 39
})
// 当前歌词对应行颜色改变
if (that.data.currentIndex != that.data.storyContent.length - 1) {//不是最后一行
// var j = 0;
for (let j = that.data.currentIndex; j < that.data.storyContent.length; j++) {
// 当前时间与前一行,后一行时间作比较, j:代表当前行数
if (that.data.currentIndex == that.data.storyContent.length - 2) { //倒数第二行
//最后一行只能与前一行时间比较
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[that.data.storyContent.length - 1][0])) {
that.setData({
currentIndex: that.data.storyContent.length - 1
})
return;
}
} else {
if (parseFloat(backgroundAudioManager.currentTime) > parseFloat(that.data.storyContent[j][0]) && parseFloat(backgroundAudioManager.currentTime) < parseFloat(that.data.storyContent[j + 1][0])) {
that.setData({
currentIndex: j
})
return;
}
}
}
}
},
событие индикатора выполнения
Когда индикатор выполнения начнет скользить, изменитеslide
Установите значение true, затемbackgroundAudioManager.onTimeUpdate()
Обновите источник данных вcurrentProcessNum
Повторно это делаться не будет, что снимает проблему дрожания индикатора выполнения.
Проблема джиттера: Как показано на рисунке, когда вы перетаскиваете индикатор выполнения для быстрой перемотки музыки вперед или назад, вы можете видеть, что маленький ползунок явно дрожит.onTimeUpdate()
Постоянный мониторинг и изменение источника данныхcurrentProcessNum
, из-за чего маленький ползунок продолжает прыгать вперед и назад во время процесса перетаскивания.
//进度条开始滑动触发
start: function (e) {
// 控制进度条停,防止出现进度条抖动
this.setData({
slide: true
})
},
Когда смахивание закончится, пройдитеbackgroundAudioManager.seek(position)
чтобы звук перескакивал науказанное место, а затем определить, сколько строк достиг текущий текст, и сразу установитьcurrentIndex
, чтобы слова были изменены в методе прыжка слов, описанном выше.marginTop
значение, текст переместится на соответствующую позицию.
//结束滑动触发
end: function (e) {
const position = e.detail.value
let backgroundAudioManager = this.data.backgroundAudioManager //获取背景音频实例
// console.log(position)
backgroundAudioManager.seek(position) //改变歌曲进度
this.setData({
currentProcessNum: position,
slide: false
})
// 判断当前是多少行
for (let j = 0; j < this.data.storyContent.length; j++) {
// console.log('当前行数', this.data.currentIndex)
// console.log(parseFloat(backgroundAudioManager.currentTime))
// console.log(parseFloat(this.data.storyContent[j][0]))
// 当前时间与前一行,后一行时间作比较, j:代表当前行数
if (position < parseFloat(this.data.storyContent[j][0])) {
this.setData({
currentIndex: j - 1
})
return;
}
}
}
- Четвертый шаг – использование
backgroundAudioManager.onEnded()
Следите за естественным окончанием фоновой музыки и вызывайте его, когда оно заканчиваетсяnextSong()
Функция, эта функция используется для воспроизведения песни из списка воспроизведения.
Воспроизвести предыдущую дорожку, воспроизвести следующую дорожку
Играйте в предыдущую песню, тогда эта песня сейчас становится главной песней, потому что каждая песня в настоящее время играет песню, будет поставленаpush()
Перейти к историческому списку, затем к текущей песне (удалить последний элемент в массиве исторического списка из массива ипробка для головыдобавить в список воспроизведения) в список воспроизведения для воспроизведения, а затем вызватьplay()
Метод просто отличный (передать последний элемент массива нового списка истории после удаления последнего элемента, то есть предпоследнего элемента исходного списка истории)
// 播放上一首歌曲
beforeSong() {![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d07ee4e5583d49b482f2046481c70053~tplv-k3u1fbpfcp-zoom-1.image)
if (app.globalData.history_songId.length > 1) { //前面有歌
app.globalData.waitForPlaying.unshift(app.globalData.history_songId.pop())//将当前播放歌曲从前插入待放列表
this.play(app.globalData.history_songId[app.globalData.history_songId.length - 1]) //播放历史歌单歌曲
} else {
this.tips('前面没有歌曲了哦', '去选歌', true)
}
},
Воспроизведение следующей песни.Если длина воспроизводимого массива больше 0, удалите первый элемент массива и верните его вplay()
метод
// 下一首歌曲
nextSong() {
if (app.globalData.waitForPlaying.length > 0) {
this.play(app.globalData.waitForPlaying.shift())//删除待放列表第一个元素并返回播放
} else {
this.tips('后面没有歌曲了哦', '去选歌', true)
}
},
Пауза и воспроизведение
Сравните просто, получите данные оригинальныеbackgroundAudioManager
, через собственныйpause()
,play()
способ воспроизведения и паузы
// 播放和暂停
handleToggleBGAudio() {
const backgroundAudioManager = this.data.backgroundAudioManager
//如果当前在播放的话
if (this.data.isPlay) {
backgroundAudioManager.pause();//暂停
} else { //如果当前处于暂停状态
backgroundAudioManager.play();//播放
}
this.setData({
isPlay: !this.data.isPlay
})
},
Суммировать
Этот проект не сложен и подходит для начинающих, потому что нет необходимости писать сложный бэкенд, просто напишите js-логику, и у вас будут большие достижения, когда вы услышите, что ваш собственный апплет-имитатор может воспроизводить музыку. в то же время, есть еще некоторые маленькие дыры, ожидающие, что вы разберетесь с ними.Когда я писал эту небольшую программу, я также столкнулся с множеством проблем.Когда я сталкиваюсь с проблемой, я сначала думаю, если я не могу понять это , иду смотреть что писали другие большие ребята Делюсь опытом, так как мой опыт не особо богат, а я только начинаю, и не думаю о решении многих проблем. указать и с нетерпением ждем нашего общего роста.
Этот проект используется только для практики и закрепления знаний, а не для коммерческого использования. Если есть какие-либо нарушения, свяжитесь со мной для исправления.
Кроме того, если вам это нравится, я позже подробно представлю общую разработку этого проекта, такую как разработка компонентов домашней страницы, поиска, входа и т. д. Спасибо, что увидели это, если вы не возражаете, пошевелите пальцами и поставьте мне лайк, спасибо!