整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

字节跳动的微前端沙盒实践

文以字节跳动的微前端项目运行时隔离任务出发,深入讲解 sandbox (后称沙盒)的技术实现,以及上述实际实现过程中一些发生过的问题。把一些关键细节和多年的采坑经验充分的分享给读者,希望能对大家有一些帮助和启发。至于对微前端的整体思考,可以参考:《前端微服务在字节跳动的打磨与应用》,希望读者在读本文之前已经有所了解。



1. 沙盒应该做什么

首先从沙盒对微前端乃至前端的意义开始讲起。沙盒对软件工程来说其概念不算是新鲜事物,仅看前端对隔离的需求也由来已久。并且根据不同的实际业务场景,已经有过非常多和有特色的探索。

古老的 Iframe

一切都从 iframe 开始讲起了,反正是个看上去很美的解决方案。没有真的用过的人肯定都会这样想象。但有些可能等到你要真的搞一个通过 iframe 全面聚合的才知道。单纯的 iframe 聚合非常麻烦,需要很多补漏的劳动量。

旧的 iframe 的方案可以在一定程度上解决了耦合问题。具体是把一个站点页面拆成 N 个 frame,每个 frame 单独跑一个独立的域名。

它的好处非常清楚,独立上下,独立运行,谁都不会妨碍谁。但是是不是这样沙盒就完成了呢?这样算不算沙盒就不好说了,会有很多不同的观点和理论。比如一些观点可能会觉得沙盒不是像这样全独立,而是以隔离模拟独立。后面我们会再次谈到这个观点,并从实现角度分享一些我们的思考成果。

因为一个完整的项目包含大量公用的功能和代码,例如登录身份、站内信,业务模块只是其中的一个部分。这部分完全用跨 window 通信实现起来很费时费力,并且单页应用了 React 或类似的加载技术展示之后,iframe 的效果也逊色很多。想要突破这些限制,困难就很多了。

古老的困难

第一点不用多说各位都会想到 deeplinking 的问题,对吧,至少这点得做到才能算是一个工程,尤其 MVC 时代以来路由一直非常重要。

还有就是各种共享的东西,比如登录怎么共享。iframe 当然也不是不行。和前面后面提到的诸多问题一样都不是不行、而是很麻烦,要针对他去解决很多困难。从效果上讲,最终也完全可以形成一个不错的 iframe 沙盒。

另一显然的困难是,组件库、组件风格的父子传递,以及 React VUE 等渲染引擎的底层代码、内存对象的传递。初步的实现是加入分片打包功能,拆 common chunk 并独立部署 CDN 上,最后在加载时,通过浏览器自身的缓存能力加速访问。但是运行时内存并不共享,对包的运行时修改也难以复用。

还有数据层的设计,数据 Store 等等。数据层至少要有一定的事件打通的功能。搞不好就牵一发动全身,改一个需求,不得不发布四五个项目。

2. 沙盒应该像什么

前面提到过我们不希望沙盒是完全独立的运行时环境,而是有分享和协同的可靠环境。这一章我们希望能说清我们希望沙盒提供怎样的功能、如何使用。

虚拟化、容器化、Docker

到这里开始讲到美好的景物了,docker。从解耦的角度看,服务端的微服务主要是通过 docker 技术实现虚拟化的底层支持,使服务开发者可以体会不到环境的区别、抹平运行时差异的。可以说对微服务来说,docker 是这些年能得到如此的发展的一个基石。

单纯说微服务的概念本身,很久之前也有这个概念的,有面向服务编程的理论。但是发展很少,兑现仍然很困难,搞虚拟机很麻烦。而且还包括开发体验的东西,我打包的镜像——要想交付一致得包含整个操作系统吗?这对开发体验影响很坏。

在 docker 得到普遍应用之前,微服务在服务端的使用主要基于虚机。相比之下使用非常复杂、维护成本提高。虚拟机不说多麻烦了,大家都懂。它吃掉的资源,和容器化技术比完全不在一个量级。还有比如当你想要打个快照连磁盘都吃掉。并且多个服务之间的资源协调和有效分配,实现起来也极端困难。

诸多扩大的成本问题直到随着 docker 的沙盒体系才得以解决。微服务才成为一个趋势。可惜的是这样的容器环境在前端浏览器内的运行时还不存在。

所以这里已经可以看出我们提出的前端沙盒、对它的期望,是让它就像 Docker(而 iframe 在这个比喻里就相当于虚拟机)那样。我们搞的这套机制是像 Docker 的前端运行时容器,像 Docker 一样让前端的拆分能轻松一点、分享容易一点、资源节约一点。当然这不是否定 iframe 的方案。

3. 沙盒应该怎么做

那么我们说的这种沙盒,这种轻量级、强调组件间协同沟通、非常节省资源的沙盒怎么做呢,下面我分 3 个方向分别介绍。(这块还不会有具体的实现,主要从可能性上分析在浏览器里怎么造沙盒。)

3.1 单进程与多进程

参考单核、操作系统进程,模拟进程切换策略。 我们的沙盒实质上在让一个浏览器去跑多个“独立”的应用,那么这里对操作系统的模仿、最终趋同一定避不开。在这个角度上,和其他语言相比 JavaScript 占了一个独特的执行特点的便宜:它自身是单线程的。我怎么做实质上也都是在一个线程内。相当于我们这个操作系统从一开始就限定了单核只有一个出力气。

那么一个操作系统,怎么做多进程并行呢, 单进程可简单通过根路由等等规则控制,每次只激活一个,大家做 context 切换即可;多进程并行就正好可以利用 JavaScript 的特性,我可以封装每个独立的事件循环。比如 setTimeout、各种事件回调的 handler,我们在实际 function 外面先切换 context,再执行你原本希望绑定的 function。这样是线程安全的。总结下来就会是下面两条:

  1. 用路由切换封装,模拟单核单进程。
  2. 用事件循环的总体封装,模拟单核多进程。

3.2 Context 切换

用 context 切换来模拟线程安全, 具体的意思是在每个隔离的子应用“进程”即将开始激活时,先查找当前被激活的、其他的子应用,然后为这个将要退出的应用录制“操作系统”的全现场状态,保存为它的 context。最后为即将激活的新“进程”恢复、新建它自己的 context。

如上面所说,我把当前状态记录为 context,保证每个子应用都适用在自己的 context 内,不影响和改变别人的 context。这个操作全部由托管了子应用的父系统统一来切换。

本次重点讲落地实践,和一些踩坑经验。欢迎大家持续关注,我们会输出更多关于这部分的实践经验。

体现删除 key 必须要遍历两次, 才能保证每个对象都遍历到一遍。这块要强调一个点,当你拿到新旧两个对象比较,遍历其中一个的 key、到另一个里面找,只这样做是不够的,因为有可能又删掉的东西。删掉导致 key 没有了,自然也遍历不到。想体现这个删除,必须要遍历两次,新旧两个对象都遍历一遍,才能知道相比之下谁多了什么谁少了什么。尤其是大家做“空闲”到新开沙盒的比较的时候,特别容易忘了这个细节。

Context 切换的性能够好吗? 先说说这个快照的空间性能。如果你有 N 个沙盒需要有多少切换的组合呢,是不是 context 的全文、或者任意两个沙盒之间的 context 差异都要完整储存?实际上不用。我们只需要记录差别、context 的变化,并且只记录他对 “idle”状态的差别。例如 A、B、C、D、E、F、G。我们不需要记录 A→B A→C 这样的切换,而是虚拟一个空闲状态:O,都是 A→O,B→O,只保存他们和 O 之间的差别。需要记录比较的变量数量从子应用个数的乘法变成了加法。写一个循环就可以快速比较完变化。

综上所述就是让每个子应用的开始和结束、互相切换,都先回到一个虚拟的“初始状态”,恢复现场,再进入被激活的沙盒状态,每次切换仅记录一个沙盒信息。避免了切换算法计算笛卡尔平方积导致比较、和保存沙盒切换信息过大的问题。

4. 字节跳动的沙盒采取的方案

虽然这个章节的名字是字节跳动的实现,并且前面提到这次我们主要分享实现层面的技术细节,但我们不会仅仅探讨和分享自己落地采用的方案,探讨的内容也会包括研究过和对比过的。如果我们认为有好的、适合其他场景的技术方案,我们也都尽量分享出来。

4.1 CSS 沙盒

先说 CSS 沙盒。 这块 webComponent 已经做了很多发展了很多了。这里忍不住要说,web 标准里一度有一个非常吸引我、让我感觉非常有意思的内容是 scoped css——就是加个 Attribute 就能结合 DOM 树限制 CSS 作用范围。后来这个标准被取消了。因为让路给了 ShadowDOM 体系。

这个我不是很理解:因为 scoped CSS 是外面的规则能进来、里面的规则不出去,但 shadowDOM 是完全的割裂。这个巨大的区别使得它们的工程意义相差甚远。后面我们会说到 css module,它的表现显然和 scoped style 一样,和 Shadow 不一样。

CSS module 和 CSS in JS 都是把样式写成或编译成脚本,同时把脚本生成的 DOM 的最外面一层加一个 nounce 的 attribute;然后再给所有受控的 CSS 规则都套上这个 "attribute"。缺点是相对麻烦了一点、并且要完全控制掌握所有 DOM 创建。在前端框架里,Angular 这样做很自然。

后面还会提到这方面最流行的 NPM 包有个好玩的 feature 可能会造成不小心的 bug。

我们采用的是 DOM 沙盒保护 head 内标签。 这样的 style 和 link 本身都可以受到沙盒统一保护。在实际应用中我们的子应用开发者在业务组件里也有用 CSS module 的,我们也不用管——反正去掉标签这个事情最安全。

DOM 沙盒就看管好某个 DOM 标签,谁要改了,沙盒切换时改回来。对绝大多数情况绑定的 style 和 link 标签都有效。但这个只限于单进程的情景。

如果如前面提到的多进程的情况(就是理解成同时有 N 个沙盒在一起运行、并行的系统)。那 CSS 肯定不能和 JavaScript 的单线程运行时一样那么搞,所以一定要用 moduled CSS。也不难搞,很多开源库可以用。即使出现大家引用同一个组件库的不同版本、各自 hack 过、失手创造了什么“幺蛾子”也不用怕,因为他们都是编译好、作用域有限了的。

用 NPM 上的 styled-component 包时要小心, 他们会根据环境变量判断环境;然后对 prod 环境启用一个叫“speedy”的模式,它将不用 innerText 写样式规则,而是用 addRules 那一整套 API。但是这套标准似乎没明确界定这个标签被从文档 DOM 树里移除时的行为和表现,也许因为显而易见 rules 也应当一起移除。但我们插回来时,这种含糊就乱套了。浏览器实际的表现是移除再插回的标签 rules 都没了。这里显然需要我们额外处理。

