基於webpack的先後端分離開發環境實戰

背景

隨着互聯網應用工程規模的日益複雜化和精細化,咱們在開發一個標準web應用的早已開始告別單幹模式,爲了提高開發效率,先後端分離的需求愈來愈被重視,前端負責展示/交互邏輯,後端負責業務/數據接口,基本上也成爲了咱們平常項目分工中的標配,可是先後端分離一直以來都是一個工程概念,每一個團隊在實現工程中都會基於自身的技術棧選擇和開發環境進行具體的實現,本文便根據自身團隊在webapck開發中搭建的先後端分離開發環境進行部分敘述。javascript

理想化的先後端分離環境

目前業界比較有表明性的先後端分離的例子是SPA(Single-page application),全部用到的展示數據都是後端經過異步接口(AJAX/JSONP/WEBSOCKET)的方式提供的,現現在最火的前端框架如:React, Vue,Angular等也都推薦採用SPA的模式進行開發而且從組件化,數據流,狀態容器再到網絡請求,單頁路由等都給出了完善的全家桶方案。從某種意義上來講,SPA確實作到了先後端分離,但這種方式存在以下幾個亟待問題:css

  • 前端開發本地開發環境下該如何突破域的限制和服務端接口進行通訊?html

  • 一條命令,可否同時完成webpack和node sever兩個進程的啓動?前端

  • 開發環境下的前端資源路徑應該如何配置?java

  • mock數據應該怎麼作?node

  • 打包構建後的文件可否直接預覽效果?webpack

針對以上的問題,咱們來看看怎樣利用webpack現有的一些機制和藉助node的環境搭配來進行逐個擊破,具體設計以下:nginx

## 不得不提的webpack-dev-

因而可知,咱們理想化的開發環境應根據具有如下幾點要求:git

  • 操做夠簡單,拉下代碼後,只須要記住僅有的幾個命令就能直接進入開發狀態github

  • 解耦夠完全,開發者只須要修改路由配置表就能無縫在多個請求接口中靈活切換

  • 資源夠清晰,全部的開發資源都能到精確可控,同時支持一鍵打包構建,單頁和多頁模式可並存

  • 配置夠靈活,能夠根據自身項目的實際狀況靈活添加各種中間件,擴展模塊和第三方插件

不得不提的webpack-dev-server

webpack自己的定位是一個資源管理和打包構建工做,自己的強大之處在於對各類靜態資源的依賴分析和預編譯,在實際開發中官方還推薦了一個快速讀取webpack配置的server環境webpack-dev-server,官方的介紹是:"Use webpack with a development server that provides live reloading. The webpack-dev-server is a little Node.js Express server, which uses the webpack-dev-middleware to serve a webpack bundle. It also has a little runtime which is connected to the server via Sock.js.",一個適用於開發環境的,基於express + webpack-dev-middleware實現的,支持實時更新,內存構建資源的開發服務器,經過簡單的配置便可知足webpack開發環境中的一系列需求,可是當咱們的開發環境日趨複雜和多樣的時候,不只須要對自定義配置的細節靈活可控,同時須要對進行加入各類第三方的插件進行功能擴展,才能最大程度的發揮webpack環境中的威力。

打造項目專屬的前端開發環境

有了理想環境下的的述求,也瞭解到了webpack-dev-server的實現精髓,那麼,咱們就能夠一步步地來打造專屬自身的開發環境:

一 、藉助node和http-proxy實現跨域通訊

