使用nextjs做一个类似productHunt的网站

本文介绍如何使用Next.js、MobX等技术构建服务器渲染(SSR)的React应用,涵盖项目搭建、状态管理及部署全流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

闲来无事,利用业余时间做了一个类似productHunt的网站,将实现过程整理一下。

一、框架

技术: nextjs + koa + react + mobx+ antd

  1. 为了更好的支持SEO,所以需要服务器渲染,框架选择nextjs;同时加入了koa配合使用。
  2. 前端一直使用自己熟悉的技术react、antd、sass。
  3. 数据管理使用mobx。

二、项目搭建

1. 初始化项目

使用create-next-app自动初始化项目,得到的结构如下:

运行 npm run dev 就可以运行项目了。

添加其他目录后如下:

  • components:组件存放目录
  • domain:数据对象结构文件,比如通过api获取的user、product数据都采用jsonapi的方式,前端需要将数据处理后方便页面使用。
  • http: api文件
  • pages: 访问的页面目录
  • static: 静态文件存放目录
  • stores:store目录
  • server.js: 启动文件

2. getInitialProps

在服务器上渲染是通过一步方式getInitialProps加载数据,并绑定在props上,同时数据将被序列化,并包含在返回的html文件中。

2. 添加mobx

用户store

import {action, observable} from 'mobx';
export default class UserStore {
  constructor(initState = {}) {
    for (const k in initState) {
      if (initState.hasOwnProperty(k)) {
        this[k] = initState[k];
      }
    }
  }
  @observable currentUser = null;
  @action setCurrentUser(data) {
    this.currentUser = data;
  }
}

代码中observableaction使用es7的装饰器语法:

observable修饰符将currentUser定义为需要被监听的状态变量,当这个变量变化时,执行这个变量的监听者。mobx是使用es5的Object.defineProperty的set、get实现对对象属性变动的监听和依赖跟踪。

action 修饰符是对transaction的一次包装,在不使用transaction时,如果在函数中修改了多个被监听变量,React组件就会被渲染多次,对此transaction提供了事务的功能,只在事务完成后才触发一次更新功能。所以尽可能在需要action的时候使用它。代码如下:

export default class UserStore {
 @observable a = 0;
 @observable b = 0;
 
 // 没有action、transaction,将触发监听者两次更新
 setData() {
   this.a = 1;
   this.b = 2;
 }
}

修改代码如下

export default class UserStore {
 @observable a = 0;
 @observable b = 0;
 
 // 触发监听者一次更新
 @action setData() {
   this.a = 1;
   this.b = 2;
 }
}

上面constructor中的代码是用于在初始化时设置数据,上面提到获取到的页面中包含了在服务器上获取的数据,所以在浏览器上执行时直接用这些数据初始化store。由于所有的store都需要设置初始数据,所以提出来作为公共部分。

base.js

export default class Base {
    constructor(initState = {}) {
      for (const k in initState) {
        if (initState.hasOwnProperty(k)) {
          this[k] = initState[k];
        }
      }
    }
  }

user.js

import {action, observable} from 'mobx';
import Base from './base';
export default class UserStore extends Base {
  @observable currentUser = null;
  @action setCurrentUser(data) {
    this.currentUser = data;
  }
 }

config.js 这里包含所有的store

import userStore from './user';
const config = {
  userStore,
}
export default config

还需要一个地方来初始化所有的store,单独写一个文件index.js

import config from './config'; // config.js 
export class Store {
  constructor(initialState = {}) {
    for (const k in config) {
      if (config.hasOwnProperty(k)) {
	      // 根据initialState数据初始化每个store;
        this[k] = new config[k](initialState[k])
      }
    }
  }
}
let store = null
export function initializeStore(initialState = {}) {
  // 服务器上不需要缓存store,下次访问重新创建,避免不同用户之间数据错乱
  if (isServer) {
    return new Store(initialState)
  }
  // 浏览器上没有时才创建。
  if (store === null) {
    store = new Store(initialState)
  }
  return store
}

由于服务器在调用initializeStore函数的时候没有任何数据,所以创建的store时都没有数据传入。在客户端调用时传入从页面中获取的默认数据。

stores目录下的文件如下:

  • index.jsx
  • base.js
  • user.js
  • config.js

下面要将store与组件结合起来,在pages目录下新建**_app.js**文件。

_app.js

import App from 'next/app'
import Layout from '../components/layout';
import {initializeStore} from '../stores'
import {Provider, observer} from 'mobx-react';
import 'antd/dist/antd.css'

class MyApp extends App {
    mobxStore = {};
    static async getInitialProps(appContext) {
        const ctx = appContext.ctx;
        // 初始化所有store
        ctx.mobxStore = initializeStore();
        const appProps = await App.getInitialProps(appContext);
        return {
          ...appProps,
          initialMobxState: ctx.mobxStore
        }
    }

    constructor(props) {
        super(props)
        const isServer = typeof window === 'undefined'
        // 浏览器渲染时从props中获取页面从服务器上返回的数据initialMobxState,服务器渲染直接使用getInitialProps函数返回的数据。
        this.mobxStore = isServer 
        ? props.initialMobxState 
        : initializeStore(props.initialMobxState);
    }

    render() {
        const { Component, pageProps } = this.props;
        // 将数据通过Provider注入组件中。
        return <Provider {...this.mobxStore}>
            <Layout>
                <Component {...pageProps} />
            </Layout>
        </Provider>
    }
}
export default MyApp

