金奔

金奔 查看完整档案

青岛编辑山东大学威海校区  |  数字媒体技术 编辑  |  填写所在公司/组织填写个人主网站
编辑

前端之路,就是攻城,拥抱变化,接受挑战

个人动态

金奔 赞了回答 · 2020-11-19

解决前端工作碰到哪些业务不能做?

什么虚拟货币交易,什么网络金融贷款,什么充值返利平台,这些个都多少在打擦边球,慎重选择,仅仅个人看法。

关注 8 回答 6

金奔 赞了回答 · 2020-11-19

解决前端工作碰到哪些业务不能做?

1.不要掩耳盗铃,大多数项目开发真不知道是干啥吗?只不过内心觉得技术无罪而已,只做合法的事情。
2.还有一些开发也不一定弄明白的,就是楼上说的资金池、什么币等,其实我觉得爬虫也要远离
3.做那些一眼能看懂商业模式的项目。

关注 8 回答 6

金奔 赞了回答 · 2020-11-19

前端人员能独自解决掉跨域问题吗?

不能哦,想想如果你不需要支付宝同意,在你的网站就可以代替用户去调用支付宝接口,那不是乱了套了
跨域必须要后端进行一定的支持

关注 9 回答 11

金奔 关注了问题 · 2020-07-27

vite是否用于替换vue cli?

尤大目前开发的这个vite工具说是要告别webpack
启动命令为vue init vite-app 启动后框架结构与vue-cli类似
当这个框架完善后是否意味着将在以webpack为构建工具的vue-cli和vite中选用一个框架开发?

关注 4 回答 2

金奔 赞了回答 · 2020-07-27

vite是否用于替换vue cli?

我已经用 vite + vue3 及其周边写了一个 demo,热更新非常快。vite 捆绑了 rollup 进行打包。短时间内它无法代替 webpack,自身及生态都需要不断完善。

当这个框架完善后是否意味着将在以webpack为构建工具的vue-cli和vite中选用一个框架开发?

我也是这么认为的。vuepress 也在迁移 vite。 基于 esm 的热更新应该是趋势。

关注 4 回答 2

金奔 赞了文章 · 2020-05-13

我在阿里是怎么工作的——写在阿里入职一周年

2019年初,笔者从腾讯跳槽到阿里。新的公司新的氛围,坦白说,阿里新岗位的工作强度大于之前在腾讯的岗位。我入职后不久后就迫于试用期答辩项目的压力,开始周末加班,后来又因为自己懒散,中断了坚持了一年多的博客更新。经过了一年多的阿里熏陶,我优化调整了自己部分的工作方法论并取得了不错的效果——重新找回了有节奏的工作生活。在这个过程中,总结了5条高效工作的经验,在这里分享给大家,希望能对大家有所帮助。

1. 优先做重要不紧急的事情

我们都知道四象限分析法,就是按照紧急/重要将事情划分到四个象限中去(如下图)。
四象限分析法.png

尽管很多人知道四象限分析法,但是对于处理的优先级往往是误解的!以我自己的经验,我之前都是按照下面的“赤橙黄绿”这个顺序来完成日常的工作:

重要且紧急的事情(又重要又紧急为啥不做)> 紧急不重要的事情(紧急的事情还是先处理一下吧)> 不重要不紧急的事情(做完了前两类事情之后,也累了,休息一下)> 重要但不紧急的事情(养足精神处理)

而这样的顺序会导致一个问题,就是每天都在赤橙黄,赤橙黄中反复,重要不紧急的事情一直排不上期,最后也变成重要且紧急的事项,然后再慌慌张张的处理。但是,重要不紧急的事情往往是一些规划/提前做准备的事情,临时抱佛脚是搞不定的。这样就逼迫自己一直在低头处理眼前的事情,不能抬头看路,永远忙忙碌碌,却很难出成果。

而实际上四象限分析法倡导的顺序是“绿赤橙黄”——先做重要不紧急的事情,这个也是《高性能人士的7个习惯》这本书中着重倡导的。

重要不紧急 > 重要且紧急 > 紧急不重要 > 不重要不紧急

可能看到这个地方你就疑惑了:那难道现在线上有bug,我不赶着去处理,而是在这边做规划或者一些提效工具吗?

其实这个问题不应该仅仅局限在这个问题上看——单纯看这个问题的话,毫无疑问是先处理线上bug——而是要考虑到如果我们做好重要但不紧急的事项,会让我们重要且紧急的事情变得很少/很容易解决。举个例子:我们之前有个业务是5年未重构的老业务,每次线上问题的定位都是0.5人天起的,更不要提还有解决问题和上线的时间。后来我们推动了重构和全链路请求串打通,现在所有的线上问题只要提供请求串,都能轻松定位到并处理。处理好一个问题的时间从3人天变到了分钟级。

