webpackcss
使用webpack對組件化的前端項目進行打包在現在是比較流行的作法。webpack解決的根本問題是處理項目中各類不一樣類型資源的依賴關係,並把他們打包成一個或多個文件,這也是我接觸webpack的初衷。在webpack以前有seajs、FIS等解決模塊化依賴問題的方案,seajs只解決模塊引入的問題,FIS在純前端的環境下顯得過於臃腫(或許是我沒有太深刻了解),webpack的優點在於解決模塊化問題的同時,也完成了一部分工做流的功能,把各個模塊提早編譯並集中起來打包,之前咱們可能要使用gulp和grunt來完成這部分工做,如今一個webpack就能解決。同時webpack還提供了熱替換、靜態資源開發服務器這些解決開發流程的功能,這讓webpack看起來很完美。html
server中使用webpack前端
好吧,首先在這裏澄清一個觀點,本篇使用webpack在服務器端打包只是提供一個解決思路,並非什麼最佳實踐。以前就在知乎上看到有人吐槽webpack在作Server-side render/Isomorphic/Universal很坑。爲何這麼講?原本服務端node自帶模塊化功能,若是在開發過程當中避免在node運行的生命週期中使用DOM和BOM對象,咱們寫的組件應該是可以直接跑在node環境中的。可是考慮到使用webpack的不一樣資源依賴的功能,狀況就不同了。若是咱們在組件中引入了圖片資源或者css,不通過webpack的loader進行加載,node是沒法直接運行的。node
這時咱們一般會想到用webpack直接把服務端運行的代碼也進行打包,把須要依賴的靜態資源用loader提早解析就好了,可是css-loader裏面也使用到了document和window,運行失敗= =。有一種解決方案是放棄靜態資源和組件一併打包,使用gulp和browserify來作構建工具,大概思路能夠參考這篇文章《Writing apps with React.js: Build using gulp.js and Browserify》。可是秉着對組件化的執着,也是對webpack更深刻使用的探究,咱們決定嘗試hack掉webpack在node環境下的各類問題。react
忽略依賴的內建模塊和node_moduleswebpack
node環境下有許多內建模塊,好比fs,path,http這些基礎模塊,webpack在編譯這些模塊的時候會報「Moudle not found」。由於webpack只會去當前運行環境目錄和設置的resolve.root目錄下去尋找,而這些內建模塊並不在這些目錄下就會報錯了。由於node環境下這些模塊的依賴可以正確的被解析,因此咱們直接忽略解析這些模塊就能夠了。而node環境中依賴的node_modules模塊,有各類各樣的問題(會有二進制的依賴模塊,好比express),由於他們都能正確地被node引用,因此咱們不但願webpack去打包,和以前的內建模塊同樣,咱們都忽略掉。
忽略內建模塊webpack提供了對應的配置參數target: node
configs/webpack/server.config.jsgit
var webpack = require('webpack'); var path = require('path'); var fs = require('fs'); var env = require(path.resolve(__dirname,'../environments')); module.exports = { entry: path.resolve(__dirname,'../..','server/server.js'), // ignore build-in modules target: 'node', output: { path: path.resolve( __dirname,'../..','dist'), filename: 'server.js' } }
忽略node_mouldes中的模塊,webpack提供了externals配置對外部環境依賴的功能,這正好可以派上用場。由於咱們不是要用一個變量對引用進行替換,而是用使用須要保留require,因此咱們在externals中須要保留require的模塊名前加上commonjs來實現這個功能,具體能夠參考webpack官網的說明。
咱們遍歷node_mouldes,依次加入到externals中:github
var nodeModules = {}; fs.readdirSync('node_modules') .filter(function(x) { return ['.bin'].indexOf(x) === -1; }) .forEach(function(mod) { nodeModules[mod] = 'commonjs ' + mod; }); module.exports = { /** same with above **/ externals: nodeModules, // ... }
忽略css和less的引用web
接下來,到了解決引入樣式的問題了,以前說過,因爲css-loader會使用dom對象,這在node環境中是行不通的,因此咱們需忽略這些引用。webpack提供NormalModuleReplacementPlugin插件來幫助咱們替換不一樣類型的資源,當匹配到是css和less類型的資源時,咱們就使用一個空的模塊去進行替換。express
/** other configs **/ plugins: [ new webpack.NormalModuleReplacementPlugin(/\.(css|less)$/, 'noop'), new webpack.IgnorePlugin(/\.(css|less)$/), new webpack.BannerPlugin('require("source-map-support").install();', { raw: true, entryOnly: false }) ], /** other configs **/
這裏使用了其餘兩個插件,IgnorePlugin插件避免作代碼分離時,對分離部分引用的css和less文件進行單獨解析打包;另外的BannerPlugin是對server打包作source map,這樣若是server代碼報錯的話,提示的錯誤代碼不會顯示打包後的代碼行數,而是打包前的代碼位置。
node環境變量
node環境下有不少有用的變量,好比__dirname、__filename、process這些變量,咱們須要告知webpack這些變量的值該如何處理。相關的配置說明在這裏。固然,咱們也可使用DefinePlugin插件來本身模擬這些環境變量來對咱們的項目進行更好的控制:
/** other configs **/ process: true, __filename: true, __dirname: true, /** other configs **/
完整的配置文件加上了一些圖片資源的直接引用處理(注意保證loader配置和客戶端配置一致,不然客戶端生成的html會和服務器生成的html產生差別,從而致使頁面二次渲染):
var webpack = require('webpack'); var path = require('path'); var fs = require('fs'); var env = require(path.resolve(__dirname,'../environments')); var nodeModules = {}; fs.readdirSync('node_modules') .filter(function(x) { return ['.bin'].indexOf(x) === -1; }) .forEach(function(mod) { nodeModules[mod] = 'commonjs ' + mod; }); module.exports = { entry: path.resolve(__dirname,'../..','server/server.js'), target: 'node', output: { path: path.resolve( __dirname,'../..','dist'), filename: 'server.js' }, module: { loaders: [ { test: /\.jsx?$/, exclude: /(node_modules|bower_components)/, loader: 'babel' }, { test: /\.((woff2?|svg)(\?v=[0-9]\.[0-9]\.[0-9]))|(woff2?|svg|jpe?g|png|gif|ico)$/, loader: 'url?name=img/[hash:8].[name].[ext]' }, { test: /\.((ttf|eot)(\?v=[0-9]\.[0-9]\.[0-9]))|(ttf|eot|otf)$/, loader: 'url?limit=10000&name=fonts/[hash:8].[name].[ext]' } ] }, externals: nodeModules, plugins: [ new webpack.NormalModuleReplacementPlugin(/\.(css|less)$/, 'react'), new webpack.BannerPlugin('require("source-map-support").install();', { raw: true, entryOnly: false }) ], resolve:{ root:[ env.inProject("app") ], alias: env.ALIAS }, resolveLoader: {root: env.inNodeMod()}, process: true, __filename: true, __dirname: true, devtool: 'eval-source-map' }
搭配gulp搭建工做流
如今打包出來的代碼已經可以在node環境中運行了。以前也提到webpack並不僅是打包工具,因此開發者功能咱們也要一併用起來。在搭建咱們的開發環境以前,咱們先整理一下咱們的思路:
如今咱們有兩份打包事後的代碼,一份是須要在客戶端運行基於頁面入口文件打包的代碼,一份是須要在服務器上運行基於服務程序打包的入口,因爲基於兩個入口打包的配置差別較大,可使用一個工廠模式來配置,也能夠直接使用兩份配置代碼;
咱們須要一份全局的配置文件協調前端代碼和後端代碼以及開發過程的工做,須要讓這份全局配置可以同時在先後端正常工做,又能兼容webpack的使用;
在開發環境中,咱們有兩份打包事後的代碼,若是須要對這兩份代碼進行熱替換操做,怎麼保證替換操做以後咱們的代碼可以正常運行;
在生產環境中,咱們怎麼去作版本控制,避免發版時出現頁面混亂的狀況。
首先第一點,由於在打包代碼有開發環境配置和生產環境配置不一樣的,咱們使用兩份代碼的形式來實現,具體實現能夠參考末尾列出的實列項目。
第二點咱們使用一個配置文件的形式去實現,由於在配置文件中可能會使用到一些node內建模塊,而客戶端的配置咱們沒有作node環境的兼容,因此,在客戶端的配置文件中,咱們用自定義插件DefinePlugin來實現配置的引入。
var env = require(path.resolve(__dirname,'../environments')); // define by us plugins: [ new webpack.DefinePlugin({ '_configs': JSON.stringify(env) }) ]
第三點的重點在這麼實現服務端代碼的熱替換,客戶端的熱替換可使用webpack的熱替換功能來實現,雖然也會趕上一些麻煩,咱們會在以後提到。服務端的熱替換實現起來較爲困難,咱們能夠配合gulp、gulp-nodemon和webpack一塊兒實現監聽代碼修改後->從新打包->重啓服務器的工做流,可是這並非熱替換的初衷,在《Live Editing JavaScript with Webpack》這篇文章中有詳細說明webpack的熱替換功能,並實現了monkey-hot-loader進行後端的熱替換,感興趣的同窗能夠仔細看看,這裏咱們就不加以說明了。基於gulp、gulp-nodemon和webpack的實現模式以下:
var gulp = require('gulp'), nodemon = require('nodemon'), webpack = require('webpack'), gutil = require('gulp-util'), argv = require('yargs').argv, path = require('path'), open = require('open'), $ = require('gulp-load-plugins')({ camelize: true }), runSequence = require('run-sequence'), serverConfig = require('./configs/webpack/server.config'), webpackConf = require('./configs/webpack/build.config')('production'), env = require('./configs/environments'); function onBuild(done) { return function(err, stats) { if (err) throw new gutil.PluginError('webpack', err) gutil.log('[webpack]', stats.toString({ colors: true })) gutil.log(argv) if (done) done() } } gulp.task('clean', function() { var clean = require('gulp-clean') return gulp.src(env.inProject("dist"), { read: true }).pipe(clean()) }) gulp.task('backend:build', function(done) { webpack(serverConfig).run(onBuild(done)); }); gulp.task('backend:watch', function() { webpack(serverConfig).watch(100, function(err, stats) { onBuild()(err, stats); nodemon.restart(); }); }); gulp.task('open', ['nodemon'], function(){ open(env.DEV_SERVER+"/__components__"); }) gulp.task('nodemon',['backend:watch'], function() { nodemon({ execMap: { js: 'node' }, script: path.join(__dirname, 'dist/server'), ignore: ['*'], watch: ['foo/'], ext: 'noop', env: { 'NODE_ENV': "development"}, args: ["--debug"] }).on('restart', function() { gutil.log('Restarted!'); }); }); gulp.task('run', ['open']); gulp.task('pack', function(done) { webpack(webpackConf, function(err, stats) { if (err) throw new gutil.PluginError('webpack', err) gutil.log('[webpack]', stats.toString({ colors: true })) gutil.log(argv) done() }) })
這裏不得不說明下,這個工做流加上webpack開發服務器對本地代碼的監聽(客戶端代碼的熱替換功能)形成的cpu消耗還有比較大的,在進行試驗項目的時候,就由於cpu消耗過高,寫代碼會有很長的延時,後來更新了一下編輯器的版本,狀況好轉了不少,因此仍是強烈建議使用熱替換的功能。
最後一點的實現能夠配合gulp-load-plugins的sourcemap功能來實現,具體實現能夠在webpack打包客戶端代碼完成後,用gulp-load-plugins生產sourcemap,在服務端比對後輸入到頁面中就行。
熱替換遇到的麻煩
在進行客戶端代碼熱替換時,由於要單獨對客戶端代碼進行監聽打包,因此咱們使用webpack的webpack dev server來支持對客戶端代碼獨立熱替換。在使用webpack開發服務器進行熱替換時有個尷尬的問題,由於咱們的應用是跑在本身寫的服務器上(這裏是兩個不一樣域名的服務器),因此熱替換髮送到webpack開發服務器的請求都跨域了。這裏有兩個解決方案,一是用webpack dev middleware將開發服務器集中在應用服務器上,二是在讓開發服務器支持跨域請求。另外若是使用了css獨立打包的話,熱替換就沒法展示效果了,由於熱替換隻能替換模塊,css獨立打包就沒法被修改了,因此咱們使用webpack hot middleware讓每次修改代碼都進行頁面刷新來更新新的樣式文件。
// load native modules var http = require('http') var path = require('path') var util = require('util') // load 3rd modules var koa = require('koa') // 容許跨域 var cors = require('koa-cors') var router = require('koa-router')() var serve = require('koa-static') var routes = require('./components.dev') // init framework var app = koa() app.use(cors()) // global events listen app.on('error', (err, ctx) => { err.url = err.url || ctx.request.url console.error(err.stack, ctx) }) routes(router, app) app.use(router.routes()) var webpackDevMiddleware = require('koa-webpack-dev-middleware') var webpack = require('webpack') var webpackConf = require('../../configs/webpack') var compiler = webpack(webpackConf) var config = require('../../configs/webpack-dev') // 爲使用Koa作服務器配置koa-webpack-dev-middleware app.use(webpackDevMiddleware(compiler, config)) // 爲實現HMR配置webpack-hot-middleware var hotMiddleware = require('webpack-hot-middleware')(compiler) // Koa對webpack-hot-middleware作適配 app.use(function* (next) { yield hotMiddleware.bind(null, this.req, this.res) yield next }) app = http.createServer(app.callback()) app.listen(4001, '127.0.0.1', () => { var url = util.format('http://%s:%d', 'localhost', 4001) console.log('Listening at %s', url) })
到這裏咱們的開發工做流基本搭建完畢了,還有不少細節部分沒有講到(客戶端的相關內容都沒有概況),可是我寫了一個demo,能夠參考一下。
終於到了總結
整體來講這篇文章介紹的方法偏向實驗性,更多的是想深刻了解webpack,多去嘗試一些技術。若是是正式引入項目的話,可能使用gulp加上browserify來搭建工做流更爲合適。
相關文章
Isomorphic React(React同構應用)一 :Server Render
Isomorphic React(React同構應用)二 :Redux
參考
Backend Apps with Webpack ( series )
Server-Side Rendering with Redux and React-Router