基于React16+Webpack4搭建前端工作流

前言

前端技術(shù)發(fā)展至今,各種框架技術(shù)層出不窮,前后端分離的開發(fā)模式也大行其道,如今,前端大多以webapp的形式存在,很多后端的工作都開始拿到前端來做,而后端只需要提供負責(zé)請求數(shù)據(jù)的API接口,前端工作開始變得龐雜,關(guān)注點由單維的頁面視效發(fā)展成了路由、測試、效率等多個方面,注意力無法聚焦,這直接導(dǎo)致了開發(fā)方式的升級換代,本文將以Webpack構(gòu)建項目工程為切入點,通過工程架構(gòu)和項目架構(gòu),以點帶面地探索一下前端項目的開發(fā)流程及其發(fā)展趨勢。

webapp架構(gòu)

在webapp模式中,前端通過后端API請求大量數(shù)據(jù),之后完成頁面渲染、頁面跳轉(zhuǎn)等行為,前端內(nèi)容多為JS代碼,甚至很少有寫HTML的行為,即使有也有很大可能作為JS代碼的一部分存在,前端變得越來越復(fù)雜,這就要求考慮其架構(gòu)問題。
一般來講,webapp的架構(gòu)分為工程架構(gòu)和項目架構(gòu)。拿工程架構(gòu)來講,主要有以下幾個方面:通過工程腳手架的搭建來解放生產(chǎn)力(即各種自動行為);通過合理的技術(shù)選型來完成項目解決方案;通過制定代碼規(guī)范(codelint,editconfig和git commit預(yù)處理)來保證團隊合作及項目質(zhì)量。此外,工程架構(gòu)需要突出‘可定制性’,并需要預(yù)期可能出現(xiàn)的問題并提前準備解決方案;其次是項目架構(gòu),主要是通過技術(shù)選型、數(shù)據(jù)解決方案、代碼風(fēng)格、合理的目錄結(jié)構(gòu)來實現(xiàn)。

為什么選擇Webpack

我們需要一款可定制的自動化工具,使得注意力更加聚焦于業(yè)務(wù)自身,而不是其他(源碼預(yù)處理、自動打包、自動更新頁面顯示、自動處理圖片依賴等),進而提高開發(fā)效率。

項目目錄結(jié)構(gòu)

client
--app.js//應(yīng)用入口
--App.jsx//頁面內(nèi)容
--sever-entry.js//用于SSR
--template.html
build
--webpack.config.client.js
--webpack.config.sever.js//用于SSR打包配置
sever//web服務(wù),用于SSR
dist //系統(tǒng)生成的文件夾,用于存放打包JS文件
package.json
.babelrc //babel配置文件

Webpack基礎(chǔ)配置

在webpack.config.client.js中最基礎(chǔ)的對象為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' //靜態(tài)資源引用時的路徑,用于區(qū)分URL是靜態(tài)資源還是API請求,為路徑前綴    
    }
}

webpack最基礎(chǔ)的如上,這時使用webpack --config build/webpack.config.client.js即可完成打包。

Webpack loader基礎(chǔ)配置

Webpack的核心是打包,而其靈魂是loader!當(dāng)項目為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高級語法暫時不能被瀏覽器執(zhí)行,需要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頁面,并根據(jù)Webpack配置插入資源
]

以上便完成了最為基礎(chǔ)的基于webpack的ReactWebApp。

Webpack SSR基礎(chǔ)配置

在Webapp開發(fā)模式中,頁面內(nèi)容由瀏覽器渲染,而為了SEO和縮短首屏?xí)r間,SSR成為了優(yōu)化的必選項。React提供了react-dom插件用來解決SSR問題。即在node環(huán)境中完成渲染,然后返回給瀏覽器一些可以顯示的HTML內(nèi)容。

//sever-entry.js中
import React from 'react'
import App from './App.jsx'
export default <App />

上面新建代碼文件(首屏代碼),可為服務(wù)端所用,由于代碼無法直接運行在node端,所以下一步還需要Webpack進行服務(wù)端打包配置。

target: 'node',
entry:{
      app: path.join(__dirname, '../client/sever-entry.js')
}
output: {
  filename: 'sever-entry.js',//導(dǎo)出文件配置成SSR專用文件
  path: path.join(__dirname, '../dist/'),
  publicPath: '',
  libraryTarget: 'commonjs2'//適用于nodeSSR的模塊打包規(guī)范
}