这样的效率让我们做到了有更多的时间来做重要不紧急的事情,而不是每天在繁重重复业务的泥潭里深陷。

2. 明确自己的核心目标

每个人在工作中都有自己的定位和目标。在看这段的时候,大家不妨先站在老板的角度上,想想老板最关心你的核心目标是什么?再站在自己的角度上,想想自己最关心的核心目标是什么?

还是以我为例,老板最关心的是我如何带领搜索小组高效/有节奏的业务&技术产出;我最关心的是我的个人竞争力(包括技术敏感度/团队管理能力/项目管理能力/代码能力)是否有持续的成长。

当你明确了自己的核心目标后,就会发现有很多事情的优先级是可以调整的——事情是做不完的,你只需要保证的是重要的事情一直在被推进,不重要的可以讨论/推掉/改方案。只有当你清楚的明确了自己的优先级,而不是把别人的优先级当成了自己优先级,你才能合理的利用上面说到的四象限分析法,而不是把所有的事情都放到重要且紧急的区间中去。最终导致做了半天都做的是收益低,投入产出差的工作,一天做下来都不知道今天干了啥。

3. 做好并行

很多年前有个经典的面试题:并发和并行的区别。这块我不多做阐述,其实只要看英文就明白:

并发:concurrent
并行:parallel
并发在微观时间层面不是同时进行的,而并行是平行的意思,就是一直同时推进的。

作为一个工程师,毫无疑问我们大家都是单核CPU,单一时间只能处理好一件事情。但是工程师要自己做好任务调度,在技术和业务的推进上,或者多件重要事项的推进上,一定要给人以并行的感觉。决不能说,这周我业务忙,所以我的技术项目就没有进展了;或者说这周技术项目来不及了,我就不管业务了把;甚至是我推荐A业务,B业务没进展了。

工程师自己要定好优先级,合理的对任务进行分解。我们都是人,不是简单的机器,不存在只能做A做不了B的情况。如果真的是A太紧急太多了,那就要考虑是不是过去重要不紧急的事项做少了——是不是我们没有沉淀出高效工具&平台、是不是排期有问题、哪些地方还可以优化?

为什么一定要做好并行?随着职责的提升,从初级到高级,从技术到管理,工程师负责的项目会越来越多,哪怕将核心目标定的再好,位于绿色象限的事项也会变多。若一开始就做单细胞生物,这个时候就会捉襟见肘。所以要一开始就拆分任务,在合理的区间中做任务调度,不断培养自己这个能力。

4. 建立自己的影响力

一定一定要珍惜自己的羽毛。一旦固有印象被建立,想再改变可能要付出10倍以上的努力才能改变

高效/靠谱的影响力有什么好处?

  • 更多的资源:没人会去挑战你的排期/核心的技术项目给你做
  • 更多的体谅:做不完不是你不努力,肯定是其他原因;偶尔搞不定也没关系
  • 尊重:业务方不再把你当工具人,而是把你当伙伴

如何在一个新环境快速建立自己的影响力?

  • 共情:多站在合作伙伴的角度考虑,别人的问题哪怕不能及时回答,也可以先表示下收到了,并给出预期的答复时间。解别人之所难
  • 努力:高效处理事项,提出完善的解决方案,能别人所不能
  • 认真:有代码洁癖,不允许可以跑但是有逻辑缺陷的代码被提交,线上bug量很少
  • 靠谱:不随便承诺,承诺了必实现。决不出现答应你晚上给,结果第二天还没有给到的情况。
  • 兜底:提前预测到可能的风险,被告知大家。不给别人犯错的机会。

个人影响力的建立,既影响自己的业务方,也自己的上下级,所以影响力的建设是贯穿在整个工作中,当然,像我也在生活中不断建设自己靠谱的形象,有百益而无一害。

5.持续反思

做到了上面几点之后,还需要持续反思。人是不可能不犯错的。我其实挺信奉一句话:做的多才会错的多。犯了错不要紧,重要的有没有解决的方案,并且保证下次不会再在同一个错误上犯错

古人云:一日三省吾身。我们做不到一日三省,在每天晚上盘点下今天的所作所为,有哪些还可以持续改进的地方。不光是反思错误,也可以反思自己或者别人做的好的地方,为什么做的好,如何将之总结为一个套路,保证以后类似的事情都可以快速套用。所谓“见贤思齐焉,见不贤而内自省也”,就是这个意思吧。

总结

以上就是我来阿里一年调整和明确的5条经验——先明确核心目标,然后将重要的事项并行推进,最终建立自己高效/靠谱的人设,不断总结反思,反复提升。当有了这个人设之后,做各种事情都是事半功倍,事情做好之后又可以增强你的人设,这样就变成了一个正向转动的飞轮/回路;在工作中有任何的梦想都有机会实现了。

希望这些经验能对你有所帮助,如果有任何问题都欢迎在评论中探讨。

PS:阿里巴巴CBU技术部招收前端开发专家,作为国内最大的B2B交易平台,阿里内部的供货中心,我们目前有P7/P6+ HC若干,期望各种能力很强、人很酷炫的小伙伴。我们位于杭州滨江,有兴趣可以赐简历至 tangmu.zc@alibaba-inc.com,多谢!也可以加微信angeltune保持联系。
查看原文

赞 53 收藏 31 评论 6

金奔 赞了文章 · 2020-04-15

面试官:为什么 Vue 中 template 有且只能一个 root ?(深度解读)

引言

在前段时间看到这样一个关于 Vue 的问题,为什么每个组件 template 中有且只能一个 root?

可能,大家在平常开发中,用的较多就是 templatehtml 的形式。当然,不排除用 JSXrender() 函数的。但是,究其本质,它们最终都会转化成 render() 函数。然后,再由 render() 函数转为 Vritual DOM(以下统称 VNode)。而 render() 函数转为 VNode 的过程,是由 createElement() 函数完成的。

因此,本次文章将会先讲述 Vue 为什么限制 template 有且只能一个 root。然后,再分析 Vue 如何规避出现多 root 的情况。那么,接下来我们就从源码的角度去深究一下这个过程!

一、为什么限制 template 有且只能有一个 root

这里,我们会分两个方面讲解,一方面是 createElement() 的执行过程和定义,另一方面是 VNode 的定义。

1.1 createElement()

createElement() 函数在源码中,被设计为 render() 函数的参数。所以官方文档也讲解了,如何使用 render() 函数的方式创建组件。

createElement() 会在 _render 阶段执行:

...
const { render, _parentVnode } = vm.$options
...
vnode = render.call(vm._renderProxy, vm.$createElement);

可以很简单地看出,源码中通过 call() 将当前实例作为 context 上下文以及 $createElement 作为参数传入。

Vue2x 源码中用了大量的 call 和 apply,例如经典的 $set() API 实现数组变化的响应式处理就用的很是精妙,大家有兴趣可以看看。

$createElement 的定义又是这样:

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
需要注意的是这个是我们手写 render() 时调用的,如果是写 template 则会调用另一个 vm._c 方法。两者的区别在于 createElement() 最后的参数前者为 true,后者为 false。

而到这里,这个 createElement() 实质是调用了 _createElement() 方法,它的定义:

export function _createElement (
  context: Component, // vm实例
  tag?: string | Class<Component> | Function | Object, // DOM标签
  data?: VNodeData, // vnode数据
  children?: any, 
  normalizationType?: number
): VNode | Array<VNode> {
    ...
}

现在,见到了我们平常使用的 createElement()庐山真面目。这里,我们并不看函数内部的执行逻辑,这里分析一下这五个参数:

  • context,是 Vue_render 阶段传入的当前实例
  • tag,是我们使用 createElement 时定义的根节点 HTML 标签名
  • data,是我们使用 createElement 是传入的该节点的属性,例如 classstyleprops 等等
  • children,是我们使用 createElement 是传入的该节点包含的子节点,通常是一个数组
  • normalizationType,是用于判断拍平子节点数组时,要用简单迭代还是递归处理,前者是针对简单二维,后者是针对多维。

可以看出,createElement() 的设计,是针对一个节点,然后带 children 的组件的 VNode 的创建。并且,它并没有留给你进行多 root 的创建的机会,只能传一个根 roottag,其他都是它的选项。

1.2 VNode

前面,我们分析了 createElement() 的调用时机,知道它最终返回的就是 VNode。那么,现在我们来看看 VNode 的定义:

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    ...
  }
  ...
}

可以看到 VNode 所具备的属性还是蛮多的,本次我们就只看 VNode 前面三个属性:

  • tag,即 VNode 对于的标签名
  • data,即 VNode 具备的一些属性
  • children,即 VNode 的子节点,它是一个 VNode 数组

显而易见的是 VNode 的设计也是一个 root,然后由 children 不断延申下去。这样和前面 createElement() 的设计相呼应,不可能会出现多 root 的情况。

1.3 小结

可以看到 VNodecreateElement() 的设计,就只是针对单个 root 的情况进行处理,最终形成树的结构。那么,我想这个时候可能有人会问为什么它们被设计树的结构?

