《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

sum墨
• 阅读 37

⭐️基础链接导航⭐️

服务器 → ☁️ 阿里云活动地址

看样例 → 🐟 摸鱼小网站地址

学代码 → 💻 源码库地址

一、前言

大家好呀,我是summo,最近发生了些事情(被裁员了,在找工作中)导致断更了,非常抱歉。刚被裁的时候还是有些难受,而且我还有房贷要还,有些压力,不过休息了一段时间,心态也平复了一些,打算一边找工作一边写文,如果有和我一样经历的同学,大家共勉!

《花100块做个摸鱼小网站! 》这个系列的前六篇已经大概把整体的流程写完了,从这篇起我会补充一些细节和组件,让我们的小网站更加丰富一些。这一篇呢我会介绍如何将用户的访问记录留下来,看着自己做的网站被别人访问是一件很有意思和很有成就感的事情。

对应的组件也就是我用红框标出来的那个,如下图: 《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

解释下PV和UV的意思,如下:

  • PV:页面访问量,即PageView,用户每次对网站的访问均被记录,用户对同一页面的多次访问,访问量累计。
  • UV:独立访问用户数:即UniqueVisitor,访问网站的一台电脑客户端为一个访客。

二、用户身份标识

用于表明一个用户身份最好的做法是做登录注册,但是一旦加了这样的逻辑,就会有很多麻烦的问题要处理,比如如何做人机验证啦、接口防刷啦,等等,这些问题不处理的话网站很容易被攻击。像我们这样的小网站,我觉得这个功能没必要,我们只需要知道有多少人访问过我们的网站就可以了。

对于这样的需求,最简单的做法是根据用户的访问IP作为标识,然后根据IP解析一下地域信息,这样就已经很不错了。而目前最常用的IP解析工具就是:ip2region,如何使用这个组件我之前写过一篇文章进行介绍了,文章链接:SpringBoot整合ip2region实现使用ip监控用户访问城市

核心代码是这段:

package com.example.springbootip.util;


import org.apache.commons.io.FileUtils;
import org.lionsoul.ip2region.xdb.Searcher;

import java.io.File;
import java.text.MessageFormat;
import java.util.Objects;

public class AddressUtil {

    /**
     * 当前记录地址的本地DB
     */
    private static final String TEMP_FILE_DIR = "/home/admin/app/";

