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的开发环境
至此,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端,是不需要直接访问数据库的,所有的数据都通过接口获得。那么我们将工程总体分为两大块,服务端,前端。
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端应用的一些步骤和注意点了。