使用 JSP+Servlet 模仿京東頁面實現購物車功能

可能很多人看到標題都不會點進來,因爲 JSP 這種老掉牙的技術很多人根本不學,所以我有些感想寫在下面。

這是在學校選課老師讓做的實驗報告,可能大家會覺得這些東西毫無意義,因爲 JSP 早就沒人使用了,原因是因爲寫頁面太繁瑣,執行速度慢,消耗內存,響應速度慢不能處理高併發等原因;但是我想覺得不能因爲他現在被淘汰了就不去學他,更不能抱着輕蔑的態度去學習這門技術,我自己在學習的過程中一直在驚歎 JSP 太強了,真的簡化了很多後臺開發,還有那些標籤技術,真的是獨具匠心,發明出這項技術的人真的非常厲害。現在越來越多的 Java 開發相關人員上來直接學習 SpringBoot 等框架,然後快速開發出一個網頁,看起來很厲害,但這是不對的,也是錯誤的,我也學過 Spring 和 SpringBoot 等流行框架,但是現在還是在老老實實的跟着學校的進度學習 JSP,因爲我覺得經典的東西一定有學習的價值,只有搞懂基礎才能提高境界,這些高大上的框架確實極大的簡化了我們的開發,但是有沒有想過,如果你一直學習這些別人封裝好的框架,其實你根本沒有一點核心競爭力,這些東西你會,別人一樣也可以學習,而且也是速成,所以我們要掌握基礎,學習底層,這樣就算別人想超過你也要付出很長時間,久而久之你就有你的核心競爭力了,所以,不要看不起任何一項技術,每一項技術都有他存在的意義。其實仔細想想,我們到底會什麼?全都是用的別人封裝好的框架,我們只會調用接口,我們寫 Java 程序調用 JDK 的接口,然後 Servlet 封裝了 JDK,接着 Spring 封裝了 Servlet ,簡化我們的開發,後來 SpringBoot 進而封裝了 Spring 框架,讓我們開發網頁觸手可得······我相信將來還會有一層一層的封裝,到最後我們寫網頁可能是幾行代碼就搞定了,那個時候可能有的 Java 程序員看似寫了一個網頁,他可能都不知道 JDK 是什麼(誇張的比喻一下),因爲封裝太多太多層了。換個視角,我甚至覺得人類本來就是調用接口生活,比如我們想吃飯,不想自己做,怎麼辦?好辦,調用美團外賣的接口,傳入參數20元,返回一份蓋澆飯。說了那麼多,就想表明一個觀點,就是調用接口雖然方便,但是誰都會調,請問你的核心競爭力是什麼?當然我也是個巨菜,沒有核心競爭力可言,目前在閱讀 JDK 源碼和學習算法,感興趣的朋友可以一起閱讀 源碼 和算法 交流

下面開始正文。


實驗一 Servlet基礎操作

先來看一下最終效果:
在這裏插入圖片描述

一、基礎功能

1、項目結構

首先來看一下項目的整體結構:
image.png

2、數據初始化

首先是數據的初始化,這裏爲了使 Servlet 容器能在一開始就加載數據,我選擇在註解中進行了如下配置:

@WebServlet(name = "ShoppingCartServlet", 
        urlPatterns = {
            "/shop/products",
            "/shop/details",
            "/shop/addCart",
            "/shop/deleteItem",
            "/shop/clearCart"}, 
        loadOnStartup = 1)

其中 urlPatterns 爲匹配的路徑。
這樣在一開始就可以加載在 init 方法中的數據了。

/**
     * 數據的初始化
     *
     * @param config 配置參數,可以獲取應用作用域
     * @throws ServletException 拋出異常
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        // 裝載數據
        Product p1 = new Product
                (1, "單反相機", "最沒有性價比的單反相機", 3306f);
        Product p2 = new Product
                (2, "雙反相機", "最不值的雙反相機", 3307f);
        Product p3 = new Product
                (3, "三反相機", "最難看的三反相機", 3308f);
        Product p4 = new Product
                (4, "四反相機", "最花裏胡哨的四反相機", 3309f);
        List<Product> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        list.add(p4);
        // 保存到應用作用域中
        config
                .getServletContext()
                .setAttribute("products", list);
    }

因爲這些數據是所有的應用都會用到的,所以把它放在 context上下文域中,代替了從數據庫查詢數據。

3、主頁

然後我們訪問頁面,會自動跳轉到 productList頁面,來看一下 index頁面的代碼:

<body>
<c:redirect url="shop/productList.jsp"/>
</body>

就只有一行代碼,是一個重定向。

訪問之後的頁面如圖:
image.png
我們可以選擇喜歡的商品,也可以查看購物車。

4、商品詳情

這裏我們先點進去商品詳情:
image.png
請求地址爲:

http://localhost:8080/WsShoppingCart/shop/details?id=1

這裏拼接了一個 id ,我們可以在 Servlet中取出來,裏看代碼:

首先是根據請求路徑,我們把不同的請求交給不同的方法來處理:

/**
     * 根據請求參數後綴來分別處理請求
     *
     * @param request 請求
     * @param response 響應
     * @throws ServletException Servlet異常
     * @throws IOException IO異常
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String uri = request.getRequestURI();
        if (uri.endsWith("products")) displayProducts(request, response);
        else if (uri.endsWith("details")) displayGoods(request, response);
        else if (uri.endsWith("addCart")) addCart(request, response);
        else if (uri.endsWith("deleteItem")) deleteCard(request, response);
        else if (uri.endsWith("clearCart")) clearCart(request, response);
    }

比如我們這次請求就會轉發到 displayGoods方法中去處理,下面來看一下該方法:

/**
     * 根據 ID 查詢用戶點擊的是哪一個商品,然後跳轉到商品詳情頁面
     * 響應請求: /shop/details
     *
     * 注意: 只接受同級目錄下的頁面請求,所以 ./ 或者不寫都可以
     *
     * @param request 請求
     * @param response 響應
     * @throws ServletException Servlet 異常
     * @throws IOException IO 異常
     */
    private void displayGoods(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Integer id = Integer.valueOf(request.getParameter("id"));
        List<Product> products = (List<Product>) getServletContext()
                .getAttribute("products");
        Product product = findById(id, products);
        if (product != null) request.setAttribute("product", product);
        request.getRequestDispatcher("./productDetails.jsp")
                .forward(request, response);
    }

