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