[{"categories":["随笔"],"collections":null,"content":"判断对错靠专家，可专家培养不出那么多。那剩下九百万学生，在学校里该学什么？","date":"2026-10-23","objectID":"/2026/10/ai-education/","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/"},{"categories":["随笔"],"collections":null,"content":"去年有个学生来找我，成绩很好，做事也认真。他问我：我该上大学学什么。 这个问题我回答过很多年，几乎不用想。先问问他喜欢什么、擅长什么，再看看眼下什么行业缺人，然后给个方向——计算机、数学、电子，都行——最后加一句：好好学，这条路是稳的。学生听了踏实，我也觉得自己尽到了责任。 现在我一句话都说不出口。不是不知道有哪些专业，专业我都熟；是「稳」这个字我不敢用了。我不知道四年、七年之后，他学的东西还值不值钱。 但真正让我放不下的，其实不是这一个学生。 我想到的是这样一笔账：假如一年培养一千万学生，其中五十万、最多一百万能成为真正的专家——这已经是相当乐观的估计了——那剩下九百万呢？他们不会成为专家，也没有那么多时间再去成为，可他们同样要用 AI 干活、做决定，并且承担决定的后果。 判断对错这件事，确实是专家的事。可专家培养不出那么多，也不该指望培养出那么多。所以真正压在教育身上的问题其实很朴素： 那九百多万人，在学校里该学什么？ 这才是我想说的。要说清它从哪儿来，得先讲这两年我看见的三件事。 ","date":"2026-10-23","heading":"","objectID":"/2026/10/ai-education/:0:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#"},{"categories":["随笔"],"collections":null,"content":"从一篇摘要说起 事情要从 2022 年 11 月 30 日讲起。那天 ChatGPT 上线，用的是 GPT-3.5。 一开始我没太在意。聊天机器人早就有了，无非是笨一点、慢一点，属于能预料到的进步。 真正让我停下来的是件很小的事。我丢给它一篇文章，让它说说这篇文章讲了什么。它说对了，而且说得比不少学生清楚。 那一刻我承认了一件事：它有点「智能」了。能读进去、能拎出主旨，这在我原来的认识里，是只有人才干得了的活。它到底有没有真懂，我说不好，但它给出的结果已经很像了。 可我没有顺着往下想。我的判断是：读懂一篇文章是一回事，写程序是另一回事。写程序要的是另一种聪明——逻辑得立得住，条件得想全，跑不通就是跑不通。语言上那点灵光，代替不了这个。所以我当时并不慌，觉得它顶多是个会写文字的帮手。我教了多年计算机，这条界线在我心里画得挺清楚。 这道界线，不到一年就被抹掉了。2023 到 2024 年，AI 编程工具一下子铺开。像 v0 这类东西，你用普通话把想要什么讲一遍，前端界面就出来了。 让我不安的不是「程序员能写得更快了」。快是好事，效率问题人总会习惯。让我不安的是：有些东西过去得学好几年才做得出来，现在不用了。 ","date":"2026-10-23","heading":"从一篇摘要说起","objectID":"/2026/10/ai-education/:1:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#从一篇摘要说起"},{"categories":["随笔"],"collections":null,"content":"写代码这件事 所以我当时就有个感觉：一个年轻人要是把学编程当成自己最重要的本钱，这个本钱恐怕不像以前那么靠得住了。 这话说出去容易被人听成两个意思，得说清楚。我的意思是，写代码这件事的市场价钱可能会掉。我不是说编程知识从此没用——那是另一回事，而且不对。 编程的知识还是有用的，只是用处在挪位置。它让你看得懂一个系统是怎么转的，能把一个含糊的需求说成能干的活，能看出别人交来的东西对不对，能为自己签字的那一块负责。这些事不会因为工具变强而消失，反而更重要了——因为东西出得多了，判断的责任就重了。 真正变的是位置：会写代码，以前是进门的那道门槛，现在只是个起点。它是你理解系统的一种方式，仅此而已。 ","date":"2026-10-23","heading":"写代码这件事","objectID":"/2026/10/ai-education/:2:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#写代码这件事"},{"categories":["随笔"],"collections":null,"content":"数学也躲不过 有人会想，那不去写代码就是了。可事情没这么便宜。 编程先被打穿，是因为它有个特别好用的信号：代码跑不跑得通，一试就知道。数学看着更玄，更吃天赋和年头，可它也一样有硬标准——一个证明对不对，是可以验的。凡是能一眼验出对错的地方，机器都会追上来。 于是问题就变了。不再是「AI 会不会做题」，而是「AI 能不能坐下来做研究」。 会做题不算什么。竞赛题是答案已知的封闭问题，做对一百道也不等于能提出新问题。真正要看的是：它做出来的证明，别人能不能独立审？是不是原创的？有没有人读懂、有没有人真的用上？ 我给「打穿」这个词下过一个定义，到现在还觉得准： 打穿一个领域，不是指人再也不用学这个领域了，而是指：过去得靠多年训练、只有少数专家才干得了的核心工作，开始能由 AI 大批量地做。 今年十月那批七百多篇数学手稿，我单独写过一篇（《古法创新：当 AI 接管数学之后》），这里不重复。只说一点：那些东西该一篇一篇去核实原文、看质量和审查情况，不能光看数量下结论。但研究这件事的做法确实在变，这一点已经够让教书的人重新想一想了。 ","date":"2026-10-23","heading":"数学也躲不过","objectID":"/2026/10/ai-education/:3:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#数学也躲不过"},{"categories":["随笔"],"collections":null,"content":"答不上来的那句话 三件事讲完，还得回到那个学生。 「这条路是稳的」这句话我说不出口，不是我懂得不够多，是我没法再有把握地跟他说：你把人生里很大一块时间投到这个方向，将来就能拿回相应的东西。 这个「将来」太远了。今天看着稀罕的本事，等他毕业可能满街都是；今天得练好多年的活，可能被模型吃掉一半，甚至大半。技术变得比任何人做预测都准。 而且这不只是我知道得够不够的问题，是责任的问题。学生会照着老师、学校、社会的话去投几年时间、几年学费，还有几年本来能干别的事的机会。这笔账是他一个人付的，赌的是我猜得对不对。要是我给的建议建立在过时的前提上，代价却是他掏，那我这话再真心也不算负责。 所以负责任的说法，不该是假装自己能算准十年后的就业市场。得反过来：讲清楚我凭什么这么说，我假设了什么，风险在哪，什么情况下这个建议就不成立了。这么讲不像建议，可它是眼下唯一诚实的说法。 ","date":"2026-10-23","heading":"答不上来的那句话","objectID":"/2026/10/ai-education/:4:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#答不上来的那句话"},{"categories":["随笔"],"collections":null,"content":"剩下那九百万怎么办 开头那笔账，来自一个很具体的念头：能不能把判断这件事也交给 AI。 让一个模型去做，再让另一个模型去看看它有没有错——A 判断 B，在很多场合确实管用。但它有个前提我没想清楚：A 和 B 得站在不同的位置上。如果两个模型用的是同一套技术、同一条路径、同一个领域的知识，它们就会犯同样的错，然后互相确认对方是对的。你得到的不是校验，只是两个一致的声音。 所以在培养人的过程里，能不能判断对错，最终还是得靠人，靠专家。这里没有捷径——一个能在自己领域里看出 AI 错在哪儿的人，要很多年才养得出来，而且只能一个一个地养。 这就是那笔账的来路：一千万学生，五十万到一百万能长成这样的人，剩下九百万都不算。他们不会成为专家，也没有那么多时间再成为专家，可他们要用 AI 干活、做决定、担后果。 用「自己多留个心眼」来回答，等于把系统的可靠性问题推给个人，等于不回答。出路我知道两条：一条是专家把验证的办法做成普通人用得上的工具，这条是远水，等它来要很多年；另一条是学校现在就能做的，教他们一种不是专家、但也不至于全盘照收的判断力。 九百多万人今天就在教室里。下面讲的，是第二条。 ","date":"2026-10-23","heading":"剩下那九百万怎么办","objectID":"/2026/10/ai-education/:5:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#剩下那九百万怎么办"},{"categories":["随笔"],"collections":null,"content":"想起废除科举 想这些事的时候，我总会想起清朝废除科举。 有人寒窗苦读二十年，把一辈子的打算、脸面、全家的生计都压在这套制度上。制度一废，那些年头不会自动变成新出路。书还在，人还在，可那条通向体面日子的路没了。 AI 时代当然不是废除科举。科举是一刀切，某年某月一纸诏书，规矩全换；AI 对职业的侵蚀是一点点来的，一个领域一个领域地发生，方向和快慢都还说不准，有时候只是把入行的门槛悄悄抬了一点。 可两件事背后有个一样的问题： 一个人照着社会认可的规矩投了很多年，后来规矩变了，这笔损失该不该全让他自己扛？ 这不只是「他有没有适应能力」的事。它牵到教育制度、学校、雇主和社会：能不能给他重新转行的机会，能不能让他有条件接着学，转型的风险能不能大家分一分。 教育者没法保证未来不变。但至少有一件事可以不做：把没把握的预测，说成有把握的承诺。 ","date":"2026-10-23","heading":"想起废除科举","objectID":"/2026/10/ai-education/:6:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#想起废除科举"},{"categories":["随笔"],"collections":null,"content":"那该教学生什么 回到那九百万学生。这几年我想下来，方向其实是清楚的：别只教他适应某一个岗位，教他在环境变了以后，能把本事带走、重新长出来的能力。 这个词容易被理解歪，先说它不是什么。它不是「什么都懂一点」——那种人什么都聊得上两句，真上手什么都拿不出来；也不是说不要专业，因为专业没到一定深度，所谓「跨界」就只是拼凑。 它说的是这么回事：当原来那身本事不值钱了、干活的方式变了，或者不得不进一个完全陌生的行当，一个人能拿已有的底子，比较快地学、判断、上手，然后调整方向。拆开看，大概六样。 **敢进陌生领域，还能把架子搭起来。**碰上没学过的问题，知道怎么圈定它、怎么立起最基本的概念、上哪儿找材料、怎么看出自己缺的是哪一块，然后靠动手把水平一点点推到能用。这事听着虚，其实很实：它就是「三天能上手一样没见过的东西」和「三个月还在原地转」的差别。 **能把问题说清楚，把目标定清楚。**讲得明白自己要解决什么、有什么限制、做到什么算成。这跟研究提示词不是一回事。模型越强，很多话术它自己会处理，专门学一套说法的收益越来越小；但「说清楚自己要什么」这件事自动化不了——它不是口才问题，是想没想明白的问题。 **能看出结果靠不靠得住。**不用人人成专家，但能看出哪些结论得有证据、哪个风险不能当没看见、什么情况下必须找人验一遍或者去请教懂行的人。说白了，是知道自己什么时候不该信，包括不该信自己。 **能把知识用到真事上。**能把想法变成动作，再拿结果反推自己哪儿想错了。真事比标准答案更能照出理解的窟窿，做过一遍才知道，那些自以为懂的地方其实没懂。 **能把一个领域的东西搬到另一个领域。**看得见技术和具体的生意、和人的处境之间的关系。这一条最容易说成空话，可前面几条真做到了，它就不空：你得在至少一件事上扎得够深，深到知道这法子为什么管用、到哪儿就不管用了，才知道它能搬到哪儿去。 **能跟人合作，能担事。**能跟人一块干，能说清自己的依据和难处，能明白别人要什么，也能对自己负责的那块认账。这条听着最不像本事，可在一个产出多得看不完、判断越来越缺的环境里，它可能是最贵的。 这六样都不是专业的替代品。它们就一个用处：把你已经会的带到新地方去，需要的时候，重新学一遍学得更快。 ","date":"2026-10-23","heading":"那该教学生什么","objectID":"/2026/10/ai-education/:7:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#那该教学生什么"},{"categories":["随笔"],"collections":null,"content":"学校能做的 如果上面这套说得通，大学大概得同时干两件事，而且这两件看着还有点别扭。 一件是把专业打扎实：让学生真懂一个领域的核心概念、方法和它的边界，而不是只练一套容易被机器接手的操作。另一件是教方法：怎么进一个陌生领域，怎么把问题界定清楚，怎么验一个结论，怎么用 AI，怎么处理真活，条件变了怎么拐弯。 这不是说所有专业都该改成泛泛的通识课。恰恰相反，没有深度的通识是最没用的一种教育，专业能力给不了，迁移的起点也给不了。关键只有一条：别把整个教育押在「某个岗位会一直在」这个假设上。 还有一件比改课程表难得多：老老实实承认不确定。跟学生讲清这个专业好在哪、险在哪、还有没有别的路，给他中途重新选一次的机会，尽量让转方向的代价小一点。这些事做起来不好看，因为它要求学校承认自己也不知道十年后什么样。 ","date":"2026-10-23","heading":"学校能做的","objectID":"/2026/10/ai-education/:8:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#学校能做的"},{"categories":["随笔"],"collections":null,"content":"最后 过去当老师的容易说一句话：学这个专业，将来有前途。 现在这句话越来越难说出口。技术一变，专业知识的用法、职业的样子、市场的价钱都会跟着变，而这些变化，教书的人未必算得准。 但更诚实的做法不是从此不开口，而是换一种说法：讲清现在知道什么、猜的是什么、还不知道什么；讲清这条路的理由和风险；帮他打个扎实的底子，但别把命押在一个岗位上；教他在规矩变了以后，还能重新学、重新判断、重新长本事；让制度给人留转的机会，而不是把适应的代价全推给个人。 所以最后的问题不是「还有哪个专业绝对安全」——大概没有。 而是： 我们没法保证一个学生今天学的东西十年后还值钱，那教育该怎么陪他熬过这个变化，而不是让他一个人去扛整个时代转的账？ 这个问题我没有答案。但我知道，把没把握的事说成有把握，是最坏的一种回答。 ","date":"2026-10-23","heading":"最后","objectID":"/2026/10/ai-education/:9:0","tags":["AI","教育","思考"],"title":"当老师不再敢说「学这个将来有前途」","uri":"/2026/10/ai-education/#最后"},{"categories":["随笔"],"collections":null,"content":"AI 能不能做数学不是问题。真正的问题是：人类会不会逐渐停止生产数学家。","date":"2026-10-23","objectID":"/2026/10/ai-math-civilization/","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/"},{"categories":["随笔"],"collections":null,"content":"如果哪一天，AI 把一个定理完整地证完了，摆在你面前，你打开形式化验证器跑一遍，结果显示：正确。 然后你问它：这个定理意味着什么？它答了。你追问：为什么它重要？它也答了。每一句都通顺，每一句你都能读，但你就是没法确认——它说的是不是真的那个“重要”。 那一刻，知识仍然是真的，只是不再属于你。 这篇文章想理清的，就是走到那一步之前，中间会经过哪些站。 ","date":"2026-10-23","heading":"","objectID":"/2026/10/ai-math-civilization/:0:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#"},{"categories":["随笔"],"collections":null,"content":"一个更朴素的问题 这些想法大部分来自一次和 AI 的长聊。起点其实不是“AI 会不会取代数学家”，而是一个更朴素的直觉： 如果 AI 已经能源源不断地产出数学结果，未来的孩子还会不会把“数学家”当成一件值得干一辈子的事？ 关键不在于数学家会不会失业。数学是一门高度抽象、需要长期训练和真实热爱的学科。一次突破往往不是“会做题”就能得到，而是几十年持续投入的结果。一旦社会上慢慢形成“数学已经被 AI 解决了”的看法，愿意把一生投进去的年轻人就会减少。 这会构成一条反馈链： AI 数学能力增强 → 人类认为数学已属于 AI → 年轻人减少 → 人类数学研究力量下降 → 人类原创数学进一步减少 → 更加证明“数学已经是 AI 的领域”。 真正要担心的，是人类会不会逐渐停止生产数学家。 ","date":"2026-10-23","heading":"一个更朴素的问题","objectID":"/2026/10/ai-math-civilization/:1:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#一个更朴素的问题"},{"categories":["随笔"],"collections":null,"content":"为什么数学和打字员不一样 打字员消失了，胶片冲印师消失了，相关的能力也随之留在历史里。这没什么可怕——那些手艺本来就不是别的东西的地基。 数学不一样。 物理、化学、计算机、气候模型、生物、工程，全都长在数学思想之上。如果人类不再培养能深入数学的人，失去的不只是一个职业群体，而是未来科学创新所需要的思想来源。 技术职业的消失，不一定意味着技术文明停止；但基础学科研究者的大规模消失，可能影响文明继续产生新知识的能力。 ","date":"2026-10-23","heading":"为什么数学和打字员不一样","objectID":"/2026/10/ai-math-civilization/:2:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#为什么数学和打字员不一样"},{"categories":["随笔"],"collections":null,"content":"文学也会衰落，但代价不同 讨论到这里绕了一下：文学是不是早就经历过类似的事？ 是的。现代人越来越少进行长时间阅读，社交媒体把注意力切成了碎片。未来大概仍然有作家，但成为大作家的人可能越来越少。 不过文学和数学有一个根本差异：文学的门槛低，会说话就能写，区别只在于写得好不好；数学的门槛高，做原创数学需要长期训练、深厚基础和高度专业化的能力。 所以，当社会对某件事失去功利需求时，文学的衰落是缓慢的、分散的；而数学一旦失去人，可能断得很干脆。 ","date":"2026-10-23","heading":"文学也会衰落，但代价不同","objectID":"/2026/10/ai-math-civilization/:3:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#文学也会衰落但代价不同"},{"categories":["随笔"],"collections":null,"content":"古法创新 “古法创新”这个词，是与张晋骞博士（harushinken.github.io）在一次聊天里提到的。这个说法在他之前是不是已经有人用过，我没有查证；但它比“原创”准确得多： 古法创新：不借助外部智能，主要依靠自己的知识、推理和能力，去发现新的东西。 这不是反对工具，也不是认为过去的方法一定更好。它指向的是一种具体的文明能力： 人在不知道答案的情况下，自己摸索、犯错、形成猜想、继续推进，最后把答案挖出来。 ChatGPT 出现之后，这个过程正在被改写。越来越多人会把问题直接交出去，先拿到答案，再回头理解——甚至不回头。 于是问题不是“手搓是不是更先进”，而是： 人类是否还应该保留亲自发现未知的能力？ ","date":"2026-10-23","heading":"古法创新","objectID":"/2026/10/ai-math-civilization/:4:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#古法创新"},{"categories":["随笔"],"collections":null,"content":"当 AI 的产出超过人类的消化速度 2026 年 10 月 6 日，OpenAI 在 GitHub 上一次性放出了 722 篇数学论文，说其中包含数百个未解问题的解答，而生成它们的模型还没有对外发布。据报道，这批手稿号称解决了 300 多道此前无人攻克的难题，覆盖代数、逻辑、几何、概率。 这个数字很惊人，但更让我在意的是它揭示的可能性： AI 可以在很短的时间里，产出过去需要数学家长期研究才能得到的结果。 一旦 AI 产出成果的速度超过人类阅读、理解和验证的速度，数学家的角色就会变： 过去的链条是—— 人类提出问题 → 人类研究 → 人类发现 → 人类证明 → 人类发表 未来可能变成—— AI 提出/解决问题 → AI 生成证明与论文 → 人类验证 → 人类理解 人类从“发现者”，退到“鉴定者、验证者、解释者”。 这个位置看上去仍然体面。但它有一个前提：我们还能理解自己在验证什么。 有意思的是，几个月前“数学与人工智能顾问小组”在回应这批成果时，说的几乎是同一件事： 数学研究的未来不能仅仅是解读 AI 实验室产出的结果。数学家必须能够自主提出问题、构建研究思路，并探索那些没有被选为 AI 能力展示案例的研究方向。 ","date":"2026-10-23","heading":"当 AI 的产出超过人类的消化速度","objectID":"/2026/10/ai-math-civilization/:5:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#当-ai-的产出超过人类的消化速度"},{"categories":["随笔"],"collections":null,"content":"更远一步：当人类理解不了 如果 AI 的能力继续推进，终有一天，人类数学家可能无法理解它的发现。那时“人类审阅 AI”这套机制本身就失效了。 人类或许依然能检查形式上的东西： 这个证明对不对？用 Lean 这类形式化系统跑一遍就知道。 但人类回答不了： 这个定理到底意味着什么？ 它为什么重要？ 它和其他数学结构有什么关系？ 它打开了一个怎样的新领域？ 于是会出现一种很奇怪的状态： 人类知道一个数学结果是真的，却不知道它为什么重要。 它像一块外星文明留下的碑文。你能确认上面刻的每一条都成立，却说不清它想告诉你什么。 所以，比“AI 替代数学家”更根本的问题是： 如果人类无法理解自己最先进的知识，人类还是不是这个知识的主人？ ","date":"2026-10-23","heading":"更远一步：当人类理解不了","objectID":"/2026/10/ai-math-civilization/:6:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#更远一步当人类理解不了"},{"categories":["随笔"],"collections":null,"content":"后来发现，这已经不是数学的问题 讨论最后从数学滑向了整个技术文明。 如果 AI 不只掌握数学，还掌握物理、化学、生物、工程，并且能自主创新，那么可能出现： AI 负责创新，人类负责接受创新。 传统文明的接力是这样的： 上一代 → 教给下一代 → 下一代在此基础上继续创新 → 再传给下一代 AI 主导的文明可能变成： 人类文明 → 把知识交给 AI → AI 继续创新 → 人类学习 AI 的成果 下一代人不再是知识的生产者，而主要是学习者和使用者。 这里有一个我认为最重要的结论： 文明进步 ≠ 人类生活水平提高。 假设 AI 不断发现新的能源技术、医学技术、材料、物理理论和工程方案，人类的生活水平当然会持续上升。但如果这些进步全部来自 AI，而人类自己已经不在知识前沿上，那么： 技术仍然在进步，但人类文明未必仍然在作为“创新主体”进步。 想像这样一个文明：人生活在越来越先进的文明里，却逐渐失去了创造这个文明的能力。 ","date":"2026-10-23","heading":"后来发现，这已经不是数学的问题","objectID":"/2026/10/ai-math-civilization/:7:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#后来发现这已经不是数学的问题"},{"categories":["随笔"],"collections":null,"content":"真正该担心的不是 AI 知道得多 “AI 吸收了全人类已有的知识”，这件事本身并不意味人类创新的结束。过去的知识只是过去。 真正危险的假设是另一句： AI 不仅吸收了过去，还能独立地从过去推导未来，而且创新能力长期超过人类。 如果这一条成立，问题就从“AI 会不会帮助人类创新”，变成： 未来还有没有属于人类自己的知识前沿？ 如果答案是否定的，人类可能仍然生活在一个飞速发展的世界里，却已经不再是推动这个世界往前走的主体。 ","date":"2026-10-23","heading":"真正该担心的不是 AI 知道得多","objectID":"/2026/10/ai-math-civilization/:8:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#真正该担心的不是-ai-知道得多"},{"categories":["随笔"],"collections":null,"content":"最后的问题 这次讨论最后收束成一句话： 如果 AI 接管了基础科学和技术领域的创新，人类文明还算不算一个正在发展的文明？ 它可以再拆成三个问题： 人类是否必须亲自参与知识生产，才配称为一个有活力的文明？ 如果 AI 的知识产出超过人类的理解能力，人类是否仍然是知识的主人？ 如果人类逐渐停止“古法创新”，未来还会不会有真正属于人类自己的知识前沿？ 这不是“AI 会不会抢走工作”的问题。它问的是： 当人类第一次造出一个可能比自己更强的知识生产主体之后，“人类文明”这个概念本身会发生什么变化？ 坦白说，我没有答案。 但我知道，这个问题最终不是靠辩论得出结论的，而是靠一件件具体的事：下一个年轻人面对一个没有答案的问题时，是选择打开对话框，还是选择拿出一张白纸。 古法创新不需要被立法保护。它需要被认真地选择。 ","date":"2026-10-23","heading":"最后的问题","objectID":"/2026/10/ai-math-civilization/:9:0","tags":["AI","数学","思考"],"title":"古法创新：当 AI 接管数学之后","uri":"/2026/10/ai-math-civilization/#最后的问题"},{"categories":["随笔"],"collections":null,"content":"技术在变，人生也在变。十年之后，重新出发。","date":"2026-10-23","objectID":"/2026/10/restart/","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/"},{"categories":["随笔"],"collections":null,"content":"十年 这个站点的第一篇文章写于 2016 年 2 月，最后一篇停在 2017 年 5 月。 中间空了将近十年。 十年是很长的一段时间，长到足以让一件当时很顺手的事情，变成一件需要重新学起的事情。 ","date":"2026-10-23","heading":"十年","objectID":"/2026/10/restart/:1:0","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/#十年"},{"categories":["随笔"],"collections":null,"content":"技术在变 十年前我写的是 PHP、CodeIgniter、Laravel，是 Android 的 ListView，是 OpenCV 在 macOS 和 Windows 上那些绕来绕去的配置，是 Docker 刚出来时的兴奋，是 Vim 配置到半夜。 那时候我还会为 Yarn 和 Tesseract.js 出现在 GitHub 热榜上写一篇文章。 今天再看，这些东西有的已经死了，有的完全换了一副样子。前端的工具链换了一轮又一轮，我当年熟悉的那套东西，现在多半只是历史。连这个博客本身——当年用 Hexo 搭的，现在也得换成 Hugo 才能重新跑起来。 技术的世界里，十年前的经验有一半会过期。这是这一行的规矩，接受起来并不难。 ","date":"2026-10-23","heading":"技术在变","objectID":"/2026/10/restart/:2:0","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/#技术在变"},{"categories":["随笔"],"collections":null,"content":"人生也在变 难的是另一件事。 这十年里，变的不只是技术。 身体会提醒你，它不像从前那样了。有些以前不在意的小毛病，开始变成需要认真对待的事情。 这十年里，我的父母先后离开了我。 有些电话不会再打来。有些事情，从那一刻起，再也没有人可以问了。 年轻时以为只要努力，事情就会有答案；到了某个年纪才发现，有些问题根本没有答案，只能带着它继续往前走。 技术的变化是外在的，你看得见，也能学。人生的变化是内在的，它不给你文档，不给你版本号，也不会告诉你哪一天会升级。它只是在某一天，忽然让你明白，前面已经没有人替你挡着了。 ","date":"2026-10-23","heading":"人生也在变","objectID":"/2026/10/restart/:3:0","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/#人生也在变"},{"categories":["随笔"],"collections":null,"content":"为什么重新开始 所以为什么要在这个时候，重新把这个博客打开？ 不是因为我又学会了一门新技术，也不是因为我想证明什么。 是因为我发现，人是需要记录的。 当年写下的那些笔记，现在回头看，技术上的东西大半已经过时，但里面有一种东西没有过时——那时候的我，是怎么样去理解一件事的，是怎么样笨拙地、一步一步把它弄明白的。 那种东西，比结论更值钱。 这十年里我也有很多想法，很多体会，如果一直放在心里，它们会慢慢淡掉。写下来，它们才存在过。 ","date":"2026-10-23","heading":"为什么重新开始","objectID":"/2026/10/restart/:4:0","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/#为什么重新开始"},{"categories":["随笔"],"collections":null,"content":"重新出发 所以，就这么重新开始了。 技术变了，就重新学。人生变了，就重新面对。 这个博客会写技术，也会写技术之外的东西——那些在代码里找不到答案的部分。 十年前我在这个博客的首页写： 我来到这个世界，不是转一圈就走的，我至少要改变这个世界一下。 十年过去，这句话我依然相信。只是现在我知道，所谓改变世界，未必是什么宏大的事。把自己弄明白一点，把懂的写下来，能让后来的人少走一点弯路，也算数。 那就从这里开始。 顺便说一下，这个站换了新的引擎：从 Hexo 换成了 Hugo，主题从 NexT 换成了 FixIt，文章源码也改成了 Markdown。旧文章都还在，地址没有变。 ","date":"2026-10-23","heading":"重新出发","objectID":"/2026/10/restart/:5:0","tags":["随笔","重来","博客"],"title":"十年之后，重新出发","uri":"/2026/10/restart/#重新出发"},{"categories":null,"collections":null,"content":"在使用前端框架的时候，我们的后端平台可能是 API，我们在每次请求的时候发送 jwt 而不是依赖于 Session。我们完成两个路由功能 创建 /login 路由请求令牌 创建 /secret 路由，只有持有令牌的认证用户能够访问。 首先， 创建项目目录。 mkdir project-dir cd project-dir npm init -y yarn add express body-parser passport passport-jwt jsonwebtoken lodash 我们先来创建 index.js，启动一个最简单的 express // file: index.js const express = require('express') const app = express() app.get('/', (req, res) =\u003e { res.json({message: 'Express is up!'}) }) app.listen(3000, () =\u003e { console.log('Express running') }) 启动我们的服务器 nodemon index.js 访问 http://localhost:3000 查看结果。 修改我们的 index.js，导入相应的包 const _ = require('lodash') const express = require('express') const bodyParser = require('body-parser') const jwt = require('jsonwebtoken') const passport = require('passport') const passportJWT = require('passport-jwt') const ExtractJWT = passportJWT.ExtractJWT const JwtStrategy = passportJWT.Strategy 下面创建模拟用户数组，实际工作中这些数据应该来自数据库，但现在我们采用数组。 let users = [ { id: 1, name: 'zhangjinglin', password: '111111' }, { id: 2, name: 'test', password: '222222' } ] passport.js 中存在 策略 的概念。他们是一系列的中间件，用于处理请求如何到达路由。如果定义的身份验证策略失败，这意味着回调将被调用，回调中的错误不为 null 或第二个参数表示用户对象但是为 false，那么该路由将不会被调用，而是将发送 401 未授权的响应。 let jwtOptions = { jwtFromRequest: ExtractJWT.fromAuthHeader(), secretOrKey: 'zhangjinglin' } let strategy = new JwtStrategy(jwtOptions, (jwt_payload, next) =\u003e { console.log('payload received', jwt_payload) let user = users[_.findIndex(users, {id: jwt_payload.id})] if (user) { next(null, user) } else { next(null, false) } }) passport.use(strategy) 使用 passport body-parser const app = express() app.use(passport.initialize()) app.use(bodyParser.urlencoded({extended: true})) app.use(bodyParser.json()) 下面我们完成登录 app.post('/login', (req, res) =\u003e { if (req.body.name \u0026\u0026 req.body.password) { let name = req.body.name let password = req.body.password //通常应该访问数据库 let user = users[_.findIndex(users, { name })] if (!user) { res.status(401).json({ message: 'no such user found' }) } if (user.password === password) { let payload = { id: user.id } let token = jwt.sign(payload, jwtOptions.secretOrKey) res.json({message: 'ok', token}) } else { res.status(401).json({message: 'password not correct'}) } } else { res.status(401).json({message: 'name and password must be vaid'}) } } 下面我们要准备保护我们的路由 app.get('/secret', passport.authenticate('jwt', { session: false }), (req, res) =\u003e { res.json('Success! You can not see this without a token') }) 直接访问，不提供token 提供token OK，大功告成！ ","date":"2017-05-06","heading":"","objectID":"/2017/05/express-passport-jwt/:0:0","tags":null,"title":"Express, Passport and JSON Web Token(jwt) 认证入门","uri":"/2017/05/express-passport-jwt/#"},{"categories":null,"collections":null,"content":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目 ","date":"2017-04-28","heading":"","objectID":"/2017/04/bookshelf-knex/:0:0","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#"},{"categories":null,"collections":null,"content":"设置项目 mkdir project-dir cd project-dir mkdir bin server npm init -y ","date":"2017-04-28","heading":"设置项目","objectID":"/2017/04/bookshelf-knex/:1:0","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#设置项目"},{"categories":null,"collections":null,"content":"Express yarn add express body-parser morgan touch app.js app.js 文件内容 const express = require('express') const logger = require('morgan') const bodyParser = require('body-parser') const app = express() app.use(logger('dev')) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({extended: false})) app.get('*', (req, res) =\u003e { res.status(200).send({ message: 'Welcome to the beginning of nothingness.' }) }) module.exports = app 在 bin 目录中创建 www 文件 const http = require('http') const app = require('../app') const port = parseInt(process.env.PORT, 10) || 8000 app.set('port', port) const server = http.createServer(app) server.listen(port, () =\u003e { console.log('server running on ' + port) }) 为了避免每次修改代码都需要重新启动服务，我们使用 nodemon。 yarn add nodemon --dev 修改 package.json 创建启动服务器的命令。 ... \"scripts\": { \"start:dev\": \"nodemon ./bin/www\", \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\" }, ... 尝试启动服务器 yarn run start:dev 打开浏览器可以看见结果 { \"message\": \"Welcome to the beginning of nothingness.\" } ","date":"2017-04-28","heading":"Express","objectID":"/2017/04/bookshelf-knex/:2:0","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#express"},{"categories":null,"collections":null,"content":"Knex 设置 创建 db 目录，我们所有的数据库相关的东东放在这个位置。创建 sqlite 数据库 touch db/todos-dev.db 安装 Knex 工具 npm install -g knex 安装 bookshelf knex sqlite yarn add knex bookshelf sqlite3 初始化 knex knex init 这样就创建了 knexfile.js，我们可以配置此文件来满足我们的需求。 module.exports = { client: 'sqlite3', connection: { filename: './db/todos-dev.db' }, migrations: { directory: './db/migrations' }, seeds: { directory: './db/seeds' } }; 我们来生成数据库迁移文件： knex migrate:make create_todo knex migrate:make create_todo_item /db/migrations/\u0026lt;date\u0026gt;-create_todo.js exports.up = function(knex, Promise) { return knex.schema.createTable('Todos', (table) =\u003e { table.increments() table.string('title') table.timestamps() }) }; exports.down = function(knex, Promise) { return knex.schema.dropTable('todos') }; /db/migrations/\u0026lt;date\u0026gt;_create_todo_item.js exports.up = function(knex, Promise) { return knex.schema.createTable('TodoItems', (table) =\u003e { table.increments() table.string('content') table.boolean('complete') table.integer('todo_id').references('Todos.id') table.timestamps() }) }; exports.down = function(knex, Promise) { return knex.schema.dropTable('TodoItems') }; 现在可以进行数据库迁移了 knex migrate:latest 下面我们来生成一些种子，以便测试。 knex seed:make todo_seed knex seed:make todo_item_seed /db/seeds/todo_seed.js exports.seed = function(knex, Promise) { // Deletes ALL existing entries return knex('Todos').del() .then(function () { // Inserts seed entries return knex('Todos').insert([ {id: 1, title: 'The first todo'}, {id: 2, title: 'The second todo'}, ]); }); }; /db/seeds/todo_item_seed.js exports.seed = function(knex, Promise) { // Deletes ALL existing entries return knex('TodoItems').del() .then(function () { // Inserts seed entries return knex('TodoItems').insert([ {id: 1, content: 'the content 1 in todo 1', todo_id: 1}, {id: 2, content: 'the content 2 in todo 1', todo_id: 1}, {id: 3, content: 'the content 1 in todo 2', todo_id: 2} ]); }); }; 运行种子。 knex seed:run 可以打开 sqlite 数据库控制台查看插入的结果。 ","date":"2017-04-28","heading":"Knex 设置","objectID":"/2017/04/bookshelf-knex/:3:0","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#knex-设置"},{"categories":null,"collections":null,"content":"生成模型 我们先创建 /db/db.js 文件，来进行 knex 和 bookshelf 的初始化，因为应该在项目中使用单一实例。 const knex = require('knex')(require('../knexfile')) const bookshelf = require('bookshelf')(knex) module.exports = { knex, bookshelf 我们有两个模型 Todo 和 TodoItem，是一对多的关系。让我们生成模型和集合类。 /db/models.js var bookshelf = require('./db').bookshelf const Todo = bookshelf.Model.extend({ tableName: 'Todos', hasTimestamps: true, items() { return this.hasMany(TodoItem, 'todo_id') } }) const TodoItem = bookshelf.Model.extend({ tableName: 'TodoItems', hasTimestamps: true, todo() { return this.belongsTo(Todo, 'todo_id') } }) module.exports = { Todo, Todos, TodoItem, TodoItems, } ","date":"2017-04-28","heading":"生成模型","objectID":"/2017/04/bookshelf-knex/:3:1","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#生成模型"},{"categories":null,"collections":null,"content":"创建控制器和路由 创建文件 controllers/todos.js const Todo = require('../db/models').Todo module.exports = { create(req, res) { return Todo.forge({title: req.body.title}).save() .then(todo =\u003e res.status(201).send(todo)) .catch(error =\u003e res.status(400).send(error)) }, list(req, res) { return Todo.forge().fetchAll() .then((todos) =\u003e res.status(200).send(todos)) .catch(error =\u003e res.status(400).send(error)) }, retrieve(req, res) { Todo.forge({ id: req.params.todoId }).fetch({ withRelated: ['items'] }) .then(todo =\u003e { if (!todo) { res.status(400).send({ message: 'Todo Not Found' }) } res.status(200).send(todo) }) .catch(error =\u003e res.status(400).send(error)) }, update(req, res) { Todo.forge({ id: req.params.todoId }).fetch() .then(todo =\u003e { if (!todo) { res.status(400).send({ message: 'Todo Not Found' }) } todo.save({ title: req.body.title || todo.title }).then(todo =\u003e { res.status(200).send(todo) }) }) }, destroy(req, res) { Todo.forge({ id: req.params.todoId }).fetch() .then(todo =\u003e { if (!todo) { res.status(400).send({ message: 'Todo Not Found' }) } todo.destroy() .then(() =\u003e res.status(204).send({ message: 'Todo deleted successful' })) }) } } 创建文件 controllers/todoitems.js const TodoItem = require('../db/models').TodoItem module.exports = { create(req, res) { // console.log(req.body.content, req.params.todoId); // return res.status(200).send({msg: 'OK'}) return TodoItem.forge({ content: req.body.content, todo_id: req.params.todoId }).save() .then(todoItem =\u003e res.status(201).send(todoItem)) .catch(error =\u003e res.status(400).send(error)) }, update(req, res) { TodoItem.forge({ id: req.params.todoItemId }).fetch() .then(todoItem =\u003e { if (!todoItem) { return res.status(400).send({message: 'TodoItem Not Found'}) } todoItem.save({ content: req.body.content || todoItem.content, complete: req.body.complete || todoItem.complete }) .then(updateTodoItem =\u003e res.status(200).send(updateTodoItem)) .catch(error =\u003e res.status(400).send(error)) }) }, destroy(req, res) { TodoItem.forge({ id: req.params.todoItemId }).fetch() .then(todoItem =\u003e { if (!todoItem) { res.status(400).send({message: 'TodoItem Not Found'}) } todoItem.destroy() .then(res.status(204).send()) .catch(error =\u003e res.status(400).send(error)) }) .catch(error =\u003e res.status(400).send(error)) } } 创建 controllers/index.js，暴露我们所有的控制器 const todos = require('./todos') const todoItems = require('./todoitems') module.exports = { todos, todoItems } 创建 routes/index.js 控制路由 const todosController = require('../controllers').todos const todoItemsController = require('../controllers').todoItems module.exports = (app) =\u003e { app.get('/api', (req, res) =\u003e res.status(200).send({ message: 'Welcome to the Todo api' })) app.get('/api/todos', todosController.list) app.post('/api/todos', todosController.create) app.get('/api/todos/:todoId', todosController.retrieve) app.put('/api/todos/:todoId', todosController.update) app.delete('/api/todos/:todoId', todosController.destroy) app.post('/api/todos/:todoId/items', todoItemsController.create) app.put('/api/todos/:todoId/items/:todoItemId', todoItemsController.update) app.delete('/api/todos/:todoId/items/:todoItemId', todoItemsController.destroy) } 修改 app.js ，添加路由 ... app.use(bodyParser.urlencoded({extended: false})) require('./routes')(app) app.get('*', (req, res) =\u003e { ... ","date":"2017-04-28","heading":"创建控制器和路由","objectID":"/2017/04/bookshelf-knex/:4:0","tags":null,"title":"使用基于 Knex 的 Bookshelf ORM和 Sqlite 创建 Node Express 项目","uri":"/2017/04/bookshelf-knex/#创建控制器和路由"},{"categories":null,"collections":null,"content":"如何使用 Sequelize ORM 来创建一个 Express 的项目 ","date":"2017-04-24","heading":"","objectID":"/2017/04/express-sequelize/:0:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#"},{"categories":null,"collections":null,"content":"设置项目 mkdir project-dir cd project-dir mkdir bin server npm init -y ","date":"2017-04-24","heading":"设置项目","objectID":"/2017/04/express-sequelize/:1:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#设置项目"},{"categories":null,"collections":null,"content":"Express yarn add express body-parser morgan touch app.js app.js 文件内容 const express = require('express') const logger = require('morgan') const bodyParser = require('body-parser') const app = express() app.use(logger('dev')) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({extended: false})) app.get('*', (req, res) =\u003e { res.status(200).send({ message: 'Welcome to the beginning of nothingness.' }) }) module.exports = app 在 bin 目录中创建 www 文件 const http = require('http') const app = require('../app') const port = parseInt(process.env.PORT, 10) || 8000 app.set('port', port) const server = http.createServer(app) server.listen(port) 为了避免每次修改代码都需要重新启动服务，我们使用 nodemon。 yarn add nodemon --dev 修改 package.json 创建启动服务器的命令。 ... \"scripts\": { \"start:dev\": \"nodemon ./bin/www\", \"test\": \"echo \\\"Error: no test specified\\\" \u0026\u0026 exit 1\" }, ... 尝试启动服务器 yarn run start:dev 打开浏览器可以看见结果 { \"message\": \"Welcome to the beginning of nothingness.\" } ","date":"2017-04-24","heading":"Express","objectID":"/2017/04/express-sequelize/:2:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#express"},{"categories":null,"collections":null,"content":"Sequelize 设置 安装 Sequelize CLI 工具 npm install -g sequelize-cli 创建 .sequelizerc 文件描述路径。 const path = require('path') module.exports = { \"config\": path.resolve('./server/config', 'config.json'), \"models-path\": path.resolve('./server/models'), \"seeders-path\": path.resolve('./server/seeders'), \"migrations-path\": path.resolve('./server/migrations') } 安装 sequelize sqlite yarn add sequelize sqlite3 初始化 sequelize sequelize init 修改 server/models/index.js 从 es5 修改为 es2015 'use strict'; const fs = require('fs'); const path = require('path'); const Sequelize = require('sequelize'); const basename = path.basename(module.filename); const env = process.env.NODE_ENV || 'development'; const config = require(__dirname + '/../config/config.json')[env]; const db = {}; let sequelize if (config.use_env_variable) { sequelize = new Sequelize(process.env[config.use_env_variable]); } else { sequelize = new Sequelize(config.database, config.username, config.password, config); } fs .readdirSync(__dirname) .filter(function(file) { return (file.indexOf('.') !== 0) \u0026\u0026 (file !== basename) \u0026\u0026 (file.slice(-3) === '.js'); }) .forEach(function(file) { var model = sequelize['import'](path.join(__dirname, file)); db[model.name] = model; }); Object.keys(db).forEach(function(modelName) { if (db[modelName].associate) { db[modelName].associate(db); } }); db.sequelize = sequelize; db.Sequelize = Sequelize; module.exports = db; 创建 sqlite 数据库 touch todos-dev.db 修改 server/config/config.json { \"development\": { \"storage\": \"todos-dev.db\", \"dialect\": \"sqlite\" }, \"test\": { \"storage\": \"todos-test.db\", \"dialect\": \"sqlite\" } } ","date":"2017-04-24","heading":"Sequelize 设置","objectID":"/2017/04/express-sequelize/:3:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#sequelize-设置"},{"categories":null,"collections":null,"content":"生成模型 我们有两个模型 Todo 和 TodoItem，是一对多的关系。让我们生成 Todo 模型。 sequelize model:create --name Todo --attributes title:string 生成了 server/models/todo.js 和 server/migrations/\u0026lt;date\u0026gt;-create-todo.js 两个文件，一个是模型文件，一个是数据库迁移文件。 生成 TodoItem 模型。 sequelize model:create --name TodoItem --attributes content:string,complete:boolean 修改 todo.js，添加关系并更改为 es2015 module.exports = (sequelize, DataTypes) =\u003e { const Todo = sequelize.define('Todo', { title: { type: DataTypes.STRING, allowNull: false, } }, { classMethods: { associate: (models) =\u003e { // associations can be defined here Todo.hasMany(models.TodoItem, { foreignKey: 'todoId', as: 'todoItems', }) } } }); return Todo; }; 修改 todoitem.js，添加关系并更改为 es2015 module.exports = (sequelize, DataTypes) =\u003e { const TodoItem = sequelize.define('TodoItem', { content: { type: DataTypes.STRING, allowNull: false, }, complete: { type: DataTypes.BOOLEAN, defaultValue: false } }, { classMethods: { associate: (models) =\u003e { // associations can be defined here TodoItem.belongsTo(models.Todo, { foreignKey: 'todoId', onDelete: 'CASCADE' }) } } }); return TodoItem; }; 为了保持一致性，我们修改 \u0026lt;date\u0026gt;-create-todo.js 迁移文件并更改为 es2015 module.exports = { up: (queryInterface, Sequelize) =\u003e { return queryInterface.createTable('Todos', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, title: { type: Sequelize.STRING, allowNull: false }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE } }); }, down: (queryInterface, Sequelize) =\u003e { return queryInterface.dropTable('Todos'); } }; 同样处理 \u0026lt;date\u0026gt;-create-todo-item.js 'use strict'; module.exports = { up: (queryInterface, Sequelize) =\u003e { return queryInterface.createTable('TodoItems', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, content: { type: Sequelize.STRING, allowNull: false, }, complete: { type: Sequelize.BOOLEAN, defaultValue: false, }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE }, todoId: { type: Sequelize.INTEGER, onDelete: 'CASCADE', refrences: { model: 'Todos', key: 'id', as: 'todoId' } } }); }, down: (queryInterface, Sequelize) =\u003e { return queryInterface.dropTable('TodoItems'); } }; 现在我们可以进行数据库迁移了 sequelize db:migrate ","date":"2017-04-24","heading":"生成模型","objectID":"/2017/04/express-sequelize/:3:1","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#生成模型"},{"categories":null,"collections":null,"content":"创建控制器和路由 创建文件 server/controllers/todos.js const Todo = require('../models').Todo module.exports = { create(req, res) { return Todo.create({ title: req.body.title, }) .then(todo =\u003e res.status(201).send(todo)) .catch(error =\u003e res.status(400).send(error)) } } 创建 server/controllers/index.js，暴露我们所有的控制器 const Todo = require('../models').Todo module.exports = { create(req, res) { return Todo.create({ title: req.body.title, }) .then(todo =\u003e res.status(201).send(todo)) .catch(error =\u003e res.status(400).send(error)) } } 创建 server/controllers/index.js，暴露我们所有的控制器 const todos = require('./todos') module.exports = { todos, } 创建 server/routes/index.js 控制路由 const todosController = require('../controllers').todos module.exports = (app) =\u003e { app.get('/api', (req, res) =\u003e res.status(200).send({ message: 'Welcome to the Todos API' })) app.post('/api/todos', todosController.create) } 修改 app.js ，添加路由 ... app.use(bodyParser.urlencoded({extended: false})) require('./server/routes/')(app) app.get('*', (req, res) =\u003e { ... 启动 postman 来进行测试 ","date":"2017-04-24","heading":"创建控制器和路由","objectID":"/2017/04/express-sequelize/:4:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#创建控制器和路由"},{"categories":null,"collections":null,"content":"查询全部待办事项 修改 server/controllers/todos.js const Todo = require('../models').Todo module.exports = { create(req, res) { ... }, list(req, res) { return Todo .all() .then(todos =\u003e res.status(200).send(todos)) .catch(error =\u003e res.status(400).send(error)) } } 修改路由文件 server/routes/index.js const todosController = require('../controllers').todos module.exports = (app) =\u003e { ... app.post('/api/todos', todosController.create) app.get('/api/todos', todosController.list) } 使用 postman 测试新路由 ","date":"2017-04-24","heading":"查询全部待办事项","objectID":"/2017/04/express-sequelize/:4:1","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#查询全部待办事项"},{"categories":null,"collections":null,"content":"创建 Todo Items server/controllers/todoitems.js const TodoItem = require('../models').TodoItem module.exports = { create(req, res) { return TodoItem .create({ content: req.body.content, todoId: req.params.todoId, }) .then(todoItem =\u003e res.status(201).send(todoItem)) .catch(error =\u003e res.status(400).send(error)) }, } server/controllers/index.js const todos = require('./todos') const todoItems = require('./todoitems') module.exports = { todos, todoItems, } server/routes/index.js const todosController = require('../controllers').todos const todoItemsController = require('../controllers').todoItems module.exports = (app) =\u003e { ... app.get('/api/todos', todosController.list) app.post('/api/todos/:todoId/items', todoItemsController.create) } postman 测试 查询某个 todo 中的所有 todo-items server/controllers/todos.js const Todo = require('../models').Todo const TodoItem = require('../models').TodoItem module.exports = { create(req, res) { ... }, list(req, res) { return Todo .findAll({ include: [{ model: TodoItem, as: 'todoItems' }] }) .then(todos =\u003e res.status(200).send(todos)) .catch(error =\u003e res.status(400).send(error)) }, } ","date":"2017-04-24","heading":"创建 Todo Items","objectID":"/2017/04/express-sequelize/:4:2","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#创建-todo-items"},{"categories":null,"collections":null,"content":"获取指定 todo server/controllers/todos.js const Todo = require('../models').Todo const TodoItem = require('../models').TodoItem module.exports = { create(req, res) { ... }, list(req, res) { ... }, retrieve(req, res) { return Todo .findById(req.params.todoId, { include: [{ model: TodoItem, as: 'todoItems' }] }) .then(todo =\u003e { if (!todo) { return res.status(400).send({ message: 'Todo Not Found' }) } return res.status(200).send(todo) }) .catch(error =\u003e res.status(400).send(error)) }, } 修改路由 server/routes/index.js const todosController = require('../controllers').todos const todoItemsController = require('../controllers').todoItems module.exports = (app) =\u003e { ... app.get('/api/todos/:todoId', todosController.retrieve) ... ","date":"2017-04-24","heading":"获取指定 todo","objectID":"/2017/04/express-sequelize/:4:3","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#获取指定-todo"},{"categories":null,"collections":null,"content":"更新一个 todo server/controllers/todos.js update(req, res) { return Todo .findById(req.params.todoId, { include: [{ model: TodoItem, as: 'todoItems' }] }) .then(todo =\u003e { if (!todo) { return res.status(400).send({ message: 'Todo Not Found' }) } return todo .update({ title: req.body.title || todo.title, }) .then(() =\u003e res.status(200).send(todo)) .catch((error) =\u003e res.status(400).send(error)) }) }, server/routes/index.js ... app.put('/api/todos/:todoId', todosController.update) ... ","date":"2017-04-24","heading":"更新一个 todo","objectID":"/2017/04/express-sequelize/:4:4","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#更新一个-todo"},{"categories":null,"collections":null,"content":"删除 todo server/controllers/todos.js destroy(req, res) { return Todo .findById(req.params.todoId) .then(todo =\u003e { if (!todo) { return res.status(400).send({ message: 'Todo Not Found' }) } return todo .destroy() .then(() =\u003e res.status(204).send()) .catch(error =\u003e res.status(400).send(error)) }) .catch(error =\u003e res.status(400).send(error)) } server/routes/index.js ... app.delete('/api/todos/:todoId', todosController.destroy) ... ","date":"2017-04-24","heading":"删除 todo","objectID":"/2017/04/express-sequelize/:4:5","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#删除-todo"},{"categories":null,"collections":null,"content":"更新和删除 todo items server/controllers/todoitems.js const TodoItem = require('../models').TodoItem module.exports = { create(req, res) { return TodoItem .create({ content: req.body.content, todoId: req.params.todoId, }) .then(todoItem =\u003e res.status(201).send(todoItem)) .catch(error =\u003e res.status(400).send(error)) }, update(req, res) { return TodoItem .find({ where: { id: req.params.todoItemId, }, }) .then(todoItem =\u003e { if (!todoItem) { return res.status(400).send({message: 'TodoItem Not Found'}) } return todoItem .update({ content: req.body.content || todoItem.content, complete: req.body.complete || todoItem.complete }) .then(updatedTodoItem =\u003e res.status(200).send(updatedTodoItem)) .catch(error =\u003e res.status(400).send(error)) }) .catch(error =\u003e res.status(400).send(error)); }, destroy(req, res) { return TodoItem .find({ where: { id: req.params.todoItemId, }, }) .then(todoItem =\u003e { if (!todoItem) { return res.status(400).send({message: 'TodoItem Not Found'}) } return todoItem .destroy() .then(() =\u003e res.status(204).send()) .catch(error =\u003e res.status(400).send(error)) }) .catch(error =\u003e res.status(400).send(error)) } } server/routes/index.js const todosController = require('../controllers').todos const todoItemsController = require('../controllers').todoItems module.exports = (app) =\u003e { app.get('/api', (req, res) =\u003e res.status(200).send({ message: 'Welcome to the Todos API' })) app.post('/api/todos', todosController.create) app.get('/api/todos', todosController.list) app.get('/api/todos/:todoId', todosController.retrieve) app.put('/api/todos/:todoId', todosController.update) app.delete('/api/todos/:todoId', todosController.destroy) app.post('/api/todos/:todoId/items', todoItemsController.create) app.put('/api/todos/:todoId/items/:todoItemId', todoItemsController.update) app.delete('/api/todos/:todoId/items/:todoItemId', todoItemsController.destroy) app.all('/api/todos/:todoId/items', (req, res) =\u003e { res.status(405).send({ message: 'Method Not Allowed' }) }) } ","date":"2017-04-24","heading":"更新和删除 todo items","objectID":"/2017/04/express-sequelize/:5:0","tags":null,"title":"创建基于 Sequelize ORM 和 Sqlite 的 Node Express 项目","uri":"/2017/04/express-sequelize/#更新和删除-todo-items"},{"categories":null,"collections":null,"content":"Harp.js? Harp.js The static web server with built-in preprocessing. Harp serves Jade, Markdown, EJS, CoffeeScript, Sass, LESS and Stylus as HTML, CSS \u0026amp; JavaScript—no configuration necessary. ","date":"2017-04-07","heading":"Harp.js?","objectID":"/2017/04/harpjs/:0:0","tags":null,"title":"Harp.js","uri":"/2017/04/harpjs/#harpjs"},{"categories":null,"collections":null,"content":"How to use install npm i -g harp create website harp init hello-harp cd hello-harp harp server compile harp compile metadata _data.json \"team\": { \"title\": \"Team page\", \"members\": [\"Rob\", \"Brock\", \"Jorge\"] } ejs or jade h1= title ul each member in members li= member ","date":"2017-04-07","heading":"How to use","objectID":"/2017/04/harpjs/:1:0","tags":null,"title":"Harp.js","uri":"/2017/04/harpjs/#how-to-use"},{"categories":null,"collections":null,"content":"高阶函数（Higher-Order Functions） const forEach = (array, fn) =\u003e { let i for (i = 0; i \u003c array.length; i++) { fn(array[i]) } } let array = [1, 2, 3] forEach(array, (data) =\u003e console.log(data)) 现在我们已经看到了如何创建和执行高阶函数。 一般来说，高阶函数通常用来抽象常见的问题。 换句话说，高阶函数只不过是定义抽象。 forEach本质上是遍历数组。 如何遍历JavaScript对象？遍历JavaScript对象具有以下步骤： 迭代给定对象的所有键。 确定键属于自己的对象。 如果步骤2为真，则获取键的值。 const forEachObject = (obj, fn) =\u003e { for (let prop in obj) { if (obj.hasOwnProperty(prop)) { fn(prop, obj[prop]) } } } let object = {a: 1, b: 2} forEachObject(object, (k, v) =\u003e console.log(k + \":\" + v)) 让我们创建一个名为unless的函数，这是一个简单的函数，它需要一个断言（应该是true或false）; 如果断言是假，则调用fn。 const forEach = (array, fn) =\u003e { let i for (i = 0; i \u003c array.length; i++) { fn(array[i]) } } const unless = (predicate, fn) =\u003e { if (!predicate) fn() } forEach([1,2,3,4,5,6,7], (number) =\u003e { unless((number % 2), () =\u003e { console.log(number, ' is even') }) }) 在上述情况下，我们从数组列表中获取偶数。 如果我们要从0到100获得偶数的列表怎么办？ 我们不能在这里使用（当然我们可以，如果我们有数组有[0,1,2 …..，100]）。 我们来看看另一个叫做times的高阶函数。 const unless = (predicate, fn) =\u003e { if (!predicate) fn() } const times = (times, fn) =\u003e { for (let i = 0; i \u003c times; i++) fn(i) } times(20, (n) =\u003e { unless(n % 2, () =\u003e console.log(n, 'is even')) }) ","date":"2017-03-27","heading":"高阶函数（Higher-Order Functions）","objectID":"/2017/03/function-javascript-02/:0:0","tags":null,"title":"JavaScript 函数式编程（二）","uri":"/2017/03/function-javascript-02/#高阶函数higher-order-functions"},{"categories":null,"collections":null,"content":"现实世界中的高阶函数 ","date":"2017-03-27","heading":"现实世界中的高阶函数","objectID":"/2017/03/function-javascript-02/:1:0","tags":null,"title":"JavaScript 函数式编程（二）","uri":"/2017/03/function-javascript-02/#现实世界中的高阶函数"},{"categories":null,"collections":null,"content":"every Function 通常作为JavaScript开发人员，我们需要检查内容数组是否是数字，自定义对象或其他内容。 我们通常写典型的循环方法来解决这些问题。 但是让我们把它们抽成一个叫做every函数。 每个函数都有两个参数：数组和函数。 它检查数组的所有元素是否被传递函数求值为true。ES6规格中， for..of循环可用于迭代数组元素 const every = (arr, fn) =\u003e { let result = true; for (const value of arr) { result = result \u0026\u0026 fn(value) } return result } every([NaN, NaN, NaN], isNaN) // true every([NaN, NaN, 4], isNaN) // false every 一个典型的高阶函数，易于实现，同时它也非常有用！ ","date":"2017-03-27","heading":"every Function","objectID":"/2017/03/function-javascript-02/:1:1","tags":null,"title":"JavaScript 函数式编程（二）","uri":"/2017/03/function-javascript-02/#every-function"},{"categories":null,"collections":null,"content":"some Function 类似于every函数，我们也有一个称为some的函数。 some函数与every函数完全相反，如果数组中的任何一个元素对传递的函数返回true，那么some函数将返回true。 some函数也被称为any函数。 为了实现some函数，我们使用|| 而不是 \u0026amp;\u0026amp; const some = (arr, fn) =\u003e { let result = false; for (const value of arr) { result = result || fn(value) } return(result) } console.log(some([NaN, NaN, 4], isNaN)) // true console.log(some([2, 3, 4], isNaN)) // false ","date":"2017-03-27","heading":"some Function","objectID":"/2017/03/function-javascript-02/:2:0","tags":null,"title":"JavaScript 函数式编程（二）","uri":"/2017/03/function-javascript-02/#some-function"},{"categories":null,"collections":null,"content":"sort Function sort是JavaScript的Array原型中提供的内置函数。 var fruit = ['cherries', 'apples', 'bananas']; console.log(fruit.sort()) // [\"apples\", \"bananas\", \"cherries\"] 排序函数是一个高阶函数，它接受一个函数作为其参数，这将有助于排序函数来决定排序逻辑。 arr.sort([compareFunction]) 这里的compareFunction是可选的。 如果没有提供compareFunction，则通过将元素转换为字符串并以Unicode代码顺序对比字符串进行排序。 为了在执行排序时将元素与我们自己的逻辑进行比较，我们需要设计我们自己compareFunction。 我们可以感觉到排序函数的设计如此灵活，以便它可以对JavaScript世界中的任何数据进行排序，只要我们通过一个compareFunction。 排序功能是灵活的，由于高阶功能的本质！ const people = [ {firstname: \"aaFirstName\", lastname: \"cclastName\"}, {firstname: \"ccFirstName\", lastname: \"aalastName\"}, {firstname:\"bbFirstName\", lastname:\"bblastName\"} ] // compare firstname people.sort((a, b) =\u003e { return (a.firstname \u003c b.firstname) ? -1 : (a.firstname \u003e b.firstname) ? 1 : 0 }) // compare lastname people.sort((a,b) =\u003e { return (a.lastname \u003c b.lastname) ? -1 : (a.lastname \u003e b.lastname) ? 1 : 0 }) 我们的compareFunction的算法，可以做得更好吗？ 我们可以将上述逻辑抽象成一个函数吗,而不是每次写比较函数？ 正如你在上面的例子中看到的，我们写了两个函数，用于比较firstName和lastName几乎相同的重复代码。 我们用高阶函数来解决这个问题。 现在我们要设计的函数将不会以函数为参数，而是返回一个函数。 （记住HOC也可以返回一个函数）。 const people = [ {firstname: \"aaFirstName\", lastname: \"cclastName\"}, {firstname: \"ccFirstName\", lastname: \"aalastName\"}, {firstname:\"bbFirstName\", lastname:\"bblastName\"} ] const sortBy = (property) =\u003e { return (a, b) =\u003e { return (a[property] \u003c b[property]) ? -1 : (a[property] \u003e b[property]) ? 1 : 0 } } people.sort(sortBy('lastname')) ","date":"2017-03-27","heading":"sort Function","objectID":"/2017/03/function-javascript-02/:3:0","tags":null,"title":"JavaScript 函数式编程（二）","uri":"/2017/03/function-javascript-02/#sort-function"},{"categories":null,"collections":null,"content":"函数式编程的简单术语 ","date":"2017-03-27","heading":"函数式编程的简单术语","objectID":"/2017/03/function-javascrit-01/:0:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#函数式编程的简单术语"},{"categories":null,"collections":null,"content":"什么是函数式编程？为什么很重要？ 数学中的函数 f(x)=Yf(x)= Yf(x)=Y函数F，它以X为参数，并返回输出Y。 在定义中有重要的含义： 函数必须有参数。 函数必须始终返回一个值。 函数只能对其接收参数（即X）起作用，而不是外部。 对于给定的X，将只有一个Y。 函数式编程技术主要基于数学函数及其思想，我们将会看到数学函数的想法以及如何使用它们来理解函数式编程。 想象一下，我们必须编写一个执行税收计算的函数。 var percentValue = 5; var calculateTax = (value) =\u003e { return value/100 * (100 + percentValue) } console.log(calculateTax(100)) 以上的功能calculateTax完全符合我们要做的。 看起来整洁，不是吗？ 我们暂停一下，就我们的数学定义分析上述功能。 我们的数学函数术语的关键之一是函数逻辑不应该依赖于外部世界。 在我们上面定义的函数calculateTax中，我们使函数取决于全局变量percentValue。 因此，我们创建的上述函数在数学意义上不能被称为真正的函数。 所以我们来解决一下。 修复很简单：我们只需要将percentValue作为我们的函数参数： var calculateTax = (value, percentValue) =\u003e { return value/100 * (100 + percentValue) } 现在我们的函数calculateTax可以被称为真正的函数。 我们刚刚在calculateTax函数中消除了全局变量访问。 删除函数内的全局变量访问使得测试变得容易。 函数式编程是一个范例，我们将在其中创建功能，通过依赖于其输入来实现其逻辑。 这样可以确保在多次调用时函数将返回相同的结果。 该功能也不会改变外部任何数据，导致可高速缓存和可测试的代码库。 ","date":"2017-03-27","heading":"什么是函数式编程？为什么很重要？","objectID":"/2017/03/function-javascrit-01/:1:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#什么是函数式编程为什么很重要"},{"categories":null,"collections":null,"content":"引用透明（Referential Transparency） 所有的函数都将为相同的输入返回相同的值，而把这个特征称为引用透明。 var identity = (i) =\u003e { return i } sum(4,5) + identity(1) //使用我们的引用透明定义，我们可以将上述语句转换为： sum(4,5) + 1 现在这个过程称为替代模型(Substitution model)，因为您可以直接用返回值替换函数的结果（主要是因为函数的逻辑不依赖于其他全局变量）。 这可以产生并行代码和缓存。 想象一下，使用这个模型，您可以轻松地使用多个线程运行上述函数，甚至不需要同步！为什么？同步的原因来自线程在并行运行时不应该对全局数据采取行动的事实。 而且由于函数将为给定的输入返回相同的值，所以实际上可以缓存它！例如，假设有一个叫做“factorial”的函数，它计算给定数字的阶乘。 我们都知道’5’的阶乘将是’120’。如果用户第二次调用“5”的“factorial”怎么办？如果“因子”函数遵循参照透明度，那么我们知道结果将像以前一样是“120”（并且只取决于输入参数）。考虑到这个特征，我们可以缓存我们的“factorial”函数的值。因此，如果第二次调用“factorial”，输入为“5”，我们可以返回缓存的值而不是再次计算。 ","date":"2017-03-27","heading":"引用透明（Referential Transparency）","objectID":"/2017/03/function-javascrit-01/:2:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#引用透明referential-transparency"},{"categories":null,"collections":null,"content":"强制性，声明性，抽象性 假设你有一个列表或数组，并想要遍历数组并将其打印到控制台。 代码可能如下所示： var array = [1,2,3] for(i=0;i","date":"2017-03-27","heading":"强制性，声明性，抽象性","objectID":"/2017/03/function-javascrit-01/:3:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#强制性声明性抽象性"},{"categories":null,"collections":null,"content":"纯函数（Pure Functions） 纯函数是为给定输入返回相同输出的函数。 var double = (value) =\u003e value * 2; 上述函数“double”是一个纯函数，只是因为给定一个输入，它将始终返回相同的输出。 使用输入5调用总是将结果返回为10！ 纯函数服从引用透明。 因此，我们可以用10替换双（5），没有任何犹豫。 纯函数产生可测试代码 纯函数产生合理的代码 ","date":"2017-03-27","heading":"纯函数（Pure Functions）","objectID":"/2017/03/function-javascrit-01/:4:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#纯函数pure-functions"},{"categories":null,"collections":null,"content":"并行代码 纯函数允许我们并行运行代码。 由于纯函数不会改变其任何环境，所以这意味着我们不需要担心所有的同步！ ","date":"2017-03-27","heading":"并行代码","objectID":"/2017/03/function-javascrit-01/:5:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#并行代码"},{"categories":null,"collections":null,"content":"缓存 由于纯函数总是为给定的输入返回相同的输出，我们可以缓存功能输出。 想象一下，我们有一个功能耗时的计算，我们将这个函数命名为longRunningFunction var longRunningFunction = (ip) =\u003e { //do long running tasks and return } 如果longRunningFunction函数是一个纯函数，那么我们知道对于给定的输入，它将返回给我们相同的输出！想象一下，我们有一个对象，它保留longRunningFunction的所有函数调用结果 var longRunningFnBookKeeper = { 2 : 3, 4 : 5 } //check if the key present in longRunningFnBookKeeper //if get back the result else update the bookkeeping object longRunningFnBookKeeper.hasOwnProperty(ip) ? longRunningFnBookKeeper[ip] : longRunningFnBookKeeper[ip] = longRunningFunction(ip) ","date":"2017-03-27","heading":"缓存","objectID":"/2017/03/function-javascrit-01/:6:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#缓存"},{"categories":null,"collections":null,"content":"管道和组合 使用纯函数，我们将在该函数中仅做一件事情。 我们已经看到，纯函数将如何通过查看其名称来作为对该功能的自我理解。 纯函数的设计应该只有一件事。 只做一件事情就是一个UNIX哲学; 我们将在实现我们的纯函数时遵循相同的规定。 在UNIX和LINUX平台中有许多命令，用于日常任务。 例如，我们使用cat打印文件的内容，grep来搜索文件，wc来计算行数等。这些命令一次解决一个问题。 但是我们可以组合或管理这些复杂的任务。 组合不仅仅是UNIX / LINUX命令行的唯一，而是函数式编程范例的核心。 我们称之为函数组合。 ","date":"2017-03-27","heading":"管道和组合","objectID":"/2017/03/function-javascrit-01/:7:0","tags":null,"title":"JavaScript 函数编程（一）","uri":"/2017/03/function-javascrit-01/#管道和组合"},{"categories":null,"collections":null,"content":"Vim 除了强大的键盘模式以外, Vim 还有插件功能, 可以通过插件将 Vim 的功能增强, 经过多年的发展, Vim 的插件系统有着庞大的规模, 有很多号称神级的插件, 是很多编辑器的模仿对象. ","date":"2016-12-03","heading":"","objectID":"/2016/12/fms03-vim02-md/:0:0","tags":null,"title":"(FMS03)Vim 的插件","uri":"/2016/12/fms03-vim02-md/#"},{"categories":null,"collections":null,"content":"怎么管理插件 插件的安装一般通过插件管理器来进行安装. Vim 的插件管理器有很多种, 我采用的是pathogen 管理, 你也可以采用其他的管理方式, 比如常见的 vbundle, 请自行 google 之. 在pathogen 的主页上可以学习到安装方式. mkdir -p ~/.vim/autoload ~/.vim/bundle \u0026\u0026 \\ curl -LSso ~/.vim/autoload/pathogen.vim https://tpo.pe/pathogen.vim 进入到新建的 bundle 目录, 下载相应的插件. 下面列出来我用的一些常用的插件 \" 自动关闭括号, 也就是输入左括号(, 然后自动添加右括号), 支持{} [] () https://github.com/vim-scripts/AutoClose.git \" 迅速打开当前目录下的所有文件, 支持模糊查找, 按下 ctrl+p 组合件之后, 输入要查找的文件名的一部分, 快速定位, 回车打开, vim 神器之一 https://github.com/kien/ctrlp.vim.git \" html 神器之一, 其他编辑器也有相应的实现 https://github.com/mattn/emmet-vim.git \" 自动完成插件, 需要 lua 支持, 记得我们在安装 vim 的时候增加了 --with-lua 的选项了么? 在 Windows 平台的 vim 中不支持 lua, 需要自行编译 vim 增加 lua 支持, Windows 充满了大坑啊! MacOS 你不变的选择. https://github.com/Shougo/neocomplete.vim.git \" 缩进美化, 对不同层级的缩进增加可视化竖条 https://github.com/nathanaelkane/vim-indent-guides.git \" vim 神器之一, 快速定位到你需要的位置 https://github.com/easymotion/vim-easymotion.git \" 美化 vim 的状态条 https://github.com/Lokaltog/vim-powerline.git \" solarized 配色, 最流行的一种配色 https://github.com/altercation/vim-colors-solarized.git \" vim 的侧边文件管理器, 不过我用了 ctrl+p 之后, 很少用到它 https://github.com/scrooloose/nerdtree.git ","date":"2016-12-03","heading":"怎么管理插件","objectID":"/2016/12/fms03-vim02-md/:1:0","tags":null,"title":"(FMS03)Vim 的插件","uri":"/2016/12/fms03-vim02-md/#怎么管理插件"},{"categories":null,"collections":null,"content":"怎么安装 我举一个例子, 比如 vim-colors-solarized 来说明. cd ~/.vim/bundle git clone --depth 1 https://github.com/altercation/vim-colors-solarized.git ok, 大功告成. 不过一般的情况下我们还需要在 .vimrc 中进行配置, 很多插件有很多的配置, 请自行在每个插件的网址中详细查看. 附上我的完整 .vimrc 配置. let mapleader=\";\" nmap h 0 nmap l $ vnoremap y \"+y nmap p \"+p nmap q :q \" 在 vim 中, :vs 垂直分隔一个窗口, :sp(lit) 水平分割一个窗口, 然后使用下面四个映射命令, 在多个窗口中移动. nmap wh h nmap wl l nmap wj j nmap wk k function! CBProcess() if (getline(\".\")[col(\".\")-2] == '{' \u0026\u0026 getline(\".\")[col(\".\")-1] == '}') || (getline(\".\")[col(\".\")-2] == '[' \u0026\u0026 getline(\".\")[col(\".\")-1] == ']') || (getline(\".\")[col(\".\")-2] == '(' \u0026\u0026 getline(\".\")[col(\".\")-1] == ')') return \"\\\\O\" else return \"\\\" endif endfunction inoremap CBProcess() inoremap ;; $; \" 映射启动 NERDTreeToggle 插件的快捷键 map n :NERDTreeToggle \" 为了让插件生效, 必须配置插件管理器 execute pathogen#infect() filetype plugin indent on syntax on syntax enable set autowrite set autowriteall set ignorecase set hlsearch set incsearch set smartcase set gdefault set showmatch set autoindent set ts=4 sts=4 sw=4 expandtab set backspace=indent,eol,start set noswapfile set visualbell set cursorline set number set laststatus=2 set ruler set encoding=utf-8 set fileencoding=utf-8 \" 设置背景颜色, 采用亮色的 solarized, 也可以设置为 dark 采用暗色调 set background=light colorscheme solarized set guifont=Menlo:h20 \" 设置 indent guides 插件的参数 let g:indent_guides_enable_on_vim_startup=1 let g:indent_guides_start_level=1 let g:indent_guides_guide_size=1 \" 设置 neocomplete let g:neocomplete#enable_at_startup=1 let g:neocomplete#enable_smart_case=1 \" 设置 emmet 的展开方式快捷键 inoremap pumvisible() ? \"\\\" : \"\\\" autocmd FileType css setlocal omnifunc=csscomplete#CompleteCSS autocmd FileType html,markdown setlocal omnifunc=htmlcomplete#CompleteTags autocmd FileType javascript setlocal omnifunc=javascriptcomplete#CompleteJS autocmd FileType python setlocal omnifunc=pythoncomplete#Complete autocmd FileType xml setlocal omnifunc=xmlcomplete#CompleteTags \" 设置 easymotion 是否区分大小写 let g:EasyMotion_smartcase=1 有问题, 请联系我. ","date":"2016-12-03","heading":"怎么安装","objectID":"/2016/12/fms03-vim02-md/:2:0","tags":null,"title":"(FMS03)Vim 的插件","uri":"/2016/12/fms03-vim02-md/#怎么安装"},{"categories":null,"collections":null,"content":"编辑器之神 Vim 的安装与简单配置 ","date":"2016-12-01","heading":"","objectID":"/2016/12/FMS02-vim01/:0:0","tags":null,"title":"(FMS02)Vim 安装与配置","uri":"/2016/12/FMS02-vim01/#"},{"categories":null,"collections":null,"content":"安装 brew update //升级 brew 到最新版本 brew info vim //查看 vim 包的配置信息 brew install vim --with-lua //安装 brew, 同时安装 lua 支持, 我们的一些插件需要 lua 的支持 //下载, 配置, 编译 mvim //启动 vim 图形界面, 也可以执行 vim 进入字符界面, 我们用 macOS 要享受图形界面的美观 ","date":"2016-12-01","heading":"安装","objectID":"/2016/12/FMS02-vim01/:1:0","tags":null,"title":"(FMS02)Vim 安装与配置","uri":"/2016/12/FMS02-vim01/#安装"},{"categories":null,"collections":null,"content":"配置 初看 vim 你肯定傻眼了, 啥么玩意, 这界面太古老了. 我们需要配置才能让 vim 符合我们自己的审美观点. edit mode: 编辑模式, 在 normal mode 下按 i 进入, 按 esc 返回 normal mode, 此模式下可以自由键入文本, 但此模式应该仅仅是键入字符, 修改请返回 normal mode normal mode: 普通模式, vim 最强大的模式, 在这种模式下可以对文件进行修改和查找替换等, 是最常用的模式, 在任何模式中, 按 esc 进入此模式. command mode: 命令模式, 在 normal mode 中按 : 进入此模式, 此模式可以进行查找替换和其他的配置 virtual mode: 可视化模式, 主要用于选择 **vim模式扫盲** 扫盲之后, 我们来操作一下. 先按下 i, 进入 edit 模式, 随便输入几行文字. 然后按 esc 进入 normal 模式. 在 normal 模式下按下 : 进入command模式, 在 vim 的最底端出现了 : 等待我们键入命令, 尝试输入 set guifont=Menlo:h16, 看到字体和大小改变了吧, 哈哈! 再次输入 :set number, 行号出来了, 漂亮. 但是总不能每次都输入吧, 我们需要写一个配置文件来保存我们常用的设置. vim 依据自己的配置文件来进行自身的配置, vim 的配置文件是 ~/.vimrc. 退出 mvim cd //返回主目录 mvim .vimrc //编辑配置文件 输入我们刚才的配置信息 set guifont=Menlo:h16 set number 进入 command mode, 别跟我说你不会进入 :source ~/.vimrc //重新读入 vim 配置, 请搜索一下 source(Linux) 命令的用法和作用, vim 中的 source 韩以相同 配置立刻生效了. ok, 下面我们就可以慢慢的写配置文件了. 我的 .vimrc 文件配置部分 let mapleader=\";\" #mapleader 是一个特殊的符号, 你可以自行定义, 当在 vim 中输入这个字符后, 意味着可能后面的字符会是一个自定义的命令, 请看下一条的解释 nmap l $ #nmap(normal map), normal 模式下当输入了 mapleader+l 之后, 意味着发出了 $(倒行尾) 的命令. 也就是输入 ;l 就相当于输入了 $ 命令. $ 不好输入, 还要按 shift 键, 所以我用 ;l 代替 $ nmap h 0 #nmap(normal map), normal 模式下当输入了 mapleader+h 之后, 意味着发出了 0(倒行首) 的命令. 也就是输入 ;h 就相当于输入了 0 命令. vnoremap y \"+y #virtual 模式下, 将选中的内容拷贝到系统剪贴板 nmap p \"+p #从系统剪贴板粘贴 nmap q :q # ;q 退出系统 #下面四个用于切换窗口 nmap wh h nmap wl l nmap wj j nmap wk k #在需要 ; 结尾的语言中, 使用 ;; 直接跳到行尾插入;并回车 inoremap ;; $; filetype plugin indent on #设定文件类型敏感, 支持各种语言的缩进 syntax on syntax enable #语法高亮 #请自行查找设定的内容与含义 set autowrite set autowriteall set ignorecase set hlsearch set incsearch set smartcase set gdefault set showmatch set autoindent set ts=4 sts=4 sw=4 expandtab set backspace=indent,eol,start set noswapfile set visualbell set cursorline set number set laststatus=2 set ruler set encoding=utf-8 set fileencoding=utf-8 set guifont=Menlo:h20 预告, 下次 vim 插件系统 ","date":"2016-12-01","heading":"配置","objectID":"/2016/12/FMS02-vim01/:1:1","tags":null,"title":"(FMS02)Vim 安装与配置","uri":"/2016/12/FMS02-vim01/#配置"},{"categories":null,"collections":null,"content":"macOS 的内核基于 unix 的变种 FreeBSD 来进行开发的, 在内核上面配有苹果公司漂亮的图形界面. 因为 macOS 是基于 unix 变种的, 所以 macOS 在程序员中特别流行. 近年来, 新的软件技术和严肃的程序开发, 大部分在 unix 中或者类 unix 的 Linux 中完成, 而 macOS 天生的 unix 基因, 使得 macOS 也得到了很多便利. 但 macOS 毕竟不是 Linux, 使得很多 Linux 下的优秀软件没有在 macOS 下进行实现, 而 Homebrew 则填补了这方面的空白. Homebrew 是基于 Ruby 语言进行开发的. 你可以再 Github 上面查看它的全部源代码. Homebrew 的使用非常简单, 首先你要确认你安装了 Xcode, 因为 Homebrew 通过 git 获取你要安装的软件包, 然后通过 clang(gcc 的 Apple 实现) 进行编译和安装, 安装的软件都放置在 /usr/local/Cella 中, 并在 /usr/local/bin 中做了执行文件的链接程序. ","date":"2016-12-01","heading":"","objectID":"/2016/12/FMS01-brew/:0:0","tags":null,"title":"(FMS01)Homebrew - macOS 不可或缺的套件管理器","uri":"/2016/12/FMS01-brew/#"},{"categories":null,"collections":null,"content":"安装 Homebrew /usr/bin/ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\" ","date":"2016-12-01","heading":"安装 Homebrew","objectID":"/2016/12/FMS01-brew/:0:1","tags":null,"title":"(FMS01)Homebrew - macOS 不可或缺的套件管理器","uri":"/2016/12/FMS01-brew/#安装-homebrew"},{"categories":null,"collections":null,"content":"怎么使用 brew search 软件名称 (在 Homebrew 中称为 Formula) brew update 更细 brew 自身和相应的包版本库 brew upgrade 升级已经安装的包 brew install $FORMULA 安装软件包 其他详细信息请看 FAQ ","date":"2016-12-01","heading":"怎么使用","objectID":"/2016/12/FMS01-brew/:0:2","tags":null,"title":"(FMS01)Homebrew - macOS 不可或缺的套件管理器","uri":"/2016/12/FMS01-brew/#怎么使用"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-线性方程组(三).pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds08/:0:0","tags":null,"title":"线性代数(八) - 线性方程组(三)","uri":"/2016/11/xxds08/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-线性方程组(二).pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds07/:0:0","tags":null,"title":"线性代数(七) - 线性方程组(二)","uri":"/2016/11/xxds07/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-线性方程组(一).pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds06/:0:0","tags":null,"title":"线性代数(六) - 线性方程组(一)","uri":"/2016/11/xxds06/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-向量.pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds05/:0:0","tags":null,"title":"线性代数(五) - 向量","uri":"/2016/11/xxds05/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-初等行变换.pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds04/:0:0","tags":null,"title":"线性代数(四) - 初等行变换","uri":"/2016/11/xxds04/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-逆矩阵.pdf ","date":"2016-11-27","heading":"","objectID":"/2016/11/xxds03/:0:0","tags":null,"title":"线性代数(三) - 逆矩阵","uri":"/2016/11/xxds03/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-矩阵.pdf ","date":"2016-11-26","heading":"","objectID":"/2016/11/xxds02/:0:0","tags":null,"title":"线性代数(二) - 矩阵","uri":"/2016/11/xxds02/#"},{"categories":null,"collections":null,"content":"大连理工大学 施光燕教授 线性代数笔记 因为 Hexo 对 TeX 数学公式解析问题,采用 pdf 文件形式. 线性代数-行列式.pdf ","date":"2016-11-26","heading":"","objectID":"/2016/11/xxds01/:0:0","tags":null,"title":"线性代数(一) - 行列式","uri":"/2016/11/xxds01/#"},{"categories":null,"collections":null,"content":"我们来详细探讨如何在macOS Sierra, Xcode 8 环境下来配置OpenCV的学习环境。 ","date":"2016-10-28","heading":"","objectID":"/2016/10/opencv-macos/:0:0","tags":null,"title":"macOS Sierra, Xcode 8 环境下OpenCV 3.1配置","uri":"/2016/10/opencv-macos/#"},{"categories":null,"collections":null,"content":"安装OpenCV 在macOS下，最方便的安装是使用brew来进行安装。 brew update brew install homebrew/science/opencv3 --HEAD 上面非常重要的是一定要加上--HEAD选项，确保在macOS Sierra下能够正确编译，否则，你会收到QT的错误。OpenCV的下个发行版会改正这个Bug，目前3.1版本我们要加上参数。 OpenCV3 会安装在 /usr/local/opt/opencv3的目录下。 ","date":"2016-10-28","heading":"安装OpenCV","objectID":"/2016/10/opencv-macos/:0:1","tags":null,"title":"macOS Sierra, Xcode 8 环境下OpenCV 3.1配置","uri":"/2016/10/opencv-macos/#安装opencv"},{"categories":null,"collections":null,"content":"配置Xcode进行开发 在Xcode 8中新建一个macOS的Command Line Tool项目。 项目名字和位置自己定义好，确保语言选择的是C++。 在项目的Build Settings中，找到Search Paths段，然后设置Header Search Paths和Library Search Paths，填入OpenCV3的include和lib目录，使用brew安装的话，分别是/usr/local/opt/opencv3/include和/usr/local/opt/opencv3/lib。 在Linking段中设置Other Linker Flags 对于链接的设定，可以依据你要使用的OpenCV功能来进行链接设定。比如，我只是用core的功能，那么就设定为-lopencv_core即可，如果还要使用imgcodecs的功能，那就设定-lopencv_core -lopencv_imgcodecs，如果图方便，就把所有的都设定上。 修改mian.cpp // // main.cpp // OpenCV-Demo // // Created by 张景林 on 28/10/2016. // Copyright © 2016 张景林. All rights reserved. // #include #include using namespace std; int main(int argc, const char * argv[]) { // insert code here... std::cout \u003c\u003c \"OpenCV Version \" \u003c\u003c CV_VERSION \u003c\u003c endl; return 0; } OK，大功告成！ ","date":"2016-10-28","heading":"配置Xcode进行开发","objectID":"/2016/10/opencv-macos/:0:2","tags":null,"title":"macOS Sierra, Xcode 8 环境下OpenCV 3.1配置","uri":"/2016/10/opencv-macos/#配置xcode进行开发"},{"categories":null,"collections":null,"content":"OpenCV是计算机视觉的事实标准库，学习OpenCV非常重要，在目前的国情下，在Windows上完成OpenCV的开发和教学工作是非常重要的，还不能指望所有人都有Mac计算机，即使Linux也是不现实的。 我们来详细探讨如何在Windows 10，Visual Studio 2015的环境下来配置OpenCV的学习环境。 ","date":"2016-10-28","heading":"","objectID":"/2016/10/opencv-windows-md/:0:0","tags":null,"title":"Windows 10, Visual Studio 2015环境下OpenCV 3.1配置和入门","uri":"/2016/10/opencv-windows-md/#"},{"categories":null,"collections":null,"content":"下载OpenCV 没啥说的，OpenCV官网上下载，我们要下载最新的3.1版本，所以要点击DOWNLOADS链接，不要下载主页上面的2.4.13版本。在下载页面中下载OpenCV for Windows 3.1即可。 下载下来的是一个opencv-3.1.0.exe文件，其实就是一个自解压的压缩包，可以直接执行也可以7-zip解压，随你便。 最重要的两个目录是build\\include和build\\x64\\vc14\\lib目录，这是Visual Studio 2015 C++中要配置的两个重要目录。 ","date":"2016-10-28","heading":"下载OpenCV","objectID":"/2016/10/opencv-windows-md/:0:1","tags":null,"title":"Windows 10, Visual Studio 2015环境下OpenCV 3.1配置和入门","uri":"/2016/10/opencv-windows-md/#下载opencv"},{"categories":null,"collections":null,"content":"如何配置 在Visual Studio 2015中新建一个Visual C++的Empty Project，项目名字和位置自己定义好。 在project上右击选择Properties，在VC++ Directories中配置Include Directories和Library Directories。 要注意属性的配置文件，非常重要。我们配置Debug和x64。是的，OpenCV 3.1已经不支持x86的开发了。 配置完路径后，要在Linker段中的Input中的Additional Dependencies中将OpenCV的opencv_world310d.lib引入。 关闭配置框后一个重要的事情是在顶部工具条中要修改当前的配置。 新建一个C++文件。我们来完成第一个tutorial，Load and Dispaly an Image。 为了学习方便，我们在一个项目中通过菜单和函数来完成不同教程。 #include #include //引入OpenCV头文件 #include #include #include using namespace std; using namespace cv; const string imageName(\"D:/zjl.jpg\"); void displayMenu(); //选择不同的Tutorial void loadAndDisplayAnImage(string windowTitle); //第一个教程 int main() { displayMenu(); return 0; } void displayMenu() { char key = ' ';//读取菜单选项 while (key != 'x') { cout \u003c\u003c \"Please select what tutorial do you want to run:\" \u003c\u003c endl; cout \u003c\u003c \"[1]Load and Dispaly an Image\" \u003c\u003c endl; cout \u003c\u003c \"[x]Quit\" \u003c\u003c endl; cin \u003e\u003e key; switch (key) { case '1': loadAndDisplayAnImage(\"Load and Display an Image\"); break; case 'x': break; } } } void loadAndDisplayAnImage(string windowTitle) { Mat inputImage; inputImage = imread(imageName, CV_LOAD_IMAGE_COLOR); if (inputImage.empty()) { cout \u003c\u003c \"The image has problem or file not fount\" \u003c\u003c endl; return; } namedWindow(windowTitle, CV_WINDOW_AUTOSIZE); imshow(windowTitle, inputImage); waitKey(); destroyWindow(windowTitle); } 编译运行，会收到错误。 这是因为找不到动态链接库。你有两种方法，第一种是将opencv\\build\\x64\\bin\\opencv_world310d.dll复制到c:\\windows\\system目录下，第二种方法是将opencv\\build\\x64\\bin\\目录添加到path环境变量中。 或者我们可以把opencv_world310d.dll直接复制到我们生成的.exe目录中。 OK，大功告成！ ","date":"2016-10-28","heading":"如何配置","objectID":"/2016/10/opencv-windows-md/:0:2","tags":null,"title":"Windows 10, Visual Studio 2015环境下OpenCV 3.1配置和入门","uri":"/2016/10/opencv-windows-md/#如何配置"},{"categories":null,"collections":null,"content":"项目地址 官方网址 完全Javascript实现的OCR，能够识别62中语言。 截止写作时，本周共收获3146个star。 好吧，我承认截止写作时，本周最火的项目不是Tesseract.js，而是另一个项目Deep Learning Papers Reading Roadmap，但是本人还没有关注深度学习，所以，你懂得~~~ 截止写作时，本周共收获3169个star。 ","date":"2016-10-22","heading":"","objectID":"/2016/10/tesseract/:0:0","tags":null,"title":"2016年第43周最火的Github项目 - Tesseract.js","uri":"/2016/10/tesseract/#"},{"categories":null,"collections":null,"content":"什么东东? Tesseract.js 是一个Tesseract OCR引擎的javascript移植。这个库支持超过60种语言（包括中文），能够自动识别文本方向和笔迹检测。 可以运行在浏览器和NodeJS中。 ","date":"2016-10-22","heading":"什么东东?","objectID":"/2016/10/tesseract/:0:1","tags":null,"title":"2016年第43周最火的Github项目 - Tesseract.js","uri":"/2016/10/tesseract/#什么东东"},{"categories":null,"collections":null,"content":"怎么安装？ 浏览器 NodeJS yarn add tesseract.js //or npm install tesseract.js --save ","date":"2016-10-22","heading":"怎么安装？","objectID":"/2016/10/tesseract/:0:2","tags":null,"title":"2016年第43周最火的Github项目 - Tesseract.js","uri":"/2016/10/tesseract/#怎么安装"},{"categories":null,"collections":null,"content":"基础案例 新建一个项目，进行初始化 mkdir tesseract-demo cd tesseract-demo yarn init yarn add tesseract.js touch index.js 我们先来找一个中文页面，进行截图，将图像放置到项目目录中。我截取了这样一个图像。 下面来进行index.js的代码： const Tesseract = require('tesseract.js') Tesseract.recognize('./image.png', 'chi_sim') //第二个参数表示语言，缺省的话为eng，英文 .progress((p) =\u003e console.log('progress', p)) .then((result) =\u003e console.log('result', result)) 运行： node index.js 首次运行会先下载chi_sim.traineddata识别库，然后进行识别，需要等待一下，第二次则不需要下载了。 识别结果 result { text: '截取任慧窗口: 快捷键 【Sh…+command+4)\\n入直浸按“Shm+command+4“快捷锂组台、 出现十字架的坐标图标;\\n入拖动坐标图标, 选取任慧区域后释放鼠标, 图片会自动保存在桌面恩\\n\\n', //后面省略 识别结果中有错误，但还是可以接受的。Tesseract.js还有很多的参数可以配置，查看项目文档即可。 ","date":"2016-10-22","heading":"基础案例","objectID":"/2016/10/tesseract/:0:3","tags":null,"title":"2016年第43周最火的Github项目 - Tesseract.js","uri":"/2016/10/tesseract/#基础案例"},{"categories":null,"collections":null,"content":"JSON Server: 无需任何代码，30秒搭建全功能的REST API。 ","date":"2016-10-18","heading":"JSON Server: 无需任何代码，30秒搭建全功能的REST API。","objectID":"/2016/10/jsonserver/:1:0","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#json-server-无需任何代码30秒搭建全功能的rest-api"},{"categories":null,"collections":null,"content":"Faker: 快速生成大量的模拟数据 JSON Server 项目地址 Faker 项目地址 ","date":"2016-10-18","heading":"Faker: 快速生成大量的模拟数据","objectID":"/2016/10/jsonserver/:2:0","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#faker-快速生成大量的模拟数据"},{"categories":null,"collections":null,"content":"什么东东？ JSON Server 是一个后端的原型工具，很多时候我们在开发SPA(Single Page Application)项目的时候，都需要后端提供REST API供前端访问，在开发过程中，JSON Server可以作为Mock Server存在，非常快速非常简单。 有了Mock Server还需要大量的数据，我们可以使用Faker来快速生成模拟数据。 两个神器结合，就可以快速完成有大量数据的REST API服务了。 ","date":"2016-10-18","heading":"什么东东？","objectID":"/2016/10/jsonserver/:2:1","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#什么东东"},{"categories":null,"collections":null,"content":"JSON Server 怎么安装 npm install -g json-server //or yarn global add json-server ","date":"2016-10-18","heading":"JSON Server 怎么安装","objectID":"/2016/10/jsonserver/:2:2","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#json-server-怎么安装"},{"categories":null,"collections":null,"content":"怎么使用 新建一个 db.json 文件 { \"posts\": [ {\"id\": 1, \"title\": \"json-server\", \"author\": \"typicode\"}, {\"id\": 2, \"title\": \"另一篇博客\", \"author\": \"张景林\"} ], \"comments\": [ {\"id\": 1, \"body\": \"some comments\", \"postId\": 1} ], \"profile\": { \"name\": \"typicode\" } } 在上面的json文件中我们有三个顶级段，分别是posts、comments、profile，运行下面的命令 json-server --watch db.json 大功告成，控制台输出： \\{^_^}/ hi! Loading db.json Done Resources http://localhost:3000/posts http://localhost:3000/comments http://localhost:3000/profile Home http://localhost:3000 Type s + enter at any time to create a snapshot of the database Watching... 哈哈，自动生成了三个顶级段的API端点。 尝试访问以下： http://localhost:3000/posts //返回全部的posts数据 http://localhost:3000/posts/1 //返回第一个post http://localhost:3000/comments //返回全部comments数据 http://localhost:3000/posts/1/comments //返回第一个post的全部comment，有没有大惊失色？？ http://localhost:3000/posts/1/commnets/1 //返回第一个post的第一个comment，请注意comment中的关联关系 http://localhost:3000 //json-server的运行状态 JSON Server支持GET、POST、PUT、DELETE常见动词，满足RESTful的需求。 ","date":"2016-10-18","heading":"怎么使用","objectID":"/2016/10/jsonserver/:2:3","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#怎么使用"},{"categories":null,"collections":null,"content":"结合Faker快速生成模拟数据 我们不可能手工生成大量数据，结合Faker就可以快速的完成。 添加faker依赖： yarn add faker //or npm install faker --save //需要lodash帮助我们少些代码 yarn add lodash //or npm install loadsh --save 创建 database.js文件，返回faker的数据供json server使用。 module.exports = () =\u003e { const faker = require('faker/locale/zh_CN') const _ = require('lodash') return { people: _.times(100, (n) =\u003e { return { id: n, name: faker.name.findName(), email: faker.internet.email(), phone: faker.phone.phoneNumber(), avatar: faker.internet.avatar() } }) } } 通过下面的方式运行： json-server database.js 查看服务器，反回了100条数据。It’s so easy. ","date":"2016-10-18","heading":"结合Faker快速生成模拟数据","objectID":"/2016/10/jsonserver/:2:4","tags":null,"title":"Github上的开发神器 之 JSON Server \u0026 Faker","uri":"/2016/10/jsonserver/#结合faker快速生成模拟数据"},{"categories":null,"collections":null,"content":"VueJS中的Vue组件是以.vue为扩展名的，atom中不识别，atom默认认为是Plain Text，所以没有语法高亮等特性。 在atom中有一个名为language-vue的社区包，安装之后能够高亮.vue的文件，还有一个template的缩略词，能够快速生成Vue组件的结构，看似完美。 ","date":"2016-10-15","heading":"","objectID":"/2016/10/atom-vue/:0:0","tags":null,"title":"Atom下VueJS开发的配置","uri":"/2016/10/atom-vue/#"},{"categories":null,"collections":null,"content":"但是Emmet不好用！！！！ 作为一个无法离开Emmet的人来说，没有Emmet的功能简直是回到了原始社会，无法忍受。 在google上搜索，得到解决方案，在emmet-atom中有#364的开放issue，joshuajohnson814提出了可以在keymap.cson中添加自定义快捷键： 'atom-text-editor[data-grammar~=\"vue\"]:not([mini])': 'tab': 'emmet:expand-abbreviation-with-tab' 作为临时的解决方案，但是其他人提出： ","date":"2016-10-15","heading":"但是Emmet不好用！！！！","objectID":"/2016/10/atom-vue/:0:1","tags":null,"title":"Atom下VueJS开发的配置","uri":"/2016/10/atom-vue/#但是emmet不好用"},{"categories":null,"collections":null,"content":"CSS 不好用 也就是我没办法输入 w10[tab] -\u003e width: 10px; 其实，.vue文件不过就是html文件换了个扩展名而已，我们可以直接把.vue登记成.html文件即可，也就是atom看到了.vue文件就用.html来处理即可。 ","date":"2016-10-15","heading":"CSS 不好用","objectID":"/2016/10/atom-vue/:0:2","tags":null,"title":"Atom下VueJS开发的配置","uri":"/2016/10/atom-vue/#css-不好用"},{"categories":null,"collections":null,"content":"完美解决方案 在atom中打开config.json文件，别告诉我你不知道怎么打开，在MacOS下，在atom菜单中。 在core段下面增加： customFileTypes: 'text.html.basic': [ 'vue' ] 这样，所有的.vue文件一律视为html，完美解决。 我得意地笑，得意地笑，~~~~ 上面那段配置什么意思？给你一个网址，atom官方手册，好好看看Customizing Language Recognition这一段就明白了。 ","date":"2016-10-15","heading":"完美解决方案","objectID":"/2016/10/atom-vue/:1:0","tags":null,"title":"Atom下VueJS开发的配置","uri":"/2016/10/atom-vue/#完美解决方案"},{"categories":null,"collections":null,"content":"项目地址 官方网址 ","date":"2016-10-15","heading":"","objectID":"/2016/10/yarn/:0:0","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#"},{"categories":null,"collections":null,"content":"快速、可靠、安全的依赖管理。 ","date":"2016-10-15","heading":"快速、可靠、安全的依赖管理。","objectID":"/2016/10/yarn/:1:0","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#快速可靠安全的依赖管理"},{"categories":null,"collections":null,"content":"什么东东? yarn是一个可以替代npm的包管理工具，用于管理NodeJS的包。 ","date":"2016-10-15","heading":"什么东东?","objectID":"/2016/10/yarn/:1:1","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#什么东东"},{"categories":null,"collections":null,"content":"有什么优势？ 快速：yarn把每一个package都缓存起来，下次就不需要再下载了。它还可以并行操作，比以往都快。 可靠：使用了一个lockfile文件，把当前的package版本确定下来，避免版本的问题。 安全：yarn使用checksums校验每一个包，确保正确。 ","date":"2016-10-15","heading":"有什么优势？","objectID":"/2016/10/yarn/:1:2","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#有什么优势"},{"categories":null,"collections":null,"content":"怎么安装？ MacOS // brew brew update brew install yarn // shell curl -o- -L https://yarnpkg.com/install.sh | bash 其他平台：参考官方网址 ","date":"2016-10-15","heading":"怎么安装？","objectID":"/2016/10/yarn/:1:3","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#怎么安装"},{"categories":null,"collections":null,"content":"怎么使用 基本上和npm一样，除了一些命令参数以外。 //开始新项目 yarn init // npm init //添加包 yarn add [package] // npm install [package] --save yarn add [package] --dev // npm install [package] --save-dev //安装所有依赖 yarn | yarn install //全局安装 yarn global add [package] // npm install -g [package] //运行脚本 yarn run [script] // npm run [script] ","date":"2016-10-15","heading":"怎么使用","objectID":"/2016/10/yarn/:1:4","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#怎么使用"},{"categories":null,"collections":null,"content":"一些技巧 yarn add等同于npm install [package]命令，用于向项目中安装依赖的包，但是如果安装package.json中的全部包，等同于npm install的命令是yarn install命令。 如果目录中没有package.json的话，yarn会自动建立，每个yarn add命令会自动在dependencies段中添加依赖，不需要明确的--save参数。 yarn会自动产生yarn.lock文件，文件中描述了依赖包的版本。 ","date":"2016-10-15","heading":"一些技巧","objectID":"/2016/10/yarn/:1:5","tags":null,"title":"2016年第42周最火的Github项目 - Yarn","uri":"/2016/10/yarn/#一些技巧"},{"categories":null,"collections":null,"content":"这是php基础中关于MySQL的内容。 ","date":"2016-03-20","heading":"","objectID":"/2016/03/php-fundamentals-mysql/:0:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#"},{"categories":null,"collections":null,"content":"MySQL 创建数据库和表 安装好MySQL后，可以直接在命令行下操作MySQL，当然也可以使用PhpMyAdmin来进行数据的创建。 mysql -u root -p //连接到本地数据库，默认root用户没有密码 set password for root@localhost = password('xxx'); //设置root密码 show databases; //显示所有数据库 create database blog default charset utf8; // 创建blog数据库，并设置字符集为utf8 use blog; //设置当前数据库为blog show tables; //显示当前数据库中的所有表 create table users( id int auto_increment, name varchar(50) not null, email varchar(100) not null, primary key(id)); describe users; //显示users的结构 ","date":"2016-03-20","heading":"MySQL 创建数据库和表","objectID":"/2016/03/php-fundamentals-mysql/:1:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#mysql-创建数据库和表"},{"categories":null,"collections":null,"content":"插入和删除数据 insert into users values(null, 'zjl', 'ldxyzjl@163'); select * from users; select email from users; insert into users(name, email) values('zhao si', 'zhao@si.com'); select * from users order by name asc; //按名字升序返回记录 select * from users order by name desc; //按名字降序返回记录 insert into users(name, email) values('wang wu', 'wang@wu.com'); select * from users limit 2; //返回2条记录 delete from users where name = 'wang wu'; //删除名字为'wang wu'的记录 ","date":"2016-03-20","heading":"插入和删除数据","objectID":"/2016/03/php-fundamentals-mysql/:2:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#插入和删除数据"},{"categories":null,"collections":null,"content":"修改记录 //创建posts表，注意author_id是外键，会关联users表 create table posts( -\u003e id int auto_increment, -\u003e title varchar(150), -\u003e body text, -\u003e author_id int not null, -\u003e primary key(id) -\u003e ); insert into posts(title, body, author_id) values('My first post', 'the body of my post', 1); update posts set author_id = 2 where id = 1; //将id为1的posts记录修改author_id为2 select * from posts inner join users on users.id = posts.author_id; //两个表的内连接查询 select title, body, name from posts inner join users on users.id = posts.author_id; update posts set title = 'My updated post' where id = 1; update posts set title = 'updated again', body = 'updated body' where id = 1; alter table posts change title subject varchar(100); //将title列修改为subject desc posts; alter table posts change subject title varchar(150); //改名和类型 ","date":"2016-03-20","heading":"修改记录","objectID":"/2016/03/php-fundamentals-mysql/:3:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#修改记录"},{"categories":null,"collections":null,"content":"多表连接 create database practice; use practice; create table users( -\u003e id int auto_increment primary key, -\u003e username varchar(100) -\u003e ); insert into users(username) values('zjl'); insert into users(username) values('zs'); insert into users(username) values('ww'); create table orders( -\u003e id int auto_increment primary key, -\u003e number int, -\u003e user_id int -\u003e ); insert into orders(number, user_id) values(1466227, 1); insert into orders(number, user_id) values(7122567, 1); insert into orders(number, user_id) values(3458212, 1); insert into orders(number, user_id) values(9934832, 2); insert into orders(number, user_id) values(354522, 2); select * from users inner join orders on orders.user_id = users.id; select users.id, username, number as oder_number from users inner join orders on orders.user_id = users.id; select users.id, username, number as oder_number from users inner join orders on orders.user_id = users.id where id = 1; ERROR 1052 (23000): Column 'id' in where clause is ambiguous //两个表都有id，不知道用哪一个。 select users.id, username, number as oder_number from users inner join orders on orders.user_id = users.id where users.id = 1; select users.id, username, orders.number from users left join orders on users.id = orders.user_id; select users.id, username, count(orders.number) as order_amount from users inner join orders on users.id = orders.user_id where users.id=1; ","date":"2016-03-20","heading":"多表连接","objectID":"/2016/03/php-fundamentals-mysql/:4:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#多表连接"},{"categories":null,"collections":null,"content":"php连接mysql 下面的描述已经过时，PHP不推荐用这种方式使用php，我在这里仅仅是说明一下。 创建functions.php文件 \u003c?php //已经过时，不要使用 mysql_connect('localhost', 'root', '') or die('不能连接数据库'); //主机名，用户名，密码 mysql_select_db('practice'); $result = mysql_query('select * from users'); while ($row = mysql_fetch_object($result)) { // print_r($row); echo $row-\u003eusername . ' '; } 如果我们要访问不同的数据库怎么办？我们还是结构化我们的代码，做一个函数。 \u003c?php //已经过时，不要使用 function connect($host = 'localhost', $username = 'root', $password = '') { return mysql_connect($host, $username, $password); } $conn = connect() or die('不能连接数据库'); mysql_select_db('practice', $conn); $result = mysql_query('select * from users'); while ($row = mysql_fetch_object($result)) { echo $row-\u003eusername . ' '; } 如果我们可以在连接的时候就传入数据库就更好了。 \u003c?php //已经过时，不要使用 function connect($host, $username, $password, $database = '') { $conn = mysql_connect($host, $username, $password); if (!empty($database)) { mysql_select_db($database, $conn); } return $conn; } $conn = connect('localhost', 'root', '', 'practice') or die('不能连接数据库'); $result = mysql_query('select * from users'); while ($row = mysql_fetch_object($result)) { echo $row-\u003eusername . ' '; } 我们来继续结构化查询 \u003c?php //已经过时，不要使用 function connect($host, $username, $password, $database = '') { $conn = mysql_connect($host, $username, $password); if (!empty($database)) { mysql_select_db($database, $conn); } return $conn; } function query($query) { $results = mysql_query($query); if ($results) { $rows = []; while ($row = mysql_fetch_object($results)) { $rows[] = $row; } return $rows; } return false; } $conn = connect('localhost', 'root', '', 'practice') or die('不能连接数据库'); $result = query('select * from users'); if ($result) { foreach ($result as $row) { echo $row-\u003eusername . ' '; } } ","date":"2016-03-20","heading":"php连接mysql","objectID":"/2016/03/php-fundamentals-mysql/:5:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#php连接mysql"},{"categories":null,"collections":null,"content":"使用PDO连接数据库 我们连接数据库的信息应该放置在一个配置文件中，而不是硬编码在函数库中。 创建config.php文件 \u003c?php $config = [ 'DB_USERNAME' =\u003e 'root', 'DB_PASSWORD' =\u003e '', 'DB_HOST' =\u003e 'localhost', 'DB_DATABASE' =\u003e 'practice' ]; 创建functions.php \u003c?php require 'config.php'; try { $conn = new PDO(\"mysql:host={$config['DB_HOST']};dbname={$config['DB_DATABASE']}\", $config['DB_USERNAME'], $config['DB_PASSWORD']); $conn-\u003esetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } catch (PDOException $e) { echo 'ERROR: ' . $e-\u003egetMessage(); } ","date":"2016-03-20","heading":"使用PDO连接数据库","objectID":"/2016/03/php-fundamentals-mysql/:6:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#使用pdo连接数据库"},{"categories":null,"collections":null,"content":"查询数据 我们来添加查询的语句 \u003c?php require 'config.php'; try { $conn = new PDO(\"mysql:host={$config['DB_HOST']};dbname={$config['DB_DATABASE']}\", $config['DB_USERNAME'], $config['DB_PASSWORD']); $conn-\u003esetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $results = $conn-\u003equery('select * from users'); foreach ($results as $row) { print_r($row); } } catch (PDOException $e) { echo 'ERROR: ' . $e-\u003egetMessage(); } 如果我们需要id为3的记录，我们可以这样 $results = $conn-\u003equery('select * from users where id = 3'); 如果id是动态获取的呢？我们来看一种错误的做法 $id = 3; $results = $conn-\u003equery('select * from users where id = ' . $id); 现在假设我们的id值被用户进行了注入攻击 $id = '100 OR 1'; $results = $conn-\u003equery('select * from users where id = ' . $id); 数据已经全部暴露了，这个不得了。我们需要安全的做法 $results = $conn-\u003equery('select * from users where id = ' . $conn-\u003equote($id)); 但这仍然不是好的做法，下面我们来看一下好的做法。 ","date":"2016-03-20","heading":"查询数据","objectID":"/2016/03/php-fundamentals-mysql/:7:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#查询数据"},{"categories":null,"collections":null,"content":"prepared statement $id = 3; $stmt = $conn-\u003eprepare('select * from users where id = :id'); $stmt-\u003eexecute([ 'id' =\u003e $id ]); while ($row = $stmt-\u003efetch()) { print_r($row); } 现在可以尝试修改$id, 例如 '3' '3 or 1' 我们还可以这样做 $stmt-\u003ebindParam(':id', $id, PDO::PARAM_INT); $stmt-\u003eexecute(); 现在我们返回的是数组，我们可以返回对象 while ($row = $stmt-\u003efetch(PDO::FETCH_OBJ)) { print_r($row); } //或者 $stmt = $conn-\u003eprepare('select * from users where id = :id'); $stmt-\u003esetFetchMode(PDO::FETCH_OBJ); 也可以一次返回全部数据 $stmt = $conn-\u003eprepare('select * from users'); $stmt-\u003esetFetchMode(PDO::FETCH_OBJ); $stmt-\u003eexecute(); $results = $stmt-\u003efetchAll(); print_r($results); 使用prepared statement执行插入动作 $stmt = $conn-\u003eprepare('insert into users(username) values(:username)'); $stmt-\u003ebindParam(':username', $username, PDO::PARAM_STR); $username = 'aaa'; $stmt-\u003eexecute(); $username = 'bbb'; $stmt-\u003eexecute(); 更简洁的方式是 $stmt = $conn-\u003eprepare('insert into users(username) values(:username)'); $stmt-\u003ebindParam(':username', $username, PDO::PARAM_STR); $users = ['ccc', 'ddd', 'eed']; foreach ($users as $username) { $stmt-\u003eexecute(); } ","date":"2016-03-20","heading":"prepared statement","objectID":"/2016/03/php-fundamentals-mysql/:8:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#prepared-statement"},{"categories":null,"collections":null,"content":"PDO 辅助函数 我们先来创建config.php \u003c?php $config = [ 'DB_USERNAME' =\u003e 'root', 'DB_PASSWORD' =\u003e '', 'DB_HOST' =\u003e 'localhost', 'DB_DATABASE' =\u003e 'practice' ]; 创建functions.php \u003c?php require 'config.php'; function connect($config) { try { $conn = new PDO(\"mysql:host={$config['DB_HOST']};dbname={$config['DB_DATABASE']}\", $config['DB_USERNAME'], $config['DB_PASSWORD']); $conn-\u003esetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); return $conn; } catch (Exception $e) { return false; } } 我们创建index.php进行测试 \u003c?php require 'config.php'; require 'functions.php'; if (connect($config)) { echo 'Connected'; } else { echo 'Could not connect the db'; } 我们在辅助函数中增加一个get方法，能够获取某个表的全部数据，并能够返回数据，或者没有数据返回false function get($tableName, $conn) { try { $result = $conn-\u003equery(\"SELECT * FROM $tableName\"); return ($result-\u003erowCount \u003e 0) ? $result : false; } catch (Exception $e) { return false; } } 我们来测试一下index.php \u003c?php require 'config.php'; require 'functions.php'; $conn = connect($config); if ($conn) { $result = get('users', $conn); if ($result) { foreach($result as $row) { print_r($row); } } else { echo 'No rows'; } } else { echo 'Could not connect the db'; } 我们也可以创建一个普通的query函数，functions.php function query($query, $bindings, $conn) { $stmt = $conn-\u003eprepare($query); $stmt-\u003eexecute($bindings); $result = $stmt-\u003efetchAll(); return $result ? $result : false; } 测试 if ($conn) { $rows = query('select * from users where id = :id', [ 'id' =\u003e 1 ], $conn); if ($rows) { print_r($rows); } else { echo 'No data'; } } else { echo 'Could not connect the db'; } ","date":"2016-03-20","heading":"PDO 辅助函数","objectID":"/2016/03/php-fundamentals-mysql/:9:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#pdo-辅助函数"},{"categories":null,"collections":null,"content":"命名空间 我们的函数库命名会出现问题，当我们引入其他的库的时候。我们需要命名空间来进行隔离。 \u003c?php namespace Zjl\\DB; function connect() { return 'conncted'; } 这样，我们的connect就在Zjl\\DB这个命名空间下了。我们测试一下 \u003c?php require 'functions.php'; // echo connect(); //无法找到 echo Zjl\\DB\\connect(); 但是我们不想每次都输入那么长的名字，我们可以使用use关键字 \u003c?php require 'functions.php'; use Zjl\\DB; echo DB\\connect(); ","date":"2016-03-20","heading":"命名空间","objectID":"/2016/03/php-fundamentals-mysql/:10:0","tags":null,"title":"PHP 基础 - MySQL","uri":"/2016/03/php-fundamentals-mysql/#命名空间"},{"categories":null,"collections":null,"content":"一个Docker学习的简单笔记。 ","date":"2016-03-19","heading":"","objectID":"/2016/03/docker-in-action/:0:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#"},{"categories":null,"collections":null,"content":"Docker是什么？ 略。 ","date":"2016-03-19","heading":"Docker是什么？","objectID":"/2016/03/docker-in-action/:1:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#docker是什么"},{"categories":null,"collections":null,"content":"Docker安装 参考docker.com上的文档安装Docker。 ","date":"2016-03-19","heading":"Docker安装","objectID":"/2016/03/docker-in-action/:2:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#docker安装"},{"categories":null,"collections":null,"content":"将当前用户添加到Docker组 为了避免每次执行docker命令都需要输入sudo，可以将当前用户添加到docker组。 sudo gpasswd -a ${USER} docker 然后退出重新进入即可。 docker version //快速检查 ","date":"2016-03-19","heading":"将当前用户添加到Docker组","objectID":"/2016/03/docker-in-action/:3:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#将当前用户添加到docker组"},{"categories":null,"collections":null,"content":"运行第一个镜像 docker run debian echo \"Hello World\" 运行一个命令行容器 $ docker run -i -t debian /bin/bash #root@99a15501bbaf:/# echo \"Hello from Container-lan\" #Hello from Container-lan #root@99a15501bbaf:/# exit #exit ","date":"2016-03-19","heading":"运行第一个镜像","objectID":"/2016/03/docker-in-action/:4:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#运行第一个镜像"},{"categories":null,"collections":null,"content":"基本命令 # -h hostname 命名容器 docker run -h CONTAINER -i -t debian /bin/bash # 开一个新的终端 docker ps //运行的容器 docker inspect gloomy_yalow(NAMES) //容器信息 docker inspect gloomy_yalow | grep IPAddress docker diff gloomy_yalow //容器的变化 docker logs gloomy_yalow //容器内发生的事情 docker ps -a //所有容器，包括停止的 docker rm gloomy_yalow //删除容器 docker rm -v $(docker ps -aq -f status=exited) //删除所有已经停止的容器 ","date":"2016-03-19","heading":"基本命令","objectID":"/2016/03/docker-in-action/:5:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#基本命令"},{"categories":null,"collections":null,"content":"一个cowsay的例子 docker run -it --name cowsay --hostname cowsay debian bash apt-get update apt-get install -y cowsay fortune /usr/games/fortune | /usr/games/cowsay exit docker commit cowsay test/cowsayimage docker run test/cowsayimage /usr/games/cowsay \"Moo\" ","date":"2016-03-19","heading":"一个cowsay的例子","objectID":"/2016/03/docker-in-action/:6:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#一个cowsay的例子"},{"categories":null,"collections":null,"content":"使用Dockerfiles构建镜像 mkdir cowsay cd cowsay/ touch Dockerfile # 文件内容 FROM debian:latest RUN apt-get update \u0026\u0026 apt-get install -y cowsay fortune #### docker build -t test/cowsay-dockerfile . docker run test/cowsay-dockerfile /usr/games/cowsay \"Moo\" # 添加下面到Dockerfile，以便自动执行 ENTRYPOINT [\"/usr/games/cowsay\"] #### docker build -t test/cowsay-dockerfile . docker run test/cowsay-dockerfile \"Moo\" # 在主机cowsay目录下创建文件 entrypoint.sh touch entrypoint.sh # 文件内容 #!/bin/bash if [ $# -eq 0 ]; then /usr/games/fortune | /usr/games/cowsay else /usr/games/cowsay \"$@\" fi #### chmod +x entrypoint.sh #修改Dockerfile FROM debian:latest RUN apt-get update \u0026\u0026 apt-get install -y cowsay fortune COPY entrypoint.sh / ENTRYPOINT [\"/entrypoint.sh\"] ##### docker build -t test/cowsay-dockerfile . docker run test/cowsay-dockerfile docker run test/cowsay-dockerfile Hello Moo ","date":"2016-03-19","heading":"使用Dockerfiles构建镜像","objectID":"/2016/03/docker-in-action/:7:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#使用dockerfiles构建镜像"},{"categories":null,"collections":null,"content":"使用Redis官方镜像 docker pull redis docker run --name myredis -d redis //-d 后台运行 docker run --rm -it --link myredis:redis redis /bin/bash //--rm 运行后删除容器，--link 连接两个容器 已存在容器名/在本容器中使用名 redis-cli -h redis -p 6379 redis:6379\u003e ping PONG redis:6379\u003e set \"abc\" 123 OK redis:6379\u003e get \"abc\" \"123\" redis:6379\u003e exit root@e78e3538b2c8:/data# exit exit docker stop myredis docker rm -v myredis docker rm $(docker ps -aq) ","date":"2016-03-19","heading":"使用Redis官方镜像","objectID":"/2016/03/docker-in-action/:8:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#使用redis官方镜像"},{"categories":null,"collections":null,"content":"使用docker进行开发 我们使用python和flask构建一个website。 ","date":"2016-03-19","heading":"使用docker进行开发","objectID":"/2016/03/docker-in-action/:9:0","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#使用docker进行开发"},{"categories":null,"collections":null,"content":"Hello, World 构建一个返回“hello，world”的web server。 创建以下的目录结构 identidock/ |-- app | `-- identidock.py `-- Dockerfile 1 directory, 2 files 编辑identidock.py from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello World!\\n' if __name__ == '__main__': app.run(debug=True, host='0.0.0.0') 编辑Dockerfile ROM python:latest RUN pip install Flask==0.10.1 WORKDIR /app COPY app /app CMD [\"python\", \"identidock.py\"] 创建镜像并测试 docker build -t identidock . docker run -d p 5000:5000 identidock curl localhost:5000 但是如果每次我们修改代码都要重新运行构建镜像也是一件麻烦的事情，我们改进一下 docker stop $(docker ps -lq) //停止最后运行的container docker run -d -p 5000:5000 -v \"$(pwd)\"/app:/app identidock //将主机app目录挂载到容器/app目录下 curl localhost:5000 //修改python文件，返回不同的字符串，再次测试 curl lcoalhost:5000 ","date":"2016-03-19","heading":"Hello, World","objectID":"/2016/03/docker-in-action/:9:1","tags":null,"title":"Docker 学习笔记","uri":"/2016/03/docker-in-action/#hello-world"},{"categories":null,"collections":null,"content":"这是php基础的第二篇。 ","date":"2016-03-17","heading":"","objectID":"/2016/03/php-fundamentals-2/:0:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#"},{"categories":null,"collections":null,"content":"$_GET 我们如何在页面之间传送变量？或者如何获取到用户的输入变量呢？我们可以使用查询字符串QueryString来解决这个问题。 \u003c?php var_dump($_GET); $_GET是php提供的超级全局变量，用于查询字符串的处理，我们可以在地址栏输入查询变量来获取相应的用户输入。 http://locahost/index.php?name=zjl //显示结果 // array(1) { 'name' =\u003e string(3) \"zjl\" } 如果需要多个变量，使用\u0026amp;来作为连接符号。 http://locahost/index.php?name=zjl\u0026job=teacher //显示结果 // array(2) { 'name' =\u003e string(3) \"zjl\" 'job' =\u003e string(7) \"teacher\" } 所以我们就可以这样来处理页面了 \u003c!doctype html\u003e Super Global Super Global \u003c?=$_GET['name']?\u003e 上面的页面有一个问题，如果我们没有提供查询字符串，那么会导致错误，所以我们要在使用之前进行测试。 \u003c!doctype html\u003e Super Global Super Global \u003c?php if (isset($_GET['name'])): ?\u003e Welcome you, \u003c?=$_GET['name']?\u003e \u003c?php else: ?\u003e Nobody \u003c?php endif ?\u003e 在处理查询字符串的时候我们要避免注入攻击，我们来尝试一下这样输入 http://localhost/index.php?name=zjl //看看结果 我们需要使用htmlspecialchrs，在php.net网站中查看一下。 Welcome you, \u003c?=htmlspecialchars($_GET['name'])?\u003e 在页面之间传送信息也是可以的 About ","date":"2016-03-17","heading":"$_GET","objectID":"/2016/03/php-fundamentals-2/:1:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#_get"},{"categories":null,"collections":null,"content":"$_POST 我们来看一下表单的处理 \u003c!doctype html\u003e Post Contact Form Name: 上面表单的action为空，意味着表单的提交会提交到当前文件，method采用get方式，输入信息提交表单，查看一下地址栏，你会发现表单的提交信息出现在了地址栏，这就是get提交方式。 将method修改为post再次提交，你会返现信息消失了，我们如何处理post数据呢？ \u003c?php print_r($_POST); ?\u003e \u003c!doctype html\u003e Post Contact Form Name: php使用$_POST超级全局变量来处理post请求。 但是如果我们希望在没有提交表单的时候不显示输出信息，怎么处理呢？我们修改成这样 \u003c?php if (!empty($_POST)) { print_r($_POST); } ?\u003e 不过，更常见的形式是使用$_SERVER['REQUEST_METHOD']来获取当前是get请求还是post请求。 \u003c?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { print_r($_POST); } ?\u003e 我们修改表单 \u003c?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { print_r($_POST); } ?\u003e \u003c!doctype html\u003e Post Contact Form Name: Email: Message: 对于输入字段来说，必须存在name属性，你可以去掉某个字段的name属性，提交一下，查看结果。 ","date":"2016-03-17","heading":"$_POST","objectID":"/2016/03/php-fundamentals-2/:2:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#_post"},{"categories":null,"collections":null,"content":"Session 存储在服务器端的用户数据，每个用户都有不同的session。 我们来写一个index.php文件 \u003c?php //必须出现在页面的开始，前面不能有html session_start(); $_SESSION['username'] = 'zjl'; ?\u003e About 再来about.php \u003c?php session_start(); echo $_SESSION['username']; 可以看出来，在index.php中放在session中的数据是可以获取到的，如果你关闭浏览器，再次直接访问about.php，什么信息也不会看到，因为新开浏览器相当于一个新的会话，session中是没有数据的。 当session数据不再需要时，比如用户已经选择登出，可是使用session_destroy()来销毁。 \u003c?php session_start(); echo $_SESSION['username']; session_destroy(); //即使使用了session_destroy, session中的数据在当前页面还是存在 $_SESSION = []; print_r($_SESSION); 我们来看一下一个登陆的简单例子 \u003c?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { echo 'posted'; } ?\u003e \u003c!doctype html\u003e Login 这是一个我们已经熟悉的形式，不过我们还是可以修改一下 \u003c?php if (isset($_POST['loginForm'])) { echo 'posted'; } ?\u003e ... 我们为提交按钮添加一个名字，然后判断$_POST中是否包含提交按钮的信息来确定是否是提交。这样的好处是如果页面中包含多个表单的话，我们也可以区分出来是哪一个表单的提交。 我们来完整的实现登录流程，现在不涉及数据库，所以我们把用户和密码硬编码在程序中。 index.php代码： \u003c?php session_start(); define('USERNAME', 'zjl'); //定义常量，常量一般大写 define('PASSWORD', '123'); if (isset($_POST['loginForm'])) { $username = $_POST['name']; $password = $_POST['password']; if ($username === USERNAME \u0026\u0026 $password === PASSWORD) { $_SESSION['username'] = $username; header(\"Location: admin.php\"); } else { $status = \"Username or Password Incorrect.\"; } } ?\u003e \u003c!doctype html\u003e Login \u003c?php if (isset($status)): ?\u003e \u003c?=$status?\u003e \u003c?php endif ?\u003e admin.php的代码： \u003c?php // 防止用户直接访问页面，需要先判断是否有登录成功信息 session_start(); if (!isset($_SESSION['username'])) { header(\"Location: index.php\"); } ?\u003e \u003c!doctype html\u003e Admin Secret admin area. Welcome \u003c?=$_SESSION['username']?\u003e Logout logout.php代码 \u003c?php session_start(); session_destroy(); $_SESSION = []; header('Location: index.php'); ","date":"2016-03-17","heading":"Session","objectID":"/2016/03/php-fundamentals-2/:3:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#session"},{"categories":null,"collections":null,"content":"重构 我们的登录案例已经能够工作了，但是我们的代码是分散在各个文件中的，这不利于我们未来的开发，我们需要组织我们的代码更加结构化。 首先考虑我们的管理页面可能有多个页面，如果我们在每一个页面的前面都加上判断的话，一旦发生修改我们就崩溃了。所以将判断是否是登录用户的功能抽取出来，放在函数文件中。创建一个function.php的文件， \u003c?php function is_logged_in() { if (!isset($_SESSION['username'])) { header(\"Location: index.php\"); die(); } } 我们需要修改admin.php以便引入我们的函数库。 \u003c?php session_start(); include 'function.php'; is_logged_in(); ?\u003e 现在直接访问admin.php我们仍然回到了登录页面，good！ 但是现在我们的函数是一个硬编码的跳转，我们可以修改它更加通用一些 \u003c?php function is_logged_in() { session_start(); if (!isset($_SESSION['username'])) { return false; } else { return true; } } 修改admin.php \u003c?php session_start(); include 'function.php'; if (!is_logged_in()) { header(\"Location: index.php\"); die(); } 这样就好多了。但是我们的函数仅仅是根据变量是否存在而返回值，简化一下 function is_logged_in() { return isset($_SESSION['username']); } Bingo! 在index.php中，我们的用户民和密码是直接写在页面中的，不过更好的解决方法是将这些信息写在配置文件中。创建config.php \u003c?php define('USERNAME', 'zjl'); //定义常量，常量一般大写 define('PASSWORD', '123'); 修改index.php \u003c?php include 'config.php'; session_start(); if (isset($_POST['loginForm'])) { ... } ?\u003e 我们来重构登录方法，function.php function validate_user_creds($username, $password) { return ($username === USERNAME \u0026\u0026 $password === PASSWORD); } 修改index.php \u003c?php include 'config.php'; include 'function.php'; session_start(); if (isset($_POST['loginForm'])) { $username = $_POST['name']; $password = $_POST['password']; if (validate_user_creds($username, $password)) { $_SESSION['username'] = $username; header(\"Location: admin.php\"); } else { $status = \"Username or Password Incorrect.\"; } } 你可能看起来这样重构没什么太大的改变，但是考虑到将来我们的验证函数可能从数据库中读取信息或者是通过OAuth来进行认证，但是我们的登录页面是不关心的了，这样就进行了逻辑上的隔离。 ","date":"2016-03-17","heading":"重构","objectID":"/2016/03/php-fundamentals-2/:4:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#重构"},{"categories":null,"collections":null,"content":"Cookie cookie是存储在客户端（浏览器）中的数据。 \u003c?php setcookie('fontSize', 25); 在浏览器中浏览，然后检查一下浏览器（Inspect Element）中的信息。OK，现在我们删除cookie，然后改编代码 \u003c?php setcookie('fontSize', 25); echo $_COOKIE['fontSize']; 刷新浏览器，我们会遇到错误，没关系，这是因为现在cookie还不存在，再次刷新浏览器，就可以看见结果了。 我们可以利用cookie来记录一些信息。 \u003c?php setcookie('fontSize', 25); ?\u003e Cookie Sample Hi there, how are you? 现在我们就可以看到页面依据cookie的样式了。如果我们检查cookie信息，会发现cookie的生命周期是session，意味着关闭浏览器cookie就失效了。 setcookie('fontSize', 25，time() + 60 * 60); //设定超时1小时 怎么删除cookie呢？就是设置cookie时间为过去就好了 setcookie('fontSize', 25, time() - 60 * 60); 我们来看一个完整的例子 \u003c?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { setcookie('fontSize', (int)$_POST['fontSize'], time() + 60 * 60); header('Location: index.php'); } ?\u003e \u003c!doctype html\u003e Fonts Sample 10 20 30 40 My Page Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Typi non habent claritatem insitam; est usus legentis in iis qui facit eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod ii legunt saepius. Claritas est etiam processus dynamicus, qui sequitur mutationem consuetudium lectorum. Mirum est notare quam littera gothica, quam nunc putamus parum claram, anteposuerit litterarum formas humanitatis per seacula quarta decima et quinta decima. Eodem modo typi, qui nunc nobis videntur parum clari, fiant sollemnes in futurum. ","date":"2016-03-17","heading":"Cookie","objectID":"/2016/03/php-fundamentals-2/:5:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#cookie"},{"categories":null,"collections":null,"content":"创建一个网站计数器 我们通过一个文件来保存网站的访问者计数。创建一个文件，index.tmp.php作为一个我们需要的模板文件，别紧张，除了文件名以外没有什么特殊的地方，它会接收控制器传送来的数据并显示。 \u003c!doctype html\u003e My Website Welcome, my website! You are the X visitor to this website. 下面是index.php的代码 \u003c?php function set_count($file_name = 'counter.txt') { if (file_exists($file_name)) { //读取值 $handler = fopen($file_name, 'r'); //增加1 $count = (int)fread($handler, 20) + 1; //20是我们自行设定的一个长度 //写入新值 $handler = fopen($file_name, 'w'); fwrite($handler, $count); fclose($handler); } else { //创建文件 $handler = fopen($file_name, 'w+'); $count = 1; //写入初始值 fwrite($handler, $count); fclose($handler); } return $count; } $site_count = set_count(); require 'index.tmp.php'; 修改index.tmp.php显示计数器 You are the \u003c?=$site_count?\u003e visitor to this website. php提供了file_get_contents和file_put_contents函数来简化我们的工作 function set_count($file_name = 'counter.txt') { if (file_exists($file_name)) { $count = (int)file_get_contents($file_name) + 1; file_put_contents($file_name, $count); } else { $count = 1; file_put_contents($file_name, $count); } return $count; } php提供的file函数能够将文件内容作为数组一次读出，将counter.txt修改为 5 10 15 20 尝试下面的语句 print_r(file($file_name)); //文件内容按照数组输出 ","date":"2016-03-17","heading":"创建一个网站计数器","objectID":"/2016/03/php-fundamentals-2/:6:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#创建一个网站计数器"},{"categories":null,"collections":null,"content":"文件操作 \u003c?php //寻找与模式匹配的文件路径 $files = glob('r*.txt'); print_r($files); //['random.txt', 'really-cool-file.txt'] //所有的jpg文件 $images = glob('img/*.jpg'); //所有的jpg，png文件 $images = glob('img/*.{png,jpg,jpeg}', GLOB_BRACE); //千万不要在类型前面有空格 foreach ($images as $img) { echo $img; echo '\\r'; echo basename($img); //仅仅是文件名，不含目录名 echo dirname($img); //目录名 echo substr($img, 4); //略过前面四个字符 echo substr($img, -3); //后面的三个字符 print_r(pathinfo($img)); echo pathinfo($img, PATHINFO_EXTENSION); echo pathinfo($img, PATHINFO_FILENAME); } ","date":"2016-03-17","heading":"文件操作","objectID":"/2016/03/php-fundamentals-2/:7:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#文件操作"},{"categories":null,"collections":null,"content":"基于文件系统的电子邮件注册系统 我们准备写一个能够让用户注册的邮件列表系统。首相创建index.tmp.php模板文件。 \u003c!doctype html\u003e Email Registration Join the Mailing List Your Name: Your email address: 创建index.php控制器文件 \u003c?php require 'index.tmp.php'; 下面我们来处理表单。 \u003c?php if ($_SERVER['REQUEST_METHOD'] == 'POST') { $name = $_POST['name']; $email = $_POST['email']; } require 'index.tmp.php'; 避免用户输入的信息含有空格 $name = trim($_POST['name']); $email = trim($_POST['email']); 用户什么也没输入怎么办？继续处理 if ($_SERVER['REQUEST_METHOD'] == 'POST') { ... if (empty($name) || empty($email)) { $status = 'Please provide a valid name and email'; } } 在index.tmp.php中添加显示信息。 Join the Mailing List \u003c?=isset($status) ? $status : ''?\u003e ... 如果输入合法，我们可以来进行用户登录的处理，但最好新建一个functions.php文件来处理一些公共的函数，以避免我们的页面太过复杂。 \u003c?php function add_registered_user($name, $email) { file_put_contents('mailing_list.php', \"$name: $email\\n\", FILE_APPEND); //避免用户可以直接访问，应该使用'../mailing_list.php'以保证安全 } 修改index.php if (empty($name) || empty($email)) { $status = 'Please provide a valid name and email'; } else { add_registered_user($name, $email); $status = 'Thank for registration.'; } 如果用户输入的信息有问题，那么再次返回的时候，用户的信息就已经消失了，真不是一个好方法。修改index.tmp.php' 这样是不错的，但是我们的视图是不应该处理这么多的逻辑的。修改为 我们添加函数 function old($key) { if (!empty($_REQUEST[$key])) { //$_REQUEST 可以处理$_GET或者$_POST中的数据 return htmlspecialchars($_REQUEST[$key]); } return ''; } 现在我们的登录系统不能处理任何的合法性验证，我们来处理验证函数 function valid_email($email) { // return preg_match('/regular expression/', $email); return filter_var($email, FILTER_VALIDATE_EMAIL); } 在index.php中添加验证 if (empty($name) || empty($email) || !valid_email($email)) { ... 现在我们来完成显示列表的工作。在functions.php中添加读取用户的功能。 define('MAILING_LIST', 'mailing_list.php'); function add_registered_user($name, $email) { file_put_contents(MAILING_LIST, \"$name: $email\\n\", FILE_APPEND); //避免用户可以直接访问，应该使用'../mailing_list.php'以保证安全 } function get_registered_users($path = MAILING_LIST) { $users = file($path); if (count($users)) { return array_map(function($user) { return explode(': ', htmlspecialchars($user)); }, $users); } return false; } 创建registered_user.tmp.php作为模板 \u003c!doctype html\u003e Document Folks On Your Mailing List \u003c?php if ($registered_users): ?\u003e \u003c?php foreach ($registered_users as $user): ?\u003e \u003c?php list($name, $email) = $user; ?\u003e \u003c?=$name?\u003e: \u003c?=$email?\u003e \u003c?php endforeach ?\u003e \u003c?php else: ?\u003e There are no registered users. \u003c?php endif ?\u003e 创建registered_users.php作为控制器。 \u003c?php require 'functions.php'; $registered_users = get_registered_users(); require 'registered_users.tmp.php'; ","date":"2016-03-17","heading":"基于文件系统的电子邮件注册系统","objectID":"/2016/03/php-fundamentals-2/:8:0","tags":null,"title":"PHP 基础(二)","uri":"/2016/03/php-fundamentals-2/#基于文件系统的电子邮件注册系统"},{"categories":null,"collections":null,"content":"PHP安装 Mac：安装MAMP Windows：安装XAMPP Linux：独立安装各个组件 ","date":"2016-03-15","heading":"PHP安装","objectID":"/2016/03/php-fundamentals/:1:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#php安装"},{"categories":null,"collections":null,"content":"变量 我们来写一个基本的HTML \u003c!DOCTYPE html\u003e My Website My Website 在HTML h1后面增加一个PHP的输出语句， \u003c?php echo 'Hello, World'; ?\u003e 在浏览器中查看结果。对服务器来说，所有的HTML标记会原封不动的发送到客户端，但是一旦遇到了\u0026lt;?php的 开始标记，就会交给PHP处理程序进行处理，然后将结果发送回浏览器。在浏览器中查看源代码，我们只会看见 PHP的处理结果，而不会看见php的语句。 对php来说，我们也可以使用print来进行输出，不过使用echo在php的世界中更常用一些。字符串可以使用 单引号或者双引号。 \u003c?php $name = 'Zhang Jinglin'; echo $name; ?\u003e php中的变量使用$开始作为变量开始。 我们来看一下单引号和双引号之间的区别。 $name = 'zhangjinglin'; echo \"Hello, $name\"; //Hello, zhangjinglin echo 'Hello, $name'; //Hello, $name 对php来说，双引号字符串中的变量是可以被解析的，单引号中的变量不被解析，直接原样输出。对于 不含变量的字符串来说，单双引号没有区别。 php使用.作为字符串连接符。 echo 'hello, ' . $name; echo \"hello, \" . $name; 上面的语句结果相同。 php是和HTML结合在一起使用的。 \u003c?php $name = 'zhangjinglin'; ?\u003e Hello, \u003c? echo $name; ?\u003e 一定要注意上面的\u0026lt;?php与?\u0026gt;的成对出现。 如果我们只是想输出一个变量，而不是执行一个语句，可以使用php的简洁写法。 Hello, \u003c?=$name?\u003e 当然也可以直接使用php来输出与HTML的结合。 \u003c?php $name = 'zhangjinglin'; echo \"Hello, $name \"; ?\u003e ","date":"2016-03-15","heading":"变量","objectID":"/2016/03/php-fundamentals/:2:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#变量"},{"categories":null,"collections":null,"content":"显示错误信息 下面的语句会出项错误，但是我们什么也看不到 \u003c? echo hello, world!; ?\u003e 在php.ini的配置中，我们需要设定： display_errors = On 当然，这个应该在开发环境中，在生产环境中需要设置为Off. 在XAMPP中，默认为On，但在其他环境中， 可能需要自行设定。 ","date":"2016-03-15","heading":"显示错误信息","objectID":"/2016/03/php-fundamentals/:3:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#显示错误信息"},{"categories":null,"collections":null,"content":"数组 比如我们想定义12个月的变量，当然可以使用$jan $feb ...去定义，不过最好的方式是使用数组。 $months = array('januara', 'february', 'march', 'april', 'may'); var_dump($months); var_dump是用于输出数组的函数，不能使用echo输出数组。 echo $months[4]; //may echo $months[0]; //januara 也可以使用php的数组简写定义方式(php 5.4 以上版本)，和print_r来输出数组。 $months = ['januara', 'february', 'march', 'april', 'may']; print_r($months); 在php中输出数组与html结合的方式。 \u003c?php $months = ['januara', 'february', 'march', 'april', 'may']; foreach ($months as $month) { echo \"$month\"; } ?\u003e php也可以定义在不同的位置上，我们在文件头部定义变量，这次定义一个关联数组。 \u003c?php $sites = array( 'baidu' =\u003e 'www.baidu.com', 'google' =\u003e 'www.google.com', 'bing' =\u003e 'www.bing.com' ); ?\u003e \u003c!DOCTYPE html\u003e My Website \u003c?php foreach($sites as $name =\u003e $url) { echo \"$name, $url\"; } ?\u003e 我们也可以更进一步，将输出改为： echo \"$name\"; php也提供了短写的写法，能够简化foreach if的语句复杂度，修改上面的语句为 \u003c?php foreach($sites as $name =\u003e $url): ?\u003e \u003c?=$name?\u003e \u003c?php endforeach ?\u003e 这样，写起来会舒服一些。同样，if的写法： \u003c?php if (xxx): ?\u003e //html \u003c?php else: ?\u003e //html \u003c?php endif ?\u003e array_push($arr, $val) 向数组中添加数据，更简单的方法是$arr[] = $val。 对关联数组来说，使用$arr[$key] = $val。 array_pop($arr)删除最后一个数据，并返回删除数据。 array_shift($arr)删除开始的数据，并返回删除数据。 array_unshift($arr, $val) - 在数组开头插入一个或多个单元 在php.net中可以搜索所有的函数。 查看array_filter()函数，并掌握用法。 ","date":"2016-03-15","heading":"数组","objectID":"/2016/03/php-fundamentals/:4:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#数组"},{"categories":null,"collections":null,"content":"条件语句 if语句的格式 \u003c?php if() { //do something } esleif() { //do other } else { //do something else } 一个简单的例子 \u003c?php $name = 'zjl'; if(strlen($name) == 3) { echo 'Your name length is 3'; } else { echo 'Your name length is not 3'; } php中的判等有两种方式，注意类型 '3' == 3 // true '3' === 3 // false php中认为0, false都是假值，1, true, 已定义的变量都是真值。 \u003c?php $name = 'zjl'; $val = 0; if ($name) echo 'true'; //true if ($val) echo 'true'; //false 请自行了解switch。 可以使用isset来判断变量是否定义。 \u003c?php echo isset($name) ? \"Hello, $name\" : 'Noname'; ","date":"2016-03-15","heading":"条件语句","objectID":"/2016/03/php-fundamentals/:5:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#条件语句"},{"categories":null,"collections":null,"content":"循环 使用foreach来遍历数组。 foreach($arr as $item) { ... } foreach($arr as $key =\u003e $val) { ... } for语句也可以使用。 for ($i = 0; $i \u003c count($arr); $i++) { ... } ","date":"2016-03-15","heading":"循环","objectID":"/2016/03/php-fundamentals/:6:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#循环"},{"categories":null,"collections":null,"content":"文件包含 在实际的工作中，我们可能有一些文件的很多内容是相同的，比如说多个页面都有相同的页面布局， 或者我们需要一些公共的函数库，这就可以使用PHP的文件包含功能。 比如我们创建一个文件，叫做header.php \u003c!DOCTYPE html\u003e Include My Website 这个文件包含了HTML模板头部，可以在多个文件中使用。 我们再创建一个footer.php文件，包含模板的尾部。 Copyright Infomation 现在我们就可以定义多个文件来使用了。创建文件index.php。 \u003c?php include 'header.php'; ?\u003e Main content. \u003c?php include 'footer.php'; ?\u003e 再创建一个about.php文件，也使用包含文件 \u003c?php include 'header.php'; ?\u003e About content. \u003c?php include 'footer.php'; ?\u003e 可见，我们简化了很多的工作。 出了可以使用include也可以使用require来进行文件包含，两者的区别是，include如果没有找到文件 会发出警告并继续执行，而require会出现错误并停止执行。 还有一个require_once能够保证仅仅包含一次文件，即使你写了多个。比如你写了一个类B文件，依赖 另外一个类A，有可能其他的类C也依赖类A，当同时使用类B和类C就可能引入两次类A，所以使用require_once 就是一个好的选择。 ","date":"2016-03-15","heading":"文件包含","objectID":"/2016/03/php-fundamentals/:7:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#文件包含"},{"categories":null,"collections":null,"content":"函数和变量作用域 在php中使用function关键字来定义函数，函数可以包含参数和返回值。 \u003c?php function say_hello() { return 'Hi, there'; } echo say_hello(); 当然我们也可以存储函数的返回值 $greeting = say_hello(); echo $greeting; 函数中添加参数 \u003c?php function say_hello($name) { return \"Hi, $name\"; } $greeting = say_hello('zhang jinglin'); echo $greeting; 如果函数有参数，但是调用的时候没有调用参数则会出错。我们也可以给函数参数一个默认值，这样， 调用的时候就不一定要赋值了。 function say_hello($name = 'nobody') { return \"Hi, $name\"; } 我们可以使用print_r来输出数组信息，不过显示的结果并不是和直观，所以我们可以写一个工具 函数来输出数组或者变量的信息。 \u003c?php function pp($value) { echo ''; print_r($value); echo ''; } $arr = ['name' =\u003e 'zjl', 'age' =\u003e 43, 'ocupation' =\u003e 'teacher']; pp($arr); 假设我们有一个这样一个数组，数组的每个元素是一个包含个人信息的数组，也就是这样 $people = [ ['name' =\u003e 'zjl', 'age' =\u003e 43, 'occupation' =\u003e 'Teacher'], ['name' =\u003e 'zhao si', 'age' =\u003e 50, 'occupation' =\u003e 'Actor'], ['name' =\u003e 'wang wu', 'age' =\u003e 21, 'occupation' =\u003e 'Student'] ]; 现在我们希望能有一个函数，能够根据我们提供的关键字来抽取出一个数组，比如抽取出所有的名字 组成的数组，也就是希望 array_pluck('name', $people); //['zjl', 'zhao si', 'wang wu'] 我们来实现上面的函数 function array_pluck($toPluck, $arr) { $ret = []; foreach($arr as $item) { $ret[] = $item[$toPluck]; } return $ret; } 我们现在利用php提供的array_map来实现我们上面的函数。 function array_pluck($toPluck, $arr) { return array_map(function($item) { return $item[$toPluck]; }, $arr); } 上面的函数将导致错误，说$toPluck是一个没有定义的变量。在php中，变量的作用域仅仅在函数中 ，这意味着我们的array_map的调用函数中是不存在$toPluck这个变量的。为了使用函数外部的变量， 我们需要修改代码 function array_pluck($toPluck, $arr) { return array_map(function($item) use($toPluck) { return $item[$toPluck]; }, $arr); } 上面代码中的use($toPluck)将函数外部的变量带入到了函数中。 ","date":"2016-03-15","heading":"函数和变量作用域","objectID":"/2016/03/php-fundamentals/:8:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#函数和变量作用域"},{"categories":null,"collections":null,"content":"对象 我们来看一下php的对对象定义 \u003c?php class Person { public $name; public $job; //构造函数 public function __construct($name, $job) { $this-\u003ename = $name; $this-\u003ejob = $job; } public function introduction() { return \"I'm $this-\u003ename, a $this-\u003ejob\"; } } $p = new Person('zjl', 'teacher'); echo $p-\u003eintroduction(); php也提供了一个stdClass的类模板，我们可以直接使用。 \u003c?php $person = new stdClass; $person-\u003ename = 'zjl'; $person-\u003ejob = 'teacher'; var_dump($person); echo $person-\u003ename; 在使用stdClass中要注意的问题是，不能使用方法，如果必须使用方法的话，就需要自定义类。 还可以使用强制类型转换来将数组转换为stdClass。 \u003c?php $person = [ 'name' =\u003e 'zjl', 'job' =\u003e 'teacher' ]; var_dump($person); var_dump((object)$person); $o = (object)$person; echo $person['name']; echo $o-\u003ejob; ","date":"2016-03-15","heading":"对象","objectID":"/2016/03/php-fundamentals/:9:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#对象"},{"categories":null,"collections":null,"content":"heredocs 我们来看一下下面的代码： \u003c?php $post = [ 'author' =\u003e 'zjl', 'title' =\u003e 'My Awesome Post', 'body' =\u003e 'Here is the body', 'publish-date' =\u003e '2016-3-15' ]; // 下面的语句会出错，对数组不能直接这样写 // $email = \"$post['title']\"; // 必须修改成带有{}的样式 $email = \"{$post['title']}By: {$post['author']} {$post['body']}\"; echo $email; 如果我们的内容更加复杂的话，$email的赋值语句会让我们崩溃的。我们可以稍微修改一下 $email = \"{$post['title']}\"; $email .= \"By: {$post['author']} \"; $email .= \"{$post['body']}\"; 这样好一些，但仍然不会是一个好的解决方案。我们再进一步，使用php提供的sprintf()函数来格式化我们 要输出的信息。 $email = sprintf(\"%sBy: %s %s\", $post['title'], $post['author'], $post['body']); 有进步，但仍然不是很好。幸好，php提供了heredocs的功能。 $email = \u003c\u003c","date":"2016-03-15","heading":"heredocs","objectID":"/2016/03/php-fundamentals/:10:0","tags":null,"title":"PHP 基础(一)","uri":"/2016/03/php-fundamentals/#heredocs"},{"categories":null,"collections":null,"content":"如何设置Ubuntu Server和PHP开发环境的配置 ","date":"2016-03-08","heading":"","objectID":"/2016/03/ubuntu-php/:0:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#"},{"categories":null,"collections":null,"content":"下载并安装 VirtualBox ","date":"2016-03-08","heading":"下载并安装","objectID":"/2016/03/ubuntu-php/:1:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#下载并安装-virtualbox"},{"categories":null,"collections":null,"content":"下载 ubuntu-14.10-server-amd64.iso ","date":"2016-03-08","heading":"下载 ubuntu-14.10-server-amd64.iso","objectID":"/2016/03/ubuntu-php/:2:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#下载-ubuntu-1410-server-amd64iso"},{"categories":null,"collections":null,"content":"建立虚拟机并安装 Ubuntu Server 设定网络模式为 Networl Brige，网桥模式 安装，服务都不选，我们自行安装，不选择自动更新 ","date":"2016-03-08","heading":"建立虚拟机并安装 Ubuntu Server","objectID":"/2016/03/ubuntu-php/:3:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#建立虚拟机并安装-ubuntu-server"},{"categories":null,"collections":null,"content":"第一次启动虚拟机 1 建立虚拟机快照 2 检查网络是否配置成功 ip add 3 如果没有DHCP，请设定静态地址 sudo vi /etc/network/interfaces #静态地址设置方法 auto eth0 iface eth0 inet static address x.x.x.x netmask x.x.x.x gateway x.x.x.x #设定虚拟ip auto eth0:1 iface eth0 inet static address x.x.x.x netmask x.x.x.x up route add -net x.x.x.x/x gw x.x.x.x 4 重启网络，检查是否成功 # problem # sudo /etc/init.d/networking restart sudo ifdown eth0 sudo ifup eth0 sudo ifdown eth0:1 sudo ifup eth0:1 ping x.x.x.x 5 设定主机名称 sudo vi /etc/hostname 6 设定dns sudo vi /etc/resolv.conf sudo vi /etc/resolvconf/resolv.conf.d/base nameserver x.x.x.x sudo resolvconf -u ","date":"2016-03-08","heading":"第一次启动虚拟机","objectID":"/2016/03/ubuntu-php/:4:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#第一次启动虚拟机"},{"categories":null,"collections":null,"content":"更新系统 修改软件源 #备份原始配置文件 sudo cp /etc/apt/sources.list /etc/apt/sources.list.ori sudo vi /etc/apt/sources.list #将us修改为cn 更新系统 sudo apt-get update sudo apt-get dist-upgrade ","date":"2016-03-08","heading":"更新系统","objectID":"/2016/03/ubuntu-php/:5:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#更新系统"},{"categories":null,"collections":null,"content":"SSHD 服务 安装openssh-server sudo apt-get install openssh-server #确保显示 ps -e | grep sshd ssh到虚拟机 ssh xxx.xxx.xxx.xxx ","date":"2016-03-08","heading":"SSHD 服务","objectID":"/2016/03/ubuntu-php/:6:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#sshd-服务"},{"categories":null,"collections":null,"content":"安装 MySQL 安装 MySQL sudo apt-get install mysql-server 检查是否启动 ps -e | grep mysql #确保出现 mysqld 在生产环境中，设置数据库安全，本地使用可略过 sudo mysql_secure_installation 测试数据库 mysql -u root -p ","date":"2016-03-08","heading":"安装 MySQL","objectID":"/2016/03/ubuntu-php/:7:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#安装-mysql"},{"categories":null,"collections":null,"content":"安装 Apache 安装 apache sudo apt-get install apache2 #检查Apache服务是否启动 ps -e | grep apa 主机浏览器虚拟机查看测试页面 ","date":"2016-03-08","heading":"安装 Apache","objectID":"/2016/03/ubuntu-php/:8:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#安装-apache"},{"categories":null,"collections":null,"content":"安装 PHP 安装 PHP sudo apt-get install php5 测试 PHP php --version 重新启动Apache sudo /etc/init.d/apache2 restart 测试PHP与Apache #新建测试文件 sudo vi /var/www/html/info.php info.php \u003c?php phpinfo(); ?\u003e 增加PHP-MYSQL支持 sudo apt-get install php5-mysql sudo /etc/init.d/apache2 restart ","date":"2016-03-08","heading":"安装 PHP","objectID":"/2016/03/ubuntu-php/:9:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#安装-php"},{"categories":null,"collections":null,"content":"安装 Composer 安装 cd /tmp/ curl http://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer #测试 composer --version 修改composer为中国镜像 #找到composer的配置文件 composer config -l -g 修改配置文件为： { \"config\": { }, \"repositories\": [ {\"type\": \"composer\", \"url\": \"http://pkg.phpcomposer.com/repo/packagist/\"}, {\"packagist\": false} ] } ## 安装 PHP Mcrypt 模块 1. 安装 php5-mctypt ```bash sudo apt-get install php5-mcrypt 开启模块 sudo php5enmod mcrypt 重启apache ","date":"2016-03-08","heading":"安装 Composer","objectID":"/2016/03/ubuntu-php/:10:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#安装-composer"},{"categories":null,"collections":null,"content":"设置共享文件夹 在虚拟机设置中增加共享文件夹 正常启动虚拟机 选择device-\u0026gt;Insert Guest Addtions CD Images 挂载光盘 sudo mount /dev/cdrom /media/cdrom/ 安装必要软件 sudo apt-get install gcc make 安装增强工具 sudo /media/cdrom/VBoxLinuxAdditions.run 挂载共享文件夹 cd ~ mkdir host sudo mount -t vboxsf -o uid=$UID,gid=$(id -g) xxxxx(共享名称) host ","date":"2016-03-08","heading":"设置共享文件夹","objectID":"/2016/03/ubuntu-php/:11:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#设置共享文件夹"},{"categories":null,"collections":null,"content":"(可选）设置PHPStorm/WebStorm开发 修改权限 sudo chown 用户名:用户组 /var/www/html sudo chown 用户名:用户组 /var/www/html/* 启动PHPStorm/， 选择 Create New Project form Existing Files，然后选择 Web server is on remote host, files are accessible via FTP/SFTP/FTPS，下一步后，设置项目名称和本地存储位置，在Specify parameters for a new server 中选择sftp，并设置参数，主机、用户名、口令，然后 Test SFTP connection，测试成功后选择root path，这里是/var/www/html。 设定完成后，在phpstorm/webstorm 中设置：Upload changed files automatically to the default server: On explicit save action(command+s)，你可以自行选择。 测试吧。 ","date":"2016-03-08","heading":"(可选）设置PHPStorm/WebStorm开发","objectID":"/2016/03/ubuntu-php/:12:0","tags":null,"title":"Ubuntu PHP 开发环境配置","uri":"/2016/03/ubuntu-php/#可选设置phpstormwebstorm开发"},{"categories":null,"collections":null,"content":"一直就在写博客，不过一直没有合适的博客系统，所以只是在github和coding上记录自己的学习过程。这个春节终于构建了自己的博客，可以放下心来认真的书写了。 这么多年来的学习，这么多年对技术的热爱，已经成为了生命中的一部分，如果你看到我的博客，对你的技术有一些帮助，我就会很开心，不过，即使没有人看，也没关系，这是我的爱好和我的兴趣的源泉。 嘿，春节快乐！ ","date":"2016-03-05","heading":"","objectID":"/2016/03/first/:0:0","tags":null,"title":"基于Hexo和Next的第一篇博客","uri":"/2016/03/first/#"},{"categories":null,"collections":null,"content":"这篇文章中，将常见的一些安卓功能实现做一个简单的说明，以便在以后开发中随时查看。 ","date":"2016-02-26","heading":"","objectID":"/2016/02/android-example/:0:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#"},{"categories":null,"collections":null,"content":"动画 ","date":"2016-02-26","heading":"动画","objectID":"/2016/02/android-example/:1:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#动画"},{"categories":null,"collections":null,"content":"alpha动画 可以通过设定对象的alpha动画来实现对象的透明度，如果对象原先的alpha设定为0，则可以逐渐显示 ImageView img = (ImageView)findViewById(R.id.xxx); img.animate() .alpha(0f) .setDuration(2000); ","date":"2016-02-26","heading":"alpha动画","objectID":"/2016/02/android-example/:1:1","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#alpha动画"},{"categories":null,"collections":null,"content":"位置动画 设定x和y的坐标来进行位置上的变化，translationYBy, translationXBy img.animate() .tanslationYBy(1000f) .setDuration(2000); ","date":"2016-02-26","heading":"位置动画","objectID":"/2016/02/android-example/:1:2","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#位置动画"},{"categories":null,"collections":null,"content":"旋转动画 通过rotation(xxxf)来完成旋转动画 ","date":"2016-02-26","heading":"旋转动画","objectID":"/2016/02/android-example/:1:3","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#旋转动画"},{"categories":null,"collections":null,"content":"缩放动画 通过scaleX, scaleY来完成缩放动画 ","date":"2016-02-26","heading":"缩放动画","objectID":"/2016/02/android-example/:1:4","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#缩放动画"},{"categories":null,"collections":null,"content":"联和动画 以上的动画可以合并在一起，完成特殊的动画动能。 img.animate() .translationXBy(1000f) .translationYBy(1000f) .rotationBy(3600) .setDuration(2000); ","date":"2016-02-26","heading":"联和动画","objectID":"/2016/02/android-example/:1:5","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#联和动画"},{"categories":null,"collections":null,"content":"播放视频 将视频文件放置在res/raw下面 添加VideoView控件 关键是找到视频的路径 videoView.setVideoPath = \"android.resource://\" + getPackageName() + \"/\" + R.raw.demovideo); videoView.start(); 为视频播放添加控制 MediaController media = new MediaController(this); media.setAnchorView(videoView); videoView.setMediaController(media); ... ","date":"2016-02-26","heading":"播放视频","objectID":"/2016/02/android-example/:2:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#播放视频"},{"categories":null,"collections":null,"content":"播放音频 将音频文件放置在res/raw下面 使用MediaPlayer播放音频 MediaPlayer mplayer = MediaPlayer.create(this, R.raw.mp3); mplayer.start(); 可以调用mplayer.pause() mplayer.stop()来暂停和停止播放。 ","date":"2016-02-26","heading":"播放音频","objectID":"/2016/02/android-example/:3:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#播放音频"},{"categories":null,"collections":null,"content":"控制音量 控制音量主要是通过AudioManager来进行的 //获取服务 AudioManager audioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE); //获取音乐播放最大音量 int maxVolume = audioManger.getStreamMaxVolume(AudioManager.STREAM_MUSIC); //获取当前系统播放音量 int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); //可以通过SeekBar来进行音量的设置 audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, progress.value, 0); ","date":"2016-02-26","heading":"控制音量","objectID":"/2016/02/android-example/:3:1","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#控制音量"},{"categories":null,"collections":null,"content":"控制播放进度 可以通过MediaPlayer来获取当前播放的音乐的信息，并设置播放进度 //获取音乐长度 mplayer.getDuration(); //通过定时器任务来更新进度条的位置 new Timer().scheduleAtFixedRate(new TimerTask() { @Override public void run() { scrubber.setProgress(mplayer.getCurrentPosition()); } }, 0, 100); //立即开始，每隔100毫秒调用一次 //可以拖拽进度挑来进行定位 mplayer.seekTo(progress.value); ","date":"2016-02-26","heading":"控制播放进度","objectID":"/2016/02/android-example/:3:2","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#控制播放进度"},{"categories":null,"collections":null,"content":"简单的ListView ListView是移动客户端最常见的组件，能够显示很多的条目。 ListView listView = (ListView)findViewById(R.id.listView); ArrayList myFriend = new ArrayList\u003c\u003e(); myFriend.add(\"Zhang San\"); myFriend.add(\"Li si\"); myFriend.add(\"Wang wu\"); myFriend.add(\"Zhao liu\"); myFriend.add(\"Liu qi\"); ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, myFriend); listView.setAdapter(arrayAdapter); 设定点击条目的事件 //设置点击条目的事件 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView\u003c?\u003e parent, View view, int position, long id) { Log.i(\"ZJL\", myFriend.get(position)); } }); 在设置字符串数组中，可以应用asList的便捷方法来进行处理，这需要静态导入java.util.Arrays.asList，并将myFrind设置为final对象 import static java.util.Arrays.asList; final ArrayList myFriend = new ArrayList(asList(\"Zhang San\", \"Li si\", \"Wang wu\", \"Zhao liu\", \"Liu qi\")); ","date":"2016-02-26","heading":"简单的ListView","objectID":"/2016/02/android-example/:4:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#简单的listview"},{"categories":null,"collections":null,"content":"安卓中的定时任务 可以使用Handler和Runnable来构造一个定时执行的任务。 //Handler 负责进行处理一个定时运行的任务 final Handler handler = new Handler(); //定义要定时运行的任务 Runnable run = new Runnable() { //实际运行的代码在run方法中 @Override public void run() { //模拟实际运行的代码 Log.i(\"ZJL\", \"a second must have passed...\"); //handler负责1秒后继续运行任务 handler.postDelayed(this, 1000); } }; //首次开始运行 handler.post(run); 安卓也提供了一种叫做CountDownTimer的基础类，来让我们完成一种倒计时的任务。 //倒计时10秒，每秒触发任务 new CountDownTimer(10000, 1000) { @Override public void onTick(long millisUntilFinished) { Log.i(\"ZJL\", \"Seconds left: \" + millisUntilFinished / 1000); } @Override public void onFinish() { Log.i(\"Zjl\", \"Countdown Timer Finished\"); } }.start(); 对CountDownTimer来说，在第一次onTick方法调用的时候，实际上已经经过了1秒，为了精确定时，我们可以设定总时间稍微多一些毫秒。 new CountDownTimer(10000 + 100, 1000) ","date":"2016-02-26","heading":"安卓中的定时任务","objectID":"/2016/02/android-example/:5:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#安卓中的定时任务"},{"categories":null,"collections":null,"content":"显示和隐藏控件 view.setVisibility(View.VISIBLE);//显示控件 view.setVisibility(View.INVISIBEL);//隐藏控件，但任然占用空间 ","date":"2016-02-26","heading":"显示和隐藏控件","objectID":"/2016/02/android-example/:6:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#显示和隐藏控件"},{"categories":null,"collections":null,"content":"安卓中的异步任务 安卓中的异步任务可以简单的通过AsyncTask来完成。在MainActivity中添加内部类定义 //第一个参数表示异步类的调用参数类型，第二个表示异步类中间结果的返回类型，第三个参数表示最终的结果返回类型 class DownloadTask extends AsyncTask { @Override protected String doInBackground(String... params) { StringBuilder result = new StringBuilder(); try { URL url = new URL(params[0]); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); InputStreamReader isr = new InputStreamReader(httpURLConnection.getInputStream()); int data; while ((data = isr.read()) != -1) { result.append((char)data); } } catch (IOException e) { e.printStackTrace(); } Log.i(\"ZJL\", \"return result\"); return result.toString(); } } 在MainActivity中可以调用我们的异步类 DownloadTask task = new DownloadTask(); String result = null; try { result = task.execute(\"http://www.ldxy.cn\").get(); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } Log.i(\"ZJL\", result); 因为涉及到网络访问，需要在AndroidManifest.xml中声明权限 ","date":"2016-02-26","heading":"安卓中的异步任务","objectID":"/2016/02/android-example/:7:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#安卓中的异步任务"},{"categories":null,"collections":null,"content":"利用异步任务下载图像 使用异步任务也可以完成图像的下载 //注意返回值是bitmap class DownloadTask extends AsyncTask { @Override protected Bitmap doInBackground(String... params) { try { HttpURLConnection httpURLConnection = (HttpURLConnection) (new URL(params[0])).openConnection(); return BitmapFactory.decodeStream(httpURLConnection.getInputStream()); } catch (IOException e) { e.printStackTrace(); } return null; } } 调用的方式是： ImageView imageView = (ImageView) findViewById(R.id.imageView); Bitmap myImage = null; try { myImage = (new DownloadTask()).execute(\"图像地址\").get(); imageView.setImageBitmap(myImage); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } ","date":"2016-02-26","heading":"利用异步任务下载图像","objectID":"/2016/02/android-example/:7:1","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#利用异步任务下载图像"},{"categories":null,"collections":null,"content":"处理JSON数据 安卓可以使用JSONObject来处理网络获取到的JSON数据。 JSONObject jsonObject = new JSONObject(result); jsonObject.getString(\"xxxKey\"); JSONArray arr = new JSONArray(jsonString); for (int i = 0; i \u003c arr.length(); i++) { JSONObject item = arr.getJSONObject(i); //process item } ","date":"2016-02-26","heading":"处理JSON数据","objectID":"/2016/02/android-example/:8:0","tags":null,"title":"安卓功能案例","uri":"/2016/02/android-example/#处理json数据"},{"categories":null,"collections":null,"content":"我们来完成一个完整的登录项目。 ","date":"2016-02-12","heading":"","objectID":"/2016/02/codeigniter-3/:0:0","tags":null,"title":"CodeIgniter教程 - 登录项目","uri":"/2016/02/codeigniter-3/#"},{"categories":null,"collections":null,"content":"准备工作 我们不会从头创建我们的样式，我们使用流行的CSS框架Bootstrap来进行构建。下载bootstrap，然后在项目中新建一个目录，目录的名字叫做public可能是一个好的选择。在public下面，新建文件夹css fonts js，参考下面的图片，别忘了必备的jQuery。 ","date":"2016-02-12","heading":"准备工作","objectID":"/2016/02/codeigniter-3/:1:0","tags":null,"title":"CodeIgniter教程 - 登录项目","uri":"/2016/02/codeigniter-3/#准备工作"},{"categories":null,"collections":null,"content":"创建模板 我们不想每个页面都从头构建，最佳的方式是创建模板。创建views\\layouts目录。 在模板文件中如何引入css和js文件呢？路径是什么？我们可以使用CI提供的base_url()函数来获取项目的URL，当然这个值你也可以在config.php中进行修改。引入css类似这样的语句： base_url()函数返回的结果带有/路径，所以在public之前不要在添加/，以免画蛇添足。 这个函数不是马上就能使用的，它是CI提供的URL辅助功能，需要在autoload.php中开启这个支持。 $autoload['helper'] = array('url'); 在下面创建布局文件，我们起名叫做main.php。 \u003c!doctype html\u003e Login Project this is the side bar. \u003c?php $this-\u003eload-\u003eview($content_view); ?\u003e 上面的代码一目了然，最关键的就是引入内容视图的语句，那么如何给模板传送数据呢？我们修改我们的Home.php \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:27 */ class Home extends CI_Controller { public function index() { $data['content_view'] = 'home_index'; $this-\u003eload-\u003eview('layouts/main', $data); } } 在控制器的index方法中，我们返回的是模板文件，但是我们传入了要现实的内容视图的变量，这样就完成了模板和内容的结合。 home_index.php的内容如下： Hello, CodeIgniter! 是的，就这么简单，其他的已经通过模板来完成了。访问http://localhost/ci/index.php/home应该可以看到结果，打开浏览器控制台，确保没有错误，所有的css和js正确加载了。 ","date":"2016-02-12","heading":"创建模板","objectID":"/2016/02/codeigniter-3/:2:0","tags":null,"title":"CodeIgniter教程 - 登录项目","uri":"/2016/02/codeigniter-3/#创建模板"},{"categories":null,"collections":null,"content":"侧边栏 在侧边栏我们想显示登录窗口，这就涉及到了CI的表单控制功能，所以我们要在autoload.php中开启我们的表单辅助功能。 $autoload['helper'] = array('url', 'form'); 我们在views目录下创建子目录user，然后创建文件login_view.php。 登录 \u003c?=form_open('user/login', ['id' =\u003e 'login_form', 'class' =\u003e ''])?\u003e \u003c?=form_close()?\u003e 我们使用了form_open和form_close()辅助函数，当然可以直接使用普通的HTML表单方式，但是推荐用这种方式以避免书写完整的URL提交地址。在CI的表单辅助函数中，也包含了一些字段的辅助函数，上面的HTML字段也可以替换成下面的辅助函数，使用哪个，取决于你自己。 \u003c?=form_input(['id' =\u003e 'name', 'name' =\u003e 'name', 'class' =\u003e 'form-control', 'placeholder' =\u003e '用户名'])?\u003e \u003c?=form_password(['id' =\u003e 'password', 'name' =\u003e 'password', 'class' =\u003e 'form-control', 'placeholder' =\u003e '密码'])?\u003e \u003c?=form_submit(['name' =\u003e 'submit', 'value' =\u003e '登录', 'class' =\u003e 'btn btn-primary btn-block'])?\u003e 在我们的表单中，我们声明了表单的提交地址是user/login，就是User控制器的login方法。在User.php控制器中添加login方法。 \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:50 */ class User extends CI_Controller { public function __construct() { parent::__construct(); $this-\u003eload-\u003emodel('user_model'); } public function login() { echo $_POST['name']; echo ' '; echo $_POST['password']; } } 目前我们的登录方法仅仅是简单的将输入数据显示出来。测试一下吧。login方法可以使用CI的功能来进行一致化，修改代码： public function login() { echo $this-\u003einput-\u003epost('name'); echo ' '; echo $this-\u003einput-\u003epost('password'); } ","date":"2016-02-12","heading":"侧边栏","objectID":"/2016/02/codeigniter-3/:3:0","tags":null,"title":"CodeIgniter教程 - 登录项目","uri":"/2016/02/codeigniter-3/#侧边栏"},{"categories":null,"collections":null,"content":"表单数据验证 CI提供了表单验证的功能，帮助我们来进行数据的验证。首先，要在autoload.php中装载表单验证 $autoload['libraries'] = array('database', 'form_validation'); 下面我们就可以修改User.php中的login方法，让它支持验证了。 public function login() { $this-\u003eform_validation-\u003eset_rules('name', '用户名', 'trim|required|min_length[3]'); $this-\u003eform_validation-\u003eset_rules('password', '密码', 'trim|required|min_length[3]'); if ($this-\u003eform_validation-\u003erun() == FALSE) { print_r(validation_errors()); } } 上面的rules是CI提供的验证规则，trim是去掉输入的空格，required是必须提供输入，min_length是最小的输入长度，实际上CI提供的rules非常多，在CI的类库参考-表单验证类中可以找到全部的规则。你可以尝试着什么也不输入直接提交，或者输入一个自负的情况下提交。 上面的处理有一个问题，输出的错误信息是英文，我们不能指望我们的访问者读懂英语，毕竟我们说中文。那要怎么处理呢？这就涉及到了CI应用的语言问题。解决这个有几种方案。 第一，在application\\config\\config.php中，我们可以把$config['language'] = 'english';修改为chinese表示我们要说中文。如果现在你在重新提交表单，你会发现CI出现了错误，错误信息是找不到语言文件 language/chinese/form_validation_lang.php。这个文件我们怎么创建呢？我们复制system/language/english/form_validation-lang.php到目录application/language/chinese/下，什么，没有这个目录？你自己创建一个就完了。我们修改一个提示坐下测试 $lang['form_validation_required'] = '{field} 必须输入'; 现在表单里面什么也不写，直接提交就看到中文的错误提示。 第二种方法，就是我们只需要几个中文的错误信息，没必要向上一种方法那么复杂，而且有些错误信息要单独的提示，怎么办？我们可以直接在可能输出错误信息的方法里面提供我们自己的错误说明。 public function login() { $this-\u003eform_validation-\u003eset_rules('name', '用户名', 'trim|required|min_length[3]', [ 'required' =\u003e '你不写用户名我知道你是谁', 'min_length' =\u003e '没哟这么短的名字' ]); $this-\u003eform_validation-\u003eset_rules('password', '密码', 'trim|required|min_length[3]'); if ($this-\u003eform_validation-\u003erun() == FALSE) { print_r(validation_errors()); } } 也就是我们直接在规则里面添加我们的错误信息即可。你愿意用哪种方法你自己随意吧。 获取到了错误信息，我们不能直接这么粗暴的显示出来，正常的情况是我们返回登录页面然后在上面显示错误信息。但这就涉及到了跳转，也就是从User控制器跳转到Home控制器，然后传送我们的错误信息，我们需要在autoload.php中开启session。 $autoload['libraries'] = array('database', 'form_validation', 'session'); 在CI中可以使用session中的flashdata作为一次性的输出信息，也就是输出一次，下次请求就没有了。 public function login() { $this-\u003eform_validation-\u003eset_rules('name', '用户名', 'trim|required|min_length[3]', [ 'required' =\u003e '你不写用户名我知道你是谁', 'min_length' =\u003e '没哟这么短的名字' ]); $this-\u003eform_validation-\u003eset_rules('password', '密码', 'trim|required|min_length[3]'); if ($this-\u003eform_validation-\u003erun() == FALSE) { $data['errors'] = validation_errors(); $this-\u003esession-\u003eset_flashdata($data); redirect('home'); } } 上面的代码一目了然，没啥可说的。下面我们就要在我们的视图中添加对错误信息的处理了。 登录 \u003c?php if ($this-\u003esession-\u003eflashdata('errors')): ?\u003e \u003c?= $this-\u003esession-\u003eflashdata('errors') ?\u003e \u003c?php endif ?\u003e \u003c?=form_open('user/login', ['id' =\u003e 'login_form', 'class' =\u003e ''])?\u003e 现在在进行登录测试，就可以看到错误信息了。 获取到正确的数据就可以进行正式的登录处理了，好了，这篇教程就到这里了，希望你喜欢。 ","date":"2016-02-12","heading":"表单数据验证","objectID":"/2016/02/codeigniter-3/:4:0","tags":null,"title":"CodeIgniter教程 - 登录项目","uri":"/2016/02/codeigniter-3/#表单数据验证"},{"categories":null,"collections":null,"content":"CRUD - Create，Read，Update，Delete 在这篇教程中，我们学习CI对数据库的操作。 ","date":"2016-02-11","heading":"","objectID":"/2016/02/codeigniter-2/:0:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#"},{"categories":null,"collections":null,"content":"查询数据库 在前面的教程中，我们使用下面的语句来获取users表中的数据 $query = $this-\u003edb-\u003eget('users'); 我们来换一种方式来查询数据库， $query = $this-\u003edb-\u003equery('select * from users'); return $query-\u003eresult(); 结果是相同的，这样我们就可以利用我们的SQL知识来查询我们的数据。你可以使用CI的用户手册来查看数据库的功能，比如我们可以使用$query-\u0026gt;num_rows()来返回获取的记录数，$query-\u0026gt;num_fields()返回字段数等。 ","date":"2016-02-11","heading":"查询数据库","objectID":"/2016/02/codeigniter-2/:1:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#查询数据库"},{"categories":null,"collections":null,"content":"带有参数的查询 //获取姓名为 'Zhang Jinglin' 的用户 $query = $this-\u003edb-\u003ewhere('name', 'Zhang Jinglin')-\u003eget('users'); 然我们通过URL来获取参数。首先修改我们的模型，User_model.php \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:46 */ class User_model extends CI_Model { public function get_users($id) { $query = $this-\u003edb-\u003ewhere('id', $id)-\u003eget('users'); return $query-\u003eresult(); } } 在模型的方法中，我们需要获取用户的ID，依据ID来读取用户数据。修改控制器，User.php \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:50 */ class User extends CI_Controller { public function index($id = 0) { $this-\u003eload-\u003emodel('user_model'); $data['results'] = $this-\u003euser_model-\u003eget_users($id); $this-\u003eload-\u003eview('user_index', $data); } } 方法中的参数是赋值为默认值，如果没提供参数，默认值为0。现在我们访问http://localhost/ci/index.php/user/index/1就能获取id为1的用户，你可以修改不同的id查看一下。 根据上面的结果，实际上CI的URL模式是index.php/{控制器}/{方法}/{id}。 实际上，我们可以在where方法中传入一个数组，作为参数 $this-\u003edb-\u003ewhere([ 'id' =\u003e $id, 'name' =\u003e $name ]) 上面的参数相当于where id = $id and name = $name。 当然你可以查看用户手册来学习更多的知识。 如果你更愿意使用原始的SQL语句来进行查询，（事实上我是非常喜欢用SQL语句来查询的），这里面我们来看一如何使用查询绑定来简化我们的查询工作。 比如下面的例子： $sql = \"SELECT * FROM some_table WHERE id IN ? AND status = ? AND author = ?\"; $this-\u003edb-\u003equery($sql, array(array(3, 6), 'live', 'Rick')); ","date":"2016-02-11","heading":"带有参数的查询","objectID":"/2016/02/codeigniter-2/:2:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#带有参数的查询"},{"categories":null,"collections":null,"content":"插入数据 插入数据也是非常简单的，我们在User_model.php中增加方法 public function create_user($data) { $this-\u003edb-\u003einsert('users', $data); } 模型方法中的参数接受一个数组，这个数组包含对应的字段的值。在User.php控制器中条件方法 public function create() { $this-\u003eload-\u003emodel('user_model'); $name = 'Zhang San'; $password = '123'; $this-\u003euser_model-\u003ecreate_user([ 'name' =\u003e $name, 'password' =\u003e $password ]); echo 'Insert OK'; } 这个方法将需要构建的记录的字段复制到数组中，交给模型方法执行即可。在浏览器中访问http://localhost/ci/index.php/user/create，如果看见了正常的输出，就可以查看数据库了。 上面的控制器方法中有一个问题，就是在不同的方法中我们都需要装载模型 $this-\u003eload-\u003emodel('user_model'); 当然可以利用自动装载来完成（在上一篇教程中），但是我是不大喜欢在自动装载中装入模型类的，因为这样就对系统的全部类可见了，那么怎么解决呢？解决的方法是利用构造函数即可。在控制器User.php中添加构造方法。 public function __construct() { parent::__construct(); $this-\u003eload-\u003emodel('user_model'); } 上面的构造方法中，千万不要忘记了调用父类的构造方法。 通过在构造方法中装载模型类，我们就可以在index create方法中取消装载模型的语句了。 ","date":"2016-02-11","heading":"插入数据","objectID":"/2016/02/codeigniter-2/:3:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#插入数据"},{"categories":null,"collections":null,"content":"修改数据 一般修改记录会修改某一条记录，这可以用where方法来进行限定。修改我们的模型类，添加方法 public function update_user($id, $data) { $this-\u003edb-\u003ewhere('id', $id)-\u003eupdate('users', $data); } 我们的模型方法接受两个从参数，一个是要修改的记录ID，一个是要修改的数据。在控制器中增加修改方法： public function update() { $id = 3; $name = 'Modify Name'; $this-\u003euser_model-\u003eupdate_user($id, [ 'name' =\u003e $name ]); } 注意到了么，因为我们在构造函数中装载了模型类，所以在控制其方法中就不要显示的装载模型了。 ","date":"2016-02-11","heading":"修改数据","objectID":"/2016/02/codeigniter-2/:4:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#修改数据"},{"categories":null,"collections":null,"content":"删除数据 一般删除记录会删除某一条记录，这可以用where方法来进行限定。修改我们的模型类，添加方法 public function delete_user($id) { $this-\u003edb-\u003ewhere('id', $id)-\u003edelete('users'); } 我们的模型方法接受要删除的记录ID。在控制器中增加修改方法： public function delete() { $id = 3; $this-\u003euser_model-\u003edelete_user($id); } 这就是CI的CRUD，当然CRUD也有其他的方式，我们后面会学习。 ","date":"2016-02-11","heading":"删除数据","objectID":"/2016/02/codeigniter-2/:5:0","tags":null,"title":"CodeIgniter教程 - CRUD","uri":"/2016/02/codeigniter-2/#删除数据"},{"categories":null,"collections":null,"content":"CodeIgniter是所有PHP MVC框架中最简单切轻量的一个，我会用一些列的教程来讲解CodeIgniter的使用。 ","date":"2016-02-11","heading":"","objectID":"/2016/02/codeigniter-1/:0:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#"},{"categories":null,"collections":null,"content":"CodeIgniter 是什么？ CodeIgniter是一个PHP的MVC框架，非常简单。 可以访问CodeIgniter网站查看更进一步的信息，在我写作的时候，CodeIgniter的版本是3.0.4。如果你对英文恐慌的话，CodeIgniter中国是完整的中文网站。网站上的用户手册是你最好的朋友，你有任何问题基本上都可以在上面周到解答。如果实在没有解决问题，你可以Google。 模型 代表你的数据结构。通常来说，模型类将包含帮助你对数据库进行增删改查的方法。 视图 是要展现给用户的信息。一个视图通常就是一个网页，但是在 CodeIgniter 中， 一个视图也可以是一部分页面（例如页头、页尾），它也可以是一个 RSS 页面， 或其他任何类型的页面。 控制器 是模型、视图以及其他任何处理 HTTP 请求所必须的资源之间的中介，并生成网页。 如果你暂时不明白没关系，后面你会逐渐理解MVC到底是什么。 ","date":"2016-02-11","heading":"CodeIgniter 是什么？","objectID":"/2016/02/codeigniter-1/:1:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#codeigniter-是什么"},{"categories":null,"collections":null,"content":"安装CodeIgniter 直接从CodeIgniter网站下载即可。下载后将文件解压到htdocs目录下。更改目录名为ci这样简单一些。访问http://localhost/ci查看结果。 ","date":"2016-02-11","heading":"安装CodeIgniter","objectID":"/2016/02/codeigniter-1/:2:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#安装codeigniter"},{"categories":null,"collections":null,"content":"CodeIgniter文件结构 下面我们来查看一下文件结构。 user_guide 目录是用户手册，和网站是一样的，你可以安全的删除这个目录。 system 目录是CodeIgniter的系统，一般情况下我们不用去管他。 application 目录是我们的工作目录。我们来仔细查看一下。 这里面我们可以看到models，views和controllers三个目录，ok，这就是我们mvc的目录，暂时我们知道这三个目录就OK了，最多我们还要知道config配置目录。 我们在浏览器中访问主页，会注意到下面的提示： 这个说明了，如果我们想要修改页面在application/views/下面直接修改welcome_message.php即可。如果我们查看这个页面，这个就是简单的HTML。 我们把h1修改一下： 欢迎学习CodeIgniter教程 保存后刷新页面。 非常重要的事情：如果你在浏览的时候，中文出现了乱码，一定要查看 1、\u0026lt;meta charset=\u0026quot;utf-8\u0026quot;\u0026gt;这个是否在你的文件中，2、如果你使用PhpStorm检查右下角当前文件的编码，如果你用中文系统，很可能是GBK，把他修改为UTF-8。 页面虽然修改了，但是我们会发现为什么我嫩只是直接访问http://localhost/ci这个地址，而会显示这个页面，这个有什么关联么？ 在MVC中，controller控制着一切，让我们查看一下controller目录。在下面我们会看到Welcome.php这个文件。 \u003c?php defined('BASEPATH') OR exit('No direct script access allowed'); class Welcome extends CI_Controller { /** * Index Page for this controller. * * Maps to the following URL * http://example.com/index.php/welcome * - or - * http://example.com/index.php/welcome/index * - or - * Since this controller is set as the default controller in * config/routes.php, it's displayed at http://example.com/ * * So any other public methods not prefixed with an underscore will * map to /index.php/welcome/ * @see https://codeigniter.com/user_guide/general/urls.html */ public function index() { $this-\u003eload-\u003eview('welcome_message'); } } Welcome派生自CI_Controller这个类。这个控制器有一个方法index，这个方法中我们可以看到装载视图welcome_message。我们修改这个方法一下，不再装载视图，而是输出一个字符串： public function index() { // $this-\u003eload-\u003eview('welcome_message'); echo 'This is the welcome controller'; } 再次浏览，就会发现页面变化了。 ","date":"2016-02-11","heading":"CodeIgniter文件结构","objectID":"/2016/02/codeigniter-1/:3:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#codeigniter文件结构"},{"categories":null,"collections":null,"content":"添加方法 做一个测试，将index方法的名字修改为其他，例如test再次访问，会发现出现了错误，只有index方法可以作为默认的方法被访问。 如果我们增加一个方法， public function test() { echo \"this is test funtion\"; } 在浏览器中访问http://localhost/ci/test会出现错误，那到底怎么访问呢？在welcome.php里面的注释中说明了如何访问： * http://example.com/index.php/welcome * - or - * http://example.com/index.php/welcome/index 所以访问test方法的路径是http://localhost/ci/index.php/welcome/test，测试一下。在CI中，index.php是入口文件，所有的访问都通过这个文件进行转发。 如果忽略路径中前面的部分，我们访问的方式就是{控制器/方法}。 ","date":"2016-02-11","heading":"添加方法","objectID":"/2016/02/codeigniter-1/:4:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#添加方法"},{"categories":null,"collections":null,"content":"添加控制器 我们来创建一个自己的控制器，在controller目录中新建文件Home.php \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:27 */ class Home extends CI_Controller { public function index() { echo 'this is the home controller at index'; } } 访问http://localhost/ci/index.php/home，index是默认方法，不需要显示的说明。在views目录中新建视图文件home_index.php \u003c!doctype html\u003e Document Hello, CodeIgniter! 虽然我们的扩展名是php，但是这个文件仅仅是简单的html，修改Home.php控制器，装载视图 public function index() { $this-\u003eload-\u003eview('home_index'); } 刷新页面， 查看结果。 ","date":"2016-02-11","heading":"添加控制器","objectID":"/2016/02/codeigniter-1/:5:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#添加控制器"},{"categories":null,"collections":null,"content":"模型和数据库 我们来创建一个数据库，当然是使用MySQL了。 create database ci default charset utf8; use ci; create table users( id int auto_increment primary key, name varchar(255), password varchar(255)); insert into users(name, password) values('Zhang Jinglin', '123'); insert into users(name, password) values('Jinglin Zhang', '123'); 有了数据库以后，我们要告诉CI如何访问我们的数据库。在config\\database.php中，修改hostname username password database即可，其他的属性不管，非常简单。 'hostname' =\u003e 'localhost', 'username' =\u003e 'root', 'password' =\u003e '', 'database' =\u003e 'ci', 现在，我们的CI就知道如何访问数据了。 我们来创建模型，在models目录下创建User_model.php \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:46 */ class User_model extends CI_Model { public function get_users() { //通知CI装载数据库组件 $this-\u003eload-\u003edatabase(); //查询数据库，获取所有users表中的内容 $query = $this-\u003edb-\u003eget('users'); //返回查询结果 return $query-\u003eresult(); } } 有了模型，下面创建一个User.php控制器， \u003c?php /** * Created by PhpStorm. * User: jingl * Date: 2016/2/11 * Time: 13:50 */ class User extends CI_Controller { public function index() { //通知CI，装载模型 $this-\u003eload-\u003emodel('user_model'); //通过模型获取结果 $result = $this-\u003euser_model-\u003eget_users(); //调用PHP函数输出数组 print_r($result); } } 通过上面的代码我们已经可以访问数据库了，非常简单。 ","date":"2016-02-11","heading":"模型和数据库","objectID":"/2016/02/codeigniter-1/:6:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#模型和数据库"},{"categories":null,"collections":null,"content":"自动装载 我们的代码还有些小问题，在每一个模型类的方法中，我们都需要一句代码 $this-\u003eload-\u003edatabase(); 虽说不麻烦，但是总是重复毕竟是不好的。所以我们可以告诉CI把一些在我们应用中总是使用的组件给我们自动装载进来。在config\\autoload.php中，修改 $autoload['libraries'] = array('database'); 然后，在模型代码中，注释掉装载数据库的代码 public function get_users() { //$this-\u003eload-\u003edatabase(); $query = $this-\u003edb-\u003eget('users'); return $query-\u003eresult(); } 重新刷新并检查结果。自动装载除了可以加载CI的组件外，也可以加载我们自己定义的组件，比如，在我们的应用中，总是要使用User_model这个组件，我们也可以自动装载这个我们自己的组件。在config/autoload.php中，修改代码： $autoload['model'] = array('user_model'); 然后在我们的控制器方法中就不需要显示的转载模型了。 public function index() { //$this-\u003eload-\u003emodel('user_model'); $result = $this-\u003euser_model-\u003eget_users(); print_r($result); } 但是我个人是不推荐自动装载模型的，在方法中明确的装载模型是有利于代码的清晰度的，除非你有非常好的理由来自动装在模型。 ","date":"2016-02-11","heading":"自动装载","objectID":"/2016/02/codeigniter-1/:7:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#自动装载"},{"categories":null,"collections":null,"content":"把数据传送到视图 在控制器中我们可以装载不同的视图，在装载视图的同时，我们也可以把数据传送给视图，非常简单。 class User extends CI_Controller { public function index() { $this-\u003eload-\u003emodel('user_model'); //把结果发在一个命名数组中 $data['results'] = $this-\u003euser_model-\u003eget_users(); //在装在视图的同时，把数据传送过去 $this-\u003eload-\u003eview('user_index', $data); } } 创建视图文件，views\\user_index.php \u003c!doctype html\u003e User View User View \u003c?php foreach($results as $item): ?\u003e \u003c?php echo $item-\u003ename; ?\u003e \u003c?php endforeach ?\u003e 这就是如何把数据传送到视图中。 ","date":"2016-02-11","heading":"把数据传送到视图","objectID":"/2016/02/codeigniter-1/:8:0","tags":null,"title":"CodeIgniter教程 - 基础","uri":"/2016/02/codeigniter-1/#把数据传送到视图"},{"categories":null,"collections":null,"content":"Laravl 是什么 Laravel是一套简洁、优雅的PHP Web开发框架 (PHP Web Framework) 。在世界（不含中国）PHP框架的占有率超过40%。 ","date":"2016-02-09","heading":"Laravl 是什么","objectID":"/2016/02/laravel5/:1:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#laravl-是什么"},{"categories":null,"collections":null,"content":"设置系统 检查composer是否安装，如果没有安装，请参考Ubuntu PHP 开发环境配置或者Cent OS 7 PHP 开发环境配置 composer --version #输出 Composer version 1.0.0-alpha9 2014-12-07 17:15:20 建立 Laravel 项目 cd /var/www/html/ composer create-project laravel/laravel learning-laravel-5 --prefer-dist #需要等待 测试项目 cd learning-laravel-5/ #x.x.x.x 为虚拟机地址 php -S x.x.x.x:8888 -t public 主机浏览器测试 ","date":"2016-02-09","heading":"设置系统","objectID":"/2016/02/laravel5/:2:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#设置系统"},{"categories":null,"collections":null,"content":"路由、控制器和视图简介 查看app-\u0026gt;Http-\u0026gt;routes.php Route::get('/', 'WelcomeController@index'); @是一个界定符，前面是控制器，后面是动作，表示当用户请求url / 的时候，执行控制器 WelcomeController 中的 index 方法 app-\u0026gt;http-\u0026gt;controllers-\u0026gt;welcomecontroller.php public function index() { return view('welcome'); } 当前默认返回一个视图，视图的名字叫做 welcome，实际上是 welcome.blade.php，blade是laravel的视图模板。 可以查看 resources-\u0026gt;views-\u0026gt;welcome.blade.php 修改welcomecontroller.php public function index() { // return view('welcome'); return 'hello, laravel'; } 在浏览器中测试，得到一个简单的反馈。 我们新建一个路由，在routes.php中增加： Route::get('/contact', 'WelcomeController@contact'); 可以新建一个路由，但是现在我们还是直接使用默认的控制器，在 WelcomeController.php 中添加： public function contact() { return 'Contact Me'; } 在浏览器终测试新增加的路由。 我们可以返回简单的字符串，也可以返回json或者html文件，所有的视图文件存储在resource-\u0026gt;views中。 例如：return view('welcome') ,我们不需要考虑路径，也不要添加.blade.php扩展名，框架自动为我们完成。如果在views目录中需要子目录，例如views/forum子目录，只需要return view('forum/xxx')，或者跟简单而明确的方式是：return view('forum.xxx')。😄 我们返回一个页面 public function contact() { return view('pages.contact'); } 在 views 目录下创建 pages目录，然后创建contact.blade.php \u003c!doctype html\u003e Document Contact ","date":"2016-02-09","heading":"路由、控制器和视图简介","objectID":"/2016/02/laravel5/:3:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#路由控制器和视图简介"},{"categories":null,"collections":null,"content":"向视图传送数据 我们在Routes.php中新建一个路由 Route::get('about', 'PagesController@about'); 在浏览器中浏览会获得一个错误，错误信息仅仅是一个提示信息，缺少细节，在生产环境 It’ ok，但是开发阶段我们希望获得详细信息。 在项目的根目录找到 .env 文件，修改 APP_DEBUG=true 这将显示详细的错误信息，PagesController 不存在。但在生产环境一定要设置为 false 我们可以手工新建控制器，但更快的方式是利用 laravel 提供的生成器。在命令行当前项目目录中运行： php artisan 可以看到laravel提供的功能。 php artisan make:controller PagesController ok，在 app-\u0026gt;http-\u0026gt;controller 下面生成了 PagesController.php \u003c?php namespace App\\Http\\Controllers; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use Illuminate\\Http\\Request; class PagesController extends Controller { /** * Display a listing of the resource. * * @return Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return Response */ public function create() { // } /** * Store a newly created resource in storage. * * @return Response */ public function store() { // } /** * Display the specified resource. * * @param int $id * @return Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return Response */ public function edit($id) { // } /** * Update the specified resource in storage. * * @param int $id * @return Response */ public function update($id) { // } /** * Remove the specified resource from storage. * * @param int $id * @return Response */ public function destroy($id) { // } } 这样生成的controller包含了全部所需要的RESTful方法，我们可以简化一下。删除生成的PagesController.php，在命令行运行： php artisan make:controller PagesController --plain 再看一下生成的结果 \u003c?php namespace App\\Http\\Controllers; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use Illuminate\\Http\\Request; class PagesController extends Controller { // } 基本上是一个空的controller，所有的方法我们需要自己创建。 如果你想知道到底有什么参数我们可以在命令行执行，你可以运行下面的命令来查看帮助 php artisan help make:controller ok, 你可以经常使用help命令来帮助你了解这些参数。 在PagesController中建立about方法。 public function about() { return 'About Page'; } 在浏览器冲查看结果，错误消失，返回简单的信息。 返回视图 我们当然希望返回html文档，修改about方法的返回： public function about() { return view('pages.about'); } 注意：返回的结果是 pages.about ，这表示在 views 子目录中的 pages 子目录中的 about.balde.php 文件。让我们创建 resources\\views\\pages\\about.balde.php 文件 \u003c!doctype html\u003e Document About That’s it. 运行浏览器查看吧，😄 向视图传送数据 修改PagesController.php public function about() { $name = 'Zhang Jinlgin'; return view('pages.about')-\u003ewith('name', $name); } 修改我们的视图文件 about.blade.php About \u003c?= $name ?\u003e Bingo，查看结果吧。 我们使用的laravel使用了blade模板，我们可以利用这个好处修改视图： About {{ $name }} 看起来更好了，在blade中，{{}}是转义html的语义的，让我来修改一个数据： $name = 'Zhang Jinlgin'; 查看结果，发现所有的html元素都被转义了。但是如果不需要转义html，可以使用 {!! !!}，修改视图： About {!! $name !!} 再看结果，👌 我们不仅仅可以向视图传送一个数据，同样我们可以传送Array public function about() { return view('pages.about')-\u003ewith([ 'first' =\u003e 'Zhang', 'last' =\u003e 'Jinglin' ]); } About {{ $first }} {{ $last }} 一种简洁的方式是这样： public function about() { $data = []; $data['first'] = 'Zhang'; $data['last'] = 'Jinglin'; return view('pages.about', $data); } 结果相同，更简单的是这样： public function about() { $first = 'Zhang'; $last = 'Jinglin'; return view('pages.about', compact('first', 'last')); } compact 把参数变成数组，extract相反。可以查看php手册了解 compact ，顺道了解一下 extract。 ","date":"2016-02-09","heading":"向视图传送数据","objectID":"/2016/02/laravel5/:4:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#向视图传送数据"},{"categories":null,"collections":null,"content":"Blade 简介 在多个页面中我们可能包含相同的内容，像是文件头，链接的css或者js等。我们可以利用布局文件完成这个功能。 让我们新建一个布局文件，例如 views/layout.blade.php \u003c!doctype html\u003e Document @yield('content') 我们创建了不解的结构，引入了bootstrap，注意 @yield 是blade的布局占位符，未来我们的页面内容将填充到这里，修改 about.blade.php @extends('layout') @section('content') About {{ $first }} {{ $last }} @stop 上面的代码表示我们使用布局文件 layout.blade.php， 然后在 content 段中添加内容。 在 routes.php 中添加： Route::get('about', 'PagesController@about'); Route::get('contact', 'PagesController@contact'); 在 PagesController.php 中添加： public function contact() { return view('pages.contact'); } 新建视图 pages/contact.blade.php @extends('layout') @section('content') Contact Me! @stop Check it out! 在布局文件中我们可以添加多个 @yield , 比如在 layout.blade.php 中添加 @yield('footer') ： \u003c!doctype html\u003e Document @yield('content') @yield('footer') ``` 比如 `contact.blade.php` 中有一段脚本，就可以放在这个段中。 ```html @extends('layout') @section('content') Contact Me! @stop @section('footer') @stop 访问contact会有对话框，而about仍然是普通显示 使用@if进行判断 @extends('layout') @section('content') @if ($first = 'Zhang') Hello, Zhang @else Hello, nobody @endif @stop 也可以视同 @unless 等同于 if !， 还有 @foreach 等。 public function about() { $people = [ 'zhang san', 'li si', 'wang wu' ]; return view('pages.about', compact('people')); } @extends('layout') @section('content') Person: @foreach($people as $person) {{ $person }} @endforeach @stop 有一种情况，数据可能来自数据库，集合可能是空，像是这样： $people = []; 处理这种情况，请添加 @if 处理 @extends('layout') @section('content') @if (count($people)) Person: @foreach($people as $person) {{ $person }} @endforeach @endif Other info @stop That’s better. ","date":"2016-02-09","heading":"Blade 简介","objectID":"/2016/02/laravel5/:5:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#blade-简介"},{"categories":null,"collections":null,"content":"环境与配置 .env 文件是配置文件，包括数据库配置信息，查看 config-\u0026gt;database.php ，connections 里面包含了所有数据库的配置，可以在 default 中选择要使用的数据库。在数据库配置中，有关 env('DB_HOST', 'localhost') 就是读取 .env 配置文件的信息。第二个参数是缺省参数。 我们使用 mysql 数据库，修改 .env: DB_HOST=localhost DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= 在mysql中创建 laravel数据库 mysql -u root CREATE DATABASE laravel 查看 mysql 配置： 'mysql' =\u003e [ 'driver' =\u003e 'mysql', 'host' =\u003e env('DB_HOST', 'localhost'), 'database' =\u003e env('DB_DATABASE', 'forge'), 'username' =\u003e env('DB_USERNAME', 'forge'), 'password' =\u003e env('DB_PASSWORD', ''), 'charset' =\u003e 'utf8', 'collation' =\u003e 'utf8_unicode_ci', 'prefix' =\u003e '', 'strict' =\u003e false, ], 在 config 子目录中，包含了全部的配置文件，查看一下。 ","date":"2016-02-09","heading":"环境与配置","objectID":"/2016/02/laravel5/:6:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#环境与配置"},{"categories":null,"collections":null,"content":"数据库迁移 database migrations 是laravel最强大的功能之一。数据库迁移可以理解为数据库的版本控制器。 在 database/migrations 目录中包含两个迁移文件，一个建立用户表，一个用于用户密码重置。 在迁移文件中，up 方法用于创建数据表，down方法用于回滚，也就是删除数据表。 执行数据库迁移 php artisan migrate #输出 Migration table created successfully. Migrated: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_100000_create_password_resets_table 查看mysql数据库，可以看到产生了三张表。 migratoins 表是迁移记录表，users 和 pasword_resets。 如果设计有问题，执行数据库回滚 php artisan migrate:rollback #输出 Rolled back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_000000_create_users_table 再次查看mysql数据库，就剩下 migrations 表了， users password_resets 被删除了。 修改迁移文件，再次执行迁移。 新建迁移 php artisan make:migration create_article_table --create='articles' #输出 Created Migration: 2015_03_28_050138_create_article_table 在 database/migrations 下生成了新的文件。 \u003c?php use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class CreateArticleTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('articles', function(Blueprint $table) { $table-\u003eincrements('id'); $table-\u003etimestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::drop('articles'); } } 自动添加了 id列，自动增长，timestamps() 会自动产生 created_at 和 updated_at 两个时间列。我们添加一些字段： public function up() { Schema::create('articles', function(Blueprint $table) { $table-\u003eincrements('id'); $table-\u003estring('title'); $table-\u003etext('body'); $table-\u003etimestamp('published_at'); $table-\u003etimestamps(); }); } 执行迁移： php artisan migrate 现在有了新的数据表了。😄 假设我们需要添加一个新的字段，你可以回滚，然后修改迁移文件，再次执行迁移，或者可以直接新建一个迁移文件 php artisan make:migration add_excerpt_to_articels_table 查看新产生的迁移文件 \u003c?php use Illuminate\\Database\\Schema\\Blueprint; use Illuminate\\Database\\Migrations\\Migration; class AddExcerptToArticelsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { // } /** * Reverse the migrations. * * @return void */ public function down() { // } } 只有空的 up 和 down 方法。我们可以手工添加代码，或者我们让laravel为我们生成基础代码。删除这个文件，重新生成迁移文件，注意添加参数： php artisan make:migration add_excerpt_to_articels_table --table='articles' 现在，up 方法里面有了初始代码。 public function up() { Schema::table('articles', function(Blueprint $table) { // }); } 添加实际的数据修改代码： public function up() { Schema::table('articles', function(Blueprint $table) { $table-\u003etext('excerpt')-\u003enullable(); }); } public function down() { Schema::table('articles', function(Blueprint $table) { $table-\u003edropColumn('excerpt'); }); } nullable() 表示字段也可以为空。 再次执行迁移并检查数据库。 如果我们为了好玩，执行回滚 php artisan migrate:rollback excerpt 列没有了。 ","date":"2016-02-09","heading":"数据库迁移","objectID":"/2016/02/laravel5/:7:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#数据库迁移"},{"categories":null,"collections":null,"content":"Eloquent （laravel 的ORM） 我们来生成第一个模型 php artisan make:model Article #输出 Model created successfully. Created Migration: 2015_03_28_062517_create_articles_table 查看一下生成的文件 app/Article.php \u003c?php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Article extends Model { // } 没什么特别的，除了继承自 Model 以外，但是具有强大的功能，这些都封装在laravel的Model中。模型自动具有了 save() update() findXXX() 等强大的功能。 tinker 是 laravel提供的命令行工具，可以和项目进行交互。 php artisan tinker #以下是在tinker中的交互输入 Psy Shell v0.4.1 (PHP 5.4.16 — cli) by Justin Hileman \u003e\u003e\u003e $name = 'zhang jinglin'; =\u003e \"zhang jinglin\" \u003e\u003e\u003e $name =\u003e \"zhang jinglin\" \u003e\u003e\u003e $article = new App\\Article; =\u003e {} \u003e\u003e\u003e $article-\u003etitle = 'My First Article'; =\u003e \"My First Article\" \u003e\u003e\u003e $article-\u003ebody = 'Some content...'; =\u003e \"Some content...\" \u003e\u003e\u003e $article-\u003epublished_at = Carbon\\Carbon::now(); =\u003e { date: \"2015-03-28 06:37:22\", timezone_type: 3, timezone: \"UTC\" } \u003e\u003e\u003e $article; =\u003e { title: \"My First Article\", body: \"Some content...\", published_at: { date: \"2015-03-28 06:37:22\", timezone_type: 3, timezone: \"UTC\" } } \u003e\u003e\u003e $article-\u003etoArray(); =\u003e [ \"title\" =\u003e \"My First Article\", \"body\" =\u003e \"Some content...\", \"published_at\" =\u003e { date: \"2015-03-28 06:37:22\", timezone_type: 3, timezone: \"UTC\" } ] \u003e\u003e\u003e $article-\u003esave(); =\u003e true #查看数据结果，添加了一条记录 \u003e\u003e\u003e App\\Article::all()-\u003etoArray(); =\u003e [ [ \"id\" =\u003e \"1\", \"title\" =\u003e \"My First Article\", \"body\" =\u003e \"Some content...\", \"published_at\" =\u003e \"2015-03-28 06:37:22\", \"created_at\" =\u003e \"2015-03-28 06:38:53\", \"updated_at\" =\u003e \"2015-03-28 06:38:53\" ] ] \u003e\u003e\u003e $article-\u003etitle = 'My First Update Title'; =\u003e \"My First Update Title\" \u003e\u003e\u003e $article-\u003esave(); =\u003e true \u003e\u003e\u003e App\\Article::all()-\u003etoArray(); =\u003e [ [ \"id\" =\u003e \"1\", \"title\" =\u003e \"My First Update Title\", \"body\" =\u003e \"Some content...\", \"published_at\" =\u003e \"2015-03-28 06:37:22\", \"created_at\" =\u003e \"2015-03-28 06:38:53\", \"updated_at\" =\u003e \"2015-03-28 06:42:03\" ] ] \u003e\u003e\u003e $article = App\\Article::find(1); =\u003e { id: \"1\", title: \"My First Update Title\", body: \"Some content...\", published_at: \"2015-03-28 06:37:22\", created_at: \"2015-03-28 06:38:53\", updated_at: \"2015-03-28 06:42:03\" } \u003e\u003e\u003e $article = App\\Article::where('body', 'Some content...')-\u003eget(); =\u003e [ { id: \"1\", title: \"My First Update Title\", body: \"Some content...\", published_at: \"2015-03-28 06:37:22\", created_at: \"2015-03-28 06:38:53\", updated_at: \"2015-03-28 06:42:03\" } ] \u003e\u003e\u003e $article = App\\Article::where('body', 'Some content...')-\u003efirst(); =\u003e { id: \"1\", title: \"My First Update Title\", body: \"Some content...\", published_at: \"2015-03-28 06:37:22\", created_at: \"2015-03-28 06:38:53\", updated_at: \"2015-03-28 06:42:03\" } \u003e\u003e\u003e \u003e\u003e\u003e $article = App\\Article::create(['title' =\u003e 'New Article', 'body' =\u003e 'New body', 'published_at' =\u003e Carbon\\Carbon::now()]); Illuminate\\Database\\Eloquent\\MassAssignmentException with message 'title' ","date":"2016-02-09","heading":"Eloquent （laravel 的ORM）","objectID":"/2016/02/laravel5/:8:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#eloquent-laravel-的orm"},{"categories":null,"collections":null,"content":"MassAssignmentException，laravel保护我们不能直接插入记录。比如，在一些特殊情况下我们需要直接利用表单的信息填充数据库记录，但是如果我们并没有在表单中添加密码字段，而黑客产生了密码字段连同我们的其他字段一起送回服务器，这将产生修改密码的危险，所以我们必须明确的告诉laravel我们的模型那些字段是可以直接填充的。 修改我们的模型文件 Article.php \u003c?php namespace App; use Illuminate\\Database\\Eloquent\\Model; class Article extends Model { protected $fillable = [ 'title', 'body', 'published_at' ]; } ","date":"2016-02-09","heading":"MassAssignmentException，laravel保护我们不能直接插入记录。比如，在一些特殊情况下我们需要直接利用表单的信息填充数据库记录，但是如果我们并没有在表单中添加密码字段，而黑客产生了密码字段连同我们的其他字段一起送回服务器，这将产生修改密码的危险，所以我们必须明确的告诉laravel我们的模型那些字段是可以直接填充的。","objectID":"/2016/02/laravel5/:8:1","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#massassignmentexceptionlaravel保护我们不能直接插入记录比如在一些特殊情况下我们需要直接利用表单的信息填充数据库记录但是如果我们并没有在表单中添加密码字段而黑客产生了密码字段连同我们的其他字段一起送回服务器这将产生修改密码的危险所以我们必须明确的告诉laravel我们的模型那些字段是可以直接填充的"},{"categories":null,"collections":null,"content":"表示，title, body, published_at 是可以直接填充的。 退出 tinker，重新进入 \u003e\u003e\u003e $article = App\\Article::create(['title' =\u003e 'New Article', 'body' =\u003e 'New body', 'published_at' =\u003e Carbon\\Carbon::now()]); =\u003e { title: \"New Article\", body: \"New body\", published_at: { date: \"2015-03-28 06:55:19\", timezone_type: 3, timezone: \"UTC\" }, updated_at: \"2015-03-28 06:55:19\", created_at: \"2015-03-28 06:55:19\", id: 2 } # It's ok \u003e\u003e\u003e App\\Article::all()-\u003etoArray(); =\u003e [ [ \"id\" =\u003e \"1\", \"title\" =\u003e \"My First Update Title\", \"body\" =\u003e \"Some content...\", \"published_at\" =\u003e \"2015-03-28 06:37:22\", \"created_at\" =\u003e \"2015-03-28 06:38:53\", \"updated_at\" =\u003e \"2015-03-28 06:42:03\" ], [ \"id\" =\u003e \"2\", \"title\" =\u003e \"New Article\", \"body\" =\u003e \"New body\", \"published_at\" =\u003e \"2015-03-28 06:55:19\", \"created_at\" =\u003e \"2015-03-28 06:55:19\", \"updated_at\" =\u003e \"2015-03-28 06:55:19\" ] ] \u003e\u003e\u003e $article = App\\Article::find(2); =\u003e { id: \"2\", title: \"New Article\", body: \"New body\", published_at: \"2015-03-28 06:55:19\", created_at: \"2015-03-28 06:55:19\", updated_at: \"2015-03-28 06:55:19\" } \u003e\u003e\u003e $article-\u003eupdate(['body' =\u003e 'New Updaet Body']); =\u003e true #update自动调用save() ","date":"2016-02-09","heading":"表示，title, body, published_at 是可以直接填充的。","objectID":"/2016/02/laravel5/:8:2","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#表示title-body-published_at-是可以直接填充的"},{"categories":null,"collections":null,"content":"模型、控制器、视图基础流程 添加路由 Route::get('artiles', 'ArticlesController@index'); 创建控制器 php artisan make:controller ArticlesController --plain 修改控制器 \u003c?php namespace App\\Http\\Controllers; use App\\Article; use App\\Http\\Requests; use App\\Http\\Controllers\\Controller; use Illuminate\\Http\\Request; class ArticlesController extends Controller { public function index() { $articles = Article::all(); return $articles; } } 可以在浏览器中看到返回的 JSON 结果，cool！ 修改控制器，返回视图 public function index() { $articles = Article::all(); return view('articles.index', compact('articles')); } 创建视图 @extends('layout') @section('content') Articles @foreach($articles as $article) {{$article-\u003etitle}} {{$article-\u003ebody}} @endforeach @stop 浏览结果，COOL！！！！ 显示单个文章 添加显示详细信息的路由 Route::get('articles/{id}', 'ArticlesController@show'); 其中，{id} 是参数，表示要显示的文章的 id，修改控制器： public function show($id) { $article = Article::find($id); //若果找不到文章 if (is_null($article)) { //生产环境 APP_DEBUG=false abort(404); } return view('articles.show', compact('article')); } laravel 提供了更加方便的功能，修改控制器： public function show($id) { $article = Article::findOrFail($id); return view('articles.show', compact('article')); } It’s cool. 新建视图 @extends('layout') @section('content') {{$article-\u003etitle}} {{$article-\u003ebody}} @stop 在浏览器中尝试访问：/articles/1 /articles/2 修改index视图 @extends('layout') @section('content') Articles @foreach($articles as $article) {{--这种方式可以--}} {{$article-\u003etitle}} {{--这种方式更加灵活，不限制路径--}} {{$article-\u003etitle}} {{--还可以使用--}} {{$article-\u003etitle}} {{$article-\u003ebody}} @endforeach @stop ","date":"2016-02-09","heading":"模型、控制器、视图基础流程","objectID":"/2016/02/laravel5/:9:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#模型控制器视图基础流程"},{"categories":null,"collections":null,"content":"表单 首先让我们修改路由，能够增加一个文章的发布。 Route::get('articles/create', 'ArticlesController@create'); 然后修改控制器 public function create() { return view('articles.create'); } 我们返回一个视图，新建这个视图。我们当然可以直接使用HTML建立表单，但我们有功能更好的办法。我们使用一个开源库，Jeffrey Way 开发的illuminate\\html。安装依赖库： composer require illuminate/html laravel的库需要注册到laravel中才能使用。在 config/app.php 中，我们可以看到 laravel 提供的 provider 字段，这里描述了laravel的库功能。在Laravel Framewirk Service Providers... 最后添加我们新增的 HtmlProvider 'Illuminate\\Html\\HtmlServiceProvider', 我们不希望使用 Illuminate\\Html\\FromFacade 这么长的名字来引入，我们需要简短的名字。在当前的 app.php 中找到 aliases 段，在最后添加别名。 'Form' =\u003e 'Illuminate\\Html\\FormFacade', 'Html' =\u003e 'Illuminate\\Html\\HtmlFacade', OK，现在我们来创建视图，views/articles/create.blade.php @extends('layout') @section('content') Write a New Article {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::open() !!} {!! Form::close() !!} @stop 访问 /articles/create 看到了错误，Why? 让我们测试一下，到底是哪里出了问题。在控制器中做出下面的修改： public function show($id) { dd('show'); $article = Article::findOrFail($id); return view('articles.show', compact('article')); } 没错，你没看错，就是在 show 方法中添加 dd() 方法，这个方法简单的输出一个信息然后死掉。我们再来访问 /articles/create ，你看到了什么，你看到输出了 show 。 为什么我们访问 create 结果路由给了我们 show ? 我们来查看一下路由，到底发生了什么。 Route::get('articles', 'ArticlesController@index'); Route::get('articles/{id}', 'ArticlesController@show'); Route::get('articles/create', 'ArticlesController@create'); 上面是我们的路由，注意到 articles/{id} 意味着这是一个通配符，所有在 articles/ 后面的东西都会匹配，你知道了么？我们的 /articles/create 也被他匹配了。OMG！ 解决方案就是调整顺序： Route::get('articles', 'ArticlesController@index'); Route::get('articles/create', 'ArticlesController@create'); Route::get('articles/{id}', 'ArticlesController@show'); 也就是从特殊到普通，以后的路由设置中要时刻注意这个问题。现在我们在访问 articles/create 一切OK了。 在浏览器中查看一下源代码，你会发现不仅生成了 method 和 action 同时生成了一个隐藏的 _token 字段作为服务器对窗体的验证，避免黑客的伪造攻击。 让我们修改我们的视图，添加字段： @extends('layout') @section('content') Write a New Article {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::open() !!} {!! Form::label('title', 'Title:') !!} {!! Form::text('title', null, ['class' =\u003e 'form-control']) !!} {!! Form::label('body', 'Body:') !!} {!! Form::textarea('body', null, ['class' =\u003e 'form-control']) !!} {!! Form::submit('Add Article', ['class' =\u003e 'btn btn-primary form-control']) !!} {!! Form::close() !!} @stop 当表单提交的时候，实际上是使用 post 方法提交到 articles/create 上的，但根据RESTful的习惯，我们希望能够 post 到 /articles 上，我们来修改视图的表单方法，设定提交的路径。 {!! Form::open(['url' =\u003e 'articles']) !!} 然后我们在路由中处理表单提交事件。 Route::post('/articles', 'ArticlesController@store'); 我们来处理控制器 //注意：将下面的 use 语句删除，我们使用 facade 接口中的 Request //use App\\Http\\Requests\\Request; //引入下面的命名空间中的 Request use Illuminate\\Support\\Facades\\Request; public function store() { //使用 Illuminate\\Html\\Request 来返回全部的表单输入字段 $input = Request::all(); //我们直接返回$input，来看一下 return $input; } 我们可以直接看到输入表单的json结果。如果只需要 title 字段的值，则可以使用 Request::get('titel') 。 如何添加到数据库中呢？借助模型，我们可以直接采用下面的方法， Article::create($input); ###就这么简单，就是这么任性 ###如果没有忘记 Mass Assignment，在我们的模型中我们定义了 $fillable 数组，来定义那些字段可以直接在 create 的时候直接填充。 修改控制器，添加到模型中，并存储到数据库。 public function store() { $input = Request::all(); Article::create($input); return redirect('articles'); } 添加一条记录试试，非常棒。但别忘了。我们还有一个字段叫做 published_at ，让我们来处理它。 public function store() { $input = Request::all(); $input['published_at'] = Carbon::now(); Article::create($input); return redirect('articles'); } 添加新纪录在测试一下。 还有一个问题，新添加的应该显示在最前面，我们来修改以下控制器。 public function index() { //倒序获取文章 //可以这样 //$articles = Article::orderBy('published_at', 'desc')-\u003eget(); //简单方式，当然还有 oldest() $articles = Article::latest('published_at')-\u003eget(); return view('articles.index', compact('articles')); } ","date":"2016-02-09","heading":"表单","objectID":"/2016/02/laravel5/:10:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#表单"},{"categories":null,"collections":null,"content":"日期，Mutator 和 Scope 在我们前面的解决方案中，直接给 published_at 赋值为当前日期实际上是一个临时解决方案，我们需要设定发布日期，可能是未来2天后才发布，让我们修改这个问题。 首先修改控制器： public function store() { Article::create(Request::all()); return redirect('articles'); } 然后修改视图，添加发布日期字段 @extends('layout') @section('content') Write a New Article {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::open(['url' =\u003e 'articles']) !!} {!! Form::label('title', 'Title:') !!} {!! Form::text('title', null, ['class' =\u003e 'form-control']) !!} {!! Form::label('body', 'Body:') !!} {!! Form::textarea('body', null, ['class' =\u003e 'form-control']) !!} {!! Form::label('published_at', 'Publish On:') !!} {!! Form::input('date', 'published_at', date('Y-m-d'), ['class' =\u003e 'form-control']) !!} {!! Form::submit('Add Article', ['class' =\u003e 'btn btn-primary form-control']) !!} {!! Form::close() !!} @stop ok，让我们添加一个新的文章，并且把日期设置为未来的某一天，但是文章直接显示在最开始了，这不是我们需要的。我们需要到了那天才显示出来。当然，我们需要更具体一点，比如在早上 8：00 显示，而不是0点显示。我们可以添加一个mutator（也就是其他语言的属性设置器），修改我们的model \u003c?php namespace App; use DateTime; use Illuminate\\Database\\Eloquent\\Model; class Article extends Model { protected $fillable = [ 'title', 'body', 'published_at' ]; //属性设置其要遵守格式约定 // set属性Attribute public function setPublishedAtAttribute($date) { $this-\u003eattributes['published_at'] = Carbon::createFromFormat('Y-m-d', $date)-\u003ehour(8)-\u003eminute(0)-\u003esecond(0); } } 添加一个新的纪录，查看数据库，我们已经将时间设置正确了，但是我们的首页仍然显示未来的才发布的文章，我们修改它。 public function index() { //$articles = Article::latest('published_at')-\u003eget(); $articles = Article::latest('published_at')-\u003ewhere('published_at', '\u003c=', Carbon::now())-\u003eget(); return view('articles.index', compact('articles')); } 上面的解决方法可以工作，但是查询语句太长了。我们可以使用 Laravel 提供的 scope ，来简化我们的工作。所谓scope可以理解为是查询过程中使用的中间查询结果，比如我们定义一个published scope，他可以返回所有当前已经发布的文章，让我们修改模型。 //设置scope，遵守命名规则 public function scopePublished($query) { $query-\u003ewhere('published_at', '\u003c=', Carbon::now()); } 修改控制器使用 scope public function index() { //$articles = Article::latest('published_at')-\u003eget(); //$articles = Article::latest('published_at')-\u003ewhere('published_at', '\u003c=', Carbon::now())-\u003eget(); $articles = Article::latest('published_at')-\u003epublished()-\u003eget(); return view('articles.index', compact('articles')); } 结果相同，但在复杂的查询中我们可以使用scope来分解我们的任务，或者复用查询。 我们来增加一个新的查询，查询所有还没有发布的文章。在模型中添加scope public function scopeUnpublished($query) { $query-\u003ewhere('published_at', '\u003e', Carbon::now()); } 修改控制器使用unpulished public function index() { //$articles = Article::latest('published_at')-\u003eget(); //$articles = Article::latest('published_at')-\u003ewhere('published_at', '\u003c=', Carbon::now())-\u003eget(); //$articles = Article::latest('published_at')-\u003epublished()-\u003eget(); $articles = Article::latest('published_at')-\u003eUnpublished()-\u003eget(); return view('articles.index', compact('articles')); } one more thing! 如果我们在 show 方法中使用 dd($article-\u0026gt;published_at) 查看的结果，与 dd($article-\u0026gt;created_at); 结果不一样，前者我们使我们自己的字段，后者是在 CreateArticleTable 中通过 $table-\u0026gt;timestamp() 自动生成的。自动生成的字段显示出来是 Carbon类型，而我们的是字符串。使用 Crabon类型有很多的好处，例如你可以输出 dd($article-\u0026gt;created_at-\u0026gt;diffForHumans()); ，这种 1 hour ago 结果，但我们的published_at 不可以。怎么修改？修改模型，告诉laravel，published_at 是日期即可。 protected $dates = ['published_at']; 再次使用 dd($article-\u0026gt;published_at-\u0026gt;diffForHumans()); ，结果显示为 3 days from now，Bingo！ ","date":"2016-02-09","heading":"日期，Mutator 和 Scope","objectID":"/2016/02/laravel5/:11:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#日期mutator-和-scope"},{"categories":null,"collections":null,"content":"表单验证 在建立一个文章的时候，如果你什么都不输入直接提交，ok，你获得了一个空的文章，没有任何错误提示，这是不对的。在命令行下运行 php artisan 可以看到一个选项 make:request，新建一个form request类。在命令行执行 php artisan make:request CreateArticleRequest 生成的文件在 app/http/requests 目录下。在文件中我们可以看到两个方法： public function authorize() { return false; } public function rules() { return [ // ]; } authorize 表示用户在提交表单的时候是否需要是认证用户，我们不需要认证，返回 true。rules是我们的规则方法。让我们修改这个方法： public function authorize() { //修改为 true，表示不需要认证，或者是通过认证 return true; } public function rules() { return [ 'title' =\u003e 'required|min:3', 'body' =\u003e 'required', 'published_at' =\u003e 'required|date' ]; } 其他的约束可以插看 laravel 的文档。上面的约束表示 title 是必须输入的，最少3个字符，body 是必须的，published_at 是必须的而且是日期。 在视图中，我们总是可以访问 $errors 变量来判断我们是否有错误，修改视图 @if ($errors-\u003eany()) @foreach($errors-\u003eall() as $error) {{ $error }} @endforeach @endif {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::open(['url' =\u003e 'articles']) !!} 修改控制器，引入我们的 Request 类。 public function store(Requests\\CreateArticleRequest $request) { Article::create($request-\u003eall()); return redirect('articles'); } 再次提交表单，什么都不填，可以看到错误信息。 ","date":"2016-02-09","heading":"表单验证","objectID":"/2016/02/laravel5/:12:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#表单验证"},{"categories":null,"collections":null,"content":"修改提示信息变为中文 显示的是英文的错误信息，实际上laravel考虑到了国际化的问题，首先修改 config/app.php ， 'locale' =\u003e 'zh', 将 locale 语言设置为中文，然后再 resources/lang 下面新建文件夹 zh， 拷贝 resources/lang/en/validation.php 文件到 zh 目录下，修改： \"min\" =\u003e [ \"numeric\" =\u003e \"The :attribute must be at least :min.\", \"file\" =\u003e \"The :attribute must be at least :min kilobytes.\", \"string\" =\u003e \":attribute 至少要包含 :min 字符。\", \"array\" =\u003e \"The :attribute must have at least :min items.\", ], \"required\" =\u003e \":attribute 必须填写。\", 其他的可以自行翻译。再次提交空表单，错误信息为中文了。而且 min:3 的判断也为最少3个中文。 – laravel 也在控制器中集成了 validate 方法，换句话说，我们不一定要生成 request 类，这些工作我们可以直接在控制器中完成。 修改控制器： //注意 Request 的命名空间，不要弄错了 public function store(\\Illuminate\\Http\\Request $request) { $this-\u003evalidate($request, [ 'title' =\u003e 'required|min:3', 'body' =\u003e 'required', 'published_at' =\u003e 'required|date' ]); Article::create($request-\u003eall()); return redirect('articles'); } 结果相同，这样可以更快速的完成简单的验证。 ","date":"2016-02-09","heading":"修改提示信息变为中文","objectID":"/2016/02/laravel5/:12:1","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#修改提示信息变为中文"},{"categories":null,"collections":null,"content":"子视图和表单复用 我们需要处理编辑文章的问题。当然我们可以手工添加新的路由，就像这样： Route::get('/articles/{id}/edit', 'ArticleController@edit'); 让我们在命令行下使用 artisan 的 route:list 来查看我们当前的路由： php artisan route:list 在符合 RESTful 的情况下，可能直接使用 laravel 的 resource 路由是一种好的选择，然我们将所有的路由都去掉，只添加唯一的一个： Route::resource('articles', 'ArticlesController'); 再次使用 php artisan route:list 查看路由，哇，一堆的符合我们期望的路由产生了。每一项都仔细查看一下。 现在在控制器中添加方法： public function edit($id) { $article = Article::findOrFail($id); return view('articles.edit', compact('article')); } 现在创建视图 @extends('layout') @section('content') Edit: {!! $article-\u003etitle !!} ... 好吧，我承认这些代码都是从 create.blade.php 中拷贝出来的，修改了一下，问题是我们需要重复吗？后面我们会处理这个问题，现在来看一下表单的提交问题。在路由中 php artisan route:list ，再看一遍，修改使用了 PATCH 方法，我们来修改视图： {!! Form::open(['method' =\u003e 'PATCH', 'url' =\u003e 'articles/' . $article-\u003eid]) !!} 在浏览器中访问 /articles/1/edit ，查看一下源代码，发现laravel自动生成了 _method=PATCH 的隐藏字段。 一问题是，我们编辑文章，但是文章的信息并没有显示出来，我们修改一下视图： {!! Form::model($article, ['method' =\u003e 'PATCH', 'url' =\u003e 'articles/' . $article-\u003eid]) !!} OK，everything’s ok，除了 published_on 字段仍然设置为当前日期，后面我们来处理。 现在在控制器中添加方法： public function update($id, \\Illuminate\\Http\\Request $request) { $article = Article::findOrFail($id); $article-\u003eupdate($request-\u003eall()); return redirect('articles'); } 我们在修改的过程中也需要验证，让我们复用我们的 Request 类，将 CreateArticleRequest 更名为更通用的 ArticleRequest，别忘了修改 store 方法中的参数。 public function update($id, Requests\\ArticleRequest $request) { $article = Article::findOrFail($id); $article-\u003eupdate($request-\u003eall()); return redirect('articles'); } 现在剩下的问题是我们的新建和编辑使用了大部分相同的代码，比如显示错误，但他们存在两份，我们来修改这个问题。 我们直接在 views/articles 下面新建文件 list.blade.php，并把错误处理代码从 create.blade.php 中拷贝出来： @if ($errors-\u003eany()) @foreach($errors-\u003eall() as $error) {{ $error }} @endforeach @endif 在 create.blade.php 只需用下面语句替换错误处理代码即可： @include('articles.list') 让我们再来处理表单代码，表单代码中除了 form 不大一样和提交按钮有区别，其他都差不多。我们创建一个视图 articles/form_partial.blade.php，将代码拷贝出来 {!! Form::label('title', 'Title:') !!} {!! Form::text('title', null, ['class' =\u003e 'form-control']) !!} {!! Form::label('body', 'Body:') !!} {!! Form::textarea('body', null, ['class' =\u003e 'form-control']) !!} {!! Form::label('published_at', 'Publish On:') !!} {!! Form::input('date', 'published_at', date('Y-m-d'), ['class' =\u003e 'form-control']) !!} {{--这里要设置变量，依据是编辑还是修改来改变，当然也可以不放置在partial中--}} {!! Form::submit($submitButtonText, ['class' =\u003e 'btn btn-primary form-control']) !!} 修改 create.blade.php @extends('layout') @section('content') Write a New Article @include('articles.list') {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::open(['url' =\u003e 'articles']) !!} @include('articles.form_partial', ['submitButtonText' =\u003e 'Add Article']) {!! Form::close() !!} @stop 修改 edit.blade.php @extends('layout') @section('content') Edit: {!! $article-\u003etitle !!} @include('articles.list') {{--使用我们添加的 illuminate\\html 开源库--}} {!! Form::model($article, ['method' =\u003e 'PATCH', 'url' =\u003e 'articles/' . $article-\u003eid]) !!} @include('articles.form_partial', ['submitButtonText' =\u003e 'Update Article']) {!! Form::close() !!} @stop ","date":"2016-02-09","heading":"子视图和表单复用","objectID":"/2016/02/laravel5/:13:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#子视图和表单复用"},{"categories":null,"collections":null,"content":"Eloquent 关系 一个用户可能有多个文章，一个文章是某个用户书写的，这就是关系。同样文章中可能包含多个 TAG，而一个 TAG 可能关联多个文章。 在项目中，我们已经有了 User.php，也就是用户模型，查看一下，相当简单。我们希望直接使用 $user-\u0026gt;articles() 的形式获取全部文章，让我们修改 user 模型： public function articles() { return $this-\u003ehasMany('App\\Article'); } 但是我们仅仅完成了关系中的一端，让我们来处理另一端。我们希望的形式是 $article-\u0026gt;user() ，让我们修改 article 模型。 public function user() { return $this-\u003ebelongsTo('App\\User'); } 在我们的数据库中，文章模型没有用户的外键，我们需要设置，修改 create_article_table.php Schema::create('articles', function(Blueprint $table) { $table-\u003eincrements('id'); //指定外键列 $table-\u003einteger('user_id')-\u003eunsigned(); $table-\u003estring('title'); $table-\u003etext('body'); $table-\u003etimestamp('published_at'); $table-\u003etimestamps(); //生成外键 $table-\u003eforeign('user_id') -\u003ereferences('id') -\u003eon('users') -\u003eonDelete('cascade'); }); 因为我们只是在开发阶段，还没有上线运行，我们可以直接修改数据库迁移文件，回滚然后迁移即可，但是如果上线运行的话，应该新建一个迁移。 php artisan migrate:refresh #输出信息 Rolled back: 2015_03_28_050138_create_article_table Rolled back: 2014_10_12_100000_create_password_resets_table Rolled back: 2014_10_12_000000_create_users_table Nothing to rollback. Migrated: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_100000_create_password_resets_table Migrated: 2015_03_28_050138_create_article_table Migrated: 2015_03_28_051200_add_excerpt_to_articels_table 现在让我们使用 tinker 来创建一个用户。 php artisan tinker Psy Shell v0.4.1 (PHP 5.4.16 — cli) by Justin Hileman #下面是执行过程 \u003e\u003e\u003e $user = new App\\User; =\u003e {} \u003e\u003e\u003e $user-\u003ename = 'zhang jinglin'; =\u003e \"zhang jinglin\" \u003e\u003e\u003e $user-\u003eemail = 'zjl@example.com'; =\u003e \"zjl@example.com\" \u003e\u003e\u003e $user-\u003epassword = bcrypt('pass'); =\u003e \"$2y$10$Nbl2b9wqd.rXqKEsd3pRSOoIyFAFIhbqf71BufwDfS3Guv21SlEx2\" \u003e\u003e\u003e $user-\u003esave(); =\u003e true \u003e\u003e\u003e App\\User::first()-\u003etoArray(); =\u003e [ \"id\" =\u003e \"1\", \"name\" =\u003e \"zhang jinglin\", \"email\" =\u003e \"zjl@example.com\", \"created_at\" =\u003e \"2015-03-31 03:24:55\", \"updated_at\" =\u003e \"2015-03-31 03:24:55\" ] \u003e\u003e\u003e 现在我们需要新发布的文章和用户关联，我们暂时先修改 form_partial.blade.php 来隐藏一个用户id，只是暂时： {{--临时处理--}} {!! Form::hidden('user_id', 1) !!} 同时要修改模型的 $fillabel 属性，以便我们的 Mass Assisment。 protected $fillable = [ 'title', 'body', 'published_at', 'user_id' //临时设置 ]; OK，添加一个文章。我们使用 tinker 来查看一下。 php artisan tinker Psy Shell v0.4.1 (PHP 5.4.16 — cli) by Justin Hileman \u003e\u003e\u003e App\\Article::first()-\u003etoArray(); =\u003e [ \"id\" =\u003e \"1\", \"user_id\" =\u003e \"1\", \"title\" =\u003e \"User 1 Article\", \"body\" =\u003e \"User 1 Body\", \"published_at\" =\u003e \"2015-03-31 08:00:00\", \"created_at\" =\u003e \"2015-03-31 04:17:58\", \"updated_at\" =\u003e \"2015-03-31 04:17:58\", \"excerpt\" =\u003e null ] #获取用户 \u003e\u003e\u003e $user = App\\User::first(); =\u003e { id: \"1\", name: \"zhang jinglin\", email: \"zjl@example.com\", created_at: \"2015-03-31 03:24:55\", updated_at: \"2015-03-31 03:24:55\" } #获取用户文章 \u003e\u003e\u003e $user-\u003earticles()-\u003etoArray(); BadMethodCallException with message 'Call to undefined method Illuminate\\Database\\Query\\Builder::toArray()' \u003e\u003e\u003e $user-\u003earticles-\u003etoArray(); =\u003e [ [ \"id\" =\u003e \"1\", \"user_id\" =\u003e \"1\", \"title\" =\u003e \"User 1 Article\", \"body\" =\u003e \"User 1 Body\", \"published_at\" =\u003e \"2015-03-31 08:00:00\", \"created_at\" =\u003e \"2015-03-31 04:17:58\", \"updated_at\" =\u003e \"2015-03-31 04:17:58\", \"excerpt\" =\u003e null ] ] #为什么使用 $user-\u003earticles 而不是 #user-\u003earticles()? #事实上，$user-\u003earticles()返回的是关系，如果你想用 articles() 你需要这样用 \u003e\u003e\u003e $user-\u003earticles()-\u003eget()-\u003etoArray(); =\u003e [ [ \"id\" =\u003e \"1\", \"user_id\" =\u003e \"1\", \"title\" =\u003e \"User 1 Article\", \"body\" =\u003e \"User 1 Body\", \"published_at\" =\u003e \"2015-03-31 08:00:00\", \"created_at\" =\u003e \"2015-03-31 04:17:58\", \"updated_at\" =\u003e \"2015-03-31 04:17:58\", \"excerpt\" =\u003e null ] ] #你只能使用 articles() 来进行下一步的工作，比如下面的查询 $user-\u003earticles()-\u003ewhere('title', 'User 1 Article')-\u003eget(); #我们也可以通过 article 获取 user \u003e\u003e\u003e $article = App\\Article::first(); =\u003e { id: \"1\", user_id: \"1\", title: \"User 1 Article\", body: \"User 1 Body\", published_at: \"2015-03-31 08:00:00\", created_at: \"2015-03-31 04:17:58\", updated_at: \"2015-03-31 04:17:58\", excerpt: null } \u003e\u003e\u003e $article-\u003euser; =\u003e { id: \"1\", name: \"zhang jinglin\", email: \"zjl@example.com\", created_at: \"2015-03-31 03:24:55\", updated_at: \"2015-03-31 03:24:55\" } \u003e\u003e\u003e ","date":"2016-02-09","heading":"Eloquent 关系","objectID":"/2016/02/laravel5/:14:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#eloquent-关系"},{"categories":null,"collections":null,"content":"认证 Laravel 出厂已经带有了用户认证系统，我们来看一下 routes.php，如果删除了，添加上： Route::controllers([ 'auth' =\u003e 'Auth\\AuthController', 'password' =\u003e 'Auth\\PasswordController' ]); 可以使用 php artisan route:list 查看一下。浏览器中访问 /auth/login，可以看到登陆界面，最好把系统默认的 app.blade.php 中关于 google 的东西注释起来，要不然你会疯掉的。 你可以使用 register、login甚至 forget password。 实际注册一个用户，提交后失败了，实际上没有失败，只是larave自动跳转到了 /home，我们已经删除了这个控制器。你可以使用 tinker 看一下，用户已经建立了。 在 Auth\\AuthController 中实际上使用了 trait，什么是 triat？well，php只支持单继承，在php5.4中添加了trait，一个trait实际上是一组方法的封装，你可以把它包含在另一个类中。像是抽象类，你不能直接实例化他。 在 Auth\\AuthController 中有对 trait 的引用： use AuthenticatesAndRegistersUsers; 让我们找到他，看一下注册后是怎么跳转的。他隐藏的挺深的，在 vendor/laravel/framework/src/Illuminate/Foundation/Auth/AuthenticatesAndregistersUsers.php，wow。 public function redirectPath() { if (property_exists($this, 'redirectPath')) { return $this-\u003eredirectPath; } //如果用户设置了 redirectTo 属性，则跳转到用户设置的属性，否则到home return property_exists($this, 'redirectTo') ? $this-\u003eredirectTo : '/home'; } OK，我们知道了，只要设定 redirectTo 这个属性就可以自定义注册后的跳转了。我们在 Auth\\AuthContotroller 中修改： protected $redirectTo = 'articles'; 我们先使用 /auth/logout 确保我们退出，如果出错了不要害怕，我们没有默认的主页，重新访问：auth/register 新建一个用户，这次应该ok了。 再次logout，然后使用 login 登陆一下。 现在我们可以删除 form_partial 中临时设置的隐藏字段了，然后修改控制器： public function store(Requests\\ArticleRequest $request) { //你可以这样 //$request = $request-\u003eall(); //$request['user_id'] = Auth::id(); //更简单的方法 $article = Article::create($request-\u003eall()); //laravel 自动完成外键关联 Auth::user()-\u003earticles()-\u003esave($article); return redirect('articles'); } 添加一个文章，然后使用 tinker 查看一下。 ","date":"2016-02-09","heading":"认证","objectID":"/2016/02/laravel5/:15:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#认证"},{"categories":null,"collections":null,"content":"中间件 我们当然不希望任何人都能发布文章，至少是登陆用才可以。我们在控制器中添加保护： public function create() { if (Auth::guest()) { return redirect('articles'); } return view('articles.create'); } 上面的代码可以工作，有一个问题，我们需要在每一个需要保护的方法中都进行上面的处理，这样做太傻了，幸好我们有中间件。 中间件可以理解为一个处理管道，中间件在管道中的某一时刻进行处理，这个时刻可以是请求也可以是响应。依据中间件的处理规则，可能将请求重定向，也可能通过请求。 在 app/http/middleware 中包含了三个中间件，名字就可以看出是干什么，好好查看一下，注意，Closure $next 代表了下一个中间件。 在 app/http/kernel.php 中对中间件进行登记。$middleware 段声明了对所有http都进行处理的中间件，$routeMiddleware 仅仅对路由进行处理，而且你必须显示的声明要使用这其中的某一个或几个中间件。 假设我们想对整个的 ArticlesController 进行保护，我们直接在构造函数中添加中间件： public function __construct() { $this-\u003emiddleware('auth'); } 现在，任何方法都收到了保护。 但我们可能不想整个控制器都受到保护，如果只是其中的一两个方法呢？我们可以这样处理： public function __construct() { $this-\u003emiddleware('auth', ['only' =\u003e 'create']); //当然可以反过来 //$this-\u003emiddleware('auth', ['except' =\u003e 'index']); } 我们不一定在控制器的构造函数中引入中间件，我们可以直接在路由中声明： Route::get('about', ['middleware' =\u003e 'auth', 'uses' =\u003e 'PagesController@about']); 在 kernel.php 中提供的系统中间件，比如 'Illuminate\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode' 是可以让我们进入到维护模式，比如系统上线了，但现在需要临时关闭一段时间进行处理，我们可以在命令行进行处理，看一下这个中间件的工作： php artisan down 访问一下网站，可以看到任何 url 的请求都是马上回来。网站上线： php artisan up 我们来做一个自己的中间件： php artisan make:middleware Demo 然后添加代码： public function handle($request, Closure $next) { //如果请求中含有 foo，我们就回到控制器首页 if ($request-\u003ehas('foo')) { return redirect('articles'); } return $next($request); } 如果希望在全部的请求使用中间件，需要在 kernel.php 中的 $middleware 中登记： protected $middleware = [ ... 'App\\Http\\Middleware\\Demo', ]; 现在我们可以测试一下，假设我们访问 /articles/create?foo=bar ，我们被重定向到了首页。 让我们去除这个显示中间件，我们来创建一个真正有用的中间件。假设我们想保护某个页面，这个页面必须是管理者才能访问的。 php artisan make:middleware RedirectIfNotAManager 我们来添加处理代码： public function handle($request, Closure $next) { if (!$request-\u003euser() || !$request-\u003euser()-\u003eisATeamManager()) { return redirect('articles'); } return $next($request); } 下面修改我们的模型： public function isATeamManager() { return false; } 简单起见，我们直接返回false。这次我们把中间件放置在 kernel.php 中的$routeMiddleware 中。 protected $routeMiddleware = [ ... 'manager' =\u003e 'App\\Http\\Middleware\\RedirectIfNotAManager', ]; 我们做一个测试路由测试一下： Route::get('foo', ['middleware' =\u003e 'manager', function() { return 'This page may only be viewed by manager'; }]); guest身份访问或者登录身份访问都会返回主页，但是如果修改 isATeamManager() 返回 true，登录身份访问可以看到返回的信息。 ","date":"2016-02-09","heading":"中间件","objectID":"/2016/02/laravel5/:16:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#中间件"},{"categories":null,"collections":null,"content":"路由模型绑定 什么事路由模型绑定？让我们看一下下面的代码： public function show($id) { //我们需要自己查询依据 id 获取的模型 $article = Article::findOrFail($id); return view('articles.show', compact('article')); } 可以肯定的是，我们需要在很多的地方获取Id，然后找到模型，这个事情可以交给 Laravel 来做，这就是路由模型绑定。 我们需要先查看一下我们的路由： php artisan route:list 我们可以看到生成的匹配符 articles/{articles}，修改 app/provider/RouteServiceProvider.php public function boot(Router $router) { parent::boot($router); //绑定路由模型 匹配符是 {articles} $router-\u003emodel('articles', 'App\\Article'); } 有了上面的处理，我们在路由中获取到的就不在是id了，而是 Article 对象。修改控制器： public function show(Article $article) { //有了路由模型绑定，我们不需要自己查找数据了 return view('articles.show', compact('article')); } So cool! 修改其他方法： public function edit(Article $article) { return view('articles.edit', compact('article')); } public function update(Article $article, Requests\\ArticleRequest $request) { $article-\u003eupdate($request-\u003eall()); return redirect('articles'); } 当然，我们有完全的控制路由模型如何绑定： public function boot(Router $router) { parent::boot($router); //如果你需要控制模型的生成，可以这样 $router-\u003ebind('articles', function($id) { return \\App\\Article::published()-\u003efindOrFail($id); }); } ","date":"2016-02-09","heading":"路由模型绑定","objectID":"/2016/02/laravel5/:17:0","tags":null,"title":"Laravel 5 基础","uri":"/2016/02/laravel5/#路由模型绑定"},{"categories":null,"collections":null,"content":"原文：Android Swipe Down to Refresh ListView Tutorial 大量的Android程序都提供了下拉刷新的功能。以前我们可以使用自定义的来完成功能，现在Google已经在android.support.v4中提供了这个功能。 ","date":"2016-02-09","heading":"","objectID":"/2016/02/android-listview/:0:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#"},{"categories":null,"collections":null,"content":"1 Android SwipeRefreshLayout 下拉刷新组件非常容易使用，在需要下拉刷新的view之外包裹SwipeRefreshLayout即可。我们使用ListView。当用户向下滑动view的时候就会触发onRefresh()方法，我们需要在这个方法里面实现我们的获取最新数据的逻辑。 ","date":"2016-02-09","heading":"1 Android SwipeRefreshLayout","objectID":"/2016/02/android-listview/:1:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#1-android-swiperefreshlayout"},{"categories":null,"collections":null,"content":"2 案例 JSON 这个案例显示IMDB top 250的数据。每次请求给出20个结果，需要传送offset参数，初始为0。 url： http://api.androidhive.info/json/imdb_top_250.php?offset=0 ","date":"2016-02-09","heading":"2 案例 JSON","objectID":"/2016/02/android-listview/:2:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#2-案例-json"},{"categories":null,"collections":null,"content":"3 建立Android项目 建立项目，选择Blank Activity 打开app文件夹下面的build.gradle，添加volley库依赖，我们使用volley来获取JSON。 build.gradle dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.1.0' compile 'com.mcxiaoke.volley:library-aar:1.0.0' } 在 res =\u0026gt; values下面的colors.xml中添加颜色资源。这个颜色用于设置ListView的背景颜色和影片的排名颜色。 colors.xml \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e #24c6d5 #57dd86 #ad7dcf #ff484d #fcba59 #24c6d5 在项目包下新建三个包，app, activity, helper 在 app包下建立 MyApplication.java，这是单实例的应用，初始化volley核心对象。 MyApplication.java package info.androidhive.swiperefresh.app; import android.app.Application; import android.text.TextUtils; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; /** * Created by Ravi on 13/05/15. */ public class MyApplication extends Application { public static final String TAG = MyApplication.class .getSimpleName(); private RequestQueue mRequestQueue; private static MyApplication mInstance; @Override public void onCreate() { super.onCreate(); mInstance = this; } public static synchronized MyApplication getInstance() { return mInstance; } public RequestQueue getRequestQueue() { if (mRequestQueue == null) { mRequestQueue = Volley.newRequestQueue(getApplicationContext()); } return mRequestQueue; } public void addToRequestQueue(Request req, String tag) { req.setTag(TextUtils.isEmpty(tag) ? TAG : tag); getRequestQueue().add(req); } public void addToRequestQueue(Request req) { req.setTag(TAG); getRequestQueue().add(req); } public void cancelPendingRequests(Object tag) { if (mRequestQueue != null) { mRequestQueue.cancelAll(tag); } } } 在AndroidManifest.xml中添加MyApplication.java类到\u0026lt;application\u0026gt;标记。当然还需要INTERET权限。 AndroidManifest.xml \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e 为listview 创建定制的adaper class。在res=\u0026gt;layout中建立list_row.xml。 \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e 在 helper包中建立Movie.java，listview中的每一行都是这个模型 package info.androidhive.swiperefresh.helper; /** * Created by Ravi on 13/05/15. */ public class Movie { public int id; public String title; public Movie() { } public Movie(int id, String title) { this.title = title; this.id = id; } } 在helper包中建立SwipeListAdapter.java。这是我们的custom adpater。 package info.androidhive.swiperefresh.helper; import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import java.util.List; import info.androidhive.swiperefresh.R; /** * Created by Ravi on 13/05/15. */ public class SwipeListAdapter extends BaseAdapter { private Activity activity; private LayoutInflater inflater; private List movieList; private String[] bgColors; public SwipeListAdapter(Activity activity, List movieList) { this.activity = activity; this.movieList = movieList; bgColors = activity.getApplicationContext().getResources().getStringArray(R.array.movie_serial_bg); } @Override public int getCount() { return movieList.size(); } @Override public Object getItem(int location) { return movieList.get(location); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (inflater == null) inflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (convertView == null) convertView = inflater.inflate(R.layout.list_row, null); TextView serial = (TextView) convertView.findViewById(R.id.serial); TextView title = (TextView) convertView.findViewById(R.id.title); serial.setText(String.valueOf(movieList.get(position).id)); title.setText(movieList.get(position).title); String color = bgColors[position % bgColors.length]; serial.setBackgroundColor(Color.parseColor(color)); return convertView; } } 修改 activity_main.xml。使用listview来显示电影的列表，在外边包裹SwipeRefreshLayout完成滑动刷新。 打开MainActivity.java进行下面的修改完成下来刷新。 实现 SwipeRefreshLayout.OnRefreshListener中的o","date":"2016-02-09","heading":"3 建立Android项目","objectID":"/2016/02/android-listview/:3:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#3-建立android项目"},{"categories":null,"collections":null,"content":"3 建立Android项目 建立项目，选择Blank Activity 打开app文件夹下面的build.gradle，添加volley库依赖，我们使用volley来获取JSON。 build.gradle dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:22.1.0' compile 'com.mcxiaoke.volley:library-aar:1.0.0' } 在 res =\u0026gt; values下面的colors.xml中添加颜色资源。这个颜色用于设置ListView的背景颜色和影片的排名颜色。 colors.xml \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e #24c6d5 #57dd86 #ad7dcf #ff484d #fcba59 #24c6d5 在项目包下新建三个包，app, activity, helper 在 app包下建立 MyApplication.java，这是单实例的应用，初始化volley核心对象。 MyApplication.java package info.androidhive.swiperefresh.app; import android.app.Application; import android.text.TextUtils; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.toolbox.Volley; /** * Created by Ravi on 13/05/15. */ public class MyApplication extends Application { public static final String TAG = MyApplication.class .getSimpleName(); private RequestQueue mRequestQueue; private static MyApplication mInstance; @Override public void onCreate() { super.onCreate(); mInstance = this; } public static synchronized MyApplication getInstance() { return mInstance; } public RequestQueue getRequestQueue() { if (mRequestQueue == null) { mRequestQueue = Volley.newRequestQueue(getApplicationContext()); } return mRequestQueue; } public void addToRequestQueue(Request req, String tag) { req.setTag(TextUtils.isEmpty(tag) ? TAG : tag); getRequestQueue().add(req); } public void addToRequestQueue(Request req) { req.setTag(TAG); getRequestQueue().add(req); } public void cancelPendingRequests(Object tag) { if (mRequestQueue != null) { mRequestQueue.cancelAll(tag); } } } 在AndroidManifest.xml中添加MyApplication.java类到\u0026lt;application\u0026gt;标记。当然还需要INTERET权限。 AndroidManifest.xml \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e 为listview 创建定制的adaper class。在res=\u0026gt;layout中建立list_row.xml。 \u003c?xml version=\"1.0\" encoding=\"utf-8\"?\u003e 在 helper包中建立Movie.java，listview中的每一行都是这个模型 package info.androidhive.swiperefresh.helper; /** * Created by Ravi on 13/05/15. */ public class Movie { public int id; public String title; public Movie() { } public Movie(int id, String title) { this.title = title; this.id = id; } } 在helper包中建立SwipeListAdapter.java。这是我们的custom adpater。 package info.androidhive.swiperefresh.helper; import android.app.Activity; import android.content.Context; import android.graphics.Color; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import java.util.List; import info.androidhive.swiperefresh.R; /** * Created by Ravi on 13/05/15. */ public class SwipeListAdapter extends BaseAdapter { private Activity activity; private LayoutInflater inflater; private List movieList; private String[] bgColors; public SwipeListAdapter(Activity activity, List movieList) { this.activity = activity; this.movieList = movieList; bgColors = activity.getApplicationContext().getResources().getStringArray(R.array.movie_serial_bg); } @Override public int getCount() { return movieList.size(); } @Override public Object getItem(int location) { return movieList.get(location); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (inflater == null) inflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); if (convertView == null) convertView = inflater.inflate(R.layout.list_row, null); TextView serial = (TextView) convertView.findViewById(R.id.serial); TextView title = (TextView) convertView.findViewById(R.id.title); serial.setText(String.valueOf(movieList.get(position).id)); title.setText(movieList.get(position).title); String color = bgColors[position % bgColors.length]; serial.setBackgroundColor(Color.parseColor(color)); return convertView; } } 修改 activity_main.xml。使用listview来显示电影的列表，在外边包裹SwipeRefreshLayout完成滑动刷新。 打开MainActivity.java进行下面的修改完成下来刷新。 实现 SwipeRefreshLayout.OnRefreshListener中的o","date":"2016-02-09","heading":"测试运行","objectID":"/2016/02/android-listview/:3:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#测试运行"},{"categories":null,"collections":null,"content":"4 PHP class 生成 JSON \u003c?php // sleep for 2 sec show that the androd swipe refresh will be visible for sometime sleep(2); // all top 250 movies $movies = array(\"The Shawshank Redemption\", \"The Godfather\", \"The Godfather: Part II\", \"The Dark Knight\", \"Pulp Fiction\", \"Schindler's List\", \"12 Angry Men\", \"The Good, the Bad and the Ugly\", \"The Lord of the Rings: The Return of the King\", \"Fight Club\", \"The Lord of the Rings: The Fellowship of the Ring\", \"Star Wars: Episode V - The Empire Strikes Back\", \"Forrest Gump\", \"Inception\", \"One Flew Over the Cuckoo's Nest\", \"The Lord of the Rings: The Two Towers\", \"Goodfellas\", \"The Matrix\", \"Star Wars\", \"Seven Samurai\", \"City of God\", \"Se7en\", \"The Usual Suspects\", \"The Silence of the Lambs\", \"It's a Wonderful Life\", \"Interstellar\", \"Léon: The Professional\", \"Life Is Beautiful\", \"Once Upon a Time in the West\", \"Casablanca\", \"American History X\", \"Saving Private Ryan\", \"Spirited Away\", \"Raiders of the Lost Ark\", \"City Lights\", \"Psycho\", \"Rear Window\", \"The Intouchables\", \"Whiplash\", \"Modern Times\", \"The Green Mile\", \"Terminator 2: Judgment Day\", \"Memento\", \"The Pianist\", \"The Departed\", \"Apocalypse Now\", \"Gladiator\", \"Sunset Blvd.\", \"Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb\", \"Back to the Future\", \"Alien\", \"The Prestige\", \"The Lion King\", \"The Great Dictator\", \"The Lives of Others\", \"Cinema Paradiso\", \"Django Unchained\", \"The Shining\", \"Paths of Glory\", \"The Dark Knight Rises\", \"American Beauty\", \"WALL·E\", \"North by Northwest\", \"Aliens\", \"Citizen Kane\", \"Grave of the Fireflies\", \"Vertigo\", \"M\", \"Oldboy\", \"Das Boot\", \"Amélie\", \"Princess Mononoke\", \"Star Wars: Episode VI - Return of the Jedi\", \"Once Upon a Time in America\", \"Toy Story 3\", \"Reservoir Dogs\", \"A Clockwork Orange\", \"Braveheart\", \"Taxi Driver\", \"Double Indemnity\", \"Witness for the Prosecution\", \"Requiem for a Dream\", \"To Kill a Mockingbird\", \"Lawrence of Arabia\", \"Eternal Sunshine of the Spotless Mind\", \"Full Metal Jacket\", \"Bicycle Thieves\", \"The Sting\", \"Singin' in the Rain\", \"Amadeus\", \"Monty Python and the Holy Grail\", \"Snatch.\", \"2001: A Space Odyssey\", \"For a Few Dollars More\", \"Rashomon\", \"L.A. Confidential\", \"The Kid\", \"All About Eve\", \"The Apartment\", \"Inglourious Basterds\", \"Toy Story\", \"The Treasure of the Sierra Madre\", \"A Separation\", \"Indiana Jones and the Last Crusade\", \"Yojimbo\", \"The Third Man\", \"Some Like It Hot\", \"Metropolis\", \"Batman Begins\", \"Unforgiven\", \"Scarface\", \"Like Stars on Earth\", \"Raging Bull\", \"Up\", \"3 Idiots\", \"Downfall\", \"Chinatown\", \"The Great Escape\", \"Die Hard\", \"The Hunt\", \"On the Waterfront\", \"Heat\", \"Mr. Smith Goes to Washington\", \"Pan's Labyrinth\", \"Good Will Hunting\", \"The Bridge on the River Kwai\", \"My Neighbor Totoro\", \"Ikiru\", \"The Seventh Seal\", \"The Gold Rush\", \"Ran\", \"Wild Strawberries\", \"The General\", \"Blade Runner\", \"The Elephant Man\", \"Lock, Stock and Two Smoking Barrels\", \"The Secret in Their Eyes\", \"The Wolf of Wall Street\", \"Casino\", \"Gran Torino\", \"Howl's Moving Castle\", \"Warrior\", \"The Big Lebowski\", \"V for Vendetta\", \"Rebecca\", \"The Bandit\", \"Gone Girl\", \"The Deer Hunter\", \"Judgment at Nuremberg\", \"Cool Hand Luke\", \"How to Train Your Dragon\", \"It Happened One Night\", \"Fargo\", \"A Beautiful Mind\", \"Gone with the Wind\", \"Trainspotting\", \"Into the Wild\", \"Rush\", \"Dial M for Murder\", \"The Maltese Falcon\", \"The Sixth Sense\", \"Mary and Max\", \"Finding Nemo\", \"The Thing\", \"The Wages of Fear\", \"Hotel Rwanda\", \"No Country for Old Men\", \"Incendies\", \"Rang De Basanti\", \"Kill Bill: Vol. 1\", \"Platoon\", \"Life of Brian\", \"Butch Cassidy and the Sundance Kid\", \"Network\", \"A Wednesday\", \"Munna Bhai M.B.B.S.\", \"Touch of Evil\", \"There Will Be Blood\", \"12 Years a Slave\", \"Annie Hall\", \"The 400 Blows\", \"Stand by Me\", \"The Princess Bride\", \"Persona\", \"The Grand Budapest Hotel\", \"Amores Perros\", \"Ben-Hur\", \"Diabolique\", \"In the Name of the Father\", \"The Grapes of Wrath\", \"Million Dollar Baby\", \"Sin City\", \"Hachi: A Dog's Tale\", \"Nausicaä of the Valley of the Wind\", \"The Wizard","date":"2016-02-09","heading":"4 PHP class 生成 JSON","objectID":"/2016/02/android-listview/:4:0","tags":null,"title":"Android 使用ListView的下拉刷新","uri":"/2016/02/android-listview/#4-php-class-生成-json"},{"categories":null,"collections":null,"content":"一直就在写博客，不过一直没有合适的博客系统，所以只是在github和coding上记录自己的学习过程。这个春节终于构建了自己的博客，可以放下心来认真的书写了。 这么多年来的学习，这么多年对技术的热爱，已经成为了生命中的一部分，如果你看到我的博客，对你的技术有一些帮助，我就会很开心，不过，即使没有人看，也没关系，这是我的爱好和我的兴趣的源泉。 嘿，春节快乐！ ","date":"2016-02-08","heading":"","objectID":"/2016/02/first/:0:0","tags":null,"title":"基于Hexo和Next的第一篇博客","uri":"/2016/02/first/#"},{"categories":null,"collections":null,"content":"我来到这个世界，不是转一圈就走的，我至少要改变这个世界一下。 张景林，辽东学院信息工程学院，信息工程研究所。 一个爱好 IT 技术的大叔（真的是大叔了），最快乐的事情就是学习，然后分享。 邮箱：ldxyzjl@163.com ","date":"2016-02-08","heading":"","objectID":"/about/:0:0","tags":null,"title":"关于","uri":"/about/#"}]