• 09.vue-charp-09 Render函数


    什么是虚拟DOM

    本章我们就来探索Vue.js用于实现Virtual Dom的Render函数用法,在介绍Render函数前,我们先来看看什么是Virtual Dom。

    Virtual Dom并不是真正意义上的DOM,而是一个轻量级的JavaScript对象,在状态发生变化时,Virtual Dom会进行Diff 运算,来更新只需要被替换的DOM,而不是全部重绘。
    与DOM操作相比,Virtual Dom是基于JavaScript计算的,所以开销会小很多。

    vNode对象通过一些特定的选项描述了真实的DOM结构。
    在Vue.js 2中,Virtual Dom就是通过一种VNode类表达的,每个DOM元素或组件都对应一个VNode对象,

    VNode 主要可以分为如下几类,

    • TextVNode 文本节点。
    • ElementVNode 普通元素节点。
    • ComponentVNode 组件节点。
    • EmptyVNode 没有内容的注释节点。
    • CloneVNode 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true。

    初步使用

    自定义锚点组件

    <div id="app">
            <div>
                <anchor :level="2" title="title-01">
                    slot-标题-1
                </anchor>
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
                <p id="title-01">标题-1</p>
            </div>
        </div>
        <script src="../lib/vue.2.6.11.js"></script>
        <script>
            Vue.component('anchor', {
                props: {
                    level: {
                        type: Number,
                        required: true
                    },
                    title: {
                        type: String,
                        default: ''
                    }
                },
                render: function (createElement) {
                    return createElement(
                        'h' + this.level,
                        [
                            createElement(
                                'a',
                                {
                                    domProps: {
                                        href: '#' + this.title
                                    }
                                },
                                this.$slots.default
                            )
                        ]
                    )
                }
            });
    
            var app = new Vue({
                el: '#app'
            })
        </script>
    </body>
    

    Render函数通过createElement参数来创建Virtual Dom,结构精简了很多。在第7章组件中介绍slot时,有提到过访问slot的用法,使用场景就是在Render函数。

    createElement用法

    基本参数

    ​ createElement构成了Vue Virtual Dom的模板,它有3个参数:

    createElement(
        // {String | Object | Function}
        // 一个HTML标签,组件选项,或一个函数
        //必须Return上述其中一个
        'div',
        // {Object}
        // 一个对应属性的数据对象,可选
        // 您可以在template中使用
        {
            // 稍后详细介绍
        },
        // {String | Array}
        // 子节点(VNodes),可选
        [
            createElement('h1', 'hello world'),
            createElement(MyComponent, {
                props: {
                    someProp: 'foo'
                }
            }),
            'bar'
        ]
    )
    
    • 第一个参数必选,可以是一个HTML标签,也可以是一个组件或函数;

    • 第二个是可选参数,数据对象,在template中使用。

    • 第三个是子节点,也是可选参数,用法一致。

    对于第二个参数“数据对象”,具体的选项如下:

    {
        //和v-bind:class一样的API
        'class': {
            foo: true,
            bar: false
        },
        //和v-bind:style一样的API
        style: {
            color: 'red',
            fontSize: '14px'
        },
        // 正常的HTML特性
        attrs: {
            id: 'foo'
    id: 'foo'
    },
    //组件props
    props: {
        myProp: 'bar'
    },
    // DOM属性
    domProps: {
        innerHTML: 'baz'
    },
    // 自定义事件监听器"on"
    //不支持如v-on:keyup.enter的修饰器
    // 需要手动匹配 keyCode
    on: {
        click: this.clickHandler
    },
    // 仅对于组件,用于监听原生事件
    // 而不是组件使用vm.$emit触发的自定义事件
    nativeOn: {
        click: this.nativeClickHandler
    },
    // 自定义指令
    directives: [
         {
            name: 'my-custom-directive',
            value: '2'
            expression: '1 + 1',
            arg: 'foo',
            modifiers: {
                   bar: true
                }
          }
        ],
        // 作用域slot
        // { name: props => VNode | Array<VNode> }
        scopedSlots: {
            default: props => h('span', props.text)
        },
        // 如果子组件有定义slot的名称
        slot: 'name-of-slot'
        // 其他特殊顶层属性
        key: 'myKey',
        ref: 'myRef'
    }
    

    示例:

    <body>
        <div id="app">
            <div>
                <ele-1></ele-1>
                <ele-2></ele-2>
            </div>
        </div>
        <script src="../lib/vue.2.6.11.js"></script>
        <script>
            Vue.component('ele-1', {
                template: '
                    <div id="element-1" :class="{show: show}"  @click="handleClick">文本内容</div>',
                data: function () {
                    return {
                        show: true
                    }
                },
                methods: {
                    handleClick: function () {
                        console.log('clicked!');
                    }
                }
            });
    
            Vue.component('ele-2', {
                data: function () {
                    return {
                        show: true
                    }
                },
                render: function (createElement) {
                    return createElement('div'
                        , {
                            // 动态绑定class,同:class
                            class: {
                                'show': this.show
                            },
                            // 普通html 特性
                            attrs: {
                                id: 'element-2'
                            },
                            //给div绑定click事件
                            on: {
                                click: this.handleClick
                            }
                        }
                        , "文本内容")
                },
                methods: {
                    handleClick: function () {
                        console.log('clicked!');
                    }
                }
            });
    
            var app = new Vue({
                el: '#app'
            })
        </script>
    </body>
    

    示例中,使用templaterender()两种方式实现相同的效果

    约束

    有的组件树中,如果VNode是组件或含有组件的slot,那么VNode必须唯一。

    对于重复渲染多个组件(或元素)的方法有很多

    通过一个循环和工厂函数就可以渲染5个重复的子组件Child。对于含有组件的slot,复用就要稍微复杂一点了,需要将slot的每个子节点都克隆一份。

    使用JavaScript代替模板功能

    在Render函数中,不再需要Vue内置的指令,比如v-if、v-for,当然,也没办法使用它们。无论要实现什么功能,都可以用原生JavaScript

    函数化组件

    Vue.js提供了一个functional的布尔值选项,设置为true可以使组件无状态和无实例,也就是没有data和this上下文。这样用render函数返回虚拟节点可以更容易渲染,因为函数化组件只是一个函数,渲染开销要小很多。
    使用函数化组件时,Render函数提供了第二个参数context来提供临时上下文。组件需要的data、props、slots、children、parent都是通过这个上下文来传递的,比如this.level要改写为context.props.level,this.$slots.default改写为context.children。

    函数化组件在业务中并不是很常用,而且也有其他类似的方法来实现,比如上例也可以用组件的is特性来动态挂载。总结起来,函数化组件主要适用于以下两个场景:

    • 序化地在多个组件中选择一个。
    • 在将children, props, data 传递给子组件之前操作它们。
    <body>
        <div id="app">
            <smart-item :data="data"></smart-item>
            <button @click="change('img')">切换为图片组件</button>
            <button @click="change('video')">切换为视频组件</button>
            <button @click="change('text')">切换为文本组件</button>
        </div>
        <script src="../lib/vue.2.6.11.js"></script>
        <script>
            // 图片组件选项
            var ImgItem = {
                props: ['data'],
                render: function (createElement) {
                    return createElement('div', [
                        createElement('p', '图片组件'),
                        createElement('img', {
                            attrs: {
                                src: this.data.url
                            }
                        })
                    ]);
                }
            };
            // 视频组件选项
            var VideoItem = {
                props: ['data'],
                render: function (createElement) {
                    return createElement('div', [
                        createElement('p', '视频组件'),
                        createElement('video', {
                            attrs: {
                                src: this.data.url,
                                controls: 'controls',
                                autoplay: 'autoplay'
                            }
                        })
                    ]);
                }
            };
            // 纯文本组件选项
            var TextItem = {
                props: ['data'],
                render: function (createElement) {
                    return createElement('div', [
                        createElement('p', '纯文本组件'),
                        createElement('p', this.data.text)
                    ]);
                }
            };
    
            Vue.component('smart-item', {
                //函数化组件
                functional: true,
                render: function (createElement, context) {
                    // 根据传入的数据,智能判断显示哪种组件
                    function getComponent() {
                        var data = context.props.data;
                        // 判断 prop: data的type 字段是属于哪种类型的组件
                        if (data.type === 'img') return ImgItem;
                        if (data.type === 'video') return VideoItem;
                        return TextItem;
                    }
                    return createElement(
                        getComponent(),
                        {
                            props: {
                                //把smart-item的prop: data传给上面智能选择的组件
                                data: context.props.data
                            }
                        },
                        context.children
                    )
                },
                props: {
                    data: {
                        type: Object,
                        required: true
                    }
                }
            })
    
            var app = new Vue({
                el: '#app',
                data: {
                    data: {}
                },
                methods: {
                    // 切换不同类型组件的数据
                    change: function (type) {
                        if (type === 'img') {
                            this.data = {
                                type: 'img',
                                url: 'https://raw.githubusercontent.com/iview/iview/master/assets/logo.png'
                            }
                        } else if (type === 'video') {
                            this.data = {
                                type: 'video',
                                url: 'http://vjs.zencdn.net/v/oceans.mp4'
                            }
                        } else if (type === 'text') {
                            this.data = {
                                type: 'text',
                                content: '这是一段纯文本'
                            }
                        }
                    }
                },
                created: function () {
                    // 初始化时,默认设置图片组件的数据
                    this.change('img');
                }
            })
    
        </script>
    </body>
    

    JXS

    使用Render函数最不友好的地方就是在模板比较简单时,写起来也很复杂,而且难以阅读出DOM结构,尤其当子节点嵌套较多时,嵌套的createElement就像盖楼一样一层层延伸下去。

    为了让Render函数更好地书写和阅读,Vue.js提供了插件babel-plugin-transform-vue-jsx来支持JSX语法。

    JSX是一种看起来像HTML,但实际是JavaScript的语法扩展,它用更接近DOM结构的形式来描述一个组件的UI和状态信息,最早在React.js里大量应用。

    示例:使用Render函数开发可排序的表格组件

    html

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8">
        <title>可排序的表格组件</title>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    
    <body>
        <div id="app" v-cloak>
            <button @click="handleAddData">添加数据</button>
            <v-table :data="data" :columns="columns"></v-table>
        </div>
        <script src="../../lib/vue.2.6.11.js"></script>
        <script src="table.js"></script>
        <script src="index.js"></script>
    </body>
    
    </html>
    

    index.js

    var app = new Vue({
        el: '#app',
        data: {
            columns: [
                {
                    title: '姓名',
                    key: 'name'
                },
                {
                    title: '年龄',
                    key: 'age',
                    sortable: true
                },
                {
                    title: '出生日期',
                    key: 'birthday',
                    sortable: true
                },
                {
                    title: '地址',
                    key: 'address'
                }
            ],
            data: [
                {
                    name: '王小明',
                    age: 18,
                    birthday: '1999-02-21',
                    address: '北京市朝阳区芍药居'
                },
                {
                    name: '张小刚',
                    age: 25, age: 25,
                    birthday: '1992-01-23',
                    address: '北京市海淀区西二旗'
                },
                {
                    name: '李小红',
                    age: 30,
                    birthday: '1987-11-10',
                    address: '上海市浦东新区世纪大道'
                },
                {
                    name: '周小伟',
                    age: 26,
                    birthday: '1991-10-10',
                    address: '深圳市南山区深南大道'
                }
            ]
        },
        methods: {
            handleAddData: function () {
                this.data.push({
                    name: '刘小天',
                    age: 19,
                    birthday: '1998-05-30',
                    address: '北京市东城区东直门'
                });
            }
        }
    });
    

    table.js

    Vue.component('vTable', {
        render: function (h) {
            var _this = this;
    
            //数据行
            var trs = [];
            this.currentData.forEach(function (row) {
                var tds = [];
                _this.currentColumns.forEach(function (cell) {
                    tds.push(h('td', row[cell.key]));
                });
                trs.push(h('tr', tds));
            });
    
            //表头行
            var ths = [];
            this.currentColumns.forEach(function (col, index) {
                if (col.sortable) {
                    ths.push(h('th', [
                        h('span', col.title),
                        // 升序
                        h('a', {
                            class: {
                                on: col._sortType === 'asc'
                            },
                            on: {
                                click: function () {
                                    _this.handleSortByAsc(index)
                                }
                            }
                        }, '↑'),
                        // 降序
                        h('a', {
                            class: {
                                on: col._sortType === 'desc'
                            },
                            on: {
                                click: function () {
                                    _this.handleSortByDesc(index)
                                }
                            }
                        }, '↓')
                    ]));
                } else {
                    ths.push(h('th', col.title));
                }
            });
    
            return h('table', [
                h('thead', [
                    h('tr', ths)
                ]),
                h('tbody', trs)
            ])
        },
        props: {
            columns: {
                type: Array,
                default: function () {
                    return [];
                }
            },
            data: {
                type: Array,
                default: function () {
                    return [];
                }
            }
        },
        data: function () {
            return {
                /*
                为了让排序后的columns和data不影响原始数据,
                给v-table组件的data选项添加两个对应的数据,
                组件所有的操作将在这两个数据上完成,不对原始数据做任何处理 
                */
                currentColumns: [],
                currentData: []
            }
        },
        methods: {
            makeColumns: function () {
                this.currentColumns = this.columns.map(function (col, index) {
                    //添加一个字段标识当前列排序的状态,后续使用
                    col._sortType = 'normal';
                    //添加一个字段标识当前列在原始数组中的索引,后续使用
                    col._index = index;
                    return col;
                });
            },
            makeData: function () {
                this.currentData = this.data.map(function (row, index) {
                    //添加一个字段标识当前行在原始数组中的索引,后续使用
                    row._index = index;
                    return row;
                });
            },
            handleSortByAsc: function (index) {
                var key = this.currentColumns[index].key;
                this.currentColumns.forEach(function (col) {
                    col._sortType = 'normal'; //排序前,先将所有列的排序状态都重置为normal,
                });
                this.currentColumns[index]._sortType = 'asc';
    
                this.currentData.sort(function (a, b) {
                    return a[key] > b[key] ? 1 : -1;
                });
            },
            handleSortByDesc: function (index) {
                var key = this.currentColumns[index].key;
                this.currentColumns.forEach(function (col) {
                    col._sortType = 'normal'; //排序前,先将所有列的排序状态都重置为normal,
                });
                this.currentColumns[index]._sortType = 'desc';
    
                this.currentData.sort(function (a, b) {
                    return a[key] < b[key] ? 1 : -1;
                });
            }
        },
        mounted() {
            // v-table 初始化时调用
            this.makeColumns();
            this.makeData();
        },
        watch: {
            /**
             * 当渲染完表格后,父级修改了data数据,
             * 比如增加或删除,v-table的currentData也应该更新,
             * 如果某列已经存在排序状态,更新后应该直接处理一次排序。
             */
            data: function () {
                this.makeData();
    
                //通过遍历currentColumns来找出是否按某一列进行过排序,
                //如果有,就按照当前排序状态对更新后的数据做一次排序操作
                var sortedColumn = this.currentColumns.filter(function (col) {
                    return col._sortType !== 'normal';
                });
    
                if (sortedColumn.length > 0) {
                    if (sortedColumn[0]._sortType === 'asc') {
                        this.handleSortByAsc(sortedColumn[0]._index);
                    } else {
                        this.handleSortByDesc(sortedColumn[0]._index);
                    }
                }
            }
        }
    });
    

    style.css

    [v-cloak]{
        display: none;
    }
    table{
         100%;
        margin-bottom: 24px;
        border-collapse: collapse;
        border-spacing: 0;
        empty-cells: show;
        border: 1px solid #e9e9e9;
    }
    
    table th{
        background: #f7f7f7;
        color: #5c6b77;
        font-weight: 600;
        white-space: nowrap;
    }
    table td, table th{
        padding: 8px 16px;
        border: 1px solid #e9e9e9;
        text-align: left;
    }
    table th a{
        display: inline-block;
        margin: 0 4px;
        cursor: pointer;
    }
    table th a.on{
        color: #3399ff;
    }
    table th a:hover{
        color: #3399ff;
    }
    

    示例:留言列表

    与之前的几个实战案例不同的是,留言列表更偏向于业务,而之前的实战(数字输入框、标签页、表格)都是独立的功能组件

    出现问题

    按照原书的代码,会出现如下bug:

    Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"
    

    大概意思是:通过props传递给子组件的value,不能在子组件内部修改props中的value值。

    需要定义一个本地的 data 属性并将这个 prop 用作其初始值,同步对组件的修改,再通知父组件更新:

    解决方案:

    Vue.component('vTextarea', {
        props: {
            value: {
                type: String,
                default: ''
            }
        },
        data: function () {
            return {
                currValue: this.value
            }
        },
        watch: {
            currValue: function (val) {
                this.$emit('input', val);
            },
            value: function (val) {
                this.currValue = val;
            }
        },
        render: function (h) {
            var _this = this;
            return h('div', [
                h('span', '留言内容:'),
                h('textarea', {
                    ...
                    on: {
                        input: function (event) {
                            _this.currValue = event.target.value;
                            _this.$emit('input', event.target.value);
                        }
                    }
                })
            ]);
        },
    
    1. 定义一个变量 data.currValue,并用props.value初始化,

    2. props.value的值是单向地由父组件流向子组件,并且是随着父组件的变量data.message变化而变化

    3. data.currValue与父组件的变量data.message,没有任何关系,两者相互独立

    4. data.currValue与父组件的变量data.message其中任何一个值发生变化,要通知对方,可以在

      子组件中添加监听watch:

        watch: {
            currValue: function (val) {
                this.$emit('input', val);
            },
            value: function (val) {
                this.currValue = val;
            }
        }
    

    5 . $emit('input', val): input 事件与 v-model结合可以自动修改绑定值

    index.html

    <!DOCTYPE html>
    <html>
    
    <head>
        <meta charset="utf-8">
        <title>留言列表</title>
        <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    
    <body>
        <div id="app" v-cloak style=" 500px;margin: 0 auto;">
            <div class="message">
                <v-input v-model="username"></v-input>
                <v-textarea v-model="message" ref="message"></v-textarea>
                <button @click="handleSend">发布</button>
            </div>
            <list :list="list" @reply="handleReply"></list>
        </div>
        <script src="../../lib/vue.2.6.11.js"></script>
        <script src="input.js"></script>
        <script src="list.js"></script>
        <script src="index.js"></script>
    </body>
    
    </html>
    

    index.js

    var app = new Vue({
        el: '#app',
        data: {
            username: '',
            message: '',
            list: []
        },
        methods: {
            handleSend: function () {
                this.list.push({
                    name: this.username,
                    message: this.message
                });
                this.message = '';
            }
        },
        methods: {
            handleSend: function () {
                if (this.username === '') {
                    window.alert('请输入昵称');
                    return;
                }
    
                if (this.message === '') {
                    window.alert('请输入留言内容');
                    return;
                }
                this.list.push({
                    name: this.username,
                    message: this.message
                });
                this.message = '';
            },
            handleReply: function (index) {
                var name = this.list[index].name;
                this.message = '回复@' + name + ':';
                this.$refs.message.focus();
            }
        }
    
    });
    

    input.js

    Vue.component('vInput', {
        props: {
            value: {
                type: [String, Number],
                default: ''
            }
        },
        data: function () {
            return {
                currValue: this.value
            }
        },
        render: function (h) {
            var _this = this;
            return h('div', [
                h('span', '昵称:'),
                h('input', {
                    attrs: {
                        type: 'text'
                    },
                    domProps: {
                        value: this.currValue
                    },
                    on: {
                        //使用v-model:动态绑定value,并且监听input事件,把输入的内容通过$emit('input')派发给父组件。
                        input: function (event) {
                            _this.currValue = event.target.value;
                            _this.$emit('input', event.target.value);
                        }
                    }
                })
            ]);
        }
    });
    
    Vue.component('vTextarea', {
        props: {
            value: {
                type: String,
                default: ''
            }
        },
        data: function () {
            return {
                currValue: this.value
            }
        },
        render: function (h) {
            var _this = this;
            return h('div', [
                h('span', '留言内容:'),
                h('textarea', {
                    attrs: {
                        placeholder: '请输入留言内容'
                    },
                    domProps: {
                        value: this.currValue
                    },
                    ref: 'message',
                    on: {
                        input: function (event) {
                            _this.currValue = event.target.value;
                            _this.$emit('input', event.target.value);
    
                        }
                    }
                })
            ]);
        },
        methods: {
            focus: function () {
                this.$refs.message.focus();
            }
        },
        watch: {
            currValue: function (val) {
                this.$emit('input', val);
            },
            value: function (val) {
                this.currValue = val;
            }
        },
    });
    

    list.js

    Vue.component('list', {
        props: {
            list: {
                type: Array,
                default: function () {
                    return [];
                }
            }
        },
        render: function (h) {
            var _this = this;
            var list = [];
            this.list.forEach(function (msg, index) {
                var node = h('div', {
                    attrs: {
                        class: 'list-item'
                    }
                }, [
                    h('span', msg.name + ':'),
                    h('div', {
                        attrs: {
                            class: 'list-msg'
                        }
                    }, [
                        h('p', msg.message),
                        h('a', {
                            attrs: {
                                class: 'list-reply'
                            },
                            on: {
                                click: function () {
                                    _this.handleReply(index);
                                }
                            }
                        }, '回复')
                    ])
                ])
                list.push(node); list.push(node);
            });
            if (this.list.length) {
                return h('div', {
                    attrs: {
                        class: 'list'
                    },
                }, list);
            } else {
                return h('div', {
                    attrs: {
                        class: 'list-nothing'
                    }
                }, '留言列表为空');
            }
        },
        methods: {
            handleReply: function (index) {
                this.$emit('reply', index);
            }
        }
    });
    

    style.css

    [v-cloak]{
        display: none;
    }
    
    *{
        padding: 0;
        margin: 0;
    }
    .message{
         450px;
        text-align: right;
    }
    .message div{
        margin-bottom: 12px;
    }
    .message span{
        display: inline-block;
         100px;
        vertical-align: top;
    }
    .message input, .message textarea{
         300px;
        height: 32px;
        padding: 0 6px;
        color: #657180;
        border: 1px solid #d7dde4;
        border-radius: 4px;
        cursor: text;
        outline: none;
    }
    .message input:focus, .message textarea:focus{
        border: 1px solid #3399ff;
    }
    .message textarea{
        height: 60px;
        padding: 4px 6px;
    }
    .message button{
        display: inline-block;
        padding: 6px 15px;
        border: 1px solid #39f;
        border-radius: 4px;
        color: #fff;
        background-color: #39f;
        cursor: pointer;
        outline: none;
    }
    .list{
        margin-top: 50px;
    }
    list-item{
        padding: 10px;
        border-bottom: 1px solid #e3e8ee;
        overflow: hidden;
    }
    .list-item span{
        display: block;
         60px;
        float: left;
        color: #39f;
    }
    .list-msg{
        display: block;
        margin-left: 60px;
        text-align: justify;
    }
    .list-msg a{
        color: #9ea7b4;
        cursor: pointer;
        float: right;
    }
    .list-msg a:hover{
        color: #39f;
    }
    .list-nothing{
        text-align: center;
        color: #9ea7b4;
        padding: 20px;
    }
    
  • 相关阅读:
    202020211 20209326 《Linux内核原理与分析》第一周作业
    指针入门,以及利用指针简单的数组逆置
    函数入门
    C语言关于处理数组元素的插入、删除、排序
    flag用法之一
    css3中的transform值matrix
    HTML5CANVAS做的打砖块游戏。。。
    用curl抓取网站数据,仿造IP、防屏蔽终极强悍解决方式
    javascript中最好用的extend继承方法
    使用CSS3中Media Queries兼容不同设备
  • 原文地址:https://www.cnblogs.com/easy5weikai/p/13285471.html
Copyright © 2020-2023  润新知