先後端分離開發中,本地前端開發調用接口會有跨域問題,通常有如下幾種解決方法:

  • 直接啓動服務端項目,再將項目中的資源url指向到前端服務中的靜態資源地址,好處在於由於始終在服務端的環境中進行資源調試,不存在接口的跨域訪問問題,可是缺陷也比較明顯,須要同時啓動兩套環境,還須要藉助nginx,charles等工具進行資源地址的代理轉發,配置比較繁瑣,對開發者對網絡的理解和環境配置要求較高,資源開銷也大;

  • CORS跨域:後端接口在返回的時候,在header中加入'Access-Control-Allow-origin':* 等配置,利用跨域資源共享實現跨域,前端部分只要求支持xhr2標準的瀏覽器,可是服務端在請求頭中須要在header中作響應頭配置,在必定程度上仍是對服務端的接口設置有必定的依賴;

  • http-proxy:用nodejs搭建本地http服務器,而且判斷訪問接口URL時進行轉發,因爲利用了http-proxy代理的模式進行了轉發,採用的是服務對服務的模式,能較爲完美解決本地開發時候的跨域問題,也是本文中推薦的方式,配置以下:

一、搭建node和http-proxy環境

npm install express # express做爲node基礎服務框架
npm install http-proxy-middleware # http-proxy的express中間件
npm install body-parser # bodyParser中間件用來解析http請求體
npm install querystring    # querystring用來字符串化對象或解析字符串

工程項目下能夠新建一個server的文件夾放置node資源,以下所示:

server  
    ├── main.js
    ├── proxy.config.js
    ├── routes
    └── views

二、編寫代理配置腳本:

proxy.config.js中能夠配置對應須要代理的url和目標url,以下:

const proxy = [
  {
    url: '/back_end/auth/*',
    target: 'http://10.2.0.1:8351'
  },
  {
    url: '/back_end/*',
    target: 'http://10.2.0.1:8352'
  }
];
module.exports = proxy;

main.js中的配置以下:

const express = require('express')
const bodyParser = require('body-parser')
const proxy = require('http-proxy-middleware')
const querystring = require('querystring')

const app = express()

// make http proxy middleware setting
const createProxySetting = function (url) {
  return {
    target: url,
    changeOrigin: true,
    headers: {
      Accept: 'application/json',
      'X-Requested-With': 'XMLHttpRequest'
    },
    onProxyReq: function (proxyReq, req) {
      if (req.method === 'POST' && req.body) {
        const bodyData = querystring.stringify(req.body)
        proxyReq.write(bodyData)
      }
    }
  }
}

// parse application/json
app.use(bodyParser.json())

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

// proxy
proxyConfig.forEach(function (item) {
   app.use(item.url, proxy(createProxySetting(item.target)))
})

// eg: http://127.0.0.1:3000/back_end/oppor => http://10.2.0.1:8352/back_end/oppor

經過以上的配置咱們就能輕鬆將指定url下的請求自動轉發到匹配成功的目標接口下。

> NODE_ENV=development node ./server/main.js

isDebug: true
[HPM] Proxy created: /  ->  http://10.2.0.1:8351
[HPM] Proxy created: /  ->  http://10.2.0.1:8352
Listening at 192.168.1.104:3000

webpack built d558389f7a9a453af17f in 2018ms
Hash: d558389f7a9a453af17f
Version: webpack 1.14.0
Time: 2018ms

2、將webpack配置和node server進程打通

一、解耦webpack中的配置

因爲webpack在開發和生產環境中常常須要作各類配置的切換,官方也提供了DefinePlugin來進行環境參數設置,可是大量的判斷語句侵入webpack.config中其實會致使代碼的可讀性和複用性變差,也容易形成代碼冗餘,咱們在此能夠對配置文件進行重構,將以前的webpack配置文件拆解成了webpack.config.js,project.config.js和environments.config.js三個文件,三個文件各司其職,又可互相協做,減小維護成本,以下:

  • environments.config.js: 主要的做用就是存放在特定環境下的須要變化的配置參數,包含有:publicpath, devtools, wanings,hash等

  • project.config.js:主要的做用是存放於項目有關的基礎配置,如:server,output,loader,externals,plugin等基礎配置;經過一個overrides實現對environments中的配置信息重載。

  • webpack.config.js:主要是讀取project.config.js中的配置,再按標準的webpack字段填入project中的配置信息,原則上是該文件的信息只與構建工具備關,而與具體的項目工程無關,能夠作到跨項目間複用。

config 
    ├── environments.config.js
    ├── project.config.js
    └── webpack.config.js

