React+Nodejs中間層(一):環(huán)境搭建+實現(xiàn)接口轉(zhuǎn)發(fā)

Koa+React+Webpack環(huán)境配置

1. 初衷

最近,因疫情在家寫了一些東西.想到之前去實習(xí)的時候歧譬,公司用的Nodejs做中間層,實現(xiàn)數(shù)據(jù)格式的處理陷猫,請求轉(zhuǎn)發(fā)秫舌,SSR等功能,想自己也折騰下這么個東西绣檬,剛好最近或多或少的學(xué)了一些Webpack足陨,就從webpack搭一個Koa作為中間層,頁面路由娇未,同時使用React的這么一個腳手架吧墨缘,只是一個自己的玩具吧,哈哈哈哈.

2. 使用中間層的作用

我理解的幾個nodejs的作用

  • 整合后端接口和接口轉(zhuǎn)發(fā):比如一個功能需要多個不同的接口零抬,可以先把請求打到Node層镊讼,通過Node層去訪問多個Java接口,之后將多個接口數(shù)據(jù)進行拼接即可.
  • 解決跨域問題:如果直接使用瀏覽器發(fā)起http請求到后端平夜,會存在跨域問題,使用nodejs獲取數(shù)據(jù),可以繞過跨域問題(本文進行實踐)
  • 利于SEO:利用Node層實現(xiàn)模板渲染功能党瓮,有利于SEO(第二部分進行實踐)

3. 搭建Koa2+React+Webpack開發(fā)環(huán)境

看了網(wǎng)上一些文章姜挺,其實自己對于中間層有點迷糊兼贸,這里的想法是利用Koa的模板渲染功能,在渲染的時候加載打包的React頁面的js文件吃溅,通過打包的js文件實現(xiàn)模板渲染react頁面(但是不知道這個是怎么實現(xiàn)SSR的)溶诞,請教網(wǎng)上各位大佬,謝謝大家了.

因此上述項目的本質(zhì)上還是一個koa的項目,使用koa2的腳手架搭建一個koa2的后端環(huán)境.

1. 項目目錄

選區(qū)_060.png
  1. 創(chuàng)建一個build文件夾,在下面創(chuàng)建一個webpack.config.dev.js用于配置webpack的打包配置
  2. 創(chuàng)建一個client文件夾,下面主要存放頁面
  3. routes主要用于koa的路由,api接口和頁面的渲染功能
  4. views主要是渲染頁面的模板
  5. public中的javascript主要用于存放打包的頁面js,這個publick頁面是koa中配置的靜態(tài)路徑的根目錄,之后通過頁面加載的靜態(tài)資源文件(打包完畢的js文件都放在這里,之后網(wǎng)頁加載的時候通過靜態(tài)資源進行加載)

2. Webpack環(huán)境配置

1. 依賴安裝
  • babal相關(guān)
    • babel-loader
    • babel-polyfill
    • @babel/core
    • @babel/plugin-transform-runtime
    • @babel/preset-env
    • @babel/preset-react
    • @babel/runtime
    • @babel/runtime-corejs3
  • 其他loader
    • css-loader
    • style-loader
    • file-loader
    • url-loader
  • 其他插件
    • html-webpack-plugin
    • clean-webpack-plugin
2. babel和loader的常規(guī)配置
module: {
    rules: [
        {
            test: /\.jsx?/,
            loader: "babel-loader",
            options: {
                presets: ["@babel/preset-env", "@babel/preset-react"],
                plugins: [
                    [
                        "@babel/plugin-transform-runtime",
                        {
                            corejs: 3
                        }
                    ]
                ]
            },
            exclude: /node_modules/
        },
        {
            test: /\.css/,
            loader: ["style-loader", "css-loader"],
            exclude: /node_modules/
        },
        {
            test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|ico)$/,
            use: [
                {
                    loader: "url-loader",
                    options: {
                        limit: 10240, //
                        esModule: false
                    }
                }
            ],
            exclude: /node_modules/
        }
    ]
},

注意的點: css-loader和style-loader的順序决侈,執(zhí)行的時候是倒序出棧執(zhí)行的螺垢,所以要反著配置

3. 多頁面配置

配置考慮如下問題

  • 應(yīng)該每個頁面是多入口的情況,如果要配置多個入口
  • 多個入口的文件會引用共有庫颜及,例如React甩苛,所以在打包的時候應(yīng)該優(yōu)化,分離打包公用包
  • 使用html-webpack-plugin生成模板頁面俏站,通過koa中的ctx.render方法進行渲染讯蒲,因此需要配置多生成html文件,僅加載該文件中用到的打包的js文件
4. 公共包Chunk的打包過程:

這里的name肄扎,指定了打包得到的公共包的chunkName這里的vendor,之后在配置html-webpack-plugin的時候按照chunkName進行打包的時候可以配置為vendor

optimization: {
    splitChunks: {
        cacheGroups: {
            commons: {
                name: "vendor",
                chunks: "all"
            }
        }
    }
}
5. 多文件入口和html-webpack-plugin配置

這里就是將client下面的目錄文件夾配置成對應(yīng)的頁面入口,動態(tài)加載多頁面和打包html的配置

const fs = require("fs");

const getEntryDir = () => {
    const dir = fs.readdirSync(path.resolve(__dirname, "../client/"));

    let entry = {};
    let webpackPlugins = [];

    dir.forEach(item => {
        // 配置頁面的name和對應(yīng)的入口路徑墨林,完成多頁面配置
        entry = {
            ...entry,
            [item]: path.resolve(__dirname, `../client/${item}/index.jsx`)
        };
        // htmlWebpackPlugin的配置需要中使用chunks對對應(yīng)的HTML頁面進行打包即可
        webpackPlugins.push(
            // chunks可以
            new htmlWebpackPlugin({
                template: path.resolve(__dirname, "../views/template.html"),
                filename: path.resolve(__dirname, `../views/${item}.html`),
                chunks: [item, "vendor"]
            })
        );
    });

    return { entry, webpackPlugins };
};

3. 整體配置

const config = getEntryDir();

module.exports = {
  mode: "development",
  entry: config.entry,
  output: {
    // 這里的path為打包文件生成的地址,這里要配置到Koa文件的根目錄下7胳簟P竦取!
    // 這樣在加載js的時候才能加載出來衡载,不然會出現(xiàn)找不到靜態(tài)資源的問題
    path: path.resolve(__dirname, "../public/javascripts/"),
    filename: "[name].[hash:8].bundle.js",
    chunkFilename: "[name].[chunkhash:8].js",
    // 這里配置的是引入打包js的目錄搔耕,如果配置成/public/javascripts
    // 那么在html-webpack-plugin中引入的js文件路徑就為/public/javascripts/a.js
    // 配置為/javascripts/的話 就是/javascripts/a.js
    publicPath: "/javascripts/"
  },
  module: {
    rules: [
      {
        test: /\.jsx?/,
        loader: "babel-loader",
        options: {
          presets: ["@babel/preset-env", "@babel/preset-react"],
          plugins: [
            [
              "@babel/plugin-transform-runtime",
              {
                corejs: 3
              }
            ]
          ]
        },
        exclude: /node_modules/
      },
      {
        test: /\.css/,
        loader: ["style-loader", "css-loader"],
        exclude: /node_modules/
      },
      {
        test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|ico)$/,
        use: [
          {
            loader: "url-loader",
            options: {
              limit: 10240, //
              esModule: false
            }
          }
        ],
        exclude: /node_modules/
      }
    ]
  },
  resolve: {},
  devtool: "cheap-source-map",
  plugins: [new CleanWebpackPlugin(), ...config.webpackPlugins],
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          name: "vendor",
          chunks: "all"
        }
      }
    }
  }
};

4. 配置scripts

{
    "scripts": {
        "start": "node bin/www",
        "devBuild": "npx webpack --config ./build/webpack.config.dev.js"
    },
}

之后用devBuild即可完成打包,使用start啟動后端服務(wù)即可.

4. 測試環(huán)境

編寫一個Entry下的index.js頁面

// Entry/index.js
import React, { Component } from "react";
import ReactDom from 'react-dom';

export default class App extends Component {
  render() {
    return <div>這是一個共有頁面</div>;
  }
}

ReactDom.render(<App />, document.querySelector('.app'))

Koa配置 (這里就是官方的配置痰娱,沒啥自己配置的弃榨,需要注意的是views的路徑)

const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')

const index = require('./routes/index')
const users = require('./routes/users')

// error handler
onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

// 這里調(diào)用了views中間件后,會向ctx中添加一個render方法梨睁,渲染views文件夾下對應(yīng)的文件
// 如果沒有擴展名鲸睛,會自動補全一個ejs作為文件后綴
app.use(views(__dirname + '/views', {
  extension: 'ejs'
}))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

Koa路由配置

router.get("/", async (ctx, next) => {
  await ctx.render("Entry.html");
});

執(zhí)行:

yarn devBuild && yarn start

打開127.0.0.1:3000頁面可見到對應(yīng)效果

選區(qū)_061.png

5. 實現(xiàn)使用node層轉(zhuǎn)發(fā)請求

