Nhảy tới nội dung

· Một phút để đọc

Đại dương xanh và đại dương đỏ là 2 thuật ngữ được giới thiệu trong tác phẩm “Chiến lược Đại dương xanh” của W.Chan Kim và Renee Mauborgne. Nếu ví von thị trường là đại dương còn cá là các doanh nghiệp, thì sắc đỏ chính là máu của lũ cá mập cắn xé lẫn nhau để giành thức ăn; sắc xanh tức là không có đổ máu.

Đại dương đỏ là ẩn dụ cho thị trường truyền thống đầy rẫy các “tay chơi” và sự cạnh tranh vô cùng ác liệt. Đại dương xanh ám chỉ khoảng trống thị trường có ít người khai thác và mức độ cạnh tranh không đáng kể.

Điểm qua trong làng di động, tính đến thời điểm quý một năm 2021, Google Play Store đã có đến 3,48 triệu ứng dụng trong khi Apple Store có đến 2,22 triệu ứng dụng. Nhìn qua hai con số trên ta đủ biết đại dương của native app đã đỏ ngòm rồi. Thay vì đưa bơi vào đại dương đỏ để phải đương đầu với lũ cá mập hung tợn, tại sao bạn tìm đến đại dương xanh mặc sức vẫy vùng?

Kim và Mauborgne cho rằng đại dương xanh không hiển hiện sẵn, mà cần phải khai phá hoặc tạo ra; việc khai thác luôn tiềm ẩn nhiều rủi ro, đòi hỏi tầm nhìn và chiến lược dài hơi của doanh nghiệp. Tuy nhiên, xét trong bối cảnh ngành công nghiệp di động thì điều này không hẳn đúng. Các đại dương xanh mini app đang mở cửa chào đón các nhà phát triển ứng dụng.

Ở nước ta, thị trường mini app ở trong giai đoạn bình minh. Số lượng mini app trong một nền tảng chỉ bằng con số lẻ so với số lượng ứng dụng trong Google Play Store hay Apple Store. Đây là thời cơ vàng cho các ứng dụng tìm được chỗ đứng.

Hơn thế nữa, đường đến đại dương xanh của nhà phát triển mini app đã được dọn sẵn phần nào. Đơn cử là: Mini app của họ đã có sẵn một lượng khổng lồ khách hàng tiềm năng - lượng người dùng super app. Nhiều tính năng đã được super-app tạo sẵn, mini app chỉ việc dùng mà thôi./ Thời gian chi phí ngắn và chi phí thấp.

Ắt hẳn nhiều bạn sẽ băn khoăn: số lượng mini app của một nền tảng sẽ tăng theo thời gian; ngày nào đó đại dương này sẽ từ xanh hoá đỏ. Đó là một điều thực tế khó tránh khỏi. Có điều, nếu bạn tham gia mini app vào buổi đầu, bạn sẽ khai thác được vô số lợi thế. Khi đại dương đỏ hoá, bạn đã là cá mập. Trâu chậm uống nước đục mà bạn!

Bạn có muốn đồng hành cùng Tiki mini app trong đại dương xanh không?

· Một phút để đọc

Show off status badge -.-

Introduction

Mục đích của bài viết này là đưa ra một ví dụ đơn giản mà tôi đã sử dụng để viết unit test cho Tiki Tini App. Hi vọng bài viết giúp các bạn phần nào trong việc tìm tài liệu tham khảo, chọn lựa một đáp án phù hợp với quá trình kiểm thử và phát triển ứng dụng của các bạn.

Bài viết mang tính chia sẻ, hi vọng các bạn có thể đóng góp ý kiến của mình ở github repo issues.

Dưới đây là phần code của custom component mà chúng ta sẽ cùng test:

// tiki-tiniapp-with-unit-tests/app/src/components/the-button/index.js

import QRCode from "qrcode";
import Utils from "qrcode/lib/renderer/utils";

const createComponent = async () => {
const componentConfig = ({
data: {
link:"https://developers.tiki.vn",
size: 0,
},
onInit() {
this.generateQR = this.generateQR.bind(this)
},
methods: {
async tapButton() {
return await this.generateQR(this.data.link)
},
generateQR(url) {
return new Promise((resolve,reject) => {
try {
const canvas = my.createCanvasContext("qr-code");
const opts = Utils.getOptions({});
const qrData = QRCode.create(url);
const size = Utils.getImageWidth(qrData.modules.size, opts);
this.setData({ size, qrData });
canvas.getImageData({
width: size,
height: size,
success:(image)=> {
try {
Utils.qrToImageData(image.data, qrData, opts);
canvas.putImageData(image,0,0);
this.setData({ size, image: image.data });
resolve()
} catch (error) {
reject(error)
}
},


});
} catch (error) {
console.error("generateQR", error)
// I dont want to handle error here, just want to show the log
throw error;
}
})
}
}
});
Component(componentConfig);
return componentConfig
}
if (!my.TEST ) {
createComponent();
}
export default createComponent;


