• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

从Rust语言学习经验,并应用到前端JS中

武飞扬头像
juejin
帮助213

前言

Rust 是当下最热门、最先进的语言之一,前端领域的新三剑客更被戏称为"js, ts, rs",不过 rust 在前端领域更多的是参与到基础建设(swc 等)、wasm(yew 等)等方面,而 wasm 实际上在大多数场景下性能都不如拥有 JIT 的 js,所以主要场景就只剩下了基础建设

那么对于我们这些日常工作中不常接触基础建设的纯前端人来说,能从 rust 中学习到一些什么东西呢,这就是本文所关注的

变量默认不可变

不像其他大多数语言,rust 放弃使用 readonly / const 之类的修饰符来表示变量不可变,而是选择了变量默认不可变,使用 mut 修饰符来显示表示变量是可变的。这强迫开发者必须思考这个变量是不是真的需要变化,牺牲了灵活性获得了更好的可维护性

let a = 1;
a = 2; // error!
let mut b = 1;
b = 2; // right

随着语言的发展,mutable 越来越被认为是有害的,幸运的是,js 也可以很方便地声明不可变变量,所以我们的变量也应该默认是 const 的(eslint 可以检查)

依赖类型系统构建更安全的代码

开启严格模式

相较于 ts,rust 的类型推导能力更加强大

fn main() {
    // 因为有类型说明,编译器知道 `elem` 的类型是 u8。
    let elem = 5u8;
    // 创建一个空向量 vector(即不定长的,可以增长的数组)。
    let mut vec = Vec::new();
    // 这时候,vec 是 Vec<T>
    vec.push(elem);
    // 这时候,vec 变成了 Vec<u8>
    vec.push("s") // error, expected `u8`, found `&str`
}

而 ts 没有这个能力,导致有些时候我们的代码不能得到类型系统的保护,开启严格模式可以显著减少这类问题,比如:

// 非严格模式
const [state, setState] = useState([]);
       ^^^ state is any[]
setState([1])
setState(["xxx"]) // state 可以被赋予任意值
// 严格模式
const [state, setState] = useState([]);
       ^^^ state is never[]
setState([1]) // error
// 必须提前定义类型
const [state, setState] = useState<number[]>([]);
setState([1]) // right
setState(["xxx"]) // error

更安全的异常处理

在有些前端代码中充斥着各种 try catch,这是一种非常低效且不安全的错误处理机制,这里不展开讨论。我们看看 rust 是怎么做的

在绝大多数情况下,rust 要求你必须承认你的代码是可能出错的,并提前采取行动来处理这些错误。错误分为两类:可恢复错误和不可恢复错误。rust 用 Result<T, E> 处理可恢复错误,用 panic! 处理不可恢复错误。这里我们不讨论异常处理的哲学,只看可恢复错误的处理方式

// 背景知识,rust 内置了 Result 枚举,没有错误时返回 Ok(T),错误时返回 Err(E)
enum Result<T, E> {
    Ok(T),
    Err(E),
}

// 编写我们的业务逻辑
fn may_fail() -> Result<Happy, Sad> {
    // 业务逻辑
}

// 编写一个 wrap 函数统一处理错误
fn call_and_handle() -> Result<(), ()> {
  match may_fail() {
    Ok(happy) => {
      println!(":)");
      Ok(())
    },
    Err(sad) => {
      eprintln!(":(");
      // 在这里编写统一的错误处理逻辑
      Err(())
    }
  }
}

fn caller() -> Result<(), ()> {
  call_and_handle()?; // ? 是一个宏,在 Err 发生时终止函数的执行
  call_and_handle()?;
  call_and_handle()?;
  // 除了 match,还可以用 if let 更快捷地处理某些场景
  if let Ok(happy) = may_fail() {
    println!("Ok! {:?}", happ);
  } else {
    // 解构失败。切换到失败情形。
    println!("some thing error!");
  };
  println!("I am so happy right now");
  Ok(())
}

fn main() -> Result<(), ()> {
  caller()?;
  caller()?;
  caller()
}

可以看到,核心的处理思想是:错误链是可传播的,一旦一个地方定义了可能发生的错误,那么任意引用的地方都得考虑错误情况,否则编译就会报错,所谓错误,仅仅是代码逻辑的另一个分支罢了

ts 同样可以做到这点:

// 一个简单封装的 axios 请求
export interface CustomAxiosResponse<T> {
  success: true;
  msg?: string;
  data: T;
}

