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

cc来了,下一代react状态管理解决方案,你准备好了吗

C_Cwelcomtoccworldquick-startdemo:https:github.comfantastics…github地址欢迎大家star,给我更大的动力。简介硝烟
C_C welcom to cc world

quick-start demo: https://github.com/fantastics…

github地址

欢迎大家star,给我更大的动力。

简介

  • 硝烟四起

众所周知,react本身只是非常优雅的解决了视图层渲染工作,但是随着应用越来越大,庞大的react组件群体之间状态相互其实并不是孤立的,需要一个方案管理把这些状态集中管理起来,从而将model和view的边界划分得更加清楚,针对于此,facebook官方对于react状态管理给了一个flux架构并有一套自己的实现,但是社区里并不满足于此,基于对flux的理解各个第三方做着给出了的自己的解决方案,状态管理框架的战争从此拉开序幕,随着redux横空出世,大家默默接受了redux的 dispatch action、hit reducer、comibine new state、render new view的理念,在redux世界里,组件需要关心的状态变化都由props注入进来,connect作为中间的桥梁将react组件与redux关联起来,通过mapStateToProps将redux里定义好的state映射到组件的props上,以此达到让react组件订阅它需要关心的state变化的目的。

  • 一统天下

随着redux生态逐渐完善,大家默认的把redux当做了react状态管理的首选解决方案,所以redux已经在react状态管理框架里一统天下,通过github star发现,另一个流行的状态管理框架mobx页已经锁定第二的位置,用另一种思路来给大家展示原来状态可以这么管理,状态管理的格局似乎基本已经洗牌完成,可是作为redux重度使用者的我并不满足与此,觉得在redux世界里,通过props层层穿透数据,通过provider包裹整个react app应用,只是解决了状态的流动问题,而组件的通信任然非常间接与尴尬,依靠redux来完成不是不可以,只是对比vue,任然觉得少了些什么……

  • why cc

react-control-center并不是简单的立足于状态管理,而是想为你提供更多的有趣玩法,因为现有的状态管理解决方案已经非常成熟(但是在某些场景未必真的好用),所以cc从一开始设计就让其api对现有的组件入侵非常之小,你可以在redux项目里局部使用cc来把玩cc的状态管理思路,可以从一个组件开始,慢慢在开始渐进式的修改到其他地方,仅仅使用一个register函数,将你的react类注册为cc类,那么从cc类生成的cc实例,将给你带来以下新的特性、新的概念、新的思路。

1 所有cc实例都拥有 emiton 的能力,无论组件间嵌套关系多复杂,实现组件间通信将会是如此轻松。

实际上实例还拥有更精准的emitIdentity, emitWith, onIdentity方法,让用户基于更精准的力度去发射或者接收。

emitIdentity(eventName:string, identity:string, …args), 第一位参数是事件名,第二位参数是认证串,剩余参数是on的handler函数的实际接收参数,当很多相同组件(如以CcClass:BookItem生成了多个CcInstance)订阅了同一个事件,但是你只希望通知其中一个触发handler调用时,emitIdentity就能派上用场了。

onIdentity(eventName:string, identity:string, hancler:function),监听emitIdentity发射的事件。

emitWith(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string})从一个更精准的角度来发射事件,寻找指定模块下,指定cc类名的,指定identity的监控函数去触发执行,具体过程这里先略过,先看下面关于模块和cc类的介绍,在回过头来理解这里更容易。

off(eventName:string, option?:{module?:string, ccClassKey?:string, identity?:string}),取消监听。

这些函数在cc的顶层api里都有暴露,当你的cc app运行起来之后,你可以打开console,输入cc并回车,你会发现这些函数已经全部绑定在window.cc对象下了,你可以直接调用他们来完成快速验证哦,而非通过ccInstance去触发^_^

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
@cc.register('Foo')
class Foo extends Component{
componentDidMount(){
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from});
});
//cc是不允许一个cc实例里对同一个事件名监听多次的,这里fooSignal监听了两次,cc会默认使用最新的监听函数,所以上面个监听变成了无效的监听
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from:`--${from}--`});
});
this.$$on('fooSignalWithIdentity', 'xxx_id_wow',()=>{
this.setState({signal, from});
})
}
}
@cc.register('Bar')
class Bar extends Component{
render(){






}
}

2 所有cc实例都可以针对自己的state的任意key定义
computed 函数,cc会在key的值发生变化自动计算新的computed值并缓存起来,在实例里定义的computed会收集到实例的refComputed对象里。

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
@cc.register('Foo')
class Foo extends Component{
constructor(props, context){
super(props, context);
this.state = {woo:'woo cc!'};
}
$$computed(){
return {
wow(wow){
return `computed wow ${wow}`;
}
}
}
componentDidMount(){
this.$$on('fooSignal',(signal, from)=>{
this.setState({signal, from});
});
}
changeWow = (e)=>{
this.setState({wow: e.currentTarget.value});
}
render(){
return (


{this.state.wow}
{this.$$refComputed.wow}


);
}
}

3 注册为cc类的时候,为该cc类设定了一个该cc类所属的
模块 ,并通过sharedStateKeys声明关心该模块里哪些key(可以是任意的key,也可以是这个模块的所有key)的变化,则由改cc类产生的cc实例共同监听着这些key对应值的变化,任何一个cc实例改变了这些sharedStateKeys里的值,其他cc实例都能感知到它的变化并自动被cc触发渲染。

import cc from 'react-control-center';
import React,{Component, Fragment} from 'react';
class Foo extends Component{
render(){
return

any jsx fragment here

}
}
//将Foo注册为一个共享FooOfM1模块所有key变化的cc类FooOfM1
const FooOfM1 = cc.register('FooOfM1', {module:'M1', sharedStateKeys:'all'})(Foo);
//将Foo注册为一个共享FooOfM2模块key1和key2变化的cc类FooOfM2
const FooOfM2 = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2']})(Foo);
//将Foo注册为一个共享FooOfM2模块key1和key2变化,且共享global模块g1变化的cc类FooOfM2G
const FooOfM2G = cc.register('FooOfM2', {module:'M2',sharedStateKeys:['key1','key2','key3'],globalStateKeys:['g1']})(Foo);
//不设定任何参数,只写cc类名,cc会把Foo注册为一个属于default模块的cc类
const JustWantToOwnCcAbility = cc.register('JustWantToOwnCcAbility')(Foo);
//cc同时也为register提供简写函数
//const FooOfM2G = cc.r('FooOfM2',{m:'M2',s:['key1','key2','key3'],g:['g1']})(Foo})

4 注意在3里我们提到一个概念
模块,对于cc来说一个完整的模块包括以下属性:state、reducer、init、computed,这些参数都是调用cc.startup时注入,注意,cc虽然不需要用户像redux那样要给顶层App组件包裹一层但是要求用户在app入口文件的第一句话那里触发cc.startup 让整个cc运行起来,store、reducer、init、computed就是cc.startup需要的参数

《cc来了,下一代react状态管理解决方案,你准备好了吗》

  • store是一个object对象,store里的各个key就表示模块名,对应的值就是各个模块对应的state,一个cc实例除了setState方法能够触发修改state,还可以通过dispatch方法派发action对象去修改state,此时具体的数据合成逻辑就体现在下面要说的reducer里了
  • recuder是一个object对象,recuder里的各个key表示reducer的模块名,通常用户可以定义和state的模块名保持一致,但是可以定义另外的模块名,所以这里的模块指的是reducerModuel,不强求用户定义时和stateModule保持一致,stateModule对应的值一个普通的json对象,key为函数名,值为处理函数,即处理旧state并合成新state的方法,cc支持函数为普通函数、生成器函数、async函数。

上面提到了dispatch函数需要传递一个action对象,一个标准的action必须包含type、payload 2个属性,表示cc要去查recuder里某个模块下type映射函数去修改某个模块的state,具体是什么模块的type映射函数和什么模块对应的state,参见action剩余的两个可缺省的属性module和reducerModule的设定规则,注意,这里再一次提到了reducerModule,以下规则就体现了为什么cc允许reducer模块名可以自由定义:

不指定module和reducerModule的话,cc去查reducer里当前cc实例所属模块下的type映射函数去修改当前cc实例所属模块的state。

指定了module,而不指定reducerModule的话,cc去查reducer里module下的type映射函数去修改module模块的state。

不指定module,指定reducerModule的话,cc去查reducer里reducerModule下type映射函数去修改当前触发dispatch函数的cc实例所属的module模块的state。

指定了module,同时也指定了reducerModule的话,cc去查reducer里reducerModule下type映射函数去修改module模块的state。

之所以这样设计是因为考虑到让用户可以自由选择reducer的模块描述方式,因为对于cc来说,dispatch派发的action只是为了准确找到reducer里的处理函数,而reducer的模块定义并不需要强制和state保持一致给了用户更多的选择去划分reducer的领域

  • init是一个object对象,key是模块名,严格对应stateModule,值是一个函数,如果用户为某个模块定义了init函数表示用户希望有机会再次初始化某个模块的state,通常是异步请求后端来的数据重新赋值给模块对应的state
  • computed是一个object对象,key是模块名,严格对应stateModule,值是一个moduleComputedObject,moduleComputedObject的key指的就是某个module的某个key,value就是为这个key定义的计算函数,函数的第一为参数就是key的原始值,cc实例里通过moduleComputed对象取到计算后的新值,特别地,为global模块定义的moduleComputedObject对象,在cc实例里通过globalComputed对象取到计算后的新值

//code in index.js
import api from '@foo/bar/api';
cc.startup({
isModuleMode:true,//表示cc以模块化方式启动,默认是false,cc鼓励用户使用模块化管理状态,更容易划分领域的边界
store:{
$$global{//$$global是cc的内置模块,用户如果没有显式的定义,cc会自动注入一个,只不过是一个不包含任何key的对象
themeColor:'pink',
},
m1:{
name:'zzk',
age:30,
books:[],
error:'',
},
m2:{
wow:'wow',
signal:'haha',
}
},
reducer:{
m1:{
//state表示调用dispatch的cc实例对应的state,moduleState只描述的是cc实例所属的模块的state,更多的解释看下面的4 5 6 7 8这些点。
//特别的注意,如果该方法是因为某个reducer的函数里调用的dispatch函数而被触发调用的,此时的state始终指的是最初的那个在cc实例里触发dispatch时那个cc实例的state,而moduleState始终指向的是指定的module的的state!!!
changeName:function({payload,state,moduleState,dispatch}){
const newName = payload;
dispatch({module:'m2',type:'changeSignal',payload:'wow!dispatch in reducer function block'});
return {name:newName};
},
//支持生成器函数
changeAge:function*({payload,state,moduleState,dispatch}){
const newAge = payload;
const result = yield api.verifyAge(newAge);
if(result.error)return({error:result.error});
else return {name:newName};
},
//支持async
changeAge:async function({payload:{pageIndex,pageSize}}){
const books = yield api.getBooks(pageIndex, pageSize);
return {books};
}
},
m2:{
changeSignal:function({payload:signal,dispatch}){
//注意m1/changeName里指定了修改m2模块的数据,其实这里可以一次性return {signal, wow:'just show reducerModule'}来修改数据,
//但是故意的调用dispatch找whatever/generateWow来触发修改m2的wow值,是为了演示显示的指定reducerModule的作用
dispatch({module:'m2',reducerModule:'whatever',type:'generateWow',payload:'just show reducerModule'})
return {signal};
}
},
whatever:{//一个刻意和stateModule没有保持一致的reducerModule
generateWow:function({payload:wow}){
return {wow};
}
},
$$global:{//为global模块指定reducer函数
changeThemeColor:function({payload:themeColor}){
return {themeColor}
}
}
},
init:{
$$global:setState=>{//为global模块指定state的初始化函数
api.getThemeColor().then(themeColor=>{
setState({themeColor})
}).catch(err=>console.log(err))
}
},
computed:{
m1:{
name(name){//reverse name
return name.split('').reverse().join('');
}
}
}
})

4 注意第3点里,注册一个react类到某个模块里成为cc类时,sharedStateKeys可以是这个模块里的任意key,因为cc允许注册不同的react类到同一个模块,例如模块M里拥有5个key为f1、f2、f3、f4、f5, ccClass1通过sharedStateKeys观察模块M的f1、f2, ccClass2通过sharedStateKeys观察模块M的f2、f3、f4,当ccClass1的某个实例改变了f2的值,那么ccClass1的其他实例和ccClass2的所有实例都能感知到f2的变化并被cc触发渲染。

5 cc有一个内建的global模块,所有的ccClass都天生的拥有观察global模块key值变化的能力,注册成为cc类时通过globalStateKeys观察模块global里的任意key。

6 所有cc实例上可以通过prop ccOption设定storedStateKeys,表示该实例上的这些key是需要被cc存储的,这样在该cc实例销毁然后再次挂载回来的时候,cc可以把这些key的值恢复回来。

7 一个cc实例的state的key除了上面所提到的global、 shared、stored这三种类型,剩下的一种key就是默认的temporary类型了,这种key对应的值随着组件销毁就丢失了,再次挂载cc实例时会读取state里的默认值。

8 结合4 5 6 7来看,cc实例里的state是由cc类上申明的sharedStateKeys、globalStateKeys,和cc实例里ccOption申明的storedStateKeys对应的值,再加上剩下的默认的temporaryStateKeys对应的值合并得出来。

《cc来了,下一代react状态管理解决方案,你准备好了吗》

9 和react实例一样,触发cc实例render方法,依然是经典的setState方法,以及上面提到的dispatch定位reducer方法去修改,除了这两种cc还有更多自由的选择,如invoke,effect,xeffect允许用户直接调用自己定义的函数去修改state,同reducer函数一样,可以是普通函数、generator函数、async函数。

这样的方式让用户有了更多的选择去触发修改state,cc并不强制用户使用哪一种方式,让用户自己摸索和组合更多的最佳实践

invoke一定是修改当前cc实例的state,只需要传入第一位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数。

effect允许用户修改其他模块的state,第一位参数是moduleName,第二位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数。

xeffect和effect一样,允许用户修改其他模块的state,第一位参数是moduleName,第二位参数为具体的用户自定义执行函数,剩余的其他参数都是执行函数需要的参数,和effect不一样的地方是xeffect调用的执行函数的参数列表,第一位是cc注入的ExecuteContext对象,里面包含了module, state, moduleState, xeffect,剩下的参数才对应的是是用户调用xeffect是除第一第二位参数以外的其他参数

import React,{Component, Fragment} from 'react';
import cc from 'react-control-center';
function* loginFn(changedBy, p1, p2 ,p3){
return {changedBy, p1:p1+'--tail', p2:'head--'+p2 ,p3}
}
function* forInvoke(changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
function* forEffect(changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
function* forXeffect({module, state, moduleState, xeffect}, changedBy, p1, p2 ,p3){
const result = yield loginFn(changedBy, p1, p2 ,p3);
return result;
}
@cc.register('Foo')
class Foo extends Component{
constructor(props, context){
super(props, context);
this.state = {changedBy:'none', p1:'', p2:'' ,p3:''};
}
render(){
const {changedBy, p1, p2, p3} = this.state;
//注,该cc类模块没有显式的声明模块,会被cc当做$$default模块的cc类
return (

changedBy {changedBy}

p1 {p1} p2 {p2} p3 {p3}





);
}
}

10 cc定位的容器型组件的状态管理,通常情况一些组件和model非常的有业务关系或者从属关系,我们会把这些react类注册为某个moudle的cc类,观察这个模块中的状态变化,但是有些组件例如一个Workpace类的确需要观察很多模块的状态变化,不算是某个模块对应的视图组件,此时除了用上面说的sharedToGlobalMapping功能,将需要观察各个模块的部分状态映射到global里,然后注册Workpace时为其设定globalStateKeys,就能达到观察多个模块的状态变化的目的之外,cc还提供另一种思路,注册Workpace时设定stateToPropMapping,就可以观察恩义模块的任意key的值变化,和sharedToGlobalMapping不同之处在于,stateToPropMapping要从this.$$propState里取值,sharedToGlobalMapping是从this.state取值,当然stateToPropMapping不需要模块主动的将某些key映射到global里,就能达到跨模块观察状态变化的目录,cc鼓励用户精确对状态归类,并探索最佳组合和最佳实践

// these code written in https://github.com/fantasticsoul/rcc-simple-demo/tree/master/src/cc-use-case/WatchMultiModule
// you can update the rcc-simple-demo lastest version, and run it, then switch tab watch-multi-module, you will see what happen
import React from 'react';
import cc from 'react-control-center';
class WatchMultiModule extends React.Component {
render() {
console.log('%c@@@ WatchMultiModule', 'color:green; border:1px solid green;');
console.log(`type cc.setState('todo',{todoList:[{id:Date.now()+'_1',type:'todo',content:'nono'},{id:Date.now()+'_2',type:'todo',content:'nono'}]}) in console`);
const { gbc, alias_content, counter_result, todoList } = this.$$propState;
return (


open your console

type and then enter to see what happen cc.setState('counter',{result : 888} )

type and then enter to see what happen cc.setGlobalState( {content:'wowowo'} );

{gbc}

{alias_content}

{counter_result}

{todoList.length}


);
}
}
const stateToPropMapping = {
'$$global/borderColor': 'gbc',
'$$global/content': 'alias_content',
'counter/result': 'counter_result',
'todo/todoList': 'todoList',
};
//two way to declare watching multi module cc class
export default cc.connect('WatchMultiModule', stateToPropMapping)(WatchMultiModule);
//export default cc.register('WatchMultiModule', {stateToPropMapping})(WatchMultiModule);

github地址:https://github.com/fantastics…

gitee地址:https://gitee.com/nick_zhong/…

quick-start demo: https://github.com/fantastics…

期待大家试用并给出修改意见,真心希望能够亲爱你的能够感受的cc的魅力和强大,因为作为redux使用者的我(3年了快),不管是用了原生的redux还是dva封装后的redux,个人都觉得没有cc使用那么的爽快……先从示例项目开始体验吧^_^,期待着你和我有一样的感受


推荐阅读
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • C++字符字符串处理及字符集编码方案
    本文介绍了C++中字符字符串处理的问题,并详细解释了字符集编码方案,包括UNICODE、Windows apps采用的UTF-16编码、ASCII、SBCS和DBCS编码方案。同时说明了ANSI C标准和Windows中的字符/字符串数据类型实现。文章还提到了在编译时需要定义UNICODE宏以支持unicode编码,否则将使用windows code page编译。最后,给出了相关的头文件和数据类型定义。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • 本文介绍了Android 7的学习笔记总结,包括最新的移动架构视频、大厂安卓面试真题和项目实战源码讲义。同时还分享了开源的完整内容,并提醒读者在使用FileProvider适配时要注意不同模块的AndroidManfiest.xml中配置的xml文件名必须不同,否则会出现问题。 ... [详细]
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 深度学习中的Vision Transformer (ViT)详解
    本文详细介绍了深度学习中的Vision Transformer (ViT)方法。首先介绍了相关工作和ViT的基本原理,包括图像块嵌入、可学习的嵌入、位置嵌入和Transformer编码器等。接着讨论了ViT的张量维度变化、归纳偏置与混合架构、微调及更高分辨率等方面。最后给出了实验结果和相关代码的链接。本文的研究表明,对于CV任务,直接应用纯Transformer架构于图像块序列是可行的,无需依赖于卷积网络。 ... [详细]
  • 本文介绍了在MFC下利用C++和MFC的特性动态创建窗口的方法,包括继承现有的MFC类并加以改造、插入工具栏和状态栏对象的声明等。同时还提到了窗口销毁的处理方法。本文详细介绍了实现方法并给出了相关注意事项。 ... [详细]
  • 使用eclipse创建一个Java项目的步骤
    本文介绍了使用eclipse创建一个Java项目的步骤,包括启动eclipse、选择New Project命令、在对话框中输入项目名称等。同时还介绍了Java Settings对话框中的一些选项,以及如何修改Java程序的输出目录。 ... [详细]
author-avatar
庄蜗怕
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有