environments.config.js中的關鍵實現:

// Here is where you can define configuration overrides based on the execution environment.
// Supply a key to the default export matching the NODE_ENV that you wish to target, and
// the base configuration will apply your overrides before exporting itself.
module.exports = {
  // ======================================================
  // Overrides when NODE_ENV === 'development'
  // ======================================================
  development : (config) => ({
    compiler_public_path : `http://${config.server_host}:${config.server_port}/`
  }),

  // ======================================================
  // Overrides when NODE_ENV === 'production'
  // ======================================================
  production : (config) => ({
    compiler_base_route      : '/apps/',
    compiler_public_path     : '/static/',
    compiler_fail_on_warning : false,
    compiler_hash_type       : 'chunkhash',
    compiler_devtool         : false,
    compiler_stats           : {
      chunks       : true,
      chunkModules : true,
      colors       : true
    }
  })
}

project.config.js中的關鍵實現:

// project.config.js
const config = {
  env : process.env.NODE_ENV || 'development',

  // ----------------------------------
  // Project Structure
  // ----------------------------------
  path_base  : path.resolve(__dirname, '..'),
  dir_client : 'src',
  dir_dist   : 'dist',
  dir_public : 'public',
  dir_server : 'server',
  dir_test   : 'tests',

  // ----------------------------------
  // Server Configuration
  // ----------------------------------
  server_host : ip.address(), // use string 'localhost' to prevent exposure on local network
  server_port : process.env.PORT || 3000,

  // ----------------------------------
  // Compiler Configuration
  // ----------------------------------
  compiler_devtool         : 'source-map',
  compiler_hash_type       : 'hash',
  compiler_fail_on_warning : false,
  compiler_quiet           : false,
  compiler_public_path     : '/',
  compiler_stats           : {
    chunks : false,
    chunkModules : false,
    colors : true
  }
};

// 在此經過讀取環境變量讀取environments中對應的配置項,對前面的配置項進行覆蓋
const environments = require('./environments.config')
const overrides = environments[config.env]
if (overrides) {
  debug('Found overrides, applying to default configuration.')
  Object.assign(config, overrides(config))
} else {
  debug('No environment overrides found, defaults will be used.')
}
module.exports = config

webpack.config.js中的關鍵實現:

const webpack = require('webpack')
const project = require('./project.config')
const debug = require('debug')('app:config:webpack')
const UglifyJsParallelPlugin = require('webpack-uglify-parallel')

const __DEV__ = project.globals.__DEV__
const __PROD__ = project.globals.__PROD__

const webpackConfig = {
  name    : 'client',
  target  : 'web',
  devtool : project.compiler_devtool,
  resolve : {
    modules: [project.paths.client(), 'node_modules'],
    extensions: ['.web.js', '.js', '.jsx', '.json']
  },
  module : {}
}

if (__DEV__) {
  debug('Enabling plugins for live development (HMR, NoErrors).')
  webpackConfig.plugins.push(
      new webpack.HotModuleReplacementPlugin()
  )
} else if (__PROD__) {
  debug('Enabling plugins for production (UglifyJS).')
  webpackConfig.plugins.push(
    new webpack.optimize.OccurrenceOrderPlugin(),
    new webpack.optimize.DedupePlugin(),
    new UglifyJsParallelPlugin({
      workers: os.cpus().length,
      mangle: true,
      compressor: {
        warnings: false,
        drop_debugger: true,
        dead_code: true
      }
    })
  )
}

由此可知,三者間的注入關係以下:

environments -> project -> webpack

二、整合webpack在開發環境中依賴的中間件

參考webapck-dev-server中的實現,咱們能夠將webpack-dev-middleware和webpack-hot-middleware加入到咱們的express配置中,

npm install webpack-dev-middleware
npm install webpack-hot-middleware

具體配置以下:

const express = require('express')
const debug = require('debug')('app:server')
const webpack = require('webpack')
const webpackConfig = require('../config/webpack.config')
const project = require('../config/project.config')

const app = express()

