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. 項目目錄
- 創(chuàng)建一個build文件夾,在下面創(chuàng)建一個webpack.config.dev.js用于配置webpack的打包配置
- 創(chuàng)建一個client文件夾,下面主要存放頁面
- routes主要用于koa的路由,api接口和頁面的渲染功能
- views主要是渲染頁面的模板
- 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)效果
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;