webpack先後端合體開發博客

項目簡介

博客採用先後端分離,前端用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
複製代碼

1. 初始化項目

npm init -y
npm i webpack webpack-cli --save-dev
複製代碼

2.構建基礎架構-安裝插件

  • 2.1 實現每次編譯前自動清空dist目錄,安裝clean-webpack-plugin
npm i clean-webpack-plugin --save-dev
複製代碼
  • 2.2 實現從HTML模板自動生成最終HTML,安裝html-webpack-plugin
npm i html-webpack-plugin --save-dev
複製代碼
  • 2.3 配置typescript環境,安裝ts-loader、typescript
npm i ts-loader typescript --save-dev
複製代碼
  • 2.4 搭建開發環境的熱監測服務器,安裝webpack-dev-server
npm i webpack-dev-server --save-dev
複製代碼
  • 2.5 構建項目
| - client
| - node_modules
| - server
	| - public
	| - views
		| - index.html
.gitignore
| - package-lock.json
| - package.json
| - README.md
複製代碼

3.webpack配置生產環境和開發環境

新建三個配置文件,webpack.common.js、webpack.dev.js、webpack.prod.jsvue

  • 3.1 webpack.common.js
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']
    }
}
複製代碼
  • 3.2 webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    // 熱監測服務器,動態監測並實時更新頁面
    devServer: {
        contentBase: './server/public/dist',
        // 默認端口爲8080
        port: 8081,
        // 開啓熱更新
        hot: true
    }
});
複製代碼
  • 3.3 webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    // 方便追蹤源代碼錯誤
    devtool: '#source-map'
});
複製代碼
  • 3.4修改package.json
"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"
  }
複製代碼

4.解決ES6轉ES5

  • 4.1 安裝babel系列依賴
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

  • 4.2 修改webpack.common.js,這裏代碼的做用是,在編譯時把js文件中ES6轉成ES5:
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/
            }
        ]
    }
}
複製代碼

5.配置vue開發環境

  • 5.1安裝vue-loader、vue、vue-template-compiler、css-loader
npm i vue-loader vue vue-template-compiler css-loader -S
複製代碼
  • 5.2配置webpack.common.js
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"]
}
複製代碼
  • 5.3記一次坑

啓動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

6.在vue裏使用stylus

  • 6.1安裝依賴包
npm install style-loader --save-dev
複製代碼
npm install stylus-loader stylus --save-dev
複製代碼
  • 6.2在webpack.common.js裏配置
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記得重啓項目。

  • 6.3如今咱們想把樣式經過link方式引入

先安裝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.處理圖片資源

  • 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/
            }
        ]
    }
}
複製代碼

8.前端開啓GZIP壓縮

gzip就是GNUzip的縮寫,是一個文件壓縮程序,能夠將文件壓縮進後綴爲.gz的壓縮包。而咱們前端所講的gzip壓縮優化,就是經過gzip這個壓縮程序,對資源進行壓縮,從而下降請求資源的文件大小。**gzip壓縮能力很強,壓縮力度可達到70%。

  • 8.1安裝compression-webpack-plugin
npm i compression-webpack-plugin -D
複製代碼
  • 8.2在webpack.common.js裏配置
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
    plugins: [
        ......
        new CompressionWebpackPlugin({
            test: /\.(js|css)$/,
            threshold: 10240  // 這裏對大於10k的js和css文件進行壓縮
        })
    ]
}
複製代碼

注意事項:compression-webpack-plugin使用會受版本影響,版本太高會衝突報錯。解決方案:從新安裝較低版本的包

9.使用At-UI

AT-UI 是一款基於 Vue.js 2.0 的前端 UI 組件庫,主要用於快速開發 PC 網站中後臺產品.

  • 9.1 安裝
npm i at-ui -S
複製代碼

因爲at-ui的樣式已經獨立成一個項目了,所以這裏能夠npm安裝at-ui-style。本人這裏直接使用的CDN方式引入以減少開銷。

  • 9.2打包運行後報錯
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/
            }
複製代碼

10.製做導航欄

  • 10.1登陸或者註冊時隱藏導航欄
<header v-if="$route.name !== 'register'"><header-section></header-section></header>
複製代碼
  • 10.2用bcrypt存儲的密碼,必定要設置足夠的長度,不然會一直返回false。

11.登陸token校驗

  • 11.1安裝依賴
npm i jsonwebtoken --save
npm i koa-jwt --save
複製代碼
  • 11.2TS2304: Cannot find name 'localStorage'

