服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

uni-app 黑魔法探秘 (一)——重写内置标签

日期: 来源:奇舞精选收集编辑:牧羊

一、背景

政采前端团队的移动端跨端解决方案选择的是 uni-app。跨端方案的好处就是一码多端,即书写一次就可以输出到 web、小程序、Anroid、iOS 等各端。既然是开发,那必然少不了配套的组件库和方法库,而我们公司因为历史原因存在一些的非 uni-app 的项目,vue2 和 vue3 也都在用,而重构的收益又不高,那如何开发一套多个技术栈下都能用的组件库&方法库就成为了我们最大的挑战。

在 uni-app 项目的开发过程中,我和小伙伴们不断为 uni-app 中一些写法感到好奇。譬如如何重写内置标签、类似 c++ 中预处理器指令(https://learn.microsoft.com/zh-cn/cpp/preprocessor/preprocessor-directives?view=msvc-170)的条件编译、为什么 vue 文件中我没加 scoped 也会自动加上命名空间。后续深入了解才发现,uni-app 魔改了 vue 运行时,并且自制了一些 webpack 插件来实现。所以笔者希望通过《uni-app 黑魔法探秘》系列文章,将探索 uni-app、vue、webpack 的心路历程分享给大家,希望通过这一系列文章,最终读者自己也能实现一个简单的跨端框架。

开篇就先从如何重写内置标签讲起。什么是内置标签呢?就是 html 里约定的一些元素(https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element),譬如 div、button 等和 svg 里约定的一些标签譬如 image、view 等。

二、准备工作

先做些准备工作,创建两个项目。 

1)通过 vue-cli 生成一个 vue2 项目,后续重写内置的组件的逻辑放在这个项目里。

vue create vue2-project

2)再通过 vue-cli 生成一个 uni-app 项目,作为对照组。

vue create -p dcloudio/uni-preset-vue uni-app-project

PS:本人 node 版本为 14.19.3,vue-cli 版本为 4.5.13,vue-cli 生成的项目中 vue 版本为 2.7.14

三、重写 html 标签,以 button 标签举例

<button>click me</button>

先让我们看看 uni-app 会把上面的 button 代码转换成什么样:

重写 button 标签有什么难的呢。我直接写一个组件,然后注册不就可以用了吗?

<template>
<uni-button>
<slot />
</uni-button>
</template>

<script>
export default {
name: 'VUniButton'
}
</script>

<template>
<div id="app">
<button>click me!</button>
</div>
</template>

<script>
import UniButton from './components/UniButton.vue'

export default {
name: 'App',
components: {
'button': UniButton,
}
}
</script>

结果是 button 根本没有被编译成 uni-button,并且控制台里能看到明显的错误

通过错误堆栈找到 vue.runtime.esm.js@4946中对应的代码

关键词是 isBuiltInTag、config.isReservedTag,再顺藤摸瓜找到如下源码:

var isBuiltInTag = makeMap('slot,component', true);

var isHTMLTag = makeMap('html,body,base,head,link,meta,style,title,' + 'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' + 'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' + 'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' + 's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' + 'embed,object,param,source,canvas,script,noscript,del,ins,' + 'caption,col,colgroup,table,thead,tbody,td,th,tr,' + 'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' + 'output,progress,select,textarea,' + 'details,dialog,menu,menuitem,summary,' + 'content,element,shadow,template,blockquote,iframe,tfoot');
// this map is intentionally selective, only covering SVG elements that may
// contain child elements.
var isSVG = makeMap('svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,' + 'foreignobject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,' + 'polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view', true);
var isReservedTag = function(tag) {
  return isHTMLTag(tag) || isSVG(tag);
};

可以看到 vue 源码中枚举了 html 标签,这些标签默认不允许被重写。

通过搜索 uni-app 源码发现,isReservedTag 这个方法其实是可以重写的(虽然 vue 文档里没标注)。改写的代码如下:

const overrideTags = ['button']
// 1)先保存原来的 isReservedTag 逻辑
const oldIsReservedTag = Vue.config.isReservedTag;
Vue.config.isReservedTag = function (tag) {
  // 2)在遇到 button 标签的时候,不认为是个内置标签
  if (overrideTags.indexOf(tag) !== -1) {
    return false;
  }
  // 3)非 button 标签再走原来的内置标签的判断逻辑
  return oldIsReservedTag(tag);
};

增加上述代码后并添加样式后, uni-button 已经能成功被渲染出来了

但是控制台中还是有一行报错

还是通过错误的堆栈定位到 vue.runtime.esm.js@6274isUnknownElement这个方法。

然后发现也是可以被重写的,重写逻辑如下

// ignoredElements 默认是 [],所以直接覆盖即可
Vue.config.ignoredElements = [
  'uni-button',
];

