其他章節請看:
本篇將完成登入模組
。效果和 spug 相同:
需求
如下:
主頁
,沒有登入的情況下存取系統會重定向到登入頁,登入成功後再次回到之前的頁面
。系統對談過期後,請求會重定向到登入頁。Tip:退出登入
在進入系統後進行,暫不不管。
登入頁是進入系統的門戶
,登入頁繪製邏輯比較簡單(單個模組的開發比較簡單)。
首先要解決
:根據 url 不同,進入登入頁
還是系統
主頁。這裡需要使用路由器。
詳情請看 react 路由、react 路由原理
Tip:實現的核心是 Router,以及 history 包。
需求
:瀏覽器輸入 /(http://localhost:3010/
) 進入登入頁,其他路徑進入系統。
實現如下:
<Router history={history}>
管理路由:// spug\src\index.js
import { history, updatePermissions } from 'libs';
// 許可權、token 相關
updatePermissions();
ReactDOM.render(
// Router 是路由器,用於管理路由
// `history: object` 用來導航的 history 物件。
<Router history={history}>
<ConfigProvider locale={zhCN} getPopupContainer={() => document.fullscreenElement || document.body}>
<App/>
</ConfigProvider>
</Router>,
document.getElementById('root')
);
其中 history
用於導航 history 物件(此用法在路由官網中)。執行 history.push 時不僅會改變瀏覽器的 url,而且路由也會發生變化(請看本篇「history={history} 的作用」章節)
// spug\src\libs\index.js
import _http from './http';
// 僅對 history 包的匯出
import _history from './history';
// 裡面有 updatePermissions
export * from './functools';
export * from './router';
export const http = _http;
export const history = _history;
export const VERSION = 'v3.0.5';
history 僅對 history 包的匯出,在 這裡 中已介紹。
/
則進入登入頁,否則進入系統( Layout 是 antd 中的 Layout 元件,對 404 的介面反饋也 Layout 模組中進行了處理)。// spug\src\App.js
class App extends Component {
render() {
return (
// 只渲染其中一個 Route
// exact 精確匹配
// component={Login} 路由元件(不同於一般元件,其 props 中有路由相關方法。)
<Switch>
<Route path="/" exact component={Login} />
{/* 沒有匹配則進入 Layout */}
<Route component={Layout} />
</Switch>
);
}
}
export default App;
<Router history={history}>
。Tip: StrictMode(一個用來突出顯示應用程式中潛在問題的工具。與 Fragment 一樣) 仍舊保留。
// myspug\src\index.js
import React from 'react';
+import { Router } from 'react-router-dom';
+import { history } from '@/libs';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
+ // StrictMode 是一個用來突出顯示應用程式中潛在問題的工具。與 Fragment 一樣,StrictMode 不會渲染任何可見的 UI。它為其後代元素觸發額外的檢查和警告。
+ // 嚴格模式檢查僅在開發模式下執行;它們不會影響生產構建。
<React.StrictMode>
- <ConfigProvider locale={zhCN}>
- <App />
- </ConfigProvider>
+ <Router history={history}>
+ <ConfigProvider locale={zhCN}>
+ <App />
+ </ConfigProvider>
+ </Router>
</React.StrictMode>
);
libs/index.js
,主要是匯出 history:// myspug\src\libs\index.js
import _http from './http';
import _history from './history';
export const http = _http;
export const history = _history;
export const VERSION = 'v1.0.0';
/
則進入登入頁,如果是其他 url 則進入 HelloWorld(用來模擬 Layout)// myspug\src\App.js
import { Component } from 'react';
// 登入元件
import Login from './pages/login';
// 模擬 Layout 元件
import HelloWorld from './HelloWord'
import { Switch, Route } from 'react-router-dom';
// 定義一個類元件
class App extends Component {
render() {
return (
// 只渲染其中一個 Route
// exact 精確匹配
// component={Login} 路由元件(不同於一般元件,其 props 中有路由相關方法。)
<Switch>
<Route path="/" exact component={Login} />
{/* 沒有匹配則進入 Layout */}
<Route component={HelloWorld} />
</Switch>
);
}
}
export default App;
// myspug\src\pages\login\index.js
export default function() {
return <div>登入頁</div>
}
// myspug\src\HelloWord.js
export default function HelloWorld() {
return <div>hello world!</div>
}
測試
結果如下:
瀏覽器:http://localhost:3000/
顯示: 登入頁
瀏覽器:http://localhost:3000/home
顯示: hello world!
在 請求資料 一文中我們曾有一個疑惑
:spug 官網中執行 history.push 不僅可以切換url,而且路由也發生了變化。
筆者測試發現:是入口頁 <Router history={history}>
中 history 的功勞。
驗證步驟如下:
// myspug\src\libs\http.js
import http from 'axios'
import history from './history'
// 將其匯出
window._history = history;
http://localhost:3000/
並在控制檯中輸入:執行:_history.push('/home')
url 變成 http://localhost:3000/home 瀏覽器顯示:hello world!
執行:_history.push('/')
url 變成 http://localhost:3000/ 瀏覽器顯示:登入頁
Tip:如果刪除入口頁的 history={history}
,瀏覽器控制檯將報錯如下,提示沒有 location 屬性,無法進行路由匹配:
Warning: Failed prop type: The prop `history` is marked as required in `Router`, but its value is `undefined`.
Uncaught TypeError: Cannot read property 'location' of undefined
我們初步解決了登入頁和主頁(或系統)之間的跳轉(或路由)。
下面我們完整分析 spug 中登入模組的實現,比如登入繪製、普通登入和LDAP登入...
登入模組程式碼都在 spug/src/pages/login
目錄下,一個 js 檔案,一個樣式檔案:
Administrator@-WK-10 MINGW /e/spug/src/pages/login
$ ls
bg.png index.js login.module.css
login.module.css 是登入模組的樣式檔案,前文 已分析過樣式,這裡不再冗餘。
登入的核心
全在 index.js 中。
我們參照登入介面說一下 index.js
的結構:
函數式的元件
,返回的 div 包括兩部分:登入資訊輸入區、網站底部統一資訊區Form.useForm
建立表單資料域進行控制語法
store
的初始化用於對應模組的使用驗證碼
倒計時// spug\src\pages\login\index.js
/**
* Copyright (c) OpenSpug Organization. https://github.com/openspug/spug
* Copyright (c) <[email protected]>
* Released under the AGPL-3.0 License.
*/
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Tabs, Modal, message } from 'antd';
import { UserOutlined, LockOutlined, CopyrightOutlined, GithubOutlined, MailOutlined } from '@ant-design/icons';
import styles from './login.module.css';
import history from 'libs/history';
import { http, updatePermissions } from 'libs';
// store 是 mobx 中的狀態集中器。這裡是初始化 pages 下的 config、deploy、exec、host等模組中的某欄位
import envStore from 'pages/config/environment/store';
import appStore from 'pages/config/app/store';
import requestStore from 'pages/deploy/request/store';
import execStore from 'pages/exec/task/store';
import hostStore from 'pages/host/store';
// 函陣列件
export default function () {
// FormInstance 經 Form.useForm() 建立的 form 控制範例。FormInstance 有一系列方法,例如
// 注:useForm 是 React Hooks 的實現,只能用於函陣列件,class 元件請檢視下面的例子(https://ant.design/components/form-cn#components-form-demo-control-hooks)
// Tip:我們推薦使用 Form.useForm 建立表單資料域進行控制。如果是在 class component 下,你也可以通過 ref 獲取資料域。(https://ant.design/components/form-cn#components-form-demo-control-ref)
const [form] = Form.useForm();
// 驗證碼倒計時
const [counter, setCounter] = useState(0);
// 控制登入按鈕
const [loading, setLoading] = useState(false);
// 登入型別預設是 default
const [loginType, setLoginType] = useState('default');
// 驗證碼。預設關閉
const [codeVisible, setCodeVisible] = useState(!false);
const [codeLoading, setCodeLoading] = useState(false);
// 元件掛載後執行。相當於 componentDidMount()
useEffect(() => {
envStore.records = [];
appStore.records = [];
requestStore.records = [];
requestStore.deploys = [];
hostStore.records = null;
hostStore.groups = {};
hostStore.treeData = [];
execStore.hosts = [];
}, [])
// 相當於 componentDidMount() 和 componentDidUpdate()(counter 變化時會執行)
// 定時器,重新獲取驗證碼倒計時。
useEffect(() => {
setTimeout(() => {
// 預設是 0,故不會執行。當設定有效值時會執行,例如 30
if (counter > 0) {
setCounter(counter - 1)
}
}, 1000)
}, [counter])
// 登入
function handleSubmit() {
// form 是 FormInstance。
// getFieldsValue - 獲取一組欄位名對應的值,會按照對應結構返回
const formData = form.getFieldsValue();
// 如果顯示了「驗證碼」卻沒有輸入,提示
if (codeVisible && !formData.captcha) return message.error('請輸入驗證碼');
// 登入中...
setLoading(true);
// 設定登入型別:default 或 ldap
formData['type'] = loginType;
// formData2 {username: '1', password: '2', captcha: '3', type: 'default'}
console.log('formData2', formData)
http.post('/api/account/login/', formData)
// 官網返回: {"data": {"id": 1, "access_token": "4b6f1a9b8d824908abb9613695de57f8", "nickname": "\u7ba1\u7406\u5458", "is_supper": true, "has_real_ip": true, "permissions": []}, "error": ""}
.then(data => {
// 某種處理邏輯
if (data['required_mfa']) {
setCodeVisible(true);
setCounter(30);
setLoading(false)
// 使用者請求時沒有真實ip則安全警告
} else if (!data['has_real_ip']) {
Modal.warning({
title: '安全警告',
className: styles.tips,
content: <div>
未能獲取到存取者的真實IP,無法提供基於請求來源IP的合法性驗證,詳細資訊請參考
<a target="_blank"
href="https://spug.cc/docs/practice/"
rel="noopener noreferrer">官方檔案</a>。
</div>,
onOk: () => doLogin(data)
})
} else {
doLogin(data)
}
}, () => setLoading(false))
}
// 將登入返回的資料存入本地,並更新許可權和 token
function doLogin(data) {
// id
localStorage.setItem('id', data['id']);
// token
localStorage.setItem('token', data['access_token']);
// 暱稱
localStorage.setItem('nickname', data['nickname']);
// is_supper
localStorage.setItem('is_supper', data['is_supper']);
// 許可權
localStorage.setItem('permissions', JSON.stringify(data['permissions']));
// 許可權和 token 相關。
updatePermissions();
// 登入成功則進入系統主頁或未登入前存取的頁面
// 更具體就是:切換 Url。進入主頁或登入前的頁面(記錄在 from 中)
// react通過history.location.state來攜帶引數
// 例如 spug\src\libs\http.js 中的:history.push('/', {from: history.location})
if (history.location.state && history.location.state['from']) {
history.push(history.location.state['from'])
} else {
history.push('/home')
}
}
// 獲取驗證碼
function handleCaptcha() {
// 請求中...
setCodeLoading(true);
const formData = form.getFieldsValue(['username', 'password']);
formData['type'] = loginType;
// formData {username: '1', password: '2', type: 'default'}
console.log('formData', formData)
http.post('/api/account/login/', formData)
// 30 秒後獲得驗證碼
.then(() => setCounter(30))
.finally(() => setCodeLoading(false))
}
return (
<div className={styles.container}>
<div className={styles.formContainer}>
{/* 僅做樣式,預設選中第一個 tabpane。沒有索引標籤內容 */}
<Tabs className={styles.tabs} onTabClick={v => setLoginType(v)}>
<Tabs.TabPane tab="普通登入" key="default" />
<Tabs.TabPane tab="LDAP登入" key="ldap" />
</Tabs>
{/* 使用 Form.useForm 建立表單資料域進行控制 */}
<Form form={form}>
<Form.Item name="username" className={styles.formItem}>
<Input
size="large"
// 關閉自動完成的選項
autoComplete="off"
placeholder="請輸入賬戶"
// 人頭像的 icon
prefix={<UserOutlined className={styles.icon} />} />
</Form.Item>
<Form.Item name="password" className={styles.formItem}>
<Input
size="large"
type="password"
autoComplete="off"
placeholder="請輸入密碼"
// 按下回車的回撥。即提交
onPressEnter={handleSubmit}
// 鎖的icon
prefix={<LockOutlined className={styles.icon} />} />
</Form.Item>
{/* 驗證碼。預設關閉 */}
{/* 這裡展示了 Form.Item 巢狀用法 */}
<Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}>
<div style={{ display: 'flex' }}>
<Form.Item noStyle name="captcha">
<Input
size="large"
autoComplete="off"
placeholder="請輸入驗證碼"
prefix={<MailOutlined className={styles.icon} />} />
</Form.Item>
{counter > 0 ? (
<Button disabled size="large" style={{ marginLeft: 8 }}>{counter} 秒後重新獲取</Button>
) : (
<Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
onClick={handleCaptcha}>獲取驗證碼</Button>
)}
</div>
</Form.Item>
</Form>
<Button
// block 屬性將使按鈕適合其父寬度。
block
size="large"
type="primary"
className={styles.button}
loading={loading}
onClick={handleSubmit}>登入</Button>
</div>
{/* 網站底部統一資訊。這裡是`官網`、`github 地址`、`檔案` */}
<div className={styles.footerZone}>
<div className={styles.linksZone}>
<a className={styles.links} title="官網" href="https://spug.cc" target="_blank"
rel="noopener noreferrer">官網</a>
<a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined /></a>
<a title="檔案" href="https://spug.cc/docs/about-spug/" target="_blank"
rel="noopener noreferrer">檔案</a>
</div>
<div style={{ color: '#fff' }}>Copyright <CopyrightOutlined /> {new Date().getFullYear()} By Spug</div>
</div>
</div>
)
}
Tip:登入樣式(pages\login\login.module.css
)僅僅是一些樣式,直接從 spug 拷貝即可
新建 pages\login\index.js
檔案,內容如下:
Tip: 與 spug 中 login\index.js 類似,微做如下調整:
@/libs/history
// myspug\src\pages\login\index.js
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Tabs, Modal, message } from 'antd';
import { UserOutlined, LockOutlined, CopyrightOutlined, GithubOutlined, MailOutlined } from '@ant-design/icons';
import styles from './login.module.css';
// 調整下參照路徑:libs/history 改成 @/libs/history
import history from '@/libs/history';
import { http, updatePermissions } from '@/libs';
// 函陣列件
export default function () {
// antd 官網:我們推薦使用 Form.useForm 建立表單資料域進行控制。如果是在 class component 下,你也可以通過 ref 獲取資料域。(https://ant.design/components/form-cn#components-form-demo-control-ref)
// FormInstance 經 Form.useForm() 建立的 form 控制範例。FormInstance 有一系列方法,例如
// 注:useForm 是 React Hooks 的實現,只能用於函陣列件,class 元件請檢視下面的例子(https://ant.design/components/form-cn#components-form-demo-control-hooks)
const [form] = Form.useForm();
// 驗證碼倒計時
const [counter, setCounter] = useState(0);
// 控制登入按鈕
const [loading, setLoading] = useState(false);
// 登入型別預設是 default
const [loginType, setLoginType] = useState('default');
// 驗證碼。預設關閉。筆者將其開啟
const [codeVisible, setCodeVisible] = useState(!false);
const [codeLoading, setCodeLoading] = useState(false);
// 相當於 componentDidMount() 和 componentDidUpdate()(counter 變化時會執行)
// 定時器,重新獲取驗證碼倒計時。
useEffect(() => {
setTimeout(() => {
// 預設是 0,故不會執行。當設定有效值時會執行,例如 30
if (counter > 0) {
setCounter(counter - 1)
}
}, 1000)
}, [counter])
// 登入
function handleSubmit() {
// getFieldsValue - 獲取一組欄位名對應的值,會按照對應結構返回
// form 是 FormInstance。
const formData = form.getFieldsValue();
// 如果顯示了「驗證碼」卻沒有輸入,提示
if (codeVisible && !formData.captcha) return message.error('請輸入驗證碼');
// 登入中...
setLoading(true);
// 設定登入型別:default 或 ldap
formData['type'] = loginType;
// formData2 {username: '1', password: '2', captcha: '3', type: 'default'}
console.log('formData2', formData)
http.post('/api/account/login/', formData)
// 官網返回: {"data": {"id": 1, "access_token": "4b6f1a9b8d824908abb9613695de57f8", "nickname": "\u7ba1\u7406\u5458", "is_supper": true, "has_real_ip": true, "permissions": []}, "error": ""}
.then(data => {
// 某種處理邏輯,我們可以去除這個分支
if (data['required_mfa']) {
setCodeVisible(true);
setCounter(30);
setLoading(false)
} else if (!data['has_real_ip']) { // 使用者請求時沒有真實ip則安全警告
Modal.warning({
title: '安全警告',
className: styles.tips,
content: <div>
未能獲取到存取者的真實IP,無法提供基於請求來源IP的合法性驗證,詳細資訊請參考
<a target="_blank"
href="https://spug.cc/docs/practice/"
rel="noopener noreferrer">官方檔案</a>。
</div>,
onOk: () => doLogin(data)
})
} else {
doLogin(data)
}
}, () => setLoading(false))
}
// 將登入返回的資料存入本地,並更新許可權和 token
function doLogin(data) {
// id
localStorage.setItem('id', data['id']);
// token
localStorage.setItem('token', data['access_token']);
// 暱稱
localStorage.setItem('nickname', data['nickname']);
// is_supper
localStorage.setItem('is_supper', data['is_supper']);
// 許可權
localStorage.setItem('permissions', JSON.stringify(data['permissions']));
// 許可權和 token 相關。
updatePermissions();
// 登入成功則進入系統主頁或未登入前存取的頁面
// 更具體就是:切換 Url。進入主頁或登入前的頁面(記錄在 from 中)
// react通過history.location.state來攜帶引數
// 例如 spug\src\libs\http.js 中的:history.push('/', {from: history.location})
if (history.location.state && history.location.state['from']) {
history.push(history.location.state['from'])
} else {
history.push('/home')
}
}
// 獲取驗證碼
function handleCaptcha() {
// 請求中...
setCodeLoading(true);
const formData = form.getFieldsValue(['username', 'password']);
formData['type'] = loginType;
// formData {username: '1', password: '2', type: 'default'}
console.log('formData', formData)
http.post('/api/account/login/', formData)
// 30 秒後獲得驗證碼
.then(() => setCounter(30))
.finally(() => setCodeLoading(false))
}
return (
<div className={styles.container}>
<div className={styles.formContainer}>
{/* 僅做樣式,預設選中第一個 tabpane。沒有索引標籤內容 */}
<Tabs className={styles.tabs} onTabClick={v => setLoginType(v)}>
<Tabs.TabPane tab="普通登入" key="default" />
<Tabs.TabPane tab="LDAP登入" key="ldap" />
</Tabs>
{/* 使用 Form.useForm 建立表單資料域進行控制 */}
<Form form={form}>
<Form.Item name="username" className={styles.formItem}>
<Input
size="large"
// 關閉自動完成的選項
autoComplete="off"
placeholder="請輸入賬戶"
// 人頭像的 icon
prefix={<UserOutlined className={styles.icon} />} />
</Form.Item>
<Form.Item name="password" className={styles.formItem}>
<Input
size="large"
type="password"
autoComplete="off"
placeholder="請輸入密碼"
// 按下回車的回撥。即提交
onPressEnter={handleSubmit}
// 鎖的icon
prefix={<LockOutlined className={styles.icon} />} />
</Form.Item>
{/* 驗證碼。預設關閉 */}
{/* 這裡展示了 Form.Item 巢狀用法 */}
<Form.Item hidden={!codeVisible} name="captcha" className={styles.formItem}>
<div style={{ display: 'flex' }}>
<Form.Item noStyle name="captcha">
<Input
size="large"
autoComplete="off"
placeholder="請輸入驗證碼"
prefix={<MailOutlined className={styles.icon} />} />
</Form.Item>
{counter > 0 ? (
<Button disabled size="large" style={{ marginLeft: 8 }}>{counter} 秒後重新獲取</Button>
) : (
<Button size="large" loading={codeLoading} style={{ marginLeft: 8 }}
onClick={handleCaptcha}>獲取驗證碼</Button>
)}
</div>
</Form.Item>
</Form>
<Button
// block 屬性將使按鈕適合其父寬度。
block
size="large"
type="primary"
className={styles.button}
loading={loading}
onClick={handleSubmit}>登入</Button>
</div>
{/* 網站底部統一資訊。這裡是`官網`、`github 地址`、`檔案` */}
<div className={styles.footerZone}>
<div className={styles.linksZone}>
<a className={styles.links} title="官網" href="https://spug.cc" target="_blank"
rel="noopener noreferrer">官網</a>
<a className={styles.links} title="Github" href="https://github.com/openspug/spug" target="_blank"
rel="noopener noreferrer"><GithubOutlined /></a>
<a title="檔案" href="https://spug.cc/docs/about-spug/" target="_blank"
rel="noopener noreferrer">檔案</a>
</div>
<div style={{ color: '#fff' }}>Copyright <CopyrightOutlined /> {new Date().getFullYear()} By Spug</div>
</div>
</div>
)
}
登入頁中引入了 updatePermissions(import { http, updatePermissions } from '@/libs';
)。
Tip:updatePermissions 的作用用於更新 functools.js 模組中的 X_TOKEN(spug中沒有前端沒有清除 X_TOKEN) 和 Permission變數。
我們將 spug 中的相關程式碼弄過來。步驟如下:
// myspug\src\libs\functools.js
+// 准許。許可權相關。模組私有
+let Permission = {
+ isReady: false,
+ isSuper: false,
+ permissions: []
+};
+
// 由 updatePermissions() 更新
export let X_TOKEN;
+
+
+// 被入口頁(src/index.js)和登入頁(src/pages/login/index.js)呼叫
+export function updatePermissions() {
+ // 讀取 localStorage 項
+ // 只在登入時設定:localStorage.setItem('token'
+ X_TOKEN = localStorage.getItem('token');
+ Permission.isReady = true;
+ Permission.isSuper = localStorage.getItem('is_supper') === 'true';
+ try {
+ Permission.permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
+ } catch (e) {
+
+ }
+}
// myspug\src\libs\index.js
// 匯出一切。注:沒有匯出預設值
export * from './functools';
// myspug\src\index.js
import React from 'react';
import { history, updatePermissions } from '@/libs';
const root = ReactDOM.createRoot(document.getElementById('root'));
+ // 許可權和 token 相關。
+ updatePermissions();
驗證步驟如下:
http://localhost:3000/
進入登入頁在登入頁輸入登入資訊,登入成功進入主頁
修改瀏覽器 url(http://localhost:3000/log
)回車進入系統,控制檯執行 _history.push('/', { from: _history.location })
模擬請求過期重置到登入頁,再次輸入登入資訊登入,回到原來頁面(log)
myspug 登入頁有一個小bug,Tabs 下沒有顯示哪個選中了。就像這樣:
發現是選中的進度條沒有動態設定寬度,width 一直為 0。懷疑是 myspug 中 antd-按需引入-css 有問題,但 antd 其他元件(例如分頁、form等)沒有問題,Tabs也僅發現這一個樣式問題,去除按需引入 css 也沒解決。
筆者暫時未深入,或許簡化環境,從頭開始可以找到問題
spug 前端這裡普通登入
和 LDAP 登入
是相同處理的,都是輸入使用者名稱和密碼。
只要公司給員工分配了 LDAP(可實現公司內部多系統的統一登入) 的使用者名稱和密碼,該員工則可直接使用 LDAP 方式登入系統,無需再重複註冊。
spug 中輸入使用者名稱、密碼,登入成功後,後端返回資料中包含 token(即後端分配給使用者的一個登入標識
),前端將其儲存在 localStorage 中,後續前端所有的請求都將會帶上這個標識(token),後端通過這個標識識別使用者否有許可權存取該請求,如果 token 過期,則返回 401 告訴前端「對談過期,請重新登入」。
spug 中有個模組(functools.js),定義了一個私有變數,匯出了兩個變數。
// myspug\src\libs\functools.js
let Permission = {
isReady: false,
...
};
// 由 updatePermissions() 更新
export let X_TOKEN;
export function updatePermissions() {
X_TOKEN = localStorage.getItem('token');
Permission.isReady = true;
}
在登入模組中僅匯入 updatePermissions,登入成功後會執行該方法,會給 X_TOKEN 賦值。而在其他模組(例如 http.js)僅匯入 X_TOKEN,這時 X_TOKEN 就會有值。
筆者測試如下:
// myspug\src\HelloWord.js
import { X_TOKEN } from "./libs/functools"
export default function HelloWorld() {
return <div>hello world!。token = {X_TOKEN}</div>
}
hello world!。token = xxxxxxxx...
其他章節請看: