HOME

Browserify + SASS + BrowserSync + Gulp 高效前端开发环境配置

之前开发的前端应用都是些比较简单的页面,开发的时候一般就是 Sublime,然后命令行里面启动 sass --watch 以及 coffee --watch 就行了,修改代码以后自动编译,但是浏览器需要手动刷新。凑合着也能用,所以也就一直这样没有去理会别的解决方案了。

工作了以后,编写的应用规模变大了很多,而且前端的依赖也变得复杂了。传统的方式显然是解决不了问题的。这段时间,我一直在寻找一套高效的开发环境,要求如下:

  • 使用 CommonJS 进行依赖引用
  • ES6 支持
  • React + JSX 支持
  • SASS 支持
  • 修改以后自动高速编译,即便是很大的依赖
  • 修改 JS、HTML 以后浏览器自动刷新
  • 修改 CSS 浏览器使用 Style Injection 刷新
  • 生产环境下合并压缩

先后尝试了 Browserify + Gulp 以及 Webpack 的解决方案,最终还是选定 Browserify + Gulp,完美满足了以上几个要求。

第一次尝试使用了 Browserify + Gulp,配合使用 BrowserSync 进行浏览器自动刷新以及 Style Injection,使用 Notifier 在错误发生时推送通知,配置文件如下:

var gulp = require("gulp")
var sass = require("gulp-ruby-sass")
var browserify = require("browserify")
var reactify = require("reactify")
var babelify = require("babelify")
var vinylSource = require("vinyl-source-stream")
var browserSync = require("browser-sync").create()
var autoprefixer = require("gulp-autoprefixer")
var cssnano = require("gulp-cssnano")
var uglify = require("gulp-uglify")
var buffer = require("vinyl-buffer");
var notifier = require("node-notifier")
var fs = require("fs")

var source = {
  script: ["src/**/*.js", "src/**/*.jsx"],
  style: "sass/**/*.sass",
};

var dest = {
  script: "js/",
  style: "css/",
};

var pages = ["dashboard", "data-analysis", "login", "campaign-overall"]
var current = "reporting"

gulp.task("serve", ["sass", "browserify"], function() {
  browserSync.init({
    ghostMode: false,
    server: "./",
  });
  gulp.watch(source.style, ["sass"]);
  gulp.watch(source.script, ["script-watch"]);
  gulp.watch(["./*.html"], function() {
    browserSync.reload();
  });
});

gulp.task("sass", function() {
  return sass("sass/" + current + ".sass", { style: "expanded" })
        .on("error", function(err) {
          console.error("Error!", err.message);
        })
        .pipe(browserSync.stream())
        .pipe(gulp.dest(dest.style));
});

gulp.task("script-watch", ["browserify"], function() {
  browserSync.reload();
});

gulp.task("browserify", function() {
  return browserify("./src/" + current + ".jsx")
      .transform(babelify)
      .transform(reactify)
      .bundle()
        .on("error", function(err) {
          var reg = /(.*\/)(.*)(?= while)/
          if (reg.test(err.message)) {
            notifier.notify({
              title: "Browserify Error!",
              message: err.message.match(reg)[2],
            })
          }

          console.log("[Error]: " + err.message);
          this.emit("end");
        })
        .pipe(vinylSource(current + ".js"))
        .pipe(gulp.dest(dest.script))
});

gulp.task("build-js", function() {
  pages.map(function(name, index) {
    var filename = "./src/" + name + ".jsx"
    if (!fs.existsSync(filename)) {
      return
    }

    return browserify(filename)
       .transform(babelify)
       .transform(reactify)
       .bundle()
       .pipe(vinylSource(name + ".js"))
       .pipe(buffer())
       .pipe(uglify())
       .pipe(gulp.dest(dest.script))
  })
})

gulp.task("build-css", function() {
  pages.map(function(name) {
    var filename = "./sass/" + name + ".sass"
    if (!fs.existsSync(filename)) {
      return
    }

    return sass(filename, { style: "expanded" })
            .on("error", function(err) {
              console.error("Error!", err.message);
            })
            .pipe(autoprefixer({
              browsers: ["last 2 versions"],
              cascade: false,
            }))
            .pipe(cssnano())
            .pipe(gulp.dest(dest.style));
  })
})

