前言
前端技术发展至今,各种框架技术层出不穷,前后端分离的开发模式也大行其道,如今,前端大多以webapp的形式存在,很多后端的工作都开始拿到前端来做,而后端只需要提供负责请求数据的API接口,前端工作开始变得庞杂,关注点由单维的页面视效发展成了路由、测试、效率等多个方面,注意力无法聚焦,这直接导致了开发方式的升级换代,本文将以Webpack构建项目工程为切入点,通过工程架构和项目架构,以点带面地探索一下前端项目的开发流程及其发展趋势。
webapp架构
在webapp模式中,前端通过后端API请求大量数据,之后完成页面渲染、页面跳转等行为,前端内容多为JS代码,甚至很少有写HTML的行为,即使有也有很大可能作为JS代码的一部分存在,前端变得越来越复杂,这就要求考虑其架构问题。
一般来讲,webapp的架构分为工程架构和项目架构。拿工程架构来讲,主要有以下几个方面:通过工程脚手架的搭建来解放生产力(即各种自动行为);通过合理的技术选型来完成项目解决方案;通过制定代码规范(codelint,editconfig和git commit预处理)来保证团队合作及项目质量。此外,工程架构需要突出‘可定制性’,并需要预期可能出现的问题并提前准备解决方案;其次是项目架构,主要是通过技术选型、数据解决方案、代码风格、合理的目录结构来实现。
为什么选择Webpack
我们需要一款可定制的自动化工具,使得注意力更加聚焦于业务自身,而不是其他(源码预处理、自动打包、自动更新页面显示、自动处理图片依赖等),进而提高开发效率。
项目目录结构
client
--app.js//应用入口
--App.jsx//页面内容
--sever-entry.js//用于SSR
--template.html
build
--webpack.config.client.js
--webpack.config.sever.js//用于SSR打包配置
sever//web服务,用于SSR
dist //系统生成的文件夹,用于存放打包JS文件
package.json
.babelrc //babel配置文件
Webpack基础配置
在webpack.config.client.js中最基础的对象为entry和output,分别为打包文件入口和输出文件出口。
module.exports = {
entry:{
app: path.join(__dirname, '../client/app.js')//使用绝对路径
}
output:{
filename: '[name].[hash].js', //入口文件只要有更改,hash值便更改,用于浏览器缓存
path: path.join(__dirname, '../dist/'),
publicPath: '/public' //静态资源引用时的路径,用于区分URL是静态资源还是API请求,为路径前缀
}
}
webpack最基础的如上,这时使用webpack --config build/webpack.config.client.js
即可完成打包。
Webpack loader基础配置
Webpack的核心是打包,而其灵魂是loader!当项目为react时,需要配置loader使得webpack可以识别JSX
//需要首先安装babel-loader和babel-core
module:{
rules: [
{
test: /.jsx$/,
loader: 'babel-loader'
},
{
test: /.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}
配置babel
描述react组件的jsx语法和JS高级语法暂时不能被浏览器执行,需要babel进行“翻译”,固需进行下面的配置:
//首先安装babel-preset-es2015 babel-preset-es2015-loose babel-preset-react
//在。babelrc中编辑:
{
"presets": [
["es2015", {"loose": true}],
"react"
]
}
此时webpack可以编译jsx和es6语法了。
Webpack插件机制
安装浏览器打开插件html-webpack-plugin
plugins:[
new HTMLPlugin({
template: path.join(__dirname, '../client/template.html')
}),//自动生成一模板HTML页面,并根据Webpack配置插入资源
]
以上便完成了最为基础的基于webpack的ReactWebApp。
Webpack SSR基础配置
在Webapp开发模式中,页面内容由浏览器渲染,而为了SEO和缩短首屏时间,SSR成为了优化的必选项。React提供了react-dom插件用来解决SSR问题。即在node环境中完成渲染,然后返回给浏览器一些可以显示的HTML内容。
//sever-entry.js中
import React from 'react'
import App from './App.jsx'
export default <App />
上面新建代码文件(首屏代码),可为服务端所用,由于代码无法直接运行在node端,所以下一步还需要Webpack进行服务端打包配置。
target: 'node',
entry:{
app: path.join(__dirname, '../client/sever-entry.js')
}
output: {
filename: 'sever-entry.js',//导出文件配置成SSR专用文件
path: path.join(__dirname, '../dist/'),
publicPath: '',
libraryTarget: 'commonjs2'//适用于nodeSSR的模块打包规范
}
配置好后,前后端分别打包(可用脚本配置),即可生成一个HTML页面,2个JS文件(前后端各一个),通过express提供中间层SSR服务,通过’react-dom/server‘ 模块提供的SSR方法进行服务端渲染,即将SSR的内容插入到body中,再将整个页面返回到浏览器
//在server端
const express = require('express')
const ReactSSR = require('react-dom/server')
const fs = require('fs')
const path = require('path')
const template = fs.readFileSync(path.join(_dirname, '../dist/index.html'),'ntf-8')
const serverEntry = require('../dist/server-entry').default //此为webpack打包生成的文件
const app = express()
app.use('./public', express.static(path.join(__dirname, '../dist'))) //处理静态文件
app.get(' * ', function(req, res){
res.send(template.replace('<app><app/>', ReactSSR.renderToString(serverEntry)))
})
app.listen(1234)
上面为最简单的SSR,而完整的SSR流程是把SSR的内容替换到模板HTML页面的<App/>中,再将整个页面返回到客户端浏览器:这时整个SSR算是完整走通了。
Webpack-dev-server配置
开发过程中,可以使用webpack提供的一些组件来提高开发效率。
const isDev = process.env.NODE_ENV === 'development'//判断是否为开发环境,通过启动命令加以区分。一般情况下,dev-server会配置在开发环境下。
//webpack-dev-sever在dist目录下启动服务器
devServer :{
host: '0.0.0.0',//可以通过多种方式访问
port: 8888,
contentBase: path.join(__dirname, '../dist/'),//静态文件目录
hot: true,//需要在react中配置相关热更新模块
overlay: {//显示错误信息
errors: true
},
publicPath: '/public',//访问静态文件时均要加上这个路径前缀
historyApiFallback: {
index: './public/index.html'
}
}
其中,需要用模块cross-env
来解决跨平台的环境变量问题。
hot-module-replacement配置
webpack提供的局部无刷新更新插件,需要在.babelrc中配置。
"plugins": ["react-hot-loader/babel"]
//在app.js中
import { AppContainer} from 'react-hot-loader'//热更新代码需要被“AppContainer”进行包裹:
const root = document.getElementById('root'))
const render = Component => {
ReactDOM.hydrate(
<AppContainer>
<Component />
<AppContainer/>,
root
)
}
if(module.hot){
module.hot.accept('./App.jsx', () => {
const NextApp = require('./App.jsx').default
render(NextApp)
})
}
在webpack中添加插件:
publicPath:'/public/'//如果没有第二个/,会有404错误,影响热更新
plugins:[
new HTMLPlugin({
template: path.join(__dirname, '../client/template.html')
}),//自动生成一模板HTML页面,并根据Webpack配置插入资源
new webpackHotModuleReplacementPlugin()//热更新插件
]
为了使热更新生效,需要在入口添加新的打包文件:
entry:{
app: ['react-hot-loader/patch', //热更新要用到的内容
path.join(__dirname, '../client/app.js')//使用绝对路径]
}
此时,修改代码便可以完成无刷新的热更新更能。
开发环境下的SSR
在开发环境下配置SSR时,需要使用客户端的模板文件和服务端的bundle文件,所以在开发环境下的SSR配置重点分为两块:由于开发环境下没有静态文件生成,不能直接通过路径获取,可以在webpack-dev-server中通过网络请求获得模板文件;通过webpack和对应配置文件获得存放于内存中的bundle.js文件内容:
//通过向webpack-dev-sever动态请求模板文件
const getTemplate = () => {
return new Promise((resolve, reject) => {
axios.get('http://localhost:8888/public/index.html')
.then(res => {
resolve(res.data)
})
.catch(reject)
})
}
//通过webpack API监听bundle.js文件变化并写入内存,并以模块导出,在dev-static.js中
const NativeModule = require('module')
const vm = require('vm')
const getModuleFromString = (bundle, filename) => {
const m = { exports: {} }
const wrapper = NativeModule.wrap(bundle)
const script = new vm.Script(wrapper, {
filename: filename,
displayErrors: true,
})
const result = script.runInThisContext()
result.call(m.imports, m.exports, require, m)
return m
}
const serverCompiler = webpack(serverConfig)
serverCompiler.outputFileSystem = mfs//webpack配置项,直接写入内存
let serverBundle
serverCompiler.watch({}, (err, stats) => {//监听文件变化
if (err) throw err
stats = stats.toJson()
stats.errors.forEach(err => console.error(err))
stats.warnings.forEach(warn => console.warn(warn))
const bundlePath = path.join(
serverConfig.output.path,
serverConfig.output.filename
)
const bundle = mfs.readFileSync(bundlePath, 'utf-8')
const m = getModuleFromString(bundle, 'server-entry.js')//string内容转化成模块
serverBundle = m.exports.default
})
//最后通过express路由完成SSR
const proxy = require('http-proxy-middleware')//express的请求代理插件
app.use('/public', proxy({targ: 'http://localhost: 8888'}))//处理静态文件
app.get('*', function (req, res) {
getTemplate().then(emplate => {
const content ReactDomSever.rendToString(serverBundle)
res.send(template.replace('<!--app-->', content))
})
})
eslink与editconfig
以上,基本完成了一个webapp项目的工程脚手架的搭建。而为了有利于团队合作和提高开发效率,有必要用eslink和editconfig来对代码进行有效规范。
//在clint目录下新建的.eslintrc文件中
{
"parser": "babel-eslint",
"env": {
"browser": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"extends": "airbnb",
"rules": {
"semi": [0]//不写分号
}
}
这样,在每次代码编译前,需要先检查代码规范,若有规范错误,则停止继续编译。
//在webpack配置文件中的rules添加如下:
{
enforce: 'pre',
test: /.(js|jsx)$/,
loader: 'eslint-loader',
exclude: [
path.resolve(__dirname, '../node_modules')
]
}
此外,还需要配置editconfig来规避由于系统差异带来的某些错误。
//在根目录下新建的.editconfig文件中:
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
目前为止可以优化的点
- 首先,webpack配置文件中的公共部分可以提取出来,可使解构更清晰,更利于代码阅读。
const webpackMerge = require('webpack-merge')
const baseConfig = require('./webpack.base') //webpack配置的公共部分
const config = webpackMerge(baseConfig, {
//非公共配置
})
- 处理favicon.ico(相关组件请去Github上搜)。
- 通过nodemon监听文件变化自动重启服务。
//在根目录下的nodemon.json文件中配置
{
"restartable": "rs",
"ignore": [
".git",
"node_modules/**/node_modules",
".eslintrc",
"client",
"build"
],
"env": {
"NODE_ENV": "development"
},
"verbose": true,
"ext": "js"
}
此时,项目工程架构完毕。
项目架构
views //此目录用于存放项目功能模块,子目录的划分取决于路由
--topic
----index.jsx //每个页面的入口文件
config //配置,路由,第三方类库等
--router.js
store // 数据相关
--app-state.js
--store.js
components // 非业务组件和共用组件
配置路由
路由用于页面跳转,HTML5中的history api能够在URL发生变化的时候被JS监听,进而通过JS向后台API请求数据,最终呈现新的页面出来,而在这个过程中history api会阻止浏览器刷新页面的行为,在history api出现之前也可以用hash来完成此操作。
在React项目中,当然要使用react-router。
import React from 'react'
import {
Route,
Redirect,
} from 'react-router-dom'
import TopicList from '../views/topic-list/index'
import TopicDetail from '../views/topic-detail/index'
import TestApi from '../views/test/api-test'
//React16中新增数组写法
export default () => [
<Route path="/" render={() => <Redirect to="/list" />} exact key="first" />,// exact为精确匹配
<Route path="/list" component={TopicList} key="list" />,
<Route path="/detail" component={TopicDetail} key="detail" />,
<Route path="/test" component={TestApi} key="test" />,
]
上面代码为<Route />用法,且需要在外层包裹<BrowserRouter />方能正常使用。
此时,当出现如下问题时:
可以在webpack配置文件中添加:
resolve: {
extensions: ['.js', '.jsx']
}//可以在导入模块时忽略后缀名
Store配置
众所周知,React的数据流向只能从父组件向子组件传递,顾名思义为单向数据流,但是在实际情况下,有时需要子组件通过回调函数向父组件传递数据,当项目庞大,组件嵌套过深时,这种引用回调的方式将变得不可维护,因此,React诞生初期,FB团队就提出了flux这一数据存储分发方案,其中数据存储的地方,我们称之为Store。flux方案中,不同数据存于不同Store,进而优化诞生了Redux,其所有数据均由一个Store统一管理,基于深拷贝的Store对象一经更改,所有组件立刻重新渲染,而得益于React虚拟DOM的优异性能,使得Redux在性能上变得可行。本文将使用另一种数据解决方案:Mobx。
Mobx相对于Redux更易上手,学习成本更低,与Redux全局重新渲染不同的是,Mobx进行局部刷新。
//在app-state.js中:
import {
observable,
computed,
action,
} from 'mobx'
export default class AppState {
constructor({ count, name } = { count: 0, name: 'Jokcy' }) {
this.count = count
this.name = name
}
@observable count
@observable name
@computed get msg() {
return `${this.name} say count is ${this.count}`
}
@action add() {
this.count += 1
}
@action changeName(name) {
this.name = name
}
toJson() {
return {
count: this.count,
name: this.name,
}
}
}
此外需要向React-Router那样在最外层包裹,并在页面入口文件中注入。
注意:当使用装饰器时,需要babel做相关配置,安装相关插件。
API代理
Webapp的数据一般由请求后端API接口获得,一般通过node服务层配合网络请求库(如axios)实现后端API代理,具体实现需根据API文档。
当向后端API通过POST方法请求数据时,为了保证数据对象的“纯洁性”,一般需要使用ES6新方法:Object.assign() 传递一个新对象出去。
SSR优化
当项目架构加入了react-router和store后,SSR需要根据路由返回的新内容进行渲染,且SSR的渲染内容需要被客户端所利用,而不是客户端再次发起一次 HTTP请求,所以需要对SSR进行优化。
//在server-entry.js中,同时这个文件在开发模式下被webpack打包到内存中:
......
import { StaticRouter } from 'react-router-dom'
import { Provider, useStaticRendering } from 'mobx-react'
import { createStoreMap } from './store/store'
// 让mobx在服务端渲染的时候不会重复数据变换,即方法不被重复调用
useStaticRendering(true)
export default (stores, routerContext, url) => (
<Provider {...stores}>
<StaticRouter context={routerContext} location={url}>
<App />
</StaticRouter>
</Provider>
)
//SSR中,需要每次新增一个Store对象,防止每次SSR时,store对象中的数据相互污染
export { createStoreMap }
//store.js中
import AppStateClass from './app-state'
export const AppState = AppStateClass
export default {
AppState,
}
//下面的函数专门用于SSR
export const createStoreMap = () => {
return {
appState: new AppState(),
}
}
上文中,由于SSR端的入口文件已经改变,所以应在dev-static.js中进行如下修改:
......
const routerContext = { }
const app = serverBundle(createStoreMap(), routerContext, req.url)
......
当路由配置有Redirect的时候,react-router会在routerContext加上“url”属性,SSR应检查此属性是否存在,如存在,在Server端直接跳转。
//dev-static.js中,SSR代码后添加:
if(routerContext.url){
res.status(302).setHeader('Location', routerContext.url) //直接跳转
res.end()
return
}
SSR时有时会异步获取数据后,再完成SSR的整个过程,React并没有提供SSR异步方法,这是需要使用工具库"react-async-bootstrapper":
//dev-static.js中
const app = serverBundle(createStoreMap(), routerContext, req.url)
asyncBootstrapper(app).then(() => {
//SSR流程
})
然后在store中通过调用“asyncBootstrapper”方法异步获取数据后,完成余下SSR流程。
此时,Server端已经完成数据渲染,但是client端还需要进行数据同步。
//在app-state.js中有:
......
constructor({ count, name } = { count: 0, name: 'Jokcy' }) {
this.count = count
this.name = name
}
toJson() {
return {
count: this.count,
name: this.name,
}
}
toJson方法使得store对象在SSR后获取最新的数据,然后只需在client初始化时填充此数据并通过constructor方法初始化store,即可完成前后端数据同步。
//在dec-server.js中:
const getStoreState = (stores) => {
return Object.keys(stores).reduce((result, storeName) => {
result[storeName] = stores[storeName].toJson()
return result
}, {})
}
state = getStoreState(stores)//获取SSR后的新数据
最后通过模板引擎(ejs)将数据插入,完成前后端同步。
//在webpack.config.client.js中添加插件:
new HTMLPlugin({
template: '!!ejs-compiled-loader!' + path.join(__dirname, '../client/server.template.ejs'),
filename: 'server.ejs'
})
const html = ejs.render(template, {
appString: content,
initialState: serialize(state), //object转为string
})
res.send(html)
const initialState = window.__INITIAL__STATE__ || {}
......
<Provider testMobx={new MobxStore(initialState.testMobx)}>
......
解决Mobx错误
上述问题表示多个Mobx实例被启动,即通过webpack打包生成的前后两个bundle文件中包含两个Mobx实例:
此时需要在webpack配置文件进行如下改动:
externals: Object.keys(require('../package.json').dependencies),//node环境下,package.json中的依赖可以通过npm install安装,所以不用进行打包。
SSR补充
- 为了优化SEO,在SSR的时候需要设置Header中的相关信息,如title、meta标签等:
const Helmet = require('react-helmet').default
asyncBootstrap(app).then(() => {
......
const helmet = Helmet.rewind()
onst html = ejs.render(template, {
......
meta: helmet.meta.toString(),
title: helmet.title.toString(),
style: helmet.style.toString(),
link: helmet.link.toString(),
......
})
......
}
对应的打包的SSR信息为:
<Helmet>
<title>This is topic list</title>
<meta name="description" content="This is description" />
</Helmet>
- SSR时,服务需要请求本地的地址(127.0.0.1),所以需要在webpack.config.server.js添加如下插件:
plugins: [
new webpack.DefinePlugin({
'process.env.API_BASE': '"http://127.0.0.1:3333"'
})
]
打包优化
在之前的webpack.config中,打包后的文件体积过大,因为这个文件中包含了很多库的全部代码,客户端每次请求的时候都要重新加载,这将无法充分利用浏览器缓存,加重加载压力,这时需要修改webpack.config配置,将不变的内容打一个包,将变化的内容打另一个包:
//在正式环境下
config.entry = {
app: path.join(__dirname, '../client/app.js')
vendor: [
'react',
.......//所有第三方包
]
}
config.output.filename = '[name].[chunkhash],js'
config.plugins.push(
new webpack.optimize.RuntimeChunkPlugin({
name: 'vendor'//使得app引用的包不再打包进app中
}),
new webpack.optimize.RuntimeChunkPlugin({//每次webpack打包生成的代码放于此文件,并无限压缩
name: 'manifast'
}),
new webpack.NamedModulesPlugin(),//为异步加载的模块命名
new webpack.DefinePlugin({
'process.env': JSON.stringify('production')
}),
new webpack.NamedChunksPlugin((chunk) => {
if(chunk.name){
return chunk.name
}
return chunk.mapModules(m => path.relative(m.context, m.request)).join('_')
})
)
完善SSR
此时,开启SSR服务,系统直接报错:
可见此错误基本是由SSR端无法对css提供支持所致,由于SSR端无法提供DOM操作,所以无法直接照搬客户端的webpack配置(即style-loader),不过不用当心,webpack提供了官方的css打包方案:
extract-text-webpack-plugin
,该插件用于在JS(X)中提取CSS,用法如下:
//webpack4.X安装此插件时需要加上@next
const ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader", // 编译后用什么loader来提取css文件
use: "css-loader" // 指需要什么样的loader去编译文件,这里由于源文件是.css所以选择css-loader
})
}
]
},
plugins: [
new ExtractTextPlugin("styles.css"),
]
}
其次,由于我使用的Material-UI,SSR需要做如下配置(官网可查):
//在项目入口处用 MuiThemeProvider 包裹:
import React from 'react'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import getMuiTheme from 'material-ui/styles/getMuiTheme'
const muiTheme = getMuiTheme({
userAgent: 'all'
})
import Routes from './config/router'
export default class extends React.Component {
render() {
return (
<MuiThemeProvider muiTheme={muiTheme}>
<Routes key='routes'/>
</MuiThemeProvider>
)
}
}
SSR处理静态文件
由于在 webpack 配置中指定了output的publicPath为public,所以静态资源路径以/public开头,因此需要做如下解析:
koa
使用koa-static-plus
插件
const koaStaticPlus =require('koa-static-plus')
app.use(koaStaticPlus(path.join(__dirname, '../dist'), {
pathPrefix: '/public' //路径前缀
}))
express
app.use('/public', express.static(path.join(__dirname, '../dist')))
附录
web开发有哪些常用的网络优化方式?
- 合并资源文件,减少HTTP请求
- 压缩资源文件
- 合理利用浏览器缓存,通过计算文件内容得出一个哈希值,并通过前后哈希值是否相同来决定用浏览器缓存资源还是向后端重新发起请求
2.浏览器输入网址回车后到底发生了什么?
- 解析URL
解析的主要内容如下:
传输协议:https
服务器:www
域名:nextsticker.cn
端口:默认80不显示
以及目录、文件名参数等 - DNS解析
查询浏览器缓存
检查系统hosts文件映射
检查路由器缓存
查询ISP的DNS服务器
递归查询:从根域名服务器到顶级域名服务器再到极限域名服务器依次搜索对应目标域名的IP(从右往左) - 浏览器与服务器建立TCP连接(三次握手)
第一次握手:客户端向服务器端发送请求(SYN=1) 等待服务器确认
第二次握手:服务器收到请求并确认,回复一个指令(SYN=1,ACK=1)
第三次握手:客户端收到服务器的回复指令并返回确认(ACK=1) - 传输数据
- 浏览器渲染
Post请求的格式有哪些?
- application/x-www-form-urlencoded
POST http://www.example.com HTTP/1.1
Content-Type: application/x-www-form-urlencoded;charset=utf-8
key=value&testKey=testValue
Content-Type 被指定为 application/x-www-form-urlencoded;提交的数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。大部分服务端语言都对这种方式有很好的支持。很多时候,我们用 Ajax 提交数据时,用的就是这种方式。
- multipart/form-data
我们使用表单上传文件时,必须让 form 的 enctyped 等于这个值。
POST http://www.example.com HTTP/1.1
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryrGKCBY7qhFd3TrwA
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="text"
title
------WebKitFormBoundaryrGKCBY7qhFd3TrwA
Content-Disposition: form-data; name="file"; filename="chrome.png"
Content-Type: image/png
PNG ... content of chrome.png ...
------WebKitFormBoundaryrGKCBY7qhFd3TrwA--
首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。然后 Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 –boundary 开始,紧接着内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。消息主体最后以 –boundary– 标示结束。
- application/json
由于 JSON 规范的流行,除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify,服务端语言也都有处理 JSON 的函数,其更方便提交复杂的结构化数据,特别适合 RESTful 的接口。 - text/xml
reactSSR异步获取数据时请求80端口的解决方案
配置SSR时,通过react组件react-async-bootstrappe
异步获取数据时,意外请求了本地的80端口,可是我的SSR地址为3333端口,API接口地址为3000端口。这是为什么呢?
因为react-async-bootstrappe
提供的方法请求数据时,通过node url parse 去解析/api/admin/all,然后再传给相应的如 http request模块,其默认就是80端口,所以将请求发送给了127.0.0.1:80。
至于解决方法,很简单,将来自react-async-bootstrappe
的请求地址配置为http://localhost:3333/api/admin/all
,即完整地址即可。