Table of contents


About unit test in Tiki Tini App

Unit testing là phương pháp kiểm thử trên từng đơn vị của source code (class, function,..) và được coi là phương pháp cơ bản nhất mà một developer cần biết. Vì vậy sự có mặt của unit test trong dự án phần mềm của bạn thường là một điều hiển nhiên và khá quan trọng.

Đối với các Tini App, hiện tại phía Tiki chưa cung cấp cụ thể một lựa chọn nào để tích hợp Unit testing vào quá trình phát triển Tini app. Bạn cần tự setup phương án sử dụng unit test phù hợp với ứng dụng của bạn.


Environment setup

// jest.config.js file
module.exports = () => {
return {
verbose: true,
transformIgnorePatterns: ["node_modules/(?!@ngrx|(?!deck.gl)|ng-dynamic)"],
automock: false,
setupFiles: ["./mocking/setupJestMock.js"],
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.ts?$",
collectCoverageFrom: ["app/**/*.{js,ts}"],
};
};

Source code structure

Cấu trúc dự án được sắp đặt đơn giản như sau:

├── __tests__
│ ├── component.the-button.test.ts
│ └── page.home.test.ts
├── app # Tini App source code
│ ├── package.json
# các dependencies phục vụ cho product development
│ ├── src
│ │ ├── app.js
│ │ ├── app.json
│ │ ├── app.tcss
│ │ ├── components
│ │ │ └── the-button
│ │ │ ├── index.js
│ │ │ ├── index.json
│ │ │ ├── index.tcss
│ │ │ └── index.txml
│ │ ├── home
│ │ │ └── index.js
│ │ │ ├── index.json
│ │ │ ├── index.tcss
│ │ │ └── index.txml
│ └── yarn.lock
├── babel.config.js
├── jest.config.js
├── mocking
│ └── setupJestMock.js
├── package.json
# các dependencies phục vụ cho local development
└── yarn.lock

Setup test dependencies

Trong bài chia sẻ này tôi sẽ dùng jest và babel. Các bạn có thể tham khảo dev dependencies dưới đây mà tôi sử dụng:

// dev dependencies in ./packages.json
{
"@babel/core": "^7.15.8",
"@babel/preset-env": "^7.15.8",
"@babel/preset-typescript": "^7.15.0",
"@types/jest": "^27.0.2",
"babel-jest": "^27.2.5",
"husky": "^7.0.2",
"jest": "^27.2.5",
"jest-canvas-mock": "^2.3.1",
"typescript": "^4.4.4"
}


Example tests

Mocking

Ý tưởng ở đây là khi mock các entities, chúng ta sẽ intercept global function Component hay Page để lấy các config, init data, methods và lifecycle methods sau đó map lại references(binding for the this context of methods) và trigger các unit mà ta cần test. Thông qua việc này ta có thể lấy được outputs cần test.

Các global JSAPI đơn giản là api của Tini App cung cấp nên chúng ta không cần test lại chúng, hãy giả định outputs của chúng luôn đúng và chỉ tập trung phần code của bạn.

Với usecase của tôi ở ví dụ này, tôi cần mock các api liên quan đến canvas nên tôi sẽ sử dụng canvas từ dom hỗ trợ bởi package jest-canvas-mock để thực hiện nghiệm vụ này. Phần code dưới đây khá đơn giản nên tôi không muốn diễn giải lại. Và good code documents itself:

Good code documents itself

Previews code:

import 'jest-canvas-mock';
import createComponent from "../app/src/components/the-button";

// typings
type AsyncReturnType<T extends (...args: any) => any> =
T extends (...args: any) => Promise<infer U> ? U :
T extends (...args: any) => infer U ? U :
any
type PureConfig = AsyncReturnType<typeof createComponent>;
type ComponentMethodsType = Pick<PureConfig, 'methods'>
type ComponentType = Omit<PureConfig, 'methods'> & ComponentMethodsType["methods"]
let Instance: ComponentType = {} as unknown as ComponentType;


// mocking
const componentTargetMocker = {
async handler(ConfigObj) {
Instance = this;
// intercept setData to get the render's data
Instance.data = ConfigObj.data;
Instance.setData = async (data, cb) => new Promise((resolve) => {
Object.assign(Instance.data, data)
resolve(Instance.data);
cb(Instance.data)
if (ConfigObj.didUpdate) ConfigObj.didUpdate();
});
// mocking lifecycles and doing reference binding
Instance.onInit = ConfigObj.onInit.bind(Instance);
if (ConfigObj.didMount) Instance.didMount = ConfigObj.didMount.bind(Instance);
if (ConfigObj.didUpdate) Instance.didUpdate = ConfigObj.didUpdate.bind(Instance);
if (ConfigObj.methods) Object.entries(ConfigObj.methods).forEach(([key, method]) => {
if (typeof method === 'function') {
ConfigObj.methods[key] = ConfigObj.methods[key].bind(Instance);
Instance[key] = method;
}
})
// trigger life cycle
await Instance.onInit();
await Instance?.didMount?.();
}
}
componentTargetMocker.handler = componentTargetMocker.handler.bind({});
const mockComponentCreator = jest.fn(componentTargetMocker.handler);


