Skip to content

Commit 7b39bed

Browse files
committed
refactor: use better modern mode and cors implementation
BREAKING CHANGE: The `corsUseCredentials` option has been replaced by the new `crossorigin` option.
1 parent c4843ef commit 7b39bed

File tree

14 files changed

+101
-57
lines changed

14 files changed

+101
-57
lines changed

docs/config/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,16 @@ module.exports = {
162162

163163
Setting this to `false` can speed up production builds if you don't need source maps for production.
164164

165-
### corsUseCredentials
165+
### crossorigin
166166

167-
- Type: `boolean`
168-
- Default: `false`
167+
- Type: `string`
168+
- Default: `undefined`
169+
170+
Configure the `crossorigin` attribute on `<link rel="stylesheet">` and `<script>` tags in generated HTML.
171+
172+
Note that this only affects tags injected by `html-webpack-plugin` - tags directly added in the source template (`public/index.html`) are not affected.
169173

170-
In modern mode, the generated HTML will include `<script type="module">`, which is [loaded with CORS always enabled](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors). By default, it is treated as `crossorigin="anonymous"`, setting this option to `true` will use `crossorigin="use-credentials"` instead.
174+
See also: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
171175

172176
### configureWebpack
173177

docs/guide/browser-compatibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ The cool part though is that there are no special deployment requirements. The g
6363
For a Hello World app, the modern bundle is already 16% smaller. In production, the modern bundle will typically result in significantly faster parsing and evaluation, improving your app's loading performance.
6464
6565
::: tip
66-
`<script type="module">` is loaded [with CORS always enabled](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors). This means your server must return valid CORS headers such as `Access-Control-Allow-Origin: *`. If you want to fetch the scripts with credentials, use the [corsUseCredentials](../config/#corsusecredentials) option.
66+
`<script type="module">` is loaded [with CORS always enabled](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors). This means your server must return valid CORS headers such as `Access-Control-Allow-Origin: *`. If you want to fetch the scripts with credentials, set the [crossorigin](../config/#crossorigin) option to `use-credentials`.
6767
6868
Also, modern mode uses an inline script to avoid Safari 10 loading both bundles, so if you are using a strict CSP, you will need to explicitly allow the inline script with:
6969

docs/zh/config/README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,16 @@ module.exports = {
160160

161161
如果你不需要生产环境的 source map,可以将其设置为 `false` 以加速生产环境构建。
162162

163-
### corsUseCredentials
163+
### crossorigin
164164

165-
- Type: `boolean`
166-
- Default: `false`
165+
- Type: `string`
166+
- Default: `undefined`
167+
168+
设置生成的 HTML 中 `<link rel="stylesheet">``<script>` 标签的 `crossorigin` 属性。
169+
170+
需要注意的是该选项仅影响由 `html-webpack-plugin` 在构建时注入的标签 - 直接写在模版 (`public/index.html`) 中的标签不受影响。
167171

168-
在现代模式下,生成的 HTML 会包含 `<script type="module">`,这需要[始终开启 CORS 才能被载入](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors)。默认情况下,它被处理为 `crossorigin="anonymous"`,将这个选项设置为 `true` 后则会换用 `crossorigin="use-credentials"`
172+
更多细节可查阅: [CROS setting attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes)
169173

170174
### configureWebpack
171175

docs/zh/guide/browser-compatibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Vue CLI 会产生两个应用的版本:一个现代版的包,面向支持 [E
6363
对于一个 Hello World 应用来说,现代版的包已经小了 16%。在生产环境下,现代版的包通常都会表现出显著的解析速度和运算速度,从而改善应用的加载性能。
6464

6565
::: tip 提示
66-
`<script type="module">` [需要配合始终开启的 CORS 进行加载](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors)。这意味着你的服务器必须返回诸如 `Access-Control-Allow-Origin: *` 的有效的 CORS 头。如果你想要通过认证来获取脚本,可使用 [corsUseCredentials](../config/#corsusecredentials) 选项
66+
`<script type="module">` [需要配合始终开启的 CORS 进行加载](https://jakearchibald.com/2017/es-modules-in-browsers/#always-cors)。这意味着你的服务器必须返回诸如 `Access-Control-Allow-Origin: *` 的有效的 CORS 头。如果你想要通过认证来获取脚本,可使将 [crossorigin](../config/#crossorigin) 选项设置为 `use-credentials`
6767

6868
同时,现代浏览器使用一段内联脚本来避免 Safari 10 重复加载脚本包,所以如果你在使用一套严格的 CSP,你需要这样显性地允许内联脚本:
6969

packages/@vue/cli-service/__tests__/build.spec.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ test('build', async () => {
2525

2626
const index = await project.read('dist/index.html')
2727
// should split and preload app.js & vendor.js
28-
expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload>/)
29-
expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload>/)
28+
expect(index).toMatch(/<link [^>]+js\/app[^>]+\.js rel=preload as=script>/)
29+
expect(index).toMatch(/<link [^>]+js\/chunk-vendors[^>]+\.js rel=preload as=script>/)
3030
// should preload css
31-
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload>/)
31+
expect(index).toMatch(/<link [^>]+app[^>]+\.css rel=preload as=style>/)
3232

3333
// should reference favicon with correct base URL
3434
expect(index).toMatch(/<link rel=icon href=\/favicon.ico>/)
@@ -55,6 +55,10 @@ test('build', async () => {
5555
})
5656

5757
afterAll(async () => {
58-
await browser.close()
59-
server.close()
58+
if (browser) {
59+
await browser.close()
60+
}
61+
if (server) {
62+
server.close()
63+
}
6064
})

packages/@vue/cli-service/__tests__/modernMode.spec.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ test('modern mode', async () => {
3030
expect(index).toMatch(/<script type=module src=\/js\/app\.\w{8}\.js>/)
3131

3232
// should use <link rel="modulepreload" crossorigin=use-credentials> for modern bundle
33-
expect(index).toMatch(/<link [^>]*js\/chunk-vendors\.\w{8}\.js rel=modulepreload>/)
34-
expect(index).toMatch(/<link [^>]*js\/app\.\w{8}\.js rel=modulepreload>/)
33+
expect(index).toMatch(/<link [^>]*js\/chunk-vendors\.\w{8}\.js rel=modulepreload as=script>/)
34+
expect(index).toMatch(/<link [^>]*js\/app\.\w{8}\.js rel=modulepreload as=script>/)
3535

3636
// should use <script nomodule> for legacy bundle
3737
expect(index).toMatch(/<script src=\/js\/chunk-vendors-legacy\.\w{8}\.js nomodule>/)
@@ -41,17 +41,17 @@ test('modern mode', async () => {
4141
const { safariFix } = require('../lib/webpack/ModernModePlugin')
4242
expect(index).toMatch(`<script>${safariFix}</script>`)
4343

44-
// Test corsUseCredentials
45-
await project.write('vue.config.js', `module.exports = { corsUseCredentials: true }`)
44+
// Test crossorigin="use-credentials"
45+
await project.write('vue.config.js', `module.exports = { crossorigin: 'use-credentials' }`)
4646
const { stdout: stdout2 } = await project.run('vue-cli-service build --modern')
4747
expect(stdout2).toMatch('Build complete.')
4848
const index2 = await project.read('dist/index.html')
4949
// should use <script type="module" crossorigin=use-credentials> for modern bundle
5050
expect(index2).toMatch(/<script type=module src=\/js\/chunk-vendors\.\w{8}\.js crossorigin=use-credentials>/)
5151
expect(index2).toMatch(/<script type=module src=\/js\/app\.\w{8}\.js crossorigin=use-credentials>/)
5252
// should use <link rel="modulepreload" crossorigin=use-credentials> for modern bundle
53-
expect(index2).toMatch(/<link [^>]*js\/chunk-vendors\.\w{8}\.js rel=modulepreload crossorigin=use-credentials>/)
54-
expect(index2).toMatch(/<link [^>]*js\/app\.\w{8}\.js rel=modulepreload crossorigin=use-credentials>/)
53+
expect(index2).toMatch(/<link [^>]*js\/chunk-vendors\.\w{8}\.js rel=modulepreload as=script crossorigin=use-credentials>/)
54+
expect(index2).toMatch(/<link [^>]*js\/app\.\w{8}\.js rel=modulepreload as=script crossorigin=use-credentials>/)
5555

5656
// start server and ensure the page loads properly
5757
const port = await portfinder.getPortPromise()

packages/@vue/cli-service/__tests__/multiPage.spec.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ test('build w/ multi page', async () => {
8181

8282
const assertSharedAssets = file => {
8383
// should split and preload vendor chunk
84-
expect(file).toMatch(/<link [^>]*js\/chunk-vendors[^>]*\.js rel=preload>/)
84+
expect(file).toMatch(/<link [^>]*js\/chunk-vendors[^>]*\.js rel=preload as=script>/)
8585
// should split and preload common js and css
86-
expect(file).toMatch(/<link [^>]*js\/chunk-common[^>]*\.js rel=preload>/)
87-
expect(file).toMatch(/<link [^>]*chunk-common[^>]*\.css rel=preload>/)
86+
expect(file).toMatch(/<link [^>]*js\/chunk-common[^>]*\.js rel=preload as=script>/)
87+
expect(file).toMatch(/<link [^>]*chunk-common[^>]*\.css rel=preload as=style>/)
8888
// should load common css
8989
expect(file).toMatch(/<link href=\/css\/chunk-common\.\w+\.css rel=stylesheet>/)
9090
// should load common js
@@ -95,9 +95,9 @@ test('build w/ multi page', async () => {
9595
const index = await project.read('dist/index.html')
9696
assertSharedAssets(index)
9797
// should preload correct page file
98-
expect(index).toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload>/)
99-
expect(index).not.toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload>/)
100-
expect(index).not.toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload>/)
98+
expect(index).toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload as=script>/)
99+
expect(index).not.toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload as=script>/)
100+
expect(index).not.toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload as=script>/)
101101
// should prefetch async chunk js and css
102102
expect(index).toMatch(/<link [^>]*css\/chunk-\w+\.\w+\.css rel=prefetch>/)
103103
expect(index).toMatch(/<link [^>]*js\/chunk-\w+\.\w+\.js rel=prefetch>/)
@@ -109,9 +109,9 @@ test('build w/ multi page', async () => {
109109
const foo = await project.read('dist/foo.html')
110110
assertSharedAssets(foo)
111111
// should preload correct page file
112-
expect(foo).not.toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload>/)
113-
expect(foo).toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload>/)
114-
expect(foo).not.toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload>/)
112+
expect(foo).not.toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload as=script>/)
113+
expect(foo).toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload as=script>/)
114+
expect(foo).not.toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload as=script>/)
115115
// should not prefetch async chunk js and css because it's not used by
116116
// this entry
117117
expect(foo).not.toMatch(/<link [^>]*css\/chunk-\w+\.\w+\.css rel=prefetch>/)
@@ -124,9 +124,9 @@ test('build w/ multi page', async () => {
124124
const bar = await project.read('dist/bar.html')
125125
assertSharedAssets(bar)
126126
// should preload correct page file
127-
expect(bar).not.toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload>/)
128-
expect(bar).not.toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload>/)
129-
expect(bar).toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload>/)
127+
expect(bar).not.toMatch(/<link [^>]*js\/index[^>]*\.js rel=preload as=script>/)
128+
expect(bar).not.toMatch(/<link [^>]*js\/foo[^>]*\.js rel=preload as=script>/)
129+
expect(bar).toMatch(/<link [^>]*js\/bar[^>]*\.js rel=preload as=script>/)
130130
// should prefetch async chunk js and css
131131
expect(bar).toMatch(/<link [^>]*css\/chunk-\w+\.\w+\.css rel=prefetch>/)
132132
expect(bar).toMatch(/<link [^>]*js\/chunk-\w+\.\w+\.js rel=prefetch>/)

packages/@vue/cli-service/lib/commands/build/resolveAppConfig.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
module.exports = (api, args, options) => {
22
const config = api.resolveChainableWebpackConfig()
33
const targetDir = api.resolve(args.dest || options.outputDir)
4-
const { corsUseCredentials } = options
54

65
// respect inline build destination in copy plugin
76
if (args.dest && config.plugins.has('copy')) {
@@ -19,7 +18,6 @@ module.exports = (api, args, options) => {
1918
.plugin('modern-mode-legacy')
2019
.use(ModernModePlugin, [{
2120
targetDir,
22-
corsUseCredentials,
2321
isModernBuild: false
2422
}])
2523
} else {
@@ -28,7 +26,6 @@ module.exports = (api, args, options) => {
2826
.plugin('modern-mode-modern')
2927
.use(ModernModePlugin, [{
3028
targetDir,
31-
corsUseCredentials,
3229
isModernBuild: true
3330
}])
3431
}

packages/@vue/cli-service/lib/config/app.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ module.exports = (api, options) => {
250250
}
251251
}
252252

253+
// CORS and Subresource Integrity
254+
if (options.crossorigin != null || options.integreity) {
255+
webpackConfig
256+
.plugin('cors')
257+
.use(require('../webpack/CorsPlugin'), [{
258+
crossorigin: options.crossorigin,
259+
integreity: options.integreity
260+
}])
261+
}
262+
253263
// copy static assets in public/
254264
const publicDir = api.resolve('public')
255265
if (!isLegacyBundle && fs.existsSync(publicDir)) {

packages/@vue/cli-service/lib/options.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const schema = createSchema(joi => joi.object({
1212
parallel: joi.boolean(),
1313
devServer: joi.object(),
1414
pages: joi.object(),
15-
corsUseCredentials: joi.boolean(),
15+
crossorigin: joi.string().valid(['', 'anonymous', 'use-credentials']),
1616

1717
// css
1818
css: joi.object({
@@ -91,8 +91,11 @@ exports.defaults = () => ({
9191
pages: undefined,
9292

9393
// <script type="module" crossorigin="use-credentials">
94-
// #1656, #1867
95-
corsUseCredentials: false,
94+
// #1656, #1867, #2025
95+
crossorigin: undefined,
96+
97+
// subresource integrity
98+
integreity: false,
9699

97100
css: {
98101
// extract: true,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module.exports = class CorsPlugin {
2+
constructor ({ crossorigin, integrity }) {
3+
this.crossorigin = crossorigin || (integrity ? '' : undefined)
4+
this.integrity = integrity
5+
}
6+
7+
apply (compiler) {
8+
const ID = `vue-cli-cors-plugin`
9+
compiler.hooks.compilation.tap(ID, compilation => {
10+
compilation.hooks.htmlWebpackPluginAlterAssetTags.tap(ID, data => {
11+
if (this.crossorigin != null) {
12+
[...data.head, ...data.body].forEach(tag => {
13+
if (tag.tagName === 'script' || tag.tagName === 'link') {
14+
tag.attributes.crossorigin = this.crossorigin
15+
}
16+
})
17+
}
18+
})
19+
})
20+
}
21+
}

packages/@vue/cli-service/lib/webpack/ModernModePlugin.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ const path = require('path')
55
const safariFix = `!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();`
66

77
class ModernModePlugin {
8-
constructor ({ targetDir, corsUseCredentials, isModernBuild }) {
8+
constructor ({ targetDir, isModernBuild }) {
99
this.targetDir = targetDir
10-
this.corsUseCredentials = corsUseCredentials
1110
this.isModernBuild = isModernBuild
1211
}
1312

@@ -41,11 +40,19 @@ class ModernModePlugin {
4140
compiler.hooks.compilation.tap(ID, compilation => {
4241
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(ID, async (data, cb) => {
4342
// use <script type="module"> for modern assets
44-
const modernAssets = data.body.filter(a => a.tagName === 'script' && a.attributes)
45-
modernAssets.forEach(a => {
46-
a.attributes.type = 'module'
47-
if (this.corsUseCredentials) {
48-
a.attributes.crossorigin = 'use-credentials'
43+
data.body.forEach(tag => {
44+
if (tag.tagName === 'script' && tag.attributes) {
45+
tag.attributes.type = 'module'
46+
}
47+
})
48+
49+
// use <link rel="modulepreload"> instead of <link rel="preload">
50+
// for modern assets
51+
data.head.forEach(tag => {
52+
if (tag.tagName === 'link' &&
53+
tag.attributes.rel === 'preload' &&
54+
tag.attributes.as === 'script') {
55+
tag.attributes.rel = 'modulepreload'
4956
}
5057
})
5158

@@ -70,13 +77,7 @@ class ModernModePlugin {
7077
})
7178

7279
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tap(ID, data => {
73-
data.html = data.html
74-
// use <link rel="modulepreload"> instead of <link rel="preload">
75-
// for modern assets
76-
.replace(/(<link as=script .*?)rel=preload>/g, this.corsUseCredentials
77-
? '$1rel=modulepreload crossorigin=use-credentials>'
78-
: '$1rel=modulepreload>')
79-
.replace(/\snomodule="">/g, ' nomodule>')
80+
data.html = data.html.replace(/\snomodule="">/g, ' nomodule>')
8081
})
8182
})
8283
}

packages/@vue/cli-service/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"@intervolga/optimize-cssnano-plugin": "^1.0.5",
2525
"@vue/cli-overlay": "^3.0.0-rc.11",
2626
"@vue/cli-shared-utils": "^3.0.0-rc.11",
27-
"@vue/preload-webpack-plugin": "^1.0.0",
27+
"@vue/preload-webpack-plugin": "^1.1.0",
2828
"@vue/web-component-wrapper": "^1.2.0",
2929
"acorn": "^5.7.1",
3030
"address": "^1.0.3",

yarn.lock

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,9 +1045,9 @@
10451045
execa "^0.10.0"
10461046
q "^1.5.1"
10471047

1048-
"@vue/preload-webpack-plugin@^1.0.0":
1049-
version "1.0.0"
1050-
resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.0.0.tgz#08f156532909824da2aad258e151742d1e8f822e"
1048+
"@vue/preload-webpack-plugin@^1.1.0":
1049+
version "1.1.0"
1050+
resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.0.tgz#d768dba004261c029b53a77c5ea2d5f9ee4f3cce"
10511051

10521052
"@vue/test-utils@^1.0.0-beta.20":
10531053
version "1.0.0-beta.21"

0 commit comments

Comments
 (0)