From 9207d9e86a952c62c5115ede749480d766083849 Mon Sep 17 00:00:00 2001 From: bitjian Date: Thu, 22 Aug 2024 15:54:24 +0800 Subject: [PATCH] =?UTF-8?q?=20#11=20=E6=89=8B=E5=86=99ssr=E6=B8=B2?= =?UTF-8?q?=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 182 ++++++++++ readme.md | 481 ++++++++++++++++++++++++++ spa_easy/index.template.html | 9 + spa_easy/package.json | 17 + spa_easy/renderer.js | 38 ++ spa_ssr/.babelrc | 12 + spa_ssr/.editorconfig | 9 + spa_ssr/.gitignore | 14 + spa_ssr/.postcssrc.js | 10 + spa_ssr/README.md | 21 ++ spa_ssr/build/build.js | 41 +++ spa_ssr/build/check-versions.js | 54 +++ spa_ssr/build/logo.png | Bin 0 -> 6849 bytes spa_ssr/build/utils.js | 95 +++++ spa_ssr/build/vue-loader.conf.js | 22 ++ spa_ssr/build/webpack.base.conf.js | 82 +++++ spa_ssr/build/webpack.dev.conf.js | 95 +++++ spa_ssr/build/webpack.prod.conf.js | 149 ++++++++ spa_ssr/build/webpack.server.conf.js | 41 +++ spa_ssr/config/dev.env.js | 7 + spa_ssr/config/index.js | 69 ++++ spa_ssr/config/prod.env.js | 4 + spa_ssr/index.html | 11 + spa_ssr/package.json | 66 ++++ spa_ssr/server.js | 45 +++ spa_ssr/src/App.vue | 23 ++ spa_ssr/src/assets/logo.png | Bin 0 -> 6849 bytes spa_ssr/src/components/HelloWorld.vue | 113 ++++++ spa_ssr/src/components/Hellossr.vue | 15 + spa_ssr/src/entry-client.js | 10 + spa_ssr/src/entry-server.js | 23 ++ spa_ssr/src/main.js | 18 + spa_ssr/src/router/index.js | 22 ++ spa_ssr/static/.gitkeep | 0 34 files changed, 1798 insertions(+) create mode 100644 .gitignore create mode 100644 readme.md create mode 100644 spa_easy/index.template.html create mode 100644 spa_easy/package.json create mode 100644 spa_easy/renderer.js create mode 100644 spa_ssr/.babelrc create mode 100644 spa_ssr/.editorconfig create mode 100644 spa_ssr/.gitignore create mode 100644 spa_ssr/.postcssrc.js create mode 100644 spa_ssr/README.md create mode 100644 spa_ssr/build/build.js create mode 100644 spa_ssr/build/check-versions.js create mode 100644 spa_ssr/build/logo.png create mode 100644 spa_ssr/build/utils.js create mode 100644 spa_ssr/build/vue-loader.conf.js create mode 100644 spa_ssr/build/webpack.base.conf.js create mode 100755 spa_ssr/build/webpack.dev.conf.js create mode 100644 spa_ssr/build/webpack.prod.conf.js create mode 100644 spa_ssr/build/webpack.server.conf.js create mode 100644 spa_ssr/config/dev.env.js create mode 100644 spa_ssr/config/index.js create mode 100644 spa_ssr/config/prod.env.js create mode 100644 spa_ssr/index.html create mode 100644 spa_ssr/package.json create mode 100644 spa_ssr/server.js create mode 100644 spa_ssr/src/App.vue create mode 100644 spa_ssr/src/assets/logo.png create mode 100644 spa_ssr/src/components/HelloWorld.vue create mode 100644 spa_ssr/src/components/Hellossr.vue create mode 100644 spa_ssr/src/entry-client.js create mode 100644 spa_ssr/src/entry-server.js create mode 100644 spa_ssr/src/main.js create mode 100644 spa_ssr/src/router/index.js create mode 100644 spa_ssr/static/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f373574 --- /dev/null +++ b/.gitignore @@ -0,0 +1,182 @@ +######## 公共部分,每个项目保持此部分一致,不可更改,并且放最前面 ######## +### 安全相关 ### +# 公司明确的敏感文件泄漏风险 +.DS_Store +.bashrc +.kshrc +.zshrc +.bash_history +.bash_logout +.bash_profile +known_hosts +.mysql_history +authorized_keys +.svn +*.out +user.cfg +global.cfg +config.ini +.htaccess + +# 日志泄漏风险 +logs +*.log.* +*.log + +# 备份文件泄漏风险 +*.tgz +*.zip +*.tar +*.7z +*.bz2 +*.rar +*.gz +*.xz +*.Z +*.bak +*.back +*.backup +*.swp +*.un~ + +### Node.js相关 ### +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +# 有的项目需要,提交并无泄漏源码风险,保留 +# bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +# 因为发布触发执行构建有些麻烦,有些项目使用构建好把构建结果也提交的方式,先保留 +# .nuxt +# 业务有些旧项目在用dist目录,所以不做统一屏蔽 +# dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Java项目相关 ### +# Compiled class file +/classes/ +/WEB-INF/classes + +# Eclipse Project Files +.classpath +.project +.settings + +# IntelliJ IDEA Files +*.iml +*.ipr +*.iws +.idea +######## 公共部分结束 ######## + +######## 各项目自定义部分,根据需要增加或者屏蔽 ######## +# 请自由发挥 +######## 自定义部分结束 ######## +node_modules +*node_modules +*config.local.js +.DS_store +*.DS_store +package-lock.json +logger +upload +config.json +*local/ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..124693c --- /dev/null +++ b/readme.md @@ -0,0 +1,481 @@ +# 手写ssr渲染 +## 前言 +在正式搭建项目之前,我们还是要回顾下vue服务器端渲染的一些特性。 +服务器端渲染的 Vue.js 应用程序,是使vue应用既可以在客户端(浏览器)执行,也可以在服务器端执行,我们称之为“同构”或“通用”。 +之所以能够实现同构,是因为在客户端和服务端都创建了vue应用程序,并都用webpack进行打包,生成了server bundle和client bundle。server bundle用于服务器渲染,client bundle是一个客户端的静态标记,服务器渲染好html页面片段后,会发送给客户端,然后混合客户端静态标记,这样应用就具有vue应用的特性。 +需要注意是: + +服务器端渲染过程中,只会调用beforeCreate和created两个钩子函数,其它的只会在客户端执行。那么以前spa应用中,在created中创建一个setInterval,然后在destroyed中将其销毁的类似操作就不能出现了,服务器渲染期间不会调用销毁钩子函数,所以这个定时器会永远保留下来,服务器很容易就崩了。 +由于服务器可客户端是两种不同的执行平台环境,那么一些特定平台的API就不能用了,比如window和document,在node.js(比如created钩子函数)中执行就会报错。并且,我们使用的第三方API中,需要确保能在node和浏览器都能正常运行,比如axios,它向服务器和客户端都暴露相同的 API(浏览器的源生XHR就不行) + +## 安装依赖包 +```bash +npm install vue@2 vue-server-renderer express --save +``` + +## 1.创建一个简单的ssr渲染 + +### 创建一个vue ssr 渲染器 +> renderer.js +```javascript +// 第一步创建一个vue实例 +const Vue = require('vue') +const app = new Vue({ + data: { + message: 'Hellooooo Vue SSR' + }, + template: '
{{message}}
' +}) + +// 第二步创建一个 ssr 渲染器 +const renderer = require('vue-server-renderer').createRenderer() + +// 第三步将vue实例渲染为html字符串 + +renderer.renderToString(app, (err, html) => { + if (err) throw err + console.log(html) +}) +``` + +### 创建ssr模板文件 +vue渲染的内容会替换 `` +> index.template.html +```html + + + + Hello + + + + + +``` + +### 将模板文件内容设置到渲染器里 +> renderer.js +```javascript +// 读取模板文件内容 +const templateStr = fs.readFileSync(path.join(__dirname,'index.template.html'), 'utf-8') +// 第二步创建一个 ssr 渲染器 +const renderer = require('vue-server-renderer').createRenderer({ + template: templateStr +}) +``` + +### 创建一个express服务 +> renderer.js +```javascript +const server = require('express')() +// 创建一个express服务,将渲染的内容返回给前端 +server.get('*', (req, res) => { + // 第三步将vue实例渲染为html字符串 + renderer.renderToString(app, (err, html) => { + if (err) { + res.status(500).end('Internal Server Error') + return + } + console.log(html) + res.end(html) + }) +}) +// 设置监听端口 +server.listen(3300,()=>{ + console.log('server is running') +}) +``` + +## 2.利用vue-cli创建一个ssr模板 + +### 安装vue-cli脚手架 +```bash + npm i @vue/cli -g + npm i -g @vue/cli-init +``` + +### 创建项目 + +``` bash +npm init webapck spa_ssr +cd spa_ssr +npm i & npm run dev +``` + +### 安装vue-server-renderer + +```bash +npm install vue-server-renderer --save-dev +``` + +### 改造src下的文件 + +``` +src +├── router +│ └── index.js +├── components +│ └── HelloSsr.vue +├── App.vue +├── main.js +├── entry-client.js # 仅运行于浏览器 +└── entry-server.js # 仅运行于服务器 +``` + +#### 通用入口main.js和路由router.js改造 +main.js作为浏览器和服务器通用创建实例入口,需要改造成工厂函数来创建实例,保证各自独立性。且因单线程的机制,在服务器端渲染时,过程中有类似于单例的操作,那么所有的请求都会共享这个单例的操作。 + +```js +// main.js +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import {createRouter} from './router' + +Vue.config.productionTip = false + +/* eslint-disable no-new */ +const createApp = () => { + const router = createRouter() + const app = new Vue({ + router, + render: h => h(App) + }) + return {app, router} +} +export {createApp} +``` +同样router.js也需要通过工厂函数创建 +```js +// router.js +import Vue from 'vue' +import Router from 'vue-router' +import HelloWorld from '@/components/HelloWorld' +import HelloSsr from '@/components/HelloSsr' + +Vue.use(Router) + +export function createRouter () { + return new Router({ + mode: 'history', + routes: [ + { + path: '/', + name: 'HelloWorld', + component: HelloWorld + },{ + path: '/ssr', + name: 'HelloSsr', + component: HelloSsr + }] + }) +} +``` + +#### 客户端 entry-client.js +> 客户端的entry要做的很简单,就是将vue实例挂载到DOM上,只不过,考虑到可能存在异步组件,需要等到路由将异步组件加载完毕,才进行此操作。 +```js +// entry-client.js +import {createApp} from './main.js' +// 客户端入口,就是创建vue实例,并等异步组件加载完毕进行挂载 +const {app, router} = createApp() +router.onReady(() => { + app.$mount('#app') +}) +``` + +#### 服务端 entry-server.js + +> 服务器entry要做的有两步:1.解析服务器端路由;2.返回一个vue实例用于渲染。 +```js +// entry-server.js +import { createApp } from './main' +export default context => { + // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, + // 以便服务器能够等待所有的内容在渲染前, + // 就已经准备就绪。 + return new Promise((resolve, reject) => { + const { app, router } = createApp() + // 设置服务器端 router 的位置 + router.push(context.url) + // 等到 router 将可能的异步组件和钩子函数解析完 + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + // 匹配不到的路由,执行 reject 函数,并返回 404 + if (!matchedComponents.length) { + // eslint-disable-next-line + return reject({ code: 404 }) + } + // Promise 应该 resolve 应用程序实例,以便它可以渲染 + resolve(app) + }, reject) + }) +} +``` + +### webpack 配置 +vue相关代码已处理完毕,接下来就需要对webpack打包配置进行修改了。 官方推荐了下面配置: +``` js + build + ├── webpack.base.conf.js # 基础通用配置 + ├── webpack.client.conf.js # 客户端打包配置 + └── webpack.server.conf.js # 服务器端打包配置 +``` + +### webpack.base.conf.js修改 + +#### 1.修改入口配置 +> 修改webpack.base.conf.js的entry入口配置为:./src/entry-client.js,来生成客户端的构建清单client manifest。 +```javascript +// webpack.base.conf.js +module.exports = { + entry: { + // app: './src/main.js' + app: './src/entry-client.js' // <-修改入口文件改为 + }, + // ... +} +``` +### webpack.prod.conf.js修改 + +#### 1.引入SSR渲染插件client-plugin +> 在客户端的配置prod中,我们需要引入一个服务器端渲染的插件client-plugin,用来生成vue-ssr-client-manifest.json(用作静态资源注入),同时,我们需要把HtmlWebpackPlugin给去掉,在SPA应用中,我们用它来生成index.html文件,但是这里我们有vue-ssr-client-manifest.json之后,服务器端会帮我们做好这个工作。 +```javascript +// webpack.prod.conf.js +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') +// ... + plugins: [ + new webpack.DefinePlugin({ + 'process.env': env, + 'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV + }), + // ... + // 以下内容注释(或去除) + // new HtmlWebpackPlugin({ + // filename: config.build.index, + // template: 'index.html', + // inject: true, + // minify: { + // removeComments: true, + // collapseWhitespace: true, + // removeAttributeQuotes: true + // // more options: + // // https://github.com/kangax/html-minifier#options-quick-reference + // }, + // // necessary to consistently work with multiple chunks via CommonsChunkPlugin + // chunksSortMode: 'dependency' + // }), + // ... + // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。 + new VueSSRClientPlugin() + ] +// ... +``` + +### 修改utils.js +> 在客户端渲染中,CSS 可以通过 JavaScript 动态注入到 DOM 中,因此可以使用 ExtractTextPlugin 将 CSS 提取到单独的文件中。但在服务端渲染中,服务器需要生成完整的 HTML 字符串并发送给客户端,因此不能依赖 JavaScript 来注入 CSS。 +#### 修改 css loaders 引入方式 +```js +// utils.js +function generateLoaders { + ... + // if (options.extract) { + // return ExtractTextPlugin.extract({ + // use: loaders, + // fallback: 'vue-style-loader' + // }) + // } else { + // return ['vue-style-loader'].concat(loaders) + // } + return ['vue-style-loader'].concat(loaders) + // ... +} +``` + +#### 注释webpack.prod.conf.js的 ExtractTextPlugin 插件 +```js +// webpack.prod.conf.js + plugins:[ + // ... + // new ExtractTextPlugin({ + // filename: utils.assetsPath('css/[name].[contenthash].css'), + // // Setting the following option to `false` will not extract CSS from codesplit chunks. + // // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. + // // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, + // // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 + // allChunks: true, + // }), + // ... +] +``` + +### webpack.server.conf.js配置 + +#### 1.引入 webpack-node-externals, 引入并使用server-plugin + +> 这里使用了`webpack-node-externals`来加快构建速度和减小打包体积,所以我们要先安装一下它:`npm install webpack-node-externals --save-dev`。 +> 和prod配置一样,这里需要引入并使用`server-plugin`插件来生成`vue-ssr-server-bundle.json`。这东西是用来等会做服务器端渲染的。 + +```js +// webpack.server.conf.js +const webpack = require('webpack') +const merge = require('webpack-merge') +const nodeExternals = require('webpack-node-externals') +const baseConfig = require('./webpack.base.conf.js') +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') + +module.exports = merge(baseConfig, { + // 将 entry 指向应用程序的 server entry 文件 + entry: './src/entry-server.js', + // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), + // 并且还会在编译 Vue 组件时, + // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 + target: 'node', + // 对 bundle renderer 提供 source map 支持 + devtool: 'source-map', + // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) + output: { + libraryTarget: 'commonjs2' + }, + module: {}, + // https://webpack.js.org/configuration/externals/#function + // https://github.com/liady/webpack-node-externals + // 外置化应用程序依赖模块。可以使服务器构建速度更快, + // 并生成较小的 bundle 文件。 + externals: nodeExternals({ + // 不要外置化 webpack 需要处理的依赖模块。 + // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, + // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 + whitelist: /\.css$/ + }), + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.VUE_ENV': '"server"' + }), + // 这是将服务器的整个输出 + // 构建为单个 JSON 文件的插件。 + // 默认文件名为 `vue-ssr-server-bundle.json` + new VueSSRServerPlugin() + ] +}) +``` + +### package.json打包命令修改 + +> 这里需要先安装`cross-env`。(`cross-env`用来防止使用`NODE_ENV =production`来设置环境变量时,Windows命令提示会报错) +```js +npm install --save-dev cross-env +``` +> 打包命令 +```json +// package.json +"scripts": { + //... + "build:client": "node build/build.js", + "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules", + "build": "rimraf dist && npm run build:client && npm run build:server" +} +``` + +### 修改index.html + +> 插入一个``注释标记,用来标识服务器渲染的html代码片段插入的地方,同时删掉原先的`
`。 +> 服务器端会在这个标记的位置自动生成一个`
`,客户端会通过`app.$mount('#app')`挂载到服务端生成的元素上,并变为响应式的。 + +```html + + + + + + spa_ssr + + + + + +``` + +### 打包构建 + +> 在dist目录下会生成两个json文件:vue-ssr-server-bundle.json和vue-ssr-client-manifest.json,用于服务端端渲染和静态资源注入。 + +```sh +npm run build +``` + +### 构建服务器端 + +> 安装`express`服务 + +```sh +npm install express --save +``` + +> 在根目录下创建server.js,代码主要分为3步: + +> 采用createBundleRenderer来创建renderer,我们引入之前生成好的json文件,并读取index.html作为外层模板; +> 设置路由,当请求指定路由的时候,设置请求头,调用渲染函数,将渲染好的html返回给客户端; +> 监听3001端口。 + +```js +const express = require('express') +const app = express() + +const fs = require('fs') +const path = require('path') +const { createBundleRenderer } = require('vue-server-renderer') + +const resolve = file => path.resolve(__dirname, file) + +// 生成服务端渲染函数 +const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), { + // 模板html文件 + template: fs.readFileSync(resolve('./index.html'), 'utf-8'), + // client manifest + clientManifest: require('./dist/vue-ssr-client-manifest.json') +}) + +function renderToString (context) { + return new Promise((resolve, reject) => { + renderer.renderToString(context, (err, html) => { + err ? reject(err) : resolve(html) + }) + }) +} +app.use(express.static('./dist')) + +app.use(async(req, res, next) => { + try { + const context = { + title: '服务端渲染测试', // {{title}} + url: req.url + } + // 设置请求头 + res.set('Content-Type', 'text/html') + const render = await renderToString(context) + // 将服务器端渲染好的html返回给客户端 + res.end(render) + } catch (e) { + console.log(e) + // 如果没找到,放过请求,继续运行后面的中间件 + next() + } +}) + +app.listen(3000) +``` + +> 启动服务 + +```sh +node server.js +``` + +> 访问路由 +访问localhost:3000/ssr,就能获取我们之前定义好的页面。 + +## 引用 +[带你走近Vue服务器端渲染(VUE SSR)](https://juejin.cn/post/6844903656827912206) \ No newline at end of file diff --git a/spa_easy/index.template.html b/spa_easy/index.template.html new file mode 100644 index 0000000..5afaa35 --- /dev/null +++ b/spa_easy/index.template.html @@ -0,0 +1,9 @@ + + + + Hello + + + + + diff --git a/spa_easy/package.json b/spa_easy/package.json new file mode 100644 index 0000000..8f9955d --- /dev/null +++ b/spa_easy/package.json @@ -0,0 +1,17 @@ +{ + "name": "002ssr", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.19.2", + "vue": "^2.7.16", + "vue-server-renderer": "^2.7.16" + } +} diff --git a/spa_easy/renderer.js b/spa_easy/renderer.js new file mode 100644 index 0000000..524941f --- /dev/null +++ b/spa_easy/renderer.js @@ -0,0 +1,38 @@ +// 第一步创建一个vue实例 +const Vue = require('vue') +const fs = require('fs') +const path = require('path') +const server = require('express')() + +const app = new Vue({ + data: { + message: 'Hellooooo Vue SSR' + }, + template: '
{{message}}
' +}) + +// 读取模板文件内容 +const templateStr = fs.readFileSync(path.join(__dirname,'index.template.html'), 'utf-8') +// 第二步创建一个 ssr 渲染器 +const renderer = require('vue-server-renderer').createRenderer({ + template: templateStr +}) + + + +// 创建一个express服务,将渲染的内容返回给前端 +server.get('*', (req, res) => { + // 第三步将vue实例渲染为html字符串 + renderer.renderToString(app, (err, html) => { + if (err) { + res.status(500).end('Internal Server Error') + return + } + console.log(html) + res.end(html) + }) +}) +// 设置监听端口 +server.listen(3300,()=>{ + console.log('server is running') +}) \ No newline at end of file diff --git a/spa_ssr/.babelrc b/spa_ssr/.babelrc new file mode 100644 index 0000000..3a280ba --- /dev/null +++ b/spa_ssr/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] + } + }], + "stage-2" + ], + "plugins": ["transform-vue-jsx", "transform-runtime"] +} diff --git a/spa_ssr/.editorconfig b/spa_ssr/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/spa_ssr/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/spa_ssr/.gitignore b/spa_ssr/.gitignore new file mode 100644 index 0000000..541a820 --- /dev/null +++ b/spa_ssr/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules/ +/dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/spa_ssr/.postcssrc.js b/spa_ssr/.postcssrc.js new file mode 100644 index 0000000..eee3e92 --- /dev/null +++ b/spa_ssr/.postcssrc.js @@ -0,0 +1,10 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + "postcss-import": {}, + "postcss-url": {}, + // to edit target browsers: use "browserslist" field in package.json + "autoprefixer": {} + } +} diff --git a/spa_ssr/README.md b/spa_ssr/README.md new file mode 100644 index 0000000..2e99411 --- /dev/null +++ b/spa_ssr/README.md @@ -0,0 +1,21 @@ +# spa_ssr + +> A Vue.js project + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# build for production and view the bundle analyzer report +npm run build --report +``` + +For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). diff --git a/spa_ssr/build/build.js b/spa_ssr/build/build.js new file mode 100644 index 0000000..8f2ad8a --- /dev/null +++ b/spa_ssr/build/build.js @@ -0,0 +1,41 @@ +'use strict' +require('./check-versions')() + +process.env.NODE_ENV = 'production' + +const ora = require('ora') +const rm = require('rimraf') +const path = require('path') +const chalk = require('chalk') +const webpack = require('webpack') +const config = require('../config') +const webpackConfig = require('./webpack.prod.conf') + +const spinner = ora('building for production...') +spinner.start() + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) throw err + webpack(webpackConfig, (err, stats) => { + spinner.stop() + if (err) throw err + process.stdout.write(stats.toString({ + colors: true, + modules: false, + children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. + chunks: false, + chunkModules: false + }) + '\n\n') + + if (stats.hasErrors()) { + console.log(chalk.red(' Build failed with errors.\n')) + process.exit(1) + } + + console.log(chalk.cyan(' Build complete.\n')) + console.log(chalk.yellow( + ' Tip: built files are meant to be served over an HTTP server.\n' + + ' Opening index.html over file:// won\'t work.\n' + )) + }) +}) diff --git a/spa_ssr/build/check-versions.js b/spa_ssr/build/check-versions.js new file mode 100644 index 0000000..3ef972a --- /dev/null +++ b/spa_ssr/build/check-versions.js @@ -0,0 +1,54 @@ +'use strict' +const chalk = require('chalk') +const semver = require('semver') +const packageConfig = require('../package.json') +const shell = require('shelljs') + +function exec (cmd) { + return require('child_process').execSync(cmd).toString().trim() +} + +const versionRequirements = [ + { + name: 'node', + currentVersion: semver.clean(process.version), + versionRequirement: packageConfig.engines.node + } +] + +if (shell.which('npm')) { + versionRequirements.push({ + name: 'npm', + currentVersion: exec('npm --version'), + versionRequirement: packageConfig.engines.npm + }) +} + +module.exports = function () { + const warnings = [] + + for (let i = 0; i < versionRequirements.length; i++) { + const mod = versionRequirements[i] + + if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { + warnings.push(mod.name + ': ' + + chalk.red(mod.currentVersion) + ' should be ' + + chalk.green(mod.versionRequirement) + ) + } + } + + if (warnings.length) { + console.log('') + console.log(chalk.yellow('To use this template, you must update following to modules:')) + console.log() + + for (let i = 0; i < warnings.length; i++) { + const warning = warnings[i] + console.log(' ' + warning) + } + + console.log() + process.exit(1) + } +} diff --git a/spa_ssr/build/logo.png b/spa_ssr/build/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 { + const notifier = require('node-notifier') + + return (severity, errors) => { + if (severity !== 'error') return + + const error = errors[0] + const filename = error.file && error.file.split('!').pop() + + notifier.notify({ + title: packageConfig.name, + message: severity + ': ' + error.name, + subtitle: filename || '', + icon: path.join(__dirname, 'logo.png') + }) + } +} diff --git a/spa_ssr/build/vue-loader.conf.js b/spa_ssr/build/vue-loader.conf.js new file mode 100644 index 0000000..33ed58b --- /dev/null +++ b/spa_ssr/build/vue-loader.conf.js @@ -0,0 +1,22 @@ +'use strict' +const utils = require('./utils') +const config = require('../config') +const isProduction = process.env.NODE_ENV === 'production' +const sourceMapEnabled = isProduction + ? config.build.productionSourceMap + : config.dev.cssSourceMap + +module.exports = { + loaders: utils.cssLoaders({ + sourceMap: sourceMapEnabled, + extract: isProduction + }), + cssSourceMap: sourceMapEnabled, + cacheBusting: config.dev.cacheBusting, + transformToRequire: { + video: ['src', 'poster'], + source: 'src', + img: 'src', + image: 'xlink:href' + } +} diff --git a/spa_ssr/build/webpack.base.conf.js b/spa_ssr/build/webpack.base.conf.js new file mode 100644 index 0000000..ab777c9 --- /dev/null +++ b/spa_ssr/build/webpack.base.conf.js @@ -0,0 +1,82 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const config = require('../config') +const vueLoaderConfig = require('./vue-loader.conf') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + + + +module.exports = { + context: path.resolve(__dirname, '../'), + entry: { + app: './src/entry-client.js' // <-修改入口文件改为 + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + publicPath: process.env.NODE_ENV === 'production' + ? config.build.assetsPublicPath + : config.dev.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@': resolve('src'), + } + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('media/[name].[hash:7].[ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + } + ] + }, + node: { + // prevent webpack from injecting useless setImmediate polyfill because Vue + // source contains it (although only uses it if it's native). + setImmediate: false, + // prevent webpack from injecting mocks to Node native modules + // that does not make sense for the client + dgram: 'empty', + fs: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty' + } +} diff --git a/spa_ssr/build/webpack.dev.conf.js b/spa_ssr/build/webpack.dev.conf.js new file mode 100755 index 0000000..070ae22 --- /dev/null +++ b/spa_ssr/build/webpack.dev.conf.js @@ -0,0 +1,95 @@ +'use strict' +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const path = require('path') +const baseWebpackConfig = require('./webpack.base.conf') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') +const portfinder = require('portfinder') + +const HOST = process.env.HOST +const PORT = process.env.PORT && Number(process.env.PORT) + +const devWebpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) + }, + // cheap-module-eval-source-map is faster for development + devtool: config.dev.devtool, + + // these devServer options should be customized in /config/index.js + devServer: { + clientLogLevel: 'warning', + historyApiFallback: { + rewrites: [ + { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, + ], + }, + hot: true, + contentBase: false, // since we use CopyWebpackPlugin. + compress: true, + host: HOST || config.dev.host, + port: PORT || config.dev.port, + open: config.dev.autoOpenBrowser, + overlay: config.dev.errorOverlay + ? { warnings: false, errors: true } + : false, + publicPath: config.dev.assetsPublicPath, + proxy: config.dev.proxyTable, + quiet: true, // necessary for FriendlyErrorsPlugin + watchOptions: { + poll: config.dev.poll, + } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': require('../config/dev.env') + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. + new webpack.NoEmitOnErrorsPlugin(), + // https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + inject: true + }), + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.dev.assetsSubDirectory, + ignore: ['.*'] + } + ]) + ] +}) + +module.exports = new Promise((resolve, reject) => { + portfinder.basePort = process.env.PORT || config.dev.port + portfinder.getPort((err, port) => { + if (err) { + reject(err) + } else { + // publish the new Port, necessary for e2e tests + process.env.PORT = port + // add port to devServer config + devWebpackConfig.devServer.port = port + + // Add FriendlyErrorsPlugin + devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ + compilationSuccessInfo: { + messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], + }, + onErrors: config.dev.notifyOnErrors + ? utils.createNotifierCallback() + : undefined + })) + + resolve(devWebpackConfig) + } + }) +}) diff --git a/spa_ssr/build/webpack.prod.conf.js b/spa_ssr/build/webpack.prod.conf.js new file mode 100644 index 0000000..a59a3af --- /dev/null +++ b/spa_ssr/build/webpack.prod.conf.js @@ -0,0 +1,149 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const CopyWebpackPlugin = require('copy-webpack-plugin') +// const HtmlWebpackPlugin = require('html-webpack-plugin') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') +const UglifyJsPlugin = require('uglifyjs-webpack-plugin') +// 引入一个服务器端渲染的插件client-plugin,用来生成vue-ssr-client-manifest.json(用作静态资源注入) +const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') + +const env = require('../config/prod.env') + +const webpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true, + }) + }, + devtool: config.build.productionSourceMap ? config.build.devtool : false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash].js'), + chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') + }, + plugins: [ + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env, + 'process.env.VUE_ENV': '"client"' // 增加process.env.VUE_ENV + }), + new UglifyJsPlugin({ + uglifyOptions: { + compress: { + warnings: false + } + }, + sourceMap: config.build.productionSourceMap, + parallel: true + }), + // extract css into its own file + // new ExtractTextPlugin({ + // filename: utils.assetsPath('css/[name].[contenthash].css'), + // // Setting the following option to `false` will not extract CSS from codesplit chunks. + // // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. + // // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, + // // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 + // allChunks: true, + // }), + // Compress extracted CSS. We are using this plugin so that possible + // duplicated CSS from different components can be deduped. + new OptimizeCSSPlugin({ + cssProcessorOptions: config.build.productionSourceMap + ? { safe: true, map: { inline: false } } + : { safe: true } + }), + // generate dist index.html with correct asset hash for caching. + // you can customize output by editing /index.html + // see https://github.com/ampedandwired/html-webpack-plugin + // new HtmlWebpackPlugin({ + // filename: config.build.index, + // template: 'index.html', + // inject: true, + // minify: { + // removeComments: true, + // collapseWhitespace: true, + // removeAttributeQuotes: true + // // more options: + // // https://github.com/kangax/html-minifier#options-quick-reference + // }, + // // necessary to consistently work with multiple chunks via CommonsChunkPlugin + // chunksSortMode: 'dependency' + // }), + // keep module.id stable when vendor modules does not change + new webpack.HashedModuleIdsPlugin(), + // enable scope hoisting + new webpack.optimize.ModuleConcatenationPlugin(), + // split vendor js into its own file + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks (module) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf( + path.join(__dirname, '../node_modules') + ) === 0 + ) + } + }), + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + minChunks: Infinity + }), + // This instance extracts shared chunks from code splitted chunks and bundles them + // in a separate chunk, similar to the vendor chunk + // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk + new webpack.optimize.CommonsChunkPlugin({ + name: 'app', + async: 'vendor-async', + children: true, + minChunks: 3 + }), + + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.build.assetsSubDirectory, + ignore: ['.*'] + } + ]), + // 此插件在输出目录中生成 `vue-ssr-client-manifest.json`。 + new VueSSRClientPlugin(), + ] +}) + +if (config.build.productionGzip) { + const CompressionWebpackPlugin = require('compression-webpack-plugin') + + webpackConfig.plugins.push( + new CompressionWebpackPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: new RegExp( + '\\.(' + + config.build.productionGzipExtensions.join('|') + + ')$' + ), + threshold: 10240, + minRatio: 0.8 + }) + ) +} + +if (config.build.bundleAnalyzerReport) { + const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin + webpackConfig.plugins.push(new BundleAnalyzerPlugin()) +} + +module.exports = webpackConfig diff --git a/spa_ssr/build/webpack.server.conf.js b/spa_ssr/build/webpack.server.conf.js new file mode 100644 index 0000000..896b93c --- /dev/null +++ b/spa_ssr/build/webpack.server.conf.js @@ -0,0 +1,41 @@ +const webpack = require('webpack') +const merge = require('webpack-merge') +const nodeExternals = require('webpack-node-externals') +const baseConfig = require('./webpack.base.conf.js') +const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') + +module.exports = merge(baseConfig, { + // 将 entry 指向应用程序的 server entry 文件 + entry: './src/entry-server.js', + // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), + // 并且还会在编译 Vue 组件时, + // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 + target: 'node', + // 对 bundle renderer 提供 source map 支持 + devtool: 'source-map', + // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) + output: { + libraryTarget: 'commonjs2' + }, + module: {}, + // https://webpack.js.org/configuration/externals/#function + // https://github.com/liady/webpack-node-externals + // 外置化应用程序依赖模块。可以使服务器构建速度更快, + // 并生成较小的 bundle 文件。 + externals: nodeExternals({ + // 不要外置化 webpack 需要处理的依赖模块。 + // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, + // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 + whitelist: /\.css$/ + }), + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.VUE_ENV': '"server"' + }), + // 这是将服务器的整个输出 + // 构建为单个 JSON 文件的插件。 + // 默认文件名为 `vue-ssr-server-bundle.json` + new VueSSRServerPlugin() + ] +}) diff --git a/spa_ssr/config/dev.env.js b/spa_ssr/config/dev.env.js new file mode 100644 index 0000000..1e22973 --- /dev/null +++ b/spa_ssr/config/dev.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"' +}) diff --git a/spa_ssr/config/index.js b/spa_ssr/config/index.js new file mode 100644 index 0000000..b4d3323 --- /dev/null +++ b/spa_ssr/config/index.js @@ -0,0 +1,69 @@ +'use strict' +// Template version: 1.3.1 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + dev: { + + // Paths + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + + // Various Dev Server settings + host: 'localhost', // can be overwritten by process.env.HOST + port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined + autoOpenBrowser: false, + errorOverlay: true, + notifyOnErrors: true, + poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- + + + /** + * Source Maps + */ + + // https://webpack.js.org/configuration/devtool/#development + devtool: 'cheap-module-eval-source-map', + + // If you have problems debugging vue-files in devtools, + // set this to false - it *may* help + // https://vue-loader.vuejs.org/en/options.html#cachebusting + cacheBusting: true, + + cssSourceMap: true + }, + + build: { + // Template for index.html + index: path.resolve(__dirname, '../dist/index.html'), + + // Paths + assetsRoot: path.resolve(__dirname, '../dist'), + assetsSubDirectory: 'static', + assetsPublicPath: '/', + + /** + * Source Maps + */ + + productionSourceMap: true, + // https://webpack.js.org/configuration/devtool/#production + devtool: '#source-map', + + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + } +} diff --git a/spa_ssr/config/prod.env.js b/spa_ssr/config/prod.env.js new file mode 100644 index 0000000..a6f9976 --- /dev/null +++ b/spa_ssr/config/prod.env.js @@ -0,0 +1,4 @@ +'use strict' +module.exports = { + NODE_ENV: '"production"' +} diff --git a/spa_ssr/index.html b/spa_ssr/index.html new file mode 100644 index 0000000..4b1b898 --- /dev/null +++ b/spa_ssr/index.html @@ -0,0 +1,11 @@ + + + + + + spa_ssr + + + + + diff --git a/spa_ssr/package.json b/spa_ssr/package.json new file mode 100644 index 0000000..ebbaa92 --- /dev/null +++ b/spa_ssr/package.json @@ -0,0 +1,66 @@ +{ + "name": "spa_ssr", + "version": "1.0.0", + "description": "A Vue.js project", + "author": "bitjian ", + "private": true, + "scripts": { + "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", + "start": "npm run dev", + "build:client": "node build/build.js", + "build:server": "cross-env NODE_ENV=production webpack --config build/webpack.server.conf.js --progress --hide-modules", + "build": "rimraf dist && npm run build:client && npm run build:server" + }, + "dependencies": { + "vue": "^2.5.2", + "vue-router": "^3.0.1" + }, + "devDependencies": { + "autoprefixer": "^7.1.2", + "babel-core": "^6.22.1", + "babel-helper-vue-jsx-merge-props": "^2.0.3", + "babel-loader": "^7.1.1", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-plugin-transform-vue-jsx": "^3.5.0", + "babel-preset-env": "^1.3.2", + "babel-preset-stage-2": "^6.22.0", + "chalk": "^2.0.1", + "copy-webpack-plugin": "^4.0.1", + "css-loader": "^0.28.0", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "html-webpack-plugin": "^2.30.1", + "node-notifier": "^5.1.2", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.2.0", + "portfinder": "^1.0.13", + "postcss-import": "^11.0.0", + "postcss-loader": "^2.0.8", + "postcss-url": "^7.2.1", + "rimraf": "^2.6.0", + "semver": "^5.3.0", + "shelljs": "^0.7.6", + "uglifyjs-webpack-plugin": "^1.1.1", + "url-loader": "^0.5.8", + "vue-loader": "^13.3.0", + "vue-server-renderer": "^2.7.16", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.5.2", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-server": "^2.9.1", + "webpack-merge": "^4.1.0", + "webpack-node-externals": "^3.0.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/spa_ssr/server.js b/spa_ssr/server.js new file mode 100644 index 0000000..08fd7e5 --- /dev/null +++ b/spa_ssr/server.js @@ -0,0 +1,45 @@ +const express = require('express') +const app = express() + +const fs = require('fs') +const path = require('path') +const { createBundleRenderer } = require('vue-server-renderer') + +const resolve = file => path.resolve(__dirname, file) + +// 生成服务端渲染函数 +const renderer = createBundleRenderer(require('./dist/vue-ssr-server-bundle.json'), { + // 模板html文件 + template: fs.readFileSync(resolve('./index.html'), 'utf-8'), + // client manifest + clientManifest: require('./dist/vue-ssr-client-manifest.json') +}) + +function renderToString (context) { + return new Promise((resolve, reject) => { + renderer.renderToString(context, (err, html) => { + err ? reject(err) : resolve(html) + }) + }) +} +app.use(express.static('./dist')) + +app.use(async(req, res, next) => { + try { + const context = { + title: '服务端渲染测试', // {{title}} + url: req.url + } + // 设置请求头 + res.set('Content-Type', 'text/html') + const render = await renderToString(context) + // 将服务器端渲染好的html返回给客户端 + res.end(render) + } catch (e) { + console.log(e) + // 如果没找到,放过请求,继续运行后面的中间件 + next() + } +}) + +app.listen(3000) diff --git a/spa_ssr/src/App.vue b/spa_ssr/src/App.vue new file mode 100644 index 0000000..d74c648 --- /dev/null +++ b/spa_ssr/src/App.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/spa_ssr/src/assets/logo.png b/spa_ssr/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 GIT binary patch literal 6849 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 +
+

