使用nextjs做一個類似productHunt的網站

閒來無事,利用業餘時間做了一個類似productHunt的網站vwood,將實現過程整理一下。

一、框架

技術: 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:在寫項目時查看了他人的文章,如果文章引用了您的代碼請告知,將加上原文鏈接_

歡迎各位指正文章的錯誤或不合理之處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章