Giter Club home page Giter Club logo

blog's People

Contributors

shinken008 avatar

Watchers

 avatar  avatar

blog's Issues

[email protected] 微信小程序不完全升级指南

前言

[email protected] 发版以来,基本上保持在一周的时间发版,主要是修复 bug。通过社区可以了解到,[email protected] 发版带来了重大变革,其中对 H5 和小程序场景进行重构,提供了新的特性和新的架构。

[email protected] 发版之后,社区一直很活跃,不少用户开始使用一键式(命令)升级。

$ taro update project [version]

敲完之后是这样:

还有这样的:

看到这些我差点笑出声,这是我在 issue 在讨论 3.x 时看到的,这位同学版本应该是敲错了。不得不说逛社区是件很有意思的事情,有的时候看 issue 能发现一些同道中人,你踩的坑别人也在前赴后继[手动尴尬]。

Taro 目前还有很多的 ,现在有 600+ 的 issue 数,平摊到各个平台其实不会很多,咋看一下 V-3 issue 也就几十个。也有可能是用户基数的关系,毕竟刚发版不久,幸存者偏差。这里我不是要劝退想要升级的同学,就像官方说的,”没有枪,没有炮,没有轮子自己造“,不要怂,就是*!首先我们先看看 [email protected] 特性。

以下是在小程序场景下升级 [email protected] ,其他端升级仅供参考和学习。

[email protected] 特性

  • 开放式框架
  • 渲染 HTML 字符串
  • 充满争议的 CSS-In-JS
  • 虚拟列表 VirtualList
  • 预渲染 Prerender
  • source-map 支持
  • 更快的构建速度
  • 更快的运行速度
  • Babel@7
  • ...

看到它那么多特性,似乎很诱人,详细参考 官方文档,文档有的东西在这里就不过多的赘述了。

下面是如何用上它呢?如果是新项目那很好办,只需要实现新功能,对于旧项目呢?”买不了吃亏,买不了上当“,以其忍受旧的技术栈,不如升级玩出新花样。

[email protected] 是个啥

首先先简单了解下框架原理和解决的问题。

框架

下面是引用官方的一张架构图:

解决了什么问题
  • 提供了上面新的特性,比如解决开发体验上的问题;
  • 得益于新的编译运行机制,我们不用去遵守以前的最佳实践(约束)了。官方最佳实践
    • JSX 变量在模板语法使用 this.state.x,编译的时候生成未赋值的 data 值,需要定义一个变量赋值 this.state.x
    • 不必遵守 render* 渲染 JSX 的约定;
    • 不必遵守 on* 渲染传递函数名的约定;
    • ...

从架构层面上讲,Taro 从一个编译型框架变成了一个运行时框架,基本上曾经的 Taro 无法运行的代码在 Taro Next 中完全没有压力。[email protected] 在运行时维护了一个 DOM 模型,使得编译的时候不去做 data xml 转化,从而规避掉了编译带来的 bug,同时降低学习成本,PC 端开发的同学也能轻松接入。

看起来挺香的。了解了一些知识后,下面我们先从依赖开始升级。

[email protected] 依赖

以下是基于 [email protected] 升级到基于 [email protected] 框架 Taro@lastest(@3.0.7) 的历程。

框架选型

对用户来说,开放式框架给了用户更多地选择,原来的版本只支持的类 React 语法,现在完全可以使用社区的其他框架,比如 Vue 2Vue 3jQuery。在这里因为我是一个 React 的重度用户,比起官方的 Nerv,倾向于 React。而且 React 是一个非常优秀的开源项目,有庞大的团队在维护。这里安利一下 React[email protected] 发布了,只是一个过渡版本,消除了设计上的隐患,帮助用户更安全的升级过渡,新的功能特性放在 React@18 发布,有没有一种服务到家的感觉(这里有点要黑 [email protected] break change 式升级的嫌疑)。

跑题了,下面继续介绍 Taro升级,下面是选择 React 框架需要改造的地方:

  • 安装 React 技术栈所使用的 npm 包;
  • 框架 APITaro 移到第三方框架,比如:import React, { Component } from 'react'
  • 配置 config.framework: 'react'
  • babel.config.js 配置 React 相关 babel,这里可以使用官方的 babel-preset-taro,下面【配置更改】会讲到;

Babel@7

[email protected] 使用的 babel@6,了解 babel 同学应该知道 6.x 版本和 7.x 版本的差异,比如 Presets ,新的 Proposals 等命名,babel@7 使用 @babel/x scope 代替 babel/x,防止被占用。

