使用eggjs+typescript+react开发全栈项目(博客CMS)

2,664 阅读8分钟

前言

在各大网站中,最基础,也最广泛的项目就是内容管理系统(CMS)。本文希望通过开发一个简易的博客系统,来熟悉每个模块的使用,并将这些模块组合起来完成一个完整系统的搭建,学以致用。

项目将用到以下开源框架:

  • eggjs
  • typescript
  • sequlize
  • react
  • react-dom
  • redux
  • antd

文章包括以下知识点:

  • 使用sequelize-cli完成数据库迁移(migration)
  • 使用egg-view-assets实现前后端HMR
  • 使用egg-passport完成鉴权
  • 日志操作
  • 图片上传
  • 中间件使用:自定义错误处理
  • 数据库事务
  • 路由切割

项目线上地址:kapeter.com

项目截图:

屏幕快照 2021-03-16 下午4.56.52.png

建立项目

项目使用typescript开发,因此在用脚手架搭建项目时,选择ts模板。

$ mkdir egg-blog && cd egg-blog
$ npm init egg --type=ts
$ npm i
$ npm run dev

这个时候项目其实已经跑起来了。可以根据自己的需求新增几个文件夹,最终形成的目录结构如下:

egg-project
├── package.json
├── app.ts (可选)
├── agent.ts (可选)
├── app
|   ├── router.ts
│   ├── controller
│   |   └── home.ts
│   ├── service (可选)
│   |   └── user.ts
│   ├── middleware (可选)
│   |   └── response_time.ts
│   ├── schedule (可选)
│   |   └── my_task.ts
│   ├── public (可选)
│   |   └── reset.css
│   ├── view (可选)
│   |   └── home.html
│   └── extend (可选)
│       ├── helper.ts (可选)
│       ├── request.ts (可选)
│       ├── response.ts (可选)
│       ├── context.ts (可选)
│       ├── application.ts (可选)
│       └── agent.ts (可选)
├── config
|   ├── plugin.ts
|   ├── config.default.ts
│   ├── config.prod.ts
|   ├── config.test.ts (可选)
|   ├── config.local.ts (可选)
|   └── config.unittest.ts (可选)
├── build
|   ├── webpack.dev.config.ts
|   ├── webpack.prod.config.ts
├── client
|   ├── index.tsx
|   ├── index.scss
├── database
|   ├── migrations
|   ├── seeders
|   ├── config.json
└── test
    ├── middleware
    |   └── response_time.test.ts
    └── controller
        └── home.test.ts

大部分目录结构是eggjs自有的,可以通过文档了解:eggjs目录结构。这里介绍一下,自定义的部分:

  • build:存放webpack配置
  • client:存放前端代码
  • database:存放数据库相关配置,用于数据库迁移

设置数据库(MySQL)

以前使用SQL语句建表,这种方式既麻烦也会出现很多问题,比如多人协作下,表结构不一致。在其他语言框架中,都会提供一个命令行工具来实现数据库迁移,比如PHP框架Laravel的migrate模块。

在nodejs中,也有不少命令行工具来实现这个功能。本项目选择的ORM框架是sequelize.js,因此使用它对应的命令行工具sequelize-cli

sequelize-cli中文文档

安装该工具:

$ npm install --save sequelize-cli

设置相关路径,在根目录上新建一个.sequelizerc文件:

const path = require("path")

module.exports = {
  'config': path.join(__dirname, 'database/config.json'),
  'migrations-path': path.join(__dirname, 'database/migrations'),
  'seeders-path': path.join(__dirname, 'database/seeders'),
  'models-path': path.join(__dirname, 'app/model'),
};

database/config.json中,配置数据库。

{
  "development": {
    "username": "kapeter",
    "password": "123456",
    "database": "egg_blog_local",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "port": 3306,
    "charset": "UTF8_GENERAL_CI"
  },
  "test": {
    "username": "kapeter",
    "password": "123456",
    "database": "egg_blog_test",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "port": 3306,
    "charset": "UTF8_GENERAL_CI"
  },
  "production": {
    "username": "kapeter",
    "password": "123456",
    "database": "egg_blog_production",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "port": 3306,
    "charset": "UTF8_GENERAL_CI"
  }
}