// ------------------------------------
// Apply Webpack HMR Middleware
// ------------------------------------
if (project.env === 'development') {
  const compiler = webpack(webpackConfig)

  debug('Enabling webpack dev and HMR middleware')
  app.use(require('webpack-dev-middleware')(compiler, {
    publicPath  : webpackConfig.output.publicPath,
    contentBase : project.paths.client(),
    hot         : true,
    quiet       : project.compiler_quiet,
    noInfo      : project.compiler_quiet,
    lazy        : false,
    stats       : project.compiler_stats
  }))
  
  // webpack_hmr
  app.use(require('webpack-hot-middleware')(compiler, {
    path: '/__webpack_hmr'
  }))

  // proxy
  ....... 
}

module.exports = app.listen(project.server_port, function (err) {
  if (err) {
    console.log(err)
    return
  }
  var uri = project.server_host + ':' + project.server_port
  console.log('Listening at ' + uri + '\n')
});

這樣當咱們執行下述的時候,就能一鍵完成webpack基礎配置,熱更新以及epxress服務的啓動,而且能夠徹底根據express的配置說明來自定義擴展咱們的前端開發資源。

ENV=development node ./bin/dev-server.js

3、前端資源路徑設計

實際開發中,全部涉及到的前端資源咱們進行歸類通常會有以下幾種:

  • html:html頁面,結合到服務後通常稱爲模板資源,是全部資源的入口和結果呈現頁;

  • js:javascript執行腳本資源,基於現代Javascript框架開發後一般還須要藉助babel,typescript等進行編譯處理,分爲build先後build後兩套代碼;

  • css:樣式資源,若是採用了less,sass等工具處理後會也會從.less和.sass編譯成.css文件;

  • static: 靜態資源,一般會包含有font,image,audio,video等靜態文件,結合到服務框架中通常須要設定特定的訪問路徑,直接讀取文件加載。

在wepback的配置中,前端資源路徑咱們一般是藉助path和publicPath
對構建出來的前端資源進行索引,因爲webpack採用了基於內存構建的方式,path一般用來用來存放打包後文件的輸出目錄,publicPath則用來指定資源文件引用的虛擬目錄,具體示例以下:

module.exports = {
  entry: path.join(__dirname,"src","entry.js"),
  output: {
    /*
        webpack-dev-server環境下,path、publicPath、--content-base 區別與聯繫
        path:指定編譯目錄而已(/build/js/),不能用於html中的js引用。
        publicPath:虛擬目錄,自動指向path編譯目錄(/assets/ => /build/js/)。html中引用js文件時,必須引用此虛擬路徑(但實際上引用的是內存中的文件,既不是/build/js/也不是/assets/)。
        --content-base:必須指向應用根目錄(即index.html所在目錄),與上面兩個配置項毫無關聯。
        ================================================
        發佈至生產環境:
        1.webpack進行編譯(固然是編譯到/build/js/)
        2.把編譯目錄(/build/js/)下的文件,所有複製到/assets/目錄下(注意:不是去修改index.html中引用bundle.js的路徑)
    */
    path: path.join(__dirname,"build","js"),
    publicPath: "/assets/",
    //publicPath: "http://cdn.com/assets/",//你也能夠加上完整的url,效果與上面一致(不須要修改index.html中引用bundle.js的路徑,但發佈生產環境時,須要使用插件才能批量修改引用地址爲cdn地址)。
    filename: 'bundle.js'
  }
};

有了如上的概念,咱們就能夠將path,publicpath和express中的配置結合起來,同時因爲在開發環境中咱們的的資源入口一般又會按特定的目錄來進行文件存放,以下圖所示:

project  
├── LICENSE
├── README.md
├── app.json
├── dist
├── bin
├── config
├── package.json
├── postcss.config.js
├── public
├── server
├── src
└── yarn.lock

從中不難發現node server中須要配置的資源目錄每每又會和webpack的工程目錄重疊,那麼咱們就須要在express中進行相應的配置,才能實現資源的正確索引。

一、html模板資源讀取