我們首先從請求路徑中獲取到了請求參數 id,然後從 context中獲取到了之前存進去的商品,這裏調用了一個 findById 方法,來看一下這個方法:

/**
     * 根據 id 查詢相關內容是否在作用域中
     *
     */
    private Product findById(Integer id, List<Product> products) {
        for (Product product : products) {
            if (id.equals(product.getId())) {
                return product;
            }
        }
        return null;
    }

它的作用就是根據 id 查詢商品,如果查到了就返回,否則返回爲空。

這樣我們獲取這個對象之後就再把它存放到請求作用域中,然後將請求轉發到 productDetails 頁面。也就是之前我們看到的頁面。

5、添加商品到購物車

然後我們可以在文本框中輸入加入購物車的商品的數量:

image.png
如果我們點擊按鈕,他會發送一個請求,我們使用這個方法來處理這個請求:

/**
     * 添加到購物車
     * 響應請求: /shop/addCart
     *
     * @param request 請求
     * @param response 響應
     * @throws IOException 異常
     */
    private void addCart(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ServletContext context = getServletContext();
        HttpSession session = request.getSession();
        Integer id = Integer.parseInt(request.getParameter("id"));
        Integer quality = Integer.parseInt(request.getParameter("quality"));
        List<Product> products = (List<Product>) context
                .getAttribute("products");
        // 初始化購物車
        Map<Product, Integer> cart = (Map<Product, Integer>) session
                .getAttribute("cart");
        if (cart == null) {
            cart = new HashMap<>();
        }
        Product product = findById(id, products);
        if (product != null) {
            cart.put(product, quality);
            session.setAttribute("cart", cart);
        }
        displayProducts(request, response);
    }

那他會發送什麼請求呢?

<form action="addCart" method="post">
    <input type="hidden" name="id" value="${requestScope.product.id}">

我們可以查看 form 表單,而且我們加了一個隱藏域,這樣在 Servlet 中纔可以獲取商品的 id 然後放入 session 域中。並且重定向到 productList 頁面中。

image.png
我們可以點擊查看購物車:
image.png
那麼 cart 頁面是怎麼獲取數據的呢?

這裏用到了 jstl 的標籤庫以及 el 表達式:

<c:forEach items="${sessionScope.cart}" var="cart">
                <tr>
                    <td>📌${cart.value}</td>
                    <td>${cart.key.name}</td>
                    <td><span style="color: darkorange">${cart.key.price}</span>💰</td>
                    <td><span style="color: darkorange">${cart.key.price * cart.value}</span>💰</td>
                    <c:if test="${sessionScope.cart.size() >= 2}">
                        <td>
<%--                            <a href="deleteItem?id=${cart.key.id}">刪除💨</a>--%>
                            <input type="button" value="刪除" onclick="confirmDel(${cart.key.id})">
                        </td>
                    </c:if>
                </tr>
                <c:set var="total" value="${cart.value * cart.key.price + total}"/>
                <c:set var="sum" value="${sum + cart.value}"/>
            </c:forEach>

這樣就可以輸出到頁面上了,而且我們還可以刪除商品。

6、從購物車中刪除商品

從購物車中刪除商品需要 cart 頁面發送一個請求,然後在 Servlet 頁面中處理請求。

/**
     * 刪除商品
     * /shop/deleteItem
     *
     * @param request 請求
     * @param response 響應
     */
    private void deleteCard(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Integer id = Integer.parseInt(request.getParameter("id"));
        ServletContext context = getServletContext();
        HttpSession session = request.getSession();
        List<Product> products = (List<Product>) context
                .getAttribute("products");
        Map<Product, Integer> cart = (HashMap) session
                .getAttribute("cart");

        Product product = findById(id, products);
        if (product != null) {
            cart.remove(product);
            session.setAttribute("cart", cart);
        }
        response.sendRedirect("./cart.jsp");
    }