    /**
     * 根据IP地址查询登录来源
     *
     * @param ip
     * @return
     */
    public static String getCityInfo(String ip) {
        try {
            // 获取当前记录地址位置的文件
            String dbPath = Objects.requireNonNull(AddressUtil.class.getResource("/ip2region/ip2region.xdb")).getPath();
            File file = new File(dbPath);
            //如果当前文件不存在,则从缓存中复制一份
            if (!file.exists()) {
                dbPath =    TEMP_FILE_DIR + "ip.db";
                System.out.println(MessageFormat.format("当前目录为:[{0}]", dbPath));
                file = new File(dbPath);
                FileUtils.copyInputStreamToFile(Objects.requireNonNull(AddressUtil.class.getClassLoader().getResourceAsStream("classpath:ip2region/ip2region.xdb")), file);
            }
            //创建查询对象
            Searcher searcher = Searcher.newWithFileOnly(dbPath);
            //开始查询
            return searcher.searchByStr(ip);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //默认返回空字符串
        return "";
    }

    public static void main(String[] args) {
        System.out.println(getCityInfo("1.2.3.4"));
    }
}

三、功能实现

为了解耦逻辑,我使用了一个注解:@VisitLog,只要将该注解放在IndexController的index方法上即可。同时为了统计用户的访问数据,我们需要设计一张访问记录表将数据存下来,并设计一个小组件用来展示这些数据,具体流程如下文。

1. 后端部分

(1)访问记录表设计

建表语句

-- `summo-sbmy`.t_sbmy_visit_log definition

CREATE TABLE `t_sbmy_visit_log` (
  `id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理主键',
  `device_type` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '设备类型,手机还是电脑',
  `ip` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '访问',
  `address` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT 'IP地址',
  `time` int DEFAULT NULL COMMENT '耗时',
  `method` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '调用方法',
  `params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '参数',
  `gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
  `gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
  `creator_id` bigint DEFAULT NULL COMMENT '创建人',
  `modifier_id` bigint DEFAULT NULL COMMENT '更新人',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=oDEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

DO、Mapper、Repository等文件

还记得我在第三篇介绍的那个DO生成插件吗,在config.properties改下表名和DO名,双击mybatis-generator:generate就可以生成对应的DO、Mapper、xml了。

《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

(2)VisitLog注解

VisitLog.java

package com.summo.sbmy.aspect;

/**
 * 访问标识注解
 */
public @interface VisitLog {
}

VisitLogAspect.java

package com.summo.sbmy.aspect.visit;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.summo.sbmy.common.util.AddressUtil;
import com.summo.sbmy.common.util.HttpContextUtil;
import com.summo.sbmy.common.util.IpUtil;
import com.summo.sbmy.dao.entity.SbmyVisitLogDO;
import com.summo.sbmy.dao.repository.SbmyVisitLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import static com.summo.sbmy.common.util.DeviceUtil.isFromMobile;


@Slf4j
@Aspect
@Component
public class VisitLogAspect {

    @Autowired
    private SbmyVisitLogRepository sbmyVisitLogRepository;

    @Pointcut("@annotation(com.summo.sbmy.aspect.visit.Log)")
    public void pointcut() {
        // do nothing
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取request
        HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
        // 请求的类名
        MethodSignature signature = (MethodSignature)joinPoint.getSignature();
        Method method = signature.getMethod();
        String className = joinPoint.getTarget().getClass().getName();
        // 请求的方法名
        String methodName = signature.getName();
        String ip = IpUtil.getIpAddr(request);
        String address = AddressUtil.getAddress(ip);
        SbmyVisitLogDO sbmyVisitLogDO = SbmyVisitLogDO.builder().deviceType(isFromMobile(request) ? "手机" : "电脑").method(
            className + "." + methodName + "()").ip(ip).address(AddressUtil.getAddress(address)).build();
        // 请求的方法参数值
        Object[] args = joinPoint.getArgs();
        // 请求的方法参数名称
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paramNames = u.getParameterNames(method);
        if (args != null && paramNames != null) {
            // 创建 key-value 映射用于生成 JSON 字符串
            Map<String, Object> paramMap = new LinkedHashMap<>();
            for (int i = 0; i < paramNames.length; i++) {
                if (args[i] instanceof HttpServletRequest || args[i] instanceof HttpServletResponse) {
                    continue;
                }
                paramMap.put(paramNames[i], args[i]);
            }
            // 使用 Fastjson 将参数映射转换为 JSON 字符串
            String paramsJson = JSON.toJSONString(paramMap);
            sbmyVisitLogDO.setParams(paramsJson);
        }
        long beginTime = System.currentTimeMillis();
        Object proceed = joinPoint.proceed();
        long end = System.currentTimeMillis();
        sbmyVisitLogDO.setTime((int)(end - beginTime));
        sbmyVisitLogRepository.save(sbmyVisitLogDO);
        return proceed;
    }

    /**
     * 参数构造器¬
     *
     * @param params
     * @param args
     * @param paramNames
     * @return
     * @throws JsonProcessingException
     */
    private StringBuilder handleParams(StringBuilder params, Object[] args, List paramNames)
        throws JsonProcessingException {
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof Map) {
                Set set = ((Map)args[i]).keySet();
                List<Object> list = new ArrayList<>();
                List<Object> paramList = new ArrayList<>();
                for (Object key : set) {
                    list.add(((Map)args[i]).get(key));
                    paramList.add(key);
                }
                return handleParams(params, list.toArray(), paramList);
            } else {
                if (args[i] instanceof Serializable) {
                    Class<?> aClass = args[i].getClass();
                    try {
                        aClass.getDeclaredMethod("toString", new Class[] {null});
                        // 如果不抛出 NoSuchMethodException 异常则存在 toString 方法 ,安全的 writeValueAsString ,否则 走 Object的
                        // toString方法
                        params.append(" ").append(paramNames.get(i)).append(": ").append(
                            JSONObject.toJSONString(args[i]));
                    } catch (NoSuchMethodException e) {
                        params.append(" ").append(paramNames.get(i)).append(": ").append(
                            JSONObject.toJSONString(args[i].toString()));
                    }
                } else if (args[i] instanceof MultipartFile) {
                    MultipartFile file = (MultipartFile)args[i];
                    params.append(" ").append(paramNames.get(i)).append(": ").append(file.getName());
                } else {
                    params.append(" ").append(paramNames.get(i)).append(": ").append(args[i]);
                }
            }
        }
        return params;
    }
}