globalThis.my.createCanvasContext = (id) => {
const canvas = document.createElement('CANVAS') as HTMLCanvasElement;
canvas.id = id;
const ctx = canvas.getContext('2d');
const _getImgData = ctx.getImageData;
ctx.getImageData = (opt: any) => {
return opt.success(_getImgData(0, 0, opt.width, opt.height))
}
return ctx
}
globalThis.Component = mockComponentCreator;


Tests

Dựa vào phần mocking ở phía trên, chúng ta đã có thể thêm và pass các bài test cơ bản hay gặp mà không gặp quá nhiều khó khăn:

passed tests

// stupid tests

describe("Myapp: common stupid test cases:", () => {

beforeAll(() => {
createComponent();
})

test("Component() has been called", (done) => {
expect(mockComponentCreator).toHaveBeenCalledTimes(1)
done();
});
test("tab generate QR with expected size", (done) => {
Instance.generateQR("https://developers.tiki.diferent-here");
expect((Instance.data.size)).toEqual(148)
done();
});

test("component data matches snapshot", (done) => {
Instance.tapButton()
expect((JSON.stringify(Instance.data, null, 2))).toMatchSnapshot()
done();
});
});


Source code

Các bạn có thể xem source code từ bài viết này tại github.com/cute-me-on-repos/tiki-tiniapp-with-unit-tests


Conclusion

Tuân thủ nguyên tắc của unit test là test theo đơn vị, các phần còn thiếu thì chúng ta có thể sử dụng mocks. Cá nhân tôi thấy nhiều bạn thường muốn test quá nhiều thứ trong unit test và đôi khi nhầm lẫn với integration testing. Việc có nên tách biệt hai khái niệm này hay không không quá quan trọng, và việc quyết định mức độ test coverage bao nhiêu là đến từ developer với những test cases được chính developer đó định đoạt chứ không phải hoàn toàn dựa vào những thứ như jest --coverage. Phía reactjs cũng có đọan ghi chú sau vào ngay trang chủ của họ về tài liệu kiểm thử:

With components, the distinction between a “unit” and “integration” test can be blurry. If you’re testing a form, should its test also test the buttons inside of it? Or should a button component have its own test suite? Should refactoring a button ever break the form test? ( * ⓘ - reactjs/docs/testing)

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


thien.ly from the cute me on repo team

· Một phút để đọc

Bạn thử tưởng tượng dồn hết tâm huyết xây dựng một ứng dụng cực chất, song lại phát hành trên nền tảng ứng dụng ít phổ biến. Thế cơ may nào để ứng dụng này thành công không? Phải nói rằng nền tảng là điều kiện cần để một ứng dụng thành công.

Thị trường mini app tại Việt Nam đang bước vào giai đoạn bùng nổ với khá nhiều nền tảng mini app. Tuy vậy, hầu như chưa có những bài đánh giá hay nhận xét chi tiết về các nền tảng mini app Việt. Đứng trước nhiều lựa chọn nhưng thiếu thông tin, các nhà phát triển ắt sẽ bối rối không biết phải xây dựng ứng dụng của mình trên nền tảng mini app.

