Midway.js 集成 Remix run


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 还处在一个快速迭代的过程中,一些早期教程中提到的命令、代码可能已经不可用了。
本文可能也会在某一版上游更新后变得不可用,领会方法是关键,切勿生搬硬套。

集成所需完成的核心工作并其实不复杂:

  1. remix build 生成两部分文件
    • server 入口文件,供 midway 调用
    • assets 静态文件,供 midway 托管
  2. midway 中间件调用 remix server 入口文件 来响应路由请求
  3. midway 使用官方组件 静态文件托管 | Midway 托管 remix assets 静态文件
  4. 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 等和路径相关关键字
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值