前端 Webpack + Backbone + React 项目环境集成

前端项目的环境及脚手架多种多样,也形成了各种生态体系。目前主流的三大前端生态 React、Vue、Angular,Vue 和 Angular 是用于构建 Web 应用程序的两个顶级 JavaScript 框架,好处在于可以快捷构建项目。React 相对于这两个框架来说其实并不算框架,只能将它看做一个灵活多变的视图层脚手架,所以要想将它完整的构建成一个框架还需要我们配合其生态圈的各种插件来实现数据绑定,最常用的如 Flux、Redux、Mobx。目前我们基本采用的就是 React + Redux 的组合形式构成了一个 基本的 MVC 或者 MVVM 的框架来构建我们的项目应用。正因为它单独用作视图渲染的虚拟 Dom 单向数据流体系,让我们可以将它作为视图层的插件来集成到我们的 Backbone 项目之中,让我们老的 Backbone 项目也可以集成 React 组件化的开发。

Backbone + Marionette 背景介绍

Backbone 是一个 MV 框架,它可以将数据呈现为 Model,同时提供了 Model 的集合 Collection。Backbone 有属于自己的 View,通过绑定视图的 render 函数到模型的 “change” 事件 — 模型数据会即时的显示在 UI 中。
Marionette 可以看做是因 Backbone 所衍生的一个视图层插件,为 Backbone 提供模板视图绑定。不用显式定义 render 方法,而是由 ItemView 本身完成将数据渲染到模板,并将视图追加到 el,减少了很多流程化的操作。同时 Marionette 还有很多事件和回调方法如:onBeforeRender、onRender、onBeforeDestroy、onDestroy、onShow 等,这些方法就形成了 Backbone 项目的生命周期,我们可以在这些生命周期函数中处理相关逻辑。

React 视图

React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式,React 应用程序的组成部分:元素和组件。React 采用虚拟 Dom 的模式,通过 jsx 的语法糖让 UI 和数据直接关联,而不像传统的 Html 和 Js 一样必须分开通过操作 Html 节点来渲染 Model 数据,而是直接在同一个组件中直接通过匹配 state 来渲染 Model 到 UI 中,这样就可以从属性操作、事件处理和手动 DOM 更新这些在构建应用程序时必要的操作中解放出来。

注:虚拟 Dom 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调。

首先,我们需要了解一下 React 的工作原理,如图:

React 是由单向接收数据流改变虚拟 Dom,并根据虚拟 Dom 的差异化来批量渲染真是 Dom 的过程。

其次,我们需要了解一下 React 的生命周期,如图:

最好,就是 React 的核心,组件:

Webpack

webpack 是一个现代 JavaScript 应用程序的静态模块打包器。

首先安装 webpack

1
npm install webpack --save

接下来创建 webpack.config.js 文件,并在文件中输出且使用。

webpack 最重要是下面的四个概念:

1、入口(entry)

即 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。

1
2
3
var env = process.env.BUILD_ENV ? process.env.BUILD_ENV : "none";
var entry = "./env/env-config/" + env + ".js";
entry: [entry, "./assets/src/app.styles.js", "./assets/src/app.js"]

一般来说单页面应用的入口文件即 app.js,这里引入 env.js 和 styles.js 是兼容 gulp 中配置环境变量及全局样式的处理。webpack 本身提供的直接在编辑时处理环境变量的方法。

1
2
3
4
5
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production")
}
}),

2、输出(output)

输出项目并打包 js 文件。

1
2
3
4
output: {
path: path.join(__dirname, "../public/assets/"),
filename: "javascripts/bundle.js"
},

3、loader

在输出规则中配置的加载器。

1
2
3
module: {
rules: []
},

4、插件(plugins)

帮助我们在打包过程中实现压缩、环境配置、三方库引用、文件复制等一系列操作的工具。

1
plugins: []

上面简单讲述了关于 webpack 的一些主要概念,接下来进入真正的项目构建及 react 兼容过程。

引入 React 配置 Webpack

在配置了入口和输出路劲之后,我们开始正式配置 loader 及 plugin。React 既可以工作在 ES6 环境中也可以工作在 ES5 环境中,目前主流的应用都是采用 ES6 甚至 ES7 的 JavaScript 语法标准,其本质都是将其语法最终在浏览器输出为 ES5 的标准来加载渲染。所以这里就需要我们配置相关 loader 来实现它。

1、安装 react react-dom prop-types 到项目基本库中

1
npm install react react-dom prop-types --save

安装 react 核心库,react-dom 是供我们获取真实 dom 以及 react 本身将虚拟 dom 和真实 dom 进行同步的库,prop-types 是 react 一个定义原型链属性、类型、默认值的库

2、安装 babel-loader@8.0.0-beta.0 @babel/core @babel/preset-env @babel/preset-react 到依赖库中,并创建 .babelrc 文件

1
npm install babel-loader@8.0.0-beta.0 @babel/core @babel/preset-env @babel/preset-react --save-dev

bale-loader 是专门用来将 ES5 及其以上 JavaScript 语法标准转换为 ES5 的加载器,其核心为 @babel/core,@babel/preset-env 和 @babel/preset-react 是将 bale 转换定义为预设转换指定环境及 react 环境,这里的指定环境一般为标准环境。

1
2
3
4
5
6
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": []
}
// .babelrc

3、配置 babel-loader 到 webpack 中

1
2
3
4
5
{
test: /.(js|jsx)\$/,
loader: "babel-loader?cacheDirectory=true",
exclude: /node_modules/
}

编译时自动匹配 js 或者 jsx 文件进行 babel 转换并缓存,同时忽略转换 node_modules 库。这里 cacheDirectory=true 在我们进行转换过程中会优先读取缓存中的转换结果并差异化新的转换文件,这样可以提高我们的语法转换速度。

4、安装 @babel/runtime 或者 @babel/runtime-corejs2

1
npm install @babel/runtime --save-dev 或者 npm install @babel/runtime-corejs2 --save-dev

这两个插件都是 polyfill 包,及转换过程中的语法规则补全库,因为 babel 默认只转换标准 ES 语法规则,所以我们在项目中可能用到的一些非标准语法规则比如 Object.assign 无法转换,所以就需要这个补全插件来帮我们实现转换。

注:@babel/runtime 插件是全局引入,使用这个插件会侵入全局变量,相对来说 @babel/runtime-corejs2 不会侵入改变全局变量,所以我们这里安装后者。

5、安装 @babel/plugin-transform-runtime

1
npm install @babel/plugin-transform-runtime --save-dev

@babel/plugin-transform-runtime 和 @babel/runtime 是相互匹配使用的。这个插件引用 @babel/runtime 中的 polyfill 进行 es6 向 es5 转化时自动补充 es5 不支持的特性。

1
2
3
4
5
6
7
8
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
["@babel/plugin-transform-runtime", { "corejs": 2 }]
]
}
// .babelrc

6、兼容 ES7 标准

因为目前的 babel7 不再支持 @babel/preset-stage-0 等阶段预设。所以我们需要安装相应插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  "plugins": [
    // Stage 0
    "@babel/plugin-proposal-function-bind", // 函数绑定
 
    // Stage 1
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-logical-assignment-operators",
    ["@babel/plugin-proposal-optional-chaining", { "loose": false }],
    ["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }],
    ["@babel/plugin-proposal-nullish-coalescing-operator", { "loose": false }],
    "@babel/plugin-proposal-do-expressions",
 
    // Stage 2
    ["@babel/plugin-proposal-decorators", { "legacy": true }], // 装饰器
    "@babel/plugin-proposal-function-sent",
    "@babel/plugin-proposal-export-namespace-from",
    "@babel/plugin-proposal-numeric-separator",
    "@babel/plugin-proposal-throw-expressions",
 
    // Stage 3
    "@babel/plugin-syntax-dynamic-import", // 语法动态引入
    "@babel/plugin-syntax-import-meta",
    ["@babel/plugin-proposal-class-properties", { "loose": false }], // 解析类的属性
    "@babel/plugin-proposal-json-strings"
  ]
}

上面列举了 ES7 从 Stag0 到 Stage3 四个阶段的插件。这里我们安装我们常用的

1
npm install @babel/plugin-syntax-dynamic-import @babel/plugin-proposal-function-bind @babel/plugin-transform-object-assign @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties @babel/plugin-transform-arrow-functions --save-dev

注:这里单独说明下因为我们的项目比较旧,不支持箭头函数,虽然经过上面的转换我们可以在 react 中使用箭头函数,但是在原始项目中依然不能,所以需要单独引入 @babel/plugin-transform-arrow-functions 来转换 ES5 中的箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-function-bind",
["@babel/plugin-transform-runtime", { "corejs": 2 }],
"@babel/plugin-transform-object-assign",
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-arrow-functions",
]
}
// .babelrc

