Skip to main content

· 17 min read

Những gì bạn sẽ học trong bài blog này

Cách xây dựng một thư viện component UI cho dự án tini-app và triển khai package trên npm (Node package management)

Mục lục

  1. Mở đầu
  2. Chuẩn bị
  3. Tiến hành xây dựng thư viện
  4. Triển khai thư viện lên npm registry
  5. Kết luận

Mở đầu:

Nếu bạn là một lập trình viên của Tini App, việc sử dụng là các package như tini-ui, tini-style không hề xa lạ đối với với chúng ta. Các thư viện được sinh ra nhằm giúp lập trình viên có thể phát triển ứng dụng Mini App một cách nhanh chóng và hiệu quả. Và có bao giờ bạn đã thắc mắc về việc làm thế nào để xây dựng ra các thư viện tương tự để phục vụ cho quá trình phát triển cũng như đóng góp cho cộng đồng lập trình viên? Trong bài hướng dẫn ngày hôm nay chúng ta sẽ cùng nhau từng bước xây dựng ra thư viện và triển khai lên npm nhé!

Chuẩn bị

  • Cơ bản

    Trước khi bắt đầu phần chính, bạn cần phải chuẩn bị một số thứ được liệt kê dưới đây:
  1. Github account
  2. Cài đặt Node.js & npm (Hướng dẫn)
  3. NPM account

Sau khi qua bước chuẩn bị này, chúng ta sẽ bắt đầu tiến hành vào công việc chính nào!

  • Tên package

Khi chọn tên cho một package sẽ được tạo mới, các bạn cần phải kiểm tra tên của package có khả dụng trên trang chủ của npm hay chưa. Tên package cần phải được xem là duy nhất nếu bạn phát hành như một unscoped (public) package. Ví dụ như: rez-deploy, lodash, node-fetch...

Tuy nhiên, nếu package của bạn là có gắn với scope cụ thể hoặc là private thì tên package không cần phải là duy nhất và thông thường các loại package này sẽ có định dạng như: @username/package-name, @org/package-name. Bạn có thể tìm hiểu thêm về scoped packages tại đây

🔥 Quan trọng: Có một lưu ý về tên của package khi bạn tích hợp vào tini-app đó là nếu bạn đang sử dụng package như một phần của tini-app, tức là Sử dụng các biến toàn cục như: Page, Component, App... thì bạn cần phải đặt tên cho package theo định dạng được white-list như sau: @tiki.vn/package-name, @tikivn/package-name, @tiki-miniapp*/packagename, tiki-miniapp*.

Trong quá trình biên dịch của framework, các package có tên nằm trong white list trên sẽ được hiểu như một phần của tini-app và bạn có thể sử dụng các biến toàn cụ được liệt kê phía trên. Nếu tên package không tuân thủ theo việc đặt tên này thì trình biên dịch sẽ báo lỗi trong quá trình quá biên dịch. Trong bài blog này mình sẽ tạo một organization trên npm tên là tiki-miniapp-rez và package-name là tini-ui để thoả đều kiện phía trên. Lúc này package của chúng ta sẽ có định dạng: @tiki-miniapp-rez/tini-ui.

Bạn cũng có thể tạo ra một unscoped package và đặt tên theo định dạng với prefix tiki-miniapp vẫn được xem là hợp lí nhé. Ví dụ: tiki-miniapp-{package-name}, tiki-miniapplib... (Lưu ý với tên của unscope package sẽ phải là unique trên npm registry)

  • Tạo một repo trên Github

Bây giờ chúng ta sẽ tạo một repo trên Github để thuận tiện cho việc quản lý code và xem như open source để các lập trình viên có thể cùng nhau phát triển thư viện. Hãy nhớ thêm README và chọn license như MIT license nhé.

  • Cấu hình & đăng nhập vào npm

Bạn có thể cấu hình một số thông tin như name, email, website (nếu có) trong npm. Các thông tin này sẽ được thêm vào file package.json của một project khi nó được tạo ra.

$ npm set init.author.name "your-name"
$ npm set init.author.email "your-email"
$ npm set init.author.url "your-website-url"

Sau khi đã setup xong thì các bạn hãy gọi lệnh npm login từ terminal để login vào npm với các thông tin như: Username, password, Email được tạo từ trang npm trước đó. Ngoài ra bạn sẽ phải nhập thêm mã OTP nếu tài khoản có bật tính năng 2FA.

Tiếp theo chúng ta sẽ cấu hình org scope cho một package cụ thể với lệnh sau, các thông tin chi tiết về cấu hình cho org bạn có thể xem thêm tại đây:

$ cd /path/to/package
$ npm config set scope <org-name>

// Kiểm tra lại các configs
$ npm config list
  • Khởi tạo project

Khi bạn đang đứng ở thư mục chứa project đã setup từ ban đầu, khởi tạo với lệnh npm init hoặc npm init --scope=@my-org (đối với scoped module) và kiểm tra lại các thông tin trong file package.json như: name, version (nên được gán với giá trị "0.0.0" ở lần khởi tạo), description, git repository, keywords & license.

// packge.json

{
"name": "@tiki-miniapp-rez/tini-ui",
"version": "0.0.0",
"description": "Tini UI",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chanhchung-tiki/rez-ui.git"
},
"keywords": ["test", "ui"],
"author": "chanhchung <chanh.chung@tiki.vn>",
"license": "MIT",
"bugs": {
"url": "https://github.com/chanhchung-tiki/rez-ui/issues"
},
"homepage": "https://github.com/chanhchung-tiki/rez-ui#readme"
}

Tiến hành xây dựng thư viện

Cấu trúc của thư viện được tổ chức như sau:

- es/ -> Thư mục chứa các file sau khi được biên dịch.
|- checkbox/
| index.js
| index.json
| index.tcss
| index.txml
- scripts/ -> Các helper scripts khi gọi npm script
| complier.js
| release.js
- src/ -> Thư mục chứa code chính
|- checkbox/
| index.js
| index.json
| index.tcss
| index.txml
.gitignore
babel.config.js
jsconfig.json
package.json
README.md
  • Khởi tạo custom component

Mình sẽ tạo ra một checkbox component với các props được nhận vào như: checked, disabled và hàm onChange để thực hiện việc toggle status của checkbox

// src/checkbox/index.js

/**
* @typedef {Object} Props
* @property {boolean} checked
* @property {boolean} disabled
* @property {() => any} onChange
*
*/
Component({
props: {
checked: false,
disabled: false,
onChange: () => {}
},
didMount() {
this._updateDataSet();
},
didUpdate() {
this._updateDataSet();
},
methods: {
_updateDataSet() {
this.dataset = {};
for (const key in this.props) {
if (/data-/gi.test(key)) {
this.dataset[key.replace(/data-/gi, '')] =
this.props[key];
}
}
},
onChange(event) {
const { checked, disabled } = this.props;
if (disabled) return;

const value = checked ? false : true;
event.detail = { value };
const _onChange = this.props.onChange;
if (typeof _onChange === 'function') {
event.target = { ...event.target, dataset: this.dataset };
_onChange(event);
}
}
}
});

Tiếp theo đến các cấu hình của component trong file json:

// src/checkbox/index.json
{
"component": true,
"componentLifeCycleV2": "YES"
}

Styling cho checkbox:

/* src/checkbox/index.tcss */

.custom-checkbox {
--tf-checkbox-custom-color: #1a94ff;
--background-selection-default: #f5f5fa;
--border-selection-default: #dddde3;
--background-selection-disable: #c4c4cf;
--border-selection-disable: #ebebf0;
position: relative;
display: inline-block;
box-sizing: border-box;
width: 20px;
height: 20px;
background-color: var(--background-selection-default);
border: 1px solid;
border-color: var(--border-selection-default);
border-radius: 4px;
}

.custom-checkbox.custom-checkbox--checked {
background-color: var(--tf-checkbox-custom-color);
border-color: var(--tf-checkbox-custom-color);
}

.custom-checkbox.custom-checkbox--checked.custom-checkbox--disabled {
background-color: var(--background-selection-disable);
}

.custom-checkbox.custom-checkbox--disabled {
background-color: var(--background-selection-default);
border-color: var(--border-selection-disable);
}

.custom-checkbox.custom-checkbox--disabled:hover {
border-color: none;
}

.custom-checkbox .t-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

.custom-checkbox .t-icon .success-icon {
width: 16px;
height: 16px;
}

Và cuối cùng chính là file txml chứa các thẻ cơ bản trong tini-framework để tạo ra UI:

<!-- src/checkbox/index.txml -->

<view
class="custom-checkbox {{checked ? 'custom-checkbox--checked' : ''}} {{disabled ? 'custom-checkbox--disabled' : ''}}"
catchTap="onChange">
<view tiki:if="{{checked}}" class="t-icon">
<icon type="success" color="#fff" size="{{16}}" />
</view>
</view>

Vậy là chúng ta đã hoàn thành được một component đơn giản trong thư viện rồi. Tiếp theo sẽ đến bước biên dịch các file trong source code với 2 công cụ chính là BabelGlup.

Giải thích nhanh về 2 công cụ trên:

  • Babel - Javascript Complier: hỗ trợ biên dịch mã nguồn javascript khi viết bằng ECMAScript 2015+ sang phiên bản mã nguồn tương thích với các môi trường và trình duyệt cũ. Ngoài ra Babel còn làm được các việc như: transform syntax, polyfill một số tính năng còn thiếu ở môi trường javascript đang chạy...

  • Glup: là công cụ như một build tool giành cho Javascript giúp lập trình viên có thể tự động hoá và cải thiện workflow một các hiệu quả. Glup cung cấp các api để làm việc với file: sao chép các file từ thư mục source sang dest, watch các file để chỉ ra sự thay đổi trong quá trình develop và thực thi các tác vụ khi file có sự thay đổi...

  • Biên dịch và build thư viện

Cài đặt một số package cần thiết:

$ npm install --save-dev @babel/core @babel/preset-env babel-plugin-module-resolver gulp gulp-babel

Thực hiện cấu hình cho babel:

// babel.config.json

{
"presets": [
// Hỗ trợ cho lập trình viên sử dụng các tính năng mới nhất của Javascript và tối ưu đóng gói mã nguồn
[
"@babel/preset-env",
{
"loose": true, // Mã nguồn sau khi biên dịch sẽ giống ES5 của javascript
"modules": false // Tắt tính năng transform ES module syntax
}
]
// Mở rộng: sử dụng @babel/preset-typescript nếu bạn muốn viết file logic bằng typescript
],
"plugins": [
// Hỗ trợ resolve các module khi import (Optional)
[
"module-resolver",
{
"root": ["./src"],
"alias": {
"@": "./src"
},
"extensions": [".js"]
}
]
]
}
// Cấu hình hỗ trợ các tính năng như: code completion, code suggestion nếu bạn sử dụng IDE Visual Code
// jsconfig.json

