1.0.0 • Published 1 month ago

kdan-cloud v1.0.0

Weekly downloads
-
License
MIT
Repository
-
Last release
1 month ago

Kdan Cloud

License: MIT Version: 1.0.0

Table of Contents

TOC


About New Kdan Cloud for v4

  • 兼容 v3 版本 share folder 流程,folder owner 若是從 v3 API 發送邀請的就會進到 pages/share/folder/index 走接受邀請流程。
  • 支援 request link 頁面,但不支援建立 request link。

Get Started Immediately

  1. install dependency package in project

    yarn

  2. run development mode

    OPENSSL_PASS=${secret_key} yarn dev

  3. run test

    yarn test

  4. run production mode

    yarn build OPENSSL_PASS=${secret_key} yarn start

  5. visit http://localhost:3000 with browser


Introduction

file storage and sharing file or folder web app

  • 檔案上傳或下載
  • 共享檔案或文件夾
  • 線上預覽檔案
  • 新增刪除檔案或資料夾
  • 搬移檔案或資料夾

Folder Structure

__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 端預處理


Deploy

preparing

merge 到 preparing 分支後,由 gitlab 自動部署到 preparing 環境

production

merge 到 master 分支後建立 tag,由 gitlab 自動部署到 production 環境


Coding Rule

coding rule 使用 airbnb 的設定,設定檔為.eslintrc.js

eslint-config-airbnb


Layout and Style

我們拆成數個組件寫 CSS-in-JS,使用 styled-components package,將 css class 組件化。部分共用的樣式放在 global 資料夾裡。

styled-components


Locales

多語使用了 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)

next-i18next


Features

Routes

由於需要 SSR 和快速開發故選用了 next.js 當作伺服器端的宣染框架,在 next.js 的路由都是透過 pages 資料夾裡的檔案名增來做路由關聯。若要自訂路由請參照 next.js 教學

custom server

Lazy Loading

為了讓使用者有更好的體驗以及搜尋引擎優化,當使用者訪問頁面時我們只載入上方 Header 和 Banner,剩下的部分透過 next/dynamic 延遲載入。

Redirect To CN

使用https://pro.ip-api.com判斷IP是否為中國,若是中國就轉址到https://cloud.kdan.cn

pro-ip

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();
        }
      });
  });

Login Flow


使用者登入實作了 Oauth2 機制,當使用者按下登入會轉址到 member center,並輸入帳號密碼登入成功後返回/kdanmobile/callback,整個過程在使用了 passport.js 完成。

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');
  },
);

Upload 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);
};

Modal

在此專案呢,使用了很多的彈出視窗,在 modules/modal 裡新增彈出視窗的內容,之後在 containers/Modal.js 裡 import 在 modules 資料夾裡的元件,透過 switch case 根據 modalType 切換內容。

使用方法

import React from 'react';

const Header = () => (
  <Modal
    modalType="email_input"
    ...
  />
);

export default Header;

Redux and Redux-Saga

我們使用 Redux 將跨組件共用的狀態統一管理。在 Redux 的世界裡我們呼叫 action 來描述狀態的改變。而某些情境需要複雜的業務邏輯,為了讓 action 和 reducer 保持單純,我們使用 redux-saga 把邏輯寫在這個 middleware,透過 saga 提供的 effects api 可以監聽 action 並執行邏輯返回一個新的 action 把結果寫到 redux store。

Redux-saga

為了確保每次狀態更動都是 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;
      ...
    }
  })
);

Immer


Test

用 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"');
  });
});

Jest @testing-library/react