html做爲webpack中的templates,在express中則會變成views,讀取方式會發生變化,因此咱們須要對資源進行以下配置:

npm install ejs  #讓express支持html模板格式
const ejs = require('ejs')
const app = express()

// view engine, 默承認以指向template
app.set('views', project.paths.template()) 
app.engine('.html', ejs.__express)
app.set('view engine', 'html')

// 經過配置讓express讀取webpack的內存打包資源下的template文件
app.use('/home', function (req, res, next) {
 const filename = path.join(compiler.outputPath, 'index.html'')
 compiler.outputFileSystem.readFile(filename, (err, result) => {
    if (err) {
     return next(err)
    }
   res.set('content-type', 'text/html')
     res.send(result)
     res.end()
   })
})

//讓express全部的路由請求都落到index.html中,再有前端框架中的前端路由接管頁面的跳轉
app.use('*', function (req, res, next) {
    const filename = path.join(compiler.outputPath, 'index.html')
    compiler.outputFileSystem.readFile(filename, (err, result) => {
      if (err) {
        return next(err)
      }
      res.set('content-type', 'text/html')
      res.send(result)
      res.end()
    })
    
    /*也能夠指定到特定的views文件下進行模板資源讀取*/
     res.render('home.html', {
       name:'home.html'
     })
  })

二、js和css資源讀取
js和css的引用地址在webpack的開發環境中一般會指向publicpath,因此在開發頁面中會直接以下嵌入以下地址,因爲是採用絕對地址指向,因此無需作任何配置:

<link rel="stylesheet" href="http://127.0.0.1:3000/css/app.qxdfa323434adfc23314.css"/>
<script src="http://127.0.0.1:3000/js/app.ab92c02d96a1a7cd4919.js"></script>

三、靜態資源讀取
其餘相似font,images等靜態讀取,咱們能夠將一個圖片放到工程結構中的public下,則訪問地址能夠按以下書寫,支持真實路徑和虛擬路徑:

// 真實路徑,根目錄訪問:/demo.png -> /pulbic/demo.png
app.use(express.static(project.paths.public()))

// 真實路徑,子目錄訪問:/static/demo.png -> /pulbic/static/demo.png
app.use(express.static(project.paths.public()))

// 虛擬路徑,跟目錄訪問:/static/demo.png -> /pulbic/demo.png
app.use('/static/', express.static(project.paths.public()))

// 虛擬路徑,子目錄訪問:/static/img/demo.png -> /pulbic/img/demo.png
app.use('/static/', express.static(project.paths.public()))

經過以上配置,咱們就能夠在訪問開發地址( eg: localhost:3000 )時便可獲得所需的所有前端資源。

4、mock數據模擬

做爲前端常常須要模擬後臺數據,咱們稱之爲mock。一般的方式爲本身搭建一個服務器,返回咱們想要的數據,既然咱們已經將express集成到了咱們的開發環境下,那麼實現一個mock就會很是簡單,如下介紹兩種mock數據的方式。

一、配置專屬的mock路由模塊
咱們能夠在咱們的server項目下的routes模塊中加入一個mock模塊,以下所示:

server
├── main.js
├── mock
│   ├── opporList.json
├── routes
│   ├── index.js
│   └── mock.js
└── views
    └── home.html

而後再在咱們的server下的配置文件中導入mock模塊配置:

// main.js
const mock = require('./routes/mock')
if (project.env === 'development') {
  // mock routes
  app.use('/mock, mock) 
}

routes中的mock.js中寫入以下mock數據配置便可:

const express = require('express')
const router = express.Router()
const opporList = require('../mock/opporList.json');
const Mock = require('mockjs');

// 直接讀取json文件導出
router.get('/backend/opporList', function (req, res) {
  res.json(opporList)
})

// 基於mockjs生成數據, 優點在於對項目代碼無侵入,而且支持fetch,xhr等多種方式的攔截
router.get('/backend/employee', function (req, res) {
  var data = Mock.mock({
    // 屬性 list 的值是一個數組,其中含有 1 到 10 個元素
    'list|1-10': [{
      // 屬性 id 是一個自增數,起始值爲 1,每次增 1
      'id|+1': 1
    }]
  })
  res.json(data)
})

module.exports = router

配置完成後,訪問以下地址便可拿到mock數據:

再利用咱們的proxy.config修改node-proxy配置,將測試自動轉到mock目標地址下:

const proxy = [
    {
      url: '/backend/*',
      target: "http://127.0.0.1:3000/mock"
    }
]
module.exports = proxy

二、搭建獨立的mock服務
若是企業中有部署獨立的mock服務器,如puer+mock:咱們也能夠經過修改簡單的proxy.config來直接實現須要mock的請求地址轉發,相對修改就比較簡單,以下:

const proxy = [
    {
      url: '/backend/*',
      target: "http://10.4.31.11:8080/mock"
    }
]
module.exports = proxy

5、預覽打包後的資源效果

當咱們開發完成後,wepback經過編譯能夠獲得咱們須要的各類靜態資源,這類文件一般是做爲靜態資源存在,須要放到cdn或者部署到服務器上才能訪問,可是咱們經過簡單的配置也能夠直接在本地環境下直接預覽打包後的資源效果,具體操做以下:

1. 找到構建資源生成目錄, 確認構建資源已存在:
dist
├── css
│   ├── app.5f5af15a.css
│   ├── login.7cb6ada6.css
│   └── vendors.54895ec1.css
├── images
│   ├── login_bg.8953d181.png
│   ├── logo.01cf3dce.png
│   └── wap_ico.e4e9be83.png
├── index.html
├── js
│   ├── app.eb852be2.js
│   ├── login.9a049514.js
│   ├── manifest.c75a01fc.js
│   └── vendors.20a872dc.js
└── login.html

2. 修改express的文本配置信息,加入構建完成後的靜態資源地址配置:

app.set('views', project.paths.dist()) 

if (project.env === 'development') {
  ....
} else {
  debug(
    'Server is being run outside of live development mode'
  )
  
  // 配置預覽環境下的proxy.config,通常能夠指向測試環境地址
  const proxyConfig = require('./proxy.test.config')
  const routes = require('./routes')
  proxyConfig.forEach(function (item) {
    app.use(item.url, proxy(createProxySetting(item.target)))
  })
  
  // 修改靜態資源指向地址,能夠直接配置到dist目錄下
  app.use(project.compiler_public_path,express.static(project.paths.dist())
  
  // 配置訪問路由url,並在設置置真實的template文件地址,與webpack中的htmlplugin下的filename配置路徑保持一致,通常都在dist目錄下        
  app.use(project.compiler_base_route, routes)
}

3. 啓動預覽頁面,訪問:localhost:3000便可

NODE_ENV=production node ./server/main.js

完整工程結構目錄結構參考

Project
├── LICENSE
├── README.md
├── app.json
├── bin
│   ├── compile.js
│   └── dev-server.js
├── config
│   ├── environments.config.js
│   ├── karma.config.js
│   ├── npm-debug.log
│   ├── project.config.js
│   └── webpack.config.js
├── package.json
├── postcss.config.js
├── public
│   ├── favicon.ico
│   ├── humans.txt
│   └── robots.txt
├── server
│   ├── main.js
│   ├── proxy.config.js
│   ├── routes
│   └── views
├── src
│   ├── api
│   ├── components
│   ├── containers
│   ├── index.html
│   ├── layouts
│   ├── main.js
│   ├── routes
│   ├── static
│   ├── store
│   └── until
├── tests
│   ├── components
│   ├── layouts
│   ├── routes
│   ├── store
│   └── test-bundler.js
└── yarn.lock

工程演示demo

小結

將webpack的各種高級特性和node基礎服務有效相結合,按需打造專屬自身項目的開發平臺,不只能將項目體系從簡單的頁面開發轉向工程化標準邁進,更能極大的改善前端開發的體驗,提高開發效率。

相關文章
相關標籤/搜索