{
"compilerOptions": {
"target": "es6",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

Tiếp theo chúng ta sẽ tạo ra một helper script có tên là complier.js nằm trong thư mục scripts để hỗ trợ biên dịch và build code

// scripts/complier.js

const path = require('path');
const gulp = require('gulp');
const babel = require('gulp-babel');

const isProduction = process.env.NODE_ENV === 'production';
let dist = path.join(__dirname, '..', 'es');
const basePath = path.join(__dirname, '..', 'src');
const extTypes = ['js', 'tcss', 'json', 'txml', 'sjs'];

const argv = process.argv.slice(2);
const indexOutdir = argv.findIndex(
(item) => item === '--out-dir'
);

// Server for development process
if (!isProduction && indexOutdir !== -1) {
dist = argv[indexOutdir + 1];
}

/*
Mở rộng: bạn có thể thêm 1 task cho việc biên dịch typescript -> javascript tương tự
Lưu ý: thêm 1 giá "ts" vào mảng extTypes nữa nhé!
*/
gulp.task('js', () => {
return gulp
.src(`${basePath}/**/*.js`)
.pipe(babel())
.on('error', (err) => console.log({ err }))
.pipe(gulp.dest(dist));
});

gulp.task('tcss', () => {
return gulp.src(`${basePath}/**/*.tcss`).pipe(gulp.dest(dist));
});

gulp.task('json', () => {
return gulp.src(`${basePath}/**/*.json`).pipe(gulp.dest(dist));
});

gulp.task('txml', () => {
return gulp.src(`${basePath}/**/*.txml`).pipe(gulp.dest(dist));
});

gulp.task('sjs', () => {
return gulp.src(`${basePath}/**/*.sjs`).pipe(gulp.dest(dist));
});

const build = gulp.series(...extTypes);
// Build code dựa trên các file có extension được liệt kê trong mảng extTypes
// Đường dẫn đích sẽ tuỳ thuộc vào NODE_ENV và tham số --out-dir được truyền vào
build();

if (!isProduction) {
/*
Với mode development:
Chúng ta sẽ sử dụng glup để watch các file change
và cập nhật trực tiếp vào thư mục build để code mới có thể được áp dụng ngay lập tức
*/
extTypes.forEach((type) => {
const watcher = gulp.watch(
`${basePath}/**/*.${type}`,
gulp.series(type)
);

watcher.on('change', function (path) {
console.log(`File ${path} was changed`);
});

watcher.on('add', function (path) {
console.log(`File ${path} was added`);
});

watcher.on('unlink', function (path) {
console.log(`File ${path} was removed`);
});
});
}

Trong file complier.js, chúng ta sẽ sử dụng glup để tạo ra các task làm các nhiệm vụ như đọc 1 file với glob được định nghĩa và tiến hành thực thi 1 số tác vụ như biên dịch bằng babel hoặc stream các file này và ghi đến một đường dẫn output nào đó. Ngoài ra mình còn sử dụng glup.watch để hỗ trợ quan sát sự thay đổi của file trong quá trình develop

Chi tiết về các api làm việc với file các bạn có thể xem thêm tại đây.

Sau khi xây dựng được script complier thì chúng ta sẽ đi cập nhật lại file package.json một số field để hỗ trợ việc run script nhé. File package.json sau khi được cập nhật:

{
"name": "@tiki-miniapp-rez/tini-ui",
"version": "0.0.0",
"description": "Tini UI",
// Package khi được đóng gói sẽ gồm các file trong thư mục es
"files": ["es"],
// Thêm field main nếu thư viện là một module
// Giá trị của field này chính là entry point chính thức trỏ đến file module chính
// Chi tiết bạn có thể xem thêm tại đây:
// https://docs.npmjs.com/cli/v8/configuring-npm/package-json#main
// "main": "index.js",

// Cấu hình chế độ publish package
"publishConfig": {
"access": "public"
},
"scripts": {
"clean": "rm -rf es",
"dev": "npm run clean && node scripts/complier.js",
"build": "npm run clean && NODE_ENV=production node scripts/complier.js",
// npm script for publishing package
"pub:patch": "node scripts/release.js --version-type patch",
"pub:minor": "node scripts/release.js --version-type minor",
"pub:major": "node scripts/release.js --version-type major"
},
"repository": {
"type": "git",
"url": "git+https://github.com/chanhchung-tiki/rez-ui.git"
},
"keywords": ["test", "ui"],
"author": "chanhchung <chanh.chung@tiki.vn>",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.18.5",
"@babel/preset-env": "^7.18.2",
"babel-plugin-module-resolver": "^4.1.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0"
},
"dependencies": {},
"bugs": {
"url": "https://github.com/chanhchung-tiki/rez-ui/issues"
},
"homepage": "https://github.com/chanhchung-tiki/rez-ui#readme"
}

Chạy thử lệnh npm run dev và xem kết quả, ở mode development thì glup sẽ tự động kiểm tra sự thay đổi của các file và build ra mã nguồn mới tương ứng vào thư mục es.

Trường hợp nếu bạn không truyền vào --out-dir khi gọi lệnh thì mặc định thư mục đích chứa các file build sẽ là es. Giả sử mình muốn build thư viện vào một project demo thì lúc này mình sẽ gọi lệnh như sau:

$ npm run dev -- --out-dir <<out-dir>>

Eg:
$ npm run dev -- --out-dir ~/Documents/Tini/repos/demo-ui-lib/node_modules/@tiki-miniapp-rez/tini-ui/es

Sau khi đã hoàn thành các bước xây dựng thư viện thì bây giờ chúng ta sẽ đến bước cuối cùng đó là phát hành thư viện lên npm thôi nào!

Triển khai thư viện lên npm registry

Mình sẽ thêm một số cấu hình trong file package.json như ở phía trên để phục vụ cho việc triển khai thư viện lên npm, các cấu hình bao gồm các trường sau: files, publishConfig, và một số npm run script như:

  • npm run pub:patch: Phát hành thư viện với version được tăng lên với giá trị patch (Khi có sự thay đổi trong code liên quan đến việc fix bug, format code...). Ví dụ: 0.0.0 -> 0.0.1 .
  • npm run pub:minor: Phát hành thư viện với version được tăng lên với giá trị minor (Khi có sự thay đổi trong code liên quan đến việc thêm các function, component, module mới...) Ví dụ: 0.0.0 -> 0.1.0 .
  • npm run pub:major: Phát hành thư viện với version được tăng lên với giá trị major (Khi có sự thay đổi lớn về thư viện, có thể không còn tương thích về các API hoặc component trong thư viện như trước đó) Ví dụ: 0.0.0 -> 1.0.0 .

Nếu để ý thì với các npm script trên thì mình sẽ gọi một helper script là release.js trong folder scripts để thực thi việc này. Chúng ta sẽ đi chi tiết xem script này sẽ làm gì nhé!

const { execSync } = require('child_process');
const fs = require('fs');

// Helper function
const getPackageJSON = () => {
const packageJSON = fs.readFileSync('./package.json', 'utf-8');
return JSON.parse(packageJSON);
};

const setPackageJSONVersion = (version) => {
const packageJSON = getPackageJSON();
packageJSON.version = version;
fs.writeFileSync(
'./package.json',
JSON.stringify(packageJSON, null, 2)
);
};

const argv = process.argv.slice(2);
const indexVersionType = argv.findIndex(
(arg) => arg === '--version-type'
);
const versionTypes = ['major', 'minor', 'patch'];
let semanticVersionType = 'minor';
if (
indexVersionType !== -1 &&
versionTypes.includes(argv[indexVersionType + 1])
) {
semanticVersionType = argv[indexVersionType + 1];
}

// Xử lí publish package:

try {
// Step: 1 - Build lib
execSync('npm run build');

// Step: 2
// Bump up version and add tag
// (Require the tree of git clean: not exist files change)
execSync(
`npm version ${semanticVersionType} -m 'Bump up package with version %s'`
);
// Step: 3 - Publish package
execSync('npm publish');
// Step: 4 - Publish code
execSync('git push origin master:master && git push -f --tags');
} catch (error) {
console.group('Error detail:');
console.log('An error occur while release package: ', error);
console.groupEnd();
}

Quá trình triển khai package lên npm sẽ bao gồm các bước sau:

  • Bước 1: Build code với lệnh npm run build để cho ra các file của thư viện nằm trong thư mục es
  • Bước 2: Tăng version trong package.json và đồng thời comit với một message được định nghĩa sẵn kèm theo tạo git tag. Lệnh thực hiện:
$ npm version ${versionType} -m 'Bump up package with version %s'

Lưu ý: Trong quá trình run script có thể bị lỗi Git working directory not clean do chúng ta chưa comit các file thay đổi ở project hiện tại. Việc này mình nghĩ sẽ là bắt buộc nên làm trước khi bạn muốn publish một version mới, giúp việc quản lý các comit trở nên rõ ràng hơn và đồng thời tránh việc thiếu xót các file khi gắn tag trong git. Bạn có thể bypass bước này khi thêm 1 tham số vào lệnh npm như sau: npm version ${versionType} -m 'Bump up package with version %s' -f . Bạn có thể tham khảo thêm về npm versionđây nhé.

  • Bước 3: Phát hành package lên npm bằng npm publish. Do package của chúng ta đang thuộc bởi một org nên việc publish lên npm sẽ là private package, tuy nhiên mình đã thêm 1 config vào file package.json là publishConfig với field "access": "publish". Ngoài ra bạn cũng có thể làm một cách khác khi publish package với public mode khi gọi lệnh npm publish --access public.

Lưu ý: Ở bước phát hành có thể bị lỗi trong lúc chạy script release.js do account npm có bật yêu cầu nhập mã OTP khi publish package. Bạn có thể tắt việc nhập mã OTP trong mục Account > Two-Factor Authentication > Additional Options để giải quyết vấn đề này.

  • Bước 4: Sau khi phát hành package thành công thì sẽ đến bước cuối cùng là pushlish code và kèm tag lên git
$ git push origin master:master && git push -f --tags

Bây giờ chúng ta hãy kiểm tra thành quả thôi nào 🎉

Cài đặt thư viện vào tini-app và xem kết quả

$ npm install @tiki-miniapp-rez/tini-ui --save

Kết luận

Vậy là chúng ta đã hoàn thành xong các bước xây dựng thư viện cho tini-app và triển khai lên npm rồi đấy. Việc xây dựng các thư viện hỗ trợ cho quá trình phát triển tini-app cũng là một trong những yếu tố quan trọng góp phần giúp cho dự án thành công.

Mình hy vọng qua bài blog chia sẻ này có thể giúp các bạn biết thêm thứ gì đó hữu ích, và trên hết là cảm ơn các bạn đã giành ra thời gian để đọc những gì mình chia sẻ ❤️

Cuối cùng mình xin gửi đến các bạn repo github example chứa toàn bộ mã nguồn: https://github.com/chanhchung-tiki/rez-ui.

· 5 min read

JS API


Nội dung bao gồm:

  1. Giới thiệu.
  2. JS API là gì?
  3. Các nhóm JS API.
  4. Cơ chế hoạt động của JS API.
  5. Developer có thể tự viết JS API cho Tini App không?

Giới thiệu

Chắc hẳn các bạn developer đã rất quen thuộc với Tini App. Tini App được phát triển bởi Tiki, Tini App cung cấp rất nhiều công cụ để build 1 ứng dụng native app như components, JS API, Server Side API, Tini UI,… Ít hay nhiều các bạn developer đã từng tương tác với JS API, nhưng liệu rằng tất cả chúng ta đã hiểu rõ nó như thế nào và nó hoạt động ra sao? Bài viết này sẽ giúp các bạn developer có thể nắm rõ hơn JS API trong Tini App.

JS API là gì?

Để đơn giản hoá trong việc phát triển app trên Tiki, Tini App Framework cung cấp đa dạng các JS API cần thiết để truy cập tới thiết bị, giao diện cũng như open services của Tiki. Các developer có thể dễ dàng và nhanh chóng gọi các JS API này để hoàn thành các tác vụ liên quan. JS API chính là API.

Ví dụ dưới đây là một JS API dùng để get location của thiết bị, chúng ta chỉ cần gọi my.getLocation và truyền vào giá trị cacheTimeout = 1 .

my.getLocation({
cacheTimeout: 1,
success: (res) => {
console.log('location: ', res);
},
fail: (e) => {
console.log(e);
},
complete: (e) => {
console.log(e);
}
});
  • Hàm callback success sẽ được thực thi khi được gọi JS API thành công và trả về location hiện tại.
  • Hàm callback fail sẽ được thực thi khi gọi không thành công.
  • Hàm callback complete được thực thi dù kết quả thành công hay không.

Các nhóm JS API

JS API giải quyết rất nhiều vấn đề và được chia thành các nhóm chính theo chức năng như sau:

JS API

  • Nhóm chuyên xử lý về giao diện như hiển thị thông báo, hiển thị prompt nhập thông tin. Các hiển thị về app như get title, set background. Các công cụ hỗ trợ nhập liệu như Option Menu, Date Picker…
  • Nhóm truy cập thiết bị người dùng như contact, open app setting, compass, gia tốc cảm biến…
  • Nhóm open services Tiki như lấy user token, user info, address…
  • Nhóm media cho phép phát video, nhạc, làm game.
  • Nhóm network giúp kết nối thông qua socket
  • Nhóm còn lại như Storage, File, Ads…

Cơ chế hoạt động của JS API

Để giải thích về cơ chế hoạt động, xin phép được lấy JS API my.getLocation ở trên làm ví dụ cho các bạn dễ hình dung.

JS API

Trong sơ đồ trên, trước hết kể đến là Worker:

  • Tất cả các JS API được đăng ký ở đây và được alias qua my nên sau khi chúng ta gọi my.getLocation thì sẽ trigger qua hàm getLocation ở worker này.

Kế tiếp là Core:

  • Core sẽ trigger handler và khi nhận được event từ worker bao gồm name = getLocationparam kèm theo, nó sẽ tiến hành kiểm tra xem plugin nào được đăng ký là LocationBridgePlugin. Sau đó sử dụng plugin này để kiểm tra xem nếu App có quyền getLocation thì sẽ call qua App host (Tiki App) và lấy kết quả trả về cho worker, ngược lại sẽ trả về lỗi cho worker. Vì với một số JS API yêu cầu phải được cấp phát quyền mới có thể sử dụng.

Cuối cùng là App host:

  • Chính là Tiki App và Tiki Web, nó cung cấp implement cho các JS API. Theo ví dụ ở trên app host sẽ cung cấp 1 implement là LocationImplement, vì vậy việc lấy location trả về cho core phải được để ở app host.

Vậy tiến trình theo trình tự là Worker → Core → Tiki App, và cả 3 processor này giao tiếp với nhau thông qua bridge.

Với cơ chế hoạt động như thế thì worker sẽ không có cách nào gọi trực tiếp qua Tiki App để get location mà phải gọi qua core. Nên JS API không thể nào truy cập trực tiếp tới những tính năng của Native App Tiki, do đó đảm bảo được tính security của Tini App.

Developer có thể tự viết JS API không?

  • Hiện tại Tini App chưa cung cấp cơ chế cho các developer tự viết JS API, nhưng với mong muốn phát triển rộng rãi, trong tương lai gần, nhóm phát triển sẽ support các bạn developer viết JS API thông qua cơ chế plugin.

Cảm ơn các bạn đã theo dõi bài viết.

***

TINI APP - Nền tảng xây dựng ứng dụng tối ưu

· 7 min read

Tini App Studio

Trong nội dung của phần mở đầu đã đề cập tới mục tiêu của chuỗi bài viết Quá trình xây dựng IDE cho Tini App là tập trung vào các thành phần và quá trình xây dựng IDE. Tuy nhiên, để dễ dàng hơn trong việc tiếp cận những kiến thức đó. Ở phần này, chúng ta sẽ giới thiệu sơ lược về Tini App, những khái niệm liên quan và tại sao phải dành thời gian để xây dựng IDE.


Nội dung bao gồm:

  1. Phần mở đầu
  2. Sơ lược về Tini App và tại sao phải xây dựng IDE? (Bạn đang đọc bài này)
  3. Ngôn ngữ cho Tini App và công cụ compiler
  4. Chọn giải pháp và xây dựng editor
  5. Giả lập môi trường ứng dụng Tini App và các cộng cụ debug
  6. Xây dựng các công cụ Tini Console và định hướng phát triển trong tương lai

Super App là gì?

Super App dịch ra tiếng Việt là siêu ứng dụng. Khái niệm này mới xuất hiện trong một vài năm gần đây, khi mà các doanh nghiệp công nghệ bắt đầu chuyển mình trở thành một công ty đa dịch vụ.

Với mô hình truyền thống, một ứng dụng sẽ sở hữu một dịch vụ nhất định và trong đó sẽ có các tính năng nhằm phục vụ, phát triển lợi ích cho dịch vụ (Tạm gọi là dịch vụ gốc).

Tini App Studio

Ví dụ Super App

Với Super App, ứng dụng không chỉ còn gói gọn trong một dịch vụ, mà người dùng có thể tìm kiếm và sử dụng thêm các dịch vụ vệ tinh xung quanh cùng với dịch vụ gốc. Ví dụ, thay vì chúng ta phải cài đặt rất nhiều ứng dụng cho mỗi loại dịch vụ khác nhau như: Thương mại điện tử, ví điện tử, bảo hiểm, … Thì giờ đây, chỉ cần cài đặt một Super App và ở đó tích hợp tất cả các dịch vụ mà chúng ta cần. Việc này không chỉ mang lại lợi ích cho doanh nghiệp mà còn thuận tiện hơn cho người sử dụng, mang tới một trải nghiệm liền mạch.

Những giải pháp để xây dựng Super App

Giải pháp đầu tiên cho việc chuyển đổi ứng dụng của doanh nghiệp trở thành một Super App là “Tự thân làm nên tất cả”. Cụ thể, doanh nghiệp sẽ chủ động xây dựng đội ngũ để làm các tính năng cho từng dịch vụ của đối tác.

  • Ưu điểm: Chủ động kiểm soát được chất lượng sản phẩm đạt mức tốt nhất, an toàn nhất cho doanh nghiệp.

  • Nhược điểm: Cần một đội ngũ nhân sự đủ lớn và có kinh nghiệm trong lĩnh vực đó. Vì khác với xây dựng tính năng, một dịch vụ có thể có rất nhiều tính năng phức tạp và theo đặc thù riêng của từng doanh nghiệp.

Giải pháp thứ hai thuê nhân sự bên ngoài hoặc giao cho doanh nghiệp đối tác phát triển.

  • Ưu điểm: Tốc độ kết nối và phát triển các dịch vụ của đối tác sẽ nhanh hơn vì hầu như các tính năng đều sẽ do đối tác cung cấp.

  • Nhược điểm: Chất lượng sản phẩm bị phụ thuộc, không có nhiều các giải pháp tự động để kiểm tra chất lượng sản phẩm và cũng có thể sẽ ảnh hưởng tới vấn đề bảo mật nếu việc chọn giải pháp công nghệ chưa tốt.

Những công nghệ để xây dựng Super App

Giải pháp công nghệ đạt hiểu quả tốt nhất về mặt hiệu suất ứng dụng là dùng Native Code (Kotlin cho Android, Swift cho iOS hay Javascript cho Web, …). Nhược điểm của giải pháp này là cần một đội ngũ nhân sự đủ lớn và nếu có nhân sự bên ngoài nhiều khả năng phải cấp quyền cho họ truy cập vào source code của mình.

Giải pháp công nghệ thứ hai là Cross-Platform như: React-Native, Flutter, Ionic, … Những giải pháp này có thể sẽ giảm một chút về mặt hiệu suất so với Native Code nhưng sẽ mang lại tốc độ phát triển ứng dụng và giảm chi phí nhân sự. Nhược điểm cũng giống như Native Code, vẫn cần đội ngũ nhân sự đủ lớn và rủi ro về mặt bảo mật.

Sử dụng WebView để hiển thị website có sẵn từ đối tác cũng là một giải pháp thường được sử dụng. Ưu điểm là chúng ta có thể tái sử dụng được ứng dụng có sẵn, nhiệm vụ là ta cần viết các cơ chế để giao tiếp với các ứng dụng đó. Nhược điểm dễ thấy là ứng dụng Web đôi khi sẽ cho trải nghiệm không tốt về mặt hiệu suất.

Tini App Studio

Ví dụ Super App và Mini App

Mini App là một khái niệm mới, được bắt nguồn từ thị trường Trung Quốc. Mini App được hiểu đơn giản là một ứng dụng nhỏ, có thể xem như là một dịch vụ của đối tác; và được Super App như là một kênh phân phối ứng dụng tới người sử dụng. Mini App có thể tận dụng được hết các lợi thế của những công nghệ nói trên và khắc phục điểm yếu là phải cấp quyền truy cập source code của doanh nghiệp cho đối tác.

Tini App là gì?

Tini App là một dự án Mini App do Tiki phát triển. Trên thị trường hiện tại có rất nhiều giải pháp để xây dựng Mini App và giải pháp dễ triển khai nhất là xây dựng các bộ thư viện (Bao gồm các APIs giao tiếp với Super App, các thành phần giao diện được xây dựng sẵn) để cung cấp cho đối tác và kèm theo các tài liệu hướng dẫn. Các giải pháp này đều phải phụ thuộc vào công nghệ mà Super App đang sử dụng, ví dụ như Native Code, Cross-Platform; và có thể tiềm ẩn những nguy cơ về bảo mật vì đang chạy trên cùng một môi trường với Super App, ví dụ đọc thông tin từ bộ nhớ để lấy dữ liệu trái phép, …

Tini App Studio

Ví dụ quy trình phát triển một Tini App

Đối với Tini App, Tiki chọn giải pháp có thể xem là tốt nhất, đã được thị trường Trung Quốc xây dựng và chứng minh mức độ hiệu quả. Thay vì sử dụng các công nghệ có sẵn để cung cấp cho đối tác, Tiki xây dựng riêng bộ ngôn ngữ và các công cụ cho Tini App để các đối tác dựa trên đó phát triển sản phẩm của mình. Sau đó, ứng dụng sẽ được chuyển đổi sao cho phù hợp với từng nền tảng, từng thiết bị khác nhau. Quá trình chuyển đối này đều phải thông qua các công cụ của Tiki xây dựng, giảm thiểu tối đa rủi ro có thể gặp phải. Có thể hiểu đơn giản, Tini App như là một hệ điều hành thu nhỏ trên Tiki.

Tại sao phải xây dựng IDE cho Tini App

Chính vì xây dựng bộ ngôn ngữ riêng, Tini App cần có công cụ lập trình, kiểm thử và các giải pháp đính kèm để hỗ trợ tối đa, thuận tiện cho việc phát triển ứng dụng.

***

Để hiểu rõ hơn, ở các phần tiếp theo của chuỗi bài viết. Mình sẽ chia sẻ nhiều hơn về kiến trúc và các công cụ phát triển một Tini App. Các bạn cũng có thể truy cấp website developers.tiki.vn và cộng đồng Tini App community.tiki.vn để có thêm nhiều thông tin.

· 3 min read

Tini App Studio

Với sự phát triển không ngừng của ngành công nghiệp 4.0 và quá trình chuyển đổi số của các doanh nghiệp đang ngày càng tăng. Các khái niệm mới bắt đầu ra đời, điển hình như: AI, Big Data, IoT, Cloud, Blockchain, … Chắc hẳn các cụm từ này không còn quá xa lạ với các bạn, nó hầu như được đề cập khắp mọi nơi, ở các sản phẩm công nghệ đang có trên thị trường.

Cùng với sự phát triển đó, các doanh nghiệp công nghệ đang dần xoay chuyển để trở thành một hệ sinh thái đa dịch vụ, nơi mà khách hàng có thể tìm kiếm và sử dụng được nhiều loại hình dịch vụ khác nhau trên cùng một nền tảng ứng dụng. Ví dụ như Tiki, ngoài việc mua các sản phẩm và dịch vụ do Tiki cung cấp, khách hàng có thể lựa chọn các loại hình dịch vụ khác từ các đối tác bên ngoài như: Đặt vé máy bay, mua bảo hiểm, dịch vụ tài chính điện tử, hay thậm chí có thể đặt lịch cắt tóc trên Tiki.

Với việc xoay chuyển để trở thành một hệ sinh thái đa dịch vụ, trong thế giới của các nhà phát triển ứng dụng công nghệ có một khái niệm mới được ra đời, đó là Super App (Siêu ứng dụng).

Ở nội dung của chuỗi bài viết “Quá trình xây dựng IDE cho Tini App” chúng ta sẽ đề cập tới Super App là gì; những khó khăn trong việc phát triển Super App; Tini App ra đời như thế nào và có mối liên hệ gì với Super App; để phát triển Tini App đạt hiệu quả tốt nhất ta cần xây dựng những gì. Ta cũng dành thời gian để đi sâu hơn về công nghệ và kiến trúc của IDE cho Tini App.


Nội dung bao gồm:

  1. Phần mở đầu (Bạn đang đọc bài này)
  2. Sơ lược về Tini App và tại sao phải xây dựng IDE?
  3. Ngôn ngữ cho Tini App và công cụ compiler
  4. Chọn giải pháp và xây dựng editor
  5. Giả lập môi trường ứng dụng Tini App và các cộng cụ debug
  6. Xây dựng các công cụ Tini Console và định hướng phát triển trong tương lai

Lưu ý: Bài viết được viết dưới góc nhìn của một người làm sản phẩm. Tập trung chủ yếu vào mục tiêu tìm kiếm giải pháp xây dựng hiệu quả. Không nhằm mục đích đánh giá hay so sánh bất kì công nghệ nào.

· 14 min read

Tối ưu hoá luôn là một trong những vấn đề quan trọng nhất khi xây dựng bất kỳ ứng dụng nào. Nếu ứng dụng tải càng nhanh, sử dụng càng mượt, thì sẽ đem được trải nghiệm tốt cho người sử dụng, giúp ứng dụng của bạn sẽ thu hút và giữ chân được nhiều người dùng hơn.

Quá trình tối ưu hoá có thể tốn khá nhiều thời gian và công sức, bao gồm cả việc làm thế nào ứng dụng được tải nhanh nhất, UI/UX mang lại trải nghiệm tốt nhất cũng như đảm bảo ứng dụng không bị chậm trong quá trình sử dụng,...Trong quá trình xây dựng Tini App, đội ngũ phát triển đã cố gắng tối ưu các thành phần bên dưới như framework, jsAPI, components,... để đảm bảo các ứng dụng Tini App được tải và vận hành tốt nhất có thể. Tuy nhiên, việc tối ưu ứng dụng còn cần sự góp sức từ bản thân các nhà phát triển app. Trong phạm vi bài viết này, mình sẽ chia sẻ với các bạn một số kinh nghiệm để tăng tốc độ của Tini App. 🚀

Giảm thiểu kích thước của App

Khi một ứng dụng Tini App được phát hành trên Kho ứng dụng của Tiki. Mặc định sẽ không được đính kèm vào trong app của Tiki, mà chỉ được tải về khi người dùng truy cập vào ứng dụng đó, việc này sẽ đảm bảo được kích thước của app Tiki sẽ nhẹ hơn vì ứng dụng chỉ được tải khi thực sự cần thiết. Do đó, việc app của bạn có kích thước càng nhẹ, thì sẽ tải càng nhanh.

Một Tini App có giới hạn dung lượng là 5Mb, bạn có thể yêu cầu tăng kích thước lên nếu cần thiết, tuy nhiên sẽ thông qua quá trình kiểm định của team để được duyệt. Nhưng như mình nói từ trước, Tini App có kích thước càng nhẹ, thì sẽ được load càng nhanh.

Các thành phần được tải về trong một Tini App mình sẽ chia thành 2 thành phần cơ bản: Code để chạy và Tài nguyên của ứng dụng.

  • Code để chạy bao gồm: code trong file js, txml, tcss, sjs và các thư viện trong node_modules (nếu có)

    Để tối ưu hoá dung lượng code để chạy, bạn chỉ cần tuân theo nguyên tắc: chỉ dùng nếu thực sự cần thiết, không dùng thì xoá những phần thừa đi. Ví dụ: sẽ không cần phải cài package momentjs chỉ để format ngày tháng, hay chỉ sử dụng các hàm như isEmpty mà phải cài cả package lodash vào. Đây là những hàm mà bạn hoàn toàn có thể tự viết hoặc tham khảo trên mạng, việc loại bỏ các thư viện/code không cần thiết không chỉ giúp code bạn nhẹ hơn mà còn giúp bạn hiểu rõ hơn về code và cách làm của hàm đó. Tất nhiên bù lại nó sẽ tốn thời gian hơn để bạn phải tự làm điều đó, nhưng đối với mình, đó là sự đánh đổi đáng giá. Hãy nhớ: càng nhẹ càng tốt 😄

    Phía framework Tini App cũng sẽ đảm bảo quá trình minify source code để làm giảm dung lượng code của các bạn lại, và trong tương lai, có thể bọn mình sẽ hỗ trợ thêm các cách để lazy load các page để làm giảm dung lượng file đầu tiên được load tới mức nhanh nhất có thể.

  • Các tài nguyên bao gồm: hình ảnh, video, audio, json, font,...

    Trong các tài nguyên trên, có vẻ hình ảnh là tài nguyên thường được sử dụng nhiều nhất. Nếu bạn nào đã làm qua với component image thì sẽ biết có 3 cách để khai báo src cho hình: dùng đường dẫn trong app, dùng base 64 và dùng link CDN. Trong đó mình khuyến khích không nên dùng ảnh base 64 cho các hình có dung lượng lớn, ngoài ra, nếu trong app bạn sử dụng một hình mà hầu như có rất ít sự thay đổi dù có nâng lên version và app của bạn thường xuyên có sự thay đổi version, thì bạn nên dùng một CDN riêng để lưu hình ảnh đó. Bởi vì các tài nguyên trong Tini App sẽ được lưu trữ trong CDN của Tiki, và mỗi lần app của bạn thay đổi version thì đường dẫn của file hình đó sẽ bị thay đổi theo version mới, điều đó dẫn đến không tối ưu về phía cache của app. Vì vậy, nếu bên bạn không có CDN, hoặc với những hình ảnh nhỏ gọn thì có thể bỏ vào app, còn không thì bạn nên tải ảnh đó lên CDN và dùng một link cố định.

    Đối với font, mặc định Tini App sẽ sử dụng font Inter cho toàn bộ app. Việc thay đổi một font khác sẽ ảnh hưởng tới trả nghiệm tải app cũng như tăng dung lượng của app lên nếu các file font được đính kèm trong app. Nếu bạn có nhu cầu dùng font khác, bạn có thể xem jsAPI loadFontFace

    Với các tài nguyên khác như video, audio, json,... bạn cũng nên cân nhắc về dung lượng của chúng để có thể dùng CDN hay bỏ vào app một cách hợp lý

Tối ưu hình ảnh

Mình sẽ dành cả một phần để nói về tối ưu hình ảnh trong app, bởi vì hầu như hình ảnh xuất hiện rất nhiều chỗ và hầu như page nào cũng sẽ có hình. Việc tối ưu được hình ảnh sẽ mang đến những trải nghiệm đáng giá cho app của bạn.

Giảm dung lượng hình ảnh

Vâng, lại là giảm dung lượng. Mình lại phải nhắc đi nhắc lại từ này vì nó thật sự rất cần thiết. Hình ảnh và các tài nguyên càng nhẹ thì nó hiển thị càng nhanh, ngoài ra nó sẽ giúp tiết kiệm ...4G của người dùng :)