配置好后,前后端分別打包(可用腳本配置),即可生成一個HTML頁面,2個JS文件(前后端各一個),通過express提供中間層SSR服務(wù),通過’react-dom/server‘ 模塊提供的SSR方法進行服務(wù)端渲染,即將SSR的內(nèi)容插入到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'))) //處理靜態(tài)文件
app.get(' * ', function(req, res){
  res.send(template.replace('<app><app/>', ReactSSR.renderToString(serverEntry)))
})
app.listen(1234)

上面為最簡單的SSR,而完整的SSR流程是把SSR的內(nèi)容替換到模板HTML頁面的<App/>中,再將整個頁面返回到客戶端瀏覽器:這時整個SSR算是完整走通了。

Webpack-dev-server配置

開發(fā)過程中,可以使用webpack提供的一些組件來提高開發(fā)效率。

const isDev = process.env.NODE_ENV === 'development'//判斷是否為開發(fā)環(huán)境,通過啟動命令加以區(qū)分。一般情況下,dev-server會配置在開發(fā)環(huán)境下。
//webpack-dev-sever在dist目錄下啟動服務(wù)器
devServer :{
  host: '0.0.0.0',//可以通過多種方式訪問
  port: 8888,
  contentBase: path.join(__dirname, '../dist/'),//靜態(tài)文件目錄
  hot: true,//需要在react中配置相關(guān)熱更新模塊
  overlay: {//顯示錯誤信息
    errors: true
  },
publicPath: '/public',//訪問靜態(tài)文件時均要加上這個路徑前綴
historyApiFallback: {
    index: './public/index.html'
  }
}

其中,需要用模塊cross-env來解決跨平臺的環(huán)境變量問題。

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頁面,并根據(jù)Webpack配置插入資源
  new webpackHotModuleReplacementPlugin()//熱更新插件
]

為了使熱更新生效,需要在入口添加新的打包文件:

entry:{
 app: ['react-hot-loader/patch', //熱更新要用到的內(nèi)容
        path.join(__dirname, '../client/app.js')//使用絕對路徑]
}

此時,修改代碼便可以完成無刷新的熱更新更能。

開發(fā)環(huán)境下的SSR

在開發(fā)環(huán)境下配置SSR時,需要使用客戶端的模板文件和服務(wù)端的bundle文件,所以在開發(fā)環(huán)境下的SSR配置重點分為兩塊:由于開發(fā)環(huán)境下沒有靜態(tài)文件生成,不能直接通過路徑獲取,可以在webpack-dev-server中通過網(wǎng)絡(luò)請求獲得模板文件;通過webpack和對應(yīng)配置文件獲得存放于內(nèi)存中的bundle.js文件內(nèi)容:

//通過向webpack-dev-sever動態(tài)請求模板文件
const getTemplate = () => {
  return new Promise((resolve, reject) => {
    axios.get('http://localhost:8888/public/index.html')
      .then(res => {
        resolve(res.data)
      })
      .catch(reject)
  })
}
//通過webpack API監(jiān)聽bundle.js文件變化并寫入內(nèi)存,并以模塊導(dǎo)出,在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配置項,直接寫入內(nèi)存
let serverBundle
serverCompiler.watch({}, (err, stats) => {//監(jiān)聽文件變化
  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內(nèi)容轉(zhuǎn)化成模塊
  serverBundle = m.exports.default
})
//最后通過express路由完成SSR
const proxy = require('http-proxy-middleware')//express的請求代理插件

app.use('/public', proxy({targ: 'http://localhost: 8888'}))//處理靜態(tài)文件

app.get('*', function (req, res) {
    getTemplate().then(emplate => {
      const content ReactDomSever.rendToString(serverBundle)
      res.send(template.replace('<!--app-->', content))
    })
  })

eslink與editconfig

以上,基本完成了一個webapp項目的工程腳手架的搭建。而為了有利于團隊合作和提高開發(fā)效率,有必要用eslink和editconfig來對代碼進行有效規(guī)范。

//在clint目錄下新建的.eslintrc文件中
{
  "parser": "babel-eslint",
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  "extends": "airbnb",
  "rules": {
      "semi": [0]//不寫分號
    }
}

這樣,在每次代碼編譯前,需要先檢查代碼規(guī)范,若有規(guī)范錯誤,則停止繼續(xù)編譯。

//在webpack配置文件中的rules添加如下:
{
     enforce: 'pre',
     test: /.(js|jsx)$/,
     loader: 'eslint-loader',
     exclude: [
     path.resolve(__dirname, '../node_modules')
     ]
}

此外,還需要配置editconfig來規(guī)避由于系統(tǒng)差異帶來的某些錯誤。

//在根目錄下新建的.editconfig文件中:
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

目前為止可以優(yōu)化的點

  1. 首先,webpack配置文件中的公共部分可以提取出來,可使解構(gòu)更清晰,更利于代碼閱讀。
const webpackMerge = require('webpack-merge') 
const baseConfig = require('./webpack.base') //webpack配置的公共部分

const config = webpackMerge(baseConfig, {
  //非公共配置
})
  1. 處理favicon.ico(相關(guān)組件請去Github上搜)。
  2. 通過nodemon監(jiān)聽文件變化自動重啟服務(wù)。
//在根目錄下的nodemon.json文件中配置
{
  "restartable": "rs",
  "ignore": [
    ".git",
    "node_modules/**/node_modules",
    ".eslintrc",
    "client",
    "build"
  ],
  "env": {
    "NODE_ENV": "development"
  },
  "verbose": true,
  "ext": "js"
}

此時,項目工程架構(gòu)完畢。

項目架構(gòu)

views //此目錄用于存放項目功能模塊,子目錄的劃分取決于路由
--topic
----index.jsx //每個頁面的入口文件
config //配置,路由,第三方類庫等
--router.js
store // 數(shù)據(jù)相關(guān)
--app-state.js
--store.js
components // 非業(yè)務(wù)組件和共用組件

配置路由

路由用于頁面跳轉(zhuǎn),HTML5中的history api能夠在URL發(fā)生變化的時候被JS監(jiān)聽,進而通過JS向后臺API請求數(shù)據(jù),最終呈現(xiàn)新的頁面出來,而在這個過程中history api會阻止瀏覽器刷新頁面的行為,在history api出現(xiàn)之前也可以用hash來完成此操作。
在React項目中,當(dāng)然要使用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中新增數(shù)組寫法
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 />方能正常使用。
此時,當(dāng)出現(xiàn)如下問題時:


解析出錯

可以在webpack配置文件中添加:

resolve: {
  extensions: ['.js', '.jsx']
}//可以在導(dǎo)入模塊時忽略后綴名

Store配置

眾所周知,React的數(shù)據(jù)流向只能從父組件向子組件傳遞,顧名思義為單向數(shù)據(jù)流,但是在實際情況下,有時需要子組件通過回調(diào)函數(shù)向父組件傳遞數(shù)據(jù),當(dāng)項目龐大,組件嵌套過深時,這種引用回調(diào)的方式將變得不可維護,因此,React誕生初期,F(xiàn)B團隊就提出了flux這一數(shù)據(jù)存儲分發(fā)方案,其中數(shù)據(jù)存儲的地方,我們稱之為Store。flux方案中,不同數(shù)據(jù)存于不同Store,進而優(yōu)化誕生了Redux,其所有數(shù)據(jù)均由一個Store統(tǒng)一管理,基于深拷貝的Store對象一經(jīng)更改,所有組件立刻重新渲染,而得益于React虛擬DOM的優(yōu)異性能,使得Redux在性能上變得可行。本文將使用另一種數(shù)據(jù)解決方案:Mobx。
Mobx相對于Redux更易上手,學(xué)習(xí)成本更低,與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那樣在最外層包裹,并在頁面入口文件中注入。
注意:當(dāng)使用裝飾器時,需要babel做相關(guān)配置,安裝相關(guān)插件。

API代理

Webapp的數(shù)據(jù)一般由請求后端API接口獲得,一般通過node服務(wù)層配合網(wǎng)絡(luò)請求庫(如axios)實現(xiàn)后端API代理,具體實現(xiàn)需根據(jù)API文檔。
當(dāng)向后端API通過POST方法請求數(shù)據(jù)時,為了保證數(shù)據(jù)對象的“純潔性”,一般需要使用ES6新方法:Object.assign() 傳遞一個新對象出去。

SSR優(yōu)化

當(dāng)項目架構(gòu)加入了react-router和store后,SSR需要根據(jù)路由返回的新內(nèi)容進行渲染,且SSR的渲染內(nèi)容需要被客戶端所利用,而不是客戶端再次發(fā)起一次 HTTP請求,所以需要對SSR進行優(yōu)化。

//在server-entry.js中,同時這個文件在開發(fā)模式下被webpack打包到內(nèi)存中:
......
import { StaticRouter } from 'react-router-dom'
import { Provider, useStaticRendering } from 'mobx-react'
import { createStoreMap } from './store/store'
// 讓mobx在服務(wù)端渲染的時候不會重復(fù)數(shù)據(jù)變換,即方法不被重復(fù)調(diào)用
useStaticRendering(true)

export default (stores, routerContext, url) => (
  <Provider {...stores}>
    <StaticRouter context={routerContext} location={url}>
      <App />
    </StaticRouter>
  </Provider>
)
//SSR中,需要每次新增一個Store對象,防止每次SSR時,store對象中的數(shù)據(jù)相互污染
export { createStoreMap }
//store.js中
import AppStateClass from './app-state'

export const AppState = AppStateClass

export default {
  AppState,
}
//下面的函數(shù)專門用于SSR
export const createStoreMap = () => {
  return {
    appState: new AppState(),
  }
}

上文中,由于SSR端的入口文件已經(jīng)改變,所以應(yīng)在dev-static.js中進行如下修改:

......
const routerContext = { }
const app = serverBundle(createStoreMap(), routerContext, req.url)
......

當(dāng)路由配置有Redirect的時候,react-router會在routerContext加上“url”屬性,SSR應(yīng)檢查此屬性是否存在,如存在,在Server端直接跳轉(zhuǎn)。

//dev-static.js中,SSR代碼后添加:
if(routerContext.url){
  res.status(302).setHeader('Location', routerContext.url) //直接跳轉(zhuǎn)
  res.end()
  return
}

SSR時有時會異步獲取數(shù)據(jù)后,再完成SSR的整個過程,React并沒有提供SSR異步方法,這是需要使用工具庫"react-async-bootstrapper":

//dev-static.js中
const app = serverBundle(createStoreMap(), routerContext, req.url)
asyncBootstrapper(app).then(() => {
  //SSR流程
})

然后在store中通過調(diào)用“asyncBootstrapper”方法異步獲取數(shù)據(jù)后,完成余下SSR流程。
此時,Server端已經(jīng)完成數(shù)據(jù)渲染,但是client端還需要進行數(shù)據(jù)同步。

//在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后獲取最新的數(shù)據(jù),然后只需在client初始化時填充此數(shù)據(jù)并通過constructor方法初始化store,即可完成前后端數(shù)據(jù)同步。

//在dec-server.js中:
const getStoreState = (stores) => {
  return Object.keys(stores).reduce((result, storeName) => {
    result[storeName] = stores[storeName].toJson()
    return result
  }, {})
}

state = getStoreState(stores)//獲取SSR后的新數(shù)據(jù)

最后通過模板引擎(ejs)將數(shù)據(jù)插入,完成前后端同步。

//在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轉(zhuǎn)為string
})
res.send(html)
const initialState = window.__INITIAL__STATE__ || {}
......
<Provider testMobx={new MobxStore(initialState.testMobx)}>
......

