Cách Tạo Web App Từ Google Sheets Bằng Apps Script [Hướng Dẫn 2027]
![Ảnh minh họa bài viết: Cách Tạo Web App Từ Google Sheets Bằng Apps Script [Hướng Dẫn 2027]](/images/blog/tao-web-app-tu-google-sheets-apps-script.png)
Để tạo web app từ Google Sheets bằng Apps Script, bạn mở Extensions > Apps Script, viết hàm doGet() và doPost() trong Code.gs để xử lý dữ liệu, thiết kế giao diện bằng file HTML riêng, rồi bấm Deploy để lấy link. Toàn bộ quy trình miễn phí hosting, miễn phí SSL, không cần thuê developer — trong khi website thường tốn 50-500 nghìn đồng mỗi tháng cho hosting. Web app phù hợp làm form thu thập dữ liệu, dashboard nội bộ hoặc portal khách hàng, chỉ cần JavaScript và HTML cơ bản.
Web App Từ Google Sheets Là Gì Và Vì Sao Miễn Phí Cho Mọi Người?
Bạn có biết rằng Google Sheets không chỉ là bảng tính? Với Google Apps Script, bạn có thể biến bất kỳ spreadsheet nào thành một ứng dụng web hoàn chỉnh - có giao diện HTML đẹp mắt, xử lý form, hiển thị dữ liệu, thậm chí xây dựng cả hệ thống CRUD (Create, Read, Update, Delete) - và tất cả đều miễn phí hosting, miễn phí domain, miễn phí SSL.
Apps Script Web App là giải pháp tuyệt vời cho các tình huống:
- Form thu thập dữ liệu đẹp hơn Google Forms, tuỳ chỉnh 100% giao diện
- Dashboard nội bộ cho team tra cứu thông tin từ Sheets
- Portal khách hàng để theo dõi đơn hàng, tra cứu bảo hành
- Hệ thống chấm điểm, đánh giá với giao diện thân thiện
- Landing page đơn giản với form liên hệ lưu vào Sheets
- Internal tools cho doanh nghiệp mà không cần thuê developer
Ưu điểm Web App từ Google Sheets:
| Tiêu chí | Web App (Apps Script) | Website thường |
|---|---|---|
| Chi phí hosting | MIỄN PHÍ (Google server) | 50-500K/thang |
| SSL (HTTPS) | Có sẵn | Phải cài đặt |
| Database | Google Sheets (quen thuộc) | MySQL, MongoDB... |
| Deploy | 1 click | CI/CD, FTP, Docker... |
| Authentication | Google Account (có sẵn) | Tự xây dựng |
| Yêu cầu kỹ thuật | JavaScript + HTML cơ bản | Full-stack developer |
Bài viết này sẽ hướng dẫn bạn:
- Hieu cấu trúc dự án Web App (Code.gs + HTML files)
- Nắm vững doGet/doPost - 2 hàm cốt lõi của Web App
- Sử dụng HtmlService để tạo giao diện đẹp
- Xây dựng CRUD operations đầy đủ (Create/Read/Update/Delete)
- Thực hành với 3 ví dụ thực tế kèm code mẫu
- Biết cách deploy và chia sẻ Web App cho người khác dùng
- Nắm rõ giới hạn và tips bảo mật quan trọng
Cấu Trúc Dự Án Web App Trong Apps Script Gồm Những Gì?
Một dự án Web App trong Apps Script gồm 3 thành phần chính:
Cấu trúc thư mục:
Apps Script Project/
|-- Code.gs (Server-side: doGet, doPost, xu ly data)
|-- Page.html (Client-side: giao dien nguoi dung)
|-- Stylesheet.html (CSS styles)
|-- JavaScript.html (Client-side JavaScript)
|-- appsscript.json (Manifest - cau hinh project)
Code.gs là file server-side, viết bằng JavaScript (chạy trên server Google). Đây là nơi bạn định nghĩa các hàm xử lý dữ liệu, đọc/ghi Google Sheets, và trả về HTML cho người dùng.
HTML files là giao diện người dùng. Bạn có thể tạo nhiều file .html cho các trang khác nhau. CSS và JavaScript của client cũng được viết trong file .html riêng (vì Apps Script không hỗ trợ file .css hay .js riêng).
appsscript.json là file cấu hình, định nghĩa quyền truy cập (scopes), runtime version, và các thiết lập khác.
Cách Setup Project Apps Script Như Thế Nào?
Bước 1: Mở Google Sheets và tạo Apps Script project
- Mở Google Sheets bất kỳ (hoặc tạo mới tại sheets.new)
- Vào menu Extensions > Apps Script
- Cửa sổ Script Editor sẽ mở ra với file Code.gs mặc định
- Đổi tên project (click vào "Untitled project" ở góc trái trên)
Bước 2: Tạo file HTML
- Trong Script Editor, click dấu + bên cạnh "Files"
- Chon HTML
- Đặt tên file (VD: "Page", "Index", "Dashboard"...)
- File .html sẽ được tạo với template HTML5 cơ bản
Luu y: Khi tạo file HTML, không cần thêm đuôi .html - Apps Script tự động thêm. VD: nhập "Page" sẽ tạo file "Page.html".
doGet Và doPost Trong Code.gs Hoạt Động Ra Sao?
doGet() va doPost() là 2 hàm đặc biệt trong Apps Script. Khi bạn deploy Web App:
- doGet(e) được gọi khi người dùng truy cập URL bằng trình duyệt (HTTP GET)
- doPost(e) được gọi khi có POST request (VD: submit form, webhook)
3.1 doGet - Hiển thị giao diện
// Code.gs - Ham doGet co ban
function doGet(e) {
// Tra ve file HTML lam giao dien
return HtmlService.createHtmlOutputFromFile('Page')
.setTitle('Ung Dung Quan Ly')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// Phien ban nang cao voi Template (cho phep truyen du lieu vao HTML)
function doGet(e) {
var template = HtmlService.createTemplateFromFile('Page');
// Truyen du lieu tu server vao HTML template
template.pageTitle = 'Dashboard Quan Ly Don Hang';
template.userName = Session.getActiveUser().getEmail();
template.currentDate = Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy HH:mm');
return template.evaluate()
.setTitle('Dashboard')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
// Xu ly tham so URL (VD: ?page=dashboard&id=123)
function doGet(e) {
var page = e.parameter.page || 'home';
var template;
if (page === 'dashboard') {
template = HtmlService.createTemplateFromFile('Dashboard');
} else if (page === 'form') {
template = HtmlService.createTemplateFromFile('Form');
} else {
template = HtmlService.createTemplateFromFile('Home');
}
return template.evaluate()
.setTitle('My App - ' + page)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
3.2 doPost - Nhận dữ liệu từ form hoặc webhook
// Code.gs - Ham doPost xu ly form submit
function doPost(e) {
try {
var data = JSON.parse(e.postData.contents);
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
// Luu du lieu vao sheet
sheet.appendRow([
new Date(),
data.name || '',
data.email || '',
data.phone || '',
data.message || ''
]);
return ContentService.createTextOutput(JSON.stringify({
status: 'success',
message: 'Du lieu da duoc luu thanh cong!'
})).setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({
status: 'error',
message: error.toString()
})).setMimeType(ContentService.MimeType.JSON);
}
}
3.3 HtmlService - Tạo giao diện
HtmlService cung cấp 3 cách tạo HTML:
// Cach 1: Tu file HTML
HtmlService.createHtmlOutputFromFile('Page')
// Cach 2: Tu string (cho HTML don gian)
HtmlService.createHtmlOutput('<h1>Xin chao!</h1>')
// Cach 3: Tu Template (KHUYEN DUNG - cho phep truyen du lieu)
var template = HtmlService.createTemplateFromFile('Page');
template.data = getSheetData();
template.evaluate()
Template syntax cho phép nhúng code Apps Script vào HTML:
<!-- Page.html -->
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 8px 0; }
.btn { background: #1a73e8; color: white; border: none; padding: 10px 20px; border-radius: 6px; cursor: pointer; }
.btn:hover { background: #1557b0; }
</style>
</head>
<body>
<h1>Dashboard</h1>
<p>Xin chao! Hom nay la: <?= currentDate ?></p>
<!-- Vong lap hien thi du lieu tu Sheets -->
<? var data = getData(); ?>
<? for (var i = 0; i < data.length; i++) { ?>
<div class="card">
<h3><?= data[i][0] ?></h3>
<p>Email: <?= data[i][1] ?></p>
<p>Trang thai: <?= data[i][2] ?></p>
</div>
<? } ?>
</body>
</html>
3.4 Include Pattern - Tách CSS và JS riêng
Để code sạch sẽ, tách CSS và JavaScript ra file riêng rồi include vào:
// Code.gs - Ham include file
function include(filename) {
return HtmlService.createHtmlOutputFromFile(filename).getContent();
}
// Page.html - Su dung include
// <!DOCTYPE html>
// <html>
// <head>
// <base target="_top">
// <?!= include('Stylesheet') ?>
// </head>
// <body>
// <h1>My App</h1>
// <div id="content"></div>
// <?!= include('JavaScript') ?>
// </body>
// </html>
<!-- Stylesheet.html -->
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: #f5f5f5;
color: #333;
}
.container { max-width: 960px; margin: 0 auto; padding: 20px; }
.card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 20px;
margin-bottom: 16px;
}
.btn-primary {
background: #1a73e8;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover { background: #1557b0; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; font-weight: 600; margin-bottom: 4px; }
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { border: 1px solid #ddd; padding: 10px; text-align: left; }
.table th { background: #f0f0f0; font-weight: 600; }
.table tr:hover { background: #f9f9f9; }
.badge { padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 600; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
</style>
Cách Thực Hiện CRUD Operations Trên Google Sheets?
Đây là phần quan trọng nhất. CRUD (Create, Read, Update, Delete) là 4 thao tác cơ bản để tương tác với dữ liệu trong Google Sheets từ Web App.
4.1 google.script.run - Cầu nối Client và Server
Trong Web App, client-side JavaScript giao tiếp với server-side Apps Script thông qua google.script.run:
// Client-side (trong file HTML)
// Goi ham server-side va xu ly ket qua
google.script.run
.withSuccessHandler(function(result) {
console.log('Thanh cong:', result);
// Xu ly ket qua o day
})
.withFailureHandler(function(error) {
console.error('Loi:', error);
alert('Co loi xay ra: ' + error.message);
})
.getDataFromSheet(); // Ten ham trong Code.gs
4.2 CREATE - Thêm dữ liệu mới
// ====== SERVER-SIDE (Code.gs) ======
function addNewRecord(formData) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
// Tao ID tu dong
var lastRow = sheet.getLastRow();
var newId = lastRow > 0 ? 'ID-' + (lastRow + 1).toString().padStart(4, '0') : 'ID-0001';
// Them dong moi
sheet.appendRow([
newId,
formData.name,
formData.email,
formData.phone,
formData.address,
new Date(), // Ngay tao
'Active' // Status mac dinh
]);
return {
success: true,
id: newId,
message: 'Da them thanh cong: ' + formData.name
};
}
// ====== CLIENT-SIDE (JavaScript.html) ======
// <script>
function submitForm() {
var formData = {
name: document.getElementById('name').value,
email: document.getElementById('email').value,
phone: document.getElementById('phone').value,
address: document.getElementById('address').value
};
// Validate
if (!formData.name || !formData.email) {
alert('Vui long dien ten va email!');
return;
}
// Hien loading
document.getElementById('submitBtn').disabled = true;
document.getElementById('submitBtn').textContent = 'Dang xu ly...';
google.script.run
.withSuccessHandler(function(result) {
if (result.success) {
alert(result.message);
document.getElementById('myForm').reset();
loadData(); // Refresh bang du lieu
}
document.getElementById('submitBtn').disabled = false;
document.getElementById('submitBtn').textContent = 'Them moi';
})
.withFailureHandler(function(error) {
alert('Loi: ' + error.message);
document.getElementById('submitBtn').disabled = false;
document.getElementById('submitBtn').textContent = 'Them moi';
})
.addNewRecord(formData);
}
// </script>
4.3 READ - Đọc và hiển thị dữ liệu
// ====== SERVER-SIDE (Code.gs) ======
function getAllRecords() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
if (data.length <= 1) return []; // Chi co header
var records = [];
var headers = data[0];
for (var i = 1; i < data.length; i++) {
var record = {};
for (var j = 0; j < headers.length; j++) {
record[headers[j]] = data[i][j];
}
records.push(record);
}
return records;
}
// Tim kiem theo dieu kien
function searchRecords(keyword) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
var results = [];
var keywordLower = keyword.toLowerCase();
for (var i = 1; i < data.length; i++) {
var rowStr = data[i].join(' ').toLowerCase();
if (rowStr.indexOf(keywordLower) !== -1) {
results.push({
id: data[i][0],
name: data[i][1],
email: data[i][2],
phone: data[i][3],
status: data[i][6]
});
}
}
return results;
}
// Lay 1 record theo ID
function getRecordById(id) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === id) {
return {
id: data[i][0],
name: data[i][1],
email: data[i][2],
phone: data[i][3],
address: data[i][4],
createdAt: data[i][5],
status: data[i][6]
};
}
}
return null;
}
// ====== CLIENT-SIDE (JavaScript.html) ======
// <script>
function loadData() {
document.getElementById('tableBody').innerHTML = '<tr><td colspan="6">Dang tai...</td></tr>';
google.script.run
.withSuccessHandler(function(records) {
var html = '';
if (records.length === 0) {
html = '<tr><td colspan="6">Chua co du lieu</td></tr>';
} else {
for (var i = 0; i < records.length; i++) {
var r = records[i];
var badgeClass = r.status === 'Active' ? 'badge-success' : 'badge-danger';
html += '<tr>'
+ '<td>' + r.id + '</td>'
+ '<td>' + r.name + '</td>'
+ '<td>' + r.email + '</td>'
+ '<td>' + r.phone + '</td>'
+ '<td><span class="badge ' + badgeClass + '">' + r.status + '</span></td>'
+ '<td>'
+ '<button onclick="editRecord(\'+ "'" + r.id + "'" + '\)" class="btn-sm">Sua</button> '
+ '<button onclick="deleteRecord(\'+ "'" + r.id + "'" + '\)" class="btn-sm btn-danger">Xoa</button>'
+ '</td>'
+ '</tr>';
}
}
document.getElementById('tableBody').innerHTML = html;
document.getElementById('recordCount').textContent = 'Tong: ' + records.length + ' ban ghi';
})
.withFailureHandler(function(error) {
document.getElementById('tableBody').innerHTML = '<tr><td colspan="6">Loi: ' + error.message + '</td></tr>';
})
.getAllRecords();
}
// Goi khi trang tai xong
window.onload = loadData;
// </script>
4.4 UPDATE - Cập nhật dữ liệu
// ====== SERVER-SIDE (Code.gs) ======
function updateRecord(id, updatedData) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === id) {
var rowNumber = i + 1; // Sheets rows bat dau tu 1
sheet.getRange(rowNumber, 2).setValue(updatedData.name);
sheet.getRange(rowNumber, 3).setValue(updatedData.email);
sheet.getRange(rowNumber, 4).setValue(updatedData.phone);
sheet.getRange(rowNumber, 5).setValue(updatedData.address);
return { success: true, message: 'Da cap nhat: ' + id };
}
}
return { success: false, message: 'Khong tim thay ID: ' + id };
}
4.5 DELETE - Xóa dữ liệu
// ====== SERVER-SIDE (Code.gs) ======
function deleteRecord(id) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === id) {
sheet.deleteRow(i + 1);
return { success: true, message: 'Da xoa: ' + id };
}
}
return { success: false, message: 'Khong tim thay ID: ' + id };
}
// Soft delete (danh dau da xoa thay vi xoa han)
function softDeleteRecord(id) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === id) {
sheet.getRange(i + 1, 7).setValue('Deleted');
sheet.getRange(i + 1, 8).setValue(new Date());
return { success: true, message: 'Da xoa (soft): ' + id };
}
}
return { success: false, message: 'Khong tim thay ID: ' + id };
}
Cách Deploy Web App Từ Apps Script?
Sau khi viết code xong, bạn cần deploy để Web App hoạt động. Đây là hướng dẫn chi tiết:
Bước 1: Deploy lần đầu
- Trong Script Editor, click Deploy > New deployment
- Click icon bánh răng (gear) > chọn Web app
- Điền thông tin:
- Description: Mô tả phiên bản (VD: "v1.0 - Initial release")
- Execute as: Chọn "Me" (chạy bằng tài khoản của bạn)
- Who has access: Chọn quyền truy cập (xem bảng dưới)
- Click Deploy
- Copy Web app URL - đây là link truy cập ứng dụng
| Tùy chọn "Who has access" | Mô tả | Phù hợp cho |
|---|---|---|
| Only myself | Chỉ mình bạn truy cập được | Testing, tool cá nhân |
| Anyone with Google account | Bất kỳ ai có Google account | Ứng dụng nội bộ công ty |
| Anyone | Bất kỳ ai (không cần đăng nhập) | Form công khai, landing page |
Bước 2: Cập nhật phiên bản
Khi bạn sửa code và muốn cập nhật Web App:
- Click Deploy > Manage deployments
- Click icon bút chì (edit) của deployment hiện tại
- O Version, chon New version
- Thêm mô tả phiên bản mới
- Click Deploy
Lưu ý quan trọng: Nếu bạn không chọn "New version", người dùng sẽ vẫn thấy phiên bản cũ! Đây là lỗi thường gặp nhất khi làm Web App. Luôn tạo New version khi muốn cập nhật.
Phần 6: 3 Ví Dụ Thực Tế Kèm Code Mẫu
Ví dụ 1: Form Đăng Ký Sự Kiện
Tạo form đăng ký sự kiện đẹp hơn Google Forms, tự động gửi email xác nhận, hiển thị số người đã đăng ký, và tự động đóng form khi hết slot.
// ====== Code.gs ======
var SHEET_NAME = 'Registrations';
var MAX_SLOTS = 50;
var EVENT_NAME = 'Workshop Google Sheets Nang Cao';
var EVENT_DATE = '15/03/2026 - 9:00 AM';
function doGet() {
var template = HtmlService.createTemplateFromFile('EventForm');
template.eventName = EVENT_NAME;
template.eventDate = EVENT_DATE;
template.slotsRemaining = getRemainingSlots();
return template.evaluate()
.setTitle('Dang Ky: ' + EVENT_NAME)
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getRemainingSlots() {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
var registeredCount = Math.max(0, sheet.getLastRow() - 1); // Tru header
return MAX_SLOTS - registeredCount;
}
function registerForEvent(formData) {
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
var remaining = getRemainingSlots();
if (remaining <= 0) {
return { success: false, message: 'Rat tiec, su kien da het slot dang ky!' };
}
// Kiem tra trung email
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][2] === formData.email) {
return { success: false, message: 'Email nay da dang ky roi!' };
}
}
// Luu dang ky
var registrationId = 'REG-' + new Date().getTime();
sheet.appendRow([
registrationId,
formData.name,
formData.email,
formData.phone,
formData.company || '',
new Date(),
'Confirmed'
]);
// Gui email xac nhan
try {
MailApp.sendEmail({
to: formData.email,
subject: 'Xac nhan dang ky: ' + EVENT_NAME,
htmlBody: '<div style="font-family: Arial; max-width: 600px; margin: 0 auto;">'
+ '<h2 style="color: #1a73e8;">Dang ky thanh cong!</h2>'
+ '<p>Xin chao <strong>' + formData.name + '</strong>,</p>'
+ '<p>Ban da dang ky thanh cong su kien <strong>' + EVENT_NAME + '</strong>.</p>'
+ '<p>Thoi gian: ' + EVENT_DATE + '</p>'
+ '<p>Ma dang ky: <strong>' + registrationId + '</strong></p>'
+ '<p>Vui long giu email nay de check-in tai su kien.</p>'
+ '<hr><p style="color: #666;">Gui tu SheetStore - sheet.com.vn</p>'
+ '</div>'
});
} catch(e) {
Logger.log('Khong gui duoc email: ' + e.toString());
}
return {
success: true,
message: 'Dang ky thanh cong! Ma: ' + registrationId,
remaining: remaining - 1
};
}
File EventForm.html sẽ chứa giao diện form với các trường: Họ tên, Email, SĐT, Công ty. Hiển thị số slot còn lại và tự động disable form khi hết slot. Sử dụng CSS để tạo giao diện đẹp với progress bar hiển thị tỷ lệ đăng ký.
Ví dụ 2: Dashboard Tra Cứu Đơn Hàng
Tạo dashboard để khách hàng hoặc nhân viên tra cứu trạng thái đơn hàng chỉ bằng mã đơn.
// ====== Code.gs ======
function doGet(e) {
var template = HtmlService.createTemplateFromFile('OrderDashboard');
return template.evaluate()
.setTitle('Tra Cuu Don Hang - SheetStore')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function lookupOrder(orderNumber) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('DonHang');
var data = sheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === orderNumber) {
return {
found: true,
order: {
orderNumber: data[i][0],
customerName: data[i][1],
orderDate: Utilities.formatDate(new Date(data[i][2]), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy HH:mm'),
items: data[i][3],
totalAmount: data[i][4],
status: data[i][5],
shippingAddress: data[i][6],
trackingNumber: data[i][7] || 'Chua co',
estimatedDelivery: data[i][8] ? Utilities.formatDate(new Date(data[i][8]), 'Asia/Ho_Chi_Minh', 'dd/MM/yyyy') : 'Dang cap nhat'
}
};
}
}
return { found: false };
}
function getDashboardStats() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('DonHang');
var data = sheet.getDataRange().getValues();
var stats = {
total: data.length - 1,
pending: 0,
processing: 0,
shipped: 0,
delivered: 0,
todayOrders: 0,
todayRevenue: 0
};
var today = Utilities.formatDate(new Date(), 'Asia/Ho_Chi_Minh', 'yyyy-MM-dd');
for (var i = 1; i < data.length; i++) {
var status = data[i][5];
if (status === 'Pending') stats.pending++;
else if (status === 'Processing') stats.processing++;
else if (status === 'Shipped') stats.shipped++;
else if (status === 'Delivered') stats.delivered++;
var orderDate = Utilities.formatDate(new Date(data[i][2]), 'Asia/Ho_Chi_Minh', 'yyyy-MM-dd');
if (orderDate === today) {
stats.todayOrders++;
stats.todayRevenue += Number(data[i][4]) || 0;
}
}
return stats;
}
Dashboard hiển thị: ô tìm kiếm nhập mã đơn, kết quả chi tiết với timeline trạng thái (Pending → Processing → Shipped → Delivered), thông tin vận chuyển, và các card thống kê tổng quan. Giao diện responsive trên mobile.
Ví dụ 3: Hệ Thống Đánh Giá (Rating System)
Tạo hệ thống đánh giá sản phẩm/dịch vụ với star rating, cho phép người dùng đánh giá và xem tổng hợp kết quả.
// ====== Code.gs ======
function doGet(e) {
var template = HtmlService.createTemplateFromFile('RatingPage');
template.items = getItemsWithRatings();
return template.evaluate()
.setTitle('Danh Gia San Pham')
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
function getItemsWithRatings() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var itemSheet = ss.getSheetByName('Items');
var ratingSheet = ss.getSheetByName('Ratings');
var items = itemSheet.getDataRange().getValues();
var ratings = ratingSheet.getDataRange().getValues();
var result = [];
for (var i = 1; i < items.length; i++) {
var itemId = items[i][0];
var itemRatings = [];
var totalScore = 0;
for (var j = 1; j < ratings.length; j++) {
if (ratings[j][1] === itemId) {
itemRatings.push({
userName: ratings[j][2],
score: ratings[j][3],
comment: ratings[j][4],
date: ratings[j][5]
});
totalScore += Number(ratings[j][3]);
}
}
var avgRating = itemRatings.length > 0 ? (totalScore / itemRatings.length).toFixed(1) : 0;
result.push({
id: itemId,
name: items[i][1],
description: items[i][2],
image: items[i][3] || '',
avgRating: avgRating,
totalRatings: itemRatings.length,
recentReviews: itemRatings.slice(-5).reverse() // 5 review moi nhat
});
}
return result;
}
function submitRating(ratingData) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Ratings');
// Validate
if (!ratingData.itemId || !ratingData.score || !ratingData.userName) {
return { success: false, message: 'Vui long dien day du thong tin!' };
}
if (ratingData.score < 1 || ratingData.score > 5) {
return { success: false, message: 'Diem danh gia phai tu 1 den 5!' };
}
sheet.appendRow([
'R-' + new Date().getTime(),
ratingData.itemId,
ratingData.userName,
ratingData.score,
ratingData.comment || '',
new Date()
]);
// Tinh lai trung binh
var newAvg = calculateAverageRating(ratingData.itemId);
return {
success: true,
message: 'Cam on ban da danh gia!',
newAverage: newAvg
};
}
function calculateAverageRating(itemId) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Ratings');
var data = sheet.getDataRange().getValues();
var total = 0;
var count = 0;
for (var i = 1; i < data.length; i++) {
if (data[i][1] === itemId) {
total += Number(data[i][3]);
count++;
}
}
return count > 0 ? (total / count).toFixed(1) : '0';
}
Giao diện hiển thị danh sách sản phẩm dạng card, mỗi card có: tên, mô tả, star rating (sao vàng), số lượng đánh giá, và nút "Đánh giá ngay". Khi click sẽ hiện modal với 5 sao để chọn và ô nhập nhận xét. Kết quả cập nhật realtime sau khi submit.
Cần Lưu Ý Gì Về Bảo Mật Cho Web App?
Web App từ Google Sheets là công cụ mạnh, nhưng cần chú ý bảo mật để tránh bị lạm dụng. Dưới đây là các tips quan trọng:
7.1 Validate Input Phía Server
KHÔNG BAO GIỜ chỉ validate phía client. Người dùng có thể bypass JavaScript và gửi dữ liệu trực tiếp lên server. Luôn validate lại trong Code.gs:
function addRecord(formData) {
// 1. Kiem tra du lieu bat buoc
if (!formData.name || !formData.email) {
throw new Error('Thieu thong tin bat buoc');
}
// 2. Validate email format
var emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
if (!emailRegex.test(formData.email)) {
throw new Error('Email khong hop le');
}
// 3. Validate do dai
if (formData.name.length > 100 || formData.email.length > 100) {
throw new Error('Du lieu qua dai');
}
// 4. Lam sach du lieu (chong XSS)
formData.name = formData.name.replace(/</g, '').replace(/>/g, '');
formData.email = formData.email.trim().toLowerCase();
// 5. Luu vao sheet
// ...
}
7.2 Giới Hạn Truy Cập
// Kiem tra quyen truy cap
function checkAccess() {
var userEmail = Session.getActiveUser().getEmail();
var allowedDomains = ['company.com', 'partner.com'];
var allowedEmails = ['admin@gmail.com', 'manager@gmail.com'];
// Kiem tra domain
var userDomain = userEmail.split('@')[1];
if (allowedDomains.indexOf(userDomain) !== -1) return true;
// Kiem tra email cu the
if (allowedEmails.indexOf(userEmail) !== -1) return true;
return false;
}
function doGet(e) {
if (!checkAccess()) {
return HtmlService.createHtmlOutput(
'<h1>Access Denied</h1><p>Ban khong co quyen truy cap ung dung nay.</p>'
);
}
// Tiep tuc hien thi app...
return HtmlService.createTemplateFromFile('Page').evaluate();
}
7.3 Rate Limiting
// Gioi han so request tu 1 nguoi trong 1 phut
function checkRateLimit(userEmail) {
var cache = CacheService.getScriptCache();
var key = 'rate_' + userEmail;
var count = cache.get(key);
if (count && Number(count) >= 30) { // Max 30 requests/phut
throw new Error('Ban dang gui qua nhieu yeu cau. Vui long doi 1 phut.');
}
cache.put(key, count ? (Number(count) + 1).toString() : '1', 60); // TTL 60 giay
}
function addRecord(formData) {
var userEmail = Session.getActiveUser().getEmail() || 'anonymous';
checkRateLimit(userEmail);
// Tiep tuc xu ly...
}
7.4 Logging và Audit Trail
// Ghi log moi thao tac
function logAction(action, details) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var logSheet = ss.getSheetByName('AuditLog');
if (!logSheet) {
logSheet = ss.insertSheet('AuditLog');
logSheet.appendRow(['Timestamp', 'User', 'Action', 'Details', 'IP']);
}
logSheet.appendRow([
new Date(),
Session.getActiveUser().getEmail() || 'anonymous',
action,
JSON.stringify(details),
'' // IP khong lay duoc trong Apps Script
]);
}
// Su dung
function deleteRecord(id) {
logAction('DELETE', { recordId: id });
// Thuc hien xoa...
}
Web App Từ Google Sheets Có Những Giới Hạn Gì?
Web App từ Google Sheets là công cụ tuyệt vời cho nhiều tình huống, nhưng không phải là giải pháp cho mọi thứ. Hiểu rõ giới hạn sẽ giúp bạn chọn công nghệ phù hợp.
| Giới hạn | Chi tiết | Cách xử lý |
|---|---|---|
| Execution time: 6 phút | Mỗi lần chạy script tối đa 6 phút. Quá thời gian sẽ bị timeout. | Chia nhỏ tác vụ, dùng batch processing, pagination |
| Concurrent users hạn chế | Khoảng 30 người dùng đồng thời. Nhiều hơn sẽ bị chậm hoặc lỗi. | Cache kết quả, dùng CacheService, tối ưu query |
| Không có custom domain | URL có dạng script.google.com/macros/... Không thể dùng tên miền riêng. | Dùng iframe embed hoặc redirect từ domain riêng |
| Không hỗ trợ file upload trực tiếp | Không thể dùng <input type="file"> bình thường. | Dùng google.script.run với base64 encoding hoặc Google Picker API |
| Tốc độ phụ thuộc vào Google server | Response time thường 1-3 giây cho request đơn giản. Sheets càng nhiều data càng chậm. | Cache data, pagination, chỉ load dữ liệu cần thiết |
| Giới hạn API calls | UrlFetchApp: 20,000 calls/ngày. MailApp: 100 emails/ngày (cá nhân). | Batch requests, queue system, upgrade Google Workspace |
Khi nào NÊN dùng Web App từ Sheets?
- Internal tools cho team < 30 người
- Form thu thập dữ liệu cần custom UI
- Dashboard báo cáo nội bộ
- Prototype nhanh trước khi xây ứng dụng chính thức
- Landing page đơn giản với form liên hệ
- Tool cá nhân (to-do list, habit tracker, expense tracker)
Khi nào KHÔNG nên dùng Web App từ Sheets?
- Ứng dụng cần > 100 người dùng đồng thời
- Cần tốc độ phản hồi < 500ms
- Cần custom domain và SEO
- E-commerce với giao dịch thanh toán
- Ứng dụng mobile native
- Dữ liệu nhạy cảm cần bảo mật cấp cao (y tế, tài chính)
Làm Sao Để Web App Chạy Nhanh Hơn?
Một số kỹ thuật giúp Web App của bạn chạy nhanh hơn và trải nghiệm người dùng tốt hơn:
9.1 Sử dụng CacheService
// Cache du lieu thuong truy cap (TTL 10 phut)
function getProductList() {
var cache = CacheService.getScriptCache();
var cachedData = cache.get('productList');
if (cachedData) {
return JSON.parse(cachedData);
}
// Neu khong co cache, doc tu sheet
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Products');
var data = sheet.getDataRange().getValues();
var products = [];
for (var i = 1; i < data.length; i++) {
products.push({
id: data[i][0],
name: data[i][1],
price: data[i][2],
stock: data[i][3]
});
}
// Luu cache 10 phut (600 giay)
cache.put('productList', JSON.stringify(products), 600);
return products;
}
// Xoa cache khi data thay doi
function clearProductCache() {
CacheService.getScriptCache().remove('productList');
}
9.2 Pagination - Phân trang dữ liệu
// Thay vi load toan bo, chi load tung trang
function getRecordsPage(pageNumber, pageSize) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName('Data');
var totalRows = sheet.getLastRow() - 1; // Tru header
var totalPages = Math.ceil(totalRows / pageSize);
var startRow = (pageNumber - 1) * pageSize + 2; // +2 vi co header va Sheets bat dau tu 1
var numRows = Math.min(pageSize, totalRows - (pageNumber - 1) * pageSize);
if (numRows <= 0) return { records: [], totalPages: totalPages, currentPage: pageNumber };
var range = sheet.getRange(startRow, 1, numRows, sheet.getLastColumn());
var data = range.getValues();
var records = [];
for (var i = 0; i < data.length; i++) {
records.push({
id: data[i][0],
name: data[i][1],
email: data[i][2],
status: data[i][3]
});
}
return {
records: records,
totalPages: totalPages,
currentPage: pageNumber,
totalRecords: totalRows
};
}
9.3 Loading State và UX
// Client-side: Hien thi loading spinner
// <script>
function showLoading() {
document.getElementById('loading').style.display = 'flex';
document.getElementById('content').style.opacity = '0.5';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.opacity = '1';
}
function loadData() {
showLoading();
google.script.run
.withSuccessHandler(function(result) {
hideLoading();
renderTable(result);
})
.withFailureHandler(function(error) {
hideLoading();
showError(error.message);
})
.getAllRecords();
}
function showError(message) {
var errorDiv = document.getElementById('error');
errorDiv.textContent = message;
errorDiv.style.display = 'block';
setTimeout(function() { errorDiv.style.display = 'none'; }, 5000);
}
function showSuccess(message) {
var successDiv = document.getElementById('success');
successDiv.textContent = message;
successDiv.style.display = 'block';
setTimeout(function() { successDiv.style.display = 'none'; }, 3000);
}
// </script>
Phần 10: Câu Hỏi Thường Gặp (FAQ)
1. Web App có mất phí không?
Hoàn toàn miễn phí. Google Apps Script không tính phí hosting, SSL, hay bandwidth. Bạn chỉ cần tài khoản Google (Gmail miễn phí). Nếu dùng Google Workspace (trước đây là G Suite), bạn sẽ có giới hạn cao hơn (VD: 1500 emails/ngày thay vì 100).
2. Tôi có thể dùng framework CSS như Bootstrap, Tailwind trong Web App không?
Có! Bạn có thể include CSS framework qua CDN link trong file HTML. VD: thêm link Bootstrap CSS vào phần <head>. Tuy nhiên, nên dùng bản minified và chỉ include những component cần thiết để trang tải nhanh. Tailwind thì nên dùng CDN play version hoặc build trước rồi copy CSS vào.
3. Làm sao để debug khi Web App bị lỗi?
Server-side: dùng Logger.log() trong Code.gs, xem log tại Executions (trong Script Editor). Client-side: dùng console.log() và mở Developer Tools (F12) trong trình duyệt. Tip: deploy bản "Test" (click "Test deployments") để test nhanh mà không cần tạo version mới.
4. Web App có hoạt động trên mobile không?
Có, Web App chạy trên trình duyệt mobile bình thường. Tuy nhiên, bạn cần tự làm responsive design (dùng CSS media queries, flexbox, grid). Google không tự động tối ưu giao diện cho mobile. Khuyến nghị: dùng viewport meta tag và test trên nhiều kích thước màn hình.
5. Có thể kết nối Web App với nhiều Google Sheets khác nhau không?
Có. Dùng SpreadsheetApp.openByUrl() hoặc SpreadsheetApp.openById() để mở bất kỳ spreadsheet nào mà tài khoản của bạn có quyền truy cập. VD: Web App đọc dữ liệu từ 1 Sheets "Products", ghi vào Sheets "Orders" khác, và gửi email từ Gmail - tất cả trong cùng 1 script.
Tổng Kết
Web App từ Google Sheets bằng Apps Script là công cụ tuyệt vời để xây dựng ứng dụng web đơn giản mà không mất đồng nào cho hosting, domain, hay server. Với kiến thức HTML/CSS/JavaScript cơ bản và hiểu biết về Apps Script, bạn có thể tạo ra những tool nội bộ mạnh mẽ cho doanh nghiệp chỉ trong vài ngày.
Tóm tắt những gì bạn đã học:
- 1. Cấu trúc dự án: Code.gs (server) + HTML files (client) + Stylesheet + JavaScript
- 2. doGet/doPost: 2 hàm cốt lõi xử lý HTTP requests
- 3. HtmlService: Tạo giao diện với Template syntax (<?= ?> và <? ?>)
- 4. google.script.run: Cầu nối giữa client và server
- 5. CRUD operations: Create, Read, Update, Delete dữ liệu trong Sheets
- 6. Deploy: 1 click deploy với 3 mức quyền truy cập
- 7. Bảo mật: Validate server-side, rate limiting, access control, audit log
- 8. Tối ưu: CacheService, pagination, loading states
Hãy bắt đầu với ví dụ đơn giản nhất - một form thu thập dữ liệu. Khi đã quen với workflow, bạn sẽ nhanh chóng mở rộng thành dashboard, portal, và nhiều hơn nữa. Và khi nhu cầu vượt quá khả năng của Apps Script, bạn có thể chuyển sang các giải pháp chuyên nghiệp hơn mà vẫn giữ Google Sheets làm "database" quen thuộc.
Muốn Có Ngay Hệ Thống Quản Lý Chuyên Nghiệp?
Thay vì tự xây từng dòng code, hãy sử dụng phần mềm có sẵn với giao diện đẹp, tính năng đầy đủ, và hỗ trợ kỹ thuật
Khám phá SheetStoreBài viết liên quan:
- Google Apps Script Từ Cơ Bản Đến Nâng Cao - Nền tảng lập trình vững chắc
- Tự Động Hóa Doanh Nghiệp Với Google Sheets - 10 quy trình tự động hóa
- Xử Lý Dữ Liệu Lớn Trên Google Sheets - Tối ưu hiệu suất
- Tạo Hệ Thống CRM Trên Google Sheets - Quản lý khách hàng
📚 Bài Viết Liên Quan
- Template Google Sheets Ngân Sách Marketing 2027: Lập Kế Hoạch và Theo Dõi ROI
- Template Google Sheets Theo Dõi Dự Án IT và Phần Mềm 2027: Agile Sprint Board Đơn Giản
- AI và Google Sheets 2027: Kết Hợp ChatGPT, Gemini Để Tự Động Hóa Công Việc
- Google Sheets Nâng Cao Bài 6: ARRAYFORMULA - Tự Động Hóa Công Thức Hàng Loạt
Bài viết liên quan
- Google Apps Script Từ Cơ Bản Đến Nâng Cao - Hướng Dẫn Toàn Diện 2027
- Google Sheets Nâng Cao Bài 5: Apps Script - Tự Động Hóa Với JavaScript
- Google Apps Script: 10 Script Mẫu Copy-Paste Ngay Cho Doanh Nghiệp 2027
- Code Google Apps Script Tích Hợp Webhook Đa Hệ Thống 2026
- Sửa Lỗi Bad Request Error 400 Trong Apps Script Google Sheets 2026
Chia sẻ bài viết:
Tuân Hoang
Đội ngũ SheetStore
Google Workspace Certified, 5+ years experience
Công cụ liên quan
Giải pháp cho bài viết này
Bạn thấy bài viết hữu ích?
Đăng ký nhận thông báo khi có bài viết mới.