Có nhiều cách để giảm dung lượng của hình, bạn có thể giảm từ lúc xuất hình ra từ các công cụ như Photoshop, Illustrator hoặc sử dụng các công cụ miễn phí trên mạng như https://tinypng.com/ đối với các hình có định dạng jpeg, png hoặc webp. Phải nói là mình cực kỳ thích trang này, bởi nó cực kỳ hữu ích, bạn chỉ việc quăng hình của mình lên và bùm... dung lượng hình của bạn sẽ giảm đi một cách đáng kể 🔥 Nếu hình ảnh bạn không đòi hỏi sự trong suốt (transparent), bạn nên dùng ảnh dạng jpeg thay vì png, với định dạng webp, hiện tại phía Tini App chưa hỗ trợ tuy nhiên bọn mình sẽ cố gắng hỗ trợ trong thời gian tới. Đối với hình svg, bạn cũng có thể dùng trang này để giảm dung lượng: https://jakearchibald.github.io/svgomg/

Sử dụng CDN xịn xò

Như mình nói ở trên, các tài nguyên trong Tini App sẽ được lưu trữ trên CDN của Tiki, và phía Tiki CDN sẽ đảm bảo tài nguyên của bạn được cache và được nén với gzip. Tuy nhiên đối với các tài nguyên riêng của bạn, hãy nên dùng một CDN hỗ trợ cả cache và nén như gzip nhé

