文章
问答
冒泡
使用next.js 创建web端应用

1.项目背景
在当前项目中,我们经常会把项目分为,web端,移动端,管理后台,等等。为了实现接口的共用 ,项目的设计是把web端,移动端都使用同样的的接口去获取数据。由于业务性质,需要考虑到搜索引擎的抓取,所以使用了next.js来实现ssr。
2.准备工作
2.1 配置.npmrc文件,设置淘宝的源

registry=[https://registry.npm.taobao.org/](https://registry.npm.taobao.org/) 
disturl=[https://registry.npm.taobao.org/node](https://registry.npm.taobao.org/node) 
sass_binary_site=[https://npm.taobao.org/mirrors/node-sass/](https://npm.taobao.org/mirrors/node-sass/) 
fse_binary_host_mirror=[https://npm.taobao.org/mirrors/fsevents](https://npm.taobao.org/mirrors/fsevents)



2.2 初始化工程。并添加 next,react,react-dom依赖

{
  "name": "next-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "next",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^9.3.6",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }
}

2.3 在工程下添加pages文件夹,并创建一个测试文件,index.jsx。

import React from "react";

export default ()=>{
    return <div>index</div>
}

根据package.json 的脚本 执行 npm run dev ,则会启动next的开发环境
image.png
至此,next的基本环境已经完成。
3. 配置自定义服务端
由于一般情况下,简单的基于pages下目录的路径是不能满足项目需求的,所以,我们还需要自定义的服务来处理next的启动。这里我们选择了koa。
3.1 koa同样需要去调用next的渲染功能,这个时候,我们就需要活动next的对象。创建server文件夹,并在下面创建一个NextApp的文件。

import next from "next";

const dev = process.env.NODE_ENV !== 'production';
export default next({ dev });



然后创建 index.js这个文件 ,作为koa的服务启动文件。

import http from "http";
import app from "./NextApp";
import Koa from "koa";

app.prepare().then(() => {
    const server = new Koa();
    http.createServer(server.callback()).listen("3000", () => {
        console.log(`> Ready on [http://localhost:3000](http://localhost:3000)`)
    });
});

koa服务的基本配置也完成了。

4.工程结构设计
上面的步骤我们已经把一个最基本的Koa和nextjs的环境配置好了。但是,一个工程需要从多方面考虑,设计一个合理的工程结构,才能让代码有更好的可读性。
根据架构设计,该工程定义为web端,是不需要直接访问数据库的,所有的数据都通过接口获得。那么我们将工程总体分为两大块,服务端,前端。
image.png
5.细节设计
5.1 关于接口调用
我们已经清楚,项目的数据来源都是来自接口,那么,我们就会存在服务端和前端都去调用接口的情况。项目中,我们通过axios作为http client。那么下面问题来了,我们是两个调用环境,仅仅一个axios 实例是不够的,我们需要将前端和服务端的axios独立配置。
5.1.2 proxy
如果是一个基于nginx部署的纯静态项目,我们可以通过nginx的反向代理来将请求转发到接口服务器。但是,此处我们是一个koa的项目,当然,我们也可以请求服务端,然后服务端再去请求接口服务,这样固然是可行的,却是增加了很多不必要的工作量。解决思路就是直接用Koa做代理,进行请求的转发。这里,我们使用 koa-proxies 来实现。

const proxy = require('koa-proxies');
server.use(proxy("/api", {
    target: "[http://api-ithere/](http://api-ithere/)",
    changeOrigin: true,
    logs: true
}));

5.1.3 ctx的共享
上面我们已经解决了前端的问题,下面我们需要来解决服务端的请求问题。服务端是直接调用接口,这个是没有问题的,不需要转发,但是,服务端有服务端的问题,就是服务端是拿不到浏览器上的cookie数据的,我们的接口是无状态设计,是需要获取请求方带来的token。但是,koa 的ctx 是没法从外部获得的,只能通过参数层层传递。如果我不想在每个请求都把ctx传过来怎么办呢?这里我们通过nodejs的 async_hooks 来实现。
创建一个AsyncHookCtx.js文件

const asyncHooks = require("async_hooks");
const ctxStore = new Map();

const asyncHook = asyncHooks.createHook({
    init(asyncId,type,triggerAsyncId,resource){
        const parentCtx = ctxStore.get(triggerAsyncId);
        if(parentCtx){
            ctxStore.set(asyncId,parentCtx);
        }
    },
    destroy(asyncId) {
        ctxStore.delete(asyncId);
    }
});
asyncHook.enable();

class AsyncHookCtx {

    static setCtx(ctx){
        let key = asyncHooks.executionAsyncId();
        ctxStore.set(key,ctx);
    }
    static getCtx(){
        return ctxStore.get(asyncHooks.executionAsyncId());
    }
}
export default AsyncHookCtx



这样,我们就可以通过静态方法获取到ctx了。 当然,ctx 不是平白来的,需要先给他赋值。这个时候,koa 的中间件(middleware)就发挥作用了。
创建CtxKoa.js

import AsyncHookCtx from "../../async_hooks/AsyncHookCtx";

function koaCtx(root, ops) {

    return async function koaCtx(ctx, next) {
        try {
            AsyncHookCtx.setCtx(ctx);
            await next();
        } catch (e) {
            throw e;
        }
    }

}

export default koaCtx



5.1.4 axios 的自定义
WebAxios

require('promise.prototype.finally').shim();
require('es6-promise').polyfill();
import axios from "axios";
import CookieUtils from "../util/CookieUtils";
import ExceptionUtils from "../util/ExceptionUtils";

const WebAxios = axios.create({
    baseURL: '/api',
    headers: {
        "Content-Type": "application/json;charset=UTF-8"
    }
});

WebAxios.interceptors.request.use((request) => {
    let token = CookieUtils.getToken();
    if (token) {
        request.headers["Authorization"] = "Bearer " + token;
    }
    return request
});
export default WebAxios;



ServerAxios

import axios from "axios";
import config from "../../bootstrap/AppConfig";
import logger from "../../bootstrap/logger";
import AsyncHookCtx from "../../async_hooks/AsyncHookCtx";
import {COOKIE_AUTHORIZATION} from "../../constant/CookieConstants";
import {HEADER_AUTHORIZATION} from "../../constant/HeaderConstants";

let ServerAxios = axios.create({
    baseURL: config.get("api.base-url"),
    headers: {
        "Content-Type": "application/json;charset=UTF-8"
    }
});

ServerAxios.interceptors.request.use((request) => {
    if (process.env.NODE_ENV !== 'production') {
        // console.log(request);
    }
    let ctx = AsyncHookCtx.getCtx();
    if (ctx) {
        let token = ctx.cookies.get(COOKIE_AUTHORIZATION);
        if (token) {
            request.headers[HEADER_AUTHORIZATION] = token;
        }

    }
    request.headers.host = null;
    return request
});

export default ServerAxios;



5.2 api的接口
由于前端和服务端都需要去调用接口,那么接口定义文件,我们只需要写一个即可。比如这样

class TagApi {
    constructor(axios) {
        this.axios = axios;
    }

    save(data){
        return this.axios.put(`/tag/tags/save`,data);
    }

    queryTags(params){
        return this.axios.get(`/tag/tags/all`,{params:params});
    }

}
export default TagApi



很明显了,这个Api 是需要实例化的。但是,要注意的是 ,不要在每个调用处是实例化。 在统一的地方进行实例化。

import ServerAxios from "../../conf/axios/ServerAxios";
import RouteApi from "../../api/user/RouteApi";
import ArticleApi from "../../api/article/ArticleApi";
import ArticleCollectionApi from "../../api/article/ArticleCollectionApi";
import WeChatWebsiteApi from "../../api/wechat/WeChatWebsiteApi";
import WeChatOfficialAccountApi from "../../api/wechat/WeChatOfficialAccountApi";
import UserProfileApi from "../../api/user/UserProfileApi";


export const routeApi = new RouteApi(ServerAxios)
export const userProfileApi = new UserProfileApi(ServerAxios);

export const articleApi = new ArticleApi(ServerAxios);
export const articleCollectionApi = new ArticleCollectionApi(ServerAxios);

export const weChatWebsiteApi = new WeChatWebsiteApi(ServerAxios);
export const weChatOfficialAccountApi = new WeChatOfficialAccountApi(ServerAxios);



为什么用统一实例化? 因为多次实例化,会导致有的里面的axios是undefined
5.3 配置文件
配置文件的问题之前有写过 https://www.ithere.net/article/171

以上就是 使用next.js 创建web端应用的一些步骤和注意点了。

next.js

关于作者

落雁沙
非典型码农
获得点赞
文章被阅读