使用node層轉(zhuǎn)發(fā)和整合接口以及避免跨域的問題解決方法

  • 頁面將請求打到指定的node路由處
  • node層使用http庫(request等其他都行),轉(zhuǎn)發(fā)請求到后端服務(wù)器
  • 得到數(shù)據(jù)后返回接口的數(shù)據(jù)值
1. 編寫一個調(diào)用本地Django服務(wù)的Api接口
import ReactDom from "react-dom";
import React, { Component } from "react";
import axios from "axios";

class App extends Component {
    async getUserInfo() {
        const data = await axios.get("/cartList", { params: { userId: 7 } });
        console.log(data);
    }

    async getDirect() {
        const data = await axios.get(
            "http://127.0.0.1:8000/campus/api/getCartList?userId=7&start=1&pageSize=5"
        );
        console.log(data);
    }

    render() {
        return (
            <div>
                User Page
                <button onClick={this.getUserInfo}>獲取用戶信息</button>
                <button onClick={this.getDirect}>直接訪問服務(wù)器</button>
            </div>
        );
    }
}

ReactDom.render(<App />, document.querySelector(".app"));
  • 上面如果是直接調(diào)用服務(wù)器的話會報非同源的錯誤
  • 通過nodejs的話就可以避免這個問題
2. node層配置
  • 通過opt配置相應(yīng)的參數(shù)坡贺,http.request通過回調(diào)函數(shù)監(jiān)聽數(shù)據(jù)返回情況
  • 這個data狀態(tài)下返回的chunk是一個buffer類型官辈,我們需要將他拼接起來然后轉(zhuǎn)換為json字符串
  • 利用JSON.parse轉(zhuǎn)換為對象返回
// 定義了一個利用http轉(zhuǎn)發(fā)Api的函數(shù)getResponse
const http = require("http");
const queryString = require("querystring");

module.exports = {
  getResponse(hostname, port, path, param, success, errFn, processFn) {
    let data = "";

    const query = queryString.stringify(param);

    path += "?" + query;

    const opt = {
      hostname,
      port,
      path,
      method: "GET",
      headers: {
        "Content-Type": "application/json"
      }
    };

    return new Promise((resolve, reject) => {
      const req = http.request(opt, response => {
        response.on("error", err => {
          console.err(err);
          errFn && errFn(err);
          reject(err);
        });

        response.on("end", () => {
          success && success(data);
          resolve(JSON.parse(data));
        });

        response.on("data", chunk => {
          processFn && processFn(chunk.toString(), data);
          data += chunk.toString();
        });
      });

      req.end();
    });
  }
};

接收到參數(shù)返回給前端的邏輯

const router = require("koa-router")();
const { getResponse } = require("../utils.js");

router.get("/user", async (ctx, next) => {
    await ctx.render("User.html");
});

router.get("/cartList", async ctx => {
    const { userId } = ctx.query;

    const content = { userId, start: 1, pageSize: 5 };

    const data = await getResponse(
        "127.0.0.1",
        8000,
        "/campus/api/getCartList",
        content
    );

    ctx.body = data;
});

router.get("/", async (ctx, next) => {
    await ctx.render("Entry.html");
});

module.exports = router;

6. 效果

Peek 2020-03-18 15-57.gif
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市遍坟,隨后出現(xiàn)的幾起案子拳亿,更是在濱河造成了極大的恐慌,老刑警劉巖政鼠,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件风瘦,死亡現(xiàn)場離奇詭異,居然都是意外死亡公般,警方通過查閱死者的電腦和手機万搔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門胡桨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瞬雹,你說我怎么就攤上這事昧谊。” “怎么了酗捌?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵呢诬,是天一觀的道長。 經(jīng)常有香客問我胖缤,道長尚镰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任哪廓,我火速辦了婚禮狗唉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘涡真。我一直安慰自己分俯,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布哆料。 她就那樣靜靜地躺著缸剪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪东亦。 梳的紋絲不亂的頭發(fā)上杏节,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機與錄音典阵,去河邊找鬼拢锹。 笑死,一個胖子當(dāng)著我的面吹牛萄喳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蹋半,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼他巨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了减江?” 一聲冷哼從身側(cè)響起染突,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎辈灼,沒想到半個月后份企,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡巡莹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年司志,在試婚紗的時候發(fā)現(xiàn)自己被綠了甜紫。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡骂远,死狀恐怖囚霸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情激才,我是刑警寧澤拓型,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站瘸恼,受9級特大地震影響劣挫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜东帅,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一压固、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冰啃,春花似錦邓夕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至扇调,卻和暖如春矿咕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狼钮。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工碳柱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人熬芜。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓莲镣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親涎拉。 傳聞我的和親對象是個殘疾皇子瑞侮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容