而针对这个问题,有两个方面,一方面是树形结构的 VNode 转为真实 DOM 后,我们只需要将根 VNode 的真实 DOM 挂载到页面中。另一方面是 DOM 本身就是树形结构,所以 VNode 也被设计为树形结构,而且之后我们分析 template 编译阶段会提到 AST 抽象语法树,它也是树形结构。所以,统一的结构可以实现很方便的类型转化,即从 ASTRender 函数,从 Render 函数到 VNode,最后从 VNode 到真实 DOM

并且,可以想一个情景,如果多个 root,那么当你将 VNode 转为真实 DOM 时,挂载到页面中,是不是要遍历这个 DOM Collection,然后挂载上去,而这个阶段又是操作 DOM 的阶段。大家都知道的一个东西就是操作 DOM非常昂贵的。所以,一个 root 的好处在这个时候就体现出它的好处了。

其实这个过程,让我想起红宝书中在讲文档碎片的时候,提倡把要创建的 DOM 先添加到文档碎片中,然后将文档碎片添加到页面中。

二、如何规避出现多 root 的情况

2.1 template 编译过程

在我们平常的开发中,通常是在 .vue 文件中写 <template>,然后通过在 <template> 中创建一个 div 来作为 root,再在 root 中编写描述这个 .vue 文件的 html 标签。当然,你也可以直接写 render() 函数。

在文章的开始,我们也说了在 Vue 中无论是写 template 还是 render,它最终会转成 render() 函数。而平常开发中,我们用 template 的方式会较多。所以,这个过程就需要 Vue 来编译 template

编译 template 的这个过程会是这样:

  • 根据 template 生成 AST(抽象语法树)
  • 优化 AST,即对 AST 节点进行静态节点或静态根节点的判断,便于之后 patch 判断
  • 根据 AST 可执行的函数,在 Vue 中针对这一阶段定义了很多 _c_l 之类的函数,就其本质它们是对 render() 函数的封装

这三个步骤在源码中的定义:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 生成 AST
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
      // 优化 AST
    optimize(ast, options)
  }
  // 生成可执行的函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
需要注意的是 Vue-CLI 提供了两个版本,Runtime-CompilerRuntime,两者的区别,在于前者可以将 template 编译成 render() 函数,但是后者必须手写 render() 函数

而对于开发中,如果你写了多个 root 的组件,在 parse 的时候,即生成 AST 抽象语法树的时候,Vue 就会过滤掉多余的 root,只认第一个 root

parse 的整个过程,其实就是正则匹配的过程,并且这个过程会用栈来存储起始标签。整个 parse 过程的流程图:

然后,我们通过一个例子来分析一下,其中针对多 root 的处理。假设此时我们定义了这样的 template

<div><span></span></div><div></div>

显然,它是多 root 的。而在处理第一个 <div> 时,会创建对应的 ASTElement,它的结构会是这样:

{
    type: 1,
    tag: "div",
    attrsList: [],
    attrsMap: {},
    rawAttrsMap: {},
    parent: undefined,
    children: [],
    start: 0,
    end: 5
}

而此时,这个 ASTElement 会被添加到 stack 中,然后删除原字符串中的 <div>,并且设置 root 为该 ASTElement

然后,继续遍历。对于 <span> 也会创建一个 ASTElement 并入栈,然后删除继续下一次。接下来,会匹配到 </span>,此时会处理标签的结束,例如于栈顶 ASTElementtag 进行匹配,然后出栈。接下来,匹配到 </div>,进行和 span 同样的操作。

最后,对于第二个 root<div>,会做和上面一样的操作。但是,在处理 </div> 时,此时会进入判断 multiple root 的逻辑,即此时字符串已经处理完了,但是这个结束标签对应的 ASTElement 并不等于我们最初定义的 root。所以此时就会报错:

Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

而且,该 ASTElement 也不会加入最终的 AST 中,所以之后也不可能会出现多个 root 的情况。
同时,这个报错也提示我们如果要用多个 root,需要借助 if 条件判断来实现。

可以看出,template 编译的 parse 阶段的最终目标就是构建一个 AST 抽象语法树。所以,它会在创建第一个 ASTElement 的时候就确定 ASTroot,从而确保 root 唯一性。

2.2 _render 过程

不了解 Vue 初始化过程的同学,可能不太清楚 _render 过程。你可以理解为渲染的过程。在这个阶段会调用 render 方法生成 VNode,以及对 VNode 进行一些处理,最终返回一个 VNode

而相比较 template 编译的过程,_render 过程的判断就比较简洁:

if (!(vnode instanceof VNode)) {
  if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
    warn(
      'Multiple root nodes returned from render function. Render function ' +
      'should return a single root node.',
      vm
    );
  }
  vnode = createEmptyVNode();
}

前面在讲 createElement 的时候,也讲到了 render() 需要返回 VNode。所以,这里是防止部分骚操作,return 了包含多个 VNode 的数组。

