万字长文详解如何搭建一个属于自己的博客(纯手工搭建)

前言

因为自己以前就搭建了自己的博客系统,那时候博客系统前端基本上都是基于vue的,而现在用的react偏多,于是用react对整个博客系统进行了一次重构,还有对以前存在的很多问题进行了更改与优化。系统都进行了服务端渲染SSR的处理。

博客地址传送门

本项目完整的代码:GitHub 仓库

本文篇幅较长,会从以下几个方面进行展开介绍:

  1. 核心技术栈
  2. 目录结构详解
  3. 项目环境启动
  4. Server端源码解析
  5. Client端源码解析
  6. Admin端源码解析
  7. HTTPS创建

核心技术栈

  1. React 17.x (React 全家桶)
  2. Typescript 4.x
  3. Koa 2.x
  4. Webpack 5.x
  5. Babel 7.x
  6. Mongodb (数据库)
  7. eslint + stylelint + prettier (进行代码格式控制)
  8. husky + lint-staged + commitizen +commitlint (进行 git 提交的代码格式校验跟 commit 流程校验)

核心大概就是以上的一些技术栈,然后基于博客的各种需求进行功能开发。像例如授权用到的jsonwebtoken,@loadable,log4js模块等等一些功能,我会下面各个功能模块展开篇幅进行讲解。

package.json 配置文件地址

目录结构详解

|-- blog-source
    |-- .babelrc.js   // babel配置文件
    |-- .commitlintrc.js // git commit格式校验文件,commit格式不通过,禁止commit
    |-- .cz-config.js // cz-customizable的配置文件。我采用的cz-customizable来做的commit规范,自己自定义的一套
    |-- .eslintignore // eslint忽略配置
    |-- .eslintrc.js // eslint配置文件
    |-- .gitignore // git忽略配置
    |-- .npmrc // npm配置文件
    |-- .postcssrc.js // 添加css样式前缀之类的东西
    |-- .prettierrc.js // 格式代码用的,统一风格
    |-- .sentryclirc // 项目监控Sentry
    |-- .stylelintignore // style忽略配置
    |-- .stylelintrc.js // stylelint配置文件
    |-- package.json
    |-- tsconfig.base.json // ts配置文件
    |-- tsconfig.json // ts配置文件
    |-- tsconfig.server.json // ts配置文件
    |-- build // Webpack构建目录, 分别给client端,admin端,server端进行区别构建
    |   |-- paths.ts
    |   |-- utils.ts
    |   |-- config
    |   |   |-- dev.ts
    |   |   |-- index.ts
    |   |   |-- prod.ts
    |   |-- webpack
    |       |-- admin.base.ts
    |       |-- admin.dev.ts
    |       |-- admin.prod.ts
    |       |-- base.ts
    |       |-- client.base.ts
    |       |-- client.dev.ts
    |       |-- client.prod.ts
    |       |-- index.ts
    |       |-- loaders.ts
    |       |-- plugins.ts
    |       |-- server.base.ts
    |       |-- server.dev.ts
    |       |-- server.prod.ts
    |-- dist // 打包output目录
    |-- logs // 日志打印目录
    |-- private // 静态资源入口目录,设置了多个
    |   |-- third-party-login.html
    |-- publice // 静态资源入口目录,设置了多个
    |-- scripts // 项目执行脚本,包括启动,打包等等
    |   |-- build.ts
    |   |-- config.ts
    |   |-- dev.ts
    |   |-- start.ts
    |   |-- utils.ts
    |   |-- plugins
    |       |-- open-browser.ts
    |       |-- webpack-dev.ts
    |       |-- webpack-hot.ts
    |-- src // 核心源码
    |   |-- client // 客户端代码
    |   |   |-- main.tsx // 入口文件
    |   |   |-- tsconfig.json // ts配置
    |   |   |-- api // api接口
    |   |   |-- app // 入口组件
    |   |   |-- appComponents // 业务组件
    |   |   |-- assets // 静态资源
    |   |   |-- components // 公共组件
    |   |   |-- config // 客户端配置文件
    |   |   |-- contexts // context, 就是用useContext创建的,用来组件共享状态的
    |   |   |-- global // 全局进入client需要进行调用的方法。像类似window上的方法
    |   |   |-- hooks // react hooks
    |   |   |-- pages // 页面
    |   |   |-- router // 路由
    |   |   |-- store // Store目录
    |   |   |-- styles // 样式文件
    |   |   |-- theme // 样式主题文件,做换肤效果的
    |   |   |-- types // ts类型文件
    |   |   |-- utils // 工具类方法
    |   |-- admin // 后台管理端代码,同客户端差不太多
    |   |   |-- .babelrc.js
    |   |   |-- app.tsx
    |   |   |-- main.tsx
    |   |   |-- tsconfig.json
    |   |   |-- api
    |   |   |-- appComponents
    |   |   |-- assets
    |   |   |-- components
    |   |   |-- config
    |   |   |-- hooks
    |   |   |-- pages
    |   |   |-- router
    |   |   |-- store
    |   |   |-- styles
    |   |   |-- types
    |   |   |-- utils
    |   |-- models // 接口模型
    |   |-- server // 服务端代码
    |   |   |-- main.ts // 入口文件
    |   |   |-- config // 配置文件
    |   |   |-- controllers // 控制器
    |   |   |-- database // 数据库
    |   |   |-- decorators // 装饰器,封装了@Get,@Post,@Put,@Delete,@Cookie之类的
    |   |   |-- middleware // 中间件
    |   |   |-- models // mongodb模型
    |   |   |-- router // 路由、接口
    |   |   |-- ssl // https证书,目前我是本地开发用的,线上如果用nginx的话,在nginx处配置就行
    |   |   |-- ssr // 页面SSR处理
    |   |   |-- timer // 定时器
    |   |   |-- utils // 工具类方法
    |   |-- shared // 多端共享的代码
    |   |   |-- loadInitData.ts
    |   |   |-- type.ts
    |   |   |-- config
    |   |   |-- utils
    |   |-- types // ts类型文件
    |-- static // 静态资源
    |-- template // html模板

