<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我用vue3和egg開發(fā)了一個(gè)早報(bào)學(xué)習(xí)平臺,帶領(lǐng)群友走向技術(shù)大佬

          共 23494字,需瀏覽 47分鐘

           ·

          2022-08-15 22:42


          點(diǎn)擊上方 前端陽光,關(guān)注公眾號

          回復(fù)加群,加入技術(shù)交流群交流群


          • 項(xiàng)目功能介紹

          • 技術(shù)棧介紹

          • 前端實(shí)現(xiàn)

            • 創(chuàng)建項(xiàng)目

            • 按需引入antd組件

            • 首頁

          • 后端實(shí)現(xiàn)

            • 創(chuàng)建項(xiàng)目

            • 文章的獲取

            • 分析html,獲取文章列表

            • 發(fā)送信息到企業(yè)微信群

          • 總結(jié)

          • 好文推薦


          項(xiàng)目功能介紹

          該項(xiàng)目的出發(fā)點(diǎn)是獲取最新最值得推薦的文章以及面經(jīng),供群友們學(xué)習(xí)使用。帶領(lǐng)前端陽光的群友們一起成為技術(shù)大佬。

          當(dāng)點(diǎn)擊掘金的時(shí)候,就會(huì)獲取掘金當(dāng)前推薦的前端文章

          當(dāng)點(diǎn)擊??途W(wǎng)的時(shí)候,就會(huì)獲取到最新的前端面經(jīng)

          點(diǎn)擊【查看】就會(huì)跳到文章詳情頁

          勾選后點(diǎn)擊確認(rèn),就會(huì)把文章標(biāo)題拼接到右邊的輸入框中,然后點(diǎn)擊發(fā)送,就會(huì)將信息發(fā)送到學(xué)習(xí)群里供大家閱讀。

          • 項(xiàng)目源碼已經(jīng)放到github,歡迎fork,歡迎star。
          • 地址:https://github.com/Sunny-lucking/morning-news

          項(xiàng)目啟動(dòng):分別進(jìn)入server和client項(xiàng)目,執(zhí)行npm i安裝相關(guān)依賴,然后啟動(dòng)即可。

          技術(shù)棧介紹

          本項(xiàng)目采用的是前后端分離方案

          前端使用:vue3 + ts + antd

          后端使用:egg.js + puppeter

          前端實(shí)現(xiàn)

          創(chuàng)建項(xiàng)目

          使用vue-cli 創(chuàng)建vue3的項(xiàng)目。

          按需引入antd組件

          借助babel-plugin-import實(shí)現(xiàn)按需引入

          npm install babel-plugin-import --dev

          然后創(chuàng)建配置.babelrc文件就可以了。

          {
            "plugins": [
              ["import", { "libraryName""ant-design-vue""libraryDirectory""es""style""css" }] // `style: true` 會(huì)加載 less 文件
            ]
          }

          我們可以把需要引入的組件統(tǒng)一寫在一個(gè)文件里

          antd.ts

          import {
            Button,
            Row,
            Col,
            Input,
            Form,
            Checkbox,
            Card,
            Spin,
            Modal,
          from "ant-design-vue";

          const FormItem = Form.Item;

          export default [
            Button,
            Row,
            Col,
            Input,
            Form,
            FormItem,
            Checkbox,
            Card,
            Spin,
            Modal,
          ];

          然后在入口文件里面use應(yīng)用它們main.js

          import { createApp } from "vue";
          import App from "./App.vue";
          import antdCompArr from "@/antd";

          const app = createApp(App);
          antdCompArr.forEach((comp) => {
            app.use(comp);
          });

          app.mount("#app");

          首頁

          其實(shí)就一個(gè)頁面,所以,直接寫在App.vue了

          布局比較簡單,直接亮html

          <template>
            <div class="pape-wrap">
              <a-row :gutter="16">
                <a-col :span="16">
                  <a-card
                    v-for="group in paperList"
                    :key="group.name"
                    class="box-card"
                    shadow="always"
                  >

                    <div class="clearfix">
                      <span>{{ group.name }}</span>
                    </div>
                    <div class="channels">
                      <a-button
                        :style="{ 'margin-top': '10px', 'margin-left': '10px' }"
                        size="large"
                        v-for="item in group.list"
                        :key="item.href"
                        class="btn-channel"
                        @click="onClick(item)"
                      >

                        {{ item.name }}
                      </a-button>
                    </div>
                  </a-card>
                </a-col>
                <a-col :span="8">
                  <a-form>
                    <a-form-item
                      :laba-col="{ span: 24 }"
                      label="支持markdown輸入"
                      label-align="left"
                    >

                      <a-textarea
                        v-model:value="content"
                        placeholder="暫支持mardown語法"
                        show-count
                      />

                    </a-form-item>
                    <a-form-item>
                      <a-button @click="handleSendMsg"> 發(fā)消息 </a-button>
                    </a-form-item>
                  </a-form>
                </a-col>
              </a-row>

              <a-modal
                v-model:visible="visible"
                custom-class="post-modal"
                title="文章列表"
                @ok="handleComfirm"
              >

                <a-spin tip="Loading..." :spinning="isLoading">
                  <div class="post-list">
                    <div :style="{ borderBottom: '1px solid #E9E9E9' }">
                      <a-checkbox
                        v-model="checkAll"
                        :indeterminate="indeterminate"
                        @change="handleCheckAll"
                        >
          全選</a-checkbox
                      >

                    </div>
                    <br />
                    <a-checkbox-group v-model:value="checkedList">
                      <a-checkbox
                        :value="item.value"
                        v-for="item in checkoptions"
                        :key="item.value"
                      >

                        {{ item.label }}
                        <a
                          class="a-button--text"
                          style="font-size: 14px"
                          target="_blank"
                          :href="item.value"
                          @click.stop
                        >

                          &nbsp; &nbsp;查看</a
                        >

                      </a-checkbox>
                    </a-checkbox-group>
                  </div>
                </a-spin>

                <span>
                  <a-button @click="handleComfirm">確認(rèn)</a-button>
                </span>
              </a-modal>
            </div>
          </template>

          主要就是遍歷了paperList,而paperList的值是前端寫死的。在constant文件里

          export const channels = [
            {
              name"前端",
              list: [
                {
                  name"掘金",
                  bizType"juejin",
                  url"https://juejin.cn/frontend",
                },
                {
                  name"segmentfault",
                  bizType"segmentfault",
                  url"https://segmentfault.com/channel/frontend",
                },
                {
                  name"Chrome V8 源碼",
                  bizType"zhihu",
                  url"https://zhuanlan.zhihu.com/v8core",
                },
                {
                  name"github-Sunny-Lucky前端",
                  bizType"githubIssues",
                  url"https://github.com/Sunny-lucking/blog/issues",
                },
              ],
            },
            {
              name"Node",
              list: [
                {
                  name"掘金-后端",
                  bizType"juejin",
                  url"https://juejin.cn/frontend/Node.js",
                },
              ],
            },
            {
              name"面經(jīng)",
              list: [
                {
                  name"??途W(wǎng)",
                  bizType"newcoder",
                  url"https://www.nowcoder.com/discuss/experience?tagId=644",
                },
              ],
            },
          ];

          點(diǎn)擊按鈕的時(shí)候,出現(xiàn)彈窗,然后向后端發(fā)起請求,獲取相應(yīng)的文章。

          點(diǎn)擊方法如下:

          const onClick = async (item: any) => {
            visible.value = true;
            currentChannel.value = item.url;
            if (cache[currentChannel.value]?.list.length > 0) {
              const list = cache[currentChannel.value].list;
              state.checkedList = cache[currentChannel.value].checkedList || [];
              state.postList = list;
              return list;
            }
            isLoading.value = true;
            state.postList = [];
            const { data } = await getPostList({
              link: item.url,
              bizType: item.bizType,
            });
            if (data.success) {
              isLoading.value = false;
              const list = data.data || [];
              state.postList = list;
              cache[currentChannel.value] = {};
              cache[currentChannel.value].list = list;
            } else {
              message.error("加載失敗!");
            }
          };

          獲得文章渲染之后,勾選所選項(xiàng)之后,點(diǎn)擊確認(rèn),會(huì)將所勾選的內(nèi)容拼接到content里

          const updateContent = () => {
            const date = moment().format("YYYY/MM/DD");
            // eslint-disable-next-line no-useless-escape
            const header = `<font color=\"#389e0d\">前端早報(bào)-${date}</font>,歡迎大家閱讀。\n>`;
            const tail = `本服務(wù)由**前端陽光**提供技術(shù)支持`;
            const body = state.preList
              .map((item, index) => `#### ${index + 1}${item}`)
              .join("\n");
            state.content = `${header}***\n${body}\n***\n${tail}`;
          };

          const handleComfirm = () => {
            visible.value = false;
            const selectedPosts = state.postList.filter((item: any) =>
              state.checkedList.includes(item.href as never)
            );
            const selectedList = selectedPosts.map((item, index) => {
              return `[${item.title.trim()}](${item.href})`;
            });
            state.preList = [...new Set([...state.preList, ...selectedList])];
            updateContent();
          };

          然后點(diǎn)擊發(fā)送,就可以將拼接的內(nèi)容發(fā)送給后端了,后端拿到后再轉(zhuǎn)發(fā)給企業(yè)微信群

          const handleSendMsg = async () => {
            const params = {
              content: state.content,
            };
            await sendMsg(params);
            message.success("發(fā)送成功!");
          };

          前端的內(nèi)容就講到這里,大家可以直接去看源碼:https://github.com/Sunny-lucking/morning-news

          后端實(shí)現(xiàn)

          創(chuàng)建項(xiàng)目

          后端是使用egg框架實(shí)現(xiàn)的

          快速生成項(xiàng)目

          npm init egg

          可以直接看看morningController的業(yè)務(wù)邏輯,其實(shí)主要實(shí)現(xiàn)了兩個(gè)方法,一個(gè)是獲取文章列表頁返回給前端,一個(gè)是發(fā)送消息。

          export default class MorningPaper extends Controller {
            public async index() {
              const link = this.ctx.query.link;
              const bizType = this.ctx.query.bizType;
              let html = '';
              if (!link) {
                this.fail({
                  msg'入?yún)⑿r?yàn)不通過',
                });
                return;
              }
              const htmlResult = await this.service.puppeteer.page.getHtml(link);
              if (htmlResult.status === false) {
                this.fail({
                  msg'爬取html失敗,請稍后重試或者調(diào)整超時(shí)時(shí)間',
                });
                return;
              }
              html = htmlResult.data as string;
              const links = this.service.morningPaper.index.formatHtmlByBizType(bizType, html) || [];
              this.success({
                data: links.filter(item => !item.title.match('招聘')),
              });
              return;
            }

            /**
             * 推送微信機(jī)器人消息
             */

            async sendMsg2Weixin() {
              const content = this.ctx.query.content;
              if (!content) {
                this.fail({
                  resultObj: {
                    msg'入?yún)?shù)據(jù)異常',
                  },
                });
                return;
              }
              const token = this.service.morningPaper.index.getBizTypeBoken();
              const status = await this.service.sendMsg.weixin.index(token, content);
              if (status) {
                this.success({
                  resultObj: {
                    msg'發(fā)送成功',
                  },
                });
                return;
              }

              this.fail({
                resultObj: {
                  msg'發(fā)送失敗',
                },
              });
              return;
            }
          }

          文章的獲取

          先看看文章是怎么獲取的。

          首先是調(diào)用了puppeter.page的getHtml方法

          該方法是利用puppeter生成一個(gè)模擬的瀏覽器,然后模擬瀏覽器去瀏覽頁面的邏輯。

           public async getHtml(link) {
              const browser = await puppeteer.launch(this.launch);
              const page: any = await browser.newPage();
              await page.setViewport(this.viewport);
              await page.setUserAgent(this.userAgent);
              await page.goto(link);
              await waitTillHTMLRendered(page);
              const html = await page.evaluate(() => {
                return document?.querySelector('html')?.outerHTML;
              });
              await browser.close();
              return {
                statustrue,
                data: html,
              };
            }

          這里需要注意的是,需要await waitTillHTMLRendered(page);,它的作用是檢查頁面是否已經(jīng)加載完畢。

          因?yàn)?,進(jìn)入頁面,page.evaluate的返回可能是頁面還在加載列表當(dāng)中,所以需要waitTillHTMLRendered判斷當(dāng)前頁面的列表是否加載完畢。

          看看這個(gè)方法的實(shí)現(xiàn):每隔一秒鐘就判斷頁面的長度是否發(fā)生了變化,如果三秒內(nèi)沒有發(fā)生變化,默認(rèn)頁面已經(jīng)加載完畢

          const waitTillHTMLRendered = async (page, timeout = 30000) => {
            const checkDurationMsecs = 1000;
            const maxChecks = timeout / checkDurationMsecs;
            let lastHTMLSize = 0;
            let checkCounts = 1;
            let countStableSizeIterations = 0;
            const minStableSizeIterations = 3;

            while (checkCounts++ <= maxChecks) {
              const html = await page.content();
              const currentHTMLSize = html.length;

              // eslint-disable-next-line no-loop-func
              const bodyHTMLSize = await page.evaluate(() => document.body.innerHTML.length);

              console.log('last: ', lastHTMLSize, ' <> curr: ', currentHTMLSize, ' body html size: ', bodyHTMLSize);

              if (lastHTMLSize !== 0 && currentHTMLSize === lastHTMLSize) { countStableSizeIterations++; } else { countStableSizeIterations = 0; } // reset the counter

              if (countStableSizeIterations >= minStableSizeIterations) {
                console.log('Page rendered fully..');
                break;
              }

              lastHTMLSize = currentHTMLSize;
              await page.waitForTimeout(checkDurationMsecs);
            }
          };

          分析html,獲取文章列表

          上述的行為只會(huì)獲取了那個(gè)頁面的整個(gè)html,接下來需要分析html,然后獲取文章列表。

          html的分析其實(shí) 是用到了cheerio,cheerio的用法和jQuery一樣,只不過它是在node端使用的。

          已獲取掘金文章列表為例子:可以看到是非常簡單地就獲取到了文章列表,接下來只要返回給前端就可以了。

            getHtmlContent($): Link[] {
              const articles: Link[] = [];
              $('.entry-list .entry').each((index, ele) => {
                const title = $(ele).find('a.title').text()
                  .trim();
                const href = $(ele).find('a.title').attr('href');
                if (title && href) {
                  articles.push({
                    title,
                    hrefthis.DOMAIN + href,
                    index,
                  });
                }
              });
              return articles;
            }

          發(fā)送信息到企業(yè)微信群

          這個(gè)業(yè)務(wù)邏輯主要有兩步,

          首先要獲取我們企業(yè)微信群的機(jī)器人的token,

          接下來就將token 拼接成下面這樣一個(gè)url

          `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${token}`

          然后利用egg 的curl方法發(fā)送信息就可以了

          export default class Index extends BaseService {
            public async index(token, content): Promise<boolean> {
              const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${token}`;
              const data = {
                msgtype'markdown',
                markdown: {
                  content,
                },
              };
              const result: any = await this.app.curl(url, {
                method'POST',
                headers: {
                  'Content-Type''application/json',
                },
                data,
              });
              if (result.status !== 200) {
                return false;
              }
              return true;
            }
          }

          后端的實(shí)現(xiàn)大抵如此,大家可以看看源碼實(shí)現(xiàn):https://github.com/Sunny-lucking/morning-news

          總結(jié)

          至此,一個(gè)偉大的工程就打造完畢。

          群員在我的帶領(lǐng)下,技術(shù)突飛猛進(jìn)。。。

          撒花撒花。。

          好文推薦

          這是我的github,歡迎大家star:https://github.com/Sunny-lucking/blog


          往期推薦


          優(yōu)秀文章匯總:https://github.com/Sunny-lucking/blog

          內(nèi)推:https://www.yuque.com/peigehang/kb

          技術(shù)交流群


          我組建了技術(shù)交流群,里面有很多 大佬,歡迎進(jìn)來交流、學(xué)習(xí)、共建?;貜?fù) 加群 即可。后臺回復(fù)「電子書」即可免費(fèi)獲取 27本 精選的前端電子書!回復(fù)內(nèi)推,可內(nèi)推各廠內(nèi)推碼



             “分享、點(diǎn)贊、在看” 支持一波??


          瀏覽 55
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  丁香av| 无码啪啪| 精品欧美乱伦 | 一色逼毛| 奇米久久爱 |