分类: 编程教程

  • React自学教程 | React前端框架零基础入门学习

    React自学教程 | React前端框架零基础入门学习

    为什么选择学习React?

    如果你是前端开发者,或者想进入前端领域,React几乎是你绕不开的技术栈。它在GitHub上的星标数常年霸榜,全球数百万网站都在用它,国内的字节跳动、腾讯、阿里等大厂更是重度依赖React技术。

    很多人问我,React和Vue、Angular比有什么区别?我的感受是,React的生态最成熟,工作机会最多,但学习曲线相对陡峭一些。一旦你掌握了React的思想,再学其他框架会轻松很多。

    对于完全零基础的朋友,建议先补一下HTML、CSS、JavaScript的基础再来看React。如果你已经有一点前端基础,那么大概2-3个月就能达到能上手项目的水平。

    React前端开发学习路线图,从JavaScript基础到项目实战四阶段进阶,2-3个月入门路径

    React学习路线规划

    第一阶段:打牢JavaScript基础(2-4周)

    React本质上是JavaScript框架,所以JS基础至关重要。这个阶段你需要重点掌握:

    ES6+语法:let和const、箭头函数、模板字符串、解构赋值、Promise、async/await。这些是现代JavaScript的标配,React代码里随处可见。

    推荐学习资源

    • MDN Web Docs的JavaScript教程(免费,权威)
    • 《JavaScript高级程序设计》(红宝书)前10章
    • 阮一峰的ES6教程(中文免费资源,质量很高)

    学习建议:不要在这个阶段停留太久。ES6的每个特性都能讲半天,但实际用起来很快就熟悉了。我的经验是先过一遍基础概念,然后写代码时遇到不懂的再回头查。

    第二阶段:React核心概念(3-4周)

    当你对JavaScript有了基本感觉,就可以开始学React了。建议从官方文档入手,它是我见过最友好的框架文档。

    必须掌握的核心概念

    JSX:这是React的语法扩展,写起来像HTML但其实是JavaScript。一开始会觉得奇怪,写多了就习惯了。我当年第一次看到const element = <h1>Hello</h1>时懵了一下,但半小时后就“真香”了。

    组件:React应用就是由一个个组件构成的。组件可以是函数形式(函数组件)或类形式(类组件),现在主流是函数组件+Hook的方式。

    Props和State:Props是组件的输入参数,State是组件内部的状态管理。理解这两者的区别很重要——Props是从父组件传进来的,State是自己管理的。

    推荐学习路径

    1. 先看官方文档的”Quick Start”部分,大概2-3小时
    2. 然后跟着官方Tutorial做一个Todo List,大概4-6小时
    3. 之后可以看B站上的视频教程辅助理解

    推荐学习资源

    • React官方文档(react.dev)- 必读
    • BiliBili React教程(免费,选择播放量高的)
    • 《深入React技术栈》(进阶用)

    第三阶段:React Hooks(2-3周)

    Hooks是React 16.8引入的特性,彻底改变了React的写法。现在面试必问Hooks,所以这块必须学透。

    最重要的Hooks

    useState:管理组件状态,每个新手都是从它开始。

    jsx

    import { useState } from 'react';
    
    function Counter() {
      const [count, setCount] = useState(0);
      return (
        <button onClick={() => setCount(count + 1)}>
          点击次数: {count}
        </button>
      );
    }
    

    useEffect:处理副作用,比如数据请求、订阅等。刚学时容易和生命周期函数搞混,建议多写多理解。

    useContext:在组件树中传递数据,避免层层传递props的噩梦。

    useRef:访问DOM元素,或者保存不触发重新渲染的数据。

    useMemo和useCallback:性能优化用的,新手阶段可以先跳过,以后面试前再回来研究。

    第四阶段:周边工具和生态(2-4周)

    学完核心概念后,你需要学习React的开发工具链。

    必须掌握的

    Node.js和npm/yarn/pnpm:Node.js让JavaScript能运行在服务器端,npm是包管理工具。这些是前端开发的基础环境。

    Vite:目前最主流的React项目构建工具,比之前的Create React App快很多。新项目基本都是用Vite创建。

    React Router:做单页面应用必备的路由管理工具。

    状态管理:入门可以用useState和useContext,想深入可以学Redux或Zustand。

    CSS方案:可以用传统的CSS模块、CSS-in-JS,或者现在流行的Tailwind CSS。Tailwind上手快,配置好后写样式很高效。

    实用学习技巧和建议

    写代码比看教程重要

    我发现很多人喜欢看视频、看教程,但就是不动手。这是自学最大的坑——你以为看懂了,但一写代码就各种报错。

    我的建议是每学一个概念,就自己写一个Demo。看官方文档时,把例子手敲一遍,然后改改参数看看效果。哪怕只是改个颜色、改个文字,也比光看不练强一百倍。

    从小项目开始

    学完基础概念后,不要急着去做大项目,很容易半途而废。从小东西开始:

    • 做一个简单的计算器
    • 做一个待办事项列表
    • 做一个天气预报页面(调免费API)
    • 模仿一个你喜欢的网站首页

    每个小项目都能巩固你的知识,而且完成率高,能给你正反馈。

    学会看报错信息

    React的报错信息其实写得很清楚,但新手看到红字就慌。我的建议是认真读报错信息,它会告诉你哪一行、什么错误。比盲目百度强多了。

    善用搜索引擎

    遇到问题很正常,90%的问题别人都遇到过。善用Google(或者必应)、Stack Overflow、GitHub Issues搜索。你遇到的问题大概率能找到答案。

    免费学习资源汇总

    官方资源(强烈推荐)

    资源链接说明
    React官方文档react.dev最权威的学习资料
    React官方教程react.dev/learn/tutorial-tic-tac-toe官方Todo List教程
    React官方示例react.dev/learn/responding-to-events官方示例代码

    视频教程

    资源平台说明
    React入门BiliBili免费,选播放量高的
    2024最新React教程BiliBili系统全面
    React技术栈慕课网付费但质量高

    项目实践

    资源链接说明
    React官方示例react.dev/examples官方提供的各种示例
    GitHub React示例github.com/reactjs官方维护的示例仓库
    CodePen Reactcodepen.io在线写React代码

    社区和问答

    资源链接说明
    React中文社区zhubai.iaotic.com中文技术社区
    掘金React标签juejin.cn中文技术文章平台
    SegmentFaultsegmentfault.com技术问答社区

    常见问题解答

    Q:需要设计基础吗?

    A:不需要。React是逻辑层,你写CSS/Style是样式层,分工的。但懂一点CSS会让你的界面更美观。

    Q:学React需要学TypeScript吗?

    A:现在招聘里TypeScript几乎是标配了。我的建议是先学JavaScript版本的React,等基础扎实了再学TypeScript。一口吃不成胖子。

    Q:React Native和React有什么区别?

    A:React用于Web开发,React Native用于手机App开发。两者思想类似,但API不同。学完Web React后,入门React Native会轻松很多。

    Q:找工作需要达到什么水平?

    A:一般需要能独立完成中小型项目,理解组件化开发,会用状态管理,了解常见的性能优化。高级职位还需要懂原理、懂源码。

    学习资源推荐

    1. React官方文档(react.dev)- 从这里开始,不要跳过
    2. MDN JavaScript教程– JavaScript基础
    3. BiliBili React教程– 视频辅助学习
    4. GitHub官方示例– 实战代码参考
    5. 掘金/思否– 中文技术社区

    总结

    React学习是一个循序渐进的过程,不要想着一口吃成胖子。先把JavaScript基础打牢,然后跟着官方文档学React核心概念,再学习Hooks和周边工具,最后通过项目实战巩固。

    记住,写代码比看教程重要。每天坚持敲代码,比周末看一天视频有效得多。

    祝你在React学习的道路上越走越顺!如果遇到问题,欢迎在社区提问,你不是一个人在战斗。

    相关自学资源

  • Go语言编程自学教程 | Go语言学习资源 | 免费在线课程

    Go语言编程自学教程 | Go语言学习资源 | 免费在线课程

    为什么选择Go语言

    在编程语言的世界里,Go语言就像一匹黑马,短短几年就成为后端开发领域的香饽饽。如果你正在考虑学习一门后端语言,Go绝对值得你认真考虑。

    Go语言的优势非常实在。首先是学习曲线平缓,它的语法简洁清晰,没有那些花里胡哨的特性,变量声明、函数定义都一目了然。就算你之前没接触过编程,也能较快地上手。其次是性能出色,Go是编译型语言,执行效率很高,特别适合处理高并发场景。再就是生态成熟,Docker、Kubernetes、Terraform这些大名鼎鼎的云原生工具都是用Go写的,相关的开源库和框架非常丰富。

    对于想转行做后端开发的朋友来说,Go语言是个不错的切入点。相比Java的繁重架构,Go更加轻量灵活;相比Python的性能瓶颈,Go又能满足生产环境的要求。而且现在招聘市场上对Go开发的需求一直在涨,尤其是云原生、DevOps、区块链这些领域,Go开发者的薪资待遇也比较可观。

    Go语言学习路线图,展示基础入门、核心特性、Web开发实战、项目实战四个阶段学习路径

    Go语言自学资源推荐

    官方文档与教程

    Go语言中文网https://studygolang.com/)是中文学习者必收藏的站点。这个网站不仅有官方文档的中文翻译,还有丰富的教程文章、问答社区和开源项目推荐。它的”Go语言标准库”部分对每个标准包都有详细的中文说明,特别适合初学者查阅。

    菜鸟教程Go语言教程https://m.runoob.com/go/)也是入门的绝佳选择。教程内容从基础语法讲起,逐步深入到函数、接口、并发等核心概念,每个知识点都配有代码示例,可以在线运行验证。对零基础自学者非常友好。

    如果你英文水平还不错,Go语言官方文档https://go.dev/doc/)一定要看看。官方文档的”Tour of Go”系列是公认的最佳入门教程,通过交互式练习让你边学边练,学习效果特别好。

    视频课程推荐

    B站上现在有大量优质的Go语言教学视频,这里推荐几个播放量和口碑都不错的内容:

    【Go语言教程零基础入门】 这类入门视频通常时长在8-15小时左右,从环境搭建讲起,逐步覆盖变量、数据类型、控制结构、函数、结构体、接口、并发等核心内容。适合完全零基础的学习者,跟着视频敲代码是很好的学习方式。

    【Go Web开发实战】 学完基础语法后,这类实战课程能帮你把知识串联起来。通常会从搭建Web服务器开始,讲到路由处理、数据库操作、API设计等实际开发技能。有些课程还会带你完整实现一个博客系统或电商后端,非常锻炼动手能力。

    【Go语言并发编程】 并发是Go语言最核心的特性,这类专题课程会深入讲解goroutine和channel的用法,以及常见的并发模式。内容稍微有些难度,建议有基础后再深入学习。

    经典书籍电子版

    对于喜欢看书的同学,《Go语言实战》《Go程序设计语言》(即”The Go Programming Language”)是公认的好书。前者偏实战,通俗易懂;后者是Go语言之父写的,理论深度更强。两本书的电子版在很多平台都能找到。

    Go语言学习路线规划

    第一阶段:基础入门(2-3周)

    这个阶段的目标是掌握Go语言的基本语法和编程思维。建议按照以下顺序学习:

    环境搭建是第一步。去Go官网下载安装包,配置好GOPATH或使用Go Modules管理项目。IDE推荐VS Code配合Go插件,或者直接用JetBrains的GoLand,功能更强大但需要付费。

    基础语法包括变量声明、数据类型、运算符、控制语句(if/for/switch)、数组和切片、map字典、字符串处理。这部分内容和其他编程语言大同小异,有其他语言基础的话学起来会很快。

    函数要重点学习多返回值、变长参数、匿名函数、闭包这些Go特有的语法。特别是多返回值,这在错误处理中非常有用。

    第二阶段:核心特性(2-3周)

    结构体和方法:Go没有类的概念,通过结构体和方法来实现面向对象编程。要理解值接收器和指针接收者的区别,以及如何设计自定义类型。

    接口是Go语言最精髓的部分。要理解接口的隐式实现、多态的思想,以及空接口interface{}的用法。interface{}相当于其他语言的Object类型,可以存储任意类型的值。

    并发编程是Go的杀手锏。goroutine让并发编程变得极其简单,channel又提供了安全的通信机制。这部分需要多动手实践,理解CSP并发模型的核心思想。可以尝试写一些并发下载文件、并发处理数据的程序来练手。

    第三阶段:Web开发实战(3-4周)

    学完核心语法后,就可以进入实战阶段了。建议学习以下内容:

    HTTP基础:了解HTTP协议的工作原理,学会使用net/http包处理HTTP请求和响应。能够搭建一个简单的Web服务器,处理GET和POST请求。

    路由处理:学习使用gorilla/mux或gin框架来管理路由。推荐直接学习Gin框架,它是目前最流行的Go Web框架,API设计优雅,性能出色。

    数据库操作:学习使用database/sql包操作MySQL或PostgreSQL,以及使用ORM框架如GORM来简化数据库操作。掌握CRUD的基本流程。

    API设计:学习RESTful API的设计规范,能够设计清晰的接口。了解JSON序列化、参数校验、错误处理等常见问题的处理方式。

    第四阶段:项目实战(持续)

    学以致用是最好的学习方式。可以尝试做以下类型的项目来巩固知识:

    RESTful API服务:实现一个完整的CRUD API,包含用户管理、文章发布等功能。涉及数据库设计、路由设计、中间件、参数校验、错误处理等完整流程。

    命令行工具:用Go写一些实用的命令行工具,比如文件处理工具、图片批量处理工具等。Go编译出来的二进制文件可以直接运行,不需要安装运行时,很适合做这类工具。

    Web服务:完整实现一个博客系统或者Todo应用,从前端到后端全部自己搞定。如果想前后端都练习,可以用Vue或React做前端,Go做后端API。

    Go语言学习建议

    多动手写代码

    编程是门实践性很强的技能,看再多教程不如自己动手敲一遍。建议每学一个知识点,就自己写代码验证一下。遇到不会的问题,先自己思考,思考不出来再去查资料。解决bug的过程往往能学到更多东西。

    从官方示例学起

    Go语言官方文档的质量非常高,每个标准包都有详细的说明和示例代码。养成看官方文档的习惯,很多问题都能在里面找到答案。官方还提供了一些优秀的开源项目,比如go/test这些项目的代码质量都很高,有余力的话读一读收获很大。

    加入社区交流

    一个人学习容易遇到瓶颈,建议加入一些Go语言学习群或社区。现在有很多Go语言的技术社区、知乎专栏、知识星球等,遇到问题可以提问,和别人交流能拓宽思路。看到别人提出的问题也可以思考一下,如果是自己会怎么解决。

    关注优质博客

    推荐关注一些Go语言领域的技术博主,他们经常会分享实战经验、最佳实践、踩坑记录等内容。比如一些大厂工程师的技术博客,质量都很高。关注多了之后,你会慢慢了解业界在用什么技术、流行什么框架、遇到什么问题,视野会更开阔。

    常见问题解答

    Q:Go语言适合完全没有编程基础的人学吗?

    A:Go语言的语法确实相对简单,对新手比较友好。但如果你完全是编程小白,建议先花一两周时间学习一下计算机基础知识,了解变量、函数、循环这些基本概念后,再学Go会轻松很多。

    Q:学Go还是学Java/Python好?

    A:没有绝对的答案,关键看你的目标。Java生态成熟,企业级应用多;Python在数据科学、AI领域强;Go在云原生、DevOps、微服务领域有优势。如果你想做后端开发,Go是个不错的选择;如果对AI更感兴趣,Python可能更合适。

    Q:学完基础后多久能找到Go开发工作?

    A:这个问题因人而异。通常来说,学完基础语法和Web开发,能独立完成一些小型项目,大概需要2-3个月的时间。但要达到企业用人标准,还需要更多的项目经验和工程实践。建议多做一些完整项目,积累经验后再去找工作。

    总结

    Go语言是一门上手容易、功能强大、前景广阔的语言,非常适合作为后端开发的入门语言或进阶选择。通过本文推荐的免费自学资源和系统的学习路线,相信你能顺利开启Go语言的学习之旅。

    记住,编程学习没有捷径,多写代码、多做项目、多思考总结,才是提升的不二法门。祝你学习顺利!

    相关学习资源推荐:

  • SQL数据库自学教程 | SQL学习资源 | 免费在线课程

    SQL数据库自学教程 | SQL学习资源 | 免费在线课程

    为什么选择学习SQL数据库

    很多人在学编程的时候,会纠结要不要学数据库。我的建议是:只要你跟数据打交道,就早点学

    SQL作为最通用的数据库查询语言,几乎所有IT岗位都会用到它。后端开发者要用它操作数据库,数据分析师要用它提取数据,产品经理偶尔也要查数据做决策。学会SQL,相当于给自己多开了一扇门。

    而且SQL语法相对简单,入门门槛不高。我见过很多非计算机专业的朋友,通过自学SQL成功转行数据分析或者找到运营相关的工作。它的学习曲线比较平缓,适合零基础的学习者。

    学习SQL最大的好处是能直接看到结果。你写一条查询语句,马上就能看到返回的数据,这种即时反馈让学习过程更有成就感。

    SQL学习路线扁平流程图,基础语法CRUD、条件过滤、聚合分组、多表关联四阶段与数据库类型对比

    SQL数据库学习路线

    第一阶段:SQL基础语法

    刚开始学SQL,最重要的是理解几个核心操作,简称CRUD

    查询数据(SELECT)
    这是SQL最常用的操作。比如你想查看用户表里所有用户信息,可以用:

    sql

    SELECT * FROM users;
    

    如果只要看用户名和邮箱两列:

    sql

    SELECT username, email FROM users;
    

    插入数据(INSERT)
    往表里添加新记录:

    sql

    INSERT INTO users (username, email) VALUES ('张三', 'zhangsan@example.com');
    

    更新数据(UPDATE)
    修改已有的数据:

    sql

    UPDATE users SET email = 'new@example.com' WHERE username = '张三';
    

    删除数据(DELETE)
    删除不需要的记录:

    sql

    DELETE FROM users WHERE username = '张三';
    

    这几个操作涵盖了日常工作中80%的数据库操作需求。把这四个学明白,你就已经入门了。

    第二阶段:条件查询和过滤

    学会基础查询后,要学怎么过滤数据。WHERE子句是最常用的条件筛选工具:

    sql

    SELECT * FROM orders 
    WHERE status = 'completed' 
    AND amount > 100;
    

    这条语句会查询所有已完成且金额大于100的订单。

    常用的条件运算符包括:

    • = 等于
    • !=<> 不等于
    • >< 大于、小于
    • >=<= 大于等于、小于等于
    • ANDOR 组合条件
    • INNOT IN 在某个集合内/外

    第三阶段:聚合和分组

    当数据量大的时候,你可能需要对数据进行统计汇总。这时候就要用到聚合函数和GROUP BY:

    sql

    SELECT category, COUNT(*) as total, AVG(price) as avg_price
    FROM products
    GROUP BY category
    HAVING COUNT(*) > 10;
    

    这个查询会统计每个分类下的商品数量和平均价格,并且只显示商品数量超过10个的分类。

    第四阶段:多表关联查询

    实际工作中,数据往往分散在多个表里。这时候需要用JOIN来关联查询:

    sql

    SELECT u.name, o.order_no, o.amount
    FROM users u
    INNER JOIN orders o ON u.id = o.user_id
    WHERE o.amount > 500;
    

    JOIN有几种类型:

    • INNER JOIN:只返回两个表都有的记录
    • LEFT JOIN:返回左表所有记录,即使右表没有匹配
    • RIGHT JOIN:返回右表所有记录
    • FULL OUTER JOIN:返回两个表的所有记录

    推荐学习的数据库类型

    MySQL – 最流行的开源数据库

    MySQL是全球使用最广泛的免费开源数据库。它安装简单,文档丰富,社区活跃,非常适合初学者。

    学习资源推荐:

    • MySQL官方文档:最权威的学习资料,每个知识点都有详细说明
    • W3Schools SQL教程:在线互动学习平台,可以边学边练
    • LeetCode数据库题库:刷题巩固SQL技能,很多面试都会从这里出题

    PostgreSQL – 功能强大的进阶选择

    如果你想深入学习数据库,PostgreSQL是很好的选择。它支持更多高级特性,比如JSON数据类型、全文搜索、地理信息处理等。很多对数据库要求高的项目都会选择PostgreSQL。

    SQLite – 轻量级入门神器

    SQLite是最轻量的数据库,整个数据库就是一个文件。它不需要安装服务,适合初学者理解数据库原理,也适合做小项目的数据存储。

    实战项目练习建议

    光学不练肯定不行。建议按这个顺序做练习:

    练习一:学生信息管理系统
    设计一个简单的学生表,包含学号、姓名、班级、成绩等字段。然后练习:

    • 查询某个班级的所有学生
    • 统计每个班的平均分
    • 找出不及格的学生

    练习二:电商订单分析
    模拟电商场景,设计用户表、商品表、订单表。然后练习:

    • 统计每个用户的订单数量和总消费
    • 找出销量最好的商品
    • 分析每月销售额趋势

    练习三:社交媒体数据统计
    模拟微博或微信场景,设计用户表、关注表、动态表。练习:

    • 找出关注数最多的用户
    • 统计每个用户发布的动态数量
    • 找出被关注最多的用户

    学习SQL的常见问题

    Q:需要安装什么软件?
    A:初学者可以用在线SQL环境,比如SQLFiddle或者DB Fiddle,不需要安装任何东西。如果想本地练习,Windows推荐安装MySQL,Mac推荐用Homebrew安装MySQL或PostgreSQL。

    Q:SQL和Python先学哪个?
    A:建议先学SQL。SQL语法简单,学起来快,而且很多Python教程会默认你懂SQL。学会SQL后再学Python的数据处理库(如Pandas)会更容易。

    Q:学SQL能找什么工作?
    A:数据分析师、商业分析师、后端开发、数据运营、产品经理(需要查数据做决策)这些岗位都要求SQL技能。

    Q:多久能学会SQL基础?
    A:如果每天投入1-2小时,两到三周可以掌握基础语法。一个月左右可以做简单的数据分析项目。

    学习资源汇总

    资源类型推荐资源特点
    在线教程W3Schools SQL免费、互动、边学边练
    在线教程SQLZoo实战练习型
    刷题平台LeetCode数据库面试必备、难度适中
    视频教程B站MySQL教程视频讲解、适合零基础
    电子书《SQL必知必会》经典入门书籍
    数据库工具Navicat可视化数据库管理

    总结

    SQL数据库是数据时代的基础技能,学习门槛不高,但用处非常广泛。无论你是想转行数据分析、提升工作效率,还是做技术储备,SQL都是值得投资学习的技能。

    建议的学习顺序是:先掌握SELECT、INSERT、UPDATE、DELETE这四个基础操作,然后学习WHERE条件和聚合函数,接着攻克JOIN关联查询,最后通过实战项目巩固所学。

    记住,SQL是门实践性很强的技能。看十遍教程不如动手写十行代码。找个在线练习平台,开始你的SQL学习之旅吧!

  • C++编程自学教程 | C++学习资源 | 免费在线课程

    C++编程自学教程 | C++学习资源 | 免费在线课程

    前言

    C++可能是最有”深度”的编程语言之一。它既能让你深入理解计算机底层原理,又是游戏开发、嵌入式系统、高性能服务器等领域的主力语言。很多人想学C++,却被”太难”的印象劝退,其实只要找对方法,从零基础到能写实用项目并没有想象中那么遥远。

    这篇文章会带你了解C++学习的正确路径,推荐真正好用的免费资源,让你的自学之旅少走弯路。

    一、C++为什么值得学

    1.1 行业应用广泛

    C++的应用场景非常广泛:

    • 游戏开发:大部分3A游戏引擎(Unity、Cocos)底层核心都用C++
    • 嵌入式开发:智能硬件、汽车电子、航空航天等领域
    • 高性能服务器:高频交易系统、搜索引擎后端
    • 桌面软件:Photoshop、Office的插件和核心模块
    • 编译器:很多编程语言的编译器本身就是用C++写的
    C++学习四阶段阶梯图,基础语法面向对象STL现代C++循序渐进

    1.2 薪资待遇可观

    根据2026年招聘数据,C++开发工程师的平均薪资在国内一线城市可以达到20-40K,具备架构能力的高级工程师更是稀缺。

    1.3 提升编程思维

    学习C++的过程会强迫你理解内存管理、指针、面向对象等核心概念,这些知识会让你学习其他语言时更加轻松。

    二、开发环境快速搭建

    学习编程的第一步是搭建开发环境。对于零基础自学者,我推荐以下方案:

    2.1 Windows系统

    推荐组合:Visual Studio 2022 Community + C++桌面开发工作负载

    这是最省心的方案,安装时勾选”使用C++的桌面开发”即可,一键配置好所有工具链。

    另一个轻量选择是VS Code + MinGW-w64,适合喜欢简洁编辑器的同学。

    2.2 macOS系统

    直接使用Xcode自带的Clang编译器,或者通过Homebrew安装最新版本的LLVM:

    bash

    brew install llvm
    

    2.3 Linux系统

    Ubuntu/Debian系统一行命令搞定:

    bash

    sudo apt install build-essential cmake
    

    然后用VS Code配合C++插件,体验也很好。

    三、C++学习路线图

    3.1 第一阶段:基础语法(2-3周)

    这个阶段要掌握的核心内容:

    • 变量、数据类型、运算符
    • 条件判断(if-else、switch)
    • 循环结构(for、while)
    • 函数定义和调用
    • 数组和字符串基础

    第一个程序

    cpp

    #include <iostream>
    using namespace std;
    
    int main() {
        cout << "Hello, C++ World!" << endl;
        return 0;
    }
    

    不要小看这段代码,它包含了C++程序的基本结构:头文件引入、主函数、输出语句。把这几行代码敲进电脑,运行出结果,你的C++学习就正式开始了。

    3.2 第二阶段:面向对象编程(3-4周)

    C++的核心优势就是面向对象。这个阶段要掌握:

    • 类和对象的概念
    • 构造函数和析构函数
    • 继承和多态
    • 封装和访问控制
    • 运算符重载

    一个简单的类示例

    cpp

    class Student {
    private:
        string name;
        int age;
        double score;
    
    public:
        Student(string n, int a, double s) : name(n), age(a), score(s) {}
        
        void displayInfo() {
            cout << "姓名:" << name << endl;
            cout << "年龄:" << age << endl;
            cout << "成绩:" << score << endl;
        }
        
        bool isExcellent() {
            return score >= 90;
        }
    };
    

    3.3 第三阶段:STL标准模板库(2-3周)

    STL是C++的宝藏,里面封装了大量实用的数据结构和算法。必须掌握:

    • vector:动态数组,比普通数组更灵活
    • string:字符串处理,比C风格字符串好用太多
    • map/set:键值对和集合容器
    • algorithm:排序、查找、遍历等常用算法

    STL使用示例

    cpp

    #include <vector>
    #include <algorithm>
    #include <iostream>
    using namespace std;
    
    int main() {
        vector<int> numbers = {5, 2, 8, 1, 9};
        sort(numbers.begin(), numbers.end());
        
        for (int num : numbers) {
            cout << num << " ";
        }
        // 输出:1 2 5 8 9
        return 0;
    }
    

    3.4 第四阶段:现代C++特性(持续学习)

    2026年的C++已经进入现代C++时代(C++11/14/17/20/23成为主流)。相比传统写法,现代C++更加安全、高效:

    • auto类型推导:让代码更简洁
    • 智能指针:再也不用担心内存泄漏
    • Lambda表达式:配合STL算法如虎添翼
    • 范围for循环:遍历容器更优雅

    现代写法 vs 传统写法

    cpp

    // 传统写法
    for (int i = 0; i < numbers.size(); i++) {
        cout << numbers[i] << endl;
    }
    
    // 现代写法
    for (auto num : numbers) {
        cout << num << endl;
    }
    

    四、优质免费学习资源推荐

    4.1 在线教程

    LearnCpp.com(强烈推荐)

    • 完全免费,持续更新到C++23标准
    • 章节清晰,从入门到高级循序渐进
    • 每章都有代码示例,可直接运行
    • 网址:https://www.learncpp.com

    菜鸟教程C++

    4.2 视频课程

    B站黑马程序员C++教程

    • 讲解细致,配套练习题
    • 完全免费,适合零基础
    • 搜索关键词:”黑马程序员C++”

    The Cherno C++系列(YouTube)

    • 国外优质系列,深入底层原理
    • 需要一定英语基础
    • B站有中文搬运版

    4.3 经典书籍

    虽然视频免费,但书籍能提供更系统的知识体系:

    • 《C++ Primer Plus》:入门首选,通俗易懂
    • 《C++ Primer》:进阶必备,权威全面
    • 《A Tour of C++》:Bjarne Stroustrup所写,快速浏览C++全貌

    4.4 练习平台

    学编程最重要的就是多动手写代码:

    • LeetCode:算法题,C++题解最丰富
    • 牛客网:国内大厂面试题
    • Exercism C++ track:循序渐进的小练习

    五、常见问题解答

    Q1:零基础能学会C++吗?

    完全可以,但要有心理准备。C++学习曲线确实比Python这类语言陡峭,主要难在概念多(比如指针、内存管理)。我的建议是:不要急于求成,每个知识点都动手写代码验证,慢慢来反而更快。

    Q2:C++和Python先学哪个?

    如果你想深入理解计算机原理,以后做系统级开发、游戏引擎,选C++。如果急着做数据分析、人工智能、Web开发,选Python。但如果时间充裕,先学C++再学Python会让你对编程有更深的理解。

    Q3:学C++每天要花多少时间?

    如果每天2小时左右:

    • 零基础到入门:2-3个月
    • 入门到能写小项目:3-4个月
    • 能达到求职水平:6个月以上

    关键是坚持,碎片化学习效果不好,建议每天固定时间专注学习。

    六、实践项目推荐

    光学不练假把式。学完基础后一定要做项目巩固,这里推荐几个适合新手的:

    1. 猜数字游戏:练习条件判断和循环
    2. 学生信息管理系统:练习类和文件操作
    3. 简单计算器:练习函数和界面交互
    4. 命令行 Todo List:练习容器和结构体

    做项目的过程中,你会发现自己对很多概念理解得更深刻了。

    结语

    C++确实不是一门简单的语言,但它的价值值得你投入时间。从Hello World开始,一行一行敲代码,一个概念一个概念理解,几个月后你回头看,会发现自己已经走了很远。

    如果学习过程中遇到问题,多去Stack Overflow和CSDN搜索,你会发现大部分坑前人都踩过。保持好奇心和耐心,你一定能学会C++。

    相关学习资源

  • Java编程自学教程 | Java学习资源 | 免费在线课程

    Java编程自学教程 | Java学习资源 | 免费在线课程

    为什么选择Java?

    在编程世界里,Java绝对是个”老大哥”级别的存在。从1995年诞生至今,Java一直是企业级开发的主力军。TIOBE编程语言排行榜上,Java常年稳居前五,国内百度指数日均搜索量超过13000次,说明什么?说明这语言依然很香!

    Java的应用场景特别广泛

    • 企业级后台开发:银行系统、电商平台后台基本都用Java
    • Android应用开发:虽然Kotlin崛起,但Java依然是Android开发的主流语言
    • 大数据技术:Hadoop、Spark这些大数据框架都是Java写的
    • 服务器端应用:Spring、SpringBoot这些框架让Java开发效率飞起

    所以学Java,就业面是真的宽。不信你去招聘网站搜搜,Java开发工程师的岗位数量永远名列前茅。

    Java 自学全流程配图 - 基础语法、数据库、Web 开发、Spring Boot 框架学习路径,代码实战与项目演练指南

    第一步:做好准备工作

    硬件准备

    说实话,Java对电脑要求真不高。你那台大学用了四年的老笔记本可能都够用。我建议:

    • 内存:8GB起步,16GB更舒服(跑IDEA不卡)
    • 硬盘:256GB SSD足够,现在电脑基本都标配
    • CPU:i5或同等性能就行,别被那些”必须i7″的帖子吓到

    软件准备

    **JDK(Java Development Kit)**是必须装的,这是Java开发的基础环境。

    下载地址:https://www.oracle.com/java/technologies/downloads/

    建议选择JDK 17或JDK 21 LTS版本,这两个是长期支持版本,稳定性好,企业用得多。

    **IDE(集成开发环境)**的选择:

    IDE特点适合人群
    IntelliJ IDEA功能强大,智能提示超准进阶开发者
    Eclipse免费开源,插件丰富习惯免费工具的
    VS Code轻量级,可配置喜欢简洁的

    新手我推荐IntelliJ IDEA社区版,免费且足够用,而且企业里也大量使用这个。

    第二步:Java基础入门(1-2个月)

    学习路线

    plaintext

    Java基础语法 → 面向对象 → 集合框架 → 异常处理 → IO流 → 常用类库
    

    核心知识点

    1. Java基础语法

    这是地基,必须打牢:

    • 变量和数据类型(int、double、String、boolean…)
    • 运算符(算术、比较、逻辑)
    • 流程控制(if-else、for、while、switch)
    • 数组(一维数组、二维数组)

    java

    // 来个Hello World热身
    public class HelloWorld {
        public static void main(String[] args) {
            System.out.println("Hello, Java!");
        }
    }
    

    2. 面向对象编程(OOP)

    这是Java的核心思想,必须深入理解:

    • 类和对象(类是模板,对象是实例)
    • 封装(把数据藏起来,给统一访问入口)
    • 继承(子类继承父类,复用代码)
    • 多态(同一个方法,不同对象不同表现)

    java

    // 面向对象示例:定义一个学生类
    public class Student {
        private String name;  // 封装:私有属性
        private int age;
        
        // 构造方法
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        // Getter和Setter
        public String getName() {
            return name;
        }
        
        public void study() {
            System.out.println(name + "正在学习Java");
        }
    }
    

    3. 集合框架

    Java的灵魂工具箱,开发中天天用:

    • List(ArrayList、LinkedList):有序可重复
    • Set(HashSet、TreeSet):无序不可重复
    • Map(HashMap、TreeMap):键值对存储

    java

    import java.util.ArrayList;
    import java.util.HashMap;
    
    public class CollectionDemo {
        public static void main(String[] args) {
            // 列表:存多个学生
            ArrayList<String> students = new ArrayList<>();
            students.add("张三");
            students.add("李四");
            
            // 映射:学号-姓名
            HashMap<Integer, String> studentMap = new HashMap<>();
            studentMap.put(1001, "张三");
            studentMap.put(1002, "李四");
        }
    }
    

    4. 异常处理

    程序出错了怎么办?用try-catch捕获:

    java

    try {
        int result = 10 / 0;  // 会抛出ArithmeticException
    } catch (ArithmeticException e) {
        System.out.println("除数不能为0!");
    } finally {
        System.out.println("这里一定会执行");
    }
    

    推荐学习资源

    在线课程

    1. B站 – 韩顺平Java零基础入门
      • 优点:讲得细,适合零基础,每个知识点都有代码演示
      • 链接:搜索”韩顺平 零基础学Java”

    2. 尚硅谷Java零基础教程
      • 优点:视频质量高,课程体系完整
      • 链接:搜索”尚硅谷 Java基础”

    3. 菜鸟教程Java教程

    书籍

    • 《Java核心技术卷I》:经典入门书,知识点全面
    • 《Head First Java》:图文并茂,适合小白

    第三步:数据库与Web基础(1-2个月)

    数据库(MySQL)

    动态网站离不开数据存储,MySQL是入门首选:

    核心知识点

    • DDL(数据定义语言):CREATE、DROP、ALTER
    • DML(数据操作语言):INSERT、UPDATE、DELETE
    • DQL(数据查询语言):SELECT、WHERE、ORDER BY
    • 表关联:JOIN、LEFT JOIN

    sql

    -- 创建学生表
    CREATE TABLE student (
        id INT PRIMARY KEY AUTO_INCREMENT,
        name VARCHAR(50) NOT NULL,
        age INT,
        create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    
    -- 插入数据
    INSERT INTO student (name, age) VALUES ('张三', 20), ('李四', 21);
    
    -- 查询学生
    SELECT * FROM student WHERE age > 20 ORDER BY age DESC;
    

    学习资源

    • 视频:MySQL基础教程(B站搜索”MySQL基础教程 通俗易懂”)
    • 练习:SQLZoo(https://sqlzoo.net/)在线练习

    前端基础(HTML/CSS/JavaScript)

    Web开发三件套,不用精通但要会:

    • HTML:网页骨架,了解常用标签就行
    • CSS:美化网页,让页面好看
    • JavaScript:网页交互,实现动态效果

    JDBC

    Java连接数据库的官方API,必须掌握:

    java

    import java.sql.*;
    
    public class JdbcDemo {
        public static void main(String[] args) {
            String url = "jdbc:mysql://localhost:3306/test";
            String user = "root";
            String password = "123456";
            
            try (Connection conn = DriverManager.getConnection(url, user, password);
                 Statement stmt = conn.createStatement();
                 ResultSet rs = stmt.executeQuery("SELECT * FROM student")) {
                
                while (rs.next()) {
                    System.out.println("姓名:" + rs.getString("name"));
                }
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    

    第四步:JavaWeb开发(1-2个月)

    学习路线

    plaintext

    Servlet → JSP → Session/Cookie → Filter → Listener → MVC模式
    

    核心知识点

    Servlet:JavaWeb的核心,处理HTTP请求:

    java

    @WebServlet("/hello")
    public class HelloServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
                throws ServletException, IOException {
            resp.setContentType("text/html;charset=UTF-8");
            PrintWriter out = resp.getWriter();
            out.println("<h1>Hello, JavaWeb!</h1>");
        }
    }
    

    JSP:让HTML里能写Java代码(虽然现在不推荐,但要知道):

    jsp

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <body>
        <h1>当前时间:<%= new java.util.Date() %></h1>
    </body>
    </html>
    

    Session和Cookie

    • Cookie:存在浏览器,小量数据
    • Session:存在服务器,更安全

    第五步:主流框架(2-3个月)

    Spring Boot

    现在企业做JavaWeb,基本都是Spring Boot的天下了。它把Spring家族的东西整合好,让你”开箱即用”。

    特点

    • 快速构建项目
    • 自动配置
    • 内嵌服务器(Tomcat、Jetty)
    • 生态丰富

    java

    @SpringBootApplication
    @RestController
    public class DemoApplication {
        
        @GetMapping("/hello")
        public String hello() {
            return "Hello, Spring Boot!";
        }
        
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    

    MyBatis

    数据库访问层框架,比JDBC爽多了:

    xml

    <!-- mapper接口 -->
    public interface UserMapper {
        @Select("SELECT * FROM user WHERE id = #{id}")
        User findById(Long id);
        
        @Insert("INSERT INTO user(name) VALUES(#{name})")
        void insert(User user);
    }
    

    学习资源

    第六步:项目实战(持续)

    学了这么多,不做项目等于没学。推荐几个适合新手的项目:

    入门级

    1. 个人博客系统

    • 技术栈:Spring Boot + MyBatis + Thymeleaf
    • 功能:文章发布、分类、标签、评论
    • 学习资源:GitHub搜索”blog-system”

    2. 员工管理系统

    • 技术栈:Spring Boot + MySQL + Layui
    • 功能:CRUD、员工管理、部门管理
    • 学习价值:完整走一遍增删改查

    进阶级

    3. 秒杀系统

    • 技术栈:Spring Boot + Redis + RabbitMQ
    • 学习重点:高并发、缓存、消息队列
    • 面试价值:秒杀是高频面试题

    4. 仿外卖平台

    • 技术栈:Spring Boot + MySQL + Redis + 微信小程序
    • 功能:用户端、商家端、配送端

    学习建议与心态调整

    给自学者的几点忠告

    1. 别只看视频,要动手敲代码

    这是最大的坑。很多人”刷”完了整套视频,觉得自己会了,结果一动手就抓瞎。正确姿势是:看5分钟视频,立刻关掉,自己敲一遍,敲不出来再回去看。

    2. 遇到问题先自己解决

    善用搜索引擎和Stack Overflow。程序员80%的时间都在解决bug,这是常态。

    3. 坚持写学习笔记

    推荐用Notion或语雀,记录每天学了什么、踩了什么坑。笔记是你最宝贵的财富。

    4. 加入学习社群

    一个人自学容易放弃,找几个志同道合的伙伴,互相监督,共同进步。

    学习时间规划

    阶段内容建议时间
    入门期Java基础、面向对象每天2小时,持续6-8周
    进阶期数据库、Web基础每天3小时,持续6-8周
    框架期Spring Boot、MyBatis每天3小时,持续8-10周
    项目期实战项目每天4小时,持续4-6周

    相关资源推荐

    资源类型推荐名称链接
    在线课程菜鸟教程Javahttps://www.runoob.com/java/java-tutorial.html
    在线课程B站韩顺平Java零基础搜索”韩顺平 零基础学Java”
    在线课程尚硅谷Java基础搜索”尚硅谷 Java基础教程”
    书籍Java核心技术卷I各大电商平台有售
    书籍Head First Java各大电商平台有售
    社区CSDN Java频道https://www.csdn.net/
    社区掘金Javahttps://juejin.cn/

    结语

    Java自学是一条漫长的路,但只要方向对了,坚持下去就会有收获。从Hello World到独立完成项目,每个程序员都是这么过来的。

    记住:编程不是天赋,而是技能。技能靠的是练习,不是智商。你不需要有多聪明,只需要足够坚持。

    现在就去装JDK,打开IDE,开始你的Java编程之旅吧!有问题随时搜索引擎,你会感谢自己当初的选择。

    加油,未来的Java工程师!

    本文由自学导航网站整理发布,定期更新优质编程学习资源。

  • Shell脚本编程教程:从入门到实战

    Shell脚本编程教程:从入门到实战

    一、Shell脚本基础概念

    什么是Shell?

    Shell是Unix/Linux系统的命令解释器,它是用户与操作系统内核之间的桥梁。我们平时在终端输入的命令,实际上都是由Shell来解释执行的。常见的Shell类型包括:

    • Bash (Bourne Again Shell):Linux默认Shell,功能最强大
    • Zsh (Z Shell):功能丰富,可定制性强
    • Fish (Friendly Interactive Shell):用户友好,语法简单
    Shell脚本示例配图 - Bash脚本代码展示

    第一个Shell脚本

    让我们从最简单的例子开始:

    bash

    #!/bin/bash
    # 这是我的第一个Shell脚本
    echo "Hello, Shell World!"
    
    # 输出当前日期和时间
    echo "当前时间是:$(date)"
    

    保存为hello.sh,然后执行:

    bash

    chmod +x hello.sh    # 添加执行权限
    ./hello.sh           # 运行脚本
    

    提示#!/bin/bash称为shebang,它告诉系统使用哪个解释器来执行脚本。

    二、变量与数据类型

    定义和使用变量

    Shell中的变量定义非常灵活,不需要声明类型:

    bash

    #!/bin/bash
    
    # 定义字符串变量
    name="Linux"
    version="Ubuntu 22.04"
    
    # 定义数字变量
    count=100
    
    # 使用变量(两种方式)
    echo "系统名称: $name"
    echo "版本信息: ${version}"
    
    # 只读变量
    readonly PI=3.14159
    
    # 删除变量
    temp="临时数据"
    unset temp
    

    特殊变量

    Shell提供了一些特殊变量来处理脚本参数:

    bash

    #!/bin/bash
    
    echo "脚本名称: $0"
    echo "第一个参数: $1"
    echo "第二个参数: $2"
    echo "所有参数: $@"
    echo "参数个数: $#"
    echo "当前进程ID: $$"
    echo "上一个命令退出状态: $?"
    
    # 运行示例:./script.sh arg1 arg2
    

    变量替换

    bash

    #!/bin/bash
    
    var="hello"
    
    # 默认值替换
    echo "${var:-"默认值"}"      # 输出: hello
    echo "${unset_var:-"默认值"}" # 输出: 默认值
    
    # 设置默认值
    echo "${var:="新值"}"        # 输出: hello,var保持不变
    echo "${unset_var:="新值"}"  # 输出: 新值,unset_var被赋值
    
    # 检查变量是否已设置
    echo "${var:?"变量未设置"}"   # 输出: hello
    # echo "${unset:?"变量未设置"}" # 会报错退出
    

    三、条件判断与流程控制

    基础条件判断

    Shell中使用test命令或[ ]进行条件判断:

    bash

    #!/bin/bash
    
    # 数值比较
    num1=10
    num2=20
    
    if [ $num1 -eq $num2 ]; then
        echo "相等"
    elif [ $num1 -lt $num2 ]; then
        echo "$num1 小于 $num2"
    else
        echo "$num1 大于 $num2"
    fi
    
    # 常见比较操作符
    # -eq: 等于 (equal)
    # -ne: 不等于 (not equal)
    # -lt: 小于 (less than)
    # -le: 小于等于 (less or equal)
    # -gt: 大于 (greater than)
    # -ge: 大于等于 (greater or equal)
    

    字符串判断

    bash

    #!/bin/bash
    
    str1="hello"
    str2="world"
    str3="hello"
    
    # 字符串比较
    if [ "$str1" = "$str3" ]; then
        echo "字符串相等"
    fi
    
    if [ -z "$empty_str" ]; then
        echo "字符串为空"
    fi
    
    if [ -n "$str1" ]; then
        echo "字符串非空"
    fi
    
    # 常见字符串操作符
    # =: 相等
    # !=: 不等
    # -z: 字符串长度为0
    # -n: 字符串长度不为0
    

    文件测试

    bash

    #!/bin/bash
    
    file="/path/to/file.txt"
    
    if [ -e "$file" ]; then
        echo "文件存在"
    fi
    
    if [ -f "$file" ]; then
        echo "是普通文件"
    fi
    
    if [ -d "$file" ]; then
        echo "是目录"
    fi
    
    if [ -r "$file" ]; then
        echo "可读"
    fi
    
    if [ -w "$file" ]; then
        echo "可写"
    fi
    
    if [ -x "$file" ]; then
        echo "可执行"
    fi
    

    case语句

    对于多条件判断,case语句更加清晰:

    bash

    #!/bin/bash
    
    grade="B"
    
    case $grade in
        "A")
            echo "优秀"
            ;;
        "B")
            echo "良好"
            ;;
        "C")
            echo "及格"
            ;;
        *)
            echo "未知等级"
            ;;
    esac
    
    # 通配符示例
    read -p "请输入命令: " cmd
    
    case $cmd in
        start)
            echo "启动服务..."
            ;;
        stop)
            echo "停止服务..."
            ;;
        restart)
            echo "重启服务..."
            ;;
        *)
            echo "未知命令"
            ;;
    esac
    

    四、循环结构

    for循环

    bash

    #!/bin/bash
    
    # 遍历列表
    for fruit in apple banana orange; do
        echo "我喜欢吃: $fruit"
    done
    
    # 使用C风格语法
    for ((i=0; i<5; i++)); do
        echo "计数器: $i"
    done
    
    # 遍历文件
    for file in *.txt; do
        echo "处理文件: $file"
    done
    
    # 使用seq命令
    for num in $(seq 1 5); do
        echo "数字: $num"
    done
    
    # 简化写法
    for i in {1..10}; do
        echo $i
    done
    

    while循环

    bash

    #!/bin/bash
    
    # 基本while循环
    count=1
    while [ $count -le 5 ]; do
        echo "计数: $count"
        ((count++))
    done
    
    # 读取文件行
    while read line; do
        echo "读取到: $line"
    done < file.txt
    
    # 无限循环(配合break使用)
    while true; do
        read -p "输入q退出: " input
        if [ "$input" = "q" ]; then
            break
        fi
        echo "你输入了: $input"
    done
    
    # until循环(条件为假时继续)
    num=1
    until [ $num -gt 5 ]; do
        echo "until循环: $num"
        ((num++))
    done
    

    循环控制

    bash

    #!/bin/bash
    
    # break和continue
    for i in {1..10}; do
        if [ $i -eq 3 ]; then
            continue  # 跳过当前迭代
        fi
        if [ $i -eq 8 ]; then
            break     # 退出循环
        fi
        echo "数字: $i"
    done
    

    五、函数定义与使用

    函数基础

    bash

    #!/bin/bash
    
    # 定义函数
    function greet() {
        echo "你好,$1!"
    }
    
    # 调用函数
    greet "小明"
    greet "小红"
    
    # 带返回值的函数
    function add() {
        local sum=$(($1 + $2))
        return $sum  # 返回值只能是0-255
    }
    
    add 10 20
    result=$?
    echo "计算结果: $result"
    

    函数作用域

    bash

    #!/bin/bash
    
    variable="全局变量"
    
    function demo() {
        local local_var="局部变量"
        variable="修改后的全局变量"
        
        echo "函数内: $variable"
        echo "函数内: $local_var"
    }
    
    demo
    echo "函数外: $variable"
    # echo "函数外: $local_var"  # 这会报错
    

    函数参数

    bash

    #!/bin/bash
    
    function process() {
        echo "参数个数: $#"
        echo "所有参数: $@"
        echo "第一个参数: $1"
        echo "第二个参数: $2"
    }
    
    process "arg1" "arg2" "arg3"
    

    六、实战案例

    案例1:自动备份脚本

    bash

    #!/bin/bash
    # 自动备份脚本
    
    # 配置
    BACKUP_DIR="/backup"
    SOURCE_DIR="/data"
    DATE=$(date +%Y%m%d_%H%M%S)
    BACKUP_FILE="backup_${DATE}.tar.gz"
    
    # 创建备份目录
    mkdir -p "$BACKUP_DIR"
    
    # 执行备份
    if tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" "$SOURCE_DIR" 2>/dev/null; then
        echo "备份成功: ${BACKUP_FILE}"
        
        # 计算备份大小
        size=$(du -h "${BACKUP_DIR}/${BACKUP_FILE}" | cut -f1)
        echo "备份大小: $size"
        
        # 清理7天前的备份
        find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +7 -delete
        echo "清理旧备份完成"
    else
        echo "备份失败"
        exit 1
    fi
    

    案例2:批量重命名文件

    bash

    #!/bin/bash
    # 批量重命名脚本
    
    directory=$1
    prefix=$2
    
    if [ -z "$directory" ] || [ -z "$prefix" ]; then
        echo "用法: $0 <目录> <文件名前缀>"
        exit 1
    fi
    
    cd "$directory" || exit 1
    
    count=1
    for file in *; do
        if [ -f "$file" ]; then
            extension="${file##*.}"
            new_name="${prefix}_${count}.${extension}"
            mv "$file" "$new_name"
            echo "重命名: $file -> $new_name"
            ((count++))
        fi
    done
    
    echo "完成,共处理 $((count-1)) 个文件"
    

    案例3:系统监控脚本

    bash

    #!/bin/bash
    # 系统监控脚本
    
    echo "========== 系统监控报告 =========="
    echo "生成时间: $(date)"
    echo ""
    
    # CPU使用率
    echo "【CPU使用率】"
    top -bn1 | head -5
    
    # 内存使用
    echo ""
    echo "【内存使用】"
    free -h
    
    # 磁盘使用
    echo ""
    echo "【磁盘使用】"
    df -h | grep -E "^/dev"
    
    # 进程Top5
    echo ""
    echo "【CPU占用前5的进程】"
    ps aux --sort=-%cpu | head -6
    
    # 网络连接
    echo ""
    echo "【网络连接统计】"
    netstat -an | grep ESTABLISHED | wc -l
    echo "当前建立连接数"
    

    七、调试技巧

    调试选项

    bash

    #!/bin/bash
    
    # 启用调试模式
    # -x: 打印每条命令及其参数
    # -v: 打印输入的行
    # -n: 检查语法但不执行
    
    set -x  # 启用命令跟踪
    echo "这是调试信息"
    set +x  # 关闭命令跟踪
    
    # 另一种方式:运行时指定
    # bash -x script.sh
    

    错误处理

    bash

    #!/bin/bash
    
    # 遇到错误立即退出
    set -e
    
    # 未定义的变量视为错误
    set -u
    
    # 管道中任何一个命令失败,整个管道失败
    set -o pipefail
    
    # 使用trap捕获错误
    function error_handler() {
        echo "错误发生在第 $1 行"
    }
    
    trap 'error_handler $LINENO' ERR
    
    # 测试
    ls /nonexistent  # 会触发错误处理
    

    八、常见问题与最佳实践

    最佳实践

    1. 总是使用引号:避免变量为空时出现问题
      bash# 正确 if [ "$var" = "value" ]; then # 错误 if [ $var = "value" ]; then
    2. 使用[[ ]]替代[ ]:更强大的条件判断
      bash# 支持更多操作 if [[ $str =~ pattern ]]; then
    3. 添加错误处理:让脚本更健壮
      bashset -euo pipefail
    4. 添加使用说明:方便他人和自己理解
      bashfunction usage() { echo "用法: $0 [选项]" echo "选项:" echo " -h: 显示帮助" echo " -v: 详细模式" }
    5. 使用日志记录:便于问题排查
      bashLOG_FILE="/var/log/script.log" function log() { echo "[$(date)] $1" | tee -a "$LOG_FILE" }

    性能优化

    bash

    #!/bin/bash
    
    # 避免子shell,使用内联操作
    # 慢:
    for item in $(cat file.txt); do
        process $item
    done
    
    # 快:
    while read item; do
        process $item
    done < file.txt
    
    # 使用数组而非字符串拼接
    array=()
    for i in {1..1000}; do
        array+=("item_$i")
    done
    

    九、总结

    Shell脚本是每个Linux使用者必须掌握的技能。通过本教程的学习,你应该已经掌握了:

    • Shell变量的定义和使用
    • 条件判断和流程控制
    • 循环结构的各种用法
    • 函数的定义和调用
    • 实际场景中的脚本编写
    • 调试技巧和最佳实践

    继续练习,多写脚本解决实际问题,你会发现Shell脚本的强大之处。推荐进一步学习正则表达式、awk、sed等文本处理工具,它们与Shell脚本配合使用可以完成更复杂的任务。

    相关资源

    阅读更多

  • Vue3 Composition API实战教程:像搭积木一样构建组件

    Vue3 Composition API实战教程:像搭积木一样构建组件

    引言

    我认识一个朋友,之前写了两年Vue2,最近开始用Vue3重构项目。第一次看到Composition API的时候,他发了条朋友圈:”这不就是把options API拆开写吗?”

    说实话,我一开始也是这么想的。但当我用它写了两个实际项目之后,发现这玩意儿确实香。

    为什么?因为以前写组件,所有逻辑都堆在data、methods、computed、watch这四个选项里。同一个功能相关的代码被拆得七零八落,找起来头疼,改起来更头疼。Composition API把同一个功能的代码聚在一起,逻辑清晰多了。

    这篇文章就是把我从”看不懂Composition API”到”真香”的过程记录下来,帮你少走弯路。

    Vue3响应式系统配图 - setup函数与组合式函数演示

    环境准备:搭建Vue3项目

    使用Vite创建项目

    Vite是Vue官方推荐的构建工具,比Vue CLI快很多。

    bash

    # 创建项目
    npm create vite@latest my-vue-app -- --template vue
    
    # 进入项目目录
    cd my-vue-app
    
    # 安装依赖
    npm install
    
    # 启动开发服务器
    npm run dev
    

    项目结构大概是这样的:

    plaintext

    my-vue-app/
    ├── src/
    │   ├── assets/
    │   ├── components/
    │   ├── App.vue
    │   └── main.js
    ├── index.html
    ├── package.json
    └── vite.config.js
    

    Vue3组件的基本结构

    打开App.vue,你会看到这样的代码:

    vue

    <script setup>
    import { ref, computed, onMounted } from 'vue'
    
    // 响应式数据
    const count = ref(0)
    
    // 计算属性
    const doubleCount = computed(() => count.value * 2)
    
    // 方法
    function increment() {
      count.value++
    }
    
    // 生命周期钩子
    onMounted(() => {
      console.log('组件挂载完成')
    })
    </script>
    
    <template>
      <div>
        <p>计数:{{ count }}</p>
        <p>双倍:{{ doubleCount }}</p>
        <button @click="increment">+1</button>
      </div>
    </template>
    
    <style scoped>
    button {
      padding: 8px 16px;
      background: #42b883;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    </style>
    

    这就是Vue3 Composition API的基本写法。注意<script setup>语法,这是官方推荐的写法,比普通<script>更简洁。

    响应式系统:Vue3的核心

    响应式是Vue的灵魂。数据变化,界面自动更新,不需要手动操作DOM。

    ref和reactive

    Vue3有两种创建响应式数据的方式:

    ref:基础类型的响应式

    vue

    <script setup>
    import { ref } from 'vue'
    
    // ref用于基础类型(string、number、boolean等)
    const count = ref(0)
    const name = ref('小明')
    const isActive = ref(false)
    
    // 访问值用.value
    function increment() {
      count.value++
    }
    
    function changeName() {
      name.value = '小红'
    }
    </script>
    
    <template>
      <div>
        <p>{{ name }} 点击了 {{ count }} 次</p>
        <button @click="increment">点我</button>
        <p v-if="isActive">显示中</p>
      </div>
    </template>
    

    reactive:对象的响应式

    vue

    <script setup>
    import { reactive } from 'vue'
    
    // reactive用于对象和数组
    const user = reactive({
      name: '张三',
      age: 25,
      email: 'zhangsan@example.com'
    })
    
    function birthday() {
      user.age++
    }
    
    function updateEmail() {
      user.email = 'new_' + user.email
    }
    </script>
    
    <template>
      <div>
        <p>姓名:{{ user.name }}</p>
        <p>年龄:{{ user.age }}</p>
        <p>邮箱:{{ user.email }}</p>
        <button @click="birthday">过生日</button>
      </div>
    </template>
    

    什么时候用ref,什么时候用reactive?

    我的经验是:

    • 基础类型用ref
    • 对象、数组用reactive
    • 或者统一用ref,因为ref也可以包装对象:const user = ref({...})

    深度响应式

    默认情况下,ref和reactive都是深度响应式的:

    vue

    <script setup>
    import { ref, reactive } from 'vue'
    
    const obj = reactive({
      nested: { count: 0 },
      arr: ['foo', 'bar']
    })
    
    function mutateDeeply() {
      // 这些修改都会触发响应式更新
      obj.nested.count++
      obj.arr.push('baz')
    }
    
    const nestedRef = ref({
      value: { count: 0 }
    })
    
    function mutateNestedRef() {
      // ref包装的对象,修改时需要用.value
      nestedRef.value.count++
    }
    </script>
    

    computed计算属性

    计算属性根据其他响应式数据计算得出,有缓存特性:

    vue

    <script setup>
    import { ref, computed } from 'vue'
    
    const firstName = ref('张')
    const lastName = ref('三')
    
    // 只读计算属性
    const fullName = computed(() => {
      return `${lastName.value}${firstName.value}`
    })
    
    // 可写计算属性
    const fullNameWritable = computed({
      get: () => `${lastName.value}${firstName.value}`,
      set: (value) => {
        // "李四" -> lastName="李", firstName="四"
        if (value.length >= 2) {
          lastName.value = value[0]
          firstName.value = value.slice(1)
        }
      }
    })
    
    function updateName() {
      fullNameWritable.value = '王五'
    }
    </script>
    
    <template>
      <div>
        <p>姓名:{{ fullName }}</p>
        <button @click="updateName">改为王五</button>
      </div>
    </template>
    

    watch监听器

    当响应式数据变化时执行某些操作:

    vue

    <script setup>
    import { ref, watch } from 'vue'
    
    const question = ref('')
    const answer = ref('')
    
    // 监听单个响应式数据
    watch(question, async (newQuestion, oldQuestion) => {
      if (newQuestion.includes('?')) {
        answer.value = '思考中...'
        // 模拟异步请求
        setTimeout(() => {
          answer.value = '这是我的答案'
        }, 1000)
      }
    })
    
    // 监听多个数据
    const first = ref('')
    const second = ref('')
    
    watch([first, second], ([newFirst, newSecond], [oldFirst, oldSecond]) => {
      console.log(`从 ${oldFirst},${oldSecond} 变为 ${newFirst},${newSecond}`)
    })
    </script>
    
    <template>
      <div>
        <input v-model="question" placeholder="输入问题..." />
        <p>{{ answer }}</p>
      </div>
    </template>
    

    watchEffect是另一个选择,它会自动追踪依赖:

    vue

    <script setup>
    import { ref, watchEffect } from 'vue'
    
    const url = ref('https://api.example.com/data')
    
    watchEffect(async () => {
      // 这里的代码会自动追踪url.value的变化
      const response = await fetch(url.value)
      const data = await response.json()
      console.log('获取到数据:', data)
    })
    
    // 改变url会自动触发上面的代码重新执行
    </script>
    

    生命周期钩子

    每个组件从创建到销毁会经历多个阶段,生命周期钩子让你在各个阶段执行代码。

    vue

    <script setup>
    import { 
      onMounted, 
      onUpdated, 
      onUnmounted,
      onBeforeMount,
      onBeforeUpdate,
      onBeforeUnmount,
      onActivated,
      onDeactivated,
      onErrorCaptured
    } from 'vue'
    
    // 组件挂载前
    onBeforeMount(() => {
      console.log('组件即将挂载')
    })
    
    // 组件挂载后(DOM已创建)
    onMounted(() => {
      console.log('组件已挂载')
      // 适合:获取DOM、绑定事件、发送请求
    })
    
    // 组件更新前
    onBeforeUpdate(() => {
      console.log('组件即将更新')
    })
    
    // 组件更新后(DOM已更新)
    onUpdated(() => {
      console.log('组件已更新')
      // 适合:获取更新后的DOM
    })
    
    // 组件卸载前
    onBeforeUnmount(() => {
      console.log('组件即将卸载')
    })
    
    // 组件卸载后
    onUnmounted(() => {
      console.log('组件已卸载')
      // 适合:清理定时器、取消订阅、解绑事件
    })
    
    // 错误捕获(Vue3新增)
    onErrorCaptured((err, instance, info) => {
      console.error('捕获到错误:', err)
      console.error('错误信息:', info)
      return false // 阻止错误继续传播
    })
    </script>
    

    对比Vue2的选项和Vue3的Composition API写法:

    Vue2选项Vue3钩子函数说明
    beforeCreatesetup()替代
    createdsetup()替代
    beforeMountonBeforeMount挂载前
    mountedonMounted挂载后
    beforeUpdateonBeforeUpdate更新前
    updatedonUpdated更新后
    beforeDestroyonBeforeUnmount卸载前
    destroyedonUnmounted卸载后

    组合式函数:Composition API的精髓

    组合式函数(Composables)是Composition API最强大的特性。它让你把相关逻辑抽离成可复用的函数。

    什么是组合式函数

    简单说,组合式函数就是一个用Composition API写的有复用价值的函数:

    javascript

    // useCounter.js
    import { ref, computed } from 'vue'
    
    export function useCounter(initialValue = 0) {
      const count = ref(initialValue)
      
      const doubleCount = computed(() => count.value * 2)
      
      function increment() {
        count.value++
      }
      
      function decrement() {
        count.value--
      }
      
      function reset() {
        count.value = initialValue
      }
      
      return {
        count,
        doubleCount,
        increment,
        decrement,
        reset
      }
    }
    

    在组件中使用组合式函数

    vue

    <!-- Counter.vue -->
    <script setup>
    import { useCounter } from './useCounter'
    
    // 在组件中使用
    const { count, doubleCount, increment, decrement, reset } = useCounter(10)
    </script>
    
    <template>
      <div>
        <p>计数:{{ count }}</p>
        <p>双倍:{{ doubleCount }}</p>
        <button @click="increment">+1</button>
        <button @click="decrement">-1</button>
        <button @click="reset">重置</button>
      </div>
    </template>
    

    现在任何组件想用计数器功能,只需调用这个函数,不需要继承、不需要mixin,代码清晰多了。

    实战:封装一个请求数据的组合式函数

    javascript

    // useFetch.js
    import { ref, watchEffect, isRef } from 'vue'
    
    export function useFetch(url) {
      const data = ref(null)
      const error = ref(null)
      const loading = ref(false)
      
      async function doFetch() {
        loading.value = true
        error.value = null
        
        try {
          // 支持传入ref或普通字符串
          const urlToFetch = isRef(url) ? url.value : url
          const response = await fetch(urlToFetch)
          
          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`)
          }
          
          data.value = await response.json()
        } catch (e) {
          error.value = e
        } finally {
          loading.value = false
        }
      }
      
      // 如果url是ref,自动追踪变化并重新请求
      if (isRef(url)) {
        watchEffect(doFetch)
      } else {
        doFetch()
      }
      
      return { data, error, loading, refresh: doFetch }
    }
    

    使用这个函数:

    vue

    <script setup>
    import { ref } from 'vue'
    import { useFetch } from './useFetch'
    
    // 直接传字符串
    const { data: users, loading: usersLoading, error: usersError } = useFetch('/api/users')
    
    // 传ref,当id变化时自动重新请求
    const userId = ref('1')
    const { data: userDetail, loading, error } = useFetch(
      computed(() => `/api/users/${userId.value}`)
    )
    
    function changeUser() {
      userId.value = String(parseInt(userId.value) + 1)
    }
    </script>
    
    <template>
      <div>
        <button @click="changeUser">切换用户</button>
        
        <div v-if="loading">加载中...</div>
        <div v-else-if="error">出错了:{{ error.message }}</div>
        <div v-else>
          <p>用户详情:{{ userDetail }}</p>
        </div>
      </div>
    </template>
    

    实战:封装一个本地存储的组合式函数

    javascript

    // useStorage.js
    import { ref, watch } from 'vue'
    
    export function useStorage(key, defaultValue) {
      // 从localStorage读取初始值
      const storedValue = localStorage.getItem(key)
      const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)
      
      // 监听变化,自动保存到localStorage
      watch(data, (newValue) => {
        if (newValue === null || newValue === undefined) {
          localStorage.removeItem(key)
        } else {
          localStorage.setItem(key, JSON.stringify(newValue))
        }
      }, { deep: true })
      
      return data
    }
    

    使用:

    vue

    <script setup>
    import { useStorage } from './useStorage'
    
    // 第一个参数是localStorage的key
    // 第二个参数是默认值
    const username = useStorage('username', '')
    const theme = useStorage('theme', 'light')
    const preferences = useStorage('preferences', { fontSize: 14, language: 'zh' })
    
    function toggleTheme() {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
    }
    </script>
    
    <template>
      <div>
        <input v-model="username" placeholder="输入用户名" />
        <p>当前主题:{{ theme }}</p>
        <button @click="toggleTheme">切换主题</button>
        <p>首选项:{{ preferences }}</p>
      </div>
    </template>
    

    依赖注入:跨层级传递数据

    props层层传递很麻烦?依赖注入帮你解决。

    provide和inject

    父组件提供数据,后代组件直接获取:

    vue

    <!-- Parent.vue -->
    <script setup>
    import { provide, ref } from 'vue'
    
    const theme = ref('light')
    const user = ref({ name: '张三', role: 'admin' })
    
    // 提供给所有后代组件
    provide('theme', theme)
    provide('user', user)
    
    // 也可以提供函数
    provide('toggleTheme', () => {
      theme.value = theme.value === 'light' ? 'dark' : 'light'
    })
    </script>
    
    <template>
      <div :class="theme">
        <ChildComponent />
      </div>
    </template>
    

    vue

    <!-- GrandChild.vue(不需要父传子的props) -->
    <script setup>
    import { inject } from 'vue'
    
    // 注入数据
    const theme = inject('theme')
    const user = inject('user')
    const toggleTheme = inject('toggleTheme')
    </script>
    
    <template>
      <div>
        <p>当前主题:{{ theme }}</p>
        <p>用户:{{ user.name }} ({{ user.role }})</p>
        <button @click="toggleTheme">切换主题</button>
      </div>
    </template>
    

    带默认值的注入

    vue

    <script setup>
    import { inject } from 'vue'
    
    // 第二个参数是默认值
    const config = inject('config', { apiUrl: '/api', timeout: 5000 })
    
    // 或者用函数提供默认值(适合引用类型)
    const theme = inject('theme', () => ref('light'), true)
    </script>
    

    模板引用:操作DOM

    虽然Vue推崇数据驱动DOM,但有些时候还是需要直接操作DOM元素。

    ref绑定元素

    vue

    <script setup>
    import { ref, onMounted } from 'vue'
    
    // 创建ref
    const inputRef = ref(null)
    const canvasRef = ref(null)
    
    onMounted(() => {
      // 访问DOM元素
      inputRef.value.focus()
      
      // 操作Canvas
      const ctx = canvasRef.value.getContext('2d')
      ctx.fillStyle = 'green'
      ctx.fillRect(0, 0, 100, 100)
    })
    </script>
    
    <template>
      <div>
        <!-- 绑定ref -->
        <input ref="inputRef" type="text" />
        <canvas ref="canvasRef" width="200" height="200"></canvas>
      </div>
    </template>
    

    ref绑定组件实例

    vue

    <!-- ChildComponent.vue -->
    <script setup>
    import { ref } from 'vue'
    
    const count = ref(0)
    
    function increment() {
      count.value++
    }
    
    function getCount() {
      return count.value
    }
    
    // 暴露方法给父组件
    defineExpose({
      count,
      increment,
      getCount
    })
    </script>
    

    vue

    <!-- ParentComponent.vue -->
    <script setup>
    import { ref } from 'vue'
    import ChildComponent from './ChildComponent.vue'
    
    const childRef = ref(null)
    
    function handleClick() {
      console.log('子组件count:', childRef.value.count)
      childRef.value.increment()
      console.log('调用后count:', childRef.value.getCount())
    }
    </script>
    
    <template>
      <div>
        <ChildComponent ref="childRef" />
        <button @click="handleClick">操作子组件</button>
      </div>
    </template>
    

    实战:Todo应用

    学完上面的知识,来写个完整的Todo应用练练手。

    项目结构

    plaintext

    src/
    ├── components/
    │   ├── TodoList.vue
    │   ├── TodoItem.vue
    │   ├── TodoInput.vue
    │   └── TodoFilters.vue
    ├── composables/
    │   └── useTodos.js
    ├── App.vue
    └── main.js
    

    useTodos.js:封装Todo逻辑

    javascript

    // composables/useTodos.js
    import { ref, computed, watch } from 'vue'
    
    export function useTodos() {
      // 状态
      const todos = ref([])
      const filter = ref('all') // all, active, completed
      
      // 本地存储
      const STORAGE_KEY = 'vue3-todos'
      
      // 初始化时从localStorage读取
      const storedTodos = localStorage.getItem(STORAGE_KEY)
      if (storedTodos) {
        try {
          todos.value = JSON.parse(storedTodos)
        } catch (e) {
          console.error('读取本地存储失败:', e)
        }
      }
      
      // 保存到localStorage
      watch(todos, (newTodos) => {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(newTodos))
      }, { deep: true })
      
      // 计算属性
      const filteredTodos = computed(() => {
        if (filter.value === 'active') {
          return todos.value.filter(t => !t.completed)
        } else if (filter.value === 'completed') {
          return todos.value.filter(t => t.completed)
        }
        return todos.value
      })
      
      const activeCount = computed(() => 
        todos.value.filter(t => !t.completed).length
      )
      
      const completedCount = computed(() => 
        todos.value.filter(t => t.completed).length
      )
      
      // 方法
      function addTodo(title) {
        if (!title.trim()) return
        
        todos.value.push({
          id: Date.now(),
          title: title.trim(),
          completed: false,
          createdAt: new Date().toISOString()
        })
      }
      
      function toggleTodo(id) {
        const todo = todos.value.find(t => t.id === id)
        if (todo) {
          todo.completed = !todo.completed
        }
      }
      
      function deleteTodo(id) {
        const index = todos.value.findIndex(t => t.id === id)
        if (index !== -1) {
          todos.value.splice(index, 1)
        }
      }
      
      function editTodo(id, newTitle) {
        const todo = todos.value.find(t => t.id === id)
        if (todo && newTitle.trim()) {
          todo.title = newTitle.trim()
        }
      }
      
      function clearCompleted() {
        todos.value = todos.value.filter(t => !t.completed)
      }
      
      function setFilter(newFilter) {
        filter.value = newFilter
      }
      
      return {
        todos,
        filter,
        filteredTodos,
        activeCount,
        completedCount,
        addTodo,
        toggleTodo,
        deleteTodo,
        editTodo,
        clearCompleted,
        setFilter
      }
    }
    

    TodoInput.vue:输入组件

    vue

    <script setup>
    import { ref } from 'vue'
    
    const emit = defineEmits(['add'])
    
    const inputValue = ref('')
    
    function handleSubmit() {
      if (inputValue.value.trim()) {
        emit('add', inputValue.value)
        inputValue.value = ''
      }
    }
    </script>
    
    <template>
      <form @submit.prevent="handleSubmit" class="todo-input">
        <input 
          v-model="inputValue"
          type="text"
          placeholder="输入新任务..."
          class="input-field"
        />
        <button type="submit" class="add-btn">添加</button>
      </form>
    </template>
    
    <style scoped>
    .todo-input {
      display: flex;
      gap: 8px;
      margin-bottom: 16px;
    }
    
    .input-field {
      flex: 1;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 6px;
      font-size: 16px;
    }
    
    .input-field:focus {
      outline: none;
      border-color: #42b883;
    }
    
    .add-btn {
      padding: 12px 24px;
      background: #42b883;
      color: white;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      font-size: 16px;
    }
    
    .add-btn:hover {
      background: #3aa876;
    }
    </style>
    

    TodoItem.vue:单个任务组件

    vue

    <script setup>
    import { ref } from 'vue'
    
    const props = defineProps({
      todo: {
        type: Object,
        required: true
      }
    })
    
    const emit = defineEmits(['toggle', 'delete', 'edit'])
    
    const isEditing = ref(false)
    const editValue = ref('')
    
    function startEdit() {
      editValue.value = props.todo.title
      isEditing.value = true
    }
    
    function saveEdit() {
      if (editValue.value.trim()) {
        emit('edit', props.todo.id, editValue.value)
      }
      isEditing.value = false
    }
    
    function cancelEdit() {
      isEditing.value = false
    }
    </script>
    
    <template>
      <li class="todo-item" :class="{ completed: todo.completed }">
        <div v-if="isEditing" class="edit-mode">
          <input 
            v-model="editValue"
            @keyup.enter="saveEdit"
            @keyup.escape="cancelEdit"
            class="edit-input"
          />
          <button @click="saveEdit" class="save-btn">保存</button>
          <button @click="cancelEdit" class="cancel-btn">取消</button>
        </div>
        <div v-else class="view-mode">
          <input 
            type="checkbox"
            :checked="todo.completed"
            @change="$emit('toggle', todo.id)"
            class="checkbox"
          />
          <span class="title">{{ todo.title }}</span>
          <button @click="startEdit" class="edit-btn">编辑</button>
          <button @click="$emit('delete', todo.id)" class="delete-btn">删除</button>
        </div>
      </li>
    </template>
    
    <style scoped>
    .todo-item {
      display: flex;
      align-items: center;
      padding: 12px;
      background: #f9f9f9;
      border-radius: 6px;
      margin-bottom: 8px;
    }
    
    .todo-item.completed .title {
      text-decoration: line-through;
      color: #999;
    }
    
    .checkbox {
      width: 20px;
      height: 20px;
      margin-right: 12px;
      cursor: pointer;
    }
    
    .title {
      flex: 1;
      font-size: 16px;
    }
    
    button {
      padding: 6px 12px;
      margin-left: 8px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .edit-btn {
      background: #e0e0e0;
      color: #333;
    }
    
    .delete-btn {
      background: #ff4444;
      color: white;
    }
    
    .edit-mode {
      display: flex;
      width: 100%;
    }
    
    .edit-input {
      flex: 1;
      padding: 8px;
      border: 1px solid #42b883;
      border-radius: 4px;
      font-size: 16px;
    }
    
    .save-btn {
      background: #42b883;
      color: white;
      margin-left: 8px;
    }
    
    .cancel-btn {
      background: #e0e0e0;
      color: #333;
    }
    </style>
    

    TodoFilters.vue:筛选组件

    vue

    <script setup>
    defineProps({
      filter: {
        type: String,
        required: true
      },
      activeCount: {
        type: Number,
        required: true
      },
      completedCount: {
        type: Number,
        required: true
      }
    })
    
    const emit = defineEmits(['setFilter', 'clearCompleted'])
    
    const filters = [
      { key: 'all', label: '全部' },
      { key: 'active', label: '进行中' },
      { key: 'completed', label: '已完成' }
    ]
    </script>
    
    <template>
      <div class="filters">
        <div class="filter-buttons">
          <button
            v-for="f in filters"
            :key="f.key"
            @click="emit('setFilter', f.key)"
            :class="{ active: filter === f.key }"
            class="filter-btn"
          >
            {{ f.label }}
          </button>
        </div>
        
        <div class="stats">
          <span>进行中:{{ activeCount }}</span>
          <span>已完成:{{ completedCount }}</span>
          <button 
            v-if="completedCount > 0"
            @click="emit('clearCompleted')"
            class="clear-btn"
          >
            清除已完成
          </button>
        </div>
      </div>
    </template>
    
    <style scoped>
    .filters {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 12px 0;
      border-top: 1px solid #eee;
    }
    
    .filter-buttons {
      display: flex;
      gap: 8px;
    }
    
    .filter-btn {
      padding: 6px 12px;
      border: 1px solid #ddd;
      background: white;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .filter-btn.active {
      background: #42b883;
      color: white;
      border-color: #42b883;
    }
    
    .stats {
      display: flex;
      gap: 16px;
      align-items: center;
      font-size: 14px;
      color: #666;
    }
    
    .clear-btn {
      padding: 4px 8px;
      background: #ff4444;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 12px;
    }
    </style>
    

    TodoList.vue:整合所有组件

    vue

    <script setup>
    import { useTodos } from '../composables/useTodos'
    import TodoInput from './TodoInput.vue'
    import TodoItem from './TodoItem.vue'
    import TodoFilters from './TodoFilters.vue'
    
    const {
      filter,
      filteredTodos,
      activeCount,
      completedCount,
      addTodo,
      toggleTodo,
      deleteTodo,
      editTodo,
      clearCompleted,
      setFilter
    } = useTodos()
    </script>
    
    <template>
      <div class="todo-list">
        <h1 class="title">Vue3 Todo应用</h1>
        
        <TodoInput @add="addTodo" />
        
        <ul class="todos">
          <TodoItem
            v-for="todo in filteredTodos"
            :key="todo.id"
            :todo="todo"
            @toggle="toggleTodo"
            @delete="deleteTodo"
            @edit="editTodo"
          />
        </ul>
        
        <div v-if="filteredTodos.length === 0" class="empty">
          暂无任务
        </div>
        
        <TodoFilters
          :filter="filter"
          :active-count="activeCount"
          :completed-count="completedCount"
          @set-filter="setFilter"
          @clear-completed="clearCompleted"
        />
      </div>
    </template>
    
    <style scoped>
    .todo-list {
      max-width: 600px;
      margin: 40px auto;
      padding: 24px;
      background: white;
      border-radius: 12px;
      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
    }
    
    .title {
      text-align: center;
      color: #42b883;
      margin-bottom: 24px;
    }
    
    .todos {
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    .empty {
      text-align: center;
      padding: 40px;
      color: #999;
    }
    </style>
    

    App.vue:主组件

    vue

    <script setup>
    import TodoList from './components/TodoList.vue'
    </script>
    
    <template>
      <div class="app">
        <TodoList />
      </div>
    </template>
    
    <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    body {
      background: #f5f5f5;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }
    
    .app {
      min-height: 100vh;
      padding: 20px;
    }
    </style>
    

    运行效果:

    bash

    npm run dev
    

    打开浏览器访问http://localhost:5173,一个完整的Todo应用就完成了。

    总结

    好了,这篇Vue3 Composition API教程就到这里。让我回顾一下今天学的内容:

    1. 环境搭建:Vite创建Vue3项目,基本组件结构
    2. 响应式系统:ref和reactive的区别,深度响应式
    3. 计算属性和监听器:computed、watch、watchEffect
    4. 生命周期钩子:从创建到销毁的各个阶段
    5. 组合式函数:封装可复用的逻辑
    6. 依赖注入:provide和inject跨层级传递数据
    7. 模板引用:ref绑定DOM和组件
    8. 实战项目:完整的Todo应用

    说实话,Composition API确实比Options API更灵活、更强大。它让你能够:

    • 把相关逻辑放在一起,不用在data、methods、computed之间跳来跳去
    • 抽离出可复用的组合式函数,比mixin清晰多了
    • 更好地组织TypeScript代码(虽然本教程没用TypeScript,但Vue3对TS的支持很好)

    如果你之前用Vue2,可以渐进式迁移。新项目建议直接上Composition API,老项目可以逐步把新组件改成新写法。

    关于内链方面,你可以继续学习TypeScript入门完全指南,TypeScript+Vue3是绝配。或者学习RESTful API接口设计规范,了解如何设计后端API配合Vue3前端使用。

    常见问题

    Q:Composition API和Options API哪个好?

    A:Composition API更好。它让代码逻辑更集中,更容易复用,也更适合TypeScript。新项目建议用Composition API。

    Q:需要完全放弃Vue2吗?

    A:不需要。Vue3完全兼容Options API语法,老项目可以慢慢迁移。可以先在新组件使用Composition API,老组件保持Options API写法。

    Q:什么时候用ref,什么时候用reactive?

    A:建议统一用ref。reactive有一些坑(比如解构后失去响应式),ref没有这个问题,而且语法更一致。

    Q:组合式函数为什么要用”use”开头?

    A:这是一个约定俗成的命名方式,类似于React的”hooks”命名。它让代码更易读,一看就知道这是个组合式函数,可以自动追踪依赖。

    Q:Vue3适合做大型项目吗?

    A:非常适合。Composition API让代码更容易组织和复用,TypeScript支持也很好。我见过不少几百个组件的大型Vue3项目,代码依然保持很好的可维护性。

    如果你有任何问题或建议,欢迎在评论区交流!

  • TypeScript入门完全指南:从JavaScript到类型安全

    TypeScript入门完全指南:从JavaScript到类型安全

    引言

    说实话,我第一次听说TypeScript的时候是拒绝的。”JavaScript不是挺好的吗?为什么还要学一个新的语言?”这是我当时的真实想法。

    但当我写过一个万行级别的JavaScript项目,被那些奇怪的类型隐式转换折磨得夜不能寐之后,我终于决定给TypeScript一个机会。结果嘛——真香。

    这篇文章就是想把我从”TypeScript是什么”到”能写简单项目”的过程完整记录下来,希望你少走一些弯路。

    TypeScript类型系统配图 - 基础类型与接口实战示例

    环境搭建:5分钟启动TypeScript

    安装Node.js

    TypeScript运行在Node.js环境中,所以第一步是安装Node.js。

    Windows/macOS用户:

    直接去Node.js官网下载LTS版本,安装过程没什么坑,一路下一步就行。装完之后打开终端验证:

    bash

    node --version
    npm --version
    

    Linux用户:

    bash

    # Ubuntu/Debian
    sudo apt update
    sudo apt install nodejs npm
    
    # 验证安装
    node --version
    npm --version
    

    安装TypeScript编译器

    有了npm之后,安装TypeScript就是一行命令的事:

    bash

    npm install -g typescript
    

    验证安装:

    bash

    tsc --version
    # 应该输出类似:Version 5.4.3
    

    第一个TypeScript程序

    创建一个文件夹,比如叫ts-hello,然后在里面新建一个文件hello.ts

    typescript

    // hello.ts
    function greet(name: string): string {
        return `你好,${name}!欢迎学习TypeScript。`;
    }
    
    const message = greet("小明");
    console.log(message);
    

    注意这个.ts后缀,这就是TypeScript文件的标志。

    然后在终端里编译它:

    bash

    tsc hello.ts
    

    执行完你会发现多了一个hello.js文件。没错,TypeScript代码需要编译成JavaScript才能运行。运行编译结果:

    bash

    node hello.js
    # 输出:你好,小明!欢迎学习TypeScript。
    

    使用配置文件

    项目里文件多了,每次手动tsc 文件名就太麻烦了。这时候需要一个tsconfig.json配置文件。

    bash

    tsc --init
    

    这会在当前目录生成一个默认配置文件。打开它,你会看到很多选项,这里先关注几个常用的:

    json

    {
        "compilerOptions": {
            "target": "ES2020",          // 编译目标JavaScript版本
            "module": "commonjs",         // 模块系统
            "outDir": "./dist",          // 编译输出目录
            "rootDir": "./src",          // 源码目录
            "strict": true,              // 开启严格模式(建议!)
            "esModuleInterop": true,      // 允许默认导入
            "skipLibCheck": true          // 跳过库文件检查
        },
        "include": ["src/**/*"],         // 要编译的文件
        "exclude": ["node_modules"]     // 排除的文件
    }
    

    配置好之后,把你的.ts文件放到src目录,执行tsc就会自动编译所有文件到dist目录。

    基础类型:TypeScript的核心

    这是TypeScript最重要的地方。类型系统让代码在写的时候就能发现错误,而不是等到运行时才崩溃。

    基础类型有哪些

    JavaScript的那些基础类型TypeScript都支持,只是在写法上有点区别:

    typescript

    // 字符串
    let name: string = "张三";
    let greeting: string = `你好,${name}`;
    
    // 数字
    let age: number = 25;
    let price: number = 99.9;
    
    // 布尔值
    let isStudent: boolean = true;
    
    // 数组
    let numbers: number[] = [1, 2, 3, 4, 5];
    let names: Array<string> = ["小明", "小红", "小刚"];  // 另一种写法
    
    // 元组:固定长度和类型的数组
    let person: [string, number] = ["张三", 25];
    
    // 枚举:一组有名字的常量
    enum Status {
        Pending,    // 0
        Active,     // 1
        Completed   // 2
    }
    let currentStatus: Status = Status.Active;
    
    // 任意类型:当你真的不确定是什么类型时
    let anything: any = 4;
    anything = "hello";
    anything = true;
    
    // void:表示没有返回值
    function logMessage(): void {
        console.log("这是一条日志");
    }
    
    // null 和 undefined
    let n: null = null;
    let u: undefined = undefined;
    

    类型注解和类型推断

    TypeScript有两种方式确定类型:

    类型注解:你手动告诉编译器这是什么类型

    typescript

    let count: number = 10;
    let username: string = "admin";
    

    类型推断:TypeScript根据上下文自动推断类型

    typescript

    let count = 10;        // TypeScript推断这是number类型
    let username = "admin"; // TypeScript推断这是string类型
    
    // 后续再赋值其他类型会报错
    count = "hello";  // 错误!count只能是number
    

    实际开发中,没必要每个变量都写类型注解,TypeScript够聪明的时候会自己推断。但函数参数、返回值、复杂对象这些地方,类型注解是必要的。

    接口:定义对象的形状

    接口是TypeScript最强大的特性之一,它让你定义一个对象的结构:

    typescript

    // 定义一个用户接口
    interface User {
        id: number;
        username: string;
        email: string;
        age?: number;           // 可选属性
        readonly createdAt: Date;  // 只读属性
    }
    
    // 创建符合接口的用户对象
    const user: User = {
        id: 1,
        username: "zhangsan",
        email: "zhangsan@example.com",
        age: 25,
        createdAt: new Date()
    };
    
    // 缺少必填属性会报错
    const badUser: User = {
        id: 2,
        username: "lisi"
        // 错误!缺少email和createdAt
    };
    
    // 尝试修改只读属性会报错
    user.createdAt = new Date();  // 错误!createdAt是只读的
    

    接口还能定义方法:

    typescript

    interface Calculator {
        (a: number, b: number): number;  // 函数的形状
        description: string;
    }
    
    const add: Calculator = (a, b) => a + b;
    add.description = "加法计算器";
    

    联合类型和交叉类型

    有时候一个变量可能有多种类型,这时候用联合类型:

    typescript

    // 联合类型:可以是字符串或数字
    let id: string | number;
    id = "123";    // OK
    id = 123;      // OK
    id = true;     // 错误!布尔值不行
    
    // 用联合类型限制函数参数
    function printId(id: string | number): void {
        if (typeof id === "string") {
            console.log(`字符串ID: ${id.toUpperCase()}`);
        } else {
            console.log(`数字ID: ${id.toFixed(2)}`);
        }
    }
    

    交叉类型:把多个类型合并成一个:

    typescript

    interface Person {
        name: string;
        age: number;
    }
    
    interface Employee {
        company: string;
        salary: number;
    }
    
    // 交叉类型:既是Person又是Employee
    type Worker = Person & Employee;
    
    const worker: Worker = {
        name: "张三",
        age: 30,
        company: "某科技公司",
        salary: 15000
    };
    

    函数:参数和返回值的类型

    TypeScript对函数的类型检查非常严格,这是好事。

    函数类型表达式

    typescript

    // 声明一个函数类型
    type AddFunction = (a: number, b: number) => number;
    
    // 实现这个函数
    const add: AddFunction = (x, y) => x + y;
    
    // 箭头函数写法
    const multiply: AddFunction = (x, y) => x * y;
    

    可选参数和默认参数

    typescript

    // 可选参数:参数名后面加?
    function buildName(firstName: string, lastName?: string): string {
        if (lastName) {
            return `${firstName} ${lastName}`;
        }
        return firstName;
    }
    
    console.log(buildName("张"));        // "张"
    console.log(buildName("张", "三"));   // "张三"
    
    // 默认参数:直接在参数列表赋值
    function greet(name: string, greeting: string = "你好"): string {
        return `${greeting},${name}!`;
    }
    
    console.log(greet("小明"));          // "你好,小明!"
    console.log(greet("小明", "早上好")); // "早上好,小明!"
    

    剩余参数

    typescript

    // 收集剩余参数为数组
    function sum(...numbers: number[]): number {
        return numbers.reduce((total, num) => total + num, 0);
    }
    
    console.log(sum(1, 2, 3));        // 6
    console.log(sum(1, 2, 3, 4, 5)); // 15
    

    函数重载

    同一个函数名,根据不同的参数类型返回不同类型:

    typescript

    // 函数重载签名
    function reverse(x: string): string;  // 输入字符串返回字符串
    function reverse(x: number): number;   // 输入数字返回数字
    function reverse(x: string | number): string | number {
        if (typeof x === "string") {
            return x.split("").reverse().join("");
        }
        return Number(x.toString().split("").reverse().join(""));
    }
    
    console.log(reverse("hello"));  // "olleh"
    console.log(reverse(12345));    // 54321
    

    泛型:让类型灵活起来

    泛型是TypeScript最难但最有用的部分。简单说,泛型就是”类型的变量”。

    为什么需要泛型

    先看一个没有泛型的例子:

    typescript

    // 这个函数返回数组的第一个元素
    function getFirst(arr: any[]): any {
        return arr[0];
    }
    
    const num = getFirst([1, 2, 3]);    // 返回any类型
    const str = getFirst(["a", "b"]);   // 返回any类型
    
    // 调用时丢失了具体类型信息
    num.toFixed(2);  // 编译器不知道num是number,可能报错
    

    用泛型重写:

    typescript

    // T是类型参数,在调用时自动推断
    function getFirst<T>(arr: T[]): T | undefined {
        return arr[0];
    }
    
    const num = getFirst([1, 2, 3]);        // 推断T为number
    const str = getFirst(["a", "b"]);       // 推断T为string
    
    num.toFixed(2);   // OK!TypeScript知道num是number
    str.toUpperCase(); // OK!TypeScript知道str是string
    

    泛型约束

    有时候需要对泛型类型进行限制:

    typescript

    // 约束T必须有length属性
    interface HasLength {
        length: number;
    }
    
    function logLength<T extends HasLength>(arg: T): void {
        console.log(`长度是:${arg.length}`);
    }
    
    logLength("hello");      // OK,字符串有length
    logLength([1, 2, 3]);    // OK,数组有length
    logLength({ length: 10 }); // OK,对象有length
    logLength(123);          // 错误!数字没有length
    

    多类型参数

    typescript

    // 交换两个变量的值
    function swap<T, U>(tuple: [T, U]): [U, T] {
        return [tuple[1], tuple[0]];
    }
    
    const result = swap([1, "hello"]);
    console.log(result);  // ["hello", 1]
    

    实用技巧:工程实践

    类型守卫

    类型守卫让你在条件判断中缩小变量类型范围:

    typescript

    interface Cat {
        meow(): void;
    }
    
    interface Dog {
        bark(): void;
    }
    
    function isCat(animal: Cat | Dog): animal is Cat {
        return (animal as Cat).meow !== undefined;
    }
    
    function speak(animal: Cat | Dog) {
        if (isCat(animal)) {
            animal.meow();  // TypeScript知道这是Cat
        } else {
            animal.bark();  // TypeScript知道这是Dog
        }
    }
    

    keyof和索引类型

    typescript

    interface User {
        id: number;
        name: string;
        email: string;
    }
    
    // keyof User 等于 "id" | "name" | "email"
    function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
        return obj[key];
    }
    
    const user: User = {
        id: 1,
        name: "张三",
        email: "zhangsan@example.com"
    };
    
    const userName = getProperty(user, "name");    // string
    const userId = getProperty(user, "id");        // number
    const userAge = getProperty(user, "age");      // 错误!"age"不在User中
    

    工具类型

    TypeScript内置了很多有用的工具类型:

    typescript

    interface User {
        id: number;
        name: string;
        email: string;
    }
    
    // Partial:所有属性变为可选
    type PartialUser = Partial<User>;
    // 等于 { id?: number; name?: string; email?: string; }
    
    // Required:所有属性变为必填
    type RequiredUser = Required<User>;
    
    // Pick:挑选部分属性
    type UserPreview = Pick<User, "id" | "name">;
    // 等于 { id: number; name: string; }
    
    // Omit:排除部分属性
    type UserWithoutEmail = Omit<User, "email">;
    // 等于 { id: number; name: string; }
    
    // Record:创建指定键值对类型
    type UserMap = Record<string, User>;
    const users: UserMap = {
        "zhangsan": { id: 1, name: "张三", email: "zhangsan@example.com" },
        "lisi": { id: 2, name: "李四", email: "lisi@example.com" }
    };
    
    // Exclude:排除联合类型中的某些类型
    type Status = "pending" | "active" | "completed" | "failed";
    type ActiveStatus = Exclude<Status, "pending" | "failed">;
    // 等于 "active" | "completed"
    

    实战:一个简单的任务列表

    学完上面的知识,来动手写个小项目吧。

    项目结构

    plaintext

    task-app/
    ├── src/
    │   ├── types.ts      # 类型定义
    │   ├── task.ts        # 任务类
    │   ├── taskList.ts    # 任务列表
    │   └── index.ts       # 入口文件
    ├── dist/              # 编译输出
    ├── tsconfig.json
    └── package.json
    

    types.ts:类型定义

    typescript

    // 任务状态枚举
    export enum TaskStatus {
        Todo = "todo",
        InProgress = "in_progress",
        Done = "done"
    }
    
    // 任务接口
    export interface Task {
        id: string;
        title: string;
        description?: string;
        status: TaskStatus;
        createdAt: Date;
        completedAt?: Date;
    }
    
    // 创建任务的输入类型
    export interface CreateTaskInput {
        title: string;
        description?: string;
    }
    
    // 更新任务的输入类型
    export interface UpdateTaskInput {
        title?: string;
        description?: string;
        status?: TaskStatus;
    }
    

    task.ts:任务类

    typescript

    import { Task, TaskStatus, CreateTaskInput, UpdateTaskInput } from "./types";
    
    export class TaskItem implements Task {
        id: string;
        title: string;
        description?: string;
        status: TaskStatus;
        createdAt: Date;
        completedAt?: Date;
    
        constructor(input: CreateTaskInput) {
            this.id = this.generateId();
            this.title = input.title;
            this.description = input.description;
            this.status = TaskStatus.Todo;
            this.createdAt = new Date();
        }
    
        private generateId(): string {
            return `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        }
    
        update(input: UpdateTaskInput): void {
            if (input.title !== undefined) {
                this.title = input.title;
            }
            if (input.description !== undefined) {
                this.description = input.description;
            }
            if (input.status !== undefined) {
                this.status = input.status;
                if (input.status === TaskStatus.Done) {
                    this.completedAt = new Date();
                } else {
                    this.completedAt = undefined;
                }
            }
        }
    
        toString(): string {
            const statusText = {
                [TaskStatus.Todo]: "待办",
                [TaskStatus.InProgress]: "进行中",
                [TaskStatus.Done]: "已完成"
            };
            return `[${statusText[this.status]}] ${this.title}`;
        }
    }
    

    taskList.ts:任务列表管理

    typescript

    import { Task, TaskStatus, CreateTaskInput, UpdateTaskInput } from "./types";
    import { TaskItem } from "./task";
    
    export class TaskList {
        private tasks: Map<string, TaskItem> = new Map();
    
        create(input: CreateTaskInput): Task {
            const task = new TaskItem(input);
            this.tasks.set(task.id, task);
            return task;
        }
    
        findById(id: string): Task | undefined {
            return this.tasks.get(id);
        }
    
        findAll(): Task[] {
            return Array.from(this.tasks.values());
        }
    
        findByStatus(status: TaskStatus): Task[] {
            return this.findAll().filter(task => task.status === status);
        }
    
        update(id: string, input: UpdateTaskInput): Task | undefined {
            const task = this.tasks.get(id);
            if (!task) {
                return undefined;
            }
            task.update(input);
            return task;
        }
    
        delete(id: string): boolean {
            return this.tasks.delete(id);
        }
    
        clear(): void {
            this.tasks.clear();
        }
    
        getStats(): { total: number; todo: number; inProgress: number; done: number } {
            const all = this.findAll();
            return {
                total: all.length,
                todo: all.filter(t => t.status === TaskStatus.Todo).length,
                inProgress: all.filter(t => t.status === TaskStatus.InProgress).length,
                done: all.filter(t => t.status === TaskStatus.Done).length
            };
        }
    }
    

    index.ts:入口文件

    typescript

    import { TaskStatus } from "./types";
    import { TaskList } from "./taskList";
    
    // 创建任务列表实例
    const taskList = new TaskList();
    
    // 添加一些任务
    const task1 = taskList.create({
        title: "学习TypeScript",
        description: "完成TypeScript入门教程"
    });
    
    const task2 = taskList.create({
        title: "搭建项目框架",
        description: "使用React + TypeScript"
    });
    
    const task3 = taskList.create({
        title: "写单元测试"
    });
    
    // 更新任务状态
    taskList.update(task1.id, { status: TaskStatus.InProgress });
    
    // 打印所有任务
    console.log("=== 所有任务 ===");
    taskList.findAll().forEach(task => {
        const taskItem = taskList.findById(task.id)!;
        console.log(taskItem.toString());
    });
    
    // 打印统计
    console.log("\n=== 任务统计 ===");
    const stats = taskList.getStats();
    console.log(`总计:${stats.total}`);
    console.log(`待办:${stats.todo}`);
    console.log(`进行中:${stats.inProgress}`);
    console.log(`已完成:${stats.done}`);
    
    // 删除一个任务
    taskList.delete(task3.id);
    console.log("\n删除任务后:", taskList.findAll().length, "个任务");
    

    编译运行:

    bash

    tsc
    node dist/index.js
    

    输出:

    plaintext

    === 所有任务 ===
    [进行中] 学习TypeScript
    [待办] 搭建项目框架
    [待办] 写单元测试
    
    === 任务统计 ===
    总计:3
    待办:2
    进行中:1
    已完成:0
    
    删除任务后: 2 个任务
    

    总结

    好,写到这里你应该对TypeScript有了比较完整的认识。让我简单总结一下今天学的内容:

    1. 环境搭建:Node.js + TypeScript编译器,5分钟搞定
    2. 基础类型:string、number、boolean、数组、枚举、接口
    3. 函数类型:参数类型、返回值类型、可选参数、默认参数、泛型
    4. 泛型:让类型像变量一样灵活使用
    5. 实用技巧:类型守卫、keyof、工具类型
    6. 实战项目:一个完整的任务列表应用

    坦白说,TypeScript的入门门槛确实比JavaScript高一些,但一旦你习惯了写类型注解,你会发现代码质量提升了一大截——Bug少了,代码更好维护了,IDE的智能提示也更准确了。

    如果你之前一直在写JavaScript,强烈建议你新项目尝试用TypeScript,或者把旧项目一点点迁移过去。不需要一步到位,可以先从简单的地方开始加类型注解,慢慢来。

    关于内链方面,你可以继续学习Vue3 Composition API实战教程或者RESTful API接口设计规范,这些都是前端开发者的必备技能。

    常见问题

    Q:TypeScript和JavaScript有什么区别?

    A:TypeScript是JavaScript的超集,添加了类型系统和面向对象的特性。TypeScript代码需要编译成JavaScript才能在浏览器或Node.js中运行。简单说,TypeScript = JavaScript + 类型系统 + 编译。

    Q:TypeScript难学吗?

    A:对于有JavaScript基础的开发者来说,上手TypeScript并不难。最基本的类型注解几乎不需要额外学习。最难的部分是泛型和高级类型,但这些可以在实际项目中慢慢深入。

    Q:所有项目都需要用TypeScript吗?

    A:不一定。小型项目、原型项目用JavaScript更灵活。但中大型项目、团队协作项目、需要长期维护的项目,用TypeScript能显著提升代码质量和可维护性。

    Q:如何从JavaScript迁移到TypeScript?

    A:可以把tsconfig.json中的strict设置为false,然后逐个文件把.js改成.ts。TypeScript会兼容大部分JavaScript语法,然后慢慢加上类型注解。

    希望这篇教程对你有帮助。如果有问题或建议,欢迎在评论区交流!

  • RAG检索增强生成实战教程:让大模型拥有企业知识库

    RAG检索增强生成实战教程:让大模型拥有企业知识库

    正文

    一、为什么RAG成为2026年最火AI技能

    去年这个时候,如果你问一家企业怎么用大模型,答案多半是”直接调API”。但今年,画风变了。

    我接触的十几家企业里,有八成以上都在搞私有知识库。他们发现一个问题:直接用通用大模型回答客户咨询,准确率只有60%出头,但加上RAG技术后,准确率能拉到90%以上。这背后的逻辑很简单——大模型再强,也不可能知道你公司内部的产品文档、客服话术、FAQ库。RAG就是解决这个问题的。

    RAG技术架构流程图,文档切分向量化检索生成全流程

    根据2026年最新招聘数据,掌握RAG技术的开发者薪资普遍比同资历传统开发岗高出30%到50%。有些猎头甚至直接说:”现在招AI应用开发,RAG是必问项。”

    所以,不管你是做后端开发、数据分析,还是纯AI方向,学RAG都是稳赚不赔的选择。

    二、RAG到底是怎么工作的

    在说具体实现之前,先聊清楚RAG的工作原理。很多教程一上来就贴代码,看完还是懵的。咱们把RAG拆成三个环节来讲。

    第一环节:知识库准备

    这个环节做的事情是把原始文档切分成小块,然后转成向量存入向量数据库。为什么切成小块?因为大模型每次能处理的上下文有限,把长文档切成若干小块,每次只检索最相关的几块喂给模型,效率和准确率都会更高。

    第二环节:用户问题检索

    用户提问后,系统先把这个问题转成向量,然后在向量数据库里找语义最接近的知识块。这里有个关键点:不是关键词匹配,是语义匹配。比如问”怎么重置密码”,系统能匹配到”找回账户访问权限”相关内容,靠的就是向量相似度。

    第三环节:生成回答

    把检索到的知识块和用户问题一起拼成提示词,扔给大模型生成回答。这样做的好处是回答有据可查,不是大模型凭空编的。

    整个流程大概就是这样。核心价值一句话概括:让大模型在回答问题前先”查资料”,然后基于资料组织答案。

    三、环境准备与依赖安装

    先说下我的测试环境:Python 3.10,16G内存。实际生产环境建议32G以上。

    先创建个项目目录,然后安装依赖:

    bash

    mkdir rag-project && cd rag-project
    python -m venv venv
    source venv/bin/activate  # Windows用 venv\Scripts\activate
    
    pip install langchain langchain-community langchain-chroma \
        chromadb openai tiktoken pypdf python-docx \
        streamlit python-dotenv
    

    如果网络慢,可以用国内镜像:

    bash

    pip install -i https://mirrors.aliyun.com/pypi/simple/ langchain langchain-community
    

    接下来需要准备OpenAI的API Key。如果用国内模型,可以替换成智谱GLM或者百度千帆的接口,这里先以OpenAI为例演示。

    创建.env文件:

    bash

    OPENAI_API_KEY=sk-your-api-key-here
    

    四、知识库文档处理全流程

    4.1 文档加载与清洗

    先准备一份测试文档。我创建了一个简单的Markdown文件:

    markdown

    # 产品FAQ
    
    ## 如何重置密码
    访问登录页面,点击"忘记密码",输入注册邮箱,系统会发送重置链接。
    
    ## 如何联系客服
    工作时间:周一至周五 9:00-18:00
    邮箱:support@example.com
    电话:400-123-4567
    
    ## 退款政策
    支持7天内无理由退款,超过7天需提供充分理由。
    虚拟商品一经购买不支持退款。
    

    保存为docs/faq.md

    现在写文档加载的代码:

    python

    from langchain_community.document_loaders import TextLoader
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    def load_and_split_documents(file_path: str):
        """加载并切分文档"""
        loader = TextLoader(file_path, encoding='utf-8')
        documents = loader.load()
        
        # 文本切分器,chunk_size是每段最大字符数
        # chunk_overlap是相邻两段的重复字符数,保证上下文连贯
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=200,
            chunk_overlap=50,
            length_function=len,
        )
        
        chunks = text_splitter.split_documents(documents)
        
        print(f"原始文档数:{len(documents)}")
        print(f"切分后块数:{len(chunks)}")
        
        return chunks
    
    if __name__ == "__main__":
        chunks = load_and_split_documents("docs/faq.md")
        for i, chunk in enumerate(chunks):
            print(f"\n--- 块 {i+1} ---")
            print(chunk.page_content[:100] + "...")
    

    运行后输出:

    plaintext

    原始文档数:1
    切分后块数:5
    
    --- 块 1 ---
    # 产品FAQ
    
    ## 如何重置密码
    访问登录页面,点击"忘记密码",输入注册邮箱...
    
    --- 块 2 ---
    ...忘记密码",输入注册邮箱,系统会发送重置链接。
    
    ## 如何联系客服...
    
    --- 块 3 ---
    ...邮箱:support@example.com
    电话:400-123-4567
    
    ## 退款政策
    ...
    

    可以看到,切分器保留了markdown的结构,同时让相邻块之间有50个字符的重叠,这样检索时不会丢失上下文。

    4.2 向量化与向量数据库存储

    接下来是核心步骤——把文本块转成向量,存入Chroma向量数据库。

    python

    from langchain_community.embeddings import OpenAIEmbeddings
    from langchain_chroma import Chroma
    import os
    from dotenv import load_dotenv
    
    load_dotenv()
    
    def create_vector_store(chunks, persist_directory="vector_db"):
        """创建向量数据库"""
        embeddings = OpenAIEmbeddings()
        
        # 如果数据库已存在,先删除
        if os.path.exists(persist_directory):
            import shutil
            shutil.rmtree(persist_directory)
        
        # 创建向量数据库
        vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=embeddings,
            persist_directory=persist_directory
        )
        
        print(f"向量数据库已创建,共存储 {vectorstore._collection.count()} 个向量")
        
        return vectorstore
    
    if __name__ == "__main__":
        chunks = load_and_split_documents("docs/faq.md")
        vectorstore = create_vector_store(chunks)
    

    Chroma是轻量级向量数据库,安装简单,适合本地开发测试。生产环境可以考虑Milvus、Pinecone或者阿里云向量检索服务。

    五、检索与问答实现

    5.1 相似度检索

    现在实现检索功能:

    python

    def retrieve_relevant_chunks(vectorstore, query, top_k=3):
        """检索最相关的文档块"""
        results = vectorstore.similarity_search_with_score(query, k=top_k)
        
        print(f"\n查询:{query}")
        print(f"检索到 {len(results)} 个相关块:\n")
        
        for i, (doc, score) in enumerate(results):
            print(f"【结果 {i+1}】相似度分数:{score:.4f}")
            print(f"内容:{doc.page_content[:150]}...")
            print()
        
        return results
    
    if __name__ == "__main__":
        chunks = load_and_split_documents("docs/faq.md")
        vectorstore = create_vector_store(chunks)
        
        # 测试几个不同类型的问题
        queries = [
            "密码忘了怎么办",
            "退款有什么要求",
            "上班时间客服电话多少"
        ]
        
        for query in queries:
            retrieve_relevant_chunks(vectorstore, query)
    

    运行结果:

    plaintext

    查询:密码忘了怎么办
    检索到 2 个相关块:
    【结果 1】相似度分数:0.3523
    内容:访问登录页面,点击"忘记密码",输入注册邮箱...
    
    【结果 2】相似度分数:0.4821
    内容:如何重置密码...
    

    注意看,检索到的是”如何重置密码”相关内容,而不是”密码忘了”这个关键词。这说明向量检索确实能理解语义。

    5.2 RAG问答链

    检索只是第一步,接下来要把检索结果喂给大模型生成回答:

    python

    from langchain_openai import ChatOpenAI
    from langchain.chains import RetrievalQA
    
    def create_rag_chain(vectorstore):
        """创建RAG问答链"""
        llm = ChatOpenAI(
            model_name="gpt-3.5-turbo",
            temperature=0.3  # 温度低一点,回答更准确
        )
        
        chain = RetrievalQA.from_chain_type(
            llm=llm,
            chain_type="stuff",  # 把检索结果塞到一个上下文里
            retriever=vectorstore.as_retriever(search_kwargs={"k": 2}),
            return_source_documents=True  # 返回引用的源文档
        )
        
        return chain
    
    def ask_question(chain, question):
        """向RAG系统提问"""
        print(f"\n{'='*50}")
        print(f"用户问题:{question}")
        print('='*50)
        
        result = chain.invoke({"query": question})
        
        print(f"\n【回答】\n{result['result']}")
        print(f"\n【参考来源】")
        for doc in result['source_documents']:
            print(f"- {doc.page_content}")
        
        return result
    
    if __name__ == "__main__":
        chunks = load_and_split_documents("docs/faq.md")
        vectorstore = create_vector_store(chunks)
        chain = create_rag_chain(vectorstore)
        
        ask_question(chain, "密码忘了怎么处理?")
    

    输出结果:

    plaintext

    【回答】
    根据知识库内容,如果忘记了密码,请按照以下步骤操作:
    
    1. 访问登录页面
    2. 点击"忘记密码"链接
    3. 输入您注册时使用的邮箱地址
    4. 系统会发送一封包含重置链接的邮件
    5. 点击邮件中的链接,按照提示设置新密码
    
    【参考来源】
    - 访问登录页面,点击"忘记密码",输入注册邮箱,系统会发送重置链接。
    - 如何重置密码
    

    可以看到,RAG系统不仅给出了准确的答案,还标注了信息来源。这是纯大模型做不到的。

    六、进阶优化技巧

    上面是一个最基础的RAG流程,实际项目里还有很多可以优化的地方。

    6.1 混合检索

    单纯用向量检索有时候会漏掉精确关键词匹配的情况。可以用混合检索——向量检索加关键词检索并行:

    python

    from langchain_community.retrievers import EnsembleRetriever
    
    def create_hybrid_retriever(vectorstore, texts):
        """创建混合检索器"""
        from langchain_community.retrievers import BM25Retriever
        
        # BM25是基于关键词的检索
        bm25_retriever = BM25Retriever.from_texts(texts)
        bm25_retriever.k = 2
        
        # 向量检索器
        vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
        
        # 混合检索,按权重合并结果
        ensemble_retriever = EnsembleRetriever(
            retrievers=[bm25_retriever, vector_retriever],
            weights=[0.3, 0.7]  # 关键词权重30%,向量权重70%
        )
        
        return ensemble_retriever
    

    6.2 Query改写优化

    用户的问题有时候表达不清,直接检索可能匹配不到正确内容。可以先让大模型改写问题:

    python

    def rewrite_query(query):
        """让LLM改写查询,使其更清晰"""
        llm = ChatOpenAI(temperature=0)
        
        prompt = f"""将以下用户问题改写为一个清晰、完整的检索查询。
        要求:使用正式的技术语言,包含可能的关键词。
        
        原问题:{query}
        
        改写后:"""
        
        rewritten = llm.invoke(prompt)
        return rewritten.content.strip()
    
    # 测试
    original = "密码忘了咋整"
    rewritten = rewrite_query(original)
    print(f"原问题:{original}")
    print(f"改写后:{rewritten}")
    

    七、构建完整可用的问答界面

    最后,用Streamlit快速搭一个可交互的问答界面:

    python

    import streamlit as st
    
    st.set_page_config(page_title="企业知识库问答", page_icon="💬")
    st.title("💬 企业知识库问答助手")
    
    if "chain" not in st.session_state:
        from main import create_rag_chain, create_vector_store, load_and_split_documents
        chunks = load_and_split_documents("docs/faq.md")
        vectorstore = create_vector_store(chunks)
        st.session_state.chain = create_rag_chain(vectorstore)
    
    question = st.text_input("请输入您的问题:", placeholder="例如:密码忘了怎么处理?")
    
    if question:
        with st.spinner("正在思考中..."):
            result = st.session_state.chain.invoke({"query": question})
            
        st.success("回答:")
        st.write(result['result'])
        
        with st.expander("查看参考来源"):
            for doc in result['source_documents']:
                st.write(f"- {doc.page_content}")
    

    运行命令:

    bash

    streamlit run app.py
    

    浏览器打开 http://localhost:8501 就能看到一个完整的问答界面了。

    八、写在最后

    整个RAG流程走下来,核心就这么几步:文档加载、文本切分、向量化存储、相似度检索、生成回答。看起来不难,但要做好其实有很多细节需要注意。

    比如文档切分策略,块太大容易引入噪音,块太小又可能丢失上下文。再比如检索阈值设置,低了会召回一堆无关内容,高了可能漏掉正确答案。这些都需要在项目里慢慢调优。

    我的建议是:先跑通整个流程,理解每个环节在做什么,然后再根据自己的业务场景调整参数和策略。纸上得来终觉浅,亲手实践一次比看十篇教程都有用。

    如果你在实践过程中遇到问题,欢迎在评论区留言,咱们一起讨论。

    相关推荐:

  • JavaScript入门教程:前端开发第一步

    JavaScript入门教程:前端开发第一步

    前言

    记得我第一次想学编程的时候,在网上搜”学编程从哪开始”,结果出来一堆Python、Java、C++,越看越懵。后来有个前辈跟我说:”想写网页?先学JavaScript吧。”

    我当时还纳闷,JavaScript和Java是什么关系?是Java的弟弟吗?

    后来才知道,它俩除了名字里都有”Java”之外,完全是两回事。JavaScript是专门给网页用的编程语言,运行在浏览器里,能让网页”动”起来——点击按钮弹出提示、输入内容实时验证、滚动加载更多内容,这些都是JavaScript的功劳。

    最棒的是,JavaScript现在不只能写网页了,还能写服务器(Node.js)、手机App(React Native)、桌面程序(Electron)……学会了JavaScript,感觉整个编程世界都向你敞开了大门。

    JavaScript 基础知识结构图 - 变量函数 DOM 事件与异步编程图解

    JavaScript是什么?

    一句话解释JavaScript

    JavaScript诞生于1995年,最初是为了让网页有点”交互感”——比如表单验证、弹窗提示之类的。那时候网页就是静态的HTML文档,点个按钮要么跳转到新页面,要么啥反应没有。JavaScript的出现,让网页终于能”活”起来了。

    打个比方,HTML就是网页的”骨架”,CSS是网页的”皮肤”,而JavaScript就是网页的”肌肉”——让网页能动起来。

    JavaScript能做什么

    经过二十多年的发展,JavaScript已经成为:

    • Web开发必备技能:任何现代网站都离不开JavaScript
    • 全栈开发语言:前端用React/Vue,后端用Node.js
    • 跨平台开发:手机App用React Native,桌面程序用Electron
    • 最流行的编程语言之一:常年霸占GitHub使用量榜首

    JavaScript的特点

    1. 简单易学:语法相对简洁,入门门槛低
    2. 运行即见:浏览器里按F12就能写代码看效果
    3. 应用广泛:从网页到服务器,从手机到桌面
    4. 生态丰富:npm上有上百万个第三方库

    环境准备

    浏览器内置开发工具

    学JavaScript最简单的入门方式就是浏览器。按F12打开开发者工具,切换到”Console”标签,你就可以直接写JavaScript代码了。

    试试在控制台输入:

    javascript

    console.log("Hello, JavaScript!");
    

    按回车,你会看到控制台输出了”Hello, JavaScript!”。这就是你的第一行JavaScript代码!

    VS Code编辑器

    写更长的代码需要编辑器。推荐VS Code,它是微软出的免费编辑器,对JavaScript支持非常好。

    下载地址:https://code.visualstudio.com/

    安装好之后,创建一个index.html文件:

    html

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>我的第一个JavaScript网页</title>
    </head>
    <body>
        <h1>Hello JavaScript!</h1>
        <div id="app"></div>
        
        <script>
            // 在这里写JavaScript代码
            document.getElementById("app").innerHTML = "<p>JavaScript已加载!</p>";
            console.log("脚本执行了!");
        </script>
    </body>
    </html>
    

    用浏览器打开这个HTML文件,你会看到页面内容被JavaScript修改了。

    JavaScript基础语法

    变量声明

    JavaScript里有三种声明变量的方式:

    javascript

    // let:声明可重新赋值的变量
    let name = "小明";
    name = "小红"; // 可以重新赋值
    
    // const:声明常量,不能重新赋值
    const PI = 3.14159;
    // PI = 3.14; // 错误!常量不能重新赋值
    
    // var:旧式的声明方式,不推荐使用(有很多坑)
    var age = 18;
    

    为什么推荐用let和const?

    javascript

    // var的问题:可以重复声明,容易出错
    var count = 1;
    var count = 2; // 不报错,容易造成bug
    
    // let解决:不能重复声明
    let count = 1;
    let count = 2; // 报错!
    
    // const解决:声明时必须赋值,且不能改
    const PI = 3.14159;
    PI = 3.14; // 报错!
    
    // const用于对象:引用不能改,但属性可以改
    const user = { name: "小明" };
    user.name = "小红"; // 可以!只改属性,不改引用
    user = {}; // 报错!不能改引用
    

    数据类型

    JavaScript有几种基本数据类型:

    javascript

    // 字符串(String)
    let greeting = "你好,世界!";
    let name = '小明';
    let template = `Hello, ${name}!`; // 模板字符串
    
    // 数字(Number)
    let count = 42;
    let price = 19.99;
    let scientific = 1.5e 6; // 科学计数法:1500000
    
    // 布尔值(Boolean)
    let isActive = true;
    let isFinished = false;
    
    // 空值和未定义
    let empty = null; // 空对象
    let notDefined = undefined; // 未定义
    
    // 对象(Object)
    let user = {
        name: "小明",
        age: 18,
        isStudent: true,
        sayHello: function() {
            return "你好!";
        }
    };
    
    // 数组(Array)
    let fruits = ["苹果", "香蕉", "橙子"];
    let mixed = [1, "hello", true, null];
    
    // 函数(Function)
    let add = function(a, b) {
        return a + b;
    };
    

    运算符

    javascript

    // 算术运算符
    let a = 10, b = 3;
    console.log(a + b);   // 13,加法
    console.log(a - b);   // 7,减法
    console.log(a * b);   // 30,乘法
    console.log(a / b);   // 3.333...,除法
    console.log(a % b);   // 1,取余
    console.log(a ** b);  // 1000,幂运算(10的3次方)
    console.log(++a);     // 11,自增
    console.log(--b);     // 2,自减
    
    // 比较运算符
    console.log(5 == "5");   // true,宽松相等(类型转换后比较)
    console.log(5 === "5");  // false,严格相等(类型不同直接不等)
    console.log(5 != "5");   // false
    console.log(5 !== "5");  // true
    console.log(3 > 2);      // true
    console.log(3 >= 3);     // true
    
    // 逻辑运算符
    console.log(true && false); // false,与运算
    console.log(true || false); // true,或运算
    console.log(!true);         // false,非运算
    
    // 短路运算
    console.log(true || console.log("不会执行")); // true
    console.log(false && console.log("不会执行")); // false
    

    条件判断

    javascript

    let score = 85;
    
    // if-else语句
    if (score >= 90) {
        console.log("优秀");
    } else if (score >= 60) {
        console.log("及格");
    } else {
        console.log("不及格");
    }
    
    // 三元运算符(简单条件时用)
    let result = score >= 60 ? "及格" : "不及格";
    console.log(result);
    
    // switch语句(多条件分支)
    let day = 3;
    switch (day) {
        case 1:
            console.log("星期一");
            break;
        case 2:
            console.log("星期二");
            break;
        case 3:
            console.log("星期三");
            break;
        default:
            console.log("其他日子");
    }
    
    // 多条件判断(现代写法)
    let grade = score >= 90 ? "A" : 
                score >= 80 ? "B" : 
                score >= 60 ? "C" : "D";
    

    循环

    javascript

    // for循环(最基本)
    for (let i = 0; i < 5; i++) {
        console.log(i); // 输出 0, 1, 2, 3, 4
    }
    
    // while循环
    let count = 0;
    while (count < 3) {
        console.log(count);
        count++;
    }
    
    // do-while循环(至少执行一次)
    let num = 0;
    do {
        console.log(num);
        num++;
    } while (num < 0); // 条件为false,但会执行一次
    
    // for...of循环(遍历数组)
    let fruits = ["苹果", "香蕉", "橙子"];
    for (let fruit of fruits) {
        console.log(fruit);
    }
    
    // for...in循环(遍历对象属性)
    let user = { name: "小明", age: 18, city: "北京" };
    for (let key in user) {
        console.log(key + ": " + user[key]);
    }
    
    // 数组的forEach方法
    fruits.forEach((fruit, index) => {
        console.log(`${index + 1}. ${fruit}`);
    });
    

    函数

    函数是JavaScript的核心,它是组织代码的基本单元。

    javascript

    // 函数声明
    function greet(name) {
        return "你好," + name + "!";
    }
    
    // 函数表达式
    const sayHi = function(name) {
        return "Hi, " + name;
    };
    
    // 箭头函数(ES6新增,推荐)
    const add = (a, b) => a + b;
    const greetArrow = (name) => "你好," + name + "!";
    
    // 带默认参数的函数
    function greetWithDefault(name = "陌生人") {
        return "你好," + name;
    }
    
    // 函数调用
    console.log(greet("小明"));     // 你好,小明!
    console.log(add(1, 2));        // 3
    console.log(greetWithDefault()); // 你好,陌生人!
    
    // 立即执行函数(IIFE)
    (function() {
        console.log("立即执行!");
    })();
    
    // 回调函数
    function doSomething(callback) {
        console.log("做点什么...");
        callback();
    }
    
    doSomething(() => console.log("回调执行了!"));
    

    DOM操作:让网页动起来

    DOM(Document Object Model)是JavaScript操作网页的接口。通过DOM,你可以获取、修改、添加、删除HTML元素。

    获取元素

    javascript

    // 通过ID获取(最常用)
    let title = document.getElementById("title");
    
    // 通过标签名获取
    let paragraphs = document.getElementsByTagName("p");
    
    // 通过类名获取
    let cards = document.getElementsByClassName("card");
    
    // 通过选择器获取(CSS选择器语法,推荐)
    let firstCard = document.querySelector(".card");  // 第一个匹配
    let allCards = document.querySelectorAll(".card"); // 所有匹配的
    
    // 获取body
    let body = document.body;
    
    // 获取表单元素
    let form = document.querySelector("form");
    let inputs = form.elements; // 获取所有表单控件
    

    修改元素内容

    javascript

    // 修改文本内容
    let heading = document.querySelector("h1");
    heading.textContent = "新的标题";   // 纯文本(安全,防止XSS)
    heading.innerHTML = "<strong>加粗的标题</strong>"; // 支持HTML
    
    // 修改样式
    heading.style.color = "red";
    heading.style.fontSize = "24px";
    heading.style.fontWeight = "bold";
    
    // 添加/删除类
    heading.classList.add("highlight");
    heading.classList.remove("old-style");
    heading.classList.toggle("active"); // 切换类
    
    // 修改属性
    let link = document.querySelector("a");
    link.href = "https://example.com";
    link.target = "_blank";
    link.setAttribute("data-id", "123"); // 设置自定义属性
    
    // 获取属性
    let href = link.getAttribute("href");
    let id = link.dataset.id; // 获取data-*属性
    

    事件处理

    事件是用户和网页交互的动作,比如点击、鼠标悬停、键盘输入等。

    javascript

    // 获取按钮
    let button = document.querySelector("#myButton");
    
    // 添加点击事件
    button.addEventListener("click", function(event) {
        console.log("按钮被点击了!");
        console.log("事件对象:", event);
    });
    
    // 使用箭头函数(更简洁)
    button.addEventListener("click", (e) => {
        console.log("点击了:", e.target);
    });
    
    // 带参数的事件处理
    button.addEventListener("click", () => {
        handleClick(123);
    });
    
    function handleClick(id) {
        console.log("处理的ID:", id);
    }
    
    // 常见的其他事件
    // mouseover/mouseout - 鼠标悬停
    // mousedown/mouseup - 鼠标按下/抬起
    // keydown/keyup - 键盘按键
    // submit - 表单提交
    // change - 输入框内容变化且失去焦点
    // input - 输入框内容实时变化
    // focus/blur - 获得/失去焦点
    
    // 表单提交事件
    let form = document.querySelector("form");
    form.addEventListener("submit", (e) => {
        e.preventDefault(); // 阻止默认提交行为
        console.log("表单提交了!");
        // 在这里发送数据
    });
    
    // 键盘事件
    document.addEventListener("keydown", (e) => {
        if (e.key === "Enter") {
            console.log("按下了回车键!");
        }
        if (e.ctrlKey && e.key === "s") {
            e.preventDefault();
            console.log("Ctrl+S 快捷键");
        }
    });
    

    实战:做一个简单的计数器

    完整的HTML+CSS+JavaScript示例:

    html

    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>计数器</title>
        <style>
            body {
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                display: flex;
                justify-content: center;
                align-items: center;
                min-height: 100vh;
                margin: 0;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            }
            .counter {
                background: white;
                padding: 40px;
                border-radius: 20px;
                box-shadow: 0 10px 40px rgba(0,0,0,0.2);
                text-align: center;
                min-width: 300px;
            }
            .counter h2 {
                color: #333;
                margin-bottom: 10px;
            }
            .count {
                font-size: 80px;
                color: #333;
                margin: 20px 0;
                font-weight: bold;
            }
            button {
                padding: 15px 35px;
                font-size: 18px;
                border: none;
                border-radius: 8px;
                cursor: pointer;
                margin: 0 8px;
                transition: all 0.3s;
                color: white;
                font-weight: bold;
            }
            .decrease { background: #ff6b6b; }
            .reset { background: #95a5a6; }
            .increase { background: #4ecdc4; }
            button:hover {
                transform: translateY(-2px);
                box-shadow: 0 5px 15px rgba(0,0,0,0.2);
            }
            button:active {
                transform: translateY(0);
            }
        </style>
    </head>
    <body>
        <div class="counter">
            <h2>计数器</h2>
            <div class="count" id="count">0</div>
            <button class="decrease" onclick="decrease()">减少</button>
            <button class="reset" onclick="reset()">重置</button>
            <button class="increase" onclick="increase()">增加</button>
        </div>
    
        <script>
            // 初始化计数器
            let count = 0;
            const countDisplay = document.getElementById("count");
    
            // 更新显示
            function updateDisplay() {
                countDisplay.textContent = count;
                
                // 根据数字大小改变颜色
                if (count > 0) {
                    countDisplay.style.color = "#4ecdc4";
                } else if (count < 0) {
                    countDisplay.style.color = "#ff6b6b";
                } else {
                    countDisplay.style.color = "#333";
                }
            }
    
            // 增加
            function increase() {
                count++;
                updateDisplay();
                // 添加点击动画效果
                countDisplay.style.transform = "scale(1.2)";
                setTimeout(() => {
                    countDisplay.style.transform = "scale(1)";
                }, 100);
            }
    
            // 减少
            function decrease() {
                count--;
                updateDisplay();
                countDisplay.style.transform = "scale(0.8)";
                setTimeout(() => {
                    countDisplay.style.transform = "scale(1)";
                }, 100);
            }
    
            // 重置
            function reset() {
                count = 0;
                updateDisplay();
            }
    
            // 键盘控制
            document.addEventListener("keydown", (e) => {
                if (e.key === "ArrowUp" || e.key === "+") {
                    increase();
                } else if (e.key === "ArrowDown" || e.key === "-") {
                    decrease();
                } else if (e.key === "r" || e.key === "R") {
                    reset();
                }
            });
        </script>
    </body>
    </html>
    

    异步编程:Promise和async/await

    传统网页要获取新数据需要刷新整个页面,AJAX让网页可以在不刷新的情况下获取服务器数据。

    Promise基础

    Promise是ES6引入的异步编程解决方案,比回调函数更清晰。

    javascript

    // 创建Promise
    const myPromise = new Promise((resolve, reject) => {
        // 模拟异步操作
        setTimeout(() => {
            const success = true;
            if (success) {
                resolve("操作成功!");
            } else {
                reject("操作失败!");
            }
        }, 1000);
    });
    
    // 使用Promise
    myPromise
        .then(result => console.log(result))
        .catch(error => console.error(error))
        .finally(() => console.log("无论成功失败都会执行"));
    

    async/await

    async/await是Promise的语法糖,让异步代码看起来像同步代码。

    javascript

    // 定义async函数
    async function fetchUserData() {
        try {
            // await等待Promise完成
            const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
            const user = await response.json();
            console.log(user);
            return user;
        } catch (error) {
            console.error("获取数据失败:", error);
        }
    }
    
    // 调用async函数
    fetchUserData().then(user => {
        console.log("获取到的用户:", user.name);
    });
    

    fetch API获取数据

    javascript

    // 简单的GET请求
    fetch("https://jsonplaceholder.typicode.com/users")
        .then(response => {
            if (!response.ok) {
                throw new Error("网络响应失败");
            }
            return response.json();
        })
        .then(users => {
            users.forEach(user => {
                console.log(`${user.name} - ${user.email}`);
            });
        })
        .catch(error => console.error("请求失败:", error));
    
    // 更现代的async/await写法
    async function getUsers() {
        try {
            const response = await fetch("https://jsonplaceholder.typicode.com/users");
            if (!response.ok) throw new Error("获取失败");
            
            const users = await response.json();
            users.forEach(user => {
                console.log(`${user.name} - ${user.email}`);
            });
        } catch (error) {
            console.error("请求失败:", error);
        }
    }
    
    getUsers();
    
    // 发送POST请求
    async function createUser(userData) {
        try {
            const response = await fetch("https://jsonplaceholder.typicode.com/users", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json"
                },
                body: JSON.stringify(userData)
            });
            
            const result = await response.json();
            console.log("创建成功:", result);
            return result;
        } catch (error) {
            console.error("创建失败:", error);
        }
    }
    
    createUser({
        name: "张三",
        email: "zhangsan@example.com"
    });
    

    常用JavaScript库

    Lodash

    实用的工具函数库,提供数组、对象、字符串等操作的辅助函数。

    javascript

    // 安装:npm install lodash
    // 或CDN引入:<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
    
    import _ from 'lodash';
    
    // 防抖函数
    const debouncedSearch = _.debounce(searchFunction, 300);
    
    // 节流函数
    const throttledScroll = _.throttle(handleScroll, 100);
    
    // 深拷贝对象
    const copied = _.cloneDeep(originalObject);
    
    // 数组去重
    const unique = _.uniq([1, 2, 2, 3, 3, 3]); // [1, 2, 3]
    
    // 数组扁平化
    const flat = _.flattenDeep([1, [2, [3, [4]]]]); // [1, 2, 3, 4]
    
    // 查找满足条件的第一个元素
    const user = _.find(users, { active: true });
    
    // 对象合并
    const merged = _.assign({}, obj1, obj2);
    

    Axios

    比fetch更好用的HTTP请求库。

    javascript

    // 安装:npm install axios
    import axios from 'axios';
    
    // GET请求
    axios.get('https://api.example.com/users')
        .then(response => console.log(response.data));
    
    // POST请求
    axios.post('https://api.example.com/users', {
        name: '小明',
        age: 18
    })
        .then(response => console.log(response.data))
        .catch(error => console.error('请求失败:', error));
    
    // 同时发送多个请求
    async function loadData() {
        const [users, posts] = await Promise.all([
            axios.get('/api/users'),
            axios.get('/api/posts')
        ]);
        console.log(users.data, posts.data);
    }
    
    // 设置全局配置
    axios.defaults.baseURL = 'https://api.example.com';
    axios.defaults.timeout = 10000;
    axios.defaults.headers.common['Authorization'] = 'Bearer token';
    

    常见错误和调试

    语法错误

    javascript

    // 常见错误1:忘记引号配对
    let str = '这是一个字符串"; // 语法错误
    
    // 常见错误2:变量未声明
    console.log(nmae); // ReferenceError: name is not defined
    
    // 常见错误3:数组越界
    let arr = [1, 2, 3];
    console.log(arr[10]); // undefined
    
    // 常见错误4:在异步回调中访问this
    class Timer {
        constructor() {
            this.time = 0;
            setTimeout(function() {
                this.time++; // 这里的this不是Timer实例!
            }, 1000);
        }
    }
    
    // 解决:使用箭头函数或bind
    setTimeout(() => {
        this.time++;
    }, 1000);
    

    使用console调试

    javascript

    // 基本输出
    console.log("普通信息");
    
    // 警告和错误
    console.warn("警告信息");
    console.error("错误信息");
    
    // 查看对象(更详细)
    let obj = { name: "小明", age: 18 };
    console.log(obj); // 普通日志
    console.dir(obj); // 交互式属性列表
    console.table(obj); // 表格形式显示
    
    // 分组显示
    console.group("用户信息");
    console.log("姓名:", obj.name);
    console.log("年龄:", obj.age);
    console.groupEnd();
    
    // 格式化输出
    console.log("用户%s今年%d岁", obj.name, obj.age);
    
    // 计时
    console.time("循环");
    for (let i = 0; i < 10000; i++) {}
    console.timeEnd("循环");
    

    使用断点调试

    在浏览器开发者工具的Sources面板中:

    1. 点击行号添加断点
    2. 刷新页面执行到断点处暂停
    3. 在右侧面板查看变量值
    4. 使用调试工具栏单步执行

    JavaScript学习路线

    作为一个过来人,我的建议是:

    1. HTML/CSS基础:先学会写静态网页
    2. JavaScript基础:变量、函数、DOM操作
    3. ES6+新特性:箭头函数、模块化、async/await
    4. 框架学习:Vue、React、Angular选一个
    5. 工程化:Webpack、Vite、npm/yarn

    推荐的学习资源

    • MDN Web Docs:最权威的JavaScript文档
    • 《JavaScript高级程序设计》:经典书籍
    • 阮一峰的博客:中文教程写得很好
    • LeetCode:刷算法题巩固基础

    总结

    JavaScript是一门很”宽容”的语言,语法相对简单,入门门槛低,但要想精通却需要不断实践。

    今天这篇文章涵盖了JavaScript入门的大部分核心内容:

    • JavaScript简介和应用场景
    • 变量声明和数据类型
    • 运算符和表达式
    • 条件判断和循环
    • 函数定义和调用
    • DOM操作和事件处理
    • 异步编程基础
    • 常用JavaScript库
    • 调试技巧

    建议的学习方法是:多动手敲代码。不要只看教程,看完一个知识点就自己写几个例子试试。遇到问题就Google,很多你遇到的问题别人早就遇到过了。

    相关推荐

    动手实践是最好的学习方式,打开你的编辑器,开始写代码吧!