结语

通过阅读,我想大家也明白了为什么 Vue 中 template 有且只能一个 root ?Vue 这样设计的出发点可能很简单,为了减少挂载时 DOM 的操作。但是,它是如何处理多 root 的情况,以及相关的 VNodeASTcreateElement() 等等关键点,个人认为都是很值得深入了解的。

写作不易,如果你觉得有收获的话,可以帅气三连击!!!
查看原文

赞 25 收藏 15 评论 3

金奔 赞了回答 · 2020-02-23

解决axios无法发起跨域请求

而withCredentials=true只是说明该跨域请求允许带上cookie信息.

关注 21 回答 11

金奔 赞了文章 · 2019-10-22

魅族官网基于 next.js 重构实践总结与分享

项目背景

俗话说,脱离业务谈代码的都是耍流氓。在此我先简单介绍下重构项目的背景。

截图镇楼:魅族官网首页

在 2015 年,公司前端大佬猫哥基于 FIS3 深度定制开发了一套前端工程体系 mz-fis,该框架经历3年来的网站改版升级需求,都很好的完成了需求任务。 但随着项目越来越大,以及前端技术快速迭代。老项目的痛点越发明显。

此次重构解决了那些痛点

1.随着项目越来越大,前端编译打包流程巨慢。(算上图片视频等资源,仓库有3.9G大小)
2.运营需要经常改动网站内容,由于需要SEO,哪怕改几个字也需要前端打包发布。
3.旧框架的核心还是Jquery,虽然结果3年开发积累了很多组件,但在数据维护、模块化以及开发体验上已经落后了。

以上痛点想必手上有老项目的,都感同身受。改起来伤筋动骨,但不改吧工作效率太低了。

此次重构需要满足哪些要求

再说说重构的基本要求,咱得渐进增强而不是优雅降级。:D

1.支持SEO,也就是说需要服务端渲染。
2.解放前端、测试劳动力,让运营在网站内容管理平台编辑数据后发布,官网及时生效。(不同于传统AJAX,这里数据需要SEO)。
3.支持多国语言。
4.需要新旧框架同存,同域名下无缝对接,要求两套工作流都可以正常工作。(一些不频繁改动的页面,可以不改,减少重构成本)。
5.更快的页面性能、更畅快的开发体验和更好可维护性。

此次重构技术选型

首先,服务端渲染 SSR 是没跑了,它可以更快渲染首屏,同时对 SEO 更友好。

于是我在带着鸭梨与小兴奋寻遍各大SSR方案后,最终选择了 Next.js
Next.js 是一个轻量级的 React 服务端渲染应用框架。目前在 github 已获得 4W+ 的 star。

之所以火爆,是因为它有以下优点:
1.默认服务端渲染模式,以文件系统为基础的客户端路由
2.代码自动分隔使页面加载更快
3.简洁的客户端路由(以页面为基础的)
4.以webpack的热替换为基础的开发环境
5.使用React的JSX和ES6的module,模块化和维护更方便
6.可以运行在其他Node.js的HTTP 服务器上
7.可以定制化专属的babel和webpack配置

这里不做过多讲解了,大家可以访问 next.js中文网github地址了解更多。

重构过程中遇到的问题以及解决方案

问题一:网站采用 next.js 的 start 模式服务,还是 export 出静态化文件让 ngxin 做web服务

两种方案都可行,但各有优缺点。

考虑到运营并不在乎那点等待时间,相比之下项目稳定性更重要。于是选择方案二:「export 出静态化文件让 ngxin 做web服务」。

ok~ 选定后要做的就是静态化了。

问题二:如何静态化

如何做呢?

恩... 最简单的就是 cd 到项目目录下 npm run build && npm run export 下,打包出文件到./out文件夹,然后打个zip包扔服务器上。
当然,为了运营数据及时更新,你得24小时不停重复以上步奏,还不能手抖出错。

为了不被同事打死,我设计了一套开发流程,在项目中写一个shell脚本(感谢 @青菜童鞋,做了容错和美化):

#!/bin/bash
echo node版本:$(node -v)
BASEDIR=$(dirname $0)
cd ${BASEDIR}/../
sudo npm run build