相关依赖包升级

  • 使用 npm 挨个安装升级的需要 babel 包了;
  • 安装官方 babel-preset-taro
  • 使用 cli。将 taro-cli 升级到3.x,执行 taro update project[version] 命令。建议使用 cli 升级命令最好是已经升级到 3.x 版本,对于2到3的升级,建议手动升级各个依赖包,因为只能cli 检测 dependencies 某一些包 进行升级。

这里我使用的是手动升级,先删除对应老版本的 npm 包,再安装新版本

npm i @babel/runtime @tarojs/components @tarojs/runtime @tarojs/taro @tarojs/react [email protected] [email protected]
npm i -D @tarojs/cli @types/webpack-env @types/react @tarojs/mini-runner @babel/core @tarojs/webpack-runner babel-preset-taro eslint-config-taro eslint eslint-plugin-react eslint-plugin-import eslint-plugin-react-hooks stylelint

各个端的相关包。比如原生小程序转过来会用 with-weapp 包一层

npm i @tarojs/with-weapp // withWeapp 接受一个小程序规范的 Page/App 构造器参数,转换为对应框架规范的组件实例
npm i @tarojs/taro-weapp // 微信小程序
npm i @tarojs/taro-alipay //解决方案支付宝小程序

新增的包。这里使用安装 babel presets 或者插件,因为我偷懒,安装 babel-preset-taro,然后在 babel-config.js 配置上老项目使用的插件和预设,参考文档配置或者 代码

npm i @babel/runtime @tarojs/react
npm i @tarojs/runtime // Taro 运行时。在小程序端连接框架(DSL)渲染机制到小程序渲染机制,连接小程序路由和生命周期到框架对应的生命周期。在 H5/RN 端连接小程序生命周期规范到框架生命周期
npm i -D @babel/core babel-preset-taro // babel 相关

删除 babel@7 以下的包。安装 babel-preset-taro 可配置相关的 babel,如果有些 babel presets pluginsbabel-preset-taro 没有集成的话,请手动安装

npm remove babel-plugin-transform-class-properties babel-plugin-transform-decorators-legacy babel-plugin-transform-jsx-stylesheet babel-plugin-transform-object-rest-spread babel-plugin-transform-runtime babel-preset-env babel-runtime @tarojs/plugin-babel

删除老的 nerv 框架相关的包

npm remove nervjs nerv-devtools

配置更改

  • babel-config.js。在 [email protected] 之前是 babel 配置在 config/index 配置文件里面,现在把它放在根目录的 babel 配置文件里面

    module.exports = {
      presets: [
        ['taro', {
          framework: 'react',
          ts: true // 是否启用ts
        }]
      ],
    };
  • .eslintrc。在 [email protected] eslint-plugin-taro 废弃,因为之前的配置以及编码最佳实践将不适用。如果开启了 ts 编译,还需要安装@typescript-eslint/parser@typescript-eslint/eslint-plugin

    {
      "extends": ["taro/react"]
    }

[email protected] 业务代码改造

页面 config 独立文件

Taro 1.x,2.x 时,配置是在页面实例里面去配置 config,编译后输出 x.json 配置文件。在 [email protected] 里,需要在同级目录里新增一个独立的 JS 文件去配置:x.config.js

在这里需要注意的一点是,跟页面代码一起的 config 会通过 webpack 编译,而独立成 x.config.js 只会被 babel-register 进行编译,在项目里刚好 config 配置的 pages 常量是根据不同的场景定义的 webpack 常量,所以 config.js 取不到 webpack 常量配置报错。解决方案是,使用 babel-plugin-transform-define,主要不能与 webpack 常量冲突,需要额外定义 config.js 里面的常量。这种解决方式毕竟不优雅,官方考虑在后续添加这个特性,相关issue

config/index 配置文件

  • Config.framework 配置当前小程序使用的框架 React | Vue | Nerv
  • Config.defineContants[email protected] 常量的 value 可以使用 key: '我是字符串' 。在[email protected] 编译会报错,要用 String 再包一层,比如 key: JSON.stringify('我是字符串'),这里建议定义的常量值都使用 JSON.stringify 处理一下。
  • Config.terser 压缩配置项,terser 配置只在生产模式下生效。如果你正在使用 watch 模式,又希望启用 terser,那么则需要设置,process.env.NODE_ENVproduction,将不会生成 source-map 文件。

