Apps Script用Sheet生成動態網頁(22): 移植上傳HTML功能

第4篇中我們製作了HTML檔案上傳功能,接著就來移植到React上。
這裡上傳功能一樣使用Google Apps Script的AIP確認當前使用者的E-mail,如果沒有先登入google帳號上傳檔案就會被內部檢核機制阻止。未來這部份會再改成使用會員登入後,就能上傳HTML檔案。拭目以待!

1. Apps Script的部份(後端/伺服端)

新增API

因為要支援上傳檔案,所以暴露給前端的API新增uploadHtmlFile,整體實作放在content,因為html會是一種內容Content。

src/server/index.js
import * as contentFunctions from './content';
...
global.uploadHtmlFile = contentFunctions.uploadHtmlFile;

實作content.js部份因為需要確認使用者身份所以引用user,因為我們會把內容儲存在sheet中,所以在sheet中新增getContentSheet()。這裡的setContentToSheet()有設定匯出export是因為之後還可以拿來做共用功能。

src/server/content.js
import { getContentSheet, findIndexInColumn } from './sheet';
import * as User from './user';

export const COLUMN_IDX_OF_NAME = 0; // 第一個直欄,又稱A欄,用來儲存名稱
export const COLUMN_IDX_OF_CONTENT = 1; // 第二個直欄,又稱B欄,用來儲存內容

export function setContentToSheet(name, content, sheetName) {
const sheet = getContentSheet(sheetName);
const rowIdx = findIndexInColumn(name, COLUMN_IDX_OF_NAME, sheet);
if (rowIdx < 0) {
sheet.appendRow([name, content]);
} else {
sheet.getRange(1 + rowIdx, 1 + COLUMN_IDX_OF_CONTENT).setValue(content); // 設定一個儲存格內容
}
}

export function uploadHtmlFile(form) {
User.validateUploadPermission();
const FILE_SIZE_LIMIT = 10 * 1042 * 1024; // 10MB
const name = form['the-name'];
const file = form['the-file'];
if (!name || !file || file.size <= 0) throw new Error('名稱或檔案大小有問題');
if (file.length > FILE_SIZE_LIMIT)
throw new Error(`檔案超出限制>${FILE_SIZE_LIMIT}位元組`);
if (file.type !== 'text/html' && typeof file.contents !== 'string')
throw new Error(`檔案類型錯誤:${file.type}`);
setContentToSheet(name, file.contents);
return { name, url: `?show=html&name=${name}` };
}

在sheet.js內增加取用Content Sheet的功能,這裡使用js的一個技法,如果沒有給分頁名稱(sheetName)就取第一個分頁,有給定就取特定的分頁(sheet)

src/server/sheet.js
export function getContentSheet(sheetName) {
const book = SpreadsheetApp.openByUrl(SHEET_URL);
const sheet =
sheetName == null ? book.getSheets()[0] : book.getSheetByName(sheetName);
return sheet;
}

增加權限

上面content.js中使用到了User的validateUploadPermission()函數,是用來確認是否有權限上傳檔案。不過為了取得使用者的E-Mail需要增加權限

src/server/user.js
export function validateUploadPermission(notThrow) {
const e = Session.getEffectiveUser();
const a = Session.getActiveUser();
const result = e?.getEmail() === a?.getEmail();
if (!notThrow && !result)
throw new Error(`您沒有權限上傳檔案,請確認你已經登入Google帳號`);
return result;
}


appsscript.json裡頭增加auth/userinfo.email才能取用戶e-mail
"oauthScopes": [
"https://www.googleapis.com/auth/drive",
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/userinfo.email"
],

2. React的部份(前端/使用者端)

增加Route及選單

跟前兩篇移植不同的是,上傳HTML不是已經建立選單跟route的項目,所以要先新增

src/client/demo-bootstrap/App.jsx 裡頭引用上傳HTML的頁面元件
import UploadHtml from './pages/UploadHtml';

在route裡頭新增路徑跟上傳HTML元件
<Route path="/upload-html">
<UploadHtml />
</Route>

再到src/client/demo-bootstrap/components/MyNav.jsx修改選單元件,引用一個上傳圖示
import {
...
BsFillCloudUploadFill,
...
} from 'react-icons/bs';

加入新的選單項目,linkTo與前面的路徑要相同,然後在設定圖示元件
<IconNavLink
linkTo="/upload-html"
iconComp={<BsFillCloudUploadFill />}
>
上傳
</IconNavLink>

建立表單元件

和登入表單的作法一樣,我們新增一個FileForm元件呈現上傳表單,這裡為了方便日後修改,指定上傳表單的幾個文字是可以修改的,titleName, titleFile, titleSubmit分別對應名稱、選檔案、以及上傳按鈕的標題。
src/client/demo-bootstrap/components/FileForm.jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Form, Row, Col, Button } from 'react-bootstrap';

const FileForm = ({
onSubmit,
titleName = '檔案名稱',
titleFile = '請選檔案',
titleSubmit = '上傳',
}) => {
return (
<Form onSubmit={onSubmit}>
<Form.Group as={Row} className="mb-3" controlId="file_name">
<Form.Label column sm={2}>
{titleName}
</Form.Label>
<Col sm={10}>
<Form.Control type="text" name="the-name" required />
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" controlId="file_chooser">
<Form.Label column sm={2}>
{titleFile}
</Form.Label>
<Col sm={10}>
<Form.Control
type="file"
name="the-file"
accept="text/html"
required
/>
</Col>
</Form.Group>
<Row>
<Button variant="primary" type="submit">
{titleSubmit}
</Button>
</Row>
</Form>
);
};

FileForm.propTypes = {
onSubmit: PropTypes.func,
titleName: PropTypes.string,
titleFile: PropTypes.string,
titleSubmit: PropTypes.string,
};

export default FileForm;

上傳檔案機制

整體上傳邏輯跟登入表單很像,上傳完之後會顯示伺服器返回的連結,使用者可以點擊連結過去查看。
src/client/demo-bootstrap/pages/UploadHtml.jsx
import React, { useState } from 'react';
import { Container, Row } from 'react-bootstrap';
import FileForm from '../components/FileForm';

import Server from '../../utils/server';

const { serverFunctions } = Server;

const UploadHtml = () => {
const [msg, showText] = useState(null);
const [link, setLink] = useState(null);

const onSubmit = event => {
event.preventDefault();
showText('上傳中,請稍候...');
serverFunctions
.uploadHtmlFile(event.target)
.then(response => {
showText('上傳成功');
setLink({
url: response.url,
title: response.name,
});
})
.catch(error => {
console.error(error);
showText(`上傳失敗,錯誤訊息:${error.message}`);
});
};

return (
<Container>
<h1>上傳HTML</h1>
<FileForm titleName="網頁名稱" onSubmit={onSubmit} />
<Row>{msg}</Row>
{link && (
<Row>
<a href={link.url}>{link.title}</a>
</Row>
)}
</Container>
);
};

export default UploadHtml;

需要特別說明的部份是,雖然伺服器返回的連結link.url是網站內的URL,但是因為不是router所控制的,只能用HTML的標籤<a href="連結">來處理,否則無法正常連結到上傳的檔案。

4. 成果展示




留言