这里使用到了一些工具类,代码我已经上传到仓库了,大家直接down下来就行。

(3)注解使用

在IndexController.java中加入该注解即可

package com.summo.sbmy.web.controller;

import com.summo.sbmy.aspect.visit.VisitLog;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    @VisitLog
    public String index(){
        return "index";
    }
}

2. 前端部分

(1)新建VisitorLog组件

代码如下:

<template>
  <div class="stats-card-container">
    <el-card class="stats-card-main">
      <!-- 突出显示的今日 PV -->
      <div class="stats-section">
        <div class="stats-value-main">{{ statsData.todayPv }}</div>
        <div class="stats-label-main">今日 PV</div>
      </div>
      <!-- 其他统计数据,以更紧凑的形式显示 -->
      <div class="stats-section stats-others">
        <div class="stats-item">
          <div class="stats-value-small">{{ statsData.todayUv }}</div>
          <div class="stats-label-small">今日 UV</div>
        </div>
        <div class="stats-item">
          <div class="stats-value-small">{{ statsData.allPv }}</div>
          <div class="stats-label-small">总 PV</div>
        </div>
        <div class="stats-item">
          <div class="stats-value-small">{{ statsData.allUv }}</div>
          <div class="stats-label-small">总 UV</div>
        </div>
      </div>
    </el-card>
  </div>
</template>
<script>
import apiService from "@/config/apiService.js";
export default {
  name: "VisitorLog",
  data() {
    return {
      statsData: {
        todayPv: 0,
        todayUv: 0,
        allPv: 0,
        allUv: 0,
      },
    };
  },
  created() {
    this.fetchVisitorCount(); // 组件创建时立即调用一次
    this.startPolling(); // 启动定时器
  },
  beforeDestroy() {
    this.stopPolling(); // 在组件销毁前清理定时器
  },
  methods: {
    fetchVisitorCount() {
      apiService
        .get("/welcome/queryVisitorCount")
        .then((res) => {
          // 处理响应数据
          this.statsData = res.data.data;
        })
        .catch((error) => {
          // 处理错误情况
          console.error(error);
        });
    },
    startPolling() {
      // 定义一个方法来启动周期性的定时器
      this.polling = setInterval(() => {
        this.fetchVisitorCount();
      }, 1000 * 60 * 60); // 每60000毫秒(1分钟)调用一次
    },
    stopPolling() {
      // 定义一个方法来停止定时器
      clearInterval(this.polling);
    },
  },
};
</script>

<style scoped>
>>> .el-card__body{
  padding: 10px;
}
.stats-card-container {
  max-width: 400px;
  margin-bottom: 10px;
}

