diff --git a/index.html b/index.html
index aee05e6..d42b91e 100644
--- a/index.html
+++ b/index.html
@@ -2,14 +2,6 @@
- AO3 Mirror
-
-
-
-
-
-
-
diff --git a/server.js b/server.js
index 95cc39e..72d769d 100644
--- a/server.js
+++ b/server.js
@@ -2,17 +2,15 @@ import fs from 'node:fs/promises'
import express from 'express'
import cookieParser from 'cookie-parser'
import { compress } from 'compress-json'
-// Constants
+
const isProduction = process.env.NODE_ENV === 'production'
const port = process.env.PORT || 5173
const base = process.env.BASE || '/'
-// Cached production assets
const templateHtml = isProduction
- ? await fs.readFile('./dist/client/index.html', 'utf-8')
- : ''
+ ? await fs.readFile('./dist/client/index.html', 'utf-8')
+ : ''
-// Create http server
const app = express()
app.use(cookieParser());
@@ -21,73 +19,110 @@ const MESSAGE = {
0: 'Unknown'
}
-// Add Vite or respective production middlewares
-/** @type {import('vite').ViteDevServer | undefined} */
let vite
if (!isProduction) {
- const { createServer } = await import('vite')
- vite = await createServer({
- server: { middlewareMode: true },
- appType: 'custom',
- base,
- })
- app.use(vite.middlewares)
+ const { createServer } = await import('vite')
+ vite = await createServer({
+ server: { middlewareMode: true },
+ appType: 'custom',
+ base,
+ })
+ app.use(vite.middlewares)
} else {
- //const compression = (await import('compression')).default
- const sirv = (await import('sirv')).default
- //app.use(compression())
- app.use(base, sirv('./dist/client', { extensions: [] }))
+ const sirv = (await import('sirv')).default
+ app.use(base, sirv('./dist/client', { extensions: [] }))
}
// Serve HTML
app.use('*all', async (req, res) => {
- try {
- const url = req.originalUrl.replace(base, '')
- console.log(`Request ${url}`)
- /** @type {string} */
- let template
- /** @type {import('./src/entry-server.js').render} */
- let render, getRoute
- if (!isProduction) {
- // Always read fresh template in development
- template = await fs.readFile('./index.html', 'utf-8')
- template = await vite.transformIndexHtml(url, template)
- const module = await vite.ssrLoadModule('/src/entry-server.js')
- render = module.render
- getRoute = module.getRoute
- } else {
- template = templateHtml
- const module = await import('./dist/server/entry-server.js')
- render = module.render
- getRoute = module.getRoute
- }
- const { router, code } = await getRoute(url)
- if (code != 200 && !req.accepts('html')) {
- res.status(code).set({ 'Content-Type': 'text/plain' })
- res.write(MESSAGE[code] || MESSAGE[0])
- res.end()
- return
- }
- const { stream, piniaState } = await render(router, req.cookies, req.headers.host)
- const [htmlStart, htmlEnd] = template.split('')
- res.status(code).set({ 'Content-Type': 'text/html' })
- res.write(htmlStart)
- for await (const chunk of stream) {
- if (res.closed) break
- res.write(chunk)
- }
- const piniaStateContent = JSON.stringify(compress(piniaState))
- const stateScript = ``
- res.write(htmlEnd.replace('', stateScript))
- res.end()
- } catch (e) {
- vite?.ssrFixStacktrace(e)
- console.log(e.stack)
- res.status(500).end(e.stack)
- }
+ try {
+ const url = req.originalUrl.replace(base, '')
+ console.log(`Request ${url}`)
+ let template
+ let render, getRoute
+ if (!isProduction) {
+ // Always read fresh template in development
+ template = await fs.readFile('./index.html', 'utf-8')
+ template = await vite.transformIndexHtml(url, template)
+ const module = await vite.ssrLoadModule('/src/entry-server.js')
+ render = module.render
+ getRoute = module.getRoute
+ } else {
+ template = templateHtml
+ const module = await import('./dist/server/entry-server.js')
+ render = module.render
+ getRoute = module.getRoute
+ }
+ const { router, code, title, metas, meta } = await getRoute(url)
+ if (code != 200 && !req.accepts('html')) {
+ res.status(code).set({ 'Content-Type': 'text/plain' })
+ res.write(MESSAGE[code] || MESSAGE[0])
+ res.end()
+ return
+ }
+ const { stream, piniaState, headState } = await render(router, req.cookies, req.headers.host)
+ const [htmlStart, htmlEnd] = template.split('')
+ if (meta) {
+ const buffer = []
+ let headReady = false
+ for await (const chunk of stream) {
+ if (res.closed) break
+ if (headReady) res.write(chunk)
+ else {
+ if (headState.ready) {
+ res.status(headState.code || code).set({ 'Content-Type': 'text/html' })
+ const heads = [`${ headState.title || title }`]
+ for (const item of [ ...metas, ...headState.meta ]) {
+ const properties = []
+ for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
+ heads.push(``)
+ }
+ res.write(htmlStart.replace('',heads.join('')))
+ for (const item of buffer) res.write(item)
+ res.write(chunk)
+ headReady = true
+ } else buffer.push(chunk)
+ }
+ }
+ if (!headState.ready) {
+ console.warn('Page not set meta ready! No stream render at all!')
+ const heads = [`${ title }`]
+ for (const item of metas) {
+ const properties = []
+ for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
+ heads.push(``)
+ }
+ res.write(htmlStart.replace('',heads.join('')))
+ for await (const chunk of buffer) {
+ if (res.closed) break
+ res.write(chunk)
+ }
+ }
+ } else {
+ res.status(code).set({ 'Content-Type': 'text/html' })
+ const heads = [`${ title }`]
+ for (const item of metas) {
+ const properties = []
+ for (const [key, value] of Object.entries(item)) properties.push(`${key}="${value}"`)
+ heads.push(``)
+ }
+ res.write(htmlStart.replace('',heads.join('')))
+ for await (const chunk of stream) {
+ if (res.closed) break
+ res.write(chunk)
+ }
+ }
+ const piniaStateContent = JSON.stringify(compress(piniaState))
+ const stateScript = ``
+ res.write(htmlEnd.replace('', stateScript))
+ res.end()
+ } catch (e) {
+ vite?.ssrFixStacktrace(e)
+ console.log(e.stack)
+ res.status(500).end(e.stack)
+ }
})
-// Start http server
app.listen(port, () => {
- console.log(`Server started at http://localhost:${port}`)
+ console.log(`Server started at port ${port}`)
})
diff --git a/src/composables/apiRequest.js b/src/composables/apiRequest.js
index 70bb934..9160540 100644
--- a/src/composables/apiRequest.js
+++ b/src/composables/apiRequest.js
@@ -20,9 +20,7 @@ function replaceUrl(url) {
}
export function useApiRequest(method, url, data, config = {}) {
- const start = Date.now()
const baseURL = getEndpoint()
- // 若为 GET 请求,将 data 转为查询参数拼接到 URL 上
const fullURL = method === 'GET' && data
? `${baseURL}${url}?${objectToQueryString(data)}`
: `${baseURL}${url}`
@@ -45,14 +43,14 @@ export function useApiRequest(method, url, data, config = {}) {
}
)
const exec = async () => {
- await execute()
+ const start = Date.now()
+ try { await execute() }
+ catch (e) {}
const stop = Date.now()
return {
status: response.value?.status || (error.value?.response?.status ?? -1),
data: response.value?.data || error.value?.response?.data || null,
duration: stop - start,
- start,
- stop,
error: error.value,
isSSR: import.meta.env.SSR,
}
diff --git a/src/entry-server.js b/src/entry-server.js
index 3bb10d5..dd7cae3 100644
--- a/src/entry-server.js
+++ b/src/entry-server.js
@@ -1,7 +1,7 @@
import { renderToWebStream } from 'vue/server-renderer'
import { createApp } from './main'
-import { createSSRRouter } from './router.js'
+import { createSSRRouter, defaultHead } from './router.js'
export async function getRoute(_url) {
const router = createSSRRouter()
@@ -9,15 +9,24 @@ export async function getRoute(_url) {
await router.isReady()
const route = router.currentRoute.value.matched[0]
const code = route.meta.code || 200
- return { router, code }
+ return { router, code, meta: route.meta.meta || false,
+ title: route.meta.title || route.meta.name || defaultHead.title,
+ metas: [...defaultHead.meta, ...route.meta.metas || []]
+ }
}
export async function render(router, cookies, host) {
const { app, pinia } = createApp()
app.use(router)
- const ctx = { cookies, host }
+ const headState = {
+ ready: false,
+ code: null,
+ title: null,
+ meta: []
+ }
+ const ctx = { cookies, host, headState }
const stream = renderToWebStream(app, ctx)
const piniaState = pinia.state.value
- return { stream, piniaState }
+ return { stream, piniaState, headState }
}
diff --git a/src/router.js b/src/router.js
index 73126b6..bdaf41f 100644
--- a/src/router.js
+++ b/src/router.js
@@ -1,5 +1,25 @@
import { createMemoryHistory, createWebHistory, createRouter } from 'vue-router'
+export const defaultHead = {
+ title: 'AO3 Mirror',
+ meta: [
+ { name: 'description',
+ content: 'ArchiveOfOurOwn 镜像站, 使用 Vue 3 与 MDUI 2 重写界面, 优化 UI/UX' },
+ { name: 'author',
+ content: 'UnknownMp' },
+ { property: 'og:title',
+ content: 'AO3 Mirror' },
+ { property: 'og:description',
+ content: 'ArchiveOfOurOwn 重构镜像' },
+ { property: 'og:image',
+ content: 'https://cdn.unknownmp.top/website/ao3mirror.svg' },
+ { property: 'og:url',
+ content: 'https://ao3.unknownmp.top' },
+ { property: 'og:type',
+ content: 'website' },
+ ]
+}
+
export const createSSRRouter = () => createRouter({
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
scrollBehavior(to, from, savedPosition) {
@@ -25,7 +45,8 @@ export const createSSRRouter = () => createRouter({
component: () => import('./views/Work.vue'),
meta: {
title: '阅读',
- hidden: true
+ hidden: true,
+ meta: true
}
},{
path: '/work/:id/:cid',
@@ -33,7 +54,8 @@ export const createSSRRouter = () => createRouter({
component: () => import('./views/Work.vue'),
meta: {
title: '阅读',
- hidden: true
+ hidden: true,
+ meta: true
}
},{
path: '/search/simple',
@@ -48,7 +70,6 @@ export const createSSRRouter = () => createRouter({
name: '设置',
component: () => import('./views/Settings.vue'),
meta: {
- title: '设置',
order: 90
},
},{
@@ -56,7 +77,6 @@ export const createSSRRouter = () => createRouter({
name: '关于',
component: () => import('./views/About.md'),
meta: {
- title: '',
order: 100
},
},{
@@ -64,7 +84,6 @@ export const createSSRRouter = () => createRouter({
name: '开发人员选项',
component: () => import('./views/Developer.vue'),
meta: {
- title: '',
hidden: true
},
},{
diff --git a/src/ssr/headHelper.js b/src/ssr/headHelper.js
new file mode 100644
index 0000000..3d9f525
--- /dev/null
+++ b/src/ssr/headHelper.js
@@ -0,0 +1,47 @@
+import { useSSRContext } from 'vue'
+
+export function useHeadHelper() {
+ if (!import.meta.env.SSR) {
+ return {
+ headReady: () => {},
+ setMeta: () => {},
+ addMeta: () => {},
+ setTitle: () => {},
+ setCode: () => {},
+ }
+ }
+
+ const ssrContext = useSSRContext()
+
+ if (!ssrContext || !ssrContext.headState) {
+ return {
+ headReady: () => {},
+ setMeta: () => {},
+ addMeta: () => {},
+ setTitle: () => {},
+ setCode: () => {},
+ }
+ }
+
+ return {
+ headReady: () => {
+ ssrContext.headState.ready = true
+ },
+ setMeta: (metas = []) => {
+ ssrContext.headState.meta = [
+ ...ssrContext.headState.meta,
+ ...metas
+ ]
+ },
+ addMeta: (meta) => {
+ ssrContext.headState.meta.push(meta)
+ },
+ setTitle: (title) => {
+ ssrContext.headState.title = title
+ },
+ setCode: (code) => {
+ ssrContext.headState.code = code
+ }
+ }
+}
+
diff --git a/src/stores/route.js b/src/stores/route.js
index de1a803..922833e 100644
--- a/src/stores/route.js
+++ b/src/stores/route.js
@@ -2,6 +2,8 @@ import { ref, computed, watch } from 'vue'
import { defineStore } from 'pinia'
import { useRouter, useRoute } from 'vue-router'
+import { useHeadHelper } from '../ssr/headHelper.js'
+
export const useRouteStore = defineStore('route', () => {
const router = useRouter()
const route = useRoute()
@@ -16,7 +18,9 @@ export const useRouteStore = defineStore('route', () => {
)
const lastFromDrawer = ref(0)
const customTitle = ref(null)
- const title = computed(() => customTitle.value || route.meta.title || route.name)
+ const title = import.meta.env.SSR ?
+ computed(() => route.meta.title || route.name) :
+ computed(() => customTitle.value || route.meta.title || route.name)
function drawerPress(target) {
if (lastFromDrawer.value == 0) {
lastFromDrawer.value = 1
diff --git a/src/stores/workRead.js b/src/stores/workRead.js
index 3765192..05fc039 100644
--- a/src/stores/workRead.js
+++ b/src/stores/workRead.js
@@ -62,9 +62,10 @@ export const useWorkReadState = defineStore('workRead', () => {
if (result.status == 200) {
setData(result.data)
state.value = 'ready'
- } else {
- id.value = target
+ } else if (result.status == 404) {
state.value = import.meta.env.SSR ? 'ssrnotfound' : 'notfound'
+ } else if (result.status == 401) {
+ state.value = 'unauth'
}
}
return {
diff --git a/src/views/Work.vue b/src/views/Work.vue
index 5285b41..5c95979 100644
--- a/src/views/Work.vue
+++ b/src/views/Work.vue
@@ -4,6 +4,8 @@ import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
+import { useHeadHelper } from '../ssr/headHelper.js'
+
import { useWorkReadState } from '@/stores/workRead.js'
import { useRouteStore } from '@/stores/route.js'
@@ -43,11 +45,23 @@ const categoryName = {
fm: '女/男'
}
-onServerPrefetch(async () => await workReadState.loadWork(route.params.id, route.params.cid))
+onServerPrefetch(async () => {
+ const headHelper = useHeadHelper()
+ await workReadState.loadWork(route.params.id, route.params.cid)
+ if (workReadState.state == 'ready') headHelper.setTitle(workReadState.title)
+ else if (workReadState.state == 'ssrnotfound') {
+ headHelper.setTitle('文章未找到')
+ headHelper.setCode(404)
+ } else if (workReadState.state == 'unauth') {
+ headHelper.setTitle('访问被阻止')
+ headHelper.setCode(401)
+ }
+ headHelper.headReady()
+})
onMounted(async () => {
watch(() => workReadState.state, (value) => { if (value == 'ready') routeState.customTitle = workReadState.title })
- if (workReadState.state != 'ssrnotfound') await workReadState.loadWork(route.params.id, route.params.cid)
+ if (workReadState.state != 'ssrnotfound' && workReadState.state != 'ready') await workReadState.loadWork(route.params.id, route.params.cid)
if (workReadState.state == 'ready') {
routeState.customTitle = workReadState.title
if (workReadState.cid !== null && parseInt(route.params.cid) != workReadState.cid) {
@@ -94,28 +108,34 @@ async function switchWorkWithIndex(target) {
-
+
+ 文章不存在...
+ 是不是链接没有复制完全?
+ ID: {{ workReadState.id }}
+
+ CID: {{ workReadState.cid }}
+
+ 返回
+
+ 访问被阻止!
+ 上游 AO3 不允许匿名用户访问该作品
+ ID: {{workReadState.id}}
+
+ CID: {{ workReadState.cid }}
+
+ 返回
+
+ 路径格式错误
+ ID: {{ $route.params.id }}
+
+ CID: {{ $route.params.id }}
+
+ 返回
+
加载中...
-
- 文章不存在...
- 是不是链接没有复制完全?
- ID: {{ workReadState.id }}
-
- CID: {{ workReadState.cid }}
-
- 返回
-
-
- 路径格式错误
- ID: {{ $route.params.id }}
-
- CID: {{ $route.params.id }}
-
- 返回
-
{{ workReadState.title }}
@@ -198,15 +218,6 @@ async function switchWorkWithIndex(target) {
-
- 文章不存在...
- 是不是链接没有复制完全?
- ID: {{workReadState.id}}
-
- CID: {{ workReadState.cid }}
-
- 返回
-
{{ workReadState.title }}
{{ workReadState.pesud }}