Browse Source

最基础的展示版本

master
npmrun 4 years ago
commit
bad07e32f2
  1. 8
      .babelrc
  2. 1
      .gitignore
  3. 95
      build/setup-dev-server.js
  4. 80
      build/webpack.base.config.js
  5. 29
      build/webpack.client.config.js
  6. 41
      build/webpack.server.config.js
  7. 2
      dist/app.861ecf6e9a9de782243f.js
  8. 1
      dist/app.861ecf6e9a9de782243f.js.map
  9. 8
      dist/manifest.d2d3e546256e88a4c784.js
  10. 1
      dist/manifest.d2d3e546256e88a4c784.js.map
  11. 68
      dist/vue-ssr-client-manifest.json
  12. 210
      dist/vue-ssr-server-bundle.json
  13. 37
      manifest.json
  14. 7618
      package-lock.json
  15. 51
      package.json
  16. BIN
      public/logo-144.png
  17. 1
      readme.md
  18. 103
      server.js
  19. 23
      src/App.vue
  20. 12
      src/app.js
  21. 8
      src/entry-client.js
  22. 6
      src/entry-server.js
  23. 16
      src/index.template.html

8
.babelrc

@ -0,0 +1,8 @@
{
"presets": [
["@babel/preset-env", { "modules": false }]
],
"plugins": [
"syntax-dynamic-import"
]
}

1
.gitignore

@ -0,0 +1 @@
node_modules

95
build/setup-dev-server.js

@ -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;
};

80
build/webpack.base.config.js

@ -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,
},
};

29
build/webpack.client.config.js

@ -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"
},
},
});

41
build/webpack.server.config.js

@ -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()
]
})

2
dist/app.861ecf6e9a9de782243f.js

@ -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

1
dist/app.861ecf6e9a9de782243f.js.map

@ -0,0 +1 @@
{"version":3,"file":"app.861ecf6e9a9de782243f.js","sources":["webpack:///app.861ecf6e9a9de782243f.js"],"mappings":"AAAA","sourceRoot":""}

8
dist/manifest.d2d3e546256e88a4c784.js

File diff suppressed because one or more lines are too long

1
dist/manifest.d2d3e546256e88a4c784.js.map

@ -0,0 +1 @@
{"version":3,"file":"manifest.d2d3e546256e88a4c784.js","sources":["webpack:///manifest.d2d3e546256e88a4c784.js"],"mappings":"AAAA;;;;;;AA+PA","sourceRoot":""}

68
dist/vue-ssr-client-manifest.json

@ -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
]
}
}

210
dist/vue-ssr-server-bundle.json

File diff suppressed because one or more lines are too long

37
manifest.json

@ -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"
}

7618
package-lock.json

File diff suppressed because it is too large

51
package.json

@ -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"
}
}

BIN
public/logo-144.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

1
readme.md

@ -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)

103
server.js

@ -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}`)
})

23
src/App.vue

@ -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>

12
src/app.js

@ -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 }
}

8
src/entry-client.js

@ -0,0 +1,8 @@
import { createApp } from './app'
// 客户端特定引导逻辑……
const { app } = createApp()
// 这里假定 App.vue 模板中根元素具有 `id="app"`
app.$mount('#app')

6
src/entry-server.js

@ -0,0 +1,6 @@
import { createApp } from './app'
export default context => {
const { app } = createApp()
return app
}

16
src/index.template.html

@ -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…
Cancel
Save