使用命令行生成一个迁移文件。

$ npx sequelize-cli model:generate --name User --attributes email:string

个人不太习惯写很长的命令行代码,所以attributes参数是去文件里补全的。

这个命令会生成两个文件:

  • models 文件夹中创建了一个user模型文件
  • migrations 文件夹中创建了一个名字像XXXXXXXXXXXXXX-create-user.js的迁移文件。

首先来看,迁移文件。

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      username: {
        type: Sequelize.STRING(50),
        allowNull: false,
        unique: true
      },
      email: {
        type: Sequelize.STRING(50),
        allowNull: false,
        unique: true
      },
      password: {
        type: Sequelize.STRING,
        allowNull: false,
      },
      created_at: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updated_at: {
        allowNull: false,
        type: Sequelize.DATE      
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('users');
  }
};

里面有两个方法,up用于创建表,down用于删除表,是一组互逆的操作。在up方法里面可以看到字段的描述,上文也提过了,我们可以自由修改这些字段属性,不需要在命令行里全部写齐。

然后执行,迁移命令。

$ npx sequelize-cli db:migrate

可以看到数据库已经生成了这张表。

如果发现迁移文件有问题,比如字段属性写错了等等,我们还可以执行回滚任务,撤销这次任务。

$ npx sequelize-cli db:migrate:undo

如果问题出现在已上线运行的数据库中,此时使用撤销任务会导致数据丢失。因此,我们需要通过另一个迁移文件来修改表,而不是删除重建。

这里是一个实际场景:博客系统上线后,我想在目录表(category)中新增一个字段。

database/migrations中新增一个迁移文件20200715032239-add-type-in-category.js

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.addColumn('categories', 'type', {
      type: Sequelize.STRING,
      allowNull: false,
      defaultValue: 'article'
    })
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.removeColumn('categories', 'type')
  }
};

类似的方法,但函数名从createTable变成了addColumn,也就是说,这次的命令是新增字段。然后执行这个文件,我们就可以在不删除表的情况下,对线上数据库表进行升级。

再来看models文件。官方生成的是独立使用的文件,我们用的是eggjs框架,所以需要做点改变。

import { Application } from 'egg';

export default (app: Application): any => {
  const { STRING, INTEGER } = app.Sequelize;

  const User = app.model.define('user', {
    id: { type: INTEGER, primaryKey: true, autoIncrement: true },
    username: STRING(50),
    email: STRING(50),
    password: STRING
  });

  return class extends User {}
}

改造点就是把model绑定到eggjsapp对象上。这里需要结合egg-sequelize使用,下文会提到。

有了表之后,我们可以造一点测试数据进去。

$ npx sequelize-cli seed:generate --name article

这里举的例子是,在文章表(article)中批量添加50篇文章。通过faker插件,我们创建一些测试文章,然后把数据批量添加入数据库表。

const faker = require('faker');

module.exports = {
  up: (queryInterface, Sequelize) => {
    let articles = [];
    for (let i = 0; i < 50; i++) {
      articles[i] = {
        title: faker.lorem.sentence(),
        category_id: faker.random.number({min: 1, max: 5}),
        desc: faker.lorem.paragraph(),
        content: faker.lorem.paragraphs(),
        created_at: new Date(),
        updated_at: new Date()
      }
    }
    return queryInterface.bulkInsert("articles", articles, {});
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('articles', null, {});
  }
};

路由配置

数据库生成之后,我们来加些路由。

分析项目,大致可以把路径分为两类。

  • 前端路由,如/admin,用来导航到对应的前端页面。
  • 接口路由,如/api/article,以/api为前缀,用于前端异步请求。

这里我们使用egg-router-plus来切割这两类路由。

app/router.ts:

import { Application } from 'egg';
import API from './router/api'
import WEB from './router/web'