页面实例的改动

  • Taro.Component 替换为 React.Component

  • 路由信息 this.$router is undefined

    this.$routerTaro.getCurrentInstance().router(或者 Taro.Current.router)代替。在项目入口文件 app.js 实例里面 componentWillMount, componentDidMount 取到 Taro.Current.router is null,路由信息只有在componentDidShow生命周期才能读取到,因为 componentWillMount, componentDidMount 生命周期 App.onLaunch 方法里面触发,生命周期触发之后赋值 Current.router代码实现

  • Taro.getApp() is undefined

    在 实例化(createReactApp)之前调用 Taro.getApp() 将返回 undefined。我们的业务代码比如在 app.js 最顶层 require(./common.js),如果 common.js 包含 Taro.getApp() [x] 使用将会报错。

  • 页面实例方法的改变,比如onShareMessage 改成 static onShareMessage

    参考代码实现

  • e.currentTarget.dataset 空对象;

    e.currentTarget.dataset 不兼容。解决方案:使用 func.bind(this, ...args) 绑定参数的形式,参考 issue。这个解决方案改动也很多,绑定了 dataset 都要进行改造。我还是支持 dataset 的实现,这样框架更接近原生。

  • withWeapp 问题;

    官方并没有在发布 3.0 的时候对 withWeapp 改造,导致 withWeapp 几个报错,this.$routerthis.state 取值都是 undefined;解决方案:1、不用这个装饰器。但是通常我们有不得以的苦衷,比如业务代码里面大量使用,更改代码的成本太高。2、官方PR,预计在 [email protected] 发布,并不会等太久;

  • 没有了 this.$scopethis.$componentType 的概念;

    只是实现改变了,runtime 维护一个 DOM 内存结构,所以能直接取了。

    // [email protected]
    const ctx = Taro.createCanvasContext("image-cropper", this.$scope);
    const selectQuery = Taro.createSelectorQuery().in(this.$scope);
    
    // [email protected]
    const ctx = Taro.createCanvasContext("image-cropper");
    const selectQuery = Taro.createSelectorQuery();
  • ...

[email protected] 多场景下框架的兼容

[email protected] ,小程序分包我们可以使用宿主小程序的框架,但在升级 [email protected] 过程中,我们需要分析 2.x 和 3.x 框架版本能不能做到共用。这个章节我们针对主包、分包和插件场景下对小程序的运行、打包和包大小进行分析,演示多场景框架升级的可行性。

独立运行

在58小程序体系里面,经常会有同一份源码编译到不同端的场景,以及借助内部工程化管理工具 MPS 更为便捷的管理分包,组件。在分包和插件的场景下,是一份这样的目录结构:

分包

在分包场景中,为了减少包大小,分包尽量减少了依赖第三方 vendors 的注入,比如 Taro,共用主包的 vendor,删掉了无效的 app.xproject.config.json。在升级 [email protected] 的过程中,由于框架机制的改变,而无法做到与低于 3.0 的版本做到框架的共用。因为在3.x,各个 Page 依赖入口文件 app.js createReactApp(App: React.ComponentClass, react: typeof React, reactdom, config: AppConfig) 创建的 Current(包含 app , router , page )和 Reconciler 实例,就是 page 依赖执行 app.js 文件(参考代码实现)。但是分包的 app.js 不是作为入口文件执行的,咋办呢?

我想到的是在各个文件 require('./**/app.js'),在加载完整个分包后执行 app.js,实现 App 的实例化。

// app.js
export default class App extends React.Component {
  componentDidMount() {
    console.log('app.onLaunch Taro.Current:', Taro.Current);
  }
  render() {
    return this.props.children;
  }
}
// home/list.js
export default class Home extends Component {
  componentDidMount() {
    console.log('page.onLoad Taro.Current:', Taro.Current);
  }
  componentDidShow() {
    console.log('page.onShow Taro.Current:', Taro.Current);
  }
  render() {
    return (
      <View>hello world</View>
    )
  }
}

可以看到,在入口文件我们取到实例,且在 Page 页面也能取到实例。但是有个问题发现了没,我们是跟主包共用一个 App 实例,这会导致我们改掉 App 实例的内容影响到主包。

注入 app.js 方案在分包场景是没有问题的,但是怎么解决 App 实例影响问题呢?这个问题等下再回答,我们再来以插件场景进行尝试这个方案。

插件

还是跟分包一样的入口文件和 page 页面。我们先看下编译后的 app.js 代码长啥样:

这就是 [email protected] 编译后的入口文件,下面我们引入这个插件:

发现在插件模式下,微信小程序没有提供 App 实例。

分析分包和插件页面加载方案

从上面可以看出,require('./**/app.js')方案 从运行机制上来看可行,但是存在一些问题:

  • 分包引入 app.js 执行会引入主包 App 实例被改问题;

  • 插件模式下,微信小程序没有提供 App 实例报错;

  • 怎么给每个 Page 注入 app.js

怎么解决上面的问题呢?所以我们想到,我们能不能在分包和插件自己实现一个 App 函数,并且提供一个 onLaunch 空方法,让taro-runtime 能够执行createReactApp 呢?下面是我们使用插件 @mps/mps-taro-plugin/dist/MpsRuntimeTaroPlugin 实现了一个 App 函数:

export function App(config) {
  config.onLaunch({})
}

并且通过 webpack.ProvidePlugin 注入到 app.js 中,打包后的文件是这样的:

我们再来手动引入 app.js 试试,没有报错,程序能正常运行。

我们不能每个 Page 都手动注入 app.js 吧?接下来如何给每个 Page 引入 app.js 呢?这时候我们可以通过 webpack 插件做这件事情,刚好我们现有 @mps/mps-taro-plugin/dist/MpsBusinessTaroPlugin 已经提供了这个能力,安装 @mps/[email protected] 版本。下面是提供 App 函数和给每个 Page 注入 app.js 的插件配置:

plugins: [
	'@mps/mps-taro-plugin/dist/MpsRuntimeTaroPlugin',
   ['@mps/mps-taro-plugin/dist/MpsBusinessTaroPlugin', {
      commonChunks: ['app'], 
   }],
]

撒花!通过以上的方式解决了多场景下框架版本的兼容性问题。

打包文件

入口文件处理

多场景如何定义入口文件,这是一个问题,我们经常在入口文件主包需要引用一一些 sdk 之外,然而在分包和插件场景并不需要使用到。比如在 Taro2.x 的是按 WEBPACK_CONST 条件将 import 进来模块赋值给已经赋值过的变量,引入 target 模块定义的上下文在没有使用的时候会被 tree-shaking 掉,其实这应该是一个 bug。或者说是现在的 webpack + uglyfy(terser) 目前没有实现的一个特性(类消除也是后面实现的),理论上编译器只有在静态分析100%确定没问题的情况下才会删,不会去分析程序流,意味着你的分包和插件在不使用的一些代码都会被打包进来。听起来可能不好理解,我们直接看代码,感兴趣的同学可以尝试下,分别放在 [email protected][email protected] 编译:

// App.js 入口文件
import { cube } from './util.js';
let value = null;
if (WEBPACK_CONST) { // 场景常量,WEBPACK_CONST = false
	value = cube(2);
}

// utils.js
console.log('before utils');
export function square(x) {
  console.log('square'); 
  return x * x;
}

export function cube(x) {
  console.log('cube');
  return x * x * x;
}
  • [email protected] 下,不会被删掉;

    Development,console.log('before utils'); 没被删掉,且 unused 模块也没有被删掉。

    Production,console.log('before utils'); 没被删掉, unused 模块被删掉。

  • [email protected]

    Development,Production,console.log('before utils'); 被删掉,unused 模块被删掉

如果入口文件沿用 [email protected] 的写法会带来几个问题:

  • importrequirerequirePlugin 等引入上下文会执行;

  • 影响包体的大小;

  • [email protected] dev 环境不会 tree-shaking,参考上面的配置说明 【config/index 配置文件】;

解决上面问题很简单,我们用 webpak + require + defineConstants 的方式对各个场景按需引入,比如:if (WEBPACK_CONST) { require('..') },当 WEBPACK_CONST (编译常量)为 false 的时候,Webpack 编译分析不会把条件外的 require 内容引入进来。有了这个结论我们就对入口文件进行了处理:

// app.js
let App = () => null;
if (WEBPACK_CONST === 'a') {
  App = require('./app.a').default;
}
if (WEBPACK_CONST === 'b') {
  App = require('./app.b').default;
}
if (WEBPACK_CONST === 'c') {
  App = require('./app.c').default;
}

export default App;

// app.a.js
import React from 'react'
import Taro from "@tarojs/taro"

export default class App extends React.Component {
  componentDidMount() {
    console.log('app.onLaunch Taro.Current:', Taro.Current);
  }
  render() {
    return this.props.children;
  }
}

咋一看,上面的文件是解决了,但是不太优雅,如果支持配置指定入口文件是不是更好。可惜的是, [email protected] 在小程序场景中的将入口文件写死了 app.x,所以不得不使用这种方式去做到分入口加载。感兴趣的同学可以看看这个 PRentryFileName 其实已经支持在 H5RN 里面配置。

包大小

玩游戏最怕的是”一顿操作猛如虎,一看战绩零杠五“。要是包大小不通过,所有的解决方案都是白忙活。下面是我通过开发者工具统计的包大小信息:

[email protected] [email protected]
主包 1469.0kb 1369.0kb
分包 1246.1kb 1235.0kb
插件 1493.0kb 1456.0kb

根据上面统计的信息,在主包,分包和插件场景下,现有的包大小没有超过原来的包大小。

总结

在这里,我们可以认为升级 Taro@3x 在现有的58体系里面是可行的,改动的范围也是可以接受的。