以上就是项目大概的文件目录,上面已经描述了文件的基本作用,下面我会详细博客功能的实现过程。目前博客系统各端没有拆分出来,接下里会有这个打算。

项目环境启动

确保你的node版本在10.13.0 (LTS)以上,因为Webpack 5Node.js 的版本要求至少是 10.13.0 (LTS)

执行脚本,启动项目

首先从入口文件开始:

"dev": "cross-env NODE_ENV=development TS_NODE_PROJECT=tsconfig.server.json ts-node --files scripts/start.ts"
"prod": "cross-env NODE_ENV=production TS_NODE_PROJECT=tsconfig.server.json ts-node --files scripts/start.ts"

1. 执行入口文件scripts/start.js

// scripts/start.js
import path from 'path'
import moduleAlias from 'module-alias'

moduleAlias.addAliases({
   
  '@root': path.resolve(__dirname, '../'),
  '@server': path.resolve(__dirname, '../src/server'),
  '@client': path.resolve(__dirname, '../src/client'),
  '@admin': path.resolve(__dirname, '../src/admin'),
})

if (process.env.NODE_ENV === 'production') {
   
  require('./build')
} else {
   
  require('./dev')
}

设置路径别名,因为目前各端没有拆分,所以建立别名(alias)好查找文件。

2. 由入口文件进入开发development环境的搭建

首先导出webpack各端的各自环境的配置文件。

// dev.ts
import clientDev from './client.dev'
import adminDev from './admin.dev'
import serverDev from './server.dev'
import clientProd from './client.prod'
import adminProd from './admin.prod'
import serverProd from './server.prod'
import webpack from 'webpack'

export type Configuration = webpack.Configuration & {
   
  output: {
   
    path: string
  }
  name: string
  entry: any
}
export default (NODE_ENV: ENV): [Configuration, Configuration, Configuration] => {
   
  if (NODE_ENV === 'development') {
   
    return [clientDev as Configuration, serverDev as Configuration, adminDev as Configuration]
  }
  return [clientProd as Configuration, serverProd as Configuration, adminProd as Configuration]
}

webpack的配置文件,基本不会有太大的区别,目前就贴一段简单的webpack配置,分别有 server,client,admin 不同环境的配置文件。具体可以看博客源码

import webpack from 'webpack'
import merge from 'webpack-merge'
import {
    clientPlugins } from './plugins' // plugins配置
import {
    clientLoader } from './loaders' // loaders配置
import paths from '../paths'
import config from '../config'
import createBaseConfig from './base' // 多端默认配置

const baseClientConfig: webpack.Configuration = merge(createBaseConfig(), {
   
  mode: config.NODE_ENV,
  context: paths.rootPath,
  name: 'client',
  target: ['web', 'es5'],
  entry: {
   
    main: paths.clientEntryPath,
  },
  resolve: {
   
    extensions: ['.js', '.json', '.ts', '.tsx'],
    alias: {
   
      '@': paths.clientPath,
      '@client': paths.clientPath,
      '@root': paths.rootPath,
      '@server': paths.serverPath,
    },
  },
  output: {
   
    path: paths.buildClientPath,
    publicPath: paths.publicPath,
  },
  module: {
   
    rules: [...clientLoader],
  },
  plugins: [...clientPlugins],
})
export default baseClientConfig