export default (app: Application) => {
  API(app);
  WEB(app);
};

app/router/api.ts:

import { Application } from 'egg';

export default (app: Application) => {
  const { router, controller } = app;
  const apiController = controller.api;
  const apiRouter = router.namespace('/api');
  
  apiRouter.resources('category', '/category', apiController.category);
  apiRouter.resources('article', '/article', apiController.article);
  apiRouter.resources('gallery', '/gallery', apiController.gallery);
  apiRouter.post('/upload', apiController.tool.upload);
};

通过egg-router-plus,我们可以给统一的路由前缀一个命名空间,便于管理。

resources方法是eggjs提供的方法,用于生成一套RESTful风格的路由,可以减少代码量,有兴趣的可以看他的官方文档

sequelize连接数据库

上文是创建数据库,这里是要让应用连接到数据库,让业务层可以修改数据。上文也提到过,我们使用egg-sequelize作为ORM框架,可以理解为sequelizeeggjs版本。

eggjs插件如何引入,官方已经讲的很详细了,这里不在赘述。

config/config.local.ts里面设置数据库配置。

config.sequelize = {
  dialect: 'mysql',
  // host
  host: '127.0.0.1',
  // 端口号
  port: 3306,
  // 用户名
  username: 'root',
  // 密码
  password: '123456',
  // 数据库名
  database: 'egg_blog_local',
  // 时区
  timezone: '+08:00',
};

app/controller/api/article.ts加个方法, 根据id来获取文章内容。

async show() {
  const { ctx, service } = this;
  const { id } = ctx.params;

  let article = await service.article.findById(id);
  
  ctx.body = {
      code: 0,
      message: 'success',
      data: article
  };
}

然后在app/service/article.ts里添加findById方法。

async findById(id: number): Promise<any> {
    const { Article } = this.ctx.model;

    return Article.findByPk(id);
}

这里this.ctx.model就是上文提到的sequelize实例,通过实例,我们就可以使用sequelize的方法来查询数据库。

此时,使用浏览器或者postman访问网址/api/article/1,就能看到对应数据了(该测试数据就是刚才通过faker生成的测试数据)。

前端代码集成方案

首先,我们先把eggjs推荐的模版引擎egg-view-nunjucks和静态资源管理插件egg-view-assets集成进来。

指定一下html模板。

config.view = {
  root: path.join(appInfo.baseDir, 'app/view'),
  mapping: {
    '.html': 'nunjucks',
  },
};

app/controller/admin.ts里面,渲染相应的html页面。

await ctx.render("admin", {}, {
  templatePath: path.join(app.config.baseDir, 'app/view/admin.html'),
  templateViewEngine: 'nunjucks',
});
<!doctype html>
<html>

<head>
  <title>Admin / KaPeter</title>
  <link href="https://fonts.font.im/css?family=Roboto:300,400" rel="stylesheet">
  {{ helper.assets.getStyle('admin.css') | safe }}
</head>

<body>
  <div id="app"></div>
  {{ helper.assets.getScript('admin.js') | safe }}
</body>

</html>

helper.assets可以获取对应的文件映射关系,加载css和js文件。

接着,来配置webpack,这里需要区分开发和线上环境。

