Making the address
Project site
The Pika-Music API server references Binaryify’s NeteaseCloudMusicApi
Updated instructions
[2020-10-28] Project supports WebPack 5 packaging.
Project Technical features:
- PWA support. Pwa-enabled browsers can be installed to the desktop
- Implement react-SSR framework
- Implement Dynamic Import combined with SSR
- Implement the webPack Module/Nomudule pattern packaging
- Realize the whole station picture lazy loading
The node back-end uses KOA
Other features:
- The back end supports HTTP2
- Android supports lock screen music control
Website screenshot
Technical features
Introduction to the React-SSR framework
The main idea reference is NextJS. When the first screen is rendered, the server calls the getInitialProps(store) method of the component and injects the redux Store. GetInitialProps obtains the data on the page and stores it in the Redux Store. At the client hydrate, data is fetched from the Redux Store and injected into the SWR’s initialData. Subsequent page data fetching and updating uses the SWR’s capabilities. Non-ssr pages will use SWR directly.
ConnectCompReducer: ConnectCompReducer: ConnectCompReducer
class ConnectCompReducer {
constructor() {
this.fetcher = axiosInstance
this.moment = moment
}
getInitialData = async() = > {throw new Error("child must implememnt this method!")}}Copy the code
Every page that implements SSR needs to inherit from this class, such as the main page:
class ConnectDiscoverReducer extends ConnectCompReducer {
// The Discover page will implement getInitialProps by calling getInitialData and injecting the Redux Store
getInitialData = async store => {}
}
export default new ConnectDiscoverReducer()
Copy the code
Discover the JSX:
import discoverPage from "./connectDiscoverReducer"
const Discover = memo(() = > {
/ / banner data
const initialBannerList = useSelector(state= > state.discover.bannerList)
// Inject the banner data into the 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)
}
Copy the code
Obtaining server data:
// matchedRoutes: the matched routing page, which needs to be combined with dynamic import, as described in the next section
const setInitialDataToStore = async (matchedRoutes, ctx) => {
// Get the redux store
const store = getReduxStore({
config: {
ua: ctx.state.ua,
},
})
// timeout occurs after 600ms, and data acquisition is interrupted
await Promise.race([
Promise.allSettled(
matchedRoutes.map(item= > {
return Promise.resolve(
// Call the page's getInitialProps methoditem.route? .component?.getInitialProps?.(store, ctx) ??null,)}),),new Promise(resolve= > setTimeout(() = > resolve(), 600)),
]).catch(error= > {
console.error("renderHTML 41,", error)
})
return store
}
Copy the code
Implement Dynamic Import combined with SSR
To encapsulate page dynamic import, the important handling is to retry after loading error and avoid page loading flash:
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 avoids page loading flash
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
// Load error, retry
if (error) return loading({ error, retry: this.load })
if (isTimeout) return loading({ pastDelay: true })
if (Comp) return <Comp {. this.props} / >
return null}}Copy the code
Tags dynamically loaded components for server identification:
const asyncLoader = ({ loader, loading, pastDelay }) = > {
const importable = props= > (
<Loadable
loader={loader}
loading={loading}
pastDelay={pastDelay}
{. props} / >
)
/ / tag
importable.isAsyncComp = true
return importable
}
Copy the code
After encapsulating the dynamic loading of a page, there are two considerations:
- SSR requires active implementation of dynamically routed components, otherwise the server will not render the components themselves
- Loading a dynamically split component without first loading it in the browser will cause the loading state of the component to flash. So load the dynamic routing component before rendering the page.
The specific code is as follows:
The server loads the dynamic component marked 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
}
Copy the code
Load dynamic components on the browser side:
const clientPreloadReady = async routes => {
try {
// Match the components of the current page
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)
}
}
Copy the code
Finally, load the dynamically detached component first on the browser side with reactdom.hydrate:
clientPreloadReady(routes).then(() = > {
render(<App store={store} />.document.getElementById("root"))})Copy the code
The module/nomudule model
Main implementation idea: WebPack first packages the code that supports ES Module according to the configuration of Webpack.client.js, which produces index.html. Then webpack according to webpack. Client. Lengacy. Js configuration, use the step index. The HTML template, packaging does not support the es module code, Insert script Nomodule and script type=”module” script. The main dependencies are the hooks of the HTML WebPack Plugin. Webpack. Client. Js and webpack. Client. Lengacy. Js major difference is the configuration of Babel and 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
}
Copy the code
A webPack plugin that inserts script Nomodule and script type=”module” into HTML is linked to github.com/mbaxszy7/pi…
The whole station picture lazy loading
The implementation of lazy loading of images uses IntersectionObserver and image lazy loading supported by the browser
const pikaLazy = options= > {
// If the browser natively supports lazy image loading, set the current image to lazy loading
if ("loading" in HTMLImageElement.prototype) {
return {
lazyObserver: imgRef= > {
load(imgRef)
},
}
}
// When the current image appears in the current viewport, the image is loaded
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 {
// Set the view image
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)
}
},
}
}
Copy the code
PWA
The PWA’s cache control and update capabilities use Workbox. But add cache delete logic:
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= > {
// Delete the cache that does not match the current cache
return caches.delete(cacheName)
}),
)
}),
)
})
Copy the code
The PWA cache control strategy for the project was to choose StaleWhileRevalidate, which first demonstrates the cache (if any) and then PWA updates the cache. Since the project uses SWR, the library polls the page data or requests updates as the page goes from hidden to displayed, thus achieving the purpose of caching the pWA updates.
Browser compatibility
IOS >=10, Andriod >=6
Local development
The node version
The node version > = 13.8
Enable SSR mode for local development
- npm run build:server
- npm run build:client:modern
- nodemon –inspect ./server_app/bundle.js
Enable the CSR mode for local development
npm run start:client