热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

React組件設想技能

React組件設想組件分類展現組件和容器組件展現組件容器組件關注事物的展現關注事物怎樣事變可以包含展現和容器組件,而且平常會有DOM標籤和css款式可以包含展現和容器組件,而且不會
React組件設想

組件分類

展現組件和容器組件

展現組件容器組件
關注事物的展現關注事物怎樣事變
可以包含展現和容器組件,而且平常會有DOM標籤和css款式可以包含展現和容器組件,而且不會有DOM標籤和css款式
常常許可經由歷程this.props.children通報供應數據和行動給容器組件或許展現組件
對第三方沒有任何依靠,比方store 或許 flux action挪用flux action 而且供應他們的回調給展現組件
不要指定數據怎樣加載和變化作為數據源,一般採納較高階的組件,而不是自身寫,比方React Reduxconnect(), Relay的createContainer(), Flux UtilsContainer.create()
僅經由歷程屬性獵取數據和回調null
很少有自身的狀況,縱然有,也是自身的UI狀況null
除非他們須要的自身的狀況,性命周期,或機能優化才會被寫為功用組件null

下面是一個可以會常常寫的組件,批評列表組件,數據交互和展現都放到了一個組件內里。

// CommentList.js
class CommentList extends React.Component {
constructor() {
super();
this.state = { comments: [] }
}
componentDidMount() {
$.ajax({
url: "/my-comments.json",
dataType: 'json',
success: function(comments) {
this.setState({comments: comments});
}.bind(this)
});
}
render() {
return

    {this.state.comments.map(renderComment)}
;
}
renderComment({body, author}) {
return
  • {body}—{author}
  • ;
    }
    }

    我們對上面的組件舉行拆分,把他拆分紅容器組件 CommentListContainer.js 和展現組件 CommentList

    // CommentListContainer.js
    class CommentListContainer extends React.Component {
    constructor() {
    super();
    this.state = { comments: [] }
    }
    componentDidMount() {
    $.ajax({
    url: "/my-comments.json",
    dataType: 'json',
    success: function(comments) {
    this.setState({comments: comments});
    }.bind(this)
    });
    }
    render() {
    return ;
    }
    }
    // CommentList.js
    class CommentList extends React.Component {
    constructor(props) {
    super(props);
    }
    render() {
    return

      {this.props.comments.map(renderComment)}
    ;
    }
    renderComment({body, author}) {
    return
  • {body}—{author}
  • ;
    }
    }

    上風:

    • 展現和容器更好的星散,更好的邃曉運用程序和UI
    • 重用性高,展現組件可以用於多個差別的state數據源
    • 展現組件就是你的調色板,可以把他們放到零丁的頁面,在不影響運用程序的狀況下,讓設想師調解UI
    • 迫使你星散標籤,到達更高的可用性

    有狀況組件和無狀況組件

    下面是一個最簡樸的無狀況組件的例子:

    function HelloComponent(props, /* context */) {
    return

    Hello {props.name}

    }
    ReactDOM.render(, mountNode)

    可以看到,底本須要寫“類”定義(React.createClass 或許 class YourComponent extends React.Component)來建立自身組件的定義(有狀況組件),現在被精簡成了只寫一個 render 函數。更值得一提的是,由於僅僅是一個無狀況函數,React 在襯着的時刻也省掉了將“組件類” 實例化的歷程。

    連繫 ES6 的解構賦值,可以讓代碼更精簡。比方下面這個 Input 組件:

    function Input({ label, name, value, ...props }, { defaultTheme }) {
    const { theme, autoFocus, ...rootProps } = props
    return (
    htmlFor={name}
    children={label || defaultLabel}
    {...rootProps}
    >
    name={name}
    type="text"
    value={value || ''}
    theme={theme || defaultTheme}
    {...props}
    />
    )}
    Input.cOntextTypes= {defaultTheme: React.PropTypes.object};

    無狀況組件不像上述兩種要領在挪用時會建立新實例,它建立時始終堅持了一個實例,防止了不必要的搜檢和內存分派,做到了內部優化。

    無狀況組件不支撐 “ref”

    高階組件

    高階組件經由歷程函數和閉包,轉變已有組件的行動,本質上就是 Decorator 形式在 React 的一種完成。

    當寫着寫着無狀況組件的時刻,有一天遽然發明須要狀況處置懲罰了,那末無需完整返工:)
    每每我們須要狀況的時刻,這個需求是可以重用的。

    高階組件加無狀況組件,則大大增強了全部代碼的可測試性和可維護性。同時不停“誘使”我們寫出組合性更好的代碼。

    高階函數

    function welcome() {
    let username = localStorage.getItem('username');
    console.log('welcome ' + username);
    }
    function goodbey() {
    let username = localStorage.getItem('username');
    console.log('goodbey ' + username);
    }
    welcome();
    goodbey();

    我們發明兩個函數有一句代碼是一樣的,這叫冗餘唉。(日常平凡可以會有一大段代碼的冗餘)。

    下面我們要寫一个中心函數,讀取username,他來擔任把username通報給兩個函數。

    function welcome(username) {
    console.log('welcome ' + username);
    }
    function goodbey(username) {
    console.log('goodbey ' + username);
    }
    function wrapWithUsername(wrappedFunc) {
    let newFunc = () => {
    let username = localStorage.getItem('username');
    wrappedFunc(username);
    };
    return newFunc;
    }
    welcome = wrapWithUsername(welcome);
    goodbey = wrapWithUsername(goodbey);
    welcome();
    goodbey();

    好了,我們內里的 wrapWithUsername 函數就是一個“高階函數”。
    他做了什麼?他幫我們處置懲罰了 username,通報給目的函數。我們挪用終究的函數 welcome的時刻,基本不必體貼 username是怎樣來的。

    聞一知十的高階組件

    下面是兩個冗餘的組件。

    import React, {Component} from 'react'
    class Welcome extends Component {
    constructor(props) {
    super(props);
    this.state = {
    username: ''
    }
    }
    componentWillMount() {
    let username = localStorage.getItem('username');
    this.setState({
    username: username
    })
    }
    render() {
    return (

    welcome {this.state.username}

    )
    }
    }
    export default Welcome;

    import React, {Component} from 'react'
    class Goodbye extends Component {
    constructor(props) {
    super(props);
    this.state = {
    username: ''
    }
    }
    componentWillMount() {
    let username = localStorage.getItem('username');
    this.setState({
    username: username
    })
    }
    render() {
    return (

    goodbye {this.state.username}

    )
    }
    }
    export default Goodbye;

    我們可以經由歷程方才高階函數的思想來建立一个中心組件,也就是我們說的高階組件。

    import React, {Component} from 'react'
    export default (WrappedComponent) => {
    class NewComponent extends Component {
    constructor() {
    super();
    this.state = {
    username: ''
    }
    }
    componentWillMount() {
    let username = localStorage.getItem('username');
    this.setState({
    username: username
    })
    }
    render() {
    return
    }
    }
    return NewComponent
    }

    import React, {Component} from 'react';
    import wrapWithUsername from 'wrapWithUsername';
    class Welcome extends Component {
    render() {
    return (

    welcome {this.props.username}

    )
    }
    }
    Welcome = wrapWithUsername(Welcome);
    export default Welcome;

    import React, {Component} from 'react';
    import wrapWithUsername from 'wrapWithUsername';
    class Goodbye extends Component {
    render() {
    return (

    goodbye {this.props.username}

    )
    }
    }
    Goodbye = wrapWithUsername(Goodbye);
    export default Goodbye;

    看到沒有,高階組件就是把 username 經由歷程 props 通報給目的組件了。目的組件只管從 props內里拿來用就好了。

    為了代碼的復用性,我們應當只管削減代碼的冗餘。

    1. 提取同享的state,假如有兩個組件都須要加載一樣的數據,那末他們會有雷同的 componentDidMount 函數。
    2. 找出反覆的代碼,每一個組件中constructor 和 componentDidMount都乾著一樣的事變,別的,在數據拉取時都邑顯現Loading… 案牘,那末我們應當思索怎樣運用高階組件來提取這些要領。
    3. 遷徙反覆的代碼到高階組件
    4. 包裹組件,而且運用props替代state
    5. 只管地簡化

    組件開闢基本思想

    單功用準繩

    運用react時,組件或容器的代碼在基本上必需只擔任一塊UI功用。

    讓組件堅持簡樸

    • 假如組件基本不須要狀況,那末就運用函數定義的無狀況組件。
    • 從機能上來講,函數定義的無狀況組件 > ES6 class 定義的組件 > 經由歷程 React.createClass() 定義的組件。
    • 僅通報組件所須要的屬性。只要當屬性列表太長時,才運用{...this.props}舉行通報。
    • 假如組件內里有太多的推斷邏輯(if-else語句)一般意味着這個組件須要被拆分紅更細的組件或模塊。
    • 運用邃曉的定名可以讓開闢者邃曉它的功用,有助於組件復用。

    基本準則

    • shouldComponentUpdate中防止不必要的搜檢.
    • 只管運用不可變數據範例(Immutable).
    • 編寫針對產物環境的打包設置(Production Build).
    • 經由歷程Chrome Timeline來紀錄組件所消耗的資本.
    • componentWillMount或許componentDidMount內里經由歷程setTimeOut或許requestAnimationFram來耽誤實行那些須要大批盤算的使命.

    組件開闢技能

    form表單里的受控組件和不受控組件

    受控組件

    在大多數狀況下,我們引薦運用受控組件來完成表單。在受控組件中,表單數據由 React 組件擔任處置懲罰。下面是一個典範的受控組建。

    class NameForm extends React.Component {
    constructor(props) {
    super(props);
    this.state = {value: ''};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
    }
    handleChange(event) {
    this.setState({value: event.target.value});
    }
    handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
    }
    render() {
    return (




    );
    }
    }

    設置表單元素的value屬性以後,其顯現值將由this.state.value決議,以滿足React狀況的統一數據理念。每次鍵盤敲擊以後會實行handleChange要領以更新React狀況,顯現值也將跟着用戶的輸入轉變。

    關於受控組件來講,每一次 state(狀況)變化都邑伴有相干聯的處置懲罰函數。這使得可以直接修正或考證用戶的輸入和提交表單。

    不受控組件

    由於不受控組件的數據泉源是 DOM 元素,當運用不受控組件時很輕易完成 React 代碼與非 React 代碼的集成。假如你願望的是疾速開闢、不請求代碼質量,不受控組件可以肯定程度上削減代碼量。不然。你應當運用受控組件。

    平常狀況下不受控組件我們運用ref來獵取DOM元素舉行操縱。

    class NameForm extends React.Component {
    constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    }
    handleSubmit(event) {
    alert('A name was submitted: ' + this.input.value);
    event.preventDefault();
    }
    render() {
    return (




    );
    }
    }

    組件前提推斷

    三元函數組件推斷襯着

    const sampleCompOnent= () => {
    return isTrue ?

    True!

    :

    false!


    };

    運用&&表達式替代不必要的三元函數

    const sampleCompOnent= () => {
    return isTrue ?

    True!

    :
    };

    const sampleCompOnent= () => {
    return isTrue &&

    True!


    };

    須要注重的是假如isTrue 為 0 ,實在會轉換成 false,然則在頁面中顯現的時刻,&&照樣會返回0顯現到頁面中。

    多重嵌套推斷

    // 題目代碼
    const sampleCompOnent= () => {
    return (


    {flag && flag2 && !flag3
    ? flag4
    ?

    Blah


    : flag5
    ?

    Meh


    :

    Herp


    :

    Derp


    }

    )
    };

    處理計劃:

    • 最好計劃: 將邏輯移到子組件內部
    • 運用IIFE(Immediately-Invoked Function Expression 馬上實行函數)
    • 滿足前提的時刻運用return強迫跳出函數

    const sampleCompOnent= () => {
    const basicCOndition= flag && flag2 && !flag3;
    if (!basicCondition) return

    Derp

    ;
    if (flag4) return

    Blah

    ;
    if (flag5) return

    Meh

    ;
    return

    Herp


    }

    setState異步性

    在某些狀況下,React框架出於機能優化斟酌,可以會將屢次state更新兼并成一次更新。正由於如此,setState實際上是一個異步的函數。 假如在挪用setState()函數以後嘗試去接見this.state,你獲得的可以照樣setState()函數實行之前的效果。

    然則,有一些行動也會阻撓React框架自身關於屢次state更新的兼并,從而讓state的更新變得同步化。 比方: eventListeners, Ajax, setTimeout 等等。

    React框架之所以在挑選在挪用setState函數以後馬上更新state而不是採納框架默許的體式格局,即兼并屢次state更新為一次更新,是由於這些函數挪用(fetch,setTimeout等瀏覽器層面的API挪用)並不處於React框架的高低文中,React沒有方法對其舉行掌握。React在此時採納的戰略就是實時更新,確保在這些函數實行以後的其他代碼能拿到準確的數據(即更新過的state)。

    處理setState函數異步的方法?

    依據React官方文檔,setState函數實際上吸收兩個參數,个中第二個參數範例是一個函數,作為setState函數實行后的回調。經由歷程傳入回調函數的體式格局,React可以保證傳入的回調函數肯定是在setState勝利更新this.state以後再實行。

    this.setState({count: 1}, () => {
    console.log(this.state.count); // 1
    })

    React源碼中setState的完成

    ReactComponent.prototype.setState = function(partialState, callback) {
    invariant(
    typeof partialState === 'object' ||
    typeof partialState === 'function' ||
    partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
    'function which returns an object of state variables.'
    );
    this.updater.enqueueSetState(this, partialState);
    if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
    }
    };

    updater的這兩個要領,和React底層的Virtual Dom(假造DOM樹)的diff算法有嚴密的關聯,所以真正決議同步照樣異步的實際上是Virtual DOMdiff算法。

    依靠注入

    React中,想做依靠注入(Dependency Injection)實在相稱簡樸。可以經由歷程props來舉行通報。然則,假如組件數目許多,而且組件嵌套條理很深的話,這類體式格局就不太適宜。

    高階組件

    // inject.jsx
    var title = 'React Dependency Injection';
    export default function inject(Component) {
    return class Injector extends React.Component {
    render() {
    return (
    {...this.state}
    {...this.props}
    title={ title }
    />
    )
    }
    };
    }

    // Title.jsx
    export default function Title(props) {
    return

    { props.title }

    ;
    }

    // Header.jsx
    import inject from './inject.jsx';
    import Title from './Title.jsx';
    var EnhancedTitle = inject(Title);
    export default function Header() {
    return (



    );
    }

    context

    React v16.3.0 之前的 Context:

    var cOntext= { title: 'React in patterns' };
    class App extends React.Component {
    getChildContext() {
    return context;
    }
    // ...
    }
    App.childCOntextTypes= {
    title: PropTypes.string
    };

    class Inject extends React.Component {
    render() {
    var title = this.context.title;
    // ...
    }
    }
    Inject.cOntextTypes= {
    title: PropTypes.string
    };

    之前的 Context 作為一個試驗性子的 API,直到 React v16.3.0 版本前都一向不被官方所首倡去運用,其主要緣由就是由於在子組件中運用 Context 會損壞 React 運用的分型架構。

    這裏的分形架構指的是從抱負的 React 運用的根組件樹中抽取的恣意一部分都還是一個可以直接運轉的子組件樹。在這個子組件樹之上再包一層,就可以將它無縫地移植到恣意一個其他的根組件樹中。

    但假如根組件樹中有恣意一個組件運用了支撐透傳的 Context API,那末假如把包含了這個組件的子組件樹零丁拿出來,由於缺少了供應 Context 值的根組件樹,這時候的這個子組件樹是沒法直接運轉的。

    而且他有一個致命缺點:任何一个中心通報的組件shouldComponentUpdate 函數返回false,組件都不會獲得更新。

    新的Context Api

    新的Context Api 採納聲明式的寫法,而且可以透過shouldComponentUpdate 函數返回false的組件繼承向下流傳,以保證目的組件肯定可以吸收到頂層組件 Context 值的更新,一舉處理了現有 Context API 的兩大弊病,也終究成為了 React 中的第一級(first-class) API

    新的 Context API 分為三個構成部分:

    1. React.createContext 用於初始化一個 Context
    2. XXXContext.Provider作為頂層組件吸收一個名為 valueprop,可以吸收恣意須要被放入 Context 中的字符串,数字,以至是函數。
    3. XXXContext.Consumer作為目的組件可以出現在組件樹的恣意位置(在 Provider 以後),吸收 children prop,這裏的 children 必需是一個函數(cOntext=> ())用來吸收從頂層傳來的 Context

    const ThemeCOntext= React.createContext('light');
    class App extends React.Component {
    render() {
    return (



    );
    }
    }
    function Toolbar(props) {
    return (




    );
    }
    function ThemedButton(props) {
    return (

    {theme =>

    );
    }
    _handleButtonClick() {
    console.log(`Button is clicked inside ${ this.state.name }`);
    // 將致使
    // Uncaught TypeError: Cannot read property 'state' of null
    }
    }

    我們可以經由歷程下面三種體式格局簡樸完成this指向的綁定:

    • constructor 中事前綁定 this._buttOnClick= this._handleButtonClick.bind(this);
    • 挪用時運用箭頭函數
    • ES7中的綁定操縱符

    給setState傳入回調函數

    setState() 不僅能接收一個對象,還能接收一個函數作為參數呢,該函數接收該組件前一刻的 state 以及當前的 props 作為參數,盤算和返回下一刻的 state。

    // assuming this.state.count === 0
    this.setState({count: this.state.count + 1});
    this.setState({count: this.state.count + 1});
    this.setState({count: this.state.count + 1});
    // this.state.count === 1, not 3
    this.setState((prevState, props) => ({
    count: prevState.count + props.increment
    }));

    // Passing object
    this.setState({ expanded: !this.state.expanded });
    // Passing function
    this.setState(prevState => ({ expanded: !prevState.expanded }));

    組件切換技能

    import HomePage from './HomePage.jsx';
    import AboutPage from './AboutPage.jsx';
    import UserPage from './UserPage.jsx';
    import FourOhFourPage from './FourOhFourPage.jsx';
    const PAGES = {
    home: HomePage,
    about: AboutPage,
    user: UserPage
    };
    const Page = (props) => {
    const Handler = PAGES[props.page] || FourOhFourPage;
    return
    };

    React style

    組件分類

    基本組件, 規劃組件, 排版組件

    給無狀況的純UI組件運用款式

    請堅持款式闊別那些離不開state的組件. 比方路由, 視圖, 容器, 表單, 規劃等等不該當有任何的款式或許css class出現在組件上. 相反, 這些龐雜的營業組件應當有一些帶有基本功用的無狀況UI組件構成.

    class SampleComponent extends Component {
    render() {
    return (


    name='username'
    value={username}
    OnChange={this.handleChange}/>
    type='password'
    name='password'
    value={password}
    OnChange={this.handleChange}/>
    type='submit'
    children='Sign In'/>

    )
    }
    }
    // 表達組件(帶款式)
    const Button = ({
    ...props
    }) => {
    const sx = {
    fontFamily: 'inherit',
    fontSize: 'inherit',
    fontWeight: 'bold',
    textDecoration: 'none',
    display: 'inline-block',
    margin: 0,
    paddingTop: 8,
    paddingBottom: 8,
    paddingLeft: 16,
    paddingRight: 16,
    border: 0,
    color: 'white',
    backgroundColor: 'blue',
    WebkitAppearance: 'none',
    MozAppearance: 'none'
    }
    return (

    推荐阅读
    • 本文讨论了如何在codeigniter中识别来自angularjs的请求,并提供了两种方法的代码示例。作者尝试了$this->input->is_ajax_request()和自定义函数is_ajax(),但都没有成功。最后,作者展示了一个ajax请求的示例代码。 ... [详细]
    • PHP图片截取方法及应用实例
      本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
    • 本文介绍了如何使用n3-charts绘制以日期为x轴的数据,并提供了相应的代码示例。通过设置x轴的类型为日期,可以实现对日期数据的正确显示和处理。同时,还介绍了如何设置y轴的类型和其他相关参数。通过本文的学习,读者可以掌握使用n3-charts绘制日期数据的方法。 ... [详细]
    • 如何使用Java获取服务器硬件信息和磁盘负载率
      本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
    • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
    • 不同优化算法的比较分析及实验验证
      本文介绍了神经网络优化中常用的优化方法,包括学习率调整和梯度估计修正,并通过实验验证了不同优化算法的效果。实验结果表明,Adam算法在综合考虑学习率调整和梯度估计修正方面表现较好。该研究对于优化神经网络的训练过程具有指导意义。 ... [详细]
    • Ihavethefollowingonhtml我在html上有以下内容<html><head><scriptsrc..3003_Tes ... [详细]
    • 本文介绍了使用FormData对象上传文件同时附带其他参数的方法。通过创建一个表单,将文件和参数添加到FormData对象中,然后使用ajax发送POST请求进行文件上传。在发送请求时,需要设置processData为false,告诉jquery不要处理发送的数据;同时设置contentType为false,告诉jquery不要设置content-Type请求头。 ... [详细]
    • 开发笔记:加密&json&StringIO模块&BytesIO模块
      篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
    • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
    • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
    • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
    • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
      原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
    • 从零基础到精通的前台学习路线
      随着互联网的发展,前台开发工程师成为市场上非常抢手的人才。本文介绍了从零基础到精通前台开发的学习路线,包括学习HTML、CSS、JavaScript等基础知识和常用工具的使用。通过循序渐进的学习,可以掌握前台开发的基本技能,并有能力找到一份月薪8000以上的工作。 ... [详细]
    • 本文介绍了Java后台Jsonp处理方法及其应用场景。首先解释了Jsonp是一个非官方的协议,它允许在服务器端通过Script tags返回至客户端,并通过javascript callback的形式实现跨域访问。然后介绍了JSON系统开发方法,它是一种面向数据结构的分析和设计方法,以活动为中心,将一连串的活动顺序组合成一个完整的工作进程。接着给出了一个客户端示例代码,使用了jQuery的ajax方法请求一个Jsonp数据。 ... [详细]
    author-avatar
    英雄醉酒惜红颜_527
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有