模式是 Vue CLI 项目中一个重要的概念。默认情况下,一个 Vue CLI 项目有三个模式:
development
模式用于 vue-cli-service serve
test
模式用于 vue-cli-service test:unit
production
模式用于 vue-cli-service build
和 vue-cli-service test:e2e
你可以通过传递 --mode
选项参数为命令行覆写默认的模式。例如,如果你想要在构建命令中使用开发环境变量:
vue-cli-service build --mode development
当运行 vue-cli-service
命令时,所有的环境变量都从对应的环境文件中载入。如果文件内部不包含 NODE_ENV
变量,它的值将取决于模式,例如,在 production
模式下被设置为 "production"
,在 test
模式下被设置为 "test"
,默认则是 "development"
。
NODE_ENV
将决定您的应用运行的模式,是开发,生产还是测试,因此也决定了创建哪种 webpack 配置。
例如通过将 NODE_ENV
设置为 "test"
,Vue CLI 会创建一个优化过后的,并且旨在用于单元测试的 webpack 配置,它并不会处理图片以及一些对单元测试非必需的其他资源。
同理,NODE_ENV=development
创建一个 webpack 配置,该配置启用热更新,不会对资源进行 hash 也不会打出 vendor bundles,目的是为了在开发的时候能够快速重新构建。
当你运行 vue-cli-service build
命令时,无论你要部署到哪个环境,应该始终把 NODE_ENV
设置为 "production"
来获取可用于部署的应用程序。
NODE_ENV
如果在环境中有默认的
NODE_ENV
,你应该移除它或在运行vue-cli-service
命令的时候明确地设置NODE_ENV
。
你可以在你的项目根目录中放置下列文件来指定环境变量:
.env # 在所有的环境中被载入
.env.local # 在所有的环境中被载入,但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
一个环境文件只包含环境变量的“键=值”对:
FOO=bar
VUE_APP_NOT_SECRET_CODE=some_value
警告
不要在你的应用程序中存储任何机密信息(例如私有 API 密钥)!
环境变量会随着构建打包嵌入到输出代码,意味着任何人都有机会能够看到它。
请注意,只有 NODE_ENV
,BASE_URL
和以 VUE_APP_
开头的变量将通过 webpack.DefinePlugin
静态地嵌入到客户端侧的代码中。这是为了避免意外公开机器上可能具有相同名称的私钥。
想要了解解析环境文件规则的细节,请参考 dotenv。我们也使用 dotenv-expand 来实现变量扩展 (Vue CLI 3.5+ 支持)。例如:
FOO=foo
BAR=bar
CONCAT=$FOO$BAR # CONCAT=foobar
被载入的变量将会对 vue-cli-service
的所有命令、插件和依赖可用。
环境文件加载优先级
为一个特定模式准备的环境文件 (例如
.env.production
) 将会比一般的环境文件 (例如.env
) 拥有更高的优先级。此外,Vue CLI 启动时已经存在的环境变量拥有最高优先级,并不会被
.env
文件覆写。
.env
环境文件是通过运行vue-cli-service
命令载入的,因此环境文件发生变化,你需要重启服务。
假设我们有一个应用包含以下 .env
文件:
VUE_APP_TITLE=My App
和 .env.staging
文件:
NODE_ENV=production
VUE_APP_TITLE=My App (staging)
vue-cli-service build
会加载可能存在的 .env
、.env.production
和 .env.production.local
文件然后构建出生产环境应用。vue-cli-service build --mode staging
会在 staging 模式下加载可能存在的 .env
、.env.staging
和 .env.staging.local
文件然后构建出生产环境应用。这两种情况下,根据 NODE_ENV
,构建出的应用都是生产环境应用,但是在 staging 版本中,process.env.VUE_APP_TITLE
被覆写成了另一个值。
只有以 VUE_APP_
开头的变量会被 webpack.DefinePlugin
静态嵌入到客户端侧的包中。你可以在应用的代码中这样访问它们:
console.log(process.env.VUE_APP_SECRET)
在构建过程中,process.env.VUE_APP_SECRET
将会被相应的值所取代。在 VUE_APP_SECRET=secret
的情况下,它会被替换为 "secret"
。
除了 VUE_APP_*
变量之外,在你的应用代码中始终可用的还有两个特殊的变量:
NODE_ENV
- 会是 "development"
、"production"
或 "test"
中的一个。具体的值取决于应用运行的模式。BASE_URL
- 会和 vue.config.js
中的 publicPath
选项相符,即你的应用会部署到的基础路径。所有解析出来的环境变量都可以在 public/index.html
中以 HTML 插值中介绍的方式使用。
提示
你可以在 vue.config.js
文件中计算环境变量。它们仍然需要以 VUE_APP_
前缀开头。这可以用于版本信息:
process.env.VUE_APP_VERSION = require('./package.json').version
module.exports = {
// config
}
有的时候你可能有一些不应该提交到代码仓库中的变量,尤其是当你的项目托管在公共仓库时。这种情况下你应该使用一个 .env.local
文件取而代之。本地环境文件默认会被忽略,且出现在 .gitignore
中。
.local
也可以加在指定模式的环境文件上,比如 .env.development.local
将会在 development 模式下被载入,且被 git 忽略。
当你运行 vue-cli-service build
时,你可以通过 --target
选项指定不同的构建目标。它允许你将相同的源代码根据不同的用例生成不同的构建。
应用模式是默认的模式。在这个模式中:
index.html
会带有注入的资源和 resource hintpublic
中的静态资源会被复制到输出目录中关于 IE 兼容性的提醒
在库模式中,项目的
publicPath
是根据主文件的加载路径动态设置的(用以支持动态的资源加载能力)。但是这个功能用到了document.currentScript
,而 IE 浏览器并不支持这一特性。所以如果网站需要支持 IE 的话,建议使用库之前先在页面上引入 current-script-polyfill。
注意对 Vue 的依赖
在库模式中,Vue 是外置的。这意味着包中不会有 Vue,即便你在代码中导入了 Vue。如果这个库会通过一个打包器使用,它将尝试通过打包器以依赖的方式加载 Vue;否则就会回退到一个全局的
Vue
变量。要避免此行为,可以在
build
命令中添加--inline-vue
标志。
vue-cli-service build --target lib --inline-vue
你可以通过下面的命令将一个单独的入口构建为一个库:
vue-cli-service build --target lib --name myLib [entry]
File Size Gzipped
dist/myLib.umd.min.js 13.28 kb 8.42 kb
dist/myLib.umd.js 20.95 kb 10.22 kb
dist/myLib.common.js 20.57 kb 10.09 kb
dist/myLib.css 0.33 kb 0.23 kb
这个入口可以是一个 .js
或一个 .vue
文件。如果没有指定入口,则会使用 src/App.vue
。
构建一个库会输出:
dist/myLib.common.js
:一个给打包器用的 CommonJS 包 (不幸的是,webpack 目前还并没有支持 ES modules 输出格式的包)dist/myLib.umd.js
:一个直接给浏览器或 AMD loader 使用的 UMD 包dist/myLib.umd.min.js
:压缩后的 UMD 构建版本dist/myLib.css
:提取出来的 CSS 文件 (可以通过在 vue.config.js
中设置 css: { extract: false }
强制内联)警告
如果你在开发一个库或多项目仓库 (monorepo),请注意导入 CSS 是具有副作用的。请确保在
package.json
中移除"sideEffects": false
,否则 CSS 代码块会在生产环境构建时被 webpack 丢掉。
当使用一个 .vue
文件作为入口时,你的库会直接暴露这个 Vue 组件本身,因为组件始终是默认导出的内容。
然而,当你使用一个 .js
或 .ts
文件作为入口时,它可能会包含具名导出,所以库会暴露为一个模块。也就是说你的库必须在 UMD 构建中通过 window.yourLib.default
访问,或在 CommonJS 构建中通过 const myLib = require('mylib').default
访问。如果你没有任何具名导出并希望直接暴露默认导出,你可以在 vue.config.js
中使用以下 webpack 配置:
module.exports = {
configureWebpack: {
output: {
libraryExport: 'default'
}
}
}
兼容性提示
Web Components 模式不支持 IE11 及更低版本。更多细节
注意对 Vue 的依赖
在 Web Components 模式中,Vue 是外置的。这意味着包中不会有 Vue,即便你在代码中导入了 Vue。这里的包会假设在页面中已经有一个可用的全局变量
Vue
。
你可以通过下面的命令将一个单独的入口构建为一个 Web Components 组件:
vue-cli-service build --target wc --name my-element [entry]
注意这里的入口应该是一个 *.vue
文件。Vue CLI 将会把这个组件自动包裹并注册为 Web Components 组件,无需在 main.js
里自行注册。也可以在开发时把 main.js
作为 demo app 单独使用。
该构建将会产生一个单独的 JavaScript 文件 (及其压缩后的版本) 将所有的东西都内联起来。当这个脚本被引入网页时,会注册自定义组件 <my-element>
,其使用 @vue/web-component-wrapper
包裹了目标的 Vue 组件。这个包裹器会自动代理属性、特性、事件和插槽。请查阅 @vue/web-component-wrapper
的文档了解更多细节。
注意这个包依赖了在页面上全局可用的 Vue
。
这个模式允许你的组件的使用者以一个普通 DOM 元素的方式使用这个 Vue 组件:
<script src="https://unpkg.com/vue"></script>
<script src="path/to/my-element.js"></script>
<!-- 可在普通 HTML 中或者其它任何框架中使用 -->
<my-element></my-element>
当你构建一个 Web Components 组件包的时候,你也可以使用一个 glob 表达式作为入口指定多个组件目标:
vue-cli-service build --target wc --name foo 'src/components/*.vue'
当你构建多个 web component 时,--name
将会用于设置前缀,同时自定义元素的名称会由组件的文件名推导得出。比如一个名为 HelloWorld.vue
的组件携带 --name foo
将会生成的自定义元素名为 <foo-hello-world>
。
当指定多个 Web Components 组件作为目标时,这个包可能会变得非常大,并且用户可能只想使用你的包中注册的一部分组件。这时异步 Web Components 模式会生成一个 code-split 的包,带一个只提供所有组件共享的运行时,并预先注册所有的自定义组件小入口文件。一个组件真正的实现只会在页面中用到自定义元素相应的一个实例时按需获取:
vue-cli-service build --target wc-async --name foo 'src/components/*.vue'
File Size Gzipped
dist/foo.0.min.js 12.80 kb 8.09 kb
dist/foo.min.js 7.45 kb 3.17 kb
dist/foo.1.min.js 2.91 kb 1.02 kb
dist/foo.js 22.51 kb 6.67 kb
dist/foo.0.js 17.27 kb 8.83 kb
dist/foo.1.js 5.24 kb 1.64 kb
现在用户在该页面上只需要引入 Vue 和这个入口文件即可:
<script src="https://unpkg.com/vue"></script>
<script src="path/to/foo.min.js"></script>
<!-- foo-one 的实现的 chunk 会在用到的时候自动获取 -->
<foo-one></foo-one>
在构建 Web Components 组件或库时,入口点不是 main.js
,而是 entry-wc.js
文件,该文件由此生成: https://github.com/vuejs/vue-cli/blob/dev/packages/%40vue/cli-service/lib/commands/build/resolveWcEntry.js
因此,要在 Web Components 组件的目标中使用 vuex ,你需要在 App.vue
中初始化存储 (store):
import store from './store'
// ...
export default {
store,
name: 'App',
// ...
}
如果你用 Vue CLI 处理静态资源并和后端框架一起作为部署的一部分,那么你需要的仅仅是确保 Vue CLI 生成的构建文件在正确的位置,并遵循后端框架的发布方式即可。
如果你独立于后端部署前端应用——也就是说后端暴露一个前端可访问的 API,然后前端实际上是纯静态应用。那么你可以将 dist
目录里构建的内容部署到任何静态文件服务器中,但要确保正确的 publicPath。
dist
目录需要启动一个 HTTP 服务器来访问 (除非你已经将 publicPath
配置为了一个相对的值),所以以 file://
协议直接打开 dist/index.html
是不会工作的。在本地预览生产环境构建最简单的方式就是使用一个 Node.js 静态文件服务器,例如 serve:
npm install -g serve
# -s 参数的意思是将其架设在 Single-Page Application 模式下
# 这个模式会处理即将提到的路由问题
serve -s dist
history.pushState
的路由如果你在 history
模式下使用 Vue Router,是无法搭配简单的静态文件服务器的。例如,如果你使用 Vue Router 为 /todos/42/
定义了一个路由,开发服务器已经配置了相应的 localhost:3000/todos/42
响应,但是一个为生产环境构建架设的简单的静态服务器会却会返回 404。
为了解决这个问题,你需要配置生产环境服务器,将任何没有匹配到静态文件的请求回退到 index.html
。Vue Router 的文档提供了常用服务器配置指引。
如果前端静态内容是部署在与后端 API 不同的域名上,你需要适当地配置 CORS。
如果你使用了 PWA 插件,那么应用必须架设在 HTTPS 上,这样 Service Worker 才能被正确注册。
云开发 CloudBase 是一个云原生一体化的 Serverless 云平台,支持静态网站、容器等多种托管能力,并提供简便的部署工具 CloudBase Framework) 来一键部署应用。
步骤一:安装云开发 CloudBase CLI
CloudBase CLI 集成了 CloudBase Framework) 的能力,全局安装 CloudBase CLI 请运行以下命令:
npm install -g @cloudbase/cli
步骤二:一键部署
在项目根目录运行以下命令部署 Vue CLI 创建的应用,在部署之前可以先 开通环境
cloudbase init --without-template
cloudbase framework:deploy
CloudBase CLI 首先跳转到控制台进行登录授权,然后将会交互式进行以下步骤
确认信息后会立即进行部署,部署完成后,可以获得一个自动 SSL,CDN 加速的网站应用,你也可以搭配使用 Github Action 来持续部署 Github 上的 Vue 应用。
除了部署一个纯静态的 Vue CLI 项目之外,还可以快速一键部署混合的全栈 Vue 应用:
cloudbase init --template vue
快速创建和部署一个包含 Serverless 云函数后端的 Vue 应用cloudbase init --template nuxt-ssr
快速创建和部署一个包含 SSR 和 Serverless 云函数后端的 Vue 应用详细信息请查看 CloudBase Framework 的部署项目示例
手动推送更新
在 vue.config.js
中设置正确的 publicPath
。
如果打算将项目部署到 https://<USERNAME>.github.io/
上, publicPath
将默认被设为 "/"
,你可以忽略这个参数。
如果打算将项目部署到 https://<USERNAME>.github.io/<REPO>/
上 (即仓库地址为 https://github.com/<USERNAME>/<REPO>
),可将 publicPath
设为 "/<REPO>/"
。举个例子,如果仓库名字为“my-project”,那么 vue.config.js
的内容应如下所示:
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/my-project/'
: '/'
}
在项目目录下,创建内容如下的 deploy.sh
(可以适当地取消注释) 并运行它以进行部署:
#!/usr/bin/env sh
# 当发生错误时中止脚本
set -e
# 构建
npm run build
# cd 到构建输出的目录下
cd dist
# 部署到自定义域域名
# echo 'www.example.com' > CNAME
git init
git add -A
git commit -m 'deploy'
# 部署到 https://<USERNAME>.github.io
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master
# 部署到 https://<USERNAME>.github.io/<REPO>
# git push -f git@github.com:<USERNAME>/<REPO>.git master:gh-pages
cd -
使用 Travis CI 自动更新
仿照上面在 vue.config.js
中设置正确的 publicPath
。
安装 Travis CLI 客户端:gem install travis && travis --login
生成一个拥有“repo”权限的 GitHub 访问令牌。
授予 Travis 访问仓库的权限:travis set GITHUB_TOKEN=xxx
(xxx
是第三步中的个人访问令牌)
在项目根目录下创建一个 .travis.yml
文件。
language: node_js
node_js:
- "node"
cache: npm
script: npm run build
deploy:
provider: pages
skip_cleanup: true
github_token: $GITHUB_TOKEN
local_dir: dist
on:
branch: master
将 .travis.yml
文件推送到仓库来触发第一次构建。
根据 GitLab Pages 文档的描述,所有的配置都在根目录中的.gitlab-ci.yml
文件中。下面的范例是一个很好的入门:
# .gitlab-ci.yml 文件应放在你仓库的根目录下
pages: # 必须定义一个名为 pages 的 job
image: node:latest
stage: deploy
script:
- npm ci
- npm run build
- mv public public-vue # GitLab Pages 的钩子设置在 public 文件夹
- mv dist public # 重命名 dist 文件夹 (npm run build 之后的输出位置)
artifacts:
paths:
- public # artifact path 一定要在 /public , 这样 GitLab Pages 才能获取
only:
- master
通常, 你的静态页面将托管在 https://yourUserName.gitlab.io/yourProjectName 上, 所以你可以创建一个 initial vue.config.js
文件去 更新 BASE_URL
要匹配的值 :
// vue.config.js 位于仓库的根目录下
// 确保用 GitLab 项目的名称替换了 `YourProjectName`
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/yourProjectName/'
: '/'
}
请阅读在 GitLab Pages domains 的文档来学习更多关于页面部署 URL 的信息。注意,你也可以使用自定义域名。
在推送到仓库之前提交 .gitlab-ci.yml
和 vue.config.js
文件。GitLab CI 的管道将会被触发: 当成功时候, 到 Settings > Pages
查看关于网站的链接。
npm run build
或 yarn build
dist
也可以查看 vue-cli-plugin-netlify-lambda。
如果使用 Vue Router 的 history 模式,你需要在 /public
目录下创建一个 _redirects
文件:
# 单页应用的 Netlify 设置
/* /index.html 200
详细信息请查看 Netlify 重定向文档。
Render 提供带有全托管 SSL,全球 CDN 和 GitHub 持续自动部署的免费静态站点托管服务。
Static Site
npm run build
或者 yarn build
dist
大功告成!构建结束时你的应用便会在你的 Render URL 上线。
如果使用 Vue Router 的 history 模式,你需要在站点的 Redirects/Rewrites
设置中添加以下改写规则:
/*
/index.html
Rewrite
详细信息请查看 Render 的重定向和改写及自定义域名文档。
创建一个新的 Firebase 项目 Firebase console。 请参考文档。
确保已经全局安装了 firebase-tools :
npm install -g firebase-tools
在项目的根目录下, 用以下命令初始化 firebase
:
firebase init
Firebase 将会询问有关初始化项目的一些问题。
hosting
。public
目录设为 dist
(或构建输出的位置) 这将会上传到 Firebase Hosting。// firebase.json
{
"hosting": {
"public": "dist"
}
}
yes
设置项目为一个单页应用。 这将会创建一个 index.html
在 dist
文件夹并且配置 hosting
信息。// firebase.json
{
"hosting": {
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}
执行 npm run build
去构建项目。
在 Firebase Hosting
部署项目,执行以下命令 :
firebase deploy --only hosting
如果需要在部署的项目中使用的其他 Firebase CLI 功能, 执行 firebase deploy
去掉 --only
参数。
现在可以到 https://<YOUR-PROJECT-ID>.firebaseapp.com
访问你的项目了。
请参考 Firebase 文档 来获取更多细节。
Vercel 是一个网站和无服务器 (Serverless) API 云平台,你可以使用你的个人域名 (或是免费的 .vercel.app
URL) 部署你的 Vue 项目。
步骤一:安装 Now CLI
要使用 npm 安装其命令行界面,运行以下命令:
npm install -g vercel
步骤二:部署
在项目根目录运行以下命令部署你的应用:
vercel
此外,你还可以使用他们的 GitHub 或 GitLab 集成服务。
大功告成!
你的站点会开始部署,你将获得一个形如 https://vue.now-examples.now.sh/ (或.vercel.app
)的链接。
开箱即用地,请求会被自动改写到 index.html
(除了自定义的静态文件) 并带有合适的缓存请求头。
未完成 | 欢迎参与贡献。
创建 static.json
文件:
{
"root": "dist",
"clean_urls": true,
"routes": {
"/**": "index.html"
}
}
将 static.json
加入 Git
git add static.json
git commit -m "add static configuration"
部署到 Heroku
heroku login
heroku create
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static
git push heroku master
详细信息:https://gist.github.com/hone/24b06869b4c1eca701f9
要使用 Surge 进行部署,步骤非常简单。
首先,通过运行 npm run build
来构建项目。如果还没有安装 Surge 的命令行工具,可以通过运行命令来执行此操作:
npm install --global surge
然后 cd 进入项目的 dist/
文件夹,然后运行 surge
并按照屏幕提示操作 。如果是第一次使用 Surge,它会要求设置电子邮件和密码。确认项目文件夹以及输入首选域来查看正在部署的项目,如下所示。
project: /Users/user/Documents/myawesomeproject/dist/
domain: myawesomeproject.surge.sh
upload: [====================] 100% eta: 0.0s (31 files, 494256 bytes)
CDN: [====================] 100%
IP: **.**.***.***
Success! - Published to myawesomeproject.surge.sh
通过访问 myawesomeproject.surge.sh
来确保你的项目已经成功的用 Surge 发布,有关自定义域名等更多设置详细信息,可以到 Surge’s help page 查看。
如 Bitbucket 文档 创建一个命名为 <USERNAME>.bitbucket.io
的仓库。
如果你想拥有多个网站, 想要发布到主仓库的子文件夹中。这种情况下就要在 vue.config.js
设置 publicPath
。
如果部署到 https://<USERNAME>.bitbucket.io/
, publicPath
默认将被设为 "/"
,你可以选择忽略它。
如果要部署到 https://<USERNAME>.bitbucket.io/<SUBFOLDER>/
,设置 publicPath
为 "/<SUBFOLDER>/"
。在这种情况下,仓库的目录结构应该反映 url 结构,例如仓库应该有 /<SUBFOLDER>
目录。
在项目中, deploy.sh
使用以下内容创建并运行它以进行部署:
#!/usr/bin/env sh
# 当发生错误时中止脚本
set -e
# 构建
npm run build
# cd 到构建输出的目录
cd dist
git init
git add -A
git commit -m 'deploy'
git push -f git@bitbucket.org:<USERNAME>/<USERNAME>.bitbucket.io.git master
cd -
在 Docker 容器中使用 Nginx 部署你的应用。
安装 Docker
在项目根目录创建 Dockerfile
文件
FROM node:10
COPY ./ /app
WORKDIR /app
RUN npm install && npm run build
FROM nginx
RUN mkdir /app
COPY --from=0 /app/dist /app
COPY nginx.conf /etc/nginx/nginx.conf
在项目根目录创建 .dockerignore
文件
设置 .dockerignore
文件能防止 node_modules
和其他中间构建产物被复制到镜像中导致构建问题。
**/node_modules
**/dist
在项目根目录创建 nginx.conf
文件
Nginx
是一个能在 Docker 容器中运行的 HTTP(s) 服务器。它使用配置文件决定如何提供内容、要监听的端口等。参阅 Nginx 设置文档 以了解所有可能的设置选项。
下面是一个简单的 Nginx
设置文件,它会在 80
端口上提供你的 Vue 项目。页面未找到
/ 404
错误使用的是 index.html
,这让我们可以使用基于 pushState()
的路由。
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /app;
index index.html;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
构建你的 Docker 镜像
docker build . -t my-app
# Sending build context to Docker daemon 884.7kB
# ...
# Successfully built 4b00e5ee82ae
# Successfully tagged my-app:latest
运行你的 Docker 镜像
这个例子基于官方 Nginx
镜像,因此已经设置了日志重定向并关闭了自我守护进程。它也提供了其他有利于 Nginx 在 Docker 容器中运行的默认设置。更多信息参阅 Nginx Docker 仓库。
docker run -d -p 8080:80 my-app
curl localhost:8080
# <!DOCTYPE html><html lang=en>...</html>