const config: webpack.Configuration = {
    entry: {
        index: './client/index/index.tsx',
        login: './client/login/index.tsx',
        admin: './client/admin/index.tsx'
    },
    output: {
        path: path.resolve(__dirname, 'public'),
        publicPath: 'http://127.0.0.1:8888/',
        filename: '[name].js'
    },
    resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json']
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'babel-loader'
            },
            {
                test: /\.(less|css)/,
                use: ExtractTextPlugin.extract({
                    fallback: 'style-loader',
                    use: [{
                        loader: 'css-loader', // translates CSS into CommonJS
                    }, {
                        loader: 'less-loader',
                        options: {
                            modifyVars: {
                                'primary-color': '#15c0b0',
                                'link-color': '#15c0b0',
                            },
                            javascriptEnabled: true
                        }
                    }
                    ],
                    publicPath: '../'
                })
            },
            {
                test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
                loader: 'url-loader',
                options: {
                    limit: 10000,
                    name: 'img/[name].[ext]'
                }
            },
            {
                test: /\.(ttf|eot|woff|woff2)$/,
                loader: 'url-loader',
                options: {
                    limit: 10000,
                    name: 'fonts/[name].[ext]',
                },
            },
        ]
    },
    plugins: [
        new ExtractTextPlugin('[name].css'),
        new webpack.DefinePlugin({
            'process.env': {
                NODE_ENV: JSON.stringify('development')
            }
        })
    ],

    devServer: {
        port: 8888,
        contentBase: path.join(__dirname, '../public'),
        historyApiFallback: true,
        host: '127.0.0.1',
        disableHostCheck: true,
        headers: {
            "Access-Control-Allow-Origin": "*"
        }
    },

    devtool: 'source-map'
}

重点关注outputdevServer配置,主项目运行在7001端口,我们另起一个8888端口来运行webpack-dev-server。要注意的是,因为js是在7001端口运行,所以在执行热更新的时候,会出现跨域问题,所以要设置devServerheader,使之支持跨域。

然后去config/config.local.ts里面设置启动devServer的语句, 同时完成文件映射。

config.assets = {
    devServer: {
        command: 'webpack-dev-server --config ./build/webpack.dev.config.ts --inline --hot',
        port: 8888,
        devServer: {
            env: {
                ESLINT: 'none',
                PUBLIC_PATH: 'http://127.0.0.1:8888',
            },
            debug: true
        }
    }
}

上文提到helper.assets.getScript('admin.js')就会转化成http://127.0.0.1:8888/admin.js。此时,运行npm run dev,就会同时启动node服务(7001端口)和webpack(8888端口),同时支持前后端HMR,就可以愉快地进行开发了。

然后来看正式环境webpack配置。其他配置都一样,就是要加一个manifest组件。

new ManifestPlugin({
    fileName: path.resolve(__dirname, '../config/manifest.json'),
    publicPath: ""
})

此时,执行打包命令,最终文件会输出到public文件夹下,同时会在config文件夹下生成一份manifest.json。比如:

{
  "admin.js": "js/admin.fb2ffa.js",
  "admin.css": "css/admin.fb2ffa.css",
  "index.js": "js/index.fb2ffa.js",
  "index.css": "css/index.fb2ffa.css",
  "login.js": "js/login.fb2ffa.js",
  "login.css": "css/login.fb2ffa.css"
}

此时,helper.assets.getScript('admin.js')就会转化成/assets/js/admin.fb2ffa.js。这个时候,只有一个node服务,devServer是不需要的。

这样我们就把前端代码集成到项目中了。

如果你是用babel-loader打包ts文件,需要关注一下这个坑:Typescript+eggjs发布注意事项

鉴权方案

这里使用官方文档中提到的egg-passport + passport-local方案来完成鉴权。

引入插件就不说了。

app/router/web.ts中新增鉴权路由。

// 展示登录页面
router.get('/login', controller.auth.login);
// 执行登录
router.post('/login', passport.authenticate('local', { successRedirect: '/admin', failureRedirect: '/login?err=1' }));
// 退出登录
router.get('/logout', controller.auth.logout);

app/controller/auth.ts里面写具体实现。

class AuthController extends Controller {
  async login() {
    const { ctx,app } = this;
    if (!ctx.isAuthenticated()) {
      await ctx.render("login", {}, {
        templatePath: path.join(app.config.baseDir, 'app/view/login.html'),
        templateViewEngine: 'nunjucks',
      });
    } else {
      ctx.redirect('/admin');
    }
  }

  async logout() {
    const { ctx } = this;
    ctx.logout();
    await ctx.redirect("/login");
  }
}

判断当前用户是否登录(ctx.isAuthenticated()),未登录显示登录页面,已登录用户直接跳转后台。

app.ts中添加鉴权方法, 同时将登录信息记入日志。

