UmiJs整合Egg

7 篇文章 1 订阅
5 篇文章 0 订阅

一. 初始化项目

这里,我们用UmiJs脚手架来初始化一个项目:

1.创建一个名为Umi_Egg的空文件夹:
在这里插入图片描述
2.在该文件夹目录中输入命令:

npm create @umijs/umi-app

结果如下,项目结构为:
在这里插入图片描述
3.在根目录下,创建Egg项目

npm init egg --type=simple

运行如下:
在这里插入图片描述
4.注意,我们这里利用脚手架分别创建了两个项目,自然而然的会生成两个package.json文件,这里我们将里面的内容整合下,保留最外层的package.json文件,内容如下:

{
  "private": true,
  "name": "umi_egg",
  "scripts": {
    "start": "egg-scripts start --daemon --title=egg-server-app",
    "stop": "egg-scripts stop --title=egg-server-app",
    "clean": "ets clean",
    "build": "umi build",
    "postinstall": "umi generate tmp",
    "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
    "dev": "npm run clean && egg-bin dev --port 4396",
    "test": "umi-test",
    "debug": "egg-bin debug --port 4396",
    "test:coverage": "umi-test --coverage"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,less,md,json}": [
      "prettier --write"
    ],
    "*.ts?(x)": [
      "prettier --parser=typescript --write"
    ]
  },
  "egg": {
  	"typescript": true,
    "declarations": true
  },
  "dependencies": {
    "@ant-design/pro-layout": "^6.5.0",
    "axios": "^0.27.2",
    "egg": "^2.29.3",
    "egg-cors": "^2.2.3",
    "egg-http-proxy": "^1.0.1",
    "egg-scripts": "^2.13.0",
    "egg-socket.io": "^4.1.6",
    "egg-view-assets": "^1.6.1",
    "egg-view-ejs": "^2.0.1",
    "react": "17.x",
    "react-dom": "17.x",
    "umi": "^3.5.23"
  },
  "devDependencies": {
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "@typescript-eslint/eslint-plugin": "^5.21.0",
    "@umijs/preset-react": "1.x",
    "@umijs/test": "^3.5.23",
    "autod": "^3.0.1",
    "autod-egg": "^1.1.0",
    "egg-bin": "^4.11.0",
    "egg-ci": "^1.11.0",
    "egg-mock": "^3.21.0",
    "egg-ts-helper": "^1.25.9",
    "eslint": "^7.32.0",
    "eslint-config-egg": "^9.0.0",
    "eslint-plugin-react": "^7.25.0",
    "lint-staged": "^10.0.7",
    "prettier": "^2.2.0",
    "typescript": "^4.6.4",
    "yorkie": "^2.0.0"
  }
}

5.对目录做出如下调整:(mock以及app下的test目录也没啥用,可以删掉)
在这里插入图片描述
更改后目录结构如下:
在这里插入图片描述
6.最重要的环节:

npm install 

二. Umi和Egg的细节整合

到这里,初始化工作也完成了,接下来开始讲两个框架进行整合。

咱们先来启动下项目,看看是否能够正常启动并访问,大家使用命令:

npm run dev

结果应该如下:
在这里插入图片描述
访问对应的URL:http://127.0.0.1:4396/结果如下:如果正常访问则说明项目是没什么问题的
在这里插入图片描述

2.1 使用TypeScript

为什么要使用TypeScript呢,这里做个解释,TypeScript本身就是作为JavaScript的一个超集语言。而TypeScript可以用于编写前端页面(index.tsx)以及后端代码(index.ts),方便语言的一个统一。

1.安装TypeScript:

npm install typescript
npm install @typescript-eslint/eslint-plugin --save-dev

此时,我们就能将后端代码改成这样,例如:
controller/home.ts文件:

import { Controller } from 'egg';

class HomeController extends Controller {
  getData() {
    this.ctx.body = this.service.userService.getUserName();
  }
}
export default HomeController;

router.ts文件:

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Application } from 'egg';

export default (app: Application) => {
  const { controller, router } = app;
  router.prefix('/zong*');
  router.post('/user/getData', controller.home.getData);
};