Bài viết này sẽ mách nước cho các nhà phát triển cách lựa chọn nền tảng mini app.

  • Đầu tiên, bạn nên quan sát đến lượng người dùng của super app, lượng truy cập vào super app mỗi ngày, … Đây chính nguồn người dùng tiềm năng đối với mini app của bạn. Khi người dùng vào super app, một phần trong số họ sẽ xài thử các tiện ích đi cùng (tức mini app). Đương nhiên lượng khách hàng tiềm năng càng khổng lồ thì cơ hội cho phổ biến cho mini ap của bạn càng lớn.

  • Thứ nhì, hãy để mắt bộ framework để xây dựng mini app. Mỗi nền tảng mini app là sân chơi riêng của nhà cung cấp nền tảng; phát hành bao nhiêu API, bao nhiêu component, dùng ngôn ngữ lập trình nào cú pháp ra sao, … là quyền của họ. Gặp phải ngôn ngữ trong framework khó xài, API, component thiếu trước hụt sau, bạn sẽ tiêu tốn nhiều nguồn lực, thời gian và tiền bạc để tạo những mini app, thậm chí phải loại bỏ một số tính năng khỏi ứng dụng của mình.

    Framework dù khó dù dễ, thủ tục xây dựng mini app dù phức tạp hay đơn giản đều cần tài liệu hướng dẫn. Bạn có thể tìm thấy vô vàn tài liệu, diễn đàn về Kotlin và Python dành cho ứng dụng Android, hay Swift và Objective C dành cho ứng dụng iOS, đơn giản là vì chúng quá đổi phổ biến. Trái lại, các nền tảng mini app tại Việt Nam còn khá mới mẻ; hầu như chưa có các tài liệu do tác giả bên ngoài biên soạn, các diễn đàn hay cộng đồng bên ngoài chia sẻ tri thức, kinh nghiệm.

  • Kế đến, bạn cần quan đến sự minh bạch trong chính sách của nền tảng. Nền tảng có bộ nguyên tắc vận hành, quy tắc xét duyệt rõ ràng không? Tự dưng ngày đẹp trời nào đó, ứng dụng của bạn bị gỡ khỏi kho ứng dụng của nền tảng mà chẳng có lấy một lời giải thích thoả đáng.

  • Cuối cùng là trải nghiệm lập trình viên/tester và trải nghiệm người dùng.

    • Nếu như nền tảng sở hữu code editor riêng, nó sẽ mang đến sự tiện lợi cho lập trình viên cũng như tester. Code editor riêng sẽ highlight cú pháp, keywords của framework cũng như gợi ý (autocomplete) hàm, component, API và cú pháp với độ chính xác cao. Bởi lẽ code editor này được tối ưu hoá để chỉ viết code cho một bộ framework. Trái lại, việc xài các code editor đa dụng sẵn có có thể khiến highlight và autocomplete hoạt động thiếu chính xác. Kết quả giảm hiệu suất làm việc của các lập trình viên.
    • Đã có code editor riêng mà còn tích hợp trình giả lập là một điểm cộng lớn. Lập trình viên hay tester có thể kiểm thử các tính năng nhanh chóng ngay trên máy tính mà không cần tốn công đem ứng dụng lên lên thiết bị di động.
    • Code editor riêng và trình giả lập là 2 trải nghiệm về phía đội ngũ phát triển, còn trải nghiệm phía người dùng thì thế nào? Người dùng chủ yếu tương tác với phần mềm thông qua giao diện. Bạn hãy thử hình dung nút Close các mini app khác nhau cùng một nền tảng mini app nằm được bố trí ở vị trí khác nhau: đối với mini app này thì nút Close nằm bên phải tab bar, đối với mini app kia thì nút Close nằm trái navigation bar, đối với mini app nọ thì nút Close nằm trong app menu. Kiểu này há chẳng phải gây khó khăn cho người dùng. Nếu có nền tảng guideline về thiết kế, sẽ mang giao diện đồng nhất cho các mini app, tăng trải nghiệm người dùng.

Theo bạn đánh giá, Tiki mini app đáp ứng được bao nhiêu tiêu chí vừa nêu??

· Một phút để đọc

1. Bài toán của Tini App

Khi page đầu tiên của Tini App được mở lên, Tini App Core sẽ khởi động một WebView.

WebView này sẽ đóng vai trò là Render của Tini App, và có nhiệm vụ load 2 file Javascripts lên

  • file render framework chứa các code của framework để khởi tạo các kết nối
  • file render app chứa code mà developers viết cho ứng dụng của họ.

Hai file này sẽ được load lần lượt theo thứ tự ở trên, file framework được load trước, sau đó tới file app.

Sau khi 2 file này được load xong, Render sẽ khởi tạo ra một Web Worker. Tương tự như Render, Worker cũng cần phải load 2 files

  • file worker framework chứa các code của framework để khởi tạo kết nối từ Worker tới Render, và từ Worker tới Core
  • file worker app chứa toàn bộ logic để điểu khiển app - file này được sinh ra từ code của developer

Với các yêu cầu như trên, chúng ta nên sắp xếp để load các file Javascript như nào để cho tốc độ load file là tốt nhất?

2. Chuẩn bị môi trường

Để giả lập được thời gian load các file JS, chúng ta sẽ tạo ra một file server.js có nội dung như sau

const express = require('express');
const app = express();
const port = 3000;

app.use(function (req, res, next) {
const timeout = req.query.timeout || 0;
setTimeout(next, timeout);
});

app.get('/index.js', (req, res) => {
const env = req.query.env || 'web';
const timeout = req.query.timeout || 0;
res.header('Content-type', 'text/javascript');
res.header('cache-control', 'max-age=604800');
res.send(
`console.log("hello world ${timeout} from ${env} at", Date.now() - self.START_TIME);`
);
});

app.listen(port, () => {
console.log(
`Example app listening at http://localhost:${port}`
);
});

Trong đoạn code kể trên, chúng ta tạo ra một server chạy ở cổng 3000.

Server này cho phép chúng ta query vào file index.js. File index.js nhận vào 2 tham số

  • timeout là thời gian giả lập để server có thể trả về nội dung của file, timeout được tính bằng giây. VD: timeout=1, tức là server sẽ mất 1s để trả về nội dung file
  • env là môi trường mà script này sẽ được chạy, mặc định là web. Tuỳ vào giá trị của env mà file index.js sẽ được trả về với nội dung như sau
console.log(
'hello world ${timeout} from ${env}',
Date.now() - self.START_TIME
);