Sử dụng lazyload

Hãy lazyload hình nếu có thể, nó không chỉ giúp app bạn được load nhanh hơn, tiết kiệm dung lượng mạng mà có giảm thiểu số lượng bộ nhớ (memory) mà thiết bị phải dùng. Trong component image của Tini App đã hỗ trợ thuộc tính lazy-load, bạn có thể xem thêm tại đây. Tuy nhiên trong quá trình lazyload, khu vực hiển thị hình ảnh của bạn có thể sẽ bị trống mất một chỗ, điều này sẽ không thực sự tốt về mặt giao diện. Bạn có thể giải quyết bằng cách làm một skeleton hình ảnh với lazyload

<image
src="https://LINK_IMAGE"
lazy-load="{{true}}"
default-source="https://salt.tikicdn.com/ts/tiniapp/53/55/7f/61855c8b38d161f172616efc27783cdc.png"
/>

Cách làm ở đây là mình sẽ dùng một hình có dung lượng cực nhỏ để làm hình mặc định trong default-source, bạn có thể xem hình đó ở đây https://salt.tikicdn.com/ts/tiniapp/53/55/7f/61855c8b38d161f172616efc27783cdc.png. Với kích thước chỉ ~200B, nó hầu như sẽ được load ngay lập tức, và ngay khi hình trong src được tải xong, nó sẽ thay thế hình trong default-source, như vậy có thể đảm bảo cả việc lazyload hình và tránh tình trạng bị trống ở vùng hình ảnh chưa được tải xong

Một cách khác để bạn có thể làm với lazyload là kiểm tra tình trạng mạng của người dùng, nếu mạng chậm bạn có thể sử dụng các hình ảnh dung lượng nhỏ để thay thế, bạn có thể xem jsAPI getNetworkType tại đây

Giảm thiểu số lượng hình ảnh được tải cùng lúc

Quá nhiều hình ảnh được tải cùng lúc sẽ ảnh hưởng đến tốc độ tải app của bạn. Bạn có thể sử dụng cách thức lazyload mình nói ở trên để giảm thiểu hình ảnh được tải cùng lúc. Tuy nhiên vì một lý do nào đó mà bạn không muốn lazyload, thì bạn có thể sử dụng các phương pháp như infinite scroll để khi trang của bạn được cuộn xuống dưới thì mới tải thêm. Sẽ rất tốt nếu bạn có thể kết hợp cả infinite scroll và lazyload

setData

setData là phương thức dùng để thay đổi dữ liệu (data) từ tầng logic (page/component) sang tới tầng view (txml) để giao diện có thể cập nhật lại. Việt setData không hợp lý có thể gây ra những lỗi không muốn về performance của app. Các lỗi dễ gặp nhất đối với setData là gửi quá nhiều dữ liệu xuống hoặc gọi nhiều setData cùng lúc.

Giảm thiểu dữ liệu truyền vào setData

Khi hàm setData được gọi, sẽ diễn ra quá trình serialize dữ liệu và một số xử lý bên dưới, do đó nếu dữ liệu quá lớn được truyền vào setData, có thể sẽ gây nghẽn quá trình truyền dữ liệu, làm ảnh hưởng đến quá trình render view.

Bạn nên hạn chế truyền dữ liệu quá lớn vào setData, đặc biệt là với các dữ liệu dạng danh sách (list), khi đó bạn có thể sử dụng các thủ thuật như lazyload, infinite scroll để lấy các data cần thiết thay vì một lượng lớn dữ liệu ngay từ đầu.

Ngoài ra, bạn có thể hàm $spliceData để có thể giảm số dữ liệu được truyền xuống. Thay vì truyền toàn bộ data, bạn chỉ cần truyền những dữ liệu bị thay đổi.

$spliceData nhận vào giá trị là key: value, trong đó key là một giá trị linh động, bạn có thể truyền vào vị trí của mảng, hoặc thuộc tính (property) của một đối tượng (object). Bạn có thể xem ở đoạn code bên dưới

Page({
data: {
array: [1, 2, 3]
},
async loadMore() {
return new Promise((resolve) => {
setTimeout(() => {
resolve([4, 5, 6]);
}, 1000);
});
},
async onLoad() {
const newArray = await this.loadMore();

// Không nên
this.setData({ array: this.data.array.concat(newArray) });

// Sử dụng với $spliceData
this.$spliceData({
[`array.${this.data.array.length - 1}`]: newArray
});
}
});

Ở ví dụ trên, ban đầu mình khởi tạo giá trị data.array là một mảng đơn giản với 3 phần tử, sau đó trong onLoad mình sẽ gọi thêm một async function loadMore để lấy thêm 3 phần tử nữa. Bạn có thể làm cách đơn giản là bỏ thêm data vào array ban đầu sau đó set toàn bộ data đó xuống, tuy nhiên nếu đây là một mảng phức tạp với rất nhiều phần tử và nhiều thuộc tính trong đó, kích thước dữ liệu truyền xuống sẽ tăng dần nếu bạn gọi loadMore nhiều lần. Với cách làm bên dưới, bạn sử dụng $spliceData, rồi truyền tên dữ liệu trong data, ở đây là array, kèm theo vị trí mà bạn muốn thay đổi, trong trường hợp này là mình muốn thay đổi biến array ở vị trí thứ 2 (bắt đầu từ 0), như vậy giá trị mới sẽ được thay đổi thành array.2 = [4, 5, 6]. Sau khi gọi xong, giá trị của array sẽ thành [1, 2, 3, 4, 5, 6]

Nếu chỉ có nhu cầu thay đổi một thuộc trong đối tượng, hoặc một phần tử nào đó trong mảng, bạn cũng có thể sử dụng setData theo cách sau:

Page({
data: {
app: {
name: 'Tini App',
company: 'Tiki'
},
array: [
{
value: 0
},
{
value: 1
}
]
},
onLoad() {
// 1. Đổi giá trị thuộc tính của đối tượng
// Không nên
this.setData({
app: { ...this.data.app, country: 'Vietnam' }
});
// Chỉ thay đổi thuộc tính cần thiết
this.setData({ 'app.country': 'Vietnam' });

// 2. Đổi giá trị phần tử trong mảng
// Không nên
this.setData({
array: this.data.array.map((item, index) =>
index === 1 ? { value: 2 } : item
)
});
// Chỉ đổi đúng vị trí của phần tử trong mảng
this.setData({ 'array[1]': { value: 2 } });
}
});

Giảm thiểu số lần gọi setData

Nếu bạn gọi setData quá nhiều lần cùng lúc, sẽ dẫn đến bị nghẽn khi truyền dữ liệu, đồng thời có thể gây ra tình trạng UI bị render nhiều lần, hoặc tệ hơn nữa là UI bị block lại, dẫn đến người dùng không thể tương tác trên app của bạn. Tình trạng này rất dễ xảy ra nếu bạn gọi setData trong các sự kiện page scroll hoặc swipe, khi đó sự phản hồi từ giao diện tới người có thể bị chậm trễ, gây ra những trải nghiệm không tốt.

Cách làm đơn giản là bạn hãy xử lý hết các logic trong một hàm rồi hãy gọi setData. Dưới đây là một trường hợp bạn nên tránh khi phải gọi setData tới 2 lần