service/UserService.ts文件:

import { Service } from 'egg';

class UserService extends Service {
  getUserName() {
    return 'LJJ';
  }
}
export default UserService;

在这里,有的读者可能会发现,在写代码的时候,通过controler.xxx的时候,没有对应的提示代码。这里需要借助egg-ts-helper插件来生成对应的声明(dev依赖中已经添加了):

package.json文件中的scripts属性下添加脚本命令:

"tsc": "ets && tsc -p tsconfig.json",

同时,我们要在tsconfig.json文件中做对应的配置,目的是防止一些不必要的目录被egg-ts-helper插件执行,生成对应的js文件,可能会出现很多Error。内容如下:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "moduleResolution": "node",
    "importHelpers": true,
    "jsx": "react",
    "esModuleInterop": true,
    // "outDir":"./dist",
    "baseUrl": "./",
    "strict": true,
    "paths": {
      "@public/*": ["public/*"],
      "@/*": ["src/*"],
      "@@/*": ["src/.umi/*"],
      "@defineType/*": ["./model/*"]
    },
    "resolveJsonModule": true,
    "suppressImplicitAnyIndexErrors": true,
    "allowSyntheticDefaultImports": true,
    "noImplicitAny": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "charset": "utf8",
    "allowJs": false,
    "pretty": true,
    "noEmitOnError": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "strictPropertyInitialization": false,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "inlineSourceMap": true,
    "skipDefaultLibCheck": true
  },
  "exclude": [
    "app/public",
    "app/views",
    "node_modules",
    "lib",
    "es",
    "dist",
    "**/__test__",
    "test",
    "docs",
    "tests"
  ]
}

然后运行命令

npm run tsc

运行结果如下:(下面的错误其实没关系,因为前端的代码我们还没有改动过,先放着,不过可以看到红框地方,插件会生成对应的声明文件)
在这里插入图片描述
生成好后,咱们写代码的时候就会有这样的效果:
在这里插入图片描述
小伙伴本记得运行下npm run clean哦。

2.2 使用Eslint

配置下相关的eslint,这样开发起来会规范一点,更改下.eslintrc.json文件:

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:@typescript-eslint/recommended",
    "eslint-config-egg/typescript"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 12,
    "sourceType": "module"
  },
  "plugins": ["react", "@typescript-eslint"],
  "rules": {
    "indent": ["error", 2, { "SwitchCase": 1 }],
    "react/display-name": ["off", { "ignoreTranspilerName": true }],
    "react/prop-types": ["off"],
    "@typescript-eslint/no-var-requires": 0,
    "no-bitwise": ["off"]
  }
}

