ECharts 中国地图 china.js 注册、下钻与按需加载实践

发布时间:2026/10/26 17:43:36
ECharts 中国地图 china.js 注册、下钻与按需加载实践

简介:这份压缩包面向使用 ECharts 制作中国地图和省级区域可视化的前端开发者、数据分析人员及可视化学习者,重点解决网页地图底图缺失、全国与地方数据难以分层展示的问题。包内共 35 个 js 文件,压缩包约 841KB,以 china.js 全国地图脚本和各地省级地图脚本为主,可直接配合 ECharts 的 geo、series 配置使用,覆盖全国轮廓、省份边界及区域数据呈现等常见需求。已有 5705 人学习下载,适合作为地图类图表入门与项目实践的参考素材。读者可获得较完整的地图脚本集合,用于区域着色、标注提示、缩放漫游、点击交互和动态数据更新等场景,也能据此梳理全国与省级地图的配置方式,减少自行整理地理数据的时间,为省市级可视化页面开发提供现成基础。

1. 一个只有地图容器、却怎么都不出图的下午

那年接手一个运营看板,页面上摆着一个 800×600 的div,需求写得干脆:中国地图、各省份销售额染色、点击下钻到省。我照着记忆写完echarts.init,写上series.type = 'map'、map: 'china',刷新,屏幕上干干净净,控制台也安安静静。排查了快半小时才想起来,ECharts 5 之后内置地图数据被拆出去了,china.js这类地图文件得自己拿、自己registerMap。这就是 china.js 中国及各地 js 文件压缩包的价值所在——它不是组件库,是一批按行政级别切好的地图数据脚本,专门喂给 echarts 的地图组件。做数据大屏、区域经营看板、门店或物流分布图的前端,还有那些内网环境里不方便临时去抠 GeoJSON 的人,拿到它就能少走这一段弯路。

2. 压缩包里到底是什么:registerMap 的注册机制与文件构成

先把一个误解掰正:这套文件里没有任何图表逻辑,没有主题、没有配置项、没有封装好的组件。它只是「数据」,只不过穿了js的外衣。真正的渲染能力来自 ECharts 本身,地图文件负责把一份行政区边界的几何数据塞进 ECharts 的全局地图注册表,让series.type = 'map'或者geo组件能按名字找到它。理解了这层关系,后面所有的加载、下钻、按需引入才有得谈。

2.1 从 registerMap 说起:js 文件与 GeoJSON 的等价关系

打开任意一个地图 js,剥掉外层包装,核心只有一句echarts.registerMap(名字, GeoJSON)。外层那段 UMD 包装是为了兼容三种运行环境:AMD、CommonJS 和直接<script>引入。所以同一份数据,在 Webpack 项目里require能拿到registerMap的副作用,在纯静态页里<script src>也能自动执行注册。

// 一份典型地图 js 的内部结构(结构示意,非原始代码) (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD:配合 RequireJS 这类加载器 define(['exports'], factory); } else if (typeof exports === 'object') { // CommonJS / Node 打包环境 factory(exports); } else { // 浏览器全局:挂到 window.echarts 上 factory((root.echarts = root.echarts || {})); } })(this, function (exports) { // 全部内容就只有这一次注册调用 exports.registerMap('某省级行政区', { type: 'FeatureCollection', // GeoJSON 顶层类型,固定写法 UTF8Encoding: true, // 部分老文件带的编码标记,可忽略 features: [ { type: 'Feature', properties: { name: '下辖某市', // 与数据里 name 对齐的关键字段 adcode: 000000, // 行政区划代码,下钻和联动的稳定主键 cp: [116.4, 39.9] // 区域中心点经纬度,做标签定位用 }, geometry: { type: 'Polygon', // 或 MultiPolygon,海岛类常见后者 coordinates: [ /* 环状坐标数组,闭合多边形 */ ] } } ] }); });

第一段是环境判定,决定了这份文件在打包器和浏览器里都能直接用。第二段是真正干活的地方:registerMap的第一个参数是地图名,之后option里写map: '某省级行政区'时必须和它逐字一致,中文、空格、后缀都不能差;第二个参数是标准 GeoJSON 的FeatureCollection,features数组里每个元素是一个行政区面。properties.adcode这个字段值得单独记一笔,name可能因为简称/全称对不上导致染色失败,但adcode是数字主键,做下钻、做区域联动、做数据表关联时用它,比用名字稳得多。

