-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vue 服务端渲染的配置和原理 #24
Labels
Comments
// weback 通用配置
'use strict'
const path = require('path')
// utils主要用来处理css-loader和vue-style-loader的
const utils = require('./utils')
const config = require('../config')
// vue-loader.conf配置文件是用来解决各种css文件的,定义了诸如css,less,sass之类的和样式有关的loader
const vueLoaderConfig = require('./vue-loader.conf')
var webpack = require("webpack")
// 此函数是用来返回当前目录的平行目录的路径,因为有个'..'
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
// 检测文件后缀名
test: /\.(js|vue)$/,
loader: 'eslint-loader',
// 加载之前进行预处理
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
// 导出
module.exports = {
context: path.resolve(__dirname, '../'),
// 定义入口文件
entry: {
app: './src/main.js'
},
target: 'web',
// 定义输出相关
output: {
// 路径是config目录下的index.js中的build配置中的assetsRoot,也就是dist目录
path: config.build.assetsRoot,
// 文件名称这里使用默认的name也就是main
filename: '[name].js',
// 上线地址,也就是真正的文件引用路径,如果是production生产环境,其实这里都是 '/'
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
// 定义省略规则
// resolve是webpack的内置选项,顾名思义,决定要做的事情,也就是说当使用 import "jquery",该如何去执行这件事情就是resolve配置项要做的,import jQuery from "./additional/dist/js/jquery" 这样会很麻烦,可以起个别名简化操作
resolve: {
extensions: ['.js', '.vue', '.json', '.css'],
alias: {
// 后面的$符号指精确匹配,也就是说只能使用 import vuejs from "vue" 这样的方式导入vue.esm.js文件,不能在后面跟上 vue/vue.js
'vue$': 'vue/dist/vue.esm.js',
// resolve('src') 其实在这里就是项目根目录中的src目录,使用 import somejs from "@/some.js" 就可以导入指定文件,是不是很高大上
'@': resolve('src'),
}
},
// module用来解析不同的模块 test 正则匹配,loader 处理模块,enforce 编译之前的处理,include 指定处理目录,options loader的一些参数
module: {
rules: [
// 在开发环境下 对于以.js或.vue后缀结尾的文件(在src目录下或test目录下的文件),使用eslint进行文件语法检测。
...(config.dev.useEslint ? [createLintingRule()] : []),
{
// 对vue文件使用vue-loader,该loader是vue单文件组件的实现核心,专门用来解析.vue文件的
test: /\.vue$/,
loader: 'vue-loader',
// 将vueLoaderConfig当做参数传递给vue-loader,就可以解析文件中的css相关文件
options: vueLoaderConfig
},
{
// 对jsx文件使用babel-loader转码
test: /\.jsx$/,
loader: 'babel-loader'
},
{
// 对js文件使用babel-loader转码,该插件是用来解析es6等代码
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
// 对图片相关的文件使用 url-loader 插件,这个插件的作用是将一个足够小的文件生成一个64位的DataURL
// 可能有些老铁还不知道 DataURL 是啥,当一个图片足够小,为了避免单独请求可以把图片的二进制代码变成64位的
// DataURL,使用src加载,也就是把图片当成一串代码,避免请求,神不神奇??
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
// 限制 10000 个字节一下的图片才使用DataURL
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
// 音频文件后缀
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
// 小于10000字节时的时候处理
limit: 10000,
// 文件名为name.7位hash的
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'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('common.js'),
new webpack.ProvidePlugin({
jQuery: "jquery",
$: "jquery"
})
]
} Uploading 第二章结束的代码-webpack3.zip… |
使用vue-meta处理元信息
// ./clent/server-entry.js
// 服务端渲染入口文件
import createApp from './create-app'
export default context => {
return new Promise((resolve, reject) => {
const { app, router, store } = createApp()
router.push(context.url) // 服务端环境主动跳转页面,加载组件
router.onReady(() => {
// router.onReady一般在服务端渲染用到
// router.push()后,等到所有异步操作执行完成才会调用本回调函数
// 开始获取数据
// 根据 context.url 匹配到响应组件
const matchedComponents = router.getMatchedComponents()
if (!matchedComponents.length) {
// 没有匹配到组件,返回错误,并结束
return reject(new Error('no component matched'))
}
// 服务端渲染的时候使用vue-meta的方式
context.meta = app.$meta()
resolve(app)
})
})
}
// ./server/router/server-render.js
// 服务端渲染
const ejs = require('ejs')
module.exports = async (ctx, renderer, template) => {
ctx.headers['Content-Type'] = 'text/html' // 服务端渲染最终返回html页面
const context = { url: ctx.path } // 用于传入vue-server-renderer里
try {
const appString = await renderer.renderToString(context)
// 从context获取meta
const { title } = context.meta.inject()
// 渲染
const html = ejs.render(template, {
appString,
style: context.renderStyles(), // 带style标签的完整字符串
scripts: context.renderScripts(),
title: title.text(), // 带标签
})
ctx.body = html // 返回完整页面
} catch (error) {
console.log('render error:', error)
throw error
}
}
// ./server/server.template.ejs
// 帮助生成服务端渲染文件的模版
// npm i ejs -S
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
// <title>Document</title>
<%- title %>
// <%= %> 会转义特殊字符
<%- style %> // 不转译
</head>
<body>
<div id="root">
<%- appString %>
</div>
<%- scripts %>
</body>
</html>
// ./client/client-entry.js
// 服务端渲染的客户端入口
import createApp from './create-app'
const { app, router } = createApp()
// 路由就绪后渲染
router.onReady(() => {
app.$mount('#root')
})
// App.vue
<template>
<div>
<p>{{ textA }}</p>
<p>{{ textPlus }}</p>
</div>
</template>
<script>
// Actions,Mutations 帮助函数
import {
mapState,
mapGetters,
mapActions, // 对应异步操作
mapMutations // 对应同步操作
} from 'vuex'
export default {
metaInfo: {
title: 'wang \'s App', // 进入页面时vue-meta会自动修改页面title值,下级组件的meta 会覆盖上级
},
methods: {
...mapActions(['updateCountAsync', 'a/add', 'textAction']), // b模块未声明命名空间,textAction无需写模块名
...mapMutations(['updateCount', 'a/updateText']), // 帮助函数声明模块化的 updateText mutation
// Vuex 默认会将全部mutation,包括模块化的,放入全局mutations
// 启用命名空间后,调用模块内的mutations --- a/updateText
},
mounted() {
this['a/add']()
this.textAction() // b模块未声明命名空间
},
computed: {
// 模块后,带命名空间的调用
// textA(){
// return this.$store.state.a.text
// }
// 使用帮助函数调用模块化的 state
...mapState({
textA: state => state.a.text, // 必须使用方法返回
textC: state => state.c.text, // 使用动态注册的模块c
}),
// 使用帮助函数调用模块化的 启用命名空间的 // 'a/textPlus'
// 'a/textPlus' 无法在模版中直接使用
// ...mapGetters(['fullName', 'a/textPlus'])
// 使用帮助函数 重命名getters
...mapGetters({
'fullName': 'fullName', // 全局的getter
'textPlus': 'a/textPlus', // a模块启用命名空间的getter - 'a/textPlus' 重命名为 'textPlus',可在模版使用
})
}
}
</script> |
生产环境服务端渲染
// ./server/routers/static.js
// 处理静态路径的中间件
const Router = require('koa-router')
const send = require('koa-send')
const staticRouter = new Router({ prefix: '/public' }) // staticRouter 只处理 '/public' 开头的路径
staticRouter.get('/*', async ctx => {
await send(ctx, ctx.path)
})
module.exports = staticRouter
// ./server/server.js
// npm i koa -S
// npm i koa-router -S
// npm i axios -S 服务端也会使用
// npm i memory-fs -D 只在开发环境使用, 类似nodejs fs,扩展了fs.不会把文件写入磁盘,而是写入内存,节省时间,提高效率
// 只有 nodejs可以提供服务端渲染
const Koa = require('koa')
// npm i koa-send -S // 帮助发送静态资源文件
const send = require('koa-send')
const path = require('path')
const staticRouter = require('./routers/static')
//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 (error) {
console.log(error)
ctx.status = 500 // 服务器发生错误,返回500
if (isDev) {
// 开发环境 直接显示在页面上 提供给开发者的页面
ctx.body = error.message
} else {
// 正式环境 提供给用户的优化后的页面
ctx.body = 'please try again later'
}
}
})
// 处理favicon.ico资源
app.use(async (ctx, next) => {
if (ctx.path === '/favicon.ico') {
await send(ctx, '/favicon.ico', { root: path.join(__dirname, '../') })
} else {
await next()
}
})
app.use(staticRouter.routes())
.use(staticRouter.allowedMethods())
let pageRouter
if (isDev) {
pageRouter = require('./routers/dev-ssr')
} else {
pageRouter = require('./routers/ssr')
}
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}`)
})
// ./server/routers/ssr.js
// 处理正式环境的服务端渲染
const Router = require('koa-router')
const path = require('path')
const fs = require('fs')
const serverRender = require('./server-render')
const VueServerRender = require('./vue-server-render')
const clientManifest = require('../../public/vue-ssr-client-manifest.json')
const renderer = VueServerRender.createBundleRenderer(
path.join(__dirname, '../../server-build/vue-ssr-server-bundle.json'),
{
inject: false,
clientManifest
}
)
const template = fs.readFileSync(
path.join(__dirname, '../server.template.ejs'), 'utf-8'
)
const pageRouter = new Router()
pageRouter.get('*', async (ctx) => {
await serverRender(ctx, renderer, template)
})
module.exports = pageRouter
// package.json
{
"script": {
"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"
"dev:server": "nodemon server/server.js", // 启动后,有修改自动重启服务
"dev": "concurrently \"npm run dev:client\" \"npm run dev:server\" ",
"build:client": "cross-env NODE_ENV=production webpack --config build/webpack.config.client.js",
"build:server": "cross-env NODE_ENV=production webpack --config build/webpack.config.server.js", // 服务端渲染
"build": "npm run clean && npm run build:client && npm run build:server",
"clean": "rimraf public && rimraf server-build",
"start": "cross-env NODE_ENV=production node server/server.js", // 启动服务器
}
}
// 开发时 npm run dev:server ,npm run dev:client
// 访问 loaclhost:3333 服务端渲染
// npm i concurrently -D // 一次启动两个服务,接受字符串参数
// ./build/webpack.config.base.js
const config = {
target: 'web',
// entry: path.join(__dirname, 'client/index.js'),
entry: path.join(__dirname, 'client/client-entry.js'), // 服务端渲染入口
output: {
filename: 'bundle.[hash:8].js',
path: path.join(__dirname, '../public'),
publicPath: 'http://127.0.0.1:8000/public/' // 指定整个静态资源的路径
}
}
|
Vue服务端渲染 — Vue.js |
Open
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
服务端渲染的配置和原理
The text was updated successfully, but these errors were encountered: