React 高效开发环境的搭建

前言

React 是 Facebook 开源出来的目前比较流行和热门的前端框架,因其基于组件的开发符合当下和未来的前端发展趋势。

React 还引入了 JSX 语法,对于初次接触 JSX 的人可能会感觉有点别扭,但是当你用过一遍之后就会发觉已经对其爱不释手了。不得不赞叹 Facebook 的开发者,为了能让开发更简便,而想出来的牛逼的解决方案。当你深入使用 React,你会发觉整个 React 的设计思想都是令人叹为观止的。

Hello world

对于初次接触 React 的开发者而言,想要使用它创建一个 Hello world 应用倒不难,先来看看官方文档给出的使用方法。

需要在页面中分别载入 React 框架和 JSXTransformer 这个 JSX 的编译转换库。

<script src="react.js"></script>
<script src="JSXTransformer.js"></script>

Hello world 的应用代码无论是内联还是外链,都需要在 script 标签上加上 type="text/jsx" 的自定义类型,用以标明该代码中包含了 JSX 的语法,以方便 JSXTransformer 对其进行编译转换。

<div id="example"></div>
<script type="text/jsx">
  React.render(
    <h1>Hello, world!</h1>,
    document.getElementById('example')
  );
</script>

模块化的尝试

这种开发方式开发一个简单的应用没什么问题,如果你想要使用 React 开发一个大应用,那么势必会想使用模块化的开发方式把大应用拆分成很多小组件。

在开发阶段模块需要异步加载,那么 JSX 依赖 type="text/jsx" 来进行实时编译就有问题了。如果你使用 RequireJSSeaJS 这类模块加载器的话,可能需要借助额外的插件来实现在加载完模块后再对其进行编译解析,加载和编译转换的操作都要在浏览器中实现。这种开发环境比较复杂,尤其还要在页面中引入 JSXTtansformer 和加载器的库文件,使用代价比较高。

实时编译和合并

除了在浏览器中进行编译,还可以将编译转换的工作移到浏览器之外,在 node 环境中进行编译。我们可以使用 browserify 或者 webpack,本文将以 browserify 为例来讲解如何搭建一个实时编译的开发环境,当然除了 browserify 还有构建工具 gulp

ES6(2015) & ES7(2016)

除了基于组件化的开发,React 对新的 ES6ES7 的支持度都很好,所以说 React 能适应未来前端发展趋势并不是泛泛而谈。

虽然 ES6 已经正式发布,但是大部分现代化的浏览器到目前为止其实都对其支持度不佳,但这并不妨碍我们使用,因为还有推动了 ES6 发展的 Babel。借助 Babel,我们可以将 ES6 的代码转换成能兼容主流浏览器的 ES5 的语法。

基于上面提到的几个方面,我们的 React 开发环境主要需要实现以下功能:

  • JSX 的编译转换
  • ES6ES7 的编译转换
  • 实时合并模块
  • 开发时保存文件则自动刷新浏览器的 livereloadserver 功能

目录结构

myapp
  |
  + dist // 用于发布上线的构建目录
  + src  // 开发的根目录
      |
      + ...
      + style
          |
          + css
          + less
      + js
          |
          + lib
              |
              + react.js
              + babel-external-helpers.js
              + ...
          + index.js  // 开发时的app入口文件
          + bundle.js // browserify合并好的入口文件
      + index.html  // 默认的启动页面
      + ...
  + gulpfile.js

index.html 访问入口页面

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>React Demo</title>
<body>

<!-- 用于将应用插入到页面的根节点 -->
<div class="root" id="root"></div>

<!-- 第三方库文件 -->
<script src="/js/lib/react.js"></script>
<script src="/js/lib/babel-external-helpers.js"></script>

<!-- browserify转换后的应用文件 -->
<script src="/js/bundle.js"></script>
</body>
</html>

livereload

使用 gulp-connet 插件可以很轻松的搭建一个 livereload 的服务器。

var connect = require('gulp-connect');

gulp.task('connect', function () {
    connect.server({
        root: 'src', // server的root目录
        port: 3001,  // server绑定的端口号
        livereload: true,   // 开启livereload的功能
        fallback: 'src/index.html' 
    });
});

其中的 fallback 配置项值得注意,使用上面的任务启动了 server 后,通过 http://localhost:3001 来访问,会默认查找 root 目录下的 index.html 文件,当访问其他的路径如:http://localhost:3001/users,服务器会报 404 的错误,这是正常的,因为这个简单的服务器并没有 route 功能,如果配置了 fallback: 'src/index.html',那么服务器不会直接报404了,而是回退到 src/index.html。如果你的 React 应用有 route 功能,这个配置项非常有用,假如你已经有了一个 http://localhost:3001/usersroute,当你在该路径下刷新页面的时候他就会正确的保持 route 状态,否则会直接报 404。

各种 xxxify 的应用

  • browsify 用于合并模块
  • watchify 配合 browserify 实现实时合并
  • babelify 合并模块时对 ES6ES7 进行编译转换,同时还可以对 JSX 进行编译转换
  • cacheify 提供缓存服务,提升编译转换的性能
var browserify = require('browserify');
var watchify = require('watchify');
var babelify = require('babelify');
var cacheify = require('cacheify');
var connect = require('gulp-connect');


// browserify的配置项
var browserifyOpts = {
    entries: 'src/js/index.js', // 入口文件
    debug: true, // 是否包含sourcemap
    insertGlobals: true, 
    detectGlobals: false
};

var bundle = function () {
    return watcher.bundle()
        .on('error', function (err) {
            console.log(err.message);
            console.log(err.stack);
        })
        .pipe(source('bundle.js'))
        .pipe(gulp.dest('src/js'))
        .pipe(connect.reload());
};

var babelifyCache = cacheify(babelify.configure({
    externalHelpers: true, // 不将babel的helper代码插入到模块中
    stage: 0 // 该模式能支持诸如static关键字等ES7的特性
}), db);

var bundler = browserify(browserifyOpts)
    .transform(babelifyCache);

var watcher = watchify(bundler)
    .on('update', bundle)
    .on('log', gutil.log);

gulp.task('watch-js', bundle);

browserify 的配置项中包含了 insertGlobalsdetectGlobals,其中 insertGlobals 用于跳过检测始终插入定义的 process global __filename __dirname,如果确定不会用到这些全局变量就设置为 true 吧。detectGlobals 用于检测当前代码中是否包含了上述的全局变量。如果代码中确定根本用不到那些全局变量设置这2个配置项能提升合并的性能。

babelify 的配置项的 externalHelpers 比较重要,在使用 babel 将 ES6 转换成 ES5 的代码时,会默认将一些诸如 class 的实现代码都插入到转换后的模块中,如果你有很多个模块,它都会插入一遍这些代码,此时就会有很多冗余的代码。将该配置设置为 true 后,则不回插入这些实现代码。此时要将这些代码已外链的形式引入到页面中,官方有详细的说明 文档

<script src="/js/lib/babel-external-helpers.js"></script>

browserify 在合并的时候会提供一个使用中间件的 transform 入口,有一句软件开发的名言,大意是:任何复杂的系统,都可以通过引入一个中间层来得到简化。browserify 有非常多的 transform 中间件,资源相当丰富。

在上面的代码中请注意 babelifyreactify 使用时的先后顺序。

除了 watch js,还可以对 html 和 css 进行 watch。如果项目有使用到 sassless 这类预处理框架,同样可以实现实时编译。

var connect = require('connect');
var less = require('gulp-less');

var htmlSrcPath = 'src/**/*.html';
var lessSrcPath = 'src/style/less/**/*.less';
var cssDestPath = 'src/style/css';

// watch html
gulp.task('watch-html', function () {
    gulp.watch(htmlSrcPath, function () {
        return gulp.src(htmlSrcPath)
            .pipe(connect.reload());
    });
});

// watch css 包含了 less 的实时编译
gulp.task('watch-css', function () {
    var lessTransformer = function () {
        return gulp.src(lessSrcPath)
            .pipe(less())
            .pipe(gulp.dest(cssDestPath))
            .pipe(connect.reload());
    };

    lessTransformer();

    gulp.watch(lessSrcPath, lessTransformer);
});

上述的功能使用 webpack 也同样会有替代的解决方案,具体选用哪个框架看自己的喜好了。个人认为如果是基于 gulp,后续的构建工作也会更轻松些。

这是我的 React 系列文章的第一篇,由于涉及的面确实比较广,为了能尽量说的详细些,篇幅有点长。

后续更新:React 官方已经推荐抛弃 JSXTransformer 转而使用 babel,后者已经默认内置了对 JSX 的转换。

原载于:雨夜带刀’s Blog
本文链接:https://blog.yiguochen.com/gulp-browserify-for-react.html
如需转载请以链接形式注明原载或原文地址。

“React 高效开发环境的搭建”目前已有 3 条评论