闲来无事,利用业余时间做了一个类似productHunt的网站,将实现过程整理一下。
一、框架
技术: 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:在写项目时查看了他人的文章,如果文章引用了您的代码请告知,将加上原文链接_。
欢迎各位指正文章的错误或不合理之处。