Project introduction

The procedure of this project is very detailed, anyone who follows the steps can reproduce 100%.

Blog adopts front and back end separation, front end is developed with VUE + TS +stylus, based on MVVM mode; The back end is developed with KOA2 +mysql+ Sequelize ORM, based on MVC pattern. The front and back ends are integrated by Webpack, and the production mode and development mode are separated configuration of Webpack. Finally, the front-end packaged DIST and back-end Server are uploaded to the server, and the front-end code is treated as a static resource on the back-end.

Due to the limited time before, the blog functions only login, registration, writing articles, modify articles, modify nicknames to change avatar, follow, comment, interested students can continue to add, such as reply, like, share, classification, label, recommendation and other functions. In addition to functionality, styles can be changed. Originally I was trying to imitate the nuggets, time was limited, did a few days did not write.

Blog Demo address

Making the address

The directory structure

Blog ├─.babelrc ├─.DockerIgnore ├─.Gitignore ├─Dockerfile ├─ Package-Lock. json ├─ Package.json ├─ TsConfig.json ├ ─ webpack.com mon. Js ├ ─ webpack. Dev. Js ├ ─ webpack. Prod. Js ├ ─ static | └ defaultAvatar. PNG ├ ─ client | ├ ─ App. Vue | ├ ─ index. The ts | ├ ─ the router. Ts | ├ ─ types | | └ vue. Which s | ├ ─ pages | | ├ ─ an Author. The vue | | ├ ─ Backend. Vue | | ├ ─ Blog. Vue | | ├ ─ Career. Vue | | ├ ─ Focus. Vue | | ├ ─ Freebie. Vue | | ├ ─ Frontend. Vue | | ├ ─ Home. Vue | | ├ ─ the Login. The vue | | ├ ─ the Register. The vue | | ├ ─ Search. Vue | | ├ ─ UserCenter. Vue | | └ Write. Vue | ├ ─ lib | | ├ ─ axiosInterceptor. Ts | | ├ ─ dateFormat, ts | | └ textFilter. Ts | ├ ─ config | | ├ ─ aliOss. Ts | | └ store. Ts | ├ ─ components | | ├ ─ AuthorCard. Vue | | ├ ─ Card. The vue | | ├ ─ CommentCard. Vue | | ├ ─ Header. The vue | | └ SearchCard. Vue ├ ─ server | ├ ─ app. Ts | ├ ─ the router. The ts | ├ ─ views | | └ index. The 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 | | ├ ─ the ts | | └ view ts ├ ─ node_modulesCopy the code

1. Initialize the project

npm init -y
npm i webpack webpack-cli --save-dev
Copy the code

2. Build the infrastructure – Install plug-ins

  • 2.1 Automatically empty dist directory before compilation and install clean-webpack-plugin
npm i clean-webpack-plugin --save-dev
Copy the code
  • 2.2 Implement automatic generation of final HTML from HTML template, install html-webpack-plugin
npm i html-webpack-plugin --save-dev
Copy the code
  • 2.3 Configuring the typescript Environment and installing TS-Loader and typescript
npm i ts-loader typescript --save-dev
Copy the code
  • 2.4 Set up a heat monitoring server for the development environment and install Webpack-dev-server
npm i webpack-dev-server --save-dev
Copy the code
  • 2.5 Building a Project
| - client
| - node_modules
| - server
	| - public
	| - views
		| - index.html
.gitignore
| - package-lock.json
| - package.json
| - README.md
Copy the code

3. Webpack configures the production environment and development environment