2.2 目录结构与命名规则:全国包、省级包与特殊文件

压缩包解开之后,大致是三个层次:一份全国地图,一批省级地图,外加少量特殊用途文件。文件名基本就是行政区名称,中文命名居多,个别版本会带-或拼音后缀做区分。判断该引哪份的标准很简单——看你的map字段要填什么名字,以及用户能下钻到第几级。

文件类型内容体积量级使用场景
全国地图 js省级行政区面数据数百 KB 至 1 MB 级首屏总览、全国分布
省级地图 js地级行政区面数据数十 KB 至 200 KB 级点击省份后下钻
特殊区域文件单个地市或特殊区域几十 KB局部放大、专题图
配套 geoJSON与 js 同源的几何数据与 js 相近走异步加载路线时用

体积那一列只能给量级,因为同一份数据在压缩、精度保留程度不同的情况下差异很大。实际经验是:全国包塞进首屏 bundle,加上 ECharts 本体和业务代码,首屏 JS 很容易堆到 1.5 MB 以上,移动端或弱网场景下首屏白屏时间会明显拉长。这也是后面第 6 章要讲按需加载的原因。

2.3 三个容易被忽略的字段:name、adcode 与 cp

name是渲染和匹配的主力,但它是人写的,容易出现简繁、简称全称、新旧名称不一致的问题。adcode是数字,稳定,适合做关联主键。cp是中心点坐标,做地图标签、做飞线起点、做聚合点的默认位置都用得上,很多自己转 GeoJSON 的团队会漏掉它,导致标签只能飘在多边形几何中心上,一个狭长地市的名字会落在很奇怪的位置。

还有一个坐标系问题必须提前说清楚:地图 js 里的经纬度是常规地理坐标(WGS84 近似),如果你的散点数据来自某些使用偏移坐标的在线底图服务,两者叠加会出现整体平移,看起来像「点飘到海里去了」。这不是 echarts 的玄学,是坐标系没对齐。解决办法是让业务数据在入库时就统一到同一套坐标,别在渲染层做补偿。

提示:判断一份地图文件能不能用,最省事的办法不是看图好不好看,是在控制台打印echarts.getMap('地图名'),能拿到对象且geoJson.features.length不为 0,说明注册成功。

3. 跑起来:把中国地图挂到页面上

理论讲完,落到能跑的东西上。这一章的目标是:新建一个 HTML 文件,双击打开就能看到中国地图,并且能把一份最朴素的数据染上去。不依赖构建工具,不依赖脚手架,先把链路打通,再谈工程化。

3.1 最小可运行页面:八行 option 出图

先准备目录:一个index.html,一个lib文件夹放 ECharts 的 dist 文件,一个map文件夹放地图 js。地图文件的引入顺序必须在 ECharts 之后,因为它的包装函数会去找window.echarts,早于 ECharts 引入就会直接报echarts is not defined。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8" /> <title>地图最小示例</title> <style> /* 容器必须有明确宽高,否则 init 出来是 0×0,图不会显示 */ #map { width: 900px; height: 600px; } </style> </head> <body> <div id="map"></div> <!-- 1. 先加载 echarts 本体 --> <script src="./lib/echarts.min.js"></script> <!-- 2. 再加载地图数据,它会自动执行 registerMap --> <script src="./map/china.js"></script> <script> const chart = echarts.init(document.getElementById('map')); const option = { tooltip: { trigger: 'item' }, series: [{ type: 'map', map: 'china', // 必须与 registerMap 里的名字完全一致 roam: true, // 允许缩放和平移,大屏场景建议开 label: { show: false },// 省份多时先关掉,避免文字重叠 itemStyle: { areaColor: '#f3f6fb', borderColor: '#c9d3e3' }, emphasis: { itemStyle: { areaColor: '#ffe7a3' } // 悬停高亮 }, data: [] // 数据后补,格式 [{ name: '某省', value: 100 }] }] }; chart.setOption(option); // 容器尺寸变化时必须手动调,否则拖窗口会错位 window.addEventListener('resize', () => chart.resize()); </script> </body> </html>

