Midway 最近可以支持 esm 了,目前仅限于 koa 框架。cjs 和 esm 在集成过程中没什么大区别,笔者还没有习惯纯 esm 开发,暂未切换,仅为个人喜好和选型结果。ESModule 使用指南 | Midway
撰写本文时的最新版本为:
- @midwayjs/core 3.12.3
- @midwayjs/koa 3.12.3
- @remix-run/node 2.1.0
为什么选择 midway
midway 和 koa,在我看来有点像 django 和 flask,很多东西官方都封装好了,开发起来会快速一些。
为什么选择 remix
midway 有一体化框架,但是已经许久没更新了,估计是废了。
也可以选择直接静态托管独立前端项目编译后的 dist 目录,但是这样开发过程中会比较繁琐,且需要手动添加全部前端路由映射到 index.html,否则刷新无法访问。
当然一切看自行选型结果,本文默认读者已了解前端方案中 create-react-app、vite-react-ts、nextjs、remix 等各自的区别。
集成
remix 还处在一个快速迭代的过程中,一些早期教程中提到的命令、代码可能已经不可用了。
本文可能也会在某一版上游更新后变得不可用,领会方法是关键,切勿生搬硬套。
集成所需完成的核心工作并其实不复杂:
remix build
生成两部分文件- server 入口文件,供 midway 调用
- assets 静态文件,供 midway 托管
- midway 中间件调用 remix server 入口文件 来响应路由请求
- midway 使用官方组件 静态文件托管 | Midway 托管 remix assets 静态文件
- 对
remix dev
进行支持- 前端热更新
- 编译文件缓存处理
创建 remix 目录
官方教程很清楚,不展开。将 react 代码放入 app 目录中。
修改 midway 主目录(可以不改)
midway dev 时默认主目录为 src,重命名为 server,可以不改。修改后启动命令中加上 --sourceDir server
。
"dev:midway": "cross-env NODE_ENV=local midway-bin dev --sourceDir server --ts"
remix.config.js 设置
- assetsBuildDirectory 默认为 public/build
- publicPath 默认为 /build/
- serverBuildPath 设置为 server/remix/index.js
- serverModuleFormat 设置为 cjs
/** @type {import('@remix-run/dev').AppConfig} */
module.exports = {
serverBuildPath: 'server/remix/index.js',
serverModuleFormat: 'cjs',
};
更多参考官方文档 remix.config.js | Remix
预编译 remix 项目
运行 remix build
后会生成两部分文件:
- server/remix/ server 入口文件
- public/build/ assets 静态文件
midway 中间件调用 remix server 入口文件
部分代码摘录于 GitHub - michaelhelvey/remix-koa-adapter: Koa request server handler for Remix
import { IMiddleware, IMidwayApplication, Init, Singleton } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';
import {
createRequestHandler as createRemixRequestHandler, ServerBuild, writeReadableStreamToWritable,
} from '@remix-run/node';
import { join } from 'path';
@Singleton()
export class RemixMiddleware implements IMiddleware<Context, NextFunction> {
build: ServerBuild;
@Init()
async init() {}
resolve(app: IMidwayApplication) {
// 也可以放在 init 里,我放在这里是为了方便拼接目录
if (!this.build) {
const BUILD_DIR = join(app.getBaseDir(), 'remix');
this.build = require(BUILD_DIR);
}
const handleRequest = createRemixRequestHandler(this.build);
return async (ctx: Context, next: NextFunction) => {
const request = this.createRemixRequest(ctx);
const response = await handleRequest(request);
await this.sendRemixResponse(ctx, response);
return next();
};
}
createRemixRequest(ctx: Context) {
const origin = `${ctx.protocol}://${ctx.host}`;
const url = new URL(ctx.url, origin);
// Abort action/loaders once we can no longer write a response
const controller = new AbortController();
ctx.res.on('close', () => controller.abort());
const init: RequestInit = {
method: ctx.method,
headers: this.createRemixHeaders(ctx.headers),
// Cast until reason/throwIfAborted added
// https://github.com/mysticatea/abort-controller/issues/36
signal: controller.signal as Exclude<RequestInit['signal'], undefined>,
};
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
init.body = ctx.request.body as BodyInit;
}
return new Request(url.href, init);
}
createRemixHeaders(requestHeaders: Context['headers']) {
const headers = new Headers();
for (const [key, values] of Object.entries(requestHeaders)) {
if (values) {
if (Array.isArray(values)) {
for (const value of values) {
headers.append(key, value);
}
} else {
headers.set(key, values);
}
}
}
return headers;
}
async sendRemixResponse(ctx: Context, response: Response) {
ctx.status = response.status;
ctx.message = response.statusText;
for (const [key, values] of Object.entries(response.headers)) {
for (const value of values) {
ctx.append(key, value);
}
}
if (response.body) {
await writeReadableStreamToWritable(response.body, ctx.res);
}
}
ignore(ctx: Context) {
// remix 也有自带的 404 页面,需要过滤路由
return ctx.path.startsWith('/api/');
}
static getName(): string {
return 'remix';
}
}
托管 remix assets 静态文件
- 引入组件依赖 @midwayjs/static-file
- 配置
// ...
staticFile: {
dirs: {
default: {
// 为 remix.config.js 的 publicPath 值
prefix: '/build/',
// 为 remix.config.js 的 assetsBuildDirectory 值
dir: 'public/build',
}
}
// ...
esm 的部分写法限制
- 不再支持 alias path,需要用 Node.js 自带的 子路径导出 代替
- ts 中,import 的文件必须指定后缀名,且后缀名为 js
- 不能使用 require 只能使用 import 关键字
- 不能使用 __dirname,__filename 等和路径相关关键字