7、严格模式

因为我们的项目比较老,所以 babel 默认的严格模式开启的情况下,很多语法并不支持所以我们还需要关闭严格模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
npm install @babel/plugin-transform-modules-commonjs --save-dev
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-function-bind",
["@babel/plugin-transform-runtime", { "corejs": 2 }],
"@babel/plugin-transform-object-assign",
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-arrow-functions",
[
"@babel/plugin-transform-modules-commonjs",
{
"strictMode": false
}
]
]
}
// .babelrc

到这里位置,我们解决了兼容语法转换的问题,基本就算完成环境配置的一半了,我们引入 React 之后还需要引入 UI 库 antd,所以最终配置如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
npm install antd --save
npm install babel-plugin-import --save-dev
{
"presets": ["@babel/preset-env", "@babel/preset-react"],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-proposal-function-bind",
["@babel/plugin-transform-runtime", { "corejs": 2 }],
"@babel/plugin-transform-object-assign",
["@babel/plugin-proposal-decorators", { "legacy": true }],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-arrow-functions",
[
"@babel/plugin-transform-modules-commonjs",
{
"strictMode": false
}
],
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
]
]
}
// .babelrc

8、handlebars 在 Backbone 中广泛定义模板语法

1
{ test: /\.hbs$/, loader: "handlebars-loader" }

9、css、stylus、scss、less 样式定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
},
{
test: /\.styl$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!stylus-loader"
})
},
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader!sass-loader"
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: [
{
loader: "css-loader",
options: {
modules: true,
localIdentName: "[path][name]--[local]--[hash:base64:5]"
}
},
{ loader: "less-loader" }
]
})
}

注:ExtractTextPlugin 插件为了帮助我们将所有样式文件导出成一个单独的 css 文件而使用

10、定义 resolve

1
2
3
4
5
6
alias: {
src: path.resolve(__dirname, "assets/src/"),
images: path.resolve(__dirname, "assets/images/")
...
}

这里将我们的文件路径和图片资源定义绝对路径,也可以在这里定义手动导入三方库的绝对路径

11、配置 plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
plugins: [
// 环境配置 (产品环境下配置)
new webpack.DefinePlugin({
"process.env": {
NODE_ENV: JSON.stringify("production")
}
}),
// 外部三方库全局定义引用
new webpack.ProvidePlugin({
_: "lodash",
jQuery: "jquery",
$: "jquery",
Backbone: "backbone",
Tether: "tether/tether",
Highcharts: "highcharts"
}),
// css 输出
new ExtractTextPlugin("stylesheets/bundle.css"),
// 资源文件复用
new CopyWebpackPlugin([
{
from: "assets/images",
to: "../static/lib/img"
}
]),
// 浏览器端口代理 (开发环境下配置)
new BrowserSyncPlugin({
host: "localhost",
port: 3001,
proxy: "http://localhost:3000/"
})
// 该项目在rails中进行代码压缩,所以这里就不要进行压缩了
// new webpack.optimize.UglifyJsPlugin({
// compress: {
// warnings: false
// }
// })
]

12、最后我们还需要设置 devTool 来为我们在开发环境和产品环境输出 sourcemap 来调试

development: cheap-module-eval-source-map
production: none(默认)

到此为止项目环境兼容集成告一段落,当然还有些细节配置就不再赘述,我们的配置中也还可以再加入一些,比如 eslint 来检测规范代码,这里因项目加入 eslint 成本太高暂时先不考虑。

React 组件开发

完成环境转换配置之后,我们终于可以进行 React 组件的开发了,我们在 Backbone 项目中进行 React 组件开发主要是将 React 的 View 层替换 Bckbone 的 template 模板

引入:

1
2
3
import React from "react";
import ReactDOM from "react-dom";
import SelectProduct from "src/ReactComponent/SelectProduct";

在引入 React 库和组件头文件之后,我们进行渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="select-product-view" />
ui: {
selectProductView: ".select-product-view"
},
mountSelectProductView: function() {
const wrapper = this.ui.selectProductView[0];
ReactDOM.render(
<SelectProduct
ref={this.selectProductRef}
onChange={this.onProductChange}
/>,
wrapper
);
},
export default class SelectProduct extends React.Component {}

上面简单的介绍了在环境集成之后进行 React 组件开发的基本过程。也是我们最终想要达到的目的