我是从发布不久开始接入,可以说从 [email protected] 一路踩坑过来的,开始的时候因为 api 改动大有点打击信心,但随着了解了一些运行机制之后和简单的开发体验后,越发觉得 [email protected] 颠覆式的重构反而能让 Taro 走的更远。有兴趣的同学可以按照上面的步骤进行升级,基本上没啥问题,有啥问题可以给提 issuePR,官方人手不多,一起帮忙加特性(改 bug )。还是那句话,”没有枪,没有炮,没有轮子自己造“。

参考资料

[1] Taro 3 正式版发布:开放式跨端跨框架解决方案: https://aotu.io/notes/2020/06/30/taro-3-0-0/index.html

[2] 从旧版本迁移到 Taro Next: https://nervjs.github.io/taro/docs/migration

[3] Taro Next 发布预览版: [https://juejin.im/post/6844904063675400199](

闭包的一些用法

1.封装变量

闭包可以帮助把一些不暴露在全局变量封装成“私有变量”。假如有个计算乘机的简单函数:

var mult = function(){
    var a = 1;
    for (var i = arguments.length - 1; i >= 0; i--) {
        a = arguments[i];
    }
    return a;
}

mult函数接受一些number类的参数,并返回这些参数的乘机。对于已经进行过计算的参数乘机,再次额外的计算是一种浪费,我们可以运用到缓存的知识提高函数的性能:

var cache = {};
var mult = function(){
    var argument = Array.prototype.sort.apply(arguments);
    var args = Array.prototype.join.call(argument, ',');
    if(cache[args]){
        return cache[args];
    }
    var a = 1;
    for (var i = arguments.length - 1; i >= 0; i--) {
        a = a * arguments[i];
    }
    return cache[args] = a;
}

console.log(mult(1,2,3))

我们又看到cache只在函数内使用,但是又是跟函数暴露在同一个作用域,不如把它封装在函数内部,减少页面的全局变量,以免变量在其他地方修改而引发的错误。

var mult = function(){
    var cache = {};
    return function(){
        var argument = Array.prototype.sort.apply(arguments);
        var args = Array.prototype.join.call(argument, ',');
        if(cache[args]){
            return cache[args];
        }
        var a = 1;
        for (var i = arguments.length - 1; i >= 0; i--) {
            a = a * arguments[i];
        }
        return cache[args] = a;
    }
}

console.log(mult()(2,3,4));  //24

提炼函数是一种技巧。如果能将大函数代码独立成来,有助于代码的复用,而且对于独立出来的小函数如果有个好的命名,也能起到注释的作用。如果这些小函数不需要在程序的其他地方使用,最好把它用闭包封装起来。

var mult = function(){
    var cache = {};
    var calc = function(){
        var a = 1;
        for (var i = arguments.length - 1; i >= 0; i--) {
            a = a * arguments[i];
        }
        return a;
    };
    return function(){
        var argument = Array.prototype.sort.apply(arguments);
        var args = Array.prototype.join.call(argument, ',');
        if(args in cache){
            return cache[args];
        }
        return cache[args] = calc.apply(null, arguments);
    }
}

console.log(mult()(1,3,4));     //12

2.延续局部变量的寿命

img对象经常用于数据上报,如下所示:

var report = function(src){
    var img = new Image();
    img.src = src;
};

report('http://htmljs.b0.upaiyun.com/uploads/1398932756598-angularjs.jpg');

有些浏览器使用report函数进行数据上报会丢失30%左右的数据,也就是说report函数并不是每一次都成功发起http请求。丢失的原因是img是report函数中的局部变量,当report函数调用结束后,img局部变量随即被销毁,而此时或许还没来得及发出http请求,所以此次请求就会丢失掉。我们用闭包将img变量封装起来,就能解决请求丢失的问题:

var report = function(){
    var imgs = [];
    return function(src){
        var img = new Image();
        imgs.push(img);
        img.src = src;
    }
}

report()('http://htmljs.b0.upaiyun.com/uploads/1398932756598-angularjs.jpg');

AngularJS: 使用Scope时的6个陷阱

1398932756598-angularjs

在使用AngularJS中的scope时,会有6个主要陷阱。如果你理解AngularJS背后的概念的话,这6个点其实非常的简单。但是在具体讲述这6个陷阱之前我们先要讲两个其它的概念。

概念1: 双向数据绑定

双向数据绑定是AngularJS中非常重要的一个部分。一般的绑定对于我们来说已经非常熟悉了。即使你没有听说过双向数据绑定,你一定使用过它。

普通的绑定一般是用来数据数据的,它实际上是模板引擎的一个基本概念:

Hello {{username}}

如果将变量username设置为John Doe,上面的例子会被渲染为:

Hello John Doe!

这是双向数据绑定的第一个方向。你可以在文档中查看ng-bind的详细内容。

在模板中这个功能已经足够用了,因为模板本来就是用来输出内容的。然而,在使用HTML构建用户界面时你依然可以使用双向数据绑定来处理用户输入。下面是一个例子:

<input ng-model='username'>
<p>Hello {{username}}</p>

只有当框架本身支持逆向绑定时,上面的例子才可以在不需要施加任何额外魔法的前提下正常运行(网络onkeyup或者onchang事件吧!)。

这是双向绑定的第二个方向。你可以在文档中查看ng-model的详细内容。

如果你将两个方向一起使用,你就已经使用了AngularJS中的双向数据绑定,它将能够整合从视图到模型之间的数据。

而绑定中数据的来源,我们叫做作用域(scope)。

和其他的数据绑定框架不同,AngularJS并没有将对象包装在存取器中,正是因为如此,你不需要定义一个包含特定getter和setter的对象。出去其中的一些功能(像是$broadcast,$apply,$digest,$emit以及$watch)和引用(像是$parentScope),作用域基本上就是一个包含一些属性和值的普通对象。你可以像对待一个普通对象一样在scope中存取数据,同时这些发生在作用域中的变化并不会被作用域本身所识别。任何变化都需要使用$apply方法来调用一个digest循环。然而,如果没有特别指明的话你并不需要去关注这件事。

有时,每发生一次变化就去调用一个digest循环并不是很合适的做法因为这势必会影响应用的性能。比如一个聊天客户端,它每秒都会在scope中添加一些特定的信息。为了让你的应用不至于变得慢吞吞,你最好限制digest循环的数量。简而言之,通过使用$scope.$apply()方法隐式调用AngularJS中的digest循环将会运行模板中的所有表达式和监视器。

概念2: 声明式UI

在AngularJS中,你需要遵循的一条规则就是“创建可充用的组件指令来扩展你的HTML”,因为它可以保持你的代码的可重用性。

因为你很可能是一位jQuery开发者,你可能已经非常熟悉了“jQuery”式的开发方式,例如添加CSS样式的方式(addClass()函数)和隐藏元素(hide()函数)的方式。这样的方法被称为是命令式的:

你显式的告诉计算机你想要在特定的环境下运行代码,例如将代码包裹在一个if语句中。
AngularJS使用的方法是声明式的:

你需要在视图中声明如何显示一个特定的环境。

假设你现在有一个导航列表,其中包含一些项目。如果一个项目被选中了,这个项目应该添加一个叫做active的类。

在下面的例子中,第一个项目被标记为active:

<ul class="navigation">
    <li class="item item1 active">Item 1</li>
    <li class="item item2">Item 2</li>
    <li class="item item3">Item 3</li>
<ul> 

jQuery式的编程方式会首先移除所有的active类,并在其中一个项目上添加active类。但是究竟应该在哪一个项目上添加active类呢?你必须在JavaScript中提供一个额外的绑定来决定添加类的项目,可能是一个额外的类或者一些data-属性。

我们来看看AngularJS应该怎么做:

<ul class="navigation">
    <li ng-repeat="item in items"
        class="item"
        ng-class="{'active': item.id == activeItem}">{{item.title}}</li>
</ul>   

为了代码能够正常运行,我们需要在作用域中添加以下内容:

$scope.activeItem = 'item1';
$scope.items = [{
    id: 'item1', title: 'Item 1'
}, {
    id: 'item2', title: 'Item 2'
}, {
    id: 'item3', title: 'Item 3'
}];  

首先,这个例子使用了ng-repeat指令,这个指令将会迭代所有的items中的项目并且按照同样的顺序创建HTML元素。在这个例子中创建了三个li元素。

ng-class指令声明式的描述了active类应该在什么时候被使用。这个类仅仅只会在item.id==activeItem结果为true时被添加。由于我们有双向数据绑定,因此当你将$scope.activeItem修改为item2时,标签也会自动发生改变。你不需要编写任何代码来修改你的业务逻辑。在AngularJS中,行为应该在模板中被描述。

这意味着,你可以使用声明式的方式来轻松地创建标签栏,滑动按钮,自动滚屏区域,可拖拽窗口或者一个上下文菜单。

在讨论完了AngularJS中的双向数据绑定以及声明式UI之后,我们来看看在使用这些技术时会遇到的问题。

陷阱1: Scope digester和表达式

当在视图或者监视器中使用表达式时,你应该总是记住每当AngularJS认为需要的时候,表达式总是会被调用。因此,可能并不能获得函数的性能,你甚至可能错过一些change事件。

这意味着:

带有一个ng-repeat的表达式将会分别调用每个项目。另外,AngularJS将会使用repeat指令来决定数据变化。
一个表达式可能在一次digest循环中被多次估值(evaluation)。当你使用多个指令或者额外的作用于监视器时,这种情况会发生。
即使在作用域不会改变时依然会被估值。
如果表达式包含一个函数,在函数的返回值发生变化时,表达式不会被估值。但是在函数的定义发生变化时会被估值。
例如,我们拥有一个表达式: stat === getUserState()。有以下几种可能情况:

函数仅仅返回scope.currentUserState: 此时我们可以抛弃函数,直接使用数据。这种表达式在未来会逐渐被优化。
这个函数会进行一些业务逻辑计算: 每次表达式被估值时,这些逻辑都会运行。更好的方法是在作用域中计算和编写当前用户状态。这种方法将能把逻辑和用户状态、视图进行解耦。一般来说数据就是作用域,作用域就是数据。
函数会从作用域之外的地方获取数据: 这种方法非常非常不好。作用域/AngularJS在发生变化时并不会得到通知。记住只有在AngularJS认为作用域发生了变化时,它才会调用一个digest循环,所有表达式才会受到影响。
有时,第二种、第三种情况会同时发生。

如果你使用了外部的数据(或者数据变化) – 例如,一个外部的jQuery插件会改变状态 – 你必须为作用域提供这些数据。给定一个指令,你可能会有一个能够访问当前作用域的回调函数。你可能会注意到作用域上的任何变化将不会更新任何的UI,因为AngularJS不会注意到作用域发生了变化。

然而,你可以调用AngularJS中的$scope.$apply()函数,它将会调用所有digest循环,监视器和相关数据估值。

尽管如此,你还是应该尽量避免使用$apply()或者它的兄弟$digest()。在真实的外部事件(jQuery回调,浏览器事件回调等等)之外,你可能会实现错误的代码架构。

注意到如果你在一个正在运行的digest循环中调用一个digest/apply,你可能会遇到像是”Digest already in progress”这样的错误。这也是为什么应该在表达式中避免函数。

下面的代码是一种普遍的错误使用函数方法:

<ul>
    <li ng-repeat="item in loadItems()">{{item.title}}</li>
</ul>

这里出现的问题是调用了一个loadItems()函数。这个表达式将不会被正确的估值:这个指令本身会添加一些原数据到模型中以决定列表中的哪些项目应该被添加,移除或者仅仅是移动。建议的做法是在ng-repeat中使用数组。告诉你自己:调用loadItems是命令式的,我们应该声明式的给定数据。

最佳实践:

不要在表达式中使用函数。
不要使用表达式所在作用域以外的数据。
当应用外部数据变化时使用$scope.$apply()。
使用这些最佳实践将能够获取高效的代码,同时也不会错过事件。

陷阱2: 引用一个DOM元素

在指令中使用DOM元素是正确的。可以将它们存放在一个变量中。但是永远不要再作用域中存储DOM元素。

DOM元素是巨大的DOM树的一部分,同时DOM树的本性是它知道自己的父元素,子元素和兄弟元素。如果你旨在作用域中存储了一个DOM元素,作用域digest循环将会查找它本身以及它的父元素和父元素的父元素。这意味着digest将会检查整个DOM树来查找变化的部分。如果你觉得这还不够疯狂,还有更恐怖的事情:因为每个DOM元素都会拥有额外的引用,digest循环将会不止一次的遍历整个DOM树。

你并不像这样做,因为这很疯狂。

最佳实践:

不要在作用域中存储DOM元素,这回引起内存泄露。

陷阱3: 在指令外面使用DOM元素

不要在指令外面使用DOM元素。很多的服务都会轻易的产生一个DOM树,因为他们通常是单个的,全局的,以及无状态的实例,像是一个REST API的一个实例。

一个控制器中的DOM引用会纸箱一个错过的指令或者一些错过的行为。

真正的情况是,将一个控制器的DOM引用抽取到一个指令中是非常消耗资源的。但是如果你理解了这个问题以及它的影响,但是还是想要这么做,也没关系。但是你很快就要去遇到的事实是控制器会绑定到一个特定的模板,同时由控制器引起的DOM变化将不会体现到AngularJS的作用域和视图中。

最佳实践:

不要在指令外部获取DOM元素因为指令可以将控制器、服务和DOM进行解耦。因此这样我们获得了更大灵活性,代码也更容易去测试和使用。

陷阱4:不使用内建方法

我在前面提到了$apply()和$digest()的用法以及它们的影响。如果许多外部事件需要额外的$apply()调用,它将会引起很多麻烦。因此我建议你深入阅读AngularJS文档,使用一些内建指令,比如使用$timeout()而不是使用window.timeout(),前者会隐式的调用$rootScope.$apply()。

你应该使用内建的$http方法而不是外部的XHR包装,它将返回一个$q promise。执行这个promise的任何回调函数都会调用$rootScope.$apply()。一些返回$q promise的模块将会隐式的调用$rootScope.$apply()。

最佳实践:

使用内建指令,因为它们能够让你写出简单友好的代码。

陷阱5: 令人费解的“当前作用域”

作用域的层级结构式非常聪明的做法,但是如果你理解的不是很深入,你将会很痛苦。在你的根作用域中你可以定义一些全局全局变量,它们将可以在所有的自作用域中使用(除了隔离作用域) – 原型继承将会“找到”这些属性。在DOM中你也可以再一个普通的控制器中定义作用域来分享数据。

但是这里有一个阻塞:它只能在单方面上运行。但是这也不错,因为你不想将本地作用域中的数据暴露给其他作用域。

<span>Outside Controller: Your name is: {{username}}</span>
<div ng-controller="SignupController">
    <span>Inside Controller: Your name is: {{username}}</span>
    <fieldset legend="User details">
        <input ng-model="username">
    </fieldset>
</div>

尝试着改变input中的值,它可以正常运行但是只针对于内部的绑定。在控制器以外的绑定的值将不会变化。这是为什么?答案存在于“什么是我的当前作用域?”中。

例如,我们有两个作用域:总体的rootScope作用域和一个通过控制器(在这里是SignupController)隐式创建的作用域。

当你在input字段中输入一个新值。当前的作用域会被赋上一个叫做username的新属性。因为准确来说input字段所在的控制器的作用域就是当前作用域,这个属性也会被赋予这个作用域。就像JavaScript中的原型继承一样,这意味着这个属性在父作用域中不可用。因为我们知道这件事,所有这很好理解。

你可能会想:我定义了一个初始值!你可以试试,但是它依然不管用,因为数据就像一个字符串一样依然只是停留在当前的作用域中。如果你将$rootScope.username赋值为””,你最终将得到两个叫做username的属性,一个位于根作用域中,另一个存在于我们编写的控制器中。

为了解决这个问题,你应该使用一个包装好的模型。换句话说,你应该在模型中使用'.'。
对上面的例子进行一些修改:使用user.name而不是username。

<span>Outside Controller: Your name is: {{user.name}}</span>
<div ng-controller="SignupController">
    <span>Inside Controller: Your name is: {{user.name}}</span>
    <fieldset legend="User details">
        <input ng-model="user.name">
    </fieldset>
</div>

数据绑定现在被赋值给了user.name。因为如果在当前作用域下找不到user对象,$rootScope.user会被隐式的读取,因此这个问题得以解决。除此之外它也能够帮助你将模型结构化。这确实是一个双赢的方法。
但是你还是会发现你还是很容易犯错误,因为有许多内建的AngularJS指令 -- 或者是你自己创建的指令 -- 会创建自己的子作用域。比如说下面的这些指令:

ng-controller:一个控制器有自己的作用域(因为它会在作用域中赋予行为)。
ng-form:将会使用一个特别的表单控制器,因此会产生一个新的作用域。注意:

会创建一个ng-form的实例。
ng-repeat:每一个项目都有自己的子作用域(因为’item’是循环的内容)。
ng-switch:改变了DOM因此它拥有自己的作用域。
ng-view: 或多或少有些不相关,因为你总是会在ng-view下指明一个控制器。
最佳实践:

为了避免无结构化的内容和错误的作用域上下文以及使用指令隐式生成的作用域所产生的问题,不要在没有包装的对象上绑定一个未经绑定的数据。

陷阱6: 没有正确使用jQuery

AngularJS实现了一个jQuery的子集jQLite。它的基本操作和jQuery非常相似,然而,它并不是完整的jQuery。如果你需要使用完整的jQuery实现,你需要在AngularJS被载入之前加载jQuery。只有这样,AngularJS才会跳过jQLite而使用jQuery。否则二者都会被载入进去,AngularJS使用jQLite,其他部分使用jQuery。

最佳实践:

在AngularJS之前载入jQuery。
总结

本文为AngularJS的初级开发者提供了6个常常会遇到的陷阱。如果你之前使用的是jQuery, 那么你应该记住在AngularJS中应该使用声明式的方法而非命令式的方法。如果你尝试走jQuery的老路,那么你注定会在AngularJS中失败。

试着理解将作用域作为获取数据的场所,如果你试着从其他地方获取数据,最终将会出现问题。

使用上面提到的最佳实践,并确保你在编写AngularJS应用的过程中也探索了API文档。正确的使用其中的功能。

确保你合适的解耦你的应用:使用指令,控制器,服务和模板。显然你并不需要将代码分散到许多组件中,根据你的需要使用框架。

如果你都遵循了这些规则,你一定能够享受在AngularJS编程。

本文译自AngularJS: 6 Common Pitfalls Using Scopes,原文地址http://thenittygritty.co/angularjs-pitfalls-using-scopes

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.