app.passport.verify(async (ctx, user) => {
    const existUser = await ctx.model.User.findOne({
        where: {
            username: user.username,
            password: crypto.createHmac('sha256', app.config.authSalt).update(user.password).digest('hex')
        }
    });
    if (existUser) {
        ctx.getLogger("passportLogger").info(`${user.username} logined ip: ${ctx.ip}`);
    }
    return existUser;
});

自定义中间件

每一个错误单独处理很麻烦,并且代码量会很多,因此,我们需要创建一个中间件来捕获这些错误,进行统一处理。

config/config.default.ts里面注册这个中间件。

config.middleware = [ 'errorHandler' ];

然后,来实现这个中间件。

export default function errorHandlerMiddleWare(): any {
  return async (ctx: Context, next: () => Promise<any>) => {
    try {
      await next();
    } catch (err) {
      // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志
      ctx.app.emit('error', err, ctx);

      const status = err.status || 500;

      switch (status) {
        case 1:
          ctx.body = {
            code: status,
            message: "存在重复key"
          }
          break;
        case 401:
          ctx.body = {
            code: status,
            message: "无操作权限"
          }
          break;
        case 422:
          ctx.body = {
            code: status,
            message: "参数错误"
          }
          break;
        default:
          // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息
          const message = ctx.app.config.env === 'prod' ? 'Internal Server Error' : err.message;   
          ctx.body = { 
            code: -1, 
            message: message
          };
          break;
      }

      ctx.status = status === 500 ? 500 : 200;
    }
  };
}

将整个过程用trycatch包裹起来,我们在业务代码里只需要抛出对于的错误码,都会被这个中间件捕获,然后我们可以根据不同的错误码返回不同的错误信息。

自定义日志设置

系统默认是按照天来切割日志文件,对于博客这种小流量网站来说,每日访问量可能不足100,生成的日志文件多是空白的。因此我们需要修改默认设置,改成按文件大小来拆分。

// 日志按文件大小分割
config.logrotator = {
  filesRotateBySize: [
    path.join(appInfo.root, 'logs', appInfo.name, 'egg-web.log'),
    path.join(appInfo.root, 'logs', appInfo.name, 'common-error.log'),
    path.join(appInfo.root, 'logs', appInfo.name, 'egg-agent.log'),
    path.join(appInfo.root, 'logs', appInfo.name, 'egg-schedule.log'),
    path.join(appInfo.root, 'logs', appInfo.name, 'miku-web.log')
  ],
  maxFileSize: 16 * 1024 * 1024
};

还有个场景是,我想设置一个只有登录记录的日志。可以通过官方提供的扩展设置来完成。

  // 自定义日志
  config.customLogger = {
    passportLogger: {
      file: path.join(appInfo.root, 'logs', appInfo.name, 'miku-passport.log')
    }
  }

使用起来也方便,在上文的鉴权方案里已经出现过了。

ctx.getLogger("passportLogger").info(`${user.username} logined ip: ${ctx.ip}`);

图片上传方案

在写文章的时候,添加图片是必不可少的。这里结合simplemde编辑器做一个图片上传方案。

在编辑器初始化时,对image模块进行自定义。这里的操作是创建一个上传对话框。

simplemde = new SimpleMDE({
    element: mdeElement,
    toolbar: [
        "bold", "italic", "heading", "|", "quote", "ordered-list", "unordered-list", "|", "code",
        {
            name: "image",
            action: () => {
                _this.setState({
                    visible: true
                })
            },
            className: "fa fa-image",
            title: "upload",
        },
        "link", "table", "|", "preview", "side-by-side", "fullscreen", "|", "guide"
    ]
})

异步上传这个图片文件,上传成功后,关闭对话框,拼接内容,并将内容输出到当前光标处。