Page({
data: {
counter1: 0,
counter2: 0
},
onLoad() {
const newCounter1 = this.data.counter1 + 1;
this.setData({ counter1: newCounter1 });

const newCounter2 = this.data.counter2 + 1;
this.setData({ counter2: newCounter2 });
}
});

Thay vào đó hãy gọi khi xử lý tất cả logic xong:

Page({
data: {
counter1: 0,
counter2: 0
},
onLoad() {
const newCounter1 = this.data.counter1 + 1;
const newCounter2 = this.data.counter2 + 1;
this.setData({
counter1: newCounter1,
counter2: newCounter2
});
}
});

Trong trường hợp bạn cần gọi nhiều setData cùng lúc, hãy sử dụng $batchedUpdates

Page({
data: {
counter: 0
},
plus() {
setTimeout(() => {
this.$batchedUpdates(() => {
this.setData({
counter: this.data.counter + 1
});
this.setData({
counter: this.data.counter + 1
});
});
}, 200);
}
});
<!-- pages/index/index.txml -->
<view> {{counter}} </view>
<button onTap="plus">+2</button>

Trong ví dụ trên, mỗi lần bấm vào button, giá trị counter sẽ tăng lên 2. Bởi vì $batchedUpdates sẽ kết hợp các setData thành một lần gọi duy nhất, tránh trường hợp setData bị gọi nhiều lần và UI không phải bị re-render quá nhiều

Ngoài việc giảm thiểu số lượng dữ liệu khi gọi setData hoặc tránh việc gọi setData liên tục nhiều lần, nếu page của bạn chứa nhiều component, và mỗi component thực hiện một logic riêng, khi việc setData chỉ dẫn đến sự thay đổi một component mà không phải toàn bộ, hãy cân nhắc mang setData và dữ liệu đó vào component, như vậy việc trigger re-render chỉ xảy ra ở trong component cần sự thay đổi đó. Bạn có thể sử dụng cách sau nếu cần gọi setData từ page tới component

component/index/index
Component({
didMount() {
this.$page.xxcomponent = this;
}
});
page/index/index
Page({
onPageScroll(e) {
if (this.xxcomponent) {
this.xxcomponent.setData({
scrollTop: e.scrollTop
});
}
}
});

Với cách làm trên, bạn chỉ việc gán component của bạn vào $page (page hiện tại) với giá trị là this, vậy là bạn có thể gọi các phương thức của component đó từ trong page, ví dụ ở trên là mình gọi setData vào component khi page scroll. Bạn có thể xem thêm về component object ở đây.

· 6 min read

Đợt tháng rồi, mình có đi cafe với 1 ông anh ngồi tám chuyện này kia thì mình có giới thiệu cho ảnh về Tini App của Tiki. Cái mình nói ảnh vậy nè ...

Công nghệ Tini App của Tiki đi kèm với hệ thống framework đơn giản, hiệu quả với đa dạng các thành phần giao diện cũng như APIs cần thiết cho phép các nhà phát triển xây dựng ứng dụng với trải nghiệm native app trên nền tảng Tiki một cách dễ dàng nhất có thể.

Là một người đã có dày dặn chinh chiến nhiều với ngôn ngữ lập trình và nhiều dự án, cái ảnh nói lại mình vậy nè:

Ủa vậy Tini App này có support Typescript hông? Giờ ai cũng dùng TypeScript thôi à.

Rồi nói cho mình nghe 1 loạt điểm nổi bật của nó. Cá nhân mình cũng đã có tiếp cận với TypeScript trong React không biết sang Tini App thì nó sẽ thế nào.

Hãy đồng hành cùng mình trong bài viết này nha!

2. Why Typescript ?

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.

TypeScript được phát triển bởi Microsoft và cộng đồng mã nguồn mở. "Superset" ở đây có thể được hiểu là: TypeScript = JavaScript + some amazing stuffs 😆

Ngoài sở hữu những đặc điểm của JavaScript, mình thấy rõ nhất ưu điểm ở TypeScript là về việc hỗ trợ static typing. Lúc đầu tiếp cận, mọi người có thể cảm thấy "không thoải mái" lắm vì đã quen với dynamic typing bên JavaScript rồi đúng hông? 😤 Và mình cũng thế TypeScript khó tính quá mà.

Nhưng mà suy nghĩ xa đi 1 chút, chúng ta có thể quản lý dữ liệu và luồng dữ liệu chặt chẽ hơn nhờ có TypeScript.

Vào việc cấu hình 1 project Tini App với Typescript nào ...

3. How to config

Thì chuyện là hiện tại compiler của tiniapp chỉ hỗ trợ ES chưa support cho TS 😢 nhưng mà tương lai sẽ có nha.

Do đó ý tưởng của mình ở đây là gì? Tadaa..

Đơn giản là mình sẽ compile code TypeScript/Less sang JS/CSS thôi nè.

Theo ý tưởng trên thì mình sẽ viết 1 script cấu hình tasks để compile TS sang ES, rồi dùng ES đó làm entry cho Tini App.

es folder ở đây sẽ được coi như là dist folder.

- scripts/
| compiler.js
- es/
|- pages/
| index.js
| index.json
| index.tcss
| index.txml
| app.js
| app.tcss
| app.json
- src/
|- pages/
| index.ts
| index.less
| index.json
| index.txml
| app.json
| app.ts
| app.less
- types/
| index.d.ts
tsconfig.json
babel.config.js
package.json

Vậy đầu tiên làm gì ta?

Mình sẽ chọn 1 tool Task Runner có các tính năng mình cần dùng chủ yếu là: Tasks, Minify, Complile, Reload.

Mình chọn gulp để làm phần này nha. Các bạn có thể sử dụng tool khác để thay thế.

Bắt đầu thôi đi từng cái nào.

  1. Install packages

packages.json

{
"name": "app-typescript",
"version": "0.1.0",
"tiki": {
"appIdentifier": "app.id.typescript",
"buildNumber": 1
},
"scripts": {
"clean": "rm -rf es",
"dev": "yarn clean && node scripts/compiler.js"
},
"devDependencies": {
"@babel/core": "^7.13.15",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.13.15",
"@babel/preset-env": "^7.13.15",
"@babel/preset-typescript": "^7.1.0",
"@types/jest": "^26.0.23",
"@typescript-eslint/eslint-plugin": "^4.21.0",
"@typescript-eslint/parser": "^4.21.0",
"eslint": "^7.24.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-prettier": "^3.3.1",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-clean-css": "^4.0.0",
"gulp-less": "^4.0.1",
"gulp-rename": "^2.0.0",
"prettier": "^2.2.1",
"stylelint": "^13.12.0",
"stylelint-config-standard": "^21.0.0",
"typescript": "^4.2.4"
},
"dependencies": {
"@babel/runtime": "^7.15.3"
}
}

babel.config.js

module.exports = {
presets: [
[
'@babel/preset-env',
{
loose: true,
modules: false
}
],
'@babel/preset-typescript'
],
plugins: [
'@babel/plugin-syntax-dynamic-import',
'@babel/plugin-transform-runtime'
]
};

Tiếp tục là file script nè: compiler.js

const path = require('path');
const gulp = require('gulp');
const less = require('gulp-less');
const rename = require('gulp-rename');
const cleanCss = require('gulp-clean-css');
const babel = require('gulp-babel');

const dist = path.join(__dirname, '..', 'es');
const src = path.join(__dirname, '..', 'src');
const extTypes = ['ts', 'less', 'json', 'txml', 'sjs'];

// LESS

gulp.task('less', () =>
gulp
.src(`${src}/**/*.less`)
.pipe(less())
.on('error', (e) => console.error(e))
.pipe(cleanCss())
.pipe(
rename({
extname: '.tcss'
})
)
.pipe(gulp.dest(dist))
);

// TS

gulp.task('ts', () =>
gulp
.src(`${src}/**/*.ts`)
.pipe(babel())
.on('error', (err) => {
console.log(err);
})
.pipe(gulp.dest(dist))
);

// JS

gulp.task('js', () =>
gulp
.src(`${src}/**/*.js`)
.pipe(babel())
.on('error', (err) => {
console.log(err);
})
.pipe(gulp.dest(dist))
);

gulp.task('json', () =>
gulp.src(`${src}/**/*.json`).pipe(gulp.dest(dist))
);

gulp.task('txml', () =>
gulp.src(`${src}/**/*.txml`).pipe(gulp.dest(dist))
);

gulp.task('sjs', () =>
gulp.src(`${src}/**/*.sjs`).pipe(gulp.dest(dist))
);

gulp.task('tcss', () =>
gulp
.src([`${src}/**/*.tcss`, `!${src}/**/*.skip.tcss`])
.pipe(gulp.dest(dist))
);

const build = gulp.series(...extTypes);
build();

extTypes.forEach((type) => {
const watcher = gulp.watch(
`${src}/**/*${type}`,
gulp.series(type)
);
watcher.on('change', (event) => {
console.log(`File ${event} was change`);
});
watcher.on('add', (event) => {
console.log(`File ${event} was add`);
});
watcher.on('unlink', (event) => {
console.log(`File ${event} was remove`);
});
});

Bạn có thể cài đặt thêm tini-types để có thể có các gợi ý code trên các IDE

yarn add @tiki.vn/tini-types

Sau đó config như sau:

tsconfig.json

{
"compilerOptions": {
"baseUrl": ".",
"target": "es6",
"noImplicitThis": true,
"noImplicitUseStrict": true,
"typeRoots": [
"types/index.d.ts",
"node_modules/@tiki.vn/tini-types/types/index.d.ts"
]
},
"include": [
"types/**/*",
"src/**/*",
"node_modules/@tiki.vn/tini-types/types/**/*"
]
}

Code Tini App sẽ được gợi ý rất nhiệt tình 😋

Tuy nhiên việc gợi ý code này đã được support sẵn khi bạn dùng Tini Studio để code nha.

Và cuối cùng là nhớ cấu hình project của mình root là folder es nè.

project.config.json

{
"miniprogramRoot": "es"
}

Done 😋 chạy lệnh thôi nào ...

yarn run dev

Example: https://github.com/anhhuynh5/tiniapp-typescript

4. Example

Ở đây mình viết example component thôi nha. Thế là xong.

interface TestMethods {
onClick?: () => void;
}
interface TestComponentProps extends TestMethods {
className?: string;
style?: string;
value: string;
}

Component({
props: {
value: '',
onClick: () => {},
} as TestComponentProps,

methods: {
onTest() {
const a: number = 5;
console.log(a);
},
},
});

5. Sumup

Vậy là chúng ta đã cùng nhau tìm hiểu cách build 1 project Tini App với TypeScript/Less rồi nè. Như mình đã nói hiện tại thì compiler của Tini App chưa suppport cho TypeScript nhưng trong tương lai gần sẽ có sớm thôi nha. 💥

Với sức mạnh nổi bật của TypeScript, việc tích hợp vào Tini App sẽ phù hợp với các dự án dài hạn hoặc phục vụ cho việc phát triển một thư viện. Nếu chưa chọn dùng TypeScript cũng chẳng sao, TypeScript thực sự tốt nhưng JavaScript không gặp bất lợi gì quá lớn cả, nó vẫn đáng để chúng ta tin tưởng.

TypeScript là một-lựa-chọn tốt, nhưng nó không thay thế hoàn toàn JavaScript 😋

Hi vọng bài viết của mình giúp bạn có thêm tài liệu tham khảo khi áp dụng TypeScript cho dự án Tini App của bạn.

Example: https://github.com/anhhuynh5/tiniapp-typescript

· 4 min read

Chào các bạn, lại là mình đây. Vậy là đã gần một năm kể từ khi mình bắt đầu dùng Tini App. Mình thấy Tini App tiết kiệm thời gian khá nhiều để bạn phát triển một ứng dụng, từ bước setup môi trường, code cho tới triển khai nó lên Tiki App. Tuy nhiên, do là một framework mới nên sẽ còn rất nhiều thiếu sót và các thư viện hỗ trợ cũng hạn chế. Vì vậy, hôm nay mình sẽ hướng dẫn các bạn thư viện tini-recycle để tăng tốc độ code của bạn cho một dự án lớn.

I. Vì sao sử dụng tini-recycle

Bây giờ, các bạn xem 2 code mẫu bên dưới

// page1/index.js
Page({
data: { user: null, loading: true },
async onLoad() {
this.setData({ loading: true });
let user = await getCurrentUser();
if (!user) user = await this.login();
this.setData({ user, loading: false });
},
async login() {
// Logic To Login
},
});
// page2/index.js
Page({
data: { user: null, loading: true },
async onLoad() {
this.setData({ loading: true });
let user = await getCurrentUser();
if (!user) user = await this.login();
this.setData({ user, loading: false });
},
async login() {
// Logic To Login
},
});

Các bạn có thể thấy, 2 page không khác nhau lắm về flow login của user nhưng phải lặp lại 2 lần. Các bạn có thể tách các function login để sử dụng lại. Tuy nhiên, phần onLoad các bạn không có cách nào tách ra được vì có các method như this.setData.

