谈一谈 IPA 上传到 App Store Connect 的几种方法
📒博客主页: 开心档博客主页 🎉欢迎关注🔎点赞👍收藏⭐留言📝 📌本文由开心档原创! 📆51CTO首发时间:🌴2022年12月12日🌴 ✉️这世界很喧嚣,做自己就好! 🙏作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!⭐本文介绍⭐1、前言关于上传2、Xcode利用Application Loader 是一款 Apple 工具能够帮助您将 App 的二进制文件上传至 App Store。3、Application Loader当然,Xcode 这种方式,是需要有源代码情况下,才能上传。所以,就会有没有源代码的情况,怎么上传的情况啦!Application Loader 就是这样一种方式:Application Loader 是一款 Apple 工具能够帮助您将 App 的二进制文件上传至 App Store。Application Loader 上传速度快、连接稳定并且具备早期验证警告功能。登陆界面:主界面:其实,如果了解可能的原因,在我看来有几点。第1点是,单独维护这样一个软件,需要人力,因为,如果不依赖于 Xcode,在一台电脑只安装了 Application Loader,那个肯定需要安装 Command Line Tools 这个命令行工具,如果是安装 Xcode 默认也带上,如果更新了 Xcode 版,也会跟随升级,所以,维护 Application Loader 软件,不只是单独的一个应用入口,当然,也是因为这个 Application Loader 做了一些早期验证警告:上传第2点,Application Loader 需要的人并不多,站在开发者环境,大多数开发者负责上传 ipa 包,另外,批量上传内购品项,一定很多人不知道,所以,苹果也去掉了。开发者后台也去掉了,所以,Application Loader 现在是集成在 Xcode 中,说不定,那天就直接去掉。第3点,越来越多的声音,希望苹果能通过 App Store Connect 后台能直接上传 ipa 包、批量创建内购品项等功能。但根据我观察这几年的 WWDC,苹果对 App Store Connect 后台进行了比较大的改变,2022年08月23日就是对 App Store Connect 和 Apple Developer 后台,2个账号体系合并,主线上,还是整个系统性上,对于功能和UI界面上,不知道有没有相关计划。我的猜测,还是有希望的。因为近年来,跨平台开发, React Native/ Weex / Flutter,其实,可以不需要依赖 Xcode、macOS 进行开发,打包上传 ipa 却需要一台 macOS 和 Xcode,有一点不可理解?(当然,也不排除苹果希望大家因此,能多卖出几台 Mac 电脑,也许我的猜测是错的吧,但愿~)具体关于https://help.apple.com/itc/apploader/4、altool您可以使用所以,Application Loader 应用界面下,也是基于 altool 命令来处理 ipa 文件。明白了这点,对于命令行就没有什么问题啦。若要在上传之前验证构建版本或将有效构建版本自动上传至Application Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/对于这点,如果有做过自动化打包、上传发布或所以,若要运行--validate-app),或者上传(--upload-app)操作:$ altool --validate-app -f file -u username [-p password] [--output-format xml]$ altool --upload-app -f file -u username [-p password] [--output-format xml]命令参数说明:您想让 Application Loader 以结构化的 XML 格式还是非结构化的文本格式返回输出信息。默认情况下,Application Loader 以文本格式返回输出信息。5、Transporter也许,对于一般的开发者来说,altool 已经能满足基本的上传 ipa 文件的需求。但是,正好前面说的,如果你需要进行批量创建内购品项,还有其它操作,可能大多数开发者不知道,苹果除了 iOS,还有非常多的服务, iTunes Connect 帐户(图书发行商或音乐提供商)、 iTunes Store、Apple Books,尽管我们中国地区有些服务或者非常少用。所以,苹果提供Transporter 来处理大量和差异化数据的操作的工具(可以在 macOS、Windows 和 Linux 操作系统上安装和运行 Transporter。):Transporter 是 Apple 基于 Java 的命令行工具,用于进行大量目录交付。您可以使用 Transporter 将预生成的内容以 Store 数据包的形式交付至 iTunes Store、Apple Books 和 App Store。不论您使用这里,只会介绍用Transporter 命令来上传 ipa 文件,更多的功能和说明,大家可以查看官方文档(中文):https://help.apple.com/itc/transporteruserguide/注意: 下面命令中的 iTMSTransporter 是一个变量名,【重要事项】 作为一名 App 开发者,您可以在已安装 Xcode 或 Application Loader 的情况下使用 Transporter,或者您也可以手动下载 Transporter。有关如何为 App 开发者安装 Transporter 的信息,请参见。因为我们默认都安装了Transporter 命令,我们引用 Xcode 中的 Application Loader 里的 iTMSTransporter, 在 .bash_profile 添加了一个别名,这样可以在任何目录路径调用 iTMSTransporter 命令:alias iTMSTransporter='`xcode-select --print-path`/../Applications/Application Loader.app/Contents/MacOS/itms/bin/iTMSTransporter' 注:1、其中 xcode-select --print-path:iTMSTransporter。2、当然,也可能通过设置全局环境变量来直接使用命令,添加 TRANSPORTER_HOME 环境变量。要添加 TRANSPORTER_HOME 环境变量,请在您的 .bash_profile 中添加以下行:export TRANSPORTER_HOME=。例如,如果您安装了 Xcode,则该行应如下所示:export TRANSPORTER_HOME=`xcode-select --print-path`/../Applications/Application Loader.app/Contents/MacOS/itms/bin其中,我们除了刚才说的检查和上传模式外,可能会用于这个命令的几种模式,Lookup Metadata 模式检索您之前上传的某个iTMSTransporter -m lookupMetadata -u [user] -p [password] -apple_id(-apple_ids) -destination [output_path]Provider 模式确定您有权限为哪些帐户交付内容。iTMSTransporter -m provider -u [user] -p [password]Verify 模式验证您的iTMSTransporter -m verify -u [user] -p [password] -f [itmsp_path] [-vp <text | json>]Upload 模式检查您的素材和iTMSTransporter -m upload -u [user] -p [password] -f [itmsp_path]关于这些模式的参数,苹果文档有非常详细的说明,虽然需要花一点的脑子去理解(文档真的很~),好了。下面简单说明一下,上传命令怎么使用吧上传iTMSTransporter -m upload -u xxx@xxx.com -p xxx -f /Users/HTC/Desktop/Upload.itmspxxx@xxx.com :App Store Connect 账号xxx :App Store Connect 账号的密码/Users/HTC/Desktop/Upload.itmsp :这个一个目录,Upload.itmsp 是一个文件夹名字,不是文件,里面包含2个文件,一个就是要上传的 ipa 文件,另一个是一个 xml ,描述这个 ipa 文件的信息。ipa_metadata.xml:<?xml versinotallow="1.0" encoding="UTF-8"?><package versinotallow="software5.10" xmlns="http://apple.com/itunes/importer"> <software_assets apple_id="{apple_id}" app_platform="{app_platform}"> <asset type="{archive_type}"> <data_file> <size>{file_size}</size> <file_name>{file_name}</file_name> <checksum type="md5">{file_md5}</checksum> </data_file> </asset> </software_assets></package>需要修改{apple_id} :这个 ipa 文件对应的app的 apple id{app_platform} : app的平台,填写ios{archive_type} :归档类型,填写bundle{file_size} :ipa 文件的大小{file_name} :ipa 文件的名字{file_md5}:一些重要参数说明:总结最后,这就是几种上传ipa包的方法,当然,如果经验丰富的开发者,可能使用过 fastlane 、shenzhen 这样的自动化工具命令,也是可以上传 ipa 文件,如果你研究过它们的源代码,你就会发现,他们使用的命令就是 iTMSTransporter,这也正是,我想写这篇文章的原因。现在大家在开发过程中,一直想提升自己,想进阶,想成为高手,然而找不到途径?我希望,大家不要忽视开发过程的每一个重要的环节,这就是进阶的途径!愿大家都能感悟达到~最后的最后,想说一下最近不怎么更新博客的原因?除了比较忙外(什么时候闲!),写好一篇文章,需要去考查相关的资料和知识,对每一行文字,都要精斟细酌,因为当我看到博客的访问量越来越多人时,为了不误导大家,所以需要承担的责任感觉也大了。这也是写文章的好处吧,除了整理思维,体系构建,表达自己,还有分享,责任,担当中国IT技术传承的一份子,安乐~finally,五一快乐!致敬劳动者!🙏作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
数据驱动 - 学习vue源码系列2
数据驱动 - 学习vue源码系列2决定跟着黄轶老师的 vue2 源码课程好好学习下vue2的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~将vue 的源码clone 到本地,切换到分支2.6。Introduction数据驱动是vue的核心思想。数据的变化,JQuery通过 操作DOM 更新视图,而vue会自动更新视图,解耦 DOM 和数据。本节重点,分析 <div id="app">{{message}}</div>怎么变成<div id="app">hello</div>。切记:分析源码,先做主线任务,然后再是支线任务~~~new Vue 发生了什么来找找,实现这样的功能,都涉及到了哪些文件。从入口文件开始找,Vue 在src/core/instance/index.js定义的:function Vue(options) {
if (!(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
// 这里就执行这一个方法
this._init(options);
}
// 这些都是在原型上挂载相应方法的
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);这里还有个小细节:书写顺序上,虽然this._init在initMixin(Vue);....之前,但实际上,this._init只有new的时候才执行,而new之前,initMixin(Vue);....已经执行,所以,this._init能调用所有原型上的方法。从挂载原型的方法上面,显然可以找到initMixin是定义_init方法的,于是找到init.js:依次各种方法,但是什么才是实现{{message}}变成hello的相关代码呢?小技巧:增加调试想要找到这个问题的答案,其实就是建个 demo,然后引入vue文件,在init的相关代码打上debugger, 发现哪个执行完之后,{{message}}变成hello了,那就找到了相关的代码了!我这边偷懒了点,直接在克隆下来的 vue 库里,建个z.html,引入vue:<div id="app">
<div>{{message}}</div>
</div>
<!-- 这里改成自己的路径 -->
<script src="vue/dist/vue.js"></script>
<script>
new Vue({
el: "#app",
data: {
message: "hello",
},
});
</script>然后搜下_init,在这里打上断点,然后再浏览器里运行z.htmlGot it!这里可以断定vm.$mount是让{{message}}变成hello的关键方法!为什么能 this.message方法已经找到,这里再说另外一个事,为什么data里面定义的属性,能直接this.xx访问呢?在使用一次debugger,这次仔细看下,this里面的属性,开始非常少,然后越来越多:哟西!很快就发现执行initState(vm)之后,this上面就有了message属性,显然这个文件做了相关的功能。在state.js里看看:// initState方法
initData(vm);
// initData方法,这里看到在实例上将data挂载在`this._data`属性上
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
proxy(vm, `_data`, key);
// proxy方法,这里其实做了映射,当访问this.xx的时候,就会访问 this._data.xx
Object.defineProperty(target, key, sharedPropertyDefinition);
Vue 实例挂载的实现挂载的实现,重点就是this.$mount方法的实现。寻找特定方法的定义,src全局搜索下$mount发现定义$mount有三处:platforms/web/entry-runtime-with-compiler.jsplatforms/web/runtime/index.jsplatforms/weex/runtime/index.js其实也很好理解,之前说过 vue 的三种平台web、weex、服务器,最后一个肯定不需要,前两者,weex 里不支持模板是字符串或者 el 是字符串的模式,所以weex里只有一个,而web里有两种web/entry-runtime-with-compiler.js先看看web/entry-runtime-with-compiler.js缓存原有的$mount,重写的时候,是在原有的$mount上增加功能el先判断是否传入,然后统一由query获取dom拿到 dom 之后,先看下是不是body或者html,是的话,警告并终止如果options有render函数直接返回原有的$mount;没有的话,将template或者el转化为render函数之后才调用原有的$mount重点看下,这里,怎么将template或者el转化为render函数,其实就是找到DOM字符串没有render函数,就两种情况:有template或者el,将最终的DOM字符串赋值给template优先处理template:template是一个字符串,只支持以#开头,当做 id 获取元素,返回元素的innerHTML,template是一个元素的话,返回元素的innerHTML其他情况不支持没有template,看下el,获取 el 的outerHTML,在赋值给template然后将DOM字符串用compileToFunctions处理成 render 函数// 缓存原来的,重写的时候,在原有的基础上增加功能
const mount = Vue.prototype.$mount;
// 重写
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 如果传入el的话,就去拿到el,query返回dom元素
el = el && query(el);
/* istanbul ignore if */
// 不能是body,html
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== "production" &&
warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
);
return this;
}
// 缓存options
const options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template;
if (template) {
// template是字符串的话,只支持#开头
if (typeof template === "string") {
if (template.charAt(0) === "#") {
template = idToTemplate(template);
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production" && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
);
}
}
} else if (template.nodeType) {
// 元素的话 直接返回内容
template = template.innerHTML;
}
} else if (el) {
// 即便有el,还是统一赋值给template
template = getOuterHTML(el);
}
if (template) {
// template会统一转化为render函数
const { render, staticRenderFns } = compileToFunctions( template, );
options.render = render;
options.staticRenderFns = staticRenderFns;
}
}
return mount.call(this, el, hydrating);
};
Vue.compile = compileToFunctions;
export default Vue;
小技巧:只有非正式环境,才有 warn这个小技巧很容易在日常的开发中使用const isProduction =
process.env.NODE_ENV === "production"(!isProduction) && warn("some warn");
小技巧:判断元素是不是 body 或者 html这个也很容易const isBodyOrHtml = el === document.body || el === document.documentElement;
小技巧:先处理异常情况总会先处理异常情况,最后处理相对正常的情况if(someError){
return ...
}
return ...
小技巧:el 可以是字符串,也可以是元素的写法这个一旦涉及到获取元素,就很好使用function query(el) {
if (typeof el === "string") {
const selected = document.querySelector(el);
if (!selected) {
process.env.NODE_ENV !== "production" &&
warn("Cannot find element: " + el);
return document.createElement("div");
}
return selected;
}
return el;
}
元素的判断,也可以用xx.nodeType小技巧:在已有的函数上增加新功能如果希望在已有的函数上增加新的功能,可以先缓存原有的方法,然后重写,这样的好处是不需要到原有的函数中增删代码,而且不同的条件,可能需要不同的重写,灵活性更高:let print = function (name) {
console.log(name);
};
print("hello");
// 想增加新功能的话
let oldPrint = print;
print = function (name) {
console.log("想另外加功能");
oldPrint.call(this, name);
};
print("hello");
$mount:platforms/web/runtime/index.js上面$mount改写,改的就是platforms/web/runtime/index.js定义的$mount,这里注意runtime肯定是需要的,所以先实现这个。而compiler看情况,所以另外的文件实现。继续看,这里的定义import { mountComponent } from "core/instance/lifecycle";
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
代码很明了,拿到el,然后让mountComponent实现,继续探索core/instance/lifecycle.js定义了updateComponent,然后实例化Watcher,专业名词渲染watcherimport Watcher from "../observer/watcher";
function mountComponent(vm) {
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
// 渲染watcher,updateComponent传到这里来了
new Watcher( vm, updateComponent, noop, {}, true /* isRenderWatcher */ );
return vm;
}
渲染watcher的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法首先生成虚拟 Node,最终调用 vm._update 更新 DOMWatcher在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数。render先重点看下_render,打印 vm 实例,看到这个方法在原型上面,全局搜下,很快就知道在core/instance/render.js里:_render函数,返回的是vnode,而vnode是由$createElement这个方法返回的Vue.prototype._render = function () {
// ...
// render self
let vnode;
try {
// 这里注意,第一个参数是this,先不用管,render的真正参数是` vm.$createElement`
vnode = render.call(vm._renderProxy, vm.$createElement);
} catch (e) {
// ...
}
// if the returned array contains only a single node, allow it
// set parent
vnode.parent = _parentVnode;
return vnode;
};
理解 vnode 和 patchvnode 其实就是虚拟dom(virtual dom),因为真实的 node(real dom)属性和方法都非常多,频繁操作费性能,而 vnode 有相对精简的属性,其本身很容易映射成真实的 node,当然 vnode 和 node 本质上都是对象。patch 就是将 vnode 变成真实的 node,并且插入到文档里(渲染)看个 demo,直观感受 vnode 和 patch,可以在本地运行:<body>
<div id="app"></div>
<script>
// vnode本质上就是对象,{"sel":"div","data":{},"text":"Hello World"},等同于描述<div>hello</div>
let vnode = new VNode("div", "hello");
console.log(JSON.stringify(vnode));
let app = document.querySelector("#app"); // 真实dom
/* patch的功能:将第二个vnode转化为真实dom,并插入到文档中,销毁掉第一个vnode,。
* 这里有个细节,如果首个节点是真实节点,内部会转化为vnode,然后进行操作
*/
patch(app, vnode);
// patch之后,vnode上面的elm就有了
console.log(vnode);
/* 各个函数的定义 */
// VNode的类
function VNode(tag, text, elm) {
this.sel = tag;
this.text = text;
this.elm = elm;
}
// 创建vnode
function createElement(tag, text, elm) {
return new VNode(tag, text, elm);
}
// 真实的dom转化为vnode
function elToVNode(el) {
return createElement(el.tagName, el.textContent, el);
}
// patch将第二个vnode转化为真实的dom,然后插入到文档中。第一个节点存在的意义上和第二个节点进行比较,从而精准的进行渲染
function patch(oldVNode, newVNode) {
// 第一个节点是真实节点的话,转化为vnode
if (oldVNode.nodeType) {
oldVNode = elToVNode(oldVNode);
}
// 将第二个节点转化为真实的dom
let node = document.createElement(newVNode.sel);
node.textContent = newVNode.text;
newVNode.elm = node;
// 插入到文档中
oldVNode.elm.parentNode.insertBefore(node, oldVNode.elm);
// 销毁掉第一个节点(这里是销毁,但很多时候不是)
oldVNode.elm.parentNode.removeChild(oldVNode.elm);
return newVNode.elm;
}
</script>
</body>
这样对vnode和patch有个概念之后,继续源码~源码中的createElement,返回的是vnode很容易找到就在src/core/vdom/create-element.js。createElement实际上内部调用_createElement,其返回一个vnode。children 表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组,根据normalizationType规范的方法也不一样。(normalizationType主要区分render 函数是编译生成的还是用户手写的)import { normalizeChildren, simpleNormalizeChildren } from './helpers/index'
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 当data参数不存在的时候,后面的参数前置
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 针对不同的normalizationType,对children做不同的处理,其实就是扁平化children
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// platform built-in elements
// 是保留标签的话,直接创建vnode,vnode支持字符串和组件类型,但这里暂时只看字符串就好
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
}
}
children 规范化src/core/vdom/helpers/normalize-children.js的两个方法:simpleNormalizeChildren这个方法很简单,如果children是二维的话,拍平,children 始终是[vnode,vnode],这里也只考虑到二维的情况normalizeChildren这个稍微麻烦一点,考虑到多维的情况,需要递归,最后也是拍平// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren(children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children);
}
}
return children;
}
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren(children: any): ?Array<VNode> {
return isPrimitive(children)
? [createTextVNode(children)]
: Array.isArray(children)
? normalizeArrayChildren(children)
: undefined;
}
function isTextNode(node): boolean {
return isDef(node) && isDef(node.text) && isFalse(node.isComment);
}
function normalizeArrayChildren(
children: any,
nestedIndex?: string
): Array<VNode> {
const res = [];
let i, c, lastIndex, last;
for (i = 0; i < children.length; i++) {
c = children[i];
if (isUndef(c) || typeof c === "boolean") continue;
lastIndex = res.length - 1;
last = res[lastIndex];
// nested
if (Array.isArray(c)) {
if (c.length > 0) {
c = normalizeArrayChildren(c, `${nestedIndex || ""}_${i}`);
// merge adjacent text nodes 合并
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
c.shift();
}
res.push.apply(res, c);
}
}
}
return res;
}
小技巧:扁平化二维数组其实扁平化二维数组,可以利用下concat:function flat(arr) {
return [].concat(...arr);
}
[].concat(1,[4])就是[1,4]update其实大部分功能就是patch找特定的方法,基本都是搜索,后期不再赘述。_update在src/core/instance/lifecycle.js// hydrating服务端才用到,否则就是false
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
// 缓存之前的dom
const prevEl = vm.$el;
// 缓存之前的vnode
const prevVnode = vm._vnode;
const restoreActiveInstance = setActiveInstance(vm);
// 新生成的vnode赋值
vm._vnode = vnode;
if (!prevVnode) {
// initial render 首次渲染走这里 首次的时候 挂载真实的el上 这里的patch和上面的例子相似 根据vnode创建真实的dom,然后插入到文档中,__patch__返回真实的dom
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
console.log(vm.$el);
}
};
仔细看下__patch__,这里需要层层向上找,其实和上面的简单例子的主体逻辑很像了但这里为什么用createPatchFunction生成patch呢?vue 有三种平台,前面说过了,服务器端不需要渲染 dom,忽略。而 web 和 weex 都需要渲染,但“平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。因此每个平台都有各自的 nodeOps 和 modules,所以存放在各自的平台里。但是 patch 的主要逻辑是相似的,这样通过 createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了,这种函数柯里化的编程技巧也非常值得学习。/* src/platforms/web/runtime/index.js */
import { patch } from "./patch";
// 浏览器需要,服务器不需要,noop是空函数
Vue.prototype.__patch__ = inBrowser ? patch : noop;
/* src/platforms/web/runtime/patch.js */
import { createPatchFunction } from "core/vdom/patch";
// nodeOps是增删改查dom的方法合集,modules是attrs, klass, events, domProps等这种的解析
export const patch: Function = createPatchFunction({ nodeOps, modules });
/* src/core/vdom/patch.js */
export function createPatchFunction(backend) {
// ...定义很多和真实dom相关的函数
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// 首次渲染,是真实的dom 这边只贴出与其相关的代码
const isRealElement = isDef(oldVnode.nodeType);
// replacing existing element
// 缓存 旧的dom
const oldElm = oldVnode.elm;
// 缓存 旧的dom 父元素
const parentElm = nodeOps.parentNode(oldElm);
// create new node 根据新的vnode创建dom 并在父元素里旧元素前面插入新的dom
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
);
// destroy old node
if (isDef(parentElm)) {
// 移除旧的dom
removeVnodes(parentElm, [oldVnode], 0, 0);
}
// 返回 新的dom
return vnode.elm;
};
}debugger 调试_update<div id="app">{{message}}</div>
<!-- 这里换成绝对路径 -->
<script src="vue/dist/vue.js"></script>
<script>
const vm2 = new Vue({
el: "#app",
data: {
message: "hello",
},
render(createElement) {
return createElement(
"section",
{
attrs: { id: "box" },
},
this.message
);
},
});
</script>
在dist/vue.js,这里打上断点Vue.prototype._update = function (vnode, hydrating) {
debugger;
};
这里务必明确,开始的#app是真实的 dom,而后面的section#box是vnode,这里的参数vnode是指section#box,而vm.$el开始指的是#app总结当引入 vue,首次渲染的过程大概如下:initMixin(Vue); // ... 引入vue的时候,Vue.prototype已经挂载了各模块属性和方法。 src/core/instance/init.js
new Vue({ el: "#app" }); // 用户创建vm实例
this._init(options); // src/core/instance/index.js
vm.$mount(vm.$options.el); // _init方法里 src/core/instance/init.js
template = getOuterHTML(el);
const { render, staticRenderFns } = compileToFunctions(template);
options.render = render;
mount.call(this, el, hydrating); // $mount方法里 compiler这里重点是 将template或者el的dom字符串都会变成render函数,之后才是调用runtime的$mount方法 src/platforms/web/entry-runtime-with-compiler.js
mountComponent(this, el, hydrating); // $mount方法里 runtime里,此时this已经有了render函数了 src/platforms/web/runtime/index.js
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */) // mountComponent方法里 建了一个渲染watcher,执行`vm._update(vm._render(), hydrating)`,_render是返回vnode,_update将这个vnode变成真实dom,然后插入到文档中(渲染),首次渲染,_render其实就是执行参数中的render函数 src/core/instance/lifecycle.js
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
return return vnode // _render函数里 src/core/instance/render.js
vm.$el = vm.__patch__(vm.$el, vnode) // _update函数里 __patch__就是创建真实dom和插入到文档中 src/core/instance/lifecycle.js
Vue.prototype.__patch__ = inBrowser ? patch : noop // __patch__就是patch方法 src/platforms/web/runtime/index.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
// web下的patch由createPatchFunction生成 src/platforms/web/runtime/patch.js
function createPatchFunction(backend){
const { modules, nodeOps } = backend
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
nodeOps.createElement(tag, vnode)
}
return function patch (oldVnode, vnode, hydrating, removeOnly) {
const oldElm = oldVnode.elm
// 缓存 旧的dom 父元素
const parentElm = nodeOps.parentNode(oldElm)
// create new node 根据新的vnode创建dom 并在父元素里旧元素前面插入新的dom
createElm( vnode, insertedVnodeQueue, parentElm, nodeOps.nextSibling(oldElm) )
// 移除旧的dom
removeVnodes(parentElm, [oldVnode], 0, 0)
// 返回 新的dom
return vnode.elm
}
} // 这里的生成patch函数利用不同平台的modules,但patch的逻辑却相似 src/core/vdom/patch.js
这里在借助黄轶老师的图,表示从主线上把模板和数据如何渲染成最终的 DOM 的过程:引用黄轶老师的 vue2 源码解密课程vue.js 技术揭秘Snabbdom 的使用
准备工作 - 学习vue源码系列1
准备工作 - 学习vue源码系列1决定跟着黄轶老师的vue2源码课程好好学习下vue2的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~将vue的源码clone到本地,切换到分支2.6。认识 flowFlow 是 facebook 出品的 JavaScript 静态类型检查工具。 vue使用其进行类型检测。怎么使用 flow安装:npm i flow-bin -g创建配置文件:flow init创建一个 js 文件/*@flow*/
function split(str) {
return str.split(" ");
}
split(11);执行flow命令,就可以检测了,这边会发现报错flow 的工作方式通常类型检查分成 2 种方式:类型推断:通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。(上面的例子就是推断)类型注释:事先注释好我们期待的类型,Flow 会基于这些注释来判断。(推荐)类型注释和 ts 非常像:/*@flow*/
function add(x: number, y: number): number {
return x + y;
}
add("Hello", 11);
// 执行 flow,会报错一般在需要flow检查的文件首行加上/*@flow*/如果可以为undefined或者null的话:var foo: string | void = null;
var foo: ?string = null;flow 在 vue 源码中的使用flow 可以自定义类型,vue 的主目录下.flowconfig 文件, 它是 Flow 的配置文件。 这其中的 [libs] 部分用来描述包含指定库定义的目录,默认是名为 flow 的目录。遇到某个类型,想要看数据结构的话,就翻看这里。vue 源码目录设计主要看src的设计:功能模块拆分的非常清楚,让阅读性和可维护性都很优雅。vue 源码构建Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下。Rollup 主要是构建 js 文件,不处理其他的文件,相比 webpack 更轻量。所谓构建,就是运行命令之后,生成最终的文件。构建脚本构建脚本是package.json,其中的的build命令,就是构建命令。vue 的三种运行环境:web(普通的 web 端)ssr(服务端)weex(原生)构建过程src/build.js其实就是获取配置,然后根据环境参数过滤,最后在dist目录生成相应的js文件。为了让配置中的entry/dist路径更具可读性,vue进行了一些技巧性的操作:const builds = {
"web-runtime-cjs-dev": {
// 这里的 web会被替换成web的绝对路径,resolve就是做这个事的
entry: resolve("web/entry-runtime.js"),
// 同理 dist也是如此
dest: resolve("dist/vue.runtime.common.dev.js"),
format: "cjs",
env: "development",
banner,
},
// ...
};再看下resolve:const resolve = (p) => {
const base = p.split("/")[0];
// // aliases就是 路径的别名哈希表 {compile:"compiler文件夹的绝对地址",core:.....}
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1));
} else {
return path.resolve(__dirname, "../", p);
}
};format属性format表示 最后生成的 js 文件符合什么规范:commonJS 规范,其实就是require/module.exportsESModule 规范,其实就是import/exportumd 规范,算是兼容模式,(function (window, factory) {
if (typeof exports === "object") {
module.exports = factory();
} else if (typeof define === "function" && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});小技巧:打印错误和文件大小有两个函数,可以日常使用function getSize (code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
// catch(logError) 这样使用非常方便
function logError (e) {
console.log(e)
}Runtime Only VS Runtime + Compiler其实这两个是相对的。使用vue的时候,如果template是字符串的话,如下new Vue({
template: '<div>{{ hi }}</div>'
})这种代码一般是在运行环境里,由运行环境将template编译成render函数,这里特别注意,编译是运行环境做的,这样vue就必须包含怎么编译的代码,专业名词就是Compiler当然如果代码里没有这样的字符串,自然也就不需要Compiler。显然加上Compiler既增加了vue的体积,又让运行环境干编译,会更费时。那另外一种就好理解了,所谓的Runtime其实就是没有Compiler的vue代码,一般开发是用.vue文件表示模板,而.vue文件,是在你的编辑器里,也称为编译环境里,将其编译成render函数(依靠webpack的vue-loader插件),这样运行环境自然不需要编译,只需要运行就行,专业名词就是Runtime显然Runtime既轻量,又省时。说句人话,开发的时候如果模板没有字符串的话,直接Runtime就好,不需要Compiler从入口开始先找到定义vue的地方!dist的vue.js是由entry-runtime-with-compiler.js生成的,一层层往上找线索~// entry-runtime-with-compiler.js
import Vue from './runtime/index.js'
// runtime/index.js
import Vue from 'core/index'
// core/index.js
import Vue from './instance/index'
// instance/index 找到啦!
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}这里用函数的形式定义了类,好处是方便在Vue.prototype上面拓展,这样拓展的方法和属性可以分布在别的文件,方便维护。小技巧:怎么判断有没有用new调用其实就是this是不是属于当前实例function C(){
if(!this instanceOf C){console.log('C is a constructor and should be called with the `new` keyword')}
}小技巧:函数的形式定义类,方便拓展class关键字的方式,拓展会不方便,反而函数的形式更加便捷,可以将方法和属性分门别类在其他文件function C(){}
// getName.js
C.prototype.getName = function getName(){}引用黄轶老师的vue2源码解密课程vue.js 技术揭秘
weex开发-使用weex-ui绑定事件源注意事项
在使用weex-ui提供的控件绑定事件源的时候,一定要注意事件源的绑定。新手更需要额外注意,简单举个例子,就拿wxc-stepper来说,使用方法如下:<template>
<div class="wrapper">
<div class="demo">
<text class="text">无配置:</text>
<wxc-stepper></wxc-stepper>
</div>
<div class="demo">
<text class="text">{min:2,max:10,step:2,defaultValue:4}</text>
<wxc-stepper default-value="4"
step="2"
max="10"
min="2"
@wxcStepperValueChanged="stepperValueChange"></wxc-stepper>
</div>
<div class="demo">
<text class="text">禁用</text>
<wxc-stepper default-value="4"
step="2"
max="10"
min="2"
:disabled="isDisabled"></wxc-stepper>
</div>
<div class="demo">
<text class="text">input只读:</text>
<wxc-stepper :read-only="isOnlyRead"></wxc-stepper>
</div>
</div>
</template>
<script>
import { WxcStepper } from 'weex-ui';
export default {
components: { WxcStepper },
data: () => ({
value: 4,
isDisabled: true,
isOnlyRead: true
}),
methods: {
stepperValueChange (e) {
console.log(e.value);
}
}
};
</script>以上代码来自weex-ui。如果你想把default-value="4"通过网络请求的数据进行绑定,default-value="item.index"但是发现没有任何内容显示出来,原因是没有进行绑定,需要这么写::default-value="item.index"具体可以查看vue中的描述v-bind 指令可以用于响应式地更新 HTML 特性所以需要动态绑定数据的时候一定要使用v-bind,这也是vue最基础的语法。
weex开发 - 方法的映射,在weex调用fetch方法,实际调用同名的原生方法,在回调中把数据传递回js
方法的映射通过标题的解释也可以很明显的看出来,是通过在js上调用和原生同名的方法,在原生返回数据中把这些数据回传给JS的一种方法,这么做的原因是weex中的网络请求不能满足我们客户端网络安全和各种配置的需求。具体的做法,首先在weex中,我们写一个名为fetch.js的网络请求文件,将所有的方法都写在这里:const request = weex.requireModule('fetchJS');
export function getListInfor (这里可以传入参数) {
let dic = {page : "1", userID: "1234"};
return new Promise((resolve, reject) => {
stream.fetch({
method: 'post',
url: 'https://xxxxxxxxxxxxxxxxxxxxx',
type: 'json',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(dic)
}, (response) => {
console.log('hello'+JSON.stringify(response.data));
//如果你的数据外层没有套data,那就直接写response
resolve(response.data);
}, () => {})
})
}这样,我们的js方法就写好了,那怎么通过调用这个方法映射到原生的方法呢?在原生种我们这么写:.h文件
#import <Foundation/Foundation.h>
#import "WeexSDK.h"
@interface WXEventModule : NSObject<WXModuleProtocol>
@end
.m文件
#import "WXEventModule.h"
@implementation WXEventModule
@synthesize weexInstance;
WX_EXPORT_METHOD(@selector(fetch:callback:progressCallback:))
//!!!这个方法一定要跟我下面的一样哦,或者你可以去SKD里面搜索这个方法,里面提供了几个方法,一定要和这几个方法名一模一样,否则自己写的名字系统根本不认
- (void)fetch:(NSDictionary *)options callback:(WXModuleKeepAliveCallback)callback progressCallback:(WXModuleKeepAliveCallback)progressCallback
{
[[RequestManager sharedInstance] wx_request:options finished:^(id result) {
//这里博主在数据外层套了一层data,不套的话js里面直接就是你的result层级这里层级一定不要搞错了,js和原生务必保持一致
callback(@{@"data": result}, false);
}];
}
//ps:这个是封装方法的内部
- (void)wx_request:(NSDictionary *)dic finished:(void(^)(id result))handler
{
//这里的参数url,body,method必须和js传过来的保持一致才能拿到
NSString *url = dic[@"url"];
NSDictionary *bodyDic = dic[@"body"];
NSString *method = dic[@"method"];
NSURLRequest *request;
if ([method isEqualToString:@"get"]) {
xxxxxx//get的不同设置
}
else
{
xxxxxx//post的不同蛇粥
}
//这是博主的网络请求方法,大家把自己的方法带入就可以,不要钻牛角尖
[self request:request requestCompletionHandler:^(NSMutableDictionary *dic, NSString *errorMsg) {
handler(dic);
}];
}
//最后哆嗦一句,千万不要忘记注册,这些基本的博主就不说了,不会的可以看下面链接
[WXSDKEngine registerModule:@"fetchJS" withClass:NSClassFromString(@"WXEventModule")];weex-自定义module这个js方法的使用一定要告诉大家,不知道的情况还真是很难琢磨:import * as fetchRequest from '../network/fetch';
request.getListInfor(这里需要参数的话和请求中保持一致).then(
data=>{
console.log(JSON.stringify(data));
this.listArray = data;
}
);以上就把方法的映射说完了,其实也不麻烦,其中的钩子函数和goback函数值得大家深究,不是很懂js,vue的可能看不懂,这不要紧,慢慢学习,weex学习路,与君共勉。
weex开发 - 加载index.js崩溃,白屏(may it has been destroyed so method:fireEvent is ignored,Url must be passe)
最近,博主在做weex项目中发现了一个奇怪的问题,分别会报如下几个错误:1.[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]
2.<Weex>[info]WXBridgeContext.m:552, No send queue for instance:<WXSDKInstance: 0x7ff1645261d0; id = 0; rootView = (null); url= (null)>, may it has been destroyed so method:fireEvent is ignored
3.<Weex>[error]WXSDKInstance.m:149, Url must be passed if you use renderWithURL博主加载index.js代码是这么写的://跳转
WXDemoViewController *demo = [[WXDemoViewController alloc] init];
demo.url = [NSURL URLWithString:[NSString stringWithFormat:@"file://%@/index.js",[NSBundle mainBundle].bundlePath]];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:demo];
[[UIApplication sharedApplication] delegate].window.rootViewController = nav;
//加载
NSURL *URL = [self testURL: [self.url absoluteString]];
NSString *randomURL = [NSString stringWithFormat:@"%@%@random=%d",URL.absoluteString,URL.query?@"&":@"?",arc4random()];
[_instance renderWithURL:[NSURL URLWithString:randomURL] options:@{@"bundleUrl":randomURL} data:nil];其中其他的初始化和配置博主就不一一贴出来了,不会的可以weex platform add ios来增加,weex run ios查看,也可以在生成的工程中打开target运行查看。以上和通过weex platform add ios生成的工程写法一样,但是却莫名其妙白屏,现实url为空,博主又尝试了将这样写:[_instance renderWithURL:[NSURL URLWithString:self.url] options:@{@"bundleUrl":self.url.absoluteString} data:nil];
或
//urlStr是一个字符串
demo.urlStr = [NSString stringWithFormat:@"file://%@/index.js",[NSBundle mainBundle].bundlePath];
[_instance renderWithURL:[NSURL URLWithString:urlStr] options:@{@"bundleUrl":urlStr} data:nil];但是结果,经过[NSURL URLWithString:urlStr]之后的NSURL类型却是空,前者显示为空白,后者直接崩溃了,是不是很奇怪,博主经过断点进入到WXSDKInstance.m中发现如下代码,可以知道罪魁祸首是url=nil导致的。但是明显url不应该为空的,所以就很奇怪了。//后者出现的问题
- (void)renderWithURL:(NSURL *)url options:(NSDictionary *)options data:(id)data
{
if (!url) {
WXLogError(@"Url must be passed if you use renderWithURL");
return;
}
self.needValidate = [[WXHandlerFactory handlerForProtocol:@protocol(WXValidateProtocol)] needValidate:url];
WXResourceRequest *request = [WXResourceRequest requestWithURL:url resourceType:WXResourceTypeMainBundle referrer:@"" cachePolicy:NSURLRequestUseProtocolCachePolicy];
[self _renderWithRequest:request options:options data:data];
[WXTracingManager startTracingWithInstanceId:self.instanceId ref:nil className:nil name:WXTNetworkHanding phase:WXTracingBegin functionName:@"renderWithURL" options:@{@"bundleUrl":url?[url absoluteString]:@"",@"threadName":WXTMainThread}];
}
在WXBridgeContext.m文件中,也有着一段代码
//前者出现的问题
- (void)executeJsMethod:(WXCallJSMethod *)method
{
WXAssertBridgeThread();
if (!method.instance) {
WXLogError(@"Instance doesn't exist!");
return;
}
NSMutableArray *sendQueue = self.sendQueue[method.instance.instanceId];
//sendQueue莫名其妙变nil了,实在是找不到任何原因,一步步运行,也毫无头绪
if (!sendQueue) {
WXLogInfo(@"No send queue for instance:%@, may it has been destroyed so method:%@ is ignored", method.instance, method.methodName);
return;
}
[sendQueue addObject:method];
[self performSelector:@selector(_sendQueueLoop) withObject:nil];
}经过以上探索,发现似乎和instance和url有关,所以尝试直接把url写死在加载的地方: NSString *bundleUrl = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/index.js",[NSBundle mainBundle].bundlePath]].absoluteString;
[_instance renderWithURL:[NSURL URLWithString:bundleUrl] options:@{@"bundleUrl":bundleUrl} data:nil];发现困扰了博主快两天的问题就这么解决了,最后的结论是没有结论,猜测可能在初始化SDK的时候有个时间差,有些东西没有初始化好,因为属性传值绝对不可能变nil的啊,也许目前这样写也只是一个权宜之计,但却真正的解决了这个问题,在官方没回复前就先这么些吧,有结果再更新。补充一下,还有一个坑造成这个问题的原因,你的url中有中文,需要进行转码:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
weex开发- 无法找到模块“weex-vue-render”的声明文件。引入vue报错,无法找到引入的vue模块
在引入了vuex之后编译报错,引入如下:这是weex-dev生成的文件,里面可以明显看到无法找到声明文件下面看这个js文件内容:也没有什么特别的,但是在引入的时候,确偏偏报了无法找到模块的错误,看下面:enter.js文件做了引入操作:import store from './xxxxx/store/store'原因是store.js有vue的内容,经查证,引入vue的时候不能用相对地址,参考其他的模块引入:将.换成@符号即可编译通过:import store from '@/xxxxx/store/store'
week-iOS的扩展之内置模块,将原生模块做成标签在weex中使用
在自定义module这篇博客中,博客解析了如何自定义module来和weex的js做交互,当时有提到定义原生模块为标签,地址和Demo请点击上方链接查看,这篇博客,博主将详细解释如果把weex中不支持的模块在weex中嵌入。在iOS中:1.创建一个继承于WXComponent的类,这个和官方的说法没什么差别。2.需要实现两个方法官方说明中需要实现loadView方法来覆盖父类方法,因为一个 component 默认对应于一个 view,如果未覆盖 loadView 提供自定义 view, 会使用 WXComponent 基类中的 WXView, WXView 是继承自 UIView 的一个派生 view。说的比较拗口,简而言之,看你自己的心情,不覆盖的话你就通过addSubView的方式来添加view即可,或者直接用你写好的模块覆盖loadView,比如:- (UIView *)loadView
{
return [MyView new];
}这样你只需要让myview=self.view就可以了,两者差别不大,只是实例的view不一样,没什么影响。3.做完这些,你就可以到appdelegate中来注册你这个自定义的模块了[WXSDKEngine registerComponent:@"slfview" withClass:[MyComponent class]];
//注意一点,这里是registerComponent,博主已开始不小心弄成了module,结果怎么都显示不出来。4.完成以上操作,你就可以在weex中直接使用<slfview></slfview>
//这就是我们刚刚注册成功的标签,直接使用,还可以根据需要添加css样式注:在这之前一定要先注册module [WXAppConfiguration setAppGroup:@"AliApp"];
[WXAppConfiguration setAppName:@"WeexDemo"];
[WXAppConfiguration setAppVersion:@"1.8.3"];
[WXAppConfiguration setExternalUserAgent:@"ExternalUA"];
[WXSDKEngine initSDKEnvironment];
//这是上面注册模块的代码
[WXSDKEngine registerComponent:@"slfview" withClass:[MyComponent class]];
[WXSDKEngine registerModule:@"WXEventModule" withClass:NSClassFromString(@"WXEventModule")];
[WXSDKEngine registerHandler:[WXImgLoaderDefaultImpl new] withProtocol:@protocol(WXImgLoaderProtocol)];详情可以查看这篇博客