gulp.task("build", ["build-js", "build-css"])
gulp.task("default", ["serve"]);

这个方案满足了以上所有需求,除了在监听文件变化时再次编译,基本上随着入口 js 文件变得越来越大,编译时间越来越长,后来每次修改需要等待 8 秒钟左右,这实在是让人无法接受,这促使了我寻找别的方案来把他换掉。

第二次尝试使用了 Webpack。用 React 的人不可能不知道 Webpack,正好借此机会,好好去研究了一下 Webpack。Webpack 自带一个 webpack-dev-server,能够实现自动刷新功能。SASS、ES6、JSX 可以使用 Loader 进行处理。我摸索了一下配置方案如下(CSS 使用了 Stylus),配置文件 webpack.config.js

var webpack = require("webpack")
var path = require("path")

module.exports = {
  devtool: "eval",
  entry: {
    "campaign-overall": "./src/campaign-overall.jsx",
    vendors: [
        "jquery",
        "react",
    ],
  },
  output: {
    path: path.join(__dirname, "./js"),
    filename: "[name].js",
    publicPath: "/js/",
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendors",
      filename: "vendors.js",
      minChunks: Infinity,
    }),
  ],
  resolve: {
    alias: {
      "highcharts": path.join(__dirname, "assets/vendor/highcharts.js"),
    },
  },
  module: {
    noParse: [
        /highcharts/,
    ],
    loaders: [
      {
        test: /\.jsx$/,
        loader: "react-hot!jsx!babel",
      },
      {
        test: /\.js$/,
        loader: "babel",
        exclude: /node_modules/,
      },
      {
        test: /\.styl$/,
        loader: "style!css!stylus",
      },
      {
        test: /\.css$/,
        loader: "style!css",
      },
      {
        test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
        loader: "url?limit=10000&mimetype=application/font-woff",
      },
      {
        test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
        loader: "url?limit=10000&mimetype=application/font-woff",
      },
      {
        test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
        loader: "url?limit=10000&mimetype=application/octet-stream",
      },
      {
        test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
        loader: "file",
      },
      {
        test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
        loader: "url?limit=10000&mimetype=image/svg+xml",
      },
    ],
    resolve: {
      extensions: ["", ".js", ".jsx"],
    },
  },
}

通过使用 webpack-dev-server --hot --inline 配合 react-hot-loader 可以使用 React 的 Hot Module Replacement 来进行动态效果刷新(浏览器不刷新)。

但是这个方案的缺点,我需要用 CommonJS 的方式去使用 CSS,比如说在入口文件里面 import "main.styl"。虽然有这篇很出名的文章 React: CSS in JS by vjeux。里面的很多观点我都很赞同。比如 CSS 的隔离问题,确实十分头痛。但我不觉得用 JS 来写 CSS 正确的解决方案。而且在预处理器的帮助下,文中提到的很多问题是可以解决的。

在使用 Webpack 的情况下,如果要使用单独的 SASS,并能在修改以后进行Style Injection,刷新浏览器样式,需要使用 Gulp 之类的工具,但是这样的话,配置 webpack-dev-server又很麻烦。

Webpack 在 watch 情况下,重新编译的速度非常快。究其原因,无非是 Webpack 缓存了上一次编译的结果。而 Browseriy 的方案之所以重新编译慢,是因为每次 Gulp 调用 browserify 的时候,都相当于重新完整编译了一次,自然是非常慢的。顺着这个思路,我便去找找看有没有 Browserify 的缓存插件,果然被我找到了,Bingo! browserify-incremental。browserify-incremental 会产生一个缓存文件,除了第一次(the very first),之后的所有编译都会非常快。

最终的配置文件如下:

var gulp = require("gulp")
var sass = require("gulp-ruby-sass")
var browserify = require("browserify")
var reactify = require("reactify")
var babelify = require("babelify")
var vinylSource = require("vinyl-source-stream")
var browserSync = require("browser-sync").create()
var autoprefixer = require("gulp-autoprefixer")
var cssnano = require("gulp-cssnano")
var uglify = require("gulp-uglify")
var buffer = require("vinyl-buffer")
var notifier = require("node-notifier")
var fs = require("fs")
var browserifyInc = require("browserify-incremental")

