React 开发简易 App
结合 React 技术开发简易的查询豆瓣电影 App ,开发过程中使用 Node.js 搭建代理服务器,跨域请求豆瓣电影的一些 API 接口。
应用详细代码访问我的 github 地址。
路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| export default class AppContainer extends React.Component { render() { return ( <div> <h1>App</h1> <ul> <li><Link to="/about">About</Link></li> <li><Link to="/inbox">Inbox</Link></li> </ul> {/* 路由匹配的内容 */} {this.props.children} </div> ) } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export default class Routers extends React.Component { render() { return ( <Router history={hashHistory}> <Route path="/" component={App}> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> <Route path="messages/:id" component={Message} /> </Route> </Route> </Router> ) } } ReactDOM.render( <Routers/>, document.getElementById('app') )
|
添加首页
当 URL 为 / 时,我们想渲染一个在 App 中的组件。不过在此时,App 的 render 中的 this.props.children 还是 undefined。这种情况我们可以使用 IndexRoute 来设置一个默认页面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export default class Routers extends React.Component { render() { return ( <Router history={hashHistory}> <Route path="/" component={App}> {/* 当 url 为/时渲染 Dashboard */} <IndexRoute component={Dashboard} /> <Route path="about" component={About} /> <Route path="inbox" component={Inbox}> {/* 使用 /messages/:id 替换 messages/:id */} <Route path="/messages/:id" component={Message} /> {/* 跳转 /inbox/messages/:id 到 /messages/:id */} <Redirect from="messages/:id" to="/messages/:id" /> </Route> </Route> </Router> ) } }
|
进入和离开的hook
1 2 3 4 5 6 7
| <Route path="about" component={Inbox} onEnter={() => console.log('进入了Inbox路由')} onLeave={() => console.log('离开了Inbox路由')} > </Route>
|
路由匹配原理
路径语法
路由路径是匹配一个(或一部分)URL 的 一个字符串模式。大部分的路由路径都可以直接按照字面量理解,除了以下几个特殊的符号:
- :paramName – 匹配一段位于 /、? 或 # 之后的 URL。 命中的部分将被作为一个参数
- () – 在它内部的内容被认为是可选的
- – 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并创建一个 splat 参数
1 2 3
| <Route path="/hello/:name"> <Route path="/hello(/:name)"> // 匹配 /hello, /hello/michael 和 /hello/ryan <Route path="/files/*.*"> // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg
|
History
- browserHistory
- 去掉 URL 地址中的
#标记
- 刷新后无法找到原网页
- hashHistory
- createMemoryHistory
默认路由 (IndexRoute) 与 IndexLink
如果你在这个 app 中使用 <Link to="/">Home</Link> , 它会一直处于激活状态,因为所有的 URL 的开头都是 / 。 这确实是个问题,因为我们仅仅希望在 Home 被渲染后,激活并链接到它。
如果需要在 Home 路由被渲染后才激活的指向 / 的链接,请使用 <IndexLink to="/">Home</IndexLink>
组件内部跳转
1 2 3 4 5 6 7 8 9 10
| static contextTypes = { router: PropTypes.object } componentDidMount() { setTimeout(() => { this.context.router.push('/home') }, 3000) }
|
组件外部跳转
虽然在组件内部可以使用 this.context.router 来实现导航,但许多应用想要在组件外部使用导航。
1 2 3 4 5 6 7 8 9
| import {hashHistory} from 'react-router'
export default { getMovieListData() { setTimeout(function() { hashHistory.push('/home') }, 3000) } }
|
在 React 中使用 CSS
直接在组件中引入对应的css, 每个组件根标签设置一个独立的class类名
等待效果切换
使用this.state = {}, 通过修改state的值来切换等待效果
通过代理服务器请求数据
自己的网页访问自己的代理服务器也存在跨域问题, 只要浏览器与服务器端口号和 ip 不一致就会存在跨域问题
- fetch
- 语法简洁, 更加语义化
- 未来取代XMLHttpRequest对象
- 基于标准Promise实现, 支持async/await
- 同构方便, 使用isomorphic-fetch, 在浏览器及Node中均能使用
Promise 实现
Promise/A+规范解决异步回调深层嵌套问题
Promise/A+规范本质是一种书写格式的改变
- Angular: $q 服务
- Node: q 模块, co, then
- ES6: Promise, yield
- ES7: async await
promise有三种状态
- Unfulfilled(Pending未完成, 初始状态)
- Fulfilled(Resloved已完成)
- Failed(Rejected失败, 拒绝)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var promise = new Promise((resolve, reject) => { fetch(url).then(response => { if (response.ok) { return response.json() } else { console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`) } }).then(data => { resolve(data) }).catch(err => { reject(err) }) })
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| promise .then(function(data) { }, function(err) { });
promise .then(function(data) { }) .catch(function(err) { });
|
promise 对象使用时必须通过 then 方法调用改变状态, 上一个 then 里面的输入值就是下一个 then 里面的输出值
一般来说,不要在 then 方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法
如果 then 的第二个参数存在, 发生错误走 then 的第二个参数, 否则走 catch
渲染电影列表页面
循环渲染子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| renderMovieList = () => { return ( <div ref={div => (this.theDiv = div)} className="movieList_container"> {this.state.movieListData.map(this.renderItem)} <div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div> </div> ) }
renderItem = (item) => { return ( <div className="movieList_item" key={item.id} onClick={() => this.goDetail(item.id)}> <img src={item.images.small} alt=""/> <div> <h1>{item.title}</h1> <span>{item.year}</span> </div> </div> ) }
|
通过 fetch 请求数据
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| fetch = (movieType) => { if (this.state.messages.movieType !== movieType) { console.log(6) this.theDiv.scrollTop = 0 this.theDiv.onscroll = null this.setState({ isLoading: true, isBottom: false, movieListData: [], messages: { movieType: movieType, count: 10, start: 0, pageIndex: 1 } }) return } console.log(7) let messages = Object.assign({}, this.state.messages) let movieListData = [].concat(this.state.movieListData) messages.movieType = movieType messages.start = (messages.pageIndex - 1) * messages.count messages.pageIndex++
const message = JSON.stringify(messages) const promise = service.getMovieListData(message) promise.then( data => { console.log(data) if (movieListData.length > 0) { movieListData = movieListData.concat(data.subjects) } else { movieListData = data.subjects } this.setState({ isLoading: false, isBottom: false, movieListData: movieListData, messages: messages }) } ).catch(err => { console.error(err) }) }
|
添加 CSS 样式
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
| .movieList_container { height: 100%; overflow-y: scroll; }
.movieList_item { display: -webkit-flex; display: -moz-flex; display: -ms-flex; display: -o-flex; display: flex; height: 10rem; }
.movieList_item img { width: 5rem; height: 9rem; }
.movieList_item div { display: flex; flex-direction: column; justify-content: space-evenly; }
.movieList_hide { display: none; }
.movieList_show { background-color: lawngreen; text-align: center; }
|
请求接口改造和对象深拷贝
请求接口改造
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
| import config from '../js/config.js'
export default { getMovieListData(message) { console.log(message) return new Promise((resolve, reject) => { const url = `${config.HTTP}${config.SERVER_PATH}:${config.PORT}/api/getMovieListData?message=${message}` console.log(url)
fetch(url) .then(response => { if (response.ok) { return response.json() } else { console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`) } }) .then(data => { console.log(data) resolve(data) }) .catch(err => { reject(err) }) }) } }
|
对象深拷贝
1 2 3
| let messages = Object.assign({}, this.state.messages) let movieListData = [].concat(this.state.movieListData)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var obj = { name: 'tom', age: 18, data: { sex: '男', dog: 'jim' } }
var str = JSON.stringify(obj) var obj1 = JSON.parse(str)
console.log(obj1) console.log(obj === obj1) console.log(obj.data === obj1.data)
|
电影类别切换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| componentWillReceiveProps(nextProps) { console.log(2) this.fetch(nextProps.params.movieType) } componentDidUpdate(prevProps, prevState) { if (this.state.isLoading) { console.log(3) this.fetch(this.state.messages.movieType) } else { console.log(4) this.theDiv.onscroll = null this.addEventListener() } }
|
1 2 3 4 5 6 7 8 9 10 11
| changeMovieType = (movieType) => { this.setState({ movieType: movieType }) }
<div className="movie_menu"> <Link onClick={() => this.changeMovieType('in_theaters')} className={this.state.movieType === 'in_theaters' ? 'movie_current' : ''} to="/movieList/in_theaters">正在热映</Link> <Link onClick={() => this.changeMovieType('coming_soon')} className={this.state.movieType === 'coming_soon' ? 'movie_current' : ''} to="/movieList/coming_soon">即将上映</Link> <Link onClick={() => this.changeMovieType('top250')} className={this.state.movieType === 'top250' ? 'movie_current' : ''} to="/movieList/top250">Top250</Link> </div>
|
不可变数据结构
Immutable 与 SImmutable
页面下滑加载更多数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| addEventListener = () => { this.theDiv.onscroll = e => { if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) { console.log(5) if (this.state.isBottom) { return } this.fetch(this.state.messages.movieType) this.setState({ isBottom: true }) } } }
|
用户加载数据提示, 并且防止多次触发滚动事件
下滑加载数据提示
1 2 3 4 5 6 7 8 9
| renderMovieList = () => { return ( <div ref={div => (this.theDiv = div)} className="movieList_container"> {this.state.movieListData.map(this.renderItem)} <div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div> </div> ) }
|
防止多次触发滚动事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| addEventListener = () => { this.theDiv.onscroll = e => { if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) { console.log(5) if (this.state.isBottom) { return } this.fetch(this.state.messages.movieType) this.setState({ isBottom: true }) } } }
|
切换电影类别时重置数据
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
| fetch = (movieType) => { if (this.state.messages.movieType !== movieType) { console.log(6) this.theDiv.scrollTop = 0 this.theDiv.onscroll = null this.setState({ isLoading: true, isBottom: false, movieListData: [], messages: { movieType: movieType, count: 10, start: 0, pageIndex: 1 } }) return } ... ... ... }
|
电影详细页面
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| import React from 'react' import PropTypes from 'prop-types' import Loading from '../components/Loading.jsx' import service from '../services/movieListData.js'
import '../styles/movieDetail.css'
export default class MovieDetailContainer extends React.Component { constructor(props) { super(props) this.state = { isLoading: true, movieDetailData: [] } } static propTypes = { params: PropTypes.object } componentDidMount() { this.fetch(this.props.params.id) } fetch = (id) => { const promise = service.getMovieDetailData(id) promise.then( data => { console.log(data) this.setState({ isLoading: false, movieDetailData: data, }) } ).catch(err => { console.error(err) }) } renderLoading = () => { return ( <div className="movieDetail_container"> <Loading/> </div> ) } renderMovieDetail = () => { return ( <div className="movieDetail_container"> <div className="movieDetail_image"> <img src={this.state.movieDetailData.images.large} alt=""/> </div> <h1>{this.state.movieDetailData.title}</h1> <p>{this.state.movieDetailData.year}</p> <p>{this.state.movieDetailData.summary}</p> </div> ) } render() { if (this.state.isLoading) { return this.renderLoading() } return this.renderMovieDetail() } }
|
电影搜索页面
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| import React from 'react' import PropTypes from 'prop-types' import Loading from '../components/Loading.jsx' import service from '../services/movieListData.js'
import '../styles/movieList.css'
export default class MovieSearchContainer extends React.Component { constructor(props) { super(props) this.state = { isLoading: true, isBottom: false, movieListData: [], messages: { keyword: this.props.params.keyword, count: 10, start: 0, pageIndex: 1 } } } static propTypes = { params: PropTypes.object } static contextTypes = { router: PropTypes.object } componentDidMount() { this.fetch(this.state.messages.keyword) } componentWillReceiveProps(nextProps) { this.fetch(nextProps.params.keyword) } componentDidUpdate(prevProps, prevState) { if (this.state.isLoading) { this.fetch(this.state.messages.keyword) } else { this.theDiv.onscroll = null this.addEventListener() } } addEventListener = () => { this.theDiv.onscroll = e => { if (e.target.scrollHeight === e.target.scrollTop + e.target.offsetHeight) { if (this.state.isBottom) { return } this.fetch(this.state.messages.keyword) this.setState({ isBottom: true }) } } } fetch = (keyword) => { if (this.state.messages.keyword !== keyword) { this.theDiv.scrollTop = 0 this.theDiv.onscroll = null this.setState({ isLoading: true, isBottom: false, movieListData: [], messages: { keyword: keyword, count: 10, start: 0, pageIndex: 1 } }) return } console.log(7) let messages = Object.assign({}, this.state.messages) let movieListData = [].concat(this.state.movieListData) messages.keyword = keyword messages.start = (messages.pageIndex - 1) * messages.count messages.pageIndex++
const message = JSON.stringify(messages) const promise = service.searchMovieListData(message) promise.then( data => { console.log(data) if (movieListData.length > 0) { movieListData = movieListData.concat(data.subjects) } else { movieListData = data.subjects } this.setState({ isLoading: false, isBottom: false, movieListData: movieListData, messages: messages }) } ).catch(err => { console.error(err) }) } goDetail = (id) => { this.context.router.push(`/movieDetail/${id}`) } renderLoading = () => { return ( <div className="movieList_container"> <Loading/> </div> ) } renderItem = (item) => { return ( <div className="movieList_item" key={item.id + Math.random()} onClick={() => this.goDetail(item.id)}> <img src={item.images.small} alt=""/> <div> <h1>{item.title}</h1> <span>{item.year}</span> </div> </div> ) } renderMovieList = () => { return ( <div ref={div => (this.theDiv = div)} className="movieList_container"> {this.state.movieListData.map(this.renderItem)} <div className={this.state.isBottom ? 'movieList_show' : 'movieList_hide'}>正在玩命加载中, 请稍候......</div> </div> ) } render() { if (this.state.isLoading) { return this.renderLoading() } return this.renderMovieList() } }
|
封装 Loading 通用组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import React from 'react' import Loading from 'react-loading'
import './Loading.css'
export default class ComponentName extends React.Component { constructor(props) { super(props) this.state = {
} } render() { return ( <div className="loading_component"> <Loading className="loading_loading" type="spin" color="red"/> </div> ) } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| .loading_component { height: 100%; display: flex; justify-content: center; align-items: center; }
.loading_loading { height: 100px!important; width: 100px!important; }
|
结合 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
| import React from 'react' import PropTypes from 'prop-types' import { message as Message, Form, Input, Tooltip, Icon, Select, Button } from 'antd' import service from '../services/aboutService.js'
import '../styles/about.scss' import 'antd/dist/antd.css'
const FormItem = Form.Item const Option = Select.Option
class AboutContainer extends React.Component { constructor(props) { super(props) this.state = {
} } static propTypes = { form: PropTypes.object } handleSubmit = (e) => { e.preventDefault(); this.props.form.validateFieldsAndScroll((err, values) => { if (err) { console.error(err) return } this.sendFeedback(values) }) } handleReset = () => { this.props.form.resetFields() } sendFeedback = (message) => { const messageObj = JSON.stringify(message) const promise = service.sendFeedback(messageObj) promise.then( data => { if (data.status === 'OK') { this.handleReset() Message.success('您的意见反馈我们已经收到, 请耐心等待问题的解决!') } } ).catch( err => { console.error(err) } ) } render() { const { getFieldDecorator } = this.props.form const formItemLayout = { labelCol: { xs: { span: 24 }, sm: { span: 6 }, }, wrapperCol: { xs: { span: 24 }, sm: { span: 14 }, }, } const tailFormItemLayout = { wrapperCol: { xs: { span: 24, offset: 0, }, sm: { span: 14, offset: 6, }, }, } const prefixSelector = getFieldDecorator('prefix', { initialValue: '86', })( <Select style={{ width: 60 }}> <Option value="86">+86</Option> <Option value="87">+87</Option> </Select> ) return ( <Form onSubmit={this.handleSubmit}> <FormItem {...formItemLayout} label={( <span> 反馈信息 <Tooltip title="What do you want other to call you?"> <Icon type="question-circle-o" /> </Tooltip> </span> )} hasFeedback > {getFieldDecorator('feedback', { rules: [{ required: true, message: 'Please input your feedback!', whitespace: true }], })( <Input /> )} </FormItem> <FormItem {...formItemLayout} label="联系电话" > {getFieldDecorator('phone', { rules: [{ pattern: /^[0-9]{11}$/, required: true, message: 'Please input your phone number!' }], })( <Input addonBefore={prefixSelector} style={{ width: '100%' }} /> )} </FormItem> <FormItem {...formItemLayout} label={( <span> 联系姓名 <Tooltip title="What do you want other to call you?"> <Icon type="question-circle-o" /> </Tooltip> </span> )} hasFeedback > {getFieldDecorator('name', { rules: [{ required: true, message: 'Please input your name!', whitespace: true }], })( <Input /> )} </FormItem> <FormItem {...tailFormItemLayout}> <Button type="primary" htmlType="submit">Register</Button> </FormItem> </Form> ) } }
export default Form.create()(AboutContainer)
|
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
| import config from '../js/config.js'
export default { sendFeedback(message) { return new Promise((resolve, reject) => { console.log(message) const url = `${config.HTTP}${config.SERVER_PATH}:${config.PORT}/api/sendFeedback` console.log(url) fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `message=${message}` }) .then(response => { if (response.ok) { return response.json() } else { console.error(`服务器忙,请稍后再试;\r\nCode:${response.status}`) } }) .then(data => { console.log(data) resolve(data) }) .catch(err => { reject(err) }) }) } }
|
代码异步加载与部署
修改路由配置文件, 使用 getComponent 方法
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| import React from 'react' import {Router, Route, Redirect, browserHistory, IndexRoute} from 'react-router'
import AppContainer from '../containers/AppContainer.jsx' import HomeContainer from '../containers/HomeContainer.jsx'
export default class Routers extends React.Component { constructor(props) { super(props) this.state = {
} } render() { return ( <Router history={browserHistory}> <Route path='/' component={AppContainer}> <IndexRoute component={HomeContainer}/> <Route path='home' component={HomeContainer}/> <Route path='movie' getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/MovieContainer.jsx').default) }, 'movie') } } onEnter={() => console.log('进入了movie路由')} onLeave={() => console.log('离开了movie路由')} > <IndexRoute getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/MovieListContainer.jsx').default) }, 'movieList') } } /> {/* 绝对路由带有'/',不带'/'的路由基于父组件的路径 */} <Route path='/movieList/:movieType' getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/MovieListContainer.jsx').default) }, 'movieList') } } /> <Route path='/movieDetail/:id' getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/MovieDetailContainer.jsx').default) }, 'movieDetail') } } /> <Route path='/movieSearch/:keyword' getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/MovieSearchContainer.jsx').default) }, 'movieSearch') } } /> {/* 路由重定向 */} <Redirect from='movieList' to='/movieList'/> <Redirect from='movieDetail' to='/movieDetail'/> <Redirect from='movieSearch' to='/movieSearch'/> </Route> <Route path='about' getComponent={ (nextState, callback) => { require.ensure([], require => { callback(null, require('../containers/AboutContainer.jsx').default) }, 'about') } } /> </Route> </Router> ) } }
|
部署服务器
- 使用 FileZila 软件登录自己的服务器
- 将 webpack2 打包好的生产文件上传到服务器
- 使用 gitbash 的
ssh root@xxx.xx.x.xx 命令登录服务器
- 进入上传的生产文件目录, 使用pm2模块启动服务
pm2 start index.js
- 接下来就可以访问服务器地址的指定端口了