我們從 session 域中獲取對象之後再刪除該對象,因爲它本身是一個 Map 集合,最後重新存到 session 域中,然後重定向到他自己實現刷新效果。

刪除之後:
image.png

7、總體的流程演示

下面通過 GIF 演示一下:

2020_04_20.gif

二、擴展功能

下面實現拓展功能:

1、清空購物車

cart 頁面:

<a href="./clearCart">清空購物車❗</a>

Servlet 中的代碼:

/**
     * 清空購物車
     * 響應請求: /shop/clearCart
     *
     * @param request
     * @param response
     */
    private void clearCart(HttpServletRequest request, HttpServletResponse response) throws IOException {
        request.getSession().removeAttribute("cart");
        response.sendRedirect("./cart.jsp");
    }

就是從會話作用域中移除掉所有數據。

2、顯示購物車中的商品種類數量和商品總數量

我們先在 forEach 循環中設置一個值 sum 和 total,用於記錄商品數量與總數量。

	<c:set var="total" value="${cart.value * cart.key.price + total}"/>
	<c:set var="sum" value="${sum + cart.value}"/>
</c:forEach>


<tr>
                <td>✍總計 <span style="color: darkorange">${total}</span>💰</td>
                <td>總數量 ${sum}</td>
                <td></td>
                <td></td>
                <td><a href="./clearCart">清空購物車❗</a></td>
</tr>

image.png

3、購物車爲空的提示

使用 jstl 實現,使用相關標籤:

<c:choose>
    <c:when test="${sessionScope.cart != null}">
	</c:when>
    	<c:otherwise>
    	    <h2>購物車中沒有商品 🙁</h2>
	</c:otherwise
</c:choose>

這樣一旦購物車爲空就會進入 otherwise 語句。

image.png

4、刪除時提示是否確認刪認

使用 JavaScript 實現:

<script>
        function confirmDel(param) {
            if (window.confirm("您確定要刪除這件美麗的商品嗎?")) {
                document.location = "deleteItem?id=" + param;
            }
        }
</script>

<input type="button" value="刪除" onclick="confirmDel(${cart.key.id})">

image.png

5、在輸入數量時,如果不是數字,要提示

使用 JavaScript 的正則表達式實現:

<script>
        test = function () {
            var quality = document.getElementById("quality");
            var reg = /^[0-9]*$/g;
            if (!reg.test(quality.value)) {
                alert("請輸入數字");
            }
        };
</script>

<input type="text" name="quality" id="quality" onkeyup="test()">

這樣就可以了。

image.png

三、關於數據源

由於我們沒有使用數據庫,所以自己造了數據,但是不太真實也很麻煩,所以我後來使用爬蟲爬了京東的數據,然後模仿他的頁面寫了一個 jsp:

首先來看一下如何爬取數據? 我這裏使用的是 jsoup 包,代碼如下:

private void initProduct(String keyWords) {
        String url = "https://search.jd.com/Search?keyword=" + keyWords + "&enc=utf-8";
        List<Product> list = new ArrayList<>();
        try {
            Document parse = Jsoup.parse(new URL(url), 30000);
            Element element = parse.getElementById("J_goodsList");
            Elements li = element.getElementsByTag("li");
            for (Element el : li) {
                count++;
                String img = el.getElementsByTag("img").eq(0).attr("source-data-lazy-img");
                String price = el.getElementsByClass("p-price").eq(0).text();
                String title = el.getElementsByClass("p-name").eq(0).text();
                Product product =
                        new Product(count, title, img, Float.valueOf(price.split("¥")[1]));
                list.add(product);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        this
                .getServletContext()
                .setAttribute("products", list);
    }

然後寫一個接口,當有請求過來的時候就可以從請求中獲取關鍵字,然後查詢,再重定向到首頁,實現展示商品的功能:

/**
     * 搜索功能
     * /shop/search
     *
     */
    private void doSearch(
            HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        String keyWords = request.getParameter("keyword");
        System.out.println("數據初始化");
        this.getServletContext().removeAttribute("products");
        initProduct(keyWords);
        System.out.println("轉發到商品首頁");
        displayProducts(request, response);
    }

感興趣的同學可以去 Github
上面查看相關代碼。

四、總結

  • 通過這次實驗,鞏固了 Servlet 的基本操作,以及 JSP 的操作,體會了 JSP 的頁面強大之處,JSP 太強了!!!(逃
  • 還使用了 jstl 的表達式,用起來很方便,就算不會 java 的人也能輕鬆實現 java 服務端和客戶端代碼的編寫,太強了!
  • 各個域之間的存儲數據,讓我更清楚的明白了域的區別的與聯繫,適合什麼樣的場景就用什麼樣的域。

相關源碼已上傳至 Github 地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章