var source = {
  script: ["src/**/*.js", "src/**/*.jsx"],
  style: "sass/**/*.sass",
}
var dest = {
  script: "js/",
  style: "css/",
}

var current = "new-campaign"

var pages = [
    "dashboard",
    "data-analysis",
    "login",
    "campaign-overall",
    "new-campaign",
]

gulp.task("serve", ["sass", "browserify"], function() {
  browserSync.init({
    ghostMode: false,
    server: "./",
  })
  gulp.watch(source.style, ["sass"])
  gulp.watch(source.script, ["script-watch"])
  gulp.watch(["./*.html"], function() {
    browserSync.reload()
  })
})

gulp.task("sass", function() {
  return sass("sass/" + current + ".sass", { style: "expanded" })
        .on("error", function(err) {
          notifier.notify({
            title: "SASS Error!",
            message: err.message,
          })
          console.error("Error!", err.message)
        })
        .pipe(browserSync.stream())
        .pipe(gulp.dest(dest.style))
})

gulp.task("script-watch", ["browserify"], function() {
  browserSync.reload()
})

gulp.task("browserify", function() {
  var currentFile = "./src/" + current + ".jsx"
  return getBundler(currentFile)
      .pipe(vinylSource(current + ".js"))
      .pipe(gulp.dest(dest.script))
})

function getBundler(filename) {
  var config = {
    cache: {},
    packageCache: {},
    fullPaths: true,
    cacheFile: "./browserify-cache.json",
  }
  bundler = browserify(filename, config)
      .transform(babelify)
      .transform(reactify)

  return browserifyInc(bundler)
      .bundle()
      .on("error", handleError)
}

function handleError(err) {
  var reg = /(.*\/)(.*)(?= while)/
  if (reg.test(err.message)) {
    notifier.notify({
      title: "Browserify Error!",
      message: err.message.match(reg)[2],
    })
  }

  console.log("[Error]: " + err.message)
  this.emit("end")
}

gulp.task("build-js", function() {
  pages.map(function(name, index) {
    var filename = "./src/" + name + ".jsx"
    if (!fs.existsSync(filename)) {
      return
    }

    return getBundler(filename)
       .pipe(vinylSource(name + ".js"))
       .pipe(buffer())
       .pipe(uglify())
       .pipe(gulp.dest(dest.script))
  })
})

gulp.task("build-css", function() {
  pages.map(function(name) {
    var filename = "./sass/" + name + ".sass"
    if (!fs.existsSync(filename)) {
      return
    }

    return sass(filename, { style: "expanded" })
            .on("error", function(err) {
              console.error("Error!", err.message)
            })
            .pipe(autoprefixer({
              browsers: ["last 2 versions"],
              cascade: false,
            }))
            .pipe(cssnano())
            .pipe(gulp.dest(dest.style))
  })
})

gulp.task("build", ["build-js", "build-css"])
gulp.task("default", ["serve"])

开发时只要启动 gulp,自动编译然后打开浏览器,修改 JS、HTML 代码以后浏览器自动刷新(即便是很大的依赖也只有 1s 左右,事实上,因为缓存的原因,不管多大的依赖,编译时间都会非常短,因为一次修改的代码数量总是不多的),修改样式表自动刷新样式(使用 Style Injection 不刷新浏览器),生产部署时 gulp build 即可。在加上两个屏幕,可以做到这边修改那边立即刷新的效果,这是目前让我十分满意的解决方案。

PS: 注意BrowserSync 的 GhostMode,开起这个选项以后,BrowserSync 会自动模拟事件,比如你在一个 Tab 上滚动了页面,另一个打开该页面的 Tab 会自动滚动。这个特点会导致非常奇怪的 Bug,比如你的事件只触发了一下,但是事件监听器却运行了两次,让人百思不得解。还记得那个夜晚花了很长时间才定位到是 BrowserSync 的问题。T_T