概述
之前前端项目的结构采用 node 层 + 前端界面方式构建和部署,解决方案是 node 层服务用 eggjs 界面用 webpack + vue2 实现,这种方案已经有一些成熟的实现方法
随着 vue 推出 3.0 版本和 vite 工具推出,现在需要一个解决方案,实现 eggjs 作为服务而前端使用 vite + vue3 搭建的整体解决方案
因为 vite 无论从服务启动速度、构建速度、打包体积等方面都要优于 webpack,因此这种方案肯定要比之前的方案要好很多
项目采用 typescript 开发,可直接看完整实现
方案
原理
在开发阶项目首先启动一个 eggjs 服务,当有 http 访问时启动 vite 服务,在开发阶段访问 egg 页相当于用代理去访问 vite 服务获取 html 内容,再经过转换将页面中的资源引用路径替换成 vite 服务请求地址访问资源的路径,最终由 egg 服务视图引擎输出 html
这样做的目是开发时统一编辑 vite 的入口 html,项目构建后生成用于 eggjs 线上环境服务访问的模板资源和静态资源,在线上环境直接输出生成的 html
目录及依赖包
主要目录结构及说明如下所示:
egg-vite-vue3
├─ app # eggjs 实现
│ ├─ controller
│ │ └─ home.ts
│ ├─ public
│ └─ service
│ └─ Test.ts
├─ config # eggjs 配置相关
│ ├─ config.default.ts
│ ├─ config.local.ts
│ ├─ config.prod.ts
│ └─ plugin.ts
├─ scripts # 构建脚本
│ └─ build.sh
├─ src # 前端实现
│ ├─ App.vue
│ ├─ main.ts
│ └─ shim-vue.d.ts
├─ test
│ └─ app
│ ├─ controller
│ │ └─ home.test.ts
│ └─ service
│ └─ Test.test.ts
├─ README.md
├─ appveyor.yml
├─ index.html # 前端入口页面
├─ package.json
├─ postcss.config.js
├─ tailwind.config.js
├─ tsconfig.json # 项目调试用 tsconfig
├─ tsconfig.prod.json # 项目发布用 tsconfig
├─ vite.config.ts # vite 相关配置
└─ yarn.lock
项目中引用了如下包依赖:
{
"dependencies": {
"egg": "^2.6.1",
"egg-decorator-router": "^1.0.7",
"egg-scripts": "^2.6.0",
"egg-view-nunjucks": "^2.3.0",
"egg-vite-plugin": "^1.0.1"
},
"devDependencies": {
"@types/egg": "^1.5.0",
"@types/mocha": "^8.2.1",
"@types/node": "^14.14.31",
"@types/supertest": "^2.0.0",
"@vitejs/plugin-legacy": "^1.3.1",
"@vitejs/plugin-vue": "^1.1.5",
"@vue/compiler-sfc": "^3.0.7",
"autod": "^3.0.1",
"autod-egg": "^1.1.0",
"autoprefixer": "^10.2.4",
"egg-bin": "^4.11.0",
"egg-ci": "^1.8.0",
"egg-mock": "^4.0.1",
"eslint": "^7.21.0",
"eslint-config-egg": "^9.0.0",
"mkdirp": "^1.0.4",
"mocha": "5.2.0",
"tslib": "^2.1.0",
"typescript": "^4.2.2",
"vite": "^2.0.4",
"vue": "^3.0.7"
}
}
egg-view-nunjucks
是依赖项,必须安装
vue3 以及 @vitejs/plugin-vue
@vue/compiler-sfc
等 vue 相关的也要安装,@vitejs/plugin-legacy
是为了使构建的页面兼容那些不支持 module script 的浏览器
vite.config.ts
里的定义
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig({
build: {
manifest: true,
},
plugins: [vue(), legacy({ targets: ['defaults', 'not IE 11'] })],
});
eggjs 插件和配置
egg-vite-plugin
这个包实现了对 vite 服务的控制和视图的输出,是为了实现这个方案单独开发的 eggjs 插件
config/plugin.ts
中配置方法如下
import { EggPlugin } from 'egg';
const plugin: EggPlugin = {
// static: true,
nunjucks: {
enable: true,
package: 'egg-view-nunjucks',
},
vitePlugin: {
// 启用 egg-vite-plugin
enable: true,
package: 'egg-vite-plugin',
},
decoratorRouter: {
enable: true,
package: 'egg-decorator-router',
},
};
export default plugin;
vite 服务的启动实际上是 eggjs 的中间件实现,只有在开发环境下才需要启用 vite 服务,在 config/config.local.ts
中定义如下
import { EggAppConfig, PowerPartial } from 'egg';
export default () => {
const config: PowerPartial<EggAppConfig> = {};
config.vitePlugin = {
devServer: true,
};
return config;
};
其中 vitePlugin
中的 devServer
属性代表启用 vite 服务中间件
视图的输出还是用的 nunjucks
模板,并且也用到了静态资源,在 config/config.default.ts
中定义
import { EggAppConfig, EggAppInfo, PowerPartial } from 'egg';
import * as path from 'path';
export default (appInfo: EggAppInfo) => {
const config = {} as PowerPartial<EggAppConfig>;
config.keys = appInfo.name + '_1614221169780_4319';
config.middleware = [];
const bizConfig = {
sourceUrl: `https://github.com/eggjs/examples/tree/master/${appInfo.name}`,
};
// 视图输出定义
config.view = {
defaultViewEngine: 'nunjucks',
root: path.join(appInfo.baseDir, 'dist'), //
mapping: {
'.html': 'nunjucks',
},
};
// 静态资源定义
config.static = {
prefix: '/assets/', //
dir: [
path.join(appInfo.baseDir, 'app/public'),
path.join(appInfo.baseDir, 'dist/assets'),
],
};
config.security = {
csrf: { enable: false },
};
return {
...config,
...bizConfig,
};
};
开发阶段是通过代理获取界面输出,发布后才会通过 eggjs 的插件实现界面的输出,配置里 config.view
和 config.static
来设置资源路径相关
设置里视图的根路径是 dist,vite 构建的结果都输出到 dist 里,eggjs 用 dist 里的 html 页作为输出模板
config.view = {
defaultViewEngine: 'nunjucks',
root: path.join(appInfo.baseDir, 'dist'), //
mapping: {
'.html': 'nunjucks',
},
};
静态资源的前缀是 /assets/
,vite 输出的静态资源前缀默认是 assets,也要从 dist/assets
里获取资源
config.static = {
prefix: '/assets/', //
dir: [
path.join(appInfo.baseDir, 'app/public'),
path.join(appInfo.baseDir, 'dist/assets'),
],
};
开发
定义一个 controller 输出 eggjs 视图,这里用了 egg-decorator-router
实现像一般后端服务通过声明特性的方式来自动生成 router
controller 中使用 egg-vite-plugin
提供的扩展方法 await ctx.vite.render('index.html')
输出视图
import { Controller } from 'egg';
import { Route, HttpGet, HttpPost } from 'egg-decorator-router';
@Route()
export default class HomeController extends Controller {
@HttpGet('/')
@HttpGet('*')
public async index() {
const { ctx } = this;
const renderData: any = {
serverText: 'title text',
};
await ctx.vite.render('index.html', renderData);
}
@HttpPost('/api')
public apiPost() {
const { ctx } = this;
ctx.body = ctx.request.body;
}
}
vite.render
方法内判断是否启用了 ViteDevServer 并获取服务地址,在开发环境下 vite 的服务地址不需要额外配置,自动从启动的服务实例获取,替换模板页面中资源的引用路径,在线上环境则直接输出模板视图
在 vite.config.ts 中也可以任意定义 vite 服务地址,这里会自动识别
这里 render 的资源是 index.html
,开发环境下会获取并转发输出 http://[ip]:[port]/index.html
的请求结果,在线上环境则输出模板 index.html
发布
ts 的编译
eggjs 运行都是基于 js 文件, 因为是基于 typescript 开发,在发布时需要执行编译来生成 js 文件
实现 ts 文件的编译
ets && tsc -p tsconfig.prod.json
tsconfig.json 的完整定义:
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./output"
},
"exclude": [
"app/public",
"app/views",
"node_modules*",
"src",
"vite.config.ts"
]
}
这里定义了 tsconfig.prod.json
来用于编译,除了引用 ./tsconfig.json
的配置外,还要排除前端页面相关的目录,前端页面构建是由 vite 实现的
tsc 命令默认会将 ts 文件的编译结果放到和 ts 文件同一个目录,这里用 outDir
属性将编译结果放到另一个位置
前端的构建
使用 vite build -c vite.config.ts
来构建前端,得到 dist
文件夹
目录整合
要把 eggjs 编译的结果和 vite 构建的静态资源整合到一起,直接用一段脚本来实现 scripts/build.sh
,最终生成 output 目录
output
├─ app
│ ├─ controller
│ │ └─ home.js
│ └─ service
│ └─ Test.js
├─ config
│ ├─ config.default.js
│ ├─ config.local.js
│ ├─ config.prod.js
│ └─ plugin.js
├─ dist
│ ├─ assets
│ │ ├─ About-legacy.c3f1bf9e.js
│ │ ├─ About.b856361a.js
│ │ ├─ Home-legacy.dcbf344a.js
│ │ ├─ Home.09cdf4f7.js
│ │ ├─ element-icons.a30f5b3b.ttf
│ │ ├─ element-icons.ab40a589.woff
│ │ ├─ index-legacy.fe3045c7.js
│ │ ├─ index.861f201c.css
│ │ ├─ index.e5001c38.js
│ │ ├─ polyfills-legacy.3e7a3c9b.js
│ │ ├─ vendor-legacy.675af630.js
│ │ └─ vendor.4c85d832.js
│ ├─ index.html
│ └─ manifest.json
└─ package.json
package.json 里 eggScriptsConfig
设置 eggjs 运行的相关属性
{
"eggScriptsConfig": {
"daemon": true,
"env": "prod",
"title": "egg-vite-vue3"
}
}
直接在根目录下执行 egg-scripts start ./output
可启动项目
也可以对 output 目录单独构执行 npm install —production
只安装运行需要的包,之后构建 docker 镜像运行
关于 vite + vue2
如果你还在用 vue2 请参考 github.com/fyl080801/e…
安装依赖
npm i
启动项目
npm run dev
构建前端
npm run dist
线上运行和停止
npm run start
npm run stop
关于开发环境某些资源引用问题
在开发调试过程中,如果存在通过路径引用 vite 服务资源的情况,会出现资源加载不正常现象,因为访问的是 eggjs 服务,该服务下没有对应的静态资源
比如访问 http://127.0.0.1:7001/node_modules/element-ui/lib/theme-chalk/fonts/element-icons.woff
实际上资源在 http://127.0.0.1:3000/node_modules/element-ui/lib/theme-chalk/fonts/element-icons.woff
上,也就是说 vite 服务 http://127.0.0.1:3000
提供的资源
解决的方法是设置 egg-vite-plugin
实现路径的代理转发
在 config/config.local.ts
文件中加入如下设置
config.vitePlugin = {
devServer: true,
targets: [
/^(\/node_modules)/g,
'/assets/(.*)'
],
}
targets 是数组,定义了匹配路径规则的请求会自动转发到 vite 服务上,支持正则表达式和字符串
只有在开发环境下需要这样做,线上环境不存在问题
相关链接
名称 | 说明 | Github | Gitee |
---|---|---|---|
egg-vite-vue3 | 完整解决方案实现 | github.com/fyl080801/e… | gitee.com/fyl080801/e… |
egg-vite-plugin | eggjs 集成 vite 服务插件 | github.com/fyl080801/e… | gitee.com/fyl080801/e… |
egg-decorator-router | eggjs 装饰器路由插件 | github.com/fyl080801/e… | gitee.com/fyl080801/e… |