到现在为止 button 已经能被正确渲染了,main.js中的代码如下

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 为了解决报错 [Vue warn]: Unknown custom element: <uni-button> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
// 默认是 [],所以直接覆盖即可
Vue.config.ignoredElements = [
  'uni-button',
];

// 为了解决报错 [Vue warn]: Do not use built-in or reserved HTML elements as component id: button
const overrideTags = ['button']
const oldIsReservedTag = Vue.config.isReservedTag;
Vue.config.isReservedTag = function (tag) {
  if (overrideTags.indexOf(tag) !== -1) {
    return false;
  }
  return oldIsReservedTag(tag);
};


new Vue({
  render: h => h(App),
}).$mount('#app')

四、重写 svg 标签,以 image 举例

我们根据上述流程再实现一个 image 组件

<template>
<uni-image>
<img :src="src" >
</uni-image>
</template>

<script>
export default {
name: 'image',
props: {
src: {
type: String,
default: ""
}
},
}
</script>

<style>
uni-image {
width: 320px;
height: 240px;
display: inline-block;
overflow: hidden;
position: relative;
}
</style>

结果发现元素是正确的,但是没有被页面上没有展示。

通过 uni-app 源码(uni-app/lib/h5/ui.js@24)搜索发现其中有这样一段代码,添加后可以正确渲染了

const oldGetTagNamespace = Vue.config.getTagNamespace

const conflictTags = ['switch', 'image', 'text', 'view']

Vue.config.getTagNamespace = function (tag) {
  if (~conflictTags.indexOf(tag)) { // svg 部分标签名称与 uni 标签冲突
    return false
  }
  return oldGetTagNamespace(tag) || false
}

尝试理解一下,在 vue.runtime.esm.js 中搜索关键词 getTagNamespace,最终定位在这一关键行 vue.runtime.esm.js@@2873。有无上述一行代码的区别是 ns 的值为 svg 还是 false

继续找到 getTagNamespace 实现逻辑,发现 vue 会通过 getTagNamespace 这个方法决定创建元素时使用的方法是 createElement 还是 createElementNS。

// vue.runtime.esm.js@6263
function getTagNamespace(tag) {
  if (isSVG(tag)) {
    return 'svg';
  }
  // basic support for MathML
  // note it doesn't support other MathML elements being component roots
  if (tag === 'math') {
    return 'math';
  }
}

// vue.runtime.esm.js@6240
var namespaceMap = {
  svg: 'http://www.w3.org/2000/svg',
  math: 'http://www.w3.org/1998/Math/MathML'
};


// vue.runtime.esm.js@6317
function createElement(tagName, vnode) {
  var elm = document.createElement(tagName);
  if (tagName !== 'select') {
    return elm;
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple');
  }
  return elm;
}
function createElementNS(namespace, tagName) {
  return document.createElementNS(namespaceMap[namespace], tagName);
}

命名空间存在的意义是,一个文档可能包含多个软件模块的元素和属性,在不同软件模块中使用相同名称的元素或属性,可能会导致识别和冲突问题,而 xml 命名空间可以解决该问题。

所以为什么没法渲染的原因显而易见了。因为把一个通过 svg 命名空间的创建出来的元素,挂载在非 svg 标签下。解决方案也很好理解,重写 getTagNamespace 方法,在遇到 image 标签时,认为其不是个 svg 标签,通过 createElement 方法创建在默认命名空间下即可。

最后 main.js中的代码如下

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 下面三条参考 uni-app 源码实现 uni-app/lib/h5/ui.js

// ① 为了解决报错 [Vue warn]: Unknown custom element: <uni-button> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Vue.config.ignoredElements = [
  'uni-button',
  'uni-image',
];

// ② 为了解决报错 [Vue warn]: Do not use built-in or reserved HTML elements as component id: button
const overrideTags = ['button', 'image']
// 1)先保存原来的 isReservedTag 逻辑
const oldIsReservedTag = Vue.config.isReservedTag;
Vue.config.isReservedTag = function (tag) {
  // 2)在遇到 button 标签的时候,不认为是个内置标签
  if (overrideTags.indexOf(tag) !== -1) {
    return false;
  }
  // 3)非 button 标签再走原来的内置标签的判断逻辑
  return oldIsReservedTag(tag);
};

// ③ 为了解决 image 标签虽然被解析,但是渲染不出来的问题
// svg 部分标签名称与 uni 标签冲突
const conflictTags = [
  'image', 
  // 'switch', 
  // 'text', 
  // 'view',
];
const oldGetTagNamespace = Vue.config.getTagNamespace;
Vue.config.getTagNamespace = function (tag) {
  // “~”运算符(位非)用于对一个二进制操作数逐位进行取反操作。位非运算实际上就是对数字进行取负运算,再减 1
  // 等价于 conflictTags.indexOf(tag) > -1
  if (~conflictTags.indexOf(tag)) {
    return false;
  }
  return oldGetTagNamespace(tag);
};