Ví dụ: khi request vào http:/localhost:3000/index.js?timeout=1&env=web, server sẽ trả về nội dung

console.log(
'hello world 1 from web',
Date.now() - self.START_TIME
);

Ở đây, chúng ta giả thiết rằng các script trước đó đã khởi tạo sẵn một biến global tên là START_TIME. Vì vậy, khi script trên được chạy, script có thể cho chúng ta biết, thời gian mà script được browser evaluate

3. Load Javascript trên Main Thread

Để giả lập việc load file trên Main Thread, chúng ta sẽ tạo ra file index.html như sau

<head>
<script>
var START_TIME = Date.now();
</script>
</head>
<body>
<div>hello</div>
<script src="http://localhost:3000/index.js?timeout=2"></script>
<script src="http://localhost:3000/index.js?timeout=1"></script>
</body>

Trong thẻ head, chúng ta chạy một đoạn script để khởi tạo biến START_TIME. Sau đó, chúng ta đặt các thẻ script ở cuối thẻ body, mục đích là để browser khi load các thẻ script này sẽ không block nội dung của body trả về.

Ở đây, chúng ta load 2 scripts, script đầu tiên tốn 2s để load, còn script tiếp theo chỉ tốn duy nhất 1s.

Câu hỏi là, thời gian để browser download 2 scripts trên sẽ là bao nhiêu? Là 2s hay là 3s?

Chúc mừng bạn, nếu câu trả lời của bạn là 2s. Bạn đã rất hiểu cách mà browser load các script đó. Trong giai đoạn parse HTML, browser sẽ phát hiện ra trang web này cần phải load 2 scripts. Và browser sẽ thực hiện download 2 scripts này song song với nhau. Sau khi download xong, browser mới thực hiện evaluate các scripts theo thứ tự mà chúng được sắp xếp. Trong trường hợp này, mặc dù script với timeout=1 dù được download xong trước script với timeout=2, tuy nhiên, do script với timeout=2 được sắp xếp trước, nên code của script với timeout=2 vẫn được chạy trước.

Browser sẽ in ra console nội dung như sau

hello world 2 from web at 2005
hello world 1 from web at 2006

Chú ý: để kiểm tra đúng behaviour của browser, chúng ta cần đảm bảo là load file index.html trên môi trường incognito chưa có cache các requests, vì sau khi các requests đã được cache lại rồi, thời gian download các requests sẽ không còn đúng nữa.

4. Load Javascript trên Worker

Trên Main Thread, browser sẽ load các scripts song song với, vậy khi load các scripts tron Web Worker thì sao?

Để load scripts trên Web Worker, chúng ta có 2 tình huống:

  • khởi tạo script cho một Web Worker. Đây là script dùng để khởi tạo Web Worker, chúng ta sẽ load các script này thông qua việc sử dụng hàm new Worker(url, options)
  • sau khi Web Worker được tạo ra, Web Worker có thể có nhu cầu muốn load thêm các scripts khác

2 tình huống này sẽ có những cách hành xử khác nhau.

4.1. Load scripts để khởi tạo Web Worker

Để khởi tạo một Web Worker, chúng ta sẽ sử dụng hàm

new Worker(url, options);

Để kiểm tra xem browser sẽ thực hiện việc load script url của Web Worker như nào, chúng ta chuẩn bị đoạn code như sau

<head>
<script>
var START_TIME = Date.now();
</script>
</head>
<body>
<div>hello</div>
<script src="http://localhost:3000/index.js?timeout=2000"></script>
<script src="http://localhost:3000/index.js?timeout=1000"></script>
<script>
console.log(
'main thread start worker at',
Date.now() - START_TIME
);
const worker = new Worker(`/worker.js?timeout=1000`);
worker.postMessage({
type: 'init',
startTime: START_TIME
});
</script>
</body>

Ở đây, chúng ta giả lập load một file worker.js ở cuối cùng, sau khi đã load được các file js ở trên Main Thread. File worker này tốn 1s để download. Sau khi Web Worker được khởi tạo, Main Thread sẽ gửi một message tới Worker để gửi thời gian mà Main Thread start. Trên Worker, chúng ta sẽ kiểm tra thời gian mà Web Worker bắt đầu được chạy, từ thời gian này chúng ta có thể biết được browser thực hiện việc download Worker scripts ra sao

File worker.js có nội dung như sau

self.onmessage = (e) => {
if (e.data.type === 'init') {
self.START_TIME = e.data.startTime;
console.log(
'worker receive init message at',
Date.now() - self.START_TIME
);
}
};

Nếu nhìn vào console của browser chúng ta sẽ thấy nội dung như sau

hello world 2000 from web at 2005
hello world 1000 from web at 2005
main thread start worker at 2006
worker receive init message at 3006

Điều này có nghĩa là: tại thời điểm browser nhận được chỉ thị new Worker('/worker.js?timeout=1000'), browser mới bắt đầu thực hiện việc download script worker.js?timeout=1000.

