閒來無事,利用業餘時間做了一個類似productHunt的網站vwood,將實現過程整理一下。
一、框架
技術: nextjs + koa + react + mobx+ antd
- 爲了更好的支持SEO,所以需要服務器渲染,框架選擇nextjs;同時加入了koa配合使用。
- 前端一直使用自己熟悉的技術react、antd、sass。
- 數據管理使用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;
}
}
代碼中observable、action使用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:在寫項目時查看了他人的文章,如果文章引用了您的代碼請告知,將加上原文鏈接_。
歡迎各位指正文章的錯誤或不合理之處。