Vậy hướng giải quyết là gì? Tini Recycle sẽ giúp bạn giải quyết vấn đề này

II. Sử dụng Tini Recycle như thế nào

1. Cài đặt

yarn add tini-recycle
Hoặc
npm install --save tini-recycle

2. Basic Code

import { $page } from "tini-recycle";

const authHook = () => [
{
data: { user: null, loading: true },
async onLoad() {
this.setData({ loading: true });
let user = await getCurrentUser();
if (!user) user = await this.login();
this.setData({ user, loading: false });
},
async login() {
// Logic To Login
},
},
];

$page(
authHook(),
{
data: {
// others state
},
onTap() {
console.log(this.data.user, this.data.loading)
// Return user and loading data
}
}
);

3. Các method

import { $page, $component, hooks } from "tini-recycle"
$page(...hooks: Hook[])
$component(...hooks: Hook[])

type Config = TiniPageConfig | TiniComponentConfig;
type Hook = Config | [Hook] | (config: Config) => Hook

III. Các hooks thường dùng

Cài đặt

import { hooks } from "tini-recycle"

hooks.hookLoadMore Chỉ support cho $page

type Option = {
throttleWait: number, // default 50 - Nhận sự kiện scroll mỗi {throttleWait} giây
threshold: number, // default 1000 - Nhận sự kiện khi end of scroll trước {threshold}px
disabled: boolean, // default fale - Stop sự kiện loadmore
methodName: string, // default "onLoadMore" - Tên method được gọi khi cuộn xuống dưới cùng
}
hooks.hookLoadMore: (option: Option) => any

Ví dụ

$page(
hooks.hookLoadMore({ methodName: 'onLoadMore', throttleWait: 50, threshold: 300 }),
{
data: {
items: [],
},
page: 1,
async onLoadMore() {
const { items, page } = await api.getItems({ page: this.page });
this.page = page;
this.setData({ items: [...this.data.items, ...items] });
}
}
)

hooks.hookQueryParser Hook giúp chuyển giá trị query trong onLoad từ string sang Object Chi tiết: https://developers.tiki.vn/docs/framework/miniapp-page/life-cycle#onload

() => any
$page(
hooks.hookQueryParser(),
{
onLoad(query) {
console.log(typeof query); // Object not string
},
}
);

hooks.hookMapPropsToMethods

(mapping: Record<[methodName: string],[propName: string]>) => any

Ví dụ

$component(
hooks.hookMapPropsToMethods(["handleLogin", "onLogin"]),
{
onTap() {
this.handleTap(); // === this.props.onLogin()
},
}
);

hooks.hookMapPropsToData

type Data = Record<string,any>;
type Props = Record<string,any>;
((props: Props, data: Data) => [newData: Data]) => any

Ví dụ

$component(
hooks.hookMapPropsToData(function (props) {
return { id: props.id.toString() };
}),
{
onTap() {
console.log(this.data.id);
},
}
);

IV. Global Hooks

Ở trên các bạn sẽ sử dụng các hook cho từng page. Ví dụ hooks.hookQueryParser() gần như page nào cũng sử dụng. Vậy cách nào để apply nó cho tất cả page

// app.js
$page.addBeforeAll(hooks.hookQueryParser());

Ngoài ra Tini Recycle còn cung cấp các method global hook khác như

// app.js
$page.addBeforeAll(hook: Hook);
$page.addAfterAll(hook: Hook);
$component.addBeforeAll(hook: Hook);
$component.addAfterAll(hook: Hook);

V. Tổng kết

Ở trên mình đã giới thiệu qua về Tini Recycle, các hook cơ bản cho một dự án Tini App. Ở bài viết sau mình sẽ giới thiệu cho các bạn một số addvance hook để các bạn có thể code pro hơn nhé. Ngoài ra các bạn có nhu cầu sử dụng hook nào khác thì đừng ngại để dưới comment nhé. Mình sẽ hỗ trợ bạn hết mình 😘😘😘

Github: https://github.com/huynguyen6tiki/tini-recycle

· 6 min read

🏁 Recap

phần 1phần 2, chúng ta đã cùng nhau tìm hiểu về Shopping Template và "thực chiến" với 2 pages: HomeSearch. Ở phần này, mình sẽ cùng các bạn tiếp tục xây dựng giỏ hàng và thực hiện mock payment.

Mục lục

I. Quản lý state trong Tini App

II. Xây dựng giỏ hàng

  1. Navigate đến giỏ hàng
  2. Thêm sản phầm vào giỏ hàng
  3. Sửa số lượng sản phẩm
  4. Xoá sản phầm khỏi giỏ hàng
  5. Thêm + xoá coupon
  6. Tạo payment (Mở rộng)

III. Tổng kết


I. Quản lý state trong Tini App

  • Mình đã có một bài viết nói về cách tiếp cận và xử lý state trong Tini App. Bạn có thể đọc bài viết Quản lý state trong Tini App - Cái nhìn tổng quan và cách tiếp cận Phần 1Phần 2.
  • Ý tưởng chung ở đây chúng ta sẽ dùng app như một global store - nơi mà mọi page và component đều có thể truy cập tới bằng method getApp().
  • Kết hợp Event Emitter để thực hiện render khi app thay đổi.

📌 Xem thêm getApp()

app.js
App({
cartEvent: new EventEmitter(),

cart: {
buyer: {},
seller: {},
orderedProducts: [],
productId: '',
shippingFee: 0,
price: 0,
total: 0,
coupon: {
name: '',
discount: 0,
isValid: false,
},
},

// ...
});

II. Xây dựng giỏ hàng

Cart Page

Như video demo ở trên, ở Cart Page chúng ta sẽ có các actions:

  1. Navigate đến giỏ hàng
  2. Thêm sản phầm vào giỏ hàng
  3. Sửa số lượng sản phẩm
  4. Xoá sản phẩm khỏi giỏ hàng
  5. Thêm + xoá coupon
  6. Tạo payment (Mở rộng)

Hãy cùng đi từng action nào

1. Navigate đến giỏ hàng

Để navigate đến giỏ hàng, ta sẽ sử dụng jsapi my.navigateTo()

pages/detail/index.js
navigateToCart () {
my.navigateTo({ url: `pages/cart/index` });
};

📌 Xem thêm my.navigateTo()

Tuy nhiên, để gắn sự kiện cho icon ở navigation bar, chúng ta phải khai báo trong event onCustomIconEvent

pages/detail/index.js
onCustomIconEvent(e) {
navigateToCart();
},

📌 Xem thêm onCustomIconEvent()

  • Mỗi khi sử dụng my.navigateTo(), framework sẽ push page đó vào screen stack.
  • Để pop screen ra khỏi stack, ta có thể bấm vào nút back trên navigation bar hoặc sử dụng jsapi my.navigateBack()

📌 Xem thêm my.navigateBack()

2. Thêm sản phẩm vào giỏ hàng

Khi tap vào button Add to cart, chúng ta sẽ gọi addProduct() từ app để add sản phầm vào giỏ hàng

pages/detail/index.js
addToCart() {
getApp().addProduct(this.data.product);
},

Ta sẽ implement addProduct() ở app như sau:

  • Tìm kiếm xem ở giỏ hàng đã có sản phẩm đó chưa.
  • Nếu chưa có thì add vào và cho số lượng là 1.
  • Nếu có rồi thì tăng số lượng lên 1 đơn vị.
  • Tính toán lại giá bằng cách gọi calculatePrices() - Hàm này sẽ tính toán lại giá và emit event CART_UPDATE.
app.js
addProduct(product) {
const position = this.cart.orderedProducts.findIndex(
(item) => item.id === product.id,
);
if (position !== -1) this.cart.orderedProducts[position].quantity += 1;
else this.cart.orderedProducts.push({ ...product, quantity: 1 });

this.calculatePrices();
},
calculatePrices() {
const { shippingFee, coupon, orderedProducts } = this.cart;
const price = orderedProducts.reduce((acc, curr) => {
return acc + curr.price * curr.quantity;
}, 0);
const total = price > 0 ? price + shippingFee - coupon.discount : 0;
this.cart = {
...this.cart,
price,
total,
};

this.cartEvent.emit(EMITTERS.CART_UPDATE, this.cart);
},

pages/cart, ta sẽ lắng nghe event CART_UPDATE được emit ở trên và update data cart mới từ đó render lại thông tin cart

pages/cart/index.js
async onLoad() {
this.disposableCollection.push(
app.cartEvent.on(EMITTERS.CART_UPDATE, (cart) =>
this.setData({
cart,
}),
),
);
},
cart

3. Sửa số lượng sản phẩm

Khi tap button + hoặc - hoặc nhập trực tiếp số vào component stepper, ta sẽ gọi changeQuantityProduct() của app

📌 Xem thêm stepper

pages/detail/index.js
onChangeQuantityProduct(product, quantity) {
getApp().changeQuantityProduct(product, quantity);
},

Ta sẽ implement changeQuantityProduct() ở app như sau:

  • Tìm sản phẩm trong cart.
  • Update lại số lượng theo số lượng trong param nhận được.
  • Tính toán lại giá bằng cách gọi calculatePrices() tương tự như trên.
app.js
changeQuantityProduct(product, quantity) {
const position = this.cart.orderedProducts.findIndex(
(item) => item.id === product.id,
);
if (position !== -1) {
this.cart.orderedProducts[position].quantity = quantity;
}

this.calculatePrices();
},

4. Xoá sản phầm khỏi giỏ hàng

Khi tap vào icon close, chúng ta sẽ lưu sản phẩm được chọn lại (vì trong cart có thể sẽ có nhiều sản phẩm) và show modal xác nhận

modal confirm

📌 Xem thêm modal

components/order-list/index.js
confirmRemoveOrder(product) {
this.selectedProduct = product;
this.setData({
modal: {
isShow: true,
headers: ['Confirmation'],
descriptions: ['Do you want to remove this product from your cart?'],
leftButton: 'Yes',
rightButton: 'No',
},
});
},

Khi tap vào Yes, removeProduct() ở app sẽ được gọi và nhận vào product được chọn mà ta đã lưu lại ở trên.

onRemoveProduct(product) {
app.removeProduct(product);
},

Ta sẽ implement removeProduct() ở app như sau:

  • Tìm sản phẩm trong cart.
  • Remove sản phẩm khỏi cart.
  • Tính toán lại giá bằng cách gọi calculatePrices() tương tự như trên.
app.js
removeProduct(product) {
const position = this.cart.orderedProducts.findIndex(
(item) => item.id === product.id,
);
if (position !== -1) this.cart.orderedProducts.splice(position, 1);

this.calculatePrices();
},

5. Thêm + xoá coupon

Tương tự như trên, ta cũng implement selectCoupon()removeCoupon() trong app. Sau đó calculatePrices() cũng sẽ được gọi để tính toán lại giá thành và trigger event update UI của cart.

coupon
async selectCoupon(code) {
try {
const coupon = await getCouponFromCodeAPI(code);
this.cart.coupon = coupon;

this.calculatePrices();
} catch {}
},

removeCoupon() {
this.cart.coupon = {
name: '',
discount: 0,
isValid: false,
};

this.calculatePrices();
},

6. Tạo payment (Mở rộng)

Bạn có thể để tạo payment thông qua hệ thống payment của Tiki thông qua jsapi my.makePayment().

📌 Xem thêm my.makePayment()


🔚 III. Tổng kết

Một lần nữa, cảm ơn các bạn đã đọc cuối bài, qua 3 phần của blog Xây dựng Shopping Template cùng Tini App chúng ta đã tìm hiểu cách cài đặt 3 pages lớn nhất của template là Home Page, Search Page, Cart Page đồng thời biết được data flows của một ứng dụng Tini Apps.

Vì thời gian có hạn nên các ví dụ ở bài blog này có thể chưa đầy đủ vì thế mình có đính kèm link github ở đây, bạn có thể clone về và ngâm cứu sâu hơn.

Hi vọng vài viết sẽ giúp ích cho bạn trong quá trình tìm hiểu và xây dựng ứng dụng trên nền tảng Tini App 🎉

· 7 min read

🏁 Recap

phần 1, chúng ta đã tìm hiểu được:

  • Shopping Template là gì
  • Chức năng và cấu trúc dự án
  • Cách khởi tạo project Tini App
  • Xây dựng Home page:
    • Phân tích và break layout
    • Xây dựng các component
    • Data và gọi API

Ở phần 2 này, mình sẽ cùng các bạn tiếp tục xây dựng shopping template.

Mục lục

I. Xây dựng Search Page

  1. Phân tích layout
  2. Tái sử dụng components
  3. Component search-bar
  4. Component recent-search
  5. Component filter
  6. Component sort
  7. Component filter-list
  8. Empty