解決Mobx錯誤

Mobx error

上述問題表示多個Mobx實例被啟動,即通過webpack打包生成的前后兩個bundle文件中包含兩個Mobx實例:


Mobx error

此時需要在webpack配置文件進行如下改動:

externals: Object.keys(require('../package.json').dependencies),//node環(huán)境下,package.json中的依賴可以通過npm install安裝,所以不用進行打包。

SSR補充

  1. 為了優(yōu)化SEO,在SSR的時候需要設(shè)置Header中的相關(guān)信息,如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(),
        ......
      })
......
}

對應(yīng)的打包的SSR信息為:

<Helmet>
  <title>This is topic list</title>
  <meta name="description" content="This is description" />
</Helmet>
  1. SSR時,服務(wù)需要請求本地的地址(127.0.0.1),所以需要在webpack.config.server.js添加如下插件:
plugins: [
  new webpack.DefinePlugin({
    'process.env.API_BASE': '"http://127.0.0.1:3333"'
  })
]

打包優(yōu)化

在之前的webpack.config中,打包后的文件體積過大,因為這個文件中包含了很多庫的全部代碼,客戶端每次請求的時候都要重新加載,這將無法充分利用瀏覽器緩存,加重加載壓力,這時需要修改webpack.config配置,將不變的內(nèi)容打一個包,將變化的內(nèi)容打另一個包:

//在正式環(huán)境下
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服務(wù),系統(tǒng)直接報錯:

錯誤信息

可見此錯誤基本是由SSR端無法對css提供支持所致,由于SSR端無法提供DOM操作,所以無法直接照搬客戶端的webpack配置(即style-loader),不過不用當(dāng)心,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需要做如下配置(官網(wǎng)可查):

//在項目入口處用 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處理靜態(tài)文件

由于在 webpack 配置中指定了output的publicPath為public,所以靜態(tài)資源路徑以/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開發(fā)有哪些常用的網(wǎng)絡(luò)優(yōu)化方式?

  • 合并資源文件,減少HTTP請求
  • 壓縮資源文件
  • 合理利用瀏覽器緩存,通過計算文件內(nèi)容得出一個哈希值,并通過前后哈希值是否相同來決定用瀏覽器緩存資源還是向后端重新發(fā)起請求

