
Halo semuanya, saya ingin membagikan hasil pemikiran saya tentang apa itu aplikasi web modern. Sebagai contoh, pertimbangkan merancang papan buletin untuk komik. Dalam arti tertentu, produk yang dipermasalahkan ini dirancang untuk pemirsa Geeks dan simpatisan, yang memungkinkan Anda untuk menunjukkan kebebasan dalam antarmuka. Dalam komponen teknis, sebaliknya, perhatian terhadap detail diperlukan.
Sebenarnya, saya tidak mengerti apa pun di komik, tapi saya suka pasar loak, terutama dalam format forum, yang populer di nol. Oleh karena itu, asumsi (mungkin salah), yang darinya kesimpulan berikut mengalir, hanya satu - jenis interaksi utama dengan aplikasi yang dilihat, pengumuman posting sekunder dan diskusi.
Tujuan kami adalah membuat aplikasi sederhana, tanpa pengetahuan teknis peluit tambahan, bagaimanapun, konsisten dengan kenyataan modern. Persyaratan utama yang ingin saya capai adalah:
Sisi server:
a) Melakukan fungsi menyimpan, memvalidasi, mengirim data pengguna ke klien
b) Operasi di atas menghabiskan sejumlah sumber daya yang dapat diterima (waktu, termasuk)
c) Aplikasi dan data dilindungi dari vektor serangan populer
d) Ini memiliki API sederhana untuk klien pihak ketiga dan interaksi interserver
e) Cross-platform, penyebaran sederhana
Sisi klien:
a) Menyediakan fungsionalitas yang diperlukan untuk membuat dan menggunakan konten
b) Antarmuka nyaman untuk penggunaan reguler, jalur minimum untuk tindakan apa pun, jumlah maksimum data per layar
c) Keluar dari komunikasi dengan server, semua fungsi yang tersedia dalam situasi ini tersedia
d) Antarmuka menampilkan versi negara saat ini dan konten, tanpa me-reboot dan menunggu
d) Memulai ulang aplikasi tidak mempengaruhi kondisinya
f) Jika memungkinkan, gunakan kembali elemen DOM dan kode JS
g) Kami tidak akan menggunakan pustaka dan kerangka kerja pihak ketiga saat runtime
h) Tata letak adalah semantik untuk aksesibilitas, pengurai, dll.
i) Navigasi konten utama dapat diakses menggunakan URL dan keyboard
Menurut pendapat saya, persyaratan logis, dan sebagian besar aplikasi modern pada tingkat tertentu memenuhi persyaratan ini. Mari kita lihat apa yang terjadi dengan kami (tautan ke sumber dan demo di akhir posting).
Peringatan:- Saya ingin meminta maaf kepada penulis gambar yang tidak dikenal yang digunakan dalam demo tanpa izin, serta GΓΆsse G., Prozorovskaya B. D. dan penerbit "Library of Florence Pavlenkov" karena menggunakan kutipan dari karya "Siddhartha".
- Penulis bukan programmer sejati, saya tidak merekomendasikan menggunakan kode atau teknik yang digunakan dalam proyek ini jika Anda tidak tahu apa yang Anda lakukan.
- Saya minta maaf atas gaya kode ini, bisa saja ditulis lebih mudah dibaca dan jelas, tetapi ini tidak menyenangkan. Proyek untuk jiwa dan teman, seperti yang mereka katakan.
- Saya juga meminta maaf untuk tingkat melek huruf, terutama dalam teks bahasa Inggris. Tahun Berbicara Dari May Hart.
- Kinerja prototipe yang disajikan diuji dalam [kromium 70; linux x86_64; 1366x768], saya akan sangat berterima kasih kepada pengguna platform dan perangkat lain untuk pesan kesalahan.
- Ini adalah prototipe dan topik yang diusulkan untuk diskusi - pendekatan dan prinsip, saya meminta agar semua kritik terhadap implementasi dan sisi estetika disertai dengan argumen.
Server
Bahasa untuk server adalah golang. Bahasa yang sederhana dan cepat dengan perpustakaan standar yang sangat baik dan dokumentasi ... sedikit mengganggu. Pilihan awal jatuh pada elixir / erlang, tetapi karena saya sudah tahu go (relatif), diputuskan untuk tidak memperumitnya (dan paket yang diperlukan hanya untuk pergi).
Penggunaan kerangka kerja web di komunitas-go tidak dianjurkan (layak, layak untuk diakui), kami memilih kompromi dan menggunakan mikroframework labstack / echo , sehingga mengurangi jumlah rutinitas dan, menurut saya, tidak kehilangan banyak dalam kinerja.
Kami menggunakan tidwall / buntdb sebagai databasenya. Pertama, solusi built-in lebih nyaman dan mengurangi biaya overhead, dan kedua, in-memory + key / value - modis, bergaya Cepat dan tidak perlu cache. Kami menyimpan dan memberikan data dalam JSON, hanya memvalidasi ketika berubah.
Pada i3 generasi kedua, pencatat internal menunjukkan waktu eksekusi untuk berbagai permintaan mulai 0,5 hingga 10 ms. Menjalankan wrk pada mesin yang sama juga menunjukkan hasil yang cukup untuk tujuan kita:
β comico git:(master) wrk -t2 -c500 -d60s http://localhost:9001/pub/mtimes Running 1m test @ http://localhost:9001/pub/mtimes 2 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 20.74ms 16.68ms 236.16ms 72.69% Req/Sec 13.19k 627.43 15.62k 73.58% 1575522 requests in 1.00m, 449.26MB read Requests/sec: 26231.85 Transfer/sec: 7.48MB
β comico git:(master) wrk -t2 -c500 -d60s http://localhost:9001/pub/goods Running 1m test @ http://localhost:9001/pub/goods 2 threads and 500 connections Thread Stats Avg Stdev Max +/- Stdev Latency 61.79ms 65.96ms 643.73ms 86.48% Req/Sec 5.26k 705.24 7.88k 70.31% 628215 requests in 1.00m, 8.44GB read Requests/sec: 10454.44 Transfer/sec: 143.89MB
Struktur proyek
Paket comico / model dibagi menjadi tiga file:
model.go - berisi deskripsi tipe data dan fungsi umum: pembuatan / pemutakhiran (buntdb tidak membedakan antara operasi ini dan kami memeriksa keberadaan catatan secara manual), validasi, penghapusan, pengambilan satu catatan, dan pengambilan daftar;
rules.go - berisi aturan validasi untuk tipe dan fungsi logging tertentu;
files.go - bekerja dengan gambar.
Tipe Mtimes menyimpan data pada perubahan terakhir dari tipe yang tersisa dalam database, sehingga memberi tahu klien data mana yang telah berubah.
Paket comico / bd berisi fungsi umum untuk berinteraksi dengan database: membuat, menghapus, memilih, dll. Buntdb menyimpan semua perubahan pada file (dalam kasus kami, satu detik), dalam format teks, yang nyaman dalam beberapa situasi. File db tidak diedit, perubahan dalam hal keberhasilan transaksi ditambahkan ke akhir. Semua upaya saya untuk melanggar integritas data tidak berhasil, dalam kasus terburuk, perubahan pada detik terakhir hilang.
Dalam implementasi kami, setiap jenis sesuai dengan database terpisah dalam file terpisah (kecuali untuk log yang disimpan secara eksklusif dalam memori dan diatur ulang ke nol setelah reboot). Hal ini sebagian besar disebabkan oleh kemudahan cadangan dan administrasi, nilai tambah kecil - transaksi terbuka untuk mengedit blok akses ke hanya satu jenis data.
Paket ini dapat dengan mudah diganti dengan yang serupa menggunakan database lain, SQL, misalnya. Untuk melakukan ini, cukup mengimplementasikan fungsi-fungsi berikut:
func Delete(db byte, key string) error func Exist(db byte, key string) bool func Insert(db byte, key, val string) error func ReadAll(db byte, pattern string) (str string, err error) func ReadOne(db byte, key string) (str string, err error) func Renew(db byte, key string) (err error, newId string)
Paket comico / cnst berisi beberapa konstanta yang diperlukan di semua paket (tipe data, tipe tindakan, tipe pengguna). Selain itu, paket ini berisi semua pesan yang dapat dibaca oleh manusia yang dengannya server kami akan menanggapi dunia luar.
Paket comico / server berisi informasi perutean. Juga, hanya beberapa baris (terima kasih kepada pengembang Echo), otorisasi menggunakan JWT, CORS, header CSP, logger, distribusi statis, gzip, sertifikat otomatis ACME, dll. Dikonfigurasi.
Titik Masuk API
URL | Data | Deskripsi |
---|
dapatkan / pub / (barang | posting | pengguna | cmnts | file) | - | Mendapatkan berbagai pengumuman, posting, pengguna, komentar, file yang relevan |
dapatkan / pub / mtimes | - | Mendapatkan waktu perubahan terakhir untuk setiap tipe data |
pos / pub / login | {id *: login, pass *: password} | Mengembalikan token JWT dan durasinya |
post / pub / pass | {id *, pass *} | Membuat pengguna baru jika datanya benar |
menempatkan / api / pass | {id *, pass *} | Pembaruan kata sandi |
posting | put / api / barang | {id *, auth *, judul *, ketik *, harga *, teks *, gambar: [], Tabel: {key: value}} | Buat / perbarui iklan |
posting | put / api / posting | {id *, auth *, judul *, ketik *, teks *} | Buat / perbarui posting forum |
posting | put / api / pengguna | {id *, judul, jenis, status, juru tulis: [], abaikan: [], Tabel: {key: value}} | Buat / Perbarui Pengguna |
post / api / cmnts | {id *, auth *, pemilik *, ketik *, untuk, teks *} | Pembuatan komentar |
delete / api / (barang | posting | pengguna | cmnts) / [id] | - | Menghapus entri dengan id |
dapatkan / api / aktivitas | - | Memperbarui waktu baca terakhir dari komentar yang masuk untuk pengguna saat ini |
get / api / (berlangganan | abaikan) / [tag] | - | Menambah atau menghapus tag (jika ada) kepada pengguna dalam daftar langganan / abaikan |
pos / api / unggah / (barang | pengguna) | multipart (nama, file) | Mengunggah iklan foto / avatar pengguna |
* - bidang yang wajib diisi
api - memerlukan otorisasi, pub - no
Dengan permintaan dapatkan yang tidak cocok dengan di atas, server mencari file di direktori untuk statis (misalnya, / img / * - images, /index.html - klien).
Titik api mana pun akan mengembalikan kode respons 200 jika berhasil, 400 atau 404 untuk kesalahan, dan pesan singkat jika perlu.
Hak aksesnya sederhana: membuat entri tersedia untuk pengguna yang berwenang, mengedit penulis dan moderator, admin dapat mengedit dan menunjuk moderator.
API dilengkapi dengan anti-perusak paling sederhana: tindakan dicatat bersama dengan id pengguna dan IP, dan, dalam kasus akses yang sering, kesalahan dikembalikan meminta Anda untuk menunggu sedikit (berguna terhadap tebakan kata sandi).
Pelanggan
Saya suka konsep web'a reaktif, saya pikir sebagian besar situs / aplikasi modern harus dilakukan dalam kerangka konsep ini, atau sepenuhnya statis. Di sisi lain, situs sederhana dengan megabita kode JS tidak bisa tidak tertekan. Menurut pendapat saya, masalah ini (dan tidak hanya) dapat diselesaikan oleh Svelte. Kerangka kerja ini (atau lebih tepatnya bahasa untuk membangun antarmuka reaktif) tidak kalah dengan Vue dalam fungsionalitas yang diperlukan, tetapi memiliki keunggulan yang tidak dapat disangkal - komponen dikompilasi ke dalam vanilla JS, yang mengurangi ukuran bundel dan beban pada mesin virtual (bundle.min.js.gz pasar loak kami sederhana, menurut standar saat ini, 24KB). Detailnya dapat ditemukan di dokumentasi resmi.
Kami memilih pasar loak SvelteJS untuk sisi klien dari pasar loak, kami berharap yang terbaik untuk Rich Harris dan pengembangan lebih lanjut dari proyek ini!
PS Saya tidak ingin menyinggung siapa pun. Saya yakin bahwa setiap spesialis dan setiap proyek memiliki alat sendiri.
Pelanggan / Data
URL
Kami gunakan untuk navigasi. Kami tidak akan mensimulasikan dokumen multi-halaman, melainkan, kami menggunakan halaman hash dengan parameter kueri. Untuk transisi, Anda dapat menggunakan <a> yang biasa tanpa js.
Bagian terkait dengan tipe data: / # barang , / # posting , / # pengguna .
Parameter :? Id = record_id ,? Halaman = page_number ,? Cari = search_query .
Beberapa contoh:
- / # posts? id = 1542309643 & halaman = 999 & search = {auth: anon} - posting bagian, id posting - 1542309643 , halaman komentar - 999 , permintaan pencarian - {auth: anon}
- / # barang? halaman = 2 & pencarian = siddhartha - barang bagian, halaman bagian - 2 , permintaan pencarian - siddhartha
- / # goods? search = wer {key: value} t - section goods , permintaan pencarian - terdiri dari pencarian substring wert di header atau teks iklan dan nilai substring di properti kunci dari bagian tabular bagian dari iklan
- / # goods? search = {model: 100, display: 256} - Saya pikir semuanya jelas di sini dengan analogi
Fungsi parsing dan pembuatan URL dalam implementasi kami terlihat seperti ini:
window.addEventListener('hashchange', function() { const hash = location.hash.slice(1).split('?'), result = {} if (!!hash[1]) hash[1].split('&').forEach(str => { str = str.split('=') if (!!str[0] && !!str[1]) result[decodeURI(str[0]).toLowerCase()] = decodeURI(str[1]).toLowerCase() }) result.type = hash[0] || 'goods' store.set({ hash: result }) }) function goto({ type, id, page, search }) { const { hash } = store.get(), args = arguments[0], query = [] new Array('id', 'page', 'search').forEach(key => { const value = args[key] !== undefined ? args[key] : hash[key] || null if (value !== null) query.push(key + '=' + value) }) location.hash = (type || hash.type || 'goods') + (!!query.length ? '?' + query.join('&') : '') }
API
Untuk bertukar data dengan server, kami akan menggunakan fetch api. Untuk mengunduh catatan yang diperbarui pada interval pendek, kami membuat permintaan ke / pub / mtimes , jika waktu perubahan terakhir untuk jenis apa pun berbeda dari yang lokal, kami memuat daftar jenis ini. Ya, itu mungkin untuk menerapkan pemberitahuan pembaruan melalui SSE atau WebSockets dan pemuatan tambahan, tetapi dalam hal ini kita dapat melakukannya tanpa itu. Apa yang kami dapatkan:
async function GET(type) { const response = await fetch(location.origin + '/pub/' + type) .catch(() => ({ ok: false })) if (type === 'mtimes') store.set({ online: response.ok }) return response.ok ? await response.json() : [] } async function checkUpdate(type, mtimes, updates = {}) { const local = store.get()._mtimes, net = mtimes || await GET('mtimes') if (!net[type] || local[type] === net[type]) return const value = updates['_' + type] = await GET(type) local[type] = net[type]; updates._mtimes = local if (!!value && !!value.sort) store.set(updates) } async function checkUpdates() { setTimeout(() => checkUpdates(), 30000) const mtimes = await store.GET('mtimes') new Array('users', 'goods', 'posts', 'cmnts', 'files') .forEach(type => checkUpdate(type, mtimes)) }
Untuk pemfilteran dan pagination, kami menggunakan properti Svelte yang dihitung, berdasarkan data navigasi. Arah nilai yang dihitung adalah: item (array catatan yang berasal dari server) => ignoredItems (catatan yang difilter berdasarkan daftar abaikan pengguna saat ini) => scribedItems (memfilter catatan menurut daftar langganan, jika diaktifkan) => curItem dan curItem (menghitung catatan saat ini tergantung pada bagian) => filteredItems (memfilter catatan tergantung pada permintaan pencarian, jika hanya ada satu catatan - memfilter komentar di atasnya) => maxPage (menghitung jumlah halaman berdasarkan 12 catatan / komentar per halaman) => pagedItem (mengembalikan array terakhir dengan posting / komentar berdasarkan nomor halaman saat ini).
Komentar dan gambar ( komentar dan _ gambar ) dihitung secara terpisah, dikelompokkan berdasarkan jenis dan catatan pemilik.
Perhitungan terjadi secara otomatis dan hanya ketika data terkait berubah, data antara terus-menerus ada di memori. Dalam hal ini, kami membuat kesimpulan yang tidak menyenangkan - untuk sejumlah besar informasi dan / atau pemutakhirannya yang sering, sejumlah besar sumber daya dapat dihabiskan.
Cache
Menurut keputusan untuk membuat aplikasi offline, kami menerapkan penyimpanan catatan dan beberapa aspek keadaan di localStorage, file gambar di CacheStorage. Bekerja dengan localStorage sangat sederhana, kami setuju bahwa properti dengan awalan "_" secara otomatis disimpan dan dipulihkan saat reboot ketika diubah. Maka solusi kami mungkin terlihat seperti ini:
store.on('state', ({ changed, current }) => { Object.keys(changed).forEach(prop => { if (!prop.indexOf('_')) localStorage.setItem(prop, JSON.stringify(current[prop])) }) }) function loadState(state = {}) { for (let i = 0; i < localStorage.length; i++) { const prop = localStorage.key(i) const value = JSON.parse(localStorage.getItem(prop) || 'null') if (!!value && !prop.indexOf('_')) state[prop] = value } store.set(state) }
File sedikit lebih rumit. Pertama-tama, kita akan menggunakan daftar semua file yang relevan (dengan waktu pembuatan) yang berasal dari server. Saat memperbarui daftar ini, kami membandingkannya dengan nilai-nilai lama, meletakkan file-file baru di CacheStorage, menghapus yang sudah usang dari sana:
async function cacheImages(newFiles) { const oldFiles = JSON.parse(localStorage.getItem('_files') || '[]') const cache = await caches.open('comico') oldFiles.forEach(file => { if (!~newFiles.indexOf(file)) { const [ id, type ] = file.split(':') cache.delete(`/img/${type}_${id}_sm.jpg`) }}) newFiles.forEach(file => { if (!~oldFiles.indexOf(file)) { const [ id, type ] = file.split(':'), src = `/img/${type}_${id}_sm.jpg` cache.add(new Request(src, { cache: 'no-cache' })) }}) }
Kemudian, Anda perlu mendefinisikan kembali perilaku pengambilan sehingga file tersebut diambil dari CacheStorage tanpa terhubung ke server. Untuk melakukan ini, Anda harus menggunakan ServiceWorker. Pada saat yang sama, kami akan mengonfigurasi file lain untuk di-cache untuk operasi di luar server:
const CACHE = 'comico', FILES = [ '/', '/bundle.css', '/bundle.js' ] self.addEventListener('install', (e) => { e.waitUntil(caches.open(CACHE).then(cache => cache.addAll(FILES)) .then(() => self.skipWaiting())) }) self.addEventListener('fetch', (e) => { const r = e.request if (r.method !== 'GET' || !!~r.url.indexOf('/pub/') || !!~r.url.indexOf('/api/')) return if (!!~r.url.lastIndexOf('_sm.jpg') && e.request.cache !== 'no-cache') return e.respondWith(fromCache(r)) e.respondWith(toCache(r)) }) async function fromCache(request) { return await (await caches.open(CACHE)).match(request) || new Response(null, { status: 404 }) } async function toCache(request) { const response = await fetch(request).catch(() => fromCache(request)) if (!!response && response.ok) (await caches.open(CACHE)).put(request, response.clone()) return response }
Ini terlihat agak canggung, tetapi menjalankan fungsinya.
Klien / Antarmuka
Struktur komponen:
index.html | main.js
== header.html - berisi logo, bilah status, menu utama, menu navigasi lebih rendah, formulir pengiriman komentar
== sid.html - adalah wadah untuk semua komponen modal
==== goodForm.html - formulir untuk menambahkan dan mengedit iklan
==== userForm.html - edit formulir dari pengguna saat ini
====== tableForm.html - sebuah fragmen formulir untuk memasukkan data tabular
==== postForm.html - formulir untuk posting forum
==== login.html - form login / registrasi
==== activity.html - menampilkan komentar yang ditujukan kepada pengguna saat ini
==== goodImage.html - lihat iklan foto utama dan tambahan
== main.html - wadah untuk konten utama
==== goods.html - daftar atau kartu pengumuman tunggal
==== users.html - sama untuk pengguna
==== posts.html - Saya pikir sudah jelas
==== cmnts.html - daftar komentar pada posting saat ini
====== cmntsPager.html - pagination untuk komentar
- Di setiap komponen, kami mencoba meminimalkan jumlah tag html.
- Kami menggunakan kelas hanya sebagai indikator negara.
- Kami mengambil fungsi yang mirip dengan toko (properti toko langsing dan metode dapat digunakan langsung dari komponen dengan menambahkan awalan '$' ke mereka).
- Sebagian besar fungsi mengharapkan acara pengguna atau perubahan properti tertentu, memanipulasi data keadaan, menyimpan hasil pekerjaan mereka kembali ke keadaan dan mengakhiri. Dengan demikian, koherensi kecil dan ekstensibilitas kode tercapai.
- Untuk kecepatan transisi dan peristiwa UI lainnya, kami memisahkan, sejauh mungkin, manipulasi dengan data yang terjadi di latar belakang dan tindakan yang terkait dengan antarmuka, yang pada gilirannya menggunakan hasil perhitungan saat ini, membangun kembali jika perlu, sisanya akan dilakukan oleh kerangka kerja.
- Data formulir yang akan diisi disimpan di Penyimpanan lokal untuk setiap input untuk mencegah kehilangan mereka.
- Di semua komponen, kami menggunakan mode abadi di mana properti-objek dianggap berubah hanya ketika tautan baru diterima, terlepas dari perubahan di bidang, sehingga mempercepat aplikasi kami sedikit, meskipun karena peningkatan kecil dalam jumlah kode.
Pelanggan / Manajemen
Untuk mengontrol menggunakan keyboard, kami menggunakan kombinasi berikut:
Alt + s / Alt + a - mengalihkan halaman catatan ke depan / ke belakang, karena satu catatan mengalihkan halaman komentar.
Alt + w / Alt + q - pindah ke rekaman berikutnya / sebelumnya (jika ada), bekerja dalam mode daftar, rekaman tunggal dan tampilan gambar
Alt + x / Alt + z - menggulir halaman ke bawah / atas. Pada tampilan gambar, alihkan gambar maju / mundur
Escape - menutup jendela modal, jika terbuka, kembali ke daftar, jika satu entri terbuka, membatalkan permintaan pencarian dalam mode daftar
Alt + c - berfokus pada bidang pencarian atau komentar, tergantung pada mode saat ini
Alt + v - aktifkan / nonaktifkan mode tampilan foto untuk satu iklan
Alt + r - membuka / menutup daftar komentar yang masuk untuk pengguna yang diotorisasi
Alt + t - Mengalihkan tema terang / gelap
Alt + g - Daftar Iklan
Alt + u - Pengguna
Alt + p - forum
Saya tahu bahwa di banyak browser kombinasi ini digunakan oleh browser itu sendiri, tetapi untuk chrome saya, saya tidak dapat menemukan sesuatu yang lebih nyaman. Saya akan senang dengan saran Anda.
Selain papan ketik, tentu saja Anda dapat menggunakan konsol peramban. Misalnya, store.goBack () , store.nextPage () , store.prevPage () , store.nextItem () , store.prevItem () , store.search (stringValue) , store.checkUpdate ('goods' || ' users '||' posts '||' files '||' cmnts ') - melakukan apa yang tersirat namanya; store.get (). komentar dan store.get () ._ gambar - mengembalikan file dan komentar yang dikelompokkan; store.get (). ignoredItems dan store.get (). scribedItems adalah daftar catatan yang Anda abaikan dan lacak. Daftar lengkap semua data antara dan perhitungan tersedia dari store.get () . Saya tidak berpikir bahwa ada orang yang membutuhkan ini dengan serius, tetapi, misalnya, memfilter catatan oleh pengguna dan menghapus bagi saya cukup nyaman dari konsol.
Kesimpulan
Di sinilah Anda dapat mengakhiri perkenalan Anda dengan proyek, Anda dapat menemukan rincian lebih lanjut dalam kode sumber. Akibatnya, kami mendapat aplikasi yang cukup cepat dan ringkas, di sebagian besar validator, keamanan, kecepatan, ketersediaan, dll. Checker, itu menunjukkan hasil yang baik tanpa optimasi yang ditargetkan.
Saya ingin tahu pendapat masyarakat bagaimana membenarkan pendekatan untuk mengatur aplikasi yang digunakan dalam prototipe, apa jebakannya, apa yang akan Anda implementasikan dengan cara yang secara fundamental berbeda?
Kode sumber, contoh instruksi instalasi dan demo di sini (tolong merusak untuk menguji dalam kerangka KUHP).
Catatan tambahan. Kesimpulan sedikit pedagang. Katakan padaku, dengan level seperti itu, apakah benar-benar mungkin untuk memulai pemrograman demi uang? Jika tidak, apa yang harus dicari pertama-tama, jika demikian, beri tahu saya di mana mereka mencari pekerjaan yang menarik pada tumpukan yang sama sekarang. Terima kasih
Catatan tambahan. Sedikit lagi tentang uang dan pekerjaan. Bagaimana Anda menyukai ide ini: misalkan seseorang siap untuk mengerjakan proyek yang menarik baginya untuk gaji apa pun, namun, data tentang tugas dan pembayaran mereka akan tersedia untuk umum (ketersediaan dan kode untuk menilai kualitas kinerja yang diinginkan), jika pembayaran secara signifikan di bawah pasar, pesaing dari pemberi kerja dapat menawarkan banyak uang untuk melakukan tugas mereka, jika lebih tinggi - banyak pemain akan dapat menawarkan layanan mereka dengan harga lebih rendah. Akankah skema seperti itu dalam beberapa situasi lebih optimal dan seimbang menyeimbangkan pasar (TI)?