II. Tạm kết


I. Xây dựng Search Page

Search Page

1. Phân tích layout

Break nhỏ layout này ta sẽ được các components:

2. Tái sử dụng components

category-section và product-section

Hai components product-sectioncategory-section được đã được sử dụng ở Home page. Tương tự ở home, product-section ở search page hoàn toàn không thay đổi, còn ở category-section thì chúng ta có thể overwrite css một chút để biến nó thành dạng row và có thể swipe (vuốt).

pages/search/index.txml
<category-section
className="search-category-list"
categories="{{categories}}"
onTapCategory="goToCategoryDetail"
/>
pages/search/index.tcss
.search-category-list .category-grid-layout {
display: flex;
overflow-x: auto;
padding-left: 20px;
}

Ở đây mình sẽ sử dụng component search-bar của tini-ui. Tuy nhiên để thuận tiện cho việc customize, mình đã wrap lại thành một component search-bar riêng của mình:

  • Nhận vào value và hiển thị.
  • Khi user nhập thì gọi onInput() ở page nhận biết để set lại giá trị mới cho value.
  • Khi bấm vào nút Close (X) ở cuối thì set value về rỗng.
  • Khi user nhấn "Enter" hoặc bấm vào icon kính lúp thì gọi onConfirm (Để lưu history)
  • Khi user nhập và sau đó ngưng trong 400ms thì gọi onSearch ở page để tiến hành gọi API tìm kiếm, nếu chưa ngưng đủ 400ms mà user tiếp tục nhập thì reset lại từ đầu (Đây là kĩ thuật debounce search bạn có thể tìm hiểu thêm bằng cách search từ khoá này).
components/search-bar/index.json
{
"component": true,
"usingComponents": {
"search-bar": "@tiki.vn/tini-ui/es/search-bar/index"
}
}
components/search-bar/index.js
Component({
props: {
className: '',
placeholder: 'Search products',
value: '',
onInput: () => {},
onSearch: () => {},
onConfirm: () => {}
},

methods: {
isTyping: null,
_onChangeSearchInput(event) {
const { value } = event.detail;
this.props.onInput(value);
},
_clearSearchInput() {
this.props.onInput('');
},
_onSearch() {
this.props.onSearch(this.props.value);
},
_onConfirm(event) {
this.props.onConfirm(event.detail.value);
}
},

// Life cycle
didUpdate() {
if (this.isTyping) {
clearTimeout(this.isTyping);
}
this.isTyping = setTimeout(() => {
this._onSearch();
}, 400);
}
});
components/search-bar/index.txml
<search-bar
value="{{value}}"
placeholder="{{placeholder}}"
maxLength="{{100}}"
onTapCloseIcon="_clearSearchInput"
onInput="_onChangeSearchInput"
onTapSearchIcon="_onConfirm"
/>

📌 Xem souce code tại đây

  • Đây là component khá đơn giản, chúng ta sẽ nhận vào 1 array recentKeys là danh sách các từ khoá được tìm kiếm gần đây.
  • Khi tap vào view item thì sẽ gọi onClickItem.
  • Khi tap vào nút close thì sẽ gọi onRemoveItem.
  • Vì nút close nằm trong item view - nơi đã sẵn lắng nghe 1 sự kiện onTap nên khi tiếp tục gắn onTap vào nút close thì khi tap cả 2 sự kiện sẽ được trigger (1 của nút close và 1 của item view). Để giải quyết vấn đề này ta sẽ sử dụng catchTap ở nút close thay vì onTap.

📌 Xem thêm Event type

components/recent-search/index.txml
<view
class="{{className}} flex justify-between items-center py-x-small {{index !== recentKeys.length - 1 ? 'border-bottom-gray' : ''}}"
tiki:for="{{recentKeys}}"
data-item="{{item}}"
onTap="_onClickItem">
<view>{{ item }}</view>
<view
class="flex items-center"
data-item="{{item}}"
catchTap="_onRemoveItem">
<icon type="close" color="#808089" />
</view>
</view>

pages/search/index.js
onConfirm(searchTerm) {
this.onSearch(searchTerm);
this.addNewRecentKey(searchTerm);
},
  • Ý tưởng của addNewRecentKey là khi nhận được keyword, chúng ta sẽ lưu keyword đó vào Storage. Storage lưu trữ các các keyword dưới dạng mảng, tương tự local storage của browser, dữ liệu sẽ được lưu ở bộ nhớ của thiết bị và tồn tại qua những lần mở/tắt app.
  • Vì mình chỉ muốn lưu tối đa maxSearch keywords nên trước khi lưu, mình sẽ dùng slice để loại bỏ đi các keywords cũ.

📌 Xem thêm Storage

async addNewRecentKey(searchTerm) {
if (!searchTerm || searchTerm.length === 0) return;

const keysSearch = await getStorage('recent-search');
let recentKeys = keysSearch ? keysSearch.slice(0, this.maxSearch) : [];
if (recentKeys.includes(searchTerm)) {
recentKeys = recentKeys.filter((k) => k !== searchTerm);
}
const newKeys = [searchTerm, ...recentKeys.slice(0, this.maxSearch - 1)];
setStorage('recent-search', newKeys);
this.setData({
recentKeys: newKeys,
});
},
  • Và khi remove, hãy nhớ remove ở Storage luôn nhé
async removeSearchKey(key) {
const recentKeys = await getStorage('recent-search');
const removedKeys = recentKeys.filter((k) => k !== key);
setStorage('recent-search', removedKeys);
this.setData({
recentKeys: removedKeys,
});
},

📌 Xem souce code tại đây

5. Component filter

5a. Component filtered-button

Ta sẽ sử dụng component chip của tini-ui.

📌 Xem thêm chip

components/filtered-button/index.txml
<chip
active="{{totalFilters}}"
content="Filter {{totalFilters ? `(${totalFilters})` : ''}}"
prefixImage="{{totalFilters ? '/assets/icons/filter-active.svg' : '/assets/icons/filter.svg'}}"
onClick="_onClick"
onLeftClick="_onClick"
/>

📌 Xem souce code tại đây

5b. Filter bottom-sheet

Để đơn giản, mình chỉ xin demo option price như hình. Với UI trên, ta sẽ sử dụng component bottom-sheetchip của tini-ui.

📌 Xem thêm bottom-sheet

components/filter/index.txml
  <block tiki:if="{{isShow}}">
<bottom-sheet
title="Filter"
onClose="_onClose"
>
<view class="filter-content-section p-medium">
<text class="font-bold">Price</text>
<view class="filter-chip-list flex flex-wrap">
<view
tiki:for="{{filters.prices}}"
tiki:key="value"
>
<chip
active="{{_selectedFilters.priceOption.value === item.value}}"
className="filter-chip mr-2x-small mt-small"
content="{{item.label}}"
data-item="{{item}}"
onClick="onSelectPrice"
/>
</view>
</view>
</view>
<view slot="footer" class="filter-footer flex w-full px-medium py-2x-small">
<button
class="w-full mr-4x-small"
shape="pill"
type="outline"
onTap="onReset"
>
Reset
</button>
<button
class="w-full ml-4x-small"
shape="pill"
onTap="_onSelect"
>
Apply
</button>
</view>
</bottom-sheet>
</block>

📌 Xem souce code tại đây

6. Component sort

6a. Button sort

Tương tự filtered-button, ta sẽ sử dụng component chip của tini-ui.

📌 Xem thêm chip

components/sort/index.txml
<chip
active
content="{{selectedSort.label ? selectedSort.label : 'Sort'}}"
prefixImage="/assets/icons/sort-active.svg"
onClick="_onShow"
onLeftClick="_onShow"
/>

6b. Sort bottom-sheet

Tương tự filter bottom-sheet, ta sẽ sử dụng component bottom-sheetchip của tini-ui.

📌 Xem thêm bottom-sheet

components/filter/index.txml
<block tiki:if="{{isShow}}">
<bottom-sheet
title="Sort"
onClose="_onClose"
>
<view class="padding-inset-bottom px-medium">
<view
tiki:for="{{sorts}}"
tiki:key="value"
class="sort-item flex justify-between items-center py-x-small {{item.value === selectedSort.value ? 'sort-item-active' : ''}}"
data-item="{{item}}"
onTap="_onSelect"
>
<text>{{item.label}}</text>
<icon tiki:if="{{item.value === selectedSort.value}}" type="success_glyph" color="#00AB56" />
</view>
</view>
<view slot="footer"/>
</bottom-sheet>
</block>

📌 Xem souce code tại đây

7. Component filter-list

  • Chúng ta sẽ có 1 array là danh sách các filters đã chọn.
  • Khi tap vào nút Close (X) thì sẽ gọi onRemoveFilter.
  • Tương tự như recent-key, ta sẽ gắn sự kiện catchTap cho nút Close (X) thay vì onTap

📌 Xem thêm chip 📌 Xem thêm Event type

components/filter-list/index.txml
<view
tiki:if="{{formattedSelectedFilters.length}}"
class="category-detail-selected flex bg-gray10 py-small px-medium hide-scroll-bar">
<view
tiki:for="{{formattedSelectedFilters}}"
tiki:key="key"
class="category-detail-selected-item {{index === formattedSelectedFilters.length - 1 ? 'pr-medium' : 'pr-2x-small'}}"
>
<chip
className="bg-white"
content="{{item.value}}"
suffixImage="/assets/icons/close.svg"
data-item="{{item}}"
onRightClick="_onRemoveFilter"
/>
</view>
</view>

📌 Xem souce code tại đây

8. Component empty

Đây có lẽ là component dễ nhất, ta chỉ cần một vài class của tini-style để hoàn thành UI này.

components/empty/index.txml
<view class="flex flex-col items-center {{className}}">
<image
class="empty-image mb-large"
src="/assets/images/empty.png"
mode="widthFix"
/>
<text class="text-medium font-bold mb-5x-small">{{title}}</text>
<text>{{description}}</text>
</view>

📌 Xem souce code tại đây


🔚 II. Tạm kết

Một lần nữa, cảm ơn các bạn đã đọc cuối bài, bài cũng đã dài nên hẹn các bạn ở phần 3, chúng ta sẽ cùng xây dựng page Cart Hi vọng vài viết sẽ giúp ích cho bạn trong quá trình tìm hiểu và xây dựng ứng dụng trên nền tảng Tini App 🎉

· 11 min read

Mục lục

A. Giới thiệu

  • I. Shopping Template là gì ?
  • II. Chức năng
  • III. Cấu trúc dự án

B. Bắt tay vào việc

  • I. Setup project
  • II. Xây dựng Home page
    1. Phân tích layout
    2. Component banners
    3. Component section-title
    4. Component product
    5. Component product-section
    6. Component category
    7. Component category-section
    8. Component category-carousel
    9. Tổng thể
    10. Mock data và API

C. Tạm kết


🏁 A. Giới thiệu

I. Shopping Template là gì ?

Shopping Template là open souce template hỗ trợ lập trình viên xây dựng một ứng dụng mua sắm đơn giản bằng nền tảng Tini App. Hôm nay, chúng ta sẽ cùng nhau xây dựng lại template này từ đầu.

II. Chức năng

  • Xem thông tin cơ bản shop
  • Banners quảng cáo
  • Danh mục sản phẩm
  • Bộ sưu tập sản phẩm
  • Thông tin chi tiết sản phẩm
  • Sắp xếp và lọc sản phẩm
  • Tìm kiếm sản phẩm
  • Quản lý giỏ hàng
  • Thanh toán (mock)
  • Danh sách orders
  • Thông tin account

Bạn có thể quét mã QR hoặc click vào link này để trải nghiệm demo:

QR code

III. Cấu trúc dự án

src

└── assets
│ └───icons
│ └───images

└── components
│ └───banners
│ └───product-section
│ └───category-section
│ └───...

└── pages
│ └───home
│ └───category
│ └───search
│ └───...

└── services

└── utils

└── services

└── app.js
└── app.json
└── app.tcss
└── package.json

🚀 B. Bắt tay vào việc