在MyApp中我们可以获取用户的数据,方便在页面中的使用,下面代码中如果能获取到authInfo对象就获取用户信息,getInitialProps修改为:

    static async getInitialProps(appContext) {
        const ctx = appContext.ctx;
        ctx.mobxStore = initializeStore();
        const appProps = await App.getInitialProps(appContext);
        if (typeof ctx.query.authInfo === 'object' && null !== ctx.query.authInfo) {
            const userResult = await getUser({
                token: ctx.query.authInfo.token,
            })
            let user = FormatUser(userResult.data);
            ctx.mobxStore.userStore.setCurrentUser(user);
        }
        return {
          ...appProps,
          initialMobxState: ctx.mobxStore
        }
    }

authInfo对象在server.js文件的路由通过cookie中数据构建。

在页面中使用observer将Products变为监听者,同时通过inject将userStore的数据传入Products

import React from 'react';
import Head from 'next/head'
import Style from './style.scss';
import { inject, observer } from 'mobx-react'

@inject('userStore')
@observer
class Products extends React.Component {
  static async getInitialProps({ pathname, asPath, query, mobxStore , req}) {
    //获取产品列表
    //xxxxx
    return {}
  }

  componentDidMount() {
    console.log(this.props.userStore.currentUser)
  }
  
  render() {
    return <div className={Style.products}>
      <Head>
        <title>产品 | vwood</title>
      </Head>
      <div>
        product
      </div>
    </div>
  }
}

export default Products;

next.config.js

next内置了从变异到打包的所有功能,可通过next.config.js添加自己的配置,比如我们要使用图片就需要添加loader,要使用scss也需要zeit/next-sass插件。

const withCss = require('@zeit/next-css')
const withSass = require('@zeit/next-sass');
const webpack = require('webpack');

if (typeof require !== 'undefined') {
  require.extensions['.css'] = file => {}
}

const config = {
  distDir: '_next',
  webpack: (config, {
    dev,
  }) => {
    config.module.rules.push({
      test: /\.(ico|gif|png|jpg|jpeg|webp)$/,
      use: [{
        loader: "url-loader",
        options: {
          limit: 8192,
          fallback: {
            loader: 'file-loader',
          }
        }
      }]
    });
    config.plugins.push(
      new webpack.DefinePlugin({
        'isDev': JSON.stringify(dev),
      }))

    return config
  },
}
// withCss得到的是一个next的config配置
module.exports = withCss({
  ...withSass({
    ...config,
    cssModules: true,
    cssLoaderOptions: {
      importLoaders: 1,
      localIdentName: "[local]___[hash:base64:5]",
    }
  }),
  cssModules: false,
});

server.js

// server.js
const Koa = require('koa');
const Router = require('koa-router');
const next = require('next');
const dev = process.env.NODE_ENV !== 'production'
const app = next({
  dev
})
const handle = app.getRequestHandler()
const PORT = 3000;
// 等到pages目录编译完成后启动服务响应请求
app.prepare().then(() => {
  const server = new Koa()
  const router = new Router()
  router.get('/', async ctx => {
    const params = {
      // authInfo: getToken(ctx),
      ...ctx.query,
    };
    await app.render(ctx.req, ctx.res, '/', params)
    ctx.respond = false
  })
  router.get('/products', async ctx => {
    const params = {
      ...ctx.query,
      // authInfo: getToken(ctx),
    }
    await app.render(ctx.req, ctx.res, '/products', params);
    ctx.respond = false;
  })
  // 如果没有配置nginx做静态文件服务,下面代码请务必开启
  router.get('*', async ctx => {
    await handle(ctx.req, ctx.res)
    ctx.respond = false
  })
  server.use(async (ctx, next) => {
    ctx.res.statusCode = 200
    await next();
  })
  server.use(router.routes())
  server.listen(PORT, () => {
    console.log(`koa server listening on ${PORT}`)
  })
})

编译代码

在package.json中添加如下代码

  "scripts": {
    "dev": "NODE_ENV=development && node server.js",
    "build": "rm -rf _next && NODE_ENV=production next build",
    "start": "NODE_ENV=production node server.js",
  },

通过npm run dev在本地运行代码,npm run build打包项目。

nginx配置

server {
	listen 80;
	server_name vwood.xyz www.vwood.xyz;
	return 301 https://$host$request_uri;
}

server {
	#SSL                            访问端口号为 443
	listen 443 ssl http2;
	#填写绑定证书的域名
	server_name vwood.xyz www.vwood.xyz;
	#启用                             SSL 功能
	ssl on;
	#证书文件名称
	ssl_certificate // xxxxxxx .crt文件配置;
	#私钥文件名称
	ssl_certificate_key // xxxxxxx .key文件配置;
	ssl_session_timeout 10m;
	ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
	ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
	ssl_prefer_server_ciphers on;

	include /etc/nginx/default.d/*.conf;

	gzip on;
	gzip_min_length 1k;
	gzip_buffers 4 16k;
	gzip_comp_level 5;
	gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/javascript;

	root // xxxxx 项目路径;

	location / {
		proxy_pass http://localhost:port;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_set_header Host $host;
		proxy_set_header X-Nginx-Proxy true;
		proxy_cache_bypass $http_upgrade;
	}

	#要缓存文件的后缀,可以在以下设置。
	location ~ .*\.(css|js)(.*) {
		expires 30d;
		add_header Cache-Control must-revalidate;
	}

	location ~ .*\.(gif|jpg|png)(.*) {
		expires 30d;
		add_header Cache-Control must-revalidate;
	}
	error_page 404 /404.html;
	location = /40x.html {
	}

	error_page 500 502 503 504 /50x.html;
	location = /50x.html {

	}
}

PS:在写项目时查看了他人的文章,如果文章引用了您的代码请告知,将加上原文链接_

欢迎各位指正文章的错误或不合理之处。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值