配置好后,效果如图:(记得设置下保存的时候自动使用eslint
在这里插入图片描述

到这里为止,我们后端已经写完了,那么如何在前端页面调用后端的接口呢?

2.3 前端发起请求

我们利用axios来发起请求,先下载对应的包:

npm install axios

src/utils目录下创建一个工具类:axiosHelper.ts

import axios from 'axios';

export default async function(
  prefixUrl: string,
  methodName: string,
  data: any,
) {
  let response;
  try {
    response = await axios({
      url:
        location.protocol +
        '//' +
        window.location.host +
        '/zong' +
        prefixUrl +
        methodName,
      method: 'post',
      data,
      withCredentials: true,
    });
  } catch (error) {
    console.log(error);
  }
  return response;
}

page/index.tsx文件:

import React from 'react';
import { Button } from 'antd';
import axios from '../utils/axiosHelper';

const UserPage = () => {
  return <Button onClick={() => axios('/user', '/getData', { msg: 'hello' }).then(val => { console.log(val); })}>数据获取</Button>;
};

export default UserPage;

到这里,前端页面也写好了,那么大家可以从这个角度来思考下以下问题:

  • 通过Egg的脚手架创建的后端程序,运行命令为:egg-bin dev
  • 通过UmiJs脚手架创建的前端程序,运行命令为:umi dev

两者命令不一样,启动时的默认端口也不一样,那么怎么去同时启动呢?一定要配置跨域吗?若是如此,这还能叫做Umi整合Egg吗?不对。接下来开始讲解本文的细节部分。

2.4 利用egg-view模板来加载umiJs

到目前为止,项目中比较重要,但是又没有动过的,只有egg的配置文件了:
在这里插入图片描述
话不多说,我们直接上代码(后面再讲具体的流程):
1.更改config.default.ts文件(自己改下后缀哦):先npm install cross-env

/* eslint-disable @typescript-eslint/no-unused-vars */
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';

export default (appInfo: EggAppInfo): any => {
  /**
   * built-in config
   * @type {Egg.EggAppConfig}
   **/
  const config = {} as PowerPartial<EggAppConfig>;
  // 业务相关配置,这里是拿到我们的启动环境
  const bizConfig = {
    envName: appInfo.pkg.config.env.toLowerCase(),
  };
  // use for cookie sign key, should change to your own and keep security
  config.keys = appInfo.name + '_1650629680875_8410';

  // add your middleware config here
  config.middleware = [];

  config.security = {
    csrf: {
      enable: false,
    },
  };
  // 开启ejs模板的使用,这里一定要配置,否则egg不会使用ejs模板,会报错
  config.view = {
    mapping: {
      '.ejs': 'ejs',
    },
  };
  // 本地代理,启动umi dev,监听8007端口
  config.assets = {
    publicPath: '/public',
    devServer: {
      // 这里是本地的一个代理,用了cross-env命令,因此需要npm install cross-env
      // 同时UMI_ENV=dev命令,若使用,则必须拥有一个名为.umirc.dev.ts的文件
      // 也因此后续要创建两个文件,一个.umirc.dev.ts,一个.umirc.ts,一般用于生产和本地开发配置的区分
      command: 'cross-env UMI_ENV=dev umi dev --port=8007',
      port: 8007,
      env: {
      	// 这里的baseDir也就是 下文的 server.js文件中赋值的。程序启动的时候,会自动读取
        APP_ROOT: appInfo.baseDir,
        BROWSER: 'none',
        ESLINT: 'none',
        SOCKET_SERVER: 'http://127.0.0.1:8007',
        PUBLIC_PATH: 'http://127.0.0.1:8007',
      },
    },
  };

  return {
    ...config,
    ...bizConfig,
  };
};

2.更改plugin.ts文件:

/* eslint-disable @typescript-eslint/no-unused-vars */
import { EggPlugin } from 'egg';

const plugin: EggPlugin = {
  assets: {
    enable: true,
    package: 'egg-view-assets',
  },
  ejs: {
    enable: true,
    package: 'egg-view-ejs',
  },
  httpProxy: {
    enable: true,
    package: 'egg-http-proxy',
  },
  cors: {
    enable: true,
    package: 'egg-cors',
  },
};
export default plugin;

3.根目录下创建文件.umirc.dev.ts,和原有的.umirc.ts文件内容也跟下面的配置保持一致。

import { defineConfig } from 'umi';

export default defineConfig({
  base: '/zong/',
  nodeModulesTransform: {
    type: 'none',
  },
  manifest: {
    fileName: './config/manifest.json',
    publicPath: '/public/',
  },
  // 打成的包输出的路径
  outputPath: './app/public',
  publicPath: '/public/',
  fastRefresh: {},
});

4.根目录下创建server.js文件:

/* eslint-disable indent */
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/no-var-requires */
const path = require('path');
const { Application } = require('egg');
const pkj = require('./package.json');

const app = new Application({
  baseDir: path.resolve('./'),
});

app.ready(() => app.listen(pkj.config.port));

5.根目录下创建app.config.js文件:

module.exports = {
  Env: 'dev',
};

6.根目录下创建app.ts文件:

import { Application } from 'egg';

class AppBootHook {
  private app: Application;

  constructor(app: Application) {
    this.app = app;
  }

  configWillLoad() {
    // 此时 config 文件已经被读取并合并,但是还并未生效
    // 这是应用层修改配置的最后时机
    // 这里则加入一个版本号,目的是为了下文ejs模板里面,加载umiJs后,能保证每次项目重新发布(启动的时候),不会因为缓存而导致未更新
    this.app.config.fileVersion = new Date().getTime().toString();
  }
}

module.exports = AppBootHook;

7.在app目录下创建view目录,用于放ejs文件,同时创建index.ejs文件:
在这里插入图片描述
内容如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Test</title>

    <% if (envName == "dev") { %>
        <%- helper.assets.getStyle('umi.css') %>
            <% } else { %>
                <link rel="stylesheet" type="text/css" href='/<%- contextPath %>/public/umi.css?v=<%- fileVersion %>' />
                <% } %>
</head>

<body>
    <div id='root' class='subRootContent'>
    </div>
    <script>
        window.resourceBaseUrl = '<%= helper.assets.resourceBase %>';
        <% if (envName != "dev") { %>
        window.staticUrl = '/<%- contextPath %>/public'
        window.resourceBaseUrl = '/<%- contextPath %><%= helper.assets.resourceBase %>';
        <% } %>
        window.publicPath = resourceBaseUrl
    </script>
    <% if (envName == 'dev') { %>
        <%- (helper.assets.getScript('umi.js')) %>
            <% } else { %>
                <script src='/<%- contextPath %>/public/umi.js?v=<%- fileVersion %>'></script>
                <% } %>
</body>

</html>

8.在config目录下,创建manifest.json映射文件:

{
  "umi.css": "umi.css",
  "umi.js": "umi.js",
  "index.html": "index.html"
}

9.修改controller/home.ts文件:增加一个index方法,主要是使用index.ejs模板。

async index() {
    const { config } = this;
    const { envName, fileVersion } = config;
    // 这里的data数据,加载到ejs模板后,直接可以通过对应的属性名来获取对应的值。
    // 同时注意,要使用ejs模板,必须要有个contextPath
    const data = {
      envName,
      fileVersion,
      contextPath: 'zong',
    };
    await this.ctx.render('index.ejs', data);
}

10.修改路由,增加监听:这样页面上的任何一个请求,都会被Egg路由捕捉到,然后到对应的index方法下去执行。

router.get('**/**', controller.home.index);
router.post('**/**', controller.home.index);

最后先npm run build,在npm run dev,项目倘若启动成功:会出现以下字样

  • 4396是Egg的端口,出现了说明Egg程序运行成功。
  • 8007端口是umi dev命令的一个启动指定端口,出现了说明前端也运行成功了。

在这里插入图片描述
运行成功后,访问页面http://localhost:4396/zong/,可以看到,我们用Egg程序的4396端口就能访问前端Umi的页面,同时还能够正常的获得请求。说明整合成功了。
在这里插入图片描述

然后我将代码的源码发给大家,大家可以对照下代码,若出错了,看下是否哪里配置有问题(也有可能是我写的不够详细或者漏说,若哪里漏说了,还请指正)源码

大家把代码拉下来,npm install后就可以跑了。

三. 整合的原理和相关知识点

3.1 项目启动过程

1.npm run dev的时候,实际上跑的命令是egg-bin dev,因此,本项目跑起来后实际上是一个Egg项目!

2.程序会在启动的过程中,加载这几个文件:

  • app.config.js:配置了个Env属性,只在本地开发有效。
  • server.js:配置了项目的地址baseDir
  • config/config.default.ts:Egg的相关配置。
  • 其他的略。

3.egg配置中,我们使用了assets代理:主要通过command命令,执行了umi dev命令,同时指定端口8007。

config.assets = {
  publicPath: '/public',
  devServer: {
    command: 'cross-env UMI_ENV=dev umi dev --port=8007',
    port: 8007,
    env: {
      APP_ROOT: appInfo.baseDir,
      BROWSER: 'none',
      ESLINT: 'none',
      SOCKET_SERVER: 'http://127.0.0.1:8007',
      PUBLIC_PATH: 'http://127.0.0.1:8007',
    },
  },
};

4.到这里,前后端实际上都是运行成功了。那么我们在URL上,通过4396(Egg后端端口)就能访问前端页面的原理是啥?大家请看路由:

router.get('**/**', controller.home.index);
router.post('**/**', controller.home.index);

此时,URL上访问的任何路径,都会被该路由捕捉,毕竟**/**摆在这里😂,然后会根据路由指定的controller.home.index方法去执行对应的逻辑。

async index() {
  const { config } = this;
  const { envName, fileVersion } = config;
  const data = {
    envName,
    fileVersion,
    contextPath: 'zong',
  };
  await this.ctx.render('index.ejs', data);
}

那么这里我们将版本号、环境属性、URL的相关路径都塞到了ejs里面,我们来看下ejs:注意这里我只是在上文基础上加了注释,实际上不能这么写的哦。

<head>
	// 这里也就是很普通的一个HTML文件的头
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Test</title>
	// 如果环境是dev,那么我们会根据项目里配置的assets,通过它来走代理,自动获取名为umi.css的文件。umi.css是umiJs打包后必会生成的东西。
    <% if (envName == "dev") { %>
        <%- helper.assets.getStyle('umi.css') %>
            <% } else { %>
            	// 否则,我们将加载项目静态目录public下的umi.css文件,记住,生产上访问的都是静态文件。前端都是打包打好的
                <link rel="stylesheet" type="text/css" href='/<%- contextPath %>/public/umi.css?v=<%- fileVersion %>' />
                <% } %>
</head>

<body>
    <div id='root' class='subRootContent'>
    </div>
    <script>
    	// resourceBaseUrl,staticUrl,都是必须要配置的,这里的脚本,可以说是一个固定的模板了,要和项目内的前缀URL保持一致,我这里配置的是zong
        window.resourceBaseUrl = '<%= helper.assets.resourceBase %>';
        <% if (envName != "dev") { %>
        window.staticUrl = '/<%- contextPath %>/public'
        window.resourceBaseUrl = '/<%- contextPath %><%= helper.assets.resourceBase %>';
        <% } %>
        window.publicPath = resourceBaseUrl
    </script>
    // 同理上述的umi.css文件,实际上我们的前端页面都打包到umi.js了,
    <% if (envName == 'dev') { %>
        <%- (helper.assets.getScript('umi.js')) %>
            <% } else { %>
                <script src='/<%- contextPath %>/public/umi.js?v=<%- fileVersion %>'></script>
                <% } %>
</body>

5.我们可以发现,访问URL:http://localhost:4396/zong/的时候,实际上Egg渲染了ejs模板,而ejs模板里面,引入了umi.js文件。umi.js文件里面控制了前端页面的相关路由。访问/zong/实际上对应的就是我们的页面src/pages/index.tsx。因此是能够访问到的。

3.2 几个注意点

1 package.json文件中,若项目使用TypeScript,不可缺少配置"typescript": true,否则读取egg配置的时候,都是读取以js为结尾的文件。

"egg": {
    "typescript": true,
    "declarations": true
  },

2.若项目配置的相关的前缀,那么ejs模板里的contextPath和base属性要保持一致,例如:
.umirc.ts文件:

import { defineConfig } from 'umi';

export default defineConfig({
  base: '/zong/',
  // 省略
});

router.ts文件:

// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Application } from 'egg';

export default (app: Application) => {
  const { controller, router } = app;
  // 路由的前缀
  router.prefix('/zong*');

  router.post('/user/getData', controller.home.getData);
  router.get('**/**', controller.home.index);
  router.post('**/**', controller.home.index);
};

home.ts文件中,塞入到ejs模板里的contextPath属性:

async index() {
    const { config } = this;
    const { envName, fileVersion } = config;
    const data = {
      envName,
      fileVersion,
      contextPath: 'zong',
    };
    await this.ctx.render('index.ejs', data);
}

(照着格式写就行啦)

3.router.ts文件中并没有配置相关的路由routes若不配置,那么程序会根据约定式路由,自动生成对应的路由,因此我们只需要关注在pages目录下写页面即可。无需自己手动配置路由。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值