I. Setup project

  • Tạo một project Tini App mới (Xem thêm).
  • Để sử dụng các Advance Component như textfield, stepper,... chúng ra sẽ cài đặt tini-ui (Xem thêm).
  • Để style các component một các đơn giản chúng ta có thể sử dụng tini-style (tini-style đã được built-in tron tini-ui, bạn chỉ cần import trong file app.tcss (Xem thêm).
  • Thêm query-string để truyền query giữ các pages dễ dàng hơn (Xem thêm).
package.json
{
"name": "miniapp-tini-shop",
"version": "1.0.1",
"tiki": {
"appIdentifier": "vn.tiki.tinishop",
"buildNumber": 1
},
"dependencies": {
"@tiki.vn/tini-ui": "0.10.0",
"query-string": "^7.0.1"
}
}

II. Xây dựng Home page

home page

1. Phân tích layout

Break nhỏ layout này ta sẽ được các components:

2. Component banners

Ý tưởng của banners khá đơn giản, chúng ta sẽ có nhận vào 1 mảng banners là array các url image, sau đó dùng component được built-in sẵn trong framework là carousel để show các images ấy dưới dạng carousel.

📌 Xem thêm carouselimage

components/banners/index.js
Component({
props: {
banners: []
}
});
components/banners/index.txml
<carousel
tiki:else
autoplay
indicator-dots
circular
class="banners"
>
<carousel-item
tiki:for="{{banners}}"
tiki:key="id"
>
<image
class="w-full h-full"
src="{{item.image}}"
mode="widthFix"
/>
</carousel-item>
</carousel>

📌 Xem souce code tại đây

3. Component section-title

Chúng ta có thể dễ dàng nhận ra hình ảnh này được lặp đi lặp lại nhiều lần trong home page, thế nên tại sao chúng ta lại không component hoá nó nhỉ, đây cũng là một component khá dễ để chúng ta làm quen với các class được cung cấp bởi tini-style.

ClassCSS
.flexdisplay: flex;
.justify-betweenjustify-content: space-between;
.items-centeralign-items: center;
.font-boldfont-weight: 700;
.text-mediumfont-size: 16px;

📌 Các giá trị css như 700 và 16px là giá trị mặc định, bạn có thể customize các giá trị này thông qua việc chỉnh sửa css token 📌 Xem thêm tini-style

📌 Xem thêm texticon

components/section-title/index.js
Component({
props: {
title: '',
onTapActionButton: () => {}
},

methods: {
_onTapActionButton() {
this.props.onTapActionButton();
}
}
});
components/section-title/index.txml
<view class="flex justify-between items-center mt-4x-small mb-medium">
<text class="font-bold text-medium">
{{title}}
</text>
<view
class="flex items-center"
onTap="_onTapActionButton"
>
<text class="text-brand mr-2x-small">View more</text>
<icon type="arrow_right" color="#1A94FF"/>
</view>
</view>


📌 Xem souce code tại đây

4. Component product

Ở component product này phần layout cũng khá dễ, chúng ta chỉ cần sử dụng một vài class của tini-style như flexboxspacing là có thể hoàn thành cơ bản layout này.

Mình xin giới thiệu cái đặc biệt ở component này chính là phần format giá tiền. Ví dụ data chúng ta nhận được là price: 3569000, làm sao để format con số này thành 3.569.000 đ và hiển thị trong txml.

Có nhiều cách để làm việc này, tuy nhiên trong khuôn khổ bài này mình xin chia sẻ với các bạn cách mình hay sử dụng nhất đó là dùng sjs .

📌 Xem thêm sjs

Bước 1: Tạo file common.sjs
utils/common.sjs
// Nhiệm vụ của function này là convert number 1000 --> 1.000 đ

export const moneyFormatter = (number, currency = ' ₫') => {
return parseInt(number).toLocaleString('vi-VN') + currency;
};
Bước 2: Import function trên vào file index.txml của product
components/product/index.txml
<import-sjs from="../../utils/common.sjs" name="{moneyFormatter}"></import-sjs>
Bước 2: Sử dụng function
components/product/index.js
{
{
moneyFormatter(product.price);
}
}

📌 Xem souce code tại đây

5. Component product-section

Ở home page chúng ta có 2 loại product-section:

  • Loại vertical có dạng grid 2 cột, scroll xuống để xem thêm sản phẩm

  • Loại horizontal có dạng slide, vuốt sang trái để xem thêm sản phầm

Chúng ta sẽ có một component với các props sau:

  • type sẽ có giá trị là horizontal hoặc vertical để phân xác định dạng hiển thị
  • products là array chứa các sản phẩm
  • onTapProduct là callback sẽ được gọi khi nhấn vào product
components/product-section/index.js
Component({
props: {
type: 'vertical',
products: [],
onTapProduct: () => {}
},

methods: {
_onTapProduct(product) {
this.props.onTapProduct(product);
}
}
});

Ở file .txml, chúng ta sẽ dùng template để tách biệt txml của 2 dạng vertical và horizontal

📌 Xem thêm template

components/product-section/index.txml
<view class="{{className}}">
<template name="vertical">
<view class="product-section-vertical">
<block
tiki:for="{{products}}"
tiki:key="id"
>
<product product="{{item}}" onTapProduct="_onTapProduct"/>
</block>
</view>
</template>

<template name="horizontal">
<view class="product-section-horizontal hide-scroll-bar">
<block
tiki:for="{{products}}"
tiki:key="id"
>
<product product="{{item}}" onTapProduct="_onTapProduct"/>
</block>
</view>
</template>

<template
is="{{type}}"
data="{{isLoading, products}}"
/>
</view>

Ở file .tcss:

  • Với dạng vertical ta sẽ dùng grid và set 2 cột.
  • Với dạng horizontal ta sẽ dùng flex và cho overflow-x auto.
components/product-section/index.tcss
.product-section-vertical {
display: grid;
grid-template-columns: 1fr 1fr;
}

.product-section-horizontal {
display: flex;
overflow-x: auto;
}

📌 Xem souce code tại đây

6. Component category

Tương tự component product, đây là một component đơn giản chúng ta chỉ cần sử dụng một vài class của tini-style như flexboxspacing là có thể hoàn thành cơ bản layout này.

📌 Xem souce code tại đây

7. Component category-section

Component category-section sẽ có layout giống product-section dạng vertical, chỉ khác là category-section sẽ có 4 cột thay vì 2 và component con là category thay vì product.

components/category-section/index.tcss
.product-section-vertical {
display: grid;
grid-template-columns: repeat(4, 1fr);
}

📌 Xem souce code tại đây

category-carousel

Thoạt nhìn qua category-carousel có vẻ giống category-section. Tuy nhiên bản chất category-carousel sẽ gồm một hoặc nhiều category-section dưới dạng carousel.

Chúng ta sẽ có một component với các props và method sau sau:

  • categories là array chứa các category con
  • onTapCategory là callback sẽ được gọi khi nhấn vào category
  • activatedCategory để xác định index của category-section đang được active
  • onCategoryChange sẽ được gọi khi chúng ta vuốt và set lại giá trị activatedCategory
components/category-carousel/index.js
Component({
props: {
categories: [],
onTapCategory: () => {},
activatedCategory: 0
},

methods: {
_onTapCategory(category) {
this.props.onTapCategory(category);
},

onCategoryChange(event) {
this.setData({
activatedCategory: event.detail.current
});
}
}
});
components/category-carousel/index.txml
<carousel
indicator-dots="{{categories.length > 1}}"
onChange="onCategoryChange"
>
<carousel-item tiki:for="{{categories}}">
<category-section
className="category-carousel {{index === activatedCategory ? 'category-carousel-active' : index < activatedCategory ? 'category-carousel-prev' : 'category-carousel-next'}} {{index === categories.length - 1 ? 'category-carousel-last' : ''}}"
categories="{{item}}"
onTapCategory="_onTapCategory"
/>
</carousel-item>
</carousel>

Ứng với mỗi vị trí prev, active, next, last ta sẽ có css khác nhau để tạo ra hiệu ứng như video ở trên.

components/category-carousel/index.tcss
.category-carousel {
transition: ease-in-out 0.3s;
}

.category-carousel-prev {
transform: translateX(-22px);
}

.category-carousel-active,
.category-carousel-next {
transform: translateX(22px);
}

.category-carousel-active.category-carousel-last {
padding: 0 8px;
transform: unset;
}

📌 Xem souce code tại đây

9. Tổng thể

Như vậy chúng ta đã đi qua tất cả các component ở home page, bây giờ ta có thể ghép chúng lại với nhau:

pages/home/index.txml
<view tiki:else>
<banners
class="home-banners"
banners="{{banners}}"
/>

<view class="home-section">
<section-title
title="Hot deals"
/>
<product-section
type="horizontal"
className="home-hot-deals"
products="{{hotDealProducts}}"
onTapProduct="onTapProduct"
/>
</view>

<view class="home-section home-category">
<section-title
title="Category"
onTapActionButton="goToCategory"
/>
<category-carousel
categories="{{categories}}"
onTapCategory="goToCategoryDetail"
/>
</view>

<view class="home-section">
<section-title
title="Featured"
/>
<product-section
products="{{featuredProducts}}"
onTapProduct="onTapProduct"
/>
</view>

<view class="home-section">
<section-title
title="New"
/>
<product-section
products="{{newProducts}}"
onTapProduct="onTapProduct"
/>
</view>
</view>

Khai báo tên page

pages/home/index.json
{
"defaultTitle": "Shop Name",
"usingComponents": {
"banners": "components/banners/index",
"section-title": "components/section-title/index",
"product-section": "components/product-section/index",
"category-carousel": "components/category-carousel/index"
}
}

Thêm icon giỏ hàng

pages/home/index.js
//...
async onReady() {
my.addIconsToNavigationBar({
icons: [
{
image: image,
width: 48,
height: 48,
},
],
padding: 8,
});
},

Thêm tab bar bằng cách khai bảo ở app.json:

app.json
{
"window": {
"defaultTitle": "Tini shop"
},
"pages": ["pages/home/index"],
"tabBar": {
"borderTopActiveColor": "#1A94FF",
"borderTopColor": "#EBEBF0",
"items": [
{
"name": "Home",
"pagePath": "pages/home/index",
"icon": "/assets/icons/tab-home.png",
"activeIcon": "assets/icons/tab-home-active.png"
}
]
}
}

Và chúng ta sẽ có kết quả như hình:

10. Mock data và API

Bạn sẽ thắc mắc là data đâu để render như vậy. Câu trả lời là chúng ta sẽ xây dựng mock data và lấy chúng thông qua API của Github. Vì mình đã publish chúng nên bạn có thể sử dụng chung mock data với mình mà không phải xây dựng lại từ đầu. Ví dụ bạn muốn lấy danh sách sản phẩm mới, bạn có thể gọi tới API sau: https://raw.githubusercontent.com/tikivn/miniapp-getting-started/main/shop/src/services/mock/new-products.json

Ở Tini App, bạn không cần và không sử dụng được các package khác để tạo một request API, thay vào đó Tini App đã cung cấp cho bạn một jsapi là my.request.

📌 Xem thêm my.request

my.request có dạng callback nên để thuận tiện mình sẽ wrap nó lại thành một async function:

services/request.js
export const request = async ({
path,
method = 'GET',
headers = {},
data
}) => {
return new Promise((resolve, reject) => {
my.request({
url: `${BASE_URL}/${path}.json`,
headers: {
'Content-Type': 'application/json',
...headers
},
method,
data,
success: (res) => {
resolve(res);
},
fail: (err) => {
reject(err);
}
});
});
};

Cài đặt các API gọi đến từng end-point tương ứng:

services/index.js
export const getShopInfoAPI = () => {
return request({ path: '/shop' });
};

export const getCategoriesAPI = () => {
return request({ path: '/categories' });
};

export const getFeaturedProductsAPI = () => {
return request({ path: '/featured-products' });
};

export const getNewProductsAPI = () => {
return request({ path: '/new-products' });
};

Và ở page, mình sẽ gọi các API này ở life cycle method onLoad() và dùng setData() để gán các giá trị mới nhận được vào data đồng thời trigger re-render.

📌 Xem thêm Page và setData 📌 Xem thêm Page's life cycle

pages/home/index.js
Page({
data: {
shop: {},
categories: [],
featuredProducts: [],
newProducts: [],
banners: [],
hotDealProducts: []
},

async loadData() {
try {
const [
shop,
categories,
featuredProducts,
newProducts,
banners,
hotDealProducts
] = await Promise.all([
getShopInfoAPI(),
getCategoriesAPI(),
getFeaturedProductsAPI(),
getNewProductsAPI(),
getBannersAPI(),
getHotDealProductsAPI()
]);

this.setData({
shop,
featuredProducts,
newProducts,
banners,
hotDealProducts,
categories: group(categories, 8)
});
} catch {
this.setData({
isLoading: false
});
}
},

async onReady() {
this.loadData();
}
});

📌 Xem souce code tại đây


🔚 C. Tạm kết

Chúng ta đã đi qua các phần:

  • Shopping Template là gì
  • Chức năng và cấu trúc dự án
  • Cách khởi tạo project Tini App
  • Xây dựng Home page:
    • Phân tích và break layout
    • Xây dựng các component
    • Data và gọi API

Rất cảm ơn bạn và cũng xin chúc mừng bạn đã cùng mình tìm hiểu xây dựng từ đầu một ứng dụng Tini App thực sự. Vì bài viết đã dài nên mình xin kết thúc phần 1 ở đây. Xin hẹn gặp lại các bạn ở phần 2 và cùng nhau xây dựng những pages tiếp theo Chúc các bạn có thời gian bổ ích và vui vẻ với Tini App. 🎉