{{ msg }}

+

Essential Links

+ +

Ecosystem

+ +
+ + + + + + diff --git a/spa_ssr/src/components/Hellossr.vue b/spa_ssr/src/components/Hellossr.vue new file mode 100644 index 0000000..8986c37 --- /dev/null +++ b/spa_ssr/src/components/Hellossr.vue @@ -0,0 +1,15 @@ + + + diff --git a/spa_ssr/src/entry-client.js b/spa_ssr/src/entry-client.js new file mode 100644 index 0000000..faa3d57 --- /dev/null +++ b/spa_ssr/src/entry-client.js @@ -0,0 +1,10 @@ +import {createApp} from './main.js' +// 客户端入口,就是创建vue实例,并等异步组件加载完毕进行挂载 + +const {app, router} = createApp() + + +router.onReady(() => { + app.$mount('#app') +}) + diff --git a/spa_ssr/src/entry-server.js b/spa_ssr/src/entry-server.js new file mode 100644 index 0000000..bb98b10 --- /dev/null +++ b/spa_ssr/src/entry-server.js @@ -0,0 +1,23 @@ +// entry-server.js +import { createApp } from './main' +export default context => { + // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, + // 以便服务器能够等待所有的内容在渲染前, + // 就已经准备就绪。 + return new Promise((resolve, reject) => { + const { app, router } = createApp() + // 设置服务器端 router 的位置 + router.push(context.url) + // 等到 router 将可能的异步组件和钩子函数解析完 + router.onReady(() => { + const matchedComponents = router.getMatchedComponents() + // 匹配不到的路由,执行 reject 函数,并返回 404 + if (!matchedComponents.length) { + // eslint-disable-next-line + return reject({ code: 404 }) + } + // Promise 应该 resolve 应用程序实例,以便它可以渲染 + resolve(app) + }, reject) + }) +} diff --git a/spa_ssr/src/main.js b/spa_ssr/src/main.js new file mode 100644 index 0000000..9af4c67 --- /dev/null +++ b/spa_ssr/src/main.js @@ -0,0 +1,18 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import {createRouter} from './router' + +Vue.config.productionTip = false + +/* eslint-disable no-new */ +const createApp = () => { + const router = createRouter() + const app = new Vue({ + router, + render: h => h(App) + }) + return {app, router} +} +export {createApp} diff --git a/spa_ssr/src/router/index.js b/spa_ssr/src/router/index.js new file mode 100644 index 0000000..6449568 --- /dev/null +++ b/spa_ssr/src/router/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue' +import Router from 'vue-router' +import HelloWorld from '@/components/HelloWorld' +import HelloSsr from '@/components/HelloSsr' + +Vue.use(Router) + +export function createRouter () { + return new Router({ + mode: 'history', + routes: [ + { + path: '/', + name: 'HelloWorld', + component: HelloWorld + },{ + path: '/ssr', + name: 'HelloSsr', + component: HelloSsr + }] + }) +} diff --git a/spa_ssr/static/.gitkeep b/spa_ssr/static/.gitkeep new file mode 100644 index 0000000..e69de29