commit
bad07e32f2
23 changed files with 8419 additions and 0 deletions
@ -0,0 +1,8 @@ |
|||
{ |
|||
"presets": [ |
|||
["@babel/preset-env", { "modules": false }] |
|||
], |
|||
"plugins": [ |
|||
"syntax-dynamic-import" |
|||
] |
|||
} |
@ -0,0 +1 @@ |
|||
node_modules |
@ -0,0 +1,95 @@ |
|||
const fs = require("fs"); |
|||
const path = require("path"); |
|||
const MFS = require("memory-fs"); |
|||
const webpack = require("webpack"); |
|||
const chokidar = require("chokidar"); |
|||
const clientConfig = require("./webpack.client.config"); |
|||
const serverConfig = require("./webpack.server.config"); |
|||
|
|||
/** |
|||
* 读取文件 |
|||
*/ |
|||
const readFile = (fs, file) => { |
|||
try { |
|||
return fs.readFileSync(path.join(clientConfig.output.path, file), "utf-8"); |
|||
} catch (e) {} |
|||
}; |
|||
|
|||
module.exports = function setupDevServer(app, templatePath, cb) { |
|||
let bundle; |
|||
let template; |
|||
let clientManifest; |
|||
|
|||
let ready; |
|||
const readyPromise = new Promise((r) => { |
|||
ready = r; |
|||
}); |
|||
const update = () => { |
|||
if (bundle && clientManifest) { |
|||
ready(); |
|||
cb(bundle, { |
|||
template, |
|||
clientManifest, |
|||
}); |
|||
} |
|||
}; |
|||
|
|||
// read template from disk and watch
|
|||
template = fs.readFileSync(templatePath, "utf-8"); |
|||
chokidar.watch(templatePath).on("change", () => { |
|||
template = fs.readFileSync(templatePath, "utf-8"); |
|||
console.log("index.html template updated."); |
|||
update(); |
|||
}); |
|||
|
|||
// modify client config to work with hot middleware
|
|||
clientConfig.entry.app = [ |
|||
"webpack-hot-middleware/client", |
|||
clientConfig.entry.app, |
|||
]; |
|||
clientConfig.output.filename = "[name].js"; |
|||
clientConfig.plugins.push( |
|||
new webpack.HotModuleReplacementPlugin(), |
|||
new webpack.NoEmitOnErrorsPlugin() |
|||
); |
|||
|
|||
const clientCompiler = webpack(clientConfig); |
|||
const devMiddleware = require("webpack-dev-middleware")(clientCompiler, { |
|||
publicPath: clientConfig.output.publicPath, |
|||
}); |
|||
|
|||
// dev middleware
|
|||
app.use(devMiddleware); |
|||
clientCompiler.hooks.done.tap("done", (stats) => { |
|||
console.log("编译完成"); |
|||
stats = stats.toJson(); |
|||
stats.errors.forEach((err) => console.error(err)); |
|||
stats.warnings.forEach((err) => console.warn(err)); |
|||
if (stats.errors.length) return; |
|||
clientManifest = JSON.parse( |
|||
readFile(devMiddleware.context.outputFileSystem, "vue-ssr-client-manifest.json") |
|||
); |
|||
update(); |
|||
}); |
|||
|
|||
// hot middleware
|
|||
app.use( |
|||
require("webpack-hot-middleware")(clientCompiler, { heartbeat: 5000 }) |
|||
); |
|||
|
|||
// watch and update server renderer
|
|||
const serverCompiler = webpack(serverConfig); |
|||
const mfs = new MFS(); |
|||
serverCompiler.outputFileSystem = mfs; |
|||
serverCompiler.watch({}, (err, stats) => { |
|||
if (err) throw err; |
|||
stats = stats.toJson(); |
|||
if (stats.errors.length) return; |
|||
|
|||
// read bundle generated by vue-ssr-webpack-plugin
|
|||
bundle = JSON.parse(readFile(mfs, "vue-ssr-server-bundle.json")); |
|||
update(); |
|||
}); |
|||
|
|||
return readyPromise; |
|||
}; |
@ -0,0 +1,80 @@ |
|||
const path = require("path"); |
|||
|
|||
const { VueLoaderPlugin } = require("vue-loader"); |
|||
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin"); |
|||
|
|||
module.exports = { |
|||
output: { |
|||
path: path.resolve(__dirname, "../dist"), |
|||
publicPath: "/dist/", |
|||
filename: "[name].[chunkhash].js", |
|||
}, |
|||
resolve: { |
|||
alias: { |
|||
public: path.resolve(__dirname, "../public"), |
|||
}, |
|||
}, |
|||
devtool: "#cheap-module-source-map", |
|||
module: { |
|||
noParse: /es6-promise\.js$/, // avoid webpack shimming process
|
|||
rules: [ |
|||
{ |
|||
test: /\.vue$/, |
|||
loader: "vue-loader", |
|||
options: { |
|||
compilerOptions: { |
|||
preserveWhitespace: false, |
|||
}, |
|||
}, |
|||
}, |
|||
{ |
|||
test: /\.js$/, |
|||
loader: "babel-loader", |
|||
exclude: /node_modules/, |
|||
}, |
|||
{ |
|||
test: /\.(png|jpg|gif|svg)$/, |
|||
loader: "url-loader", |
|||
options: { |
|||
limit: 10000, |
|||
name: "[name].[ext]?[hash]", |
|||
}, |
|||
}, |
|||
{ |
|||
test: /\.css?$/, |
|||
use: [ |
|||
'vue-style-loader', |
|||
'css-loader' |
|||
], |
|||
}, |
|||
{ |
|||
test: /\.less?$/, |
|||
use: [ |
|||
'vue-style-loader', |
|||
'css-loader', |
|||
'less-loader' |
|||
], |
|||
}, |
|||
{ |
|||
test: /\.scss?$/, |
|||
use: [ |
|||
'vue-style-loader', |
|||
'css-loader', |
|||
{ |
|||
loader: 'sass-loader', |
|||
options: { |
|||
// 你也可以从一个文件读取,例如 `variables.scss`
|
|||
// 如果 sass-loader 版本 = 8,这里使用 `prependData` 字段
|
|||
// 如果 sass-loader 版本 < 8,这里使用 `data` 字段
|
|||
additionalData: `$color: red;` |
|||
} |
|||
} |
|||
], |
|||
}, |
|||
], |
|||
}, |
|||
plugins: [new VueLoaderPlugin(), new FriendlyErrorsPlugin()], |
|||
performance: { |
|||
hints: false, |
|||
}, |
|||
}; |
@ -0,0 +1,29 @@ |
|||
const webpack = require("webpack"); |
|||
const { merge } = require('webpack-merge'); |
|||
const base = require("./webpack.base.config"); |
|||
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin"); |
|||
|
|||
module.exports = merge(base, { |
|||
mode: process.env.NODE_ENV || "development", |
|||
entry: { |
|||
app: "./src/entry-client.js", |
|||
}, |
|||
plugins: [ |
|||
// strip dev-only code in Vue source
|
|||
new webpack.DefinePlugin({ |
|||
"process.env.NODE_ENV": JSON.stringify( |
|||
process.env.NODE_ENV || "development" |
|||
), |
|||
"process.env.VUE_ENV": '"client"', |
|||
}), |
|||
new VueSSRClientPlugin(), |
|||
], |
|||
|
|||
optimization: { |
|||
splitChunks: { |
|||
chunks: "all", |
|||
minChunks: 1, |
|||
name: "manifest" |
|||
}, |
|||
}, |
|||
}); |
@ -0,0 +1,41 @@ |
|||
const webpack = require('webpack') |
|||
const { merge } = require('webpack-merge'); |
|||
const base = require('./webpack.base.config') |
|||
const nodeExternals = require('webpack-node-externals') |
|||
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') |
|||
|
|||
module.exports = merge(base, { |
|||
mode: process.env.NODE_ENV || "development", |
|||
// 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import),
|
|||
// 并且还会在编译 Vue 组件时,
|
|||
// 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。
|
|||
target: 'node', |
|||
// 对 bundle renderer 提供 source map 支持
|
|||
devtool: 'source-map', |
|||
// 将 entry 指向应用程序的 server entry 文件
|
|||
entry: './src/entry-server.js', |
|||
// 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports)
|
|||
output: { |
|||
filename: 'server-bundle.js', |
|||
libraryTarget: 'commonjs2' |
|||
}, |
|||
// https://webpack.js.org/configuration/externals/#function
|
|||
// https://github.com/liady/webpack-node-externals
|
|||
// 外置化应用程序依赖模块。可以使服务器构建速度更快,
|
|||
// 并生成较小的 bundle 文件。
|
|||
externals: nodeExternals({ |
|||
// do not externalize CSS files in case we need to import it from a dep
|
|||
allowlist : [/\.css$/, /\?vue&type=style/] |
|||
}), |
|||
|
|||
// 这是将服务器的整个输出
|
|||
// 构建为单个 JSON 文件的插件。
|
|||
// 默认文件名为 `vue-ssr-server-bundle.json`
|
|||
plugins: [ |
|||
new webpack.DefinePlugin({ |
|||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), |
|||
'process.env.VUE_ENV': '"server"' |
|||
}), |
|||
new VueSSRServerPlugin() |
|||
] |
|||
}) |
@ -0,0 +1,2 @@ |
|||
!function(e){function t(t){for(var r,a,c=t[0],l=t[1],i=t[2],s=0,f=[];s<c.length;s++)a=c[s],Object.prototype.hasOwnProperty.call(o,a)&&o[a]&&f.push(o[a][0]),o[a]=0;for(r in l)Object.prototype.hasOwnProperty.call(l,r)&&(e[r]=l[r]);for(p&&p(t);f.length;)f.shift()();return u.push.apply(u,i||[]),n()}function n(){for(var e,t=0;t<u.length;t++){for(var n=u[t],r=!0,c=1;c<n.length;c++){var l=n[c];0!==o[l]&&(r=!1)}r&&(u.splice(t--,1),e=a(a.s=n[0]))}return e}var r={},o={0:0},u=[];function a(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=r,a.d=function(e,t,n){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(a.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)a.d(n,r,function(t){return e[t]}.bind(null,r));return n},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="/dist/";var c=window.webpackJsonp=window.webpackJsonp||[],l=c.push.bind(c);c.push=t,c=c.slice();for(var i=0;i<c.length;i++)t(c[i]);var p=l;u.push([11,1]),n()}([,function(e,t,n){var r=n(7);r.__esModule&&(r=r.default),"string"==typeof r&&(r=[[e.i,r,""]]),r.locals&&(e.exports=r.locals);(0,n(12).default)("895d7142",r,!0,{})},,,,,function(e,t,n){"use strict";n(1)},function(e,t,n){"use strict";n.r(t);var r=n(2),o=n.n(r),u=n(3),a=n.n(u)()(o.a);a.push([e.i,"*[data-v-d2943b3a] {\n color: rebeccapurple;\n}\n","",{version:3,sources:["webpack://./src/App.vue"],names:[],mappings:"AAAA;EACE,oBAAoB;AACtB",sourcesContent:["*[data-v-d2943b3a] {\n color: rebeccapurple;\n}\n"],sourceRoot:""}]),t.default=a},,,,function(e,t,n){"use strict";n.r(t);var r=n(5),o={data:function(){return{}},mounted:function(){console.log("asd")}},u=(n(6),n(4)),a=Object(u.a)(o,(function(){var e=this.$createElement;return(this._self._c||e)("div",{attrs:{id:"app"}},[this._v("\n hello world!!!\n")])}),[],!1,null,"d2943b3a",null).exports;({app:new r.a({render:function(e){return e(a)}})}).app.$mount("#app")}]); |
|||
//# sourceMappingURL=app.861ecf6e9a9de782243f.js.map
|
@ -0,0 +1 @@ |
|||
{"version":3,"file":"app.861ecf6e9a9de782243f.js","sources":["webpack:///app.861ecf6e9a9de782243f.js"],"mappings":"AAAA","sourceRoot":""} |
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
|||
{"version":3,"file":"manifest.d2d3e546256e88a4c784.js","sources":["webpack:///manifest.d2d3e546256e88a4c784.js"],"mappings":"AAAA;;;;;;AA+PA","sourceRoot":""} |
@ -0,0 +1,68 @@ |
|||
{ |
|||
"publicPath": "/dist/", |
|||
"all": [ |
|||
"app.861ecf6e9a9de782243f.js", |
|||
"app.861ecf6e9a9de782243f.js.map", |
|||
"manifest.d2d3e546256e88a4c784.js", |
|||
"manifest.d2d3e546256e88a4c784.js.map" |
|||
], |
|||
"initial": [ |
|||
"manifest.d2d3e546256e88a4c784.js", |
|||
"app.861ecf6e9a9de782243f.js" |
|||
], |
|||
"async": [], |
|||
"modules": { |
|||
"336a7835": [ |
|||
2, |
|||
3 |
|||
], |
|||
"165589ea": [ |
|||
0, |
|||
1 |
|||
], |
|||
"98dff2f8": [ |
|||
2, |
|||
3 |
|||
], |
|||
"08194f7f": [ |
|||
2, |
|||
3 |
|||
], |
|||
"b1fe4c2a": [ |
|||
2, |
|||
3 |
|||
], |
|||
"9dec38a2": [ |
|||
2, |
|||
3 |
|||
], |
|||
"1ab9b00f": [ |
|||
0, |
|||
1 |
|||
], |
|||
"388fc7b2": [ |
|||
0, |
|||
1 |
|||
], |
|||
"4dc8caee": [ |
|||
2, |
|||
3 |
|||
], |
|||
"4544517c": [ |
|||
2, |
|||
3 |
|||
], |
|||
"fe4c40fa": [ |
|||
2, |
|||
3 |
|||
], |
|||
"5163fcfe": [ |
|||
0, |
|||
1 |
|||
], |
|||
"3ae59d9f": [ |
|||
2, |
|||
3 |
|||
] |
|||
} |
|||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,37 @@ |
|||
{ |
|||
"name": "Vue Hackernews 2.0", |
|||
"short_name": "Vue HN", |
|||
"icons": [{ |
|||
"src": "/public/logo-120.png", |
|||
"sizes": "120x120", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-144.png", |
|||
"sizes": "144x144", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-152.png", |
|||
"sizes": "152x152", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-192.png", |
|||
"sizes": "192x192", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-256.png", |
|||
"sizes": "256x256", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-384.png", |
|||
"sizes": "384x384", |
|||
"type": "image/png" |
|||
}, { |
|||
"src": "/public/logo-512.png", |
|||
"sizes": "512x512", |
|||
"type": "image/png" |
|||
}], |
|||
"start_url": "/", |
|||
"background_color": "#f2f3f5", |
|||
"display": "standalone", |
|||
"theme_color": "#f60" |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,51 @@ |
|||
{ |
|||
"name": "ssr-demo", |
|||
"version": "1.0.0", |
|||
"description": "", |
|||
"main": "index.js", |
|||
"scripts": { |
|||
"start": "node server.js", |
|||
"build": "rimraf dist && npm run build:client && npm run build:server", |
|||
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.client.config.js --progress", |
|||
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.config.js --progress", |
|||
// npm install 之后会自动运行 |
|||
"postinstall": "npm run build" |
|||
}, |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC", |
|||
"dependencies": { |
|||
"express": "^4.17.1", |
|||
"less-loader": "^5.0.0", |
|||
"vue": "^2.6.13" |
|||
}, |
|||
"devDependencies": { |
|||
"@babel/core": "^7.14.3", |
|||
"@babel/preset-env": "^7.14.4", |
|||
"babel-loader": "^8.2.2", |
|||
"babel-plugin-syntax-dynamic-import": "^6.18.0", |
|||
"chokidar": "^3.5.1", |
|||
"compression": "^1.7.4", |
|||
"cross-env": "^7.0.3", |
|||
"css-loader": "^5.2.6", |
|||
"friendly-errors-webpack-plugin": "^1.7.0", |
|||
"less": "^4.1.1", |
|||
"lru-cache": "^6.0.0", |
|||
"memory-fs": "^0.5.0", |
|||
"node-sass": "^6.0.0", |
|||
"rimraf": "^3.0.2", |
|||
"route-cache": "^0.4.5", |
|||
"sass-loader": "^12.0.0", |
|||
"url-loader": "^4.1.1", |
|||
"vue-loader": "^15.9.7", |
|||
"vue-server-renderer": "^2.6.13", |
|||
"vue-style-loader": "^4.1.3", |
|||
"vue-template-compiler": "^2.6.13", |
|||
"webpack": "^4.46.0", |
|||
"webpack-cli": "^4.7.0", |
|||
"webpack-dev-middleware": "^5.0.0", |
|||
"webpack-hot-middleware": "^2.25.0", |
|||
"webpack-merge": "^5.7.3", |
|||
"webpack-node-externals": "^3.0.0" |
|||
} |
|||
} |
After Width: | Height: | Size: 3.0 KiB |
@ -0,0 +1 @@ |
|||
## [文档](https://ssr.vuejs.org/zh/#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E6%B8%B2%E6%9F%93-ssr-%EF%BC%9F) |
@ -0,0 +1,103 @@ |
|||
// server.js
|
|||
const express = require('express') |
|||
const path = require('path') |
|||
const LRU = require('lru-cache') |
|||
const microcache = require('route-cache') |
|||
const compression = require('compression') |
|||
const resolve = file => path.resolve(__dirname, file) |
|||
const { createBundleRenderer } = require('vue-server-renderer') |
|||
|
|||
const useMicroCache = process.env.MICRO_CACHE !== 'false' |
|||
|
|||
const serverInfo = |
|||
`express/${require('express/package.json').version} ` + |
|||
`vue-server-renderer/${require('vue-server-renderer/package.json').version}` |
|||
|
|||
const app = express() |
|||
|
|||
function createRenderer (bundle, options) { |
|||
// https://github.com/vuejs/vue/blob/dev/packages/vue-server-renderer/README.md#why-use-bundlerenderer
|
|||
return createBundleRenderer(bundle, Object.assign(options, { |
|||
// for component caching
|
|||
// cache: LRU({
|
|||
// max: 1000,
|
|||
// maxAge: 1000 * 60 * 15
|
|||
// }),
|
|||
// this is only needed when vue-server-renderer is npm-linked
|
|||
basedir: resolve('./dist'), |
|||
// recommended for performance
|
|||
runInNewContext: false |
|||
})) |
|||
} |
|||
|
|||
|
|||
let renderer |
|||
let readyPromise |
|||
const templatePath = resolve('./src/index.template.html') |
|||
|
|||
readyPromise = require('./build/setup-dev-server')( |
|||
app, |
|||
templatePath, |
|||
(bundle, options) => { |
|||
renderer = createRenderer(bundle, options) |
|||
} |
|||
) |
|||
|
|||
const serve = (path, cache) => express.static(resolve(path), { |
|||
maxAge: cache ? 1000 * 60 * 60 * 24 * 30 : 0 |
|||
}) |
|||
|
|||
app.use(compression({ threshold: 0 })) |
|||
app.use('/dist', serve('./dist', true)) |
|||
app.use('/public', serve('./public', true)) |
|||
app.use('/manifest.json', serve('./manifest.json', true)) |
|||
app.use('/service-worker.js', serve('./dist/service-worker.js')) |
|||
app.use(microcache.cacheSeconds(1, req => useMicroCache && req.originalUrl)) |
|||
|
|||
function render (req, res) { |
|||
const s = Date.now() |
|||
|
|||
res.setHeader("Content-Type", "text/html") |
|||
res.setHeader("Server", serverInfo) |
|||
|
|||
const handleError = err => { |
|||
if (err.url) { |
|||
res.redirect(err.url) |
|||
} else if(err.code === 404) { |
|||
res.status(404).send('404 | Page Not Found') |
|||
} else { |
|||
// Render Error Page or Redirect
|
|||
res.status(500).send('500 | Internal Server Error') |
|||
console.error(`error during render : ${req.url}`) |
|||
console.error(err.stack) |
|||
} |
|||
} |
|||
const context = { |
|||
title: 'hsello', |
|||
meta: ` |
|||
<meta charset="UTF-8"> |
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<meta name="keyword" content="vue,ssr"> |
|||
<meta name="description" content="vue srr demo"> |
|||
`,
|
|||
url: req.url |
|||
} |
|||
console.log(renderer); |
|||
renderer.renderToString(context, (err, html) => { |
|||
if (err) { |
|||
return handleError(err) |
|||
} |
|||
res.send(html) |
|||
console.log(`whole request: ${Date.now() - s}ms`) |
|||
}) |
|||
} |
|||
|
|||
app.get('*', (req, res) => { |
|||
readyPromise.then(() => render(req, res)) |
|||
}) |
|||
|
|||
const port = process.env.PORT || 8080 |
|||
app.listen(port, () => { |
|||
console.log(`server started at localhost:${port}`) |
|||
}) |
@ -0,0 +1,23 @@ |
|||
<template> |
|||
<div id="app"> |
|||
hello world!!! |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data(){ |
|||
return { |
|||
|
|||
} |
|||
}, |
|||
mounted(){ |
|||
console.log('asd'); |
|||
} |
|||
} |
|||
</script> |
|||
<style scoped lang="less"> |
|||
*{ |
|||
color: red; |
|||
} |
|||
</style> |
@ -0,0 +1,12 @@ |
|||
import Vue from 'vue' |
|||
import App from './App.vue' |
|||
|
|||
// 导出一个工厂函数,用于创建新的
|
|||
// 应用程序、router 和 store 实例
|
|||
export function createApp () { |
|||
const app = new Vue({ |
|||
// 根实例简单的渲染应用程序组件。
|
|||
render: h => h(App) |
|||
}) |
|||
return { app } |
|||
} |
@ -0,0 +1,8 @@ |
|||
import { createApp } from './app' |
|||
|
|||
// 客户端特定引导逻辑……
|
|||
|
|||
const { app } = createApp() |
|||
|
|||
// 这里假定 App.vue 模板中根元素具有 `id="app"`
|
|||
app.$mount('#app') |
@ -0,0 +1,6 @@ |
|||
import { createApp } from './app' |
|||
|
|||
export default context => { |
|||
const { app } = createApp() |
|||
return app |
|||
} |
@ -0,0 +1,16 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="en"> |
|||
<head> |
|||
<title>{{ title }}</title> |
|||
{{{ meta }}} |
|||
<link rel="manifest" href="/manifest.json"> |
|||
<style> |
|||
#skip a { position:absolute; left:-10000px; top:auto; width:1px; height:1px; overflow:hidden; } |
|||
#skip a:focus { position:static; width:auto; height:auto; } |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div id="skip"><a href="#app">skip to content</a></div> |
|||
<!--vue-ssr-outlet--> |
|||
</body> |
|||
</html> |
Loading…
Reference in new issue