[TOC]
- 兼容 v3 版本 share folder 流程,folder owner 若是從 v3 API 發送邀請的就會進到 pages/share/folder/index 走接受邀請流程。
- 支援 request link 頁面,但不支援建立 request link。
-
install dependency package in project
yarn
-
run development mode
OPENSSL_PASS=${secret_key} yarn dev
-
run test
yarn test
-
run production mode
yarn build OPENSSL_PASS=${secret_key} yarn start
-
visit http://localhost:3000 with browser
file storage and sharing file or folder web app
- 檔案上傳或下載
- 共享檔案或文件夾
- 線上預覽檔案
- 新增刪除檔案或資料夾
- 搬移檔案或資料夾
__test__---------測試腳本
apis-------------API 腳本
components---- UI 元件(通常是無狀態)
containers------ 綁定狀態的元件(通常有狀態)
config---------- 環境變數設定檔
constants-------共用常數
helpers--------- 幫忙函示
global---------- 共用樣式
pages---------- 根據檔案名稱與路由做關聯
hoc-------------high order component
modules--------landing page 元件和彈出視窗模組
public---------- 靜態檔案放這裡
redux-----------redux action, reducer and saga
server----------透過 express 自訂路由和 server 端預處理
merge 到 preparing 分支後,由 gitlab 自動部署到 preparing 環境
merge 到 master 分支後建立 tag,由 gitlab 自動部署到 production 環境
coding rule 使用 airbnb 的設定,設定檔為.eslintrc.js
我們拆成數個組件寫 CSS-in-JS,使用 styled-components package,將 css class 組件化。部分共用的樣式放在 global 資料夾裡。
多語使用了 next-i18next,添加的方式為請到 public/locales/[locale]/[namespace],在 namespace 這隻 json 檔新增翻譯的內容,根據你的命名空間使用 useTranslation 這個 HOC 即可。
import React from 'react'
import { useTranslation } from 'next-i18next'
const Footer = ({ t }) => {
render() {
return (
<footer>{t('description')}</footer>
)
}
}
export default useTranslation('footer')(Footer)
由於需要 SSR 和快速開發故選用了 next.js 當作伺服器端的宣染框架,在 next.js 的路由都是透過 pages 資料夾裡的檔案名增來做路由關聯。若要自訂路由請參照 next.js 教學
為了讓使用者有更好的體驗以及搜尋引擎優化,當使用者訪問頁面時我們只載入上方 Header 和 Banner,剩下的部分透過 next/dynamic 延遲載入。
使用https://pro.ip-api.com判斷IP是否為中國,若是中國就轉址到https://cloud.kdan.cn
server/index.js
server.use((req, res, _next) => {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
fetch(`https://pro.ip-api.com/json/${ip}?key=Y6uuwTkrwOiyozZ`)
.then(_res => _res.json)
.then((_res) => {
if (_res.countryCode === 'CN' && req.headers.host !== 'cloud.kdan.cn') {
res.redirect(config.HOST_CN);
} else {
_next();
}
});
});
使用者登入實作了 Oauth2 機制,當使用者按下登入會轉址到 member center,並輸入帳號密碼登入成功後返回/kdanmobile/callback,整個過程在使用了 passport.js 完成。
server/authStrategy.js
const kdanStrategy = new OAuth2Strategy(
{
authorizationURL: `${config.MEMBER_CENTER}/oauth/authorize`,
tokenURL: `${config.MEMBER_CENTER}/oauth/token`,
clientID: global.env.CLIENT_ID,
clientSecret: global.env.CLIENT_SECRET,
callbackURL: `${config.HOST}${kdanCallbackPath}`,
},
(accessToken, refreshToken, profile, done) => {
done(null, { accessToken, refreshToken });
},
);
server/middleware/auth.js
router.get(
'/kdanmobile/callback',
passport.authenticate('provider', {
failureRedirect: '/error',
session: false,
}),
(req, res) => {
res.cookie('access_token', req.user.accessToken, {
expires: new Date(Date.now() + 2 * 3600 * 1000),
sameSite: 'None',
secure: !isDev,
});
...
res.redirect('/files');
},
);
上傳檔案使用 AWS SDK 所提供的 upload 方法上傳至 S3,流程是先呼叫 createUploadMission API,後端會返回 credentials、missionId、objectKey,再將返回的 data 跟檔案一同當成參數傳給 upload method。
helpers/aws.js
export const uploadFile = ({
credentials, file, missionId, objectKey, accessToken, bucket,
}) => {
const options = {
accessKeyId: credentials.access_key_id,
secretAccessKey: credentials.secret_access_key,
sessionToken: credentials.session_token,
region: 'us-east-1',
};
const s3 = new S3(options);
...
return s3.upload(param);
};
在此專案呢,使用了很多的彈出視窗,在 modules/modal 裡新增彈出視窗的內容,之後在 containers/Modal.js 裡 import 在 modules 資料夾裡的元件,透過 switch case 根據 modalType 切換內容。
使用方法
import React from 'react';
const Header = () => (
<Modal
modalType="email_input"
...
/>
);
export default Header;
我們使用 Redux 將跨組件共用的狀態統一管理。在 Redux 的世界裡我們呼叫 action 來描述狀態的改變。而某些情境需要複雜的業務邏輯,為了讓 action 和 reducer 保持單純,我們使用 redux-saga 把邏輯寫在這個 middleware,透過 saga 提供的 effects api 可以監聽 action 並執行邏輯返回一個新的 action 把結果寫到 redux store。
為了確保每次狀態更動都是 Immutable,使用 Immer 提供的 produce function 他接受兩個參數分別是 current state 和 producer funtion 執行後將會回傳一個 new immutable tree。
import produce from "immer"
const initialState = [
{
todo: "Try immer",
done: false
}
];
const reducer = (state = initialState, action) => (
produce(baseState, draftState => {
switch (action.type) {
case ACTION_TYPE:
draftState.push({todo: "Tweet about it"})
draftState[1].done = true
break;
...
}
})
);
用 Jest 搭配 testing-library 做單元測試,使用後者來宣染出 DOM 節點並取得內容,再用 Jest 提供的方法斷言。
note: 部分元件我們會調用到package的HOC,像是多語useTranslation和redux的connect,但因為單元測試時只會實例當前元件,所以我們要在__mocks__資料夾裡撰寫對應的mock function
config/jest.config.js
module.exports = {
rootDir: '../',
transform: {
'\\.(js|jsx)?$': 'babel-jest',
},
testMatch: ['<rootDir>/__test__/(*.)test.{js, jsx}'],
moduleFileExtensions: ['js', 'jsx', 'json'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
};
__test__/header.test.js
import React from 'react';
import { render, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import Header from '../components/Header';
describe('test Header', () => {
afterEach(cleanup);
test('test Landing Page Header', () => {
const {
getByTestId,
} = render(<Header isLanding />);
expect(getByTestId('display_logo').innerHTML).toContain('data-kind="main-logo"');
});
});