nsbp-cli 0.2.33 → 0.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/templates/basic/README.md +15 -5
- package/templates/basic/docs/DEVELOPMENT_GUIDE.md +7 -2
- package/templates/basic/src/Routers.tsx +2 -1
- package/templates/basic/src/client/index.tsx +6 -6
- package/templates/basic/src/component/Layout.tsx +4 -4
- package/templates/basic/src/containers/Home.tsx +17 -6
- package/templates/basic/src/containers/Photo.tsx +32 -14
- package/templates/basic/src/reducers/photo.ts +1 -1
- package/templates/basic/src/server/index.ts +1 -5
- package/templates/basic/src/server/photo.ts +7 -0
- package/templates/basic/src/server/utils.tsx +5 -4
- package/templates/basic/src/services/photo.ts +9 -1
- package/templates/basic/src/store/index.ts +1 -2
- package/templates/basic/src/utils/index.ts +66 -6
- package/templates/basic/src/utils/usePreserveNSBP.ts +58 -0
package/README.md
CHANGED
|
@@ -147,11 +147,11 @@ node ./bin/nsbp.js --help # Test CLI locally
|
|
|
147
147
|
|
|
148
148
|
- **Package Name**: `nsbp-cli`
|
|
149
149
|
- **Bin Command**: `nsbp` (install globally and run `nsbp --help`)
|
|
150
|
-
- **Version**: `0.2.
|
|
150
|
+
- **Version**: `0.2.35`
|
|
151
151
|
- **Dependencies**: chalk, commander, fs-extra, inquirer
|
|
152
152
|
- **Package Manager**: Uses pnpm (also compatible with npm)
|
|
153
153
|
- **Node Version**: >=16.0.0
|
|
154
154
|
|
|
155
155
|
## License
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
MIT
|
package/package.json
CHANGED
|
@@ -144,14 +144,24 @@ docker-compose exec app env | grep NODE_ENV
|
|
|
144
144
|
|
|
145
145
|
### 本地访问
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
**服务端渲染**(默认,对 SEO 友好)
|
|
148
|
+
```
|
|
148
149
|
http://localhost:3001/
|
|
150
|
+
```
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
+
**客户端渲染**(禁用 SSR)
|
|
153
|
+
```
|
|
154
|
+
http://localhost:3001/?nsbp=0
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**服务端渲染回退**(如果 SSR 失败,自动回退到客户端渲染)
|
|
158
|
+
```
|
|
159
|
+
http://localhost:3001/?nsbp=1&from=link
|
|
160
|
+
```
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
162
|
+
> **参数说明**:`nsbp` 参数控制渲染模式
|
|
163
|
+
> - `nsbp=1` 或省略:服务端渲染(SSR,默认)
|
|
164
|
+
> - `nsbp=0`:客户端渲染(CSR)
|
|
155
165
|
|
|
156
166
|
## Docker 部署
|
|
157
167
|
|
|
@@ -25,10 +25,15 @@ pnpm run dev:build:start # 启动服务器
|
|
|
25
25
|
|
|
26
26
|
### 3. 访问应用
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
-
|
|
28
|
+
- **服务端渲染**(默认,SEO 友好): http://localhost:3001/
|
|
29
|
+
- **客户端渲染**(禁用 SSR): http://localhost:3001/?nsbp=0
|
|
30
|
+
- **服务端渲染回退**(SSR 失败时回退到 CSR): http://localhost:3001/?nsbp=1&from=link
|
|
30
31
|
- **BrowserSync**: http://localhost:3000/
|
|
31
32
|
|
|
33
|
+
> **参数说明**:`nsbp` 参数控制渲染模式
|
|
34
|
+
> - `nsbp=1` 或省略:服务端渲染(SSR,默认)
|
|
35
|
+
> - `nsbp=0`:客户端渲染(CSR)
|
|
36
|
+
|
|
32
37
|
## 📝 开发工作流
|
|
33
38
|
|
|
34
39
|
### 提交代码
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { loadData as homeLoadData } from '@services/home'
|
|
3
|
+
import { loadData as photoLoadData } from '@services/photo'
|
|
3
4
|
import loadable from '@loadable/component'
|
|
4
5
|
|
|
5
6
|
const Loading = () => {
|
|
@@ -33,7 +34,7 @@ export default [
|
|
|
33
34
|
{
|
|
34
35
|
path: '/photo',
|
|
35
36
|
element: <Photo />,
|
|
36
|
-
loadData:
|
|
37
|
+
loadData: photoLoadData, // 使用 photo 的 loadData 来预取图片数据
|
|
37
38
|
key: 'photo'
|
|
38
39
|
}
|
|
39
40
|
]
|
|
@@ -9,13 +9,13 @@ import Theme from '@components/Theme'
|
|
|
9
9
|
import { loadableReady } from '@loadable/component'
|
|
10
10
|
|
|
11
11
|
const App = () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
setStore(getStore(window?.context?.state))
|
|
12
|
+
// 优先使用服务端预取的状态初始化 store
|
|
13
|
+
const [store, setStore] = useState(() => {
|
|
14
|
+
if (isSEO() && window?.context?.state) {
|
|
15
|
+
return getStore(window.context.state)
|
|
17
16
|
}
|
|
18
|
-
|
|
17
|
+
return getStore()
|
|
18
|
+
})
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
21
|
<Theme>
|
|
@@ -4,17 +4,17 @@ import Loading from './Loading'
|
|
|
4
4
|
|
|
5
5
|
interface LayoutProps {
|
|
6
6
|
children: React.ReactNode
|
|
7
|
-
query?: {
|
|
7
|
+
query?: { nsbp?: string | number }
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const Layout = ({ children, query }: LayoutProps) => {
|
|
11
|
-
let
|
|
11
|
+
let nsbp: string | number | undefined
|
|
12
12
|
if (query !== undefined && query !== null) {
|
|
13
|
-
|
|
13
|
+
nsbp = query.nsbp
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const [pageLoad, setPageLoad] = useState(
|
|
17
|
-
|
|
17
|
+
nsbp !== undefined ? parseInt(String(nsbp), 10) : 0
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
useEffect(() => {
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
|
|
2
2
|
import { Link } from 'react-router-dom'
|
|
3
3
|
import Layout from '@components/Layout'
|
|
4
4
|
import { Helmet } from 'react-helmet'
|
|
5
|
+
import { isSEO, usePreserveNSBP } from '@/utils'
|
|
5
6
|
import {
|
|
6
7
|
GlobalStyle,
|
|
7
8
|
PageWrapper,
|
|
@@ -66,20 +67,23 @@ interface PhotoMenuItem {
|
|
|
66
67
|
const Home: React.FC = () => {
|
|
67
68
|
const [menu, setMenu] = useState<PhotoMenuItem[]>([])
|
|
68
69
|
const [loading, setLoading] = useState(true)
|
|
70
|
+
const { withNSBP } = usePreserveNSBP()
|
|
69
71
|
|
|
70
72
|
useEffect(() => {
|
|
71
73
|
if (typeof window === 'undefined') return
|
|
72
74
|
|
|
73
75
|
setLoading(true)
|
|
76
|
+
|
|
77
|
+
// 检查是否为客户端渲染模式
|
|
78
|
+
const isClientMode = isSEO() === 0
|
|
79
|
+
|
|
74
80
|
// 先检查服务端是否已预取了图片菜单数据
|
|
75
81
|
const serverMenu = window?.context?.state?.photo?.menu || {}
|
|
76
82
|
const serverMenuArray = Array.isArray(serverMenu) ? serverMenu : []
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} else {
|
|
82
|
-
// 如果服务端没有预取,则在客户端获取
|
|
84
|
+
// 客户端渲染模式下,始终发起新请求;服务端渲染模式下,如果有预取数据则使用预取数据
|
|
85
|
+
if (isClientMode || serverMenuArray.length === 0) {
|
|
86
|
+
// 客户端获取数据
|
|
83
87
|
fetch('/getPhotoMenu')
|
|
84
88
|
.then((res) => {
|
|
85
89
|
if (!res.ok) throw new Error(`Status ${res.status}`)
|
|
@@ -93,6 +97,10 @@ const Home: React.FC = () => {
|
|
|
93
97
|
setMenu([])
|
|
94
98
|
})
|
|
95
99
|
.finally(() => setLoading(false))
|
|
100
|
+
} else {
|
|
101
|
+
// 使用服务端预取的数据
|
|
102
|
+
setMenu(serverMenuArray)
|
|
103
|
+
setLoading(false)
|
|
96
104
|
}
|
|
97
105
|
}, [])
|
|
98
106
|
|
|
@@ -467,7 +475,10 @@ export const getPhotoMenu = (req: any, res: any) => {
|
|
|
467
475
|
) : menu.length > 0 ? (
|
|
468
476
|
<PhotoGrid>
|
|
469
477
|
{menu.map((item) => (
|
|
470
|
-
<Link
|
|
478
|
+
<Link
|
|
479
|
+
key={item.name}
|
|
480
|
+
to={withNSBP(`/photo?dic=${item.name}`)}
|
|
481
|
+
>
|
|
471
482
|
<PhotoCard>
|
|
472
483
|
<PhotoImageWrapper>
|
|
473
484
|
<PhotoImage
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { Fragment, useState, useEffect } from 'react'
|
|
1
|
+
import React, { Fragment, useState, useEffect, useRef } from 'react'
|
|
2
2
|
import { connect } from 'react-redux'
|
|
3
3
|
import { Link, useLocation } from 'react-router-dom'
|
|
4
4
|
import Header from '@components/Header'
|
|
@@ -6,10 +6,10 @@ import Layout from '@components/Layout'
|
|
|
6
6
|
import { Helmet } from 'react-helmet'
|
|
7
7
|
import { Container, Row } from '@styled/photo'
|
|
8
8
|
import { motion } from 'framer-motion'
|
|
9
|
-
import { isSEO, getLocationParams } from '@/utils'
|
|
9
|
+
import { isSEO, getLocationParams, usePreserveNSBP } from '@/utils'
|
|
10
10
|
import { useCurrentFlag } from '@utils/clientConfig'
|
|
11
11
|
import _ from 'lodash'
|
|
12
|
-
import {
|
|
12
|
+
import { loadDataForContainer } from '@services/photo'
|
|
13
13
|
|
|
14
14
|
const springSettings = { type: 'spring' as const, stiffness: 170, damping: 26 }
|
|
15
15
|
const NEXT = 'show-next'
|
|
@@ -17,8 +17,11 @@ const NEXT = 'show-next'
|
|
|
17
17
|
const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
18
18
|
const location = useLocation()
|
|
19
19
|
let { from } = query
|
|
20
|
+
const { withNSBP } = usePreserveNSBP()
|
|
20
21
|
const photos = Array.isArray(data) ? data : []
|
|
21
22
|
const [currPhoto, setCurrPhoto] = useState(0)
|
|
23
|
+
// 使用 ref 来跟踪初始的 dic 值,用于区分首次加载和分类切换
|
|
24
|
+
const initialDicRef = useRef<string | null>(null)
|
|
22
25
|
|
|
23
26
|
const [currPhotoData, setCurrPhotoData] = useState(photos[0] || [0, 0, ''])
|
|
24
27
|
|
|
@@ -70,23 +73,35 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
useEffect(() => {
|
|
73
|
-
const currentDic = getLocationParams('dic')
|
|
76
|
+
const currentDic = getLocationParams('dic') || ''
|
|
77
|
+
|
|
78
|
+
// 初始化时记录初始 dic 值
|
|
79
|
+
if (initialDicRef.current === null) {
|
|
80
|
+
initialDicRef.current = currentDic
|
|
81
|
+
}
|
|
74
82
|
|
|
75
83
|
const doGetPhotoMenu = () => {
|
|
76
84
|
getPhotoMenu(currentDic)
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
|
|
87
|
+
// 判断是否需要加载数据:
|
|
88
|
+
// 1. 客户端渲染模式(isSEO() === 0)- 总是加载
|
|
89
|
+
// 2. 服务端渲染模式:
|
|
90
|
+
// - 如果没有数据(hasNoData)- 需要加载
|
|
91
|
+
// - 如果分类切换(isCategoryChanged)- 需要加载
|
|
92
|
+
const isClientMode = isSEO() === 0
|
|
93
|
+
const hasNoData = !data || data.length === 0
|
|
94
|
+
const isCategoryChanged = currentDic !== initialDicRef.current
|
|
95
|
+
|
|
96
|
+
// 客户端渲染模式:总是需要加载数据
|
|
97
|
+
// 服务端渲染模式:只有在没有数据或分类切换时才加载
|
|
98
|
+
if (isClientMode || hasNoData || isCategoryChanged) {
|
|
80
99
|
doGetPhotoMenu()
|
|
81
|
-
} else {
|
|
82
|
-
if (from === 'link') {
|
|
83
|
-
doGetPhotoMenu()
|
|
84
|
-
}
|
|
85
100
|
}
|
|
86
101
|
|
|
87
102
|
// 重置到第一张
|
|
88
103
|
setCurrPhoto(0)
|
|
89
|
-
}, [location?.search])
|
|
104
|
+
}, [location?.search, from])
|
|
90
105
|
|
|
91
106
|
return (
|
|
92
107
|
<Fragment>
|
|
@@ -101,7 +116,10 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
101
116
|
<Row>
|
|
102
117
|
{_.map(menu, (item: any, index: number) => {
|
|
103
118
|
return (
|
|
104
|
-
<Link
|
|
119
|
+
<Link
|
|
120
|
+
key={`menu${index}`}
|
|
121
|
+
to={withNSBP(`/photo?dic=${item.name}`)}
|
|
122
|
+
>
|
|
105
123
|
{item.name}
|
|
106
124
|
</Link>
|
|
107
125
|
)
|
|
@@ -131,9 +149,9 @@ const Photo = ({ query, data, menu, getPhotoMenu }: any) => {
|
|
|
131
149
|
className="demo4-photo"
|
|
132
150
|
src={
|
|
133
151
|
photos[i][2]
|
|
134
|
-
?
|
|
152
|
+
? isSEO() === 1
|
|
135
153
|
? `/images/${photos[i][2]}`
|
|
136
|
-
: photos[i][2]
|
|
154
|
+
: `/images/${photos[i][2]}`
|
|
137
155
|
: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='
|
|
138
156
|
}
|
|
139
157
|
initial={false}
|
|
@@ -167,7 +185,7 @@ const mapStateToProps = (state: any) => {
|
|
|
167
185
|
|
|
168
186
|
const mapDispatchToProps = (dispatch: any) => ({
|
|
169
187
|
getPhotoMenu: (dic: any) => {
|
|
170
|
-
dispatch(
|
|
188
|
+
dispatch(loadDataForContainer(null, dic))
|
|
171
189
|
}
|
|
172
190
|
})
|
|
173
191
|
|
|
@@ -41,7 +41,6 @@ if (process.env.ENABLE_RATE_LIMIT === '1') {
|
|
|
41
41
|
legacyHeaders: false // Disable the `X-RateLimit-*` headers
|
|
42
42
|
})
|
|
43
43
|
app.use('/api', limiter)
|
|
44
|
-
console.log('🛡️ Rate limiting enabled for /api routes')
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
// 4. Static file serving (disable dotfiles access)
|
|
@@ -79,15 +78,12 @@ app.get('/getPhotoMenu', (req, res) => {
|
|
|
79
78
|
|
|
80
79
|
// Catch-all middleware for SSR
|
|
81
80
|
app.use((req, res) => {
|
|
82
|
-
// console.log('req.url', req.url, req.headers)
|
|
83
81
|
render(req, res)
|
|
84
82
|
})
|
|
85
83
|
|
|
86
84
|
const PORT = process.env.PORT || 3001
|
|
87
85
|
app.listen(PORT, () => {
|
|
88
|
-
console.log(`Server listening on port ${PORT}`)
|
|
89
|
-
console.log(`🔒 Security headers enabled`)
|
|
90
86
|
if (process.env.ENABLE_RATE_LIMIT === '1') {
|
|
91
|
-
|
|
87
|
+
// Rate limiting active
|
|
92
88
|
}
|
|
93
89
|
})
|
|
@@ -85,6 +85,13 @@ export const getPhotoWH = (req: any, res: any) => {
|
|
|
85
85
|
return res.status(403).json({ error: 'Access denied' })
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// 检查目录是否存在
|
|
89
|
+
if (!fs.existsSync(photoPath)) {
|
|
90
|
+
return res
|
|
91
|
+
.status(404)
|
|
92
|
+
.json({ error: 'Directory not found', details: photoPath })
|
|
93
|
+
}
|
|
94
|
+
|
|
88
95
|
const getFileList = (dir: string, list: string[]) => {
|
|
89
96
|
const arr = fs.readdirSync(dir)
|
|
90
97
|
arr.forEach((item) => {
|
|
@@ -19,10 +19,10 @@ export const render = (req: any, res: any) => {
|
|
|
19
19
|
const matchRoutes: any = []
|
|
20
20
|
const promises = []
|
|
21
21
|
|
|
22
|
-
let {
|
|
22
|
+
let { nsbp } = query
|
|
23
23
|
|
|
24
|
-
if (
|
|
25
|
-
|
|
24
|
+
if (nsbp !== undefined && nsbp !== '') {
|
|
25
|
+
nsbp = parseInt(nsbp, 10)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
routers.some((route) => {
|
|
@@ -33,7 +33,8 @@ export const render = (req: any, res: any) => {
|
|
|
33
33
|
if (item?.loadData) {
|
|
34
34
|
const promise = new Promise((resolve, reject) => {
|
|
35
35
|
try {
|
|
36
|
-
|
|
36
|
+
// 将 query 参数传递给 loadData,确保能正确预取数据
|
|
37
|
+
store.dispatch(item?.loadData(resolve, query))
|
|
37
38
|
} catch (e) {
|
|
38
39
|
reject()
|
|
39
40
|
}
|
|
@@ -57,6 +57,14 @@ const getData = (callback: any, dic: any) => {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// 用于路由预取数据的 loadData 函数
|
|
61
|
+
export const loadData = (resolve: any = null, query: any = {}) => {
|
|
62
|
+
// 从 URL 查询参数中获取 dic
|
|
63
|
+
const { dic } = query
|
|
64
|
+
return getData(resolve, dic || '')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 用于容器内部调用的 loadData 函数(保持向后兼容)
|
|
68
|
+
export const loadDataForContainer = (resolve: any = null, dic = '') => {
|
|
61
69
|
return getData(resolve, dic)
|
|
62
70
|
}
|
|
@@ -5,8 +5,7 @@ const combineReducer = combineReducers({ ...reducers })
|
|
|
5
5
|
|
|
6
6
|
const getStore = (stateParam = {}) => {
|
|
7
7
|
return configureStore({
|
|
8
|
-
reducer:
|
|
9
|
-
combineReducer(state || stateParam, action),
|
|
8
|
+
reducer: combineReducer,
|
|
10
9
|
preloadedState: stateParam || {},
|
|
11
10
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware()
|
|
12
11
|
})
|
|
@@ -13,15 +13,73 @@ export const getLocationParams = (param: string) => {
|
|
|
13
13
|
|
|
14
14
|
export const isSEO = () => {
|
|
15
15
|
if (typeof window !== 'undefined') {
|
|
16
|
-
const
|
|
17
|
-
if (
|
|
18
|
-
return parseInt(
|
|
16
|
+
const nsbp = getLocationParams('nsbp')
|
|
17
|
+
if (nsbp !== '') {
|
|
18
|
+
return parseInt(nsbp, 10)
|
|
19
19
|
}
|
|
20
|
-
return
|
|
20
|
+
return 1
|
|
21
21
|
}
|
|
22
|
-
return
|
|
22
|
+
return 1
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* 获取当前的 nsbp 参数值
|
|
27
|
+
* @returns nsbp 参数值,如果没有则返回空字符串
|
|
28
|
+
*/
|
|
29
|
+
export const getNSBPParam = () => {
|
|
30
|
+
return getLocationParams('nsbp')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 在 URL 中保留/添加 nsbp 参数
|
|
35
|
+
* @param url - 原始 URL
|
|
36
|
+
* @param nsbpValue - 可选的 nsbp 值,如果不提供则使用当前 URL 中的值
|
|
37
|
+
* @returns 保留了 nsbp 参数的 URL
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* appendNSBPParam('/photo?dic=test')
|
|
41
|
+
* // 如果当前 URL 是 /?nsbp=0,返回 '/photo?dic=test&nsbp=0'
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* appendNSBPParam('/photo?dic=test', '0')
|
|
45
|
+
* // 返回 '/photo?dic=test&nsbp=0'
|
|
46
|
+
*/
|
|
47
|
+
export const appendNSBPParam = (url: string, nsbpValue?: string): string => {
|
|
48
|
+
// 如果指定了 nsbpValue 则使用它,否则使用当前 URL 中的值
|
|
49
|
+
const nsbp = nsbpValue !== undefined ? nsbpValue : getNSBPParam()
|
|
50
|
+
|
|
51
|
+
// 如果没有 nsbp 值,直接返回原 URL
|
|
52
|
+
if (!nsbp) {
|
|
53
|
+
return url
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 判断 URL 中是否已有查询参数
|
|
57
|
+
const separator = url.includes('?') ? '&' : '?'
|
|
58
|
+
return `${url}${separator}nsbp=${nsbp}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 从 URL 中移除 nsbp 参数
|
|
63
|
+
* @param url - 原始 URL
|
|
64
|
+
* @returns 移除了 nsbp 参数的 URL
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* removeNSBPParam('/photo?dic=test&nsbp=0')
|
|
68
|
+
* // 返回 '/photo?dic=test'
|
|
69
|
+
*/
|
|
70
|
+
export const removeNSBPParam = (url: string): string => {
|
|
71
|
+
const urlObj = new URL(
|
|
72
|
+
url,
|
|
73
|
+
typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
|
|
74
|
+
)
|
|
75
|
+
urlObj.searchParams.delete('nsbp')
|
|
76
|
+
return urlObj.pathname + urlObj.search
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 保留 handleLink 作为向后兼容的函数
|
|
81
|
+
* @deprecated 使用 appendNSBPParam 替代
|
|
82
|
+
*/
|
|
25
83
|
export const handleLink = (link: string) => {
|
|
26
84
|
let result = link
|
|
27
85
|
|
|
@@ -31,7 +89,9 @@ export const handleLink = (link: string) => {
|
|
|
31
89
|
} else {
|
|
32
90
|
result += '?'
|
|
33
91
|
}
|
|
34
|
-
result += '
|
|
92
|
+
result += 'nsbp=1'
|
|
35
93
|
}
|
|
36
94
|
return result
|
|
37
95
|
}
|
|
96
|
+
|
|
97
|
+
export { usePreserveNSBP } from './usePreserveNSBP'
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { appendNSBPParam } from './index'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* React Hook 用于在路由跳转时保留 nsbp 参数
|
|
6
|
+
* @returns 包含三个方法的对象
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const { withNSBP, withNSBPValue, withoutNSBP } = usePreserveNSBP()
|
|
10
|
+
*
|
|
11
|
+
* // 保留当前的 nsbp 参数
|
|
12
|
+
* <Link to={withNSBP('/photo?dic=test')}>
|
|
13
|
+
*
|
|
14
|
+
* // 指定 nsbp 值
|
|
15
|
+
* <Link to={withNSBPValue('/photo?dic=test', '0')}>
|
|
16
|
+
*
|
|
17
|
+
* // 移除 nsbp 参数
|
|
18
|
+
* <Link to={withoutNSBP('/photo?dic=test&nsbp=0')}>
|
|
19
|
+
*/
|
|
20
|
+
export const usePreserveNSBP = () => {
|
|
21
|
+
return useMemo(
|
|
22
|
+
() => ({
|
|
23
|
+
/**
|
|
24
|
+
* 保留当前 URL 中的 nsbp 参数
|
|
25
|
+
* @param url - 原始 URL
|
|
26
|
+
* @returns 保留了 nsbp 参数的 URL
|
|
27
|
+
*/
|
|
28
|
+
withNSBP: (url: string) => appendNSBPParam(url),
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 指定 nsbp 参数值
|
|
32
|
+
* @param url - 原始 URL
|
|
33
|
+
* @param value - nsbp 参数值
|
|
34
|
+
* @returns 添加了指定 nsbp 值的 URL
|
|
35
|
+
*/
|
|
36
|
+
withNSBPValue: (url: string, value: string) =>
|
|
37
|
+
appendNSBPParam(url, value),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 移除 URL 中的 nsbp 参数
|
|
41
|
+
* @param url - 原始 URL
|
|
42
|
+
* @returns 移除了 nsbp 参数的 URL
|
|
43
|
+
*/
|
|
44
|
+
withoutNSBP: (url: string) => {
|
|
45
|
+
if (typeof window === 'undefined') return url
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const urlObj = new URL(url, window.location.origin)
|
|
49
|
+
urlObj.searchParams.delete('nsbp')
|
|
50
|
+
return urlObj.pathname + urlObj.search
|
|
51
|
+
} catch {
|
|
52
|
+
return url
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
56
|
+
[]
|
|
57
|
+
)
|
|
58
|
+
}
|