add documentation site with markdown rendering (M12)
This commit is contained in:
535
portal/internal/web/docs/guides/best-practices.md
Normal file
535
portal/internal/web/docs/guides/best-practices.md
Normal file
@@ -0,0 +1,535 @@
|
||||
# Best Practices
|
||||
|
||||
Guidelines for building high-quality Mosis apps that users love.
|
||||
|
||||
## Performance
|
||||
|
||||
### Minimize DOM Queries
|
||||
|
||||
Cache element references instead of querying repeatedly:
|
||||
|
||||
```lua
|
||||
-- Bad: Queries on every frame
|
||||
function updateScore()
|
||||
document:GetElementById("score").inner_rml = tostring(score)
|
||||
end
|
||||
|
||||
-- Good: Cache the reference
|
||||
local scoreElement
|
||||
|
||||
function onLoad()
|
||||
scoreElement = document:GetElementById("score")
|
||||
end
|
||||
|
||||
function updateScore()
|
||||
scoreElement.inner_rml = tostring(score)
|
||||
end
|
||||
```
|
||||
|
||||
### Batch DOM Updates
|
||||
|
||||
Group multiple changes together:
|
||||
|
||||
```lua
|
||||
-- Bad: Multiple separate updates
|
||||
elem1.style.color = "red"
|
||||
elem2.style.color = "red"
|
||||
elem3.style.color = "red"
|
||||
|
||||
-- Good: Use a class
|
||||
parent:SetClass("error-state", true)
|
||||
```
|
||||
|
||||
### Use Efficient Data Structures
|
||||
|
||||
```lua
|
||||
-- For frequent lookups, use tables as maps
|
||||
local itemLookup = {}
|
||||
for i, item in ipairs(items) do
|
||||
itemLookup[item.id] = item
|
||||
end
|
||||
|
||||
-- O(1) lookup instead of O(n) search
|
||||
local item = itemLookup["item-123"]
|
||||
```
|
||||
|
||||
### Clean Up Timers
|
||||
|
||||
Always clear intervals when navigating away:
|
||||
|
||||
```lua
|
||||
local updateInterval
|
||||
|
||||
function onScreenLoad()
|
||||
updateInterval = setInterval(function()
|
||||
updateData()
|
||||
end, 1000)
|
||||
end
|
||||
|
||||
function onScreenUnload()
|
||||
if updateInterval then
|
||||
clearInterval(updateInterval)
|
||||
updateInterval = nil
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### Lazy Load Content
|
||||
|
||||
Don't load everything at startup:
|
||||
|
||||
```lua
|
||||
-- Load data when user scrolls to section
|
||||
function onSectionVisible(sectionId)
|
||||
if not loadedSections[sectionId] then
|
||||
loadSectionData(sectionId)
|
||||
loadedSections[sectionId] = true
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## User Experience
|
||||
|
||||
### Provide Feedback
|
||||
|
||||
Show users that actions are happening:
|
||||
|
||||
```lua
|
||||
function onSubmit()
|
||||
-- Show loading state immediately
|
||||
submitButton:SetClass("loading", true)
|
||||
submitButton:SetAttribute("disabled", "disabled")
|
||||
|
||||
http.post(url, data, function(response)
|
||||
submitButton:SetClass("loading", false)
|
||||
submitButton:RemoveAttribute("disabled")
|
||||
|
||||
if response.ok then
|
||||
showSuccess("Saved!")
|
||||
else
|
||||
showError("Failed to save")
|
||||
end
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
### Handle Errors Gracefully
|
||||
|
||||
Never show raw error messages to users:
|
||||
|
||||
```lua
|
||||
http.get(url, function(response)
|
||||
if response.ok then
|
||||
displayData(json.decode(response.body))
|
||||
else
|
||||
-- User-friendly message
|
||||
showMessage("Unable to load data. Please check your connection.")
|
||||
|
||||
-- Log details for debugging
|
||||
console.error("API error:", response.status, response.body)
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
### Make Touch Targets Large Enough
|
||||
|
||||
Minimum 48dp for touchable elements:
|
||||
|
||||
```css
|
||||
.button {
|
||||
min-width: 48dp;
|
||||
min-height: 48dp;
|
||||
padding: 12dp 24dp;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
min-height: 56dp;
|
||||
padding: 16dp;
|
||||
}
|
||||
```
|
||||
|
||||
### Support Undo for Destructive Actions
|
||||
|
||||
```lua
|
||||
local deletedItem = nil
|
||||
local undoTimeout = nil
|
||||
|
||||
function deleteItem(itemId)
|
||||
deletedItem = items[itemId]
|
||||
items[itemId] = nil
|
||||
updateList()
|
||||
|
||||
showUndoSnackbar("Item deleted", function()
|
||||
-- Undo callback
|
||||
items[itemId] = deletedItem
|
||||
deletedItem = nil
|
||||
updateList()
|
||||
end)
|
||||
|
||||
-- Clear undo after 5 seconds
|
||||
undoTimeout = setTimeout(function()
|
||||
deletedItem = nil
|
||||
permanentlyDelete(itemId)
|
||||
end, 5000)
|
||||
end
|
||||
```
|
||||
|
||||
### Remember User State
|
||||
|
||||
Restore position and selections when returning:
|
||||
|
||||
```lua
|
||||
function onScreenUnload()
|
||||
storage.set("list_scroll_position", scrollContainer.scroll_top)
|
||||
storage.set("selected_tab", currentTab)
|
||||
end
|
||||
|
||||
function onScreenLoad()
|
||||
local scrollPos = storage.get("list_scroll_position")
|
||||
if scrollPos then
|
||||
scrollContainer.scroll_top = scrollPos
|
||||
end
|
||||
|
||||
local tab = storage.get("selected_tab")
|
||||
if tab then
|
||||
selectTab(tab)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
### Use Local Variables
|
||||
|
||||
Local variables are faster and prevent global pollution:
|
||||
|
||||
```lua
|
||||
-- Bad: Global
|
||||
count = 0
|
||||
|
||||
-- Good: Local
|
||||
local count = 0
|
||||
|
||||
-- Good: Module-level local
|
||||
local Utils = {}
|
||||
local cache = {} -- Private to module
|
||||
```
|
||||
|
||||
### Handle Edge Cases
|
||||
|
||||
```lua
|
||||
function divide(a, b)
|
||||
if b == 0 then
|
||||
console.warn("Division by zero")
|
||||
return 0
|
||||
end
|
||||
return a / b
|
||||
end
|
||||
|
||||
function getUsername(user)
|
||||
if not user then
|
||||
return "Unknown"
|
||||
end
|
||||
return user.name or user.email or "Unknown"
|
||||
end
|
||||
```
|
||||
|
||||
### Use Meaningful Names
|
||||
|
||||
```lua
|
||||
-- Bad
|
||||
local t = {}
|
||||
local n = 0
|
||||
|
||||
-- Good
|
||||
local userScores = {}
|
||||
local attemptCount = 0
|
||||
|
||||
-- Bad
|
||||
function p(x)
|
||||
return x * 100
|
||||
end
|
||||
|
||||
-- Good
|
||||
function toPercentage(decimal)
|
||||
return decimal * 100
|
||||
end
|
||||
```
|
||||
|
||||
### Keep Functions Small
|
||||
|
||||
Each function should do one thing:
|
||||
|
||||
```lua
|
||||
-- Bad: Does too much
|
||||
function processUser(userId)
|
||||
local user = fetchUser(userId)
|
||||
validateUser(user)
|
||||
updateUserStats(user)
|
||||
sendWelcomeEmail(user)
|
||||
logActivity(user)
|
||||
return formatUserResponse(user)
|
||||
end
|
||||
|
||||
-- Good: Composed of small functions
|
||||
function processNewUser(userId)
|
||||
local user = fetchUser(userId)
|
||||
if not isValidUser(user) then
|
||||
return nil, "Invalid user"
|
||||
end
|
||||
initializeUserStats(user)
|
||||
queueWelcomeEmail(user)
|
||||
return user
|
||||
end
|
||||
```
|
||||
|
||||
### Comment Why, Not What
|
||||
|
||||
```lua
|
||||
-- Bad: Describes what (obvious from code)
|
||||
-- Increment counter by 1
|
||||
counter = counter + 1
|
||||
|
||||
-- Good: Explains why
|
||||
-- Reset retry count after successful connection
|
||||
-- to prevent unnecessary backoff on next attempt
|
||||
retryCount = 0
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
### Validate All Input
|
||||
|
||||
```lua
|
||||
function searchItems(query)
|
||||
-- Sanitize input
|
||||
if type(query) ~= "string" then
|
||||
return {}
|
||||
end
|
||||
|
||||
query = query:sub(1, 100) -- Limit length
|
||||
query = query:gsub("[^%w%s]", "") -- Remove special chars
|
||||
|
||||
return performSearch(query)
|
||||
end
|
||||
```
|
||||
|
||||
### Don't Trust External Data
|
||||
|
||||
```lua
|
||||
http.get(url, function(response)
|
||||
local success, data = pcall(function()
|
||||
return json.decode(response.body)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
console.error("Invalid JSON from API")
|
||||
return
|
||||
end
|
||||
|
||||
-- Validate structure
|
||||
if type(data.items) ~= "table" then
|
||||
console.error("Missing items array")
|
||||
return
|
||||
end
|
||||
|
||||
processItems(data.items)
|
||||
end)
|
||||
```
|
||||
|
||||
### Never Store Secrets in Code
|
||||
|
||||
```lua
|
||||
-- Bad: Hardcoded API key
|
||||
local API_KEY = "sk-12345abcde"
|
||||
|
||||
-- Good: Use environment/config
|
||||
local apiKey = config.get("api_key")
|
||||
```
|
||||
|
||||
### Sanitize Display Content
|
||||
|
||||
When displaying user-generated content, prevent injection:
|
||||
|
||||
```lua
|
||||
function displayComment(text)
|
||||
-- Escape HTML entities
|
||||
text = text:gsub("&", "&")
|
||||
text = text:gsub("<", "<")
|
||||
text = text:gsub(">", ">")
|
||||
|
||||
commentElement.inner_rml = text
|
||||
end
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
### Use Semantic Elements
|
||||
|
||||
```xml
|
||||
<!-- Bad: Divs for everything -->
|
||||
<div class="button" onclick="submit()">Submit</div>
|
||||
|
||||
<!-- Good: Proper elements -->
|
||||
<button onclick="submit()">Submit</button>
|
||||
|
||||
<!-- Good: Headings create hierarchy -->
|
||||
<h1>Settings</h1>
|
||||
<h2>Account</h2>
|
||||
<h2>Notifications</h2>
|
||||
```
|
||||
|
||||
### Provide Text Alternatives
|
||||
|
||||
```xml
|
||||
<!-- Images should describe their purpose -->
|
||||
<img src="icons/search.tga" alt="Search"/>
|
||||
|
||||
<!-- Icons with meaning need labels -->
|
||||
<button aria-label="Close">
|
||||
<img src="icons/close.tga"/>
|
||||
</button>
|
||||
```
|
||||
|
||||
### Ensure Color Contrast
|
||||
|
||||
Text should have at least 4.5:1 contrast ratio:
|
||||
|
||||
```css
|
||||
/* Good contrast */
|
||||
.light-text {
|
||||
color: #ffffff;
|
||||
background-color: #1a1a2e; /* Contrast: 12.6:1 */
|
||||
}
|
||||
|
||||
/* Bad contrast */
|
||||
.low-contrast {
|
||||
color: #888888;
|
||||
background-color: #666666; /* Contrast: 1.3:1 */
|
||||
}
|
||||
```
|
||||
|
||||
### Don't Rely on Color Alone
|
||||
|
||||
```css
|
||||
/* Bad: Only color indicates error */
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
/* Good: Icon + color + text */
|
||||
.error {
|
||||
color: #ff4444;
|
||||
}
|
||||
.error::before {
|
||||
content: "⚠ ";
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Error States
|
||||
|
||||
Don't just test the happy path:
|
||||
|
||||
```lua
|
||||
-- Test these scenarios:
|
||||
-- 1. Empty data
|
||||
-- 2. Network failure
|
||||
-- 3. Invalid input
|
||||
-- 4. Timeouts
|
||||
-- 5. Missing permissions
|
||||
```
|
||||
|
||||
### Test Navigation Flows
|
||||
|
||||
Ensure users can:
|
||||
- Navigate forward and back
|
||||
- Return to the home screen
|
||||
- Handle the back button at any screen
|
||||
|
||||
### Test Edge Cases
|
||||
|
||||
- Very long text/names
|
||||
- Empty lists
|
||||
- Maximum values
|
||||
- Rapid repeated actions
|
||||
- Interrupted operations
|
||||
|
||||
### Use Debug Logging
|
||||
|
||||
```lua
|
||||
local DEBUG = true
|
||||
|
||||
function debugLog(...)
|
||||
if DEBUG then
|
||||
print("[DEBUG]", ...)
|
||||
end
|
||||
end
|
||||
|
||||
-- In production build, set DEBUG = false
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
### Use Meaningful Version Numbers
|
||||
|
||||
Follow semantic versioning:
|
||||
- **MAJOR**: Breaking changes
|
||||
- **MINOR**: New features, backward compatible
|
||||
- **PATCH**: Bug fixes
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.1.3",
|
||||
"version_code": 15
|
||||
}
|
||||
```
|
||||
|
||||
### Write Good Release Notes
|
||||
|
||||
```
|
||||
Version 2.1.0
|
||||
|
||||
New Features:
|
||||
- Added dark mode support
|
||||
- New export to PDF feature
|
||||
|
||||
Improvements:
|
||||
- Faster loading times
|
||||
- Better error messages
|
||||
|
||||
Bug Fixes:
|
||||
- Fixed crash when opening empty files
|
||||
- Fixed date format on some devices
|
||||
```
|
||||
|
||||
### Test Before Submitting
|
||||
|
||||
1. Run on the Designer
|
||||
2. Test all features manually
|
||||
3. Check on a real device if possible
|
||||
4. Verify all assets load correctly
|
||||
5. Test offline behavior
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
Before submitting your app:
|
||||
|
||||
- [ ] All features work as expected
|
||||
- [ ] Error states are handled gracefully
|
||||
- [ ] Loading states shown during async operations
|
||||
- [ ] Touch targets are at least 48dp
|
||||
- [ ] Text is readable (contrast ratio ≥ 4.5:1)
|
||||
- [ ] No console errors in normal usage
|
||||
- [ ] Timers and intervals cleaned up properly
|
||||
- [ ] User data persists correctly
|
||||
- [ ] App works after fresh install
|
||||
- [ ] Version number and code are updated
|
||||
- [ ] Release notes are meaningful
|
||||
|
||||
## See Also
|
||||
|
||||
- [UI Design Guide](ui-design.md) - Design patterns
|
||||
- [Lua Scripting Guide](lua-scripting.md) - Code patterns
|
||||
- [Troubleshooting](../troubleshooting.md) - Common issues
|
||||
Reference in New Issue
Block a user