Do vậy browser sẽ phải mất 3s để download tất cả 3 scripts (bao gồm 2 scripts index.js?timeout=1, index.js?timeout=2worker.js?timeout=1).

Vậy có cách nào để chúng ta báo cho browser download file worker.js trước khi chúng ta khởi tạo Web Worker không?

Ở đây chúng ta có thể nghĩ tới 2 giải pháp, sử dụng thẻ <link rel='preload'> để báo cho browser rằng chúng ta cần load (download + parse) file worker.js trước, hoặc sử dụng thẻ <link rel='prefetch'> để báo browser rằng chúng ta cần download file worker.js trước.

Do preload không có hỗ trợ load file type cho file khởi tạo worker, nên chúng ta sử dụng thẻ prefetch.

Chúng ta sửa lại đoạn code html như sau

<head>
<link
rel="prefetch"
href="http://localhost:3000/worker.js?timeout=1000"
/>
<script>
var START_TIME = Date.now();
</script>
</head>
<body>
<div>hello</div>
<script src="http://localhost:3000/index.js?timeout=2000"></script>
<script src="http://localhost:3000/index.js?timeout=1000"></script>
<script>
console.log(
'main thread start worker at',
Date.now() - START_TIME
);
const worker = new Worker(`worker.js?timeout=1000`);
worker.postMessage({
type: 'init',
startTime: START_TIME
});
</script>
</body>

Sau khi chạy xong, chúng ta có thể thấy console có nội dung như sau

hello world 2000 from web at 2005
hello world 1000 from web at 2005
main thread start worker at 2006
worker receive init message at 2007

Tuy nhiên, đáng buồn là khi sử dụng prefetchpreload để load worker, thì cách làm này chỉ work với Chrome, còn không works với iOS. Trên iOS tôi cũng chưa biết có cách làm nào tốt hơn để load các file worker không. Nếu các bạn biết vui lòng chia sẻ cho tôi với nhé.

Một điểm rất hay của Chrome khi sử dụng prefetch đó là Chrome đủ thông minh để tận dụng lại các request. Ví dụ, nếu thời gian để download worker.js thay vì chỉ tốn 1s, mà thành tốn 4s, thì tại thời điểm browser nhận được chỉ thị new Worker('http://localhost:3000/worker.js?timeout=4'), request prefetch vẫn chưa thực hiên xong. Dù vậy, Chrome sẽ không tạo mới request nữa, mà chờ cho request prefetch thực hiện xong rồi sử dụng kết quả của request này để chạy tiếp.

Do vậy, bằng cách sử dụng prefetch, chúng ta có thể đảm bảo các JS file được download song song với nhau trên Main Thread (tuy nhiên Browser vẫn giới hạn số lượng các requests có thể download song song cùng nhau, mặc định Chrome chỉ hỗ trợ 6 concurrent requests per domain).

Do vậy, nhìn chung để Web Worker có thể bắt đầu nhanh nhất có thể trên cả Android và iOS, cách làm tốt nhất vẫn nên khởi tạo Web Worker càng sớm càng tốt

4.2. Load scripts trong Web Worker

Trong Web Worker, để có thể import được một script, chúng ta sử dụng hàm importScripts.

Ví dụ, với file worker.js ở trên, chúng ta muốn load 2 files index.js?timeout=300index.js?timeout=400, chúng ta sẽ làm như sau

// worker.js
self.onmessage = (e) => {
if (e.data.type === 'init') {
self.START_TIME = e.data.startTime;
console.log(
'worker receive init message at',
Date.now() - self.START_TIME
);
importScripts(
'http://localhost:3000/index.js?timeout=400&env=worker'
);
importScripts(
'http://localhost:3000/index.js?timeout=300&env=worker'
);
}
};

Ở đây chúng ta thực hiện việc load scripts sau khi nhận được message từ Main Thead.

Chú ý rằng hàm importScripts là hàm đồng bộ, tức là khi hàm này chạy, browser phải chờ cho scripts được download xong thì mới tiếp tục chạy các lệnh tiếp theo. Vì vậy với đoạn code ở trên, chúng ta sẽ phải mất 700ms để download hết các scripts.

Đoạn code sẽ in ra màn hình nội dung như sau

hello world 2000 from web at 2011
hello world 1000 from web at 2011
main thread start worker at 2011
worker receive init message at 2020
hello world 400 from worker at 2423
hello world 300 from worker at 2726

Để giảm thời gian download, chúng ta có thể tiếp tục sử dụng prefetch

<head>
<link
rel="prefetch"
href="http://localhost:3000/worker.js?timeout=1000"
/>
<link
rel="prefetch"
href="http://localhost:3000/index.js?timeout=300&env=worker"
/>
<link
rel="prefetch"
href="http://localhost:3000/index.js?timeout=400&env=worker"
/>
<script>
var START_TIME = Date.now();
</script>
</head>
<body>
<div>hello</div>
<script src="http://localhost:3000/index.js?timeout=2000"></script>
<script src="http://localhost:3000/index.js?timeout=1000"></script>
<script>
console.log(
'main thread start worker at',
Date.now() - START_TIME
);
const worker = new Worker(
'http://localhost:3000/worker.js?timeout=1000'
);
worker.postMessage({
type: 'init',
startTime: START_TIME
});
</script>
</body>