然后分别来处理adminclientserver端的webpack配置文件

以上几个点需要注意:

  • admin端跟client端分别开了一个服务处理webpack的文件,都打包在内存中。
  • client端需要注意打包出来文件的引用路径,因为是SSR,需要在服务端获取文件直接渲染,我把服务端跟客户端打在不同的两个服务,所以在服务端引用client端文件的时候需要注意引用路径。
  • server端代码直接打包在dist文件下,用于启动,并没有打在内存中。
const WEBPACK_URL = `${
     __WEBPACK_HOST__}:${
     __WEBPACK_PORT__}`
const [clientWebpackConfig, serverWebpackConfig, adminWebpackConfig] = getConfig(process.env.NODE_ENV as ENV)
// 构建client 跟 server
const start = async () => {
   
  // 因为client指向的另一个服务,所以重写publicPath路径,不然会404
  clientWebpackConfig.output.publicPath = serverWebpackConfig.output.publicPath = `${
     WEBPACK_URL}${
     clientWebpackConfig.output.publicPath}`
  clientWebpackConfig.entry.main = [`webpack-hot-middleware/client?path=${
     WEBPACK_URL}/__webpack_hmr`, clientWebpackConfig.entry.main]
  const multiCompiler = webpack([clientWebpackConfig, serverWebpackConfig])
  const compilers = multiCompiler.compilers
  const clientCompiler = compilers.find((compiler) => compiler.name === 'client') as webpack.Compiler
  const serverCompiler = compilers.find((compiler) => compiler.name === 'server') as webpack.Compiler

  // 通过compiler.hooks用来监听Compiler编译情况
  const clientCompilerPromise = setCompilerTip(clientCompiler, clientWebpackConfig.name)
  const serverCompilerPromise = setCompilerTip(serverCompiler, serverWebpackConfig.name)

  // 用于创建服务的方法,在此创建client端的服务,至此,client端的代码便打入这个服务中, 可以通过像 https://192.168.0.47:3012/js/lib.js 访问文件
  createService({
   
    webpackConfig: clientWebpackConfig,
    compiler: clientCompiler,
    port: __WEBPACK_PORT__
  })
  let script: any = null
  // 重启
  const nodemonRestart = () => {
   
    if (script) {
   
      script.restart()
    }
  }

  // 监听server文件更改
  serverCompiler.watch({
    ignored: /node_modules/ }, (err, stats) => {
   
    nodemonRestart()
    if (err) {
   
      throw err
    }
    // ...
  })

  try {
   
    // 等待编译完成
    await clientCompilerPromise
    await serverCompilerPromise
    // 这是admin编译情况,admin端的编译情况差不太多,基本也是运行`webpack(config)`进行编译,通过`createService`生成一个服务用来访问打包的代码。
    await startAdmin()

    closeCompiler(clientCompiler)
    closeCompiler(serverCompiler)
    logMsg(`Build time ${
     new Date().getTime() - startTime}`)
  } catch (err) {
   
    logMsg(err, 'error')
  }

  // 启动server端编译出来的入口文件来启动项目服务
  script = nodemon({
   
    script: path.join(serverWebpackConfig.output.path, 'entry.js')
  })
}
start()

createService方法用来生成服务, 代码大概如下

export const createService = ({
   webpackConfig, compiler}: {
   webpackConfig: Configurationcompiler: Compiler}) => {
   
  const app = new Koa()
  ...
  const dev = webpackDevMiddleware(compiler, {
   
    publicPath: webpackConfig.output.publicPath as string,
    stats: webpackConfig.stats
  })
  app.use(dev)
  app.use(webpackHotMiddleware(compiler))
  http.createServer(app.callback()).listen(port, cb)
  return app
}

开发(development)环境下的webpack编译情况的大体逻辑就是这样,里面会有些webpack-dev-middle这些中间件在koa中的处理等,这里我只提供了大体思路,可以具体细看源码。

3. 生成环境production环境的搭建

对于生成环境的下搭建,处理就比较少了,直接通过webpack打包就行

webpack([clientWebpackConfig, serverWebpackConfig, adminWebpackConfig], (err, stats) => {
   
    spinner.stop()
    if (err) {
   
      throw err
    }
    // ...
  })

