• react基础学习和react服务端渲染框架next.js踩坑


    react、next

    说明

    React作为Facebook 内部开发 Instagram 的项目中,是一个用来构建用户界面的优秀 JS 库,于 2013 年 5 月开源。作为前端的三大框架之一,React的应用可以说是非常的广泛。这里讲一个react服务端渲染的框架-next.js踩坑过程。

    技术栈

    react、next.js、ant design、axios
    

    大纲

    按照以下思路来写:

    react、next

    react基本语法

    react基本语法参照react文档,这里发放一个链接https://doc.react-china.org/

    以下是React重要的部分

    • JSX – 允许我们编写类似HTML的语法,转换为JavaScript对象;
    • 虚拟DOM – 实际DOM的JavaScript表示;
    • React.Component – 创建新组件的方式;
    • render(方法) – 渲染组件;
    • ReactDOM.render – 用于将模板转为 HTML 语言,并插入指定的 DOM 节点;
    • state – 组件的内部数据存储(对象),类型理解vue的data;
    • constructor(this.state) – 建立组件初始 state(状态) 的方式;
    • setState – 一种辅助方法,用于更新组件的 state(状态) 并重新渲染,类似微信小程序;
    • props – 从父组件传递给子组件的数据;
    • propTypes – 允许您控制传递给子组件的某些 props(属性) 的存在或类型;
    • defaultProps – 允许您为组件设置默认 props(属性) ;
    • className - 节点的class,css标记
    • style jsx样式控制

    jsx语法

    直接写在 JavaScript 语言之中,不加任何引号,它允许 HTML 与 JavaScript 的混写。

    //demo-1.js
    import React from 'react';
    class Demo1 extends React.Component{
      render(){
        const lists = ['我是谁','我来自哪里','我要到哪儿去'];
        return(
          <div className="list">
            <ul>
              {
                lists.map((list,index)=>{
                  return(
                    <li key={index}>{list}</li>
                  )
                })
              }  
            </ul>
          </div>
        )
      }
    }
    
    export default Demo1;
    

    上面代码体现了 JSX 的基本语法规则:遇到 HTML 标签(以 < 开头),就用 HTML 规则解析;遇到代码块(以 { 开头),就用 JavaScript 规则解析,其中注意一点react没有vue中v-for遍历,需要自己写遍历。上面代码的运行结果如下:

    页面渲染效果

    组件及组件传值

    //demo-2.js
    import React from 'react';
    
    class MyComponent extends React.Component{
      render(){
        return (
          <h1>{this.props.name}</h1>
        )
      }
    }
    
    class Demo2 extends React.Component{
      render(){
        const lists = ['我是谁','我来自哪里','我要到哪儿去'];
        return(
          <div className="list">
            <ul>
              {
                lists.map((list,index)=>{
                  return(
                    <li key={index}>
                      <MyComponent name={list}></MyComponent>
                    </li>
                  )
                })
              }  
            </ul>
            
          </div>
        )
      }
    }
    
    export default Demo2;
    

    React 允许将代码封装成组件(component),然后像插入普通 HTML 标签一样,在网页中插入这个组件。以上代码中MyComponent即为组件,需要注意的一点组件类的第一个字母必须大写,否则会报错,组件类只能包含一个顶层标签,否则也会报错。vue通过子组件中的props来传递数据,而React则是用this.props.name来传递。下图为渲染页面效果:

    渲染页面效果

    React之ref(获取真实的DOM节点)

    • 同vue的ref作用一样,组件并不是真实的DOM节点,而是存在于内存之中的一种数据结构,叫做虚拟 DOM。
    • 只有当它插入文档以后,才会变成真实的 DOM 。根据 React的设计,所有的DOM 变动,都先在虚拟 DOM上发生,然后再将实际发生变动的部分,反映在真实 DOM上,这种算法叫做 DOM diff ,它可以极大提高网页的性能表现。
    • 有时需要从组件获取真实 DOM 的节点,这时就要用到 ref 属性。
    //dem0-3.
    import React from 'react';
    
    class Demo3 extends React.Component{
      handleClick(){
        this.refs.myTextInput.focus();
      }
      render(){
        return(
          <div>
            <input type="text" ref="myTextInput" />
            <br/>
            <input type="button" value="Focus the text input" onClick={this.handleClick.bind(this)} />
          </div>
        )
      }
    }
    
    export default Demo3;
    
    

    在这里需要注意的是:

    • React的事件中如果不用剪头函数,那就要用bind来绑定this,否则需要在constructor构造函数里调用this.handleClick = this.handleClick.bind(this),这里建议用onClick={this.handleClick.bind(this)}。
    • 由于 this.refs.[refName] 属性获取的是真实 DOM ,所以必须等到虚拟 DOM 插入文档以后,才能使用这个属性,否则会报错。上面代码中,通过为组件指定 Click 事件的回调函数,确保了只有等到真实 DOM 发生 Click 事件之后,才会读取 this.refs.[refName] 属性。

    demo3

    React之this.state以及点击事件

    React中的state就相当于vue里的data数据存储,而小程序的this.setData就和React的this.setState类似。

    //demo-4.js
    import React from 'react';
    
    class Demo4 extends React.Component{
      // constructor(props){
      //   super(props)
      //   this.state = {
      //     liked:false
      //   }
      // }
      state = {
        liked: false
      }
      handleClick(){
        this.setState({liked: !this.state.liked});
      }
      render(){
        let text = this.state.liked ? '喜欢' : '不喜欢';
        return(
          <div>
            <p onClick={this.handleClick.bind(this)}>
              你 {text} 这个点击.
            </p>
          </div>
        )
      }
    }
    
    export default Demo4;
    

    上面代码是一个 LikeButton 组件,它的 constructor 方法(或者直接state声明)用于定义初始状态,也就是一个对象,这个对象可以通过 this.state 属性读取。当用户点击组件,导致状态变化,this.setState 方法就修改状态值,每次修改以后,自动调用 this.render 方法,再次渲染组件。

    由于 this.props 和 this.state 都用于描述组件的特性,可能会产生混淆。一个简单的区分方法是,this.props 表示那些一旦定义,就不再改变的特性,而 this.state 是会随着用户互动而产生变化的特性。

    demo4.gif

    利用input的onChange事件实现简单的双向绑定

    vue里面v-model一键实现的事情React没有,我们可以利用input组件的onChange事件来简单实现它,直接上代码。

    //Demo5.js
    import React from 'react';
    
    class Demo5 extends React.Component{
      // constructor(props){
      //   super(props)
      //   this.state = {
      //     text:''
      //   }
      // }
      state = {
        text: ''
      }
      handleChange(e){
        this.setState(
          {text: e.target.value}
        );
      }
      render(){
        return(
          <div>
            <p>
              你输入了{this.state.text}
            </p>
            <input type="text" placeholder="请输入..." onChange={this.handleChange.bind(this)}/>
          </div>
        )
      }
    }
    
    export default Demo5;
    

    demo5.gif

    组件的生命周期

    组件的生命周期.png

    一个React组件的生命周期分为三个部分:挂载期(Mounting)、存在更新期(Updating)和销毁时(Unmounting)。

    • Mounting:已插入真实 DOM

      当一个组件实例被创建并且插入到DOM中,以下钩子将被调用 constructor()
      继承react的props,和设置state的初始化。

    constructor(props) {
      super(props); //不能缺少
      this.state = {
        color: props.initialColor
      };
    }
    
    • Updating:当组件的props和state发生改变时,重新渲染页面是调用的。
    • Unmounting:组件从DOM中移动时调用的。
    React 为每个状态都提供了两种处理函数,will 函数在进入状态之前调用,
    did 函数在进入状态之后调用,三种状态共计五种处理函数。
    
    • componentWillMount()
    • componentDidMount()
    • componentWillUpdate(object nextProps, object nextState)
    • componentDidUpdate(object prevProps, object prevState)
    • componentWillUnmount()

    事件

    • onClick-点击事件
    • onSubmit-表单提交事件,可以配合antdesign的表单校验
    • onChange-input框数据变化,可实现数据的双向绑定(react里没有vue的v-modal)

    style jsx

    我们可以通过普通样式(className)和行内样式(LineStyle)控制React组件的样式:

    • 使用className设置样式(与CSS的选择器相同)
    import React from 'react';
    // import styles from '../../static/css/demo.css'
    
    class Demo6 extends React.Component{
      // constructor(props){
      //   super(props)
      //   this.state = {
      //     text:''
      //   }
      // }
      state = {
        text: ''
      }
      handleChange(e){
        this.setState(
          {text: e.target.value}
        );
      }
      render(){
        return(
          <div>
            <p className="style">
              你输入了{this.state.text}
            </p>
            <input className="input" type="text" placeholder="请输入..." onChange={this.handleChange.bind(this)}/>
            <style jsx>
              {
                ` .style {
                    background: #dcdcdc;
                    font-size: 20px;
                    height:40px;
                    line-height:40px;
                    padding:0 2%;
                  }
                  .input{
                    96%;
                    height:40px;
                    padding:0 2%;
                  }
                `
              }
            </style>
          </div>
        )
      }
    }
    
    export default Demo6;
    

    demo6

    为什么要用next

    Next.js是一个基于React的一个服务端渲染简约框架。它使用React语法,可以很好的实现代码的模块化,有利于代码的开发和维护。

    • 默认服务端渲染模式,以文件系统为基础的客户端路由,类似nuxt.js;

    • 与nuxt.js相同,pages文件目录即路由;

    • 以webpack的热替换为基础的开发环境(npm run dev);

    next创建的项目目录结构

    next创建的项目目录结构.png

    next路由

    • 默认从 pages 目录下取页面进行渲染返回给前端展示,路由可以使用Link(无刷新页面)或者a标签(刷新页面)
    //pages/router.js
    import Link from 'next/link';
    
    <Link prefetch href='/about?id=11'>
        <a >Link</a>
    </Link>
    
    <a href="/about?id=11">Link</a>
    
    • 接收参数:getInitialProps方法context参数或者{props.query.id},同时在getInitialProps 里面获取数据,服务端渲染会提前执行这个方法获取数据渲染到模板,这里用到axios库,前后端都可以使用
    //pages/test.js http://localhost:3000/test
    import React from 'react';
    import MyHeader from '../components/MyHeader';
    import axios from '../lib/axios';
    import { bmobConfigDevdev, bmobConfig } from '../lib/config';
    import urls from '../lib/urls';
    
    import { Table, Form, Icon, Input, Button } from 'antd';
    const FormItem = Form.Item;
    
    const config1 = {
      headers: {
        'X-Bmob-Application-Id': bmobConfig.applicationId,
        'X-Bmob-REST-API-Key': bmobConfig.restApiKey,
        'Content-Type': 'application/json'
      }
    };
    
    const config2 = {
      headers: {
        'X-Bmob-Application-Id': bmobConfigDevdev.applicationId,
        'X-Bmob-REST-API-Key': bmobConfigDevdev.restApiKey,
        'Content-Type': 'application/json'
      }
    };
    
    const myStyle = {
      body: {
         '1200px',
        margin: '10px auto'
      }
    }
    
    const columns = [{
      title: 'id',
      dataIndex: 'objectId',
      key: 'objectId',
    }, {
      title: '姓名',
      dataIndex: 'uname',
      key: 'uname',
    }, {
      title: '电话',
      dataIndex: 'uphone',
      key: 'uphone',
    }, {
      title: '地址',
      dataIndex: 'address',
      key: 'address',
    }];
    
    const dataSource = [{
      key: '1',
      name: '胡彦斌',
      age: 32,
      address: '西湖区湖底公园1号'
    }, {
      key: '2',
      name: '胡彦祖',
      age: 42,
      address: '西湖区湖底公园1号'
    }];
    
    //封装form组件
    const MyForm = (props) => (
      <Form layout="inline" onSubmit={this.handleSubmit}>
        <FormItem>
          <Button
            type="primary"
            htmlType="submit"
          >
            增加数据
          </Button>
        </FormItem>
      </Form>
    )
    
    //声明类
    class addDeleteSelectUpdate extends React.Component {
    
      render() {
        return (
          <MyHeader title="用户列表">
            <div style={myStyle.body}>
              <p className="about">传递的参数为:{this.props.query}</p>
              <p className="about">增加数据</p>
              <MyForm ></MyForm>
              <p className="about">获取数据{this.props.dataToString}</p>
              <Table dataSource={this.props.data} columns={columns} size="small" />
              <div className="">
                <p className="about">遍历</p>
                <ul>
                  {
                    this.props.data.map(els => {
                      return (
                        <li key={els.objectId}>
                          {els.address}
                        </li>
                      )
                    })
                  }
                </ul>
              </div>
              <style jsx>
                {`
                  .about {color:#666;padding:10px}
                `}
              </style>
            </div>
          </MyHeader>
        )
      }
    }
    
    /**
     * @description 获取初始数据
     */
    const isServer = typeof window === 'undefined'
    
    addDeleteSelectUpdate.getInitialProps = async function (context) {
      console.log(context.query);
      const res = await axios.get(urls.userList, config1)
      console.log(res.data);
      var query = context.query;
      return {
        data: res.data.results,
        dataToString: JSON.stringify(res.data.results),
        query: JSON.stringify(context.query)
      }
    }
    
    export default addDeleteSelectUpdate;
    

    数据请求-axios配置

    Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
    

    axios具有以下几个特点:

    • 既可以在服务端使用又可以在浏览器端使用;
    • 支持 Promise API(async await,解决地狱回调);
    • 拦截请求和响应(401跳转登录等操作);
    • 转换请求数据和响应数据;
    • 自动转换 JSON 数据。

    以下为axios配置代码:

    //lib/axios.js
    import axios from 'axios';
    import NProgress from 'nprogress';
    
    axios.defaults.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest';
    axios.defaults.withCredentials = true;
    axios.defaults.timeout = 5000;
    
    //请求之前
    axios.interceptors.request.use((config)=>{
      if(process.broswer) NProgress.start()//加一个loading
      return config;
    });
    
    //
    axios.interceptors.response.use(
      response => {
        // console.log('-------axiosResponse---------')
        if(process.broswer) NProgress.done()
        return response;
      },
      error => {
        // console.log('-------axiosError---------:');
        // console.log(error.response.status==401)
        if(process.broswer) NProgress.done()
        if(error.response&&error.response.status==401){
          window.location.href="/login";//登录失效跳转
        }
        return Promise.reject(error);
      }
    );
    
    export default axios;
    
    

    使用时在页面引入axios配置文件axios.js,具体的axios配置可以参考link-axios文档

    //pages/login.js
    import axios from '../lib/axios';
    ...
    
    axios.post(urls.login,qs.stringify(obj))
    .then(res=>{
        console.log(res);
        Router.push('/contract/list');
    })
    .catch(error=>{
        console.log(error);
    })
    ...
    

    数据请求-开发环境代理

    前后端分离以后,前端最常见的一个问题就是跨域,之前的om项目基于nuxt可以配置proxy,在next.js里需要用到express和http-proxy-middleware在服务端(node)端做一层转发,代码配置如下:

    //server.js
    var express =require('express');
    var next = require('next');
    
    const devProxy = {
      '/api': {
        target: 'http://39.107.58.75:9092/',// 目标服务器 host
        pathRewrite: {'^/api': '/'}, // 重写请求,比如我们源访问的是api/login,那么请求会被解析为/www/login
        changeOrigin: true,// 默认false,是否需要改变原始主机头为目标URL
      },
      '/jt':{
        target: 'https://api.dededemo.com/plus/weapp.php?action=index&domain=http://23jt.net&skin=weapp&ver=3.0&page=1',// 目标服务器 host
        // pathRewrite: {'^/api': '/'}, // 重写请求,比如我们源访问的是api/login,那么请求会被解析为/www/login
        changeOrigin: true,// 默认false,是否需要改变原始主机头为目标URL
      }
    }
    
    const port = parseInt(process.env.PORT, 10) || 3000
    const env = process.env.NODE_ENV
    const dev = env !== 'production'
    const app = next({
      dir: '.', // base directory where everything is, could move to src later
      dev
    })
    
    const handle = app.getRequestHandler()
    
    let server
    app
      .prepare()
      .then(() => {
        server = express()
          
        // Set up the proxy.
        if (dev && devProxy) {
          const proxyMiddleware = require('http-proxy-middleware')
          Object.keys(devProxy).forEach(function (context) {
            server.use(proxyMiddleware(context, devProxy[context]))
          })
        }
    
        // Default catch-all handler to allow Next.js to handle all other routes
        server.all('*', (req, res) => {
            req.headers['Content-Type'] = 'application/x-www-form-urlencoded';
            req.headers['X-Requested-With'] = 'XMLHttpRequest';
            handle(req, res);
            // console.log(req.body);
          }
        )
    
        server.listen(port, err => {
          if (err) {
            throw err
          }
          console.log(`> Ready on port ${port} [${env}]`)
        })
      })
      .catch(err => {
        console.log('An error occurred, unable to start the server')
        console.log(err)
      })
    
    

    配置完server.js后还需要在packge.json中配置启动脚本:

    "devdev": "cross-env NODE_ENV=development PORT=9527 node server.js",
    

    注意上线后需要在ngnix里配置反向代理,就不存在跨域的问题。

    在next.js里使用ant design

    • babel配置
    //.babelrc
    {
      "presets": [
        "next/babel"
      ],
      "plugins": [
        [
          "module-resolver", {
          "root": ["."],
          "alias": {
            "styles": "./styles"
          },
          "cwd": "babelrc"
        }],
        ["import", { "libraryName": "antd" }]
      ],
      "ignore": []
    }
    
    • 引用ant design组件,需要用到ant design什么组件按照自己需要引用即可,各个组件使用方法参考ant design的link-官方文档
    import {Form, Icon,Input, Button, Checkbox, message} from 'antd'; 
    

    部署一个next.js项目

    Next.js 项目的部署,需要一个 Node.js的服务器。
    在开发环境服务器的入口文件就使用上文中提到的 server.js,在 server.js 里添加了针对部署环境的选择,代码如下

    const dev = process.env.NODE_ENV !== 'production'
    

    为了区分部署环境,我们需要在 package.json 中修改 script 属性如下:

    "scripts": {
        "dev": "next",
        "devdev": "cross-env NODE_ENV=development PORT=9527 node server.js",
        "build": "next build",
        "start": "next start",
        "generate": "next build && next export",
        "export-h":"next export -h"
      },
    

    其中,build 命令是用于打包项目,start 命令是用于生产环境部署,devdev 命令是用于本地开发,PORT为启动端口。

    注意的几个点

    • 增加第三方npm包必须加上--save关键字,会pakage.json文件里加入依赖包;
    //如增加js-md5库
    $ npm install js-md5 --save
    
    • 组件名称必须以大写字母开头,如MyHeader
    • React的事件中如果不用剪头函数,那就要用bind来绑定this,否则需要在constructor构造函数里调用this.handleClick = this.handleClick.bind(this),这里建议用onClick={this.handleClick.bind(this)}。

    总结

    只要熟悉React开发,上手一个Next项目很容易,Next 让前端项目开发效率更高。

    参考资料

    更多

    更多前端技术请访问我的博客:https://hurely.github.io

  • 相关阅读:
    测试方案
    测试需求
    软件测试知识点总结
    自动化测试
    测试——缺陷报告包括那些内容,由什么组成
    测试——缺陷的类型
    软件测试工具简介
    测试工程师
    剑指offer系列21--二叉搜索树的后续遍历序列
    剑指offer系列20--从上到下打印二叉树
  • 原文地址:https://www.cnblogs.com/kung0806/p/10820212.html
Copyright © 2020-2023  润新知