handleUpload() {
    let formData = new FormData();
    const fileList: any = this.file ? this.file.files : [];
    formData.append('file', fileList[0])

    http.post('/api/upload', formData, {
        headers: {
            "Content-Type": "multipart/form-data"
        }
    }).then((res: any) => {
        this.setState({
            visible: false
        })
        let str = `![](${res.url})`;
        simplemde.codemirror.setCursor(this.cursorPos ? this.cursorPos : {ch: 0, line: 0, sticky: null});
        simplemde.codemirror.replaceSelection(str);
    }).catch((err: Error) => {
        console.log(err);
    })
}

再来看上传的后台代码。

async upload() {
    const { ctx, app } = this;

    if (!ctx.isAuthenticated()) throw { status: 401 };

    try {
        const file = ctx.request.files[0];

        if (!file) throw { status: 404, message: 'file not found' };

        const targetPath = path.join(app.baseDir, app.config.uploadPath, moment().format("YYYYMM"));
        // 如果不存在该文件夹就创建
        await fs.ensureDirSync(targetPath);

        const filename = new Date().getTime() + "." + file.filename.split(".").slice(-1);
        await fs.copyFileSync(file.filepath, path.join(targetPath, filename));
        ctx.body = {
            code: 0,
            message: "success",
            url: "/assets/upload/" + moment().format("YYYYMM") + "/" + filename
        };
    } catch (err) {
        console.log(err);
        throw { status: 500, message: err }
    } finally {
        await ctx.cleanupRequestFiles();
    }
}

采用eggjs内置的Multipart插件来完成文件上传,然后通过fs-extra将图片复制到指定文件夹下。这里按上传日期进行文件夹拆分,方便管理。需要注意的是,不敢请求有没有成功,都需要将临时文件清除。

数据库事务处理

在项目中,有这样的场景:删除目录时,需要同步移除该目录下的内容,这时需要同时对两张以上表进行操作。为保证数据一致性,我们需要用到数据库事务。sequelize提供数据库事务能力,我们需要做的就是和eggjs结合起来。

async destroy(id: number): Promise<any>  {
    const { ctx } = this;
    const { Category, Article, Gallery } = ctx.model;
    const category = await Category.findByPk(id);

    if (!category) throw { status: 404 };

    const t = await ctx.model.transaction();

    try {
        // 根据目录类型删除对应内容
        switch (category.type) {
            case CATEGORY_TYPE.ARTICLE:
                await Article.destroy({
                    where: {
                        category_id: category.id
                    }
                }, { transaction: t });
                break;
            case CATEGORY_TYPE.GALLERY:
                await Gallery.destroy({
                    where: {
                        category_id: category.id
                    }
                }, { transaction: t });
                break;
        }
        // 删除目录
        await category.destroy();
        return await t.commit();
      } catch (err) {
        // 事务回滚
        await t.rollback(); 
        throw { status: 500, message: err };
      }
}

上文提到过,this.ctx.modelsequelize的实例,因此通过该实例我们来得到数据库事务的对象t。先根据目录类型删除对应内容,然后删除目录。使用t.commit()执行事务,使用t.rollback()来回滚事务。

部署上线

项目是ts开发的,但egg-scirpt无法直接识别ts代码,因此,需要通过npm run tsc,来将ts文件转化成js文件。然后,执行egg-scirpt运行项目。

剩下的就是通过nginx将7001端口映射到80端口,设置一下ssl协议。

upstream miku {
    server 127.0.0.1:7001;
}

server {
   listen 80;
   server_name www.your_name.com your_name.com;
   if ( $host != 'your_name.com'  ) {
      rewrite ^/(.*) https://your_name.com/$1 redirect;
   }
   rewrite ^(.*)$  https://$host$1 redirect;
}

server {
    listen 443;
    ssl on;
    ssl_certificate   /web/nginx/cert/your_pem;
    ssl_certificate_key  /web/nginx/cert/your_key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    server_name your_name.com www.your_name.com;
    root /www/your_folder;
    client_max_body_size 20M;

    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_max_temp_file_size 0;
        proxy_pass http://miku/;
        proxy_redirect off;
        proxy_read_timeout 240s;
     }
}

到此,就可以通过域名访问你的博客网站啦。