Isomorphic React(React同构应用)三 :Bundle with Webpack

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

相关文章
相关标签/搜索