Sau khi sử dụng prefetch, console sẽ có nội dung như sau

hello world 2000 from web at 2006
hello world 1000 from web at 2007
main thread start worker at 2007
worker receive init message at 2019
hello world 400 from worker at 2021
hello world 300 from worker at 2023

Tuy nhiên, cũng giống như ở phần 4.1, prefetch không có tác dụng với iOS. Vì thế cách tốt hơn để import script trong Worker đó là chúng ta tự viết một hàm import riêng

function getScriptContent(url) {
return new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function (e) {
resolve(e.currentTarget.responseText);
};
xhr.send();
});
}

function fasterImportScripts(urls) {
return Promise.all(urls.map(getScriptContent)).then(
(contents) => {
contents.forEach((content) => {
eval(content);
});
}
);
}

Ở đây, chúng ta thực hiện download các script song song với nhau bằng cách khởi tạo các XHR request riêng biệt. Sau khi download các script xong, chúng ta mới evaluation các scripts này. Bằng cách này, chúng ta có thể đảm bảo rằng trong một Web Worker, các scripts có thể có thời gian download nhanh nhất có thể.

Kết luận

Thông qua bài viết này, chúng ta đã xem xét một số phương án để làm sao có thể load các file Javascript trong Main Thread và trong Web Worker nhanh nhất có thể.

Chúng ta cũng đã bàn tới một số phương án để có thể by-pass một số hạn chế của Safari trên iOS.

Nhìn chung, khi website có sử dụng Web Worker chúng ta có thể để ý tới các tips:

  • Sử dụng preload để load các scripts trên Main Thread
  • Sử dụng prefetch để load các scripts trên Web Worker
  • Khởi tạo Web Worker càng sớm càng tốt, nếu được nên khởi tạo Web Worker ngay lập tức nếu như thời gian downlod Worker scripts là lâu
  • Khi load các scripts trong Web Worker, có thể sử dụng XHR để đảm bảo các scripts là được download song song

· Một phút để đọc

Ngày nay ứng dụng di động tác động đến mọi khía cạnh đời sống con người, từ thông tin liên lạc, công việc, tài chính, giải trí, đi lại, đặt thức ăn, mua hàng, du lịch đến kết bạn, tìm người yêu. Khó lòng đưa ra thống kê chính xác có hết thảy bao ứng dụng di động trên toàn cầu. Tuy nhiên, dù số lượng có là bao nhiêu đi chăng nữa, chúng được chia thành vào 4 loại: native app, hybrid app, web app và mini app.

Native app

Native app được tạo ra cho một hệ điều hành cụ thể. Sẽ là bất khả thi nếu chúng ta đem ứng dụng Android chạy điện thoại iPhone.

Native app được cài đặt thông qua app store chẳng hạn Google Play Store và Apple Store. Mỗi hệ điều hành có ngôn ngữ riêng để lập trình ứng dụng. Ví dụ, để xây dựng ứng dụng trên iOS, Objective C và Swift là 2 lựa chọn lý tưởng; còn đối với Android thì Java, Python và Kotlin là những công cụ phổ biến.

Ưu điểm:

  • Native app có lợi điểm là hiệu suất nhanh và bảo mật tốt hơn so với các loại ứng dụng di động khác.
  • Loại ứng dụng này có thể can thiệp sâu vào phần cứng.

Khuyểt điểm:

  • Để ứng dụng có thể chạy trên các nền tảng khác nhau đòi hỏi phải xây dựng codebase riêng cho mỗi nền tảng. Điều đó sẽ dẫn đến việc gia tăng đáng kể thời gian, nguồn nhân lực, và chi phí cho việc phát triển, bảo trì và nâng cấp native app.
  • Người dùng phải thường xuyên cập nhật native app.

Web app

Web app không phải là một ứng dụng thực thụ, mà là website. Trong chừng mực nào đó, chúng có giao diện và cách thức hoạt động giống native app. Tuy nhiên, người dùng truy cập chúng thông qua trình duyệt. Chúng được thiết kế bằng HTML, CSS, JavaScript hay Ruby.

Ưu điểm:

  • Web app có thể chạy trên nhiều nền tảng khác nhau miễn là trình duyệt có hỗ trợ. Do đó, chỉ cần một codebase cho tất cả nền tảng; tiết kiệm thời gian và chi phí cho việc phát triển, bảo trì và nâng cấp.
  • Người luôn truy cập phiên bản mới nhất của ứng dụng.

Khuyết điểm:

  • Tốc độ của web app phụ thuộc nhiều vào kết nối Internet và lượng người truy cập.
  • Không thể khai thác đầy đủ khả năng và trải nghiệm của một nền tảng.

Hybrid App

