基于React SSR实现的仿MOO音乐风格网站,支持PWA
ccwgpt 2024-11-03 12:46 21 浏览 0 评论
前言
pika-music api 服务器参考 Binaryify 的 NeteaseCloudMusicApi
github : https://github.com/mbaxszy7/pika-music
项目技术特点:
- PWA 支持。支持PWA的浏览器可以安装到桌面
- 实现 React-SSR 框架
- 实现结合 SSR 的 Dynamic Import
- 实现 webpack module/nomudule 模式的打包
- 实现全站图片懒加载
node后端采用koa
其他特点:
- 后端支持http2
- 安卓端支持锁屏音乐控制
网站截图
技术特点介绍
React-SSR 框架介绍
主要思想参考的是 NextJS。首屏服务端渲染时,调用组件的 getInitialProps(store)方法,注入 redux store,getInitialProps 获取该页面的数据后,把数据储存到 redux store 中。在客户端 hydrate 时,从 redux store 中获取数据,然后把数据注入swr的 initialData 中,后续页面的数据获取和更新就使用了 swr 的能力。非 SSR 的页面会直接使用 swr。
下面以首页(Discover)为例:项目中有 ConnectCompReducer 这个父类:
class ConnectCompReducer {
constructor() {
this.fetcher = axiosInstance
this.moment = moment
}
getInitialData = async () => {
throw new Error("child must implememnt this method!")
}
}
每个实现 SSR 的页面都需要继承这个类,比如主页面:
class ConnectDiscoverReducer extends ConnectCompReducer {
// Discover 页面会实现的getInitialProps方法就是调用getInitialData,注入redux store
getInitialData = async store => {}
}
export default new ConnectDiscoverReducer()
Discover 的 JSX:
import discoverPage from "./connectDiscoverReducer"
const Discover = memo(() => {
// banner 数据
const initialBannerList = useSelector(state => state.discover.bannerList)
// 把banner数据注入swr的initialData中
const { data: bannerList } = useSWR(
"/api/banner?type=2",
discoverPage.requestBannerList,
{
initialData: initialBannerList,
},
)
return (
...
<BannersSection>
<BannerListContainer bannerList={bannerList ?? []} />
</BannersSection>
...
)
})
Discover.getInitialProps = async (store, ctx) => {
// store -> redux store, ctx -> koa 的ctx
await discoverPage.getInitialData(store, ctx)
}
服务端数据的获取:
// matchedRoutes: 匹配到的路由页面,需要结合dynamic import,下一小节会介绍
const setInitialDataToStore = async (matchedRoutes, ctx) => {
// 获取redux store
const store = getReduxStore({
config: {
ua: ctx.state.ua,
},
})
// 600ms后超时,中断获取数据
await Promise.race([
Promise.allSettled(
matchedRoutes.map(item => {
return Promise.resolve(
// 调用页面的getInitialProps方法
item.route?.component?.getInitialProps?.(store, ctx) ?? null,
)
}),
),
new Promise(resolve => setTimeout(() => resolve(), 600)),
]).catch(error => {
console.error("renderHTML 41,", error)
})
return store
}
自行实现结合 SSR 的 Dynamic Import
页面 dynamic import 的封装, 重要的处理是加载错误后的 retry 和 避免页面 loading 闪现:
class Loadable extends React.Component {
constructor(props) {
super(props)
this.state = {
Comp: null,
error: null,
isTimeout: false,
}
}
// eslint-disable-next-line react/sort-comp
raceLoading = () => {
const { pastDelay } = this.props
return new Promise((_, reject) => {
setTimeout(() => reject(new Error("timeout")), pastDelay || 200)
})
}
load = async () => {
const { loader } = this.props
try {
this.setState({
error: null,
})
// raceLoading 避免页面loading 闪现
const loadedComp = await Promise.race([this.raceLoading(), loader()])
this.setState({
isTimeout: false,
Comp:
loadedComp && loadedComp.__esModule ? loadedComp.default : loadedComp,
})
} catch (e) {
if (e.message === "timeout") {
this.setState({
isTimeout: true,
})
this.load()
} else {
this.setState({
error: e,
})
}
}
}
componentDidMount() {
this.load()
}
render() {
const { error, isTimeout, Comp } = this.state
const { loading } = this.props
// 加载错误,retry
if (error) return loading({ error, retry: this.load })
if (isTimeout) return loading({ pastDelay: true })
if (Comp) return <Comp {...this.props} />
return null
}
}
标记动态加载的组件,用于服务端识别:
const asyncLoader = ({ loader, loading, pastDelay }) => {
const importable = props => (
<Loadable
loader={loader}
loading={loading}
pastDelay={pastDelay}
{...props}
/>
)
// 标记
importable.isAsyncComp = true
return importable
}
封装好页面的动态加载后需要考虑两点:
- ssr 的时候需要主动去执行动态路由的组件,不然服务端不会渲染组件本身的内容
- 在浏览器端不先去加载动态 split 出的组件的话,会导致组件的 loading 状态闪现。所以,要先加载好动态路由组件,再去渲染页面。
具体代码如下:
服务端加载标记 isAsyncComp 的动态组件:
const ssrRoutesCapture = async (routes, requestPath) => {
const ssrRoutes = await Promise.allSettled(
[...routes].map(async route => {
if (route.routes) {
return {
...route,
routes: await Promise.allSettled(
[...route.routes].map(async compRoute => {
const { component } = compRoute
if (component.isAsyncComp) {
try {
const RealComp = await component().props.loader()
const ReactComp =
RealComp && RealComp.__esModule
? RealComp.default
: RealComp
return {
...compRoute,
component: ReactComp,
}
} catch (e) {
console.error(e)
}
}
return compRoute
}),
).then(res => res.map(r => r.value)),
}
}
return {
...route,
}
}),
).then(res => res.map(r => r.value))
return ssrRoutes
}
浏览器端加载动态组件:
const clientPreloadReady = async routes => {
try {
// 匹配当前页面的组件
const matchedRoutes = matchRoutes(routes, window.location.pathname)
if (matchedRoutes && matchedRoutes.length) {
await Promise.allSettled(
matchedRoutes.map(async route => {
if (
route?.route?.component?.isAsyncComp &&
!route?.route?.component.csr
) {
try {
await route.route.component().props.loader()
} catch (e) {
await Promise.reject(e)
}
}
}),
)
}
} catch (e) {
console.error(e)
}
}
最后,在浏览器端 ReactDOM.hydrate 的时候先加载动态分割出的组件:
clientPreloadReady(routes).then(() => {
render(<App store={store} />, document.getElementById("root"))
})
module/nomudule 模式
主要实现思路:
webpack 先根据 webpack.client.js 的配置打包出支持 es module 的代码,其中产出 index.html。
然后 webpack 根据 webpack.client.lengacy.js 的配置,用上一步的 index.html 为 template,打包出不支持 es module 的代码,插入 script nomodule 和script type="module" 的脚本。主要依赖的是 html webpack plugin 的相关 hooks。
webpack.client.js 和 webpack.client.lengacy.js 主要的不同是 babel 的配置和 html webpack plugin 的 template
babel presets 配置:
exports.babelPresets = env => {
const common = [
"@babel/preset-env",
{
// targets: { esmodules: true },
useBuiltIns: "usage",
modules: false,
debug: false,
bugfixes: true,
corejs: { version: 3, proposals: true },
},
]
if (env === "node") {
common[1].targets = {
node: "13",
}
} else if (env === "legacy") {
common[1].targets = {
ios: "9",
safari: "9",
}
common[1].bugfixes = false
} else {
common[1].targets = {
esmodules: true,
}
}
return common
}
实现在 html 内插入 script nomodule 和 script type="module"的 webpack 插件代码链接:https://github.com/mbaxszy7/pika-music/blob/master/module-html-plugin.js
全站图片懒加载
图片懒加载的实现使用的是 IntersectionObserver 和浏览器原生支持的image lazy loading
const pikaLazy = options => {
// 如果浏览器原生支持图片懒加载,就设置懒加载当前图片
if ("loading" in HTMLImageElement.prototype) {
return {
lazyObserver: imgRef => {
load(imgRef)
},
}
}
// 当前图片出现在当前视口,就加载图片
const observer = new IntersectionObserver(
(entries, originalObserver) => {
entries.forEach(entry => {
if (entry.intersectionRatio > 0 || entry.isIntersecting) {
originalObserver.unobserve(entry.target)
if (!isLoaded(entry.target)) {
load(entry.target)
}
}
})
},
{
...options,
rootMargin: "0px",
threshold: 0,
},
)
return {
// 设置观察图片
lazyObserver: () => {
const eles = document.querySelectorAll(".pika-lazy")
for (const ele of Array.from(eles)) {
if (observer) {
observer.observe(ele)
continue
}
if (isLoaded(ele)) continue
load(ele)
}
},
}
}
PWA
PWA 的缓存控制和更新的能力运用的是 workbox。但是加了缓存删除的逻辑:
import { cacheNames } from "workbox-core"
const currentCacheNames = {
"whole-site": "whole-site",
"net-easy-p": "net-easy-p",
"api-banner": "api-banner",
"api-personalized-newsong": "api-personalized-newsong",
"api-playlist": "api-play-list",
"api-songs": "api-songs",
"api-albums": "api-albums",
"api-mvs": "api-mvs",
"api-music-check": "api-music-check",
[cacheNames.precache]: cacheNames.precache,
[cacheNames.runtime]: cacheNames.runtime,
}
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(cacheGroup => {
return Promise.all(
cacheGroup
.filter(cacheName => {
return !Object.values(currentCacheNames).includes(`${cacheName}`)
})
.map(cacheName => {
// 删除与当前缓存不匹配的缓存
return caches.delete(cacheName)
}),
)
}),
)
})
项目的 PWA 缓存控制策略主要选择的是 StaleWhileRevalidate,先展示缓存(如果有的话),然后 pwa 会更新缓存。由于项目用了 swr,该库会查询页面的数据或者在页面从隐藏到显示时也会请求更新数据,从而达到了使用 pwa 更新的缓存的目的。
浏览器兼容
IOS >=10, Andriod >=6
本地开发
node 版本
node version >= 13.8
本地开发开启 SSR 模式
- npm run build:server
- npm run build:client:modern
- nodemon --inspect ./server\_app/bundle.js
本地开发开启 CSR 模式
npm run start:client
相关推荐
- NestJS入门教程系列一
-
介绍Nest(NestJS)是用于构建高效,可扩展的Node.js服务器端应用程序的框架。它使用渐进式JavaScript,内置并完全支持TypeScript(但开发人员仍然能够使用JavaScrip...
- 【推荐】一个网盘资源搜索与转存工具,支持移动端与PC端!
-
如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!项目介绍CloudSaver是一个基于Vue3和Express的网盘资源搜索与转存开源实用工具。它支持...
- Appium原理精讲
-
目前使用Appium新版本和旧版本的企业数目都很多,而两个版本的安装过程和api的使用又有较大的区别。但是无论表面上的东东如何变化,内部原理都是一样的。在这里我给大家介绍一下appium的核心,增进大...
- Kubernetes最小部署单元Pod
-
一、Kubernetes与Pod简介在当今云计算和容器化技术盛行的时代,Kubernetes已然成为容器编排领域的中流砥柱。它是一个开源的容器编排平台,由Google基于其内部使用的Bo...
- 最常用的四种跨域解决方案
-
前置知识什么是跨域?浏览器发送的请求地址(URL)与所在页面的地址不同(端口/协议/域名其一不同)。简言之,浏览器发出的请求url,与其所在页面的url不一样。此时,同源策略会让浏览器拒收服务器...
- Bolt.New —— 全栈AI Web自动编程
-
Bolt.New是由StackBlitz公司推出的,全栈AI工具,代码编辑、运行、部署,通通一站式搞定。它使用WebContainers技术,无需任何本地安装或配置,在浏览器中,就可以运行完整的No...
- Nodejs Express新手教程&高手进阶
-
NodejsExpress新手教程&高手进阶Express是一个NodeJS平台的框架,主要用于构于Web服务器项目。本文将通过示例介绍适合新手入门的Express基础使用,以及高手进阶知识,如:c...
- Express.js 创建Node.js Web应用
-
Express.js是一个基于Node.js的Web应用框架,框架的设计目的是构建应用的架构和简化应用的开发。框架会解决一些通用的问题,在Express.js中,Express框架会处理如:中间件、代...
- JavaScript 的 Express.js 功能及应用场景详解
-
Express.js是一个基于Node.js的轻量级Web应用框架,主要用于快速构建服务器端应用和API。它的核心功能包括以下关键点:1.路由管理URL路径与HTTP方法映射:通过...
- nodejs的express4文件下载
-
在nodejs的express框架中,下载变得非常简单,就一个方法,res.download()首先express命令行生成项目基本框架:不会的看这里:http://blog.csdn.net/zz...
- Express 系列:快速生成一个项目
-
系列预告本系列将以一个项目入手结合相关技术细节来带领大家一起学习Express这个基于Node.js的后端框架。本文首先将介绍:如何快速的生成一个具有一定结构的Express项目。Express项目结...
- nodejs的express自动生成项目框架
-
nodejs版本为:4.X,express版本为4.X1.全局安装2个模块express、express-generator在命令行输入:npminstall-gexpressnpminsta...
- express开发(一)简介与搭建
-
上周末去了趟上海书城,不愧是上海数得上号的书城,流行的科技书应有尽有,话不多说直接上图。最经典的C语言O(∩_∩)O最流行的java(づ ̄3 ̄)づ超酷的R语言/(ㄒoㄒ)/~~然而,身为一个坚定的前...
- Vue+Echarts可视化大屏系统后端框架搭建(附代码)
-
各位同学,大家好。上节课,前面我们讲解了Vue+Echarts前端部分的设计方法。这节课程,我们开始讲解使用Express进行后端设计的方法。01项目相关理论介绍什么是expressExpress是...
- Shopify电商API接口开发
-
Shopify电商API接口开发上线流程主要包括以下步骤。北京木奇移动技术有限公司,专业的软件外包开发公司,欢迎洽谈合作。前期准备-注册Shopify账号:在Shopify官网注册,用于后续开发测试...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- MVC框架 (46)
- spring框架 (46)
- 框架图 (58)
- bootstrap框架 (43)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- laravel框架 (46)
- express框架 (43)
- scrapy框架 (52)
- beego框架 (42)
- java框架spring (43)
- grpc框架 (55)
- 前端框架bootstrap (42)
- orm框架有哪些 (43)
- ppt框架 (48)
- 内联框架 (52)
- winform框架 (46)
- gui框架 (44)
- cad怎么画框架 (58)
- ps怎么画框架 (47)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)
- oracle提交事务 (47)