4.2 全局变量沙盒

另一个重要的是全局变量干扰问题。 Polyfill 等运行环境相关的全局对象、环境变量等具体实现上有非常大的差别,又全部作用于全局。它们对子应用、模块化的子组件来说,又属于自身全局外部的环境。

这块是微前端实施的一大重点。我个人觉得是这样的。可以看出来大家都不是很信。“谁不知道不要写全局变量啊?不会有这么不靠谱的人”。事实上真的试过才会发现会有好多。例如头条号里面用到了某个剪裁图片的插件库。他是个非常完善、正派、和古典的包,同时支持 React 和 JQuery。它给全局写了一个单例的实现。并且在开发调试过程中我们不同业务线的团队就真的用了这个包的不同版本。

当然这个不重要,也没有造成问题。一个比较严峻的例子是这个—— reGeneratorRuntime。它是编译 async 语法用的,在某个 config 下的 Babel 会 delete 这个对象。到底是啥原理不清楚也不需要多讲,但是非常肯定会冲突并造成问题。曾经我们的西瓜号团队的 polyfill 规则和另一个业务线就发生了这类冲突。所以要比较 delete,恢复删除,切换回去西瓜再删掉。

Identifier 是另一个关注点。 你是否完全清楚 Identifier 是什么?Identifier 就是在某个 scope 下起作用的变量名啊什么的,包括 function,let,class,const。只有 var 出来的东西特殊一些、不会占用 Identifier,以上几个会,占用后不可以重复用。

这些东西首先你遍历不了,没有枚举器;其次他们不是某个对象的成员,而仅是编译层面的名字。一旦产生了绝对删不掉。

在全局作用域下 var a 的时候,实际上是生成了一个超范围的 Identifier 并且额外在 global 上创建一个同名的 key,指向同一个地址。这是 var 语句额外的操作。这让我们可以用遍历 window 的方式来处置全局变量。

但如果你来个 const 就没办法了,没有任何办法。 class 也一样。没办法枚举也没办法删除。最多就是再用 class 关键字声明一下覆盖掉。

总之这个事不要多想,new function 包起来几乎必不可少。还可以传入如 setTimeout 这种入参,用来控制异步实现“多进程”并行。

还有个 location 不要挪, 会刷新页面。黑名单掉它。

还有个好玩的事:function 和 var 一样会额外在 window 上增加个 key。这个 property 的 configurable 是 false——也就是不能删除。但是可以赋值。

所以如果你如果光 var a,就可以 delete window.a;再写 a 就是 undefined。写个 function a,再写个 delete a 就无效。 但如果你写个 function a,再写个 var a = 1。啥效果呢,你给 window 上绑了个删不掉的数字,延续了 function a 的不可删除属性和 var a 的值。

更好玩的是 class,你 class B {} ,再 console log window.B,咋么样,undefind。再写 B = 1;然后再看 window.B 怎么样?继续 undefined 了, B = 1 没有效果。

说明潜在的某个机制在 class 关键字执行的时候,给 global 绑上了个叫 B 的 property,但是是个无法枚举和访问的 property,property 有 writable true, enumerable: true, configurable: true 之外的隐藏属性。

4.3 其他

还有好多需要进程安全的对象, 比如 cookie,但这个其实不特别重要,简单约定一个使用 path 就可以了——cookie 除了设置 domain 还可以设置 path。只不过大部分人都不设它(也就是设为根目录“/”)。

localStorage 可以也保护一下。 取决于你的业务。因为这些都属于 windows 的全局变量,所以实现一个包装过的 class 集成并模拟 localStorage 原本的行为就可以。让它所有方法都先给 key 加 prefix,再执行方法的 super。这个 prefix 可以简单地写死当前沙盒的 uuid 即可,因为 window.localStorage 作为全局变量本身就在沙盒保护之内。

5. 沙盒的其他功能

下面是最后一章节,会讲沙盒下有些特殊的东西,它们都需要额外处理。其中重点说一下埋点。多数微前端项目一个页面里的埋点已经属于不同项目了,这块就得搞清楚具体什么子应用、用的什么统计代码、需要处理哪些级别的缓存。

5.1 埋点缓存系统

像前面说的,把 Storage 缓存全部用沙盒包装过,对埋点体系来说还不算完。绝大多数埋点系统的事件发送都是异步、找网络空闲的。并且这些源码通常又在 SDK 里面、不在父工程直接控制的代码里。所以可操作余地不多。实际上只能是把缓存数据、项目信息都好好保存好,再把收集数据的缓存和产生数据时的沙盒状态对应起来。

5.2 console

沙盒可能会包一层或者多层运行时,所以 console 读会比较累。开发的时候可以额外处理一下,为开发者提供便利。 而且现在的前端项目,线上希望 console 打印越少越好、内容越正规越好,并且调试又非常忌讳别人遗留的打印干扰。这些都是沙盒可以做的。我们甚至做了把内容直接对接到采集系统里的上传 log。

具体来说,为 log 注入 callstack 是用 new Error 的方式。这样可以通过 error.stack 拿到调用堆栈。这个值直接是个字符串、是换行分割的 Markdown,可以写链接进去,也能对应到调试窗口的 source code。

同理的是当遇到真的 exception 也应当如此管控。从 catch 到异常、再次 throw 出去之前,可以给 error.stack 值全都 hack 掉。去掉不必要的 stack——比如你包的那层 new Function,删掉那一行。提示的内容也可以改。

5.3 sourceMapping

sourceMapping 是谷歌 closure 发明的一个、现在成为 ES6 标准的东西。原理是一个字符位置到字符位置的映射。那么在 new Function 下的沙盒里能不能用呢?当然可以。

我们先说说 new Function 的表现。在 chrome 里它在调试中是一个新的匿名环境:anonymous,字符行列位置就从函数字符串开头第一行开始算起。如果你是把编译并且生成了 sourceMap 后的 bundle 放到 new function 里执行,这个位置是完全对应的,不需要做任何额外的 hack。

这同时是因为 chrome 能正常识别 new Function 参数字符串末尾的 sourcemappingUrl= 的注释。对应在 callstack 里一切都对。有好多时候我们发现业务方会不放心这个事,会觉得我直接下载的 .js 被包了,不放心调试的 call stack 和 sourceMap。事实上没问题。这两方面都没问题。

另外我们之前其他场合分享也提到了,我们认为微前端的诸多必备条件,其中一个是要有一个服务发现资源和版本管理平台,管理微前端的独立发布、上下线和组合测试等等问题的。这样顺便也给了我们一个条件,可以给 sourceMap 管理起来。

结语

上面就是本次分享的全部,分享了关于微前端沙盒字节跳动两年来使用和实现的经验,以及面对这些挑战时的思考过程。非常幸运我们的项目有足够多给力的伙伴们支持,最终获得了比较大的成功,也非常明显地提升了重量级的产品的质量。

微前端和很多前沿和刚刚发展的概念一样,本身还在快速的演进和验证的过程中,我们的具体实践也一直在快速的变化,在不断地发现弱点和纠正它们,也在努力发展更多的可能。在这个从种种不完美到更完美的奋斗过程中,能给读者分享我们的成果是我们的一种荣幸。而且在分享后,如果能收到指教、讨论和建议我们会更加感激,并且非常欢迎。也欢迎更多的有识之士加入我们,联系请发往邮箱 aishiguang@bytedance.com 或关注 内推链接。

前端开发(Vue)

一、微前端概述

1. 什么是微前端?

  为了解决庞大的一整块后端服务带来的变更与扩展方面的限制,出现了微服务架构。然而,越来越重的前端工程也面临同样的问题,自然地想到了将微服务思想应用(照搬)到前端,于是有了“微前端(micro-frontends)”的概念。即,一种由独立交付的多个前端应用组成整体的架构风格。具体的,将前端应用分解成一些更小、更简单的能够独立开发、测试、部署的小块,而在用户看来仍然是内聚的单个产品。

  我们常见后台项目通常长这样:

1

  如果我们的项目需要开发某个新的功能,而这个功能另一个项目已经开发好,我们想直接复用时。

  说明:我们需要的只是别人项目的这个功能页面的内容部分,不需要别人项目的顶部导航和菜单。

一个比较笨的办法就是直接把别人项目这个页面的代码拷贝过来,但是万一别人不是 vue 开发的,或者说vue 版本、UI 库等不同,以及别人的页面加载之前操作(路由拦截,鉴权等)我们都需要拷贝过来,更重要的问题是,别人代码有更新,我们如何做到同步更新。甚至当别的项目采用其它技术栈时,如何集成?显然复制代码是行不通的。

  以前端组件的概念作类比,我们可以把每个被拆分出的子应用看作是一个应用级组件,每个应用级组件专门实现某个特定的业务功能(如商品管理、订单管理等)。这里实际上谈到了微前端拆分的原则:即以业务功能为基本单元。经过拆分后,整个系统的结构也发生了变化:

  如上图所示,左侧是传统大型单页应用的前端架构,所有模块都在一个应用内,由应用本身负责路由管理,是应用分发路由的方式;而右侧是基座模式下的系统架构,各个子应用互不相关,单独运行在不同的服务上,由基座应用根据路由选择加载哪个应用到页面内,是路由分发应用的方式。这种方式使得各个模块的耦合性大大降低,而微前端需要解决的主要问题就是如何拆分和组织这些子应用。

典型的基于vue-router的Vue应用与这种架构存在着很大的相似性:

2. 巨无霸项目的存在的问题

  • 代码越来越多,打包越来越慢,部署升级麻烦,一些插件的升级和公共组件的修改需要考虑的更多,很容易牵一发而动全身。
  • 项目太大,参与人员越多,代码规范比较难管理,代码冲突也频繁。
  • 产品功能齐全,但是客户往往只需要其中的部分功能。剥离不需要的代码后,需要独立制定版本,独立维护,增加人力成本。

  微前端的诞生也是为了解决以上问题:

  •   复用(嵌入)别人的项目页面,但是别人的项目运行在他自己的环境之上。
  •   巨无霸应用拆分成一个个的小项目,这些小项目独立开发部署,又可以自由组合进行售卖。

  使用微前端的好处:

  •   技术栈无关,各个子项目可以自由选择框架,可以自己制定开发规范。
  •   快速打包,独立部署,互不影响,升级简单。
  •   可以很方便的复用已有的功能模块,避免重复开发。

