微信网页开发,小程序webview 开发

公众号网页开发

网页授权

回调域名设置:到公众平台官网中的“开发 – 接口权限 – 网页服务 – 网页帐号 – 网页授权获取用户基本信息”的配置选项中,是填写全域名,不能是泛域名。

特殊场景下的静默授权

  1. 上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知;
  2. 对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知。

网页授权流程分为四步:

1. 引导用户进入授权页面同意授权,获取code

https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx807d86fb6b3d4fd2&redirect_uri=http%3A%2F%2Fdevelopers.xxx.com&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

授权后 重定向回来 redirect_uri/?code=CODE&state=STATE (redirect_uri 可以是后端接口地址,获取到code 后直接发起下一步)

2. 通过code换取网页授权access_token(与基础支持中的access_token不同)( 服务器发起)

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

{
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE",
  "is_snapshotuser": 1,
  "unionid": "UNIONID"
}

3. 如果需要,开发者可以刷新网页授权access_token,避免过期

https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN

4. 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

{   
  "openid": "OPENID",
  "nickname": NICKNAME,
  "sex": 1,
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",
 "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

js-sdk

js-sdk是微信公众平台 面向网页开发者提供的基于微信内的网页开发工具包,通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验

使用步骤

1 绑定域名

先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

2 引入JS文件

在需要调用JS接口的页面引入如下JS文件,(支持https):http://res.wx.qq.com/open/js/jweixin-1.6.0.js

如需进一步提升服务稳定性,当上述资源不可访问时,可改访问:http://res2.wx.qq.com/open/js/jweixin-1.6.0.js (支持https)

3 通过config接口注入权限验证配置

所有需要使用JS-SDK的页面必须先注入配置信息,否则将无法调用(同一个url仅需调用一次,对于变化url的SPA的web app可在每次url变化时进行调用)

wx.config({
  debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
  appId: '', // 必填,公众号的唯一标识
  timestamp: , // 必填,生成签名的时间戳
  nonceStr: '', // 必填,生成签名的随机串
  signature: '',// 必填,签名
  jsApiList: [] // 必填,需要使用的JS接口列表
});

4 .通过ready接口处理成功验证,error接口处理失败验证

wx.ready(function(){
  // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
});

wx.error(function(res){
  // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
});

完整示例代码

const init = async () => {
           const _unitGetSign = function () {
							return new Promise(async (resolve, reject) => {
                 // 获取签名数据
								let data = await api.get(`${config.host}/account/jssdkSignature`, {
									url: location.href,
									activityCode: activityCode,
								}, {
									method: 'GET',
									contentType: 'application/json'
								})
								wx.signatureDone = true;
								let res = data.value;
								wx.config({
									debug: false, // 留一个开关来启用调试模式
									appId: res.appId,
									timestamp: res.timestamp,
									nonceStr: res.noncestr,
									signature: res.signature,
									jsApiList: 'updateAppMessageShareData;updateTimelineShareData;checkJsApi;onMenuShareTimeline;onMenuShareAppMessage;onMenuShareQQ;onMenuShareWeibo'.split(';')
								})
								wx.ready(function () {
									resolve(res)
								})
							})
						}
       const initApis = () => {
          //需在用户可能点击分享按钮前就先调用
          wx.updateAppMessageShareData({ 
              title: '', // 分享标题
                desc: '', // 分享描述
               link: '', // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
                imgUrl: '', // 分享图标
              success: function () {
                // 设置成功
              }
           })
          wx.hideMenuItems({
								menuList: ['menuItem:copyUrl'] // 要隐藏的菜单项,只能隐藏“传播类”和“保护类”按钮
							});
       }

      if (!window.wx) {
					await	utilsDynamicFile(['http://res2.wx.qq.com/open/js/jweixin-1.6.0.js ']);
           await  _unitGetSign();  // 如果是单页面可以通过	wx.signatureDone 判断是否已授权
           initApis(shareConfig)
					} else {
						await  _unitGetSign();
             initApis(shareConfig)
					}
}

签名获取 (服务器上进行)

1.参考以下文档获取access_token(有效期7200秒,开发者必须在自己的服务全局缓存access_token)https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html

2. 用第一步拿到的access_token 采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi

3 生成签名(见链接),并返回给前端 ,连同签名用的noncestr和timestamp也返回,必须与wx.config中的nonceStr和timestamp相同。

开放标签

可以实现公众号网页跳转 小程序,App,服务号订阅通知按钮,音频播放

<wx-open-launch-weapp>

<wx-open-launch-app>

<wx-open-subscribe>

<wx-open-audio>

小程序Web-view

webview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域名

web-view 引用sdk 不需要签名授权

 const ready = async () => {
      console.log(window.__wxjs_environment === 'miniprogram') // true
			if(!window.wx){
				await utilsDynamicFile(['//res.wx.qq.com/open/js/jweixin-1.3.2.js']);
			}
      wx.miniProgram.postMessage({ data: {title, imgUrl: imagePath, link, shareCode: isNeedShareCode ? (activityConfig.selfShareCode || xCubeShareCode || '') : ''} });
    }
    if (!window.WeixinJSBridge || !window.WeixinJSBridge.invoke) {
      document.addEventListener('WeixinJSBridgeReady', ready, false)
    } else {
      ready()
    }

Umi + qiankun 没有单独域名,用二级路径转发

/xman-api 域名二级路径
/xmancloud 主应用路径
/xmancloud/playboxmicro 子应用路径
/xmancloud/commonmicro 子应用路径
/tptplaybox 活动路径

1. nginx 配置

#主应用
location /xmancloud {
    try_files $uri $uri/ /xmancloud/index.html;
    if ($request_filename ~* .*\.(?:htm|html)$) {
      add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
      add_header X-Frame-Options ALLOWALL;
    }
    alias /mnt/nasdata/nginx/html/static/xman-cloud/;
}

# 子应用
location /xmancloud/commonmicro/{
   add_header Access-Control-Allow-Origin '*';
    add_header Access-Control-Allow-Credentials 'true';
    add_header Access-Control-Allow-Methods '*';
    add_header Access-Control-Allow-Headers '*';
    if ($request_method = 'OPTIONS') {
      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,Pragma,sec-ch-ua,sec-ch-ua-mobile,Authorization,Accept';
      return 200;
    }
    if ($request_method = 'POST') {
      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,Pragma,sec-ch-ua,sec-ch-ua-mobile,Authorization,Accept';
    }
    if ($request_method = 'GET') {
      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,Pragma,sec-ch-ua,sec-ch-ua-mobile,Authorization,Accept';
    }
    if ($request_filename ~* .*\.(?:htm|html)$) {
      add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
    }
    alias /mnt/nasdata/nginx/html/static/xman-common/;
    index  index.html index.htm;
    #try_files $uri $uri/ /index.html;
}

2. 项目配置修改

主 umi 打包配置

  • cloud主应用 publicPath 修改为/xmancloud/, 这意味着静态资源引用都会加上 /xmancloud/**.js
  • 子应用publicPath 修改为/xmancloud/commonmicro/ 这意味着静态资源引用都会加上 /xmancloud/commonmicro/**
  • cloud主应用路由规则设置 base 修改为 /xmancloud, 这样路由中都会加上/xmancloud ,刷新后nginx才会定位到cloud项目
  • 子应用的base 是(主应用的base + routes里 子应用的path)注意 “/”不要重复,/xmancloud/common,而且是主应用传递给子应用的,子应用不需要自己配置,子应用配置base也不会生效;可以从子应用的.umi/pluginqiankun/lifecycles.ts 看到 props.base
  • 主应用 routes 不需要加xmancloud

注意

  • 主应用加base: /xmancloud routes path:’/common’, 如果页面路径是/common/login 也会命中子应用,但子应用收到主应用给的 base 是/xmancloud/common,页面不会展示, 页面路径需要/xmancloud/common/login 才行
  • 当主应用的base没有配置或者’/’, 主应用会把 routes 里的path,当作子应用的base 传给子应用。

前端目录

项目目录结构规范
|-- Dockerfile
|-- nginx_app.conf
|-- start.sh
|-- ***
|-- src
|  |---- env.ts // 运行时的环境判断脚本
|  |---- assets // 资源文件目录
|  |---- components // 通用组件目录
|  |---- constants // 全局常量目录
|  |---- hooks // 通用hooks,可参照ahooks
|  |---- layouts // 布局组件目录
|  |---- less // 通用样式目录
|  |---- locales // 语言包目录
|  |---- models // 公共数据处理目录
|  |---- pages // 页面目录
|  |---- services //公共接口请求目录
|  |---- utils // 工具类
|  |   |----- request.ts // 用于处理通用请求方法的文件
|  |---- app.ts
|  |---- global.less
页面代码目录结构规范
|-- pages
|  |--- login
|  |   |---- index.tsx
|  |   |---- index.less
|  |   |---- models // 非必须
|  |   |---- services // 模块独立API
|  |   |---- components // 非必须
组件代码目录结构规范
|-- components
|  |--- AComponents // 大驼峰
|  |   |---- index.tsx
|  |   |---- index.less/style.less
|  |   |---- components // 组件下可能也会出现独立的小单元
 数据处理规范
|-- src
|  |---- models // 公共数据处理目录
|  |   |----- global.ts // 处理一些通用性数据,如后端枚举等等
|  |   |----- users.ts // 用于处理用户数据
...
model内容包含store、reducer、effect等

组件命名规范(当前项目统一)
组件文件夹: 1. 大驼峰写法,FlatList(推荐为统一写法);2. 分割线写法,flat-list;
组件命名:1. 大驼峰写法,FlatListItem

通用样式、通用工具类
|-- less
|  |--- variable.less // 主题色、主题样式数据定义
|  |--- antdCover.less | antd_cover.less // antd的样式覆盖
|  |--- common.less // 通用样式
|-- utils
|  |--- request.ts // 发送请求的集合及错误统一处理
|  |--- utils.ts // 通用方法的集合
|  |--- date.ts // 日期时间的处理
...

两种组件的实现规范
class组件:
@。。。// 修饰器
export default class extends React.Component
生命周期
render

functional组件:推荐使用
使用react提供的hooks对方法进行包装
使用aHooks提供的hooks方法作为通用方法

应用发布配置
两种方式:1. 通过本地nginx文件进行配置; 2. 通过ship进行环境配置映射到nginx配置
文件依赖: Dockerfile、 nginx_app.conf、 start.sh
TODO:相关文件规范,待补充;
List: 
utils、nginx、dockerfile、start.sh、组件实现模板;
本地化登录逻辑的整合并统一化;

微前端

微前端作为用户界面的一部分,通常由许多组件组成,并使用类似于React、Vue和Angular等框架来渲染组件。每个微前端可以由不同的团队进行管理,并可以自主选择框架。虽然在迁移或测试时可以添加额外的框架,出于实用性考虑,建议只使用一种框架。

每个微前端都拥有独立的git仓库、package.json和构建工具配置。因此,每个微前端都拥有独立的构建进程和独立的部署/CI。这通常意味着,每个仓库能快速构建。

主框架的定位则仅仅是:导航路由 + 资源加载框架

路由系统

正常访问一个子应用的页面时,浏览器地址 https://app.alipay.com/subApp/123/detail, 当刷新时 主应用的路由系统已经激活,但子应用的资源可能还没有完全加载完毕,从而导致路由注册表里发现没有能匹配子应用 /subApp/123/detail 的规则, 可能出现 404。

我们需要设计这样一套路由机制:

主框架配置子应用的路由为 subApp: { url: '/subApp/**', entry: './subApp.js' },则当浏览器的地址为 /subApp/abc 时,框架需要先加载 entry 资源,待 entry 资源加载完毕,确保子应用的路由系统注册进主框架之后后,再去由子应用的路由系统接管 url change 事件,同时在子应用路由切出时,主框架需要触发相应的 destroy 事件,子应用在监听到该事件时,调用自己的卸载方法卸载应用,如 React 场景下 destroy = () => ReactDOM.unmountAtNode(container)

JS Entry vs HTML Entry

在确定了运行时载入的方案后,另一个需要决策的点是,我们需要子应用提供什么形式的资源作为渲染入口?

JS Entry 的方式通常是子应用将资源打成一个 entry script,但这个方案的限制也颇多,如要求子应用的所有资源打包到一个 js bundle 里,包括 css、图片等资源。除了打出来的包可能体积庞大之外的问题之外,资源的并行加载等特性也无法利用上

HTML Entry 则更加灵活,直接将子应用打出来 HTML 作为入口,主框架可以通过 fetch html 的方式获取子应用的静态资源,同时将 HTML document 作为子节点塞到主框架的容器中。这样不仅可以极大的减少主应用的接入成本,子应用的开发方式及打包方式基本上也不需要调整,而且可以天然的解决子应用之间样式隔离的问题(后面提到)。想象一下这样一个场景

如果是 JS Entry 方案,主框架需要在子应用加载之前构建好相应的容器节点(比如这里的 “#root” 节点),不然子应用加载时会因为找不到 container 报错。但问题在于,主应用并不能保证子应用使用的容器节点为某一特定标记元素。而 HTML Entry 的方案则天然能解决这一问题,保留子应用完整的环境上下文,从而确保子应用有良好的开发体验

HTML Entry 方案下,主框架注册子应用的方式则变成:

framework.registerApp('subApp1', { entry: '//abc.alipay.com/index.html'})

样式隔离

由于微前端场景下,不同技术栈的子应用会被集成到同一个运行时中,所以我们必须在框架层确保各个子应用之间不会出现样式互相干扰的问题。

Dynamic Stylesheet

解决方案其实很简单,我们只需要在应用切出/卸载后,同时卸载掉其样式表即可,原理是浏览器会对所有的样式表的插入、移除做整个 CSSOM 的重构,从而达到 插入、卸载 样式的目的。这样即能保证,在一个时间点里,只有一个应用的样式表是生效的。

上文提到的 HTML Entry 方案则天生具备样式隔离的特性,因为应用卸载后会直接移除去 HTML 结构,从而自动移除了其样式表。

比如 HTML Entry 模式下,子应用加载完成的后的 DOM 结构可能长这样:
<html>
  <body>
    <main id="subApp">
      // 子应用完整的 html 结构
      <link rel="stylesheet" href="//alipay.com/subapp.css">
      <div id="root">....</div>
    </main>
  </body>
</html>
当子应用被替换或卸载时,subApp 节点的 innerHTML 也会被复写

JS 隔离

解决了样式隔离的问题后,有一个更关键的问题我们还没有解决:如何确保各个子应用之间的全局变量不会互相干扰,从而保证每个子应用之间的软隔离?

针对 JS 隔离的问题,我们独创了一个运行时的 JS 沙箱。简单画了个架构图:

即在应用的 bootstrap 及 mount 两个生命周期开始之前分别给全局状态打下快照,然后当应用切出/卸载时,将状态回滚至 bootstrap 开始之前的阶段,确保应用对全局状态的污染全部清零。而当应用二次进入时则再恢复至 mount 前的状态的,从而确保应用在 remount 时拥有跟第一次 mount 时一致的全局上下文。

当然沙箱里做的事情还远不止这些,其他的还包括一些对全局事件监听的劫持等,以确保应用在切出之后,对全局事件的监听能得到完整的卸载,同时也会在 remount 时重新监听这些全局事件,从而模拟出与应用独立运行时一致的沙箱环境。

传统iframe

在入口框架中用iframe来显示子模块的页面,切换子模块时,iframe也跟着切换成对应子模块页面的url。

虽然iframe是比较容易实现的,但通常也会有一些问题:

  • 显示区域受限制,比如子项目中显示弹窗蒙层时,蒙层只会覆盖iframe区域,无法覆盖整个页面,内容也无法真正居中。
  • 页面浏览记录无法自动被记录,刷新页面后iframe又自动回到首页。
  • 全局上下文完全隔离,变量不共享,页面间通信比较麻烦,比如子项目与主题框架、子项目之间通信等,只能采用postMessage方式。
  • 速度较慢,每次进入子应用时都要重建整个上下文。

代表的框架 single-spa

微前端类型

  1. single-spa applications:为一组特定路由渲染组件的微前端。
  2. single-spa parcels: 不受路由控制,渲染组件的微前端。
  3. utility modules: 非渲染组件,用于暴露共享javascript逻辑的微前端。

微前端通信

import {thing} from 'other-microfrontend'是微前端间通信的首选方式。详细文档

性能

相比于原生应用,微前端性能更佳。这是由于懒加载 和其他相关的优化。微前端为我们提供一种迁移方式,从而解决我们原生项目中隐藏的问题。出于性能考虑,强烈建议框架(如:React, Vue, or Angular等)级别的实例仅引用一次,具体做法参考

拆分应用

大多数的微服务体系都鼓励独立的代码仓库、构建和部署。虽然 single-spa不能解决如何托管、构建或部署 代码的问题,但是这些问题与许多single-spa用户相关,因此这里讨论了一些策略。

MonorepoNPM包动态加载模块
搭建难度简单中等困难
代码是否独立NoNo
分开构建No
分别部署No
例子simple-webpack-examplesingle-spa-examplessingle-spa-login-example-with-npm-packagesSystemJS example
缺点灵活性和自由度不足,项目越来越大时,打包速度越来越慢当single-spa应用发生更改时,根应用程序应该重新安装、重新构建和重新部署

动态加载模块

创建一个父应用,允许子应用单独部署。为了实现这一点,创建一个manifest文件,当子应用部署更新时,它控制子应用的“上线”版本及加载的JavaScript文件。

改变每个子应用加载的JavaScript文件有很多的方法:

  1. Web服务器:在你的web服务器为每个子应用的正确版本创建一个动态脚本。
  2. 使用模块加载 例如 SystemJS 可以在浏览器通过动态urls下载并执行JavaScript代码。

基于single-spa的开源库qiankun

admin 菜单管理

全局state 设置

const permissionReflect = {
  '/task': [
    '/task/subTask', // 任务组详情
  ],

  '/activityBox': [
    '/activityBox/pages/selectTpl',
    '/activityBox/pages/baseInfo',
    '/activityBox/pages/detail',
    '/activityBox/pages/resourceLocation',
  ],
};
// reduce 添加菜单权限
setUserPermission(
      state,
      {
        payload: { data },
      },
    ) {
      // 菜单权限
      let permission = [];
      // // 按钮权限
      const btnPermission = [];
      // // 功能权限
      const funPermission = [];
      const permissionOpts = data.value.reduce((accumulator, currentValue) => [...accumulator, currentValue, ...currentValue.subMenus], []);
      permissionOpts.forEach(item => {
        // 按钮权限以btn_开头
        if (item && item.url) {
          if (/^btn_\w+$/.test(item.url)) {
            btnPermission.push(item.url);
          } else if (/^fun_\w+$/.test(item.url)) {
            funPermission.push(item.url);
          } else {
           // 本地附属菜单存在,则合并附属菜单
            const getReflect = permissionReflect[item.url];
            if (getReflect) {
              permission = [...permission, item.url, 
                        ...getReflect];
            } else {
              permission.push(item.url);
            }
          }
        }
      });
      return {
        ...state,
        userPermission: data.value,
        permission,
        btnPermission,
        gotPermissions: true,
      };
    },

设置 左边栏展示菜单

  1. 编写左边栏本地静态菜单 对象
  2. 菜单权限列表请求后,更新菜单
    1. 从本地菜单里删除不在菜单权限列表的 项
    2. 根据 location 判断 菜单 高亮
    3. 新的菜单渲染出左边栏

虚拟Dom 理解

一、什么是虚拟Dom

从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能

虚拟dom是对DOM的抽象,这个对象是更加轻量级的对DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟dom, 因为虚拟dom本身是js对象。

在代码渲染到页面之前,vue或者react会把代码转换成一个对象(虚拟DOM)。以对象的形式来描述真实dom结构,最终渲染到页面。在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。

在vue或者react内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。

另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。

二、为什么要用 Virtual DOM

1.保证性能下限,在不进行手动优化的情况下,提供过得去的性能

看一下页面渲染的一个流程:

  • 解析HTNL ☞ 生成DOM? ☞ 生成 CSSOM ☞ Layout ☞ Paint ☞ Compiler

下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗:

  • 真实DOM: 生成HTML字符串 + 重建所有的DOM元素
  • Virtual DOM: 生成vNode + DOMDiff + 必要的dom更新

Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道: 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。

2.跨平台

Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。

三、Virtual DOM真的比真实DOM性能好吗

  1. 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
  2. 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。

函数式编程

1. 避免包裹函数

// 这行
ajaxCall(json => callback(json));

// 等价于这行
ajaxCall(callback);

// 那么,重构下 getServerStuff
const getServerStuff = callback => ajaxCall(callback);

// ...就等于
const getServerStuff = ajaxCall // <-- 看,没有括号哦

如果一个函数被不必要地包裹起来了,而且发生了改动,那么包裹它的那个函数也要做相应的变更

httpGet('/post/2', json => renderPost(json));

如果 httpGet 要改成可以抛出一个可能出现的 err 异常,那我们还要回过头去把“胶水”函数也改了。

// 把整个应用里的所有 httpGet 调用都改成这样,可以传递 err 参数。
httpGet('/post/2', (json, err) => renderPost(json, err));

写成一等公民函数的形式,要做的改动将会少得多

httpGet('/post/2', renderPost);  // renderPost 将会在 httpGet 中调用,想要多少参数都行

后一个就显得更加通用,可重用性也更高

// 只针对当前的博客
const validArticles = articles =>
  articles.filter(article => article !== null && article !== undefined),

// 对未来的项目更友好
const compact = xs => xs.filter(x => x !== null && x !== undefined);

2. 纯函数的好处

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

var xs = [1,2,3,4,5];
// 纯的
xs.slice(0,3); //=> [1,2,3]
xs.slice(0,3); //=> [1,2,3]
xs.slice(0,3); //=> [1,2,3]

// 不纯的
xs.splice(0,3); //=> [1,2,3]
xs.splice(0,3); //=> [4,5]
xs.splice(0,3); //=> []

副作用可能包括…

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

副作用可能包含,但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态

追求“纯”的理由

可缓存性(Cacheable)

// 下面的代码是一个简单的实现,尽管它不太健壮。
var memoize = function(f) {
  var cache = {};

  return function() {
    var arg_str = JSON.stringify(arguments);
    cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
    return cache[arg_str];
  };
};

值得注意的一点是,可以通过延迟执行的方式把不纯的函数转换为纯函数:它总是会根据相同的输入返回相同的输出:给定了 url 和 params 之后,它就只会返回同一个发送 http 请求的函数。

var pureHttpCall = memoize(function(url, params){
  return function() { return $.getJSON(url, params); }
});

可测试性(Testable)

合理性(Reasonable)

很多人相信使用纯函数最大的好处是引用透明性(referential transparency)。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的

< a > javascript: URLs

a 标签里写 javascript: void(0) 或在其他

  1. If you’re just using <a> for the link styling, consider using a <button> and styling it as a link instead.
  2. If you still must use <a> for some reason, a workaround is to give it href=”#” onClick={e => e.preventDefault()} .This is not great, so prefer option 1 if you can.
  3. If you absolutely insist on using a javascript: URL, set it yourself in a ref: <a ref={node => node && node.setAttribute(‘href’, ‘…’)} >. This works for the bookmarklet case.
  4. If you have hundreds of links like this, as the original post says, one possible solution is to codemod every <a> to <Mylink> and implement either of the above solutions in Mylink. This blog post has an example of something similar.

前端 CI 测试

集成 测试 工具: Travis CI, Jest, nyc(Istanbul) mocha

Travis CI

Office Doc

Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。

先决条件 To start using Travis CI, make sure you have:

github 集成 案例

  1. github使用Travis,登陆Travis官网,用 github 登陆
  2. Click on your profile picture in the top right of your Travis Dashboard, click Settings and then the green Activate button, and select the repositories you want to use with Travis CI.
  3. Add the .travis.yml file at root, commit and push to trigger a Travis CI build
  4. Check the build status page to see if your build passes or fails according to the return status of the build command by visiting Travis CI and selecting your repository.

Travis 的运行流程很简单,任何项目都会经过两个阶段。

  • install 阶段:安装依赖
  • script 阶段: 指定构建或测试脚本

Node 项目 实例

language: node_js
node_js:
  - "8"
cache:
  directories:
    - node_modules
branches:
  only:
    - master
env:
  - DB=postgres
before_install:
  - npm i -g npm@version-number
install:
script:
  - ember test --server

部署

script阶段结束以后,还可以设置通知步骤(notification)和部署步骤(deployment),它们不是必须的. 部署的脚本可以在script阶段执行,也可以使用 Travis 为几十种常见服务提供的快捷部署功能

deploy:
  provider: pages
  skip_cleanup: true
  github_token: $GITHUB_TOKEN # Set in travis-ci.org 
     dashboard
  on:
    branch: master

nyc(Istanbul) 是 JavaScript 程序的代码覆盖率工具

参见原理

四个指标

  • 行覆盖率(line coverage):是否每一行都执行了?
  • 函数覆盖率(function coverage):是否每个函数都调用了?
  • 分支覆盖率(branch coverage):是否每个if代码块都执行了?
  • 语句覆盖率(statement coverage):是否每个语句都执行了?

nyc的使用

package.json 配置, 或者用根目录下 nyc.config.js

"nyc": {
    "all": true,
    "check-coverage": true,
    "branches": 100,
    "function": 100,
    "lines": 100,
    "statements": 100,
    "reporter": [
      "text",
      "lcov"
    ],
    "include": [
      "src"
    ],
    "sourceMap": false,
    "instrument": false,
    "require": [
      "babel-register"
    ]
  },

nyc 与测试框架组合