while true;
do
    whoami && pwd
    sudo npm run export >/dev/null 2>&1 || continue
    sudo chown -R {服务器用户名} ./out || echo 'chown Err'
    sudo cp -ar ./out/* ./www || echo 'cp Err'
    sudo chown -R {服务器用户名} ./www || echo 'chown Err'
    echo '静态化并复制完毕'
    sleep 15
done

好了,只要执行这段 shell,你的服务器就会cd到项目目录,先build构建项目,然后每间隔15秒构建一次。并输出当前环境和相关信息。

但不停 export 就够了么,显然不是。

我们知道 export 只能更新异步API请求的数据。如果对项目代码做改动,比如新增个页面啥的。那需要重新 npm run build然后再 export。

那就要按顺序完成一下小步骤:
1.kill 循环中的 export 进程;
2.等待服务器 git 拉取完代码,并且npm install 项目依赖;
3.重新 build,并且循环 export;

为了方便管理进程和输出日志,我们可以用 pm2 来维护。

// ecosystem.config.js
const path = require('path')

module.exports = {
  /**
   * Application configuration section
   * http://pm2.keymetrics.io/docs/usage/application-declaration/
   */
  apps: [
    {
      name: 'export_m',
      script: path.resolve(__dirname, 'bin/export_m.sh'),
      env: {
        COMMON_VARIABLE: 'true'
      },
      env_production: {
        NODE_ENV: 'production'
      },
      log_date_format: "YYYY-MM-DD HH:mm:ss"
    }
  ]
}

有 pm2 管理进程,我们只需在git仓库更新,并install之后,执行pm2 startOrRestart ecosystem.config.js就ok拉。

此外,实践中遇到个情况。在性能比较差的服务器上,export 进程时间长了,有可能卡死。对此可以设置linux 定时任务重启进程。当然配置高的服务器可以忽略。

1.进入服务器 输入 crontab -e
2.另起一行,输入*/30 * * * * pm2 startOrRestart {你的项目路径}/ecosystem.config.js
3.wq保存任务

搞定。

问题三:工作流以及 next.js 坑爹 build_id 的解决方案

前面解决了如何静态化,那么如何更新部署呢? 这就涉及到工作流的问题了。

此次构建大致工作流:

简单描述下图中流程:

一.npm run dev 本地开发(资源不压缩,且资源路径都在本地)


这一步就是开发,没啥好说。。。

二.npm run build,并推送资源

npm run build后,资源都被webpack压缩了。
因为设置了CDN,js、css 图片等资源的路径会被 webpack 改成 cdn 绝对地址。那么你需要把对应的资源发布到CDN服务器上。

到这细心的童鞋可能注意到图中有个 **更新 BUILD_ID,其实这里隐藏着一个 next.js 不小的坑。
**

啥坑咧?

我们随便下载一个next.js的官网 demo,在本地 build 后 npm start 一下,然后打开网页看js。

如图,next.js 生成一个长长的路径,下面的main.js 生成了一串hash。

第一个路径值,跟项目里next.js 生成的BUILD_ID内容一致

ok!这时候一切正常,接下来我们不对项目代码做任何修改,重新 build 一次

你会发现,BUILD_ID 值变了。

那么 buildID 和 url 如此善变,会引发什么问题呢?
【1】相同源码下,不同服务器生成的静态资源和引用不一致。风险大。
【2】相同源码下,多次构建内容相同,url 却不同,浪费资源,还让 CDN 缓存意义大打折扣。
【3】开发和测试人员在多服务器部署情况下,不好做版本控制,难以逆向追踪 bug。

如果翻开 next.js 源码,你会发现 next.js 每次是用一个叫 nanoid 的库随机生成 String 值。

为什么要这么设计呢?如果 next.js 生成的所有资源都能像 main.js 一样根据文件内容来 hash 命名,岂不美哉?

为此,我曾经在 next.js github 的相关 issues 上问过作者,得到的答复大概意思是,由于 next.js 服务端渲染的特性,每次 build 需要编译两次,两次编译生命周期有所不同难以映射,所以用随机的id存到 BUILD_ID 里当变量,用来解决编译文件引用和路由问题。