二、常见微前端方案

  目前主流的微前端方案包括以下几个:

  • iframe
  • 基座模式,主要基于路由分发,qiankun和single-spa就是基于这种模式
  • 组合式集成,即单独构建组件,按需加载,类似npm包的形式
  • EMP,主要基于Webpack5 Module Federation
  • Web Components

  iframe:是传统的微前端解决方案,基于iframe标签实现,技术难度低,隔离性和兼容性很好,但是性能和使用体验比较差,多用于集成第三方系统;

  基座模式:主要基于路由分发,即由一个基座应用来监听路由,并按照路由规则来加载不同的应用,以实现应用间解耦;

  组合式集成:把组件单独打包和发布,然后在构建或运行时组合。

  EMP:基于Webpack5 Module Federation,一种去中心化的微前端实现方案,它不仅能很好地隔离应用,还可以轻松实现应用间的资源共享和通信。

  Web Components:是官方提出的组件化方案,它通过对组件进行更高程度的封装,来实现微前端,但是目前兼容性不够好,尚未普及。

  总的来说,iframe主要用于简单并且性能要求不高的第三方系统;组合式集成目前主要用于前端组件化,而不是微前端;基座模式、EMP和Web Components是目前主流的微前端方案。

  目前微前端最常用的有两种解决方案:iframe 方案和 基座模式方案。

1. iframe方案

  iframe 大家都很熟悉,使用简单方便,提供天然的 js/css 隔离,也带来了数据传输的不便,一些数据无法共享(主要是本地存储、全局变量和公共插件),两个项目不同源(跨域)情况下数据传输需要依赖postMessage 。

  iframe 有很多坑,但是大多都有解决的办法:

  1. 页面加载问题

  iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载,阻塞onload 事件。每次点击都需要重新加载,虽然可以采用 display:none 来做缓存,但是页面缓存过多会导致电脑卡顿。(无法解决)

  2. 布局问题

  iframe 必须给一个指定的高度,否则会塌陷。

  解决办法:子项目实时计算高度并通过postMessage 发送给主页面,主页面动态设置 iframe高度。有些情况会出现多个滚动条,用户体验不佳。

  3. 弹窗及遮罩层的问题

  弹窗只能在 iframe 范围内垂直水平居中,没法在整个页面垂直水平居中。

  解决办法1:通过与框架页面消息同步解决,将弹窗消息发送给主页面,主页面来弹窗,对原项目改动大且影响原项目的使用。

  解决办法2:修改弹窗的样式:隐藏遮罩层,修改弹窗的位置。

  4. iframe 内的 div 无法全屏

  弹窗的全屏,指的是在浏览器可视区全屏。这个全屏指的是占满用户的屏幕。

  全屏方案,原生方法使用的是Element.requestFullscreen(),插件:vue-fullscreen。当页面在 iframe 里面时,全屏会报错,且dom 结构错乱。

  解决方案:iframe 标签设置 allow="fullscreen" 属性即可

  5. 浏览器前进/后退问题

  iframe 和主页面共用一个浏览历史,iframe 会影响页面的前进后退。大部分时候正常,iframe 多次重定向则会导致浏览器的前进后退功能无法正常使用。并且 iframe 页面刷新会重置(比如说从列表页跳转到详情页,然后刷新,会返回到列表页),因为浏览器的地址栏没有变化,iframe 的 src 也没有变化。

  6. iframe 加载失败的情况不好处理

  非同源的 iframe 在火狐及 chorme 都不支持onerror 事件。

  解决办法1:onload 事件里面判断页面的标题,是否 404 或者 500

  解决办法2:使用 try catch 解决此问题,尝试获取 contentDocument 时将抛出异常。

2. 基座模式方案

  基座模式方案以single-spa和qiankun为代表,这里我选择qiankun。

  qiankun 是蚂蚁金服开源的一款框架,它是基于single-spa 的。他在 single-spa 的基础上,实现了开箱即用,除一些必要的修改外,子项目只需要做很少的改动,就能很容易的接入。

  qiankun框架官网:https://qiankun.umijs.org/zh/。

  微前端中子项目的入口文件常见的有两种方式:JS entry 和 HTML entry。纯 single-spa 采用的是 JS entry,而 qiankun 既支持 JS entry,又支持 HTML entry。

  JS entry 的要求比较苛刻:

  (1)将 css 打包到 js 里面

  (2)去掉 chunk-vendors.js,

  (3)去掉文件名的 hash 值

  (4)将 single-spa 模式的入口文件( app.js )放置到 index.html 目录,其他文件不变,原因是要截取app.js 的路径作为 publicPath。

  建议使用 HTML entry ,使用起来和 iframe 一样简单,但是用户体验比 iframe 强很多。qiankun 请求到子项目的 index.html 之后,会先用正则匹配到其中的 js/css 相关标签,然后替换掉,它需要自己加载 js并运行,然后去掉 html/head/body 等标签,剩下的内容原样插入到子项目的容器中 。

二、微前端方案实践

  以“大数据分析”项目为例,将客户特有的需求,如“电子路单”、“数据填报”单独提取为可独立运行的子项目。

  大数据分析项目改造为主应用基座,代码仓库地址:http://192.168.1.102/zouqiongjun/big-data-web.git。

  客户自定义需求单独作为子应用项目,代码仓库地址:http://192.168.1.102/zouqiongjun/zibo-custom-web.git。

1. 各应用工程代码结构

  sass-base-web:主仓库,主要存放一些批量操作的脚本,用于聚合管理仓库和一键编译、一键部署。

  仓库代码结构如下图所示:

  big-data-web:大数据分析主应用

  zibo-custom-web:客户自定义需求,微应用仓库

  子应用可以独立运行,但是当前子应用是直接嵌套在主应用的main内容区域,所以暂时并没有单独提供左侧菜单导航,后续如有需要可以扩展和补充此功能。

2. 主应用big-data-web改造

  将普通的项目改造成 qiankun 主应用基座,需要进行三步操作:

  (1) 创建微应用容器 - 用于承载微应用,渲染显示微应用;

  (2) 注册微应用 - 设置微应用激活条件,微应用地址等等;

  (3) 启动 qiankun;

  注意:由于big-data-web主应用的路由采用的是hash模式,所以子应用的路由也应该采用hash模式。

1.1 安装 qiankun

$ yarn add qiankun # 或者 npm i qiankun -S

1.2. 在主应用中注册微应用

  为了使用keepAlive缓存,这里我们采用手动加载微应用的方式。

  当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有activeRule 规则匹配上的微应用就会被插入到指定的container 中,同时依次调用微应用暴露出的生命周期钩子。

  在views目录下,新建AppVueHash.vue,作为子应用的容器,代码如下:

<template>
<div class="zibo-custom-web">
     <div id="zibo-custom-web" class="app-view-box"></div>
</div>
</template>
<script>
export default {};
</script>
<style lang="scss" scoped>
.zibo-custom-web{
  position: relative;
}
</style>

  这个id属性要唯一,最终子应用的内容将会挂载在这里。

  ContainerOther.vue代码改造:

  <!-- 主体视图层 -->
        <div class="avue-view-contain" v-show="!isSearch">
          <keep-alive>
            <router-view
              class="avue-view keep-alive"
              v-if="$route.meta.keepAlive && isActiveRoute"
              v-show="!showAppVueHash"
            />
          </keep-alive>
          <router-view
            class="avue-view"
            v-if="!$route.meta.keepAlive && isActiveRoute"
            v-show="!showAppVueHash"
          />
          <AppVueHash v-show="showAppVueHash" />
        </div>

js代码:

import router from "@/router/router";
import store from "@/store";
import AppVueHash from "@/views/AppVueHash.vue";
import { loadMicroApp } from "qiankun";
//子项目路由前缀
const isChildRoute = path => website.childRoute.some(item => path.startsWith(item));
const apps = [
  {
    name: "/zibo-custom-web",
    entry: window.configs.VUE_APP_ZIBO_CUSTOM_URL,
    container: "#zibo-custom-web",
    props: { data: { store, router } },
    sandbox: {
      strictStyleIsolation: true // 开启样式隔离
    }
  }
];
//控制微应用手动加载
    ctrlMicroApp(path){
       if (isChildRoute(path)) {
        this.showAppVueHash = true;
        this.$nextTick(() => {
          //手动加载
          if(!this.mounted){
            this.loadApps = apps.map(item => loadMicroApp(item))
            this.mounted=true;
          }  
        });
      } else {
        this.showAppVueHash = false;
      }

  这里的container属性值,必须和AppVueHash.vue组件中的id值保持一致。

  根据url地址判断是否是子应用,如果是子应用,则手动加载,否则隐藏子应用容器,只加载主应用的router-view。

  在ContainerOther第一次加载或路由变化时手动加载微应用:

mounted() {
    this.init();
    setTheme(this.themeName);
    this.ctrlMicroApp(this.$route.path)
  },
  watch: {
    $route(val) {
      this.ctrlMicroApp(val.path);
},
    tagList(newVal,oldVal){
      let starts='';
      const childRoute = website.childRoute;
      childRoute.forEach((n,i)=>{
        if(i<childRoute.length-1){
          starts+=`^${n}|`;
        }else{
          starts+=`^${n}`;
        }
      })
      const patt = new RegExp(`${starts}`);
      //之前存在子应用页签
      const before = oldVal.some(item=>{
        return patt.test(item.value);
      });
      //现在存在子应用页签
      const now = newVal.some(item=>{
        return patt.test(item.value);
      });
      if(before && !now){
        this.mounted=false;
        this.loadApps.forEach(app=>{
          app.unmount();  
        })
      }
    } 

  监听tab页签变化,关闭页签时,需要卸载子应用。

3. qiankun 子项目zibo-custom-web

  1. 在 src 目录新增文件 public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
  1. 修改 index.html 中项目初始化的容器,不要使用 #app ,避免与其他的项目冲突,建议小驼峰写法
    <div id="appVueHash"></div>
  1. 修改入口文件 main.js:
// -----------子应用微前端start-------------------
let router = null;
let instance = null;

function render({ data = {} , container } = {}) {
  router = new VueRouter({
    routes,
  });
  instance = new Vue({
    router,
    store,
    data(){
      return {
        parentRouter: data.router,
        parentVuex: data.store,
      }
    },
    render: h => h(App),
  }).$mount(container ? container.querySelector('#appVueHash') : '#appVueHash');
}
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
//测试全局变量污染
console.log('window.a',window.a)
export async function bootstrap() {
  console.log('vue app bootstraped');
}
export async function mount(props) {
  console.log('props from main framework', props.data);
  render(props);
}
export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = "";
  instance = null;
  router = null;
}
// -----------子应用微前端end-------------------

  主要改动是:引入修改 publicPath 的文件和export 三个生命周期。

  注意:

  • webpack 的 publicPath 值只能在入口文件修改,之所以单独写到一个文件并在入口文件最开始引入,是因为这样做可以让下面所有的代码都能使用这个。
  • 路由文件需要 export 路由数据,而不是实例化的路由对象,路由的钩子函数也需要移到入口文件。
  • 在 mount 生命周期,可以拿到父项目传递过来的数据,router 用于跳转到主项目/其他子项目的路由,store 是父项目的实例化的 Vuex(也可以传递其他数据过来)。
  1. 修改打包配置 vue.config.js:
const { name } = require("./package");
module.exports = {
  outputDir: "../sass-base-web/cicd-config/test/zibo-custom-web",
  devServer: {
    port: 9010,
    // 关闭主机检查,使微应用可以被 fetch
    disableHostCheck: true,
    // 配置跨域请求头,解决开发环境的跨域问题
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
    proxy: {
      "/api": {
        //本地服务接口地址
        target: "http://192.168.10.112:10067", //cas
        ws: true,
        pathRewrite: {
          "^/api": "/",
        },
      },
    },
  },
   // 以下配置可以修复一些字体文件加载路径问题
  chainWebpack: (config) => {
    //忽略的打包文件
    config.externals({
      vue: "Vue",
      "vue-router": "VueRouter",
      vuex: "Vuex",
      axios: "axios",
      "element-ui": "ELEMENT",
    });
    config
      .plugin('html')
      .tap(args => {
        args[0].name = name;
        return args
      });
    config.module
      .rule("fonts")
      .test(/.(ttf|otf|eot|woff|woff2)$/)
      .use("url-loader")
      .loader("url-loader")
      .tap((options) => ({ name: "/fonts/[name].[hash:8].[ext]" }))
      .end();
  },
  // 自定义webpack配置
  configureWebpack: {
    output: {
      library: `${name}-[name]`, // 微应用的包名,这里与主应用中注册的微应用名称一致
      libraryTarget: "umd", // 把子应用打包成 umd 库格式
      jsonpFunction: `webpackJsonp_${name}`, //webpack打包之后保存在window中的key,各个子应用不一致
    },
  },
};

  这里主要就两个配置,一个是允许跨域,另一个是打包成 umd 格式。为什么要打包成 umd 格式呢?是为了让 qiankun 拿到其 export 的生命周期函数。

  注意: 这个 name 默认从 package.json 获取,可以自定义,只要和父项目注册时的 name 保持一致即可。

  vue.config.js中

  outputDir: "../sass-base-web/cicd-config/test/zibo-custom-web",

  这里我子应用项目编译后会将打包文件打包到sass-base-web项目中的zibo-custom-web下。

  1. 路由动态加载

  需要给子项目所有的路由都添加一个前缀,子项目的路由跳转如果之前使用的是 path 也需要修改,用name 跳转则不用。

  avue-router.js:

 const oRouter = {
          path: "/zibo-custom-web",
          name: "RouterView",
          component(resolve) {
            require(["@/components/RouterView.vue"], resolve);
          },

          children: [
            {
              path: path,
              component(resolve) {
                require([`../${component}.vue`], resolve);
              },
              name: name,
              meta: meta,
            },
          ],
        };
        aRouter.push(oRouter);

4. 状态管理,主应用和微应用之间的通信

  qiankun 通过 initGlobalState: 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过props 获取通信方法;

  onGlobalStateChange: 在当前应用监听全局状态,有变更触发 callback;

  setGlobalState: 按一级属性设置全局状态,微应用中只能修改已存在的一级属性; 换句话说只能修改主用于预先定义的属性,后面添加的属性无效。

  官方例子:发布-订阅的设计模式:

  主应用:

import { initGlobalState, MicroAppStateActions } from 'qiankun';
// 初始化 state
const actions: MicroAppStateActions = initGlobalState(state);

actions.onGlobalStateChange((state, prev) => {
  // state: 变更后的状态; prev 变更前的状态
  console.log(state, prev);
});
actions.setGlobalState(state);
actions.offGlobalStateChange();

  子应用:

// 从生命周期 mount 中获取通信方法,使用方式和 master 一致
export function mount(props) {
    props.onGlobalStateChange((state, prev) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log(state, prev);
    });
    props.setGlobalState(state);
  }

  如果主应用和子应用都是vue技术栈,可以在子应用中把数据传递给子应用,例如store。

props: { data: { store, router } },

5. 各应用之间的独立仓库以及聚合管理

  实际开发中项目存储在公司仓库中,以 gitLab 为例, 当子应用一多,全部放在一个仓库下面, 这时候就显得很臃肿了,也很庞大,大大的增加了维护成本,和开发效率;

我们可以通过 sh 脚本, 初始只需要克隆主仓库代码, 然后通过 sh 脚本去一键拉取所有子应用代码。

  这里我将单独创建一个用来编译和打包的主仓库项目sass-base-web,仓库地址:http://192.168.1.102/zouqiongjun/sass-base-web.git。

  项目根目录下,新建 script/clone-all.sh 文件,内容如下:

# 子服务 gitLab 地址
SUB_SERVICE_GIT=('http://192.168.1.102/zouqiongjun/big-data-web.git' 'http://192.168.1.102/zouqiongjun/zibo-custom-web.git')
SUB_SERVICE_NAME=('big-data-web' 'zibo-custom-web')

# 子服务
if [ ! -d "sub-service" ]; then
  echo '创建sub-service目录...'
  mkdir sub-service
fi
echo '进入sub-service目录...'
cd sub-service

# 遍历克隆微服务
for i in ${!SUB_SERVICE_NAME[@]}
do
  if [ ! -d ${SUB_SERVICE_NAME[$i]} ]; then
    echo '克隆微服务项目'${SUB_SERVICE_NAME[$i]}
    git clone ${SUB_SERVICE_GIT[$i]}
  fi
done
 
echo '脚本结束...'
# 克隆完成

  当我们启动主项目的时候,如果想要使用所有子应用的功能,子应用,需要一个一个启动,这样无论是开发还是编译都十分不便。

  考虑到国内使用npm装包可能会由于网络的原因安装失败,可以让npm使用国内淘宝镜像。

  执行命令:npm config set registry https://registry.npm.taobao.org。

  在这个主仓库项目里面,我们只需要安装一个npm-run-all插件。

  运行:yarn add npm-run-all -D或者npm i -D。

  该项目下只有一个package.json文件,用于配置编译和打包命令,代码如下:

{
  "name": "sass-big-data-web",
  "version": "1.0.0",
  "description": "`qiankun`来实现`vue`技术栈的前端微服务",
  "main": "index.js",
  "scripts": {
    "clone:all": "bash ./scripts/clone-all.sh",
    "install:zibo": "cd ./sub-service/zibo-custom-web && npm install",
    "install:main": "cd ./sub-service/big-data-web && npm install",
    "install-all": "npm-run-all install:*",
    "start:zibo": "cd ./sub-service/zibo-custom-web && npm run serve ",",
    "start:main": "cd ./sub-service/big-data-web && npm run serve",
    "start-all": "npm-run-all --parallel start:*",
    "serve-all": "npm-run-all --parallel start:*",
    "build:zibo": "cd ./sub-service/zibo-custom-web && npm run build",
    "build:main": "cd ./sub-service/big-data-web && npm run build",
    "build-all": "npm-run-all --parallel build:*"
  },
  "repository": {
    "type": "git",
    "url": "http://192.168.1.102/zouqiongjun/sass-big-data-web.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "npm-run-all": "^4.1.5"
  }
}

  要执行yarn clone:all,需要用到bash,跳转到sass-base-web目录,右键鼠标打开Git bash窗口,如下图所示:

  这样当我们把各个代码仓库的代码都拉取到sub-service这个目录下面来,如下图所示:

  代码拉取完成后, 紧接着就是下载各个项目的依赖及运行。

  运行npm run serve-all则可以自动执行package.json中配置的命令,这个命令最终会执行以下三个执行命令:

    "start:zibo": "cd ../zibo-custom-web && npm run serve ",
    "start:main": "cd ../big-data-web && npm run serve",

  这样就不需要我们自己一个一个单独的去运行各个项目了。

  总体运行步骤: 第一步 clone 主应用, 然后依次执行 yarn clone:all --> yarn install-all --> yarn start-all 即可运行整个项目。

  build-all:可以编译整个项目。

  sub-service目录,将其添加到.gitignore当中,因为在sass-base-web项目当中,我们只需要配置和编译及打包用,并不需要真正的将所有子应用的代码都提交到sass-base-web项目中,各子应用都有自己私有的仓库。

6. 子项目开发的一些注意事项

  (1)所有的资源(图片/音视频等)都应该放到src 目录,不要放在 public 或者static。资源放 src 目录,会经过 webpack 处理,能统一注入 publicPath。否则在主项目中会404。

  (2)避免 css 污染。组件内样式的 css-scoped是必须的。

  (3)给 body 、 document 等绑定的事件,请在unmount 周期清除

  (4)谨慎使用 position:fixed

  在父项目中,这个定位未必准确,应尽量避免使用,确有相对于浏览器窗口定位需求,可以用position: sticky,但是会有兼容性问题(IE不支持)。

  常见问题见官网:https://qiankun.umijs.org/zh/faq

7. 部署

1. 常规部署

  主应用和微应用都是独立开发和部署,即它们都属于不同的仓库和服务。

  场景:主应用和微应用部署到同一个服务器(同一个IP和端口)

  如果服务器数量有限,或不能跨域等原因需要把主应用和微应用部署到一起。通常的做法是主应用部署在一级目录,微应用部署在二/三级目录。

  若微应用想部署在非根目录,在微应用打包之前需要做两件事:

  • 必须配置 webpack 构建时的 publicPath 为目录名称,更多信息请看 webpack 官方说明 和vue-cli3 的官方说明。
  • history 路由的微应用需要设置 base ,值为目录名称,用于独立访问时使用。

  部署之后注意三点:

  • activeRule 不能和微应用的真实访问路径一样,否则在主应用页面刷新会直接变成微应用页面。
  • 微应用的真实访问路径就是微应用的 entry,entry 可以为相对路径。
  • 微应用的 entry 路径最后面的 / 不可省略,否则 publicPath 会设置错误,例如子项的访问路径是 http://localhost:8080/app1,那么 entry 就是 http://localhost:8080/app1/。

  通过配置 nginx 端口到目录的转发。须要对外开放子利用对应的端口,将编译好的利用文件放到对应的配置目录。

  跳转到sass-base-web目录,执行npm run build-all,会自动执行所有build:开头的命令:

    "build:zibo": "cd ../zibo-custom-web && npm run build",
    "build:control": "cd ../control-center && npm run build",
    "build:main": "cd ../big-data-web && npm run build",
    "build-all": "npm-run-all --parallel build:*"

  zibo-custom-web目录结构如下图所示:

  这里是子应用和主应用部署在同一台服务器上,且IP和端口相同,nginx不需要额外设置。

  如果子应用和主应用部署在同一台服务器上, 但是端口不同,需要修改vue.config.js中的outputDir,这个是打包编译后代码存放的路径,这个不做配置,默认会将代码编译打包到当前根目录下,并生成一个dist目录用于存放编译后的代码,如下图所示:

修改nginx.conf配置:

 #gzip  on;
      upstream gateway { server 192.168.31.136:32067;}
     # 主应用
        server {
        listen   32043;
        server_name  web;
        root  /dist;
        # 关闭端口重定向
        # port_in_redirect off;
        #charset koi8-r;
        access_log /var/log/nginx/nginx.log;

        location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location ^~/api/ {  
            proxy_read_timeout 600s;
            proxy_set_header Host $host; 
            proxy_set_header X-Real-IP $remote_addr; 
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_buffering off; 
            rewrite ^/api/(.*)$ /$1 break; 
            proxy_pass http://gateway; 
            }
        }

   # 子应用
    server {
        listen   9010;
        server_name  cus_web;
        # 子应用编译后的代码路径
        root  /zibo-custom-web;  
        # 允许跨域
        add_header Access-Control-Allow-Origin *;
        # 关闭端口重定向
        # port_in_redirect off;
        # charset koi8-r;
        access_log /var/log/nginx/nginx.log;
        location ^~/zibo-custom-web/ {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header REMOTE-HOST $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

        location ^~/api/ {  
            proxy_read_timeout 600s;
            proxy_set_header Host $host; 
            proxy_set_header X-Real-IP $remote_addr; 
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_buffering off; 
            rewrite ^/api/(.*)$ /$1 break; 
            proxy_pass http://gateway; 
            }
    }

2. docker nginx 配置

  此处 nginx 主要作用是用于端口目录转发,并配置主应用访问子应用的跨域问题。

  使用 docker 配置部署 nginx:

# docker-compose.yml
version: '3.1'
services:
  nginx:
    restart: always
    image: nginx
    container_name: nginx
    ports:
      - 8888:80
      - 8889:8889
      - 7100:7100
      - 7101:7101
    volumes:
      - /app/volumes/nginx/nginx.conf:/etc/nginx/nginx.conf
      - /app/volumes/nginx/html:/usr/share/nginx/html
      - /app/micro/portal:/app/micro/portal
      - /app/micro/app1:/app/micro/app1
      - /app/micro/app2:/app/micro/app2

  将编译后的主应用以及子应用放到对应的数据卷挂载目录即可,如主应用 /app/micro/portal。  同理,也需要将配置好的 nginx.conf 文件放到指定的数据卷挂载目录,使用 docker-compose up -d 启动即可。

  nginx 端口目录转发配置:

# nginx.conf
user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;

    #gzip  on;
    include /etc/nginx/conf.d/*.conf;
    server {
      listen    8889;
      server_name 192.168.2.192;
      location / {
        root /app/micro/portal;
        index index.html;
        try_files $uri $uri/ /index.html;
      }
    }
    server {
      listen    7100;
      server_name 192.168.2.192;

      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      location / {
        root /app/micro/app1;
        index index.html;    
        try_files $uri $uri/ /index.html;
      }
    }
    server {
      listen    7101;
      server_name 192.168.2.192;
     
      # 配置跨域访问,此处是通配符,严格生产环境的话可以指定为主应用 192.168.2.192:8889
      add_header Access-Control-Allow-Origin *;
      add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
      add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
      
      location / {
        root /app/micro/app2;
        index index.html;   
        try_files $uri $uri/ /index.html;
      }
    }}

  部署到生产,需要修改big-data-web/public/util/config.js中的VUE_APP_ZIBO_CUSTOM_URL配置项:

(function() {
  window.configs = {
    VUE_APP_CASLOGINURL: "http://192.168.1.99:32080/cas", //cas登录地址
    VUE_APP_REDIRECTURL: "http://192.168.51.61:8888/big-data/", //前端部署地址
    VUE_APP_SOCKET: "ws://192.168.31.136:32061", //websocket地址'
    VUE_APP_AMAPURLPREFIX: "https://webapi.amap.com", //高德地图地址
    VUE_APP_ZIBO_CUSTOM_URL:"http://localhost:9010",//自定义微应用地址
    //================================================

    VUE_APP_AMAPKEY: "xxxxxx" //高德key
  };
})();
 

  这里config.js是为了配置文件外置,不需要编译。

8. 总结

  尽管qiankun框架支持各个子应用使用不同的技术框架,但是都需要子应用做相应的改造,而且在其它技术栈中总是会时不时的出现各种难以预料的错误,一旦出现问题,都需要我们去改造代码。所以如果都是vue技术栈,建议使用qiankun做微前端。

  如果是第三方公司的项目接入进来,由于他们的代码不受我们控制,需要酌情考虑是否用iframe。

  参考文献:qiankun 微前端方案实践及总结

生志愿是考生本人升学意愿的真实表达,是投档及录取的重要依据。志愿一定要由考生本人填报,考生须对其所填报的志愿内容和填报操作负全责。

为了帮助考生填报志愿,我们编写“2021年河北省普通高考志愿填报须知”,供考生参考。

一、志愿填报的流程及注意事项

(一)志愿填报时间是如何安排的?

志愿填报期间,每日0时至6时为系统维护时间,不能填报志愿。

(二)志愿填报的流程是什么?

考生志愿填报采取远程网上填报方式进行。

考生须使用IE11、Chrome、Edge浏览器,其他类型及版本不予支持,由于浏览器类型及版本使用不当造成的一切后果由考生自负。在规定时间内,考生可通过河北省教育考试院官方网站(http://www.hebeea.edu.cn),点击相应链接进入普通高校招生志愿填报系统,也可以访问https://gk.hebeea.edu.cn,通过河北省普通高校招生考试信息管理与服务平台进入系统,进行志愿填报。系统暂不支持使用手机、平板电脑等移动终端填报志愿。

具体填报流程如下图所示:

1.考生须使用用户名和密码登录系统,用户名为考生本人的考生号或身份证号,密码为高考报名时考生本人所设置的密码。

2.首次登录时,考生须认真阅读《河北省2021年高考志愿填报承诺书》,并确认承诺。

3.考生在志愿填报规定时间内,点击“填报”按钮,进入“2021年河北省普通高校招生志愿填报表”页面。

4.点击所要填报的批次(段),进入该批次(段)志愿填报页面。依次选择计划性质、科类(填报高校专项计划、高水平运动队、高水平艺术团的考生还须选择获得的资格类型),然后输入院校代号、专业代号(顺序志愿还需选择是否“服从专业调剂”),填报完成后须点击“保存”按钮进行志愿保存。保存时需要输入系统登录密码。保存成功后系统会给出“XXXX批次志愿保存成功!”提示。点击“返回填报表”按钮可返回到“2021年河北省普通高校招生志愿填报表”页面,所报志愿会在“所填志愿”栏中显示出来。

在填报平行志愿(不含对口)时,需要提醒的是:

①志愿填报系统提供了多种调整志愿顺序的方法:清空、插入、上移、下移、删除、调整、拖动。顺序调整完成后,须点击“保存”按钮,否则调整志愿顺序无效。

②志愿填报页面左侧的“志愿导航”栏可以拖动,方便考生快速定位及志愿保存。

5.考生在填报过程中如需修改志愿信息,可在“批次入口选择”栏中点击相应批次(段)名称,进入志愿修改页面,在规定时间内进行志愿修改。在志愿填报截止时间前,考生可多次登录系统进行志愿修改和保存,最终以其网上最后一次修改并保存成功的志愿为准。

6.考生志愿核查无误后,点击“安全退出”按钮安全退出系统。

7.每个阶段志愿填报结束后,系统将关闭3小时(以实际操作时间为准)进行系统维护。系统维护完成并重新开放后,考生可登录志愿填报系统,查看自己所填报的志愿信息。

(三)志愿填报注意事项是什么?

1.志愿填报系统是“一个批次(段)一保存”,而不是全部批次填报后统一保存,考生每批次(段)填报志愿后,必须点击“保存”按钮进行志愿保存,否则所填志愿无效。

2.考生应在规定时间内填报志愿。未在规定时间内完成网上填报志愿的考生视为自动放弃志愿填报。

3.考生应增强自身信息安全意识。志愿填报要由考生本人操作,不要让他人代为操作。请考生不要将密码设置得过于简单,并妥善保管密码,不要将密码告诉他人,以防被他人篡改志愿,影响录取。因考生本人泄漏密码或由他人代为填报而造成的一切后果由考生承担。

4.考生要按照志愿顺序填报平行志愿。在进行志愿保存时,所填志愿序号必须连续,中间不能存在为空情况。例如填报本科批志愿时,始终未填序号1(第一行)的院校及专业,而跳过去直接填了序号2(第二行)的院校及专业,志愿填报系统不予支持。

5.多名考生使用同一台计算机填报志愿时,每位考生填报志愿后一定要点击“安全退出”按钮,并关闭所有显示本人信息的页面,否则,后一位考生所填报的志愿信息可能会覆盖前面考生的志愿信息。同时还要注意,不要同时打开两个或两个以上填报页面,以免发生错误。

6.考生在上网填报之前应先填好纸质《2021年河北省普通高校招生考生志愿填报草表》(届时可从省教育考试院官网下载打印),尽量不要在网上一边思考一边填报。这样既可以提高网上填报志愿的效率和准确度,同时又避免造成网络堵塞。考生登录系统后20分钟内无任何操作,再次进行操作时,系统会进行“考生未登录”提示,或被强制退出系统,且未进行保存过的志愿不会保存。考生须重新登录系统进行填报。

7.考生应尽早上网填报志愿,避免因网络、供电等原因而影响志愿填报。

8.对填报志愿中系统给出填报有误提示,考生应从批次(段)、计划性质、报考科类、院校代号、专业代号、选科要求、性别要求,器种、唱法或舞种要求和本人情况等几方面逐一查找出错原因。如果志愿信息存在错误无法进行确认,请在修改信息后再进行确认,否则可能导致志愿信息无效或不能体现本人最终意愿。

(四)填报志愿时考生忘记登录密码怎么办?

遗忘密码的考生可通过以下途径进行密码重置,重置后的密码为考生身份证号后8位。

1.通过手机微信重置密码。考生使用手机添加“河北省教育考试院”微信公众号,进入密码找回页面,通过人脸识别方式自助进行密码重置。

2.考生使用能够访问互联网并装有摄像头的计算机,登录河北省教育考试院普通高校招生考试信息管理与服务平台,进入志愿填报或考生信息查询系统,然后进入“密码重置”页面,通过人脸识别方式自助进行密码重置。

3.到就近报名点重置密码。考生携带本人身份证到任一报名点,通过读取身份证、人脸识别完成密码重置。

提醒考生注意:密码重置完成后,要及时登录系统修改初始密码。考生务必牢记并妥善保管,避免因密码泄漏造成不良后果。

二、志愿填报前考生需要做哪些准备?

在填报志愿前,要充分了解自己的成绩及所处的位次、招生政策、高校招生章程、招生计划等相关信息,以节省填报时间、提升填报效率,提高填报的准确度。

一是学习相关政策。考生要对当年的高考政策有一个整体的把握,清楚招生录取的相关办法和规定,熟悉各类别的批次设置、志愿设置、投档规则、录取办法以及志愿填报时间和流程。今年是我省高考综合改革落地之年,考生可登录省教育厅、省教育考试院网站,查询《河北省普通高考综合改革政策解读“50问”》(网址:https://mp.weixin.qq.com/s/TqyMxWseXo09TFWhXUHIBw)和《河北省2021年普通高校招生考试和录取工作实施方案解读》(网址:http://www.hebeea.edu.cn/html/ptgk/tzgg/2020/1229-180049-826.html),了解相关政策。

二是查看高校招生计划。招生计划内容包括计划性质、批次、科类、院校、办学类型、专业、选考科目要求、学制、学费、专业备注等信息。考生可通过河北省教育考试院汇编的《2021年河北省普通高等学校招生计划》(物理科目组合/历史科目组合·对口)进行查询。

三是阅读高校招生章程。考生可登录高校官方网站或教育部阳光高考信息公开平台,查阅相关高校2021年招生章程。招生章程主要内容包括:高校全称、校址(涉及分校、校区等须注明),层次(本科、专科),办学类型(如普通或成人高校、公办或民办高校或独立学院、高等专科学校或高等职业技术学校等),招生计划分配的原则和办法,预留计划数及使用原则,专业教学培养使用的外语语种,身体健康状况要求,进档考生录取规则(如对考生加分成绩的使用、投档成绩相同考生的处理、进档考生的专业安排办法等),学费标准,家庭经济困难学生资助政策及有关程序,颁发学历证书的学校名称、证书种类及其他信息,联系电话、网址,以及其他须知等。

四是了解《普通高等学校招生体检工作指导意见》。其中规定了对患有某些疾病或有生理缺陷者不宜报考的专业。考生应根据体检结果,对照体检工作指导意见和有关高校的招生章程,避开自己不宜报考的专业。

五是了解自己所在的位次。高考成绩公布后,省教育考试院将在官网、官微公布当年“河北省普通高校招生各类考生成绩统计表”。届时,考生可进行查看,供填报志愿参考。

六是参考其他资料。包括《全国普通高校在河北招生录取分数分布统计2018—2020》《2021年河北省普通高校招生报考指南》《2021年河北省普通高等学校招生艺术类报考指南》。其中,《全国普通高校在河北招生录取分数分布统计2018—2020》主要内容为近三年全国普通高校在河北省招生录取情况,包括分学校、分专业的录取数、最高分、最低分、平均分、差值及各专业录取分数分布统计;《2021年河北省普通高校招生报考指南》和《2021年河北省普通高等学校招生艺术类报考指南》介绍了我省高考录取的相关政策规定、操作程序和注意事项,并收集整理了近三年高校录取的相关数据和资料,这些资料为考生填报志愿作参考。

七是分析研究相关信息。收集分析相关高校的往年录取数据等多种信息,结合自己的学习兴趣、专业取向和未来发展规划,综合考虑,理性选择。

八是利用好《2021年河北省普通高校招生考生志愿填报草表》,按照批次、计划性质、科类或资格类型等提前进行志愿预选。

三、招生计划是如何编制的?

各高校在我省的招生计划,经其教育主管部门审核、汇总后上报教育部,再由教育部分送给我省。我院按照我省的格式对计划内容进行了编排、汇总,由高校核对确认后统一向社会公布。

2021年我省普通高校招生,普通类、体育类按物理科目组合、历史科目组合分别编制招生计划。艺术类不区分物理科目组合、历史科目组合,统一编制招生计划。对于只招首选科目为历史的考生或只招首选科目为物理的考生的艺术类专业,我省根据院校的要求在招生计划中予以公布。

首选科目为物理的普通类、体育类考生,查询《2021年河北省普通高等学校招生计划》(物理科目组合),首选科目为历史的普通类、体育类考生,查询《2021年河北省普通高等学校招生计划》(历史科目组合·对口)。为便于考生查阅,艺术类专业招生计划在《2021年河北省普通高等学校招生计划》(物理科目组合)和《2021年河北省普通高等学校招生计划》(历史科目组合·对口)中均有编列,但它们是同一计划,并非物理科目组合和历史科目组合各自单独的招生计划数(个别专业单独注明只招首选科目为历史或只招首选科目为物理的考生的除外)。按照招生院校要求,对首选科目有要求的艺术类专业,我省招生计划中相应注明“[只招首选科目为[历史]考生]”或“[只招首选科目为[物理]考生]”,请考生注意查看,不要填报不符合选考科目要求的专业。

对口类招生计划编制原则和方式与往年相同,对口考生查询计划可参考《2021年河北省普通高等学校招生计划》(历史科目组合·对口)。

提醒考生注意:

一是考生在志愿填报系统中所填的院校代号、专业代号等内容,均以《2021年河北省普通高等学校招生计划》(物理科目组合/历史科目组合·对口)所公布的为准。未经省教育考试院公布的招生高校和计划,一律不安排在河北省招生。考生要特别注意《招生计划》中拟报考院校(专业)的计划性质、科类及其所在的录取批次(段),并在志愿填报时正确选择。

二是《招生计划》中部分院校(专业)后注明了院校(专业)的办学地点、学费标准和外语语种、单科成绩等限制条件,也有部分院校没有注明,但是不代表学校没有要求,所以请考生报考前务必查询院校招生章程或向招生院校咨询。在招生过程中,一些院校的计划内容可能有变动,考生在填报志愿前,应了解各招生院校(专业)的具体情况和报考要求。

三是为使考生更加清楚地了解高校,今年招生计划的院校名称后面添加了办学类型。

四、在查阅招生计划时要注意哪些问题?

考生查阅招生计划时,需注意选考科目、办学性质、专业备注内对考生性别、外语语种、身体条件和面试等方面的要求。招生计划展现给考生的是同一院校下多个专业,考生填报顺序志愿时应先填写院校代号,再填写不超过6个专业代号,并根据自身实际决定是否勾选专业服从调剂选项,从而构成1个志愿;考生填报“专业(类)+学校”平行志愿时应先填写院校代号,再填写1个专业代号,从而构成1个志愿。

五、改革后的普通高校招生录取依据有什么变化?

从2021年起,我省普通高校招生依据统一高考和高中学业水平选择性考试成绩,参考综合素质评价进行录取。

六、综合素质评价在录取时如何使用?

普通高中学生综合素质评价作为高校招生录取的参考依据,投档时,我省将考生的综合素质评价情况提供给招生高校。具体使用办法由招生高校在招生章程中予以明确。

七、今年志愿填报和往年相比有什么变化?

一是高校专业增加了选考科目要求。比如,某高校某专业要求考生首选科目为物理,再选科目为化学,只有选科时选择这两个科目的考生才能填报,否则为无效志愿。二是平行志愿批次的志愿单位、数量有所调整。志愿填报由以“学校”为单位,变为以“专业(类)+学校”为单位,即一所院校一个招生专业(类)为一个志愿,不再设专业服从调剂选项。实行平行志愿的批次,普通类最多可填报96个志愿,艺术类、体育类最多可填报70个志愿。

八、院校招生专业对再选科目有几种要求?如何查询?

高校对再选科目要求分为四种:一是不提再选科目要求的,符合首选科目要求的考生均可报考;二是提出1门再选科目要求的,考生必须选考该科目方可报考,如某校某专业对化学科目提出要求,符合首选科目要求且选考化学的考生才能报考;三是提出2门再选科目要求且均须选考的,考生须同时选考这2门科目方可报考,如某校某专业对化学和生物科目提出要求,符合首选科目要求且同时选考化学和生物的考生才能报考;四是提出2门再选科目要求且只需选考其中1门的,考生选考该2门科目中的任意1门即可报考,如某校某专业要求考生选考化学或生物科目,符合首选科目要求且只要再选科目中有化学或生物的考生即可报考。

考生可以通过以下两种途径查询高校相关选考科目要求:

(1)登录各高校官方网站进行查询。

(2)通过《2021年河北省普通高等学校招生计划》进行查询。

九、如何理解“专业(类)+学校”志愿模式变化?

我省将在普通类的本科提前批B段、本科批、专科批,艺术类的本科提前批B段、专科提前批部分专业,体育类的本科提前批B段、专科提前批实行”专业(类)+学校”的平行志愿,与原来以学校为单位的志愿模式相比发生了变化。

一是填报的基本单位发生了变化。志愿填报由以“学校”为单位,变为以“专业(类)+学校”为单位,即一所院校一个招生专业(类)为一个志愿。比如原来志愿模式下,要报考北京大学的数学、物理、化学3个专业,只需填报在北京大学1个志愿单位下即可,新的志愿模式下,则需要填报北京大学+数学、北京大学+物理、北京大学+化学3个志愿单位。

二是志愿数量发生了变化。新的志愿模式下,普通类每次最多可填报96个志愿,艺术类、体育类每次最多可填报70个志愿。

三是增加了选考科目要求。改革前考生填报志愿时,同一个科类,考生基本可以填报拟报考学校的所有专业(有特殊要求的除外)。改革后,考生填报志愿须符合拟报考学校专业的选考科目要求,不符合要求的不能填报。

四是不再设专业服从调剂选项。以前的志愿模式下,部分考生因不服从院校的专业调剂,而被投档院校退档。”专业(类)+学校”模式取消了专业调剂,考生不必担心被调剂到不喜欢的专业,也不用担心出现“因不服从专业调剂而退档”的情况,但需要注意的是,有些专业对身体条件、语种等另做要求,考生即便被投档,高校也可能因考生不符合招生章程中的规定而退档。

下面以普通类本科批志愿设置为例,对比一下改革前和改革后志愿设置的变化:

十、“专业(类)+学校”平行志愿投档原则是什么?

“专业(类)+学校”平行志愿投档原则为“分数优先、遵循志愿、一次投档、不再补档”,将控制线上未录取的有志愿考生,结合高校各专业(类)要求,依据高校调档比例确定投档考生数,普通类按高考文化总成绩(含政策性加分),艺术类、体育类按综合成绩,从高分到低分排序,遵循考生的志愿顺序依次投档,由高校择优录取。

具体地讲,就是先从排在第一位的考生开始,计算机根据该生填报的1、2、3、4、5……等多个平行志愿,从第1个志愿开始依次检索,如果第1个志愿有空额就投档,如已满额,则检索第2个志愿,依此类推。一旦投档,该生后面的志愿即失效。然后再检索排在第二位的考生……直到所有计划满额或者没有符合投档条件的考生为止。如果某一考生填报的所有志愿都不符合投档条件,则不能被投档。

十一、对”专业(类)+学校”平行志愿认识误区有哪些?

一般来说对平行志愿的认识误区有三个:

误区一:认为可以一档多投或多次投档。在同一批(段)考生一次可以填报多个学校专业(类)志愿,但在实际投档过程中,投档机会最多只有一次,也就是说,即使有多个志愿符合投档要求,在这些志愿中,只能投档到排在前面的那一个,而且一旦投档,其余志愿随即失效,不再投档。投档后因某种原因被退档,即使后面还有符合投档条件的志愿,也不再投档。

误区二:认为平行志愿没有先后顺序。对考生个人来说,平行志愿是有先后顺序的。平行志愿的检索顺序就是考生所填报的1、2、3、4、5……志愿顺序,所以考生一定要精心安排志愿顺序,把最心仪的志愿填在靠前的位置。

误区三:认为实行平行志愿后没有了风险。仍然会有风险,主要有以下三种原因:平行志愿间没有拉开梯度。如果考生定位不准确,一味追求最理想的高校和专业,志愿填报过高(比如多个平行院校志愿之间没有拉开梯度),就可能导致所填报的几个志愿全部落空。填报的志愿过少。如果考生没有充分利用选择机会,只填报了较少数量的志愿,就有可能因为高考成绩未达到招生院校的投档线而不能被录取。考生的外语语种、身体条件等因素不符合高校要求的,都有可能造成退档,这些因素由高校根据招生需要确定,并在高校招生章程中公布。投档是以考生成绩和志愿顺序作为依据的,考生一旦投到某个高校的专业,但不符合高校招生章程中该专业的具体要求,就会造成退档。因此考生在填报志愿前一定要认真查看高校招生章程。

十二、填报“专业(类)+学校”平行志愿有哪些建议?

一是认真阅读《2021年河北省普通高等学校招生计划》,包括院校名称、办学性质、专业(专业类)、层次、选考科目要求、学制、学费、计划数等。

二是分析研究相关数据。受“专业(类)+学校”平行志愿投档方式的实施以及高校对选考科目的要求等影响,往年的录取分数和位次将不再具有直观的参考意义,但从先期改革省份情况看,高校往年录取数据仍具有一定参考价值。在填报志愿时,考生要依据自己的成绩和位次,综合考虑相关数据信息,特别是往年招生院校各专业的录取数据,合理选择拟填报的院校和专业。

三是选择确定填报意向。考生首先要根据自己的选考科目、成绩位次、兴趣爱好、身体状况等因素,认真阅读院校的招生章程(详细了解院校招生政策、录取规则、学科优势、专业设置、地理位置等方面信息),详细了解招生院校各专业(类)招生计划(包括选科要求、身体条件要求、学费标准、外语语种要求等),明确自己立志攻读的专业范围和院校范围。

四是认真研读高校的招生章程。要特别注意各专业对考生的要求,如身体条件、外语语种、单科成绩、综合素质评价、选考科目要求等是否符合报考条件。

五是合理安排志愿顺序。考生要把心仪的志愿填在靠前的位置,志愿之间要有合理梯度,不宜全部填报高分段也不必全部填报低分段志愿。考生应尽量填满志愿,多一个志愿,可能就多一分录取机会。

十三、往年的招生录取数据如何查询?

一是登录拟填报院校官方网站进行查询。

二是通过省教育考试院汇编的《全国普通高校在河北招生录取分数分布统计2018—2020》《2021年河北省普通高校招生报考指南》和《2021年河北省普通高等学校招生艺术类报考指南》。这些资料收集整理了近三年高校录取的相关数据和资料。

三是通过河北省高考志愿填报智能参考系统查询。考生凭使用码通过河北省招生考试信息服务网(http://www.hebeeb.com/)登录使用。详见第三十二条。

十四、对口类志愿填报和往年比有变化吗,其录取批次和志愿是怎么设置的?

对口类志愿填报和往年比没有任何变化。仍分为对口本科批和对口专科批两个批次,均设1次集中填报志愿和1次征集志愿,实行以“学校”为单位的平行志愿投档模式,每个志愿可填报5所院校(每所院校设6个专业志愿和1个专业服从调剂选项)。其中,对口本科批随本科提前批B段填报志愿、录取,对口专科批随专科提前批填报志愿、录取。

考生如被上一批次高校录取,不能再参加下一批次高校的录取;如未被上一批次高校录取,其参加下一批次高校录取的机会不受影响。

十五、物理科目组合和历史科目组合各录取批次及志愿是如何设置的?

可分为普通类、艺术类、体育类,分别进行批次设置,实行平行志愿和顺序志愿两种模式。

(一)普通类。录取批次分为本科提前批、本科批、专科提前批、专科批等四个批次。一是本科提前批。分为A、B、C三段,每段为一个独立的小批次,依次进行录取。其中,本科提前批A段包括有政治考察、面试、体检等特殊要求的国家专项计划、军队、公安、司法等本科专业。实行以学校为单位的顺序志愿模式。设1次集中填报志愿和1次征集志愿,每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。本科提前批B段包括除本科提前批A段以外的其他国家专项计划、公费师范生和免费医学定向生等本科专业。实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿。设1次集中填报志愿和1次征集志愿,每次最多可填报96个志愿。本科提前批C段包括高水平运动队、高水平艺术团、高校专项计划、定向就业招生等特殊类型招生本科专业。实行以学校为单位的顺序志愿模式。只设1次集中填报志愿,不进行志愿征集,可填报1所学校,设6个专业志愿和1个专业服从调剂选项。二是本科批。包括未列入本科提前批的普通类本科专业、地方专项计划、本科预科班(包括少数民族预科班、边防军人子女预科班)等。实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿。设1次集中填报志愿和1次征集志愿,每次最多可填报96个志愿。三是专科提前批。包括有面试、体检等特殊要求的公安、司法等专科专业。实行以学校为单位的顺序志愿模式。设1次集中填报志愿和1次征集志愿,每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。四是专科批。包括未列入专科提前批的普通类专科专业。实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿。设1次集中填报志愿和2次征集志愿,每次最多可填报96个志愿。

(二)艺术类。艺术类招生分本科提前批和专科提前批两个批次。每次志愿填报时(集中填报志愿或征集志愿),同一批次只能选择一个科类填报,各科类不能同时兼报。一是本科提前批。分为A、B、C三段,每段为一个独立的小批次,依次进行录取。本科提前批A段包括教育部批准的独立设置艺术高校、参照执行高校的有关艺术类本科专业,原“211工程”高校的艺术类校考本科专业,有特殊要求无法实行平行志愿投档的艺术类统考和校际联考等本科专业。实行以学校为单位的顺序志愿模式。设1次集中填报志愿和1次征集志愿,每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。本科提前批B段包括使用河北省艺术统考或校际联考成绩作为专业成绩进行录取,并执行我省平行志愿统一投档原则的艺术类本科专业。实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿。设1次集中填报志愿和1次征集志愿,每次最多可填报70个志愿。本科提前批C段包括除本科提前批A段、本科提前批B段以外的其他艺术类本科专业。实行以学校为单位的顺序志愿模式。设1次集中填报志愿和1次征集志愿,每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。二是专科提前批。包括艺术类专科专业。分平行志愿和顺序志愿两种模式。其中,使用河北省艺术统考或校际联考成绩作为专业成绩进行录取的,实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿,设1次集中填报志愿和1次征集志愿,每次最多可填报70个志愿;使用艺术校考成绩作为专业成绩进行录取的,实行以学校为单位的顺序志愿模式,设1次集中填报志愿和1次征集志愿,每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。

(三)体育类。体育类招生分本科提前批和专科提前批两个批次。一是本科提前批。分为A、B两段,每段为一个独立的小批次,依次进行录取。本科提前批A段包括河北体育学院少数民族传统体育项目。实行以学校为单位的顺序志愿模式。设1次集中填报志愿和1次征集志愿。本科提前批B段包括使用河北省普通体育类专业考试成绩作为专业成绩进行录取的体育类本科专业。实行以“专业(类)+学校”为单位的平行志愿模式。设1次集中填报志愿和1次征集志愿。一所院校一个招生专业(类)为一个志愿,每次最多可填报70个志愿。二是专科提前批。包括使用河北省普通体育类专业考试成绩作为专业成绩进行录取的体育类专科专业。实行以“专业(类)+学校”为单位的平行志愿模式,一所院校一个招生专业(类)为一个志愿。设1次集中填报志愿和1次征集志愿,每次最多可填报70个志愿。

考生如被上一批次(段)高校录取,不能再参加下一批次(段)高校的录取;如未被上一批次(段)高校录取,其参加下一批次(段)高校录取的机会不受影响。

十六、艺术类、体育类考生的综合成绩是如何构成的?

艺术类、体育类综合成绩的构成:

(1)美术类、音乐类(含声乐类和器乐类)、舞蹈类、书法学、服装表演类:综合成绩=高考文化总成绩(含政策性加分)×0.3+(专业成绩÷专业满分)×750×0.7,结果四舍五入保留3位小数。其中,美术类专业满分300分,音乐类(含声乐类和器乐类)、舞蹈类、书法学专业满分均为200分,服装表演类专业满分100分。

(2)戏剧与影视学类、播音与主持艺术:综合成绩=高考文化总成绩(含政策性加分)×0.7+(专业成绩÷专业满分)×750×0.3,结果四舍五入保留3位小数。戏剧与影视学类、播音与主持艺术专业满分均为200分。

(3)体育类:综合成绩=高考文化总成绩(含政策性加分)×0.3+(专业成绩÷专业满分)×750×0.7,结果四舍五入保留3位小数。体育类专业满分400分。

十七、艺术类、体育类考生可以兼报普通类专业吗?

艺术类和体育类考生,可以兼报普通类专业,但每次志愿填报时(集中填报志愿或征集志愿),同一批次(段)只能选择一个类别填报。

十八、填报军队院校志愿应注意哪些事项?

今年军队院校招生的变化主要有两点,一是考生在公布高考成绩后,于6月26日12时至6月27日12时填报军队院校志愿。二是根据教育部、中央军委政治工作部、中央军委训练管理部《关于做好2021年军队院校招收普通高中毕业生工作的通知》(军训〔2021〕112号)精神,报考军队院校考生高中阶段体质测试成绩需达到及格以上(即普通高中学业水平合格性考试体育与健康科目成绩为“合格”)。应届生体育与健康科目成绩由省中考中心提供,合格考生无需提供相关成绩证明,考生可通过河北省普通中等教育考试服务中心网站(https://www.hebeixk.com)查询本人的体育与健康科目成绩是否合格。往届生持学考成绩合格证复印件,或通过河北省普通中等教育考试服务中心网站(https://www.hebhk.com)打印的含有本人体育与健康科目成绩合格的截图,或毕业学校出具的加盖学校公章的相关成绩合格证明,在考生参加军检时向相应军检站提交。

军队院校招生计划安排在本科提前批A段,实行顺序志愿投档,设1次集中填报志愿和1次征集志愿,考生每次可填报1所学校,每所学校设6个专业志愿和1个专业服从调剂选项。

已完成军队院校(专业)志愿填报的考生,仍可在规定时间填报包括本科提前批A段在内的普通院校(专业)志愿。其中,通过省军区战备建设局组织的政治考核和军检合格的考生,其军队院校(专业)志愿生效,本科提前批A段的普通院校(专业)志愿无效;未通过省军区战备建设局组织的政治考核或军检不合格的考生,其军队院校(专业)志愿无效,本科提前批A段的普通院校(专业)志愿生效。

陆军工程大学安排在本科提前批A段的定向人防计划以及空军军医大学和海军军医大学安排在本科批的普通类招生计划,考生不用参加政治考核和军事职业适应性检测,入学后无军籍,不享受军官学员相关待遇。这两类计划随相应批次普通类专业进行填报,不属于6月26日12时至6月27日12时军队院校志愿填报范围。

具体要求详见军队院校招生相关文件,或及时关注省教育考试院官方网站查询有关招生办法。

十九、填报公安院校志愿应注意哪些事项?

报考公安院校的考生须参加由省公安厅组织的政治考察、面试、体检和体能测评且合格。公安院校招生计划安排在本科提前批A段和专科提前批,实行顺序志愿投档,设1次集中填报志愿和1次征集志愿,每批(段)每次只能填报1所院校,每所学校设6个专业志愿和1个专业服从调剂选项。公安院校的国家专项计划与其他公安类本科专业都编列在本科提前批A段,计划性质均为非定向,这两类计划分开编列,公安类的国家专项计划统一编排在所有普通类公安计划之后,同时具有相应资格的考生只能选择一类填报。

具体要求详见公安院校招生相关文件,或及时关注省教育考试院官方网站查询有关招生办法。

二十、如何填报定向就业招生志愿?

定向就业招生计划单列,实行顺序志愿投档,每个批次(段)只能填报1所院校志愿。解放军陆军工程大学定向人防计划安排在本科提前批A段,清华大学、东北大学秦皇岛分校的定向就业计划安排在本科提前批C段,详见当年招生计划。

二十一、高水平运动队招生如何填报志愿?

报考高水平运动队的考生,须通过招生院校考核合格且经教育部“阳光高考”信息平台公示无异议后方可填报。高水平运动队在本科提前批C段录取,实行顺序志愿,设1次集中填报志愿,不进行志愿征集。可填报1所学校,设6个专业志愿和1个专业服从调剂选项。

二十二、高水平艺术团招生如何填报志愿?

报考高水平艺术团的考生,须通过招生院校考核合格且经教育部“阳光高考”信息平台公示无异议后方可填报。高水平艺术团在本科提前批C段录取,实行顺序志愿,设1次集中填报志愿,不进行志愿征集。可填报1所学校,设6个专业志愿和1个专业服从调剂选项。

二十三、国家专项计划招生如何填报志愿?

国家专项计划志愿安排在本科提前批A段和B段。其中,有政治考察、面试、体检等特殊要求的国家专项计划安排在本科提前批A段,其他国家专项安排在本科提前批B段。通过国家专项计划资格审核的考生方可填报。

本科提前批A段的国家专项计划实行顺序志愿,设集中填报志愿和征集志愿,每次只能填报1所学校,设6个专业志愿和1个专业服从调剂选项。本科提前批B段的国家专项计划实行平行志愿,设集中填报志愿和征集志愿,每次最多可填报96个志愿,与免费医学定向生、公费师范生同批一起填报,同批一起录取。

二十四、高校专项计划招生如何填报志愿?

高校专项计划列在本科提前批C段,实行顺序志愿,设1次集中填报志愿,不进行志愿征集,可填报1所学校,设6个专业志愿和1个专业服从调剂选项。经有关高校考核合格且在教育部“阳光高考”信息平台公示无异议的考生方可填报。

二十五、地方专项计划招生如何填报志愿?

地方专项计划安排在本科批,设集中填报志愿和征集志愿,与其他普通类学校同批一起填报,同批一起录取,每次最多可填报96个志愿。通过地方专项计划资格审核的考生方可填报。

二十六、如何填报公费师范生(含优师专项)志愿?

教育部直属部分师范院校和我省部分师范院校继续安排公费师范生招生。普通类公费师范生安排在本科提前批B段。今年国家在我省新增优师专项,该计划属于公费师范生范畴,是为中西部欠发达地区定向培养教师的专项计划,面向全省招生,分为国家优师专项(部委属师范大学承担)和地方优师专项(河北师范大学和河北民族师范学院承担),面向我省特困片区县和扶贫重点县就业。普通类的优师专项列在普通类本科提前批B段,编列到公费师范生类,单列院校代号,院校名称后单独标注“优师专项”,与公费师范生、免费医学定向生、国家专项计划一起实行平行志愿。除以上普通类公费师范生(含优师专项)招生之外,艺术、体育、高校专项计划等招生类型的公费师范生(含优师专项)安排在相应类别和批次。考生被公费师范生(含优师专项)录取后须签订协议,详情请咨询有关院校。

二十七、如何填报预科班志愿?

预科班招生包括少数民族本科预科班和边防军人子女预科班。含在普通本科批内,设集中填报志愿和征集志愿,与其他普通类学校同批一起填报,同批一起录取,每次最多可填报96个志愿。

其中少数民族本科预科班只招收少数民族考生,边防军人子女预科班只招收通过边防军人子女资格审查的考生。

二十八、强基计划有关注意事项?

报考强基计划的考生无需在我省志愿填报系统填报强基计划志愿,按有关试点高校《招生简章》的要求报考,由高校确定录取结果。我省根据高校确定的本省拟录取考生名单,在提前批录取开始前办理录取备案手续。强基计划考生可参加我省其他各类高考志愿正常填报,如被强基计划录取,将不再参加其他各类型投档和录取工作。

二十九、投档时,考生成绩相同时如何排序?

普通类:当遇到多名考生高考文化总成绩(含政策性加分)相同时,依次按语文数学两门成绩之和、语文数学两门中的单科最高成绩、外语单科成绩、首选科目单科成绩、再选科目单科最高成绩、再选科目单科次高成绩由高到低排序投档;如仍相同,比较考生志愿顺序,顺序在前者优先投档,志愿顺序相同则全部投档,是否录取由高校决定。

军队院校、综合评价招生等有特殊投档要求的,按照有关文件执行。

艺术类、体育类:实行平行志愿模式的批次,当遇到多名考生综合成绩相同时,依次按高考文化总成绩(含政策性加分)、语文数学两门成绩之和、语文数学两门中的单科最高成绩、外语单科成绩、首选科目单科成绩、再选科目单科最高成绩、再选科目单科次高成绩由高到低排序投档;如仍相同,比较考生志愿顺序,顺序在前者优先投档,志愿顺序相同则全部投档,是否录取由高校决定。

三十、填报志愿时考生如何对待优惠加分政策?

对具备加分资格的考生,我省按照加分后的分数投档。具备国家加分项目资格的考生可在其统考成绩总分基础上增加分值投档,由高校审查决定是否录取。具备我省地方加分项目资格的考生,加分范围只适用于我省高校省内招生,在考生文化统考成绩总分基础上增加分值投档,由高校审查决定是否录取。对于同时符合多种加分条件的考生,投档时,报考省外院校的使用国家加分最高一项,报考河北省院校的使用国家加分与省内加分中最高一项。根据教育部有关规定,所有高考加分项目及分值均不得用于高校不安排分省招生计划的艺术类专业、高水平艺术团、高水平运动队、高校专项计划等招生项目。录取时是否考虑加分因素,按院校的有关规定执行。由于部分院校不承认或部分承认优惠加分政策,因此,享有优惠加分政策投档的考生,有可能因实际文化成绩较低而被院校退档。请考生在填报志愿前,务必认真阅读有关院校《招生章程》或向院校咨询。

三十一、志愿填报时间截止后还可以补报志愿吗?

不接受任何形式的补报志愿。志愿填报系统将按照规定的志愿填报时间准时开放和关闭,考生须按照有关操作流程,在规定时间内完成志愿填报。

三十二、什么是河北省高考志愿填报智能参考系统?有哪些功能?

该系统是集专业测试、信息查询、志愿智能选择等功能的一站式辅助咨询服务系统。系统将在成绩公布后开放使用,可为河北省普通高校招生普通类本科专业(不含本科提前批)志愿填报提供参考。考生只需“录信息”“挑专业”“排顺序”“导报表”四步,系统即可给出志愿填报参考建议。考生凭使用码通过河北省招生考试信息服务网(http://www.hebeeb.com/)登录使用。

6月23日后为模拟试用期,模拟数据在系统正式上线后清除。6月25日成绩公布后,系统将正式上线。

技术服务热线:13231165075/15373010897

(河北省教育考试院)