Skip to main content

· 6 min read

Mở đầu

Tini App Platform đã cung cấp cho chúng ta khoảng 200 JS APIs, tuy nhiên trong quá trình phát triển, trong một số trường hợp chúng ta vẫn muốn tạo ra thêm các JS API mới, hoặc chúng ta muốn sử dụng lại một phần JS API từ các ứng dụng đã có.

Ví dụ, bạn viết ra một Tini App A, ứng dụng này đã có chức năng cho phép người dụng upload ảnh, rồi xử lý tấm ảnh này bằng các filter. Sau đó bạn viết thêm một Tini App B, Tini App B muốn sử dụng lại các tấm ảnh đã qua filter. Thay vì phải code lại toàn bộ chức năng này trên Tini App B, Tini App Framework cho phép chúng ta sửa ứng dụng A để cung cấp API cho ứng dụng B sử dụng.

Hãy cũng tìm hiểu cách làm nhé :)

Tổng quan các bước

image

Điểm cơ bản để ứng dụng B có thể sử dụng một API được cung cấp từ ứng dựng A là ở 2 chỗ

  • Ở ứng dụng B, khi muốn sử dụng API từ ứng dụng A, ứng dụng B cần sử dụng JS API my.navigateToMiniApp với tham số callback. Tham số callback này sẽ nhận được kết quả trả về từ ứng dụng A
  • Ở ứng dụng A, khi muốn trả về dữ liệu cho ứng dụng B, ứng dụng A sẽ gọi tới JS API my.navigateBackMiniApp và truyền về kết quả thông qua tham số extraData.

Ví dụ về việc tạo ra một API mới

Chúng ta hãy cùng xây dựng một API đơn giản. API này có chức năng lấy về IP address hiện tại của thiết bị sau khi users bấm vào một nút bất kỳ

image

Chúng ta sẽ tạo ra một ứng dụng với id là vn.tiki.integration.example

Ứng dung chỉ có duy nhất một page pages/index/index. Page này được tổ chức như sau

async function sleep(second) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, second * 1000);
});
}

async function getIp() {
return new Promise((resolve, reject) => {
my.request({
url: 'https://api.ipify.org?format=json',
method: 'GET',
headers: {
'content-type': 'application/json'
},
dataType: 'json',
success: function (res) {
resolve(res.ip);
},
fail: function (res) {
reject(res);
}
});
});
}

Page({
data: {},
async onSuccess() {
this.setData({
message: 'fetching data'
});
try {
const [_, ip] = await Promise.all([sleep(1), getIp()]);
this.setData({
message: `ip: ${ip}`
});
my.navigateBackMiniApp({
extraData: {
status: 'success',
ip
}
});
} catch (e) {
this.setData({
message: `error: ${e.errorMessage}`
});
my.navigateBackMiniApp({
extraData: {
status: 'failure',
error: e.errorMessage
}
});
}
}
});

Trên page này có một button với title là Get IP Address, khi users bấm vào button này, ứng dụng sẽ gọi một request tới API server https://api.ipify.org?format=json và nhận về IP address hiện tại.

async function getIp() {
return new Promise((resolve, reject) => {
my.request({
url: 'https://api.ipify.org?format=json',
method: 'GET',
headers: {
'content-type': 'application/json'
},
dataType: 'json',
success: function (res) {
resolve(res.ip);
},
fail: function (res) {
reject(res);
}
});
});
}

Để mọi người có thể nhìn thấy rõ hơn quá trình này, tôi cố tình tạo thêm một delay 1s trong quá trình gọi API

async function sleep(second) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(null);
}, second * 1000);
});
}

Việc xử lý sự kiện click sẽ được thực hiện như sau

async onSuccess() {
this.setData({
message: 'fetching data'
});
try {
const [_, ip] = await Promise.all([sleep(1), getIp()]);
this.setData({
message: `ip: ${ip}`
});
my.navigateBackMiniApp({
extraData: {
status: 'success',
ip
}
});
} catch (e) {
this.setData({
message: `error: ${e.errorMessage}`
});
my.navigateBackMiniApp({
extraData: {
status: 'failure',
error: e.errorMessage
}
});
}
}

Sau khi nhận được IP từ server, chúng ta sẽ gọi tới hàm my.navigateBackMiniApp và truyền vào extraData là một object

{
status: 'success', ip;
}

Trong trường hợp thất bại, chúng ta trả về extraData với cấu trúc

{
status: 'failure',
error: <message>
}

Kiểm thử integration mà không cần publish app

Để kiểm thử một integration mà không publish app, bạn có thể sử dụng chức năng Kiểm thử nhanh ứng dụng

Sau khi mở bằng mã QR code, nếu integration của bạn có trả về kết quả, kết quả đó sẽ được hiển thị trên màn hình

image

Sử dụng JS API

Để sử dụng JS API vừa kể trên, chúng ta tạo ra một ứng dụng với id là vn.tiki.integration.usageex.

Ứng dụng chỉ có duy nhất một page pages/page/index với nội dung sau

async function getIp() {
return new Promise((resolve) => {
my.navigateToMiniApp({
appId: 'vn.tiki.integration.example',
path: 'pages/index/index',
callback(data) {
resolve(data);
}
});
});
}

Page({
data: {},
async onGetIp() {
this.setData({
message: 'fetching data'
});
const data = await getIp();
this.setData({
message: `data: ${JSON.stringify(data)}`
});
}
});

Ở đây, thay vì gọi trực tiếp my.navigateToMiniApp, chúng ta sẽ wrap API này với một API promise

async function getIp() {
return new Promise((resolve) => {
my.navigateToMiniApp({
appId: 'vn.tiki.integration.example',
path: 'pages/index/index',
callback(data) {
resolve(data);
}
});
});
}

Sau đó, việc sử dụng Promise với async sẽ đơn giản hơn nhiều

async onGetIp() {
this.setData({
message: 'fetching data'
});
const data = await getIp();
this.setData({
message: `data: ${JSON.stringify(data)}`
});
}

Kết luận

Thông qua bài viết này, tự bản thân chúng ta, các developers có thể cung cấp thêm các API để mở rộng khả năng của hệ sinh thái Tini App.

Vậy còn chờ gì nữa, hãy cùng nhau khám phá và tạo thêm nhiều API hữu ích nhé các bạn.

Một số ví dụ về các API mà tôi có thể nghĩ tới

  • Tạo API để thay đổi profile của người dùng Tiki
  • Tạo API để trading trên Tiki Exchange
  • Tạo API để gia hạn thời gian gửi heo vàng ...

Hy vọng trong tương lai, chúng ta có thể thấy nhiều API được mở ra trên nền tảng của Tini App hơn :)

· 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 🎉