当时作者的意思是,短期内解决不了这个特性。(囧。。。

如何解决这个难题呢?

其实 next.js 官方也考虑到这个情况。你可以在 next.config.js 里重写 build_id。

module.exports = {
  generateBuildId: async () => {
    return 'static_build_id'
  }
}

但这样,ID就写死了,更新迭代无法清客户端缓存。除非你每次发布手动更改 ID 值,这么 low 的做法显然不可取。

本次重构的解决方案是在需要发版本时执行以下操作:
1.把 logId 写入到 ./config/VERSION_ID 文件夹 ---- 这是为了方便不同服务器之间同步ID。因为生产环境没有 git 仓库。

2.
在项目 package.json 里配置 script, "update": "sh ./bin/update_version.sh"。

#!/bin/bash

echo "\033[33m ------- 开始检测 git 仓库状态 ------- \033[0m\n"

git_status=`git status`
git_pull="update your local branch"
git_clean="nothing to commit, working tree clean"


if [[ $git_status =~ $git_pull ]]
then

  echo "\033[31m ------- 请更新你的 git 仓库 ------ \033[0m \n"
  exit

else

  # 把最新版本号写入 VERSION_ID
  git_log=`git log --oneline --decorate`
  ID=${git_log:0:7}
  
  echo $ID > ./config/VERSION_ID 

  echo "------- 发布静态资源到 测试环境 -------\n"

  npm run deploy

  echo "\033[32m \n------- 版本号已更新为$ID,并成功发布资源到测试环境 -------\033[0m \n"

  echo "\033[32m \n------- 请及时 commit git 仓库,并 push 到远程 -------\033[0m \n"

  exit

fi

2.读取./config/VERSION_ID,然后存入环境变量 BUILD_ID。

#!/bin/bash
BASEDIR=$(dirname $0)
build_id=$(cat ${BASEDIR}/config/VERSION_ID)
echo --------- 编译版本号为 $build_id -----------
export BUILD_ID=$build_id

3.更改 next.config.js 配置为以下,然后 build。

module.exports = {
  generateBuildId: async () => {
    if (process.env.BUILD_ID) {
      return process.env.BUILD_ID
    }
    return 'static_build_id'
  }
}

这样,只要不做npm run update, 在不同服务器下,随便 build 多少次。内容都不会变了。

至于发布平台,本项目使用 jenkins 搭建一套。

以测试环境的配置为例:

如此,只要确保代码更新到 git,登录 jenkins 运行下任务就上测试环境拉。 当然也可以利用插件监听 git 的 push 动作自动执行任务。这个就看个人喜好了。

问题四:如何兼容旧架构

要兼容,至少得满足2点:
1.新架构不影响旧架构功能。即原来的工作流依然可以正常部署。
2.新旧架构在同域名下共存,新架构满足新增页面、迭代页面需求。

作为多页面应用。新旧架构都是用 ngxin 做 web 服务器,那么解决起来也很简单。只需要做好 ngxin 的 config 配置就好了。

以下是 ngxin 配置思维图:

nginx 配置示例

server{
    listen 80;
    listen  443;
    ssl     on;
    ssl_certificate     {crt文件};
    ssl_certificate_key {key文件};
    server_name www.meizu.com;

    root {老架构目录路径}/www.meizu.com;
    index landing.html index.html;
    ssi on;
    ssi_silent_errors on;

    error_log /data/log/nginx/error.log;
    access_log /data/log/nginx/access.log;

    location / {
        try_files $uri $uri/index.html $uri.html @node; 
    }

    location @node {
        proxy_pass http://127.0.0.1:8008;
    }

}

server{
    listen 8008;

    root {新架构目录路径}/www;
    index index.html;

    error_page 500 502 503 504 /500.html;
    error_page 404 /404.html;

    location / {
        try_files $uri $uri/index.html $uri.html 404;
    }

}

这里 80、443 端口进来会先判断第一个 root 目录是否存在对应路由。如果存在则直接响应,如果不存在,则走 8008 服务的 root 目录,都不存在则返回 404、500之类的。

如此一来,新建页面在新的工作流直接发布就行,而需要迭代,重构页面后把老项目里对应文件重命名或者删除就行。

如何支持 i18n (国际化)

由于本项目 95% 图文都托管给数据平台了,类似于 i18next 这样的本地多国语言方案,我们并不需要了。

我们只需要做以下两步:
1.按需将一个产品模板文件,导出成多个不同语言的 html。
2.静态化时,根据不同语言获取对应的数据。

先来解决第一个问题。
next.js 提供了自定义的静态化路由配置。例如:

// next.config.js
module.exports = {
  exportPathMap: async function (defaultPathMap) {
    return {
      '/': { page: '/' },
      '/about': { page: '/about' },
      '/home': { page: '/home' }
    }
  }
}

那么我们就可以获取项目 pages 目录下的文件路径来生成一个 map 表,并对其遍历改造。

/****
 * 规则:
 * 中文页面,会根据 page 目录自动生成路由
 * --------  [mapConfig] ---------
 * key 为产品名
 * [rename] 中文产品更名 (实际目录名以英文为标准)
 * [transform] 产品或页面转化为其他语言
 *
 * --------- [include] ---------
 * [include] 手动追加路由表
 *
 * --------- [exclude] ---------
 * [exclude] 手动删除路由表

*/
const glob = require('glob')

const map = {
  mapConfig: { // 在此编辑产品名称即可
    m6: {
      rename: 'meilan6',
      transform: ['en']
    },
    "16s": {
      transform: ['en']
    },
    "16xs": {
      transform: ['en']
    }
  },
  include: {  // 可以手动新增
    '/': { page: '/' }
  },
  exclude: [] // 可以手动新增
}

/** ------------------  以下为 map 表的格式转换处理   ---------------------- **/

let defaultPathMap = {}

const pathList = glob.sync('./pages/**/!(_)*.js').map(c => c.replace('./pages', '').replace(/\.js$/, '.html'))

const mapConfig = map.mapConfig

pathList.forEach(c => {
  //首页
  if (c === '/' || c === '/index.html') return false

  // 目录下的index.html
  if (/\/index\.html$/.test(c)) {
    defaultPathMap[c] = { page: c.replace(/\/index\.html$/, '') }

    // 目录下的index.html
  } else {
    defaultPathMap[c] = { page: c.replace(/\.html$/, '') }
  }

})

// 这一步是针对产品中英文重命名。比如国内 meilan6,国外为m6,由 customPathMap.js 配置
for (let key in defaultPathMap) {
  let pageName = ''
  for (let configKey in mapConfig) {
    /* eslint-disable */
    const pageReg = new RegExp(`/${configKey}[\/|\.]`)
    /* eslint-enable */
    if (pageReg.test(key)) {
      // step-1 新增中文重命名
      if (mapConfig[configKey].rename !== undefined) {
        pageName = key.replace(pageReg, `/${mapConfig[configKey].rename}/`)
        defaultPathMap[pageName] = defaultPathMap[key]
      }
      //step-2 转变国家
      if (mapConfig[configKey].transform !== undefined && mapConfig[configKey].transform.length > 0) {
        mapConfig[configKey].transform.forEach(c => {
          defaultPathMap[`/${c}${key}`] = { ...defaultPathMap[key], pageLang: c }
        })
      }
      //step-3 删除中文已经被重命名的路由
      if (mapConfig[configKey].rename !== undefined) {
        delete defaultPathMap[key]
      }
    }
  }
}

map.exclude.forEach(c => {
  delete defaultPathMap[c]
})

module.exports = {
  ...map.include,
  ...defaultPathMap
}

如此,通过编辑 mapConfig 对象,会导出一个转化后的 map 表。然后使用它。

// next.config.js
const customPathMap = require('./config/customPathMap')

module.exports = {
  exportPathMap: async function (defaultPathMap) {
    return customPathMap
  }
}

ok,现在一套模板可以渲染出两个 html 了, 比如说 pages/accessory/tw50s.js 可以渲染出 https://www.meizu.com/accesso...https://www.meizu.com/en/acce...

那接下来要做的,就是根据语言,获取不同的数据了。

第一步,根据 URL 判断页面的语言。并存入 Redux 的 Store

// pages/_app.js

import 'core-js';
import React from "react"
import { Provider } from "react-redux"
import App, { Container } from "next/app"
import withRedux from "next-redux-wrapper"
import { initStore } from '../store'

class MyApp extends App {
  /**
   * 在 _app.js 初始化国家码
   * 设置全局 store.lang,默认为 cn
   * */
  static async getInitialProps({ Component, ctx }) {
  
    const countryMap = ['cn', 'en', 'hk', 'es'] // 语言列表
    let lang = 'cn'
    const reg = /\/([a-z]+)\/?/
    const langMatch = ctx.req.url.match(reg) ? ctx.req.url.match(reg)[1] : null
    const langIndex = countryMap.indexOf(langMatch)
    
    if (langMatch && langIndex !== -1) lang = countryMap[langIndex]
    ctx.store.dispatch({ type: 'LANG_INIT', lang })

    let pageProps
    try {
      pageProps = Component.getInitialProps ? await Component.getInitialProps(ctx) : {}
    } catch (err) {
      pageProps = {}
    }
    return { pageProps };
  }

  render() {
    const { Component, pageProps, store } = this.props;
    return (
      <Container>
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      </Container>
    );
  }
}
export default withRedux(initStore)(MyApp);

第二步,在页面 getInitialProps 生命周期获取当前语言数据。

示例代码:

// pages/accessory/tw50.js

class Index extends React.PureComponent {
  static async getInitialProps(ctx) {
    // 获取页面语言
    const lang = ctx.store.getState().lang
    
    // 获取数据接口 ID 号,作为参数
    const blockIds = getBlockIds(lang, 'header', 'footer', 'subnav', 'tw50s') 
    
    let pageData
    try {
      //请求数据
      pageData = await getDmsDataById(blockIds)
      
    } catch (err) {
      pageData = {
        data: []
      }
    }
    return {
      dmsData: pageData.data, // 数据
      lang
    }
  }
}

哦了~

迟到一年的总结差不多了,虽然关于 next.js 还有不少可说的,比如 webpack 自定义配置,cdn资源发布的流程与优化等等。以后有时间有心情再给大家唠嗑。

查看原文

赞 51 收藏 20 评论 19

认证与成就

  • 获得 1 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-11
个人主页被 232 人浏览