博客采用先后端分离,前端用vue+ts+stylus开发,基于MVVM模式;后端用koa2+mysql+sequelize ORM开发,基于MVC模式。先后端由webpack进行合体,而且对webpack进行了生产模式、开发模式分离配置。最终将前端打包的dist和后端的server上传至服务器,前端代码看做是后端的静态资源。javascript
因为以前时间有限,博客的功能只作了登陆、注册、写文章、修改文章、修改昵称改头像、关注、评论,感兴趣的同窗能够持续添加,好比回复、点赞、分享、分类、标签、推荐等功能。除了功能,样式也能够更改。原本我当初是想仿掘金的,当时时间有限,作了几天就没写了。css
博客演示地址html
GitHub地址前端
blog
├─.babelrc
├─.dockerignore
├─.gitignore
├─Dockerfile
├─package-lock.json
├─package.json
├─README.md
├─tsconfig.json
├─webpack.common.js
├─webpack.dev.js
├─webpack.prod.js
├─static
| └defaultAvatar.png
├─server
| ├─app.ts
| ├─router.ts
| ├─views
| | └index.html
| ├─services
| | ├─BlogService.ts
| | ├─CommentService.ts
| | ├─FollowService.ts
| | ├─ReplyService.ts
| | ├─SortService.ts
| | └UserService.ts
| ├─public
| | ├─dist
| ├─models
| | ├─BlogModel.ts
| | ├─CommentModel.ts
| | ├─FollowModel.ts
| | ├─ReplyModel.ts
| | ├─SortModel.ts
| | └UserModel.ts
| ├─controllers
| | ├─BlogController.ts
| | ├─CommentController.ts
| | ├─FollowController.ts
| | ├─SortController.ts
| | └UserController.ts
| ├─config
| | ├─db.ts
| | └tools.ts
├─node_modules
复制代码
npm init -y
npm i webpack webpack-cli --save-dev
复制代码
npm i clean-webpack-plugin --save-dev
复制代码
npm i html-webpack-plugin --save-dev
复制代码
npm i ts-loader typescript --save-dev
复制代码
npm i webpack-dev-server --save-dev
复制代码
| - client
| - node_modules
| - server
| - public
| - views
| - index.html
.gitignore
| - package-lock.json
| - package.json
| - README.md
复制代码
新建三个配置文件,webpack.common.js、webpack.dev.js、webpack.prod.jsvue
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口
entry: {
index: './client/index.ts'
},
// 编译输出配置
output: {
// js生成到dist/js,[name]表示保留原js文件名,并跟随生成的chunkhash
filename: '[name]-[chunkhash:6].js',
// 输出到server/public,输出路径为dist,必定要绝对路径
path: path.resolve(__dirname, './server/public/dist')
},
// 插件
plugins: [
new CleanWebpackPlugin(),
// 设置html模板生成路径
new HtmlWebpackPlugin({
filename: 'index.html',
template: './server/views/index.html',
chunks: ['index']
})
],
// 配置各个模块规则
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
// 配置文件扩展名
resolve: {
extensions: ['.ts', '.js', '.vue', '.json']
}
}
复制代码
const merge = require('webpack-merge');
const common = require('./webpack.common');
module.exports = merge(common, {
// 热监测服务器,动态监测并实时更新页面
devServer: {
contentBase: './server/public/dist',
// 默认端口为8080
port: 8081,
// 开启热更新
hot: true
}
});
复制代码
const merge = require('webpack-merge');
const common = require('./webpack.common');
module.exports = merge(common, {
// 方便追踪源代码错误
devtool: '#source-map'
});
复制代码
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.prod.js --mode production",
"dev": "webpack-dev-server --open chrome --config webpack.dev.js --mode development"
}
复制代码
npm install babel-loader @babel/core @babel/preset-env --save-dev
复制代码
npm install @babel/plugin-transform-runtime @babel/plugin-transform-modules-commonjs --save-dev
复制代码
npm install @babel/runtime --save
复制代码
注意版本兼容:babel-loader8.x对应babel-core7.X,babel-loader7.x对应babel-core6.Xjava
module.exports = {
module: {
rules: [
// 处理ES6转ES5
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: [
'@babel/plugin-transform-runtime',
'@babel/plugin-transform-modules-commonjs'
]
}
},
exclude: /node_modules/
}
]
}
}
复制代码
npm i vue-loader vue vue-template-compiler css-loader -S
复制代码
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
module: {
rules: [
// 处理vue
{
test: /\.vue$/,
use: 'vue-loader'
}
]
},
plugins: [
// vue-loader必须和VueLoaderPlugin一块儿使用,不然报错
new VueLoaderPlugin()
]
}
复制代码
除此以外,在入口文件里引入.vue文件,会出现红色下划线,这是由于没有声明。所以新建types文件夹,在里面新建vue.d.ts:node
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}
复制代码
由于本项目用typescript开发,即便作出了vue的导入导出声明,也仍是会提示找不到App.vue文件。所以在项目根目录下新建tsconfig.json文件:mysql
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": true
},
"include": ["client", "server"],
"exclude": ["node_modules"]
}
复制代码
启动webpack报以下错误:webpack
ERROR in chunk index [entry]
[name]-[chunkhash:6].js
Cannot use [chunkhash] or [contenthash] for chunk in '[name]-[chunkhash:6].js' (use [hash] instead)
复制代码
这是由于在配置webpack输出filename时这么写的,所以直接使用hash便可。ios
npm install style-loader --save-dev
复制代码
npm install stylus-loader stylus --save-dev
复制代码
module.exports = {
module: {
rules: [
// 处理CSS(相似管道,优先使用css-loader处理,最后是style-loader)
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
// 处理stylus
{
test: /\.styl(us)$/,
use: ['style-loader', 'css-loader', 'stylus-loader']
}
]
}
}
复制代码
注意,每次修改了webpack记得重启项目。
先安装MiniCssExtractPlugin:
npm i mini-css-extract-plugin --save-dev
复制代码
再修改webpack.common.js,将style-loader替换掉:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
......
// 将样式抽离使用link方式引入
new MiniCssExtractPlugin({
filename: '[name]-[hash:6].css'
})
],
// 配置各个模块规则
module: {
rules: [
......
// 处理CSS(相似管道,优先使用css-loader处理,最后是style-loader)
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
// 处理stylus
{
test: /\.styl(us)$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'stylus-loader']
}
]
}
}
复制代码
7.1安装插件file-loader和url-loader,url-loader基于file-loader,因此两个都要安装。 (也能够只使用file-loader,url-loader在file-loader的基础上扩展了功能,好比能设置小于多少KB的图片进行base64转码等)
npm install file-loader url-loader --save-dev
复制代码
7.2配置webpack.common.js
module.exports = {
module: {
rules: [
// 处理图片
{
test: /\.(png|jpg|gif|eot|woff|ttf|svg|webp|PNG)$/,
loader: 'url-loader',
options: {
name: '[name]-[hash:6].[ext]',
esModule: false, // 不然图片加载src显示为object module
limit: 10240, // 小于10kb的特殊处理,转成base64
},
exclude: /node_modules/
}
]
}
}
复制代码
gzip就是GNUzip的缩写,是一个文件压缩程序,能够将文件压缩进后缀为.gz的压缩包。而咱们前端所讲的gzip压缩优化,就是经过gzip这个压缩程序,对资源进行压缩,从而下降请求资源的文件大小。**gzip压缩能力很强,压缩力度可达到70%。
npm i compression-webpack-plugin -D
复制代码
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
......
new CompressionWebpackPlugin({
test: /\.(js|css)$/,
threshold: 10240 // 这里对大于10k的js和css文件进行压缩
})
]
}
复制代码
注意事项:compression-webpack-plugin使用会受版本影响,版本太高会冲突报错。解决方案:从新安装较低版本的包
AT-UI
是一款基于 Vue.js 2.0
的前端 UI 组件库,主要用于快速开发 PC 网站中后台产品.
npm i at-ui -S
复制代码
因为at-ui的样式已经独立成一个项目了,所以这里能够npm安装at-ui-style。本人这里直接使用的CDN方式引入以减少开销。
ERROR in ./node_modules/element-ui/lib/theme-chalk/index.css
Module build failed (from ./node_modules/mini-css-extract-plugin/dist/loader.js):
ModuleParseError: Module parse failed: Unexpected character ' ' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
复制代码
解决:将url-loader替换为file-loader
// 处理图片
{
test: /\.(png|jpg|gif|eot|woff|ttf|svg|webp|PNG)(\?\S*)?$/,
loader: 'file-loader'
// options: {
// name: '[name]-[hash:6].[ext]',
// esModule: false, // 不然图片加载src显示为object module
// limit: 10240, // 小于10kb的特殊处理,转成base64
// puplicPath: './server/public'
// },
// exclude: /node_modules/
}
复制代码
<header v-if="$route.name !== 'register'"><header-section></header-section></header>
复制代码
npm i jsonwebtoken --save
npm i koa-jwt --save
复制代码
配置tsconfig.json
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"sourceMap": true,
"lib": ["DOM", "ES2016", "ES2015"]
},
"include": ["client", "server"],
"exclude": ["node_modules"]
}
复制代码
// 错误处理
app.use(async (ctx, next) => {
return next().catch(err => {
if(err.status === 401) {
ctx.status = 401;
ctx.body = 'Protected resource, use Authorization header to get access\n';
} else {
throw err;
}
})
});
// unless表示不对登陆注册作token校验(颁发token时密钥是secret)
app.use(koajwt({ secret: 'secret' }).unless({ path: [/^\/login/, /^\/register/] }));
app.use(bodyParser());
router(app);
复制代码
前端axios拦截器添加token必定要这样写,不然koa-jwt怎么都不会解析成功!切记!切记!这里我找了一下午的坑~~
let token = JSON.parse(localStorage.getItem('token'));
if(token) {
config.headers.common['Authorization'] = 'Bearer ' + token;
}
复制代码
import Vuex from 'vuex';
import Vue from 'vue';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
user: JSON.parse(localStorage.getItem('user')) || null,
token: JSON.parse(localStorage.getItem('token')) || ''
},
getters: {
getUser: state => state.user,
getToken: state => state.token
},
mutations: {
setUser(state, payload) {
state.user = payload.user;
// 数据持久化
localStorage.setItem('user', JSON.stringify(payload.user));
},
setToken(state, payload) {
state.token = payload.token;
localStorage.setItem('token', JSON.stringify(payload.token));
},
logout(state) {
localStorage.removeItem('user');
localStorage.removeItem('token');
state.user = null;
state.token = '';
}
}
});
export default store;
复制代码
// 存储用户信息
this.$store.commit('setUser', { user: res.data.user });
this.$store.commit('setToken', { token: res.data.token });
复制代码
logout() {
this.$store.commit('logout');
window.location.reload();
}
复制代码
在这里vuex更新导航栏没刷新,我就加了reload手动刷新,因为时间有限具体缘由留到后面再分析。
this.axios.get('/sort').then(res => {
this.sorts = res.data;
}, err => {
if(err.code === -1) { // token鉴权失败
this.$store.commit('logout');
this.$router.push({ name: 'home' });
}
})
复制代码
综上,vuex结合localStorage可以实现用户登陆时保存信息。**vuex 中store的数据须要放到computed 里面才能同步更新视图,切记切记!找了一天的bug,试了n多种方法,才找到是这个缘由~~**贴个连接https://blog.csdn.net/wangshang1320/article/details/98871252
<div class="img-modify">
<label for="input-img">
<at-button type="primary">点击上传</at-button>
</label>
<input type="file" name="input-img" @change="fileHandler($event)" accept="image/*">
</div>
复制代码
.img-modify
flex 9;
label
position absolute;
input
opacity 0;
width 82px;
height 31.6px;
复制代码
fileHandler(e) {
let file = e.target.files[0];
}
复制代码
当我关注了几个博主时,点击导航栏的关注,要获取他们的全部文章。刚开始我是在for循环里操做,可是这样很明显有一个问题,由于for循环是同步代码,我永远只能拿到最后一个请求的结果,因此须要解决这个问题。
由于我使用的是axios,axios自己封装了promise,并且axios提供了一个all方法批量处理异步请求结果,很是方便。首先定义一个返回的promise数组,暂且命名为promiseAll。而后拿到全部·异步结果后,经过调用axios提供的all方法批量处理回调函数里的结果。具体代码以下:
// 获取关注者的全部博文
getFollowersBlogs() {
// 先返回全部异步请求结果
let promiseAll = this.followerList.map((item) => {
return this.axios.get('/blog/email/' + item.follow_email);
});
// 再处理全部回调结果
this.axios.all(promiseAll).then(resArr => {
resArr.forEach(res => {
this.blogList = this.blogList.concat(res.data);
});
}, err => {
if(err.code === -1) { // token鉴权失败
this.$Modal.info({
content: '登陆过时,请从新登陆!'
});
this.$store.commit('logout');
this.$router.push({ name: 'login' });
}
});
}
复制代码
npm install --save js-base64
复制代码
// 引入js-base64对url加密
import { Base64 } from 'js-base64';
Vue.prototype.$Base64 = Base64;
复制代码
对参数加密:
this.$router.push({
name: 'search',
query: { keyword: this.$Base64.encode(this.searchValue) }
});
复制代码
对参数解密:
this.keyword = this.$Base64.decode(this.$route.query.keyword);
复制代码
this.axios.get('/blogs/list', {
params: {
pageSize: this.page.pageSize,
currentPage: this.page.currentPage
}
}).then(res => {
this.blogList = res.data;
}, err => {
console.error(err);
});
复制代码
let pageSize = Number(ctx.request.query.pageSize),
currentPage = Number(ctx.request.query.currentPage);
复制代码
注意数据库查询前参数转为整型,不然会报错。
<at-input v-model="checkPass" type="password" placeholder="请确认密码" size="large"
:maxlength="12" :minlength="6" @keyup.enter.native="register"></at-input> 复制代码
记一次vuex获取用户信息的坑。
由于vuex存储的是user和token,在vue中使用的时候,必须使用计算属性,不然会报错。另外,当退出登陆时,由于user和token都已经被删除,因此使用头像等时格外注意判断。
avatar: function() { if(this.$store.getters.getUser !== null) { return this.$store.getters.getUser.avatar; } return null; }, user: function() { if(this.$store.getters.getUser !== null) { return this.$store.getters.getUser; } return null; } 复制代码
// 用户与评论是一对多关系
UserModel2.hasMany(CommentModel);
CommentModel.belongsTo(UserModel2, { foreignKry: 'email' });
复制代码
// 获取博客评论(要返回用户头像和用户名,需关联表,创建一对多关系)
findBlogComments: async (blog_id) => {
return await CommentModel.findAll({
where: {
blog_id
},
include: [{
model: UserModel2,
attributes: ['username', 'avatar']
}]
})
}
复制代码
上述查询语句会报错:
SequelizeDatabaseError: Unknown column 'comment.userEmail' in 'field list'
注释掉关联声明:
// 用户与评论是一对多关系
// UserModel2.hasMany(CommentModel);
CommentModel.belongsTo(UserModel2, { foreignKey: 'email', targetKey: 'email' });
复制代码
一个模型要关联另外一个模型时,加一句声明便可,不然会报错。
注意点:
这个博客我当时花了4天时间搭建起来,主要是为了巩固webpack各类配置,以及学习typescript的使用(虽然并无怎么用ts语法)。整个过程对于本身掌握项目快速搭建颇有帮助,但愿和我入门前端不久的小伙伴们可以经过这个过程学会webpack的使用。