这段代码里几个参数值得展开。map: 'china'是唯一的地图标识,写错就是空白图,不会报错,非常隐蔽。roam: true打开滚轮缩放和拖拽,大屏上体验好,但嵌在小卡片里容易误操作,我一般只在全屏视图里开。label.show默认是开的,全国视图下 30 多个标签挤在一起会糊成一片,先关掉、只在emphasis里开悬停标签是更常见的做法。最后那个resize监听别省,容器跟着 flex 布局变宽时,不调resize地图会保持旧尺寸,看起来像「图被裁了一半」。

3.2 系列类型怎么选:map、geo 与散点图的组合

新手最容易在这里翻车:想在地图上叠散点,于是写了一个type: 'map'的系列,又写了一个type: 'scatter'的系列,结果散点坐标被当成像素值,点全堆在左上角。原因是散点系列默认用直角坐标系,要让它认地图坐标,得给它配coordinateSystem: 'geo',而geo是一个独立组件,不是map系列。

两者的区别记一句话就够:series.map是「一张会染色的地图」,geo是「一套地图坐标系」,散点、飞线、涟漪点都要挂在geo上。如果你既想染色又想叠点,通常有两种写法——用geo当底图,再加一个map系列并设置geoIndex让它复用坐标系;或者只用geo,用regions手写区域颜色。前者配置少,后者控制细。

// 常见组合:geo 打底 + map 系列染色 + 散点叠加 const option = { geo: { map: 'china', roam: true, itemStyle: { areaColor: '#eef3fa', borderColor: '#c2cfe0' } }, series: [ { type: 'map', geoIndex: 0, // 复用上面那个 geo 的坐标系和底图 data: [] // 染色数据 }, { type: 'scatter', coordinateSystem: 'geo', // 关键:声明使用地理坐标 symbolSize: 8, data: [ /* [{ name, value: [lng, lat, 数值] }] */ ] } ] };

geoIndex: 0表示这个 map 系列跟着第 0 个 geo 组件走,好处是底图只渲染一次,缩放平移时散点和面始终同步。散点的value是三元素数组[经度, 纬度, 业务值],前两个是坐标,第三个才是数值维度,visualMap和symbolSize回调都读第三位。刚上手时经常写成[值, 经度, 纬度],图能出来但位置离谱,属于典型顺序踩坑。

3.3 数据绑定与 visualMap:染色不生效的三个检查点

染色不生效是反馈最多的问题,排查顺序我固定成三步。第一步看数据的name:地图里叫「某省」,数据里写「某省份」,字符串不相等就不会匹配,这种情况用nameMap做一次重命名最省事。第二步看visualMap的seriesIndex:页面上有多个系列时,不指定索引它会作用到第一个系列上。第三步看min/max:数据全部挤在极小区间时,颜色差异肉眼看不出来,看起来像没生效。

