随着互联网应用工程规模的日益复杂化和精细化,咱们在开发一个标准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
因而可知,咱们理想化的开发环境应根据具有如下几点要求:git
操做够简单,拉下代码后,只须要记住仅有的几个命令就能直接进入开发状态github
解耦够完全,开发者只须要修改路由配置表就能无缝在多个请求接口中灵活切换
资源够清晰,全部的开发资源都能到精确可控,同时支持一键打包构建,单页和多页模式可并存
配置够灵活,能够根据自身项目的实际状况灵活添加各种中间件,扩展模块和第三方插件
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的实现精髓,那么,咱们就能够一步步地来打造专属自身的开发环境:
先后端分离开发中,本地前端开发调用接口会有跨域问题,通常有如下几种解决方法:
直接启动服务端项目,再将项目中的资源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
一、解耦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
实际开发中,全部涉及到的前端资源咱们进行归类通常会有以下几种:
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 )时便可获得所需的所有前端资源。
做为前端常常须要模拟后台数据,咱们称之为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
当咱们开发完成后,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
将webpack的各种高级特性和node基础服务有效相结合,按需打造专属自身项目的开发平台,不只能将项目体系从简单的页面开发转向工程化标准迈进,更能极大的改善前端开发的体验,提高开发效率。