.stats-card-main {
  padding: 12px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.stats-section {
  text-align: center;
}

.stats-value-main {
  font-size: 24px;
  font-weight: bold;
  color: #0A74DA;
  margin-bottom: 4px;
}

.stats-label-main {
  font-size: 14px;
  color: #333;
}

.stats-others {
  display: flex;
  justify-content: space-between;
  margin-top: 12px;
}

.stats-item {
  text-align: center;
}

.stats-value-small, .stats-label-small {
  font-size: 12px; /* 减小字体尺寸以实现更紧凑的布局 */
}

.stats-value-small {
  font-weight: bold;
  color: #333;
  margin-bottom: 2px;
}

.stats-label-small {
  color: #666;
}

@media (max-width: 400px) {
  .stats-others {
    flex-direction: column;
    align-items: center;
  }

  .stats-item {
    margin-bottom: 8px;
  }
}
</style>

(2)组件使用

在App.vue组件中引入VisitorLog组件,顺便将布局重新分一下,代码如下:

<template>
  <div id="app">
    <el-row :gutter="10">
      <el-col :span="20">
        <el-row :gutter="10">
          <el-col :span="6" v-for="(board, index) in hotBoards" :key="index">
            <hot-search-board
              :title="board.title"
              :icon="board.icon"
              :fetch-url="board.fetchUrl"
              :type="board.type"
            />
          </el-col>
        </el-row>
      </el-col>
      <el-col :span="4">
        <visitor-log />
      </el-col>
    </el-row>
  </div>
</template>

<script>
import HotSearchBoard from "@/components/HotSearchBoard.vue";
import VisitorLog from "@/components/VisitorLog.vue";
export default {
  name: "App",
  components: {
    HotSearchBoard,
    VisitorLog,
  },
  data() {
    return {
      hotBoards: [
        {
          title: "百度",
          icon: require("@/assets/icons/baidu-icon.svg"),
          type: "baidu",
        },
        {
          title: "抖音",
          icon: require("@/assets/icons/douyin-icon.svg"),
          type: "douyin",
        },
        {
          title: "知乎",
          icon: require("@/assets/icons/zhihu-icon.svg"),
          type: "zhihu",
        },
        {
          title: "B站",
          icon: require("@/assets/icons/bilibili-icon.svg"),
          type: "bilibili",
        },
        {
          title: "搜狗",
          icon: require("@/assets/icons/sougou-icon.svg"),
          type: "sougou",
        },
      ],
    };
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
  background: #f8f9fa; /* 提供一个柔和的背景色 */
  min-height: 100vh; /* 使用视口高度确保填满整个屏幕 */
  padding: 0; /* 保持整体布局紧凑,无额外内边距 */
}
</style>

咱们做出来的效果就是这样的,如下: 《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

这个小组件做起来还是简单的,主要就是监控了别人的访问IP,然后通过IP反解析出所属地域,最后将其存到数据库中。

番外:搜狗热搜爬虫

1. 爬虫方案评估

搜狗的热搜接口返回的一串JSON格式数据,这就很简单了,省的我们去解析dom,访问链接是:https://go.ie.sogou.com/hot_ranks 《花100块做个摸鱼小网站! 》第七篇—谁访问了我们的网站?

2. 网页解析代码

SougouHotSearchJob.java

package com.summo.sbmy.job.sougou;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.summo.sbmy.common.model.dto.HotSearchDetailDTO;
import com.summo.sbmy.dao.entity.SbmyHotSearchDO;
import com.summo.sbmy.service.SbmyHotSearchService;
import com.summo.sbmy.service.convert.HotSearchConvert;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Calendar;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;

import static com.summo.sbmy.common.cache.SbmyHotSearchCache.CACHE_MAP;
import static com.summo.sbmy.common.enums.HotSearchEnum.DOUYIN;
import static com.summo.sbmy.common.enums.HotSearchEnum.SOUGOU;

/**
 * @author summo
 * @version SougouHotSearchJob.java, 1.0.0
 * @description 搜狗热搜Java爬虫代码
 * @date 2024年08月09
 */
@Component
@Slf4j
public class SougouHotSearchJob {

    @Autowired
    private SbmyHotSearchService sbmyHotSearchService;

    @XxlJob("sougouHotSearchJob")
    public ReturnT<String> hotSearch(String param) throws IOException {
        log.info("搜狗热搜爬虫任务开始");
        try {
            //查询搜狗热搜数据
            OkHttpClient client = new OkHttpClient().newBuilder().build();
            Request request = new Request.Builder().url("https://go.ie.sogou.com/hot_ranks").method("GET", null)
                    .build();
            Response response = client.newCall(request).execute();
            JSONObject jsonObject = JSONObject.parseObject(response.body().string());
            JSONArray array = jsonObject.getJSONArray("data");
            List<SbmyHotSearchDO> sbmyHotSearchDOList = Lists.newArrayList();
            for (int i = 0, len = array.size(); i < len; i++) {
                //获取知乎热搜信息
                JSONObject object = (JSONObject)array.get(i);
                //构建热搜信息榜
                SbmyHotSearchDO sbmyHotSearchDO = SbmyHotSearchDO.builder().hotSearchResource(SOUGOU.getCode()).build();
                //设置知乎三方ID
                sbmyHotSearchDO.setHotSearchId(object.getString("id"));
                //设置文章标题
                sbmyHotSearchDO.setHotSearchTitle(object.getJSONObject("attributes").getString("title"));
                //设置文章连接
                sbmyHotSearchDO.setHotSearchUrl(
                        "https://www.sogou.com/web?ie=utf8&query=" + sbmyHotSearchDO.getHotSearchTitle());
                //设置热搜热度
                sbmyHotSearchDO.setHotSearchHeat(object.getJSONObject("attributes").getString("num"));
                //按顺序排名
                sbmyHotSearchDO.setHotSearchOrder(i + 1);
                sbmyHotSearchDOList.add(sbmyHotSearchDO);
            }
            if (CollectionUtils.isEmpty(sbmyHotSearchDOList)) {
                return ReturnT.SUCCESS;
            }
            //数据加到缓存中
            CACHE_MAP.put(SOUGOU.getCode(), HotSearchDetailDTO.builder()
                //热搜数据
                .hotSearchDTOList(
                    sbmyHotSearchDOList.stream().map(HotSearchConvert::toDTOWhenQuery).collect(Collectors.toList()))
                //更新时间
                .updateTime(Calendar.getInstance().getTime()).build());
            //数据持久化
            sbmyHotSearchService.saveCache2DB(sbmyHotSearchDOList);
            log.info("搜狗热搜爬虫任务结束");
        } catch (IOException e) {
            log.error("获取搜狗数据异常", e);
        }
        return ReturnT.SUCCESS;
    }
}
点赞
收藏
评论区
推荐文章
Stella981 Stella981
3年前
Shodan的http.favicon.hash语法详解与使用技巧
  在Shodan搜索中有一个关于网站icon图标的搜索语法,http.favicon.hash,我们可以使用这个语法来搜索出使用了同一icon图标的网站,不知道怎么用的朋友请参考我上一篇(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.cnblogs.com%2Fmia
Stella981 Stella981
3年前
Color Hunt 漂亮炫酷的配色小程序
利用自己的业余时间,开发了一款颜色配色方案的小程序ColorHunt。小程序主要参考ColorHunt(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fcolorhunt.co%2F)这个网站,这个网站设计真的很棒,个人也经常用,所以小程序也秉承网站的风格,没有做调整,
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第二篇—后端应用搭建和完成第一个爬虫
大家好呀,我是summo,前面已经教会大家怎么去阿里云买服务器(链接在这,需要自取),以及怎么搭建JDK、Redis、MySQL这些环境。从这篇文章开始就进入正式的编码阶段了,我们从后端开始,先把热搜数据获取到,然后再开始前端部分。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第三篇—热搜表结构设计和热搜数据存储
大家好呀,我是summo,第一篇已经教会大家怎么去阿里云买服务器,以及怎么搭建JDK、Redis、MySQL这些环境。第二篇我们把后端的应用搭建好了,并且完成了第一个爬虫(抖音)。那么这一篇我会教大家如何将爬取到的数据保存到数据库,并且可以通过接口获取到,为后面的前端界面提供数据源。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第四篇—前端应用搭建和完成第一个热搜组件
在本系列文章的早期章节中,我们已经成功地购买了服务器并配置了MySQL、Redis等核心中间件。紧接着,我们不仅建立了后端服务,还开发了我们的首个爬虫程序。后面我们还把爬取到的数据进行了保存,生成了一整套MVC的后端代码,并且提供了一个接口出来。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第五篇—通过xxl-job定时获取热搜数据
我们已经成功实现了一个完整的热搜组件,从后端到前端,构建了这个小网站的核心功能。接下来,我们将不断完善其功能,使其更加美观和实用。今天的主题是如何定时获取热搜数据。如果热搜数据无法定时更新,小网站将失去其核心价值。之前,我采用了@Scheduled注解来实现定时任务,但这种方式灵活性不足,因此我决定用更灵活的XXLJob组件来替代它。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第六篇—将小网站部署到云服务器上
到这一篇我们终于把环境搭好,也做好了几个热搜小组件,为了让我们方便展示成果或者方便自己摸鱼,我们需要将这个小网站部署上云。整体流程并不复杂,但有很多个小细节。如果某个细节处理不当,可能会导致部署失败,因此这是一个不断尝试和调整的过程。基本流程包括:修改配置、打包、上传、运行和调试,然后反复进行,直到成功。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! · 序》灵感来源
大家好呀,我是summo,这次来写写我在上班空闲(摸鱼)的时候做的一个小网站的事。去年阿里云不是推出了个活动嘛,2核2G的云服务器一年只要99块钱,懂行的人应该知道这个价格在业界已经是非常良心了,虽然优惠只有一年,但是买一台用来学习还是非常合适的。
sum墨 sum墨
1星期前
《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境
大家好呀,我是summo,前面我已经写了我为啥要做这个摸鱼小网站的原因,从这篇文章开始我会一步步跟大家聊聊我是怎么搭起这个网站的。我知道对很多新手来说,建网站可能挺头大的,不知道从哪里开始,所以我会尽量写得简单明了,让大家一看就懂,少走弯路。