Hybrid app là sự kết hợp những ưu điểm của native app và web app. Nó được cài đặt giống một native app nhưng lại có cơ chế hoạt động tương tự một web app. Công nghệ phổ biến dùng để phát triển hybrid app này phải kể đến HTML, CSS, và các loại hybrid framework của JavaScript (React Native, AngularJS, Flutter).

Ưu điểm:

  • Codebase được viết một lần và có thể triển khai trên tất cả nền tảng. Vì thế, thời gian phát triển ngắn và chi phí phát triển thấp so với native app.
  • Có khả năng hoạt động dù có kết nối Internet hay không.

Khuyết điểm:

  • Tốc độ chạy của hybrid app chậm hơn so với native app.
  • Khó có thể khai thác hết khả năng và trải nghiệm của một nền tảng.

Mini app

Sinh sau đẻ muộn so với các 3 loại ứng dụng kể trên, mini app chạy hoạt động bên trong một native app - được gọi là super app. Mỗi nền tảng mini app đều phát hành framework riêng. Thường thì framework kiểu này gọn nhẹ với cú pháp dựa trên XML, JavaScript.

Ưu điểm:

  • Hiệu năng hoạt động của mini app không thua kém so với native app.
  • Mini app mang tính đa nền; chỉ cần một codebase duy nhất cho tất cả nền tảng. Hơn thế nữa, mini app còn tận dụng được các tính năng có sẵn trên super app. Từ đó có thể suy ra, thời gian, công sức, chi phí xây dựng và nâng cấp mini app còn thấp hơn cả web app hay hybrid app.
  • Người dùng không cần phải cập nhật mini app bởi phiên bản mới nhất của mini app luôn được tìm thấy trong super app.

Khuyết điểm:

  • Mini app phụ thuộc vào super app. Người dùng phải cài super app mới có thể sử dụng mini app.

Hãy cũng nhau tìm hiểu về Tiki mini app.

· Một phút để đọc

Mini app hay còn được biết đến với cái tên mini program đang thổi luồng sinh khí mới vào ngành di động. Người dùng đang dần quen với việc sử dụng các tiện ích (do bên thứ 3 cung cấp) trong các ứng dụng di động; các tiện ích như thế không đâu xa lạ chính là mini app. Nhiều ông lớn trong ngành công nghệ không muốn bị tụt hậu trong cuộc chơi, đã hay đang chuẩn bị bắt tay vào xây dựng hệ sinh thái mini app cho riêng mình. Các doanh nghiệp nhỏ vừa phát hành ứng dụng dưới dạng mini app bên cạnh native app; thậm chí có doanh nghiệp chỉ làm mini app mà thôi.

Như đa số mọi người đều biết, mini app là ứng dụng tồn tại và hoạt động bên trong một super app - một ứng dụng di động chạy trên hệ điều hành iOS hoặc Android. Nhờ đặc điểm này mà mini app mang đến không biết bao nhiêu lợi ích cho tất cả các bên - nhà xây dựng super app, nhà phát triển mini app và người dùng đầu cuối.

Đối với nhà phát triển mini app, họ nghiễm nhiên sở hữu một ngôn ngữ đa nền mà chỉ cần xây dựng một codebase là đủ. Vì mini app "đồng hành" cùng với super app chứ không phải hệ điều hành; tức là super app đem theo các mini app của mình đến tất cả hệ điều hành mà super app hiện diện.

Việc phát triển mini app thường nhẹ nhàng hơn so với native app. Bởi lẽ mini app có thể tận dụng những tính năng có sẵn trên super app như xác thực, thanh toán, … . Thêm vào đó, framework dành cho mini app luôn tối ưu hoá. Chúng giúp giảm đáng kể thời gian và công sức phải bỏ ra để hoàn thiện một ứng dụng. Nhờ đó nhà phát triển ứng dụng thể tập trung vào việc sáng tạo, nâng cấp sản phẩm thường xuyên hơn.

Ở góc độ nhà xây dựng super app, họ đang tạo ra một hệ sinh thái đa dạng. Các mini app giúp tăng mức độ gắn bó và tương tác của người dùng với super app.

Về phía người dùng, họ có all in one. Thay vì phải lên kho ứng dụng như Apple Store và Google Play Store để tìm kiếm chắt lọc những app đáp ứng nhu cầu của mình. Họ có thể tìm thấy các mini app cần thiết ngay trong super app. Ngoài ra, còn phải kể đến mini app cực kỳ gọn nhẹ so với native app. Một mini app thường chỉ chiếm có 5 - 10 MB, trong khi native app chiếm đến vài chục đến vài trăm MB; hao tốn một lượng đáng kể bộ nhớ của điện thoại.

Mô hình mini app và super app tiêu biểu cho hình thức cộng sinh tuyệt vời.

Hãy cũng nhau tìm hiểu về Tiki mini app.

· Một phút để đọc

Bài viết này sẽ hướng dẫn các bạn làm một header cho page với nội dung được fixed on top kèm theo animation khi scroll page