const option = { visualMap: { type: 'continuous', // continuous 连续 / piecewise 分段 min: 0, max: 1000, // 别用数据的实际极值硬写,留出业务口径的上下限 left: 20, bottom: 20, text: ['高', '低'], inRange: { color: ['#e8f3ff', '#4a90e2', '#1d4f91'] }, seriesIndex: 0 // 明确指定作用于哪个系列 }, series: [{ type: 'map', map: 'china', data: [ { name: '某省A', value: 820 }, { name: '某省B', value: 145 } ] }] };

inRange.color建议三到五档,超过五档人眼分辨不出来,图例也会变长。max写死业务上限而不是数据极值,是为了让不同时间点的截图可比——今天最大值 900,明天最大值 200,若跟着数据自适应,同一个颜色在两天里代表不同含义,看图的人会误读。

4. 核心链路:地图下钻与按需加载的实现

全国地图只是入口,真正的业务价值在下钻:点某个省,加载该省的地图,展示地市级分布,再点能返回。这条链路里,容易出问题的不是 echarts 配置,而是加载时序和状态管理。省文件是异步来的,用户手快连点两次,两个请求回来顺序颠倒,地图就可能停在一个错误层级上。

4.1 下钻的两种做法:多系列预注册与动态换图

第一种做法在初始化时把所有省文件都<script>进来,一次性注册完,下钻只是切换map字段。优点是切换零延迟,缺点是首屏要下载全量数据,几十个文件加起来的体积对首屏很不友好,只适合内网或离线演示。

第二种是下钻时按需加载对应省的 js,加载完再registerMap,然后setOption换图。这是线上项目的主流做法。它带来两个额外问题:重复点击会重复插入<script>,以及返回上一级时要把map切回全国、把数据换回全国口径。下面这段封装同时解决了这两点。

// 地图加载器:缓存 + 并发去重 + 失败重试 const mapLoader = (() => { const cache = new Map(); // 已注册的地图名 -> Promise const BASE = './map/'; function loadScript(name) { return new Promise((resolve, reject) => { const el = document.createElement('script'); el.src = `${BASE}${name}.js`; el.async = true; el.onload = () => resolve(name); el.onerror = () => reject(new Error(`地图加载失败: ${name}`)); document.head.appendChild(el); }); } return function ensureMap(name) { // 已注册过就直接返回,避免重复请求和重复插入 script if (echarts.getMap(name)) return Promise.resolve(name); // 正在加载中的同名请求复用同一个 Promise,解决连点竞态 if (cache.has(name)) return cache.get(name); const task = loadScript(name) .then(() => { if (!echarts.getMap(name)) { throw new Error(`脚本已加载但未注册: ${name}`); } return name; }) .catch((err) => { cache.delete(name); // 失败要清缓存,否则永远重试不了 throw err; }); cache.set(name, task); return task; }; })();

这段封装有三个关键点。echarts.getMap(name)是同步的注册检查,能挡掉绝大多数重复加载。cache存的是 Promise 而不是布尔值,是为了让同一时刻的多个调用共享一次网络请求——用户连点两下,只会发一次。catch里删缓存是后悔药,网络抖动导致的失败如果不清理,这个地图在当前会话里就再也加载不了了。el.async = true保证不阻塞解析,但也意味着onload之后地图一定已经注册好了,因为注册语句在脚本顶层同步执行。

4.2 下钻与返回:状态机比 if-else 好维护

有了加载器,下钻逻辑本身不复杂,但层级状态必须显式记下来,否则「返回上一级」会变成猜谜。我一般维护一个栈,栈里存{ mapName, data, title },下钻时压栈,返回时出栈。

const stack = [{ mapName: 'china', title: '全国', data: nationwideData }]; async function drillTo(mapName, title, data) { try { await mapLoader(mapName); // 先确保地图注册完成 stack.push({ mapName, title, data }); render(stack[stack.length - 1]); } catch (e) { // 加载失败时保持当前层级,只给轻提示,不要清空地图 console.warn(e.message); } } function goBack() { if (stack.length <= 1) return; // 已经在最顶层,直接返回 stack.pop(); render(stack[stack.length - 1]); } function render(state) { chart.setOption({ title: { text: state.title }, series: [{ type: 'map', map: state.mapName, data: state.data }] }, true); // 第二个参数 true 表示不合并,彻底替换旧配置 }

setOption(option, true)那个true很关键。默认是合并模式,从全国切到某个省时,旧的data、旧的visualMap极值都可能残留,表现就是「换了地图但颜色还是上一级的口径」。彻底替换会丢掉一些公共样式配置,所以实践中我把公共样式抽成一个baseOption,每次Object.assign拼出来再setOption(..., true)。

4.3 交互回退:面包屑、双击返回与键盘

下钻做了,返回就一定得做,而且要显眼。最常见的做法是右上角面包屑,点任意一级直接跳回。实现上不要用pop循环,直接把栈截断到目标索引即可。

另外两个细节容易被忘:一是地图区域点击事件要用chart.on('click', params => ...)并判断params.componentType === 'series',否则点图例、点标题都会触发下钻;二是从省级返回全国时,roam的缩放状态不会自动重置,用户看到的是上次放大后的全国图,需要调chart.dispatchAction({ type: 'restore' })或者重新setOption。

4.4 渲染取舍:canvas 与 svg、大数据量下的降级

ECharts 默认用 canvas 渲染,地图面数多、边界点密的时候,canvas 在低端设备上首次渲染会明显卡顿。如果页面还要做高清截图、导出 PDF,svg 渲染器更合适,echarts.init(el, null, { renderer: 'svg' })即可切换,代价是复杂图形下 DOM 节点数量暴涨。我的判断标准是:面数过千、需要频繁重绘的选 canvas;图形简单、需要矢量导出的选 svg。

还有一个常被忽略的点是地图精度。同一份边界数据,高精度版本的点数是低精度版本的好几倍,在手机上看全国地图,那些细微的海岸线弯曲根本分辨不出来,却要付出几倍的解析和渲染时间。如果压缩包里带了简化版文件,移动端优先用简化版,这是最省事的性能优化。

5. 避坑与排查:五条踩过的记录

这一章全是现场记录,每条按「现象、原因、解决」写。地图这块的坑有个共同特点:不报错,只是不出图或者出得不对,所以排查全靠对照检查。

5.1 地图一片空白,控制台毫无提示

现象:容器宽高正常,init没报错,页面就是没有图形,连边框都没有。

原因:series.map的值和registerMap注册的名字不一致,或者地图 js 压根没加载成功。后者常见于路径写错——./map/china.js在打包后可能变成相对根目录的路径,404 了但浏览器不一定会显眼地报出来。

解决:控制台先执行echarts.getMap('china'),返回undefined就是没注册成功;再打开 Network 面板看那个 js 的响应状态。名字不一致的情况,用console.log(Object.keys(...))之类的手段把已注册的地图名列出来比对,比肉眼改快得多。

5.2 数据染不上色,只有底图

现象:地图出来了,data也传了,但所有区域都是同一个颜色。

原因:三种可能,按概率排序是name不匹配、visualMap没指定seriesIndex、min/max覆盖了数据范围。name不匹配最常见,地图里写的是全称,业务表里存的是简称。

解决:在visualMap和series之间先做一次名称归一化,用nameMap配置或者加载数据时map一遍。排查时可以临时把所有区域设成固定色,只留一个区域写value: 999,看它变不变色,能快速定位是匹配问题还是映射问题。

5.3 打包后体积翻倍,首屏白屏时间变长

现象:本地开发很快,构建上线后首屏明显变慢,产物分析里能看到一堆地图 js。

原因:用import './map/某省.js'这种静态引入,打包器会把所有引用到的地图全部打进主 chunk,哪怕用户根本没点下钻。压缩包里的文件是「全量可用」,不代表「全量该加载」。

解决:改成动态import(),让打包器单独切 chunk;或者干脆不走打包器,用第 4 章那个loadScript从静态目录按需拉。判断标准很简单——地图文件是否在首屏就必然用到,不是就异步。

5.4 缩放平移之后,散点和地图对不上

现象:初始状态点位置正确,用户滚轮放大后,点开始偏移,越放越离谱。

原因:散点系列没配coordinateSystem: 'geo',走的是直角坐标系;或者散点和地图分别用了两个独立的geo,各自缩放不同步。

解决:散点必须声明coordinateSystem: 'geo',并且与地图共用同一个geo组件(geoIndex指向它)。如果用的是series.map而非geo,那散点无法直接叠上去,需要改成geo打底的写法,这是结构问题,不是调参能救的。

5.5 从省级返回全国后,颜色和标题错乱

现象:下钻正常,点返回后地图变成全国了,但颜色还是省里的口径,标题也没变回来。

原因:setOption用了默认的合并模式,上一层的data、visualMap.max、title都残留在配置里,没被覆盖的字段会一直留着。

解决:切层级时用setOption(option, true)完全替换,或者显式把data、title、visualMap都重写一遍。我一般抽一个buildOption(state)纯函数,用栈顶状态生成完整配置,切层级只调这一处,避免东补西补。

注意:行政区划是会调整的,撤县设区、改名之后,旧地图文件里的adcode和name会和新业务表对不上。这类问题不会报错,只会表现为某几个区域永远染不上色,排查时容易被当成代码 bug。

6. 二次开发:用 GeoJSON 加动态 import 替掉整包 js

真正长期维护的项目,我不会直接把压缩包原样扔进仓库。原因有两个:地图 js 是「可执行脚本」,在严格的 CSP 策略下可能被拦;而且它把数据和注册逻辑耦在一起,做数据校验、做增量更新都不方便。更顺手的做法是把 js 转成纯 GeoJSON,自己控制加载和注册。

6.1 转换脚本:把 js 里的 GeoJSON 抽出来

写个一次性脚本,在 Node 里模拟一个假的echarts对象接住registerMap的调用,跑一遍所有 js,就能把每个文件里的 GeoJSON 落盘成.json。这样做的收益是数据与代码分离,GeoJSON 可以走 fetch 缓存、可以压缩、可以做 diff。

// convert.js —— 在 Node 环境把地图 js 转成 geoJSON 文件 const fs = require('fs'); const path = require('path'); const vm = require('vm'); const SRC_DIR = path.resolve(__dirname, 'map'); const OUT_DIR = path.resolve(__dirname, 'geo'); fs.mkdirSync(OUT_DIR, { recursive: true }); // 造一个假的 echarts 对象,只实现 registerMap const registry = {}; const sandbox = { echarts: { registerMap(name, geoJson) { registry[name] = geoJson; } }, window: {}, // 部分文件会读 window,给个空对象兜住 module: { exports: {} }, exports: {} }; sandbox.self = sandbox.window; vm.createContext(sandbox); for (const file of fs.readdirSync(SRC_DIR)) { if (!file.endsWith('.js')) continue; const code = fs.readFileSync(path.join(SRC_DIR, file), 'utf8'); try { vm.runInContext(code, sandbox); // 执行脚本,触发 registerMap } catch (e) { console.warn(`跳过 ${file}: ${e.message}`); } } // 统一写出,文件名用地图名 for (const [name, geo] of Object.entries(registry)) { const features = geo && geo.features ? geo.features : []; if (!features.length) continue; fs.writeFileSync( path.join(OUT_DIR, `${name}.json`), JSON.stringify({ type: 'FeatureCollection', features }) ); console.log(`${name}: ${features.length} 个区域`); }

vm.runInContext让脚本在一个隔离的沙箱里执行,沙箱里只有一个伪造的echarts,所以脚本调用registerMap时数据就被我们接住了,不会真的去操作 DOM。window、module、exports这几个对象是为了让 UMD 包装的三种分支都能跑通。跑完之后geo目录下就是干净的数据文件,体积通常比 js 小一截,因为省掉了包装代码和多余的转义。

6.2 按需加载与缓存:把 GeoJSON 接回 echarts

转出来之后,加载逻辑要自己写,registerMap由我们手动调。好处是可以加 HTTP 缓存、可以做加载失败重试、可以在注册前做一次数据体检。

const geoCache = new Map(); async function loadGeoMap(name) { if (echarts.getMap(name)) return name; if (geoCache.has(name)) return geoCache.get(name); const task = fetch(`/geo/${name}.json`) .then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); }) .then((geo) => { // 注册前做一次体检,避免脏数据导致空白图 if (!geo.features || !geo.features.length) { throw new Error(`地图数据为空: ${name}`); } echarts.registerMap(name, geo); return name; }) .catch((e) => { geoCache.delete(name); throw e; }); geoCache.set(name, task); return task; }

fetch走的是浏览器缓存策略,配合服务端的Cache-Control可以做到二次进入零等待,比每次插入<script>干净。体检那一步别省,数据管线出问题时,一个features: []的空对象注册进去,页面就是纯白,排查成本远高于在这里抛错。

6.3 上线前的验证清单

改完之后别急着发,我固定跑一遍下面这几项。地图这类东西出错不报异常,靠肉眼看图很容易漏掉个别区域。

验证项方法预期
注册完整性遍历所有地图名调getMap返回对象且features非空
区域数量统计features.length与业务表行政区数量一致
名称对齐数据侧name与地图侧取交集交集覆盖率接近 100%
坐标系抽一个已知坐标的点叠加落点在预期区域内
层级切换下钻两层再返回两次标题、颜色、缩放全部复位

那张表里「名称对齐」一项最值得自动化,我一般写个脚本跑一次比对,把对不上的名字打出来,人工确认是简称问题还是区划调整问题,然后决定加nameMap还是更新数据源。这件事做一次能省掉后面无数次「这个市怎么没颜色」的追问。

血泪经验是:地图数据别当成静态资源一放了之,它和业务数据一样会过期。我现在的习惯是每次区划口径有变动、或者业务方换了一批区域表,就把上面那张验证清单强制走一遍,先跑数据比对再改代码。希望帮到你。

本文还有配套的精品资源,点击获取