现代前端应用是一个工程化的系统,和jquery时代有着很大的不同,以前工作中大部分时间都在模板填充、事件绑定、DOM更新等,而现在前端应用和后端工程一样,是一个完整的系统化工程,数据流和控制流如何流转?数据该如何存放?业务方法如何管理?UI组件如何封装和路由?最后还有编译打包,优化等问题。幸运的是已有各种工具帮我们解决了绝大部分的问题,可如何将这些工具库组合在一起形成一个可用的应用,并不是一件简单的事情,例如我们需要大量的配置来使webpack和babel按照要求工作。
那如何能快速的启动工作呢?选择一个前端框架定会事半功倍,例如最常用的react-create-app、next.js和dva-cli等,都只需要简单的几步操作就可以开始进行业务开发了。如果你还没有听说过它们或者类似的框架,相信你现在正在被各种配置文件、应用结构、多人协同开发效率等问题感到困惑。这里介绍的是另外一个前端框架@symph/joy,它结合了next.js和dva的特点,是一个全栈型的框架(因为不同项目对UI的要求差异较大,所以未集成UI组件),只要你会js+react就可以开发高质量的前端应用。
react-create-app是一个比较基础的脚手架,很多东西还得开发者自己搭建,并不建议在实际工程中直接使用。
也许你现在对webpack、babel、redux还完全不了解,没关系,我们依然可以开始工作,因为这些底层技术@symph/joy已经为你准备好了,等你以后想深入定制开发,也可以轻松升级定制它们,下面开始写第一个页面。
创建项目目录
运行npm init
创建一个空工程,并填写项目的基本信息,当然也可以在一个已有的项目中直接安装。
npm install --save @symph/joy react react-dom
添加NPM脚本到package.json文件:
{
"name": "hello-word",
"version": "1.0.0",
"scripts": {
"dev": "joy"
}
}
编写第一个页面
1. 写代码
创建./src/index.js
文件,并插入以下页面代码:
import React, { Component } from 'react'
export default class Index extends Component {
render () {
return (<div>
<div>Welcome to @symph/joy!</div>
</div>)
}
}
2. 运行
最后运行yarn run dev
命令,在浏览器中输入访问地址http://localhost:3000
3. 做了什么
到此处,我们拥有了哪些呢?
- 应用入口(
./src/index.js
),一切都从这里开始,以后可以逐步实现更多功能 - 启动了一个开发服务器,支持服务端渲染和请求代理
- 一个零配置的webpack+babel编译器,确保代码在服务端和浏览器上正确运行
- ES6、7、8等高级语法支持,如:
class
、async
、@
注解、{...}
解构等 - 热更新,调试模式下,在浏览器不刷新的情况下,使更改立即生效
- 静态资源服务,在
/static/
目录下的静态资源,可通过http://localhost:3000/static/
访问 - 支持静态版本导出,可部署到静态服务上,比如Nginx和CDN上
使用样式
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三个字母代表着应用中的三种不同类别的模块,它们有各自的职责:
- Model: 管理应用的数据和行为,有初始状态,业务运行中不断更新状态
- View: 展示数据,继承React.Component
- Controller: 控制协调Model和View,例如:View的展示,绑定Model数据到View,调用Model中的业务
它们之间的工作流程如下图
图中蓝色的箭头表示数据流的方向,红色箭头表示控制流的方向,他们都是单向流,Model的业务方法在运行的过程中动态修改store的state
,state
发生改变时,绑定到该state
的View将自动更新,详细的工作流程请阅读redux文档。
如果你对Redux不了解,请不要担心,框架已经帮我们简化了这些底层细节,让我们更专注于业务开发,这里完全可以放心进行下一步操作。
下面通过一个TODO LIST例子来说明Model,View,Controller之间是如何工作的,查看示例工程。
目录结构建议采用以下形式
-project
-src
-controllers/
-TodoListController.js
-models/
-TodosModel.js
-index.js
创建Model
下面展示的是一个管理待办列表的model。
// src/models/TodosModel.js
import model from '@symph/joy/model'
import fetch from '@symph/joy/fetch'
@model()
export default class TodosModel {
// model的唯一标识,在一个应用内不能重复
namespace = 'todos';
// 初始状态
initState = {
pageSize: 5,
count: 0,
entities: [],
};
async getTodos({lastId: 0, pageSize: 5}) {
// @symph/joy fetch已集成自动代理服务,不用担心是否跨域问题
let pagedTodos = await fetch('https://www.example.com/api/hello',
{body:{lastId, pageSize}});
let {entities} = this.getState();
if (lastId === 0) {
// first page
entities = data;
} else {
entities = [...entities, ...pagedTodos];
}
// 更新state状态
this.setState({
entities,
pageSize
});
}
};
创建Controller
接下里编写一个Controller,绑定上面TodosModel
中的待办列表并展示,在界面初始化的时候加载数据。
// src/controllers/ProductsController.js
import React, {Component} from 'react';
import TodosModel from '../models/TodosModel'
import {controller} from '@symph/joy/controller'
import {autowire} from '@symph/joy/autowire'
@controller((state) => {
return {
todos: state.todos.entities // 绑定model的状态
}
})
export default class IndexController extends Component {
@autowire()
todosModel: TodosModel // 申明依赖的Model
async componentPrepare() {
// 调用Model中的业务方法
await this.todosModel.getTodos(0, 5)
}
render() {
let {todos = []} = this.props;
// View
return (
<div >
<div>Todo List</div>
<div>
{todos.map((todo, i) => {
return <div key={todo.id} >{todo.id}:{todo.content}</div>
})}
</div>
</div>
);
}
}
使用View
View只是一个普通的React.Component类,是一个纯展示组件,它的数据来自于props属性。
添加路由 Router
接下来进行第二个页面的开发,假设添加一个src/controllers/TodoDetailController.js.jsx
页面来展示待办事项的详细信息。
// src/controllers/TodoDetailController.js.jsx
import React, { Component } from 'react'
import { controller } from '@symph/joy/controller'
import { autowire } from '@symph/joy/autowire'
import TodosModel from '../models/TodosModel'
@controller((state, ownProps) => {
const todoId = Number(ownProps.match.params.id)
return {
todoId,
todo: state.todos.details[todoId]
}
})
export default class TodoDetailController extends Component {
@autowire()
todoModel: TodosModel
async componentPrepare () {
let {todoId} = this.props
await this.todoModel.getTodo(todoId)
}
render () {
let {todo} = this.props
return (
<div className={styles.root}>
<h1>Todo Detail</h1>
{
!todo
? <div className={styles.loading}>data loading...</div>
: <div>content: {todo.content}</div>
}
</div>
)
}
}
此时需要路由组件来完成页面之间的跳转,@symph/joy
使用react-router-4作为路由,我们可以直接从'@symph/joy/router'
中导入并使用。
首先对之前的src/index.js
进行改造,将首页的内容独立出来,作为一个单独的页面,方便在两个界面之间跳转。
修改入口组件
// src/index.js
import React, { Component } from 'react'
// 路由组件
import { Switch, Route, Redirect } from '@symph/joy/router'
// 我们的业务组件
import TodoListController from './controllers/TodoListController.js'
import TodoListController from './controllers/TodoDetailController.js'
export default class Main extends Component {
render () {
return (
<div>
<h1>Example Basic - Header</h1>
<Switch>
<Route exact path="/" component={TodoListController}/>
<Route exact path="/todos/:id" component={TodoDetailController}/>
</Switch>
</div>
)
}
}
打包部署
最后我们需要将应用打包,然后部署到生产机器上,@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部分等
- 多Model协同工作
- 更复杂的路由控制
- 浏览器端js按需加载、代码分割、压缩等优化处理
- 自定义webpack和babel配置等
- 404等错误页面自定义
请查看@symph/joy详细使用文档:https://lnlfps.github.io/symph-joy