博客採用先後端分離,前端用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的使用。