然后启动打包出来的入口文件 cross-env NODE_ENV=production node dist/server/entry.js

这块主要就是webpack的配置,这些配置文件可以直接点击这里进行查看

Server端源码解析

由上面的配置webpack配置延伸到他们的入口文件

// client入口
const clientPath = utils.resolve('src/client')
const clientEntryPath = path.join(clientPath, 'main.tsx')
// server入口
const serverPath = utils.resolve('src/server')
const serverEntryPath = path.join(serverPath, 'main.ts')
  • client端的入口是/src/client/main.tsx
  • server端的入口是/src/server/main.ts

因为项目用到了SSR,我们从server端来进行逐步分析。

1. /src/server/main.ts入口文件

import Koa from 'koa'
...
const app = new Koa()
/* 
  中间件:
    sendMidddleware: 对ctx.body的封装
    etagMiddleware:设置etag做缓存 可以参考koa-etag,我做了下简单修改,
    conditionalMiddleware: 判断缓存是否是否生效,通过ctx.fresh来判断就好,koa内部已经封装好了
    loggerMiddleware: 用来打印日志
    authTokenMiddleware: 权限拦截,这是admin端对api做的拦截处理
    routerErrorMiddleware:这是对api进行的错误处理
    koa-static: 对于静态文件的处理,设置max-age让文件强缓,配置etag或Last-Modified给资源设置强缓跟协商缓存
    ...
*/
middleware(app)
/* 
  对api进行管理
*/
router(app)
/* 
  启动数据库,搭建SSR配置
*/
Promise.all([startMongodb(), SSR(app)])
  .then(() => {
   
    // 开启服务
    https.createServer(serverConfig.httpsOptions, app.callback()).listen(rootConfig.app.server.httpsPort, '0.0.0.0')
  })
  .catch((err) => {
   
    process.exit()
  })

2.中间件的处理

对于中间件主要就讲一讲日志处理中间件loggerMiddleware和权限中间件authTokenMiddleware,别的中间件没有太多东西,就不浪费篇幅介绍了。

日志打印主要用到了log4js这个库,然后基于这个库做的上层封装,通过不同类型的Logger来创建不同的日志文件。
封装了所有请求的日志打印,api的日志打印,一些第三方的调用的日志打印

1. loggerMiddleware的实现
// log.ts
const createLogger = (options = {
   } as LogOptions): Logger => {
   
  // 配置项
  const opts = {
   
    ...serverConfig.log,
    ...options
  }
  // 配置文件
  log4js.configure({
   
    appenders: {
   
      // stout可以用于开发环境,直接打印出来
      stdout: {
   
        type: 'stdout'
      },
      // 用multiFile类型,通过变量生成不同的文件,我试了别的几种type。感觉都没这种方便
      multi: {
    type: 'multiFile', base: opts.dir, property: 'dir', extension: '.log' }
    },
    categories: {
   
      default: {
    appenders: ['stdout'], level: 'off' },
      http: {
    appenders: ['multi'], level: opts.logLevel },
      api: {
    appenders: ['multi'], level: opts.logLevel },
      external: {
    appenders: ['multi'], level: opts.logLevel }
    }
  })
  const create = (appender: string) => {
   
    const methods: LogLevel[] = ['trace', 'debug', 'info', 'warn', 'error', 'fatal', 'mark']
    const context = {
   } as LoggerContext
    const logger = log4js.getLogger(appender)
    // 重写log4js方法,生成变量,用来生成不同的文件
    methods.forEach((method) => {
   
      context[method] = (message: string) => {
   
        logger.addContext('dir', `/${
     appender}/${
     method}/${
     dayjs().format('YYYY-MM-DD')}`)
        logger[method](message)
      }
    })
    return context
  }
  return {
   
    http: create('http'),
    api: create('api'),
    external: create('external')
  }
}
export default createLogger


// loggerMiddleware
import createLogger, {
    LogOptions } from '@server/utils/log'
// 所有请求打印
const loggerMiddleware = (options = {
   } as LogOptions) => {
   
  const logger = createLogger(options)
  return async (ctx: Koa.Context, next: Next) => {
   
    const start = Date.now()
    ctx.log = logger
    try {
   
      await next()
      const end = Date.now() - start
      // 正常请求日志打印
      logger.http.info(
        logInfo(ctx, {
   
          responseTime: `${
     end}ms`
        })
      )
    } catch (e) {
   
      const message = ErrorUtils.getErrorMsg(e)
      const end = Date.now() - start
      // 错误请求日志打印
      logger
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wintermelon__zzz

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值