分类: 教程

  • React Native + Expo 入门级实战开发多平台应用 WhiteScreen:3. 深入开发,完成应用主体

    React Native + Expo 入门级实战开发多平台应用 WhiteScreen:3. 深入开发,完成应用主体

    感谢剪辑同学的努力工作,第三集上线。

    油管地址:https://youtu.be/0Ix-Y-MPQY0

    B站地址:https://www.bilibili.com/video/BV1CxnJzfEAk/

    继续分享 React Native+Expo 应用开发,帮大家补上全栈开发的最后一块拼图:移动应用开发。

    在这个系列视频里,我会用一个入门级小应用 WhiteScreen 作为范例,介绍如何使用 React Native+Expo 开发移动应用,并上传到应用商店。虽然国内市场基本被几个超级应用垄断,但是海外的广阔天地仍然大有可为。而 React Native 可以最大限度的利用我们已有的技术积累,让我们快速完成原型开发与需求验证,是全栈开发的不二之选。

    这期是第三次课,主要介绍:

    1. 使用 Zustand 管理状态,在页面间同步数据
    2. 使用原生组件进行布局
    3. 实现简单动画
    4. 实现本地数据存储于使用

    希望能给大家带来更多的可能性,在这个不那么容易的年代,让大家都有更多的选择余地与发展空间。

    希望大家留下宝贵的一键三连分享收藏,让更多的人能看到我的视频。

    有任何问题和建议,欢迎留言弹幕一起讨论。

    谢谢大家!

  • 【视频教程】React Native + Expo 入门级实战开发多平台应用 WhiteScreen:1. 移动应用开发现状+项目简介+技术栈简介

    【视频教程】React Native + Expo 入门级实战开发多平台应用 WhiteScreen:1. 移动应用开发现状+项目简介+技术栈简介

    前几天我在博客 感谢赞助商 Mizu Financial,重启我的自媒体之路 提到,因为工作调整,下一阶段我要捡起自媒体,在新的工作开始之前,提升一下自身的品牌价值。所以,我开启了新系列视频教程的录制。

    现在新视频终于上线,敬请观看。

    油管地址:https://youtu.be/YEPvihSIQkw

    B站地址:https://www.bilibili.com/video/BV1ZgHoztE4v/

    (更多…)
  • 系列视频制作中:Next.js 全栈开发每日一签网站

    系列视频制作中:Next.js 全栈开发每日一签网站

    之前的几个系列基本都连载完毕。前些天开了一个新坑:全栈架构师系列。但是这个系列比较难录:讲得深,应用场景就窄,观众就少,像我这种发免费课的方式就比较亏;讲得浅,我觉得也没啥意思,担心将来大家一看,“就这?”。还有一点,技术难题不是每天都能遇到,要积累适合的话题,还要不担心泄漏技术秘密,其实比较难搞。所以只录了一期,因为第二期迟迟没想好怎么录,一直也没发。

    前阵子跟 201 老同事吃饭,了解到她也“毕业”了,正在小红书上创作“每日一签”,突然我觉得可以跟以前做的 拜拜 应用结合起来,搞个每日一签网站:

    1. 固定更新能提供 SEO 价值
    2. 拜佛和求签本身就是强关联的功能,可以把用户留住

    于是我们一拍即合,就有了 每日一签 这个网站。这个网站用到以下技术栈:

    1. 后台使用 Strapi 快速搭建,部署在 Zeabur 的 Docker 服务上
    2. 全栈开发使用 Next.js,App Router,全部 SSR,数据使用 Strapi RESTful API
    3. CSS 使用 TailwindCSS,并且基于 TailwindCSS 家的 Commit 模版二次开发
    4. 网站部署在 Vercel 上
    5. 数据库使用 TiDB Cloud Serverless (这次 TiDB Cloud 在我学习 Strapi 的过程中立了大功)
    6. 存储和 CDN 使用 Cloudflare R2
    7. 统计服务也用 Vercel

    这套技术栈完全免费,各种云服务的免费额度可以跑到一个很高的量。是我非常推荐的全栈技术栈,也是各种独立开发、出海创业的首选。所以从开始我就想把它做成一套全栈开发课程。正好可以作为之前几套全栈开发系列教程的补充:

    1. 基于 Next.js + React,便于大家学习和扩展技术栈
    2. 更复杂的网站,更丰富的功能
    3. 更复杂的数据结构和 API,但是更简单的后端处理
    4. 更全面的基础设施服务

    那么说干就干,我就一边开发一边制作视频。新的课程可能包含以下章节:

    1. 项目介绍,技术栈简介(视频制作中)
    2. 使用 Monorepo 管理复杂项目
    3. 使用 Docker 部署 Strapi 应用到 Zeabur (下周一 10-14 录制,正好填上欠 Zeabur 的推广)
    4. 使用 TiDB Cloud 提供数据库服务
    5. 使用 Cloudflare R2 提供存储和 CDN
    6. 使用 Next.js + Commit 模版开发网站
    7. 使用 Shadcn 组件库
    8. 添加每日页,制作 sitemap,提升 SEO
    9. 使用前端推送功能
    10. 使用 og 开发分享功能

    希望新的系列课程能帮助大家学会现代化的全栈开发,学会使用各种现代化的研发平台,给大家开展独立开发和技术出海带来新的机会。如果你对全栈开发、独立开发、技术出海感兴趣,请关注我的系列视频和博客更新。如果你对相关技术有疑问想寻求解答,欢迎留言。

  • 给 Bootstrap Modal 增加缩放功能

    给 Bootstrap Modal 增加缩放功能

    需求

    Bootstrap 应该还是目前最流行的前端基础框架之一。因为架构方面的优势,它的侵入性很低,可以以各种方式集成到其它项目当中。在我厂的各种产品里,都有它的用武之地。

    前两天,老板抱怨,说 Modal(弹窗)在他的屏幕上太小,浪费他的 5K 显示器。

    我看了一下,按照 Bootstrap 的设计,超过 1200px 就算是 XL,此时 .modal-lg 的宽度固定在 1140px。其实 Bootstrap 这么设计也有它的道理,因为人眼聚焦后宽度有限,如果弹窗太宽的话,内容一眼看不全,也不好。不过在我厂的产品里,弹窗要呈现火焰图,所以宽一些也有好处。

    技术方案

    那么,综合来看,最合适的做法,就给 Modal 添加一个拖拽的功能:用户觉得够大了,就这么着;用户想看大一点,就自己拉大一些,然后我记录用户的选择,以便复用。

    看过我《用 `resize` 和 MutationObserver 实现缩放 DOM 并记录尺寸》的同学,应该知道 resize 这个 CSS 属性,使用它可以很方便的给元素添加缩放功能。参考 caniuse 上面的普及度,大部分新版本的浏览器都已经支持,可以放心使用。

    使用它的时候要注意两点:

    首先,我们在缩放元素的同时,也会对它的子元素、父元素同时造成影响。因为在静态文档流当中,块级元素的宽度默认是父元素 content-box 的 100%,而高度由子元素决定。所以,对一个块级元素的缩放,不可能宽过它的父元素(如果限制了宽度的话),也不可能矮于它的子元素。

    其次,拖拽手柄的显示优先级很低,会被子元素盖住,哪怕子元素没有填充任何内容。换言之,一定要有 padding 的元素才适合添加 resize 缩放。

    实施方案

    总而言之,把这个属性加在哪个元素上面,很有讲究。具体到本次需求,Bootstrap Modal,最合适添加 resize 属性的的是 modal-content,因为它有 1rem 的内边距。

    但是限制宽度的是父元素,也就是 modal-dialog,它是响应式的,会根据显示器的宽度设置一个最大宽度。如果不修改它的 max-widthmodal-content 的最大宽度就无法超过它,达不到预期效果。但是也不能改成 width,这样的话,弹窗会失去弹性,分辨率低的时候表现不好。

    所以还是要在 max-width 上做文章。如果直接去掉它,modal-dialog 的宽度就会是 100%,失去弹窗效果,所以也不能这样做。最终,我的方案是:

    1. 窗口完全展开后,获取宽高,赋给 modal-content
    2. 去掉 modal-dialogmax-width
    3. 用 MutationObserver 监测 modal-content 的宽高,保存 localStorage,以便接下来使用

    完整代码展示

    我厂的产品基于 Vue 开发,所以部分逻辑用 Vue 组件实现。

    效果演示

    为方便在 Codepen 里呈现,有部分修改。

    代码及解释

    <template lang="pug">
    .modal.simple-modal(
      :style="{display: visibility ? 'block' : 'none'}",
      @click="doCloseFromBackdrop",
    )
      .modal-dialog.modal-dialog-scrollable(
        ref="dialog",
        :class="dialogClass",
      )
        .modal-content(ref="content", :style="contentStyle")
          .modal-header.p-2
            slot(name="header")
              h4 {{title}}
            span.close(v-if="canClose", @click="doClose") ×
          .modal-body
            slot(name="body")
    </template>
    
    <script>
    import debounce from 'lodash/debounce';
    
    const RESIZED_SIZE = 'resized_width_key';
    let sharedSize = null;
    
    export default {
      props: {
        canClose: {
          type: Boolean,
          default: true,
        },
        size: {
          type: String,
          default: null,
          validator: function(value) {
            return ['sm', 'lg', 'xl'].indexOf(value) !== -1;
          },
        },
        resizable: {
          type: Boolean,
          default: false,
        },
        backdrop: {
          type: Boolean,
          default: true,
        },
        title: {
          type: String,
          default: 'Modal title',
        },
      },
    
      computed: {
        dialogClass() {
          const classes = [];
          if (this.size) {
            classes.push(`modal-${this.size}`);
          }
          if (this.resizable) {
            classes.push('modal-dialog-resizable');
          }
          if (this.resizedSize) {
            classes.push('ready');
          }
          return classes.join(' ');
        },
        contentStyle() {
          if (!this.resizable || !this.resizedSize) {
            return null;
          }
          const {width, height} = this.resizedSize;
          return {
            width: `${width}px`,
            height: `${height}px`,
          };
        },
      },
    
      data() {
        return {
          visibility: false,
          resizedSize: null,
        };
      },
    
      methods: {
        async doOpen() {
          this.visibility = true;
          this.$emit('open');
          if (this.resizable) {
            // 通过 debounce 节流可以降低函数运行次数
            const onResize = debounce(this.onEditorResize, 100);
            // 这里用 MutationObserver 监测元素尺寸
            const observer = this.observer = new MutationObserver(onResize);
            observer.observe(this.$refs.content, {
              attributes: true,
            });
    
            if (sharedSize) {
              this.resizedSize = sharedSize;
            } 
            // 第一次运行的时候,记录 Modal 尺寸,避免太大
            if (!this.resizedSize) {
              await this.$nextTick();
              // 按照张鑫旭的说法,这里用 `clientWidth` 有性能问题,不过暂时还没有更好的解决方案
              // https://weibo.com/1263362863/ImwIOmamC
              const width = this.$refs.dialog.clientWidth;
              this.resizedSize = {width};
              // 这里产生纪录之后,上面的 computed 属性就会把 `max-width` 去掉了
            }
          }
        },
        doClose() {
          this.visibility = false;
          this.$emit('close');
        },
        doCloseFromBackdrop({target}) {
          if (!this.backdrop || target !== this.$el) {
            return;
          }
          this.doClose();
        },
    
        onEditorResize([{target}]) {
          const width = target.clientWidth;
          const height = target.clientHeight;
          if (width < 320 || height < 160) {
            return;
          }
          sharedSize = {width, height};
          localStorage.setItem(RESIZED_SIZE, JSON.stringify(sharedSize));
        },
      },
    
      beforeMount() {
        const size = localStorage.getItem(RESIZED_SIZE);
        if (size) {
          this.resizedSize = JSON.parse(size);
        }
      },
    
      beforeDestroy() {
        if (this.observer) {
          this.observer.disconnect();
          this.observer = null;
        }
      },
    };
    </script>
    
    <style lang="stylus">
    .simple-modal
      background-color: rgba(0, 0, 0, 0.5)
      .modal-content
        padding 1em
    
        .close
          cursor pointer
    
    .modal-dialog-resizable
      &.ready
        max-width unset !important
    
      .modal-content
        resize both
        margin 0 auto
    </style>

    注意

    因为浏览器的异步加载机制,有可能在 modal 打开并完成布局后,高度和宽度被内容撑开导致记录不准,或者内容被异常遮盖。请读者自己想办法处理,就当练习题吧。

    总结

    本次组件开发非常符合我理想的组件模式:

    1. 充分利用浏览器原生机制
    2. 配合尽量少的 JS
    3. 需要什么功能就加什么功能,不需要大而全

    在 MVVM 框架的配合下,这样的方案很容易实现。另一方面,每个项目都有独特的使用场景,通过长期在特定场景下工作,我们可以逐步整理出适用于这个场景的组件库,不断改进该项目的开发效率。我认为这才是组件化的正道。

  • 文档的最佳实践

    文档的最佳实践

    指导群里的小伙伴做小项目 应用创意:Chrome 共享首页,有两位同学主动领命,分开前后端就开做。结果我发现他们对文档的处理非常幼稚……所以便有了这样一个分享。

    我的观点:

    1. 文档当然要写,但不要用自然语言这样写一大篇
    2. 文档如果不好好维护,将来表现和实际代码不一致,会造成更大的问题
    3. 所以开发者应该用更强的方式约束文档和代码,而不是大家主观协作
    4. 所以我们——尤其在前后端协作的时候——应该写格式化数据结构描述,然后通过编译的方式,用数据结构描述输出文档、测试、Mock Data

    下面是视频:

  • 尬聊会:第一期实录

    尬聊会:第一期实录

    这个视频斗鱼说有问题,色情暴力之类的,给我拒了。我百思不得其解。如果你看完发现问题所在,请告诉我,谢谢。

    时间锚点:

    1. [01:40] webpack该怎么学习呢?
    2. [05:20] 我为什么想搞尬聊会
    3. [07:10] 面试小经验
    4. [16:20] node怎么进行学习
    5. [21:00] 经常被问 ……源代码? 怎么读别人的源代码呢?
    6. [29:35] 初学者学习es6有什么好的方法?
    7. [34:35] 请加下,现在做跨平台,ionic2和RN选哪个好一点呢?
    8. [38:45] Phonegap
    9. [41:45 进度条这里出问题了,不知道线上怎么样] 现在的很多公司是否都不愿意带新人,兼谈学习
      1. 非常主动
      2. 胆大(有问题就问)心细(不要逮着一个人问)脸皮厚
      3. 问题必须经过自己的思考,尽量问一句话能答完的问题
    10. [44:20] 框架选择问题。Vue 作者尤雨溪的访谈
    11. [51:07] 半路出家的 JSer 学写样式
    12. [57:00] 会写页面但是不会 JS 的同学怎么学?

    (更多…)

  • 678次直播总结,兼半年总结

    678次直播总结,兼半年总结

    时间过的可真快,计划表上的大部分内容几乎还在原地踏步,半年时间就过去了。

    今年的主要目标是做视频培训课程,不过很不赶巧,春节从日本回来我们全家就陷入感冒漩涡,你方咳罢我登场,轮着来,好半个月坏半个月,一直到过完五一才渐渐摆脱感冒阴影。于是系列视频教程就被我一拖再拖,第一波联系的 51CTO 学院已经把我放弃了,我也不好意思再找她们;第二波联系的慕课网要求甚严,现在都还没完成第一次课……

    另一方面,SF 增设讲堂后我就立刻加入开始直播,如今进行了8期——好吧,其实早就完成了8期,只不过后几期越做越惨,身心俱伤,所以我休养了一个月……

    (更多…)

  • 知加 zhijia.io 即将关闭

    知加 zhijia.io 即将关闭

    今天突然从知加运营那里得到消息,知加将于7月24日关闭。其实,得知 Easy 离职回老家当独立开发者,我就猜想这个产品命不久矣。

    还是挺遗憾的,因为从产品层面来看,知加有很多过人之处,目前看来市场上还真没有同类竞品:

    (更多…)

  • 第一场 GitChat 总结

    第一场 GitChat 总结

    开始之前,先做广告吧。

    GitChat 分享 《JavaScript 异步开发全攻略》

    为解决异步函数的回调陷阱,开发社区不断摸索,终于折腾出 Promise/A+。它不增加新的语法,可以适配几乎所有浏览器;以队列的形式组织代码,易读好改;捕获异常方案也基本可用。这套方案在迭代中逐步完善,最终被吸收进 ES2015。不仅如此,ES2017 中还增加了 Await/Async,可以用顺序的方式书写异步代码,甚至可以正常抛出捕获错误,维护同一个栈。可以说彻底解决了异步回调的问题。 现在大部分浏览器和 Node.js 都已原生支持 Promise,很多类库也开始返回 Promise 对象,更有各种降级适配策略。Node.js 7+ 则实装了 Await/Async。如果您现在还不会使用,那么我建议您尽快学习一下。

    下次直播分享 前端面试攻略:JavaScript 排序与搜索

    从事前端开发的同学很多从页面仔入门,比如说我,自学比例很大,有些时候会无意中忽视一些基础,比如算法、数据结构。这些欠缺在某些时候就会显得很致命,比如说面试,或者处理大量数据的场景。所以希望这样的一场分享能够帮助大家夯实原本不太扎实的基础,将来的开发之路更加顺畅。

    目前早鸟票发送中,7月13日前门票5折,19日前75折,开播当日恢复全价。

    (更多…)