vue ssr 请求的 bundle.js 内容是 html
问题描述
本人是前端小白,在练习项目中使用 vue ssr 进行服务端渲染,使用的服务器是 koa,在开发环境下,启动服务后访问 3333 端口发现没有样式,经过多番查找,发现请求 bundle.js 时也会进入创建 vue app 的方法,导致返回的 bundle 中的内容是 html 文本。文件输出是使用 memory-FS
问题出现的环境背景及自己尝试过哪些方法
在百度谷歌上搜索了所有我能想到的关键字,少数相关的文章也尝试他们的方法
配置 koa-static,koa-send 等,可是是路径问题,都没有效果
也去官网看过了所有的教程和 API,但是没有找到解决方法
相关代码
- package.json
{
"name": "todo",
"version": "1.0.0",
"description": "todo application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "eslint --ext .js --ext .jsx --ext .vue client/",
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue client/",
"precommit": "npm run lint-fix",
"practice": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.practice.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.client.js",
"dev:client": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.client.js",
"dev:server": "cross-env NODE_ENV=development node server/server.js"
},
"author": "alavande",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.0.0",
"autoprefixer": "^9.5.1",
"babel-core": "^6.26.3",
"babel-eslint": "^10.0.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^8.0.5",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-vue-jsx": "^3.7.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-1": "^6.24.1",
"cross-env": "^5.2.0",
"css-loader": "^2.1.1",
"eslint": "^5.16.0",
"eslint-config-standard": "^12.0.0",
"eslint-plugin-html": "^5.0.3",
"eslint-plugin-import": "^2.17.2",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^4.1.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^5.2.2",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1",
"memory-fs": "^0.4.1",
"mini-css-extract-plugin": "^0.6.0",
"null-loader": "^1.0.0",
"postcss-loader": "^3.0.0",
"style-loader": "^0.23.1",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.2",
"url-loader": "^1.1.2",
"vue-loader": "^15.7.0",
"vue-style-loader": "^4.1.2",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.0",
"webpack-dev-server": "^3.3.1",
"webpack-merge": "^4.2.1",
"webpack-node-externals": "^1.7.2"
},
"dependencies": {
"axios": "^0.18.0",
"ejs": "^2.6.1",
"element-ui": "^2.7.2",
"koa": "^2.7.0",
"koa-router": "^7.4.0",
"koa-send": "^5.0.0",
"koa-static": "^5.0.0",
"vue": "^2.6.10",
"vue-router": "^3.0.6",
"vue-server-renderer": "^2.6.10",
"vuex": "^3.1.0"
}
}
- webpack.config.base.js
const path = require('path')
const createVueLoaderOptions = require('./vue-loader.config')
const mode = process.env.NODE_ENV
const isDev = mode === 'development'
const config = {
mode,
target: 'web',
entry: path.join(__dirname, '../client/index.js'),
output: {
filename: 'bundle.[hash:8].js',
path: path.join(__dirname, '../dist')
},
module: {
rules: [
{
test: /\.()$/,
loader: 'eslint-loader',
exclude: /node_modules/,
enforce: 'pre'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: createVueLoaderOptions(isDev)
},
{
test: /\.jsx$/,
loader: 'babel-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: path.join(__dirname, 'node_modules'),
include: path.join(__dirname, 'client'),
options: {
presets: ['env']
}
},
{
test: /\.(gif|jpg|jpeg|png|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 1024,
name: 'resources/[path][name].[hash:8].[ext]'
}
}
]
}
]
}
}
module.exports = config
- webpack.config.server.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpack = require('webpack')
const ExtractPlugin = require('extract-text-webpack-plugin')
const MiniExtractPlugn = require('mini-css-extract-plugin')
const baseConfig = require('./webpack.config.base')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const VueServerPlugin = require('vue-server-renderer/server-plugin')
let config
const isDev = process.env.NODE_ENV === 'development'
plugins = [
new VueLoaderPlugin(),
new MiniExtractPlugn('styles.[chunkhash:8].css'),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
})
]
if (isDev) {
plugins.push(new VueServerPlugin())
}
config = merge(baseConfig, {
target: 'node',
devtool: 'source-map',
entry: path.join(__dirname, '../client/server-entry.js'),
output: {
libraryTarget: 'commonjs2',
filename: 'server-entry.js',
path: path.join(__dirname, '../server-build')
},
externals: nodeExternals({
whitelist: [/\.css$/, /\?vue&type=style/]
}),
module: {
rules: [
{
test: /\.styl(us)?$/,
use: [
MiniExtractPlugn.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
},
'stylus-loader'
]
}
]
},
plugins,
})
module.exports = config
- webpack.config.client.js
const path = require('path')
const HTMLPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpack = require('webpack')
const merge = require('webpack-merge')
const ExtractPlugin = require('extract-text-webpack-plugin')
const baseConfig = require('./webpack.config.base')
const VueClientPlugin = require('vue-server-renderer/client-plugin')
const isDev = process.env.NODE_ENV === 'development'
const defaultPlugins = [
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: isDev ? '"development"' : '"production"'
}
}),
new HTMLPlugin({
template: path.join(__dirname, 'template.html')
}),
new VueClientPlugin()
]
const devServer = {
host: '0.0.0.0',
overlay: {
errors: true
},
hot: true,
historyApiFallback: true
}
let config
if (isDev) {
config = merge(baseConfig, {
devtool: '#cheap-module-eval-source-map',
devServer,
module: {
rules: [
{
test: /\.styl(us)?$/,
use: [
'vue-style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
},
'stylus-loader'
]
}
]
},
plugins: defaultPlugins.concat([
new webpack.HotModuleReplacementPlugin()
])
})
} else {
config = merge(baseConfig, {
entry: {
app: path.join(__dirname, '../client/index.js'),
vendor: ['vue']
},
output: {
filename: '[name].[chunkhash:8].js'
},
module: {
rules: [
{
test: /\.styl(us)?$/,
use: ExtractPlugin.extract({
fallback: 'vue-style-loader',
use: [
'css-loader',
{
loader: 'postcss-loader',
options: {
sourceMap: true
}
},
'stylus-loader'
]
})
}
]
},
plugins: defaultPlugins.concat([
new MiniCssExtractPlugin('styles.[chunkhash:8].css')
]),
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
cacheGroups: {
commons: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
})
}
module.exports = config
- server.js
const Koa = require('koa')
const pageRouter = require('./routers/dev-ssr')
const app = new Koa()
const isDev = process.env.NODE_ENV === 'development'
app.use(async (ctx, next) => {
try {
console.log(`request with path ${ctx.path}`)
await next()
} catch (err) {
console.log(err)
ctx.status = 500
if (isDev) {
ctx.body = err.message
} else {
ctx.body = 'please try again later'
}
}
})
app.use(pageRouter.routes()).use(pageRouter.allowedMethods())
const HOST = process.env.HOST || '0.0.0.0'
const PORT = process.env.PORT || 3333
app.listen(PORT, HOST, () => {
console.log(`server is listening on ${HOST}:${PORT}`)
})
- dev-ssr.js
const Router = require('koa-router')
const axois = require('axios')
const path = require('path')
const fs = require('fs')
const MemoryFS = require('memory-fs')
const webpack = require('webpack')
const VueServerRenderer = require('vue-server-renderer')
const serverRender = require('./server-render')
const serverConfig = require('../../build/webpack.config.server')
const serverCompiler = webpack(serverConfig)
const mfs = new MemoryFS()
serverCompiler.outputFileSystem = mfs
let bundle
serverCompiler.watch({}, (err, stats) => {
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.log(err))
stats.warnings.forEach(warn => console.warn(warn))
const bundlePath = path.join(
serverConfig.output.path,
/** vue-server-renderer 生成的默认文件名, 是 JSON 文件 */
'vue-ssr-server-bundle.json'
)
bundle = JSON.parse(mfs.readFileSync(bundlePath, 'utf-8'))
console.log('new bundle generated')
})
const handleSSR = async (ctx) => {
if (!bundle) {
ctx.body = '请稍等...'
return
}
const clientManifestResp = await axois.get(
'http://127.0.0.1:8000/vue-ssr-client-manifest.json'
)
const clientManifest = clientManifestResp.data
const template = fs.readFileSync(
path.join(__dirname, '../server.template.ejs'),
'utf-8'
)
const renderer = VueServerRenderer
.createBundleRenderer(bundle, {
inject: false,
clientManifest
})
await serverRender(ctx, renderer, template)
}
const router = new Router()
router.get('*', handleSSR)
module.exports = router
- server-render.js
const ejs = require('ejs')
module.exports = async (ctx, renderer, template) => {
ctx.headers['Content-Type'] = 'text/html'
const context = { url: ctx.path }
try {
const appString = await renderer.renderToString(context)
const html = ejs.render(template, {
appString,
style: context.renderStyles(),
scripts: context.renderScripts()
})
ctx.body = html
} catch (err) {
console.log('render error', err)
throw err
}
}
- server-entry.js
import createApp from './create-app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router } = createApp()
router.push(context.url)
router.onReady(() => {
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
return reject(new Error('no component matched'))
}
resolve(app)
})
})
}
- create-app.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import App from './app.vue'
import createStore from './store/store'
import createRouter from './config/router'
import './assets/styles/global.styl'
Vue.use(VueRouter)
Vue.use(Vuex)
export default () => {
const router = createRouter()
const store = createStore()
const app = new Vue({
router,
store,
render: h => h(App)
})
return { app, router, store }
}
你期待的结果是什么?实际看到的错误信息又是什么?
是路径问题还是需要特别指定静态资源加载,该怎么修改,感谢大佬。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
dev 环境中, 服务端渲染的 js 文件是从运行的 client 端拉取的, 在 base.conf 中设置 publicPath, client.conf 中设置 historyApiFallback 的路径, js 使用完整 http url 路径强制去客户端拉取, js 文件就不是 html 内容了