配置tsconfig.json

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "sourceMap": true,
        "lib": ["DOM", "ES2016", "ES2015"]
    },
    "include": ["client", "server"],
    "exclude": ["node_modules"]
}
複製代碼
  • 11.3鑑權中間件必定放在路由的前面。
// 錯誤處理
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;
    }
複製代碼

12.main組件裏登陸操做,成功後header裏導航欄用戶信息不刷新

  • 12.1vuex結合localStorage
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;
複製代碼
  • 12.2登陸組件登陸成功後調用
// 存儲用戶信息
this.$store.commit('setUser', { user: res.data.user });
this.$store.commit('setToken', { token: res.data.token });
複製代碼
  • 12.2登出時調用
logout() {
    this.$store.commit('logout');
    window.location.reload();
}
複製代碼

在這裏vuex更新導航欄沒刷新,我就加了reload手動刷新,因爲時間有限具體緣由留到後面再分析。

  • 12.3鑑權失敗調用(好比token過時了瀏覽器清除登陸信息)
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

13.vue中使用input和label實現上傳按鈕美化

  • 13.1
<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;
複製代碼
  • 13.2獲取file對象
fileHandler(e) {
    let file = e.target.files[0];
}
複製代碼

14.前端獲取全部關注者的博客,批量處理異步操做

  • 14.1問題描述

當我關注了幾個博主時,點擊導航欄的關注,要獲取他們的全部文章。剛開始我是在for循環裏操做,可是這樣很明顯有一個問題,由於for循環是同步代碼,我永遠只能拿到最後一個請求的結果,因此須要解決這個問題。

  • 14.2解決

由於我使用的是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' });
        }
    });
}
複製代碼

15.博客待完善功能

  1. 首頁分頁✔
  2. 評論✔
  3. 刪除文章
  4. 編輯文章必須修改內容才生效的問題✔
  5. 點贊
  6. 搜索頁-對題目高亮✔
  7. 前端url加密。vue裏用params傳參呢,怕刷新頁面參數丟失。用query呢,參數直接顯示在地址欄。所以這裏考慮對query加密處理。網上搜索到一種方法,用到的是base64加密。✔
  8. 反饋
  9. 登陸註冊及搜索支持按鍵enter✔
  10. 密碼修改

16.前端對url進行base64加密

  • 16.1安裝js-base64
npm install --save js-base64
複製代碼
  • 16.2在ES6+中使用,這裏將掛載到vue實例上,以供全局使用
// 引入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);
複製代碼

17.axios的get請求像post那樣傳遞參數

  • 17.1get請求時的寫法
this.axios.get('/blogs/list', {
    params: {
        pageSize: this.page.pageSize,
        currentPage: this.page.currentPage
    }
}).then(res => {
    this.blogList = res.data;
}, err => {
    console.error(err);
});
複製代碼
  • 17.2獲取參數
let pageSize = Number(ctx.request.query.pageSize),
 currentPage = Number(ctx.request.query.currentPage);
複製代碼

注意數據庫查詢前參數轉爲整型,不然會報錯。

18.監聽登陸和註冊密碼框enter事件實現登陸註冊

  • 監聽最後一個輸入框回車事件
<at-input v-model="checkPass" type="password" placeholder="請確認密碼" size="large" 
:maxlength="12" :minlength="6" @keyup.enter.native="register"></at-input> 複製代碼

19.評論

  • 19.1頁面點擊文本域顯示評論按鈕

記一次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;
}
複製代碼
  • 19.2獲取博客評論須要獲取用戶名、頭像,所以用戶表和評論表須要關聯
// 用戶與評論是一對多關係
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' }); 
複製代碼

一個模型要關聯另外一個模型時,加一句聲明便可,不然會報錯。

注意點

  1. type 若是不存在則直接用字符串表示 如:’TIMESTAMP’;
  2. 若是須要在更新表字段時記錄更新時間,可應使用 updateAt,並設置默認值和對應的字段名。
  3. 若是默認值不是具體的數值,能夠用 literal 函數去表示。
  4. tableName 表名,u 爲別名。
  5. 創建關聯關係時,若是外鍵關聯的是主鍵則不用寫 targetKey,不然須要。

20.總結

這個博客我當時花了4天時間搭建起來,主要是爲了鞏固webpack各類配置,以及學習typescript的使用(雖然並無怎麼用ts語法)。整個過程對於本身掌握項目快速搭建頗有幫助,但願和我入門前端不久的小夥伴們可以經過這個過程學會webpack的使用。

相關文章
相關標籤/搜索