现代前端应用是一个工程化的系统,和jquery时代有着很大的不同,以前的主要工作是模板填充、事件绑定、DOM更新等,而现在前端应用和后端工程一样,需要从一个完整的系统层面思考,数据流和控制流如何流转?数据该如何存放?业务方法如何管理?UI组件如何封装和路由?最后还有编译打包,优化等问题。幸运的是已有各种工具帮我们解决了绝大部分的问题,可如何将这些工具库组合在一起形成一个可用的应用,并不是一件简单的事情,例如我们需要大量的配置来使webpack和babel按照要求工作。
那如何能快速的开始工作呢?选择一个前端框架定会事半功倍,例如最常用的react-create-app、next.js和dva-cli等,都只需要简单的几步操作就可以开始进行业务开发了。如果你还没有听说过它们或者它们的同类,相信你现在正在被各种配置文件、架构结构、多人协同开发感到困惑。这里介绍的是另外一个前端框架@symph/joy,它结合了next.js和dva的特点,是一个全栈型的框架(因为每个项目对UI的要求都不一样,所以未集成UI组件),尽量让前端开发更加简洁、规范。
react-create-app是一个比较基础的脚手架,很多东西还得开发者自己搭建,并不建议在实际工程中使用。
我们现在从一个React新手的角度思考问题,对webpack、babel、redux等工具还不了解,只是想用React写一个简单的展示页面,该如何开始工作呢?
创建项目目录
运行yarn init
创建一个空工程,并填写项目的基本信息,当然也可以在一个已有的项目中直接安装。
yarn install --save @symph/joy react react-dom
添加NPM脚本到package.json文件:
{
"name": "hello-word",
"version": "1.0.0",
"scripts": {
"dev": "joy",
"build": "joy build",
"start": "joy start"
}
}
编写第一个页面
然后就可以开始你的开发工作了,创建./src/index.js
文件,并插入以下代码:
import React, { Component } from 'react'
export default class Index extends Component {
render () {
return <div>
<div>Welcome to @symph/joy!</div>
</div>
}
}
得到了什么
最后运行yarn run dev
命令,在浏览器中输入访问地址http://localhost:3000
进行上面简单的几步后,我们拥有了些什么呢?
- 应用入口(
./src/index.js
),一切都从这里开始,以后可以添加子路由、布局、Model等组件 - 启动了一个服务器,支持服务端渲染和业务请求代理转发
- 一个零配置的webpack+babel编译器,确保代码在Node.js和浏览器上正确运行
- ES6、7、8等高级语法支持,如:
class
、async
、@
注解、{...}
解构等 - 热更新,调试模式下,在浏览器不刷新的情况下,使更改立即生效
- 静态资源服务,在
/static/
目录下的静态资源,可通过http://localhost:3000/static/
访问
第二个页面,添加路由
接下来开始第二个页面的开发,假设添加一个src/about.jsx
页面。
// src/about.jsx
import React, { Component } from 'react'
export default About class extends Component {
render () {
return <div>
<div>about @symph/joy</div>
</div>
}
}
此时需要路由组件来完成页面之间的跳转,@symph/joy
使用react-router-4作为路由,并在应用初始时已集成到应用中,所以不需要开发者自己添加依赖和初始化react-router-4
,我们可以直接从'@symph/joy/router'
中导入并使用。
首先对之前的src/index.js
进行改造,将首页的内容独立出来,作为一个单独的页面,方便在两个界面之间跳转。
// src/hello.jsx
import React, { Component } from 'react'
export default Hello class extends Component {
render () {
return <div>
<div>Welcome to @symph/joy!</div>
</div>
}
}
修改入口组件
// src/index.js
import React, { Component } from 'react'
import {Switch, Route, Link} from '@symph/joy/router'
import Hello from './hello'
import About from './about'
export default class Index extends Component {
render () {
return (<div>
<div className="header">
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</div>
<Switch>
<Route exact path="/about" component={About}/>
<Route path="/" component={Hello}/>
</Switch>
</div>)
}
}
使用样式
jsx内建样式
内建了 styled-jsx 模块,支持Component内独立域的CSS样式,不会和其他组件的同名样式冲突。
// src/hello.js
import React, { Component } from 'react'
export default class Hello extends Component {
render () {
return (<div>
<div className="message">Welcome to @symph/joy!</div>
<style jsx>{`
.message {
color: blue;
}
`}</style>
<style global jsx>{`
body {
background: #F5F5F5;
}
`}</style>
</div>)
}
}
查看 styled-jsx 文档 ,获取详细使用信息。
Import CSS / LESS 文件
@symph/joy
提供了插件来处理样式,默认支持post-css、autoprefixer、css-modules、css-extract,具体使用方法请查看插件使用文档。
MVC工程模式
我们可以在页面上使用fetch方法来获取数据并展示,此时已经具备了编写一个前端应用的全部能力了。但作为一个日益复杂,需多人协同开发,且可持续维护的项目来说,还远远不够。至少我们还缺少对数据和业务方法的管理,并且要形成一个规范共识,才可持续的发展下去。
MVC模式用于规范我们的代码设计,这是一个很基础的,也是很容易理解的模式,要将上面的例子变为MVC模式,只需要添加几行声明式的注解代码,不会侵入业务代码。MVC三个字母代表着应用中的三个不同类别的部件,它们有各自的职责:
- Model: 管理应用的行为和数据,普通class类,有初始状态,业务运行中更新model状态
- View: 展示数据,继承React.Component
- Controller: 控制View的展示,绑定Model数据到View,响应用户的操作,调用Model中的业务, 继承于React.Component
它们之间的工作流程如下图
图中蓝色的箭头表示数据流的方向,红色箭头表示控制流的方向,他们都是单向流,store中的state
对象是不可修改其内部值的,状态发生改变后,都会生成一个新的state对象,且只将有变化的部分更新到界面上,这和redux的工作流程是一致的。
如果你刚接触React和Redux不久,也许对上面的概念理解起来有点困难,请不要担心,框架就是帮助我们解决掉各种复杂的概念和繁琐重复的工作而的,让我们更专注于业务开发。所以这里,我们完全可以放心进行下一步操作。
下面通过一个商品展示的例子来说明Model,View,Controller之间是如何工作的,查看示例工程。
目录结构建议采用以下形式
-project
-src
-components/
-controllers/
-ProductsController.js
-models/
-ProductsModel.js
-index.js
### 创建Model
下面展示的是一个管理商品列表的model,负责存放商品列表、分页信息等,还有获取和处理这些信息的业务方法。
// src/models/ProductsModel.js
import model from '@symph/joy/model'
import fetch from '@symph/joy/fetch'
// 使用注解的方式,申明这是一个Model
@model()
export default class ProductsModel {
// 这个model的全局名称,后面需要通过这个名称来找到model的状态。
namespace = 'products';
// model的初始化状态
initState = {
pageIndex: null,
pageSize: 10,
products: [],
};
async getProducts({pageIndex = 1, pageSize}) {
// 发送远程请求,获取数据
let pagedProducts = await fetch('https://www.example.com/api/hello',
{body:{pageIndex, pageSize}});
let {products} = this.getState();
if (pageIndex === 1) {
products = data;
} else {
products = [...products, ...pagedProducts];
}
// 更新model的状态,此时和model状态绑定的Controller也会同步更新
this.setState({
products,
pageIndex,
pageSize
});
}
};
创建Controller
接下里编写一个Controller来绑定上面ProductsModel
中的产品列表,并在界面加载的时候,调用获取产品列表的业务方法。
// src/controllers/ProductsController.js
import React, {Component} from 'react';
import ProductsModel from '../models/ProductsModel'
import controller, {requireModel} from '@symph/joy/controller'
@requireModel(ProductsModel) // 注册依赖的model
@controller((state) => { // 使用注解的方式,申明这是一个Controller
return {
products: state.products.products // 绑定ProductsModel中的产品列表
}
})
export default class ProductsController extends Component {
async componentPrepare() {
let {dispatch} = this.props;
// TOOD 这里可以打开加载动画
await dispatch({ // 调用ProductsModel中获取产品列表的业务方法
type: 'products/getProducts',
pageIndex: 1,
pageSize: 10,
});
// TOOD 这里可以关闭加载动画
}
render() {
let {products = []} = this.props; // 获取绑定的产品列表
return (
<div >
<div>Product List</div>
<div>
{products.map((product, i) => {
return <div key={product.id} onClick={this.onClickProduct.bind(product)}>{product.id}:{product.name}</div>
})}
</div>
</div>
);
}
}
使用View
View只是一个普通的React.Component类,是一个纯展示组件,它的数据来自于props属性。
打包部署
最后我们需要将应用打包部署到生产机器上,@symph/joy
提供了命令工具来处理这些事情。在package.json
的scripts
节点中添加NPM脚本:
// package.json
{
"scripts": {
"dev": "joy dev",
"build": "joy build",
"start": "joy start"
}
}
在命令行工具里使用yarn run build
命令来编译代码,生成.joy
目标目录。然后将.joy
目录上传到生产机器上,不用上传源代码文件了。最后在生产机器上执行yarn run start
命令来启动应用,此时会直接运行.joy
目录中预先编译好的代码。
静态版本输出
如果我们的前端应用没有专门的node.js服务器来运行,或者不需要服务端渲染,可以将应用编译为静态版本,将其放到静态资源服务器上,浏览器直接加载运行。
这不需要修改任何的源代码,只需添加一个NPM命令export
到package.json
文件中:
{
"scripts": {
"build": "joy build",
"export": "yarn run build && joy export"
}
}
执行yarn run export
命令完成导出工作,静态版本需要的所有文件都放在应用根目录下的out
目录中,只需要将out
目录部署到静态文件服务器就可以了。
高级功能
上文只是对前端日常开发工作做了介绍,但在实际项目中,还会遇到很多需求,例如:
- 集成到express、koa等服务端框架中
- 定义html渲染,加入js、css文件引用,自定义head部分等
- 跨域请求如何解决
- 更复杂的路由控制
- 浏览器端js按需加载、代码分割、压缩等优化处理
- 自定义webpack和babel配置等
@symph/joy
对以上问题都提出了解决方案,更多更详细的使用介绍,请查看其主页:https://lnlfps.github.io/symph-joy