历时数月,翻了两版的Blog系统终于上线了,上线了!虽然还有很多地方需要优化的,但是强行推上来了,欢迎大家批评指正~ 简单说下吧,博客分l两个端(两个端放置在同一个项目主体下,通过url不同进行访问...),前端展示就是如你所见的,还有一个后天管理端目前只开发了图片管理和文章管理两个小模块,后续如有新模块可以随时加入;大体的烹制方法如下:准备上等的vue ssr,以及新鲜出炉的webpack,最好有个秀气的盘子Mongoose,少许bable、eslint、JWT、还有vue全家桶呀,最后撒上serverWork,然后我们直接上锅吧~
通往美食的列车发车咯~ 直接上线上浏览地址吧 Sun-blog
基础架构
主要用到的技术栈如题 外加一些涉及到业务的模块,值得一提的是这次翻版,将前台整合到一起,大体上就是一套
server代码两个入口页通过路由分别对应两个前端服务~,虽然这样一定程度降低了耦合,但是同样也存在一些痛点后续再,纯手工搭建webpack4以及前端界面,后台基于element-ui组件,服务端使用koa2使用jwt进行鉴权
目录结构
大体的目录结构基本如下:
+---build
+---configs
+---framwork
+---server
+---src
| +---module
| +---front
| +---admin
+---dist
\---theme
Server 搭建
由于是纯手工搭建代码可能有些累赘,见谅
开始之前我们先简述下ssr的原理:
客户端请求 -> 服务端接收,并处理页面数据整合,吐出处理过后的`html`文件 -> 客户端直接渲染html文件,并接管服务 -> 回归客户端控制
如图:
利用koa2启动服务
入口文件server.js
import Koa from 'koa';
const app = new Koa();
const router = require('koa-router')();
router.get('*', async(ctx, next) => {
if (!renderer) {
return (ctx.body = 'waiting for compilation... refresh in a moment.');
}
ctx.body = await render(ctx, next);
});
app.use(router.routes()).use(router.allowedMethods());
// create server
app.listen(GConfig.port, () => {
console.log(`> Build await... `);
console.log(`You application is running here ${GConfig.host}:${GConfig.port}`);
});
如上就能启动一个简单的koa服务
接着我们需要将打包/热更新的代码塞给服务这里分了两个环境
if (isProd) {
// 生产环境下直接读取构造渲染器
const bundle = require('./../dist/vue-ssr-server-bundle.json');
const template = fs.readFileSync(resolve('./../dist/front.html'), 'utf-8');
renderer = createRenderer(bundle, template);
app.use(serve('./dist'));
} else {
// 开发环境下使用hot/dev middleware拿到bundle与template
require('./../build/setup-dev-server')(
app,
(bundle, template) => {
renderer = createRenderer(bundle, template);
}
);
}
由于项目是前后端使用一套服务,所有这里需要特殊处理下请求url
app.use(
convert(
historyApiFallback({
verbose: true,
index: '/admin.html',
rewrites: [
{
from: /^\/admin$/,
to: '/admin.html'
}
],
path: /^(\/admin)|(\/demo)/
})
)
);
将/admin 、/demo路径下的请求直接走/admin.html入口页面,无需ssr处理
下面我们进入webpack配置中去
webpack配置
通过上面的代码我们知道入口文件是setup-dev-server.js
我们去看下这里干了什么事吧~
其实做的事很简单
- 服务端打包一个json文件以备服务端渲染的时候用
- 正常打包一次
- 进行热更新处理(koa中热更新特殊处理下,引入
koa-webpack-middleware) 这里直接附上代码(仅保留核心代码),webpack4需要注意点后面会说
const path = require("path");
const MFS = require("memory-fs");
const webpack = require("webpack");
const clientConfig = require("./webpack.client.config");
const serverConfig = require("./webpack.server.config");
const {
devMiddleware,
hotMiddleware
} = require("koa-webpack-middleware");
module.exports = function setupDevServer(app, cb) {
let bundle;
let template;
const clientCompiler = webpack(clientConfig);
const devMiddle = devMiddleware(clientCompiler, {
publicPath: clientConfig.output.publicPath
});
app.use(devMiddle);
clientCompiler.plugin("done", () => {
const fs = devMiddle.fileSystem;
const filePath = path.join(clientConfig.output.path, 'front.html');
if (fs.existsSync(filePath)) {
template = fs.readFileSync(filePath, "utf-8");
console.log('client-over')
if (bundle) {
cb(bundle, template);
}
}
});
// hot middleware
app.use(hotMiddleware(clientCompiler));
// 服务端渲染打包
// 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();
stats.errors.forEach(err => console.error(err));
stats.warnings.forEach(err => console.warn(err));
const bundlePath = path.join(
serverConfig.output.path,
"vue-ssr-server-bundle.json"
);
bundle = JSON.parse(mfs.readFileSync(bundlePath, "utf-8"));
console.log('server-over')
if (template) {
cb(bundle, template);
}
});
};
上面我们注意到只有当
vue-ssr-server-bundle.json文件和template全部完成,再返回让vue-server-renderer中的createBundleRenderer方法处理
代码浏览到这里,忽略webpack详细配置,可以得知,我们通过koa2齐了个服务并,使用webpack打包出了两种东西,一个是供服务端使用的vue-ssr-server-bundle.json文件以及正常打包下的资源文件~
下面我们接着说webpack;
其实很明了了,webpack将配置两份,一份是打包正常资源的,一份是负责打包,给上面的出口文件使用,没错吧!
这里有几点由于前后端使用同一套,所以==入口页面两个哦==
并且各种资源代码,我们既然使用了webpack4更要体验下分块
webpack4将new HtmlWebpackPlugin() 去除添加了optimization配置项
optimization: {
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 4,
name: false,
cacheGroups: {//这里提供自定义分块方法
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // 只打包初始时依赖的第三方
// enforce: true,//排除默认配置如minChunks、maxInitialRequests
},
mavonEditor: {
name: 'chunk-mavonEditor', // 单独将 mavonEditor 拆包
priority: 18, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
test: /[\\/]node_modules[\\/]mavon-editor[\\/]/
},
swiper: {
name: 'chunk-swiper', // 单独将 swiper 拆包
priority: 19, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
test: /[\\/]node_modules[\\/]swiper[\\/]/
},
elementUI: {
name: 'chunk-elementUI', // 单独将 elementUI 拆包
priority: 20, // 权重要大于 libs 和 app 不然会被打包进 libs 或者 app
test: /[\\/]node_modules[\\/]element-ui[\\/]/
}
}
},
其他配置资源分类配置不在啰嗦
Vue ssr实现方式
注意到同学应该发现了几个问题,前后各一种打包,他们怎么相互接管的呢,还有服务端请求数据,vue提供的很多钩子不会执行呀。。。
好的我们一个一个来,我们之前说的都是大概,至于打包之前的资源文件配置这块并没有细说,现在来说吧。
index.js==客户端打包使用的入口==
import {
createApp
} from './app';
const {
app,
router,
store
} = createApp();
// store替换使client rendering和server rendering匹配
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
/**
* 异步组件
* 挂载# app
*/
router.onReady(() => {
app.$mount('#app');
});
发现有什么不同?
对过了个window.__INITIAL_STATE__这个就是服务端渲染的资源和客户端接管的那枚钥匙
服务端将数据存储在window.__INITIAL_STATE__中并传递到客户端供使用并接管
index.js==服务端打包需要的入口==
import {
createApp
} from './app';
export default context => {
// 注意下面这句话要写在export函数里供服务端渲染调用,重新初始化那store、router
function errorCallback(params) {
console.log('router onReady error!');
}
const {
app,
router,
store
} = createApp();
return new Promise((resolve, reject) => {
router.push(context.url);
router.onReady(() => {
const matchedComponents = router.getMatchedComponents();// getMatchedComponents 方法返回对应要渲染的组件
if (!matchedComponents.length) {
reject({
code: 404
});
}
Promise.all(
matchedComponents.map(component => {
if (component.preFetch) {
// 调用组件上的preFetch(这部分只能拿到router第一级别组件,子组件的preFetch拿不到)
return component.preFetch(store);
}
})
)
.then(() => {
// 暴露数据到HTMl,使得客户端渲染拿到数据,跟服务端渲染匹配
context.state = store.state;
resolve(app);
})
.catch(reject);
}, [errorCallback]);
});
};
这串代码最重要
这里调用了自定义的钩子函数preFetch拿到数据后暴露出去(其实即使暴露到之前提到的window.__INITIAL_STATE__中)
并且每次请求都重新实例化router、store
==注:== 使用ssr必须拥有第三方类似Flux的状态管理工具进行客户端和服务短的数据传递!!!
业务功能
没啥特别的业务,毕竟前台都是用来看的。。。
首页
列表图片添加了懒加载并添加了加载动画,
blog汇总页面
blog详情
这里的目录使用了
css新属性 content: counter(variate) 、counter-increment: variate;并使用highlight.js进行定制化代码样式
归档
介绍页
更新记录
要点回顾
升级webpack4 遇到的问题
基本变化
1.使用webpack4必须node版本8.0+
2.删除CommonsChunkPlugin插件
3.使用内置APIoptimization.splitChunks和optimization.runtimeChunk;webpack会默认的帮你生成共享的代码块
遇到的问题
1.TypeError: Cannot read property 'vue' of undefined (所有vue组件引入的时候都报如上错误)
解决方案:升级 vue-loader 到14.X版本
2.TypeError: Cannot read property 'babel' of undefined when going from v7 to v8 / Cannot find module '@babel/core' (babel升级版本问题)
解决方案: babel-loader 和 babel的版本需要对应,如webpack 3.X babel-loader 7.X | babel 6.X 或者 webpack 3.X babel-loader 8.X | babel 7.X
3.You may need an appropriate loader to handle this file type.
github解决方案 更新npm包不奏效 使用降级 npm install webpack@4.28.4
最终解决方案
分析:webpack@4.29 和 vue-loader@15 的锅,将webpack降级到4.28.4并vue-loader@14.2.2即可
目前只记得这些,当时几个问题卡了我一天的好不...
开启Gzip 并开启nginx缓存
Gzip
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
threshold: 10240,
// deleteOriginalAssets: false, //删除源文件,不建议
minRatio: 0.8
})
这个需要nginx配置配合哦
nginx缓存自己想设置个策略,还没有系统规划好,后期会加上
使用serverWorker进行离线缓存
刚开始自己封装的使用的时候发现一个很严重的问题,每次build之后生成的资源文件的hash不一致需要手动替换,尝试后没有发现很好的解决方案,最后无意中发现两个插件,妈妈发再也不用担心我sw玩的不溜了~遛~
推荐使用两个插件
"sw-precache-webpack-plugin": "^0.11.5",
"sw-register-webpack-plugin": "^1.0.21",
BundleAnalyzerPlugin插件分析buid文件
使用这个插件,可以很明了的分析那块可以进行优化(js优化贼溜)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer')
.BundleAnalyzerPlugin
webpackConfig.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 9999,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info'
})
)
小结
虽然上上去了,但是还存在一些问题,有空在优化和添加新的功能吧,通过这次对blog的同构,引入serverWorker以及将webpack整理和升级,还有就是nginx配置,收获还是很多的,今后有空也要开始写点东西了,大家有什么意见或建议,欢迎批评指正!(轻轻拍砖,人家还是个孩子)
Bye-bye~