2.瀏覽器輸入網(wǎng)址回車后到底發(fā)生了什么?

  1. 解析URL
    解析的主要內(nèi)容如下:
    傳輸協(xié)議:https
    服務(wù)器:www
    域名:nextsticker.cn
    端口:默認80不顯示
    以及目錄、文件名參數(shù)等
  2. DNS解析
    查詢?yōu)g覽器緩存
    檢查系統(tǒng)hosts文件映射
    檢查路由器緩存
    查詢ISP的DNS服務(wù)器
    遞歸查詢:從根域名服務(wù)器到頂級域名服務(wù)器再到極限域名服務(wù)器依次搜索對應(yīng)目標域名的IP(從右往左)
  3. 瀏覽器與服務(wù)器建立TCP連接(三次握手)
    第一次握手:客戶端向服務(wù)器端發(fā)送請求(SYN=1) 等待服務(wù)器確認
    第二次握手:服務(wù)器收到請求并確認,回復(fù)一個指令(SYN=1,ACK=1)
    第三次握手:客戶端收到服務(wù)器的回復(fù)指令并返回確認(ACK=1)
  4. 傳輸數(shù)據(jù)
  5. 瀏覽器渲染

Post請求的格式有哪些?

  1. 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;提交的數(shù)據(jù)按照 key1=val1&key2=val2 的方式進行編碼,key 和 val 都進行了 URL 轉(zhuǎn)碼。大部分服務(wù)端語言都對這種方式有很好的支持。很多時候,我們用 Ajax 提交數(shù)據(jù)時,用的就是這種方式。

  1. 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 用于分割不同的字段,為了避免與正文內(nèi)容重復(fù),boundary 很長很復(fù)雜。然后 Content-Type 里指明了數(shù)據(jù)是以 mutipart/form-data 來編碼,本次請求的 boundary 是什么內(nèi)容。消息主體里按照字段個數(shù)又分為多個結(jié)構(gòu)類似的部分,每部分都是以 –boundary 開始,緊接著內(nèi)容描述信息,然后是回車,最后是字段具體內(nèi)容(文本或二進制)。如果傳輸?shù)氖俏募?,還要包含文件名和文件類型信息。消息主體最后以 –boundary– 標示結(jié)束。

  1. application/json
    由于 JSON 規(guī)范的流行,除了低版本 IE 之外的各大瀏覽器都原生支持 JSON.stringify,服務(wù)端語言也都有處理 JSON 的函數(shù),其更方便提交復(fù)雜的結(jié)構(gòu)化數(shù)據(jù),特別適合 RESTful 的接口。
  2. text/xml

reactSSR異步獲取數(shù)據(jù)時請求80端口的解決方案

配置SSR時,通過react組件react-async-bootstrappe異步獲取數(shù)據(jù)時,意外請求了本地的80端口,可是我的SSR地址為3333端口,API接口地址為3000端口。這是為什么呢?

錯誤信息

因為react-async-bootstrappe提供的方法請求數(shù)據(jù)時,通過node url parse 去解析/api/admin/all,然后再傳給相應(yīng)的如 http request模塊,其默認就是80端口,所以將請求發(fā)送給了127.0.0.1:80。
至于解決方法,很簡單,將來自react-async-bootstrappe的請求地址配置為http://localhost:3333/api/admin/all,即完整地址即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄第1章 webpack簡介 11.1 webpack是什么? 11.2 官網(wǎng)地址 21.3 為什么使用 web...
    lemonzoey閱讀 1,816評論 0 1
  • 本人是去年 7-8月開始準備面試,過五關(guān)斬六將,最終在年末抱得網(wǎng)易歸,深深感受到高級前端面試的套路。以下是自己整理...
    前端一菜鳥閱讀 2,666評論 1 43
  • 理解javascript中的MVC MVC模式是軟件工程中一種軟件架構(gòu)模式,一般把軟件模式分為三部分,模型(Mod...
    深沉的簡單閱讀 541評論 0 0
  • 前天爸爸帶著我和一個伯伯去趕海。 我和伯伯在一個地方撿,爸爸在另一個地方撿。 我們講了好多小螃蟹,小蝦、小魚和一只...
    青青蝸牛尹奕元閱讀 232評論 1 1
  • 你身邊有很多人戴著面具,大部分帶的是笑臉面具。你很難從身邊找到幾個不戴面具的,有,也只不過是你父母,或者是你最好的...
    袁七罪閱讀 88評論 0 0

友情鏈接更多精彩內(nèi)容