new Vue({
  render: h => h(App),
}).$mount('#app')

五、总结

针对内置标签的解析,很庆幸 vue 还是留了一道后门的,不然就要魔改 vue 的源码了。uni-app 中有很多类似的黑魔法。为什么称之为黑魔法呢,因为其中使用的方法可能是官网中不会讲到的,或者是些需要巧思的。像魔术一样,看似很神奇,真的知道解决方案后就会恍然大悟。我相信这些黑魔法的出现并不是为了炫技,而更多的是在熟悉原理后的决策。

笔者上文的思路是在已知问题(内置标签无法解析)和答案(改写  Vue.config 方法)的前提进行的推理,纵然是这样,也让我颇费功夫,如此一想 uni-app 一个个技术关键点的实现就让我愈发感到敬佩和好奇,这也是我打算执笔写这系列文章的初衷。

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

相关阅读

  • Vue3的语法糖

  • 组合式API:setup()Vue 3 的 Composition API 系列里,推出了一个全新的 setup 函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点。setup是
  • 查理·芒格:顶尖高手决策的7种武器!

  • 导读思维模型会给你提供一种视角或思维框架,从而决定你观察事物和看待世界的视角。顶级的思维模型能提高你成功的可能性,并帮你避免失败。打造多元思维模型想法来自查理·芒格
  • 分享一份2022年全球手游年度数据报告

  • 2022年于移动游戏而言,是充满波折和变化的一年。后疫情时代,全球经济缓慢复苏,数字经济发展趋势凸显,移动游戏行业也成为不少国家和地区视作提振经济的发力点之一。用户增长逐渐
  • (待会删)yyds,付费搞来的,请大家低调浏览!!!

  • 今天想跟大家分享个事儿。我不是有个要好的朋友嘛?最近我发现她天天准点下班。记得有一阵子,她还天天跟我抱怨:“工作量大,每天加班加点才能完成,累得跟狗一样“。刚好前几天她不
  • 就在明天,4时29分

  • ·惊JING ZHE蛰惊蛰3月6日4时29分,我们将迎来惊蛰节气。惊蛰,又名“启蛰”,是二十四节气中的第三个节气。因为避汉景帝刘启的名讳,最终被改为惊蛰。惊蛰反映的是自然生物受节律
  • 读懂政府工作报告|China的一天

  • 春回大地,万物复苏,神州大地处处涌动着实干奋进的热潮。2023年《政府工作报告》出炉,回首过去一年的每一天,中国是怎么度过的?来源:大江网/大江新闻客户端监制:何宝庆统筹:黄铭策划:
  • 今天,东部战区官兵用实际行动向他致敬!

  • 综合各单位来稿今年是毛泽东等老一辈革命家为雷锋同志题词60周年无论时代如何变迁雷锋精神永不过时在第60个“学雷锋纪念日”来临之际东部战区座座军营组织开展形式多样的学
  • 甲流病例激增!现在打疫苗还来得及吗?

  • 近日,“甲流”“奥司他韦”等关键词频登热搜。冬春交际,气候乍暖还寒,昼夜温差起伏,季节性传染病进入了高发和流行期。近日各大医院接诊的甲流等患者增多。多地疾控部门陆续发布

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • uni-app 黑魔法探秘 (一)——重写内置标签

  • 一、背景政采前端团队的移动端跨端解决方案选择的是 uni-app。跨端方案的好处就是一码多端,即书写一次就可以输出到 web、小程序、Anroid、iOS 等各端。既然是开发,那必然少不
  • Vue3的语法糖

  • 组合式API:setup()Vue 3 的 Composition API 系列里,推出了一个全新的 setup 函数,它是一个组件选项,在创建组件之前执行,一旦 props 被解析,并作为组合式 API 的入口点。setup是
  • 奇舞周刊第 484 期 浅谈前端组件设计

  • 记得点击文章末尾的“ 阅读原文 ”查看哟~下面先一起看下本期周刊 摘要 吧~ 奇舞推荐■ ■ ■ 浅谈前端组件设计与仅承担数据处理逻辑的后端不同,前端需要负责界面渲染、数据
  • 查理·芒格:顶尖高手决策的7种武器!

  • 导读思维模型会给你提供一种视角或思维框架,从而决定你观察事物和看待世界的视角。顶级的思维模型能提高你成功的可能性,并帮你避免失败。打造多元思维模型想法来自查理·芒格