ساخت اپلیکیشن مدیریت هزینه های مالی با جاوا اسکریپت — از صفر تا صد
در این مقاله، شیوه ایجاد یک اپلیکیشن کوچک و کارآمد را بررسی میکنیم که تاریخچه هزینههای مالی شما را نگهداری میکند. این اپلیکیشن مدیریت هزینه های مالی امکان گردآوری همه رسیدها را در یک پوشه «دراپباکس» (Dropbox) فراهم میسازد و سپس میتوانید با کلیک روی یک دکمه آنها را به صورت ماهانه تنظیم کنید.
اپلیکیشنی که قصد ساخت آن را داریم به طور خاص زمانی مفید است که میخواهید هزینههای شخصی خود را حسابداری کنید چون به صورت معمول این کار را به شکل ماهانه انجام میدهیم. اینکه همه هزینههای مالی انجام شده طی یک ماه را یک جا و در یک پوشه واحد گردآوری کنیم، موجب صرفهجویی زیادی در زمانمان میشود.
در این راهنما موارد زیر بررسی شدهاند:
- ایجاد یک حساب دراپباکس و راهاندازی محیط توسعه پروژه
- ایجاد UI با استفاده از جاوا اسکریپت خالص که شامل مراحل واکشی دادهها، رندر کردن عناصر، مدیریت مقدماتی «حالت» (State) و ناوبری ساده است.
- برخی متدهای API دراپباکس برای دریافت و جابجایی فایلها نیز مورد استفاده قرار میگیرند.
راهاندازی دراپباکس
برای ساختن اپلیکیشنی بر مبنای دراپباکس، ابتدا باید یک حساب دراپباکس (+) داشته باشید. پس از این که در این وب سایت ثبت نام کردید، به بخش توسعهدهندگان (+) بروید. در این بخش در منوی سمت چپ داشبورد گزینه My apps را انتخاب کرده و روی Create app کلیک کنید.
تنظیمات زیر را انتخاب کرده و نام یکتایی برای اپلیکیشن خود انتخاب کنید.
در داشبورد، به بخش OAuth 2 زیر Generated access token بروید و روی دکمه Generate کلیک کنید تا یک accessToken برای API به دست آورید. این توکن دسترسی را برای استفاده آتی ذخیره کنید.
اکنون میتوانیم اپلیکیشن دسکتاپ دراپباکس (+) را نصب کنیم. با استفاده از اطلاعات احراز هویت جدید که به دست آوردید وارد اپلیکیشن شوید تا بتوانید پوشهای را که همان نام اپلیکیشن اخیراً ایجادشده را دارد مشاهده میکنید. ما در این مقاله از نام ExpenseOrganizer استفاده کردهایم.
برخی رسیدها و فاکتورهای خود را در این پوشه قرار دهید تا بتوانید از طریق API به آنها دسترسی داشته باشید و در زمان اتمام پروژه آنها را در پوشههای مشخصی دستهبندیشدهای به صورت زیر داشته باشید:
راهاندازی کدبیس
اکنون باید کدبیس خود را راهاندازی کنیم. ما از سادهترین ساختار ممکن استفاده میکنیم که یک فایل index.html با لینکهای به فایل جاوا اسکریپت و یک استایلشیت است. همچنین باید یک نام برای اپلیکیشن خود در تگ <title> قرار دهیم.
نکته: کد نهایی این راهنما را میتوانید در این صفحه (+) مشاهده کنید و میتوانید در صورت علاقه آن را کلون کنید. با این حال باید کلید API دراپباکس خود را به پروژه اضافه کنید تا کار کند.
نصب و افزودن دراپباکس
اکنون باید کتابخانه دراپباکس را در پروژه خود نصب کنیم. به طور معمول به این منظور باید کاری مانند زیر انجام دهیم:
npm install dropox # or yarn add dropbox
با این حال در کد Scrimba که در بخش قبل معرفی کردیم، ما صرفاً کتابخانه دراپباکس را به صورت یک وابستگی در نوار کناری چپ اضافه کردهایم چون روش افزودن پکیجهای npm در Scrimba چنین است.
گام بعدی ایمپورت کردن دراپباکس و ایجاد یک وهله از کلاس Dropbox است. ما آن را dbx مینامیم و توکن خود را به آن ارسال میکنیم و کتابخانه منتخبمان را که در این مورد fetch است واکشی میکنیم. اگر ترجیح میدهید از axios با هر کتابخانه واکشی دیگر استفاده کنید، میتوانید از آن استفاده کنید و هیچ مشکلی وجود ندارد.
1import { Dropbox } from 'dropbox';
2const accessToken = '<your-token-from-dashboard>';
3const dbx = new Dropbox({
4 accessToken,
5 fetch
6});
دریافت و نمایش هزینهها
در این بخش مراحلی که برای دریافت و نمایش هزینهها مورد نیاز است را مورد بررسی قرار میدهیم.
واکشی کردن دادهها
برای نمایش دادهها در اپلیکیشن باید ابتدا آنها را واکشی کنیم. بدین منظور رسیدهایی که در پوشه دراپباکس خود داریم را دریافت میکنیم.
به این منظور میتوانیم از متد ()filesListFolder استفاده کنیم. این متد نام یک پوشه را میگیرد و یک Promise بازگشت میدهد که وقتی resolve شود، محتوای پوشه را در اختیار ما قرار میدهد. البته این متد یک فوت کوزهگری دارد، زیرا برای تعیین یک مسیر ریشه (پوشه مبنایی که در آن هستیم) باید یک رشته خالی به صورت ‘ ‘ بنویسیم و نوشتن آن به صورت ‘/’ صحیح نیست.
1dbx.filesListFolder({
2 path: ''
3}).then(res => console.log(res))
زمانی که این متد را برای بازیابی فایلها از حساب دراپباکس فراخوانی کنیم، باید چیزی مانند تصویر زیر ببینیم:
بنابراین ما یک آرایه entries داریم که آرایهای از شیءها است. هر شیء در آرایه entries، فایل ما (و در ادامه برخی از آنها میتوانند پوشههایی باشند که دادههایمان را در آنها سازماندهی میکنیم) به همراه تگهای tag ،name ،id و مشخصههای زیاد دیگر ارائه شده است. اینک تابعی مینویسیم که هر فایل را نمایش میدهد. tag مشخصهای است که نوع مدخل بازیابی شده را مشخص میکند که یک file یا یک folder است.
زمانی که همه فایلها را واکشی کردیم، آنها را در اپلیکیشن خود ذخیره میکنیم. ما میتوانیم شیئی به نام state برای انجام این کار بسازیم. همچنین میتوانیم آرایهای داشته باشیم که files را نگهداری کند و یک رشته نیز بسازیم که رد rootPath ما را ذخیره کند.
1const state = {
2 files: [],
3 rootPath: ''
4}
رندر کردن دادهها
یکی از روشهای رندر کردن دادهها این است که یک لیست نامرتب <ul> به کد HTML خود اضافه کنیم، آن را با جاوا اسکریپت انتخاب کرده و با فایلهای واکشی شده از دراپباکس مقداردهی کنیم. در این مرحله یک placeholder به صورت Loading… به فایل index.html خود اضافه میکنیم که به محض نمایش فایلهای واکشی شده بازنویسی خواهد شد.
1<main>
2 <ul class="dbx-list js-file-list">Loading...</ul>
3</main>
اینک میتوانیم fileListElem را بسازیم که لیست نامرتب را انتخاب میکند.
1const fileListElem = document.querySelector('.js-file-list')
ما میتوانیم همه فایلهای مرتبسازی شده به صورت الفبایی را به fileListElem.innerHTML اضافه کنیم تا مطمئن شویم که پوشهها را ابتدا قرار دادهایم. سپس هر پوشه و فایل را با استفاده از ('')join به یک <id> نگاشت (map) میکنیم تا از رندر کردن آرایه به جای رشته جلوگیری کنیم.
اما در این مرحله هیچ چیز روی صفحه نمایش نمییابد. دلیل این مسئله آن است که باید دادهها را واکشی کرده و سپس فایلها را با ()renderFiles رندر کنیم. در ادامه یک تابع کمکی ()init به این منظور میسازیم.
1const init = async () => {
2 // fetch files
3 const res = await dbx.filesListFolder({
4 path: state.rootPath,
5 limit: 100
6 })
7 // update state, but first, keep the existing files,
8 // and then add newly received files
9 state.files = [...state.files, ...res.entries]
10 renderFiles()
11}
میتوان حالت را استخراج و بهروزرسانی کرده و ()renderFiles را در یک متد جداگانه درج کرد.
1const init = async () => {
2 const res = await dbx.filesListFolder({
3 path: state.rootPath,
4 limit: 100
5 })
6 updateFiles(res.entries)
7}
8const updateFiles = files => {
9 state.files = [...state.files, ...files]
10 renderFiles()
11}
12// Let's also add a reset() function.
13// It's always nice to be able to start from scratch.
14const reset = () => {
15 state.files = []
16 init()
17}
اکنون ()init را به انتهای فایل index.js اضافه میکنیم تا کل فرایند راهاندازی شده و فایلها نمایش پیدا کنند. بدین ترتیب لیست فایلهای ما نمایش مییابد گرچه کمی خلوت به نظر میرسد.
میتوان آن را بهبود بخشید و برای این لیست آیتمها یک آیکون پوشه و فایل پیشفرض قرار داد.
برای این که همه چیز زیباتر به نظر برسد، یک فایل به نام icon.js میسازیم و آیکونهای base64 SVG را آنجا اضافه میکنیم. دلیل استفاده از این نوع آیکون آن است که کاربرد آنها در این راهنما راحتتر است و دیگر نیازی به رفتن به جای دیگر و دانلود کردن آنها وجود ندارد.
1export const fileIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWZpbGUiPjxwYXRoIGQ9Ik0xMyAySDZhMiAyIDAgMCAwLTIgMnYxNmEyIDIgMCAwIDAgMiAyaDEyYTIgMiAwIDAgMCAyLTJWOXoiPjwvcGF0aD48cG9seWxpbmUgcG9pbnRzPSIxMyAyIDEzIDkgMjAgOSI+PC9wb2x5bGluZT48L3N2Zz4='
2export const folderIcon = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBjbGFzcz0iZmVhdGhlciBmZWF0aGVyLWZvbGRlciI+PHBhdGggZD0iTTIyIDE5YTIgMiAwIDAgMS0yIDJINGEyIDIgMCAwIDEtMi0yVjVhMiAyIDAgMCAxIDItMmg1bDIgM2g5YTIgMiAwIDAgMSAyIDJ6Ij48L3BhdGg+PC9zdmc+'
اینک میتوانیم آیکونها را در فایل index.js ایمپورت کنیم:
1import { fileIcon, folderIcon } from './icons.js'
در این مرحله map. را در renderFiles خود بهروزرسانی میکنیم تا آیکونهای جدید را شامل شود.
1const renderFiles = () => {
2 fileListElem.innerHTML = state.files.sort((a, b) => {
3 // ... this part stays the same
4 }).map(file => {
5 const type = file['.tag']
6 let thumbnail
7 if (type === 'file') {
8 thumbnail = fileIcon
9 } else {
10 thumbnail = folderIcon
11 }
12 return `
13 <li class="dbx-list-item ${type}">
14 <img class="dbx-thumb" src="${thumbnail}">
15 ${file.name}
16 </li>
17 `
18 }).join('')
19}
اکنون فایل ما زیباتر به نظر میرسد.
سازماندهی فایلها و پوشهها
قابلیت اصلی اپلیکیشن ما در این است که با یک کلیک همه فایلها درون پوشه جابجا میشوند و بر حسب سال و سپس درون هر پوشه بر حسب ماه سازماندهی میشوند. قبل از هر چیز باید یک دکمه در فایل index.js بسازیم.
1<button type="button" class="dbx-btn js-organize-btn">Organize</button>
در وهله دوم مقداری کد جاوا اسکریپت به آن اضافه میکنیم.
1// select the button
2const organizeBtn = document.querySelector('.js-organize-btn')
3// add an event listener to it
4organizeBtn.addEventListener('click', e => {
5 console.log(e)
6})
ما در انتهای این راهنما مجدداً به سراغ این دکمه میآییم تا تابعی که فایلها را جابجا میکند را تکمیل کنیم. برای جابجایی فایلها میتوانیم از ()filesMoveBatchV2 استفاده کنیم. این متد فایلها را به صورت دستهای از یک پوشه به پوشه دیگر جابجا میکند. در این مورد ما میخواهیم فایلها را از پوشه root به پوشههای با نام مجزا جابجا کنیم.
این متد زمانی که به صورت بخشی از تابع async استفاده شود، بهترین پیادهسازی خود را خواهد داشت.
1const moveFiles = async () => {
2 let response = await dbx.filesMoveBatchV2()
3}
این متد entries را میپذیرد که آرایهای از شیءها است و شامل مشخصههای from_path و to_path است.
1// temporarily, pseudocode values for paths
2const entries = [{
3 from_path: 'origin_folder',
4 to_path: 'destination_folder'
5}]
6const moveFiles = async () => {
7 let response = await dbx.filesMoveBatchV2({ entries })
8}
()filesMoveBatchV2 اگر فراخوانی بیدرنگ موفق باشد و در واقع فایلهای معدودی برای پردازش ارائه شده باشند، مقدار success بازگشت میدهد.
با این حال در مورد حجم جابجایی بالا، شیئی به همراه یک مشخصه به نام async_job_id بازگشت میدهد و معنی آن این است که فراخوانی شما اجرا شده است و باید آن را در مرحله بعدی یعنی زمانی که تکمیل شده و دیگر در حالت in_progress نیست، با فراخوانی filesMoveBatchCheckV2 بررسی کنیم.
1// still, pseudocode values for paths
2const entries = {
3 from_path: 'origin_folder',
4 to_path: 'destination_folder'
5}
6const moveFiles = async () => {
7 let response = await dbx.filesMoveBatchV2({ entries })
8 const { async_job_id } = response
9 if (async_job_id) {
10 do {
11 response = await dbx.filesMoveBatchCheckV2({ async_job_id })
12 console.log(res)
13 } while (response['.tag'] === 'in_progress')
14 }
15}
اینک مسیرهای صحیح را برای شیء entries پیادهسازی میکنیم و آن را به ()moveFiles اضافه میکنیم.
برای تکمیل این قابلیت باید ()moveFiles را به دکمه Organise وصل کنیم. همچنین خوب است که متن دکمه را طوری بهروزرسانی کنیم که نشان دهد پردازش جابجایی فایلها آغاز شده و در زمان اتمام کار به حالت عادی بازگردانیم.
1organizeBtn.addEventListener('click', async e => {
2 const originalMsg = e.target.innerHTML
3 e.target.disabled = true
4 e.target.innerHTML = 'Working...'
5 await moveFiles()
6 e.target.disabled = false
7 e.target.innerHTML = originalMsg
8 reset()
9})
اکنون وقتی روی دکمه کلیک کنیم، میبینیم که تغییریافته و پیام in_progress در لاگ console دیده میشود. این صرفاً برای ما است تا ببینیم که دراپباکس فایلها را جابجا میکند.
زمانی که کار پایان یافت، پوشه سالانه به دست میآید.
چنان که میبینید همه رسیدهای مثال ما مربوط به یک سال هستند. برای این که بتوانید به داخل پوشه آنگاه کنید باید بتوانید روی آنها کلیک کنید و به این منظور باید قابلیت ناوبری را تکمیل کرده باشیم. در ادامه به پیادهسازی بخش ناوبری اپلیکیشن خود میپردازیم.
ناوبری در اپلیکیشن
ناوبری در دراپباکس عملاً شبیه به ناوبری در پوشههای فایل اکسپلورر در ویندوز یا نرمافزار Finder روی سیستمهای مک است. تنها چیزی که لازم داریم این است که پوشهای که در آن قرار داریم را عوض کنیم. در ادامه ابتدا این فرایند را به صورت دستی بررسی میکنیم و سپس کد آن را نیز مینویسیم تا پردازش به صورت خودکار اجرا شود.
اگر rootPath را در state تغییر دهیم و صفحه را بارگذاری مجدد کنیم:
1const state = {
2 files: [],
3 rootPath: '/2019'
4}
همه فایلهای آوریل 2019 را به دست میآوریم:
یک بار دیگر state را به صورت دستی بهروزرسانی میکنیم تا به پوشه آوریل یعنی ماه شماره 4 برویم و صفحه را بهروزرسانی میکنیم:
1const state = {
2 files: [],
3 rootPath: '/2019/4'
4}
بدین ترتیب همه رسیدها را مشاهده میکنید:
برای این که این فرایند کمی سادهتر شود، باید یک فیلد input اضافه کنیم، به طوری که بتوانیم نام پوشهای که میخواهیم به آن برویم را وارد کنیم و نیاز نباشد که هر بار از hard-code استفاده کنیم. بدین ترتیب فیلد input را درون یک عنصر <form> قرار میدهیم که در ادامه مشاهده میکنید.
همچنین در ادامه صفحه خود را با طراحی یک هدر زیبا در index.html زیباتر میسازیم.
1<header>
2 <h1>Expense Organizer</h1>
3 <form class="root-path-wrap js-root-path__form">
4 <h2><label for="root-path"> Folder Path</label></h2>
5 <input
6 type="text"
7 name="root-path"
8 id="root-path"
9 class="js-root-path__input"
10 value="/"
11 />
12 <button aria-label="Load path" class="dbx-btn" type="submit">
13 →
14 </button>
15 </form>
16</header>
اینک صفحه اپلیکیشن ما به صورت زیر در آمده است:
چنان که مشاهده میکنید، یک دکمه نیز برای تأیید مسیری که میخواهیم برویم تعبیه کردهایم. درون فایل index.js تلاش میکنیم state را به همان چیزی که بود بازگردانیم.
1const state = {
2 files: [],
3 rootPath: ''
4}
همچنین کمی کد جاوا اسکریپت اضافه میکنیم تا rootPath را با مقادیری از فیلد input بهروزرسانی کنیم:
1const rootPathForm = document.querySelector('.js-root-path__form')
2const rootPathInput = document.querySelector('.js-root-path__input')
3rootPathForm.addEventListener('submit', e => {
4 e.preventDefault();
5 state.rootPath = rootPathInput.value === '/'
6 ? ''
7 : rootPathInput.value.toLowerCase()
8
9 reset()
10})
بدین ترتیب کار ما به پایان رسیده و ناوبری اپلیکیشن عملیاتی شده است.
سخن پایانی
بدین ترتیب ما موفق شدیم با پیگیری مراحل معرفی شده در این راهنما اپلیکیشنی بسازیم که به رسیدهای مالی ما سر و سامان میبخشد. این موفقیت بزرگی محسوب میشود. اگر همچنان حس میکنید که موفق نشدهاید همه مفاهیم مطرحشده در این راهنما را به خوبی متوجه نشدهاید پیشنهاد میکنیم به صفحه کد منبع این اپلیکیشن (+) نیز سری بزنید تا توضیحات را با تفصیل بیشتری مشاهده کنید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- 11 ترفند بسیار کاربردی جاوا اسکریپت — به زبان ساده
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
==