基于webpack的前后端分離開發環境實踐
背景
隨著互聯網應用工程規模的日益復雜化和精細化,我們在開發一個標準web應用的早已開始告別單干模式,為了提升開發效率,前后端分離的需求越來越被重視,前端負責展現/交互邏輯,后端負責業務/數據接口,基本上也成為了我們日常項目分工中的標配,但是前后端分離一直以來都是一個工程概念,每個團隊在實現工程中都會基于自身的技術棧選擇和開發環境進行具體的實現,本文便根據自身團隊在webapck開發中搭建的前后端分離開發環境進行部分敘述。
理想化的前后端分離環境
目前業界比較有代表性的前后端分離的例子是SPA(Single-page application),所有用到的展現數據都是后端通過異步接口(AJAX/JSONP/WEBSOCKET)的方式提供的,現如今最火的前端框架如:React, Vue,Angular等也都推薦采用SPA的模式進行開發并且從組件化,數據流,狀態容器再到網絡請求,單頁路由等都給出了完善的全家桶方案。從某種意義上來說,SPA確實做到了前后端分離,但這種方式存在如下幾個亟待問題:
- 前端開發本地開發環境下該如何突破域的限制和服務端接口進行通信?
- 一條命令,能否同時完成webpack和node server的啟動?
- 開發環境下的前端資源路徑應該如何配置?
- mock數據應該怎么做?
- 打包構建后的文件能否直接預覽效果?
針對以上的問題,我們來看看怎樣利用webpack現有的一些機制和借助node的環境搭配來進行逐個擊破,具體設計見下圖:
由此可見,我們理想化的開發環境應根據具備以下幾點要求:
- 操作夠簡單,拉下代碼后,只需要記住僅有的幾個命令就能直接進入開發狀態
- 解耦夠徹底,開發者只需要修改路由配置表就能無縫在多個請求接口中靈活切換
- 資源夠清晰,所有的開發資源都能到精確可控,同時支持一鍵打包構建,單頁和多頁模式可并存
- 配置夠靈活,可以根據自身項目的實際情況靈活添加各類中間件,擴展模塊和第三方插件
不得不提的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 正向代理的模式進行了轉發,采用的是服務對服務的模式,能較為完美解決本地開發時候的跨域問題,也是本文中推薦的方式,配置如下:
1、搭建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
2、編寫代理配置腳本:
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</code></pre>
通過以上的配置我們就能輕松將指定url下的請求自動轉發到匹配成功的目標接口下。
NODE_ENV=development node ./bin/dev-server.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
二、將webpack配置和node server進程打通
1、解耦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
}
})
}</code></pre>
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</code></pre>
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
}
})
)
}</code></pre>
由此可知,三者間的注入關系如下:
> environments -> project -> webpack
2、整合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')
});</code></pre>
這樣當我們執行下述的時候,就能一鍵完成webpack基礎配置,熱更新以及epxress服務的啟動,并且可以完全根據express的配置說明來自定義擴展我們的前端開發資源。
ENV=development node ./bin/dev-server.js
三、前端資源路徑設計
實際開發中,所有涉及到的前端資源我們進行歸類一般會有如下幾種:
- 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中進行相應的配置,才能實現資源的正確索引。
1、html模板資源讀取
html作為webpack中的templates,在express中則會變成views,讀取方式會發生變化,所以我們需要對資源進行如下配置:
npm install ejs #讓express支持html模板格式
const ejs = require('ejs')
const app = express()
// view engine, 默認可以指向dist
app.set('views', project.paths.dist())
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'
})
})</code></pre>
2、js和css資源讀取js和css的引用地址在webpack的開發環境中通常會指向publicpath,所以在開發頁面中會直接如下嵌入如下地址,由于是采用絕對地址指向,所以無需做任何配置:
<link rel="stylesheet" />
<script src="http://127.0.0.1:3000/js/app.ab92c02d96a1a7cd4919.js"></script>
3、靜態資源讀取其他類似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()))</code></pre>
通過以上配置,我們就可以在訪問開發地址( eg: localhost:3000 )時即可得到所需的全部前端資源。
四、mock數據模擬
作為前端經常需要模擬后臺數據,我們稱之為mock。通常的方式為自己搭建一個服務器,返回我們想要的數據,既然我們已經將express集成到了我們的開發環境下,那么實現一個mock就會非常簡單,以下介紹兩種mock數據的方式。
1、配置專屬的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</code></pre>
配置完成后,訪問如下地址即可拿到mock數據:
再利用我們的proxy.config修改node-proxy配置,將測試自動轉到mock目標地址下:
const proxy = [
{
url: '/backend/*',
target: "http://127.0.0.1:3000/mock"
}
]
module.exports = proxy
2、搭建獨立的mock服務如果企業中有部署獨立的mock服務器,如 puer+mock :我們也可以通過修改簡單的proxy.config來直接實現需要mock的請求地址轉發,相對修改就比較簡單,如下:
const proxy = [
{
url: '/backend/*',
target: "http://10.4.31.11:8080/mock"
}
]
module.exports = proxy
五、預覽打包后的資源效果
當我們開發完成后,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)
}</code></pre>
3. 啟動預覽頁面,訪問:localhost:3000即可
NODE_ENV=production node ./bin/dev-server.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
小結
將webpack的各類高級特性和node基礎服務有效相結合,按需打造專屬自身項目的開發平臺,不僅能將項目體系從簡單的頁面開發轉向工程化標準邁進,更能極大的改善前端開發的體驗,提升開發效率,有紕漏的地方也希望能多多指正。
來自:https://techblog.toutiao.com/2017/08/16/untitled-2/