Create three configuration files, webpack.common.js, webpack.dev.js, and webpack.prod.js

  • Webpack.com 3.1 mon. Js
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    / / the entry
    entry: {
        index: './client/index.ts'
    },
    // Compile the output configuration
    output: {
        // js generated to dist/js, [name] preserves the original JS file name and follows the generated chunkhash
        filename: '[name]-[chunkhash:6].js'.// Output to server/public, dist, absolute path
        path: path.resolve(__dirname, './server/public/dist')},/ / the plugin
    plugins: [
        new CleanWebpackPlugin(),
        // Set the HTML template generation path
        new HtmlWebpackPlugin({
            filename: 'index.html'.template: './server/views/index.html'.chunks: ['index']})],// Configure the rules for each module
    module: {
        rules: [{test: /\.ts$/,
                use: 'ts-loader'.exclude: /node_modules/}},// Configuration file extension
    resolve: {
        extensions: ['.ts'.'.js'.'.vue'.'.json']}}Copy the code
  • 3.2 webpack. Dev. Js
const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    // Heat monitoring server, dynamic monitoring and real-time update page
    devServer: {
        contentBase: './server/public/dist'.// The default port is 8080
        port: 8081.// Enable hot update
        hot: true}});Copy the code
  • 3.3 webpack. Prod. Js
const merge = require('webpack-merge');
const common = require('./webpack.common');

module.exports = merge(common, {
    // Easy to trace source code errors
    devtool: '#source-map'
});
Copy the code
  • 3.4 modify the package. The 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"
  }
Copy the code

4. Switch from ES6 to ES5

  • 4.1 Installing Babel series dependencies
npm install babel-loader @babel/core @babel/preset-env --save-dev
Copy the code
npm install @babel/plugin-transform-runtime @babel/plugin-transform-modules-commonjs --save-dev
Copy the code
npm install @babel/runtime --save
Copy the code

Note version compatibility: babel-loader8.x corresponds to babel-core7.x, and babel-loader7.x corresponds to babel-core6.x

  • Change ES6 from webpack.common.js to ES5 at compile time:
module.exports = {
    module: {
        rules: [
            // Process ES6 to 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/}}}]Copy the code

5. Configure the VUE development environment

  • 5.1 Installing vue-Loader, VUE, VUe-template-Compiler, and CSS-Loader
npm i vue-loader vue vue-template-compiler css-loader -S
Copy the code
  • 5.2 configure webpack.com mon. Js
const VueLoaderPlugin = require('vue-loader/lib/plugin');

module.exports = {
    module: {
        rules: [
            / / handle the vue
            {
                test: /\.vue$/,
                use: 'vue-loader'}},plugins: [
        Vue-loader must be used with VueLoaderPlugin, otherwise an error is reported
        new VueLoaderPlugin()
    ]
}
Copy the code

In addition, if you introduce a.vue file in an entry file, it will be underlined in red because there is no declaration. So create a new types folder and create vue.d.ts in it:

declare module "*.vue" {
    import Vue from "vue";
    export default Vue;
}
Copy the code

Because this project is developed in typescript, even with vue import/export declarations, the App. Vue file is not found. So create a new tsconfig.json file in the project root directory:

{
    "compilerOptions": {
        "target": "es5"."module": "commonjs"."sourceMap": true
    },
    "include": ["client"."server"]."exclude": ["node_modules"]}Copy the code
  • 5.3 Record a pit

The following error is reported when starting 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)
Copy the code

This is because it is written this way when configuring webpack to output filename, so it simply uses hash.

6. Use stylus in vue

  • 6.1 Installing dependency Packages
npm install style-loader --save-dev
Copy the code
npm install stylus-loader stylus --save-dev
Copy the code
  • 6.2 Configuration in WebPack.common. js
module.exports = {
    module: {
        rules: [
            // Handle CSS(similar to pipes, CSS -loader is preferred, style-loader is the last)
            {
                test: /\.css$/,
                use: ['style-loader'.'css-loader']},/ / deal with stylus
            {
                test: /\.styl(us)$/,
                use: ['style-loader'.'css-loader'.'stylus-loader'}]}}Copy the code

Note that restart the project every time you modify webPack.

  • 6.3 Now we want to introduce styles via link

Install MiniCssExtractPlugin first:

npm i mini-css-extract-plugin --save-dev
Copy the code

Modify webpack.common.js and replace style-loader with webpack.common.js:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    plugins: [...// Pull styles away and use link to import
        new MiniCssExtractPlugin({
            filename: '[name]-[hash:6].css'})].// Configure the rules for each module
    module: {
        rules: [...// Handle CSS(similar to pipes, CSS -loader is preferred, style-loader is the last)
            {
                test: /\.css$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader']},/ / deal with stylus
            {
                test: /\.styl(us)$/,
                use: [MiniCssExtractPlugin.loader, 'css-loader'.'stylus-loader'}]}}Copy the code

7. Manipulate image resources

  • 7.1 Installing plug-ins file-loader and url-loader. Url-loader is based on file-loader, so both should be installed. (You can also only use file-loader. Url-loader extends the function on the basis of file-loader, such as setting the base64 transcoding of images smaller than the number of KB.)

    npm install file-loader url-loader --save-dev
    Copy the code
  • 7.2 configure webpack.com mon. Js

module.exports = {
    module: {
        rules: [
            // Process the image
            {
                test: /\.(png|jpg|gif|eot|woff|ttf|svg|webp|PNG)$/,
                loader: 'url-loader'.options: {
                    name: '[name]-[hash:6].[ext]'.esModule: false.// Otherwise the image load SRC is displayed as the Object Module
                    limit: 10240.// Base64 for special processing smaller than 10KB
                },
                exclude: /node_modules/}}}]Copy the code

8. Enable GZIP compression on the front end

Gzip, short for GNUzip, is a file compression program that can compress files into a. Gz compressed package. And our front-end gzip compression optimization, is through the gzip compression program, compress the resources, so as to reduce the file size of the requested resources. ** Gzip compression ability is very strong, the compression strength can reach 70%.

  • 8.1 installation compression will – webpack – the plugin
npm i compression-webpack-plugin -D
Copy the code
  • 8.2 Configuration in webpack.common.js
const CompressionWebpackPlugin = require('compression-webpack-plugin');

module.exports = {
    plugins: [...new CompressionWebpackPlugin({
            test: /\.(js|css)$/,
            threshold: 10240  // Compress js and CSS files larger than 10K}})]Copy the code

Note: the use of compression-webpack-plugin will be affected by the version. If the version is too high, an error will be reported. Solution: Reinstall a lower version of the package

9. Use the At – the UI

At-ui is a front-end UI component library based on vue. js 2.0, which is mainly used for rapid development of background products in PC websites.

  • 9.1 installation
npm i at-ui -S
Copy the code

Since the at-UI style is already a project in its own right, this is where NPM can install at-UI-style. Here I directly use the CDN method introduced to reduce overhead.

  • 9.2 An Error Occurs After the packaging operation
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
Copy the code

Solution: Replace url-loader with file-loader

// Process the image
            {
                test: /\.(png|jpg|gif|eot|woff|ttf|svg|webp|PNG)(\? \S*)? $/,
                loader: 'file-loader'
                // options: {
                // name: '[name]-[hash:6].[ext]',
                // esModule: false, // otherwise the image loading SRC is displayed as object Module
                // limit: 10240, // Base64 for special processing smaller than 10KB
                // puplicPath: './server/public'
                // },
                // exclude: /node_modules/
            }
Copy the code

Create a navigation bar

  • 10.1 Hide the Navigation Bar during Login or Registration
<header v-if="$route.name ! == 'register'"><header-section></header-section></header>
Copy the code
  • 10.2 Password stored with bcrypt must be set to a sufficient length, otherwise false will always be returned.

11. Verify the login token

  • 11.1 Installation Dependencies
npm i jsonwebtoken --save
npm i koa-jwt --save
Copy the code
  • 11.2TS2304: Cannot find name ‘localStorage’

Configuration tsconfig. Json

{
    "compilerOptions": {
        "target": "es5"."module": "commonjs"."sourceMap": true."lib": ["DOM"."ES2016"."ES2015"]},"include": ["client"."server"]."exclude": ["node_modules"]}Copy the code
  • 11.3 Authentication middleware must be placed before routes.
// Error handling
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 {
            throwerr; }})});// Unless indicates that token verification is not performed for login registration (secret is used for token issuance)
app.use(koajwt({ secret: 'secret' }).unless({ path: [/^\/login/./^\/register/]})); app.use(bodyParser()); router(app);Copy the code

The front-end AXIos interceptor must add a token like this, otherwise koA-JWT will never parse successfully! Remember that! Remember that! I’ve been looking for a hole here all afternoon

let token = JSON.parse(localStorage.getItem('token'));
    if(token) {
        config.headers.common['Authorization'] = 'Bearer ' + token;
    }
Copy the code

12. The user information in the header navigation bar is not refreshed after a successful login in the main component

  • 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;
            // Data persistence
            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;
Copy the code
  • 12.2 Login Component Invoked after successful login
// Store user information
this.$store.commit('setUser', { user: res.data.user });
this.$store.commit('setToken', { token: res.data.token });
Copy the code
  • 12.2 Invoked upon Logout
logout() {
    this.$store.commit('logout');
    window.location.reload();
}
Copy the code

Here, the vuex update navigation bar was not refreshed, so I added reload to manually refresh. Due to limited time, specific reasons will be left for later analysis.

  • 12.3 Authentication Invocation Failure (For example, The Token Expires and the Browser Clears Login Information)
this.axios.get('/sort').then(res= > {
    this.sorts = res.data;
}, err= > {
    if(err.code === -1) {  // Token authentication failed
        this.$store.commit('logout');
        this.$router.push({ name: 'home'}); }})Copy the code

In summary, vuEX and localStorage can save user information during login. ** Store data in vuex needs to be computed to update views synchronically, so keep that in mind! N the bugs found a day, try a variety of methods, is to find the reason put one link blog.csdn.net/wangshang13 ~ ~ * *…

13. Beautify the upload button with input and label in vue

  • 13.1
<div class="img-modify"> <label for="input-img"> <at-button type="primary"> name="input-img" @change="fileHandler($event)" accept="image/*"> </div>Copy the code
.img-modify flex 9; label position absolute; input opacity 0; width 82px; 31.6 px height;Copy the code
  • 13.2 Obtaining a File Object
 fileHandler(e) {
    let file = e.target.files[0];
}
Copy the code

14. The front end gets all followers’ blogs and processes asynchronous operations in batches

  • 14.1 Problem Description

When I follow a few bloggers, I click follow in the navigation bar to get all their posts. I started out inside the for loop, but there was obviously a problem with that, because the for loop was synchronous code and I could only ever get the result of the last request, so I had to fix that.

  • 14.2 solve

Because I’m using AXIos, which itself encapsulates the promise, AND AXIos provides a convenient all method for batching asynchronous request results. Start by defining a return promise array, tentatively named promiseAll. Then, after receiving all the asynchronous results, batch process the results in the callback function by calling axios’s provided all method. The specific code is as follows:

// Get all posts from followers
getFollowersBlogs() {
    // All asynchronous requests are returned first
    let promiseAll = this.followerList.map((item) = > {
        return this.axios.get('/blog/email/' + item.follow_email);
    });
    // reprocess all callback results
    this.axios.all(promiseAll).then(resArr= > {
        resArr.forEach(res= > {
            this.blogList = this.blogList.concat(res.data);
        });
    }, err= > {
        if(err.code === -1) {  // Token authentication failed
            this.$Modal.info({
                content: 'Login expired, please log in again! '
            });
            this.$store.commit('logout');
            this.$router.push({ name: 'login'}); }}); }Copy the code

15. Blog needs to be improved

  1. Home page
  2. Comments ✔
  3. Delete articles
  4. Editing articles must be revised to make them effective
  5. give a like
  6. Search page – Highlight your bookmarks
  7. Front-end URL encryption. Vue with params parameter pass, afraid to refresh the page parameters lost. With Query, the parameters are displayed directly in the address bar. So consider the query encryption here. A method found online uses Base64 encryption. ✔
  8. feedback
  9. Click Enter ➤ to register and search
  10. Password change

16. The front-end implements Base64 encryption on the URL

  • 16.1 installation js – base64
npm install --save js-base64
Copy the code
  • 16.2 Used in ES6+, where the vUE instance is mounted for global use
// Introduce js-base64 url encryption
import { Base64 } from 'js-base64';
Vue.prototype.$Base64 = Base64;
Copy the code

Encrypt parameters:

this.$router.push({ 
 name: 'search'.query: { keyword: this.$Base64.encode(this.searchValue) } 
});
Copy the code

Decrypt parameters:

this.keyword = this.$Base64.decode(this.$route.query.keyword);
Copy the code

17. The AXIos GET request passes arguments like a POST

  • 17.1 How to Write a GET Request
this.axios.get('/blogs/list', {
    params: {
        pageSize: this.page.pageSize,
        currentPage: this.page.currentPage
    }
}).then(res= > {
    this.blogList = res.data;
}, err= > {
    console.error(err);
});
Copy the code
  • 17.2 Obtaining Parameters
let pageSize = Number(ctx.request.query.pageSize),
 currentPage = Number(ctx.request.query.currentPage);
Copy the code

Caution Before database query, the parameter is changed to an integer. Otherwise, an error is reported.

18. Listen to the event of the login and registration password box Enter to register the login

  • Listen for the return event of the last input field
<at-input v-model="checkPass" type="password" placeholder="Please confirm your password." size="large" 
:maxlength="12" :minlength="6" @keyup.enter.native="register"></at-input>
Copy the code

19. The comments

  • 19.1 Page Click the text field to display the comment button

Remember a pit for vuEX to get user information.

Because vuEX stores users and tokens, calculated attributes must be used when vuEX is used in VUE; otherwise, errors will be reported. In addition, when logging out, the user and token have been deleted, so pay special attention to the use of avatar.

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;
}
Copy the code
  • 19.2 Obtaining blog Comments Requires obtaining the user name and profile picture. Therefore, the user table must be associated with the comment table
// Users and comments are one-to-many
UserModel2.hasMany(CommentModel);
CommentModel.belongsTo(UserModel2, { foreignKry: 'email' }); 
Copy the code
// Get the comments from the blog (to return the user's avatar and user name, you need to create a one-to-many relationship)
findBlogComments: async (blog_id) => {
    return await CommentModel.findAll({
        where: {
            blog_id
        },
        include: [{
            model: UserModel2,
            attributes: ['username'.'avatar']]}})}Copy the code

The above query statement will report an error:

SequelizeDatabaseError: Unknown column ‘comment.userEmail’ in ‘field list’

Comment out association declarations:

// Users and comments are one-to-many
// UserModel2.hasMany(CommentModel);
CommentModel.belongsTo(UserModel2, { foreignKey: 'email'.targetKey: 'email' }); 
Copy the code

When a model is associated with another model, add a statement. Otherwise, an error will be reported.

Note:

  1. If type does not exist, it is directly expressed as a string, for example, ‘TIMESTAMP’.
  2. If you need to record the update time when updating a table field, use updateAt and set the default value and the corresponding field name.
  3. If the default value is not a literal value, a literal function can be used to represent it.
  4. TableName Specifies the name of the table, where u is the alias.
  5. When establishing an association, do not write targetKey if the foreign key is associated with the primary key; otherwise, do not write targetKey.

Twenty.

The blog took me four days to set up, mainly to consolidate webPack configurations and to learn how to use typescript (although I didn’t use TS syntax very much). The whole process is very helpful for me to master the rapid construction of the project. I hope that my friends who are just beginners of the front end can also learn the use of Webpack through this process.