export interface CustomAxiosResponseErr {
  success: false;
  code: string;
  msg: string;
  isHttpError: boolean;
}

export const request = async <T>(
  config: RequestConfig,
): Promise<CustomAxiosResponse<T> | CustomAxiosResponseErr> => {
  try {
    const response = await axios.create({}).request(config);
    if (response.data.code !== '000000') {
      return {
        success: false,
        code: response.data.code,
        msg: response.data.msg,
        isHttpError: false,
      };
    }
    return {
      success: true,
      data: response.data.data,
    };
  } catch (e) {
    return {
      success: false,
      code: '',
      msg: '',
      isHttpError: !isNumber(Number(e)),
    };
  }
};

// 一个获取用户信息的接口
export const getInfo = () =>
  request<UserDetail>({
    url: '/info',
    method: 'get',
  });

// 使用
async () => {
    const res = await getInfo();
    const { name } = res.data;
                     ^^^ error, 请求可能失败
    if (!res.success) {
        // do something
        return;
    }
    const { name } = res.data // right,因为上面已经处理了错误的分支
}

或者更进一步,我们可以舍弃 if else,也采用 rust 中的 Result 和 Option 结构,就像这个库做的

什么是字符串?

看起来是个很弱智的问题,在大多数语言中,字符串就是字符串,像 js 这样的脚本语言对字符串的定义更是简单——字符串就是文本片段。但是事实上,字符串可能比我们印象中更复杂。

字面量(literal)

一个容易被忽视的点是,我们写下的代码和真正运行的代码并不完全一样,中间是有一次转义的,即使大多数情况下这次转义看起来无关紧要

// 我们用引号包裹了一段文本,这就是 js 中的字符串了
// 但是更精确的说,'something' 是一段字符串字面量,但此时字面量的值和真实的变量值是相同的
const str = 'something'

// 但是某些情况下,字面量的值不再和变量值相同
const str2 = 'sss\u2714'
      ^^^ str2 is 'sss✔'

Rust 中的字符串

rust 中的字符串类型更复杂,主要可以分为 3 类:String、str、&str

str 是语言级别唯一的一种字符串类型,被称为字符串切片

&str 是 str 的引用,通常我们不会直接接触到 str,接触到的都是其引用

String 是标准库实现的字符串类型,相较于 str 拥有更多更灵活的能力。str 硬编码的、无法修改的,而 String 是可变长度的、可改变所有权的 UTF-8 编码字符串

// 字符串字面量是字符串切片
let myStr = "something";
// 相当于
let myStr: &'static str = "something"; // 'static 是生命周期,表示整个周期内有效
// 也就是说,我们写下的 "something" 是 str
// 而被赋值的变量 myStr 是对其的引用,类型是 &str

let mut myString = String::from("something"); // 或者 "something".to_string()
// 可以修改
myString.push('x');

那么,这有什么用

对于 js 这种不需要关心内存分配的脚本语言来说,似乎是“没用的知识又增加了”,不过有时候他也能帮助我们理解一些不那么直观的设计背后的原因。

看到其他文章有提到 webpack.DefinePlugin 中的字符串值必须用 JSON.stringify 或者更多一层的引号包裹起来才能正常使用,官方文档也明确说明了这一点,但是并没有详细解释原因,如果我们了解字面量的概念的话其实就很容易理解了

webpack plugin 是依赖 parser.hooks 工作的,而 parser 是依赖 ast 工作的,试想一个最简单的场景

// 我们定义了一个全局变量
new webpack.DefinePlugin({
  VERSION: "5fa3b9"
})

// 代码中使用
const a = VERSION
// 那么对于 VERSION,AST 中会解析成如下结构
{
    "type": "Identifier",
    "name": "VERSION",
    ...
}
// 替换之后
{
    "type": "Identifier",
    "name": "5fa3b9",
    ...
}
// 显然,当我们再次解析 AST 生成编译代码后, 5fa3b9 变成了一个变量而不是字符串,这时候就会报错

你可能会问,为什么替换过程中不自动处理成 "name": "'5fa3b9'" 呢,因为这里只是最简单的 Identifier 场景,事实上还有很多个其他场景不易处理。或者换个角度想,如果真有默认处理的逻辑,那我希望把 VERSION 替换为 window.version,就无法实现了

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanegjj
系列文章
更多 icon
同类精品
更多 icon
继续加载