< ![CDATA[manbetx手机版登陆]]> manbetx手机版登陆 http://www.djindien.com/ 幽灵0.11 星期四,2018年11月1日12:07:48 GMT 60 <![CDATA [我如何学会停止担心并喜欢找工作]]> <p>This is a story about finding a job as a senior software engineer in Toronto - with a twist: just having moved there from Germany</p> <p>I was prepared我有个计划但它完全失败了 - 起初我必须适应和学习这是一个过山车</ p> http://www.djindien.com/how-i-learned-to-stop-worrying-and-love-the-job-hunt-in-toronto/ 5eb051cf-6332-4bb8-8a7d-e8c64e1a0e38 多伦多 工作 manbetx万博体育 星期六,2017年11月11日17:54:31 GMT <p>This is a story about finding a job as a senior software engineer in Toronto - with a twist: just having moved there from Germany</p> <p>I was prepared我有个计划但它完全失败了 - 起初我必须适应和学习It was a roller coaster ride.</p> <p><img src="http://www.djindien.com/content/images/2017/05/pexels-photo-52608-1.jpeg" alt=""></p> <p>I tell my story because it might help others learn from my mistakes这不是< em > < / em >意味着更大的关于移民的声明或状态的科技项目It's simply about my experience.</p> <p>Please note: I will <em>not</em> mention the names of companies这种方式,我能说更多的公开没有伤害任何人的声誉——最重要的是我的Again, it is not intended to denounce anyone, but to give an insight into how I found my job.</p> <h2 id="preparation">Preparation</h2> <p>I'm a software engineer, working in software for almost 10 years, having prior startup experience and coming from a Java/Scala background with React, Go and Node experience from side projects.</p> <p>Although I had quite a good idea of what I wanted, I was open to jobs that didn't fit my criteria perfectly因为我非常享受在一个产品我最后的雇主工作,我想做一遍技术堆栈不是<em> </ em>重要,但有一些我不想再触摸的东西(如PHP和Angular.js 1.x)此外,我正在寻找拥有20到100名工程师的公司来自一个较小的创业,我想体验显著大是什么感觉The type of industry wasn't that important to me, however, shady business models like online gambling were on my blacklist.</p> <h3 id="research">Research</h3> <p>At first, I wanted to get a broad overview of the opportunities in TorontoBy searching through job boards, lists of great workplaces or "startups to watch", I compiled an extensive list of startups.</p> <p>I used Trello to keep track of all of them我总是做了一个关于一个公司的技术堆栈和他们的行业Additionally, I put them on a <a href="https://www.google.com/maps/d/">Google My Maps</a> to see where startups are usually locatedSince I didn't have an apartment yet either, it was useful to know where I should live if I wanted to be close to work.</p> <p><img src="http://www.djindien.com/content/images/2017/05/C7SNabpU4AQ6L3V.jpg_large-1.jpg" alt=""></p> <p>I categorized each company into "Hell, no!" (red), "Well..maybe" (yellow) and "Wow" (green) based on their tech, industry and what I could find on Glassdoor.</p> <h3 id="sideproject">Side Project</h3> <p>As a result of my research, I noticed that the companies I found the most interesting were not using Java but Python, Ruby or Node在几十家创业公司中,我实际上发现了一个基于Java的创业公司Fortunately, there was still enough time to tip the odds in my favor.</p> <p>I had played around with Python for a data science project at my last company我喜欢它与Ruby相比,势头似乎在Python的一面,而Ruby似乎正在下滑Since I already had quite some experience in Node from toy projects, I bet on Python.</p> <p>As a consequence, I read two Python books, one to learn the basics and one about more advanced patternsFurthermore, I practiced with a variety of coding challenge websites - which helped me to apply my new skills, learn various aspects of the language and, by looking at other user's solutions, about idiomatic Python usage.</p> <p>Around the same time, I also had an idea for a side projectSince it was my impression that having a side project was almost required these days, I got to work and started implementing my idea in Python - two birds with one stone.</p> <p>The final result is <a href="https://subvoc.stephanbehnke.com/">subvoc</a>它允许人们在电影中搜索难以理解的单词The idea being that if English is not your native language but you are quite advanced, you can see which words in the movie you just watched in English are the most difficult and you can learn their meaning.</p> <h2 id="applying">Applying</h2> <p>As part of my research I learned that most jobs are not found online, they can only be accessed through your network知道正确的人是北美的路要走虽然我一遍又一遍地阅读,但我认为它不适用于我毕竟我是一名软件工程师,我想,对我来说有很大的需求事实上,我感兴趣的每家公司都有一份大致符合我经验的职位空缺I'm going to be fine, I thought.</p> <p>Well...</p> <p>Within a few days, I had applied to five companies online我花了一些时间为他们每个人写一封求职信它们并不完美,但非常好然后我在等待所有查询填满我的收件箱</p> <p>Nothing正巧</p> <p>Even one company where I seemed to be an almost 100% fit didn't get back to me.</p> <p>I panicked.</p> <p>I was questioning everythingIs my resume not good enough? Is my cover letter bad? Is my skill set not required? Am I lacking experience? Is it because I'm not Canadian? Doubt and uncertainty were getting to me.</p> <p>I knew that going forward, this approach wouldn't work幸运的是,我已经测试了这个第一,没有目标我最受欢迎的公司But what could I do differently now?</p> <h2 id="planbthen">Plan B, then</h2> <h3 id="meetups">Meetups</h3> <p>Good thing I started going to every tech <a href="http://meetup.com/">meetup</a> in the city I could find the time forBasically, every night of the first three weeks I met people from the tech scene这是快速进入现场的关键一步At those meetups I could visit company offices, talk to fellow engineers, learn about the state of IT in Toronto and simply expand my network.</p> <p>But what can work even better is to actually present something at a meetup我设法在当地的Hack&amp; Tell获得了一席之地我展示了我的侧面项目(意外地赢得了Raspberry Pi)当我参加Python聚会时,我借此机会谈论了我在我的副项目中发现的有趣的技术和概念这让我成为人们关注的焦点And people started mentioning to me that they were hiring ...</p> <h3 id="onlinecommunity">Online Community</h3> <p>Then I learned that there are two big online tech Slack communities: TorontoJS and Techmasters自然,我加入了他们的行列,开始阅读和贡献一点然后我采取主动,并在#jobs频道询问工作机会我描述了我在找什么,并告诉他们通过Slack与我联系一些开发人员做了一次谈话实际上导致了一次采访Another one informed me about companies to avoid and eventually put me in contact with a recruiter he trusted.</p> <h3 id="recruiter">Recruiter</h3> <p>I didn't have any experience with recruiters before通常,他们在LinkedIn上写我并忽略它们现在我很绝望我跟招聘人员,向我推荐了一个小时他非常有见地问我一些帮助我意识到重要事项的问题分享了一些如何改进我的简历的技巧他还粗略地了解了我可以期待的薪水All in all, the conversation was extremely helpful.</p> <h3 id="network">Network</h3> <p>Another thing that worked in my favor was that someone from my last workplace knew a few people in Toronto from an annual HR conference她让我联系了一个人,他将我介绍给一个与许多初创公司合作的创新中心的人我们举行了一个简短的会议,我的自我介绍,我很感兴趣的公司Jackpot! The network at work.</p> <h3 id="moreonlineapplications">More online applications</h3> <p>But there were a few companies left that I simply couldn't get closer to我决定再次提交在线申请I completely re-did my resume, applying the tips I got so far, put a lot of effort into polishing my LinkedIn profile, asked for a couple more recommendations from former colleagues and invested more time into the cover letters.</p> <p>A few things that I changed in my resume:</p> <ul> <li>make the skill section even easier to spot and scan by putting it in a right-hand column instead of the bottom third</li> <li>add a short summary section where you mention how many years of experience you have in the very first line</li> <li>make sure each bullet point fits on a single line</li> <li>use colors to highlight different sections</li> </ul> <p>The result? I got two responses out of two applications I sent out.</p> <h3 id="jobmarketplace">Job Marketplace</h3> <p>I had also heard of those platforms where you put up your profile and companies start contacting you我试过<a href="https://hired.ca/"> Hired </a>设置一切花了大约两个小时当我的资料在网上,这是令人兴奋的调查正在涌入在一天之内,我有10家公司对我感兴趣不幸的是,他们中的大多数都不符合我的标准:他们是大型企业公司,不在多伦多或早期创业公司In the end, I talked to two of the companiesAll in all, it was a rewarding experience.</p> <h2 id="prescreening">Pre-Screening</h2> <p>The first hurdle of the interview process is almost always a phone call with someone from HRThere are tons of tips online about this step but here is what I observed.</p> <p>First of all, do your research! Know what the company does, what industry they are in奖励积分:阅读1-2篇博客文章并随便提及您选择的内容如果可能,请与他们一起设置帐户并浏览产品I also tried to find the most important qualities the company expects from its job ads (e.gspeed) and then used these to describe myself.</p> <p>Secondly, everyone usually asks the same questionsThe top 5 for me have been:</p> <ul> <li>Why do you want to work for us?</li> <li>Could you tell me something about yourself?</li> <li>What was your biggest accomplishment?</li> <li>Are you applying elsewhere? / Do you already have an offer?</li> <li>What are your salary expectations?</li> </ul> <p>Don't forget, this is an HR professional他们试图看看你是否可以形成完整的句子,知道你在和谁交谈,并且 - 只是非常粗略 - 适合他们正在寻找的东西Help them understand, use a few keywords from the job ad, put things in perspective and explain the positive impact you had in layman's terms.</p> <h2 id="codingchallenge">Coding Challenge</h2> <p>Coding challenges are a very distinct IT phenomenonIn what other profession do you find such an elaborate and commonplace procedure to put applicants to the test? Maybe this should give us all something to ponderWhy is it that someone who worked at various tech companies, has an active GitHub profile and a blog, is asked to go through programming puzzles? I digress.</p> <p>Here are the coding challenges I had to do:</p> <h3 id="hackerrank">HackerRank</h3> <p>HackerRank is a platform dedicated to coding challenges您可以使用多种编程语言来练习它们Their business model is to sell this platform to companies as a way to test applicants.</p> <p>My challenge involved nine exercises, for example:</p> <ul> <li>Write a regex to validate an IP4 address</li> <li><a href="https://www.hackerrank.com/challenges/balanced-brackets">Determine whether a sequence of brackets is balanced</a></li> <li>Model the design of a vending machine</li> <li><a href="http://stackoverflow.com/questions/11858790">Find the largest possible difference in an array </a></li> <li>Merge two sorted arrays</li> <li>Multiple choice: Which of these Java statements initializes an array? </li> </ul> <p>The trick is that you only have 90 minutes但至少你可以使用IDE和语言的文档Furthermore, there are tests you can run your solution against and get prompt feedback.</p> <p>To be honest, I hated that test我实际上已经在HackerRank上练习,并且认出了两个练习但是,最后我在时间到来之前只完成了大约80%I knew that the test was designed to make me fail but this didn't help me feel better.</p> <h3 id="remoteinterviewio">Remoteinterview.io</h3> <p>This test was very similar to the one before but on a different platform and much more relaxed没有倒计时This made a huge difference in how I could approach the exercises.</p> <p>The challenges were:</p> <ul> <li>Duplicate an array</li> <li>Fix the code to only print out even numbers</li> <li>Fix the code to calculate the square of two numbers</li> <li>Refactor some code to make it more functional</li> <li>Explain what <code>["123", "456", "789a", ...].map(parseInt)</code> returns</li> <li>What ECMAScript proposal are you most excited about?</li> <li>Given a set of requirements, what tech stack would you choose? </li> </ul> <p>I enjoyed this challenge much more than the previous oneThe questions didn't test whether I could find an algorithm to completely artificial problems but rather explored various aspects of programming: understanding and fixing existing code, refactoring, and higher-level thinking and opinions.</p> <p>The exception being the one that tested my knowledge of JavaScript quirks<code> parseInt </ code>有一些非常奇怪的行为但幸运的是,你可以去阅读文档,因为没有倒计时</p> <h3 id="miniproject">Mini-project</h3> <p>Another company asked me to create a web application based on a short specification我喜欢我能够选择我想要的任何语言和框架Furthermore, the project was related to the company's business and was described very well.</p> <p>While they said I shouldn't spend more than a "couple of hours" on it, it took me pretty much one and a half days to finish这主要是因为我选择了一个技术堆栈,我不像Java那样精通(但在Java中构建前端是一种痛苦)Another reason was that I hadn't used PostgreSQL in a while (was required to be used).</p> <p>Anyway, when I finished and packaged it up, I was very close to submitting it但后来我意识到我没有尝试运行我创建的包我试过了,应用程序没有启动我忘了添加其中一个目录我修好了,再次尝试,它有效Good thing I hadn't sent the broken one! </p> <p>All in all, I thought this was a fun and worthwhile challenge, albeit a bit too time-consuming.</p> <h3 id="trickyalgorithm">Tricky Algorithm</h3> <p>From another company, I received a comparatively tiny but still very difficult challenge从事件列表中,我应该找到并返回重叠的所有事件对的列表最重要的是,我被要求用他们选择的编程语言来编写它我只对这种语言有过一些经验Since it's a functional programming language, I made sure to write the algorithm as idiomatic as possible.</p> <p>Coming up with the algorithm took me about an hour - but implementing it as concise, functional and efficient as I could took me roughly three hoursI used tests to cover all the edge cases and to make sure that I was submitting a working solution.</p> <p>This was a very cool challenge我必须解决一个棘手的问题并使用新的编程语言</p> <h3 id="interactivecoding">Interactive Coding</h3> <p>Last but not least, I was asked to prepare a tiny ToDo application (which took me about an hour)Then, during a call with an engineer, I was required to add a few additional features.</p> <p>In the end, I was asked about the space and time complexity of the last, most challenging algorithm总而言之,非常简单我发现与面试官进行对话非常重要特别是,要求他澄清约束和要求会产生巨大的差异Just starting to code is usually a bad move.</p> <h3 id="ignoringthechallenge">Ignoring the Challenge</h3> <p>I got a challenge from a company and due to my prioritization and commitments, I wasn't able to work on it before the deadlineI wrote them and explained the situation, telling them that I cannot interview with them any longer since I'm too busy with other companies.</p> <p>To my surprise, they offered to fly me in to their New York headquarters straight away to meet the team我很困惑Ultimately, I didn't think the company was a good fit, though and I had to move on.</p> <h2 id="interview">Interview</h2> <h3 id="1">#1</h3> <p>Before my first interview, I wasn't really nervousI saw it as a trial run.</p> <p>At first, I was asked by the CTO to speak about my previous work experience and I got to explain my biggest accomplishment in-depth at the whiteboardThey were quite impressed.</p> <p>Then, there was a coding challenge我被要求事先准备好我的开发环境这项练习是为了实现生命游戏,与工程副总裁配对This was interesting because it wasn't purely about my coding skills but also about my communication skills.</p> <p>At first, things went quite badly我对Python的信心并不像我想的那么自信我犯了一群业余错误在前五分钟但由于测试驱动的方法,我逐渐获得信心最后,我按时完成了一个有效的解决方案Success! The CTO and VP of Engineering both commented on the solution being very succinct.</p> <p>Apparently, everyone was fairly impressed because suddenly the CEO was coming in and interviewing me我们交谈了一个小时我询问了公司面临的主要挑战和未来发展方向他问我对角色和工资的期望是什么经过一个漫长的下午,我心情非常愉快地离开了采访室,很快就期待了</p> <p>After two days, I got an email他们拒绝我很惊讶,想知道我做错了什么The person that got me the interview later found out that they were indeed impressed but didn't think they had a suitable role for me at the company at the moment.</p> <h3 id="2">#2</h3> <p>My second interview was with a company that seemed like a natural fit for me technology-wise: They use Java for everythingOn the one hand, I was confident because of my vast Java experience - but on the other hand, I didn't yet know the reason why my first interview didn't get me an offer and feared I was doing something fundamentally wrong.</p> <p>I met with three people who would each throw various tests at me.</p> <p>The first one was basically a Java quiz: What can be marked as <code>final</code>? What types of visibility exist? What is the default behavior of <code>==</code>? What is the relation between <code>hashCode</code> and <code>equals</code>? I knew most of it but it was getting harder with every question当我被问到Java <code> HashMap </ code>实现是如何工作的时候,我画了一个空白在我的整个职业生涯中,我已经使用了数十万次,但最后一次我想到哈希地图如何在内部工作是在大学毋庸置疑,它并没有那么顺利One part of me was angry at myself for not knowing that, and the other part was angry at the interviewer for asking such a low-level question.</p> <p>The second interviewer drew their app's dashboard on the whiteboard and asked me to design a REST API for it我很快就开发了一个解决方案,并讨论了RESTful和优化性能的权衡I even touched on GraphQL.</p> <p>The third interview was about design我是负责设计多人象棋游戏的对象我发现它非常人为面试官似乎也不自信It was an odd experience.</p> <p>I got a call the next morning他们给了我一个报价I told them, I'm still talking to other companies and they were okay with that but wanted an answer soon.</p> <h3 id="3">#3</h3> <p>The process for my next interview was very organized.</p> <p>When I arrived, I was given a quick tour of the office不幸的是,我的印象非常消极他们在办公桌旁边有一个公共区域(响亮!)(应该很安静!)办公室几乎完全没有自然光线 - 感觉就像一个沙坑Afterwards, I was placed in a meeting room where I had four interviews, each one hour long.</p> <p>The first one built upon a homework I was givenMy task was to adapt my solution to new requirements这是有趣的有两个采访者,我们都挤在我的13英寸笔记本电脑周围当我完成练习时,我感到有点不知所措的边缘案例和期待的面试官我没能及时完成最大的问题是,即使在采访者离开后,我的大脑仍在忙着解决最后的任务It took me quite a while to let go and re-focus.</p> <p>The next interview was about data modeling and system design起初,我的任务是建模“收据”但我的头仍然忙于最后一次练习更糟糕的是,我对整个概念感到困惑:在德国,你通常会处理发票而不是收据面试官似乎无法指导我解决这个问题Anyway, I stumbled through the exercise and was pretty disappointed how things went so far.</p> <p>But then it turned around我们还有一点时间,我遇到了系统设计问题:为收据设计OCR现在,在我进入采访之前,我在Glassdoor上阅读了一些内容并发现了这个问题的暗示在面试的前一天,我解决了两次问题它把它们吹走了我能够非常迅速地提出一个非常好的解决方案我可以说他们留下了深刻的印象I'll let you decide whether this was unethical.</p> <p>Then, I talked to two development managers about the way they worked and two 'regular' people about their culture in my last interviewNothing too spectacular.</p> <p>The next morning, I got an offer他们告诉我,我的报价是我各自职位的最高薪水事实上,它超过我对多伦多的一般薪水预期15%This gave me a huge confidence boost.</p> <h3 id="45">#4 &amp; #5</h3> <p>These interviews were very different from the others</p> <p>In the first one, I talked to a development manager for about two hours由于我事先解决了在线编码挑战,因此我们专注于高级问题有趣的是,我们讨论了如何选择工具和技术For example, how to decide between Angular and React.</p> <p>I was invited for a second interview to meet the VP of Engineering采访很快发现他们对我的角色不同于我In the end, they told me that they would get back to me - but I sent an email the next morning telling them I don't think it a good fit and that was that.</p> <h3 id="6">#6</h3> <p>I can not talk about this one since I had to sign an NDABummer.</p> <p><code>¯\_(ツ)_/¯</code></p> <p>Fast forward to the next day, I got an offer.</p> <h2 id="decision">Decision</h2> <p>After all was said and done, I had three offers他们非常不同For example, the difference between the highest and lowest salary was 23%! And that's not counting additional benefits like catered lunch, health benefits etc.</p> <p>I scheduled a phone call with a manager from each of the companies and talked to them for an hour each我们讨论细节像随叫随到的需求,我将加入的团队,我将工作等等I also consulted a friend (fellow software engineer) to get a second opinion.</p> <p>After extensive considerations, I made my choiceI picked the company where I felt I could grow further as a software engineer, that seemed to have a great work-life balance, excellent compensation and worked on stuff that mattered.</p> <h2 id="lessonslearned">Lessons Learned</h2> <p><strong>It's like a sales pipeline</strong>: I applied to 14 companies, talked to 8, interviewed at 5 and got an offer from 3You see how the numbers get smaller? If your pipeline dries out, the odds of getting an offer decline dramatically.</p> <p><strong>Talk to everyone</strong>: Even though I was hesitant to talk to a recruiter at first, it helped me immenselyHe didn't find me a job per se but gave me some insights about the local tech scene and HR best practices, I needed as a newcomer.</p> <p><strong>High-level matters:</strong> Maybe it's because I applied for more senior positions only but in general, there were hardly any low-level questions during the on-site interviews采访者对我如何设计系统,对象关系和API感兴趣当然,我也必须实际编码 - 但是没有人关心我的ACID是什么或让我在白板上编码</p> <p><strong>Side projects are not <em>that</em> helpful:</strong> So at least for a senior developer, having a side project didn't seem to make much of a difference什么也没人问我试着在我的求职信中或在电话放映期间提出全栈位置,但我不知道它是否有任何影响即便是那些痴迷于Python的公司似乎并不关心 - 这让我感到惊讶然后,一家公司甚至不会采访我,因为我没有足够的Python堆栈经验So in the end, it didn't <em>really</em> seem to help that much.</p> <p><strong>Have your personal elevator pitch ready</strong>: The only 'question' I was consistently asked in every interview by every person I met was: "Tell me about yourself." You <em>need</em> a good answer here它可以让你塑造叙事,画出自己的照片让它短暂而引人注目Make it count.</p> <p><strong>Pick one stack and go with it:</strong> During the interviews - with the better companies at least - I was allowed to choose the stack <em>I</em> was comfortable with to solve a problemThis made a huge difference and helped me 'cheat' a bit by picking a typed language (Typescript) so I didn't have to rely on my stressed out brain to provide me with all the function names on the spot.</p> <p><strong>Be able to talk about a project in depth:</strong> When the inevitable question "What are you most proud of?" comes up, it's your turn to impress你应该能够提供足够的上下文侦听器理解为什么它确实是令人印象深刻的此外,如果工程师询问您,您可以展示您的技术专长尽可能详细地了解详细信息Make it painfully obvious that you know what you are talking about.</p> <p><strong>Leverage the opportunity to prepare:</strong> In half of the interviews, I was instructed to prepare a development environment ahead of time这是你获得成功的机会我为最常见的用例预先安装了一堆帮助程序库,例如模板,发出HTTP请求和提供REST APIAnother great idea is to have a test runner ready, running on every file save for continuous feedback.</p> <p><strong>Know complexity and scalability:</strong> Every time I had to code something, I was asked about the time and space complexity as well as whether I can simply make it more efficient偶尔,我还被问到如果输入变得巨大会发生什么,所以它不再适用于一台机器在这里得到答案至关重要</p> <p><strong>Simplify your resume:</strong> First of all, my resume for the first batch of online applications was not good它太长了,不容易“扫描”由于人力资源部门的人员收到了如此多的申请,据称他们只看了10-30秒的简历在大多数采访中,我可以说面试官没有读过我的简历超过一分钟每个人都很忙The lesson here is to make it <em>suuuuper</em> easy to learn the most important facts about you基本上,应用<a href="https://en.wikipedia.org/wiki/Don%27t_Make_Me_Think">不要让我思考</a>的课程如果他们通过关键词判断你,就会输入很多关键词这不容小觑。</ p> <![CDATA [26在启动时成为开发人员的经验]]> < p >在过去的三年里,我曾在一个小的B2B启动在柏林I was the first backend developer and joined the ride of growing it from 200 to 720 business customers, from $200K to $3.2M annual revenue and from 5 to 25 employees.</p> <p>The following lessons are</p> http://www.djindien.com/26-lessons-from-being-a-developer-at-a-startup/ 2496ccb3-8fed-4f8d-A16F-5bc9ec1ef99b 启动 manbetx万博体育 周一,2017年4月10日21:06:39 GMT < p >在过去的三年里,我曾在一个小的B2B启动在柏林I was the first backend developer and joined the ride of growing it from 200 to 720 business customers, from $200K to $3.2M annual revenue and from 5 to 25 employees.</p> <p>The following lessons are a very simplistic, personal summary of what I have learned during that time没有更多,没有更少请享用</p> <p><strong>(1) Retrospectives are crucial.</strong> We regularly sat together as a team to reflect on the last iteration回顾和评估的重要性不容小觑Each participant was required to think about the things that went well and things that need improvement on their own <em>first</em>在</ em>每个人都提出了他们的想法后,小组讨论才开始<em>This way, everyone was able to bring up important issues - much better than the kind of meetings where the 'loudest' dominate the conversation.</p> <p><strong>(2) Take the time for regular 1:1s.</strong> In a busy startup environment it might be hard to justify just <em>talking</em> to your manager for an hour each week但这是我们所做的出色工作你可以深入讨论问题,及时和只是更重视作为一个员工There is no replacement for that.</p> <p><strong>(3) Just pay for developer tools.</strong> For computers, every GB or MHz contributes to being able to work faster对于软件而言,每个有助于提高效率的有用工具都很重要为开发人员提供快速,现代的工具集也表明公司重视他/她不应该讨论这个问题Period.</p> <p><strong>(4) Meet your customers.</strong> We visited our local customers regularly and had an annual company trip to customer hot spots (New York and San Francisco, for example)这一切都有所不同与客户交换电子邮件是一回事,但与他们见面是一种完全不同的体验,并亲自听取他们的痛苦和成功It has a powerful and lasting effect on the way you think about your users when you are back developing</p> <p><strong>(5) Don't let the build become slow.</strong> You know you have acted too late when the slow build becomes a running gag in the office (<a href="https://xkcd.com/303/">XKCD</a> anyone?)几乎没有什么比这更令人沮丧有缓慢的反馈循环我们在后端使用Java,在前端使用Webpack本周两者都变得越来越慢最后,它让我想把头撞在墙上从长远来看,通过购买更快的机器无法</ em>修复慢速构建<em>You need to think about the structure of your application to deal with it; modularising it, for exampleThere is no quick fix.</p> <p><strong>(6) Cross-functional teams FTW.</strong> Before we had formed cross-functional teams, we were only able to effectively tackle small projects一旦我们拥有一个由设计师加上前端和后端开发人员组成的团队,事情便开始了它使我们能够开发更大,更有影响力的功能It brought its own set of challenges but overall it was a crucial step in the evolution of the startup.</p> <p><strong>(7) Trello does not scale well as a bug or project management tool.</strong> We have used <a href="https://trello.com/">Trello</a> to manage our bugs我爱Trello但是我总是觉得不够它很难搜索,只能扩展到某一点项目管理也是如此在某一点上,简单性和易用性逐渐消失,它变得过于庞大和无效I prefer dedicated tools that actually support and facilitate what you are trying to achieve.</p> <p><strong>(8) Take on responsibility or wither away.</strong> To be successful at any company you need to take on responsibilities越大越好与企业环境的不同之处在于,这样做非常容易:角色不是一成不变的,很容易抓住无人认领或不受欢迎的责任你必须成为< em > < / em >人/加的东西来提高你的地位你需要积极主动并塑造自己的形象 - 否则其他人会为你做如果您将此与公司目标保持一致,则可获得奖励积分你可以成为一名优秀的代码架构师 - 但是当公司只重视功能开发时,你就会赌错了马</p> <p><strong>(9) Adapt or leave..或者如果你需要的话就打架。</ strong>你可能在某些事情上不同意你的管理层在这种情况下,您需要确定这些问题对您的重要性如果它非常重要,那么你<em>可以</ em>接受挑战,并为坚持你认为正确的事情而奋斗但往往这将是一场艰苦的战斗如果你周围几乎没有人支持你,或者更糟糕的是,甚至不赞同你的意见,那么你需要问自己是否值得You can either ignore it and play along or look for another job.</p> <p><strong>(10) Look for benefits that actually matter.</strong> A lot of startups boast with heaps of benefits有些人为他们的乒乓球桌感到自豪,有些人想在星期五晚上用开放式酒吧留下深刻印象,有些人则展示他们选择的高果糖玉米糖浆糖果It's a trap! Look for <em>meaningful</em> benefits like lunch and learns, an education budget or health benefits.</p> <p><strong>(11) The CEO should be able to take a vacation.</strong> When a startup is growing, the CEO has to give up more and more responsibilities since a single person cannot scale along with it基本上,首席执行官必须逐步取代自己A good indicator if this is working out is him/her deciding to go on a vacation.</p> <p><strong>(12) You need a strategy for real-time messaging.</strong> We used Slack and while it was fun at times, I think it killed a lot of the productivity我们没有一个共同的心态如何应该使用这个工具It is very important to clearly define what should end up in a chat and what is better left to email, face-to-face conversations or the wiki.</p> <p><strong>(13) You (can) influence how people perceive you - at first.</strong> Whatever you say and do will shape how others perceive you如果你周末工作,你将成为'工作狂'如果您想出新功能,您可能会成为“神奇的孩子”事情是:坚持下去通常,早期印象是最重要的It becomes increasingly harder to change how you are perceived by people around you.</p> <p><strong>(14) Balance senior and junior people.</strong> In the backend, we almost only had senior developers with a combined number of 55+ years of experience但令我惊讶的是,这导致了大量的讨论,很少取得很好的结果那些讨论非常激烈有时候我想知道每个人是否会活着另一方面,在前端,每​​个人都是初级他们显示的积极性和创造性,而意义常常错过大局和缺乏高层次的最佳实践The key is the right mix of junior and senior people.</p> <p><strong>(15) Always have a visual mock before you start coding.</strong> A new feature has many stakeholders例如,有项目经理,设计师,产品所有者,CEO,开发人员和客户他们都有自己的期望和议程传达新功能的愿景的最佳方式似乎是尽可能接近最终结果的视觉表现这一次又一次帮助我们防止误解并将每个人都放在同一页面上</p> <p><strong>(16) One office per team.</strong> It has been shown <a href="https://www.ncbi.nlm.nih.gov/pubmed/21528171">again</a>, <a href="http://www.sciencedirect.com/science/article/pii/S0272494405000538">again</a>, <a href="https://www.jstor.org/stable/255498?seq=1#page_scan_tab_contents">again</a>, <a href="http://lubswww.leeds.ac.uk/fileadmin/webfiles/cstsd/Images/PowerPoint_Presentations/Time_use_and_time_loss_-_DEGW_7Aug2010_-_FINAL.pdf">again</a>, <a href="http://onlinelibrary.wiley.com/doi/10.1002/9781119992592.ch6/summary">again</a>, <a href="https://www.ncbi.nlm.nih.gov/pubmed/11055149">again</a>, <a href="http://journals.sagepub.com/doi/abs/10.1177/0013916582145002">again</a> and <a href="https://www.researchgate.net/publication/307579266_The_demands_and_resources_arising_from_shared_office_spaces">again</a> that an open-plan office is a bad idea我认为一家公司通常希望节省办公室租金,并将其作为合作和创造力的天堂出售From experience I can tell you that you need a quiet environment to get work done - but you still need to be able to communicate easily with your team matesI found the 'one office per team' rule strikes a good balance, if you keep teams small.</p> <p><strong>(17) Extroverts dominate every discussion, unless mitigated.</strong> For an introverted person like me, the workplace can be challenging外向的人喜欢说话和写很多内向的人通常无法竞争我们处于严重的劣势,因为外向的人可以更快,更自信,更精细地回应Any company that does not address this issue loses out on great ideas and contributions by its 'silent minority'.</p> <p><strong>(18) Developers need to talk about their shared mindset.</strong> Once you go from a single developer to a team of developers, there will be conflicts每个人都不同这就是为什么它是如此重要的共享一个共同的心态关于代码风格、架构、开发和错误的过程,代码评审等等只是写下规则wiki不工作它需要在环境需要时生活,理解和改变There is no substitute for regularly talking about it.</p> <p><strong>(19) Regular status updates are motivating.</strong> Knowing that, at the next day's standup my entire team will listen to what I have worked on motivated me对于每周状态更新更是如此当我们的创业公司还很小时,每个人都用上述几句话分享了上周的成功和失败When the company grew bigger and only the team's joint efforts were presented, it was not as motivating anymore.</p> <p><strong>(20) Learning budget needs to be measured in time, not money.</strong> Although we have had a learning budget, it was hardly ever used for something other than attending conferences研讨会可能会很受欢迎 - 通常甚至连最新技术都没有但是和我的许多同事一样,我能够从博客,书籍和视频中学到新东西因此,我建议允许开发人员每年投入几天的时间来自己学习这是我们必须要做的 - 通常在周末 - 但这样公司可以直接支持这些努力</p> <p><strong>(21) Pair programming is underrated.</strong> A nice tool to share knowledge across team members or even across teams is pair programming就个人而言,我发现它比评论更有效编码过程中的互动和讨论是非常宝贵的它的工作效果通常取决于两位开发人员能够相处得多好 - 从经验中我可以说,将不同的人组合在一起实际上可以产生最好的结果,</p> <p><strong>(22) Features get stuck without a proper release process.</strong> This one might seem obvious but can easily be missed我已经目睹了几个月陷入困境的功能,因为没有人真正知道如何使用坐在分支中的完成功能It is paramount to have a clear understanding about who has which responsibilities or, even better, have someone who's job it is to take care of that.</p> <p><strong>(23) Great things happen when there is autonomy.</strong> Giving developers the opportunity to work on things on the side can become a powerful source for innovation当一家公司支持优秀的开发人员拥有的这种天然驱动器时,会发生很如果它不,最专业的开发人员将自己的机会(如加班或周末加班,从长远来看会导致不快乐甚至倦怠</p> <p><strong>(24) When you want a message to stick, say it again and again.</strong> You have an important message you want everyone to understand, share and remember你不能只说一次或者只是把它放在一个幻灯片,粗体字母你需要一次又一次地重复它它必须是清晰的你说的话应该毫无疑问For further advice read the excellent book <a href="http://makeitstick.net/">Make it Stick</a></p> <p><strong>(25) Hacks are a big part of startup culture.</strong> A startup always has more things to do than resources to do it确定优先顺序至关重要这通常意味着快速破解几乎无法运行的解决方案有时候出于好的理由,比如对一个特征进行原型设计并看它是否坚持但如果确实如此,你很少有时间回去让它变得更好然而,我一直觉得非常恼火的是可以庆祝多少黑客总觉得对我作弊但我已经接受了它的意思:黑客在任何初创公司都是必不可少的恶魔Just don't overdo it, please.</p> <p><strong>(26) Deadlines are stupid.</strong> In today's ever changing world, a deadline is an artifact of the past截止日期只会让经理感觉良好,但会杀死一切对软件开发有益的东西截止日期越远,它变得越荒谬和危险唯一更糟的是最后期限<em>加上</ em>一系列要求这让我们回到了软件开发的古老起点There is no place for deadlines in the future.</p> <p>Thank you for reading我很快就会加入另一家创业公司,这次是在多伦多我确信我将在那里学到许多新课程。</ p> <![CDATA[3 years on Google App Engine史诗评论。]]> <p>For the last 3 years I worked on an application that runs on Google App Engine这是谷歌在这里提供的一项迷人而独特的服务不像你在其他地方找到的任何东西This is my in-depth, personal take on it.</p> <p><img src="http://www.djindien.com/content/images/2017/01/google-app-engine-logo.png" alt=""></p> <h2 id="googlescloudest2008">Google's Cloud (est2008)</h2> <p>First of all, what is</p> http://www.djindien.com/3-years-on-google-app-engine-an-epic-review/ ce13c232-987c-4382-ba01-48b806ac99cd 托管 java的 数据存储 谷歌 应用程序引擎 manbetx万博体育 2017年3月13日星期一15:23:59 GMT <p>For the last 3 years I worked on an application that runs on Google App Engine这是谷歌在这里提供的一项迷人而独特的服务不像你在其他地方找到的任何东西This is my in-depth, personal take on it.</p> <p><img src="http://www.djindien.com/content/images/2017/01/google-app-engine-logo.png" alt=""></p> <h2 id="googlescloudest2008">Google's Cloud (est2008)</h2> <p>First of all, what is <a href="https://cloud.google.com/appengine/docs">Google App Engine</a> (GAE) actually? It is a platform to run your web applications on与<a href="https://www.heroku.com"> Heroku </a>一样但是当你仔细观察时会有所不同It is also a versatile cloud computing platform与<a href="https://aws.amazon.com"> AWS </a>一样但不同Let me explain.</p> <p>Google launched GAE in 2008, when cloud computing was still in its infancy亚马逊领先于他们,因为他们已经开始在2006年租用他们的IT基础设施但是对于GAE,谷歌很早就提供了一种复杂的平台即服务(PaaS),亚马逊将在2011年与其Elastic Beanstalk服务相匹配Now what is so special about GAE?</p> <p>It is a <em>fully-managed</em> application platformSo far, I do not know a platform which comes close to GAE's full package: log management, mail delivery, scaling, memcache, image manipulation, distributed Cron jobs, load balancing, version management, task queue, search, performance analysis, cloud debugging, content delivery network - and that is not even mentioning auxiliary services that have popped up on Google's cloud in the meantime like SQL, BigQuery, file storage..名单还在继续</p> <p>By using Google App Engine, you can run your app on top of (probably) the world's best infrastructure此外,您还可以获得开箱即用的功能,这些功能将至少需要Heroku上的第三方附加组件,或者如果您自己完成,则需要几周的安装时间<em>This</em> is GAE's appeal.</p> <p>Noteworthy applications that run on GAE include <a href="https://www.snapchat.com">Snapchat</a> and <a href="https://www.khanacademy.org">Khan Academy</a>.</p> <h2 id="development">Development</h2> <p>The web app I was working on all this time is a single, large Java applicationApp Engine还支持Python,PHP和Go现在您可能想知道为什么选择如此有限一个原因是,为了拥有一个完全托管的环境,Google需要将平台与环境集成你可以说环境和平台是紧耦合的That takes a lot of effort and investment which becomes very clear once you start developing for GAE.</p> <h3 id="sdk">SDK</h3> <p>Each app needs to use a special SDK (Software Development Kit) to use the APIs offered by GAESDK非常庞大例如,Java SDK下载大约为190 MBGranted, some of the JARs in there are not needed for most use cases and some only during development - but still, it certainly is not lightweight (even for Java, that is).</p> <p>The SDK is not just your bridge to the world of Google App Engine but also serves as its simulation on your local machine对于几乎所有GAE API,它都具有您可以开发的存根首先,这意味着当您在本地运行应用程序时,您将<em>完全</ em>接近它在生产中的行为方式其次,您可以轻松地针对API编写集成测试And usually this will get you very far; the mismatch between the production and stub behavior is quite small.</p> <h3 id="javaapis">Java APIs</h3> <p>Speaking of APIs, you are in for a surprise when you use certain Java APIsSince GAE runs your application in some kind of <a href="https://cloud.google.com/appengine/docs/java/runtime?csw=1#The_Sandbox">sandbox</a>, it forbids using particular Java APIs主要限制包括写入文件系统,<code> java.lang.System </ code>的某些方法以及使用Java Native Interface(JNI)使用线程和套接字也有一些特点,但稍后会有更多内容</p> <p>One interesting thing is that the Java SDK actually ensures you do not use these restricted APIs locally当您运行应用程序或仅进行集成测试时,它会使用一个Java代理来监视您的每个方法调用It immediately throws an exception for any detected violation这有助于及早发现违规行为,不仅仅是在生产中,而且还有令人讨厌的副作用在分析应用程序的性能时,代理会进行大量的违规检查In the end, it is hard to judge your app's actual performance since the more method calls you make, the more overhead the agent generates.</p> <h3 id="javadevelopmentkitjdk">Java Development Kit (JDK)</h3> <p>The next thing you might notice when you start developing is that you can <em>not</em> use Java 8尽管Java 7的生命终结时间是在2015年,但它仍然非常活跃并且在GAE上崭露头角The third highest voted issue on <a href="https://code.google.com/p/googleappengine/issues">GAE's issue tracker</a> is <a href="https://code.google.com/p/googleappengine/issues/detail?id=9537">support for Java 8</a> (the second highest is support for Python 3)It was created in 2013从那以后,关于此事的任何进展的唯一新闻是2016年App Engine邮件列表上的帖子,说明工程师正积极致力于此Well, good for you.</p> <p>Obviously, this limitation is a major annoyance for any developer就我个人而言,失踪的lambda支撑非常重当然,可以迁移到许多JVM语言之一,如Groovy,Scala或Kotlin,它们都提供了比Java 8更多的功能。但这是一项代价高昂且风险大的投资我们的项目成本太高,风险太大We also investigated the feasibility of <a href="https://github.com/orfjackal/retrolambda">retrolambda</a>, a backport of lambdas to Java 7, but did not pursue it yet although it looked promising in first tests.</p> <p>Having to stay with an old version is also a liability for the business这使得找到开发人员变得更加困难Overall application security is threatened, as well谷歌告诉我们,支持我们的生产仍将接受安全补丁JDK 7但最终,像Spring这样的所有主要图书馆都将停止支持它Eventually, you'll be <em>stuck</em>.</p> <h2 id="deployment">Deployment</h2> <p>To deploy your application, you need to create an <code>appengine-web.xml</code> configuration file在那里,您可以指定应用程序ID和版本以及一些其他设置,例如marking the app as <code>threadsafe</code> to be able to receive multiple requests per instance simultaneously.</p> <h3 id="upload">Upload</h3> <p>App Engine expects to receive your Java application as a packaged WAR file您可以使用SDK中的<code> appcfg </ code>脚本将其上传到服务器(可选)Maven和Gradle的插件可以像编写<code> mvn appengine一样简单:update </ code>对于典型的Java应用程序,上传可以<em>完全</ em>一段时间,您最好有一个快速的互联网连接Once the process finishes, you can see your newly deployed version in the Google Cloud Console:</p> <p><img src="http://www.djindien.com/content/images/2017/01/Screen-Shot-2017-01-22-at-15.15.45-2.png" alt="Google Cloud Console - Versions"></p> <h3 id="staticfiles">Static Files</h3> <p>Static files like images, stylesheets and scripts are part of any web application today在<code> appengine-web.xml </ code>文件中可以标记为静态Google将直接提供这些文件 - 无需点击您的应用程序It is not <em>exactly</em> a Content Delivery Network (CDN) since it is not distributed to hundreds of edge nodes, but it helps to reduce the load on your servers.</p> <h3 id="versions">Versions</h3> <p>The nice thing in App Engine is that everything you deploy has a specific version可以通过<code> https://&lt; version&gt; -dot-&lt; app-id&gt; .appspot.com </ code>访问每个版本But which one is <em>actually</em> live?</p> <p>You can mark a version as <code>default</code>这意味着当您转到<code> https://&lt; app-id&gt; .appspot.com </ code>(或您为该应用指定的域名)时,这将是接收所有请求的版本将版本切换为<code> default </ code>非常简单:只需按一下按钮或简单的终端命令即可GAE can switch immediately or migrate your traffic incrementally to prevent overwhelming the new version.</p> <p>There is also one option (which we never used) that allows you to distribute your traffic across multiple versionsThis allows incrementally rolling out a new version by only giving it to a fraction of the user base before making it available for everyone.</p> <p>Since it is so easy to create new versions and switch production traffic between them, GAE is a perfect platform to practice <a href="https://martinfowler.com/bliki/BlueGreenDeployment.html">blue-green deployment</a>每次我们由于新版本中的错误而需要回滚时,它都是毫不费力的通过编写一个有点智能的部署脚本,也可以实现持续交付</p> <h3 id="instances">Instances</h3> <p>Every version can run any number of instances (the only limit is your credit card)The actual number is the result of incoming traffic and the scaling configuration of your app; we'll look at that later谷歌将传入的请求分发的所有正在运行的实例之间的版本You can see a list of instances, including some basic metrics like requests and latency, in the Google Cloud Console:</p> <p><img src="http://www.djindien.com/content/images/2017/01/Screen-Shot-2017-01-22-at-15.28.57.png" alt="Google Cloud Console - Instances"></p> <p>The hardware options you can choose from to run these instances on are - let's be frank here - patheticApp Engine basically offers four different <a href="https://cloud.google.com/appengine/docs/about-the-standard-environment#instance_classes">instance classes</a> ranging from 128MB and 600MHz CPU (you read that correctly) to 1024MB and 2.4GHz CPU是的,再一次,这是事实而且真的很伤心On a developer's laptop our app started almost twice as fast as in production.</p> <h3 id="services">Services</h3> <p>So far, I have only talked about a single, monolithic applicationBut what do you do if yours consists of multiple services? App Engine has got you covered每个应用程序都是一项服务If you only have one, it is simply called <code>default</code>You can access each one directly via <code>https://&lt;version&gt;-dot-&lt;service&gt;-dot-&lt;app-id&gt;.appspot.com</code>.</p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-20-at-21.36.57.png" alt="App Engine Application, Service, Version and Instance"></p> <p>You can easily deploy multiple versions of each service, scale and monitor them separately由于每项服务都与其他服务分开,因此您可以运行任何受支持语言的组合不幸的是,一些配置设置在所有服务中共享因此,它们并非完全孤立Still, all in all, GAE seems like a good fit for microservicesGoogle也提供了有关此主题的<a href="https://cloud.google.com/appengine/docs/java/microservices-on-app-engine">详细文档</a>。</p> <p>For reasons that will become clear later, we decided to separate our application into two services: frontend (user-facing) and backend (background work)但要做到这一点,我们实际上并没有将这块巨石分成两部分 - 这可能需要几个月的时间We simply deployed the same app twice and only sent users to one service and background work to the other.</p> <h2 id="operations">Operations</h2> <p>Let's talk about what it means to <em>run</em> your application on App Engine正如您将看到的,它对您施加了许多限制但并不是所有的悲观In the end you will understand why.</p> <h3 id="applicationstartup">Application Startup</h3> <p>When App Engine starts a new instance, the app needs to initialize它将直接从用户发送HTTP请求到应用程序,或者 - 如果配置和扩展情况允许 - 发送所谓的预热请求无论哪种方式,第一个请求称为加载请求And as you can imagine, starting quickly is important.</p> <p>The instance itself on the other hand is ridiculously fast to start如果您之前在云中启动过服务器,则可能需要等待一分钟以上不在GAE上实例几乎立即开始我猜谷歌拥有一批准备好的服务器瓶颈将始终是您自己的应用程序我们的应用程序开始生产需要40多秒因此,除非我们想要将我们庞大的巨型组合分成不同的服务,否则我们需要它更有效地开始</p> <p>The app uses SpringGoogle甚至还提供了专门的文档条目:<a href="https://cloud.google.com/appengine/articles/spring_optimization">优化App Engine应用程序的Spring Framework </a>There we found the inspiration for our most important startup optimization.</p> <p>We got rid of Spring's classpath scanning它在App Engine上特别慢(可能是因为CPU很糟糕)幸运的是,有一个图书馆叫< a href = " https://github.com/atteo/classindex " > classindex < / >它使用特殊注释将类的完全限定路径写入文本文件By simply reading the beans from the text file, the Spring initialization went down by about 8-10 seconds.</p> <h3 id="requesthandling">Request Handling</h3> <p>The very first thing I have to mention here is the requirement of the App Engine to handle a user request within 60 seconds and a background request in 10 minutesWhen the application takes too long to respond, the request is aborted with a 500 status code and a <code>DeadlineExceededException</code> is thrown.</p> <p>Usually, this shouldn't be a problem如果您的应用需要超过60秒的时间来响应,那么用户很快就会消失但由于通过HTTP请求启动一个实例,这也意味着它在60秒开始在生产中,我们观察到启动时间的变化长达10秒这意味着您现在只需不到50秒即可启动应用It is not uncommon for a Java app to take that long.</p> <p>One nice little feature I'd like to highlight is the geographical HTTP headers: for each incoming user request, Google adds headers that contain the user's country, region, city as well as latitude and longitude of said city这可以<em>非常</ em>有用,例如用于预先填写电话号码国家/地区代码或检测异常帐户登录位置从我们的观察来看,准确性似乎也很高从第三方API或数据库获得具有此级别准确性的那种信息通常非常麻烦和/或昂贵So getting it for free on App Engine is a nice bonus.</p> <h3 id="backgroundwork">Background Work</h3> <h4 id="threads">Threads</h4> <p>As mentioned earlier, there are restrictions using Java threads虽然可以启动一个新线程,虽然通过自定义GAE <code> ThreadManager </ code>,但它不能“超过”它在这在实践中可能很烦人,因为第三方库当然不遵循App Engine的限制为了找到一个兼容的图书馆或改编一个看似不相容的图书馆,这些年来我们花了很多汗水和眼泪For example, we could not use the <a href="https://github.com/dropwizard/metrics">Dropwizard metrics</a> library out of the box since it relies on using a background thread.</p> <h4 id="queue">Queue</h4> <p>But there are other ways of doing background work: In the spirit of the Cloud, you apply the divide and conquer approach on the instance levelBy using <a href="https://cloud.google.com/appengine/docs/java/taskqueue/">task queues</a> you can enqueue work for later processing例如,当需要发送电子邮件时,您可以使用有效负载排队新任务(例如,收件人,主题和正文)以及<em>推送</ em>队列中的URL然后,您的一个实例将接收有效负载作为对指定端点的HTTP POST请求If it fails, App Engine will retry the operation.</p> <p>This pattern really shines when you have a lot of work to process简单地将一批独立运行的任务排入队列App Engine将负责故障处理无需自定义重试代码Just imagine how awkward it would be without it: running hundreds of tasks at once you either need to stop and start from scratch when an error occurs or carefully track which have failed and enqueue them again for another attempt.</p> <p>And just like the rest of the App Engine, task queues scale beautifully队列可以得到几乎无限的任务缺点是有效载荷最多只能达到1 MB但我们通常只是简单地将对数据的引用传递给队列But then, you need to take extra good care in your data handling since it can easily happen that something vanishes between the time you enqueue a task and the time that task is actually executed.</p> <p>The queues are configured in a <code>queue.xml</code> fileHere is an example of a push queue that fires up to one task per second with a maximum of two retries:</p> <pre><code class="language-xml">&lt;queue&gt; &lt;name&gt;my-push-queue&lt;/name&gt; &lt;rate&gt;1/s&lt;/rate&gt; &lt;retry-parameters&gt; &lt;task-retry-limit&gt;2&lt;/task-retry-limit&gt; &lt;/retry-parameters&gt; &lt;/queue&gt; </code></pre> <h4 id="cron">Cron</h4> <p>Another extremely valuable tool is the distributed CronIn a <code>cron.xml</code> you can tell GAE to issue requests at certain time intervals这些只是您的一个实例将收到的简单HTTP GET请求可能的最小间隔是每分钟一次It is very useful for regular reports, emails and cleanups.</p> <p>This is what an entry in <code>cron.xml</code> looks like:</p> <pre><code class="language-xml">&lt;cron&gt; &lt;url&gt;/tasks/summary&lt;/url&gt; &lt;schedule&gt;every 24 hours&lt;/schedule&gt; &lt;/cron&gt; </code></pre> <p>A Cron job can also be combined with <em>pull</em> queues: they allow to actively fetch a batch of tasks from a queueDepending on the use case, making an instance pull lots of tasks in a batch can be much more efficient than pushing them to the instance individually.</p> <p>Like all other App Engine configuration files, the <code>cron.xml</code> is shared across all services and versions of an application这可能是令人讨厌的在我们的示例中,有时当我们部署了添加了新Cron条目的版本时,App Engine会开始向实时(但较旧)版本上不存在的端点发送请求 - 为我们的生产错误报告生成噪音I imagine this must be even more painful when using App Engine to host microservices.</p> <p>Also, the Cron jobs are not run locally我可以理解为什么会这样:很多工作通常安排在通常繁忙的时间之外,因此甚至不会在正常工作日触发但是<em>一些</ em>每隔几分钟或几小时运行一次 - 这些都非常有趣例如,他们可能会触发通知你想在本地看到这些因为最终你会引入一个导致不良行为的变化(正如在我们的项目中多次发生的那样)并且在本地看到它可能会阻止你运送它但是在本地模拟Cron工作很棘手(不幸的是我们没有打扰)One would probably need to write an external tool that parses the <code>cron.xml</code> and then pings the according endpoints (yuck!).</p> <h3 id="scaling">Scaling</h3> <p>App Engine will take care of scaling the number of instances based on the trafficHow? Well, depending on how you have configured your applicationThere are three modes:</p> <ul> <li><strong>Automatic:</strong> This is GAE's unique selling point它将根据请求率和响应延迟等指标扩展实例数So if there is a lot of traffic or your app is slow to respond, more instances spin up.</li> <li><strong>Manual:</strong> Basically like your good old virtual private servers您告诉Google您想要多少个实例和Google提供的实例This fixed instance size is useful if you know <em>exactly</em> what traffic you are going to get.</li> <li><strong>Basic:</strong> Essentially the same as manual scaling mode but when an instance becomes idle, it is turned off.</li> </ul> <p>The most useful and interesting one here certainly is the <em>automatic mode</em>It has a few parameters that help to shed some light on how it works internally: <code>max_concurrent_requests</code>, <code>max_idle_instances</code>, <code>min_idle_instances</code> and <code>max_pending_latency</code>To quote the App Engine documentation:</p> <blockquote> <p>The App Engine scheduler decides whether to serve each new request with an existing instance (either one that is idle or accepts concurrent requests), put the request in a pending request queue, or start a new instance for that requestThe decision takes into account the number of available instances, how quickly your application has been serving requests (its latency), and how long it takes to spin up a new instance.</p> </blockquote> <p>Every time we tried to tweak those numbers, it felt like practicing black magic实际上很难在这里推断出一个好的设置Yet, these numbers determine the real-world performance of your app and hugely affect your monthly bill.</p> <p>But all in all, the automatic scaling is pretty wicked它特别适合处理背景工作(例如generating reports, sending emails) since it often - more so than user requests - comes in large, sudden bursts.</p> <p>But the thing is, Java is a terrible fit for this kind of auto scaling due to its slow startup time更糟糕的是,调度程序将请求分配给<em>启动</ em>(冷)实例是很常见的然后,进入亚秒级REST响应的所有努力都会消失自2012年以来,<a href="https://code.google.com/p/googleappengine/issues/detail?id=7865">面向用户的请求永远不会被锁定到冷实例</a>It has not even elicited the slightest comment by Google other than the status change to 'Accepted' (sounds like one of the stages of grief at this point).</p> <p>This also explains why we split our app into two servicesBefore, we often found that with a surge in background requests, the user requests would suffer这是因为App Engine极大地扩展了实例,并且由于请求在实例之间平均路由,因此导致更多用户请求命中冷实例通过拆分应用程序,我们大大减少了这种情况Also, we were able to apply different scaling strategies for the two services.</p> <p>One last thing: In a side-project, I used Go on App Engine and discovered a new perspective on the App EngineGo的特点是能够立即启动应用程序这使得App Engine和Go成为完美组合,如蝙蝠侠和罗宾Together, they embody everything I personally expected from the Cloud ever since I learned about it它真正适应工作负载,并且毫不费力地完成工作Not even the abysmal hardware options seemed to pose a real problem for Go since it is that efficient.</p> <h2 id="data">Data</h2> <p>When App Engine launched, the only database options you had were Google Datastore for structured data and Google Blobstore for binary data从那时起,他们又添加了Google Cloud SQL(托管MySQL)和谷歌云存储(如亚马逊的S3),取代了BlobstoreFrom the beginning App Engine offered a managed Memcache, as well.</p> <p>It used to be very difficult to connect to a third-party database since you could only use HTTP for communication但通常数据库需要原始TCP几年前,当Socket API发布时,这只是改变了但它仍然是< em > < / em >β,这使得它的问题选择关键任务使用因此,在数据库方面,仍有很多供应商锁定</p> <p>Anyway, in the beginning, there was only the Datastore.</p> <h3 id="datastore">Datastore</h3> <p>The Datastore is a proprietary NoSQL database, fully managed by Google它不像我以前用过的任何东西It is a massively scaling beast with very unique traits, guarantees and restrictions.</p> <p>In the early days, the Datastore was based on a master-slave setup which featured strongly consistent readsA few years in, after it had suffered a few severe outtakes, Google <a href="http://googleappengine.blogspot.ca/2011/01/announcing-high-replication-datastore.html">introduced a new configuration option</a>: High ReplicationAPI维持不变,但写的延迟增加,一些读取最终成为< em > < / em >一致(稍后详细介绍)好处是可用性显着增加它甚至拥有99.95%的正常运行时间SLA自从我使用它以来,我从未遇到过Datastore可用性的单个问题It was just something you did not have to think about.</p> <h4 id="entities">Entities</h4> <p>The basics of the Datastore are simple您可以读写<em>实体</ em>它们被归类为特定的<em>种</ em>实体由<em>属性</ em>组成属性具有名称和具有特定类型的值像<code> string </ code>,<code> boolean </ code>,<code> float </ code>或<code> integer </ code>Each entity also has a unique <em>key</em>.</p> <h4 id="writing">Writing</h4> <p>There is no schema whatsoever, though具有相同类型的实体可能看起来完全不同这使得开发非常简单:只需添加一个新属性,保存它就会存在另一方面,您需要编写自定义迁移代码来重命名属性原因是实体无法就地更新 - 必须再次加载,更改和保存根据实体的数量,这可能会成为一项非常重要的任务,因为您可能需要使用任务队列来规避请求时间要求In my experience, this leads to old property names all over the place since refactoring is so costly and dangerous.</p> <p>There are a some <a href="https://cloud.google.com/datastore/docs/concepts/limits">limits for working with entities</a>The two most critical are:</p> <ul> <li>An entity may only be 1MB in total, including additional meta data of the encoded entity</li> <li>You can only write to an entity (group, to be exact) up to once per second</li> </ul> <p>In practice, this can be an issue我们很少大小限制,但是当我们做了,这是痛苦的客户数据可能会丢失当你达到写入速率限制时,通常在下次尝试时就可以了但是,当然你必须设计你的应用程序,以尽量减少这种可能性例如,像定期更新的计数器之类的东西需要做很多工作才能做好Google even has a documentation entry on <a href="https://cloud.google.com/appengine/articles/sharding_counters">using sharding to build a counter</a>.</p> <h4 id="reading">Reading</h4> <p>An entity can be fetched by using its key or via a query按键读取非常一致,这意味着即使您在获取实体之前更新了实体,也会收到最新数据但是,查询不适用它们最终是一致的So writes are not always reflected immediately这可能导致问题并且可能需要减轻,例如通过巧妙的数据建模(例如,使用助记符作为密钥)或利用特殊的数据存储功能(例如entity groups).</p> <p>A query always specifies an entity kind and optional filters and/or sort orders必须为过滤器中使用的每个属性或作为排序键建立索引添加索引只能作为常规写入操作的一部分来完成不会自动在后台在大多数SQL数据库该指数还将增加写操作的时间和成本(稍后详细介绍)</p> <p>If a query involves multiple properties, it requires a multi-index必须在名为<code> datastore-indexes.xml </ code>的配置文件中指定Here is an example:</p> <pre><code class="language-xml">&lt;datastore-index kind="Employee" ancestor="false"&gt; &lt;property name="lastName" direction="asc" /&gt; &lt;property name="hireDate" direction="desc" &lt;/datastore-index&gt; </code></pre> <p>In contrast to other databases, the absence of a multi-index will not just result in an inefficient, slow query - it will fail immediately数据存储区尽力执行高性能查询例如,不等式过滤器仅支持单个属性当然,总有办法在脚下射击自己 - 但它们很少见</p> <p>There are several other features I cannot go into now, for example pagination, projection queries and transactionsGo to the <a href="https://cloud.google.com/appengine/docs/standard/java/datastore/api-overview">Datastore documentation</a> to learn more, it is very extensive and helpful.</p> <p>Compared to other databases the read and write operations are very slow根据我的观察,按键读取平均需要10-20ms很少见到重大偏差我最好的猜测是谷歌序列化实体,只有索引实际上保存在内存中</p> <p>The pricing model seems to support that: you pay for stored data, read, write and delete operations而已请注意,数据库内存不在该列表中操作本身也很便宜:读取100k实体成本为0.06美元,10万次写入操作成本为0.18美元 - 写入操作可以是实际的实体写入,但也可以写入每个索引如果你不写任何东西,你就不付任何代价但是在一分钟之内你就可以写出数十亿字节的数据了这里是踢球者:对于没有实体或十亿的数据库,读写性能基本相同它像疯了一样扩展</p> <h4 id="api">API</h4> <p>The API to the Datatore feels <em>very</em> low-level因此,对于任何严重的Java应用程序没有办法在< a href = " https://github.com/objectify/objectify " > Objectify < / >这是一个由Jeff Schnitzer编写的图书馆如果谷歌还没有这样做,他们应该给他写一张巨大的支票,让App Engine变得更好He wrote it for his own business but the tireless dedication over the years, extensive documentation and support he offers in forums is astoundingWith Objectify, working with the Datastore is actually fun.</p> <p>Here is an example from the documentation:</p> <pre><code class="language-java">@Entity class Car { @Id String vin; String color; } ofy().save().entity(new Car("123123", "red")).now(); Car c = ofy().load().type(Car.class).id("123123").now(); ofy().delete().entity(c); </code></pre> <p>Objectify makes it really easy to declare entities as simple classes and then takes care of all the mapping between the Datastore.</p> <p>It also has a few tricks up its sleeve例如,它带有一级缓存这意味着每当您按键请求实体时,它首先会查看请求范围的缓存,无论该实体是否已被提取这有助于提高性能但是,它也可能令人困惑,因为当您获取实体并对其进行修改但<em>不</ em>保存它时,下一次读取将产生相同的缓存,修改后的对象This can lead to Heisenbugs.</p> <h4 id="developmenttesting">Development &amp; Testing</h4> <p>Since the App Engine is a proprietary cloud database, you cannot just start it locally在计算机上运行应用程序时,SDK会启动模拟数据存储它的行为非常接近生产环境只有性能要好得多,这可能会误导人</p> <p>For running tests against the Datastore, the SDK is also able to start a local Datastore for you但是,这必须是不同的实现,因为它的行为与运行应用程序的行为不同当您意识到丢失的多索引在本地执行应用程序时会抛出错误而在测试同一查询时却不会出现错误多年来,我意外地发布了几个缺少索引的查询到生产中(通常仍然在Beta切换之后) - 尽管我对它进行了测试在与支持人员联系后,他们承认了疏忽,并承诺要解决这个问题 - 一年多后他们仍然没有</p> <h4 id="backups">Backups</h4> <p>Making backups of the Datastore is an atrocious process有手动和自动方式当然,当你有一个生产应用程序时,你想要定期备份The official way is a feature introduced in 2012 which is still in Alpha!</p> <p>By adding an entry to your <code>cron.xml</code> you can initiate the backup process条目将包括实体的名称备份以及谷歌云存储桶来拯救他们当时代已经来临,它将推出一些Python实例备份代码,遍历的数据存储和拯救他们某种专有的备份格式你的桶有趣的是,存储桶可以包含多少文件的限制,因此您最好不时使用新存储桶</p> <p>This is the absolute worst thing about the Datastore.</p> <h3 id="memcache">Memcache</h3> <p>The other crucial way to store data on App Engine is Memcache默认情况下,您将获得<em>共享</ em> MemcacheThis means, it works on a best-effort basis and there is no guarantee how much capacity it will haveThere is also the dedicated Memcache for $0.06 per GB per hour.</p> <p>Objectify is able to use this as a second-level cache只需使用<code> @Cache </ code>注释一个实体,它将在数据存储区之前询问Memcache并首先保存每个实体这会对性能产生巨大影响通常Memcache会在大约5毫秒内响应,这比数据存储快得多我不知道我们可能有任何过时的缓存问题So this works very well in production.</p> <p>The benefits of it are actually very noticeable when Memcache is down这发生在我们每年一次的一两个小时Our site was barely usable, it was that slow.</p> <h3 id="bigquery">Big Query</h3> <p><a href="https://cloud.google.com/bigquery/">BigQuery</a> is a data warehouse as a service, managed by GoogleYou import data - which can be petabytes - and can run analyses via a custom query language.</p> <p>It integrates somewhat well with the Datastore since it allows to import Datastore backup files from Google Cloud Storage我已经使用了几次,遗憾的是并不总是成功对< em > < / em >一些实体的我收到一个神秘的错误我无法弄清楚出了什么问题但是有些实体确实有效摆弄的查询语言文档后,我第一次能够产生的见解一切都认为,这是一个很好的方式来运行简单的分析如果不编写自定义代码,我肯定无法做到这一点但我并没有真正利用该服务的全部潜力我所做的所有查询都可以直接在任何SQL数据库中完成,我们的数据集非常小Only because of the way the Datastore worked did I have to resort to the BigQuery service in the first place.</p> <h2 id="monitoring">Monitoring</h2> <p>The Google Cloud Console brings a lot of features to diagnose your app's behavior in productionJust look at the Google Cloud Console navigation:</p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-27-at-19.58.46.png" alt="Google Cloud Console - Monitoring"></p> <p>This is the result of <a href="https://techcrunch.com/2014/05/07/google-acquires-cloud-monitoring-service-stackdriver/">Google's acquisition of Stackdriver</a> in 2014It still feels like a separate, standalone service - but its integration into Google Cloud Console is improving.</p> <p>Let's look at the capabilities one by one.</p> <h3 id="logging">Logging</h3> <p>It is crucial to access an application's logs quickly and with ease这在App Engine开始时真的很痛苦它曾经非常麻烦,因为它无法在应用程序的<em>所有</ em>版本中进行搜索This meant when you were looking for something, you had to know which version was online at the time - or try several, one by one它几乎无法使用Plus it was extremely slow.</p> <p>Since then, they have added useful filters to show only specific modules, versions, log levels, user agents or status codes它非常强大Still not <em>fast</em>, but it has gotten much better now compared to the early daysHere is how it looks: </p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-27-at-20.26.16.png" alt="Google Cloud Console - Logging"></p> <p>One very unique idea you can see here is that logs are always grouped by request在我遇到的所有其他工具中,例如Kibana,您将只获得与您的搜索匹配的日志行总是显示所有其他日志行匹配搜索,它会给你更多的上下文在调查日志中的问题时,我发现这非常有用,因为它可以立即帮助您更好地<em>了解</ em>发生的事情I truly miss that feature in every other log viewer I use.</p> <p>Another interesting trait of the App Engine is that each HTTP request is automatically assigned a request ID它被添加到传入的HTTP请求中并唯一地标识它这可以方便地将请求与其日志相关联例如,我们发邮件未捕获的异常发生时,包括请求ID,这使得它看起来微不足道的日志The same can be done for frontend error tracking.</p> <h3 id="metrics">Metrics</h3> <p>The Cloud Console gives access to a few basic application metrics这包括请求量和延迟,流量,内存使用情况,实例数和错误计数It is useful as a starting point when investigating an issue and when you want to get a quick first impression of the general state of the app.</p> <p>Here is an example with the app's request volume:</p> <p><img src="http://www.djindien.com/content/images/2017/01/Screen-Shot-2017-01-22-at-15.44.06.png" alt="Google Cloud Console - Graphs"></p> <h3 id="tracing">Tracing</h3> <p>Since the App Engine instance is a black box, you cannot use other tools to diagnose its performance如果日志记录控制台不够,<em> Trace </ em>页面将提供更详细的数据It allows to search for the latency distribution of certain requests.</p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-20-at-22.31.44.png" alt="Google Cloud Console - Trace"></p> <p>When you select a specific request, it opens up a timeline它显示您在日志中看不到的远程过程调用(RPC)另外,还有每个RPC类型的摘要通过单击RPC,可以获得更多详细信息,例如the response size, are shown.</p> <p>This can be extremely helpful to find the cause of a slow requestIn the following example you can see that the request makes a few fast Memcache calls and a very slow Datastore write operation.</p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-20-at-22.32.49.png" alt="Google Cloud Console - Analysis"></p> <p>The only problem is that the RPCs do not include enough information to figure out what happened exactlyFor instance, the detail view of the Datastore write operation looks like this:</p> <p><img src="http://www.djindien.com/content/images/2017/02/Screen-Shot-2017-02-21-at-08.14.16.png" alt="Google Cloud Console - Analysis Detail View"></p> <p>It does not even include the name of the updated entity这是一个巨大的烦恼,可以使整个屏幕几乎无用只有一件事可以帮助:单击“显示日志”按钮在右上角它将包括请求的日志语句< em >内联< / em >交错的rpcThis way you <em>might</em> be able to infer more details from the context.</p> <h2 id="resources">Resources</h2> <p>It is also important to point out that pricing is completely usage-based这意味着您的应用程序的成本几乎逐字节,逐小时和按操作操作这也意味着,入门非常实惠没有固定成本If hardly anyone uses your app - since there is a free quota - you do not pay anything.</p> <p>The biggest item on the bill will most certainly be for the instances, contributing about 80% in my last projectThe next big chunk is likely the Datastore read/write cost, 15% of the total cost for us.</p> <p>There is a nice interface in the Google Cloud Console to keep track of all quotas:</p> <p><img src="http://www.djindien.com/content/images/2017/03/Screen-Shot-2017-03-13-at-11.33.20.png" alt="Google Cloud Console - Quotas"></p> <p>To be more specific, when I say 'all quotas' I mean all quotas Google tells you about实际上,我们遇到了<em>隐形</ em>配额的问题但是,当API可能已经在测试版中时,我<em>认为</ em>无论如何,我们的应用程序的一部分停止工作,我们不知道为什么幸运的是,我们订阅了<a href="https://cloud.google.com/support/"> Google Cloud支持</a>They informed us about said quota and we had to rewrite a part of our application to make it work again.</p> <p>We also had one minor outage due to the confusing pricing setup有一次,我们的一个应用程序突然停止工作,只是回复了默认的错误页面我们花了十分钟才发现我们达到了我们设定的预算限额After we raised it, everything just started working again.</p> <h2 id="support">Support</h2> <p>There is a lot to be said about Google Cloud Support首先,没有它我们就会被严重的麻烦因此,拥有它是任何关键任务应用程序的必需品 - 在我看来例如,大约每年一次,我们的应用程序将停止提供请求我们没有做任何事情导致这一点联系Google支持后,我们会了解到他们已将我们的应用程序移至“不同群集”它刚刚再次运作这是一个非常可怕的情况You cannot do anything but 'pray to the Google gods'.</p> <p>Second of all, it is a hit or miss based on the support person质量差异很大有时我们需要交换十几条消息,直到他们最终理解我们为止像任何支持一样,它可能令人愤怒But in the end, they would usually resolve our issue or at least give us enough information to help us resolve it ourselves.</p> <h2 id="anewage">A New Age</h2> <p>Google is working on a new type of App Engine, the <a href="https://cloud.google.com/appengine/docs/flexible/">flexible environment</a>目前处于测试阶段它的目标是提供两个世界中最好的:App Engine上运行的简便性和舒适性以及Google Compute Engine的灵活性和强大功能It allows to use any programming platform (like Java 9!) on any of the powerful Google Compute Engine machines (like 416GB RAM!) while letting Google take care of maintaining the servers and ensuring the app is running fine.</p> <p>They have been working on this for some years already当然,我们热衷于尝试So far, <a href="https://tech.small-improvements.com/2016/09/12/running-our-app-engine-application-in-the-flexible-environment-java-8/">we weren't that thrilled</a>But let's see where Google is taking this.</p> <h2 id="designforscale">Design for Scale</h2> <p>Now, you can look at the restrictions the App Engine imposes on your app as annoyances但请忍受我一会儿App Engine由Google创建这些人知道如何构建可扩展的系统这些限制仅仅是必要的他们迫使你适应你的应用云计算的方法这是一件好事,应该拥抱如果您觉得自己正在与App Engine作战,那么您就是在反对云的“新”规则This is certainly one lesson I'm taking away from three years on Google App Engine.</p> <p>Some restrictions and annoyances are the result of neglect by Google, though感觉他们只是投入了最低限度事实上,我已经有了过去两年的这种感觉与古老的技术堆栈合作是令人沮丧的,没有任何改善的希望如果存在已知问题但是它们没有修复则令人愤怒收到关于平台前进方向的信息很少令人沮丧You feel trapped.</p> <p>All in all, I liked how App Engine allowed the development team to focus on actually building an application, making users happy and earning money谷歌在运营工作中花了很多麻烦但是,“旧”App Engine即将问世我不认为再开始新项目是个好主意If App Engine Flexible Environment on the other hand can actually fix its predecessor's major issues, it might become a very intriguing platform to develop apps on.</p> <![CDATA[The Curious Case of the Merciless Compiler]]> <p>In the movie <a href="http://www.imdb.com/title/tt0062622/">2001: A Space Odyssey</a> the computer program HAL 9000 goes rogue, showing no mercy towards the space ship's crewThat's exactly how newcomers to the <a href="https://en.wikipedia.org/wiki/Go_(programming_language)">Go programming language</a> must feel.</p> <p>Since its introduction in 2009, the language has produced gigabytes worth of online debate about its very</p> http://www.djindien.com/the-curious-case-of-the-merciless-compiler/ febc4cdd-7546-4b02-9cf5-cebcfacbde0d manbetx万博体育 太阳,2016年8月7日15:43:23 GMT <p>In the movie <a href="http://www.imdb.com/title/tt0062622/">2001: A Space Odyssey</a> the computer program HAL 9000 goes rogue, showing no mercy towards the space ship's crewThat's exactly how newcomers to the <a href="https://en.wikipedia.org/wiki/Go_(programming_language)">Go programming language</a> must feel.</p> <p>Since its introduction in 2009, the language has produced gigabytes worth of online debate about its very opinionated philosophy由于它是静态编译的语言,因此它具有编译器And just like HAL the Go compiler is quite stubborn and - dare I say - <em>merciless</em>.</p> <p>Take the case of <a href="http://stackoverflow.com/questions/19560334">this poor developer</a> who cannot believe the Go compiler could be so cruel:</p> <blockquote> <p>TBH, it is the most stupid thing I have ever seen in a programming language</p> </blockquote> <p>Odd, I thought <a href="https://www.quora.com/What-is-the-worst-mistake-ever-made-in-computer-science-and-programming-that-proved-to-be-painful-for-programmers-for-years">the invention of the null reference</a> is the clear winner hereAnyway, what brought that guy to declare that the pinnacle of stupidity had finally been reached? <strong>The Go compiler will not let you run code with unused imports.</strong></p> <p><img src="http://www.djindien.com/content/images/2016/08/hal.jpg" alt=""></p> <p>At this point, you may wonder why the compiler is so adamant about that? As far as I know, it's unique in that regardWhy, for example, doesn't it just show a warning in this case?</p> <p>Let's ask the people who ought to know: the language creators其中,你会发现两个计算机科学的传说:Rob Pike和Ken Thompson两者都参与了Unix的创建Let's look at their official best practice guide <a href="https://golang.org/doc/effective_go.html#blank_unused">Effective Go</a> for answers:</p> <blockquote> <p>It is an error to import a package [...] without using itUnused imports bloat the program and slow compilation.</p> </blockquote> <p>Fair enough没有人喜欢慢速编译器你会在网上找到令人难以置信的快速Go的编译器Its workflow feels almost like in a dynamically-typed language with a lightning-fast feedback loop.</p> <p>This is no accident</p> <p>Actually, it is one of the language's core design principles不允许未使用导入只是问题的一小部分To get a glimpse of where it all originates, examine the following excerpt from an <a href="http://www.informit.com/articles/article.aspx?p=1623555">interview with Rob Pike</a>:</p> <blockquote> <p>The <strong>starting point was long compile times</strong> —for some of our big software at Google, build times can be unreasonably long, even with our large distributed compilation clustersC和C ++中的依赖关系管理(或缺少关系)会导致编译器中的代码太多<strong>You might say that Go was conceived while waiting for a big compilation.</strong></p> </blockquote> <p>We've all seen countless memes about <a href="https://xkcd.com/303/">what developers do while waiting for the compiler</a> - but this one certainly takes the cake通过这个背景故事,您可以理解为什么Go是从头开始构建的,以便尽快编译为了达到目的,你必须做出牺牲By not allowing <em>waste</em> like unused imports through the compiler pipeline, the process is much more efficientBut to an outsider, only seeing the tip of the iceberg, this may look like "the most stupid thing in a programming language".</p> <p>But the iceberg goes much deeper.</p> <p>The pursuit for the fastest compiler possible produced another merciless decision that heavily influences every application's design: <strong>not allowing circular package dependencies.</strong> When package <code>A</code> depends on package <code>B</code> and vice versa, we have a circular package dependency, or import cycle为此,Go会比你说“循环”更快地向你发出错误</p> <p>From a design perspective, a cycle on package-level is bad news它紧紧地结合了那个周期中的一切,使其更难以改变和推理Interestingly, many other programming languages are pretty tolerant about import cycles例如,在Java中,您甚至可能根本没有注意到代码中存在循环Then why is Go so unrelenting?</p> <p>Compiler speed, againGo使用<a href="https://en.wikipedia.org/wiki/One-pass_compiler">一次性编译器</a>,它只处理每个源文件一次因此,它不像Java的<a href="https://en.wikipedia.org/wiki/Multi-pass_compiler">多遍编译器</a>那么复杂 - 但速度很快这个决定也从根本上< em > < / em >影响的设计It forced the language to be simple: the language specification can easily be read in less than a day, while you'd need an ancient redwood tree to produce enough paper for Java's specification.</p> <p>Technically, we have not yet answered why Go cannot just use warnings during developmentLuckily, the <a href="https://golang.org/doc/faq">Go FAQ</a> does:</p> <blockquote> <p>Some have asked for a compiler option to turn those checks off or at least reduce them to warningsSuch an option has not been added, though, because compiler options should not affect the semantics of the language and because the Go compiler does not report warnings, only errors that prevent compilation.</p> </blockquote> <p>After all you've read, this should come as no surprise将这些错误转化为警告将严重破坏语言的核心理念,重点是速度和简单性Therefore, it's only right to deny any such requests.</p> <p>At this point it's worth mentioning that there are easy ways around this issue during developmentThe idiomatic way is to use the underscore character for the import name:</p> <pre><code>import _ "fmt" </code></pre> <p>This way, it will simply be ignored by the compiler但是出现了一种更聪明的方式:(现在正式的)命令工具<code> goimports </ code>自动添加缺失的导入并删除未使用的导入您可以配置您选择的编辑器,以便在保存文件后运行它正如您现在所期望的那样,它是闪电般快速的它使开发人员退出循环,从一个工具解决另一个工具的问题I cannot help but smile at the irony yet the brilliance of that.</p> <p>As we have seen, the Go compiler might be considered mercilessAt least HAL 9000 had a gentle voice to soften the blow! But as you have seen, it has the very best intentions要获得简单和快速的回​​报,这是一个很小的代价最终,您的代码和工作流程会更好。</ p> <![CDATA [在JavaScript中搜索不可变的,类型安全的数据记录]]> javascript immutablejs babel http://www.djindien.com/hunt-for-immutable-type-safe-record-in-javascript/ 0cbdfd7d-eb86-45f7-a1d7-a47f237cb5e2 JavaScript的 巴别塔 manbetx万博体育 太阳,2016年3月6日15:29:39 GMT <p>自从使用Scala的<code>案例类</ code>以来,我一直想到了一个类型安全的数据记录,它也是不可变的What's not to like? It's type safe <em>and</em> immutable (duh)So I wanted to see if I can get the same thing in JavaScript - the most mutable and dynamic language known to man.</p> <pre><code class="language-javascript">class Person { givenName; familyName; } </code></pre> <p>This will serve as our starting point: a simple class in JavaScriptIt contains two class instance fields according to the <a href="https://github.com/jeffmo/es-class-fields-and-static-properties">ES Class Fields &amp; Static Properties ES7</a> proposal此外,它是可变的After an instance of <code>Person</code> is created, its fields can be changed.</p> <h2 id="immutablejs">immutable.js</h2> <p>Facebook's JavaScript library <a href="http://facebook.github.io/immutable-js/">immutable</a> offers an immutable <code>Record</code> data typeLet's see how far it takes us.</p> <pre><code class="language-javascript">const Person = Record({ givenName: '', familyName: '' }); </code></pre> <p>That's how we define our recordIf you ask me, it seems strange to be required to provide default values in order to define the record structure它混合了两个问题:结构定义和默认值Consequently, we can't require a property to be provided; there is always a default value.</p> <p>Anyway, it's nice that we can access each property directly and are not forced to use something like <code>.get()</code>.</p> <pre><code class="language-javascript">const dad = new Person({ givenName: 'Homer', familyName: 'Simpson' }); dad.givenName // 'Homer' </code></pre> <p>What is very surprising is that there is no runtime error when constructing a record with any additional properties.</p> <pre><code class="language-javascript">const dad = new Person({ givenName: 'Homer', age: 40 }); dad.age // undefined </code></pre> <p>I find this quite dangerousMaybe we can make it less problematic by adding type safety?</p> <blockquote> <p><strong>Type safety in JavaScript?</strong> There are currently two serious efforts to bring type safety to JavaScript: <a href="http://www.typescriptlang.org">TypeScript</a> and <a href="http://flowtype.org">Flow</a>From what I've seen so far, the syntax of them is almost identical.</p> </blockquote> <p>I've picked Flow for this since it seemed easier to get started with and also works well with BabelTypeScript might work as well though.</p> <p>There is an <a href="https://github.com/facebook/immutable-js/issues/203">pending issue</a> to have proper Flow type definitions for <code>immutable</code> but using its current state already works quite well.</p> <pre><code class="language-javascript">const dad = new Person({ givenName: 42 }); // type error: number is incompatible with string const dad = new Person({ givenName: 'Homer', age: 42 }); // type error: property `age` not found </code></pre> <p>That's quite nice! We get checks for wrong data types and unknown initialisation properties.</p> <p>Unfortunately, the property access doesn't seem to get checked properly.</p> <pre><code class="language-javascript">dad.age // type checks OK, returns undefined </code></pre> <p>All in all, we can get quite far with <code>Record</code>But it's not perfect.</p> <h2 id="babel">Babel</h2> <p>It seems we can't reach our goal by writing code, we need to transform codeSince JavaScript doesn't have macros to do that, we take the next best thing: a Babel plugin.</p> <blockquote> <p><strong>What's Babel?</strong> <a href="https://babeljs.io">Babel</a> was born as a transpiler that takes modern JavaScript code and emits code that can run on older platforms where some of the latest feature weren't supported yetBut it has since grown to become a more general code transformation and code generation platform.</p> </blockquote> <p>The plugin needs to know which class to make immutable and which to ignoreSo we create a new decorator to mark the class for transformation:</p> <pre><code class="language-javascript">@Record() class Person { givenName; familyName; } </code></pre> <p>Our Babel plugin will look for <code>@Record</code> and transform the code into this:</p> <pre><code class="language-javascript">@Record() class Person { constructor(init) { this.__givenName = init.givenName; this.__familyName = init.familyName; } __givenName; __familyName; get givenName() { return this.__givenName; } get familyName() { return this.__familyName; } } </code></pre> <p>Here's what happened:</p> <ul> <li>the fields were renamed to be 'private'</li> <li>getters were created to only allow read access</li> <li>a constructor was added to initialise the fields</li> </ul> <p>This is how we could use it:</p> <pre><code class="language-javascript">const dad = new Person({ givenName: 'Homer', familyName: 'Simpson' }); console.log(`created dad ${dad.givenName} ${dad.familyName}`); // will output "created dad Homer Simpson" </code></pre> <p>But it's not terribly useful, yetHow do we change it? By creating a copy! Since this can be quite cumbersome and error-prone for larger objects, we generate a method to help us with that:</p> <pre><code class="language-javascript">@Record() class Person { // ... update(update) { return new Person({ givenName: update.givenName || this.__givenName, familyName: update.familyName || this.__familyName }); } } </code></pre> <p><code>update</code> gets an object and creates a new <code>Person</code> by using the provided data or falls back to the existing data, if none is provided.</p> <p>Now we can easily create a copy of our record:</p> <pre><code class="language-javascript">const son = dad.update({ givenName: 'Bart' }); console.log(`created son ${son.givenName} ${son.familyName}`); // will output "created son Bart Simpson" </code></pre> <p>The next step is to make it type safe基本上,我们的原始代码只接收其字段的类型注释That's it.</p> <pre><code class="language-javascript">@Record() class Person { givenName: string; familyName: string; } </code></pre> <p>The plugin now mostly just copies the new type annotations to the right places but also creates two new types:</p> <pre><code class="language-javascript">@Record() class Person { constructor(init: PersonInit) { this.__givenName = init.givenName; this.__familyName = init.familyName; } __givenName: string; __familyName: string; get givenName(): string { return this.__givenName; } get familyName(): string { return this.__familyName; } update(update: PersonUpdate): Person { return new Person({ givenName: update.givenName || this.__givenName, familyName: update.familyName || this.__familyName }); } } type PersonInit = { givenName: string; familyName: string; }; type PersonUpdate = { givenName?: string; familyName?: string; }; </code></pre> <p>The first one, <code>PersonInit</code>, defines the type of the object used to initialise the record第二个,<code> PersonUpdate </ code>,定义用于创建记录副本的对象的类型重要的是要注意它包含可选属性(最后用<code>?</ code>标记)This means that the client doesn't have to specify any of them.</p> <p><strong>We now a have a type safe, immutable record.</strong></p> <pre><code class="language-javascript">new Person({ givenName: 'Homer', familyName: 'Simpson' }); // OK new Person({ givenName: 'Homer' }); // type error: property `familyName` not found new Person({ givenName: 'Homer', familyName: true }); // type error: boolean is incompatible with string new Person({ givenName: 'Homer', familyName: 'Simpson' }).update({}); // OK </code></pre> <p>Unfortunately, the type checker does <em>not</em> fail when providing unknown properties during initialisation or updateThis means we could easily have a typo for an optional field and wonder why nothing happens.</p> <pre><code class="language-javascript">const daughter = dad.update({givnam: 'Lisa'}; // OK </code></pre> <p>Apparently, this is a known limitation in FlowThe <a href="https://github.com/facebook/flow/issues/1164">workaround</a> is to <em>seal</em> the object type by adding a 'catch-all property' with a <code>void</code> type.</p> <pre><code class="language-javascript">type PersonInit = { givenName: string; familyName: string; [key: string]: void; }; type PersonUpdate = { givenName?: string; familyName?: string; [key: string]: void; }; </code></pre> <p>Now when we use an invalid property key, it fails.</p> <pre><code class="language-javascript">const daughter = dad.update({givnam: 'Lisa'}; // type error: string is incompatible with undefined </code></pre> <p>This admittedly slightly cryptic error message now tells us that <code>givnam</code> is not part of the update data type.</p> <p><em>By the way, to make this type check in Flow the configuration file <code>.flowconfig</code> needs the flag <code>unsafe.enable_getters_and_setters=true</code> to process the getters.</em></p> <hr> <p>The source code for this code generator can be found on <a href="https://github.com/stephanos/babel-plugin-immutable-record">GitHub</a></p> <![CDATA [Zero to Om - Act 6]]> <p>欢迎来到我们的下一步行动今天我们将会遇到一些额外的库,这些库可以帮助我们编写出很棒的Om应用程序Let's get started!</p> <p>As always, I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-5">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="sablono">sablono</h2> <p>In a previous post I showed you how the</p> http://www.djindien.com/zero-to-om-6/ 021d1a4d-4785-4815-aae5-fe776f257041 OM react.js JavaScript的 客户端 todomvc 模式 secretary sablono om-tools manbetx万博体育 太阳,2014年11月9日18:00:00 GMT <p>欢迎来到我们的下一步行动今天我们将会遇到一些额外的库,这些库可以帮助我们编写出很棒的Om应用程序Let's get started!</p> <p>As always, I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-5">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="sablono">sablono</h2> <p>In a previous post I showed you how the application's UI is rendered:</p> <pre><code class="language-clojure">(dom/div nil (header) (dom/input #js {:id "new-todo" :ref "newField" :placeholder "What needs to be done?" :onKeyDown #(enter-new-todo % state owner)}) (listing state) (footer state))))) </code></pre> <p>As you can see, a <code>dom/*</code> HTML element receives a map of properties: <code>#js {...}</code> or <code>nil</code> for no properties这有点尴尬没有设计师会对此感到高兴And that's where <a href="https://github.com/r0man/sablono">sablono</a> can help you turn the code into this:</p> <pre><code class="language-clojure">(html [:div (header) [:input {:id "new-todo" :ref "newField" :placeholder "What needs to be done?" :on-key-down #(enter-new-todo % state owner)}] (listing state comm) (footer state)]))) </code></pre> <p>Ah, much better! Each HTML element is represented by a keyword at the beginning of a vector然后,可选的映射定义其属性其他所有内容必须评估为另一个HTML元素,依此类推All attributes match ClojureScript's default naming conventions and are automatically converted to the camel-cased version on rendering.</p> <p>To get started with sablono you just need to include its namespace and macro:</p> <pre><code class="language-clojure">(ns todomvc.app (:require [...] [sablono.core :as html :refer-macros [html]]) </code></pre> <p>Note that by using sablono the compiled output grows from 192 KB (47 KB gzipped) to 201 KB (48 KB gzipped).</p> <h2 id="secretary">secretary</h2> <p>For the routing, meaning the mapping between application state and browser URL, we can use <a href="https://github.com/gf3/secretary">secretary</a> (great name, right?).</p> <p>When we look at the namespace declaration of <code>app.cljs</code> we can see the various items required for routing:</p> <pre><code class="language-clojure">(ns todomvc.app (:require [...] [goog.events :as events] [secretary.core :as secretary :include-macros true :refer [defroute]]) (:import [goog History] [goog.history EventType])) </code></pre> <p>That's quite a lotLet's see what we have here: </p> <ul> <li>from secretary, a macro called <code>defroute</code> and the namespace <code>secretary.core</code></li> <li>from Google Closure, the namespace <code>goog.events</code> as well as the elements <code>History</code> and <code>EventType</code> from <code>goog.history</code></li> </ul> <p>First, let's define the routing rules:</p> <pre><code class="language-clojure">(defroute "/" [] (swap! app-state assoc :showing :all)) (defroute "/:filter" [filter] (swap! app-state assoc :showing (keyword filter))) </code></pre> <p>There are two routesWhen the first one matches, it sets the <code>:showing</code> entry of the application state to <code>all</code>When the second one matches it sets it to the value of <code>filter</code>.</p> <p>But how are the routes wired to the browser? That's where our modules from Google Closure come into play:</p> <pre><code class="language-clojure">(def history (History.)) (events/listen history EventType.NAVIGATE (fn [e] (secretary/dispatch! (.-token e)))) (.setEnabled history true) </code></pre> <p>A new instance of Google Closure's <code>History</code> is created, and a new event listener calls secretary's <code>dispatch!</code> function with the current history state (the event's <code>token</code> field) for each of its navigation events.</p> <p><strong>Note</strong>: Since the <code>setEnabled</code> method fires an event for the current location immediately, it has to come after the event listener.</p> <p>The only place where the user can navigate to different URLs is the <code>footer component</code>:</p> <pre><code class="language-clojure">(defn footer [{:keys [todos] :as state}] (let [count (count (remove :completed todos)) sel (-&gt; (zipmap [:all :active :completed] (repeat "")) (assoc (:showing state) "selected"))] (html [:footer {:id "footer" :style (hidden (empty? todos))} [:span {:id "todo-count"} [:strong count] (str " " (pluralize count "item") " left")] [:ul {:id "filters"} [:li [:a {:href "#/" :class (sel :all)} "All"]] [:li [:a {:href "#/active" :class (sel :active)} "Active"]] [:li [:a {:href "#/completed" :class (sel :completed)} "Completed"]]]]))) </code></pre> <p>We can see that there are three possible routing states: <code>all</code>, <code>active</code> and <code>completed</code>对于每个州,页脚中都有一个超链接,用于触发相应的路径Nothing more to it!</p> <p>By the way, the original Om application from the last posts already included secretaryCompiled to JavaScript it was 192 KB (47 KB gzipped) but without any routing it shrinks to 172 KB (40 KB gzipped).</p> <h2 id="omtools">om-tools</h2> <p>Prismatic is a very early adopter of Om and with <a href="https://github.com/Prismatic/om-tools">om-tools</a> they provide a collection of utilities to help eliminate a few annoyances and add extra functionalityTo get started you need to include the namespace:</p> <pre><code class="language-clojure">(ns todomvc.app (:require [...] [om-tools.core :refer-macros [defcomponent]]) </code></pre> <p>Here is how the <code>todo-item</code> component looked like until now:</p> <pre><code class="language-clojure">(defn todo-item [todo owner] (reify om/IInitState (init-state [_] ...) om/IRenderState (render-state [_ state] ...) </code></pre> <p>That's a lot of boilerplate code! om-tools adds a new macro called <code>defcomponent</code> that brings simplicity to it:</p> <pre><code class="language-clojure">(defcomponent todo-item [todo owner] (init-state [_] ...) (render-state [_ state] ...) </code></pre> <p>It makes component definitions much smaller and easier to read.</p> <p>Another big feature is the integration of Prismatic's <a href="https://github.com/Prismatic/schema">schema</a> library which allows declarative data description and validationI could write an entire blog post about it but luckily <a href="http://blog.getprismatic.com/schema-0-2-0-back-with-clojurescript-data-coercion/">Prismatic already did</a>So I'll just highlight how this works in our little TodoMVC applicationFirst, we need to include the <code>schema</code> library:</p> <pre><code class="language-clojure">(ns todomvc.app (:require [...] [om-tools.core :refer-macros [defcomponent]]) </code></pre> <p>Then we define a schema for a Todo:</p> <pre><code class="language-clojure">(def Todo {:id s/Str :title s/Str :completed s/Bool}) </code></pre> <p>It's pretty self-explanatory so farNow we can add the schema to Om component parameters:</p> <pre><code class="language-clojure">(defcomponent todo-item [todo :- Todo owner] ... </code></pre> <p>The <code>:- Todo</code> connects the parameter to the schema现在,理论上,当创建组件时,验证参数But before that we need to enable the validation:</p> <pre><code class="language-clojure">(s/with-fn-validation (om/root todo-app app-state {:target (.getElementById js/document "todoapp")})) </code></pre> <p>Unfortunately, when I tried this it didn't work我还没有找到原因,但可能是因为组件是通过<code> build-all </ code>创建的不确定But the <a href="https://github.com/Prismatic/om-tools/tree/master/examples/sliders">om-tools example</a> actually works, so go on over there to see it in action.</p> <p>Anyway, to get what we came here for: let's validate the schema explicitly.</p> <pre><code class="language-clojure">(defcomponent todo-item [todo :- Todo owner] (init-state [_] (s/validate Todo todo) </code></pre> <p>Now, when we create a new Todo with the field <code>completd</code> it results in an error once the <code>todo-item</code> component is created:</p> <pre><code>Uncaught Error: Value does not match schema: {:completed missing-required-key, :completd disallowed-key} </code></pre> <p>This means we can catch errors earlier and prevent some of those subtle bugs we all know and love from dynamic languagesAll the improvements come at a price, though: the resulting JavaScript code adds up to 264 KB (61 KB gzipped) now.</p> <p>There are a few other things the library brings to the table - like mixins - so make sure to look into the <a href="https://github.com/Prismatic/om-tools">documentation</a>.</p> <hr> <p>That's it for today您已经了解了一些库如何帮助您从Om中获得更多价值。</ p> <![CDATA [Zero to Om - Act 5]]> <p>在这部分中,我们将仔细研究应用程序的构建配置,并了解它可以为我们做些什么The source code can be found on <a href="https://github.com/stephanos/om-tutorial/tree/act-5">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-4">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="thebuildconfig">The Build Config</h2> <p>The project's</p> http://www.djindien.com/zero-to-om-act-5/ 7675a490-6d06-4aa6-919a-b0fbe66306ed OM leiningen JavaScript的 谷歌关闭 cljsbuild todomvc manbetx万博体育 星期四,2014年9月25日12:51:00 GMT <p>在这部分中,我们将仔细研究应用程序的构建配置,并了解它可以为我们做些什么The source code can be found on <a href="https://github.com/stephanos/om-tutorial/tree/act-5">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-4">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="thebuildconfig">The Build Config</h2> <p>The project's build configuration is defined in <code>project.clj</code>It is written in Clojure and looks like this:</p> <pre><code class="language-clojure">;; project.clj, part 1 (defproject todomvc "0.1.0-SNAPSHOT" :jvm-opts ^:replace ["-Xms4g" "-Xmx4g" "-server"] :dependencies [[org.clojure/clojure "1.6.0"] [org.clojure/clojurescript "0.0-2280"] [org.clojure/core.async "0.1.267.0-0d7780-alpha"] [secretary "1.2.0"] [om "0.7.1"]] :plugins [[lein-cljsbuild "1.0.4-SNAPSHOT"]] </code></pre> <p>Basically, it's just one big map of vectors从<Leiningen自动加载<code>依赖项</ code><code>cljsbuild</code> is a plugin that allows to compile ClojureScript to JavaScript - its configuration is:</p> <pre><code class="language-clojure">;; project.clj, part 2 :source-paths ["src"] :cljsbuild { :builds [{:id "dev" :source-paths ["src"] :compiler { :output-to "app.js" :output-dir "out" :optimizations :none :source-map true}}]}) </code></pre> <p>Earlier we've learned that to build the application we can use <code>lein cljsbuild once dev</code>The command starts Leiningen and executes the <code>dev</code> build profile of the <code>cljsbuild</code> exactly once如果我们使用<code> auto </ code>,它会监视任何更改的源路径并触发构建Very handy during development.</p> <p>If you look into the <code>out</code> folder in the project's root directory after a build, you'll see that there is a folder for every dependency we've specified在里面你会找到库的源文件,用ClojureScript和JavaScriptAdditionally, the Google Closure library files can be found in the <code>goog</code> directory.</p> <p><strong>Note:</strong> The <code>source-map</code> option tells the plugin to generate a <code>&lt;file-name&gt;.js.map</code> file for each compiled fileThis allows the browser to show you the corresponding ClojureScript source code instead of an incomprehensible jumble of JavaScript code when there is a runtime error.</p> <h2 id="publishing">Publishing</h2> <p>There will come a time when you want to publish your application and share it with the world<code> out </ code>文件夹包含大约<strong> 2 MB的JavaScript </ strong>更不用说,他们分散在88个独立的文件Sounds like the requirements for a browser benchmark, not like a usable web application!</p> <p>So we need to reduce the number of files and the total file size这就是谷歌关闭编译器可以帮助我们Below you'll find a new build profile called <code>release</code> which will solve all of our problems:</p> <pre><code class="language-clojure">:cljsbuild { :builds [{:id "dev" ...} {:id "release" :source-paths ["src"] :compiler { :output-to "out/app.min.js" :optimizations :advanced :elide-asserts true :pretty-print false :source-map "app.js.map" :externs ["src/react-externs.js"]}}]} </code></pre> <p>In contrast to the <code>dev</code> profile it outputs a single file, uses advanced optimizations, removes assertions and does not pretty print the JavaScript codeTo run this build profile we use <code>lein cljsbuild once release</code>It takes noticeable longer than the development profile but we'll only run this when we actually want to publish the app anyway.</p> <p><strong>Note:</strong> The Closure Compiler is the bulldog among minifiers: very aggressive, stopping for no oneThat's why you need to tell it what code it should NOT remove by using 'externs files', such as <a href="https://github.com/swannodette/react-cljs/blob/master/src/react/externs/react.js">react-extern.js</a> in our case.</p> <p>The following table gives you a feeling for the influence that the different options have on the final resultIt shows the file sizes for the different 'modes' the Closure Compiler offers:</p> <table style="width: 90%; margin: 0 auto;"> <col width="50%"> <col width="25%"> <col width="25%"> <tr> <td> <strong>mode</strong> </td> <td> <strong>size</strong> </td> <td> <strong>gzip size</strong> </td> </tr> <tr> <td> whitespace </td> <td> 1343 KB </td> <td> 180 KB </td> </tr> <tr> <td> simple </td> <td> 858 KB </td> <td> 119 KB </td> </tr> <tr> <td style="width: 33%"> advanced </td> <td style="width: 33%"> 192 KB </td> <td style="width: 33%"> 47 KB </td> </tr> </table> <p><br></p> <p>The advanced mode reduces the file size by roughly 78% compared to the simple modeThe Closure Compiler achieves this minification by making a dead code analysis: removing JavaScript code that won't be used by the app.</p> <p>So the best result we can expect is 47 KB gzipped听起来很合理为了进行比较,如果没有<code>:elide-asserts true </ code>选项,高级模式的结果将增长到195 KB(48 KB gzip压缩)Without setting <code>:pretty-print false</code> it even grows to 275 KB (53 KB gzipped).</p> <p>Of course there is one thing we've left out: the footprint of the React library缩小版本为123 KB(34 KB gzip压缩)这使得总共81 KB gzip压缩I'll let you decide for yourself whether this would be an acceptable value for <em>your</em> app :)</p> <p><br></p> <p><strong>Tip of the Day:</strong> Leiningen allows you to specify an alias for single or multiple commandsThis way, we can just create a <code>publish</code> command that runs a build using the release profile - and does a cleanup beforehand:</p> <pre><code class="language-clojure">:aliases { "publish" ["do" ["cljsbuild" "clean"] ["cljsbuild" "once" "release"]] } </code></pre> <h2 id="outlook">Outlook</h2> <p>There is a project called <a href="https://github.com/thheller/shadow-build">shadow-build</a> that allows you to split the final JavaScript file into multiple files, eg putting the common code (like the standard library) into one file and the rest into separate files然后你就可以在应用程序中有单独的“领域”例如,没有进入管理部分的用户将不需要为其下载源代码Currently, getting this functionality into ClojureScript directly is <a href="http://dev.clojure.org/display/design/Google+Closure+Modules">being discussed</a>.</p> <hr> <p>This concludes our journey through the build facilities最后,我推荐了一眼< a href = " https://github.com/emezeske/lein-cljsbuild/blob/master/sample.project.clj " > cljsbuild示例< / >它记录了ClojureScript插件的所有可能选项</p> <p>In <a href="http://www.djindien.com/zero-to-om-6"><strong>Act 6</strong></a> we'll have a look at libraries that help us create better Om applications!</p> <![CDATA [Zero to Om - Act 4]]> <p>到目前为止,应用程序相当无聊它只显示数据But we want to actually use it! In this post we will take a look at how the app reacts (no pun intented) to user inputThe source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading</p> http://www.djindien.com/zero-to-om-act-4/ a7855962-2cb7-42f7-bb17-2f0c653c4d4b OM react.js JavaScript的 客户端 游标 core.async todomvc manbetx万博体育 星期四,2014年9月18日08:57:00 GMT <p>到目前为止,应用程序相当无聊它只显示数据But we want to actually use it! In this post we will take a look at how the app reacts (no pun intented) to user inputThe source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-3/">previous post</a> first if you haven't done so already.</p> <hr> <p>Managing state is tricky每个框架都有自己的检测和处理状态改变的机制Here is Om's.</p> <h2 id="innerstate">Inner State</h2> <p>In this section we will look at how state inside the component can be managed.</p> <h3 id="componentstate">Component State</h3> <p>In the previous post we've already learned that components can have inner, transient state在我们的示例中,<code> todo-item </ code>组件使用符号<code>:edit-text </ code>来分配当前文本字段的值This snippet should refresh your memory:</p> <pre><code class="language-clojure ">(defn todo-item [todo owner] (reify om/IInitState (init-state [_] {:edit-text (:title todo)}) om/IRenderState (render-state [_ state] ... (dom/input #js {:className "edit" :value (om/get-state owner :edit-text) :onChange #(change % todo owner) ...})))))) </code></pre> <p>Now, let's have a look at the event handler for <code>onChange</code>:</p> <pre><code class="language-clojure ">(defn change [e todo owner] (om/set-state! owner :edit-text (-&gt; e .-target .-value))) </code></pre> <p>The handler changes the <code>:edit-text</code> state to the input field's value<code> - &gt; </ code>是一个允许在没有额外嵌套的情况下编写语句的宏(即<code>(.- checked(。-target e))</ code>)但是在“线程”上The snippet above shows one of two options to update a component's inner state:</p> <ul> <li><code>om/set-state!</code>: sets a new value</li> <li><code>om/update-state!</code>: applies an update function to the current value</li> </ul> <p>Both will trigger an Om re-rendering.</p> <h3 id="applicationstate">Application State</h3> <p>We've learned earlier that the entire application state is kept in a single atom, the <em>root atom</em>但我们只将一个子集传递给一个组件(例如<code> todo </ code>到<code> todo-item </ code>)How do we keep the subsets in-sync with the root state?</p> <p>With <em>Cursors</em>! From Om's documentation: </p> <blockquote> <p>Cursors split one big mutable atom into smaller sub-atoms that remain in-sync with the state held in the root atom</p> </blockquote> <p>Basically you can imagine a cursor a bit like a pointer to a portion of the root state一个组件可以应用更改光标和应用程序状态会改变当应用程序状态中的数据发生更改时(例如,来自HTTP请求),Om将重新呈现依赖于更改值的所有组件Therefore the binding is two-way.</p> <p>To read values from a cursor you'll need to dereference it (eg <code>@my-cursor</code>) - except during the render phase (ie inside <code>render</code> and <code>render-state</code>) where it can be treated just like a regular valueTo change a cursor value you have two options:</p> <ul> <li><code>om/update!</code>: sets a new value</li> <li><code>om/transact!</code>: applies an update function to the current value</li> </ul> <p>Let's see how this works in our app.</p> <p>The <code>todo-item</code> component contains a checkbox field which toggles the item's statusIts <code>onChange</code> event is handled here:</p> <pre><code class="language-clojure ">(defn complete [todo] (om/transact! todo :completed #(not %))) </code></pre> <p>The <code>transact!</code> function receives (1) the component's cursor <code>todo</code> to the todo item, (2) the key <code>:completed</code> to specify which part of the data to change and (3) a function literal that simply negates the current value.</p> <p>Another example is in the main <code>todo-app</code> componentIt also contains a checkbox but this one toggles the status of all items:</p> <pre><code class="language-clojure ">(defn toggle-all [e state] (let [checked (-&gt; e .-target .-checked)] (om/transact! state :todos (fn [todos] (vec (map #(assoc % :completed checked) todos)))))) </code></pre> <p>Here the value of the checkbox binds to <code>checked</code><code> transact!</ code>更新函数使用<code> map </ code>返回一系列待办事项,<code> completed </ code>键设置为<code> checked </ code>Don't forget: we are working with immutable data so both <code>map</code> and <code>assoc</code> do not modify the data but return new values.</p> <p><strong>Note:</strong> With <code>vec</code> we make sure that the return value is always a vector because Om's cursors only work with associate data structures (ie ClojureScript maps and indexed sequential data structures, such as the vector).</p> <p>To learn more about cursors I suggest reading <a href="https://github.com/swannodette/om/wiki/Cursors">Om's Cursor docs</a>.</p> <h2 id="outerstate">Outer State</h2> <p>Often the state we'd like to manage will not be within direct reach of the componentThe two mechanisms we've learned above will not be enough then.</p> <h3 id="parentcomponents">Parent Components</h3> <p>Sometimes it is not feasible to change the application state since a sub-component often only has a cursor to a part of the state那么父母应该负责But how do they communicate with another?</p> <p>Usually, in JavaScript this is handled by callbacks or event bubbling但是在ClojureScript中我们有更好的东西:类似队列的通道The <a href="http://clojure.com/blog/2013/06/28/clojure-core-async-channels.html">Clojure blog</a> describes it perfectly:</p> <blockquote> <p>A key characteristic of channels is that they are blockingIn the most primitive form, an unbuffered channel acts as a rendezvous, any reader will await a writer and vice-versa.</p> </blockquote> <p>Basically, this allows us to easily synchronize two or more asynchronous actions - while writing sequential code这一切都可追溯到20世纪70年代的<a href="http://en.wikipedia.org/wiki/Communicating_sequential_processes"> Tony Hoare的通信顺序流程(CSP)</a>It recently gained popularity through an implementation in the <a href="http://golang.org/">Go programming language</a>.</p> <p>Channels are provided by the library <code>core.async</code>, made possible only by the power of macrosTo get started, we need to load the library at the beginning of a file:</p> <pre><code class="language-clojure ">(ns todomvc.app (:require-macros [cljs.core.async.macros :refer [go]]) (:require [cljs.core.async :refer [put! &lt;! chan]]) </code></pre> <p>The <code>todo-app</code> component uses the <code>will-mount</code> lifecycle function to create a new channel once the app is started:</p> <pre><code class="language-clojure ">(defn todo-app [{:keys [todos] :as state} owner] (reify om/IWillMount (will-mount [_] (let [comm (chan)] (om/set-state! owner :comm comm) (go (while true (let [[type value] (&lt;! comm)] (handle-event type state value)))))) </code></pre> <p><code>(chan)</code> returns a new unbuffered channel然后添加到组件的状态和关键<代码>:通讯> < /代码Finally, a <code>go</code> block is opened: inside we find an endless loop that reads a value from the channel with <code>(&lt;! comm)</code> and destructures it into a vector with two items, <code>[type value]</code></p> <p>It is important to note that reading from the channel is a blocking operation, waiting for data being sent through the channel foreverBut wouldn't the app completely freeze since JavaScript is single-threaded? Well, that's why we need to wrap all <code>&lt;!</code> calls with <code>go</code>: it is a macro that allows to have small, contained event loops within the overall global event loopThe inner ones block while the global one keeps running.</p> <p><strong>Note:</strong> The <code>comm</code> channel is now passed to every function or component that needs itTo avoid confusion: all code snippets from the previous post omitted this.</p> <p>Back to our exampleThe data that was read from the channel is passed to <code>handle-event</code>:</p> <pre><code class="language-clojure ">(defn handle-event [type state val] (case type :destroy (destroy-todo state val) :edit (edit-todo state val) :save (save-todos state) :cancel (cancel-action state) nil)) </code></pre> <p>The function uses <code>case</code> to select a form to execute based on the value of <code>type</code>它处理<code> todo-item </ code>组件可能要发送的所有事件Let's have a look at one of its event handlers that fires when you click the red 'x' on a todo item:</p> <pre><code class="language-clojure ">(defn destroy [todo comm] (put! comm [:destroy @todo])) </code></pre> <p>It uses <code>put!</code> to send the vector <code>[:destroy @todo]</code> over the channel注意事先取消引用光标的<code> @ </ code> - 所以发送的是值,而不是光标It is then read by the endless loop we saw above, passed to the <code>handle-event</code> and finally ends up as a parameter to <code>destroy-todo</code>:</p> <pre><code class="language-clojure ">(defn destroy-todo [state {:keys [id]}] (om/transact! state :todos (fn [todos] (vec (remove #(= (:id %) id) todos))))) </code></pre> <p>Here we see that the state cursor and the todo's id are used to update the application state待办事项由所有待办事项的副本替换,不包括任何具有传入ID的项目Remember, the function literal can also be written as <code>(fn [todo] (= (:id todo) id))</code>.</p> <p>The other functions in <code>handle-event</code> basically all work the same way, nothing we haven't seen by now already.</p> <p>This just scratched the surface of the possibilities of channelsTo learn more about channels I suggest reading <a href="http://rigsomelight.com/2013/07/18/clojurescript-core-async-todos.html">ClojureScript core.async todos</a> and trying out this <a href="https://github.com/swannodette/hs-async/blob/master/src/hs_async/core.cljs">source code example</a>.</p> <h3 id="domnodes">DOM nodes</h3> <p>There comes a time in every app when you need to work with an actual DOM node, eg reading and changing its value通过使用<code> ref </ code>关键字,可以将<em>引用名称</ em>分配给节点后来,例如在一个事件处理程序,节点可以访问这个名字</p> <p>In our app we need this for the 'new' text field of the <code>todo-app</code> componentHere with the newly added <code>ref</code> key:</p> <pre><code class="language-clojure ">(dom/input #js {:id "new-todo" :ref "newField" :onKeyDown #(enter-new-todo % state owner)} ...} ... </code></pre> <p>With the <code>ref</code> in place, the event handler <code>enter-new-todo</code> can access the node:</p> <pre><code class="language-clojure ">(defn enter-new-todo [e state owner] (when (== (.-which e) ENTER_KEY) (let [new-field (om/get-node owner "newField") new-field-text (string/trim (.-value new-field))] (when-not (string/blank? new-field-text) (let [new-todo {:id (guid) :title new-field-text :completed false}] (om/transact! state :todos #(conj % new-todo))) (set! (.-value new-field) ""))) false)) </code></pre> <p>When the user hits the enter key it creates a new todo: by using <code>om/get-node</code> with the parameters <code>owner</code> (the underlying React component) and <code>newField</code> (the reference name) the text field (a native browser DOM node) can be retrievedCalling <code>.-value</code> on the node reads its text content如果它不为空,则创建一个新项目并通过<code> om / transact!</ code>保存到应用程序状态Finally, the input's value is reset.</p> <p>Another example is the 'edit' text field of the <code>todo-item</code> component, now with the added <code>ref</code> key:</p> <pre><code class="language-clojure ">(dom/input #js {:ref "editField" :className "edit" ...} ... </code></pre> <p>On double-clicking the todo label the <code>edit</code> event handler is called:</p> <pre><code class="language-clojure ">(defn edit [e todo owner comm] (let [todo @todo node (om/get-node owner "editField")] (put! comm [:edit todo]) (doto owner (om/set-state! :needs-focus true) (om/set-state! :edit-text (:title todo))))) </code></pre> <p>There's a lot going on! The <code>todo</code> symbol binds to the dereferenced cursor and <code>node</code> to the input's DOM nodeThe vector <code>[:edit todo]</code> is written to the channel with <code>put!</code> (and handled by the <code>handle-event</code> function we saw earlier), putting the app into 'editing' modeAt last, the component state is updated to reflect the editing of the todo.</p> <p><strong>Note:</strong> <code>doto</code> takes <code>owner</code> and calls the function of each subsequent form with it as the first parameter.</p> <p>If you want to learn more about refs I have just what you need: <a href="http://facebook.github.io/react/docs/more-about-refs.html">More About Refs</a>.</p> <hr> <p>This concludes today's act! We have seen how state is managed in Om我们没有查看示例应用程序的<em>每个</ em>行,但您看到了最重要的部分Feel free to browse the <a href="https://github.com/stephanos/om-tutorial">full source code</a> to explore the rest of it.</p> <p>In <strong><a href="http://www.djindien.com/zero-to-om-act-5/">Act 5</a></strong> we'll learn how to publish the app properlyMeanwhile, here are a few links to dive deeper into Om:</p> <ul> <li><a href="http://blog.getprismatic.com/om-sweet-om-high-functional-frontend-engineering-with-clojurescript-and-react/">Om sweet Om</a>, describes Om's benefits in a real-life project</li> <li><a href="https://github.com/swannodette/om/wiki/Basic-Tutorial">Basic Om Tutorial</a>, an example-driven tutorial from David Nolen</li> <li><a href="http://www.djindien.com/zero-to-om-act-4/vimeo.com/97516219">Functional UI programming with React.JS and ClojureScript</a>, shows how to build an UI for the minesweeper game from scratch</li> <li><a href="https://github.com/swannodette/om/wiki/Documentation">Om's API docs</a></li> </ul> <![CDATA [Zero to Om - Act 3]]> <p>在第三篇文章中,我们将了解应用程序的初始化和呈现方式The source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-2/">previous post</a> first if you haven't done so already</p> <p>For increased comprehension we will jump through the two files and</p> http://www.djindien.com/zero-to-om-act-3/ 4b22a850-4fb1-44ff-8845-0c45f01a8d48 OM react.js JavaScript的 客户端 todomvc manbetx万博体育 星期四,2014年9月11日17:08:00 GMT <p>在第三篇文章中,我们将了解应用程序的初始化和呈现方式The source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om-act-2/">previous post</a> first if you haven't done so already</p> <p>For increased comprehension we will jump through the two files and not go through them from top to bottom.</p> <hr> <h2 id="thestate">The State</h2> <p>Most applications, at least the interesting ones, have some kind of stateIn our case we are storing two data points:</p> <ul> <li>a collection of todos</li> <li>a view filter (i.e"All", "Active" or "Completed")</li> </ul> <p>The exciting thing about Om is that we put all of the state into exactly one place:</p> <pre><code class="language-clojure ">(def app-state (atom {:showing :all :todos []})) </code></pre> <p>The symbol <code>app-state</code> binds to an <code>atom</code> of a map: the view filter (<code>showing</code>) is set to 'all' and the <code>todos</code> are an empty collection.</p> <p>As you see, a map can be defined by curly brackets: <code>{key1 val1 ..keyN valN} </ code>(<em>很高兴知道:</ em>元素数量不均匀会导致编译错误)Optionally, you could just set a <code>,</code> between pairs since a comma is treated as whitespace in Clojurescript.</p> <p>An <code>atom</code> is a so-called <em>reference value</em>, meaning that it basically points to immutable dataWhy couldn't we just use a simple symbol and make it point to new data after a change? Well, an <code>atom</code> has the ability to register watchers when you change its value with <code>swap!</code> (or <code>reset!</code>)Om leverages this to re-render components on a change of state.</p> <p>By pushing all of the application state to the edges of the system, the rest of the code remains functional since there is only a single mutable object nowPutting the state in one specific place reduces complexity and is therefore much easier to reason about.</p> <h2 id="theroot">The Root</h2> <p>Now we'll meet Om for the first time! Finally, huh? So first we need to load it:</p> <pre><code class="language-clojure">(ns todomvc.app (:require [om.core :as om :include-macros true] [om.dom :as dom :include-macros true] [...]) </code></pre> <p>Macros - since they are a special kind of function - cannot be loaded with the standard <code>require</code> but through <code>require-macros</code>However, if macros are in the same namespace - as in this case - adding <code>:include-macros true</code> has the same effect.</p> <p>To start the app we need to use the function <code>om/root</code> and tell it </p> <ul> <li>(a) what to render, </li> <li>(b) what our state is,</li> <li>(c) and where to hook into the DOM.</li> </ul> <pre><code class="language-clojure">(om/root todo-app ;; (a) app-state ;; (b) {:target (.getElementById js/document "todoapp")}) ;; (c) </code></pre> <p>Om will take it from hereIt starts a rendering loop and when the state changes it will update the components in a single batch (<em>very</em> efficiently by leveraging the browser's <a href="http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/">requestAnimationFrame</a>)</p> <p>Now to the burning question: What is <code>todo-app</code> ?</p> <h2 id="componenttodoapp">Component 'todo-app'</h2> <p><code>todo-app</code> is a function that takes two parameters: the state and the underlying React component <code>owner</code> (which Om creates for you).</p> <pre><code class="language-clojure">(defn todo-app [{:keys [todos] :as state} owner] ;; ... ) </code></pre> <p>You might be wondering why the first parameter looks so odd这是ClojureScript工作的一个强大功能:<strong>解构</ strong>它基本上允许您将符号绑定到数据结构的各个部分在这种情况下<代码>[:键[todos]]> < /代码是一个<代码>的快捷方式(待办事项:行动计划)< /代码>——结合键的值<代码>待办事项> < /代码从国家到符号<代码>待办事项> < /代码<code>:as </ code>是可选的,允许为整个结构定义别名,在这种情况下为<code> state </ code>If you want to learn more about this, I recommend reading <a href="http://blog.jayfields.com/2010/07/clojure-destructuring.html">Clojure: Destructuring</a>.</p> <p>Let's have a look at the function's body:</p> <pre><code class="language-clojure">(defn todo-app [{:keys [todos] :as state} owner] (reify om/IRender (render [_] ;; create DOM elements ... ) </code></pre> <p>The first thing you'll notice is <code>reify</code>它计算为一个实现所有指定协议的新对象A protocol is "a named set of named methods and their signatures".</p> <p>Here the <code>om/IRender</code> protocol is implemented它有一个<code> render </ code>函数 - 显然 - 将组件呈现为HTML只要它检测到组件状态发生变化,它就会被Om调用<strong>Note:</strong> The first parameter is the instance of the component itself, it is ignored with <code>_</code> since it is not needed.</p> <p>There are several protocols in Om that map more or less directly to React's life cycle steps:</p> <p><img src="http://www.djindien.com/content/images/2014/Aug/lifecycle-001-3.png" alt=""></p> <p>Basically there is: </p> <ul> <li>a creation phase ("birth") which can be used to do initialization work,</li> <li>an updating phase to react to state changes &amp; render the component,</li> <li>and a cleanup phase ("death").</li> </ul> <p>The only protocol you should never ever try to implement is <code>IShouldUpdate</code> since this is where Om really shines: it runs a comparison between old and new state in order to decide whether to re-render the component or not而且由于ClojureScript中的数据结构是不可变的,它只需要一个简单的相等检查 - 与React的昂贵的逐件比较相比,这是非常快的This is the main reason why Om currently crushes pure React.js applications in benchmarks.</p> <p>Let's get back to the <code>render</code> function from above:</p> <pre><code class="language-clojure">(dom/div nil (header) (dom/input #js {:id "new-todo" :ref "newField" :placeholder "What needs to be done?" :onKeyDown #(enter-new-todo % state owner)}) (listing state) (footer state))))) </code></pre> <p>It creates the actual DOM elemens with the help of Om's <code>om.dom</code> namespace imported earlierThese DOM functions map directly to React's DOM API, meaning that <code>dom/div</code> becomes <code>React.DOM.div</code> and has the same parameters: </p> <ul> <li><code>props</code>, a JavaScript object that modifies the component's output/behaviour</li> <li>all other (optional) parameters are children components</li> </ul> <p><img src="http://www.djindien.com/content/images/2014/Aug/todomvc2-1.png" alt=""></p> <p>In this case, the root element has four children: an input field as well as a header, a list and a footer - just three functionsAs you can see, <code>nil</code> is used if there are no props.</p> <p>Let's have a closer look at the text input:</p> <pre><code class="language-clojure">(dom/input #js {:id "new-todo" :placeholder "What needs to be done?" :onKeyDown #(enter-new-todo % state owner)}) </code></pre> <p>As we have seen before, <code>props</code> sets an <code>id</code> and <code>placeholder</code> value which will be rendered as HTML tag attributes<code>onKeyDown</code> registers an event handler that fires after the user pressed a key - calling the <code>enter-new-todo</code> with the event, <code>state</code> and <code>owner</code></p> <p><strong>Note:</strong> <code>#(...)</code> is a <em>function literal</em>基本上它是<code>的简短版本(fn [evt](输入-new-todo evt state owner))</ code>So <code>%</code> is the function's first parameter, <code>%2</code> the second and so on.</p> <p>Going back to <code>render</code> from above, the functions <code>header</code> and <code>footer</code> are not particularly interesting - let's have a look at <code>listing</code> instead:</p> <pre><code class="language-clojure">(defn listing [{:keys [todos showing editing] :as state}] (dom/section #js {:id "main" :style (hidden (empty? todos))} (dom/input #js {:id "toggle-all" :type "checkbox" :onChange #(toggle-all % state) :checked (every? :completed todos)}) (apply dom/ul #js {:id "todo-list"} (list-items todos showing editing)))) </code></pre> <p>The list of todos is wrapped in a <code>&lt;section&gt;</code> with the id <code>main</code>没有待办事项时隐藏它(通过使用我们上次看到的<code>隐藏</ code>实用程序功能)在我们找到一个复选框,选中只有在每一个todo项标记为“完成”在更改时,它会调用<code> toggle-all </ code>事件处理程序Furthermore an <code>&lt;ul&gt;</code> element is created and renders the children that it receives from the <code>list-items</code> function.</p> <p><strong>Note:</strong> The <code>apply</code> function - comparable to the correspondent JavaScript function - is necessary to convert the sequence of components from <code>list-items</code> to individual function parameters of <code>dom/ul</code>.</p> <pre><code class="language-clojure">(defn list-items [todos showing editing] (om/build-all item/todo-item todos {:key :id :fn (fn [todo] (cond-&gt; todo (= (:id todo) editing) (assoc :editing true) (not (visible? todo showing)) (assoc :hidden true)))})) </code></pre> <p><code>list-items</code> uses Om's <code>build-all</code> function to create a sequence of a component, in this case <code>todo-item</code>第二个参数是一个序列的每个组件接收,这是<代码>待办事项> < /代码And the third parameter is a map of options: </p> <ul> <li><code>:key</code> is a keyword used to look up a value in the state which will serve as a React key (<code>id</code> in our case)This makes rendering <a href="http://facebook.github.io/react/docs/multiple-components.html#dynamic-children">more efficient</a> since upon re-rendering React knows which DOM node belongs to which sequence element.</li> <li><code>:fn</code> is a function to apply to the state before rendering it使用的函数使用时髦的<code> cond-&gt; </ code>宏,<em>线程</ em>表达式<code> todo </ code>通过给定的表单对首先,它将标志<code> editing </ code>添加到任何todo,它与现在正在编辑的tod具有相同的id(如果有的话)Secondly, it adds the flag <code>hidden</code> if the completion state of the item does not match the <code>showing</code> view filter (using the utility function <code>visible?</code>).</li> </ul> <p>Now, let's have a look at the <code>todo-item</code> component.</p> <h2 id="componenttodoitem">Component 'todo-item'</h2> <pre><code class="language-clojure">(defn todo-item [todo owner] (reify om/IInitState (init-state [_] {:edit-text (:title todo)}) om/IRenderState (render-state [_ state] (let [class (cond-&gt; "" (:completed todo) (str "completed") (:editing todo) (str "editing"))] (dom/li #js {:className class :style (hidden (:hidden todo))} (dom/div #js {:className "view"} (dom/input #js {:className "toggle" :type "checkbox" :checked (and (:completed todo) "checked") :onChange #(complete todo)}) (dom/label #js {:onDoubleClick #(edit % todo owner)} (:title todo)) (dom/button #js {:className "destroy" :onClick #(destroy todo)})) (dom/input #js {:className "edit" :value (om/get-state owner :edit-text) :onBlur #(submit % todo owner) :onChange #(change % todo owner) :onKeyDown #(key-down % todo owner)})))))) </code></pre> <p>This looks pretty complex but it is actually quite simple<code> IRenderState </ code>中的<code> render </ code>与<code> IRender </ code>相比有一个额外的参数:状态它创建了一个带有两个子代的<code> li </ code>标记:div <code> #view </ code>和文本输入<code> #edit </ code>Depending on the value of <code>class</code> - which is constructed by <code>cond-&gt;</code> - one of them is displayed.</p> <p>Inside the view div a checkbox (to mark the item as completed), a label (to display the todo) and a button (to destroy it) are addedThe declared event handlers are basically self-explanatory.</p> <p>The component also implements the <code>IInitState</code> protocol: <code>init-state</code> returns a map containing the component's initialized local state这是不同的从全球或传入状态,因为它通常只是短暂的,特定组件的信息在文本输入中,您可以看到使用<code> om / get-state </ code>读取它我们不直接使用todo的标题,因为用户可能在编辑期间中止,然后我们将丢失原始值<code>edit-text</code> is simply a temporary title.</p> <hr> <p>That's it for now, kids! In <a href="http://www.djindien.com/zero-to-om-act-4"><strong>Act 4</strong></a> we'll see how the user can interact with the application.</p> <![CDATA[Zero to Om - Act 2]]> <p>在第二篇文章中,我们将逐步介绍实际的ClojureScript源代码The source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om/">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="clojurescript101">ClojureScript 101</h2> <p>Before we dive in we'll look at the philosophy of</p> http://www.djindien.com/zero-to-om-act-2/ 9406a266-002e-4a6d-87ba-6e11529d335f OM react.js JavaScript的 客户端 todomvc manbetx万博体育 星期四,2014年9月4日05:18:00 GMT <p>在第二篇文章中,我们将逐步介绍实际的ClojureScript源代码The source code can be found on <a href="https://github.com/stephanos/om-tutorial">GitHub</a>.</p> <p><strong>Note:</strong> I strongly recommend reading the <a href="http://www.djindien.com/zero-to-om/">previous post</a> first if you haven't done so already.</p> <hr> <h2 id="clojurescript101">ClojureScript 101</h2> <p>Before we dive in we'll look at the philosophy of ClojureScript.</p> <p>Since ClojureScript is a Lisp dialect, it strips away most of the syntax you may know from other programming languagesThis makes it very simple - but it looks pretty unfamiliar at first.</p> <pre><code class="language-clojure">(+ 1 1) </code></pre> <p>This expression (also called S-expression, short for <em>symbolic expression</em>) has the value <code>2</code>每一个表达式都有价值括号定义其范围Basically, <strong>everything</strong> in ClojureScript boils down to this simple form:</p> <pre><code class="language-clojure">(function param1 param2 ..paramN) </code></pre> <p>The first element in the expression is the function position, the others are its parametersThis is called <em>prefix notation</em>, in contrast to the <em>algebraic notation</em> <code>1 + 1</code> from C-like languages.</p> <p>Of course, expressions can be nested, too: </p> <pre><code class="language-clojure">(println (- 10 (* 2 5))) </code></pre> <p>This expression will print <code>0</code> since expressions are evaluated from the inside outBut interestingly, it's value is <code>nil</code> since <code>println</code> only has a side-effect and returns no meaningful value.</p> <p>Note that <code>+</code> and <code>println</code> are actually just symbols that evaluate to values, in this case functionsThey are similar to constants in this regard.</p> <p>There are three types of values that can be in the function position:</p> <ul> <li>a function, like <code>println</code> or <code>+</code></li> <li>a special form, build-in "primitives" like <code>if</code>, <code>fn</code> and <code>def</code></li> <li>a macro, functions running at <em>compile-time</em></li> </ul> <p>One more thing about macros: they are a very powerful way to extend the language and transform your codeFor example, all parameters of a function must be evaluated before it can be executed - but in the case of <code>(and (fn ...) (fn ...))</code> we don't want to evaluate the second parameter if the first one is already false! Good thing <code>and</code> is a macro: it transforms the code in a way that each parameter is executed lazily and the function returns as early as possible.</p> <p>But enough about macrosWe will now continue by going through the todo application's source code.</p> <h2 id="firststeps">First Steps</h2> <p>Our application consists of three ClojureScript source files: <code>app.cljs</code>, <code>item.cljs</code> and <code>utils.cljs</code>让我们开始吧In order to get a feeling for ClojureScript we'll look at the smallest file first: <code>utils.cljs</code>.</p> <pre><code class="language-clojure">;; utils.cljs, part #1 (ns todomvc.utils (:require [cljs.reader :as reader]) (:import [goog.ui IdGenerator])) </code></pre> <p>In the first line, a namespace for the file is specified via <code>ns</code>在这种情况下,每个ClojureScript文件都需要定义一个唯一的命名空间<code> todomvc.utils </ code>这可以防止名称冲突,可以与谷歌关闭的模块概念完美地集成注意:这不是一个巧合的名称空间匹配的文件路径(<代码> todomvc /跑龙套> < /代码),这是必须的</p> <p>In the second line, the external ClojureScript namespace <code>cljs.reader</code> is loaded with <code>:require</code>它通过必需的<code>:as </ code>命名为<code> reader </ code>,并且可以在整个文件中通过此别名引用</p> <p>In the third line, <code>:import</code> loads the module <code>IdGenerator</code> from the namespace <code>goog.ui</code> of the Google Closure libraryThe difference to <code>:require</code> is that we load specific elements, not namespaces, in order to refer to them by name.</p> <p><strong>Note</strong>: Words starting with <code>:</code> are keywordsThey are like symbols - but just evaluate to themselves instead of an arbitrary value.</p> <pre><code class="language-clojure">;; utils.cljs, part #2 (defn pluralize [n word] (if (== n 1) word ;; 'true'-block (str word "s"))) ;; 'false'-block </code></pre> <p><code>defn</code> defines a function called <code>pluralize</code> with two parameters: <code>n</code> and <code>word</code>In the body, <code>if</code> returns the original word when <code>n</code> equals 1 - otherwise a new string with the character 's' appended to <code>word</code>.</p> <p><strong>Note</strong>: <code>defn</code> is just a shortcut to define an anonymous function with <code>fn</code> and binding it to a symbol with <code>def</code>.</p> <pre><code class="language-clojure">;; utils.cljs, part #3 (defn now [] (js/Date.)) </code></pre> <p><code>now</code> is a function without any parameters它返回JavaScript的<code> Date </ code>对象的新实例由于它是本机JavaScript对象,因此必须使用前缀<code> js / </ code>来访问<code> js </ code>命名空间</p> <p><strong>Note</strong>: The inconspicuous <code>.</code> is responsible for calling the object's constructor, like <code>new</code> in JavaScript.</p> <pre><code class="language-clojure">;; utils.cljs, part #4 (defn guid [] (.getNextUniqueId (.getInstance IdGenerator))) </code></pre> <p><code>guid</code> uses the Closure library's <code>IdGenerator</code> to return a new unique ID它还展示了如何在JavaScript对象上调用方法:<code>(。methodName jsObject)</ code>,注意开头的<code>。</ code>The expression translates to <code>IdGenerator.getInstance().getNextUniqueId()</code> in JavaScript.</p> <pre><code class="language-clojure">;; utils.cljs, part #5 (defn hidden [is-hidden] (if is-hidden #js {:display "none"} #js {})) </code></pre> <p><code>hidden</code> returns the JavaScript object <code>{display: "none"}</code> when the parameter <code>is-hidden</code> is trueAs you see, to return a native JavaScript object use <code>#js {...}</code> the elements are defined in key/value pairs without any special characters like <code>:</code>.</p> <p><strong>Note</strong>: In ClojureScript neither <em>camelCase</em> nor <em>snake_case</em> are used, but <em>dash-separated-lowercase</em>.</p> <pre><code class="language-clojure">;; utils.cljs, part #6 (defn store ([ns] (store ns nil)) ;; 1 parameter ([ns edn] ;; 2 parameters (if-not (nil? edn) (.setItem js/localStorage ns (str edn)) (let [s (.getItem js/localStorage ns)] (if-not (nil? s) (reader/read-string s) []))))) </code></pre> <p><code>store</code> reads data from and writes data to the browser's local storage它包含许多有趣的新事物</p> <p>First, it's a multi-arity function这意味着在这种情况下它允许多个参数,<code> [ns] </ code>和<code> [ns edn] </ code>它类似于其他语言的函数重载常见的用例是提供参数的默认值Here it uses <code>nil</code> for the second parameter if not specified otherwise.</p> <p>Secondly, we see a few new functions<code> if-not </ code>显然是<code>的否定版本,如果</ code><code>nil?</code> (it's best practice to have the '?' suffix at the end of functions with a boolean return type) checks if the provided value equals <code>nil</code> (which is identical to <code>null</code> in JavaScript)<code>让</ code>允许在本地将符号绑定到值,类似于受限<code> def </ code>:它以绑定向量,符号/值对开始,并以表达式结尾Here it binds <code>s</code> to the result of the local storage access.</p> <p>Thirdly, it serializes and deserializes ClojureScript data structures: The textual result of <code>(str edn)</code> is written to the local storage with <code>.setItem</code> and reads from local storage with <code>.getItem</code> - deserialized with <code>(reader/read-string s)</code> from the <code>reader</code> namespace loaded earlier.</p> <p><strong>Note:</strong> <a href="https://github.com/edn-format/edn">EDN</a> (<em>extensible data notation</em>) is like an extended version of JSON with a few additional benefits like non-string map keys, sets and tags.</p> <hr> <p>That's it for nowIn <a href="http://www.djindien.com/zero-to-om-act-3"><strong>Act 3</strong></a> we'll look at the Om-specific code in detail.</p> <p>In the meantime, if you want to learn more about ClojureScript I wholeheartedly recommend the <a href="https://github.com/swannodette/lt-cljs-tutorial">tutorial for Light Table</a> from David Nolen, the creator of Om.</p> <![CDATA[Zero to Om]]> < p >这是一系列的博客文章的第一部分关于OmIf you don't know anything about Om, don't worry! You'll learn everything step by step以下是基础知识:<em> Om是Facebook React的ClojureScript界面​​</ em>If that doesn't ring any bells, again, don't worry!</p> <p><strong>NOTE</strong></p> http://www.djindien.com/zero-to-om/ 7 fe7279d e1a6 - 4972 a124 f075d2e——13134 OM react.js leiningen JavaScript的 客户端 谷歌关闭 构建工具 todomvc manbetx万博体育 星期一,2014年8月25日15:41:00 GMT < p >这是一系列的博客文章的第一部分关于OmIf you don't know anything about Om, don't worry! You'll learn everything step by step以下是基础知识:<em> Om是Facebook React的ClojureScript界面​​</ em>If that doesn't ring any bells, again, don't worry!</p> <p><strong>NOTE</strong>: I <em>just</em> started learning React.js, ClojureScript and Om这是我努力学习的方式这自己:通过你的旅程Basic knowledge about modern web development is required, though.</p> <h2 id="thestage">The Stage</h2> <p>Learning by doing is funWe are going to learn Om with the help of a little todo app.</p> <p>If you are a web developer and haven't heard of <a href="http://todomvc.com/">TodoMVC</a> yet you were either unemployed or in a coma for the last few years (you didn't miss much except for React.js)It's a "project which offers the same Todo application implemented [...] in most of the popular JavaScript frameworks of today".</p> <p>This is how it looks: <br> <img src="http://www.djindien.com/content/images/2014/Aug/todos.png" alt=""></p> <p>As the foundation of this series I will use an <a href="https://github.com/swannodette/todomvc/tree/gh-pages/labs/architecture-examples/om">implementation of the Todo app</a> written by the creator of Om himself, <a href="https://github.com/swannodette">David Nolen</a>You can <a href="http://swannodette.github.io/todomvc/labs/architecture-examples/om/">play with the app</a> to see what it can do.</p> <h2 id="theactors">The Actors</h2> <p>Let's see what technologies we need for our endeavour.</p> <h3 id="reactjs">React.js</h3> <p><img src="http://www.djindien.com/content/images/2014/Aug/logo_og.png" alt=""></p> <p><a href="http://facebook.github.io/react/">React.js</a> is a JavaScript library from Facebook for building user interfaces把它想象成MVC中的V.它通过使用内存(虚拟)DOM一次批量处理所有更新,使数据和DOM保持同步,并且<em>非常</ em>高效地使用它That's exactly how 3D engines work as well.</p> <h3 id="clojurescript">ClojureScript</h3> <p><img src="http://www.djindien.com/content/images/2014/Aug/apple-touch-icon-precomposed-1.png" alt=""></p> <p><a href="https://github.com/clojure/ClojureScript">ClojureScript</a> is a Clojure dialect that compiles to JavaScriptIt is a dynamically typed, functional programming language with a strong focus on immutable data structures.</p> <h3 id="googleclosuretools">Google Closure Tools</h3> <p><img src="http://www.djindien.com/content/images/2014/Aug/closure.png" alt=""></p> <p><a href="https://developers.google.com/closure/">Google Closure Tools</a> consist of a JavaScript library with utilities for rich, cross-browser web applications and a compiler for optimizing JavaScript.</p> <h3 id="lighttable">Light Table</h3> <p><img src="http://www.djindien.com/content/images/2014/Aug/logo.png" alt=""></p> <p><a href="http://www.lighttable.com/">Light Table</a> is an open source IDE mainly geared towards Clojure/ClojureScript developmentOne of its most interesting features is the inline evaluation of code.</p> <h3 id="leiningen">Leiningen</h3> <p><img src="http://www.djindien.com/content/images/2014/Aug/leiningen-full.jpg" alt=""></p> <p><a href="http://leiningen.org/">Leiningen</a> is a JVM-based build tool for ClojureIts <a href="https://github.com/emezeske/lein-cljsbuild">cljsbuild</a> plugin automates compiling ClojureScript into JavaScript.</p> <h3 id="om">Om</h3> <p>Finally, <a href="https://github.com/swannodette/om">Om</a> is the magic goo that combines the functional nature of React with the functional language ClojureScript and its immutable data structures, simplifying application architecture and increasing performance significantly in the process.</p> <h2 id="act1">Act 1</h2> <p>At first, let's try to run the applicationIf we checkout my - slightly modified version of the original - <a href="https://github.com/stephanos/om-tutorial">from GitHub</a> - we'll see a bunch of files:</p> <pre><code>bower_components/ todomvc-common/ src/ todomvc/ app.cljs item.cljs utils.cljs bower.json index.html project.clj readme.md </code></pre> <p><code>bower.json</code> is the manifest file for <a href="http://bower.io/">Bower</a> which manages external resourcesIn this case it downloaded a stylesheet and an image into <code>todomvc-common</code> for us.</p> <p><code>index.html</code> is the HTML file we open in our browser to showcase our appSo let's go for it! </p> <p>And ..<strong>darn</strong>!</p> <pre><code>GET file:///.../out/goog/base.js net::ERR_FILE_NOT_FOUND index.html:15 GET file:///.../app.js net::ERR_FILE_NOT_FOUND index.html:16 Uncaught ReferenceError: goog is not defined </code></pre> <p>Well, let's look at the file to see what might have gone wrong:</p> <pre><code class="language-markup">&lt;head&gt; [...] &lt;link rel="stylesheet" href="bower_components/todomvc-common/base.css"&gt; &lt;/head&gt; &lt;body&gt; &lt;section id="todoapp"&gt;&lt;/section&gt; [...] &lt;script src="http://fb.me/react-0.11.1.js"&gt;&lt;/script&gt; &lt;script src="out/goog/base.js"&gt;&lt;/script&gt; &lt;script src="app.js"&gt;&lt;/script&gt; &lt;script&gt;goog.require("todomvc.app");&lt;/script&gt; &lt;/body&gt; </code></pre> <p>The file loads a bunch of scripts and if you look at our current directory you'll quickly notice: most of them don't exist, yetWe need to build them first!</p> <p>That's where we need <code>project.clj</code>它包含Leiningen项目的构建配置Next, we run:</p> <pre><code class="language-bash">$ leiningen cljsbuild once dev Compiling ClojureScript编制“app.js”(" src "). .</code></pre> <p>Now we have a new file called <code>app.js</code> and a folder <code>out</code> with lots of files in it如果我们刷新浏览器,问题就会消失,我们会注意到一个正常运行的Todo应用Splendid!</p> <p>If we look into <code>app.js</code> we'll see something like this:</p> <pre><code class="language-javascript">goog.addDependency("base.js", ['goog'], []); goog.addDependency("../cljs/reader.js", ['cljs.reader'], ['goog.string', 'cljs.core']); goog.addDependency("../om/dom.js", ['om.dom'], ['cljs.core']); [...] </code></pre> <p>This is Google Closure at work它具有<em>模块加载器</ em>每个ClojureScript文件都转换为Closure模块每个模块都可以依赖于许多其他模块加载程序位于<code> out / goog / base.js </ code>中所有应用程序的依赖项都在<code> app.js </ code>中定义要启动我们的应用程序,我们需要加载主模块:<code> goog.require(“todomvc.app”)</ code>And that is exactly what the <code>index.html</code> does.</p> <hr> <p>This concludes the first act<br> In <a href="http://www.djindien.com/zero-to-om-act-2/"><strong>Act 2</strong></a> we will look at the ClojureScript source code in detail.</p> <![CDATA [BedCon 2014谈话]]> <p>今天我在柏林的BedCon上发表了演讲它向观众介绍了由Google创建的相当新的编程语言GoThe room was packed, some people had to actually stand in the back.</p> <p>In case you missed it, here are my slides.</p> <script async class="speakerdeck-embed" data-id="1fd842709d8e01316e29625017dd54d3" data-ratio="1.41436464088398" src="//speakerdeck.com/assets/embed.js"></script> http://www.djindien.com/talk-bedcon-2014-golang/ 1fd71cde-df72-46e8-bab8-406320e4631d manbetx万博体育 2014年4月4日星期五20:00:00 GMT <p>今天我在柏林的BedCon上发表了演讲它向观众介绍了由Google创建的相当新的编程语言GoThe room was packed, some people had to actually stand in the back.</p> <p>In case you missed it, here are my slides.</p> <script async class="speakerdeck-embed" data-id="1fd842709d8e01316e29625017dd54d3" data-ratio="1.41436464088398" src="//speakerdeck.com/assets/embed.js"></script> <![CDATA[Scala Macros Use Case: Teaching Scala]]> <p>The brand new experimental macro feature in Scala 2.10 can be used in many interesting ways: </p> <ul> <li><a href="https://github.com/pniederw/expecty">writing concise test cases</a>,</li> <li><a href="http://mandubian.com/2012/11/11/JSON-inception/">generating serialization code</a></li> <li><a href="https://github.com/paulp/declosurify">inlining methods</a></li> </ul> <p>This article shows exciting new possibilities it opens up for teaching Scala引用一位学生的话说:“这真是太棒了..and fun!".</p> <p>Exercises are</p> http://www.djindien.com/scala-macros-use-case-teaching-scala/ ecb94bc7 - 0452 - 4172 - a4ad - 851 dceb20721 斯卡拉 manbetx万博体育 星期一,2013年1月28日11:00:00 GMT <p>The brand new experimental macro feature in Scala 2.10 can be used in many interesting ways: </p> <ul> <li><a href="https://github.com/pniederw/expecty">writing concise test cases</a>,</li> <li><a href="http://mandubian.com/2012/11/11/JSON-inception/">generating serialization code</a></li> <li><a href="https://github.com/paulp/declosurify">inlining methods</a></li> </ul> <p>This article shows exciting new possibilities it opens up for teaching Scala引用一位学生的话说:“这真是太棒了..and fun!".</p> <p>Exercises are a crucial part of the "Introduction to Scala" training course I developed for German-speaking software engineersNobody wants to listen to someone else's dialog for 8 hours - they want to get a feeling for the new language, try out the new syntax, play around with new features.</p> <h3 id="designingtrainingexercises">Designing training exercises</h3> <p>But there is always the problem of making sure that the students are on the right track when solving the exercisesDepending on the size of the course you cannot look over everyone's shoulder all the time.</p> <p>This is why I decided to leverage unit tests: Students can use pre-written tests to work through the exercises step by step, knowing immediately whether they are doing something wrongWhen results are shared and discussed in the end, unpleasant surprises can be avoided.</p> <h3 id="addingmacrostoit">Adding macros to it</h3> <p>So, I hear you ask, what does that have to do with macros? Scala macros allow you to make certain tests that were not possible beforeLet's have a look.</p> <script src="https://gist.github.com/4643260.js"></script> <p>The snippet above illustrates an exercise to teach pattern matchingIn the presentation the students listened to earlier, various patterns like the alternative and wildcard pattern were introduced.</p> <p>The exercise description states: <strong>Rewrite this code in 3 lines or less</strong>This way, students have to think about how to apply the new features.</p> <p>Furthermore, they were taught that a <strong>var</strong> should be used sparselyThe unit test is able to check for that as well.</p> <p>So macros allow to test various code styles and metricsCool!</p> <h3 id="howitworks">How it works</h3> <p>Behind the scenes, the unit tests use <a href="http://docs.scala-lang.org/overviews/macros/overview.html">def macros</a> since type macros are only available in an experimental Scala 2.11 branchFirst, I thought it may not be possible with this limitation, but thanks to some help from Eugene Burmako - member of the official Scala team - it worked out great.</p> <p>The method <code>task</code> - part of the trait <code>Testable</code> that each exercise inherits from - calls a macro现在它变得有趣了由于def宏无法向现有类添加字段或方法,因此我必须创建一个基类<code> Task </ code>,其中包含我稍后需要的所有字段,例如to count the number of <code>vars</code> the code uses.</p> <table> <colgroup> <col width="40%"> <col width="60%"> </colgroup> <tr> <td> <script src="https://gist.github.com/4643398.js"></script> </td> <td> <script src="https://gist.github.com/4647618.js"></script> </td> </tr> </table> <p>Then the macro would generate a call to the method <code>registerTask</code> with an instance of type <code>Task</code>由于字段已经声明,因此可以在实例化时简单地覆盖它们Et voilà!</p> <p>The complete macro code is rather ugly and contains a few workarounds but the following snippet shows the small part where the number of <code>vars</code> is determinedAdditionally, the method to actually overwrite the field can be seen.</p> <script src="https://gist.github.com/4647666.js"></script> <p>All in all, the macro worked pretty well in my recent Scala training当判断他们的解决方案的长度和风格时,学生们非常惊讶I think it's a great way of making sure the students adhere to the rules and concepts they just learned about; preventing them to just stick to their old Java habits.</p> <p>I'm looking forward to the possibilities the type macros will bring in 2.11<strong>感谢阅读。</ strong> </ p>